|
1 | | -from unittest.mock import AsyncMock, MagicMock, patch |
| 1 | +from contextlib import AbstractContextManager, nullcontext |
| 2 | +from unittest.mock import AsyncMock, MagicMock, Mock, patch |
2 | 3 |
|
3 | 4 | import pytest |
4 | 5 | from pydantic import HttpUrl |
5 | 6 |
|
6 | | -from blueapi.config import OpaConfig |
| 7 | +from blueapi.config import OIDCConfig, OpaConfig, ServiceAccount |
7 | 8 | from blueapi.service.authorization import ( |
8 | 9 | OpaClient, |
| 10 | + validate_tiled_config, |
9 | 11 | ) |
10 | 12 |
|
11 | 13 | # Reusable client patch decorator |
|
20 | 22 | def opa_config() -> OpaConfig: |
21 | 23 | return OpaConfig( |
22 | 24 | root=HttpUrl("http://auth.example.com"), |
| 25 | + tiled_service_account_check="/auth/tiled", |
23 | 26 | ) |
24 | 27 |
|
25 | 28 |
|
| 29 | +@patch_client_session |
| 30 | +@pytest.mark.parametrize( |
| 31 | + "result,context", |
| 32 | + [ |
| 33 | + (False, pytest.raises(ValueError, match="Tiled service account is not valid ")), |
| 34 | + (True, nullcontext()), |
| 35 | + ], |
| 36 | +) |
| 37 | +async def test_tiled_service_account( |
| 38 | + session: MagicMock, |
| 39 | + opa_config: OpaConfig, |
| 40 | + result: bool, |
| 41 | + context: AbstractContextManager, |
| 42 | +): |
| 43 | + session.return_value.post = AsyncMock( |
| 44 | + return_value=MagicMock(json=AsyncMock(return_value={"result": result})) |
| 45 | + ) |
| 46 | + |
| 47 | + client = OpaClient(instrument="p99", config=opa_config) |
| 48 | + |
| 49 | + session.assert_called_once_with(base_url="http://auth.example.com/") |
| 50 | + with context: |
| 51 | + await client.require_tiled_service_account(token="foo_bar") |
| 52 | + session().post.assert_called_once_with( |
| 53 | + "/auth/tiled", |
| 54 | + json={"input": {"token": "foo_bar", "beamline": "p99", "audience": "account"}}, |
| 55 | + ) |
| 56 | + |
| 57 | + |
| 58 | +@patch_client_session |
| 59 | +async def test_exception_raised_when_opa_fails( |
| 60 | + session: MagicMock, opa_config: OpaConfig |
| 61 | +): |
| 62 | + session.return_value.post = AsyncMock(side_effect=RuntimeError("Connection failed")) |
| 63 | + async with OpaClient.for_config("p45", opa_config) as client: |
| 64 | + assert client is not None |
| 65 | + with pytest.raises(RuntimeError, match="Connection failed"): |
| 66 | + await client.require_tiled_service_account(token="foo_bar") |
| 67 | + |
| 68 | + |
26 | 69 | @patch_client_session |
27 | 70 | async def test_session_closed(session: MagicMock, opa_config: OpaConfig): |
28 | 71 | async with OpaClient.for_config("p45", opa_config): |
@@ -60,3 +103,49 @@ async def test_opa_adds_input_fields(session: MagicMock, opa_config: OpaConfig): |
60 | 103 | "foo/bar", |
61 | 104 | json={"input": {"beamline": "p45", "audience": "account", "foo": "bar"}}, |
62 | 105 | ) |
| 106 | + |
| 107 | + |
| 108 | +async def test_validate_tiled_config(): |
| 109 | + opa = MagicMock(spec=OpaClient) |
| 110 | + tiled = ServiceAccount() |
| 111 | + oidc = Mock(spec=OIDCConfig) |
| 112 | + oidc.token_endpoint = "token-endpoint" |
| 113 | + with patch("blueapi.service.authorization.TiledAuth") as auth: |
| 114 | + auth.return_value.get_access_token.return_value = "tiled-token" |
| 115 | + await validate_tiled_config(tiled, oidc, opa) |
| 116 | + |
| 117 | + auth.assert_called_once_with(tiled) |
| 118 | + opa.require_tiled_service_account.assert_called_once_with("tiled-token") |
| 119 | + |
| 120 | + |
| 121 | +@pytest.mark.parametrize( |
| 122 | + "tiled_auth,oidc,opa_client", |
| 123 | + [ |
| 124 | + (None, None, MagicMock(spec=OpaClient)), |
| 125 | + ( |
| 126 | + None, |
| 127 | + OIDCConfig(well_known_url="http://example.com", client_id="test-client"), |
| 128 | + MagicMock(spec=OpaClient), |
| 129 | + ), |
| 130 | + ("api_key", None, MagicMock(spec=OpaClient)), |
| 131 | + ( |
| 132 | + "api_key", |
| 133 | + OIDCConfig(well_known_url="http://example.com", client_id="test-client"), |
| 134 | + MagicMock(spec=OpaClient), |
| 135 | + ), |
| 136 | + (ServiceAccount(), None, MagicMock(spec=OpaClient)), |
| 137 | + ( |
| 138 | + ServiceAccount(), |
| 139 | + OIDCConfig(well_known_url="http://example.com", client_id="test-client"), |
| 140 | + None, |
| 141 | + ), |
| 142 | + ], |
| 143 | +) |
| 144 | +async def test_validate_tiled_config_with_missing_config( |
| 145 | + tiled_auth: ServiceAccount | str | None, |
| 146 | + oidc: OIDCConfig | None, |
| 147 | + opa_client: MagicMock | None, |
| 148 | +): |
| 149 | + assert await validate_tiled_config(tiled_auth, oidc, opa_client) is None |
| 150 | + if opa_client is not None: |
| 151 | + opa_client.require_tiled_service_account.assert_not_called() |
0 commit comments