Skip to content

Commit f188299

Browse files
committed
feat: added tests fo template APIs
1 parent 4d0e40e commit f188299

7 files changed

Lines changed: 398 additions & 259 deletions

File tree

app/db/repositories.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
def get_template(session: Session, template_id: int) -> Template | None:
99
return session.get(Template, template_id)
1010

11-
1211
# Form templates (contract Layer 6 registry)
1312
def create_form_template(session: Session, template: FormTemplate) -> FormTemplate:
1413
session.add(template)

tests/conftest.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from app.main import app
1616
from app.api.deps import get_db
1717
from app.core.config import API_PREFIX # single source of truth for all tests
18-
from app.models import Template, FormSubmission, Job, Input, Extraction, Incident, Form, Report # noqa: F401 — registers tables
18+
from app.models import Template, FormSubmission, FormTemplate, Job, Input, Extraction, Incident, Form, Report # noqa: F401 — registers tables
1919

2020
# ---------------------------------------------------------------------------
2121
# In-memory database
@@ -85,18 +85,25 @@ def pdf_upload(pdf_bytes):
8585
# ---------------------------------------------------------------------------
8686
@pytest.fixture
8787
def mock_controller():
88-
"""Patch Controller so create_template / fill_form don't touch the FS or LLM."""
89-
with patch("app.api.routes.templates.Controller") as tpl_cls, \
90-
patch("app.api.routes.forms.Controller") as form_cls:
91-
tpl_instance = MagicMock()
92-
tpl_instance.create_template.return_value = "src/inputs/test_template.pdf"
93-
tpl_cls.return_value = tpl_instance
94-
88+
"""Patch the forms Controller so fill_form doesn't touch the FS or LLM."""
89+
with patch("app.api.routes.forms.Controller") as form_cls:
9590
form_instance = MagicMock()
9691
form_instance.fill_form.return_value = "src/outputs/filled_output.pdf"
9792
form_cls.return_value = form_instance
9893

99-
yield {
100-
"template_ctrl": tpl_instance,
101-
"form_ctrl": form_instance,
102-
}
94+
yield {"form_ctrl": form_instance}
95+
96+
97+
@pytest.fixture
98+
def seed_template():
99+
"""Insert a legacy Template row directly (the /templates/create endpoint was
100+
removed in the contract migration). Returns a factory -> template id."""
101+
def _make(name: str = "T", pdf_path: str = "src/inputs/t.pdf", fields: dict | None = None) -> int:
102+
with Session(_engine) as session:
103+
tpl = Template(name=name, pdf_path=pdf_path, fields=fields if fields is not None else {"name": "string"})
104+
session.add(tpl)
105+
session.commit()
106+
session.refresh(tpl)
107+
return tpl.id
108+
109+
return _make

tests/test_api.py

Lines changed: 20 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -84,104 +84,21 @@ def test_list_templates_ordering(self, db):
8484
class TestTemplateEndpoints:
8585

8686
def test_list_templates_empty(self, client):
87+
"""Contract registry list is empty until a template is registered."""
8788
resp = client.get(f"{API_PREFIX}/templates")
8889
assert resp.status_code == 200
8990
assert resp.json() == []
9091

91-
def test_create_template(self, client, mock_controller):
92-
payload = {
93-
"name": "Fire Report",
94-
"pdf_path": "src/inputs/fire_report.pdf",
95-
"fields": {
96-
"Name": "string",
97-
"Date": "string",
98-
"Location": "string",
99-
},
100-
}
101-
resp = client.post(f"{API_PREFIX}/templates/create", json=payload)
102-
assert resp.status_code == 200
103-
104-
data = resp.json()
105-
assert data["id"] is not None
106-
assert data["name"] == "Fire Report"
107-
assert data["fields"]["Location"] == "string"
108-
# Plain create just persists the row; commonforms only runs via
109-
# the separate /make-fillable endpoint.
110-
mock_controller["template_ctrl"].create_template.assert_not_called()
111-
112-
def test_create_then_list(self, client, mock_controller):
113-
"""Creating a template should make it appear in the list."""
114-
client.post(f"{API_PREFIX}/templates/create", json={
115-
"name": "T1",
116-
"pdf_path": "a.pdf",
117-
"fields": {"f": "string"},
118-
})
119-
resp = client.get(f"{API_PREFIX}/templates")
120-
assert resp.status_code == 200
121-
assert len(resp.json()) == 1
122-
assert resp.json()[0]["name"] == "T1"
123-
124-
def test_upload_pdf(self, client, pdf_upload, tmp_path, monkeypatch):
125-
"""Upload a valid PDF file."""
126-
# Point the upload directory inside tmp_path (which is inside the project
127-
# for the path-safety check — we monkeypatch the check).
128-
monkeypatch.setattr(
129-
"app.api.routes.templates.PROJECT_ROOT",
130-
tmp_path,
131-
)
132-
resp = client.post(
133-
f"{API_PREFIX}/templates/upload",
134-
files=[pdf_upload],
135-
data={"directory": str(tmp_path)},
136-
)
137-
assert resp.status_code == 200
138-
data = resp.json()
139-
assert data["filename"] == "test_form.pdf"
140-
assert data["pdf_path"].endswith(".pdf")
141-
142-
def test_upload_non_pdf_rejected(self, client):
143-
import io
144-
bad_file = ("file", ("notes.txt", io.BytesIO(b"hello"), "text/plain"))
145-
resp = client.post(f"{API_PREFIX}/templates/upload", files=[bad_file])
146-
assert resp.status_code == 400
147-
assert "PDF" in resp.json()["detail"]
148-
149-
def test_preview_missing_file(self, client):
150-
resp = client.get(f"{API_PREFIX}/templates/preview", params={"path": "src/inputs/nonexistent.pdf"})
151-
assert resp.status_code == 404
152-
153-
def test_directory_traversal_blocked(self, client):
154-
import io
155-
pdf = ("file", ("evil.pdf", io.BytesIO(b"%PDF-1.4"), "application/pdf"))
156-
resp = client.post(
157-
f"{API_PREFIX}/templates/upload",
158-
files=[pdf],
159-
data={"directory": "/etc"},
160-
)
161-
assert resp.status_code == 400
162-
assert "inside the project" in resp.json()["detail"]
163-
16492

16593
# ═══════════════════════════════════════════════════════════════════════════
166-
# Form fill endpoints
94+
# Form fill endpoints (legacy pipeline — templates seeded directly in the DB
95+
# since the /templates/create endpoint was removed in the contract migration)
16796
# ═══════════════════════════════════════════════════════════════════════════
16897

16998
class TestFormEndpoints:
17099

171-
def _seed_template(self, client, mock_controller):
172-
"""Helper: create a template and return its ID."""
173-
resp = client.post(f"{API_PREFIX}/templates/create", json={
174-
"name": "Employee Form",
175-
"pdf_path": "src/inputs/employee.pdf",
176-
"fields": {
177-
"Employee's name": "string",
178-
"Employee's email": "string",
179-
},
180-
})
181-
return resp.json()["id"]
182-
183-
def test_fill_form_success(self, client, mock_controller):
184-
tpl_id = self._seed_template(client, mock_controller)
100+
def test_fill_form_success(self, client, mock_controller, seed_template):
101+
tpl_id = seed_template()
185102

186103
resp = client.post(f"{API_PREFIX}/forms/fill", json={
187104
"template_id": tpl_id,
@@ -202,8 +119,8 @@ def test_fill_form_missing_template(self, client, mock_controller):
202119
})
203120
assert resp.status_code == 404
204121

205-
def test_fill_form_template_file_not_found(self, client, mock_controller):
206-
tpl_id = self._seed_template(client, mock_controller)
122+
def test_fill_form_template_file_not_found(self, client, mock_controller, seed_template):
123+
tpl_id = seed_template()
207124
mock_controller["form_ctrl"].fill_form.side_effect = FileNotFoundError("PDF template not found")
208125

209126
resp = client.post(f"{API_PREFIX}/forms/fill", json={
@@ -283,9 +200,9 @@ def boom(*a, **k):
283200
assert resp.status_code == 200
284201
assert resp.json()["models"] == ["qwen2.5:1.5b"]
285202

286-
def test_fill_form_passes_model_override(self, client, mock_controller):
203+
def test_fill_form_passes_model_override(self, client, mock_controller, seed_template):
287204
"""A `model` in the request reaches Controller.fill_form but isn't persisted."""
288-
tpl_id = self._seed_template(client, mock_controller)
205+
tpl_id = seed_template()
289206
resp = client.post(f"{API_PREFIX}/forms/fill", json={
290207
"template_id": tpl_id,
291208
"input_text": "John Doe",
@@ -316,45 +233,27 @@ def fake_post(*args, **kwargs):
316233

317234
class TestE2EPipeline:
318235
"""
319-
Full pipeline: upload PDF → create template → fill form → verify DB state.
320-
This is the critical path that the product depends on.
236+
Legacy fill pipeline: seed template → fill form → verify DB state.
237+
Template registration via API was removed in the contract migration, so the
238+
template is seeded directly in the DB.
321239
"""
322240

323-
def test_full_flow(self, client, mock_controller, pdf_upload, tmp_path, monkeypatch, db):
324-
# -- Step 1: Upload a PDF --
325-
monkeypatch.setattr("app.api.routes.templates.PROJECT_ROOT", tmp_path)
326-
upload_resp = client.post(
327-
f"{API_PREFIX}/templates/upload",
328-
files=[pdf_upload],
329-
data={"directory": str(tmp_path)},
330-
)
331-
assert upload_resp.status_code == 200
332-
uploaded_path = upload_resp.json()["pdf_path"]
333-
assert uploaded_path.endswith(".pdf")
334-
335-
# -- Step 2: Create a template from the uploaded PDF --
336-
create_resp = client.post(f"{API_PREFIX}/templates/create", json={
337-
"name": "Incident Report",
338-
"pdf_path": uploaded_path,
339-
"fields": {
241+
def test_full_flow(self, client, mock_controller, seed_template, db):
242+
# -- Step 1: Seed a template --
243+
template_id = seed_template(
244+
name="Incident Report",
245+
pdf_path="src/inputs/incident.pdf",
246+
fields={
340247
"Officer name": "string",
341248
"Badge number": "string",
342249
"Incident date": "string",
343250
"Location": "string",
344251
"Description": "string",
345252
},
346-
})
347-
assert create_resp.status_code == 200
348-
template_id = create_resp.json()["id"]
253+
)
349254
assert template_id is not None
350255

351-
# -- Step 3: Verify template appears in list --
352-
list_resp = client.get(f"{API_PREFIX}/templates")
353-
assert list_resp.status_code == 200
354-
templates = list_resp.json()
355-
assert any(t["id"] == template_id for t in templates)
356-
357-
# -- Step 4: Fill the form --
256+
# -- Step 2: Fill the form --
358257
fill_resp = client.post(f"{API_PREFIX}/forms/fill", json={
359258
"template_id": template_id,
360259
"input_text": (

0 commit comments

Comments
 (0)