import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import ExcelJS = require('exceljs'); import PizZip from 'pizzip'; import { AnexaType, MedicalCheckupType, MedicalVerdict, PrismaClient, RiskExposureType, } from '@prisma/client'; import { DocxTemplateService } from '../src/modules/medical/services/docx-template.service'; type HttpResult = { status: number; body: unknown; text: string }; const prisma = new PrismaClient(); const warnings: string[] = []; function ok(message: string) { console.log(`OK ${message}`); } function warn(message: string) { warnings.push(message); console.warn(`WARN ${message}`); } function assert(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); } function dbNameFromUrl(url: string): string { return decodeURIComponent(new URL(url).pathname.replace(/^\//, '')); } function requireTemporaryDatabase() { const url = process.env.DATABASE_URL; if (!url) throw new Error('DATABASE_URL is required for verification'); const dbName = dbNameFromUrl(url); if (!dbName.startsWith('hrm_medpark_test_') && process.env.ALLOW_NON_TEST_DB !== 'true') { throw new Error(`Refusing to verify non-test database "${dbName}". Expected hrm_medpark_test_*.`); } return dbName; } function sourcePath(fileName: string) { return resolve(process.cwd(), '..', '..', '..', fileName); } function docxText(filePath: string): string { const zip = new PizZip(readFileSync(filePath)); const xml = Object.keys(zip.files) .filter((name) => /^word\/(document|header|footer).*\.xml$/.test(name)) .map((name) => zip.file(name)?.asText() ?? '') .join('\n'); return xml .replace(/<\/w:p>/g, '\n') .replace(/<[^>]+>/g, ' ') .replace(/\s+/g, ' ') .trim(); } async function verifySourceFiles() { const controlDocx = process.env.CONTROL_MEDICAL_DOCX ?? sourcePath('Control medical (5).docx'); const rubriciXlsx = process.env.RUBRICI_NECESARE_XLSX ?? sourcePath('Rubrici necesare (6).xlsx'); assert(existsSync(controlDocx), `Missing source file: ${controlDocx}`); assert(existsSync(rubriciXlsx), `Missing source file: ${rubriciXlsx}`); const controlText = docxText(controlDocx); for (const phrase of [ /Baza de date a angajatilor trebuie sa contina/i, /Persoana expusa profesional la radiatii ionizante/i, /Tipul controlului medical/i, /Fisa de solicitare/i, /aptitudine/i, /Apt condi/i, /Inapt temporar/i, ]) { assert(phrase.test(controlText), `Control medical checklist phrase not found: ${phrase}`); } const workbook = new ExcelJS.Workbook(); await workbook.xlsx.readFile(rubriciXlsx); const rubrici = workbook.getWorksheet('Rubrici'); const performance = workbook.getWorksheet('Sheet1'); assert(rubrici, 'Rubrici necesare workbook must contain sheet "Rubrici"'); assert(performance, 'Rubrici necesare workbook must contain sheet "Sheet1"'); const fields = new Set(); for (let rowIndex = 1; rowIndex <= rubrici.actualRowCount; rowIndex += 1) { const value = excelCellText(rubrici.getRow(rowIndex).getCell(3)).trim(); if (value) fields.add(value); } for (const field of ['IDNP', 'Nume', 'Prenume', 'Data nasterii', 'Domiciliu', 'Nr de telefon personal', 'tipul actului de identitate', 'Nr CIM']) { assert(fields.has(field), `Rubrici checklist field not found: ${field}`); } const performanceText = docxLikeSheetText(performance); for (const phrase of ['Abilitati clinice nursing', 'Judecata clinica', 'Respectarea Dress Code', 'Membru unui comitet']) { assert(performanceText.includes(phrase), `Performance checklist phrase not found: ${phrase}`); } ok('source DOCX/XLSX checklists are present and readable'); } function docxLikeSheetText(worksheet: ExcelJS.Worksheet) { const parts: string[] = []; worksheet.eachRow((row) => { row.eachCell((cell) => { const text = excelCellText(cell).trim(); if (text) parts.push(text); }); }); return parts.join(' '); } function excelCellText(cell: ExcelJS.Cell) { const value = cell.value; if (value == null) return ''; if (value instanceof Date) return value.toISOString(); if (typeof value !== 'object') return String(value); if ('text' in value && typeof value.text === 'string') return value.text; if ('result' in value && value.result != null) return String(value.result); if ('richText' in value && Array.isArray(value.richText)) { return value.richText.map((part) => part.text).join(''); } if ('hyperlink' in value && 'text' in value && typeof value.text === 'string') return value.text; return ''; } async function verifyDatabaseCoverage() { const [ employees, identityDocuments, familyMembers, educations, qualifications, trainings, sanctions, benefits, contracts, riskCards, profiles, checkups, evaluationForms, ] = await Promise.all([ prisma.employee.count(), prisma.identityDocument.count(), prisma.familyMember.count(), prisma.education.count(), prisma.qualification.count(), prisma.training.count(), prisma.disciplinarySanction.count(), prisma.benefit.count(), prisma.employmentContract.count(), prisma.workplaceRiskCard.count(), prisma.employeeMedicalProfile.count(), prisma.medicalCheckup.count(), prisma.evaluationForm.count(), ]); assert(employees >= 6, `Expected at least 6 employees, got ${employees}`); assert(identityDocuments >= employees, 'Every seeded employee should have an identity document'); assert(familyMembers >= 3, 'Expected family/contact records'); assert(educations >= employees, 'Expected education records for all employees'); assert(qualifications >= 4, 'Expected qualification records'); assert(trainings >= employees, 'Expected training records for all employees'); assert(sanctions >= 2, 'Expected disciplinary sanction scenarios'); assert(benefits >= 2, 'Expected benefit scenarios'); assert(contracts >= employees, 'Expected active CIM for all employees'); assert(riskCards >= 3, 'Expected STANDARD, radiation, and DISTANTA_DIGITAL risk cards'); assert(profiles >= employees, 'Expected medical profile for all employees'); assert(checkups >= 8, 'Expected pending and completed checkups'); assert(evaluationForms >= 3, 'Expected performance evaluation forms'); const exposureTypes = await prisma.workplaceRiskExposure.findMany({ select: { tip: true }, distinct: ['tip'] }); const seededExposureTypes = new Set(exposureTypes.map((row) => row.tip)); for (const type of Object.values(RiskExposureType)) { assert(seededExposureTypes.has(type), `Missing risk exposure type in test seed: ${type}`); } const verdicts = await prisma.medicalCheckup.findMany({ where: { verdict: { not: null } }, select: { verdict: true }, distinct: ['verdict'], }); const seededVerdicts = new Set(verdicts.map((row) => row.verdict)); for (const verdict of Object.values(MedicalVerdict)) { assert(seededVerdicts.has(verdict), `Missing medical verdict scenario: ${verdict}`); } const radiationProfiles = await prisma.employeeMedicalProfile.count({ where: { expusRadiatiiIonizante: true } }); const overexposures = await prisma.radiationOverexposure.findMany({ select: { fel: true }, distinct: ['fel'] }); const remoteCards = await prisma.workplaceRiskCard.count({ where: { tipFisa: 'DISTANTA_DIGITAL' } }); const pending = await prisma.medicalCheckup.count({ where: { verdict: null } }); assert(radiationProfiles >= 2, 'Expected at least two radiation-exposed employees'); assert(overexposures.length >= 2, 'Expected exceptional and accidental overexposure rows'); assert(remoteCards >= 1, 'Expected Anexa 4A/DISTANTA_DIGITAL risk card'); assert(pending >= 3, 'Expected pending checkups for medic inbox'); ok('database seed covers HR, performance, and Control medical scenarios'); } function extractTemplatePlaceholders(fileName: string) { const fullPath = resolve(process.cwd(), 'templates', 'docx', fileName); assert(existsSync(fullPath), `Missing DOCX template: ${fullPath}`); const zip = new PizZip(readFileSync(fullPath)); const xml = Object.keys(zip.files) .filter((name) => /^word\/(document|header|footer).*\.xml$/.test(name)) .map((name) => zip.file(name)?.asText() ?? '') .join('\n'); const opens = (xml.match(/\{/g) ?? []).length; const closes = (xml.match(/\}/g) ?? []).length; assert(opens === closes, `${fileName} has unbalanced placeholders: opens=${opens}, closes=${closes}`); const placeholders = Array.from(xml.matchAll(/\{([^{}]+)\}/g)).map((match) => match[1]); return { placeholders, opens }; } function valueForPlaceholder(name: string, index = 1) { if (name.startsWith('cb')) return index % 2 === 0 ? '☐' : '☑'; if (/data|PanaLa/i.test(name)) return '27.05.2026'; if (/email/i.test(name)) return 'hr.test@medpark.md'; if (/telefon|fax/i.test(name)) return '+373 22 000 000'; if (/idnp/i.test(name)) return `19850615000${index}`; if (/nr|numar|anNastere|ani/i.test(name)) return String(index); if (/doza/i.test(name)) return (index + 0.25).toFixed(4); return `Test ${name} ${index}`; } function sampleDocxData(placeholders: string[]) { const data: Record = {}; for (const raw of placeholders) { const name = raw.replace(/^#|\//g, ''); if (!name || raw.startsWith('#') || raw.startsWith('/')) continue; data[name] = valueForPlaceholder(name); } const row = (index: number) => { const values: Record = {}; for (const raw of placeholders) { const name = raw.replace(/^#|\//g, ''); if (!name || raw.startsWith('#') || raw.startsWith('/')) continue; values[name] = valueForPlaceholder(name, index); } values.nr = String(index); values.numePrenume = index === 1 ? 'Popescu Alexandru' : 'Ionescu Maria'; values.denumire = index === 1 ? 'Glutaraldehidă' : 'HBV/HCV/HIV'; values.tipExpunere = index === 1 ? 'X externă' : 'gamma externă'; return values; }; for (const loopName of ['angajati', 'chimici', 'pulberi', 'biologici', 'zgomot', 'vibratii', 'campEM', 'optice', 'supraexpExceptionale', 'supraexpAccidentale']) { data[loopName] = [row(1), row(2)]; } return data; } async function verifyDocxTemplates() { const service = new DocxTemplateService(); const files: Record = { ANEXA_3: 'anexa-3.docx', ANEXA_4: 'anexa-4.docx', ANEXA_4A: 'anexa-4a.docx', ANEXA_4B: 'anexa-4b.docx', ANEXA_6: 'anexa-6.docx', }; for (const [type, fileName] of Object.entries(files) as [AnexaType, string][]) { const { placeholders, opens } = extractTemplatePlaceholders(fileName); assert(opens > 0, `${fileName} should contain docxtemplater placeholders`); const rendered = service.render(type, sampleDocxData(placeholders)); const zip = new PizZip(rendered); const badParts: string[] = []; for (const name of Object.keys(zip.files).filter((entry) => /^word\/(document|header|footer).*\.xml$/.test(entry))) { const xml = zip.file(name)?.asText() ?? ''; if (/[{}]/.test(xml) || /\b(undefined|null)\b/i.test(xml)) badParts.push(name); } assert(badParts.length === 0, `${fileName} rendered XML still contains placeholders/nulls in ${badParts.join(', ')}`); } ok('all Anexa DOCX templates render cleanly through DocxTemplateService'); } async function isMinioAvailable() { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 1500); try { const response = await fetch('http://localhost:9000/minio/health/live', { signal: controller.signal }); return response.ok; } catch { return false; } finally { clearTimeout(timeout); } } async function apiRequest(baseUrl: string, method: string, path: string, body?: unknown, token?: string): Promise { let lastError: unknown; for (let attempt = 1; attempt <= 3; attempt += 1) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 20_000); try { const headers: Record = { Accept: 'application/json' }; if (body !== undefined) headers['Content-Type'] = 'application/json'; if (token) headers.Authorization = `Bearer ${token}`; const response = await fetch(`${baseUrl}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal: controller.signal, }); const text = await response.text(); let parsed: unknown = null; try { parsed = text ? JSON.parse(text) : null; } catch { parsed = text; } return { status: response.status, body: parsed, text }; } catch (error) { lastError = error; if (attempt < 3) await new Promise((resolvePromise) => setTimeout(resolvePromise, 1000 * attempt)); } finally { clearTimeout(timeout); } } throw new Error(`${method} ${path} failed after retries: ${String(lastError)}`); } function tokenFrom(body: unknown) { if (typeof body === 'object' && body && 'token' in body && typeof (body as { token: unknown }).token === 'string') { return (body as { token: string }).token; } throw new Error('dev-login response did not include token'); } async function verifyHttpSmoke(minioAvailable: boolean) { const baseUrl = process.env.API_BASE_URL; if (!baseUrl) { warn('API_BASE_URL is not set; skipped HTTP smoke tests. testdb:run starts a temporary API automatically.'); return; } const adminLogin = await apiRequest(baseUrl, 'POST', '/auth/dev-login', { username: 'test-admin', role: 'hr_admin' }); const specialistLogin = await apiRequest(baseUrl, 'POST', '/auth/dev-login', { username: 'test-specialist', role: 'hr_specialist' }); const medicLogin = await apiRequest(baseUrl, 'POST', '/auth/dev-login', { username: 'test-medic', role: 'medic_familie' }); assert(adminLogin.status === 201 || adminLogin.status === 200, `hr_admin dev-login failed: ${adminLogin.text}`); assert(specialistLogin.status === 201 || specialistLogin.status === 200, `hr_specialist dev-login failed: ${specialistLogin.text}`); assert(medicLogin.status === 201 || medicLogin.status === 200, `medic_familie dev-login failed: ${medicLogin.text}`); const adminToken = tokenFrom(adminLogin.body); const specialistToken = tokenFrom(specialistLogin.body); const medicToken = tokenFrom(medicLogin.body); for (const path of ['/dashboard/stats', '/medical/risk-cards', '/medical/upcoming-expirations']) { const response = await apiRequest(baseUrl, 'GET', path, undefined, adminToken); assert(response.status >= 200 && response.status < 300, `GET ${path} failed: ${response.status} ${response.text}`); } const employees = await prisma.employee.findMany({ where: { status: 'activ', medicalProfile: { workplaceRiskCardId: { not: null } } }, select: { id: true }, take: 5, }); assert(employees.length >= 5, 'HTTP bulk smoke needs at least five eligible employees'); const bulkBody = { employeeIds: employees.map((employee) => employee.id), tip: MedicalCheckupType.periodic, dataPlanificata: '2026-06-15', documentContext: { telefon: '+373 22 000 000', fax: '+373 22 000 001', email: 'hr.test@medpark.md', solicitant: 'Test HR Admin', functia: 'Specialist resurse umane', }, }; const forbidden = await apiRequest(baseUrl, 'POST', '/medical/bulk/initiate', bulkBody, specialistToken); assert(forbidden.status === 403, `hr_specialist bulk initiate should be 403, got ${forbidden.status}: ${forbidden.text}`); if (!minioAvailable) { warn('MinIO is not reachable on localhost:9000; skipped full upload-dependent bulk success and medic completion HTTP tests.'); ok('HTTP smoke passed for auth, read endpoints, and role-based 403'); return; } const bulk = await apiRequest(baseUrl, 'POST', '/medical/bulk/initiate', bulkBody, adminToken); assert(bulk.status >= 200 && bulk.status < 300, `hr_admin bulk initiate failed: ${bulk.status} ${bulk.text}`); assert(typeof bulk.body === 'object' && bulk.body && 'groupsCount' in bulk.body, 'bulk response should include groupsCount'); const pending = await prisma.medicalCheckup.findFirst({ where: { verdict: null }, orderBy: { createdAt: 'asc' }, }); assert(pending, 'No pending checkup found for medic completion smoke'); const complete = await apiRequest( baseUrl, 'PATCH', `/medical/checkups/${pending.id}/complete`, { verdict: MedicalVerdict.apt_conditionat, dataEfectuata: '2026-06-16', recomandari: 'Test: lucru cu dozimetru și reevaluare anuală.', valabilPanaLa: '2027-06-16', semnatDe: 'Dr. Verificare Test', }, medicToken, ); assert(complete.status >= 200 && complete.status < 300, `medic completion failed: ${complete.status} ${complete.text}`); const completed = await prisma.medicalCheckup.findUnique({ where: { id: pending.id } }); assert(completed?.verdict === MedicalVerdict.apt_conditionat, 'Medic completion did not persist verdict'); assert(completed.semnatDe === 'Dr. Verificare Test', 'Medic completion did not persist semnatDe'); assert(Array.isArray(completed.documenteGenerate), 'Completed checkup should contain generated documents'); assert(JSON.stringify(completed.documenteGenerate).includes('Anexa_6_Final'), 'Completed checkup should include final Anexa 6 document'); ok('HTTP smoke passed, including MinIO-backed document generation'); } async function main() { const dbName = requireTemporaryDatabase(); console.log(`Verifying Medpark test database ${dbName}...`); await verifySourceFiles(); await verifyDatabaseCoverage(); await verifyDocxTemplates(); const minioAvailable = await isMinioAvailable(); if (minioAvailable) ok('MinIO is reachable on localhost:9000'); else warn('MinIO is not reachable on localhost:9000; upload-dependent checks will be skipped or marked failed.'); await verifyHttpSmoke(minioAvailable); if (warnings.length > 0) { console.log('\nVerification completed with warnings:'); for (const message of warnings) console.log(`- ${message}`); } else { console.log('\nVerification completed without warnings.'); } } main() .catch((error: unknown) => { console.error(error); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });