/* eslint-disable */ /** * FILE UPLOAD TESTS — verify attachment uploads work across modules. * * Tests: observation public form upload, RCA attachment, complaint attachment * (if supported), PX Action evidence. * * Run headed: * E2E_MAXIMIZED=1 npx playwright test --headed --project chromium file-upload-test --workers=1 */ import { test } from '@playwright/test'; import { execSync } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import { attachObservers, observe, loginAndScope, OBS, BASE_URL, getE2EHospitalId } from '../../helpers/audit'; import { RoleName } from '../../helpers/helpers'; const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..'); const M = 'FileUpload'; const ADMIN: RoleName = 'hospital_admin'; type Page = import('@playwright/test').Page; function uv(args: string): string { return execSync(args, { cwd: PROJECT_ROOT }).toString(); } function shellPy(code: string): string { const tmpFile = `/tmp/e2e_up_${Date.now()}_${Math.random().toString(36).slice(2,6)}.py`; fs.writeFileSync(tmpFile, code); const out = uv(`uv run manage.py shell < ${tmpFile}`); try { fs.unlinkSync(tmpFile); } catch {} return out; } // Create a test file for upload const TEST_FILE = '/tmp/e2e_test_upload.txt'; const TEST_FILE_PDF = '/tmp/e2e_test_upload.pdf'; if (!fs.existsSync(TEST_FILE)) fs.writeFileSync(TEST_FILE, `E2E test file upload ${Date.now()}`); // Minimal valid PDF if (!fs.existsSync(TEST_FILE_PDF)) { fs.writeFileSync(TEST_FILE_PDF, '%PDF-1.4\n1 0 obj<>endobj\n2 0 obj<>endobj\n3 0 obj<>endobj\nxref\n0 4\n0000000000 65535 f \ntrailer<>\nstartxref\n0\n%%EOF'); } async function login(page: Page) { await page.context().clearCookies(); await loginAndScope(page, ADMIN, M); } async function csrfOf(page: Page): Promise { return page.context().cookies().then((c) => c.find((x) => x.name === 'csrftoken')?.value || ''); } test.describe('File Upload Tests', () => { test.describe.configure({ mode: 'serial' }); // ── 1. Observation public form: file attachment upload ────────────────── test('Observation: upload attachment via public form', async ({ page }) => { attachObservers(page, M, 'anonymous'); const hospId = await getE2EHospitalId(page); try { // Get a fresh CSRF from the public form await page.goto(`${BASE_URL}/observations/new/`); await page.waitForSelector('#observationForm, form[enctype]', { timeout: 10000 }); const csrf = await csrfOf(page); // Upload via the form's file input const fileInput = page.locator('input[type="file"][name="attachments"]').first(); if (!(await fileInput.count())) { observe(M, 'obs-upload-input', 'WARN', 'no file input found on observation form', {}); return; } await fileInput.setInputFiles(TEST_FILE); // Fill required fields + submit const ts = Date.now(); await page.locator('textarea[name="description"]').first().fill(`E2E upload test ${ts}`); await page.locator('input[name="reporter_name"]').first().fill(`E2E Upload ${ts}`).catch(() => {}); await page.locator('input[name="reporter_phone"]').first().fill('0550000000').catch(() => {}); // POST directly (the form uses AJAX but we can POST the multipart) const r = await page.context().request.post(`${BASE_URL}/observations/new/`, { multipart: { csrfmiddlewaretoken: csrf, hospital: hospId, description: `E2E upload test ${ts}`, incident_datetime: '2026-06-18 12:00:00', reporter_name: `E2E Upload ${ts}`, reporter_phone: '0550000000', attachments: { name: 'e2e_test_upload.txt', mimeType: 'text/plain', buffer: fs.readFileSync(TEST_FILE), }, }, }); let uploaded = false; try { const data = await r.json(); uploaded = data.success === true; observe(M, 'obs-upload', uploaded ? 'PASS' : 'WARN', `observation with attachment: HTTP ${r.status()} success=${data.success}`, { http: r.status() }); } catch { // Non-JSON response — check if it was a redirect (302 = success for form POST) uploaded = r.status() === 302; observe(M, 'obs-upload', uploaded ? 'PASS' : 'WARN', `observation with attachment: HTTP ${r.status()}`, { http: r.status() }); } // Verify the attachment was saved if (uploaded) { const attOut = shellPy(` import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') django.setup() from apps.observations.models import ObservationAttachment, Observation from apps.organizations.models import Hospital e = Hospital.objects.get(code='E2E-HOSP') o = Observation.objects.filter(hospital=e).order_by('-created_at').first() if o: atts = o.attachments.all() print(f'ATTACHMENTS={atts.count()} FIRST={atts.first().filename if atts.exists() else "NONE"}') else: print('ATTACHMENTS=0 FIRST=NONE') `); const m = attOut.match(/ATTACHMENTS=(\d+)\s+FIRST=(\S+)/); if (m) { observe(M, 'obs-upload-verify', parseInt(m[1]) > 0 ? 'PASS' : 'WARN', `attachments on newest observation: ${m[1]} (first=${m[2]})`, {}); } } } catch (e) { observe(M, 'obs-upload', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 2. RCA attachment upload ──────────────────────────────────────────── test('RCA: upload attachment', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); const hospId = await getE2EHospitalId(page); try { // Get/create an RCA const out = shellPy(` import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') django.setup() from apps.rca.models import RootCauseAnalysis from apps.organizations.models import Hospital h = Hospital.objects.get(id='${hospId}') r = RootCauseAnalysis.objects.filter(hospital=h).first() if r is None: r = RootCauseAnalysis.objects.create(hospital=h, title='E2E Upload RCA', description='test', severity='low', status='draft') print(f'RID={r.id}') `); const rid = out.match(/RID=(\S+)/)?.[1] || ''; if (!rid) { observe(M, 'rca-upload', 'FAIL', 'no RCA', {}); return; } // Navigate to RCA detail await page.goto(`${BASE_URL}/rca/${rid}/`); await page.waitForLoadState('domcontentloaded'); // Upload via the attachment form (if it exists on the page) const csrf = await csrfOf(page); const r = await page.context().request.post(`${BASE_URL}/rca/${rid}/attachments/add/`, { multipart: { csrfmiddlewaretoken: csrf, file: { name: 'e2e_test_upload.txt', mimeType: 'text/plain', buffer: fs.readFileSync(TEST_FILE), }, description: 'E2E test attachment', }, }); observe(M, 'rca-upload', r.status() < 400 ? 'PASS' : 'WARN', `RCA attachment upload: HTTP ${r.status()}`, { http: r.status() }); // Verify const attOut = shellPy(` import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') django.setup() from apps.rca.models import RootCauseAnalysis, RCAAttachment r = RootCauseAnalysis.objects.get(id='${rid}') att_count = r.attachments.count() if hasattr(r, "attachments") else RCAAttachment.objects.filter(rca=r).count() print('COUNT=' + str(att_count)) `); const m = attOut.match(/COUNT=(\d+)/); observe(M, 'rca-upload-verify', m && parseInt(m[1]) > 0 ? 'PASS' : 'WARN', `RCA attachments: ${m ? m[1] : '?'}`, {}); } catch (e) { observe(M, 'rca-upload', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 3. Complaint attachment (via detail page AJAX or direct upload) ───── test('Complaint: attachment upload on detail', async ({ page }) => { attachObservers(page, M, ADMIN); await login(page); try { const out = shellPy(` import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') django.setup() from apps.complaints.models import Complaint from apps.organizations.models import Hospital h = Hospital.objects.get(code='E2E-HOSP') c = Complaint.objects.filter(hospital=h).first() print(f'CID={c.id}') `); const cid = out.match(/CID=(\S+)/)?.[1] || ''; if (!cid) { observe(M, 'complaint-upload', 'FAIL', 'no complaint', {}); return; } // Check if there's a complaint attachment upload endpoint // The complaint attachment is typically uploaded via the complaint detail page's // attachment section. Let's check if there's a form. await page.goto(`${BASE_URL}/complaints/${cid}/`); await page.waitForLoadState('domcontentloaded'); await page.waitForTimeout(1000); const hasFileInput = await page.locator('input[type="file"]').count(); observe(M, 'complaint-upload-input', hasFileInput > 0 ? 'PASS' : 'WARN', `complaint detail has file input: ${hasFileInput > 0} (${hasFileInput} inputs)`, {}); if (hasFileInput > 0) { const fileInput = page.locator('input[type="file"]').first(); await fileInput.setInputFiles(TEST_FILE); // Try to submit the upload form const submitBtn = page.locator('form:has(input[type="file"]) button[type="submit"]').first(); if (await submitBtn.count()) { await submitBtn.click().catch(() => {}); await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {}); await page.waitForTimeout(1000); observe(M, 'complaint-upload', 'PASS', 'attachment uploaded via UI', {}); } else { observe(M, 'complaint-upload', 'WARN', 'file input found but no submit button', {}); } } else { observe(M, 'complaint-upload', 'WARN', 'no file upload on complaint detail (may be via API only)', {}); } } catch (e) { observe(M, 'complaint-upload', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); // ── 4. Verify uploaded files exist in media/ ─────────────────────────── test('Verify uploaded files exist in media/', async ({ page }) => { attachObservers(page, M, ADMIN); try { const out = shellPy(` import django, os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.dev') django.setup() from apps.observations.models import ObservationAttachment from apps.rca.models import RCAAttachment obs_atts = ObservationAttachment.objects.filter(filename__icontains='e2e').count() rca_atts = RCAAttachment.objects.filter(filename__icontains='e2e').count() print(f'OBS_ATTS={obs_atts} RCA_ATTS={rca_atts}') # Check media dir import os media_root = os.path.join(os.getcwd(), 'media') if os.path.isdir(media_root): files = [] for root, dirs, fnames in os.walk(media_root): for f in fnames: if 'e2e' in f.lower() or 'upload' in f.lower(): files.append(os.path.join(root, f)) print(f'MEDIA_FILES={len(files)}') for fp in files[:3]: print(f'FILE={fp} SIZE={os.path.getsize(fp)}') else: print('MEDIA_FILES=0') `); const obsM = out.match(/OBS_ATTS=(\d+)/); const rcaM = out.match(/RCA_ATTS=(\d+)/); const mediaM = out.match(/MEDIA_FILES=(\d+)/); const obsCount = obsM ? parseInt(obsM[1]) : 0; const rcaCount = rcaM ? parseInt(rcaM[1]) : 0; const mediaCount = mediaM ? parseInt(mediaM[1]) : 0; observe(M, 'obs-attachments-in-db', obsCount > 0 ? 'PASS' : 'WARN', `observation attachments with 'e2e' in name: ${obsCount}`, {}); observe(M, 'rca-attachments-in-db', rcaCount > 0 ? 'PASS' : 'WARN', `RCA attachments with 'e2e' in name: ${rcaCount}`, {}); observe(M, 'media-files-exist', mediaCount > 0 ? 'PASS' : 'WARN', `matching files in media/: ${mediaCount}`, {}); } catch (e) { observe(M, 'verify', 'FAIL', `exception: ${(e as Error).message}`, {}); } }); }); test.afterAll(async () => { const counts = OBS.reduce>((a, o) => ((a[o.status] = (a[o.status] || 0) + 1), a), {}); console.log('\n=========== FILE UPLOAD SUMMARY ==========='); console.log('Total observations:', OBS.length, JSON.stringify(counts)); console.log('===========================================\n'); });