From 40ba148579da6b45268ea8ed9eb252cbafbe9042 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Fri, 22 Nov 2024 17:12:21 +0100 Subject: [PATCH] Add captcha for rest_of_world (#675) * Require captcha for rest_of_world * Update docs * Update tests for captcha * More docs changes --- bimmer_connected/api/authentication.py | 15 ++-- bimmer_connected/cli.py | 4 +- bimmer_connected/tests/__init__.py | 1 + bimmer_connected/tests/conftest.py | 3 +- bimmer_connected/tests/test_account.py | 71 ++++++++------- bimmer_connected/tests/test_api.py | 5 +- bimmer_connected/tests/test_cli.py | 8 +- .../source/_static/captcha_north_america.html | 39 -------- docs/source/captcha.rst | 48 +++++++--- docs/source/captcha/north_america.rst | 7 ++ docs/source/captcha/north_america_form.html | 88 +++++++++++++++++++ docs/source/captcha/rest_of_world.rst | 7 ++ docs/source/captcha/rest_of_world_form.html | 88 +++++++++++++++++++ docs/source/cli.rst | 23 ++++- docs/source/index.rst | 11 ++- 15 files changed, 321 insertions(+), 97 deletions(-) delete mode 100644 docs/source/_static/captcha_north_america.html create mode 100644 docs/source/captcha/north_america.rst create mode 100644 docs/source/captcha/north_america_form.html create mode 100644 docs/source/captcha/rest_of_world.rst create mode 100644 docs/source/captcha/rest_of_world_form.html diff --git a/bimmer_connected/api/authentication.py b/bimmer_connected/api/authentication.py index 55037e3c..7012ed51 100644 --- a/bimmer_connected/api/authentication.py +++ b/bimmer_connected/api/authentication.py @@ -154,6 +154,11 @@ async def _login_row_na(self): async with MyBMWLoginClient(region=self.region, verify=self.verify) as client: _LOGGER.debug("Authenticating with MyBMW flow for North America & Rest of World.") + if not self.hcaptcha_token: + raise MyBMWCaptchaMissingError( + "Missing hCaptcha token for login. See https://bimmer-connected.readthedocs.io/en/stable/captcha.html" + ) + # Get OAuth2 settings from BMW API r_oauth_settings = await client.get( OAUTH_CONFIG_URL, @@ -185,13 +190,9 @@ async def _login_row_na(self): "code_challenge_method": "S256", } - authenticate_headers = {} - if self.region == Regions.NORTH_AMERICA: - if not self.hcaptcha_token: - raise MyBMWCaptchaMissingError("Missing hCaptcha token for North America login") - authenticate_headers = { - "hcaptchatoken": self.hcaptcha_token, - } + authenticate_headers = { + "hcaptchatoken": self.hcaptcha_token, + } # Call authenticate endpoint first time (with user/pw) and get authentication try: response = await client.post( diff --git a/bimmer_connected/cli.py b/bimmer_connected/cli.py index f74e5335..ce62b483 100644 --- a/bimmer_connected/cli.py +++ b/bimmer_connected/cli.py @@ -311,7 +311,9 @@ def _add_default_arguments(parser: argparse.ArgumentParser): parser.add_argument("username", help="Connected Drive username") parser.add_argument("password", help="Connected Drive password") parser.add_argument("region", choices=valid_regions(), help="Region of the Connected Drive account") - parser.add_argument("--captcha-token", type=str, nargs="?", help="Captcha token required for North America.") + parser.add_argument( + "--captcha-token", type=str, nargs="?", help="Captcha token required for North America and Rest of World." + ) def _add_position_arguments(parser: argparse.ArgumentParser): diff --git a/bimmer_connected/tests/__init__.py b/bimmer_connected/tests/__init__.py index b7b158f4..a8e34aa1 100644 --- a/bimmer_connected/tests/__init__.py +++ b/bimmer_connected/tests/__init__.py @@ -13,6 +13,7 @@ TEST_PASSWORD = "my_secret" TEST_REGION = Regions.REST_OF_WORLD TEST_REGION_STRING = "rest_of_world" +TEST_CAPTCHA = "P1_eY..." VIN_F31 = "WBA00000000000F31" VIN_G01 = "WBA00000000DEMO04" diff --git a/bimmer_connected/tests/conftest.py b/bimmer_connected/tests/conftest.py index db44e40f..cce2743a 100644 --- a/bimmer_connected/tests/conftest.py +++ b/bimmer_connected/tests/conftest.py @@ -14,6 +14,7 @@ ALL_CHARGING_SETTINGS, ALL_PROFILES, ALL_STATES, + TEST_CAPTCHA, TEST_PASSWORD, TEST_REGION, TEST_USERNAME, @@ -55,6 +56,6 @@ def cli_home_dir(tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.M async def prepare_account_with_vehicles(region: Optional[Regions] = None): """Initialize account and get vehicles.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, region or TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, region or TEST_REGION, hcaptcha_token=TEST_CAPTCHA) await account.get_vehicles() return account diff --git a/bimmer_connected/tests/test_account.py b/bimmer_connected/tests/test_account.py index 69b73de8..56ddeec1 100644 --- a/bimmer_connected/tests/test_account.py +++ b/bimmer_connected/tests/test_account.py @@ -24,6 +24,7 @@ from . import ( RESPONSE_DIR, + TEST_CAPTCHA, TEST_PASSWORD, TEST_REGION, TEST_REGION_STRING, @@ -40,7 +41,9 @@ @pytest.mark.asyncio async def test_login_row(bmw_fixture: respx.Router): """Test the login flow.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, get_region_from_name(TEST_REGION_STRING)) + account = MyBMWAccount( + TEST_USERNAME, TEST_PASSWORD, get_region_from_name(TEST_REGION_STRING), hcaptcha_token=TEST_CAPTCHA + ) await account.get_vehicles() assert account is not None @@ -48,7 +51,7 @@ async def test_login_row(bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_login_na(bmw_fixture: respx.Router): """Test the login flow for North America.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, Regions.NORTH_AMERICA, hcaptcha_token="SOME_TOKEN") + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, Regions.NORTH_AMERICA, hcaptcha_token=TEST_CAPTCHA) await account.get_vehicles() assert account is not None @@ -65,7 +68,9 @@ async def test_login_na_without_hcaptcha(bmw_fixture: respx.Router): async def test_login_refresh_token_row_na_expired(bmw_fixture: respx.Router): """Test the login flow using refresh_token.""" with mock.patch("bimmer_connected.api.authentication.EXPIRES_AT_OFFSET", datetime.timedelta(seconds=30000)): - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, get_region_from_name(TEST_REGION_STRING)) + account = MyBMWAccount( + TEST_USERNAME, TEST_PASSWORD, get_region_from_name(TEST_REGION_STRING), hcaptcha_token=TEST_CAPTCHA + ) await account.get_vehicles() with mock.patch( @@ -84,7 +89,9 @@ async def test_login_refresh_token_row_na_expired(bmw_fixture: respx.Router): async def test_login_refresh_token_row_na_401(bmw_fixture: respx.Router): """Test the login flow using refresh_token.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, get_region_from_name(TEST_REGION_STRING)) + account = MyBMWAccount( + TEST_USERNAME, TEST_PASSWORD, get_region_from_name(TEST_REGION_STRING), hcaptcha_token=TEST_CAPTCHA + ) await account.get_vehicles() with mock.patch( @@ -111,7 +118,9 @@ async def test_login_refresh_token_row_na_invalid(caplog, bmw_fixture: respx.Rou ] ) - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, get_region_from_name(TEST_REGION_STRING)) + account = MyBMWAccount( + TEST_USERNAME, TEST_PASSWORD, get_region_from_name(TEST_REGION_STRING), hcaptcha_token=TEST_CAPTCHA + ) account.set_refresh_token("INVALID") caplog.set_level(logging.DEBUG) @@ -211,7 +220,7 @@ async def test_vehicles(bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_vehicle_init(bmw_fixture: respx.Router): """Test vehicle initialization.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) with mock.patch( "bimmer_connected.account.MyBMWAccount._init_vehicles", wraps=account._init_vehicles, @@ -240,7 +249,7 @@ async def test_invalid_password(bmw_fixture: respx.Router): 401, json=load_response(RESPONSE_DIR / "auth" / "auth_error_wrong_password.json") ) with pytest.raises(MyBMWAuthError): - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) await account.get_vehicles() @@ -262,7 +271,7 @@ async def test_server_error(bmw_fixture: respx.Router): 500, text=load_response(RESPONSE_DIR / "auth" / "auth_error_internal_error.txt") ) with pytest.raises(MyBMWAPIError): - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) await account.get_vehicles() @@ -290,7 +299,7 @@ async def test_get_fingerprints(monkeypatch: pytest.MonkeyPatch, bmw_fixture: re + get_fingerprint_count("charging_settings") ) - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, log_responses=True) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA, log_responses=True) await account.get_vehicles() # This should have been successful @@ -310,7 +319,7 @@ async def test_get_fingerprints(monkeypatch: pytest.MonkeyPatch, bmw_fixture: re ) bmw_fixture.routes.add(state_route, "state") - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, log_responses=True) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA, log_responses=True) await account.get_vehicles() filenames = [Path(f.filename) for f in account.get_stored_responses()] @@ -363,7 +372,7 @@ async def test_set_use_metric_units(caplog): """Test (deprecated) use_metrics_units flag.""" # Default - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) assert len(caplog.records) == 0 metric_client = MyBMWClient(account.config) assert ( @@ -392,7 +401,7 @@ async def test_set_use_metric_units(caplog): @pytest.mark.asyncio async def test_refresh_token_getset(bmw_fixture: respx.Router): """Test getting/setting the refresh_token and gcid.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) assert account.refresh_token is None await account.get_vehicles() assert account.refresh_token == "another_token_string" @@ -414,7 +423,7 @@ async def test_refresh_token_getset(bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_429_retry_ok_oauth_config(caplog, bmw_fixture: respx.Router): """Test the login flow using refresh_token.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) json_429 = {"statusCode": 429, "message": "Rate limit is exceeded. Try again in 2 seconds."} @@ -441,7 +450,7 @@ async def test_429_retry_ok_oauth_config(caplog, bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_429_retry_raise_oauth_config(caplog, bmw_fixture: respx.Router): """Test the login flow using refresh_token.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) json_429 = {"statusCode": 429, "message": "Rate limit is exceeded. Try again in 2 seconds."} @@ -462,7 +471,7 @@ async def test_429_retry_raise_oauth_config(caplog, bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_429_retry_ok_authenticate(caplog, bmw_fixture: respx.Router): """Test the login flow using refresh_token.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) json_429 = {"statusCode": 429, "message": "Rate limit is exceeded. Try again in 2 seconds."} @@ -490,7 +499,7 @@ async def test_429_retry_ok_authenticate(caplog, bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_429_retry_raise_authenticate(caplog, bmw_fixture: respx.Router): """Test the login flow using refresh_token.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) json_429 = {"statusCode": 429, "message": "Rate limit is exceeded. Try again in 2 seconds."} @@ -511,7 +520,7 @@ async def test_429_retry_raise_authenticate(caplog, bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_429_retry_ok_vehicles(caplog, bmw_fixture: respx.Router): """Test waiting on 429 for vehicles.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) json_429 = {"statusCode": 429, "message": "Rate limit is exceeded. Try again in 2 seconds."} @@ -541,7 +550,7 @@ async def test_429_retry_ok_vehicles(caplog, bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_429_retry_raise_vehicles(caplog, bmw_fixture: respx.Router): """Test waiting on 429 for vehicles and fail if it happens too often.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) json_429 = {"statusCode": 429, "message": "Rate limit is exceeded. Try again in 2 seconds."} @@ -562,7 +571,7 @@ async def test_429_retry_raise_vehicles(caplog, bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_429_retry_with_login_ok_vehicles(bmw_fixture: respx.Router): """Test the login flow but experiencing a 429 first.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) json_429 = {"statusCode": 429, "message": "Rate limit is exceeded. Try again in 2 seconds."} @@ -584,7 +593,7 @@ async def test_429_retry_with_login_ok_vehicles(bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_429_retry_with_login_raise_vehicles(bmw_fixture: respx.Router): """Test the error handling, experiencing a 429, 401 and another two 429.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) json_429 = {"statusCode": 429, "message": "Rate limit is exceeded. Try again in 2 seconds."} @@ -607,7 +616,7 @@ async def test_429_retry_with_login_raise_vehicles(bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_multiple_401(bmw_fixture: respx.Router): """Test the error handling, when multiple 401 are received in sequence.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) bmw_fixture.post(VEHICLES_URL).mock( side_effect=[ @@ -623,7 +632,7 @@ async def test_multiple_401(bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_401_after_429_ok(bmw_fixture: respx.Router): """Test the error handling, when a 401 is received after exactly 3 429.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) await account.get_vehicles() json_429 = {"statusCode": 429, "message": "Rate limit is exceeded. Try again in 2 seconds."} @@ -647,7 +656,7 @@ async def test_401_after_429_ok(bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_401_after_429_fail(bmw_fixture: respx.Router): """Test the error handling, when a 401 is received after exactly 3 429.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) json_429 = {"statusCode": 429, "message": "Rate limit is exceeded. Try again in 2 seconds."} @@ -669,7 +678,7 @@ async def test_401_after_429_fail(bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_403_quota_exceeded_vehicles_usa(caplog, bmw_fixture: respx.Router): """Test 403 quota issues for vehicle state and fail if it happens too often.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) # get vehicles once await account.get_vehicles() @@ -691,7 +700,7 @@ async def test_403_quota_exceeded_vehicles_usa(caplog, bmw_fixture: respx.Router @pytest.mark.asyncio async def test_incomplete_vehicle_details(caplog, bmw_fixture: respx.Router): """Test incorrect responses for vehicle details.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) # get vehicles once await account.get_vehicles() @@ -717,7 +726,7 @@ async def test_incomplete_vehicle_details(caplog, bmw_fixture: respx.Router): @pytest.mark.asyncio async def test_no_vehicle_details(caplog, bmw_fixture: respx.Router): """Test raising an exception if no responses for vehicle details are received.""" - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) await account.get_vehicles() bmw_fixture.get("/eadrax-vcs/v4/vehicles/state").mock( @@ -737,9 +746,9 @@ async def test_no_vehicle_details(caplog, bmw_fixture: respx.Router): async def test_client_async_only(bmw_fixture: respx.Router): """Test that the Authentication providers only work async.""" - with httpx.Client(auth=MyBMWAuthentication(TEST_USERNAME, TEST_PASSWORD, TEST_REGION)) as client, pytest.raises( - RuntimeError - ): + with httpx.Client( + auth=MyBMWAuthentication(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA) + ) as client, pytest.raises(RuntimeError): client.get("/eadrax-ucs/v1/presentation/oauth/config") with httpx.Client(auth=MyBMWLoginRetry()) as client, pytest.raises(RuntimeError): @@ -763,7 +772,9 @@ async def test_pillow_unavailable(monkeypatch: pytest.MonkeyPatch, bmw_fixture: assert len(account.vehicles) == 0 # But rest_of_world and north_america should work - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, get_region_from_name("rest_of_world")) + account = MyBMWAccount( + TEST_USERNAME, TEST_PASSWORD, get_region_from_name("rest_of_world"), hcaptcha_token=TEST_CAPTCHA + ) await account.get_vehicles() assert account is not None assert len(account.vehicles) > 0 diff --git a/bimmer_connected/tests/test_api.py b/bimmer_connected/tests/test_api.py index 24903493..75496aa4 100644 --- a/bimmer_connected/tests/test_api.py +++ b/bimmer_connected/tests/test_api.py @@ -14,6 +14,7 @@ from . import ( RESPONSE_DIR, + TEST_CAPTCHA, TEST_PASSWORD, TEST_REGION, TEST_USERNAME, @@ -75,7 +76,7 @@ async def test_storing_fingerprints(tmp_path, bmw_fixture: respx.Router, bmw_log ) bmw_fixture.routes.add(state_route, "state") - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, log_responses=True) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA, log_responses=True) await account.get_vehicles() log_response_store_to_file(account.get_stored_responses(), tmp_path) @@ -100,7 +101,7 @@ async def test_fingerprint_deque(monkeypatch: pytest.MonkeyPatch, bmw_fixture: r """Test storing fingerprints to file.""" # Prepare Number of good responses - account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, log_responses=True) + account = MyBMWAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION, hcaptcha_token=TEST_CAPTCHA, log_responses=True) await account.get_vehicles() await account.get_vehicles() diff --git a/bimmer_connected/tests/test_cli.py b/bimmer_connected/tests/test_cli.py index 2a31d528..62a26bff 100644 --- a/bimmer_connected/tests/test_cli.py +++ b/bimmer_connected/tests/test_cli.py @@ -13,7 +13,7 @@ from . import RESPONSE_DIR, get_fingerprint_count, load_response -ARGS_USER_PW_REGION = ["myuser", "mypassword", "rest_of_world"] +ARGS_USER_PW_REGION = ["--captcha-token", "P1_eY...", "myuser", "mypassword", "rest_of_world"] FIXTURE_CLI_HELP = "Connect to MyBMW/MINI API and interact with your vehicle." @@ -22,7 +22,6 @@ 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 @@ -323,4 +322,7 @@ def test_captcha_unavailable(capsys: pytest.CaptureFixture): with contextlib.suppress(SystemExit): bimmer_connected.cli.main() result = capsys.readouterr() - assert result.err.strip() == "MyBMWCaptchaMissingError: Missing hCaptcha token for North America login" + assert ( + result.err.strip() + == "MyBMWCaptchaMissingError: Missing hCaptcha token for login. See https://bimmer-connected.readthedocs.io/en/stable/captcha.html" + ) diff --git a/docs/source/_static/captcha_north_america.html b/docs/source/_static/captcha_north_america.html deleted file mode 100644 index 982ddcbd..00000000 --- a/docs/source/_static/captcha_north_america.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - Form with hCaptcha - - -

-
-
-
- -

- -
- - - -
-
-

- - - \ No newline at end of file diff --git a/docs/source/captcha.rst b/docs/source/captcha.rst index 8457876a..2aefb41d 100644 --- a/docs/source/captcha.rst +++ b/docs/source/captcha.rst @@ -1,20 +1,46 @@ -Captcha (North America) -======================= +Using Captchas +============== -Login to the :code:`north_america` region requires a captcha to be solved. Submit below form and use the returned token when creating the account object. +The **first** login requires a captcha to be solved. Follow-up logins using refresh token do not require a captcha (if login data is persisted). + +.. note:: + The captcha token is only valid for a short time and can only be used once. + +Depending on your region, you will need to solve a different captcha. Please select the appropriate region below. + +- `North America `_ +- `Rest of World `_ + +Using the Python API +--------------------- + +When using the Python API, pass the token via the :code:`hcaptcha_token` argument when creating the account object. + +.. warning:: + + Ensure to save the current :code:`MyBMWAccount` instance (or at least, the :code:`refresh_token` and :code:`gcid` attributes) to avoid having to solve the captcha again. :: - account = MyBMWAccount(USERNAME, PASSWORD, Regions.REST_OF_WORLD, hcaptcha_token=HCAPTCHA_TOKEN) + account = MyBMWAccount(USERNAME, PASSWORD, REGION, hcaptcha_token=HCAPTCHA_TOKEN) -When using the CLI, pass the token via the :code:`--hcaptcha-token` argument (see `CLI documentation `_). -.. note:: - Only the first login requires a captcha to be solved. Follow-up logins using refresh token do not require a captcha. - This requires the tokens to be stored in a file (default behavior when using the CLI) or in the python object itself. +Using the CLI +------------- +When using the CLI, pass the token via the :code:`--hcaptcha-token` argument (see `CLI documentation `_). .. warning:: - The captcha token is only valid for a short time and can only be used once. -.. raw:: html - :file: _static/captcha_north_america.html + Please make sure to use the :code:`--oauth-store` (used by default) to avoid having to solve the captcha again. + +:: + + bimmerconnected status --captcha-token CAPTCHA_TOKEN USERNAME PASSWORD REGION + +After a successful login, the :code:`--captcha-token` parameter can be omitted (until a captcha is required again, indicated by a :code:`invalid login` error). + +:: + + bimmerconnected status USERNAME PASSWORD REGION + + diff --git a/docs/source/captcha/north_america.rst b/docs/source/captcha/north_america.rst new file mode 100644 index 00000000..78ccb2de --- /dev/null +++ b/docs/source/captcha/north_america.rst @@ -0,0 +1,7 @@ +Captcha (North America) +======================= + +Please see :doc:`../captcha` for general information on using captchas. + +.. raw:: html + :file: north_america_form.html diff --git a/docs/source/captcha/north_america_form.html b/docs/source/captcha/north_america_form.html new file mode 100644 index 00000000..7a666073 --- /dev/null +++ b/docs/source/captcha/north_america_form.html @@ -0,0 +1,88 @@ + + + + + + Form with hCaptcha + + +

+
+ +
+
+ +

+ +
+ + +
+
+

+ + + \ No newline at end of file diff --git a/docs/source/captcha/rest_of_world.rst b/docs/source/captcha/rest_of_world.rst new file mode 100644 index 00000000..3204587e --- /dev/null +++ b/docs/source/captcha/rest_of_world.rst @@ -0,0 +1,7 @@ +Captcha (Rest of World) +======================= + +Please see :doc:`../captcha` for general information on using captchas. + +.. raw:: html + :file: rest_of_world_form.html diff --git a/docs/source/captcha/rest_of_world_form.html b/docs/source/captcha/rest_of_world_form.html new file mode 100644 index 00000000..206d7064 --- /dev/null +++ b/docs/source/captcha/rest_of_world_form.html @@ -0,0 +1,88 @@ + + + + + + Form with hCaptcha + + +

+
+ +
+
+ +

+ +
+ + +
+
+

+ + + \ No newline at end of file diff --git a/docs/source/cli.rst b/docs/source/cli.rst index 85ac7cde..ea4c0aca 100644 --- a/docs/source/cli.rst +++ b/docs/source/cli.rst @@ -1,7 +1,26 @@ -.. _cli_module: +:mod:`bimmerconnected` CLI +========================== + +The :code:`bimmerconnected` CLI is a command line interface to interact with the BMW ConnectedDrive API. +A full documentation of the commands can be found below. + +.. warning:: + + The CLI will store your tokens in a file called :mod:`.bimmer_connected.json` in your home directory (:code:`--oauth-store` parameter). + You can move this file, but is is required to function properly (have multiple calls). + The data is stored in plain text, so protect this file accordingly. + +.. note:: + + The first login (or after a long time) requires a captcha to be solved (see :doc:`captcha`). + Follow-up logins using refresh token do not require a captcha (as data is persisted as per above). + + These follow-up logins also **do not** require the :code:`--captcha-token` parameter. :mod:`bimmerconnected` -====================== +----------------------- + +.. _cli_module: .. argparse:: :module: bimmer_connected.cli diff --git a/docs/source/index.rst b/docs/source/index.rst index 738a79c8..e7eefdd0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,13 +6,22 @@ readme +.. toctree:: + :caption: Captcha + :name: captcha + :maxdepth: 2 + :glob: + + captcha + captcha/* + + .. toctree:: :caption: Development :name: development :maxdepth: 2 :glob: - captcha development/*