Esempio n. 1
0
def test_side_effect_list():
    router = Router()
    route = router.get("https://foo.bar/").mock(
        return_value=httpx.Response(409),
        side_effect=[httpx.Response(404), httpcore.NetworkError, httpx.Response(201)],
    )

    request = httpx.Request("GET", "https://foo.bar")
    response = router.handler(request)
    assert response.status_code == 404
    assert response.request == request

    request = httpx.Request("GET", "https://foo.bar")
    with pytest.raises(httpcore.NetworkError):
        router.handler(request)

    request = httpx.Request("GET", "https://foo.bar")
    response = router.handler(request)
    assert response.status_code == 201
    assert response.request == request

    with pytest.raises(StopIteration):
        request = httpx.Request("GET", "https://foo.bar")
        router.handler(request)

    route.side_effect = None
    request = httpx.Request("GET", "https://foo.bar")
    response = router.handler(request)
    assert response.status_code == 409
    assert response.request == request
Esempio n. 2
0
async def test_verify_error(tmp_path: Path, client: AsyncClient,
                            respx_mock: respx.Router) -> None:
    config = await configure(tmp_path, "oidc")
    token = await create_upstream_oidc_token(groups=["admin"])
    assert config.oidc
    issuer = config.oidc.issuer
    config_url = urljoin(issuer, "/.well-known/openid-configuration")
    jwks_url = urljoin(issuer, "/.well-known/jwks.json")
    respx_mock.get(config_url).respond(404)
    respx_mock.get(jwks_url).respond(404)
    await mock_oidc_provider_token(respx_mock, "some-code", token)
    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)

    # Returning from OpenID Connect login should fail because we haven't
    # registered the signing key, and therefore attempting to retrieve it will
    # fail, causing a token verification error.
    r = await client.get("/login",
                         params={
                             "code": "some-code",
                             "state": query["state"][0]
                         })
    assert r.status_code == 403
    assert "token verification failed" in r.text
Esempio n. 3
0
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() % []
Esempio n. 4
0
async def mock_oidc_provider_config(
    respx_mock: respx.Router,
    keypair: Optional[RSAKeyPair] = None,
    kid: Optional[str] = None,
) -> None:
    """Mock out the API for the upstream OpenID Connect provider.

    Parameters
    ----------
    respx_mock : `respx.Router`
        The mock router.
    keypair : `gafaelfawr.keypair.RSAKeyPair`, optional
        The keypair to use.  Defaults to the configured issuer keypair.
    kid : `str`, optional
        The key ID to return.  Defaults to the first key ID in the
        configuration.
    """
    config = await config_dependency()
    assert config.oidc
    mock = MockOIDCConfig(config, keypair, kid)
    issuer = config.oidc.issuer
    config_url = urljoin(issuer, "/.well-known/openid-configuration")
    respx_mock.get(config_url).mock(side_effect=mock.get_config)
    jwks_url = urljoin(issuer, "/jwks.json")
    respx_mock.get(jwks_url).mock(side_effect=mock.get_jwks)
Esempio n. 5
0
def test_url_pattern_lookup(lookups, url, expected):
    router = Router(assert_all_mocked=False)
    route = router.get(**lookups) % 418
    request = httpx.Request("GET", url)
    response = router.handler(request)
    assert bool(response.status_code == 418) is expected
    assert route.called is expected
Esempio n. 6
0
async def test_empty_router():
    router = Router()

    request = httpx.Request("GET", "https://example.org/")
    with pytest.raises(AllMockedAssertionError):
        router.resolve(request)

    with pytest.raises(AllMockedAssertionError):
        await router.aresolve(request)
Esempio n. 7
0
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
Esempio n. 8
0
async def test_async_side_effect():
    router = Router()

    async def effect(request):
        return httpx.Response(204)

    router.get("https://foo.bar/").mock(side_effect=effect)

    request = httpx.Request("GET", "https://foo.bar/")
    response = await router.async_handler(request)
    assert response.status_code == 204
Esempio n. 9
0
def test_base_url(url, lookups, expected):
    router = Router(base_url="https://foo.bar/api/", assert_all_mocked=False)
    route = router.get(**lookups).respond(201)

    request = httpx.Request("GET", url)
    resolved = router.resolve(request)

    assert bool(resolved.route is route) is expected
    if expected:
        assert bool(resolved.response.status_code == 201) is expected
    else:
        assert resolved.response.status_code == 200  # auto mocked
Esempio n. 10
0
async def test_empty_router__auto_mocked():
    router = Router(assert_all_mocked=False)

    request = httpx.Request("GET", "https://example.org/")
    resolved = router.resolve(request)

    assert resolved.route is None
    assert resolved.response.status_code == 200

    resolved = await router.aresolve(request)

    assert resolved.route is None
    assert resolved.response.status_code == 200
Esempio n. 11
0
def test_resolve(args, kwargs, expected):
    router = Router(assert_all_mocked=False)
    route = router.route(*args, **kwargs).respond(status_code=201)

    request = httpx.Request(
        "GET", "https://foo.bar/baz/", cookies={"foo": "bar", "ham": "spam"}
    )
    resolved = router.resolve(request)

    assert bool(resolved.route is route) is expected
    if expected:
        assert bool(resolved.response.status_code == 201) is expected
    else:
        assert resolved.response.status_code == 200  # auto mocked
Esempio n. 12
0
def test_side_effect_with_reserved_route_kwarg():
    router = Router()

    def foobar(request, route):
        assert isinstance(route, Route)
        return httpx.Response(202)

    router.get(path__regex=r"/(?P<route>\w+)/").mock(side_effect=foobar)

    with warnings.catch_warnings(record=True) as w:
        request = httpx.Request("GET", "https://foo.bar/baz/")
        response = router.handler(request)
        assert response.status_code == 202
        assert len(w) == 1
Esempio n. 13
0
def test_base_url(url, expected):
    router = Router(base_url="https://foo.bar/", assert_all_mocked=False)
    route = router.route(method="GET", url="/baz/").respond(201)

    request = httpx.Request("GET", url)
    matched_route, response = router.match(request)

    assert bool(matched_route is route) is expected
    if expected:
        assert bool(response.status_code == 201) is expected
    else:
        assert not response

    response = router.resolve(request)
    assert bool(response.status_code == 201) is expected
Esempio n. 14
0
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)
Esempio n. 15
0
def test_pass_through():
    router = Router(assert_all_mocked=False)
    route = router.route(method="GET").pass_through()

    request = httpx.Request("GET", "https://foo.bar/baz/")
    matched_route, response = router.match(request)

    assert matched_route is route
    assert matched_route.is_pass_through
    assert response is request

    route.pass_through(False)
    matched_route, response = router.match(request)

    assert matched_route is route
    assert not matched_route.is_pass_through
    assert response is not None
Esempio n. 16
0
async def test_http_client(respx_mock: respx.Router) -> None:
    app = FastAPI()
    respx_mock.get("https://www.google.com").respond(200)

    @app.get("/")
    async def handler(
        http_client: AsyncClient = Depends(http_client_dependency),
    ) -> Dict[str, str]:
        assert isinstance(http_client, AsyncClient)
        await http_client.get("https://www.google.com")
        return {}

    @app.on_event("shutdown")
    async def shutdown_event() -> None:
        await http_client_dependency.aclose()

    async with LifespanManager(app):
        async with AsyncClient(app=app, base_url="http://example.com") as c:
            r = await c.get("/")
    assert r.status_code == 200
Esempio n. 17
0
def test_side_effect_decorator():
    router = Router()

    @router.route(host="ham.spam", path__regex=r"/(?P<slug>\w+)/")
    def foobar(request, slug):
        return httpx.Response(200, json={"slug": slug})

    @router.post("https://example.org/")
    def example(request):
        return httpx.Response(201, json={"message": "OK"})

    request = httpx.Request("GET", "https://ham.spam/egg/")
    response = router.resolve(request)
    assert response.status_code == 200
    assert response.json() == {"slug": "egg"}

    request = httpx.Request("POST", "https://example.org/")
    response = router.resolve(request)
    assert response.status_code == 201
    assert response.json() == {"message": "OK"}
Esempio n. 18
0
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
Esempio n. 19
0
def test_side_effect_no_match():
    router = Router()

    def no_match(request):
        request.respx_was_here = True
        return None

    router.get(url__startswith="https://foo.bar/").mock(side_effect=no_match)
    router.get(url__eq="https://foo.bar/baz/").mock(return_value=httpx.Response(204))

    request = httpx.Request("GET", "https://foo.bar/baz/")
    response = router.handler(request)
    assert response.status_code == 204
    assert response.request.respx_was_here is True
Esempio n. 20
0
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)
Esempio n. 21
0
def test_side_effect_list():
    router = Router()
    router.get("https://foo.bar/").side_effect(
        [httpx.Response(404), httpx.Response(201)])

    request = httpx.Request("GET", "https://foo.bar")
    response = router.resolve(request)
    assert response.status_code == 404
    assert response.request == request

    request = httpx.Request("GET", "https://foo.bar")
    response = router.resolve(request)
    assert response.status_code == 201
    assert response.request == request
Esempio n. 22
0
def test_pass_through():
    router = Router(assert_all_mocked=False)
    route = router.get("https://foo.bar/", path="/baz/").pass_through()

    request = httpx.Request("GET", "https://foo.bar/baz/")
    matched_route, response = router.match(request)

    assert matched_route is route
    assert matched_route.is_pass_through
    assert response is request

    with pytest.raises(PassThrough):
        router.handler(request)

    route.pass_through(False)
    matched_route, response = router.match(request)

    assert matched_route is route
    assert not matched_route.is_pass_through
    assert response is not None
Esempio n. 23
0
def test_pass_through():
    router = Router(assert_all_mocked=False)
    route = router.get("https://foo.bar/", path="/baz/").pass_through()

    request = httpx.Request("GET", "https://foo.bar/baz/")
    with pytest.raises(PassThrough) as exc_info:
        router.resolve(request)

    assert exc_info.value.origin is route
    assert exc_info.value.origin.is_pass_through

    route.pass_through(False)
    resolved = router.resolve(request)

    assert resolved.route is route
    assert not resolved.route.is_pass_through
    assert resolved.response is not None
Esempio n. 24
0
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
Esempio n. 25
0
def test_side_effect_exception():
    router = Router()
    router.get("https://foo.bar/").side_effect(httpx.ConnectError)
    router.get("https://ham.spam/").side_effect(httpcore.NetworkError)
    router.get("https://egg.plant/").side_effect(httpcore.NetworkError())

    request = httpx.Request("GET", "https://foo.bar")
    with pytest.raises(httpx.ConnectError) as e:
        router.resolve(request)
    assert e.value.request == request

    request = httpx.Request("GET", "https://ham.spam")
    with pytest.raises(httpcore.NetworkError) as e:
        router.resolve(request)

    request = httpx.Request("GET", "https://egg.plant")
    with pytest.raises(httpcore.NetworkError) as e:
        router.resolve(request)
Esempio n. 26
0
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
Esempio n. 27
0
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
Esempio n. 28
0
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$")
Esempio n. 29
0
async def test_key_retrieval(tmp_path: Path, respx_mock: respx.Router,
                             factory: ComponentFactory) -> None:
    config = await configure(tmp_path, "oidc-no-kids")
    factory.reconfigure(config)
    assert config.oidc
    verifier = factory.create_token_verifier()

    # Initial working JWKS configuration.
    jwks = config.issuer.keypair.public_key_as_jwks("some-kid")

    # Register that handler at the well-known JWKS endpoint.  This will return
    # a connection refused from the OpenID Connect endpoint.
    jwks_url = urljoin(config.oidc.issuer, "/.well-known/jwks.json")
    oidc_url = urljoin(config.oidc.issuer, "/.well-known/openid-configuration")
    respx_mock.get(jwks_url).respond(json=jwks.dict())
    respx_mock.get(oidc_url).respond(404)

    # Check token verification with this configuration.
    token = await create_upstream_oidc_token(kid="some-kid")
    assert await verifier.verify_oidc_token(token)

    # Wrong algorithm for the key.
    jwks.keys[0].alg = "ES256"
    respx_mock.get(jwks_url).respond(json=jwks.dict())
    with pytest.raises(UnknownAlgorithmException):
        await verifier.verify_oidc_token(token)

    # Should go back to working if we fix the algorithm and add more keys.
    # Add an explicit 404 from the OpenID connect endpoint.
    respx_mock.get(oidc_url).respond(404)
    jwks.keys[0].alg = ALGORITHM
    keypair = RSAKeyPair.generate()
    jwks.keys.insert(0, keypair.public_key_as_jwks("a-kid").keys[0])
    respx_mock.get(jwks_url).respond(json=jwks.dict())
    assert await verifier.verify_oidc_token(token)

    # Try with a new key ID and return a malformed reponse.
    respx_mock.get(jwks_url).respond(json=["foo"])
    token = await create_upstream_oidc_token(kid="malformed")
    with pytest.raises(FetchKeysException):
        await verifier.verify_oidc_token(token)

    # Return a 404 error.
    respx_mock.get(jwks_url).respond(404)
    with pytest.raises(FetchKeysException):
        await verifier.verify_oidc_token(token)

    # Fix the JWKS handler but register a malformed URL as the OpenID Connect
    # configuration endpoint, which should be checked first.
    jwks.keys[1].kid = "another-kid"
    respx_mock.get(jwks_url).respond(json=jwks.dict())
    respx_mock.get(oidc_url).respond(json=["foo"])
    token = await create_upstream_oidc_token(kid="another-kid")
    with pytest.raises(FetchKeysException):
        await verifier.verify_oidc_token(token)

    # Try again with a working OpenID Connect configuration.
    respx_mock.get(oidc_url).respond(json={"jwks_uri": jwks_url})
    assert await verifier.verify_oidc_token(token)