113 lines
3.4 KiB
Python
113 lines
3.4 KiB
Python
import importlib
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
from app.core.security import AuthContext, get_auth_context
|
|
from app.models.auth import AccessTokenPayload, AuthUserDocument
|
|
|
|
auth_router_module = importlib.import_module("app.api.auth.router")
|
|
|
|
|
|
def test_login_success_returns_bearer_token(client, monkeypatch: object):
|
|
user = AuthUserDocument(username="admin", password_hash="stored-hash")
|
|
|
|
async def fake_get_user_by_username(username: str) -> AuthUserDocument | None:
|
|
assert username == "admin"
|
|
return user
|
|
|
|
def fake_verify_password(plain_password: str, password_hash: str) -> bool:
|
|
return plain_password == "secret" and password_hash == "stored-hash"
|
|
|
|
def fake_create_access_token(*, subject: str) -> str:
|
|
assert subject == "admin"
|
|
return "token-123"
|
|
|
|
monkeypatch.setattr(
|
|
auth_router_module,
|
|
"get_user_by_username",
|
|
fake_get_user_by_username,
|
|
)
|
|
monkeypatch.setattr(auth_router_module, "verify_password", fake_verify_password)
|
|
monkeypatch.setattr(
|
|
auth_router_module,
|
|
"create_access_token",
|
|
fake_create_access_token,
|
|
)
|
|
|
|
response = client.post(
|
|
"/auth/login",
|
|
json={"username": "admin", "password": "secret"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"access_token": "token-123", "token_type": "bearer"}
|
|
|
|
|
|
def test_login_invalid_credentials_returns_401(client, monkeypatch: object):
|
|
async def fake_get_user_by_username(_: str) -> None:
|
|
return None
|
|
|
|
monkeypatch.setattr(
|
|
auth_router_module,
|
|
"get_user_by_username",
|
|
fake_get_user_by_username,
|
|
)
|
|
|
|
response = client.post(
|
|
"/auth/login",
|
|
json={"username": "admin", "password": "wrong"},
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
assert response.json()["detail"] == "Invalid username or password"
|
|
assert response.headers["www-authenticate"] == "Bearer"
|
|
|
|
|
|
def test_logout_revokes_active_token(client, monkeypatch: object):
|
|
captured: dict[str, object] = {}
|
|
|
|
async def fake_revoke_token(*, jti: str, expires_at: datetime) -> None:
|
|
captured["jti"] = jti
|
|
captured["expires_at"] = expires_at
|
|
|
|
monkeypatch.setattr(auth_router_module, "revoke_token", fake_revoke_token)
|
|
|
|
response = client.post("/auth/logout")
|
|
|
|
assert response.status_code == 204
|
|
assert captured["jti"] == "test-jti"
|
|
assert isinstance(captured["expires_at"], datetime)
|
|
|
|
|
|
def test_logout_skips_revocation_when_token_is_expired(
|
|
client,
|
|
fake_user: AuthUserDocument,
|
|
monkeypatch: object,
|
|
):
|
|
state = {"called": False}
|
|
|
|
async def fake_revoke_token(*, jti: str, expires_at: datetime) -> None:
|
|
state["called"] = True
|
|
|
|
monkeypatch.setattr(auth_router_module, "revoke_token", fake_revoke_token)
|
|
|
|
expired_payload = AccessTokenPayload(
|
|
sub=fake_user.username,
|
|
jti="expired-jti",
|
|
exp=int((datetime.now(UTC) - timedelta(minutes=1)).timestamp()),
|
|
)
|
|
expired_context = AuthContext(
|
|
token="expired-token",
|
|
user=fake_user,
|
|
payload=expired_payload,
|
|
)
|
|
|
|
async def override_auth_context() -> AuthContext:
|
|
return expired_context
|
|
|
|
client.app.dependency_overrides[get_auth_context] = override_auth_context
|
|
|
|
response = client.post("/auth/logout")
|
|
|
|
assert response.status_code == 204
|
|
assert state["called"] is False
|