/ tests / agents / report-delivery.test.js
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  });