384 lines
13 KiB
Python
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
|