"""Tests for user management endpoints (/api/users/*).""" import pytest # --------------------------------------------------------------------------- # Shared fixtures # --------------------------------------------------------------------------- @pytest.fixture def registered_user(client): """Register a test user and return the API response data.""" c, _ = client response = c.post("/api/users/register", json={ "email": "test@example.com", "displayName": "Test User", "photoURL": "https://example.com/photo.jpg", }) assert response.status_code == 200 return response.json() # --------------------------------------------------------------------------- # POST /api/users/register # --------------------------------------------------------------------------- class TestRegisterUser: def test_register_new_user_returns_200(self, client): c, _ = client response = c.post("/api/users/register", json={"email": "new@example.com", "displayName": "New User"}) assert response.status_code == 200 def test_register_returns_user_fields(self, client): c, _ = client response = c.post("/api/users/register", json={"email": "new@example.com", "displayName": "New User"}) data = response.json() assert data["email"] == "new@example.com" assert data["displayName"] == "New User" assert "id" in data assert "createdAt" in data assert "updatedAt" in data def test_register_returns_registered_message(self, client): c, _ = client response = c.post("/api/users/register", json={"email": "brand_new@example.com"}) assert response.json()["message"] == "User registered successfully" def test_register_existing_user_is_idempotent(self, client): c, _ = client payload = {"email": "existing@example.com"} c.post("/api/users/register", json=payload) response = c.post("/api/users/register", json=payload) assert response.status_code == 200 assert response.json()["message"] == "User already exists" def test_register_idempotent_returns_same_id(self, client): c, _ = client payload = {"email": "same@example.com"} r1 = c.post("/api/users/register", json=payload).json() r2 = c.post("/api/users/register", json=payload).json() assert r1["id"] == r2["id"] def test_register_uses_email_prefix_as_default_display_name(self, client): c, _ = client response = c.post("/api/users/register", json={"email": "johndoe@example.com"}) assert response.json()["displayName"] == "johndoe" def test_register_default_theme_is_light(self, client): c, _ = client response = c.post("/api/users/register", json={"email": "x@example.com"}) assert response.json()["theme"] == "light" def test_register_missing_email_returns_422(self, client): c, _ = client response = c.post("/api/users/register", json={"displayName": "No Email"}) assert response.status_code == 422 def test_register_without_optional_fields(self, client): c, _ = client response = c.post("/api/users/register", json={"email": "minimal@example.com"}) assert response.status_code == 200 assert response.json()["photoURL"] is None # --------------------------------------------------------------------------- # GET /api/users/by-email/{email} # --------------------------------------------------------------------------- class TestGetUserByEmail: def test_returns_existing_user(self, client, registered_user): c, _ = client email = registered_user["email"] response = c.get(f"/api/users/by-email/{email}") assert response.status_code == 200 assert response.json()["email"] == email def test_returns_all_user_fields(self, client, registered_user): c, _ = client response = c.get(f"/api/users/by-email/{registered_user['email']}") data = response.json() for field in ("id", "email", "displayName", "theme", "createdAt", "updatedAt"): assert field in data def test_nonexistent_email_returns_404(self, client): c, _ = client response = c.get("/api/users/by-email/ghost@example.com") assert response.status_code == 404 assert "User not found" in response.json()["detail"] # --------------------------------------------------------------------------- # GET /api/users/{user_id} # --------------------------------------------------------------------------- class TestGetUserById: def test_returns_existing_user(self, client, registered_user): c, _ = client user_id = registered_user["id"] response = c.get(f"/api/users/{user_id}") assert response.status_code == 200 assert response.json()["id"] == user_id def test_invalid_object_id_format_returns_400(self, client): c, _ = client response = c.get("/api/users/not-a-valid-objectid") assert response.status_code == 400 def test_nonexistent_valid_id_returns_404(self, client): c, _ = client response = c.get("/api/users/507f1f77bcf86cd799439011") assert response.status_code == 404 # --------------------------------------------------------------------------- # PUT /api/users/{user_id} # --------------------------------------------------------------------------- class TestUpdateUser: def test_update_display_name(self, client, registered_user): c, _ = client user_id = registered_user["id"] response = c.put(f"/api/users/{user_id}", json={"displayName": "Updated Name"}) assert response.status_code == 200 assert response.json()["displayName"] == "Updated Name" def test_update_theme_to_dark(self, client, registered_user): c, _ = client user_id = registered_user["id"] response = c.put(f"/api/users/{user_id}", json={"theme": "dark"}) assert response.status_code == 200 assert response.json()["theme"] == "dark" def test_update_photo_url(self, client, registered_user): c, _ = client user_id = registered_user["id"] new_url = "https://new-photo.example.com/pic.jpg" response = c.put(f"/api/users/{user_id}", json={"photoURL": new_url}) assert response.status_code == 200 assert response.json()["photoURL"] == new_url def test_update_persists_to_database(self, client, registered_user): c, _ = client user_id = registered_user["id"] c.put(f"/api/users/{user_id}", json={"displayName": "Persisted Name"}) response = c.get(f"/api/users/{user_id}") assert response.json()["displayName"] == "Persisted Name" def test_partial_update_does_not_clear_other_fields(self, client, registered_user): c, _ = client user_id = registered_user["id"] # Update only theme c.put(f"/api/users/{user_id}", json={"theme": "dark"}) response = c.get(f"/api/users/{user_id}") data = response.json() assert data["theme"] == "dark" assert data["displayName"] == "Test User" # original value preserved def test_update_nonexistent_user_returns_404(self, client): c, _ = client response = c.put("/api/users/507f1f77bcf86cd799439011", json={"displayName": "X"}) assert response.status_code == 404 def test_update_invalid_id_format_returns_400(self, client): c, _ = client response = c.put("/api/users/bad-id", json={"displayName": "X"}) assert response.status_code == 400 # --------------------------------------------------------------------------- # DELETE /api/users/{user_id} # --------------------------------------------------------------------------- class TestDeleteUser: def test_delete_user_returns_200(self, client, registered_user): c, _ = client response = c.delete(f"/api/users/{registered_user['id']}") assert response.status_code == 200 def test_delete_user_returns_deletion_counts(self, client, registered_user): c, _ = client response = c.delete(f"/api/users/{registered_user['id']}") data = response.json() assert data["user_deleted"] == 1 assert "entries_deleted" in data def test_delete_user_makes_them_unretrievable(self, client, registered_user): c, _ = client user_id = registered_user["id"] c.delete(f"/api/users/{user_id}") response = c.get(f"/api/users/{user_id}") assert response.status_code == 404 def test_delete_user_also_deletes_their_entries(self, client, registered_user): c, _ = client user_id = registered_user["id"] # Create 2 entries for this user for _ in range(2): c.post(f"/api/entries/{user_id}", json={ "encryption": { "encrypted": True, "ciphertext": "dGVzdA==", "nonce": "bm9uY2U=", "algorithm": "XSalsa20-Poly1305", } }) response = c.delete(f"/api/users/{user_id}") assert response.json()["entries_deleted"] == 2 def test_delete_nonexistent_user_returns_404(self, client): c, _ = client response = c.delete("/api/users/507f1f77bcf86cd799439011") assert response.status_code == 404 def test_delete_invalid_id_format_returns_400(self, client): c, _ = client response = c.delete("/api/users/bad-id") assert response.status_code == 400