"""Write-path route tests.""" from __future__ import annotations import pytest from app.schemas.emotion_log import EMOTION_NAMES class TestCreate: @pytest.mark.parametrize("emotion", EMOTION_NAMES) def test_accepts_every_valid_emotion(self, client, ingest_headers, emotion): response = client.post( "/api/logs", json={"dominant_emotion": emotion, "duration_seconds": 10}, headers=ingest_headers, ) assert response.status_code == 201 assert response.json()["dominant_emotion"] == emotion def test_rejects_unknown_emotion_with_422(self, client, ingest_headers): """Previously a hand-rolled validator; now a Literal, so the error is typed.""" response = client.post( "/api/logs", json={"dominant_emotion": "Ecstatic", "duration_seconds": 10}, headers=ingest_headers, ) assert response.status_code == 422 @pytest.mark.parametrize("duration", [0.5, 0, -1, 3601]) def test_rejects_out_of_range_duration(self, client, ingest_headers, duration): response = client.post( "/api/logs", json={"dominant_emotion": "Happy", "duration_seconds": duration}, headers=ingest_headers, ) assert response.status_code == 422 def test_response_includes_server_assigned_fields(self, client, ingest_headers): body = client.post( "/api/logs", json={"dominant_emotion": "Happy", "duration_seconds": 10}, headers=ingest_headers, ).json() assert isinstance(body["id"], int) assert body["timestamp"] def test_timestamp_carries_an_explicit_utc_offset(self, client, ingest_headers): """Without an offset, browsers parse the value as local time. SQLite returns a naive datetime even for a column declared timezone-aware, so the serialiser has to reattach UTC. A bare "2026-08-13T22:01:45" would shift every displayed record by the viewer's offset. """ body = client.post( "/api/logs", json={"dominant_emotion": "Happy", "duration_seconds": 10}, headers=ingest_headers, ).json() assert body["timestamp"].endswith(("Z", "+00:00")) def test_listed_timestamps_carry_an_offset(self, client, ingest_headers, session_headers): client.post( "/api/logs", json={"dominant_emotion": "Sad", "duration_seconds": 10}, headers=ingest_headers, ) items = client.get("/api/logs", headers=session_headers).json()["items"] assert items assert all(item["timestamp"].endswith(("Z", "+00:00")) for item in items) class TestBulkCreate: def test_creates_all_items(self, client, ingest_headers): items = [{"dominant_emotion": "Happy", "duration_seconds": 10} for _ in range(5)] response = client.post("/api/logs/bulk", json={"items": items}, headers=ingest_headers) assert response.status_code == 201 body = response.json() assert body["created"] == 5 assert body["rejected"] == 0 def test_reports_per_item_indexes(self, client, ingest_headers): items = [{"dominant_emotion": "Sad", "duration_seconds": 10} for _ in range(3)] body = client.post("/api/logs/bulk", json={"items": items}, headers=ingest_headers).json() assert [entry["index"] for entry in body["results"]] == [0, 1, 2] def test_rejects_empty_batch(self, client, ingest_headers): response = client.post("/api/logs/bulk", json={"items": []}, headers=ingest_headers) assert response.status_code == 422 def test_rejects_oversized_batch(self, client, ingest_headers): items = [{"dominant_emotion": "Happy", "duration_seconds": 10}] * 501 response = client.post("/api/logs/bulk", json={"items": items}, headers=ingest_headers) assert response.status_code == 422 def test_invalid_item_fails_the_batch_validation(self, client, ingest_headers): items = [ {"dominant_emotion": "Happy", "duration_seconds": 10}, {"dominant_emotion": "Nonsense", "duration_seconds": 10}, ] response = client.post("/api/logs/bulk", json={"items": items}, headers=ingest_headers) assert response.status_code == 422