letsbe-orchestrator/tests/routes/test_registration_tokens.py

301 lines
10 KiB
Python
Raw Normal View History

"""Tests for registration token endpoints."""
import hashlib
import uuid
from datetime import timedelta
import pytest
from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.base import utc_now
from app.models.registration_token import RegistrationToken
from app.models.tenant import Tenant
from app.routes.registration_tokens import (
create_registration_token,
get_registration_token,
list_registration_tokens,
revoke_registration_token,
)
from app.schemas.registration_token import RegistrationTokenCreate
@pytest.mark.asyncio
class TestCreateRegistrationToken:
"""Tests for the create_registration_token endpoint."""
async def test_create_token_success(self, db: AsyncSession, test_tenant: Tenant):
"""Successfully create a registration token."""
request = RegistrationTokenCreate(
description="Test token",
max_uses=5,
expires_in_hours=24,
)
response = await create_registration_token(
tenant_id=test_tenant.id, request=request, db=db
)
assert response.id is not None
assert response.tenant_id == test_tenant.id
assert response.description == "Test token"
assert response.max_uses == 5
assert response.use_count == 0
assert response.revoked is False
assert response.token is not None # Plaintext token returned once
assert response.expires_at is not None
async def test_create_token_stores_hash(self, db: AsyncSession, test_tenant: Tenant):
"""Token is stored as hash, not plaintext."""
request = RegistrationTokenCreate(description="Hash test")
response = await create_registration_token(
tenant_id=test_tenant.id, request=request, db=db
)
# Retrieve from database
result = await db.execute(
select(RegistrationToken).where(RegistrationToken.id == response.id)
)
token_record = result.scalar_one()
# Verify hash is stored, not plaintext
expected_hash = hashlib.sha256(response.token.encode()).hexdigest()
assert token_record.token_hash == expected_hash
assert token_record.token_hash != response.token
async def test_create_token_default_values(
self, db: AsyncSession, test_tenant: Tenant
):
"""Default values are applied correctly."""
request = RegistrationTokenCreate()
response = await create_registration_token(
tenant_id=test_tenant.id, request=request, db=db
)
assert response.max_uses == 1 # Default
assert response.description is None
assert response.expires_at is None
async def test_create_token_tenant_not_found(self, db: AsyncSession):
"""Returns 404 when tenant doesn't exist."""
fake_tenant_id = uuid.uuid4()
request = RegistrationTokenCreate()
with pytest.raises(HTTPException) as exc_info:
await create_registration_token(
tenant_id=fake_tenant_id, request=request, db=db
)
assert exc_info.value.status_code == 404
assert f"Tenant {fake_tenant_id} not found" in str(exc_info.value.detail)
@pytest.mark.asyncio
class TestListRegistrationTokens:
"""Tests for the list_registration_tokens endpoint."""
async def test_list_tokens_empty(self, db: AsyncSession, test_tenant: Tenant):
"""Returns empty list when no tokens exist."""
response = await list_registration_tokens(tenant_id=test_tenant.id, db=db)
assert response.tokens == []
assert response.total == 0
async def test_list_tokens_multiple(self, db: AsyncSession, test_tenant: Tenant):
"""Returns all tokens for tenant."""
# Create multiple tokens
for i in range(3):
token = RegistrationToken(
tenant_id=test_tenant.id,
token_hash=f"hash-{i}",
description=f"Token {i}",
)
db.add(token)
await db.commit()
response = await list_registration_tokens(tenant_id=test_tenant.id, db=db)
assert len(response.tokens) == 3
assert response.total == 3
async def test_list_tokens_tenant_isolation(
self, db: AsyncSession, test_tenant: Tenant
):
"""Only returns tokens for the specified tenant."""
# Create another tenant
other_tenant = Tenant(id=uuid.uuid4(), name="Other Tenant")
db.add(other_tenant)
# Create token for test_tenant
token1 = RegistrationToken(
tenant_id=test_tenant.id, token_hash="hash-1", description="Tenant 1"
)
# Create token for other_tenant
token2 = RegistrationToken(
tenant_id=other_tenant.id, token_hash="hash-2", description="Tenant 2"
)
db.add_all([token1, token2])
await db.commit()
response = await list_registration_tokens(tenant_id=test_tenant.id, db=db)
assert len(response.tokens) == 1
assert response.tokens[0].description == "Tenant 1"
async def test_list_tokens_tenant_not_found(self, db: AsyncSession):
"""Returns 404 when tenant doesn't exist."""
fake_tenant_id = uuid.uuid4()
with pytest.raises(HTTPException) as exc_info:
await list_registration_tokens(tenant_id=fake_tenant_id, db=db)
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
class TestGetRegistrationToken:
"""Tests for the get_registration_token endpoint."""
async def test_get_token_success(
self,
db: AsyncSession,
test_tenant: Tenant,
test_registration_token: tuple[RegistrationToken, str],
):
"""Successfully retrieve a token."""
token_record, _ = test_registration_token
response = await get_registration_token(
tenant_id=test_tenant.id, token_id=token_record.id, db=db
)
assert response.id == token_record.id
assert response.description == token_record.description
async def test_get_token_not_found(self, db: AsyncSession, test_tenant: Tenant):
"""Returns 404 when token doesn't exist."""
fake_token_id = uuid.uuid4()
with pytest.raises(HTTPException) as exc_info:
await get_registration_token(
tenant_id=test_tenant.id, token_id=fake_token_id, db=db
)
assert exc_info.value.status_code == 404
async def test_get_token_wrong_tenant(
self,
db: AsyncSession,
test_tenant: Tenant,
test_registration_token: tuple[RegistrationToken, str],
):
"""Returns 404 when token belongs to different tenant."""
token_record, _ = test_registration_token
other_tenant = Tenant(id=uuid.uuid4(), name="Other Tenant")
db.add(other_tenant)
await db.commit()
with pytest.raises(HTTPException) as exc_info:
await get_registration_token(
tenant_id=other_tenant.id, token_id=token_record.id, db=db
)
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
class TestRevokeRegistrationToken:
"""Tests for the revoke_registration_token endpoint."""
async def test_revoke_token_success(
self,
db: AsyncSession,
test_tenant: Tenant,
test_registration_token: tuple[RegistrationToken, str],
):
"""Successfully revoke a token."""
token_record, _ = test_registration_token
assert token_record.revoked is False
await revoke_registration_token(
tenant_id=test_tenant.id, token_id=token_record.id, db=db
)
# Refresh to see updated value
await db.refresh(token_record)
assert token_record.revoked is True
async def test_revoke_token_not_found(self, db: AsyncSession, test_tenant: Tenant):
"""Returns 404 when token doesn't exist."""
fake_token_id = uuid.uuid4()
with pytest.raises(HTTPException) as exc_info:
await revoke_registration_token(
tenant_id=test_tenant.id, token_id=fake_token_id, db=db
)
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
class TestRegistrationTokenIsValid:
"""Tests for the RegistrationToken.is_valid() method."""
async def test_valid_token(
self,
db: AsyncSession,
test_tenant: Tenant,
test_registration_token: tuple[RegistrationToken, str],
):
"""Token is valid when not revoked, not expired, and not exhausted."""
token_record, _ = test_registration_token
assert token_record.is_valid() is True
async def test_revoked_token_invalid(
self,
db: AsyncSession,
test_tenant: Tenant,
test_registration_token: tuple[RegistrationToken, str],
):
"""Revoked token is invalid."""
token_record, _ = test_registration_token
token_record.revoked = True
await db.commit()
assert token_record.is_valid() is False
async def test_expired_token_invalid(self, db: AsyncSession, test_tenant: Tenant):
"""Expired token is invalid."""
token = RegistrationToken(
tenant_id=test_tenant.id,
token_hash="test-hash",
expires_at=utc_now() - timedelta(hours=1),
)
db.add(token)
await db.commit()
assert token.is_valid() is False
async def test_exhausted_token_invalid(self, db: AsyncSession, test_tenant: Tenant):
"""Token that reached max_uses is invalid."""
token = RegistrationToken(
tenant_id=test_tenant.id, token_hash="test-hash", max_uses=3, use_count=3
)
db.add(token)
await db.commit()
assert token.is_valid() is False
async def test_unlimited_uses_token(self, db: AsyncSession, test_tenant: Tenant):
"""Token with max_uses=0 has unlimited uses."""
token = RegistrationToken(
tenant_id=test_tenant.id, token_hash="test-hash", max_uses=0, use_count=1000
)
db.add(token)
await db.commit()
assert token.is_valid() is True