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:
@@ -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
|
||||
Reference in New Issue
Block a user