Skip to content

Commit c96f95c

Browse files
authored
Create test_controllers.py
1 parent 10c3f7a commit c96f95c

1 file changed

Lines changed: 105 additions & 0 deletions

File tree

‎tests/test_controllers.py‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Controller-level tests: business rules without an HTTP request.
2+
3+
These exist to prove the split is real -- the rules they exercise used
4+
to live inside route handlers and could only be reached through a
5+
TestClient. Anything here that needs FastAPI has not been separated
6+
properly.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from dataclasses import dataclass
12+
13+
import pytest
14+
15+
from app.controllers import files, runs
16+
from app.controllers.errors import (
17+
Conflict,
18+
DomainError,
19+
InvalidRequest,
20+
NotFound,
21+
PayloadTooLarge,
22+
Unauthorized,
23+
)
24+
25+
26+
@dataclass
27+
class _Upload:
28+
"""Stand-in for a ConversationFile row (only stored_name matters)."""
29+
30+
stored_name: str
31+
32+
33+
class TestHarnessPrompt:
34+
"""The prompt the agent receives is assembled, not passed through."""
35+
36+
def test_no_files_still_gets_the_save_hint(self) -> None:
37+
out = runs.build_harness_prompt("summarize", [])
38+
assert out.startswith("summarize")
39+
assert "Save any result files" in out
40+
assert "Uploaded files" not in out
41+
42+
def test_uploads_are_named_by_stored_name(self) -> None:
43+
"""stored_name, not filename: the on-disk name carries a unique
44+
prefix and is the one the agent can actually open."""
45+
out = runs.build_harness_prompt("go", [_Upload("ab12cd34_sales.xlsx")])
46+
assert "ab12cd34_sales.xlsx" in out
47+
assert "Uploaded files in your working directory" in out
48+
49+
def test_multiple_uploads_are_comma_joined(self) -> None:
50+
out = runs.build_harness_prompt("go", [_Upload("a_1.xlsx"), _Upload("b_2.csv")])
51+
assert "a_1.xlsx, b_2.csv" in out
52+
53+
def test_user_prompt_is_never_mutated(self) -> None:
54+
prompt = "summarize the sheet"
55+
out = runs.build_harness_prompt(prompt, [_Upload("x_1.xlsx")])
56+
assert out.startswith(prompt)
57+
assert prompt == "summarize the sheet"
58+
59+
60+
class TestSafeFilename:
61+
@pytest.mark.parametrize(
62+
("raw", "expected"),
63+
[
64+
("sales.xlsx", "sales.xlsx"),
65+
("../../etc/passwd", "passwd"),
66+
("/abs/path/report.csv", "report.csv"),
67+
("we!rd na@me.txt", "we_rd_na_me.txt"),
68+
("", "upload"),
69+
("...", "upload"),
70+
("..", "upload"),
71+
],
72+
)
73+
def test_sanitizing(self, raw: str, expected: str) -> None:
74+
assert files.safe_filename(raw) == expected
75+
76+
def test_name_is_length_capped(self) -> None:
77+
assert len(files.safe_filename("a" * 500 + ".xlsx")) == 200
78+
79+
def test_no_result_can_escape_a_directory(self) -> None:
80+
for raw in ("../../x", "/etc/shadow", "..\\..\\win.ini", "a/b/c"):
81+
cleaned = files.safe_filename(raw)
82+
assert "/" not in cleaned and "\\" not in cleaned
83+
assert cleaned not in ("", ".", "..")
84+
85+
86+
class TestDomainErrors:
87+
"""Status codes belong to the error type, so routes need no mapping."""
88+
89+
@pytest.mark.parametrize(
90+
("exc", "code"),
91+
[
92+
(InvalidRequest, 400),
93+
(Unauthorized, 401),
94+
(NotFound, 404),
95+
(Conflict, 409),
96+
(PayloadTooLarge, 413),
97+
],
98+
)
99+
def test_status_codes(self, exc: type[DomainError], code: int) -> None:
100+
assert exc("nope").status_code == code
101+
102+
def test_detail_is_the_message(self) -> None:
103+
err = NotFound("conversation not found")
104+
assert err.detail == "conversation not found"
105+
assert str(err) == "conversation not found"

0 commit comments

Comments
 (0)