|
| 1 | +"""Tests for Bugzilla webhook actor authorization.""" |
| 2 | + |
| 3 | +from unittest.mock import AsyncMock |
| 4 | + |
| 5 | +import httpx |
| 6 | +from app.bugzilla_authorization import AUTHORIZED_GROUP_NAME, BugzillaAuthorizer |
| 7 | + |
| 8 | +BUGZILLA_API_KEY = "test-bugzilla-api-key" |
| 9 | + |
| 10 | + |
| 11 | +def _authorizer(member: bool) -> tuple[BugzillaAuthorizer, AsyncMock]: |
| 12 | + """An authorizer whose membership lookup is stubbed to ``member``.""" |
| 13 | + authorizer = BugzillaAuthorizer( |
| 14 | + "https://bugzilla.example.com/rest", |
| 15 | + BUGZILLA_API_KEY, |
| 16 | + AUTHORIZED_GROUP_NAME, |
| 17 | + ) |
| 18 | + lookup = AsyncMock(return_value=member) |
| 19 | + authorizer._is_user_in_group = lookup |
| 20 | + return authorizer, lookup |
| 21 | + |
| 22 | + |
| 23 | +async def test_is_authorized_caches_positive_lookup(): |
| 24 | + authorizer, lookup = _authorizer(member=True) |
| 25 | + |
| 26 | + assert await authorizer.is_authorized("dev@mozilla.com") is True |
| 27 | + assert await authorizer.is_authorized("dev@mozilla.com") is True |
| 28 | + lookup.assert_awaited_once_with("dev@mozilla.com", AUTHORIZED_GROUP_NAME) |
| 29 | + |
| 30 | + |
| 31 | +async def test_is_authorized_caches_negative_lookup(): |
| 32 | + authorizer, lookup = _authorizer(member=False) |
| 33 | + |
| 34 | + assert await authorizer.is_authorized("someone@example.com") is False |
| 35 | + assert await authorizer.is_authorized("someone@example.com") is False |
| 36 | + lookup.assert_awaited_once_with("someone@example.com", AUTHORIZED_GROUP_NAME) |
| 37 | + |
| 38 | + |
| 39 | +async def test_is_authorized_normalizes_login_case(): |
| 40 | + authorizer, lookup = _authorizer(member=True) |
| 41 | + |
| 42 | + assert await authorizer.is_authorized("Dev@Mozilla.com") is True |
| 43 | + assert await authorizer.is_authorized("dev@mozilla.com") is True |
| 44 | + lookup.assert_awaited_once_with("dev@mozilla.com", AUTHORIZED_GROUP_NAME) |
| 45 | + |
| 46 | + |
| 47 | +# --- the membership lookup itself, on BMO's captured payload shapes --- |
| 48 | + |
| 49 | + |
| 50 | +def _http_authorizer( |
| 51 | + monkeypatch, json_body: dict |
| 52 | +) -> tuple[BugzillaAuthorizer, list[httpx.Request]]: |
| 53 | + """An authorizer whose HTTP layer replays ``json_body``, capturing requests.""" |
| 54 | + requests: list[httpx.Request] = [] |
| 55 | + |
| 56 | + def handler(request: httpx.Request) -> httpx.Response: |
| 57 | + requests.append(request) |
| 58 | + return httpx.Response(200, json=json_body) |
| 59 | + |
| 60 | + real_async_client = httpx.AsyncClient |
| 61 | + monkeypatch.setattr( |
| 62 | + httpx, |
| 63 | + "AsyncClient", |
| 64 | + lambda **kwargs: real_async_client( |
| 65 | + transport=httpx.MockTransport(handler), **kwargs |
| 66 | + ), |
| 67 | + ) |
| 68 | + authorizer = BugzillaAuthorizer( |
| 69 | + "https://bugzilla.example.com/rest", |
| 70 | + BUGZILLA_API_KEY, |
| 71 | + AUTHORIZED_GROUP_NAME, |
| 72 | + ) |
| 73 | + return authorizer, requests |
| 74 | + |
| 75 | + |
| 76 | +async def test_lookup_authorizes_group_member(monkeypatch): |
| 77 | + authorizer, requests = _http_authorizer( |
| 78 | + monkeypatch, {"users": [{"name": "dev@mozilla.com"}], "faults": []} |
| 79 | + ) |
| 80 | + |
| 81 | + assert await authorizer.is_authorized("dev@mozilla.com") is True |
| 82 | + |
| 83 | + request = requests[0] |
| 84 | + assert request.url.host == "bugzilla.example.com" |
| 85 | + assert request.url.path == "/rest/user" |
| 86 | + assert request.url.params["names"] == "dev@mozilla.com" |
| 87 | + assert request.url.params["groups"] == AUTHORIZED_GROUP_NAME |
| 88 | + assert request.url.params["permissive"] == "1" |
| 89 | + assert request.headers["X-Bugzilla-API-Key"] == BUGZILLA_API_KEY |
| 90 | + |
| 91 | + |
| 92 | +async def test_lookup_rejects_non_member(monkeypatch): |
| 93 | + # An existing account outside the group is filtered out server-side |
| 94 | + # (live BMO shape: empty ``users``, empty ``faults``). |
| 95 | + authorizer, _ = _http_authorizer(monkeypatch, {"users": [], "faults": []}) |
| 96 | + assert await authorizer.is_authorized("outsider@example.com") is False |
| 97 | + |
| 98 | + |
| 99 | +async def test_lookup_rejects_unknown_user(monkeypatch): |
| 100 | + # With permissive=1, BMO reports an unknown login as a 200 with the error |
| 101 | + # in ``faults`` and an empty ``users`` list (live BMO shape). |
| 102 | + authorizer, _ = _http_authorizer( |
| 103 | + monkeypatch, |
| 104 | + { |
| 105 | + "users": [], |
| 106 | + "faults": [ |
| 107 | + { |
| 108 | + "error": True, |
| 109 | + "name": "ghost@example.com", |
| 110 | + "message": "There is no user named 'ghost@example.com'.", |
| 111 | + } |
| 112 | + ], |
| 113 | + }, |
| 114 | + ) |
| 115 | + assert await authorizer.is_authorized("ghost@example.com") is False |
0 commit comments