test: send-to-person workflow (assign + reassign) across complaint/inquiry/observation
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m21s

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.
This commit is contained in:
ismail 2026-06-15 15:11:54 +03:00
parent 45b75eb9ef
commit 553364b82c
3 changed files with 196 additions and 0 deletions

View File

@ -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 <complaint_id>
manage.py get_e2e_assignment_state inquiry <inquiry_id>
manage.py get_e2e_assignment_state observation <observation_id>
"""
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}")

View File

@ -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 <email>
"""
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)

View File

@ -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<string, string> {
const out = uv(`uv run manage.py get_e2e_assignment_state ${kind} ${id}`);
const s: Record<string, string> = {};
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<string> {
return page.context().cookies().then((c) => c.find((x) => x.name === 'csrftoken')?.value || '');
}
async function postForm(page: Page, url: string, data: Record<string, string>) {
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<Record<string, number>>((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');
});