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
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:
parent
45b75eb9ef
commit
553364b82c
43
apps/core/management/commands/get_e2e_assignment_state.py
Normal file
43
apps/core/management/commands/get_e2e_assignment_state.py
Normal 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}")
|
||||||
25
apps/core/management/commands/get_e2e_user_id.py
Normal file
25
apps/core/management/commands/get_e2e_user_id.py
Normal 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)
|
||||||
128
e2e/tests/workflows/send-to-person-workflow.spec.ts
Normal file
128
e2e/tests/workflows/send-to-person-workflow.spec.ts
Normal 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');
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user