From 42682f59c16569122af1ab340490a0d4c737e141 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 08:45:33 +0100 Subject: [PATCH 01/11] Revert "Revert "feat: handle bad requests (#204)" (#212)" This reverts commit 306fcb5177e881fbe3f72ff4ffecb05065f11e59. --- src/handlers/unknown.js | 16 -------------- src/index.js | 15 ++++++++++--- src/utils/daCtx.js | 7 +++++++ test/handlers/unknown.test.js | 21 ------------------- test/index.test.js | 37 +++++++++++++++++++++++++++++---- test/storage/utils/list.test.js | 25 +++++++++------------- 6 files changed, 62 insertions(+), 59 deletions(-) delete mode 100644 src/handlers/unknown.js delete mode 100644 test/handlers/unknown.test.js diff --git a/src/handlers/unknown.js b/src/handlers/unknown.js deleted file mode 100644 index dbe19289..00000000 --- a/src/handlers/unknown.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2025 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -export default function unknownHandler() { - const body = JSON.stringify({ message: 'Unknown method. Please see: https://docs.da.live for more information.' }); - return { body, status: 501 }; -} diff --git a/src/index.js b/src/index.js index 20cfb66b..a848d2b0 100644 --- a/src/index.js +++ b/src/index.js @@ -16,7 +16,6 @@ import headHandler from './handlers/head.js'; import getHandler from './handlers/get.js'; import postHandler from './handlers/post.js'; import deleteHandler from './handlers/delete.js'; -import unknownHandler from './handlers/unknown.js'; export default { /** @@ -29,7 +28,17 @@ export default { return daResp({ status: 204 }); } - const daCtx = await getDaCtx(req, env); + let daCtx; + try { + daCtx = await getDaCtx(req, env); + } catch (e) { + if (e.message === 'Invalid path') { + return daResp({ status: 400 }); + } + console.error('Error computing context', e); + return daResp({ status: 500 }); + } + const { authorized, key } = daCtx; if (!authorized) { const status = daCtx.users[0].email === 'anonymous' ? 401 : 403; @@ -57,7 +66,7 @@ export default { respObj = await deleteHandler({ req, env, daCtx }); break; default: - respObj = unknownHandler(); + respObj = { status: 405 }; } if (!respObj) return daResp({ status: 404 }); diff --git a/src/utils/daCtx.js b/src/utils/daCtx.js index 0e35887f..b4f8640d 100644 --- a/src/utils/daCtx.js +++ b/src/utils/daCtx.js @@ -59,6 +59,13 @@ export default async function getDaCtx(req, env) { const path = parts.filter((part) => part !== ''); const keyBase = path.join('/'); + const pnlc = pathname.toLocaleLowerCase(); + const validPath = `/${api}/${org}/${keyBase}`; + + if (!org || !(pnlc === validPath || pnlc === `${validPath}/`)) { + throw new Error('Invalid path'); + } + // Get the final source name daCtx.filename = path.pop() || ''; diff --git a/test/handlers/unknown.test.js b/test/handlers/unknown.test.js deleted file mode 100644 index cee3586d..00000000 --- a/test/handlers/unknown.test.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ -import assert from 'node:assert'; -import unknownHandler from '../../src/handlers/unknown.js'; - -describe('unknownHandler', () => { - it('should return unknown response', async () => { - const result = await unknownHandler({}); - assert.strictEqual(result.status, 501); - assert.strictEqual(result.body.includes('Unknown method'), true); - }); -}); diff --git a/test/index.test.js b/test/index.test.js index b86ce7a6..e6c5f2e2 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -25,7 +25,7 @@ describe('fetch', () => { it('should return a response object for unknown', async () => { const resp = await handler.fetch({ url: 'https://www.example.com', method: 'BLAH' }, {}); - assert.strictEqual(resp.status, 501); + assert.strictEqual(resp.status, 400); }); it('should return 401 when not authorized and not logged in', async () => { @@ -49,9 +49,38 @@ describe('fetch', () => { const resp = await hnd.fetch({ method: 'GET' }, {}); assert.strictEqual(resp.status, 403); }); +}); + +describe('invalid routes', () => { + const fetchStatus = async (path, method) => { + const resp = await handler.fetch({ method, url: `http://www.sample.com${path}` }, {}); + return resp.status; + }; + + const test = async (path, status) => { + const methods = ['GET', 'POST', 'PUT', 'DELETE']; + for (const method of methods) { + // eslint-disable-next-line no-await-in-loop + const s = await fetchStatus(path, method); + assert.strictEqual(s, status); + } + }; + + it('return 400 for invalid paths', async () => { + await test('/', 400); + await test('/source/owner', 400); + await test('/source//owner/repo/path/file.html', 400); + await test('/source/owner//repo/path/file.html', 400); + await test('/source/owner/repo//path/file.html', 400); + await test('/source/owner/repo/path//file.html', 400); + }); + + it('return 404 for unknown paths', async () => { + await test('/unknown/owner/repo/path/file.html', 404); + }); - it('return 404 for unknown get route', async () => { - const resp = await handler.fetch({ method: 'GET', url: 'http://www.example.com/' }, {}); - assert.strictEqual(resp.status, 404); + it('return 405 for unknown methods', async () => { + const status = await fetchStatus('/source/owner/repo/path/file.html', 'BLAH'); + assert.strictEqual(status, 405); }); }); diff --git a/test/storage/utils/list.test.js b/test/storage/utils/list.test.js index f4f3919e..daef4255 100644 --- a/test/storage/utils/list.test.js +++ b/test/storage/utils/list.test.js @@ -13,7 +13,6 @@ import assert from 'node:assert'; import sinon from 'sinon'; -import getDaCtx from '../../../src/utils/daCtx.js'; import formatList, { listCommand } from '../../../src/storage/utils/list.js'; const MOCK = { @@ -61,12 +60,8 @@ const MOCK = { ], }; -const req = new Request('https://example.com/source/adobecom'); - -const daCtx = getDaCtx(req, {}); - describe('Format object list', () => { - const list = formatList(MOCK, daCtx); + const list = formatList(MOCK); it('should return a true folder / common prefix', () => { assert.strictEqual(list[0].name, 'blog'); @@ -98,21 +93,21 @@ describe('Format object list', () => { it('should handle empty CommonPrefixes', () => { const emptyMock = { Contents: MOCK.Contents }; - const result = formatList(emptyMock, daCtx); + const result = formatList(emptyMock); assert(Array.isArray(result)); assert(result.length > 0); }); it('should handle empty Contents', () => { const emptyMock = { CommonPrefixes: MOCK.CommonPrefixes }; - const result = formatList(emptyMock, daCtx); + const result = formatList(emptyMock); assert(Array.isArray(result)); assert(result.length > 0); }); it('should handle both empty CommonPrefixes and Contents', () => { const emptyMock = {}; - const result = formatList(emptyMock, daCtx); + const result = formatList(emptyMock); assert(Array.isArray(result)); assert.strictEqual(result.length, 0); }); @@ -124,7 +119,7 @@ describe('Format object list', () => { { Prefix: 'normal-folder/' }, ], }; - const result = formatList(mockWithExtensionFolder, daCtx); + const result = formatList(mockWithExtensionFolder); const extensionFolder = result.find((item) => item.name === 'file.jpg'); assert.strictEqual(extensionFolder, undefined); const normalFolder = result.find((item) => item.name === 'normal-folder'); @@ -140,7 +135,7 @@ describe('Format object list', () => { }, ], }; - const result = formatList(mockWithComplexFile, daCtx); + const result = formatList(mockWithComplexFile); assert.strictEqual(result.length, 0); }); @@ -153,7 +148,7 @@ describe('Format object list', () => { }, ], }; - const result = formatList(mockWithHiddenFile, daCtx); + const result = formatList(mockWithHiddenFile); assert.strictEqual(result.length, 0); }); @@ -166,7 +161,7 @@ describe('Format object list', () => { }, ], }; - const result = formatList(mockWithProps, daCtx); + const result = formatList(mockWithProps); const propsItem = result.find((item) => item.name === 'test'); assert(propsItem); assert.strictEqual(propsItem.ext, undefined); @@ -183,7 +178,7 @@ describe('Format object list', () => { }, ], }; - const result = formatList(mockWithBoth, daCtx); + const result = formatList(mockWithBoth); const testItems = result.filter((item) => item.name === 'test'); assert.strictEqual(testItems.length, 1); }); @@ -196,7 +191,7 @@ describe('Format object list', () => { { Key: 'beta.html', LastModified: new Date('2025-01-01') }, ], }; - const result = formatList(mockForSorting, daCtx); + const result = formatList(mockForSorting); assert.strictEqual(result[0].name, 'alpha'); assert.strictEqual(result[1].name, 'beta'); assert.strictEqual(result[2].name, 'zebra'); From c34b2663482672a58b507abc1183f7dd27e192af Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 11:46:43 +0100 Subject: [PATCH 02/11] chore: add more tests --- .../test-folder.props._S3rver_metadata.json | 8 +++ .../test-folder.props._S3rver_object | 1 + .../test-folder.props._S3rver_object.md5 | 1 + test/it/smoke.test.js | 55 ++++++++++++++----- 4 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_metadata.json create mode 100644 test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object create mode 100644 test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object.md5 diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_metadata.json b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_metadata.json new file mode 100644 index 00000000..3a48491d --- /dev/null +++ b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_metadata.json @@ -0,0 +1,8 @@ +{ + "content-type": "application/json", + "x-amz-meta-id": "4875c756-42ce-4898-ae86-9b0d88146fbe", + "x-amz-meta-path": "test-repo/test-folder.props", + "x-amz-meta-timestamp": "1764844348129", + "x-amz-meta-users": "[{\"email\":\"anonymous\"}]", + "x-amz-meta-version": "7dd6da34-02a3-4d7f-be02-8170a695199b" +} \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object.md5 b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object.md5 new file mode 100644 index 00000000..a3f71108 --- /dev/null +++ b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object.md5 @@ -0,0 +1 @@ +99914b932bd37a50b983c5e7c90ae93b \ No newline at end of file diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index ef4c2982..8f550687 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -21,6 +21,9 @@ const SERVER_PORT = 8788; const SERVER_URL = `http://localhost:${SERVER_PORT}`; const S3_DIR = './test/it/bucket'; +const ORG = 'test-org'; +const REPO = 'test-repo'; + describe('Integration Tests: smoke tests', function () { let s3rver; let devServer; @@ -32,7 +35,7 @@ describe('Integration Tests: smoke tests', function () { port: S3_PORT, address: '127.0.0.1', directory: path.resolve(S3_DIR), - silent: true, + // silent: true, }); await s3rver.run(); @@ -56,6 +59,7 @@ describe('Integration Tests: smoke tests', function () { let started = false; devServer.stdout.on('data', (data) => { const str = data.toString(); + console.log(str); if (str.includes('Ready on http://localhost') && !started) { started = true; resolve(); @@ -100,11 +104,9 @@ describe('Integration Tests: smoke tests', function () { }); it('should get a object via HTTP request', async () => { - const org = 'test-org'; - const repo = 'test-repo'; const pathname = 'test-folder/page1.html'; - const url = `${SERVER_URL}/source/${org}/${repo}/${pathname}`; + const url = `${SERVER_URL}/source/${ORG}/${REPO}/${pathname}`; const resp = await fetch(url); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); @@ -114,11 +116,9 @@ describe('Integration Tests: smoke tests', function () { }); it('should list objects via HTTP request', async () => { - const org = 'test-org'; - const repo = 'test-repo'; const key = 'test-folder'; - const url = `${SERVER_URL}/list/${org}/${repo}/${key}`; + const url = `${SERVER_URL}/list/${ORG}/${REPO}/${key}`; const resp = await fetch(url); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); @@ -131,8 +131,6 @@ describe('Integration Tests: smoke tests', function () { }); it('should post an object via HTTP request', async () => { - const org = 'test-org'; - const repo = 'test-repo'; const key = 'test-folder/page3'; const ext = '.html'; @@ -142,7 +140,7 @@ describe('Integration Tests: smoke tests', function () { const htmlFile = new File([htmlBlob], 'page3.html', { type: 'text/html' }); formData.append('data', htmlFile); - const url = `${SERVER_URL}/source/${org}/${repo}/${key}${ext}`; + const url = `${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`; let resp = await fetch(url, { method: 'POST', body: formData, @@ -151,17 +149,44 @@ describe('Integration Tests: smoke tests', function () { assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); let body = await resp.json(); - assert.strictEqual(body.source.editUrl, `https://da.live/edit#/${org}/${repo}/${key}`); - assert.strictEqual(body.source.contentUrl, `https://content.da.live/${org}/${repo}/${key}`); - assert.strictEqual(body.aem.previewUrl, `https://main--test-repo--test-org.aem.page/${key}`); - assert.strictEqual(body.aem.liveUrl, `https://main--test-repo--test-org.aem.live/${key}`); + assert.strictEqual(body.source.editUrl, `https://da.live/edit#/${ORG}/${REPO}/${key}`); + assert.strictEqual(body.source.contentUrl, `https://content.da.live/${ORG}/${REPO}/${key}`); + assert.strictEqual(body.aem.previewUrl, `https://main--${REPO}--${ORG}.aem.page/${key}`); + assert.strictEqual(body.aem.liveUrl, `https://main--${REPO}--${ORG}.aem.live/${key}`); // validate page is here (include extension in GET request) - resp = await fetch(`${SERVER_URL}/source/${org}/${repo}/${key}${ext}`); + resp = await fetch(`${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); body = await resp.text(); assert.strictEqual(body, '

Page 3

'); }); + + it('should logout via HTTP request', async () => { + const url = `${SERVER_URL}/logout`; + const resp = await fetch(url, { + method: 'POST', + }); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + }); + + it('should list repos via HTTP request', async () => { + const url = `${SERVER_URL}/list/${ORG}`; + const resp = await fetch(url); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + + const body = await resp.json(); + assert.strictEqual(body.length, 1, `Expected 1 repo, got ${body.length}`); + assert.strictEqual(body[0].name, REPO, `Expected ${REPO}, got ${body[0].name}`); + }); + + it.only('should deal with no config found via HTTP request', async () => { + const url = `${SERVER_URL}/config/${ORG}`; + const resp = await fetch(url); + + assert.strictEqual(resp.status, 404, `Expected 404, got ${resp.status}`); + }); }); From 5fde3fe23bf533e66e7bd7f02745f850bde42ac8 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 11:47:01 +0100 Subject: [PATCH 03/11] chore: not only --- test/it/smoke.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index 8f550687..ec2fc655 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -183,7 +183,7 @@ describe('Integration Tests: smoke tests', function () { assert.strictEqual(body[0].name, REPO, `Expected ${REPO}, got ${body[0].name}`); }); - it.only('should deal with no config found via HTTP request', async () => { + it('should deal with no config found via HTTP request', async () => { const url = `${SERVER_URL}/config/${ORG}`; const resp = await fetch(url); From df9764561ef9a35e0c82bf4ff17f214f5b9ff95b Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 12:02:15 +0100 Subject: [PATCH 04/11] chore: config test --- test/it/smoke.test.js | 47 +++++++++++++++++++++++++++++++++++++++++++ wrangler.toml | 6 +++--- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index ec2fc655..2ed2f582 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -31,6 +31,14 @@ describe('Integration Tests: smoke tests', function () { before(async function () { // Increase timeout for server startup this.timeout(30000); + + // Clear wrangler state to start fresh - needed only for local testing + const fs = await import('fs'); + const wranglerState = path.join(process.cwd(), '.wrangler/state'); + if (fs.existsSync(wranglerState)) { + fs.rmSync(wranglerState, { recursive: true }); + } + s3rver = new S3rver({ port: S3_PORT, address: '127.0.0.1', @@ -189,4 +197,43 @@ describe('Integration Tests: smoke tests', function () { assert.strictEqual(resp.status, 404, `Expected 404, got ${resp.status}`); }); + + it('should post and get org config via HTTP request', async () => { + // First POST the config - must include CONFIG write permission + const configData = JSON.stringify({ + total: 2, + limit: 2, + offset: 0, + data: [ + { path: 'CONFIG', actions: 'write', groups: 'anonymous' }, + { key: 'admin.role.all', value: 'test-value' }, + ], + ':type': 'sheet', + ':sheetname': 'permissions', + }); + + const formData = new FormData(); + formData.append('config', configData); + + let url = `${SERVER_URL}/config/${ORG}`; + let resp = await fetch(url, { + method: 'POST', + body: formData, + }); + + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); + + // Now GET the config + url = `${SERVER_URL}/config/${ORG}`; + resp = await fetch(url); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + + const body = await resp.json(); + assert.strictEqual(body.total, 2, `Expected 2, got ${body.total}`); + assert.strictEqual(body.data[0].path, 'CONFIG', `Expected CONFIG, got ${body.data[0].path}`); + assert.strictEqual(body.data[0].actions, 'write', `Expected write, got ${body.data[0].actions}`); + assert.strictEqual(body.data[1].key, 'admin.role.all', `Expected admin.role.all, got ${body.data[1].key}`); + assert.strictEqual(body.data[1].value, 'test-value', `Expected test-value, got ${body.data[1].value}`); + }); }); diff --git a/wrangler.toml b/wrangler.toml index 45798418..290449de 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -61,9 +61,9 @@ services = [ ] kv_namespaces = [ - { binding = "DA_AUTH", id = "f5cc4d0150ac48d5b56948b5d147c77e" }, - { binding = "DA_CONFIG", id = "3f6f6db5c27348fabee7c2a8ed39be17" }, - { binding = "DA_JOBS", id = "66265e68cb2b422d8dc7a34b9fe2b0c9" } + { binding = "DA_AUTH", id = "local-da-auth" }, + { binding = "DA_CONFIG", id = "local-da-config" }, + { binding = "DA_JOBS", id = "local-da-jobs" } ] r2_buckets = [ From 227d6990c8a5254f15ebf2e066b758463ce6c918 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 13:55:13 +0100 Subject: [PATCH 05/11] chore: more tests --- test/index.test.js | 11 +++++------ test/utils/daCtx.test.js | 19 +++++++++++++++++++ test/utils/mocks/req.js | 3 +++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/test/index.test.js b/test/index.test.js index e6c5f2e2..237bf60b 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -25,7 +25,7 @@ describe('fetch', () => { it('should return a response object for unknown', async () => { const resp = await handler.fetch({ url: 'https://www.example.com', method: 'BLAH' }, {}); - assert.strictEqual(resp.status, 400); + assert.strictEqual(resp.status, 405); }); it('should return 401 when not authorized and not logged in', async () => { @@ -68,11 +68,10 @@ describe('invalid routes', () => { it('return 400 for invalid paths', async () => { await test('/', 400); - await test('/source/owner', 400); - await test('/source//owner/repo/path/file.html', 400); - await test('/source/owner//repo/path/file.html', 400); - await test('/source/owner/repo//path/file.html', 400); - await test('/source/owner/repo/path//file.html', 400); + await test('/source//org/repo/path/file.html', 400); + await test('/source/org//repo/path/file.html', 400); + await test('/source/org/repo//path/file.html', 400); + await test('/source/org/repo/path//file.html', 400); }); it('return 404 for unknown paths', async () => { diff --git a/test/utils/daCtx.test.js b/test/utils/daCtx.test.js index 904e6c8c..4ecdd049 100644 --- a/test/utils/daCtx.test.js +++ b/test/utils/daCtx.test.js @@ -33,19 +33,38 @@ describe('DA context', () => { }); }); + describe('Endpoint context', async () => { + let daCtx; + let daCtxNoTrail; + + before(async () => { + daCtx = await getDaCtx(reqs.endpoint, env); + daCtxNoTrail = await getDaCtx(reqs.endpointNoTrail, env); + }); + + it('should support endpoint paths', () => { + assert.strictEqual(daCtx.api, 'endpoint'); + assert.strictEqual(daCtxNoTrail.api, 'endpoint'); + }); + }); + describe('Org context', async () => { let daCtx; + let daCtxNoTrail; before(async () => { daCtx = await getDaCtx(reqs.org, env); + daCtxNoTrail = await getDaCtx(reqs.orgNoTrail, env); }); it('should return an undefined site', () => { assert.strictEqual(daCtx.site, undefined); + assert.strictEqual(daCtxNoTrail.site, undefined); }); it('should return a blank filename', () => { assert.strictEqual(daCtx.filename, ''); + assert.strictEqual(daCtxNoTrail.filename, ''); }); }); diff --git a/test/utils/mocks/req.js b/test/utils/mocks/req.js index 4ca5f9c9..4e10c674 100644 --- a/test/utils/mocks/req.js +++ b/test/utils/mocks/req.js @@ -47,11 +47,14 @@ const optsWithForceFail = { const reqs = { api: new Request('https://da.live/api/source/cq/', optsWithEmptyHead), org: new Request('https://da.live/source/cq/', optsWithEmptyHead), + orgNoTrail: new Request('https://da.live/list/cq', optsWithEmptyHead), site: new Request('https://da.live/source/cq/Geometrixx', optsWithAuth), folder: new Request('https://da.live/source/cq/Geometrixx/NFT/', optsWithExpAuth), file: new Request('https://da.live/source/cq/Geometrixx/NFT/Outreach.html', optsWithEmptyBearer), media: new Request('https://da.live/source/cq/Geometrixx/NFT/blockchain.png', optsWithForceFail), siteMulti: new Request('https://da.live/source/cq/Geometrixx', optsWithMultiAuthAnon), + endpoint: new Request('https://da.live/endpoint/', optsWithEmptyHead), + endpointNoTrail: new Request('https://da.live/endpoint', optsWithEmptyHead), }; export default reqs; From 5978f37e4bc2899d1acb7d635eee158e6c1f90f0 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 14:12:42 +0100 Subject: [PATCH 06/11] chore: clean up --- test/it/smoke.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index 2ed2f582..b89709af 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -43,7 +43,7 @@ describe('Integration Tests: smoke tests', function () { port: S3_PORT, address: '127.0.0.1', directory: path.resolve(S3_DIR), - // silent: true, + silent: true, }); await s3rver.run(); @@ -67,7 +67,6 @@ describe('Integration Tests: smoke tests', function () { let started = false; devServer.stdout.on('data', (data) => { const str = data.toString(); - console.log(str); if (str.includes('Ready on http://localhost') && !started) { started = true; resolve(); From 8de91ac4421359b0e20149866ec582fc2a0b545c Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 14:13:08 +0100 Subject: [PATCH 07/11] chore: adjust tests --- test/index.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/index.test.js b/test/index.test.js index 237bf60b..d32db98c 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -24,7 +24,7 @@ describe('fetch', () => { }); it('should return a response object for unknown', async () => { - const resp = await handler.fetch({ url: 'https://www.example.com', method: 'BLAH' }, {}); + const resp = await handler.fetch({ url: 'https://www.example.com/endpoint/repo/path/file.html', method: 'BLAH' }, {}); assert.strictEqual(resp.status, 405); }); @@ -67,7 +67,6 @@ describe('invalid routes', () => { }; it('return 400 for invalid paths', async () => { - await test('/', 400); await test('/source//org/repo/path/file.html', 400); await test('/source/org//repo/path/file.html', 400); await test('/source/org/repo//path/file.html', 400); From 127792f8f6ebb6ed7ba49fd104d15af6c28f1d33 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 14:13:28 +0100 Subject: [PATCH 08/11] fix: keep all tests green --- src/utils/daCtx.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/utils/daCtx.js b/src/utils/daCtx.js index b4f8640d..8d02d8c8 100644 --- a/src/utils/daCtx.js +++ b/src/utils/daCtx.js @@ -60,9 +60,14 @@ export default async function getDaCtx(req, env) { const keyBase = path.join('/'); const pnlc = pathname.toLocaleLowerCase(); - const validPath = `/${api}/${org}/${keyBase}`; - if (!org || !(pnlc === validPath || pnlc === `${validPath}/`)) { + let validPath = ''; + if (api) validPath += `/${api}`; + if (org) validPath += `/${org}`; + if (keyBase) validPath += `/${keyBase}`; + + if (!(pnlc === validPath || pnlc === `${validPath}/` || `${pnlc}/` === `${validPath}/` || `${pnlc}/` === validPath)) { + // if intent is not what is requested (+/- trailing slash), declare invalid throw new Error('Invalid path'); } From 8a7ba894b2cc526e15e7d24edfcd1f1f95c9c8eb Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 14:17:18 +0100 Subject: [PATCH 09/11] chore: simplify --- src/utils/daCtx.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/utils/daCtx.js b/src/utils/daCtx.js index 8d02d8c8..2964db7c 100644 --- a/src/utils/daCtx.js +++ b/src/utils/daCtx.js @@ -59,14 +59,16 @@ export default async function getDaCtx(req, env) { const path = parts.filter((part) => part !== ''); const keyBase = path.join('/'); + // Compare incoming pathname and intent we understand from it const pnlc = pathname.toLocaleLowerCase(); - let validPath = ''; if (api) validPath += `/${api}`; if (org) validPath += `/${org}`; if (keyBase) validPath += `/${keyBase}`; - if (!(pnlc === validPath || pnlc === `${validPath}/` || `${pnlc}/` === `${validPath}/` || `${pnlc}/` === validPath)) { + // Normalize paths by removing trailing slashes before comparison + const normalize = (p) => p.replace(/\/+$/, ''); + if (normalize(pnlc) !== normalize(validPath)) { // if intent is not what is requested (+/- trailing slash), declare invalid throw new Error('Invalid path'); } From 28facb39f4f203bd46daccab083a40059750c22e Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 14:22:18 +0100 Subject: [PATCH 10/11] chore: restore test --- test/index.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/index.test.js b/test/index.test.js index d32db98c..4d23c262 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -49,6 +49,11 @@ describe('fetch', () => { const resp = await hnd.fetch({ method: 'GET' }, {}); assert.strictEqual(resp.status, 403); }); + + it('return 404 for unknown get route', async () => { + const resp = await handler.fetch({ method: 'GET', url: 'http://www.example.com/' }, {}); + assert.strictEqual(resp.status, 404); + }); }); describe('invalid routes', () => { From b4896148434f2b56376735d1affd2364f96f4aea Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 14:26:47 +0100 Subject: [PATCH 11/11] chore: improve coverage --- test/index.test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/index.test.js b/test/index.test.js index 4d23c262..eeae9e4a 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -54,6 +54,19 @@ describe('fetch', () => { const resp = await handler.fetch({ method: 'GET', url: 'http://www.example.com/' }, {}); assert.strictEqual(resp.status, 404); }); + + it('should return 500 when getDaCtx throws unexpected error', async () => { + const hnd = await esmock('../src/index.js', { + '../src/utils/daCtx.js': { + default: async () => { + throw new Error('Unexpected ctx error'); + }, + }, + }); + + const resp = await hnd.fetch({ method: 'GET', url: 'http://www.example.com/source/org/repo/file.html' }, {}); + assert.strictEqual(resp.status, 500); + }); }); describe('invalid routes', () => {