Skip to content

Commit

Permalink
Improve tests and add version in CLI help output (#630)
Browse files Browse the repository at this point in the history
* Add more tests

* Include version in CLI help

* Cleanup

* Ignore mypy issue in test
  • Loading branch information
rikroe authored Jul 13, 2024
1 parent 8169b5e commit 034fc35
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 20 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.4.5
rev: v0.5.1
hooks:
- id: ruff
args:
Expand All @@ -24,7 +24,7 @@ repos:
exclude_types: [csv, json]
exclude: ^test/responses/
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
rev: v1.10.1
hooks:
- id: mypy
name: mypy
Expand Down
8 changes: 6 additions & 2 deletions bimmer_connected/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import httpx

from bimmer_connected import __version__ as VERSION
from bimmer_connected.account import MyBMWAccount
from bimmer_connected.api.regions import get_region_from_name, valid_regions
from bimmer_connected.const import DEFAULT_POI_NAME
Expand All @@ -23,7 +24,10 @@

def main_parser() -> argparse.ArgumentParser:
"""Create the ArgumentParser with all relevant subparsers."""
parser = argparse.ArgumentParser(description="Connect to MyBMW/MINI API and interact with your vehicle.")
parser = argparse.ArgumentParser(
description=(f"Connect to MyBMW/MINI API and interact with your vehicle.\n\nVersion: {VERSION}"),
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument("--debug", help="Print debug logs.", action="store_true")
parser.add_argument(
"--oauth-store",
Expand Down Expand Up @@ -333,7 +337,7 @@ def main():

if args.oauth_store.exists():
try:
account.set_refresh_token(**json.load(args.oauth_store.open()))
account.set_refresh_token(**json.loads(args.oauth_store.read_text()))
except json.JSONDecodeError:
pass

Expand Down
8 changes: 5 additions & 3 deletions bimmer_connected/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ def add_login_routes(self) -> None:
self.get("/eadrax-ucs/v1/presentation/oauth/config").respond(
200, json=load_response(RESPONSE_DIR / "auth" / "oauth_config.json")
)
self.post("/gcdm/oauth/authenticate").mock(side_effect=self.authenticate_sideeffect)
self.post("/gcdm/oauth/token").respond(200, json=load_response(RESPONSE_DIR / "auth" / "auth_token.json"))
self.post("/gcdm/oauth/authenticate", name="authenticate").mock(side_effect=self.authenticate_sideeffect)
self.post("/gcdm/oauth/token", name="token").respond(
200, json=load_response(RESPONSE_DIR / "auth" / "auth_token.json")
)

# Login to china
self.get("/eadrax-coas/v1/cop/publickey").respond(
Expand All @@ -112,7 +114,7 @@ def add_login_routes(self) -> None:
def add_vehicle_routes(self) -> None:
"""Add routes for vehicle requests."""

self.post("/eadrax-vcs/v5/vehicle-list").mock(side_effect=self.vehicles_sideeffect)
self.post("/eadrax-vcs/v5/vehicle-list", name="vehicles").mock(side_effect=self.vehicles_sideeffect)
self.get("/eadrax-vcs/v5/vehicle-data/profile").mock(side_effect=self.vehicle_profile_sideeffect)
self.get("/eadrax-vcs/v4/vehicles/state", name="state").mock(side_effect=self.vehicle_state_sideeffect)
self.get("/eadrax-crccs/v2/vehicles").mock(side_effect=self.vehicle_charging_settings_sideeffect)
Expand Down
94 changes: 81 additions & 13 deletions bimmer_connected/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
import subprocess
import sys
from pathlib import Path
from unittest import mock

import httpx
import pytest
import respx

import bimmer_connected.cli
from bimmer_connected import __version__ as VERSION

from . import get_fingerprint_count
from . import RESPONSE_DIR, get_fingerprint_count, load_response

ARGS_USER_PW_REGION = ["myuser", "mypassword", "rest_of_world"]
FIXTURE_CLI_HELP = "Connect to MyBMW/MINI API and interact with your vehicle."
Expand All @@ -19,6 +21,7 @@ def test_run_entrypoint():
result = subprocess.run(["bimmerconnected", "--help"], capture_output=True, text=True)

assert FIXTURE_CLI_HELP in result.stdout
assert VERSION in result.stdout
assert result.returncode == 0


Expand All @@ -27,6 +30,7 @@ def test_run_module():
result = subprocess.run(["python", "-m", "bimmer_connected.cli", "--help"], capture_output=True, text=True)

assert FIXTURE_CLI_HELP in result.stdout
assert VERSION in result.stdout
assert result.returncode == 0


Expand Down Expand Up @@ -138,25 +142,27 @@ def test_fingerprint(capsys: pytest.CaptureFixture, cli_home_dir: Path):
assert len(txt_files) == 0


@pytest.mark.usefixtures("bmw_fixture")
@pytest.mark.usefixtures("cli_home_dir")
def test_oauth_store_credentials(cli_home_dir: Path):
def test_oauth_store_credentials(cli_home_dir: Path, bmw_fixture: respx.Router):
"""Test storing the oauth credentials."""

assert (cli_home_dir / ".bimmer_connected.json").exists() is False

sys.argv = ["bimmerconnected", "status", *ARGS_USER_PW_REGION]
bimmer_connected.cli.main()

assert bmw_fixture.routes["token"].call_count == 1
assert bmw_fixture.routes["vehicles"].calls[0].request.headers["authorization"] == "Bearer some_token_string"

assert (cli_home_dir / ".bimmer_connected.json").exists() is True
oauth_storage = json.loads((cli_home_dir / ".bimmer_connected.json").read_text())

assert set(oauth_storage.keys()) == {"access_token", "refresh_token", "gcid"}


@pytest.mark.usefixtures("bmw_fixture")
# @pytest.mark.usefixtures("bmw_fixture")
@pytest.mark.usefixtures("cli_home_dir")
def test_oauth_load_credentials(cli_home_dir: Path):
def test_oauth_load_credentials(cli_home_dir: Path, bmw_fixture: respx.Router):
"""Test loading and storing the oauth credentials."""

demo_oauth_data = {
Expand All @@ -170,20 +176,20 @@ def test_oauth_load_credentials(cli_home_dir: Path):

sys.argv = ["bimmerconnected", "status", *ARGS_USER_PW_REGION]

with mock.patch("bimmer_connected.account.MyBMWAccount.set_refresh_token") as mock_listener:
bimmer_connected.cli.main()
bimmer_connected.cli.main()

assert mock_listener.call_count == 1
mock_listener.assert_called_once_with(**demo_oauth_data)
assert bmw_fixture.routes["token"].call_count == 0
assert bmw_fixture.routes["vehicles"].calls[0].request.headers["authorization"] == "Bearer demo_access_token"

assert (cli_home_dir / ".bimmer_connected.json").exists() is True
oauth_storage = json.loads((cli_home_dir / ".bimmer_connected.json").read_text())

assert set(oauth_storage.keys()) == {"access_token", "refresh_token", "gcid"}

assert oauth_storage["refresh_token"] != demo_oauth_data["refresh_token"]
assert oauth_storage["access_token"] != demo_oauth_data["access_token"]
assert oauth_storage["gcid"] != demo_oauth_data["gcid"]
# no change as the old tokens are still valid
assert oauth_storage["refresh_token"] == demo_oauth_data["refresh_token"]
assert oauth_storage["access_token"] == demo_oauth_data["access_token"]
assert oauth_storage["gcid"] == demo_oauth_data["gcid"]


@pytest.mark.usefixtures("bmw_fixture")
Expand Down Expand Up @@ -231,3 +237,65 @@ def test_oauth_store_credentials_disabled(cli_home_dir: Path):
bimmer_connected.cli.main()

assert (cli_home_dir / ".bimmer_connected.json").exists() is False


@pytest.mark.usefixtures("cli_home_dir")
def test_login_refresh_token(cli_home_dir: Path, bmw_fixture: respx.Router):
"""Test logging in with refresh token."""

# set up stored tokens
demo_oauth_data = {
"access_token": "outdated_access_token",
"refresh_token": "demo_refresh_token",
"gcid": "demo_gcid",
}

(cli_home_dir / ".bimmer_connected.json").write_text(json.dumps(demo_oauth_data))
assert (cli_home_dir / ".bimmer_connected.json").exists() is True

vehicle_routes = bmw_fixture.pop("vehicles")
bmw_fixture.post("/eadrax-vcs/v5/vehicle-list", name="vehicles").mock(
side_effect=[
httpx.Response(401, json=load_response(RESPONSE_DIR / "auth" / "auth_error_wrong_password.json")),
*[vehicle_routes.side_effect for _ in range(1000)], # type: ignore[list-item]
]
)

sys.argv = ["bimmerconnected", "--debug", "status", *ARGS_USER_PW_REGION]
bimmer_connected.cli.main()

assert bmw_fixture.routes["token"].call_count == 1
assert bmw_fixture.routes["vehicles"].calls[0].request.headers["authorization"] == "Bearer outdated_access_token"
assert bmw_fixture.routes["vehicles"].calls.last.request.headers["authorization"] == "Bearer some_token_string"

assert (cli_home_dir / ".bimmer_connected.json").exists() is True


@pytest.mark.usefixtures("cli_home_dir")
def test_login_invalid_refresh_token(cli_home_dir: Path, bmw_fixture: respx.Router):
"""Test logging in with an invalid refresh token."""

# set up stored tokens
demo_oauth_data = {
"refresh_token": "invalid_refresh_token",
"gcid": "demo_gcid",
}

(cli_home_dir / ".bimmer_connected.json").write_text(json.dumps(demo_oauth_data))
assert (cli_home_dir / ".bimmer_connected.json").exists() is True

bmw_fixture.post("/gcdm/oauth/token", name="token").mock(
side_effect=[
httpx.Response(401, json=load_response(RESPONSE_DIR / "auth" / "auth_error_wrong_password.json")),
*[httpx.Response(200, json=load_response(RESPONSE_DIR / "auth" / "auth_token.json")) for _ in range(1000)],
]
)

sys.argv = ["bimmerconnected", "status", *ARGS_USER_PW_REGION]
bimmer_connected.cli.main()

assert bmw_fixture.routes["token"].call_count == 2
assert bmw_fixture.routes["authenticate"].call_count == 2
assert bmw_fixture.routes["vehicles"].calls[0].request.headers["authorization"] == "Bearer some_token_string"

assert (cli_home_dir / ".bimmer_connected.json").exists() is True

0 comments on commit 034fc35

Please sign in to comment.