1- import time
1+ import base64
22from collections .abc import AsyncIterator , Iterator
33
4- import jwt
4+ import httpx
55import pytest
66from alembic import command
77from alembic .config import Config as AlembicConfig
8- from cryptography .hazmat .primitives import serialization
9- from cryptography .hazmat .primitives .asymmetric import rsa
108from sqlalchemy .ext .asyncio import (
119 AsyncEngine ,
1210 AsyncSession ,
@@ -33,7 +31,7 @@ def _key(item):
3331@pytest .fixture (scope = "session" )
3432def postgres_url () -> Iterator [str ]:
3533 with PostgresContainer ("postgres:16-alpine" ) as pg :
36- sync_url = pg .get_connection_url () # postgresql+psycopg2://...
34+ sync_url = pg .get_connection_url ()
3735 async_url = sync_url .replace ("postgresql+psycopg2://" , "postgresql+asyncpg://" )
3836 yield async_url
3937
@@ -60,72 +58,64 @@ async def session(engine: AsyncEngine) -> AsyncIterator[AsyncSession]:
6058 await s .rollback ()
6159
6260
63- @pytest .fixture (scope = "session" )
64- def signing_key ():
65- key = rsa .generate_private_key (public_exponent = 65537 , key_size = 2048 )
66- priv = key .private_bytes (
67- encoding = serialization .Encoding .PEM ,
68- format = serialization .PrivateFormat .PKCS8 ,
69- encryption_algorithm = serialization .NoEncryption (),
70- )
71- pub = key .public_key ()
72- return priv , pub
61+ # ---- CWA mock --------------------------------------------------------------
7362
7463
7564@pytest .fixture
76- def issuer () -> str :
77- return "https://test-iss.example/"
65+ def cwa_users () -> dict [str , str ]:
66+ """Mutable per-test dict of valid CWA username → password."""
67+ return {"alice" : "alicepass" , "bob" : "bobpass" }
7868
7969
8070@pytest .fixture
81- def audience () -> str :
82- return "test-aud"
71+ def cwa_transport (cwa_users : dict [str , str ]) -> httpx .MockTransport :
72+ def handler (request : httpx .Request ) -> httpx .Response :
73+ if not request .url .path .endswith ("/opds" ):
74+ return httpx .Response (404 )
75+ auth = request .headers .get ("authorization" , "" )
76+ if not auth .lower ().startswith ("basic " ):
77+ return httpx .Response (401 )
78+ try :
79+ decoded = base64 .b64decode (auth [6 :].strip ()).decode ("utf-8" )
80+ user , pw = decoded .split (":" , 1 )
81+ except Exception :
82+ return httpx .Response (401 )
83+ if cwa_users .get (user ) == pw :
84+ return httpx .Response (200 , text = "<feed/>" )
85+ return httpx .Response (401 )
86+
87+ return httpx .MockTransport (handler )
8388
8489
8590@pytest .fixture
86- def make_token (signing_key , issuer , audience ):
87- priv , _ = signing_key
88-
89- def _make (sub : str , ttl : int = 60 , ** extra ) -> str :
90- now = int (time .time ())
91- claims = {
92- "sub" : sub ,
93- "iss" : issuer ,
94- "aud" : audience ,
95- "iat" : now ,
96- "nbf" : now ,
97- "exp" : now + ttl ,
98- ** extra ,
99- }
100- return jwt .encode (claims , priv , algorithm = "RS256" , headers = {"kid" : "test-key" })
91+ def basic_header ():
92+ def _make (user : str , pw : str ) -> str :
93+ token = base64 .b64encode (f"{ user } :{ pw } " .encode ()).decode ("ascii" )
94+ return f"Basic { token } "
10195
10296 return _make
10397
10498
10599@pytest .fixture
106- def app_under_test (postgres_url , alembic_upgrade , monkeypatch , signing_key , issuer , audience ):
107- """A FastAPI app wired to the test Postgres + a static JWKS fetcher ."""
100+ def app_under_test (postgres_url , alembic_upgrade , monkeypatch , cwa_transport ):
101+ """A FastAPI app wired to the test Postgres + a mock CWA transport ."""
108102 monkeypatch .setenv ("OPDS_SYNC_DATABASE_URL" , postgres_url )
109- monkeypatch .setenv ("OPDS_SYNC_AUTHENTIK_ISSUER" , issuer )
110- monkeypatch .setenv ("OPDS_SYNC_AUTHENTIK_AUDIENCE" , audience )
103+ monkeypatch .setenv ("OPDS_SYNC_CWA_BASE_URL" , "http://test-cwa" )
111104
112- # Bust the lru_cache so the env vars take effect.
113105 from opds_sync .config import get_settings
114106
115107 get_settings .cache_clear ()
116108
117- from opds_sync .core .auth import JwksFetcher , JwtValidator
109+ from opds_sync .core .auth import CalibreAuthValidator
118110 from opds_sync .main import create_app
119111
120- _ , pub = signing_key
121-
122- class StaticJwks (JwksFetcher ):
123- async def get_signing_key (self , kid ): # type: ignore[override]
124- class _K :
125- key = pub # noqa: E701
126-
127- return _K ()
128-
129112 app = create_app ()
130- app .state .jwt_validator = JwtValidator (jwks = StaticJwks (), issuer = issuer , audience = audience )
113+ test_client = httpx .AsyncClient (
114+ transport = cwa_transport , base_url = "http://test-cwa" , timeout = 3.0
115+ )
116+ app .state .httpx_client = test_client
117+ app .state .auth_validator = CalibreAuthValidator (
118+ client = test_client ,
119+ cwa_base_url = "http://test-cwa" ,
120+ )
131121 return app
0 commit comments