def test_mod_response(): router = Router() route1a = router.get("https://foo.bar/baz/") % 409 route1b = router.get("https://foo.bar/baz/") % 404 route2 = router.get("https://foo.bar") % dict(status_code=201) route3 = router.post("https://fox.zoo/") % httpx.Response(401, json={"error": "x"}) request = httpx.Request("GET", "https://foo.bar/baz/") resolved = router.resolve(request) assert resolved.response.status_code == 404 assert resolved.route is route1b assert route1a is route1b request = httpx.Request("GET", "https://foo.bar/") resolved = router.resolve(request) assert resolved.response.status_code == 201 assert resolved.route is route2 request = httpx.Request("POST", "https://fox.zoo/") resolved = router.resolve(request) assert resolved.response.status_code == 401 assert resolved.response.json() == {"error": "x"} assert resolved.route is route3 with pytest.raises(TypeError, match="Route can only"): router.route() % []
def test_mod_response(): router = Router() route1a = router.get("https://foo.bar") % 404 route1b = router.get("https://foo.bar") % dict(status_code=201) route2 = router.get("https://ham.spam/egg/") % MockResponse(202) route3 = router.post("https://fox.zoo/") % httpx.Response( 401, json={"error": "x"}) request = httpx.Request("GET", "https://foo.bar") matched_route, response = router.match(request) assert response.status_code == 404 assert matched_route is route1a request = httpx.Request("GET", "https://foo.bar") matched_route, response = router.match(request) assert response.status_code == 201 assert matched_route is route1b assert route1a is route1b request = httpx.Request("GET", "https://ham.spam/egg/") matched_route, response = router.match(request) assert response.status_code == 202 assert matched_route is route2 request = httpx.Request("POST", "https://fox.zoo/") matched_route, response = router.match(request) assert response.status_code == 401 assert response.json() == {"error": "x"} assert matched_route is route3
async def mock_oidc_provider_token(respx_mock: respx.Router, code: str, token: OIDCToken) -> None: """Mock out the API for the upstream OpenID Connect provider. Parameters ---------- respx_mock : `respx.Router` The mock router. code : `str` The code that Gafaelfawr must send to redeem for a token. token : `gafaelfawr.models.oidc.OIDCToken` The token to return after authentication. """ config = await config_dependency() assert config.oidc mock = MockOIDCToken(config, code, token) respx_mock.post(config.oidc.token_url).mock(side_effect=mock.post_token)
async def mock_github( respx_mock: respx.Router, code: str, user_info: GitHubUserInfo, *, paginate_teams: bool = False, expect_revoke: bool = False, ) -> None: """Set up the mocks for a GitHub userinfo call. Parameters ---------- respx_mock : `respx.Router` The mock router. code : `str` The code that Gafaelfawr must send to redeem a token. user_info : `gafaelfawr.providers.github.GitHubUserInfo` User information to use to synthesize GitHub API responses. paginate_teams : `bool`, optional Whether to paginate the team results. Default: `False` expect_revoke : `bool`, optional Whether to expect a revocation of the token after returning all user information. Default: `False` """ config = await config_dependency() assert config.github mock = MockGitHub( respx_mock, config.github, code, user_info, paginate_teams, expect_revoke, ) token_url = GitHubProvider._TOKEN_URL respx_mock.post(token_url).mock(side_effect=mock.post_token) emails_url = GitHubProvider._EMAILS_URL respx_mock.get(emails_url).mock(side_effect=mock.get_emails) teams_url = GitHubProvider._TEAMS_URL respx_mock.get(url__startswith=teams_url).mock(side_effect=mock.get_teams) respx_mock.get(GitHubProvider._USER_URL).mock(side_effect=mock.get_user)
async def test_connection_error(tmp_path: Path, client: AsyncClient, respx_mock: respx.Router) -> None: config = await configure(tmp_path, "oidc") assert config.oidc return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) # Register a connection error for the callback request to the OIDC # provider and check that an appropriate error is shown to the user. token_url = config.oidc.token_url respx_mock.post(token_url).mock(side_effect=ConnectError) r = await client.get("/login", params={ "code": "some-code", "state": query["state"][0] }) assert r.status_code == 403 assert "Cannot contact authentication provider" in r.text
def test_side_effect_with_route_kwarg(): router = Router() def foobar(request, route, slug): response = httpx.Response(201, json={"id": route.call_count + 1, "slug": slug}) if route.call_count > 0: route.mock(return_value=httpx.Response(501)) return response router.post(path__regex=r"/(?P<slug>\w+)/").mock(side_effect=foobar) request = httpx.Request("POST", "https://foo.bar/baz/") response = router.handler(request) assert response.status_code == 201 assert response.json() == {"id": 1, "slug": "baz"} response = router.handler(request) assert response.status_code == 201 assert response.json() == {"id": 2, "slug": "baz"} response = router.handler(request) assert response.status_code == 501
async def test_callback_error( tmp_path: Path, client: AsyncClient, respx_mock: respx.Router, caplog: LogCaptureFixture, ) -> None: """Test an error return from the OIDC token endpoint.""" config = await configure(tmp_path, "oidc") assert config.oidc return_url = "https://example.com/foo" r = await client.get("/login", params={"rd": return_url}) assert r.status_code == 307 url = urlparse(r.headers["Location"]) query = parse_qs(url.query) # Build an error response to return from the OIDC token URL and register # it as a result. response = { "error": "error_code", "error_description": "description", } respx_mock.post(config.oidc.token_url).respond(400, json=response) # Simulate the return from the OpenID Connect provider. caplog.clear() r = await client.get( "/oauth2/callback", params={ "code": "some-code", "state": query["state"][0] }, ) assert r.status_code == 403 assert "error_code: description" in r.text assert parse_log(caplog) == [ { "event": f"Retrieving ID token from {config.oidc.token_url}", "httpRequest": { "requestMethod": "GET", "requestUrl": ANY, "remoteIp": "127.0.0.1", }, "return_url": return_url, "severity": "info", }, { "error": "error_code: description", "event": "Authentication provider failed", "httpRequest": { "requestMethod": "GET", "requestUrl": ANY, "remoteIp": "127.0.0.1", }, "return_url": return_url, "severity": "warning", }, ] # Change the mock error response to not contain an error. We should then # internally raise the exception for the return status, which should # translate into an internal server error. respx_mock.post(config.oidc.token_url).respond(400, json={"foo": "bar"}) r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) r = await client.get( "/oauth2/callback", params={ "code": "some-code", "state": query["state"][0] }, ) assert r.status_code == 403 assert "Cannot contact authentication provider" in r.text # Now try a reply that returns 200 but doesn't have the field we # need. respx_mock.post(config.oidc.token_url).respond(json={"foo": "bar"}) r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) r = await client.get( "/oauth2/callback", params={ "code": "some-code", "state": query["state"][0] }, ) assert r.status_code == 403 assert "No id_token in token reply" in r.text # Return invalid JSON, which should raise an error during JSON decoding. respx_mock.post(config.oidc.token_url).respond(content=b"foo") r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) r = await client.get( "/oauth2/callback", params={ "code": "some-code", "state": query["state"][0] }, ) assert r.status_code == 403 assert "not valid JSON" in r.text # Finally, return invalid JSON and an error reply. respx_mock.post(config.oidc.token_url).respond(400, content=b"foo") r = await client.get("/login", params={"rd": return_url}) query = parse_qs(urlparse(r.headers["Location"]).query) r = await client.get( "/oauth2/callback", params={ "code": "some-code", "state": query["state"][0] }, ) assert r.status_code == 403 assert "Cannot contact authentication provider" in r.text
def test_multiple_pattern_values_type_error(): router = Router() with pytest.raises(TypeError, match="Got multiple values for pattern 'method'"): router.post(method__in=("PUT", "PATCH")) with pytest.raises(TypeError, match="Got multiple values for pattern 'url'"): router.get("https://foo.bar", url__regex=r"https://example.org$")
def test_rollback(): router = Router() route = router.get("https://foo.bar/") % 404 pattern = route.pattern assert route.name is None router.snapshot() # 1. get 404 route.return_value = httpx.Response(200) router.post("https://foo.bar/").mock( side_effect=[httpx.Response(400), httpx.Response(201)] ) router.snapshot() # 2. get 200, post _route = router.get("https://foo.bar/", name="foobar") _route = router.get("https://foo.bar/baz/", name="foobar") assert _route is route assert route.name == "foobar" assert route.pattern != pattern route.return_value = httpx.Response(418) request = httpx.Request("GET", "https://foo.bar/baz/") response = router.handler(request) assert response.status_code == 418 request = httpx.Request("POST", "https://foo.bar") response = router.handler(request) assert response.status_code == 400 assert len(router.routes) == 2 assert router.calls.call_count == 2 assert route.call_count == 1 assert route.return_value.status_code == 418 router.snapshot() # 3. directly rollback, should be identical router.rollback() assert len(router.routes) == 2 assert router.calls.call_count == 2 assert route.call_count == 1 assert route.return_value.status_code == 418 router.patch("https://foo.bar/") assert len(router.routes) == 3 route.rollback() # get 200 assert router.calls.call_count == 2 assert route.call_count == 0 assert route.return_value.status_code == 200 request = httpx.Request("GET", "https://foo.bar") response = router.handler(request) assert response.status_code == 200 router.rollback() # 2. get 404, post request = httpx.Request("POST", "https://foo.bar") response = router.handler(request) assert response.status_code == 400 assert len(router.routes) == 2 router.rollback() # 1. get 404 assert len(router.routes) == 1 assert router.calls.call_count == 0 assert route.return_value is None router.rollback() # Empty inital state assert len(router.routes) == 0 assert route.return_value is None # Idempotent route.rollback() router.rollback() assert len(router.routes) == 0 assert route.name is None assert route.pattern == pattern assert route.return_value is None