From 553364b82c6bcd50bba72b7e4f8e899586c0c2be Mon Sep 17 00:00:00 2001 From: ismail Date: Mon, 15 Jun 2026 15:11:54 +0300 Subject: [PATCH] test: send-to-person workflow (assign + reassign) across complaint/inquiry/observation Covers the recipient_type=person branch of the unified send-to endpoint for all 3 modules: assign to px_employee, verify the assignee can open the item detail, then reassign to a different user. 3/3 pass, 0 FAIL. Harness: get_e2e_user_id + get_e2e_assignment_state CLI helpers. --- .../commands/get_e2e_assignment_state.py | 43 ++++++ .../management/commands/get_e2e_user_id.py | 25 ++++ .../workflows/send-to-person-workflow.spec.ts | 128 ++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 apps/core/management/commands/get_e2e_assignment_state.py create mode 100644 apps/core/management/commands/get_e2e_user_id.py create mode 100644 e2e/tests/workflows/send-to-person-workflow.spec.ts diff --git a/apps/core/management/commands/get_e2e_assignment_state.py b/apps/core/management/commands/get_e2e_assignment_state.py new file mode 100644 index 0000000..831ce8d --- /dev/null +++ b/apps/core/management/commands/get_e2e_assignment_state.py @@ -0,0 +1,43 @@ +""" +Test-only helper: print the assignment state (assigned_to + assigned_at) for a +complaint / inquiry / observation, so the Playwright spec can assert the +send-to-person flow. Node can't read the Django DB. + +Usage: + manage.py get_e2e_assignment_state complaint + manage.py get_e2e_assignment_state inquiry + manage.py get_e2e_assignment_state observation +""" + +from django.core.management.base import BaseCommand, CommandError + +KIND_TO_MODEL = { + "complaint": ("apps.complaints.models", "Complaint"), + "inquiry": ("apps.complaints.models", "Inquiry"), + "observation": ("apps.observations.models", "Observation"), +} + + +class Command(BaseCommand): + help = "Print assigned_to/assigned_at for a complaint/inquiry/observation (E2E helper)." + + def add_arguments(self, parser): + parser.add_argument("kind", choices=list(KIND_TO_MODEL)) + parser.add_argument("item_id", help="Item UUID") + + def handle(self, *args, **options): + kind = options["kind"] + iid = options["item_id"] + app_mod, model_name = KIND_TO_MODEL[kind] + import importlib + mdl = getattr(importlib.import_module(app_mod), model_name) + try: + it = mdl.objects.get(id=iid) + except Exception as exc: + raise CommandError(f"{kind} {iid} not found: {exc}") + + print(f"status={it.status}") + print(f"assigned_to={it.assigned_to_id or 'NONE'}") + print(f"assigned_at={'True' if it.assigned_at else 'False'}") + if it.assigned_to_id: + print(f"assigned_to_email={it.assigned_to.email}") diff --git a/apps/core/management/commands/get_e2e_user_id.py b/apps/core/management/commands/get_e2e_user_id.py new file mode 100644 index 0000000..868193c --- /dev/null +++ b/apps/core/management/commands/get_e2e_user_id.py @@ -0,0 +1,25 @@ +""" +Test-only helper: print the User id for a given email (so the Playwright spec +can build send-to-person `person_id` values). Local CLI only. + +Usage: + manage.py get_e2e_user_id +""" + +from django.core.management.base import BaseCommand, CommandError + + +class Command(BaseCommand): + help = "Print the User id for a given email (E2E test helper)." + + def add_arguments(self, parser): + parser.add_argument("email", help="User email") + + def handle(self, *args, **options): + from apps.accounts.models import User + + try: + u = User.objects.get(email=options["email"]) + except User.DoesNotExist as exc: + raise CommandError(f"User {options['email']} not found: {exc}") + print(u.id) diff --git a/e2e/tests/workflows/send-to-person-workflow.spec.ts b/e2e/tests/workflows/send-to-person-workflow.spec.ts new file mode 100644 index 0000000..5318ffc --- /dev/null +++ b/e2e/tests/workflows/send-to-person-workflow.spec.ts @@ -0,0 +1,128 @@ +/* eslint-disable */ +/** + * "Send to person" workflow E2E — assigns a complaint/inquiry/observation to a + * specific user (the `recipient_type=person` branch of the unified send-to + * endpoint). Covers assign + reassign + the assignee being able to open the item. + * + * Run headed: + * E2E_MAXIMIZED=1 E2E_TIMEOUT=120000 E2E_ACTION_TIMEOUT=12000 E2E_NAV_TIMEOUT=20000 \ + * npx playwright test --headed --project chromium send-to-person-workflow --workers=1 + */ +import { test } from '@playwright/test'; +import { execSync } from 'child_process'; +import * as path from 'path'; +import { attachObservers, observe, loginAndScope, OBS, BASE_URL } from '../../helpers/audit'; +import { RoleName } from '../../helpers/helpers'; + +const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..'); +const PXT: RoleName = 'hospital_admin'; +const ASSIGNEE: RoleName = 'px_employee'; +const REASSIGNEE_EMAIL = 'e2e-staff@px360.test'; + +type Page = import('@playwright/test').Page; + +interface KindCfg { + label: string; + module: string; + seedKind: 'complaint' | 'inquiry' | 'observation'; + sendTo: (id: string) => string; + activate: (id: string) => string; + detail: (id: string) => string; +} + +const KINDS: KindCfg[] = [ + { label: 'Complaint', module: 'SendToPerson', seedKind: 'complaint', + sendTo: (id) => `/complaints/${id}/send-to/`, activate: (id) => `/complaints/${id}/activate/`, detail: (id) => `/complaints/${id}/` }, + { label: 'Inquiry', module: 'SendToPerson', seedKind: 'inquiry', + sendTo: (id) => `/inquiries/${id}/send-to/`, activate: (id) => `/inquiries/${id}/activate/`, detail: (id) => `/inquiries/${id}/` }, + { label: 'Observation', module: 'SendToPerson', seedKind: 'observation', + sendTo: (id) => `/observations/${id}/send-to/`, activate: (id) => `/observations/${id}/activate/`, detail: (id) => `/observations/${id}` }, +]; + +function uv(args: string): string { return execSync(args, { cwd: PROJECT_ROOT }).toString(); } + +function seedId(kind: string): string { + const cmd = kind === 'complaint' ? 'seed_e2e_complaint' : `seed_e2e_dept_response ${kind}`; + const out = uv(`uv run manage.py ${cmd}`); + const m = out.match(/(?:complaint_id|item_id)=([0-9a-f-]+)/); + if (!m) throw new Error('seed parse failed: ' + out); + return m[1]; +} + +function userId(email: string): string { return uv(`uv run manage.py get_e2e_user_id ${email}`).trim(); } + +function assignState(kind: string, id: string): Record { + const out = uv(`uv run manage.py get_e2e_assignment_state ${kind} ${id}`); + const s: Record = {}; + for (const line of out.split('\n')) { const i = line.indexOf('='); if (i > 0) s[line.slice(0, i)] = line.slice(i + 1); } + return s; +} + +async function csrfOf(page: Page): Promise { + return page.context().cookies().then((c) => c.find((x) => x.name === 'csrftoken')?.value || ''); +} +async function postForm(page: Page, url: string, data: Record) { + const csrf = await csrfOf(page); + return page.context().request.post(url, { + maxRedirects: 0, + headers: { 'X-Requested-With': 'XMLHttpRequest', ...(csrf ? { 'X-CSRFToken': csrf } : {}) }, + form: { csrfmiddlewaretoken: csrf, ...data }, + }); +} +async function login(page: Page, role: RoleName) { + await page.context().clearCookies(); + await loginAndScope(page, role, 'SendToPerson'); +} + +for (const K of KINDS) { + test.describe(`${K.label} send-to-person`, () => { + test(`assign -> assignee can open -> reassign`, async ({ page }) => { + const M = `${K.label}SendToPerson`; + attachObservers(page, M, PXT); + const assigneeId = userId('e2e-px-employee@px360.test'); + const reassigneeId = userId(REASSIGNEE_EMAIL); + let itemId = ''; + try { + itemId = seedId(K.seedKind); + observe(M, 'seed', 'INFO', `${K.label} ${itemId}`, { role: PXT }); + + // 1. PX activates (send requires in_progress) then sends to a person + await login(page, PXT); + await postForm(page, `${BASE_URL}${K.activate(itemId)}`, {}); + const send = await postForm(page, `${BASE_URL}${K.sendTo(itemId)}`, { recipient_type: 'person', person_id: assigneeId, note: 'E2E assign' }); + const sendOk = send.status() === 200; + try { const j = await send.json(); observe(M, 'assign', sendOk && j.success ? 'PASS' : 'FAIL', `HTTP ${send.status()} success=${j.success}`, { role: PXT, http: send.status() }); } + catch { observe(M, 'assign', sendOk ? 'PASS' : 'FAIL', `HTTP ${send.status()}`, { role: PXT, http: send.status() }); } + + const st = assignState(K.seedKind, itemId); + observe(M, 'assign-state', st.assigned_to === assigneeId ? 'PASS' : 'FAIL', + `assigned_to=${st.assigned_to_email || st.assigned_to} (want e2e-px-employee); assigned_at=${st.assigned_at}`, { role: PXT }); + + // 2. The assignee (px_employee) can open the item detail + await login(page, ASSIGNEE); + await page.goto(`${BASE_URL}${K.detail(itemId)}`); + await page.waitForLoadState('domcontentloaded'); + const onLogin = page.url().includes('/accounts/login/'); + observe(M, 'assignee-access', onLogin ? 'FAIL' : 'PASS', + `${ASSIGNEE} opening ${K.label} detail: ${onLogin ? 'BLOCKED(login)' : 'OK'}`, { role: ASSIGNEE, url: page.url() }); + + // 3. PX reassigns to a different person + await login(page, PXT); + const re = await postForm(page, `${BASE_URL}${K.sendTo(itemId)}`, { recipient_type: 'person', person_id: reassigneeId, note: 'E2E reassign' }); + observe(M, 'reassign', re.status() === 200 ? 'PASS' : 'FAIL', `reassign HTTP ${re.status()}`, { role: PXT, http: re.status() }); + const st2 = assignState(K.seedKind, itemId); + observe(M, 'reassign-state', st2.assigned_to === reassigneeId ? 'PASS' : 'FAIL', + `assigned_to=${st2.assigned_to_email || st2.assigned_to} (want ${REASSIGNEE_EMAIL})`, { role: PXT }); + } catch (e) { + observe(M, 'flow', 'FAIL', `exception: ${(e as Error).message}`, { role: PXT }); + } + }); + }); +} + +test.afterAll(async () => { + const counts = OBS.reduce>((a, o) => ((a[o.status] = (a[o.status] || 0) + 1), a), {}); + console.log('\n=========== SEND-TO-PERSON SUMMARY ==========='); + console.log('Total observations:', OBS.length, JSON.stringify(counts)); + console.log('==============================================\n'); +});