diff --git a/tests/live-browser-source.test.mjs b/tests/live-browser-source.test.mjs index d2539df04..0f031bc86 100644 --- a/tests/live-browser-source.test.mjs +++ b/tests/live-browser-source.test.mjs @@ -42,7 +42,7 @@ describe('live-browser source contracts', () => { ); assert.match( SOURCE, - /fetch\('http:\/\/localhost:' \+ PORT \+ '\/manual-edit-stash'[\s\S]{0,260}?pageUrl: location\.pathname[\s\S]{0,80}?element: extractContext\(contextElement\)[\s\S]{0,40}?ops,/, + /fetch\('http:\/\/localhost:' \+ PORT \+ '\/manual-edit-stash\?token='[\s\S]{0,300}?pageUrl: location\.pathname[\s\S]{0,80}?element: extractContext\(contextElement\)[\s\S]{0,40}?ops,/, 'Save should stage edits through /manual-edit-stash with element context and ops', ); assert.match( diff --git a/tests/live-server.test.mjs b/tests/live-server.test.mjs index ade723679..87b9725a1 100644 --- a/tests/live-server.test.mjs +++ b/tests/live-server.test.mjs @@ -393,17 +393,17 @@ describe('live-server integration', () => { assert.ok(body.includes('__IMPECCABLE_LIVE_INIT__'), 'authorized /live.js returns the assembled bundle'); }); - it('CORS: a remote origin gets no Access-Control-Allow-Origin on any route', async () => { + it('CORS: a tokenless remote origin gets no Access-Control-Allow-Origin on any route', async () => { const evil = 'https://evil.example'; - for (const path of ['/health', `/live.js?token=${server.token}`, `/status?token=${server.token}`]) { + for (const path of ['/health', '/live.js', '/status', '/status?token=not-the-token']) { const res = await fetch(`http://localhost:${server.port}${path}`, { headers: { Origin: evil } }); assert.equal( res.headers.get('access-control-allow-origin'), null, - `remote origin must not be reflected on ${path}`, + `tokenless remote origin must not be reflected on ${path}`, ); } - // Preflight from a remote origin is likewise unauthorized to read. + // Preflight from a tokenless remote origin is likewise unauthorized to read. const preflight = await fetch(`http://localhost:${server.port}/poll`, { method: 'OPTIONS', headers: { Origin: evil, 'Access-Control-Request-Method': 'POST' }, @@ -411,6 +411,28 @@ describe('live-server integration', () => { assert.equal(preflight.headers.get('access-control-allow-origin'), null); }); + it('CORS: a non-loopback origin with the valid token is reflected (ddev-style dev hosts)', async () => { + // A dev server on a loopback alias (https://my-site.ddev.site, *.test) + // sends a non-loopback Origin, but its overlay requests carry the session + // token — that token, not the origin, is the trust signal. + const origin = 'https://my-site.ddev.site'; + const res = await fetch(`http://localhost:${server.port}/status?token=${server.token}`, { + headers: { Origin: origin }, + }); + assert.equal(res.status, 200); + assert.equal(res.headers.get('access-control-allow-origin'), origin); + assert.ok(/\bOrigin\b/i.test(res.headers.get('vary') || ''), 'Vary: Origin accompanies the reflected origin'); + + // Preflight to the same token-bearing URL is authorized too (OPTIONS hits + // the same URL, query string included). + const preflight = await fetch(`http://localhost:${server.port}/poll?token=${server.token}`, { + method: 'OPTIONS', + headers: { Origin: origin, 'Access-Control-Request-Method': 'POST' }, + }); + assert.equal(preflight.status, 204); + assert.equal(preflight.headers.get('access-control-allow-origin'), origin); + }); + it('CORS: a loopback origin is reflected with Vary: Origin', async () => { for (const origin of [ `http://localhost:${server.port}`,