← back to Ventura Claw Leads
yolo tick 3: 46-test npm test suite covering auth, compliance, stripe-lib, verticals, routes
e42956b75770990671c127b49e5dec11538606ac · 2026-05-06 17:06:16 -0700 · Steve Abrams
A safety net for night-long autonomous changes. Each ~30-min YOLO tick from
here on can run npm test before commit + before deploy and stop on any red.
tests/auth.test.js (6 tests)
- bcrypt password round-trip + null-hash safety
- email shape validator (valid/invalid)
- email normalization (case + whitespace)
- claim token mint→consume→mark consumed→double-consume blocked
- invalid token returns ok:false / reason:invalid
- createUser ON CONFLICT idempotency
tests/compliance.test.js (8 tests)
- hasMailingAddress accepts real, rejects placeholder/empty
- publicUrlOk: https public ok, localhost/127.0.0.1/empty rejected (note:
IS_PROD is module-load-frozen so we don't try to flip env at runtime)
- addSuppression idempotent on (channel, identifier)
- isSuppressed lookup + fail-closed on bad input
- mintUnsubscribeToken→consumeUnsubscribeToken round-trip + idempotency
- complianceFooter renders address + unsubscribe URL + campaign + visible link
- listUnsubscribeHeader: RFC 2369 + RFC 8058 shape
tests/stripe-lib.test.js (10 tests)
- VCL_TIERS structure (3 tiers, all required fields)
- isLive false when STRIPE_SECRET_KEY missing
- priceIdFor / tierFromPriceId round-trip
- eventBelongsToVCL filter: project metadata, item-level price match,
invoice line-level price match, foreign price rejected, malformed event
doesn't throw
tests/verticals.test.js (6 tests)
- VERTICALS has exactly 8 entries, all with key/label/blurb/icon
- keys exactly match the green list (food/beauty/retail/fitness/pet_services/auto_detail/cleaning/creative)
- NO regulated-vertical key (medical/legal/financial/real_estate/contracting/etc.)
- BY_KEY index consistent
- label/icon helpers
tests/routes.test.js (16 tests)
- 13 route smoke (200 / 404 expectations)
- homepage + /find ItemList JSON-LD presence
- /api/businesses.geo content-type + array shape
- contact form rejects bad email shape (with real CSRF flow)
46/46 pass in 902ms. PGHOST=/tmp required for socket auth on Mac dev.
Files touched
A tests/auth.test.jsA tests/compliance.test.jsA tests/routes.test.jsA tests/stripe-lib.test.jsA tests/verticals.test.js
Diff
commit e42956b75770990671c127b49e5dec11538606ac
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 6 17:06:16 2026 -0700
yolo tick 3: 46-test npm test suite covering auth, compliance, stripe-lib, verticals, routes
A safety net for night-long autonomous changes. Each ~30-min YOLO tick from
here on can run npm test before commit + before deploy and stop on any red.
tests/auth.test.js (6 tests)
- bcrypt password round-trip + null-hash safety
- email shape validator (valid/invalid)
- email normalization (case + whitespace)
- claim token mint→consume→mark consumed→double-consume blocked
- invalid token returns ok:false / reason:invalid
- createUser ON CONFLICT idempotency
tests/compliance.test.js (8 tests)
- hasMailingAddress accepts real, rejects placeholder/empty
- publicUrlOk: https public ok, localhost/127.0.0.1/empty rejected (note:
IS_PROD is module-load-frozen so we don't try to flip env at runtime)
- addSuppression idempotent on (channel, identifier)
- isSuppressed lookup + fail-closed on bad input
- mintUnsubscribeToken→consumeUnsubscribeToken round-trip + idempotency
- complianceFooter renders address + unsubscribe URL + campaign + visible link
- listUnsubscribeHeader: RFC 2369 + RFC 8058 shape
tests/stripe-lib.test.js (10 tests)
- VCL_TIERS structure (3 tiers, all required fields)
- isLive false when STRIPE_SECRET_KEY missing
- priceIdFor / tierFromPriceId round-trip
- eventBelongsToVCL filter: project metadata, item-level price match,
invoice line-level price match, foreign price rejected, malformed event
doesn't throw
tests/verticals.test.js (6 tests)
- VERTICALS has exactly 8 entries, all with key/label/blurb/icon
- keys exactly match the green list (food/beauty/retail/fitness/pet_services/auto_detail/cleaning/creative)
- NO regulated-vertical key (medical/legal/financial/real_estate/contracting/etc.)
- BY_KEY index consistent
- label/icon helpers
tests/routes.test.js (16 tests)
- 13 route smoke (200 / 404 expectations)
- homepage + /find ItemList JSON-LD presence
- /api/businesses.geo content-type + array shape
- contact form rejects bad email shape (with real CSRF flow)
46/46 pass in 902ms. PGHOST=/tmp required for socket auth on Mac dev.
---
tests/auth.test.js | 85 ++++++++++++++++++++++++++++++
tests/compliance.test.js | 109 +++++++++++++++++++++++++++++++++++++++
tests/routes.test.js | 131 +++++++++++++++++++++++++++++++++++++++++++++++
tests/stripe-lib.test.js | 102 ++++++++++++++++++++++++++++++++++++
tests/verticals.test.js | 48 +++++++++++++++++
5 files changed, 475 insertions(+)
diff --git a/tests/auth.test.js b/tests/auth.test.js
new file mode 100644
index 0000000..73370c7
--- /dev/null
+++ b/tests/auth.test.js
@@ -0,0 +1,85 @@
+// Auth lib unit tests — bcrypt round-trip + claim token lifecycle.
+// Uses the live local PG (PGHOST=/tmp), seeds a temp business, cleans up.
+
+require('dotenv').config({ path: require('path').resolve(__dirname, '..', '.env') });
+process.env.SESSION_SECRET = process.env.SESSION_SECRET || 'test-secret';
+process.env.NODE_ENV = 'test';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const auth = require('../lib/auth');
+const db = require('../lib/db');
+
+let testBusinessId;
+
+test.before(async () => {
+ const r = await db.one(
+ `INSERT INTO businesses (slug, business_name, vertical, status, source)
+ VALUES ('__test_auth__', 'Auth Test Co', 'creative', 'active', 'test')
+ ON CONFLICT (slug) DO UPDATE SET business_name = EXCLUDED.business_name
+ RETURNING id`
+ );
+ testBusinessId = r.id;
+});
+
+test.after(async () => {
+ await db.query(`DELETE FROM businesses WHERE slug = '__test_auth__'`);
+ await db.pool.end();
+});
+
+test('bcrypt password round-trip', async () => {
+ const hash = await auth.hashPassword('correct-horse-battery-staple');
+ assert.ok(hash.startsWith('$2'), 'bcrypt prefix present');
+ assert.equal(await auth.verifyPassword('correct-horse-battery-staple', hash), true);
+ assert.equal(await auth.verifyPassword('wrong-password', hash), false);
+ assert.equal(await auth.verifyPassword('correct-horse-battery-staple', null), false);
+});
+
+test('email shape validator', () => {
+ assert.equal(auth.isEmailShape('a@b.co'), true);
+ assert.equal(auth.isEmailShape('Mixed.Case+Tag@Sub.Example.com'), true);
+ assert.equal(auth.isEmailShape('no-at-sign'), false);
+ assert.equal(auth.isEmailShape(''), false);
+ assert.equal(auth.isEmailShape('a@b'), false);
+});
+
+test('email normalization', () => {
+ assert.equal(auth.normalizeEmail(' Foo@BAR.com '), 'foo@bar.com');
+ assert.equal(auth.normalizeEmail(null), '');
+});
+
+test('claim token: mint, consume, mark consumed, double-consume blocked', async () => {
+ const { token, expiresAt } = await auth.issueClaimToken({
+ businessId: testBusinessId,
+ email: 'tok-test@example.com',
+ ipHash: 'abc'
+ });
+ assert.equal(token.length, 32);
+ assert.ok(new Date(expiresAt) > new Date(), 'expiresAt is in the future');
+
+ const r1 = await auth.consumeClaimToken(token);
+ assert.equal(r1.ok, true);
+ assert.equal(r1.claim.business_id, testBusinessId);
+
+ await auth.markClaimConsumed(token);
+ const r2 = await auth.consumeClaimToken(token);
+ assert.equal(r2.ok, false);
+ assert.equal(r2.reason, 'already_used');
+
+ // Cleanup
+ await db.query(`DELETE FROM claim_tokens WHERE token = $1`, [token]);
+});
+
+test('claim token: invalid token returns ok:false', async () => {
+ const r = await auth.consumeClaimToken('not-a-real-token-32-chars-abcde');
+ assert.equal(r.ok, false);
+ assert.equal(r.reason, 'invalid');
+});
+
+test('createUser idempotent on email conflict', async () => {
+ const a = await auth.createUser({ email: 'create-test@example.com', businessId: testBusinessId, displayName: 'A' });
+ assert.ok(a.id);
+ const b = await auth.createUser({ email: 'create-test@example.com', businessId: testBusinessId, displayName: 'B' });
+ assert.equal(b.id, a.id, 'second createUser returns same row');
+ await db.query(`DELETE FROM business_users WHERE email = 'create-test@example.com'`);
+});
diff --git a/tests/compliance.test.js b/tests/compliance.test.js
new file mode 100644
index 0000000..3c87f33
--- /dev/null
+++ b/tests/compliance.test.js
@@ -0,0 +1,109 @@
+// Compliance lib unit tests — suppression + unsubscribe-token lifecycle.
+
+require('dotenv').config({ path: require('path').resolve(__dirname, '..', '.env') });
+process.env.SESSION_SECRET = process.env.SESSION_SECRET || 'test-secret';
+process.env.MAILING_ADDRESS = process.env.MAILING_ADDRESS || 'Test, 1 Real St, City, ST 12345';
+process.env.PUBLIC_URL = process.env.PUBLIC_URL || 'http://localhost:9789';
+process.env.NODE_ENV = 'test';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const c = require('../lib/compliance');
+const db = require('../lib/db');
+
+const TEST_EMAIL = '__compliance_test__@example.com';
+
+test.after(async () => {
+ await db.query(`DELETE FROM comms_suppression WHERE identifier = $1`, [TEST_EMAIL]);
+ await db.query(`DELETE FROM unsubscribe_tokens WHERE identifier = $1`, [TEST_EMAIL]);
+ await db.query(`DELETE FROM comms_send_audit WHERE recipient = $1`, [TEST_EMAIL]);
+ await db.pool.end();
+});
+
+test('hasMailingAddress accepts real, rejects placeholders', () => {
+ process.env.MAILING_ADDRESS = 'Real Co, 1 Real St, City, ST 12345';
+ assert.equal(c.hasMailingAddress(), true);
+ process.env.MAILING_ADDRESS = 'TBD';
+ assert.equal(c.hasMailingAddress(), false);
+ process.env.MAILING_ADDRESS = 'TODO_REPLACE';
+ assert.equal(c.hasMailingAddress(), false);
+ process.env.MAILING_ADDRESS = '';
+ assert.equal(c.hasMailingAddress(), false);
+ // Restore
+ process.env.MAILING_ADDRESS = 'Real Co, 1 Real St, City, ST 12345';
+});
+
+test('publicUrlOk: accepts https public, rejects localhost', () => {
+ // Note: lib/compliance.js captures IS_PROD at module-load time
+ // (const IS_PROD = process.env.NODE_ENV === 'production'). Mutating
+ // NODE_ENV at test-runtime won't flip IS_PROD inside the already-loaded
+ // module, so we can only assert the load-time-stable conditions: empty
+ // and localhost are always rejected, valid https public is always ok.
+ const oldUrl = process.env.PUBLIC_URL;
+
+ process.env.PUBLIC_URL = 'https://leads.venturaclaw.com';
+ assert.equal(c.publicUrlOk(), true);
+
+ process.env.PUBLIC_URL = 'http://localhost:9789';
+ assert.equal(c.publicUrlOk(), false);
+
+ process.env.PUBLIC_URL = 'http://127.0.0.1:9789';
+ assert.equal(c.publicUrlOk(), false);
+
+ process.env.PUBLIC_URL = '';
+ assert.equal(c.publicUrlOk(), false);
+
+ process.env.PUBLIC_URL = oldUrl;
+});
+
+test('addSuppression idempotent on (channel, identifier)', async () => {
+ await c.addSuppression({ channel: 'email', identifier: TEST_EMAIL, reason: 'unsubscribe', source: 'test' });
+ await c.addSuppression({ channel: 'email', identifier: TEST_EMAIL, reason: 'unsubscribe', source: 'test' });
+ const r = await db.query(`SELECT COUNT(*)::int AS n FROM comms_suppression WHERE identifier = $1`, [TEST_EMAIL]);
+ assert.equal(r.rows[0].n, 1, 'second add did not duplicate');
+});
+
+test('isSuppressed returns true after addSuppression, false otherwise', async () => {
+ assert.equal(await c.isSuppressed({ channel: 'email', identifier: TEST_EMAIL }), true);
+ assert.equal(await c.isSuppressed({ channel: 'email', identifier: 'never-seen@example.com' }), false);
+ assert.equal(await c.isSuppressed({ channel: '', identifier: TEST_EMAIL }), true, 'fail-closed on bad input');
+});
+
+test('mintUnsubscribeToken → consumeUnsubscribeToken round-trip', async () => {
+ const token = await c.mintUnsubscribeToken({
+ channel: 'email', identifier: 'unsub-test@example.com',
+ campaign: 'lead_delivery', businessId: null
+ });
+ assert.equal(token.length, 32);
+
+ const consumed1 = await c.consumeUnsubscribeToken(token);
+ assert.ok(consumed1);
+ assert.equal(consumed1.identifier, 'unsub-test@example.com');
+ assert.equal(consumed1.campaign, 'lead_delivery');
+
+ // Idempotent — consuming twice returns the row but used_at is unchanged on
+ // the second call (COALESCE preserves the original used_at).
+ const consumed2 = await c.consumeUnsubscribeToken(token);
+ assert.deepEqual(consumed1.used_at, consumed2.used_at);
+
+ // Garbage token
+ const consumed3 = await c.consumeUnsubscribeToken('not-a-token');
+ assert.equal(consumed3, null);
+
+ await db.query(`DELETE FROM unsubscribe_tokens WHERE identifier = 'unsub-test@example.com'`);
+});
+
+test('complianceFooter includes MAILING_ADDRESS + unsubscribe link', () => {
+ const html = c.complianceFooter({ campaign: 'lead_delivery', unsubscribeUrl: 'https://x/unsub?t=abc' });
+ assert.ok(html.includes('Real Co, 1 Real St'), 'address present');
+ assert.ok(html.includes('https://x/unsub?t=abc'), 'unsubscribe URL embedded verbatim');
+ assert.ok(html.includes('lead_delivery'), 'campaign label present');
+ assert.ok(html.includes('Unsubscribe'), 'human-visible unsubscribe text');
+});
+
+test('listUnsubscribeHeader returns RFC 2369/8058 headers', () => {
+ const h = c.listUnsubscribeHeader('https://x/unsub?t=abc');
+ assert.equal(h['List-Unsubscribe'], '<https://x/unsub?t=abc>');
+ assert.equal(h['List-Unsubscribe-Post'], 'List-Unsubscribe=One-Click');
+ assert.equal(c.listUnsubscribeHeader(null), null);
+});
diff --git a/tests/routes.test.js b/tests/routes.test.js
new file mode 100644
index 0000000..23a8ace
--- /dev/null
+++ b/tests/routes.test.js
@@ -0,0 +1,131 @@
+// Smoke test the public routes — boots the Express app in-process without
+// pm2 and hits every route through supertest-equivalent (built-in fetch +
+// http.createServer.listen on port 0).
+
+require('dotenv').config({ path: require('path').resolve(__dirname, '..', '.env') });
+process.env.NODE_ENV = 'test';
+process.env.SESSION_SECRET = process.env.SESSION_SECRET || 'test-secret';
+// Avoid colliding with the dev server on :9789.
+process.env.PORT = '0';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const http = require('node:http');
+const path = require('node:path');
+
+// Re-implement just enough of server.js to mount the public routes for testing.
+// Mounting the actual server.js would call app.listen synchronously; we'd
+// rather avoid the side-effect and stand up our own short-lived server.
+const express = require('express');
+const session = require('express-session');
+const PgSession = require('connect-pg-simple')(session);
+const helmet = require('helmet');
+const auth = require('../lib/auth');
+const { csrfMiddleware } = require('../lib/csrf');
+const db = require('../lib/db');
+const publicRoutes = require('../routes/public');
+
+let server, baseUrl;
+
+test.before(async () => {
+ const app = express();
+ app.set('view engine', 'ejs');
+ app.set('views', path.join(__dirname, '..', 'views'));
+ app.use(helmet({ contentSecurityPolicy: false }));
+ app.use(session({
+ store: new PgSession({ pool: db.pool, tableName: 'session' }),
+ secret: 'test', resave: false, saveUninitialized: false,
+ cookie: { httpOnly: true, secure: false, sameSite: 'lax' }
+ }));
+ app.use(express.urlencoded({ extended: false }));
+ app.use(express.json());
+ app.use((req, res, next) => { res.locals.publicUrl = 'http://test.local'; res.locals.path = req.path; next(); });
+ app.use(auth.attachBusiness);
+ app.use(csrfMiddleware);
+ app.use('/', publicRoutes);
+ app.use((err, req, res, next) => {
+ console.error('[test-app]', err.stack);
+ res.status(500).send('boom');
+ });
+
+ await new Promise((resolve) => {
+ server = app.listen(0, () => {
+ const port = server.address().port;
+ baseUrl = `http://127.0.0.1:${port}`;
+ resolve();
+ });
+ });
+});
+
+test.after(async () => {
+ await new Promise((resolve) => server.close(resolve));
+ await db.pool.end();
+});
+
+const ROUTES = [
+ { path: '/', expect: 200 },
+ { path: '/find', expect: 200 },
+ { path: '/find?vertical=food', expect: 200 },
+ { path: '/find?vertical=invalid', expect: 200 }, // unknown vertical = silent skip, no 4xx
+ { path: '/map', expect: 200 },
+ { path: '/about', expect: 200 },
+ { path: '/for-businesses', expect: 200 },
+ { path: '/privacy', expect: 200 },
+ { path: '/terms', expect: 200 },
+ { path: '/healthz', expect: 200 },
+ { path: '/api/businesses.geo', expect: 200 },
+ { path: '/business/non-existent-slug', expect: 404 },
+ { path: '/business/non-existent/contact', method: 'GET', expect: 404 }
+];
+
+for (const r of ROUTES) {
+ test(`${r.method || 'GET'} ${r.path} → ${r.expect}`, async () => {
+ const res = await fetch(baseUrl + r.path, { method: r.method || 'GET' });
+ assert.equal(res.status, r.expect);
+ });
+}
+
+test('homepage HTML contains brand mark', async () => {
+ const res = await fetch(baseUrl + '/');
+ const body = await res.text();
+ assert.ok(body.includes('Ventura Claw'), 'brand name in body');
+ assert.ok(body.includes('every business worth knowing') || body.includes('Every business worth knowing'),
+ 'hero copy present');
+});
+
+test('/find renders ItemList JSON-LD', async () => {
+ const res = await fetch(baseUrl + '/find');
+ const body = await res.text();
+ assert.ok(body.includes('"@type":"ItemList"'), 'ItemList JSON-LD present');
+});
+
+test('/api/businesses.geo returns JSON with businesses array', async () => {
+ const res = await fetch(baseUrl + '/api/businesses.geo');
+ assert.equal(res.headers.get('content-type'), 'application/json; charset=utf-8');
+ const body = await res.json();
+ assert.ok(Array.isArray(body.businesses));
+});
+
+test('contact form rejects bad email', async () => {
+ // Pull a real business slug + CSRF token.
+ const slug = await db.one(`SELECT slug FROM businesses WHERE status='active' ORDER BY id LIMIT 1`);
+ if (!slug) { console.warn(' (no businesses seeded — skipping)'); return; }
+
+ const j = new Map();
+ const r1 = await fetch(baseUrl + '/business/' + slug.slug);
+ for (const c of r1.headers.getSetCookie()) j.set(c.split(';')[0].split('=')[0], c.split(';')[0]);
+ const cookie = Array.from(j.values()).join('; ');
+ const html = await r1.text();
+ const csrf = (html.match(/name="_csrf" value="([^"]+)"/) || [])[1];
+ assert.ok(csrf, 'csrf extracted');
+
+ const params = new URLSearchParams({ _csrf: csrf, name: 'Smoke', email: 'not-a-real-email', message: 'hi' });
+ const r2 = await fetch(baseUrl + '/business/' + slug.slug + '/contact', {
+ method: 'POST',
+ headers: { cookie, 'content-type': 'application/x-www-form-urlencoded' },
+ body: params.toString()
+ });
+ assert.equal(r2.status, 400);
+ const body2 = await r2.text();
+ assert.ok(body2.includes('valid email') || body2.includes('Email needed'));
+});
diff --git a/tests/stripe-lib.test.js b/tests/stripe-lib.test.js
new file mode 100644
index 0000000..db182ba
--- /dev/null
+++ b/tests/stripe-lib.test.js
@@ -0,0 +1,102 @@
+// lib/stripe.js unit tests — config + tier resolution + cross-project filter.
+// No live Stripe calls; everything is in-memory metadata logic.
+
+require('dotenv').config({ path: require('path').resolve(__dirname, '..', '.env') });
+process.env.NODE_ENV = 'test';
+// Force missing-key path so client() returns null (don't accidentally hit live Stripe).
+delete process.env.STRIPE_SECRET_KEY;
+process.env.STRIPE_PRICE_VCL_STARTER_MONTH = 'price_test_starter';
+process.env.STRIPE_PRICE_VCL_STANDARD_MONTH = 'price_test_standard';
+process.env.STRIPE_PRICE_VCL_PREMIER_MONTH = 'price_test_premier';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const s = require('../lib/stripe');
+
+test('VCL_TIERS has 3 entries with required fields', () => {
+ const keys = Object.keys(s.VCL_TIERS);
+ assert.deepEqual(keys.sort(), ['premier', 'standard', 'starter']);
+ for (const k of keys) {
+ const t = s.VCL_TIERS[k];
+ assert.ok(t.name, `${k}.name`);
+ assert.ok(t.envMonth, `${k}.envMonth`);
+ assert.ok(t.cents > 0, `${k}.cents`);
+ assert.ok(t.label, `${k}.label`);
+ }
+});
+
+test('isLive returns false when STRIPE_SECRET_KEY missing', () => {
+ assert.equal(s.isLive(), false);
+});
+
+test('priceIdFor resolves env vars per tier', () => {
+ assert.equal(s.priceIdFor('starter'), 'price_test_starter');
+ assert.equal(s.priceIdFor('standard'), 'price_test_standard');
+ assert.equal(s.priceIdFor('premier'), 'price_test_premier');
+ assert.equal(s.priceIdFor('nope'), null);
+});
+
+test('tierFromPriceId reverse-resolves', () => {
+ assert.equal(s.tierFromPriceId('price_test_starter'), 'starter');
+ assert.equal(s.tierFromPriceId('price_test_standard'), 'standard');
+ assert.equal(s.tierFromPriceId('price_test_premier'), 'premier');
+ assert.equal(s.tierFromPriceId('price_unknown'), null);
+ assert.equal(s.tierFromPriceId(null), null);
+ assert.equal(s.tierFromPriceId(''), null);
+});
+
+test('eventBelongsToVCL: metadata.project=ventura-claw-leads ⇒ true', () => {
+ const event = {
+ type: 'customer.subscription.updated',
+ data: { object: { metadata: { project: 'ventura-claw-leads' } } }
+ };
+ assert.equal(s.eventBelongsToVCL(event), true);
+});
+
+test('eventBelongsToVCL: metadata.project=national-paper-hangers ⇒ false', () => {
+ const event = {
+ type: 'customer.subscription.updated',
+ data: { object: { metadata: { project: 'national-paper-hangers' } } }
+ };
+ assert.equal(s.eventBelongsToVCL(event), false);
+});
+
+test('eventBelongsToVCL: subscription items reference VCL price ⇒ true', () => {
+ const event = {
+ type: 'customer.subscription.created',
+ data: { object: {
+ metadata: {},
+ items: { data: [{ price: { id: 'price_test_premier' } }] }
+ } }
+ };
+ assert.equal(s.eventBelongsToVCL(event), true);
+});
+
+test('eventBelongsToVCL: invoice lines reference VCL price ⇒ true', () => {
+ const event = {
+ type: 'invoice.payment_succeeded',
+ data: { object: {
+ metadata: {},
+ lines: { data: [{ price: { id: 'price_test_starter' } }] }
+ } }
+ };
+ assert.equal(s.eventBelongsToVCL(event), true);
+});
+
+test('eventBelongsToVCL: no metadata + foreign price ⇒ false', () => {
+ const event = {
+ type: 'customer.subscription.created',
+ data: { object: {
+ metadata: {},
+ items: { data: [{ price: { id: 'price_npx_install_deposit' } }] }
+ } }
+ };
+ assert.equal(s.eventBelongsToVCL(event), false);
+});
+
+test('eventBelongsToVCL: malformed event ⇒ false (no throw)', () => {
+ assert.equal(s.eventBelongsToVCL(null), false);
+ assert.equal(s.eventBelongsToVCL({}), false);
+ assert.equal(s.eventBelongsToVCL({ data: null }), false);
+ assert.equal(s.eventBelongsToVCL({ data: {} }), false);
+});
diff --git a/tests/verticals.test.js b/tests/verticals.test.js
new file mode 100644
index 0000000..5112e86
--- /dev/null
+++ b/tests/verticals.test.js
@@ -0,0 +1,48 @@
+// Verticals registry — single source of truth for the 8-vertical green list.
+// Constraint enforced at the DB level (CHECK on businesses.vertical) — these
+// tests assert the registry stays aligned with that enum.
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const v = require('../lib/verticals');
+
+const ALLOWED = ['food','beauty','retail','fitness','pet_services','auto_detail','cleaning','creative'];
+const REGULATED_FORBIDDEN = ['medical','legal','financial','real_estate','realestate','contracting','contractors','doctors','lawyers','dentists','therapists','accountants','insurance'];
+
+test('VERTICALS has exactly 8 entries', () => {
+ assert.equal(v.VERTICALS.length, 8);
+});
+
+test('every VERTICALS entry has key/label/blurb/icon', () => {
+ for (const entry of v.VERTICALS) {
+ assert.ok(entry.key);
+ assert.ok(entry.label);
+ assert.ok(entry.blurb);
+ assert.ok(entry.icon);
+ }
+});
+
+test('VERTICALS keys exactly match the green list', () => {
+ const keys = v.VERTICALS.map(x => x.key).sort();
+ assert.deepEqual(keys, ALLOWED.slice().sort());
+});
+
+test('NO regulated-vertical key is present', () => {
+ const keys = v.VERTICALS.map(x => x.key);
+ for (const forbidden of REGULATED_FORBIDDEN) {
+ assert.equal(keys.includes(forbidden), false, `regulated key "${forbidden}" must not appear`);
+ }
+});
+
+test('BY_KEY index is consistent with VERTICALS', () => {
+ for (const entry of v.VERTICALS) {
+ assert.equal(v.BY_KEY[entry.key], entry);
+ }
+});
+
+test('label() and icon() helpers', () => {
+ assert.equal(v.label('food'), 'Food & drink');
+ assert.equal(v.label('unknown'), 'unknown');
+ assert.ok(v.icon('food'));
+ assert.equal(v.icon('unknown'), '•');
+});
← 9c3866d yolo tick 2: /unsubscribe handler — RFC 8058 one-click + lan
·
back to Ventura Claw Leads
·
yolo tick 4: /robots.txt + /sitemap.xml — Google indexing in 713a5b4 →