Files
sales-trainer/backend/tests/test_private_group_race.py
Macky 3c22d88bcd feat: demo SaaS + training flow security hardening (8/8 review gate passed)
- Demo accounts: super_admin-only provisioning into isolated DEMO_ORG_ID tenant,
  30-day UTC trial on first login, revocable, one-time credential delivery via
  optional SES/webhook (never persisted). Adds boto3 dependency.
- Analytics/report/export/privacy: shared bounded scan budget across users/groups/
  sessions, tenant-consistent session/user/group joins, scalar-only CSV export
  (no nested persisted-value stringification).
- Ownership/tenant isolation: canonical owner-tenant predicate for list/read/chat;
  client sees is_owned only, never owner_user_id.
- Lifecycle/races: status transition validation, analyzing is an in-progress gate
  (no duplicate reanalysis), structured-ready publication, stale-variant revalidation.
- Auth/setup/consent/JWT/OAuth/config: fail-closed consent, bounded JWT lifetime,
  provider-subject atomic OAuth identity, repeated-secret rejection, strict Persona
  trait validation.
- Chat/session/privacy: pre-seller opener redaction, corrupt-session recovery,
  role-aware completed-chat dashboard routing.
- Frontend: Training→product→personas→practice flow, demo/role/demo guards,
  is_owned-based ownership display, 320×568 and 500×768 responsive E2E.
- 8 independent exact-five-key review scopes passed; backend 509, frontend 26,
  production build 1775 modules, isolated E2E 15.
2026-08-25 06:39:06 +07:00

249 lines
8.1 KiB
Python

"""Private group lookup/create race regression tests."""
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from threading import Barrier
import pytest
from app.services.groups import GroupStore
def test_private_group_lookup_and_create_is_atomic(tmp_path: Path):
store = GroupStore(tmp_path)
barrier = Barrier(8)
def get_group():
barrier.wait(timeout=5)
return store.get_or_create_private_group(
org_id="org-1",
owner_user_id="user-1",
owner_name="Trainee",
)
with ThreadPoolExecutor(max_workers=8) as pool:
groups = list(pool.map(lambda _: get_group(), range(8)))
assert len({group["id"] for group in groups}) == 1
assert len(store.list_for_org("org-1")) == 1
assert groups[0]["owner_user_id"] == "user-1"
assert groups[0]["status"] == "draft"
assert groups[0]["personas"] == []
def test_private_group_lookup_revalidates_candidate_after_snapshot(
tmp_path: Path, monkeypatch
):
store = GroupStore(tmp_path)
existing = store.get_or_create_private_group(
org_id="org-1", owner_user_id="user-1", owner_name="Trainee"
)
original_list = store.list_for_org
transferred = False
def list_then_transfer(*, org_id):
nonlocal transferred
rows = original_list(org_id=org_id)
if rows and not transferred:
transferred = True
store.groups.update(rows[0]["id"], owner_user_id="user-2")
return rows
monkeypatch.setattr(store, "list_for_org", list_then_transfer)
selected = store.get_or_create_private_group(
org_id="org-1", owner_user_id="user-1", owner_name="Trainee"
)
assert selected["id"] != existing["id"]
assert selected["owner_user_id"] == "user-1"
@pytest.mark.parametrize("operation", ["get_or_create", "append"])
def test_private_group_lookup_fails_closed_on_malformed_candidate_id(
tmp_path: Path, monkeypatch, operation: str
):
store = GroupStore(tmp_path)
calls = 0
def malformed_rows(*, org_id):
nonlocal calls
calls += 1
if calls > 1:
raise AssertionError("malformed private-group candidates must not retry forever")
return [{"id": 0, "org_id": org_id, "owner_user_id": "user-1"}]
monkeypatch.setattr(store, "list_for_org", malformed_rows)
with pytest.raises(ValueError, match="private group state"):
if operation == "get_or_create":
store.get_or_create_private_group(
org_id="org-1", owner_user_id="user-1", owner_name="Trainee"
)
else:
store.append_private_persona(
org_id="org-1",
owner_user_id="user-1",
persona={"id": "variant", "name": "Variant"},
)
assert calls == 1
def test_private_persona_append_revalidates_target_state(
tmp_path: Path, monkeypatch
):
store = GroupStore(tmp_path)
group = store.append_private_persona(
org_id="org-1",
owner_user_id="user-1",
persona={"id": "initial", "name": "Initial"},
)
original_record_lock = store.record_lock
transitioned = False
def lock_after_transition(gid):
nonlocal transitioned
if gid == group["id"] and not transitioned:
transitioned = True
store.groups.update(gid, status="failed")
return original_record_lock(gid)
monkeypatch.setattr(store, "record_lock", lock_after_transition)
with pytest.raises(ValueError, match="not ready"):
store.append_private_persona(
org_id="org-1",
owner_user_id="user-1",
persona={"id": "variant", "name": "Variant"},
)
def test_private_groups_are_scoped_by_org_and_owner(tmp_path: Path):
store = GroupStore(tmp_path)
first = store.get_or_create_private_group(org_id="org-1", owner_user_id="user-1")
same_owner = store.get_or_create_private_group(org_id="org-1", owner_user_id="user-1")
other_org = store.get_or_create_private_group(org_id="org-2", owner_user_id="user-1")
other_owner = store.get_or_create_private_group(org_id="org-1", owner_user_id="user-2")
assert same_owner["id"] == first["id"]
assert other_org["id"] != first["id"]
assert other_owner["id"] != first["id"]
assert len(store.list_for_org("org-1")) == 2
assert len(store.list_for_org("org-2")) == 1
def test_private_persona_append_publishes_ready_group_with_persona(tmp_path: Path):
store = GroupStore(tmp_path)
group = store.append_private_persona(
org_id="org-1",
owner_user_id="user-1",
owner_name="Trainee",
persona={
"id": "variant-1",
"name": "Variant",
"channel": "facebook",
"initiation_mode": "customer",
},
)
assert group["status"] == "ready"
assert [persona["id"] for persona in group["personas"]] == ["variant-1"]
persisted = store.get(group["id"])
assert persisted["status"] == "ready"
assert [persona["id"] for persona in persisted["personas"]] == ["variant-1"]
def test_private_persona_append_can_publish_the_initial_draft(tmp_path: Path):
store = GroupStore(tmp_path)
draft = store.get_or_create_private_group(
org_id="org-1", owner_user_id="user-1", owner_name="Trainee"
)
published = store.append_private_persona(
org_id="org-1",
owner_user_id="user-1",
persona={"id": "first", "name": "First variant"},
)
assert published["id"] == draft["id"]
assert published["status"] == "ready"
assert [persona["id"] for persona in published["personas"]] == ["first"]
def test_private_group_cannot_publish_ready_without_a_persona(tmp_path: Path):
store = GroupStore(tmp_path)
draft = store.get_or_create_private_group(
org_id="org-1", owner_user_id="user-1", owner_name="Trainee"
)
with pytest.raises(ValueError, match="persona"):
store.update(draft["id"], status="ready")
def test_private_group_rejects_truthy_non_list_personas_when_ready(tmp_path: Path):
store = GroupStore(tmp_path)
with pytest.raises(ValueError, match="persona"):
store.create(
org_id="org-1",
creator_id="user-1",
owner_user_id="user-1",
title="Malformed ready group",
status="ready",
personas={"id": "not-a-list"},
)
def test_private_group_rejects_malformed_persona_traits_when_ready(tmp_path: Path):
store = GroupStore(tmp_path)
with pytest.raises(ValueError, match="persona traits"):
store.create(
org_id="org-1",
creator_id="user-1",
owner_user_id="user-1",
title="Malformed ready persona",
status="ready",
personas=[{"id": "p-1", "name": "Bad", "channel": "internal"}],
)
def test_locked_persona_appends_preserve_all_concurrent_variants(tmp_path: Path):
store = GroupStore(tmp_path)
group = store.get_or_create_private_group(org_id="org-1", owner_user_id="user-1")
barrier = Barrier(8)
def append_one(index: int):
barrier.wait(timeout=5)
with store.record_lock(group["id"]):
current = store.get(group["id"])
store.set_personas(
group["id"],
current.get("personas", []) + [{"id": f"variant-{index}", "name": str(index)}],
)
with ThreadPoolExecutor(max_workers=8) as pool:
list(pool.map(append_one, range(8)))
personas = store.get(group["id"])["personas"]
assert {persona["id"] for persona in personas} == {f"variant-{i}" for i in range(8)}
def test_record_lock_is_reentrant_for_nested_service_writes(tmp_path: Path):
store = GroupStore(tmp_path)
group = store.create(
org_id="org-1",
creator_id="user-1",
title="Nested",
sales_kit={"productName": "Test product"},
report={"summary": "Test report"},
)
with store.record_lock(group["id"]):
with store.record_lock(group["id"]):
store.set_personas(group["id"], [{"id": "persona-1", "name": "Nested"}])
assert store.get(group["id"])["personas"][0]["id"] == "persona-1"