diff --git a/src/json-pipe.js b/src/json-pipe.js index 554adbf3..91b7c09a 100644 --- a/src/json-pipe.js +++ b/src/json-pipe.js @@ -65,6 +65,13 @@ async function fetchJsonContent(state, req, res) { if (state.content.sourceBus === 'content') { keys.push(await computeSurrogateKey(`${contentBusId}${info.path}`)); keys.push(contentBusId); + const contentKeyPrefix = partition === 'preview' ? 'p_' : ''; + if (partition === 'preview') { + // temporarily provide additional preview content keys + // TODO: eventually provide either (prefixed) preview or (unprefixed) live content keys + keys.push(`${contentKeyPrefix}${await computeSurrogateKey(`${contentBusId}${info.path}`)}`); + keys.push(`${contentKeyPrefix}${contentBusId}`); + } } else { keys.push(`${ref}--${repo}--${owner}_code`); keys.push(await computeSurrogateKey(`${ref}--${repo}--${owner}${info.path}`)); @@ -104,6 +111,13 @@ async function computeSurrogateKeys(state) { if (state.content.sourceBus.includes('content')) { keys.push(await computeSurrogateKey(`${state.contentBusId}${state.info.path}`)); keys.push(state.contentBusId); + const contentKeyPrefix = state.partition === 'preview' ? 'p_' : ''; + if (state.partition === 'preview') { + // temporarily provide additional preview content keys + // TODO: eventually provide either (prefixed) preview or (unprefixed) live content keys + keys.push(`${contentKeyPrefix}${await computeSurrogateKey(`${state.contentBusId}${state.info.path}`)}`); + keys.push(`${contentKeyPrefix}${state.contentBusId}`); + } } return keys; diff --git a/src/steps/fetch-404.js b/src/steps/fetch-404.js index 22c45fbf..5ed33e17 100644 --- a/src/steps/fetch-404.js +++ b/src/steps/fetch-404.js @@ -22,7 +22,7 @@ import { getPathKey } from './set-x-surrogate-key-header.js'; */ export default async function fetch404(state, req, res) { const { - owner, repo, ref, contentBusId, + owner, repo, ref, contentBusId, partition, } = state; const ret = await state.s3Loader.getObject('helix-code-bus', `${owner}/${repo}/${ref}/404.html`); if (ret.status === 200) { @@ -46,12 +46,25 @@ export default async function fetch404(state, req, res) { `${ref}--${repo}--${owner}_404`, `${ref}--${repo}--${owner}_code`, ]; + const contentKeyPrefix = partition === 'preview' ? 'p_' : ''; + if (partition === 'preview') { + // temporarily provide additional preview content keys + // TODO: eventually provide either (prefixed) preview or (unprefixed) live content keys + keys.push(`${contentKeyPrefix}${pathKey}`); + keys.push(`${contentKeyPrefix}${contentBusId}`); + } if (state.info.unmappedPath) { - keys.push(await getPathKey({ + const unmappedPathKey = await getPathKey({ contentBusId, info: { path: state.info.unmappedPath }, - })); + }); + keys.push(unmappedPathKey); + if (partition === 'preview') { + // temporarily provide additional preview content key + // TODO: eventually provide either (prefixed) preview or (unprefixed) live content key + keys.push(`${contentKeyPrefix}${unmappedPathKey}`); + } } res.headers.set('x-surrogate-key', keys.join(' ')); diff --git a/src/steps/fetch-content.js b/src/steps/fetch-content.js index 080c7220..f017e25b 100644 --- a/src/steps/fetch-content.js +++ b/src/steps/fetch-content.js @@ -48,6 +48,13 @@ export default async function fetchContent(state, req, res) { } else { keys.push(await computeSurrogateKey(`${contentBusId}${info.path}`)); keys.push(contentBusId); + const contentKeyPrefix = partition === 'preview' ? 'p_' : ''; + if (partition === 'preview') { + // temporarily provide additional preview content keys + // TODO: eventually provide either (prefixed) preview or (unprefixed) live content keys + keys.push(`${contentKeyPrefix}${await computeSurrogateKey(`${contentBusId}${info.path}`)}`); + keys.push(`${contentKeyPrefix}${contentBusId}`); + } } res.headers.set('x-surrogate-key', keys.join(' ')); res.error = 'moved'; diff --git a/src/steps/set-x-surrogate-key-header.js b/src/steps/set-x-surrogate-key-header.js index 8a0f10bd..855ee346 100644 --- a/src/steps/set-x-surrogate-key-header.js +++ b/src/steps/set-x-surrogate-key-header.js @@ -42,11 +42,12 @@ export async function getPathKey(state) { */ export default async function setXSurrogateKeyHeader(state, req, res) { const { - contentBusId, owner, repo, ref, + contentBusId, owner, repo, ref, partition, } = state; const isCode = state.content.sourceBus === 'code'; + const contentKeyPrefix = partition === 'preview' ? 'p_' : ''; const keys = []; const hash = await getPathKey(state); if (isCode) { @@ -57,6 +58,13 @@ export default async function setXSurrogateKeyHeader(state, req, res) { keys.push(`${contentBusId}_metadata`); keys.push(`${ref}--${repo}--${owner}_head`); keys.push(contentBusId); + if (partition === 'preview') { + // temporarily provide additional preview content keys + // TODO: eventually provide either (prefixed) preview or (unprefixed) live content keys + keys.push(`${contentKeyPrefix}${hash}`); + keys.push(`${contentKeyPrefix}${contentBusId}_metadata`); + keys.push(`${contentKeyPrefix}${contentBusId}`); + } } // for folder-mapped resources, we also need to include the surrogate key of the mapped metadata if (state.mapped) { @@ -67,6 +75,17 @@ export default async function setXSurrogateKeyHeader(state, req, res) { info: { path: state.info.unmappedPath }, })); } + if (partition === 'preview') { + // temporarily provide additional preview content keys + // TODO: eventually provide either (prefixed) preview or (unprefixed) live content keys + keys.push(`${contentKeyPrefix}${hash}_metadata`); + if (state.info.unmappedPath) { + keys.push(`${contentKeyPrefix}${await getPathKey({ + contentBusId, + info: { path: state.info.unmappedPath }, + })}`); + } + } } res.headers.set('x-surrogate-key', keys.join(' ')); } diff --git a/test/json-pipe.test.js b/test/json-pipe.test.js index 58224f02..a53d9743 100644 --- a/test/json-pipe.test.js +++ b/test/json-pipe.test.js @@ -108,6 +108,17 @@ describe('JSON Pipe Test', () => { 'last-modified': 'Wed, 12 Oct 2009 17:50:00 GMT', }, }), + ) + .reply( + 'helix-content-bus', + 'foobar/live/en/index.json', + new PipelineResponse(TEST_SINGLE_SHEET, { + headers: { + 'content-type': 'application/json', + 'x-amz-meta-x-source-location': 'foo-bar', + 'last-modified': 'Wed, 12 Oct 2009 17:50:00 GMT', + }, + }), ), timer: { update: () => {}, @@ -126,7 +137,23 @@ describe('JSON Pipe Test', () => { it('fetches correct content', async () => { const state = createDefaultState(); - const resp = await jsonPipe(state, new PipelineRequest('https://json-filter.com/?limit=10&offset=5')); + let resp = await jsonPipe(state, new PipelineRequest('https://json-filter.com/?limit=10&offset=5')); + assert.strictEqual(resp.status, 200); + assert.deepStrictEqual(await resp.json(), { + ':type': 'sheet', + offset: 5, + limit: 10, + total: TEST_DATA.length, + data: TEST_DATA.slice(5, 15), + }); + assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { + 'content-type': 'application/json', + 'x-surrogate-key': 'Atrz_qDg26DmSe9a foobar p_Atrz_qDg26DmSe9a p_foobar', + 'last-modified': 'Wed, 12 Oct 2009 17:50:00 GMT', + }); + // live (code coverage) + state.partition = 'live'; + resp = await jsonPipe(state, new PipelineRequest('https://json-filter.com/?limit=10&offset=5')); assert.strictEqual(resp.status, 200); assert.deepStrictEqual(await resp.json(), { ':type': 'sheet', @@ -148,7 +175,7 @@ describe('JSON Pipe Test', () => { assert.strictEqual(resp.status, 404); assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { 'x-error': 'failed to load /config.json: 404', - 'x-surrogate-key': 'U_NW4adJU7Qazf-I pzrU-nNKQOUYNTEf ref--repo--owner_code kz8SoCaNqfp4ohQo foobar', + 'x-surrogate-key': 'U_NW4adJU7Qazf-I pzrU-nNKQOUYNTEf ref--repo--owner_code kz8SoCaNqfp4ohQo foobar p_kz8SoCaNqfp4ohQo p_foobar', }); }); @@ -165,7 +192,7 @@ describe('JSON Pipe Test', () => { assert.strictEqual(resp.status, 200); assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { 'content-type': 'application/json', - 'x-surrogate-key': 'U_NW4adJU7Qazf-I pzrU-nNKQOUYNTEf ref--repo--owner_code kz8SoCaNqfp4ohQo foobar', + 'x-surrogate-key': 'U_NW4adJU7Qazf-I pzrU-nNKQOUYNTEf ref--repo--owner_code kz8SoCaNqfp4ohQo foobar p_kz8SoCaNqfp4ohQo p_foobar', }); assert.deepStrictEqual(await resp.json(), { public: { @@ -193,7 +220,7 @@ describe('JSON Pipe Test', () => { }); assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { 'content-type': 'application/json', - 'x-surrogate-key': 'Atrz_qDg26DmSe9a foobar', + 'x-surrogate-key': 'Atrz_qDg26DmSe9a foobar p_Atrz_qDg26DmSe9a p_foobar', 'last-modified': 'Wed, 12 Oct 2009 17:50:00 GMT', }); }); @@ -214,7 +241,7 @@ describe('JSON Pipe Test', () => { 'access-control-allow-origin': '*', 'content-security-policy': 'default-src \'self\'', 'last-modified': 'Wed, 12 Oct 2009 17:50:00 GMT', - 'x-surrogate-key': 'Atrz_qDg26DmSe9a foobar', + 'x-surrogate-key': 'Atrz_qDg26DmSe9a foobar p_Atrz_qDg26DmSe9a p_foobar', 'content-type': 'application/json', }); }); @@ -229,7 +256,7 @@ describe('JSON Pipe Test', () => { 'access-control-allow-origin': '*', 'content-security-policy': 'default-src \'self\'', 'x-error': 'failed to load /en/index.json: 404', - 'x-surrogate-key': 'SIMSxecp2CJXqGYs ref--repo--owner_code Atrz_qDg26DmSe9a foobar', + 'x-surrogate-key': 'SIMSxecp2CJXqGYs ref--repo--owner_code Atrz_qDg26DmSe9a foobar p_Atrz_qDg26DmSe9a p_foobar', }); }); @@ -244,8 +271,26 @@ describe('JSON Pipe Test', () => { 'x-amz-meta-redirect-location': '/de/index.json', }, }), - ); - const resp = await jsonPipe(state, new PipelineRequest('https://json-filter.com/?limit=10&offset=5')); + ) + .reply( + 'helix-content-bus', + 'foobar/live/en/index.json', + new PipelineResponse(TEST_SINGLE_SHEET, { + headers: { + 'content-type': 'application/json', + 'x-amz-meta-redirect-location': '/de/index.json', + }, + }), + ); + let resp = await jsonPipe(state, new PipelineRequest('https://json-filter.com/?limit=10&offset=5')); + assert.strictEqual(resp.status, 301); + assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { + 'location': '/de/index.json', + 'x-surrogate-key': 'Atrz_qDg26DmSe9a foobar p_Atrz_qDg26DmSe9a p_foobar', + }); + // live (code coverage) + state.partition = 'live'; + resp = await jsonPipe(state, new PipelineRequest('https://json-filter.com/?limit=10&offset=5')); assert.strictEqual(resp.status, 301); assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { 'location': '/de/index.json', @@ -301,7 +346,7 @@ describe('JSON Pipe Test', () => { const headers = Object.fromEntries(resp.headers.entries()); assert.deepStrictEqual(headers, { 'last-modified': 'Wed, 12 Oct 2009 17:50:00 GMT', - 'x-surrogate-key': 'Atrz_qDg26DmSe9a foobar', + 'x-surrogate-key': 'Atrz_qDg26DmSe9a foobar p_Atrz_qDg26DmSe9a p_foobar', 'content-type': 'application/json', }); }); @@ -333,7 +378,7 @@ describe('JSON Pipe Test', () => { const headers = Object.fromEntries(resp.headers.entries()); assert.deepStrictEqual(headers, { 'last-modified': 'Wed, 12 Oct 2009 15:50:00 GMT', - 'x-surrogate-key': 'Atrz_qDg26DmSe9a foobar', + 'x-surrogate-key': 'Atrz_qDg26DmSe9a foobar p_Atrz_qDg26DmSe9a p_foobar', 'content-type': 'application/json', }); }); diff --git a/test/rendering.test.js b/test/rendering.test.js index 9c5b5141..ccf8dcbc 100644 --- a/test/rendering.test.js +++ b/test/rendering.test.js @@ -148,7 +148,7 @@ describe('Rendering', () => { config = DEFAULT_CONFIG; }); - async function render(url, selector = '', expectedStatus = 200) { + async function render(url, selector = '', expectedStatus = 200, partition = 'live') { const req = new PipelineRequest(url, { headers: new Map([['host', url.hostname]]), body: '', @@ -160,7 +160,7 @@ describe('Rendering', () => { org: 'adobe', site: 'helix-pages', ref: 'super-test', - partition: 'live', + partition, config, path: selector ? `${url.pathname}${selector}.html` : url.pathname, timer: { @@ -174,7 +174,7 @@ describe('Rendering', () => { } // eslint-disable-next-line default-param-last - async function testRender(url, domSelector = 'main', expStatus) { + async function testRender(url, domSelector = 'main', expStatus, partition = 'live') { if (!(url instanceof URL)) { // eslint-disable-next-line no-param-reassign url = new URL(`https://helix-pages.com/${url}`); @@ -191,7 +191,7 @@ describe('Rendering', () => { // eslint-disable-next-line no-param-reassign expStatus = expHtml === null ? 404 : 200; } - const response = await render(url, '', expStatus); + const response = await render(url, '', expStatus, partition); const actHtml = response.body; console.log(actHtml); if (expStatus === 200) { @@ -516,6 +516,8 @@ describe('Rendering', () => { it('renders 404 if content not found', async () => { await testRender('not-found', 'html'); + // preview (code coverage) + await testRender('not-found', 'html', 404, 'preview'); }); it('can render empty table row', async () => { @@ -593,12 +595,27 @@ describe('Rendering', () => { 'x-surrogate-key': 'gPHXKWdMY_R8KV2Z foo-id super-test--helix-pages--adobe_404 super-test--helix-pages--adobe_code QJqsV4atnOA47sHc', }); assert.strictEqual(body.trim(), ''); + // preview (code coverage) + const resp = await testRender(new URL('https://helix-pipeline.com/broken/folder'), 'html', 404, 'preview'); + assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { + 'access-control-allow-origin': '*', + 'content-type': 'text/html; charset=utf-8', + link: '; rel=modulepreload; as=script; crossorigin=use-credentials', + 'x-error': 'failed to load /not-a-page.md from content-bus: 404', + 'x-surrogate-key': 'gPHXKWdMY_R8KV2Z foo-id super-test--helix-pages--adobe_404 super-test--helix-pages--adobe_code p_gPHXKWdMY_R8KV2Z p_foo-id QJqsV4atnOA47sHc p_QJqsV4atnOA47sHc', + }); + assert.strictEqual(resp.body.trim(), ''); }); it('renders 301 for redirect file', async () => { loader.headers('one-section.md', 'x-amz-meta-redirect-location', 'https://www.adobe.com'); - const ret = await render(new URL('https://localhost/one-section'), '', 301); - assert.strictEqual(ret.headers.get('location'), 'https://www.adobe.com'); + let resp = await render(new URL('https://localhost/one-section'), '', 301); + assert.strictEqual(resp.headers.get('location'), 'https://www.adobe.com'); + assert.strictEqual(resp.headers.get('x-surrogate-key'), 'oHjg_WDu20CBS4rD foo-id'); + // preview (code coverage) + resp = await render(new URL('https://localhost/one-section'), '', 301, 'preview'); + assert.strictEqual(resp.headers.get('location'), 'https://www.adobe.com'); + assert.strictEqual(resp.headers.get('x-surrogate-key'), 'oHjg_WDu20CBS4rD foo-id p_oHjg_WDu20CBS4rD p_foo-id'); }); it('appends .plain.html in redirects', async () => { @@ -640,6 +657,12 @@ describe('Rendering', () => { let resp = await render(new URL('https://helix-pipeline.com/products'), '', 200); assert.match(resp.body, //); + // coverage + loader.status('products.md', 404); + loader.status('generic-product.md', 200); + resp = await render(new URL('https://helix-pipeline.com/products'), '', 200, 'preview'); + assert.match(resp.body, //); + loader.status('product1.md', 404); loader.rewrite('generic-product/metadata.json', 'metadata-product.json'); resp = await render(new URL('https://helix-pipeline.com/products/product1'), '', 200); diff --git a/test/sitemap-pipe.test.js b/test/sitemap-pipe.test.js index e81815b9..8d78e807 100644 --- a/test/sitemap-pipe.test.js +++ b/test/sitemap-pipe.test.js @@ -74,7 +74,7 @@ describe('Sitemap Pipe Test', () => { assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { 'content-type': 'text/plain; charset=utf-8', 'x-error': 'failed to load /sitemap.xml from content-bus: 404', - 'x-surrogate-key': 'RXei-6EcTEMTEIqi foobar_metadata ref--repo--owner_head foobar', + 'x-surrogate-key': 'RXei-6EcTEMTEIqi foobar_metadata ref--repo--owner_head foobar p_RXei-6EcTEMTEIqi p_foobar_metadata p_foobar', }); }); @@ -92,7 +92,7 @@ describe('Sitemap Pipe Test', () => { assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { 'content-type': 'text/plain; charset=utf-8', 'x-error': 'Failed to parse /sitemap.json: Unexpected token \'h\', "this is not JSON" is not valid JSON', - 'x-surrogate-key': 'RXei-6EcTEMTEIqi foobar_metadata ref--repo--owner_head foobar', + 'x-surrogate-key': 'RXei-6EcTEMTEIqi foobar_metadata ref--repo--owner_head foobar p_RXei-6EcTEMTEIqi p_foobar_metadata p_foobar', }); }); @@ -110,7 +110,7 @@ describe('Sitemap Pipe Test', () => { assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { 'content-type': 'text/plain; charset=utf-8', 'x-error': "Expected 'data' array not found in /sitemap.json", - 'x-surrogate-key': 'RXei-6EcTEMTEIqi foobar_metadata ref--repo--owner_head foobar', + 'x-surrogate-key': 'RXei-6EcTEMTEIqi foobar_metadata ref--repo--owner_head foobar p_RXei-6EcTEMTEIqi p_foobar_metadata p_foobar', }); }); @@ -128,7 +128,7 @@ describe('Sitemap Pipe Test', () => { assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { 'content-type': 'application/xml; charset=utf-8', 'last-modified': 'Fri, 30 Apr 2021 03:47:18 GMT', - 'x-surrogate-key': 'RXei-6EcTEMTEIqi foobar_metadata ref--repo--owner_head foobar', + 'x-surrogate-key': 'RXei-6EcTEMTEIqi foobar_metadata ref--repo--owner_head foobar p_RXei-6EcTEMTEIqi p_foobar_metadata p_foobar', }); assert.strictEqual(resp.body, ` @@ -154,7 +154,7 @@ describe('Sitemap Pipe Test', () => { assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { 'content-type': 'application/xml; charset=utf-8', 'last-modified': 'Fri, 30 Apr 2021 03:47:18 GMT', - 'x-surrogate-key': 'RXei-6EcTEMTEIqi foobar_metadata ref--repo--owner_head foobar', + 'x-surrogate-key': 'RXei-6EcTEMTEIqi foobar_metadata ref--repo--owner_head foobar p_RXei-6EcTEMTEIqi p_foobar_metadata p_foobar', }); assert.strictEqual(resp.body, ` @@ -190,7 +190,7 @@ describe('Sitemap Pipe Test', () => { assert.deepStrictEqual(Object.fromEntries(resp.headers.entries()), { 'content-type': 'application/xml; charset=utf-8', 'last-modified': 'Fri, 30 Apr 2021 03:47:18 GMT', - 'x-surrogate-key': 'RXei-6EcTEMTEIqi foobar_metadata ref--repo--owner_head foobar', + 'x-surrogate-key': 'RXei-6EcTEMTEIqi foobar_metadata ref--repo--owner_head foobar p_RXei-6EcTEMTEIqi p_foobar_metadata p_foobar', }); assert.strictEqual(resp.body, `