report-delivery.test.js
1 /** 2 * Report Delivery Unit Tests 3 * Tests deliverReport() with mocked Resend and database 4 */ 5 6 import { describe, test, mock, beforeEach } from 'node:test'; 7 import assert from 'node:assert/strict'; 8 import Database from 'better-sqlite3'; 9 import { createPgMock } from '../helpers/pg-mock.js'; 10 import { writeFileSync, mkdirSync } from 'fs'; 11 import { join } from 'path'; 12 import { tmpdir } from 'os'; 13 14 // Shared in-memory database with minimal purchases schema 15 const db = new Database(':memory:'); 16 db.exec(` 17 CREATE TABLE IF NOT EXISTS purchases ( 18 id INTEGER PRIMARY KEY AUTOINCREMENT, 19 email TEXT NOT NULL, 20 landing_page_url TEXT NOT NULL, 21 paypal_order_id TEXT UNIQUE, 22 amount INTEGER NOT NULL, 23 currency TEXT NOT NULL, 24 amount_usd INTEGER NOT NULL, 25 status TEXT NOT NULL DEFAULT 'paid', 26 report_path TEXT, 27 report_score REAL, 28 report_grade TEXT, 29 delivered_at DATETIME, 30 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 31 updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 32 ); 33 `); 34 35 // Mock Resend BEFORE importing the module under test 36 const resendSendMock = mock.fn(); 37 38 class ResendMock { 39 constructor() { 40 this.emails = { send: resendSendMock }; 41 } 42 } 43 44 mock.module('resend', { 45 namedExports: { Resend: ResendMock }, 46 }); 47 48 // Mock dotenv 49 mock.module('dotenv', { 50 defaultExport: { config: () => {} }, 51 namedExports: { config: () => {} }, 52 }); 53 54 // Mock db.js with pg-mock backed by in-memory SQLite 55 mock.module('../../src/utils/db.js', { namedExports: createPgMock(db) }); 56 57 process.env.RESEND_API_KEY = 'test-resend-key'; 58 process.env.AUDITANDFIX_SENDER_EMAIL = 'reports@auditandfix.com'; 59 60 // Create a test PDF file 61 const testPdfDir = join(tmpdir(), 'auditfix-test-delivery'); 62 mkdirSync(testPdfDir, { recursive: true }); 63 const testPdfPath = join(testPdfDir, 'test-report.pdf'); 64 writeFileSync(testPdfPath, '%PDF-1.4 test content for delivery test'); 65 66 const { deliverReport } = await import('../../src/reports/report-delivery.js'); 67 68 function insertTestPurchase(overrides = {}) { 69 const defaults = { 70 email: 'customer@example.com', 71 landing_page_url: 'https://test-business.com', 72 paypal_order_id: 'ORDER_DELIVER_1', 73 amount: 29700, 74 currency: 'USD', 75 amount_usd: 29700, 76 status: 'report_generated', 77 report_path: testPdfPath, 78 report_score: 62, 79 report_grade: 'D-', 80 }; 81 const data = { ...defaults, ...overrides }; 82 83 db.prepare( 84 `INSERT INTO purchases 85 (email, landing_page_url, paypal_order_id, amount, currency, amount_usd, status, report_path, report_score, report_grade) 86 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` 87 ).run( 88 data.email, 89 data.landing_page_url, 90 data.paypal_order_id, 91 data.amount, 92 data.currency, 93 data.amount_usd, 94 data.status, 95 data.report_path, 96 data.report_score, 97 data.report_grade 98 ); 99 100 return db.prepare('SELECT id FROM purchases ORDER BY id DESC LIMIT 1').get().id; 101 } 102 103 describe('deliverReport', () => { 104 beforeEach(() => { 105 db.exec('DELETE FROM purchases'); 106 resendSendMock.mock.resetCalls(); 107 resendSendMock.mock.mockImplementation(async () => ({ id: 'email_delivery_123' })); 108 }); 109 110 test('sends email with PDF attachment', async () => { 111 const purchaseId = insertTestPurchase(); 112 113 const result = await deliverReport(purchaseId); 114 115 assert.equal(result.success, true); 116 assert.equal(result.emailId, 'email_delivery_123'); 117 118 const call = resendSendMock.mock.calls[0]; 119 const args = call.arguments[0]; 120 121 assert.ok(args.from.includes('Audit&Fix')); 122 assert.equal(args.to, 'customer@example.com'); 123 assert.ok(args.subject.includes('CRO Audit Report')); 124 assert.ok(args.subject.includes('test-business.com')); 125 assert.ok(args.html.includes('62')); // Score 126 assert.ok(args.html.includes('C')); // Grade 127 assert.ok(args.attachments); 128 assert.equal(args.attachments.length, 1); 129 assert.ok(args.attachments[0].filename.includes('test-business.com')); 130 }); 131 132 test('updates delivered_at timestamp', async () => { 133 const purchaseId = insertTestPurchase(); 134 135 await deliverReport(purchaseId); 136 137 const purchase = db 138 .prepare('SELECT delivered_at FROM purchases WHERE id = ?') 139 .get(purchaseId); 140 assert.ok(purchase.delivered_at); 141 }); 142 143 test('throws for non-existent purchase', async () => { 144 await assert.rejects( 145 () => deliverReport(99999), 146 err => { 147 assert.ok(err.message.includes('not found')); 148 return true; 149 } 150 ); 151 }); 152 153 test('throws for purchase without report_path', async () => { 154 const purchaseId = insertTestPurchase({ 155 paypal_order_id: 'ORDER_NO_REPORT', 156 report_path: null, 157 }); 158 159 await assert.rejects( 160 () => deliverReport(purchaseId), 161 err => { 162 assert.ok(err.message.includes('no report_path')); 163 return true; 164 } 165 ); 166 }); 167 168 test('includes grade color styling in email', async () => { 169 const purchaseId = insertTestPurchase({ 170 paypal_order_id: 'ORDER_GRADE_A', 171 report_grade: 'A', 172 report_score: 92, 173 }); 174 175 await deliverReport(purchaseId); 176 177 const { html } = resendSendMock.mock.calls[0].arguments[0]; 178 assert.ok(html.includes('#38a169')); // Green for A grade 179 }); 180 });