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 @@ - - -
- - -