/** * DC-057 pricing-page catalog consistency test. * * The pricing page at status/pricing/index.html hard-codes the 4 product * IDs, prices, and labels. This test asserts that those hard-coded values * exactly match the catalog in src/billing/catalog.js — preventing drift * between the two sources. * * If a new tier is added to the catalog, this test will fail until the * pricing page is updated. If the pricing page is updated, the catalog * must change in lockstep (or this test fails the other way). */ const fs = require('fs'); const path = require('path'); const catalog = require('../../src/billing/catalog'); const PRICING_PAGE_PATH = path.join(__dirname, '..', '..', '..', 'status', 'pricing', 'index.html'); function extractTiersFromPage(html) { // Extract each `
` block, then // pull out the dollar amount in the `
` element and // the durationDays from the "N-day Pro license" string. The regex is // anchored on the tier-class open + the matching buy-btn close so we // capture the full body of each tier card regardless of how many inner // divs it has. const tierRe = /
([\s\S]*?)]*class="buy-btn"[^>]*>\s*Buy/g; const tierBlocks = [...html.matchAll(tierRe)]; return tierBlocks.map(([, productId, body]) => { const priceMatch = body.match(/
\$(\d+)<\/div>/); const durMatch = body.match(/(\d+)-day Pro license/); return { productId, priceDollars: priceMatch ? parseInt(priceMatch[1], 10) : null, durationDays: durMatch ? parseInt(durMatch[1], 10) : null, }; }); } /** * Extract the HTML body for one specific tier (from open div through the * buy-btn). Used by per-tier assertions that must NOT bleed across cards. */ function extractTierBody(html, productId) { const re = new RegExp( `
([\\s\\S]*?)]*class="buy-btn"[^>]*>\\s*Buy`, 'i' ); const m = html.match(re); return m ? m[1] : null; } describe('pricing page <-> catalog consistency (DC-057)', () => { let html; let pageTiers; beforeAll(() => { html = fs.readFileSync(PRICING_PAGE_PATH, 'utf8'); pageTiers = extractTiersFromPage(html); }); test('pricing page exists and is readable', () => { expect(html.length).toBeGreaterThan(1000); expect(pageTiers.length).toBeGreaterThan(0); }); test('every catalog product is rendered on the pricing page', () => { const catalogIds = catalog.PRODUCTS.map((p) => p.id).sort(); const pageIds = pageTiers.map((t) => t.productId).sort(); expect(pageIds).toEqual(catalogIds); }); test('every pricing-page productId appears in the catalog', () => { for (const tier of pageTiers) { const product = catalog.getProduct(tier.productId); expect(product).not.toBeNull(); } }); test('pricing-page dollar amounts match catalog amountCents', () => { for (const tier of pageTiers) { const product = catalog.getProduct(tier.productId); const expectedDollars = product.amountCents / 100; expect(tier.priceDollars).toBe(expectedDollars); } }); test('pricing-page duration strings match catalog durationDays', () => { for (const tier of pageTiers) { const product = catalog.getProduct(tier.productId); expect(tier.durationDays).toBe(product.durationDays); } }); test('catalog and pricing page agree on price label (scoped per tier card)', () => { // Per-tier priceLabel assertion: each tier card must include its // own catalog.priceLabel. A swap or misplaced label fails immediately // because the assertion checks the tier's own HTML body, not the page. for (const tier of pageTiers) { const product = catalog.getProduct(tier.productId); const body = extractTierBody(html, tier.productId); expect(body).not.toBeNull(); // The priceLabel appears in the price div of THIS tier only, // immediately followed by the closing
+ the duration block. const labelRegex = new RegExp(`
\\s*\\${product.priceLabel}\\s*
\\s*
{ // DC-057 acceptance: locked spec is ONE-TIME 30/90/180/365-day licenses // at $20/$50/$70/$99. The old monthly/annual subscription toggle // would contradict the spec. expect(html).not.toMatch(/period-monthly|period-annual/); expect(html).not.toMatch(/Subscribe to Pro/); }); test('pricing page references the success-page endpoint', () => { // The success URL is constructed server-side in stripe-client.js // (${origin}/billing/success?session_id=...). The pricing page itself // doesn't need to embed it — but the FOOTER must reference it so the // customer knows where to go after Stripe redirects. expect(html.toLowerCase()).toContain('after payment'); expect(html).toContain('/admin/license'); expect(html).toContain('/api/v1/billing/checkout'); }); test('success page (status/billing/success.html) exists and references the lookup endpoint', () => { const successPath = path.join(__dirname, '..', '..', '..', 'status', 'billing', 'success.html'); const successHtml = fs.readFileSync(successPath, 'utf8'); expect(successHtml).toContain('/api/v1/billing/lookup/'); expect(successHtml.length).toBeGreaterThan(1000); }); });