from __future__ import annotations import time from pathlib import Path from typing import Any import pytest from fastapi.testclient import TestClient as RawTestClient from resume_agent.agent import save_json from resume_agent.models import CareerProfile, TailoringPackage from resume_agent.render import render_ohmycv_resume from resume_agent.webapp import create_app TEST_USERNAME = "resume-user" TEST_PASSWORD = "test-password" class TestClient(RawTestClient): def __enter__(self) -> TestClient: super().__enter__() response = self.post( "/login", data={"username": TEST_USERNAME, "password": TEST_PASSWORD, "next": "/"}, follow_redirects=False, ) assert response.status_code == 303 return self @pytest.fixture(autouse=True) def configure_authentication(monkeypatch: Any) -> None: monkeypatch.setenv("RESUME_AGENT_USERNAME", TEST_USERNAME) monkeypatch.setenv("RESUME_AGENT_PASSWORD", TEST_PASSWORD) def sample_profile() -> CareerProfile: return CareerProfile.model_validate( { "contact": { "full_name": "Ada Example", "email": "ada@example.com", "phone": None, "location": "London", "linkedin": None, "website": None, }, "professional_identity": "Backend engineer", "differentiators": ["Reliable distributed systems"], "target_roles": ["Senior Backend Engineer"], "facts": [ { "id": "F001", "category": "experience", "statement": "Reduced API latency by 30%.", "source_name": "resume", "source_excerpt": "Reduced API latency by 30%.", } ], "skills": ["Python"], "unanswered_questions": [], } ) def sample_package() -> TailoringPackage: contact = sample_profile().contact.model_dump() item = {"text": "Reduced API latency by 30%. [F013]", "evidence_ids": ["F001"]} return TailoringPackage.model_validate( { "job": { "company": "Example Co", "role_title": "Backend Engineer", "mission": None, "requirements": [], "responsibilities": [], "culture_signals": [], "ats_keywords": ["Python"], }, "resume": { "contact": contact, "headline": "Backend Engineer", "summary": [item], "sections": [{"title": "Experience", "items": [item]}], }, "match": { "strong_matches": ["API performance"], "partial_matches": [], "genuine_gaps": [], "keywords_used": ["Python"], }, "changes_made": ["Prioritized relevant impact."], "questions_for_candidate": [], "warnings": [], } ) class FakeLLM: def __init__(self, responses: list[Any]) -> None: self.responses = responses def parse(self, schema: type[Any], instructions: str, input_text: str) -> Any: response = self.responses.pop(0) assert isinstance(response, schema) return response def list_models(self) -> list[str]: return ["test-model"] def wait_for_task(client: TestClient, task_id: str) -> dict[str, Any]: for _ in range(100): response = client.get(f"/api/tasks/{task_id}") assert response.status_code == 200 task = response.json() if task["status"] != "running": return task time.sleep(0.01) raise AssertionError("Background task did not finish in time.") def test_authentication_protects_ui_api_static_and_editor(tmp_path: Path) -> None: cv_dist = tmp_path / "cv" cv_dist.mkdir() (cv_dist / "index.html").write_text("editor", encoding="utf-8") (cv_dist / "200.html").write_text("editor", encoding="utf-8") app = create_app( profile_path=tmp_path / "profile.json", output_dir=tmp_path / "output", cv_dist=cv_dist, llm_factory=lambda: FakeLLM([]), # type: ignore[arg-type] ) with RawTestClient(app) as client: assert client.get("/healthz").status_code == 200 login_page = client.get("/login") assert login_page.status_code == 200 assert "Welcome back" in login_page.text assert client.get("/", follow_redirects=False).status_code == 303 api_response = client.get("/api/status") assert api_response.status_code == 401 assert api_response.json()["detail"].startswith("Your session has expired") assert client.get("/favicon.ico", follow_redirects=False).status_code == 303 assert client.get("/cv/", follow_redirects=False).status_code == 303 wrong = client.post( "/login", data={"username": "wrong", "password": "wrong", "next": "/"}, follow_redirects=False, ) assert wrong.status_code == 401 assert "username or password is incorrect" in wrong.text signed_in = client.post( "/login", data={ "username": TEST_USERNAME, "password": TEST_PASSWORD, "next": "https://attacker.example/steal", }, follow_redirects=False, ) assert signed_in.status_code == 303 assert signed_in.headers["location"] == "/" assert "httponly" in signed_in.headers["set-cookie"].lower() assert "samesite=strict" in signed_in.headers["set-cookie"].lower() assert client.get("/").status_code == 200 assert client.get("/api/status").status_code == 200 assert client.get("/favicon.ico").status_code == 200 assert client.get("/cv/").status_code == 200 signed_out = client.post("/logout", follow_redirects=False) assert signed_out.status_code == 303 assert signed_out.headers["location"] == "/login" assert client.get("/api/status").status_code == 401 def test_missing_authentication_configuration_fails_closed( tmp_path: Path, monkeypatch: Any ) -> None: monkeypatch.delenv("RESUME_AGENT_USERNAME") monkeypatch.delenv("RESUME_AGENT_PASSWORD") app = create_app( profile_path=tmp_path / "profile.json", output_dir=tmp_path / "output", llm_factory=lambda: FakeLLM([]), # type: ignore[arg-type] ) with RawTestClient(app) as client: assert client.get("/").status_code == 503 assert client.get("/healthz").status_code == 503 def test_root_and_status(tmp_path: Path, monkeypatch: Any) -> None: monkeypatch.delenv("RESUME_AGENT_API_KEY", raising=False) monkeypatch.delenv("OPENAI_API_KEY", raising=False) app = create_app( profile_path=tmp_path / "profile.json", output_dir=tmp_path / "output", llm_factory=lambda: FakeLLM([]), # type: ignore[arg-type] ) with TestClient(app) as client: assert client.get("/").status_code == 200 favicon = client.get("/favicon.ico") assert favicon.status_code == 200 assert favicon.headers["content-type"] == "image/x-icon" status = client.get("/api/status").json() assert status["configured"] is False assert status["profile_exists"] is False def test_build_profile_from_upload(tmp_path: Path) -> None: expected = sample_profile() app = create_app( profile_path=tmp_path / "profile.json", output_dir=tmp_path / "output", llm_factory=lambda: FakeLLM([expected]), # type: ignore[arg-type] ) with TestClient(app) as client: response = client.post( "/api/profile", files={"resume": ("resume.md", b"# Ada\nReduced API latency by 30%.")}, data={"about": ""}, ) assert response.status_code == 200 assert response.json()["facts"][0]["id"] == "F001" assert (tmp_path / "profile.json").is_file() def test_background_profile_upload_returns_pollable_task(tmp_path: Path) -> None: expected = sample_profile() app = create_app( profile_path=tmp_path / "profile.json", output_dir=tmp_path / "output", llm_factory=lambda: FakeLLM([expected]), # type: ignore[arg-type] ) with TestClient(app) as client: started = client.post( "/api/profile/start", files={"resume": ("resume.md", b"# Ada\nReduced API latency by 30%.")}, data={"about": ""}, ) task = wait_for_task(client, started.json()["task_id"]) assert started.status_code == 200 assert task["status"] == "succeeded" assert task["result"]["facts"][0]["id"] == "F001" def test_tailor_creates_downloads(tmp_path: Path) -> None: profile_path = tmp_path / "profile.json" output_dir = tmp_path / "output" save_json(sample_profile(), profile_path) expected = sample_package() app = create_app( profile_path=profile_path, output_dir=output_dir, llm_factory=lambda: FakeLLM([expected, expected]), # type: ignore[arg-type] ) with TestClient(app) as client: response = client.post( "/api/tailor", json={ "job_url": None, "job_text": "Backend engineer role requiring Python and API performance. " * 3, }, ) download = client.get("/api/download/resume.md") ohmycv_download = client.get("/api/download/resume-ohmycv.md") assert response.status_code == 200 assert response.json()["job"]["company"] == "Example Co" assert download.status_code == 200 assert "Ada Example" in download.text assert "[F013]" not in download.text assert ohmycv_download.status_code == 200 assert ohmycv_download.text.startswith('---\nname: "Ada Example"') assert "[F013]" not in ohmycv_download.text def test_background_tailoring_returns_pollable_task(tmp_path: Path) -> None: profile_path = tmp_path / "profile.json" output_dir = tmp_path / "output" save_json(sample_profile(), profile_path) expected = sample_package() app = create_app( profile_path=profile_path, output_dir=output_dir, llm_factory=lambda: FakeLLM([expected, expected]), # type: ignore[arg-type] ) with TestClient(app) as client: started = client.post( "/api/tailor/start", json={ "job_url": None, "job_text": "Backend engineer role requiring Python and API performance. " * 3, }, ) task = wait_for_task(client, started.json()["task_id"]) assert started.status_code == 200 assert task["status"] == "succeeded" assert task["result"]["job"]["company"] == "Example Co" assert (output_dir / "resume-ohmycv.md").is_file() def test_profile_based_tailoring_allows_empty_evidence_ids(tmp_path: Path) -> None: profile_path = tmp_path / "profile.json" output_dir = tmp_path / "output" save_json(sample_profile(), profile_path) flexible = sample_package() for item in flexible.resume.summary: item.evidence_ids = [] for section in flexible.resume.sections: for item in section.items: item.evidence_ids = [] app = create_app( profile_path=profile_path, output_dir=output_dir, llm_factory=lambda: FakeLLM([flexible, flexible]), # type: ignore[arg-type] ) with TestClient(app) as client: response = client.post( "/api/tailor", json={ "job_url": None, "job_text": "Backend engineer role requiring Python and API performance. " * 3, "evidence_mode": "profile", }, ) assert response.status_code == 200 assert response.json()["resume"]["summary"][0]["evidence_ids"] == [] assert (output_dir / "evidence-mode.txt").read_text() == "profile" assert "