diff --git a/src/local.js b/src/local.js index 33494e7..93c17b7 100644 --- a/src/local.js +++ b/src/local.js @@ -127,6 +127,78 @@ async function decodeProxyResponseBuffer(responseBuffer, proxyRes) { return responseBuffer.toString('utf8'); } +// window.pageSchemas is set in the first large but before later bundles (and before the small +// `if (window.campaign)` script). Edge strips +// before HTML is sent, and injecting before runs too late (React has +// already read pageSchemas). +const AFTER_CAMPAIGN_BOOTSTRAP = + /(<\/script>)(\s* but before later bundles (and before the small - // `if (window.campaign)` script). Edge strips - // before HTML is sent, and injecting before runs too late (React - // already read pageSchemas). - if (pageOverride) { - const afterCampaignBootstrap = - /(<\/script>)(\s* ` - ); - } - ), + ); + }, }) ); diff --git a/tests/local-proxy.test.js b/tests/local-proxy.test.js new file mode 100644 index 0000000..453c63f --- /dev/null +++ b/tests/local-proxy.test.js @@ -0,0 +1,148 @@ +import { describe, test } from 'vitest'; +import assert from 'node:assert/strict'; +import http from 'node:http'; +import vm from 'node:vm'; + +import express from 'express'; + +import { createCampaignProxy, injectPageOverride } from '../src/local.js'; +import { buildPageOverrideScript } from '../src/actions/pages.js'; + +/** Campaign HTML shaped like the real one: pageSchemas bootstrap, then the small window.campaign script. */ +function campaignHtml(bodyCopy = 'hello') { + return [ + 't', + ``, + '', + '', + ].join('\n'); +} + +async function listen(server) { + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + return server.address().port; +} + +describe('injectPageOverride', () => { + test('inserts the payload between the bootstrap and the window.campaign script', () => { + const out = injectPageOverride(campaignHtml(), ''); + assert.ok(out.includes('];')); + assert.ok( + out.indexOf('PAYLOAD') > out.indexOf('window.pageSchemas'), + 'payload must come after the pageSchemas bootstrap' + ); + assert.ok( + out.indexOf('PAYLOAD') < out.indexOf('if (window.campaign)'), + 'payload must come before the window.campaign script' + ); + }); + + test('returns the html untouched when there is no payload', () => { + const html = campaignHtml(); + assert.equal(injectPageOverride(html, ''), html); + }); + + // Regression: page copy legitimately contains dollar amounts. `$1`..`$9`, `$&`, + // backtick-$ and `$'` are all special in a String.replace REPLACEMENT string, so + // interpolating the payload into a template literal silently rewrites it and the + // injected `; + const out = injectPageOverride(campaignHtml(), payload); + assert.ok( + out.includes(payload), + `payload was rewritten by String.replace: ${out.slice( + out.indexOf('var copy'), + out.indexOf('var copy') + 120 + )}` + ); + }); + } + + test('a compiled page containing "$2" still yields a parseable script', () => { + // Full path: real compiled-template payload -> injection -> must still parse. + const script = buildPageOverrideScript({ + 'page-1': 'return "surviving on less than $2 a day";', + }); + const out = injectPageOverride(campaignHtml(), script); + + assert.ok(out.includes(script), 'payload must survive injection byte-for-byte'); + + const inner = script + .replace(/^\s*