API tests added with near-complete coverage over user and admin APIs. 60% overall backend. (not including E2E test in coverage)

This commit is contained in:
Leo Vasanko
2025-12-06 04:45:26 +00:00
parent bfa3281aca
commit 2bac94a548
8 changed files with 2637 additions and 11 deletions
+184
View File
@@ -0,0 +1,184 @@
"""
Tests for the user API endpoints (/auth/api/user/).
These tests cover user self-service operations:
- Display name update
- Logout all sessions
- Session management (delete specific session)
- Credential management (delete credential)
- Device addition link creation
"""
import httpx
import pytest
from tests.conftest import auth_headers
class TestUserDisplayName:
"""Tests for PUT /auth/api/user/display-name"""
@pytest.mark.asyncio
async def test_update_display_name_requires_auth(self, client: httpx.AsyncClient):
"""Update display name without auth should return 401."""
response = await client.put(
"/auth/api/user/display-name",
json={"display_name": "New Name"},
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_update_display_name_success(
self, client: httpx.AsyncClient, session_token: str
):
"""User should be able to update their display name."""
response = await client.put(
"/auth/api/user/display-name",
json={"display_name": "Updated Name"},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
@pytest.mark.asyncio
async def test_update_display_name_empty_fails(
self, client: httpx.AsyncClient, session_token: str
):
"""Empty display name should fail."""
response = await client.put(
"/auth/api/user/display-name",
json={"display_name": ""},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 400
@pytest.mark.asyncio
async def test_update_display_name_too_long_fails(
self, client: httpx.AsyncClient, session_token: str
):
"""Display name over 64 chars should fail."""
long_name = "x" * 100
response = await client.put(
"/auth/api/user/display-name",
json={"display_name": long_name},
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 400
class TestUserLogoutAll:
"""Tests for POST /auth/api/user/logout-all"""
@pytest.mark.asyncio
async def test_logout_all_requires_auth(self, client: httpx.AsyncClient):
"""Logout all without auth should return already logged out."""
response = await client.post("/auth/api/user/logout-all")
assert response.status_code == 200
data = response.json()
assert "Already logged out" in data["message"]
@pytest.mark.asyncio
async def test_logout_all_success(
self, client: httpx.AsyncClient, session_token: str
):
"""User should be able to logout from all sessions."""
response = await client.post(
"/auth/api/user/logout-all",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 200
data = response.json()
assert "Logged out" in data["message"]
# Verify session is invalidated
response2 = await client.post(
"/auth/api/validate",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response2.status_code == 401
class TestUserSessionManagement:
"""Tests for DELETE /auth/api/user/session/{session_id}"""
@pytest.mark.asyncio
async def test_delete_session_requires_auth(self, client: httpx.AsyncClient):
"""Delete session without auth should return 401."""
response = await client.delete("/auth/api/user/session/fake-session-id")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_delete_invalid_session_fails(
self, client: httpx.AsyncClient, session_token: str
):
"""Deleting invalid session ID should fail."""
response = await client.delete(
"/auth/api/user/session/invalid-session-id",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 400
@pytest.mark.asyncio
async def test_delete_nonexistent_session_returns_404(
self, client: httpx.AsyncClient, session_token: str
):
"""Deleting a properly-formatted but nonexistent session returns 404."""
# Use a valid format but non-existent session key
fake_session = "c2Vzc0FBQUFBQUFBQUFBQUFBQUE" # base64 of "sessAAAAAAAAAAAAAAAA"
response = await client.delete(
f"/auth/api/user/session/{fake_session}",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 404
class TestUserCredentialManagement:
"""Tests for DELETE /auth/api/user/credential/{uuid}"""
@pytest.mark.asyncio
async def test_delete_credential_requires_auth(self, client: httpx.AsyncClient):
"""Delete credential without auth should return 401."""
response = await client.delete(
"/auth/api/user/credential/00000000-0000-0000-0000-000000000000"
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_delete_credential_success(
self, client: httpx.AsyncClient, session_token: str, test_credential
):
"""User can delete their credential."""
response = await client.delete(
f"/auth/api/user/credential/{test_credential.uuid}",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
# Note: API allows deleting even the only credential
assert response.status_code == 200
data = response.json()
assert "deleted" in data["message"].lower()
class TestUserCreateLink:
"""Tests for POST /auth/api/user/create-link"""
@pytest.mark.asyncio
async def test_create_link_requires_auth(self, client: httpx.AsyncClient):
"""Create link without auth should return 401."""
response = await client.post("/auth/api/user/create-link")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_create_link_success(
self, client: httpx.AsyncClient, session_token: str
):
"""User should be able to create a device addition link."""
response = await client.post(
"/auth/api/user/create-link",
headers={**auth_headers(session_token), "Host": "localhost:4401"},
)
assert response.status_code == 200
data = response.json()
assert "url" in data
assert "expires" in data
assert "message" in data