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 });