Files
sales-trainer/backend/tests/test_post_review_regressions.py

960 lines
31 KiB
Python

"""Regression coverage for findings from the 2026-08-15 independent review."""
from __future__ import annotations
import io
import math
from pathlib import Path
from types import SimpleNamespace
import pytest
from app.api import analytics_routes, group_routes
from app.api.helpers import ApiError
from app.config import Config
from app.services.file_parser import ParseError, parse_pdf
from app.services.groups import GroupStore
from app.services.trainee import _session_evidence
from scripts.mock_llm import MockLLM
def _headers(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}"}
def _setup_default_admin(user_store, login) -> str:
user_store.complete_setup(
"admin",
"post-review-admin@example.com",
"post-review-admin-password",
accepted_terms=True,
accepted_terms_at="2026-08-15T00:00:00Z",
)
return login("admin", "post-review-admin-password")["token"]
def _raw_group(group_store, *, personas=None, sales_kit=None):
group = group_store.create(
org_id="org-default",
creator_id="admin",
title="Post-review group",
)
return group_store.update(
group["id"],
status="ready",
input={"product": "CRM", "channel": "social", "language": "th"},
sales_kit=sales_kit,
personas=[] if personas is None else personas,
)
def _cross_tenant_chat_session(client, user_store, login, *, status="active"):
_setup_default_admin(user_store, login)
other_org = user_store.create_org("Other chat tenant")
trainee = user_store.create_user(
org_id=other_org["id"],
username=f"stale-chat-{status}",
password="stale-chat-password",
name="Stale Chat User",
role="user",
must_setup=False,
)
token = login(trainee["username"], "stale-chat-password")["token"]
group_store = client.application.extensions["group_store"]
group = group_store.create(
org_id="org-default",
creator_id="admin",
title="Other tenant chat group",
)
group = group_store.update(
group["id"],
status="ready",
personas=[
{
"id": "hidden-persona",
"name": "Hidden Persona",
"tier": "A",
"pains": ["must not be revealed"],
}
],
)
session_id = f"stale-chat-{status}-session"
session_store = client.application.extensions["session_store"]
session_store.sessions.create(
{
"id": session_id,
"org_id": other_org["id"],
"user_id": trainee["id"],
"group_id": group["id"],
"persona_id": "hidden-persona",
"persona_name": "Hidden Persona",
"mode": "trainee",
"status": status,
"outcome": "won" if status == "finished" else None,
"messages": [],
"internal": {},
"debrief": {"score": 99} if status == "finished" else None,
},
key=session_id,
)
return token, group, session_id
def test_chat_session_reads_recheck_group_tenant_context(client, user_store, login):
token, group, session_id = _cross_tenant_chat_session(
client, user_store, login, status="active"
)
direct = client.get(f"/api/chat/sessions/{session_id}", headers=_headers(token))
resumed = client.get(
f"/api/chat/{group['id']}/personas/hidden-persona/chat/resume",
headers=_headers(token),
)
listed = client.get("/api/chat/sessions", headers=_headers(token))
assert direct.status_code == 404, direct.get_json()
assert resumed.status_code == 404, resumed.get_json()
assert listed.status_code == 200, listed.get_json()
assert listed.get_json()["sessions"] == []
def test_chat_send_rechecks_group_tenant_context_before_persona_use(
client, user_store, login
):
token, group, _session_id = _cross_tenant_chat_session(
client, user_store, login, status="active"
)
client.application.extensions["llm"] = MockLLM()
response = client.post(
f"/api/chat/{group['id']}/personas/hidden-persona/chat/send",
json={"text": "try to use stale context"},
headers=_headers(token),
)
assert response.status_code == 404, response.get_json()
def test_chat_finish_rechecks_group_tenant_context_before_debrief(
client, user_store, login
):
token, group, _session_id = _cross_tenant_chat_session(
client, user_store, login, status="finished"
)
response = client.post(
f"/api/chat/{group['id']}/personas/hidden-persona/chat/finish",
headers=_headers(token),
)
assert response.status_code == 404, response.get_json()
def test_chat_start_rechecks_resumed_session_context(client, user_store, login):
_setup_default_admin(user_store, login)
trainee = user_store.create_user(
org_id="org-default",
username="malformed-resume-mode",
password="malformed-resume-password",
name="Malformed Resume Mode",
role="user",
must_setup=False,
)
token = login(trainee["username"], "malformed-resume-password")["token"]
group_store = client.application.extensions["group_store"]
group = group_store.create(org_id="org-default", creator_id="admin", title="Resume")
group_store.update(
group["id"],
status="ready",
personas=[{"id": "resume-persona", "name": "Resume Persona", "pains": []}],
)
session_store = client.application.extensions["session_store"]
session_store.sessions.create(
{
"id": "malformed-resume-session",
"org_id": "org-default",
"user_id": trainee["id"],
"group_id": group["id"],
"persona_id": "resume-persona",
"persona_name": "Resume Persona",
"mode": "",
"status": "active",
"outcome": None,
},
key="malformed-resume-session",
)
response = client.post(
f"/api/chat/{group['id']}/personas/resume-persona/chat/start",
json={},
headers=_headers(token),
)
assert response.status_code == 404, response.get_json()
def test_weak_area_routes_ignore_sessions_with_unauthorized_group(
client, user_store, login
):
_setup_default_admin(user_store, login)
trainee = user_store.create_user(
org_id="org-default",
username="stale-weak-area-user",
password="stale-weak-area-password",
name="Stale Weak Area User",
role="user",
must_setup=False,
)
token = login(trainee["username"], "stale-weak-area-password")["token"]
group_store = client.application.extensions["group_store"]
group = group_store.create(org_id="org-default", creator_id="admin", title="Private stale")
group_store.update(
group["id"],
status="ready",
owner_user_id="another-owner",
personas=[{"id": "stale-persona", "name": "Stale Persona", "pains": []}],
)
session_store = client.application.extensions["session_store"]
session_store.sessions.create(
{
"id": "stale-weak-area-session",
"org_id": "org-default",
"user_id": trainee["id"],
"group_id": group["id"],
"persona_id": "stale-persona",
"persona_name": "Stale Persona",
"mode": "trainee",
"status": "finished",
"outcome": "lost",
"debrief": {"score": 1, "why": "price objection"},
},
key="stale-weak-area-session",
)
response = client.get("/api/me/weak-areas", headers=_headers(token))
assert response.status_code == 200, response.get_json()
assert response.get_json()["insight"]["total_sessions"] == 0
def test_chat_direct_read_fails_closed_on_malformed_session_row(
client, user_store, login, monkeypatch
):
token = _setup_default_admin(user_store, login)
session_store = client.application.extensions["session_store"]
monkeypatch.setattr(session_store.sessions, "get_or_none", lambda _sid: object())
response = client.get("/api/chat/sessions/malformed-row", headers=_headers(token))
assert response.status_code == 404, response.get_json()
def test_private_group_is_published_atomically(tmp_path: Path, monkeypatch):
store = GroupStore(tmp_path)
def fail_if_called(*args, **kwargs):
raise AssertionError("private publication must not require a second update")
monkeypatch.setattr(store, "update", fail_if_called)
group = store.get_or_create_private_group(
org_id="org-default",
owner_user_id="trainee-1",
owner_name="Trainee",
)
assert group["owner_user_id"] == "trainee-1"
assert group["status"] == "ready"
assert group["input"]["language"] == "th"
@pytest.mark.parametrize(
"owner_marker, suffix",
[(None, "none"), (0, "zero"), ("", "empty")],
)
def test_chat_rejects_falsey_private_owner_markers(
client, user_store, login, owner_marker, suffix
):
_setup_default_admin(user_store, login)
trainee = user_store.create_user(
org_id="org-default",
username=f"falsey-owner-reader-{suffix}",
password="falsey-owner-reader-password",
name="Falsey Owner Reader",
role="user",
must_setup=False,
)
token = login(trainee["username"], "falsey-owner-reader-password")["token"]
group_store = client.application.extensions["group_store"]
group = group_store.create(
org_id="org-default",
creator_id="private-owner",
title="Malformed private group",
)
group_store.update(
group["id"],
status="ready",
owner_user_id=owner_marker,
personas=[{"id": "persona-1", "name": "Persona", "tier": "A"}],
)
response = client.post(
f"/api/chat/{group['id']}/personas/persona-1/chat/start",
json={},
headers=_headers(token),
)
assert response.status_code == 403, response.get_json()
def test_group_store_ignores_non_mapping_records_in_visibility_indexes(tmp_path: Path, monkeypatch):
store = GroupStore(tmp_path)
monkeypatch.setattr(store.groups, "all", lambda: [[], "malformed"])
assert store.list_for_org("org-default") == []
assert store.list_visible_to(role="admin", org_id="org-default") == []
def test_malformed_stored_auth_records_fail_closed(client, user_store, login, monkeypatch):
token = _setup_default_admin(user_store, login)
monkeypatch.setattr(user_store, "get_user_or_none", lambda _subject: ["malformed"])
response = client.get("/api/auth/me", headers=_headers(token))
assert response.status_code == 503
assert response.get_json()["error"] == "account state unavailable"
def test_malformed_stored_org_record_fails_closed(client, user_store, login, monkeypatch):
token = _setup_default_admin(user_store, login)
monkeypatch.setattr(user_store, "get_org_or_none", lambda _org_id: ["malformed"])
response = client.get("/api/auth/me", headers=_headers(token))
assert response.status_code == 503
assert response.get_json()["error"] == "account state unavailable"
def test_group_delete_keeps_cross_tenant_stale_session(client, user_store, login):
_setup_default_admin(user_store, login)
admin = user_store.create_user(
org_id="org-default",
username="post-review-delete-admin",
password="post-review-delete-admin-password",
name="Delete Admin",
role="admin",
must_setup=False,
)
token = login(admin["username"], "post-review-delete-admin-password")["token"]
user_store.create_org("Other tenant", org_id="org-other")
group_store = client.application.extensions["group_store"]
session_store = client.application.extensions["session_store"]
group = group_store.create(
org_id="org-default", creator_id=admin["id"], title="Delete me"
)
session_store.sessions.create(
{
"id": "stale-cross-tenant-session",
"org_id": "org-other",
"user_id": "someone",
"group_id": group["id"],
"persona_id": "persona-1",
"persona_name": "Persona",
"mode": "trainee",
"status": "finished",
"outcome": "won",
},
key="stale-cross-tenant-session",
)
response = client.delete(
f"/api/groups/{group['id']}", headers=_headers(token)
)
assert response.status_code == 200, response.get_json()
assert session_store.sessions.get_or_none("stale-cross-tenant-session") is not None
def test_persona_update_skips_malformed_records_before_target(
client, user_store, login
):
token = _setup_default_admin(user_store, login)
group_store = client.application.extensions["group_store"]
group = _raw_group(
group_store,
personas=[
{"name": "missing id"},
{"id": "persona-1", "name": "Before", "tier": "A"},
],
)
response = client.put(
f"/api/groups/{group['id']}/personas/persona-1",
json={"name": "After"},
headers=_headers(token),
)
assert response.status_code == 200, response.get_json()
assert response.get_json()["persona"]["name"] == "After"
def test_report_and_persona_routes_fail_closed_on_malformed_collections(
client, user_store, login, monkeypatch
):
token = _setup_default_admin(user_store, login)
group_store = client.application.extensions["group_store"]
group = _raw_group(group_store, personas=[])
group_store.update(group["id"], personas=None)
report = client.get(
f"/api/groups/{group['id']}/report?format=json",
headers=_headers(token),
)
assert report.status_code == 200, report.get_json()
user = user_store.create_user(
org_id="org-default",
username="malformed-session-user",
password="malformed-session-password",
name="Malformed Session User",
role="user",
must_setup=False,
)
user_token = login(user["username"], "malformed-session-password")["token"]
group_store.update(
group["id"],
status="ready",
personas=[{"id": "persona-1", "name": "Persona", "tier": "A"}],
)
sessions = client.application.extensions["session_store"].sessions
monkeypatch.setattr(sessions, "all", lambda: [[], "malformed"])
personas = client.get(
f"/api/groups/{group['id']}/personas",
headers=_headers(user_token),
)
assert personas.status_code == 200, personas.get_json()
assert personas.get_json()["personas"][0]["my_outcome"] == "not_tried"
def test_trainee_outcome_requires_finished_valid_result(client, user_store, login):
_setup_default_admin(user_store, login)
group_store = client.application.extensions["group_store"]
group = _raw_group(
group_store,
personas=[{"id": "persona-1", "name": "Persona", "tier": "A"}],
)
user = user_store.create_user(
org_id="org-default",
username="outcome-gating-user",
password="outcome-gating-password",
name="Outcome User",
role="user",
must_setup=False,
)
token = login(user["username"], "outcome-gating-password")["token"]
client.application.extensions["session_store"].sessions.create(
{
"id": "active-but-won",
"org_id": "org-default",
"user_id": user["id"],
"group_id": group["id"],
"persona_id": "persona-1",
"mode": "trainee",
"status": "active",
"outcome": "won",
},
key="active-but-won",
)
response = client.get(
f"/api/groups/{group['id']}/personas",
headers=_headers(token),
)
assert response.status_code == 200, response.get_json()
assert response.get_json()["personas"][0]["my_outcome"] == "not_tried"
def test_report_skips_nested_secret_values_and_nonfinite_numbers(
client, user_store, login
):
token = _setup_default_admin(user_store, login)
group_store = client.application.extensions["group_store"]
group = _raw_group(
group_store,
personas=[
{
"id": "persona-1",
"name": "Safe persona",
"tier": "A",
"difficulty": math.nan,
}
],
sales_kit={
"productName": "CRM",
"valueProps": [{"rootCause": "must-not-leak"}, "safe value"],
"features": [math.inf, "safe feature"],
},
)
response = client.get(
f"/api/groups/{group['id']}/report?format=json",
headers=_headers(token),
)
assert response.status_code == 200, response.get_json()
markdown = response.get_json()["markdown"]
assert "must-not-leak" not in markdown
assert "rootCause" not in markdown
assert "safe value" in markdown
assert "nan" not in markdown.lower()
assert "inf" not in markdown.lower()
def test_report_json_title_rejects_nested_title_values(client, user_store, login):
token = _setup_default_admin(user_store, login)
group_store = client.application.extensions["group_store"]
group = _raw_group(group_store, personas=[])
group_store.update(group["id"], title={"secret": "must-not-leak"})
response = client.get(
f"/api/groups/{group['id']}/report?format=json",
headers=_headers(token),
)
assert response.status_code == 200, response.get_json()
assert response.get_json()["title"] == "Sales training report"
assert "must-not-leak" not in response.get_data(as_text=True)
def test_weak_area_evidence_ignores_nonfinite_progress():
evidence = _session_evidence(
{
"debrief": {
"painProgress": {"pain_discovery": float("-inf")},
"why": "ordinary explanation",
}
}
)
assert "pain progress" not in evidence
assert "ordinary explanation" in evidence
def test_pdf_budget_counts_join_separators(monkeypatch, tmp_path: Path):
class FakePage:
rect = SimpleNamespace(x0=0, y0=0, x1=10, y1=1)
def get_text(self, *args, **kwargs):
return "aa"
class FakeDocument:
page_count = 2
def __iter__(self):
return iter((FakePage(), FakePage()))
def close(self):
pass
monkeypatch.setitem(
__import__("sys").modules,
"fitz",
SimpleNamespace(open=lambda _path: FakeDocument(), Rect=lambda *args: args),
)
monkeypatch.setattr(Config, "UPLOAD_MAX_EXTRACTED_CHARS", 4)
with pytest.raises(ParseError, match="extracted text too large"):
parse_pdf(tmp_path / "separator-boundary.pdf")
def test_analytics_counts_only_trainees(client, user_store, login):
_setup_default_admin(user_store, login)
admin = user_store.create_user(
org_id="org-default",
username="analytics-count-admin",
password="analytics-count-admin-password",
name="Analytics Admin",
role="admin",
must_setup=False,
)
user_store.create_user(
org_id="org-default",
username="analytics-count-user",
password="analytics-count-user-password",
name="Analytics User",
role="user",
must_setup=False,
)
token = login(admin["username"], "analytics-count-admin-password")["token"]
response = client.get("/api/analytics", headers=_headers(token))
assert response.status_code == 200, response.get_json()
assert response.get_json()["trainee_count"] == 1
def test_analytics_excludes_finished_sessions_owned_by_tenant_admin(
client, user_store, login
):
_setup_default_admin(user_store, login)
admin = user_store.create_user(
org_id="org-default",
username="analytics-admin-session-owner",
password="analytics-admin-session-owner-password",
name="Analytics Admin Session Owner",
role="admin",
must_setup=False,
)
token = login(admin["username"], "analytics-admin-session-owner-password")["token"]
group_store = client.application.extensions["group_store"]
session_store = client.application.extensions["session_store"]
group = group_store.create(
org_id="org-default", creator_id=admin["id"], title="Admin-owned session"
)
session_store.sessions.create(
{
"id": "analytics-admin-owned-session",
"org_id": "org-default",
"user_id": admin["id"],
"group_id": group["id"],
"mode": "trainee",
"status": "finished",
"outcome": "won",
},
key="analytics-admin-owned-session",
)
response = client.get("/api/analytics", headers=_headers(token))
assert response.status_code == 200, response.get_json()
assert response.get_json()["overall"]["total_sessions"] == 0
def test_super_admin_analytics_excludes_finished_sessions_owned_by_super_admin(
client, user_store, login
):
token = _setup_default_admin(user_store, login)
super_admin = user_store.get_user("admin")
group_store = client.application.extensions["group_store"]
session_store = client.application.extensions["session_store"]
group = group_store.create(
org_id="org-default", creator_id=super_admin["id"], title="Super-admin session"
)
session_store.sessions.create(
{
"id": "analytics-super-admin-owned-session",
"org_id": "org-default",
"user_id": super_admin["id"],
"group_id": group["id"],
"mode": "trainee",
"status": "finished",
"outcome": "lost",
},
key="analytics-super-admin-owned-session",
)
response = client.get("/api/analytics", headers=_headers(token))
assert response.status_code == 200, response.get_json()
assert response.get_json()["overall"]["total_sessions"] == 0
def test_super_admin_csv_export_excludes_non_trainee_owned_sessions(
client, user_store, login
):
token = _setup_default_admin(user_store, login)
tenant_admin = user_store.create_user(
org_id="org-default",
username="export-tenant-admin",
password="export-tenant-admin-password",
name="Export Tenant Admin",
role="admin",
must_setup=False,
)
trainee = user_store.create_user(
org_id="org-default",
username="export-trainee",
password="export-trainee-password",
name="Export Trainee",
role="user",
must_setup=False,
)
group_store = client.application.extensions["group_store"]
session_store = client.application.extensions["session_store"]
group = group_store.create(
org_id="org-default", creator_id="admin", title="Global export group"
)
for key, user_id in (
("export-super-admin-session", "admin"),
("export-tenant-admin-session", tenant_admin["id"]),
("export-trainee-session", trainee["id"]),
):
session_store.sessions.create(
{
"id": key,
"org_id": "org-default",
"user_id": user_id,
"group_id": group["id"],
"persona_id": key,
"persona_name": "Export Persona",
"mode": "trainee",
"status": "finished",
"outcome": "won",
"debrief": {"score": 80},
},
key=key,
)
response = client.get("/api/analytics/export", headers=_headers(token))
assert response.status_code == 200, response.get_json()
csv_text = response.get_data(as_text=True)
assert "export-trainee" in csv_text
assert "admin" not in csv_text
assert "export-tenant-admin" not in csv_text
def test_export_token_store_failures_are_controlled(app, monkeypatch):
class BrokenStore:
def update_if(self, *args, **kwargs):
raise OSError("storage unavailable")
monkeypatch.setattr(analytics_routes, "_export_token_store", lambda: BrokenStore())
with app.app_context():
assert analytics_routes._consume_export_token(
{"jti": "export-jti", "expires_at": 2_000_000_000, "user_id": "admin", "org_id": "org-default", "role": "admin"}
) is False
def test_csv_export_has_a_row_cap(client, user_store, login, monkeypatch):
token = _setup_default_admin(user_store, login)
group_store = client.application.extensions["group_store"]
session_store = client.application.extensions["session_store"]
group = group_store.create(
org_id="org-default", creator_id="admin", title="Export group"
)
user = user_store.create_user(
org_id="org-default",
username="export-row-user",
password="export-row-password",
name="Export User",
role="user",
must_setup=False,
)
for sid in ("export-row-1", "export-row-2"):
session_store.sessions.create(
{
"id": sid,
"org_id": "org-default",
"user_id": user["id"],
"group_id": group["id"],
"persona_id": sid,
"persona_name": "Persona",
"mode": "trainee",
"status": "finished",
"outcome": "won",
"debrief": {"score": 80},
},
key=sid,
)
monkeypatch.setattr(Config, "ANALYTICS_EXPORT_MAX_ROWS", 1)
with client.application.app_context(), pytest.raises(ApiError, match="too many"):
analytics_routes._export_csv_for_actor(
{"id": "admin", "org_id": "org-default", "role": "admin"},
audit=False,
)
def test_csv_export_legacy_collection_does_not_materialize_where(
client, user_store, login, monkeypatch
):
_setup_default_admin(user_store, login)
user = user_store.create_user(
org_id="org-default",
username="legacy-export-user",
password="legacy-export-password",
name="Legacy Export User",
role="user",
must_setup=False,
)
rows = [
{
"id": "legacy-export-1",
"org_id": "org-default",
"user_id": user["id"],
"group_id": "shared-group",
"mode": "trainee",
"status": "finished",
"outcome": "won",
},
{
"id": "legacy-export-2",
"org_id": "org-default",
"user_id": user["id"],
"group_id": "shared-group",
"mode": "trainee",
"status": "finished",
"outcome": "lost",
},
]
class LegacyCollection:
def iter_all(self):
return iter(rows)
def where(self, _predicate):
raise AssertionError("legacy export must not materialize where()")
fake_stores = {
"groups": SimpleNamespace(
groups=SimpleNamespace(
iter_all=lambda: iter([{"id": "shared-group", "org_id": "org-default"}])
)
),
"sessions": SimpleNamespace(sessions=LegacyCollection()),
}
monkeypatch.setattr(analytics_routes, "_stores", lambda: fake_stores)
monkeypatch.setattr(Config, "ANALYTICS_EXPORT_MAX_ROWS", 1)
with client.application.app_context(), pytest.raises(ApiError, match="too many"):
analytics_routes._export_csv_for_actor(
{"id": "admin", "org_id": "org-default", "role": "admin"},
audit=False,
)
def test_csv_export_scans_session_collection_once(
client, user_store, login, monkeypatch
):
_setup_default_admin(user_store, login)
user = user_store.create_user(
org_id="org-default",
username="single-export-scan-user",
password="single-export-scan-password",
name="Single Export Scan User",
role="user",
must_setup=False,
)
class CountingCollection:
def __init__(self, rows):
self.rows = rows
self.calls = 0
def iter_all(self):
self.calls += 1
return iter(self.rows)
groups = CountingCollection(
[
{"id": "shared-group-1", "org_id": "org-default"},
{"id": "shared-group-2", "org_id": "org-default"},
]
)
sessions = CountingCollection(
[
{
"id": "single-export-scan-1",
"org_id": "org-default",
"user_id": user["id"],
"group_id": "shared-group-1",
"mode": "trainee",
"status": "finished",
"outcome": "won",
},
{
"id": "single-export-scan-2",
"org_id": "org-default",
"user_id": user["id"],
"group_id": "shared-group-2",
"mode": "trainee",
"status": "finished",
"outcome": "lost",
},
]
)
fake_stores = {
"groups": SimpleNamespace(groups=groups),
"sessions": SimpleNamespace(sessions=sessions),
}
monkeypatch.setattr(analytics_routes, "_stores", lambda: fake_stores)
with client.application.app_context():
response = analytics_routes._export_csv_for_actor(
{"id": "admin", "org_id": "org-default", "role": "admin"},
audit=False,
)
assert response.status_code == 200
assert sessions.calls == 1
def test_csv_export_rejects_total_scan_limit(
client, user_store, login, monkeypatch
):
_setup_default_admin(user_store, login)
user = user_store.create_user(
org_id="org-default",
username="bounded-export-scan-user",
password="bounded-export-scan-password",
name="Bounded Export Scan User",
role="user",
must_setup=False,
)
class BoundedCollection:
def __init__(self, rows):
self.rows = rows
def iter_all(self):
return iter(self.rows)
fake_stores = {
"groups": SimpleNamespace(
groups=BoundedCollection(
[{"id": "bounded-scan-group", "org_id": "org-default"}]
)
),
"sessions": SimpleNamespace(
sessions=BoundedCollection(
[
{
"id": "bounded-scan-session",
"org_id": "org-default",
"user_id": user["id"],
"group_id": "bounded-scan-group",
"mode": "trainee",
"status": "finished",
"outcome": "won",
}
]
)
),
}
monkeypatch.setattr(analytics_routes, "_stores", lambda: fake_stores)
monkeypatch.setattr(Config, "ANALYTICS_EXPORT_MAX_SCAN_RECORDS", 1)
with client.application.app_context(), pytest.raises(ApiError, match="scan limit"):
analytics_routes._export_csv_for_actor(
{"id": "admin", "org_id": "org-default", "role": "admin"},
audit=False,
)
def test_upload_rejects_control_character_filename(client, user_store, login):
token = _setup_default_admin(user_store, login)
response = client.post(
"/api/groups",
data={"files": (io.BytesIO(b"product brief"), "bad\x01.txt")},
content_type="multipart/form-data",
headers=_headers(token),
)
assert response.status_code == 400, response.get_json()