← back to NationalPaperHangers

tests/measure-route.test.js

86 lines

// /measure route tests — P3 product-handoff deep-link parsing + the security
// boundary on the "shop" return URL. Pure render assertions via supertest; no
// DB writes. Pairs with measure-kit.test.js (the pure calculator brain).
'use strict';

const { test } = require('node:test');
const assert = require('node:assert/strict');
const supertest = require('supertest');
const app = require('./app');

const request = supertest(app);

test('GET /measure renders the bare tool (no product context)', async () => {
  const r = await request.get('/measure');
  assert.equal(r.status, 200);
  assert.ok(r.text.includes('Measure any wall with your phone'), 'hero present');
  // No JSON product element when there is no product context. (The inline JS
  // still references the id, so assert the actual element tag is absent.)
  assert.ok(!r.text.includes('id="mk-product-data"'), 'no product JSON block');
  assert.ok(r.text.includes('Shop the look — Designer Wallcoverings'), 'default shop CTA');
});

test('GET /measure?... renders a product banner + prefill JSON from a DW deep-link', async () => {
  const r = await request.get('/measure').query({
    sku: 'DW-1234', name: 'Grasscloth Natural', from: 'Designer Wallcoverings',
    repeat: '18', format: 'us_standard',
    shop: 'https://designerwallcoverings.com/products/grasscloth-natural'
  });
  assert.equal(r.status, 200);
  assert.ok(r.text.includes('id="mk-product-data"'), 'product JSON block present');
  assert.ok(r.text.includes('Grasscloth Natural'), 'product name in banner');
  assert.ok(r.text.includes('SKU DW-1234'), 'sku chip');
  assert.ok(r.text.includes('18" pattern repeat') || r.text.includes('18" pattern repeat'),
    'repeat chip');
  assert.ok(r.text.includes('products/grasscloth-natural'), 'shop CTA links back to the PDP');
  assert.ok(r.text.includes('Shop Grasscloth Natural'), 'shop CTA labelled with product');
  assert.ok(r.text.includes('Measure your walls for Grasscloth Natural'), 'title suffix');
  assert.ok(r.text.includes('"format":"us_standard"'), 'format serialized for prefill');
  assert.ok(r.text.includes('"repeat":18'), 'repeat serialized for prefill');
});

test('GET /measure rejects a non-allowlisted shop host (no open redirect)', async () => {
  const r = await request.get('/measure').query({
    name: 'Evil', shop: 'https://evil.example.com/phish'
  });
  assert.equal(r.status, 200);
  assert.ok(!r.text.includes('evil.example.com'), 'evil host never echoed');
  assert.ok(r.text.includes('Shop the look — Designer Wallcoverings'), 'falls back to default CTA');
});

test('GET /measure rejects a javascript: scheme shop URL', async () => {
  const r = await request.get('/measure').query({
    name: 'XSS', shop: 'javascript:alert(1)'
  });
  assert.equal(r.status, 200);
  assert.ok(!r.text.includes('javascript:alert'), 'js scheme never echoed');
});

test('GET /measure allows brand subdomains (www.wallco.ai)', async () => {
  const r = await request.get('/measure').query({
    name: 'Mural', format: 'panels', shop: 'https://www.wallco.ai/d/123'
  });
  assert.equal(r.status, 200);
  assert.ok(r.text.includes('www.wallco.ai/d/123'), 'subdomain shop URL allowed');
  assert.ok(r.text.includes('"format":"panels"'), 'panels format serialized');
});

test('GET /measure escapes < in the product JSON block (no </script> breakout)', async () => {
  const r = await request.get('/measure').query({
    name: '</script><b>x', shop: 'https://wallco.ai/x'
  });
  assert.equal(r.status, 200);
  // The raw breakout string must not survive verbatim inside the JSON block.
  assert.ok(!r.text.includes('</script><b>x'), 'no raw </script> breakout');
  assert.ok(r.text.includes('\\u003c'), 'less-than escaped to \\u003c');
});

test('GET /measure clamps out-of-range repeat/panel values', async () => {
  const r = await request.get('/measure').query({
    name: 'Clamp', repeat: '9999', panelw: '1', format: 'panels'
  });
  assert.equal(r.status, 200);
  assert.ok(r.text.includes('"repeat":120'), 'repeat clamped to 120');
  assert.ok(r.text.includes('"panelw":6'), 'panel width clamped to 6');
});