|
1 | 1 | from urllib.parse import parse_qs, urlsplit |
2 | 2 |
|
3 | 3 | import httpx |
| 4 | +import pytest |
4 | 5 | from starlette.testclient import TestClient |
5 | 6 |
|
6 | 7 | from feedback.app import create_app |
@@ -58,6 +59,84 @@ def handler(request: httpx.Request) -> httpx.Response: |
58 | 59 | assert exchanged["repository_id"] == ["R_repo"] |
59 | 60 |
|
60 | 61 |
|
| 62 | +def test_exchange_bounds_nonexpiring_github_token_to_eight_hours(config: Config) -> None: |
| 63 | + http = httpx.AsyncClient( |
| 64 | + transport=httpx.MockTransport( |
| 65 | + lambda _: httpx.Response(200, json={"access_token": "ghu_user"}) |
| 66 | + ) |
| 67 | + ) |
| 68 | + oauth = OAuthClient( |
| 69 | + client_id="Iv1.client", |
| 70 | + client_secret="client-secret", |
| 71 | + callback_url=config.service.oauth_callback, |
| 72 | + signer=StateSigner(b"k" * 32, clock=lambda: 1_000), |
| 73 | + http=http, |
| 74 | + clock=lambda: 1_000, |
| 75 | + ) |
| 76 | + grants = CreationGrantSigner(b"k" * 32, clock=lambda: 1_000) |
| 77 | + with TestClient(create_app(config, clock=lambda: 1_000, oauth=oauth, grants=grants)) as client: |
| 78 | + authorize = client.post( |
| 79 | + "/v1/sites/cpp-social/oauth/authorize", |
| 80 | + headers={"Origin": "https://cpp.social"}, |
| 81 | + json={"challenge": pkce_challenge(VERIFIER), "nonce": NONCE}, |
| 82 | + ) |
| 83 | + exchange = client.post( |
| 84 | + "/v1/sites/cpp-social/oauth/exchange", |
| 85 | + headers={"Origin": "https://cpp.social"}, |
| 86 | + json={ |
| 87 | + "code": "temporary-code", |
| 88 | + "state": authorize.json()["state"], |
| 89 | + "verifier": VERIFIER, |
| 90 | + }, |
| 91 | + ) |
| 92 | + |
| 93 | + assert exchange.status_code == 200 |
| 94 | + assert exchange.json()["expires_at"] == 29_800 |
| 95 | + |
| 96 | + |
| 97 | +def test_exchange_logs_safe_upstream_failure_details( |
| 98 | + config: Config, caplog: pytest.LogCaptureFixture |
| 99 | +) -> None: |
| 100 | + http = httpx.AsyncClient( |
| 101 | + transport=httpx.MockTransport( |
| 102 | + lambda _: httpx.Response( |
| 103 | + 200, |
| 104 | + headers={"x-github-request-id": "request-123"}, |
| 105 | + json={"error": "bad_verification_code", "error_description": "secret detail"}, |
| 106 | + ) |
| 107 | + ) |
| 108 | + ) |
| 109 | + oauth = OAuthClient( |
| 110 | + client_id="Iv1.client", |
| 111 | + client_secret="client-secret", |
| 112 | + callback_url=config.service.oauth_callback, |
| 113 | + signer=StateSigner(b"k" * 32, clock=lambda: 1_000), |
| 114 | + http=http, |
| 115 | + clock=lambda: 1_000, |
| 116 | + ) |
| 117 | + grants = CreationGrantSigner(b"k" * 32, clock=lambda: 1_000) |
| 118 | + with TestClient(create_app(config, clock=lambda: 1_000, oauth=oauth, grants=grants)) as client: |
| 119 | + authorize = client.post( |
| 120 | + "/v1/sites/cpp-social/oauth/authorize", |
| 121 | + headers={"Origin": "https://cpp.social"}, |
| 122 | + json={"challenge": pkce_challenge(VERIFIER), "nonce": NONCE}, |
| 123 | + ) |
| 124 | + response = client.post( |
| 125 | + "/v1/sites/cpp-social/oauth/exchange", |
| 126 | + headers={"Origin": "https://cpp.social"}, |
| 127 | + json={ |
| 128 | + "code": "temporary-code", |
| 129 | + "state": authorize.json()["state"], |
| 130 | + "verifier": VERIFIER, |
| 131 | + }, |
| 132 | + ) |
| 133 | + |
| 134 | + assert response.status_code == 400 |
| 135 | + assert "upstream_code=bad_verification_code" in caplog.text |
| 136 | + assert "github_request_id=request-123" in caplog.text |
| 137 | + assert "secret detail" not in caplog.text |
| 138 | + |
| 139 | + |
61 | 140 | def test_oauth_rejects_origin_content_type_and_duplicate_fields(config: Config) -> None: |
62 | 141 | oauth = OAuthClient( |
63 | 142 | client_id="Iv1.client", |
|
0 commit comments