"""API-level happy path tests using FastAPI TestClient.""" from __future__ import annotations import pytest from fastapi.testclient import TestClient from app.db import repo_app, repo_profile @pytest.fixture() def client(): from app.main import app return TestClient(app) class TestHealth: def test_health(self, client): resp = client.get("/api/health") assert resp.status_code == 200 assert resp.json() == {"status": "ok"} class TestProfileFlow: def test_get_profile_creates_default(self, client): resp = client.get("/api/profile") assert resp.status_code == 200 data = resp.json() assert data["full_name"] == "" assert data["email"] == "" def test_update_profile(self, client): # First create client.get("/api/profile") resp = client.put( "/api/profile", json={"full_name": "Test Person", "email": "test@example.com"}, ) assert resp.status_code == 200 assert resp.json()["full_name"] == "Test Person" assert resp.json()["email"] == "test@example.com" class TestCvSectionsFlow: def test_create_and_list_sections(self, client): # Ensure profile exists client.get("/api/profile") # Create a section resp = client.post( "/api/profile/sections", json={ "kind": "experience", "title": "Software Engineer", "org": "TechCorp", "bullets": ["Built feature X", "Improved performance by 20%"], "tags": ["python", "fastapi"], "sort_order": 0, }, ) assert resp.status_code == 201 section = resp.json() assert section["kind"] == "experience" assert section["title"] == "Software Engineer" assert "python" in section["tags"] # List sections resp = client.get("/api/profile/sections") assert resp.status_code == 200 assert len(resp.json()) == 1 def test_update_and_delete_section(self, client): client.get("/api/profile") resp = client.post( "/api/profile/sections", json={"kind": "education", "title": "MSc", "org": "University"}, ) section_id = resp.json()["id"] # Update resp = client.put( f"/api/profile/sections/{section_id}", json={"title": "MSc Computer Science"}, ) assert resp.status_code == 200 assert resp.json()["title"] == "MSc Computer Science" # Delete resp = client.delete(f"/api/profile/sections/{section_id}") assert resp.status_code == 204 def test_ai_assist_mock_mode(self, client): """AI assist returns mock suggestions when no API key is set.""" client.get("/api/profile") resp = client.post( "/api/profile/sections", json={"kind": "experience", "title": "Dev", "bullets": ["did stuff"]}, ) section_id = resp.json()["id"] resp = client.post( f"/api/profile/sections/{section_id}/ai-assist", json={"instruction": "improve this bullet"}, ) assert resp.status_code == 200 assert "suggestions" in resp.json() assert len(resp.json()["suggestions"]) > 0 class TestJobPostingFlow: def test_create_posting_and_application(self, client): """POST /postings creates a job_posting + application(discovered).""" resp = client.post("/api/postings", json={"url": "https://example.com/job/456"}) assert resp.status_code == 201 app_data = resp.json() assert app_data["state"] == "discovered" # List postings resp = client.get("/api/postings") assert resp.status_code == 200 assert len(resp.json()) == 1 assert resp.json()[0]["url"] == "https://example.com/job/456" def test_score_posting(self, client): """POST /postings/{id}/score returns score + rationale.""" # Create posting resp = client.post("/api/postings", json={"url": "https://example.com/job/789"}) assert resp.status_code == 201 # Get the posting id resp_postings = client.get("/api/postings") posting_id = resp_postings.json()[0]["id"] # Score it resp = client.post(f"/api/postings/{posting_id}/score") assert resp.status_code == 200 data = resp.json() assert "score" in data assert "rationale" in data assert data["score"] > 0 class TestApplicationsFlow: def test_list_applications(self, client): client.post("/api/postings", json={"url": "https://example.com/job/list1"}) resp = client.get("/api/applications") assert resp.status_code == 200 assert len(resp.json()) >= 1 assert resp.json()[0]["state"] == "discovered" def test_transition_to_rejected(self, client): """discovered -> rejected is valid (user action).""" resp = client.post("/api/postings", json={"url": "https://example.com/job/trans1"}) app_id = resp.json()["id"] resp = client.post( f"/api/applications/{app_id}/transition", json={"to": "rejected"}, ) assert resp.status_code == 200 assert resp.json()["state"] == "rejected" def test_transition_invalid_409(self, client): """discovered -> sent is invalid -> 409.""" resp = client.post("/api/postings", json={"url": "https://example.com/job/trans2"}) app_id = resp.json()["id"] resp = client.post( f"/api/applications/{app_id}/transition", json={"to": "sent"}, ) assert resp.status_code == 409 def test_transition_to_scored_without_score(self, client): """discovered -> scored without scoring guard -> 409.""" resp = client.post("/api/postings", json={"url": "https://example.com/job/trans3"}) app_id = resp.json()["id"] resp = client.post( f"/api/applications/{app_id}/transition", json={"to": "scored"}, ) assert resp.status_code == 409 def test_transition_after_score(self, client): """discovered -> scored after scoring task completes -> success.""" resp = client.post("/api/postings", json={"url": "https://example.com/job/trans4"}) app_id = resp.json()["id"] # Score first resp_postings = client.get("/api/postings") posting_id = resp_postings.json()[0]["id"] client.post(f"/api/postings/{posting_id}/score") # Now transition to scored should succeed (score is set, state already scored by scorer) # Actually the scorer already sets state to 'scored', so let's test scored -> approved resp = client.post( f"/api/applications/{app_id}/transition", json={"to": "approved"}, ) assert resp.status_code == 200 assert resp.json()["state"] == "approved" class TestArtifactAndCoverLetter: def test_create_artifact(self, client): resp = client.post("/api/postings", json={"url": "https://example.com/job/art1"}) app_id = resp.json()["id"] resp = client.post( f"/api/applications/{app_id}/artifacts", json={"kind": "email", "content": "Dear hiring manager..."}, ) assert resp.status_code == 201 assert resp.json()["kind"] == "email" assert len(resp.json()["content_hash"]) == 64 def test_cover_letter_with_critique(self, client): resp = client.post("/api/postings", json={"url": "https://example.com/job/art2"}) app_id = resp.json()["id"] resp = client.post( f"/api/applications/{app_id}/artifacts/cover-letter", json={"letter_text": "I am writing to apply for the position. I have experience in many things."}, ) assert resp.status_code == 200 data = resp.json() assert "artifact" in data assert "critique" in data assert len(data["critique"]) > 0 def test_list_artifacts(self, client): resp = client.post("/api/postings", json={"url": "https://example.com/job/art3"}) app_id = resp.json()["id"] client.post( f"/api/applications/{app_id}/artifacts", json={"kind": "cv", "content": "CV content"}, ) resp = client.get(f"/api/applications/{app_id}/artifacts") assert resp.status_code == 200 assert len(resp.json()) >= 1 class TestTelemetry: def test_telemetry_list(self, client): # Generate a task run via ai-assist client.get("/api/profile") resp = client.post( "/api/profile/sections", json={"kind": "experience", "title": "Dev"}, ) section_id = resp.json()["id"] client.post( f"/api/profile/sections/{section_id}/ai-assist", json={"instruction": "test"}, ) resp = client.get("/api/telemetry/tasks") assert resp.status_code == 200 assert len(resp.json()) >= 1 assert resp.json()[0]["task"] == "cv_assist" assert resp.json()[0]["model"] == "mock"