Files
srbhr--resume-matcher/apps/backend/tests/integration/test_applications_api.py
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

234 lines
10 KiB
Python

"""Integration tests for the Kanban application-tracker API (real isolated DB)."""
from unittest.mock import AsyncMock, patch
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
from app.schemas.applications import APPLICATION_STATUS_ORDER
def _client():
return AsyncClient(transport=ASGITransport(app=app), base_url="http://test")
async def _seed_card(isolated_db, **kwargs):
"""Create a card directly on the DB (bypassing the LLM manual-add path)."""
defaults = dict(job_id="job-1", resume_id="res-1", status="applied")
defaults.update(kwargs)
return await isolated_db.create_application(**defaults)
class TestListAndGroup:
async def test_empty_board_has_all_seven_columns(self, isolated_db):
async with _client() as client:
resp = await client.get("/api/v1/applications")
assert resp.status_code == 200
columns = resp.json()["columns"]
assert set(columns.keys()) == set(APPLICATION_STATUS_ORDER)
assert all(columns[s] == [] for s in APPLICATION_STATUS_ORDER)
async def test_cards_grouped_by_status(self, isolated_db):
await _seed_card(isolated_db, job_id="j1", resume_id="r1", status="applied")
await _seed_card(isolated_db, job_id="j2", resume_id="r2", status="interview")
async with _client() as client:
resp = await client.get("/api/v1/applications")
columns = resp.json()["columns"]
assert len(columns["applied"]) == 1
assert len(columns["interview"]) == 1
assert columns["applied"][0]["resume_id"] == "r1"
class TestManualAdd:
async def test_manual_add_extracts_company_role(self, isolated_db):
with patch(
"app.routers.applications.extract_job_keywords",
new_callable=AsyncMock,
return_value={"company": "Acme Corp", "role": "Staff Engineer"},
):
async with _client() as client:
resp = await client.post(
"/api/v1/applications",
json={
"resume_id": "res-1",
"job_description": "We are Acme Corp hiring a Staff Engineer...",
},
)
assert resp.status_code == 200
body = resp.json()
assert body["company"] == "Acme Corp"
assert body["role"] == "Staff Engineer"
assert body["status"] == "applied"
async def test_manual_add_respects_explicit_fields_without_llm(self, isolated_db):
with patch(
"app.routers.applications.extract_job_keywords",
new_callable=AsyncMock,
) as mock_extract:
async with _client() as client:
resp = await client.post(
"/api/v1/applications",
json={
"resume_id": "res-1",
"job_description": "JD text",
"company": "Given Co",
"role": "Given Role",
"status": "saved",
},
)
assert resp.status_code == 200
# Both fields supplied → no extraction call.
mock_extract.assert_not_called()
body = resp.json()
assert body["company"] == "Given Co"
assert body["status"] == "saved"
assert body["applied_at"] is None # saved is not applied yet
class TestDetail:
async def test_detail_embeds_job_and_resume(self, isolated_db):
resume = await isolated_db.create_resume(content="# Resume")
job = await isolated_db.create_job(content="JD body text")
card = await _seed_card(isolated_db, job_id=job["job_id"], resume_id=resume["resume_id"])
async with _client() as client:
resp = await client.get(f"/api/v1/applications/{card['application_id']}")
assert resp.status_code == 200
body = resp.json()
assert body["job_content"] == "JD body text"
assert body["resume"]["resume_id"] == resume["resume_id"]
async def test_detail_tolerates_deleted_resume(self, isolated_db):
job = await isolated_db.create_job(content="JD")
card = await _seed_card(isolated_db, job_id=job["job_id"], resume_id="ghost-resume")
async with _client() as client:
resp = await client.get(f"/api/v1/applications/{card['application_id']}")
assert resp.status_code == 200
assert resp.json()["resume"] is None # never 500
async def test_detail_unknown_returns_404(self, isolated_db):
async with _client() as client:
resp = await client.get("/api/v1/applications/does-not-exist")
assert resp.status_code == 404
class TestUpdateAndMove:
async def test_patch_moves_card_across_columns(self, isolated_db):
a = await _seed_card(isolated_db, job_id="j1", resume_id="r1", status="applied")
b = await _seed_card(isolated_db, job_id="j2", resume_id="r2", status="applied")
async with _client() as client:
resp = await client.patch(
f"/api/v1/applications/{a['application_id']}",
json={"status": "interview", "position": 0},
)
assert resp.status_code == 200
assert resp.json()["status"] == "interview"
# The applied column renumbered so b is now position 0.
async with _client() as client:
board = (await client.get("/api/v1/applications")).json()["columns"]
assert board["applied"][0]["application_id"] == b["application_id"]
assert board["applied"][0]["position"] == 0
async def test_patch_notes_and_company(self, isolated_db):
card = await _seed_card(isolated_db)
async with _client() as client:
resp = await client.patch(
f"/api/v1/applications/{card['application_id']}",
json={"notes": "Recruiter call Friday", "company": "NewCo"},
)
assert resp.status_code == 200
body = resp.json()
assert body["notes"] == "Recruiter call Friday"
assert body["company"] == "NewCo"
async def test_patch_unknown_returns_404(self, isolated_db):
async with _client() as client:
resp = await client.patch("/api/v1/applications/nope", json={"notes": "x"})
assert resp.status_code == 404
class TestBulkAndDelete:
async def test_bulk_move(self, isolated_db):
a = await _seed_card(isolated_db, job_id="j1", resume_id="r1")
b = await _seed_card(isolated_db, job_id="j2", resume_id="r2")
async with _client() as client:
resp = await client.patch(
"/api/v1/applications/bulk",
json={"application_ids": [a["application_id"], b["application_id"]], "status": "rejected"},
)
assert resp.status_code == 200
assert resp.json()["affected"] == 2
async with _client() as client:
board = (await client.get("/api/v1/applications")).json()["columns"]
assert len(board["rejected"]) == 2
assert board["applied"] == []
async def test_delete_single(self, isolated_db):
card = await _seed_card(isolated_db)
async with _client() as client:
resp = await client.delete(f"/api/v1/applications/{card['application_id']}")
assert resp.status_code == 200
async with _client() as client:
board = (await client.get("/api/v1/applications")).json()["columns"]
assert board["applied"] == []
async def test_bulk_delete(self, isolated_db):
a = await _seed_card(isolated_db, job_id="j1", resume_id="r1")
b = await _seed_card(isolated_db, job_id="j2", resume_id="r2")
async with _client() as client:
resp = await client.post(
"/api/v1/applications/bulk-delete",
json={"application_ids": [a["application_id"], b["application_id"]]},
)
assert resp.status_code == 200
assert resp.json()["affected"] == 2
async with _client() as client:
board = (await client.get("/api/v1/applications")).json()["columns"]
assert board["applied"] == []
class TestRobustnessFixes:
async def test_create_dedupes_same_job_resume(self, isolated_db):
"""A second card for the same (job, resume) returns the existing one."""
first = await _seed_card(isolated_db, job_id="dup-j", resume_id="dup-r", status="applied")
second = await isolated_db.create_application(
job_id="dup-j", resume_id="dup-r", status="applied"
)
assert second["application_id"] == first["application_id"]
async with _client() as client:
board = (await client.get("/api/v1/applications")).json()["columns"]
assert len(board["applied"]) == 1
async def test_unknown_status_is_skipped_not_500(self, isolated_db):
"""A row whose status is outside the enum must not 500 the board."""
await isolated_db.create_application(job_id="j1", resume_id="r1", status="bogus_status")
await _seed_card(isolated_db, job_id="j2", resume_id="r2", status="applied")
async with _client() as client:
resp = await client.get("/api/v1/applications")
assert resp.status_code == 200
columns = resp.json()["columns"]
assert "bogus_status" not in columns
assert len(columns["applied"]) == 1 # the valid card still renders
async def test_manual_add_cleans_up_orphan_job_on_failure(self, isolated_db):
"""If application creation fails, the just-created job is removed."""
with patch.object(
isolated_db,
"create_application",
new_callable=AsyncMock,
side_effect=RuntimeError("boom"),
):
async with _client() as client:
resp = await client.post(
"/api/v1/applications",
json={
"resume_id": "res-1",
"job_description": "JD text",
"company": "Given Co",
"role": "Given Role",
},
)
assert resp.status_code == 500
stats = await isolated_db.get_stats()
assert stats["total_jobs"] == 0 # no orphan job left behind