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

384 lines
13 KiB
Python

"""Regression tests for group response redaction."""
from __future__ import annotations
from app.services.groups import GroupStore
from scripts.mock_llm import MockLLM
def _headers(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}"}
def _setup_admin(user_store, login):
user_store.complete_setup(
"admin",
"admin@example.com",
"admin-ready-password",
accepted_terms=True,
accepted_terms_at="2026-08-13T00:00:00Z",
)
return login("admin", "admin-ready-password")["token"]
def _ready_group(client, token):
client.application.extensions["llm"] = MockLLM()
headers = _headers(token)
created = client.post(
"/api/groups", json={"product": "CRM", "segment": "SME"}, headers=headers
)
assert created.status_code == 201, created.get_json()
gid = created.get_json()["group"]["id"]
analyzed = client.post(f"/api/groups/{gid}/analyze", headers=headers)
assert analyzed.status_code == 200, analyzed.get_json()
return gid, analyzed.get_json()
def test_admin_analyze_get_and_list_use_same_redaction(client, user_store, login):
_setup_admin(user_store, login)
admin = user_store.create_user(
org_id="org-default",
username="review-admin",
password="review-admin-password",
name="Review Admin",
role="admin",
email="review-admin@example.com",
must_setup=False,
)
admin_token = login(admin["username"], "review-admin-password")["token"]
gid, analyzed = _ready_group(client, admin_token)
headers = _headers(admin_token)
analyze_group = analyzed["group"]
get_group = client.get(f"/api/groups/{gid}", headers=headers).get_json()["group"]
listed = client.get(f"/api/groups/{gid}/personas", headers=headers).get_json()["personas"]
assert analyze_group.get("sales_kit") is None
assert analyze_group.get("report") is None
assert get_group.get("sales_kit") is None
assert get_group.get("report") is None
for persona in (analyze_group["personas"][0], get_group["personas"][0], listed[0]):
assert "pains" not in persona
assert "tolerance" not in persona
assert "negotiation_levers" not in persona
def test_admin_response_does_not_expose_future_secret_fields(
client, user_store, login
):
super_token = _setup_admin(user_store, login)
gid, analyzed = _ready_group(client, super_token)
source_id = analyzed["personas"][0]["id"]
stored = user_store # keep fixture dependency explicit for isolated data
group_store = client.application.extensions["group_store"]
group = group_store.get(gid)
group["personas"][0]["future_hidden_signal"] = {"secret": "must-not-leak"}
group_store.groups.replace(gid, group)
admin = stored.create_user(
org_id="org-default",
username="future-field-admin",
password="future-field-admin-password",
name="Future Field Admin",
role="admin",
email="future-field-admin@example.com",
must_setup=False,
)
token = login(admin["username"], "future-field-admin-password")["token"]
responses = [
client.get(f"/api/groups/{gid}", headers=_headers(token)).get_json()["group"]["personas"][0],
client.get(
f"/api/groups/{gid}/personas", headers=_headers(token)
).get_json()["personas"][0],
client.get(
f"/api/groups/{gid}/personas/{source_id}", headers=_headers(token)
).get_json()["persona"],
]
assert all("future_hidden_signal" not in persona for persona in responses)
def test_tenant_admin_group_view_hides_uploaded_source_payload(
client, user_store, login
):
_setup_admin(user_store, login)
group_store = client.application.extensions["group_store"]
group = group_store.create(
org_id="org-default", creator_id="admin", title="Source protected group"
)
group_store.update(
group["id"],
input={
"product": "CRM",
"segment": "SME",
"description": "safe summary",
"channel": "social",
"language": "th",
"files": ["internal.txt"],
"file_text": "provider-key=/private/path/source-secret",
},
)
admin = user_store.create_user(
org_id="org-default",
username="source-protected-admin",
password="source-protected-password",
name="Source Protected Admin",
role="admin",
must_setup=False,
)
token = login(admin["username"], "source-protected-password")["token"]
response = client.get(
f"/api/groups/{group['id']}", headers=_headers(token)
)
assert response.status_code == 200, response.get_json()
source = response.get_json()["group"]["input"]
assert source["product"] == "CRM"
assert "files" not in source
assert "file_text" not in source
assert "source-secret" not in response.get_data(as_text=True)
def test_super_admin_receives_full_group_view(client, user_store, login):
super_token = _setup_admin(user_store, login)
gid, analyzed = _ready_group(client, super_token)
assert analyzed["group"]["personas"][0]["pains"]
assert client.get(
f"/api/groups/{gid}/personas", headers=_headers(super_token)
).get_json()["personas"][0]["tolerance"]
def test_trainee_receives_revealable_persona_only(client, user_store, login):
super_token = _setup_admin(user_store, login)
gid, _ = _ready_group(client, super_token)
user = user_store.create_user(
org_id="org-default",
username="trainee-redaction",
password="trainee-redaction-password",
name="Trainee",
role="user",
must_setup=False,
)
trainee_token = login(user["username"], "trainee-redaction-password")["token"]
persona = client.get(
f"/api/groups/{gid}/personas", headers=_headers(trainee_token)
).get_json()["personas"][0]
assert "name" in persona
assert "profession" in persona
assert "pains" not in persona
assert "income" not in persona
assert "personality" not in persona
assert "tolerance" not in persona
def test_trainee_group_view_hides_uploaded_source_data(
client, user_store, login
):
super_token = _setup_admin(user_store, login)
gid, _ = _ready_group(client, super_token)
group_store = client.application.extensions["group_store"]
group = group_store.get(gid)
group_store.update(
gid,
input={
"product": "CRM",
"segment": "SME",
"description": "safe product summary",
"files": ["internal-brief.txt"],
"file_text": "CONFIDENT INTERNAL SOURCE CONTENT",
},
)
user = user_store.create_user(
org_id="org-default",
username="trainee-source-redaction",
password="trainee-source-redaction-password",
name="Trainee",
role="user",
must_setup=False,
)
trainee_token = login(user["username"], "trainee-source-redaction-password")["token"]
response = client.get(
f"/api/groups/{gid}", headers=_headers(trainee_token)
)
assert response.status_code == 200, response.get_json()
group_view = response.get_json()["group"]
assert group_view["input"]["product"] == "CRM"
assert "files" not in group_view["input"]
assert "file_text" not in group_view["input"]
def test_user_group_index_is_owner_scoped_and_revealable_only(tmp_path):
store = GroupStore(tmp_path)
private = store.create(
org_id="org-default", creator_id="owner-1", title="Private group"
)
store.update(
private["id"],
status="ready",
owner_user_id="owner-1",
input={"product": "CRM", "file_text": "private source"},
sales_kit={"productName": "CRM", "secret": "must-not-leak"},
report={"internal": "must-not-leak"},
personas=[
{
"id": "private-persona",
"name": "Private Persona",
"tier": "B",
"pains": [{"description": "hidden pain"}],
"future_hidden_signal": "must-not-leak",
}
],
)
shared = store.create(
org_id="org-default", creator_id="admin-1", title="Shared group"
)
store.update(
shared["id"],
status="ready",
input={"product": "Shared CRM"},
personas=[{"id": "shared-persona", "name": "Shared Persona", "tier": "A"}],
)
other_view = store.list_visible_to(
role="user", org_id="org-default", user_id="other-user"
)
assert {group["id"] for group in other_view} == {shared["id"]}
owner_view = store.list_visible_to(
role="user", org_id="org-default", user_id="owner-1"
)
private_view = next(group for group in owner_view if group["id"] == private["id"])
assert private_view["sales_kit"] is None
assert private_view["report"] is None
assert private_view["input"] == {"product": "CRM"}
assert private_view["personas"] == [
{
"id": "private-persona",
"name": "Private Persona",
"tier": "B",
"initiation_mode": "customer",
"channel": "facebook",
"profession": "",
"age_group": "",
"location": "",
"product_context": "",
}
]
def test_trainee_cannot_analyze(client, user_store, login):
super_token = _setup_admin(user_store, login)
gid, _ = _ready_group(client, super_token)
user = user_store.create_user(
org_id="org-default",
username="trainee-no-analyze",
password="trainee-no-analyze-password",
name="Trainee",
role="user",
must_setup=False,
)
token = login(user["username"], "trainee-no-analyze-password")["token"]
response = client.post(f"/api/groups/{gid}/analyze", headers=_headers(token))
assert response.status_code == 403, response.get_json()
def test_trainee_variant_response_is_revealable_only(client, user_store, login):
super_token = _setup_admin(user_store, login)
gid, analyzed = _ready_group(client, super_token)
user = user_store.create_user(
org_id="org-default",
username="trainee-variant",
password="trainee-variant-password",
name="Trainee",
role="user",
must_setup=False,
)
trainee_token = login(user["username"], "trainee-variant-password")["token"]
source_id = analyzed["personas"][0]["id"]
response = client.post(
f"/api/groups/{gid}/personas/{source_id}/variant",
headers=_headers(trainee_token),
)
assert response.status_code == 201, response.get_json()
persona = response.get_json()["persona"]
assert "name" in persona
assert "pains" not in persona
assert "income" not in persona
assert "tolerance" not in persona
def test_trainee_variant_is_stored_in_owner_private_group(
client, user_store, login
):
super_token = _setup_admin(user_store, login)
gid, analyzed = _ready_group(client, super_token)
user = user_store.create_user(
org_id="org-default",
username="private-variant-owner",
password="private-variant-owner-password",
name="Private Variant Owner",
role="user",
must_setup=False,
)
trainee_token = login(user["username"], "private-variant-owner-password")["token"]
headers = _headers(trainee_token)
source_id = analyzed["personas"][0]["id"]
response = client.post(
f"/api/groups/{gid}/personas/{source_id}/variant",
headers=headers,
)
assert response.status_code == 201, response.get_json()
payload = response.get_json()
variant = payload["persona"]
private_gid = payload["group_id"]
assert private_gid != gid
source_group = client.application.extensions["group_store"].get(gid)
private_group = client.application.extensions["group_store"].get(private_gid)
assert private_group["sales_kit"] == source_group["sales_kit"]
shared_personas = client.get(
f"/api/groups/{gid}/personas", headers=headers
).get_json()["personas"]
assert variant["id"] not in {p["id"] for p in shared_personas}
private_response = client.get(
f"/api/groups/{private_gid}/personas", headers=headers
)
assert private_response.status_code == 200, private_response.get_json()
private_personas = private_response.get_json()["personas"]
assert [p["id"] for p in private_personas] == [variant["id"]]
other_user = user_store.create_user(
org_id="org-default",
username="private-variant-other-user",
password="private-variant-other-password",
name="Other User",
role="user",
must_setup=False,
)
other_token = login(
other_user["username"], "private-variant-other-password"
)["token"]
other_headers = _headers(other_token)
assert client.get(
f"/api/groups/{private_gid}/personas", headers=other_headers
).status_code == 403
listed_ids = {
group["id"]
for group in client.get("/api/groups", headers=other_headers).get_json()["groups"]
}
assert private_gid not in listed_ids