Skip to content

Commit

Permalink
feat(smartcar): added support for simulated mode and feature flags (#109
Browse files Browse the repository at this point in the history
)

feat(smartcar): added support for simulated mode and feature flags
  • Loading branch information
mdheri authored Jul 21, 2022
1 parent 4997691 commit 14d0036
Show file tree
Hide file tree
Showing 13 changed files with 146 additions and 41 deletions.
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ and client secret. You'll also need to add the testing redirect URI to your appl
```
export E2E_SMARTCAR_CLIENT_ID='<Your client id>'
export E2E_SMARTCAR_CLIENT_SECRET='<Your client secret>'
export E2E_SMARTCAR_REDIRECT_URI='<Your redirect URI>'
```

4. (Optional) Pass Application Management Token and webhook id to test webhooks:
Expand Down
6 changes: 3 additions & 3 deletions REFERENCE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AuthClient

### `smartcar.AuthClient(self, client_id, client_secret, redirect_uri, test_mode=False)`
### `smartcar.AuthClient(self, client_id, client_secret, redirect_uri, mode='live')`

A client for accessing the Smartcar API

Expand All @@ -11,7 +11,7 @@ A client for accessing the Smartcar API
| `client_id` | String | **Optional**\* | Application clientId obtained from [Smartcar Developer Portal](https://dashboard.smartcar.com). |
| `client_secret` | String | **Optional**\* | Application clientSecret obtained from [Smartcar Developer Portal](https://dashboard.smartcar.com). |
| `redirect_uri` | String | **Optional**\* | RedirectURI set in [application settings](https://dashboard.smartcar.com/apps). Given URL must match URL in application settings. |
| `test_mode` | Boolean | **Optional** | Launch the Smartcar Connect in test mode. |
| `mode` | String | **Optional** | Determine what mode Smartcar Connect should be launched in. Should be one of test, live or simulated. |

##### \***Environment Variables VS Passing Arguments:**

Expand Down Expand Up @@ -711,7 +711,7 @@ A compatible vehicle is a vehicle that:

| Value | Type | Availability | Description |
|:------------------------------------------|:------------------------|:----------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------|
| `Compatibility` | typing.NamedTuple | **API v1.0 and v2.0** |The returned object with vehicle's compatibility with the permissions (scope) checked |
| `Compatibility` | typing.NamedTuple | **API v1.0 and v2.0** |The returned object with vehicle's compatibility with the permissions (scope) checked |
| `Compatibility.compatible` | Boolean | **API v1.0 and v2.0** | Whether the vehicle is compatible with the permissions |
| `Compatibility.reason` | String or None | **API v2.0 only** | One of the following string values if compatible is false, null otherwise: "VEHICLE_NOT_COMPATIBLE", "MAKE_NOT_COMPATIBLE" |
| `Compatibility.capabilities` | List | **API v2.0 only** | A list containing the set of endpoints that the provided scope value can provide authorization for. This list will be empty if compatible is false. |
Expand Down
32 changes: 24 additions & 8 deletions smartcar/auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import datetime, timedelta
from typing import List
from urllib.parse import urlencode
from warnings import warn

import smartcar.config as config
import smartcar.helpers as helpers
Expand All @@ -14,7 +15,8 @@ def __init__(
client_id=None,
client_secret=None,
redirect_uri=None,
test_mode=False,
test_mode=None,
mode="live",
):
"""
A client for accessing the Smartcar API.
Expand All @@ -28,23 +30,34 @@ def __init__(
However, if neither an environment variable nor an argument is passed in, an exception will be raised.
Args:
client_id (str): The application id, provided in the application
client_id (str, optional): The application id, provided in the application
dashboard
client_secret (str): The application secret, provided in the
client_secret (str, optional): The application secret, provided in the
application dashboard
redirect_uri (str): The URL to redirect to after the user accepts
redirect_uri (str, optional): The URL to redirect to after the user accepts
or declines the application's permissions. This URL must also be
present in the Redirect URIs field in the application dashboard
test_mode (bool, optional): Launch the Smartcar auth flow in test mode. Defaults to false.
test_mode (bool, optional): Deprecated, please use `mode` instead.
Launch Smartcar Connect in [test mode](https://smartcar.com/docs/guides/testing/).
mode (str, optional): Determine what mode Smartcar Connect should be launched in.
Should be one of test, live or simulated. Defaults to live.
"""
self.client_id = client_id or os.environ.get("SMARTCAR_CLIENT_ID")
self.client_secret = client_secret or os.environ.get("SMARTCAR_CLIENT_SECRET")
self.redirect_uri = redirect_uri or os.environ.get("SMARTCAR_REDIRECT_URI")
self.test_mode = test_mode
self.mode = mode.lower()

if test_mode is not None:
warn(
'The "testMode" parameter is deprecated, please use the "mode" parameter instead.',
DeprecationWarning,
)
self.mode = "test" if test_mode else "live"

self.auth = (self.client_id, self.client_secret)

if (
Expand All @@ -59,6 +72,10 @@ def __init__(
"with your client credentials. i.e.: "
"'SMARTCAR_CLIENT_ID', 'SMARTCAR_CLIENT_SECRET', and 'SMARTCAR_REDIRECT_URI'"
)
if self.mode not in ["test", "live", "simulated"]:
raise Exception(
"The \"mode\" parameter MUST be one of the following: 'test', 'live', 'simulated'",
)

def get_auth_url(self, scope: List[str], options: dict = None) -> str:
"""
Expand Down Expand Up @@ -106,9 +123,8 @@ def get_auth_url(self, scope: List[str], options: dict = None) -> str:
"redirect_uri": self.redirect_uri,
"approval_prompt": "auto",
"scope": " ".join(scope),
"mode": self.mode,
}
if self.test_mode:
query["mode"] = "test"

if options:
if options.get("force_prompt"):
Expand Down
2 changes: 1 addition & 1 deletion smartcar/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
AUTH_URL = os.environ.get(
"SMARTCAR_AUTH_ORIGIN", "https://auth.smartcar.com/oauth/token"
)
CONNECT_URL = "https://connect.smartcar.com"
CONNECT_URL = os.environ.get("SMARTCAR_CONNECT_ORIGIN", "https://connect.smartcar.com")
16 changes: 9 additions & 7 deletions smartcar/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,26 @@ def requester(method: str, url: str, **kwargs) -> requests.models.Response:
raise sce.SmartcarException(message="SDK_ERROR") from e


def validate_env(test_mode: bool = False) -> None:
def validate_env(mode: str = "live") -> None:
"""
Helper Function to determine if environment variables for client id
and secret are set properly.
Args:
test_mode: bool
mode: str ['test'|'simulated'|'live']
Raises:
Basic Exception
"""
prefix = "E2E_SMARTCAR" if test_mode else "SMARTCAR"
prefix = "E2E_SMARTCAR" if mode != "live" else "SMARTCAR"

if (
f"{prefix}_CLIENT_ID" not in os.environ
or f"{prefix}_CLIENT_SECRET" not in os.environ
):
raise Exception(
f'"{prefix}_CLIENT_ID", "{prefix}_CLIENT_SECRET", and '
f'"{prefix}_CLIENT_REDIRECT_URI environment variables must be set'
f'"{prefix}_CLIENT_ID" and "{prefix}_CLIENT_SECRET"'
f" environment variables must be set"
)


Expand All @@ -88,8 +88,10 @@ def format_flag_query(flags: dict) -> str:
"""
flags_str = ""

for flag in flags.keys():
flags_str += f"{flag}:{flags[flag]} "
for flag_name, flag_value in flags.items():
if type(flag_value) == bool:
flag_value = str(flag_value).lower()
flags_str += f"{flag_name}:{flag_value} "

return flags_str.strip()

Expand Down
29 changes: 23 additions & 6 deletions smartcar/smartcar.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
from datetime import datetime
from typing import List, Union
from warnings import warn

import smartcar.config as config
import smartcar.helpers as helpers
Expand Down Expand Up @@ -102,7 +103,8 @@ def get_compatibility(
3. Is compatible with the required permissions (scope) that your app is requesting
access to
Note: The `test_mode` and `test_mode_compatibility_level` options arguments are only valid for Smartcar API v1.0.
Note: The `mode` and `test_mode_compatibility_level` options arguments are only valid for Smartcar API v1.0
and `test_mode` has been deprecated
Args:
vin (str)
Expand All @@ -118,11 +120,15 @@ def get_compatibility(
version (str): Version of API you want to use
flags (dict - {str: bool}): An optional list of feature flags
flags (dict - {str: bool}, optional): An optional list of feature flags
test_mode (bool): Indicates whether the API should be invoked in test mode (as opposed to live mode)
test_mode (bool, optional): Deprecated, please use `mode` instead.
Launch Smartcar Connect in [test mode](https://smartcar.com/docs/guides/testing/).
test_mode_compatibility_level (str): This parameter is required when the API is invoked in test mode.
mode (str, optional): Determine what mode Smartcar Connect should be launched in.
Should be one of test, live or simulated.
test_mode_compatibility_level (str, optional): This parameter is required when the API is invoked in test mode.
Possible values with details are documented in our Integration Guide.
Returns:
Expand Down Expand Up @@ -160,15 +166,26 @@ def get_compatibility(
api_version = options["version"]

if api_version == "1.0":
if options.get("test_mode") is not None:
warn(
'The "testMode" parameter is deprecated, please use the "mode" parameter instead.',
DeprecationWarning,
)
params["mode"] = "test" if options.get("test_mode") else "live"
elif options.get("mode"):
params["mode"] = options.get["mode"]

if options.get("test_mode_compatibility_level"):
params["mode"] = "test"
params["test_mode_compatibility_level"] = options[
"test_mode_compatibility_level"
]
elif options.get("test_mode"):
params["mode"] = "test"

if params.mode not in ["test", "live", "simulated"]:
raise Exception(
"The \"mode\" parameter MUST be one of the following: 'test', 'live', 'simulated'",
)

# Ensuring client_id and client_secret are present
if client_id is None or client_secret is None:
raise Exception(
Expand Down
20 changes: 16 additions & 4 deletions smartcar/vehicle.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from collections import namedtuple
import json
from typing import List

import smartcar.config as config
import smartcar.helpers as helpers
import smartcar.smartcar
Expand All @@ -26,6 +25,9 @@ def __init__(self, vehicle_id: str, access_token: str, options: dict = None):
version(str, optional): Version of Smartcar API an instance of vehicle
will send requests to. This will override the instance's base url attribute.
flags(dict, optional): Object of flags where key is the name of the flag and
value is string or boolean value.
Attributes:
self.vehicle_id (str)
self.access_token (str): Access token retrieved from Smartcar Connect
Expand All @@ -36,6 +38,7 @@ def __init__(self, vehicle_id: str, access_token: str, options: dict = None):
self.access_token = access_token
self._api_version = smartcar.smartcar.API_VERSION
self._unit_system = "metric"
self._flags = {}

if options:
if options.get("unit_system"):
Expand All @@ -44,6 +47,9 @@ def __init__(self, vehicle_id: str, access_token: str, options: dict = None):
if options.get("version"):
self._api_version = options["version"]

if options.get("flags"):
self._flags = options["flags"]

def vin(self) -> types.Vin:
"""
GET Vehicle.vin
Expand Down Expand Up @@ -503,14 +509,20 @@ def set_unit_system(self, unit_system: str) -> None:
# ===========================================
# Private methods
# ===========================================
def _format_query_params(self) -> str:
"""
Returns (str): Query parameters as a query string
"""
if self._flags:
return f"?flags={helpers.format_flag_query(self._flags)}"
return ""

def _format_url(self, path: str) -> str:
"""
Returns (str): Base url with current API version.
User can change api_version attribute at will.
"""
return (
f"{config.API_URL}/v{self._api_version}/vehicles/{self.vehicle_id}/{path}"
)
return f"{config.API_URL}/v{self._api_version}/vehicles/{self.vehicle_id}/{path}{self._format_query_params()}"

def _get_headers(self, need_unit_system: bool = True) -> dict:
"""
Expand Down
10 changes: 5 additions & 5 deletions tests/auth_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import smartcar.helpers as helpers

# Verify all E2E variables are present ('E2E_<CLIENT VARIABLE>')
helpers.validate_env(test_mode=True)
helpers.validate_env(mode="test")

# Smartcar client environment variables (Required)
CLIENT_ID = os.environ["E2E_SMARTCAR_CLIENT_ID"]
Expand Down Expand Up @@ -42,8 +42,8 @@


def get_auth_client_params():
test_mode = True
return [CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, test_mode]
mode = "test"
return [CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, True, mode]


def get_code_from_url(url):
Expand Down Expand Up @@ -82,8 +82,8 @@ def run_auth_flow(auth_url, brand="CHEVROLET"):
sign_in_button = WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.ID, "sign-in-button"))
)
driver.find_element_by_id("username").send_keys(username)
driver.find_element_by_id("password").send_keys("password")
driver.find_element(By.ID, "username").send_keys(username)
driver.find_element(By.ID, "password").send_keys("password")
sign_in_button.click()

# Permissions Approval
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/test_auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_auth_client(client):
assert client.client_id is not None
assert client.client_secret is not None
assert client.redirect_uri is not None
assert client.test_mode
assert client.mode == "test"


def test_get_auth_url_single_select(client):
Expand Down
4 changes: 2 additions & 2 deletions tests/e2e/test_smartcar.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def test_get_compatibility_in_test_mode_but_no_level():
options={
"client_id": ah.CLIENT_ID,
"client_secret": ah.CLIENT_SECRET,
"test_mode": True,
"mode": "test",
},
)

Expand Down Expand Up @@ -105,7 +105,7 @@ def test_get_compatibility_without_client_id(chevy_volt):
get_compatibility(vin=chevy_volt.vin().vin, scope=["read_vehicle_info"])
except Exception as e:
assert e.args == (
'"SMARTCAR_CLIENT_ID", "SMARTCAR_CLIENT_SECRET", and "SMARTCAR_CLIENT_REDIRECT_URI environment variables must be set',
'"SMARTCAR_CLIENT_ID" and "SMARTCAR_CLIENT_SECRET" environment variables must be set',
)


Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_get_auth_url_with_options(client):
assert query_params["make"][0] == "Ford"
assert query_params["single_select"][0] == "true"
assert query_params["single_select_vin"][0] == "abcdefghi12345678"
assert query_params["flags"][0] == "flag_1:Yay flag_2:True flag_3:123"
assert query_params["flags"][0] == "flag_1:Yay flag_2:true flag_3:123"


def test_get_auth_url_without_options(client):
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@


def test_validate_env():
assert helpers.validate_env(test_mode=True) is None
assert helpers.validate_env(mode="test") is None


def test_format_flag_query():
test_flags = {"flag1": True, "flag2": 100, "flag3": "apple"}
expected_flag_str = "flag1:True flag2:100 flag3:apple"
expected_flag_str = "flag1:true flag2:100 flag3:apple"
assert helpers.format_flag_query(test_flags) == expected_flag_str
Loading

0 comments on commit 14d0036

Please sign in to comment.