- Extends GET /api/v1/tags with sort=count_desc and min_count query params - New TagsComponent at /tags (public, no auth guard) shows all tags sorted by image count - Clicking a tag navigates to /?tags=<name> for a pre-filtered library view - LibraryComponent reads ?tags= query param on init to support deep-linking from tag browser - Library header gains a "Browse tags" link to /tags for discoverability - All 15 TDD tasks complete; ruff, ng lint, and ng build clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
97 lines
2.8 KiB
Python
97 lines
2.8 KiB
Python
import jwt as pyjwt
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from app.auth.jwt_provider import JWTAuthProvider
|
|
|
|
SECRET = "test-secret-key"
|
|
USERNAME = "owner"
|
|
PASSWORD = "hunter2"
|
|
|
|
|
|
def make_provider(**kwargs) -> JWTAuthProvider:
|
|
defaults = dict(
|
|
secret_key=SECRET,
|
|
expiry_seconds=3600,
|
|
owner_username=USERNAME,
|
|
owner_password=PASSWORD,
|
|
)
|
|
return JWTAuthProvider(**{**defaults, **kwargs})
|
|
|
|
|
|
def test_create_token_is_valid_jwt():
|
|
provider = make_provider()
|
|
token = provider.create_token()
|
|
payload = pyjwt.decode(token, SECRET, algorithms=["HS256"])
|
|
assert payload["sub"] == "owner"
|
|
assert "iat" in payload
|
|
assert "exp" in payload
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_identity_returns_owner():
|
|
provider = make_provider()
|
|
token = provider.create_token()
|
|
identity = await provider.get_identity(f"Bearer {token}")
|
|
assert identity.id == "owner"
|
|
assert identity.anonymous is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_identity_raises_on_expired_token():
|
|
provider = make_provider(expiry_seconds=-1)
|
|
token = provider.create_token()
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await provider.get_identity(f"Bearer {token}")
|
|
assert exc_info.value.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_identity_raises_on_wrong_key():
|
|
provider = make_provider()
|
|
other = make_provider(secret_key="different-secret")
|
|
token = other.create_token()
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await provider.get_identity(f"Bearer {token}")
|
|
assert exc_info.value.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_identity_raises_on_garbage():
|
|
provider = make_provider()
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await provider.get_identity("Bearer not.a.real.token")
|
|
assert exc_info.value.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_identity_raises_on_missing_header():
|
|
provider = make_provider()
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await provider.get_identity(None)
|
|
assert exc_info.value.status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_identity_raises_on_missing_bearer_prefix():
|
|
provider = make_provider()
|
|
token = provider.create_token()
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await provider.get_identity(token)
|
|
assert exc_info.value.status_code == 401
|
|
|
|
|
|
def test_verify_credentials_true():
|
|
provider = make_provider()
|
|
assert provider.verify_credentials(USERNAME, PASSWORD) is True
|
|
|
|
|
|
def test_verify_credentials_false_wrong_password():
|
|
provider = make_provider()
|
|
assert provider.verify_credentials(USERNAME, "wrongpassword") is False
|
|
|
|
|
|
def test_verify_credentials_false_wrong_username():
|
|
provider = make_provider()
|
|
assert provider.verify_credentials("notowner", PASSWORD) is False
|