Skip to content

Commit

Permalink
feat: Add Vehicle Management Support (#60)
Browse files Browse the repository at this point in the history
* feat: Add Vehicle Management Support

adding get/delete methods for connections

* chore: fix lint offenses

* chore: add tests for get-connections

* chore: fix lint issues

* chore: split out paging from filters

* chore: bumping up the version

* fix: fixing an existing bug with compatibility req

- the error object was in Smartcar::Base. Fixing that
- Additionally validatiing scope for nil or empty
- Fixing the tests and adding cases to test those two scenarios which is vin and scope

* feat: fix the connections functions

- fix the args, error cases and refactor connections related methods
- fix the tests
- fix the lint

* doc: add get_connections example to readme

---------

Co-authored-by: s-ashwinkumar <[email protected]>
  • Loading branch information
JacobAndrewSmith92 and s-ashwinkumar authored Aug 18, 2023
1 parent 234555d commit eea31ca
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 7 deletions.
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ AllCops:
Metrics/AbcSize:
Enabled: false

# Lengthen module line length
Metrics/ModuleLength:
Max: 200

# Disabling this becuase we are using `set` and `get` prefixed methods to keep some commonality across SDKs
Naming/AccessorMethodName:
Enabled: false
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
smartcar (3.5.0)
smartcar (3.6.0)
oauth2 (~> 1.4)
recursive-open-struct (~> 1.1.3)

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ Example of providing a custom Faraday connection to various methods:

# Passing the custom service into a Smartcar::Vehicle object
vehicle = Smartcar::Vehicle.new(token: token, id: id, options: { service: service })

connections = Smartcar.get_connections(amt: 'amt', filter: {userId: 'user-id'}, options: {service: service})
```

## Development
Expand Down
72 changes: 69 additions & 3 deletions lib/smartcar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ class ConfigNotFound < StandardError; end

# Host to connect to smartcar
API_ORIGIN = 'https://api.smartcar.com/'
MANAGEMENT_API_ORIGIN = 'https://management.smartcar.com'
PATHS = {
compatibility: '/compatibility',
user: '/user',
vehicles: '/vehicles'
vehicles: '/vehicles',
connections: '/management/connections'
}.freeze

# Path for smartcar oauth
Expand Down Expand Up @@ -80,8 +82,8 @@ def get_api_version
# @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#compatibility-api
# and a meta attribute with the relevant items from response headers.
def get_compatibility(vin:, scope:, country: 'US', options: {})
raise InvalidParameterValue.new, 'vin is a required field' if vin.nil?
raise InvalidParameterValue.new, 'scope is a required field' if scope.nil?
raise Base::InvalidParameterValue.new, 'vin is a required field' if vin.nil?
raise Base::InvalidParameterValue.new, 'scope is a required field' if scope.nil? || scope.empty?

base_object = Base.new(
{
Expand Down Expand Up @@ -166,6 +168,70 @@ def verify_payload(amt, signature, body)
hash_challenge(amt, body.to_json) == signature
end

# Module method Returns a paged list of all vehicle connections connected to the application.
#
# API Documentation - https://smartcar.com/docs/api#get-connections
# @param amt [String] - Application Management token
# @param filters [Hash] - Optional filter parameters (check documentation)
# @param paging [Hash] - Pass a cursor for paginated results
# @param options [Hash] Other optional parameters including overrides
# @option options [Faraday::Connection] :service Optional connection object to be used for requests
# @option options [String] :version Optional API version to use, defaults to what is globally set
#
# @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-connections
# and a meta attribute with the relevant items from response headers.
def get_connections(amt:, filter: {}, paging: {}, options: {})
paging[:limit] ||= 10
base_object = Base.new(
token: generate_basic_management_auth(amt, options),
version: options[:version] || Smartcar.get_api_version,
service: options[:service],
auth_type: Base::BASIC,
url: ENV['SMARTCAR_MANAGEMENT_API_ORIGIN'] || MANAGEMENT_API_ORIGIN
)
query_params = filter.merge(paging).compact

base_object.build_response(*base_object.get(
PATHS[:connections],
query_params
))
end

def delete_connections(amt:, filter: {}, options: {})
user_id = filter[:user_id]
vehicle_id = filter[:vehicle_id]
error_message = nil
error_message = 'Filter can contain EITHER user_id OR vehicle_id, not both.' if user_id && vehicle_id
error_message = 'Filter needs one of user_id OR vehicle_id.' unless user_id || vehicle_id

raise Base::InvalidParameterValue.new, error_message if error_message

query_params = {}
query_params['user_id'] = user_id if user_id
query_params['vehicle_id'] = vehicle_id if vehicle_id

base_object = Base.new(
url: ENV['SMARTCAR_MANAGEMENT_API_ORIGIN'] || MANAGEMENT_API_ORIGIN,
auth_type: Base::BASIC,
token: generate_basic_management_auth(amt, options),
version: options[:version] || Smartcar.get_api_version,
service: options[:service]
)

base_object.build_response(*base_object.delete(
PATHS[:connections],
query_params
))
end

# returns auth token for Basic vehicle management auth
#
# @return [String] Base64 encoding of default:amt
def generate_basic_management_auth(amt, options = {})
username = options[:username] || 'default'
Base64.strict_encode64("#{username}:#{amt}")
end

private

def build_compatibility_params(vin, scope, country, options)
Expand Down
4 changes: 2 additions & 2 deletions lib/smartcar/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class InvalidParameterValue < StandardError; end
# Constant for Basic auth type
BASIC = 'Basic'

attr_accessor :token, :error, :unit_system, :version, :auth_type
attr_accessor :token, :error, :unit_system, :version, :auth_type, :url

%i[get post patch put delete].each do |verb|
# meta programming and define all Restful methods.
Expand Down Expand Up @@ -54,7 +54,7 @@ class InvalidParameterValue < StandardError; end
# @return [OAuth2::AccessToken] An initialized AccessToken instance that acts as service client
def service
@service ||= Faraday.new(
url: ENV['SMARTCAR_API_ORIGIN'] || API_ORIGIN,
url: url || ENV['SMARTCAR_API_ORIGIN'] || API_ORIGIN,
request: { timeout: DEFAULT_REQUEST_TIMEOUT }
)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/smartcar/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

module Smartcar
# Gem current version number
VERSION = '3.5.0'
VERSION = '3.6.0'
end
183 changes: 183 additions & 0 deletions spec/smartcar/integration/smartcar_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
before do
@api_origin = ENV['SMARTCAR_API_ORIGIN']
ENV['SMARTCAR_API_ORIGIN'] = 'https://pizza.pasta.pi'
@management_origin = ENV['SMARTCAR_MANAGEMENT_API_ORIGIN']
ENV['SMARTCAR_MANAGEMENT_API_ORIGIN'] = 'https://pizza.pasta.pi'
@amt = 'some-token'
WebMock.disable_net_connect!
end

after do
WebMock.allow_net_connect!
ENV['SMARTCAR_API_ORIGIN'] = @api_origin
ENV['SMARTCAR_MANAGEMENT_API_ORIGIN'] = @management_origin
end

describe '.get_compatibility' do
Expand Down Expand Up @@ -54,6 +58,32 @@
end
end

context 'when vin is nil' do
it 'should raise error' do
expect do
subject.get_compatibility(vin: nil, scope: ['scope'], options: { mode: 'invalid' })
end.to(raise_error do |error|
expect(error.message).to eq('vin is a required field')
end)
end
end

context 'when scope is nil or empty' do
it 'should raise error' do
expect do
subject.get_compatibility(vin: 'vin', scope: [], options: { mode: 'invalid' })
end.to(raise_error do |error|
expect(error.message).to eq('scope is a required field')
end)

expect do
subject.get_compatibility(vin: 'vin', scope: nil, options: { mode: 'invalid' })
end.to(raise_error do |error|
expect(error.message).to eq('scope is a required field')
end)
end
end

context 'when mode is set to simulated' do
it 'should add it in query params' do
scopes = %w[read_odometer read_location]
Expand Down Expand Up @@ -297,4 +327,157 @@
end
end
end

describe '.get_connections' do
it 'should return all connections associated with the amt (application management token)' do
header_token = subject.generate_basic_management_auth(@amt)

stub_request(:get, 'https://pizza.pasta.pi/v2.0/management/connections?limit=10')
.with(headers: { 'Authorization' => "Basic #{header_token}" })
.to_return(
{
status: 200,
headers: { 'content-type' => 'application/json; charset=utf-8' },
body:
{
connections: [
{
connectedAt: '2021-12-25T14:48:00.000Z',
userId: 'user-id-7',
vehicleId: 'vehicle-id-1'
},
{
connectedAt: '2020-10-05T14:48:00.000Z',
userId: 'user-id-6',
vehicleId: 'vehicle-id-2'
}
],
paging: { cursor: nil }
}.to_json
}
)
response = subject.get_connections(amt: @amt)

expect(response.connections.is_a?(Array)).to be_truthy
expect(response.connections[0].vehicleId).to eq('vehicle-id-1')
expect(response.paging.is_a?(OpenStruct)).to be_truthy
expect(response.paging.cursor).to be nil
end

it 'should return all connections based on given additional filters and paging options' do
header_token = subject.generate_basic_management_auth(@amt)

stub_request(:get, 'https://pizza.pasta.pi/v2.0/management/connections?limit=13&user_id=user_id&cursor=cursor')
.with(headers: { 'Authorization' => "Basic #{header_token}" })
.to_return(
{
status: 200,
headers: { 'content-type' => 'application/json; charset=utf-8' },
body:
{
connections: [
{
connectedAt: '2021-12-25T14:48:00.000Z',
userId: 'user_id',
vehicleId: 'vehicle-id-1'
},
{
connectedAt: '2020-10-05T14:48:00.000Z',
userId: 'user_id',
vehicleId: 'vehicle-id-2'
}
],
paging: { cursor: 'cursor2' }
}.to_json
}
)
response = subject.get_connections(amt: @amt, filter: { user_id: 'user_id' },
paging: { cursor: 'cursor', limit: 13 })

expect(response.connections.is_a?(Array)).to be_truthy
expect(response.connections[0].vehicleId).to eq('vehicle-id-1')
expect(response.paging.is_a?(OpenStruct)).to be_truthy
expect(response.paging.cursor).to eq('cursor2')
end
end

describe '.delete_connections' do
context 'when both user_id and vehicle_id are provided' do
it 'raises an error' do
expect do
subject.delete_connections(amt: @amt, filter: { user_id: 'user_id', vehicle_id: 'vehicle_id' })
end.to(raise_error do |error|
expect(error.message).to eq(
'Filter can contain EITHER user_id OR vehicle_id, not both.'
)
end)
end
end

context 'when neither user_id or vehicle_id is provided' do
it 'raises an error' do
expect do
subject.delete_connections(amt: @amt)
end.to(raise_error do |error|
expect(error.message).to eq(
'Filter needs one of user_id OR vehicle_id.'
)
end)
end
end

it 'deletes connections by user_id' do
header_token = subject.generate_basic_management_auth(@amt)

stub_request(:delete, 'https://pizza.pasta.pi/v2.0/management/connections?user_id=user_id')
.with(headers: { 'Authorization' => "Basic #{header_token}" })
.to_return(
{
status: 200,
headers: { 'content-type' => 'application/json; charset=utf-8' },
body:
{
connections: [
{
connectedAt: '2021-12-25T14:48:00.000Z',
userId: 'user_id',
vehicleId: 'vehicle-id-1'
}
]
}.to_json
}
)
response = subject.delete_connections(amt: @amt, filter: { user_id: 'user_id' })

expect(response.connections.is_a?(Array)).to be_truthy
expect(response.connections[0].userId).to eq('user_id')
end

it 'deletes connections by vehicle_id' do
header_token = subject.generate_basic_management_auth(@amt)

stub_request(:delete, 'https://pizza.pasta.pi/v2.0/management/connections?vehicle_id=vehicle_id')
.with(headers: { 'Authorization' => "Basic #{header_token}" })
.to_return(
{
status: 200,
headers: { 'content-type' => 'application/json; charset=utf-8' },
body:
{
connections: [
{
connectedAt: '2021-12-25T14:48:00.000Z',
userId: 'user_id',
vehicleId: 'vehicle_id'
}
]
}.to_json
}
)
response = subject.delete_connections(amt: @amt, filter: { vehicle_id: 'vehicle_id' })

expect(response.connections.is_a?(Array)).to be_truthy
expect(response.connections[0].vehicleId).to eq('vehicle_id')
end
end
end

0 comments on commit eea31ca

Please sign in to comment.