const fs = require('node:fs'); const path = require('node:path'); const postgres = require('postgres'); const FIXTURE_USERS = { superAdmin: process.env.SUPER_ADMIN_EMAIL || 'admin@allaos.com', admin: 'task-h1-admin@local.test', pdfUser: 'task-h1-pdf-user@local.test', viewer: 'task-h1-viewer@local.test' }; const PASSWORDS = { superAdmin: process.env.SUPER_ADMIN_PASSWORD || 'A3YH2Xflw1@allaos', fixture: 'TaskH1Pass!2026' }; function loadEnvFile(filePath) { if (!fs.existsSync(filePath)) { return; } const content = fs.readFileSync(filePath, 'utf8'); for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || line.startsWith('#')) { continue; } const separatorIndex = line.indexOf('='); if (separatorIndex === -1) { continue; } const key = line.slice(0, separatorIndex).trim(); let value = line.slice(separatorIndex + 1).trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } if (!(key in process.env)) { process.env[key] = value; } } } function loadLocalEnv() { loadEnvFile(path.resolve(process.cwd(), '.env.local')); loadEnvFile(path.resolve(process.cwd(), '.env')); } function requireEnv(name) { const value = process.env[name]?.trim(); if (!value) { throw new Error(`Missing required environment variable: ${name}`); } return value; } function assert(condition, message) { if (!condition) { throw new Error(message); } } function extractCookies(response) { const cookieMap = new Map(); const setCookies = typeof response.headers.getSetCookie === 'function' ? response.headers.getSetCookie() : response.headers.get('set-cookie') ? [response.headers.get('set-cookie')] : []; for (const rawCookie of setCookies) { const [pair] = rawCookie.split(';', 1); const separatorIndex = pair.indexOf('='); if (separatorIndex === -1) { continue; } cookieMap.set(pair.slice(0, separatorIndex), pair.slice(separatorIndex + 1)); } return cookieMap; } function mergeCookieMaps(base, next) { const merged = new Map(base); for (const [key, value] of next.entries()) { merged.set(key, value); } return merged; } function toCookieHeader(cookieMap) { return [...cookieMap.entries()].map(([key, value]) => `${key}=${value}`).join('; '); } async function signIn(baseUrl, email, password) { const csrfResponse = await fetch(`${baseUrl}/api/auth/csrf`); assert(csrfResponse.ok, `Unable to fetch CSRF token for ${email}`); const csrfPayload = await csrfResponse.json(); const cookies = extractCookies(csrfResponse); const body = new URLSearchParams({ email, password, csrfToken: csrfPayload.csrfToken, callbackUrl: `${baseUrl}/dashboard`, json: 'true' }); const signInResponse = await fetch(`${baseUrl}/api/auth/callback/credentials?json=true`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Cookie: toCookieHeader(cookies) }, body: body.toString(), redirect: 'manual' }); assert( signInResponse.status === 200 || signInResponse.status === 302, `Unexpected sign-in status ${signInResponse.status} for ${email}` ); const mergedCookies = mergeCookieMaps(cookies, extractCookies(signInResponse)); assert(mergedCookies.size > 0, `Sign-in did not return cookies for ${email}`); return mergedCookies; } async function fetchWithAuth(baseUrl, cookieMap, pathname, init = {}) { const response = await fetch(`${baseUrl}${pathname}`, { ...init, headers: { ...(init.headers || {}), Cookie: toCookieHeader(cookieMap) }, redirect: 'manual' }); const bytes = Buffer.from(await response.arrayBuffer()); const contentType = response.headers.get('content-type'); const disposition = response.headers.get('content-disposition'); let json = null; if (contentType?.includes('application/json')) { json = JSON.parse(bytes.toString('utf8')); } return { status: response.status, contentType, disposition, bytes, json }; } function estimatePageCount(buffer) { const text = buffer.toString('latin1'); return { startsWithPdf: text.startsWith('%PDF-'), byteLength: buffer.length }; } async function queryFixtureState(sql, approvedQuotationId) { const quotation = ( await sql` select id, organization_id, code, approved_at, approved_pdf_url, approved_snapshot, approved_template_version_id from crm_quotations where id = ${approvedQuotationId} limit 1 ` )[0] ?? null; assert(quotation, 'Approved quotation fixture not found in DB'); const [attachment, audit] = await Promise.all([ sql` select id, file_name, file_path, file_type, file_size, description from crm_quotation_attachments where quotation_id = ${approvedQuotationId} and deleted_at is null order by created_at desc limit 1 `.then((rows) => rows[0] ?? null), sql` select id, entity_type, entity_id, action, created_at from tr_audit_logs where organization_id = ${quotation.organization_id} and entity_type = 'crm_quotation_pdf_generate_approved' and entity_id = ${approvedQuotationId} order by created_at desc limit 1 `.then((rows) => rows[0] ?? null) ]); return { quotation, attachment, audit }; } async function queryTemplatePageCount(sql, templateVersionId) { const version = ( await sql` select schema_json from crm_document_template_versions where id = ${templateVersionId} limit 1 ` )[0] ?? null; const schemaJson = typeof version?.schema_json === 'string' ? JSON.parse(version.schema_json) : version?.schema_json ?? null; const schemas = schemaJson?.schemas; return Array.isArray(schemas) ? schemas.length : 0; } async function main() { loadLocalEnv(); const baseUrl = process.env.APP_URL || 'http://localhost:3000'; const sql = postgres(requireEnv('DATABASE_URL'), { prepare: false }); try { const approvedQuotation = ( await sql` select id, code from crm_quotations where code = 'QT-TASK-H1-APPROVED' limit 1 ` )[0] ?? null; const draftQuotation = ( await sql` select id, code from crm_quotations where code = 'QT-TASK-H1-DRAFT' limit 1 ` )[0] ?? null; assert(approvedQuotation, 'Approved fixture quotation is missing'); assert(draftQuotation, 'Draft fixture quotation is missing'); const superAdminCookies = await signIn(baseUrl, FIXTURE_USERS.superAdmin, PASSWORDS.superAdmin); const adminCookies = await signIn(baseUrl, FIXTURE_USERS.admin, PASSWORDS.fixture); const pdfUserCookies = await signIn(baseUrl, FIXTURE_USERS.pdfUser, PASSWORDS.fixture); const viewerCookies = await signIn(baseUrl, FIXTURE_USERS.viewer, PASSWORDS.fixture); const preview = await fetchWithAuth( baseUrl, superAdminCookies, `/api/crm/quotations/${approvedQuotation.id}/pdf-preview` ); assert(preview.status === 200, `Preview API failed with ${preview.status}`); assert(preview.contentType?.includes('application/pdf'), 'Preview did not return PDF'); assert(preview.disposition?.startsWith('inline;'), 'Preview was not inline'); const download = await fetchWithAuth( baseUrl, superAdminCookies, `/api/crm/quotations/${approvedQuotation.id}/pdf-download` ); assert(download.status === 200, `Download API failed with ${download.status}`); assert(download.contentType?.includes('application/pdf'), 'Download did not return PDF'); assert(download.disposition?.startsWith('attachment;'), 'Download was not attachment'); const generateApproved = await fetchWithAuth( baseUrl, superAdminCookies, `/api/crm/quotations/${approvedQuotation.id}/approved-pdf`, { method: 'POST' } ); assert( generateApproved.status === 200, `Approved PDF POST failed with ${generateApproved.status}` ); assert( generateApproved.disposition?.startsWith('attachment;'), 'Approved PDF POST was not attachment' ); const approvedGet = await fetchWithAuth( baseUrl, superAdminCookies, `/api/crm/quotations/${approvedQuotation.id}/approved-pdf` ); assert(approvedGet.status === 200, `Approved PDF GET failed with ${approvedGet.status}`); assert(approvedGet.disposition?.startsWith('inline;'), 'Approved PDF GET was not inline'); const nonApprovedGenerate = await fetchWithAuth( baseUrl, superAdminCookies, `/api/crm/quotations/${draftQuotation.id}/approved-pdf`, { method: 'POST' } ); assert( nonApprovedGenerate.status === 400, `Draft quotation POST expected 400, got ${nonApprovedGenerate.status}` ); const adminPreview = await fetchWithAuth( baseUrl, adminCookies, `/api/crm/quotations/${approvedQuotation.id}/pdf-preview` ); const pdfUserPreview = await fetchWithAuth( baseUrl, pdfUserCookies, `/api/crm/quotations/${approvedQuotation.id}/pdf-preview` ); const viewerPreview = await fetchWithAuth( baseUrl, viewerCookies, `/api/crm/quotations/${approvedQuotation.id}/pdf-preview` ); const viewerGenerate = await fetchWithAuth( baseUrl, viewerCookies, `/api/crm/quotations/${approvedQuotation.id}/approved-pdf`, { method: 'POST' } ); assert(adminPreview.status === 200, `Admin preview expected 200, got ${adminPreview.status}`); assert( pdfUserPreview.status === 200, `Regular permitted user preview expected 200, got ${pdfUserPreview.status}` ); assert( viewerPreview.status === 403, `Regular user without preview permission expected 403, got ${viewerPreview.status}` ); assert( viewerGenerate.status === 403, `Regular user without generate permission expected 403, got ${viewerGenerate.status}` ); const persistence = await queryFixtureState(sql, approvedQuotation.id); assert(persistence.quotation.approved_pdf_url, 'approvedPdfUrl was not saved'); assert( persistence.quotation.approved_template_version_id, 'approvedTemplateVersionId was not saved' ); assert(persistence.quotation.approved_snapshot, 'approvedSnapshot was not saved'); assert(persistence.attachment, 'Approved PDF attachment metadata was not created'); assert(persistence.audit, 'Approved PDF audit log was not created'); const absoluteStoredPath = path.join( process.cwd(), 'public', persistence.quotation.approved_pdf_url.replace(/^\//, '') ); assert(fs.existsSync(absoluteStoredPath), 'Stored approved PDF file is missing on disk'); const pdfShape = estimatePageCount(approvedGet.bytes); const approvedSnapshot = persistence.quotation.approved_snapshot; const templatePageCount = await queryTemplatePageCount( sql, persistence.quotation.approved_template_version_id ); assert(pdfShape.startsWithPdf, 'Approved PDF response does not start with %PDF header'); assert(pdfShape.byteLength > 1000, `Approved PDF size looks too small: ${pdfShape.byteLength}`); assert(templatePageCount >= 1, `Template page count is not reasonable: ${templatePageCount}`); assert( approvedSnapshot.documentData?.customer?.name, 'approvedSnapshot is missing customer block data' ); assert( approvedSnapshot.documentData?.quotation?.code === approvedQuotation.code, 'approvedSnapshot is missing quotation code' ); assert( Array.isArray(approvedSnapshot.documentData?.items) && approvedSnapshot.documentData.items.length >= 1, 'approvedSnapshot is missing item table data' ); assert( typeof approvedSnapshot.documentData?.quotation?.totalAmount === 'number', 'approvedSnapshot is missing total amount' ); assert( approvedSnapshot.documentData?.topics?.scope?.length >= 1, 'approvedSnapshot is missing scope topic data' ); assert( approvedSnapshot.documentData?.topics?.exclusion?.length >= 1, 'approvedSnapshot is missing exclusion topic data' ); assert( approvedSnapshot.documentData?.topics?.payment?.length >= 1, 'approvedSnapshot is missing payment topic data' ); assert( approvedSnapshot.documentData?.approval?.approvers?.length >= 1, 'approvedSnapshot is missing approval block data' ); assert( Object.values(approvedSnapshot.documentData?.signatures ?? {}).some(Boolean), 'approvedSnapshot is missing signature placeholder data' ); console.log( JSON.stringify( { smokeTestRecord: { approvedQuotationId: approvedQuotation.id, approvedQuotationCode: approvedQuotation.code, draftQuotationId: draftQuotation.id, draftQuotationCode: draftQuotation.code }, apiChecks: { preview: { status: preview.status, disposition: preview.disposition, bytes: preview.bytes.length }, download: { status: download.status, disposition: download.disposition, bytes: download.bytes.length }, approvedPost: { status: generateApproved.status, disposition: generateApproved.disposition, bytes: generateApproved.bytes.length }, approvedGet: { status: approvedGet.status, disposition: approvedGet.disposition, bytes: approvedGet.bytes.length }, nonApprovedPost: { status: nonApprovedGenerate.status, message: nonApprovedGenerate.json?.message ?? null } }, permissionChecks: { superAdminPreview: preview.status, adminPreview: adminPreview.status, regularUserWithPermissionPreview: pdfUserPreview.status, regularUserWithoutPermissionPreview: viewerPreview.status, regularUserWithoutPermissionGenerate: viewerGenerate.status }, persistence: { approvedPdfUrl: persistence.quotation.approved_pdf_url, approvedTemplateVersionId: persistence.quotation.approved_template_version_id, attachmentId: persistence.attachment.id, auditId: persistence.audit.id, storedFileExists: true }, outputValidation: { templatePageCount, pdfByteLength: pdfShape.byteLength, startsWithPdfHeader: pdfShape.startsWithPdf, customerName: approvedSnapshot.documentData.customer.name, quotationCode: approvedSnapshot.documentData.quotation.code, itemCount: approvedSnapshot.documentData.items.length, totalAmount: approvedSnapshot.documentData.quotation.totalAmount, scopeTopicCount: approvedSnapshot.documentData.topics.scope.length, exclusionTopicCount: approvedSnapshot.documentData.topics.exclusion.length, paymentTopicCount: approvedSnapshot.documentData.topics.payment.length, approvalStepCount: approvedSnapshot.documentData.approval.approvers.length } }, null, 2 ) ); } finally { await sql.end(); } } main().catch((error) => { console.error('[verify-task-h1] Failed:', error.message); process.exitCode = 1; });