/ tests / test-data-generator.test.js
test-data-generator.test.js
  1  /**
  2   * Tests for the Test Data Generator
  3   */
  4  import { test, describe, beforeEach } from 'node:test';
  5  import assert from 'node:assert/strict';
  6  import {
  7    createTestDb,
  8    SiteFactory,
  9    OutreachFactory,
 10    ConversationFactory,
 11    buildPipelineScenario,
 12    buildConversationChain,
 13  } from './helpers/test-data-generator.js';
 14  
 15  describe('createTestDb', () => {
 16    test('creates isolated in-memory database with full schema', () => {
 17      const db = createTestDb();
 18      const tables = db
 19        .prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
 20        .all()
 21        .map(r => r.name);
 22      assert.ok(tables.includes('sites'), 'sites table exists');
 23      assert.ok(tables.includes('messages'), 'messages table exists');
 24      db.close();
 25    });
 26  
 27    test('each call returns an independent database', () => {
 28      const db1 = createTestDb();
 29      const db2 = createTestDb();
 30      SiteFactory.found(db1);
 31      const count1 = db1.prepare('SELECT COUNT(*) as n FROM sites').get().n;
 32      const count2 = db2.prepare('SELECT COUNT(*) as n FROM sites').get().n;
 33      assert.equal(count1, 1);
 34      assert.equal(count2, 0); // db2 is isolated
 35      db1.close();
 36      db2.close();
 37    });
 38  });
 39  
 40  describe('SiteFactory', () => {
 41    let db;
 42    beforeEach(() => {
 43      db = createTestDb();
 44    });
 45  
 46    test('found() creates a site at found status', () => {
 47      const site = SiteFactory.found(db);
 48      assert.equal(site.status, 'found');
 49      assert.ok(site.domain);
 50      assert.ok(site.landing_page_url);
 51      assert.equal(site.html_dom, null);
 52      assert.equal(site.score, null);
 53    });
 54  
 55    test('assetsCaptured() has html_dom populated', () => {
 56      const site = SiteFactory.assetsCaptured(db);
 57      assert.equal(site.status, 'assets_captured');
 58      assert.ok(site.html_dom, 'html_dom should be set');
 59    });
 60  
 61    test('assetsCapturedNoHtml() has null html_dom (the bug scenario)', () => {
 62      const site = SiteFactory.assetsCapturedNoHtml(db);
 63      assert.equal(site.status, 'assets_captured');
 64      assert.equal(site.html_dom, null);
 65    });
 66  
 67    test('scored() has score and grade', () => {
 68      const site = SiteFactory.scored(db);
 69      assert.equal(site.status, 'prog_scored');
 70      assert.equal(site.score, 45);
 71      assert.equal(site.grade, 'C');
 72      // score_json is stored on filesystem (migration 121), not in DB column
 73    });
 74  
 75    test('highScore() has A+ grade', () => {
 76      const site = SiteFactory.highScore(db);
 77      assert.equal(site.grade, 'A+');
 78      assert.ok(site.score >= 95);
 79    });
 80  
 81    test('failing() has error_message and recapture_at', () => {
 82      const site = SiteFactory.failing(db);
 83      assert.equal(site.status, 'failing');
 84      assert.ok(site.error_message);
 85      assert.ok(site.recapture_at);
 86    });
 87  
 88    test('accepted overrides', () => {
 89      const site = SiteFactory.found(db, { domain: 'custom.com.au', keyword: 'custom keyword' });
 90      assert.equal(site.domain, 'custom.com.au');
 91      assert.equal(site.keyword, 'custom keyword');
 92    });
 93  
 94    test('batch() creates N sites', () => {
 95      const sites = SiteFactory.batch(db, 'found', 5);
 96      assert.equal(sites.length, 5);
 97      const uniqueDomains = new Set(sites.map(s => s.domain));
 98      assert.equal(uniqueDomains.size, 5, 'All domains should be unique');
 99    });
100  
101    test('crossBorderDuplicate() shares domain with primary site', () => {
102      const primary = SiteFactory.found(db, { country_code: 'AU', google_domain: 'google.com.au' });
103      const duplicate = SiteFactory.crossBorderDuplicate(db, primary);
104      assert.equal(duplicate.domain, primary.domain);
105      assert.notEqual(duplicate.country_code, primary.country_code);
106    });
107  });
108  
109  describe('OutreachFactory', () => {
110    let db;
111    let site;
112    beforeEach(() => {
113      db = createTestDb();
114      site = SiteFactory.proposalsDrafted(db);
115    });
116  
117    test('pending() creates pending outreach', () => {
118      const outreach = OutreachFactory.pending(db, site.id);
119      assert.equal(outreach.approval_status, 'pending');
120      assert.equal(outreach.site_id, site.id);
121      assert.ok(outreach.message_body);
122    });
123  
124    test('sent() has sent_at timestamp', () => {
125      const outreach = OutreachFactory.sent(db, site.id);
126      assert.equal(outreach.delivery_status, 'sent');
127      assert.ok(outreach.sent_at);
128    });
129  
130    test('sms() uses sms contact_method', () => {
131      const outreach = OutreachFactory.sms(db, site.id);
132      assert.equal(outreach.contact_method, 'sms');
133      assert.ok(outreach.contact_uri.startsWith('+'));
134      assert.ok(outreach.message_body.length <= 160, 'SMS should be ≤160 chars');
135    });
136  
137    test('multiChannel() creates email+sms+form outreaches', () => {
138      const outreaches = OutreachFactory.multiChannel(db, site.id);
139      assert.ok(outreaches.email);
140      assert.ok(outreaches.sms);
141      assert.ok(outreaches.form);
142      assert.equal(outreaches.email.contact_method, 'email');
143      assert.equal(outreaches.sms.contact_method, 'sms');
144      assert.equal(outreaches.form.contact_method, 'form');
145    });
146  });
147  
148  describe('ConversationFactory', () => {
149    let db;
150    let site;
151    beforeEach(() => {
152      db = createTestDb();
153      site = SiteFactory.outreachSent(db);
154      OutreachFactory.sent(db, site.id);
155    });
156  
157    test('interested() creates positive conversation', () => {
158      const conv = ConversationFactory.interested(db, site.id);
159      assert.equal(conv.sentiment, 'positive');
160      assert.equal(conv.intent, 'interested');
161    });
162  
163    test('stopRequest() creates unsubscribe intent via SMS', () => {
164      const conv = ConversationFactory.stopRequest(db, site.id);
165      assert.equal(conv.intent, 'opt-out');
166      assert.equal(conv.contact_method, 'sms');
167    });
168  
169    test('purchaseIntent() sets interested intent', () => {
170      const conv = ConversationFactory.purchaseIntent(db, site.id);
171      assert.equal(conv.intent, 'interested');
172    });
173  });
174  
175  describe('buildPipelineScenario', () => {
176    test('creates sites at all pipeline stages', () => {
177      const db = createTestDb();
178      const scenario = buildPipelineScenario(db);
179      assert.ok(scenario.found.length > 0);
180      assert.ok(scenario.assetsCaptured.length > 0);
181      assert.ok(scenario.scored.length > 0);
182      assert.ok(scenario.enriched.length > 0);
183      assert.ok(scenario.proposalsDrafted.length > 0);
184      assert.ok(scenario.failing.length > 0);
185      assert.ok(scenario.ignored.length > 0);
186  
187      const total = db.prepare('SELECT COUNT(*) as n FROM sites').get().n;
188      const allCounts = Object.values(scenario).reduce((sum, arr) => sum + arr.length, 0);
189      assert.equal(total, allCounts);
190      db.close();
191    });
192  
193    test('respects custom counts', () => {
194      const db = createTestDb();
195      const scenario = buildPipelineScenario(db, { found: 10, scored: 5 });
196      assert.equal(scenario.found.length, 10);
197      assert.equal(scenario.scored.length, 5);
198      db.close();
199    });
200  });
201  
202  describe('buildConversationChain', () => {
203    test('creates linked site → outreach → conversation', () => {
204      const db = createTestDb();
205      const { site, outreach, conversation } = buildConversationChain(db);
206      assert.ok(site.id);
207      assert.equal(outreach.site_id, site.id);
208      assert.equal(conversation.site_id, site.id);
209      db.close();
210    });
211  
212    test('respects channel and intent options', () => {
213      const db = createTestDb();
214      const { conversation } = buildConversationChain(db, {
215        channel: 'sms',
216        intent: 'notInterested',
217      });
218      assert.equal(conversation.contact_method, 'sms');
219      assert.equal(conversation.intent, 'not-interested');
220      db.close();
221    });
222  });