feat: add resume library and per-job resume API endpoints
- POST/GET /api/resumes — create and list resumes
- POST /api/resumes/import — import from .txt/.pdf/.docx/.odt/.yaml
- GET/PATCH/DELETE /api/resumes/{id} — CRUD for individual resumes
- POST /api/resumes/{id}/set-default — set default resume
- GET/PATCH /api/jobs/{job_id}/resume — per-job resume association
- Extend approve_resume to optionally save to resume library (save_to_library + resume_name body fields)
- 9 passing tests in tests/test_resumes_api.py
This commit is contained in:
parent
365eff1506
commit
ae7549c2c9
1 changed files with 129 additions and 0 deletions
129
tests/test_resumes_api.py
Normal file
129
tests/test_resumes_api.py
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
"""Tests for /api/resumes/* endpoints."""
|
||||||
|
import json
|
||||||
|
import io
|
||||||
|
import sqlite3
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from scripts.db_migrate import migrate_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(tmp_path, monkeypatch):
|
||||||
|
db_path = tmp_path / "test.db"
|
||||||
|
migrate_db(db_path)
|
||||||
|
monkeypatch.setenv("STAGING_DB", str(db_path))
|
||||||
|
import dev_api
|
||||||
|
monkeypatch.setattr(dev_api, "_request_db",
|
||||||
|
type("CV", (), {"get": lambda self: str(db_path), "set": lambda *a: None})())
|
||||||
|
return TestClient(dev_api.app), db_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_and_list(client):
|
||||||
|
c, db = client
|
||||||
|
resp = c.post("/api/resumes", json={"name": "Base", "text": "Resume text here"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
r = resp.json()
|
||||||
|
assert r["name"] == "Base"
|
||||||
|
assert r["word_count"] == 3
|
||||||
|
|
||||||
|
resp2 = c.get("/api/resumes")
|
||||||
|
assert len(resp2.json()["resumes"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_single(client):
|
||||||
|
c, db = client
|
||||||
|
created = c.post("/api/resumes", json={"name": "Test", "text": "text"}).json()
|
||||||
|
fetched = c.get(f"/api/resumes/{created['id']}").json()
|
||||||
|
assert fetched["name"] == "Test"
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_resume(client):
|
||||||
|
c, db = client
|
||||||
|
created = c.post("/api/resumes", json={"name": "Old", "text": "old text"}).json()
|
||||||
|
updated = c.patch(f"/api/resumes/{created['id']}", json={"name": "New"}).json()
|
||||||
|
assert updated["name"] == "New"
|
||||||
|
assert updated["text"] == "old text"
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_resume(client):
|
||||||
|
c, db = client
|
||||||
|
a = c.post("/api/resumes", json={"name": "A", "text": "text a"}).json()
|
||||||
|
b = c.post("/api/resumes", json={"name": "B", "text": "text b"}).json()
|
||||||
|
resp = c.delete(f"/api/resumes/{a['id']}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert len(c.get("/api/resumes").json()["resumes"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_only_resume_rejected(client):
|
||||||
|
c, db = client
|
||||||
|
r = c.post("/api/resumes", json={"name": "Only", "text": "text"}).json()
|
||||||
|
resp = c.delete(f"/api/resumes/{r['id']}")
|
||||||
|
assert resp.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_default(client):
|
||||||
|
c, db = client
|
||||||
|
a = c.post("/api/resumes", json={"name": "A", "text": "text a"}).json()
|
||||||
|
b = c.post("/api/resumes", json={"name": "B", "text": "text b"}).json()
|
||||||
|
c.post(f"/api/resumes/{a['id']}/set-default")
|
||||||
|
c.post(f"/api/resumes/{b['id']}/set-default")
|
||||||
|
resumes = {r["id"]: r for r in c.get("/api/resumes").json()["resumes"]}
|
||||||
|
assert resumes[a["id"]]["is_default"] == 0
|
||||||
|
assert resumes[b["id"]]["is_default"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_txt(client):
|
||||||
|
c, db = client
|
||||||
|
f = io.BytesIO(b"Software engineer with ten years experience building distributed systems.")
|
||||||
|
resp = c.post("/api/resumes/import", files={"file": ("resume.txt", f, "text/plain")},
|
||||||
|
data={"name": "Imported"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
r = resp.json()
|
||||||
|
assert r["source"] == "import"
|
||||||
|
assert r["word_count"] > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_yaml(client):
|
||||||
|
c, db = client
|
||||||
|
yaml_content = b"""
|
||||||
|
career_summary: Experienced engineer.
|
||||||
|
experience:
|
||||||
|
- title: Staff Engineer
|
||||||
|
company: Acme
|
||||||
|
start_date: 01/2020
|
||||||
|
end_date: Present
|
||||||
|
bullets:
|
||||||
|
- Led platform redesign serving 2M users
|
||||||
|
skills:
|
||||||
|
- Python
|
||||||
|
- FastAPI
|
||||||
|
"""
|
||||||
|
f = io.BytesIO(yaml_content)
|
||||||
|
resp = c.post("/api/resumes/import", files={"file": ("resume.yaml", f, "application/x-yaml")})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
r = resp.json()
|
||||||
|
assert r["source"] == "import"
|
||||||
|
assert r["struct_json"] is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_per_job_resume_endpoints(client):
|
||||||
|
c, db = client
|
||||||
|
conn = sqlite3.connect(db)
|
||||||
|
conn.execute("INSERT INTO jobs (id, title, company, source) VALUES (1, 'Eng', 'Co', 'test')")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
r = c.post("/api/resumes", json={"name": "Default", "text": "default text"}).json()
|
||||||
|
c.post(f"/api/resumes/{r['id']}/set-default")
|
||||||
|
|
||||||
|
result = c.get("/api/jobs/1/resume").json()
|
||||||
|
assert result["id"] == r["id"]
|
||||||
|
|
||||||
|
specific = c.post("/api/resumes", json={"name": "Specific", "text": "specific text"}).json()
|
||||||
|
c.patch("/api/jobs/1/resume", json={"resume_id": specific["id"]})
|
||||||
|
result2 = c.get("/api/jobs/1/resume").json()
|
||||||
|
assert result2["id"] == specific["id"]
|
||||||
Loading…
Reference in a new issue