From 831656b2ec887611708d50a35437c8c7e8ca8c14 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 17:12:58 +0100 Subject: [PATCH 01/32] chore: run DELETE to clean up the bucket --- test/it/smoke.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index b89709a..25e05e2 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -170,6 +170,21 @@ describe('Integration Tests: smoke tests', function () { assert.strictEqual(body, '

Page 3

'); }); + it('should delete an object via HTTP request', async () => { + const key = 'test-folder/page3'; + const ext = '.html'; + + const url = `${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`; + let resp = await fetch(url, { + method: 'DELETE', + }); + assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); + + // validate page is not here + resp = await fetch(`${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`); + assert.strictEqual(resp.status, 404, `Expected 404 Not Found, got ${resp.status}`); + }); + it('should logout via HTTP request', async () => { const url = `${SERVER_URL}/logout`; const resp = await fetch(url, { From b60b72434f6d213f26cffd53a2dc04c64b370d50 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 17:20:02 +0100 Subject: [PATCH 02/32] chore: extract it tests --- test/it/it-tests.js | 155 ++++++++++++++++++++++++++++++++++++++++++ test/it/smoke.test.js | 144 +-------------------------------------- 2 files changed, 158 insertions(+), 141 deletions(-) create mode 100644 test/it/it-tests.js diff --git a/test/it/it-tests.js b/test/it/it-tests.js new file mode 100644 index 0000000..328f9c1 --- /dev/null +++ b/test/it/it-tests.js @@ -0,0 +1,155 @@ +/* + * 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'; + +export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests', () => { + it('should get a object via HTTP request', async () => { + const pathname = 'test-folder/page1.html'; + + const url = `${SERVER_URL}/source/${ORG}/${REPO}/${pathname}`; + const resp = await fetch(url); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + + const body = await resp.text(); + assert.strictEqual(body, '

Page 1

'); + }); + + it('should list objects via HTTP request', async () => { + const key = 'test-folder'; + + const url = `${SERVER_URL}/list/${ORG}/${REPO}/${key}`; + const resp = await fetch(url); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + + const body = await resp.json(); + + const fileNames = body.map((item) => item.name); + assert.ok(fileNames.includes('page1'), 'Should list page1'); + assert.ok(fileNames.includes('page2'), 'Should list page2'); + }); + + it('should post an object via HTTP request', async () => { + const key = 'test-folder/page3'; + const ext = '.html'; + + // Create FormData with the HTML file + const formData = new FormData(); + const htmlBlob = new Blob(['

Page 3

'], { type: 'text/html' }); + const htmlFile = new File([htmlBlob], 'page3.html', { type: 'text/html' }); + formData.append('data', htmlFile); + + const url = `${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`; + let resp = await fetch(url, { + method: 'POST', + body: formData, + }); + + 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--${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}`); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + + body = await resp.text(); + assert.strictEqual(body, '

Page 3

'); + }); + + it('should delete an object via HTTP request', async () => { + const key = 'test-folder/page3'; + const ext = '.html'; + + const url = `${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`; + let resp = await fetch(url, { + method: 'DELETE', + }); + assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); + + // validate page is not here + resp = await fetch(`${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`); + assert.strictEqual(resp.status, 404, `Expected 404 Not Found, got ${resp.status}`); + }); + + 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('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}`); + }); + + 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/test/it/smoke.test.js b/test/it/smoke.test.js index 25e05e2..e15a473 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -10,12 +10,13 @@ * governing permissions and limitations under the License. */ /* eslint-disable prefer-arrow-callback, func-names */ -import assert from 'node:assert'; import S3rver from 's3rver'; import { spawn } from 'child_process'; import path from 'path'; import kill from 'tree-kill'; +import itTests from './it-tests.js'; + const S3_PORT = 4569; const SERVER_PORT = 8788; const SERVER_URL = `http://localhost:${SERVER_PORT}`; @@ -110,144 +111,5 @@ describe('Integration Tests: smoke tests', function () { } }); - it('should get a object via HTTP request', async () => { - const pathname = 'test-folder/page1.html'; - - const url = `${SERVER_URL}/source/${ORG}/${REPO}/${pathname}`; - const resp = await fetch(url); - - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); - - const body = await resp.text(); - assert.strictEqual(body, '

Page 1

'); - }); - - it('should list objects via HTTP request', async () => { - const key = 'test-folder'; - - const url = `${SERVER_URL}/list/${ORG}/${REPO}/${key}`; - const resp = await fetch(url); - - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); - - const body = await resp.json(); - - const fileNames = body.map((item) => item.name); - assert.ok(fileNames.includes('page1'), 'Should list page1'); - assert.ok(fileNames.includes('page2'), 'Should list page2'); - }); - - it('should post an object via HTTP request', async () => { - const key = 'test-folder/page3'; - const ext = '.html'; - - // Create FormData with the HTML file - const formData = new FormData(); - const htmlBlob = new Blob(['

Page 3

'], { type: 'text/html' }); - const htmlFile = new File([htmlBlob], 'page3.html', { type: 'text/html' }); - formData.append('data', htmlFile); - - const url = `${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`; - let resp = await fetch(url, { - method: 'POST', - body: formData, - }); - - 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--${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}`); - - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); - - body = await resp.text(); - assert.strictEqual(body, '

Page 3

'); - }); - - it('should delete an object via HTTP request', async () => { - const key = 'test-folder/page3'; - const ext = '.html'; - - const url = `${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`; - let resp = await fetch(url, { - method: 'DELETE', - }); - assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); - - // validate page is not here - resp = await fetch(`${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`); - assert.strictEqual(resp.status, 404, `Expected 404 Not Found, got ${resp.status}`); - }); - - 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('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}`); - }); - - 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}`); - }); + itTests(SERVER_URL, ORG, REPO); }); From 9fd0cd472320fb5a52c55ba39846a20af41113ed Mon Sep 17 00:00:00 2001 From: kptdobe Date: Thu, 4 Dec 2025 17:52:40 +0100 Subject: [PATCH 03/32] chore: sequential tests --- test/it/bucket/aem-content-local/.gitignore | 6 + test/it/bucket/aem-content-local/.gitkeep | 2 + .../test-folder.props._S3rver_metadata.json | 8 - .../test-folder.props._S3rver_object | 1 - .../test-folder.props._S3rver_object.md5 | 1 - .../page1.html._S3rver_metadata.json | 3 - .../test-folder/page1.html._S3rver_object | 1 - .../test-folder/page1.html._S3rver_object.md5 | 1 - .../page2.html._S3rver_metadata.json | 3 - .../test-folder/page2.html._S3rver_object | 1 - .../test-folder/page2.html._S3rver_object.md5 | 1 - test/it/it-tests.js | 141 ++++++++++++------ 12 files changed, 105 insertions(+), 64 deletions(-) create mode 100644 test/it/bucket/aem-content-local/.gitignore create mode 100644 test/it/bucket/aem-content-local/.gitkeep delete mode 100644 test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_metadata.json delete mode 100644 test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object delete mode 100644 test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object.md5 delete mode 100644 test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_metadata.json delete mode 100644 test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object delete mode 100644 test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object.md5 delete mode 100644 test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_metadata.json delete mode 100644 test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object delete mode 100644 test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object.md5 diff --git a/test/it/bucket/aem-content-local/.gitignore b/test/it/bucket/aem-content-local/.gitignore new file mode 100644 index 0000000..2d6e00e --- /dev/null +++ b/test/it/bucket/aem-content-local/.gitignore @@ -0,0 +1,6 @@ +# Ignore all S3rver generated files during integration tests +* +# But keep this directory in git +!.gitkeep +!.gitignore + diff --git a/test/it/bucket/aem-content-local/.gitkeep b/test/it/bucket/aem-content-local/.gitkeep new file mode 100644 index 0000000..72696af --- /dev/null +++ b/test/it/bucket/aem-content-local/.gitkeep @@ -0,0 +1,2 @@ +# This file ensures the empty directory is tracked by git +# S3rver will use this directory to store objects during integration tests \ No newline at end of file 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 deleted file mode 100644 index 3a48491..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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 deleted file mode 100644 index 9e26dfe..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object +++ /dev/null @@ -1 +0,0 @@ -{} \ 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 deleted file mode 100644 index a3f7110..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder.props._S3rver_object.md5 +++ /dev/null @@ -1 +0,0 @@ -99914b932bd37a50b983c5e7c90ae93b \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_metadata.json b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_metadata.json deleted file mode 100644 index 860d8e5..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_metadata.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "content-type": "text/html" -} \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object deleted file mode 100644 index cafecd1..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object +++ /dev/null @@ -1 +0,0 @@ -

Page 1

\ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object.md5 b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object.md5 deleted file mode 100644 index fbe1eba..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page1.html._S3rver_object.md5 +++ /dev/null @@ -1 +0,0 @@ -faf1fc7148f6811144bc58803c37cb7a \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_metadata.json b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_metadata.json deleted file mode 100644 index 860d8e5..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_metadata.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "content-type": "text/html" -} \ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object deleted file mode 100644 index 79ab2a5..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object +++ /dev/null @@ -1 +0,0 @@ -

Page 2

\ No newline at end of file diff --git a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object.md5 b/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object.md5 deleted file mode 100644 index 10a8e8f..0000000 --- a/test/it/bucket/aem-content-local/test-org/test-repo/test-folder/page2.html._S3rver_object.md5 +++ /dev/null @@ -1 +0,0 @@ -552f5670094f56c281f89df8a933514b \ No newline at end of file diff --git a/test/it/it-tests.js b/test/it/it-tests.js index 328f9c1..d2d2587 100644 --- a/test/it/it-tests.js +++ b/test/it/it-tests.js @@ -11,43 +11,47 @@ */ import assert from 'node:assert'; -export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests', () => { - it('should get a object via HTTP request', async () => { - const pathname = 'test-folder/page1.html'; +export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests', function () { + // Enable bail to stop on first failure - tests are interdependent + this.bail(true); - const url = `${SERVER_URL}/source/${ORG}/${REPO}/${pathname}`; - const resp = await fetch(url); - - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + it('delete root folder should cleanup the bucket', async () => { + const url = `${SERVER_URL}/source/${ORG}/${REPO}`; + const resp = await fetch(url, { + method: 'DELETE', + }); + assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); - const body = await resp.text(); - assert.strictEqual(body, '

Page 1

'); + // validate bucket is empty + const listResp = await fetch(`${SERVER_URL}/list/${ORG}/${REPO}`); + assert.strictEqual(listResp.status, 200, `Expected 200 OK, got ${listResp.status}`); + const listBody = await listResp.json(); + assert.strictEqual(listBody.length, 0, `Expected 0 items, got ${listBody.length}`); }); - it('should list objects via HTTP request', async () => { - const key = 'test-folder'; - - const url = `${SERVER_URL}/list/${ORG}/${REPO}/${key}`; - const resp = await fetch(url); - - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); - - const body = await resp.json(); + it('should create a repo via HTTP request', async () => { + const formData = new FormData(); + const blob = new Blob(['{}'], { type: 'application/json' }); + const file = new File([blob], `${REPO}.props`, { type: 'application/json' }); + formData.append('data', file); - const fileNames = body.map((item) => item.name); - assert.ok(fileNames.includes('page1'), 'Should list page1'); - assert.ok(fileNames.includes('page2'), 'Should list page2'); + const resp = await fetch(`${SERVER_URL}/source/${ORG}/${REPO}/${REPO}.props`, { + method: 'POST', + body: formData, + }); + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201 for marker, got ${resp.status}`); }); it('should post an object via HTTP request', async () => { - const key = 'test-folder/page3'; + // Now create the actual page + const key = 'test-folder/page1'; const ext = '.html'; // Create FormData with the HTML file const formData = new FormData(); - const htmlBlob = new Blob(['

Page 3

'], { type: 'text/html' }); - const htmlFile = new File([htmlBlob], 'page3.html', { type: 'text/html' }); - formData.append('data', htmlFile); + const blob = new Blob(['

Page 1

'], { type: 'text/html' }); + const file = new File([blob], 'page1.html', { type: 'text/html' }); + formData.append('data', file); const url = `${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`; let resp = await fetch(url, { @@ -69,31 +73,35 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); body = await resp.text(); - assert.strictEqual(body, '

Page 3

'); - }); - - it('should delete an object via HTTP request', async () => { - const key = 'test-folder/page3'; - const ext = '.html'; + assert.strictEqual(body, '

Page 1

'); - const url = `${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`; - let resp = await fetch(url, { - method: 'DELETE', + // create another page + const key2 = 'test-folder/page2'; + const ext2 = '.html'; + const formData2 = new FormData(); + const htmlBlob2 = new Blob(['

Page 2

'], { type: 'text/html' }); + const htmlFile2 = new File([htmlBlob2], 'page2.html', { type: 'text/html' }); + formData2.append('data', htmlFile2); + resp = await fetch(`${SERVER_URL}/source/${ORG}/${REPO}/${key2}${ext2}`, { + method: 'POST', + body: formData2, }); - assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); - - // validate page is not here - resp = await fetch(`${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`); - assert.strictEqual(resp.status, 404, `Expected 404 Not Found, got ${resp.status}`); + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); }); - it('should logout via HTTP request', async () => { - const url = `${SERVER_URL}/logout`; - const resp = await fetch(url, { - method: 'POST', - }); + it('should list objects via HTTP request', async () => { + const key = 'test-folder'; + + const url = `${SERVER_URL}/list/${ORG}/${REPO}/${key}`; + const resp = await fetch(url); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + + const body = await resp.json(); + + const fileNames = body.map((item) => item.name); + assert.ok(fileNames.includes('page1'), 'Should list page1'); + assert.ok(fileNames.includes('page2'), 'Should list page2'); }); it('should list repos via HTTP request', async () => { @@ -107,6 +115,21 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' assert.strictEqual(body[0].name, REPO, `Expected ${REPO}, got ${body[0].name}`); }); + it('should delete an object via HTTP request', async () => { + const key = 'test-folder/page2'; + const ext = '.html'; + + const url = `${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`; + let resp = await fetch(url, { + method: 'DELETE', + }); + assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); + + // validate page is not here + resp = await fetch(`${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`); + assert.strictEqual(resp.status, 404, `Expected 404 Not Found, got ${resp.status}`); + }); + it('should deal with no config found via HTTP request', async () => { const url = `${SERVER_URL}/config/${ORG}`; const resp = await fetch(url); @@ -114,6 +137,14 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' assert.strictEqual(resp.status, 404, `Expected 404, got ${resp.status}`); }); + it('should delete root folder', async () => { + const url = `${SERVER_URL}/source/${ORG}/${REPO}`; + const resp = await fetch(url, { + method: 'DELETE', + }); + assert.strictEqual(resp.status, 204, `Previous test should have logged out, 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({ @@ -152,4 +183,26 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' 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}`); }); + + it('cannot recreate root folder because of auth (previous test should setup auth)', async () => { + const formData = new FormData(); + const blob = new Blob(['{}'], { type: 'application/json' }); + const file = new File([blob], `${REPO}.props`, { type: 'application/json' }); + formData.append('data', file); + + const resp = await fetch(`${SERVER_URL}/source/${ORG}/${REPO}/${REPO}.props`, { + method: 'POST', + body: formData, + }); + assert.strictEqual(resp.status, 401, `Previous test should have setup auth, got ${resp.status}`); + }); + + 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}`); + }); }); From fa8bb0dff87b3d585ce03c2bd9fda2845b1b5032 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 8 Dec 2025 14:01:13 +0100 Subject: [PATCH 04/32] chore: linting --- test/it/it-tests.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/it/it-tests.js b/test/it/it-tests.js index d2d2587..2f9b58b 100644 --- a/test/it/it-tests.js +++ b/test/it/it-tests.js @@ -11,6 +11,7 @@ */ import assert from 'node:assert'; +// eslint-disable-next-line func-names export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests', function () { // Enable bail to stop on first failure - tests are interdependent this.bail(true); From a5f97840aff01223fcded8509090df4a8caea484 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 8 Dec 2025 15:54:43 +0100 Subject: [PATCH 05/32] chore: postdeploy tests --- .gitignore | 2 + deploy-ci-version.sh | 64 +++++ package-lock.json | 573 ++++++++++++++++++++++-------------------- package.json | 8 +- prepare-deploy.js | 3 + test/it/it-tests.js | 19 +- test/it/smoke.test.js | 109 ++++---- 7 files changed, 455 insertions(+), 323 deletions(-) create mode 100755 deploy-ci-version.sh diff --git a/.gitignore b/.gitignore index ecf6a87..81a8a1f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ coverage # Wrangler temp .wrangler +wrangler-versioned.toml +.deployment-env # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/deploy-ci-version.sh b/deploy-ci-version.sh new file mode 100755 index 0000000..7b71271 --- /dev/null +++ b/deploy-ci-version.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# 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. + +set -e + +ENVIRONMENT="ci" + +# Prepare version and capture version +echo "Preparing version for environment: $ENVIRONMENT" +VERSION=$(node prepare-deploy.js) + +if [ -z "$VERSION" ]; then + echo "Error: Failed to get version from prepare-deploy.js" + exit 1 +fi + +# Get current git branch name +BRANCH=$(git rev-parse --abbrev-ref HEAD) + +if [ -z "$BRANCH" ]; then + echo "Error: Failed to get git branch name" + exit 1 +fi + +echo "Creating version: $VERSION from branch: $BRANCH" + +# Deploy with branch tag and version message and capture output +# wrangler deploy -e "$ENVIRONMENT" -c wrangler-versioned.toml --message "v$VERSION" --tag "$BRANCH" +OUTPUT=$(wrangler versions upload -e "$ENVIRONMENT" -c wrangler-versioned.toml --message "$ENVIRONMENT: v$VERSION - branch: $BRANCH" --tag "$BRANCH" 2>&1) + +# Display the output +echo "$OUTPUT" + +# Parse the deployment information +WORKER_VERSION_ID=$(echo "$OUTPUT" | grep "Worker Version ID:" | sed 's/.*Worker Version ID: //') +VERSION_PREVIEW_URL=$(echo "$OUTPUT" | grep "Version Preview URL:" | sed 's/.*Version Preview URL: //') + +# Write to a file that can be sourced +cat > .deployment-env << EOF +export WORKER_VERSION_ID="$WORKER_VERSION_ID" +export VERSION_PREVIEW_URL="$VERSION_PREVIEW_URL" +export VERSION_PREVIEW_ORG="ci-test-org-$BRANCH" +EOF + +# probably useless... +wrangler versions deploy -y -e ci --version-id $WORKER_VERSION_ID + +echo "" +echo "Version deployment complete!" +echo "----------------------------------------" +echo "Deployment information:" +echo "WORKER_VERSION_ID=$WORKER_VERSION_ID" +echo "VERSION_PREVIEW_URL=$VERSION_PREVIEW_URL" +echo "----------------------------------------" + diff --git a/package-lock.json b/package-lock.json index 939fd37..17151af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,8 @@ "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-s3": "3.726.1", - "@aws-sdk/s3-request-presigner": "3.468.0", - "@cloudflare/workers-types": "4.20251126.0", + "@aws-sdk/s3-request-presigner": "^3.468.0", + "@cloudflare/workers-types": "4.20251205.0", "@ssttevee/cfw-formdata-polyfill": "0.2.1", "jose": "6.0.10" }, @@ -33,7 +33,7 @@ "semantic-release-slack-bot": "4.0.2", "sinon": "21.0.0", "tree-kill": "1.2.2", - "wrangler": "4.10.0" + "wrangler": "4.53.0" } }, "node_modules/@actions/core": { @@ -1556,9 +1556,9 @@ "license": "MIT" }, "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", - "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.1.tgz", + "integrity": "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { @@ -1569,14 +1569,14 @@ } }, "node_modules/@cloudflare/unenv-preset": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.1.tgz", - "integrity": "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==", + "version": "2.7.13", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.13.tgz", + "integrity": "sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==", "dev": true, "license": "MIT OR Apache-2.0", "peerDependencies": { - "unenv": "2.0.0-rc.15", - "workerd": "^1.20250320.0" + "unenv": "2.0.0-rc.24", + "workerd": "^1.20251202.0" }, "peerDependenciesMeta": { "workerd": { @@ -1585,9 +1585,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20250409.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250409.0.tgz", - "integrity": "sha512-smA9yq77xsdQ1NMLhFz3JZxMHGd01lg0bE+X3dTFmIUs+hHskJ+HJ/IkMFInkCCeEFlUkoL4yO7ilaU/fin/xA==", + "version": "1.20251202.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251202.0.tgz", + "integrity": "sha512-/uvEAWEukTWb1geHhbjGUeZqcSSSyYzp0mvoPUBl+l0ont4NVGao3fgwM0q8wtKvgoKCHSG6zcG23wj9Opj3Nw==", "cpu": [ "x64" ], @@ -1602,9 +1602,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20250409.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250409.0.tgz", - "integrity": "sha512-oLVcf+Y5Qun8JHcy1VcR/YnbA5U2ne0czh3XNhDqdHZFK8+vKeC7MnVPX+kEqQA3+uLcMM1/FsIDU1U4Na0h1g==", + "version": "1.20251202.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20251202.0.tgz", + "integrity": "sha512-f52xRvcI9cWRd6400EZStRtXiRC5XKEud7K5aFIbbUv0VeINltujFQQ9nHWtsF6g1quIXWkjhh5u01gPAYNNXA==", "cpu": [ "arm64" ], @@ -1619,9 +1619,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20250409.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250409.0.tgz", - "integrity": "sha512-D31B4kdC3a0RD5yfpdIa89//kGHbYsYihZmejm1k4S4NHOho3MUDHAEh4aHtafQNXbZdydGHlSyiVYjTdQ9ILQ==", + "version": "1.20251202.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20251202.0.tgz", + "integrity": "sha512-HYXinF5RBH7oXbsFUMmwKCj+WltpYbf5mRKUBG5v3EuPhUjSIFB84U+58pDyfBJjcynHdy3EtvTWcvh/+lcgow==", "cpu": [ "x64" ], @@ -1636,9 +1636,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20250409.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250409.0.tgz", - "integrity": "sha512-Sr59P0TREayil5OQ7kcbjuIn6L6OTSRLI91LKu0D8vi1hss2q9FUwBcwxg0+Yd/x+ty/x7IISiAK5QBkAMeITQ==", + "version": "1.20251202.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20251202.0.tgz", + "integrity": "sha512-++L02Jdoxz7hEA9qDaQjbVU1RzQS+S+eqIi22DkPe2Tgiq2M3UfNpeu+75k5L9DGRIkZPYvwMBMbcmKvQqdIIg==", "cpu": [ "arm64" ], @@ -1653,9 +1653,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20250409.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250409.0.tgz", - "integrity": "sha512-dK9I8zBX5rR7MtaaP2AhICQTEw3PVzHcsltN8o46w7JsbYlMvFOj27FfYH5dhs3IahgmIfw2e572QXW2o/dbpg==", + "version": "1.20251202.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20251202.0.tgz", + "integrity": "sha512-gzeU6eDydTi7ib+Q9DD/c0hpXtqPucnHk2tfGU03mljPObYxzMkkPGgB5qxpksFvub3y4K0ChjqYxGJB4F+j3g==", "cpu": [ "x64" ], @@ -1670,9 +1670,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "4.20251126.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20251126.0.tgz", - "integrity": "sha512-DSeI1Q7JYmh5/D/tw5eZCjrKY34v69rwj63hHt60nSQW5QLwWCbj/lLtNz9f2EPa+JCACwpLXHgCXfzJ29x66w==", + "version": "4.20251205.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20251205.0.tgz", + "integrity": "sha512-7pup7fYkuQW5XD8RUS/vkxF9SXlrGyCXuZ4ro3uVQvca/GTeSa+8bZ8T4wbq1Aea5lmLIGSlKbhl2msME7bRBA==", "license": "MIT OR Apache-2.0", "peer": true }, @@ -1758,9 +1758,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", "cpu": [ "ppc64" ], @@ -1775,9 +1775,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", "cpu": [ "arm" ], @@ -1792,9 +1792,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", "cpu": [ "arm64" ], @@ -1809,9 +1809,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", "cpu": [ "x64" ], @@ -1826,9 +1826,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", "cpu": [ "arm64" ], @@ -1843,9 +1843,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", "cpu": [ "x64" ], @@ -1860,9 +1860,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", "cpu": [ "arm64" ], @@ -1877,9 +1877,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", "cpu": [ "x64" ], @@ -1894,9 +1894,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", "cpu": [ "arm" ], @@ -1911,9 +1911,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", "cpu": [ "arm64" ], @@ -1928,9 +1928,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", "cpu": [ "ia32" ], @@ -1945,9 +1945,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", "cpu": [ "loong64" ], @@ -1962,9 +1962,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", "cpu": [ "mips64el" ], @@ -1979,9 +1979,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", "cpu": [ "ppc64" ], @@ -1996,9 +1996,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", "cpu": [ "riscv64" ], @@ -2013,9 +2013,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", "cpu": [ "s390x" ], @@ -2030,9 +2030,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", "cpu": [ "x64" ], @@ -2047,9 +2047,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", "cpu": [ "arm64" ], @@ -2064,9 +2064,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", "cpu": [ "x64" ], @@ -2081,9 +2081,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", "cpu": [ "arm64" ], @@ -2098,9 +2098,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", "cpu": [ "x64" ], @@ -2114,10 +2114,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", "cpu": [ "x64" ], @@ -2132,9 +2149,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", "cpu": [ "arm64" ], @@ -2149,9 +2166,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", "cpu": [ "ia32" ], @@ -2166,9 +2183,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", "cpu": [ "x64" ], @@ -3431,6 +3448,48 @@ "node": ">=12" } }, + "node_modules/@poppinss/colors": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.5.tgz", + "integrity": "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/dumper/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.2.tgz", + "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==", + "dev": true, + "license": "MIT" + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -4292,6 +4351,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/is": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.1.tgz", + "integrity": "sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", @@ -5096,6 +5168,13 @@ "text-hex": "1.0.x" } }, + "node_modules/@speed-highlight/core": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.12.tgz", + "integrity": "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/@ssttevee/blob-ponyfill": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@ssttevee/blob-ponyfill/-/blob-ponyfill-0.1.0.tgz", @@ -5592,16 +5671,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/as-table": { - "version": "1.0.55", - "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", - "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "printable-characters": "^1.0.42" - } - }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -6828,13 +6897,6 @@ "dev": true, "license": "MIT" }, - "node_modules/data-uri-to-buffer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", - "dev": true, - "license": "MIT" - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -6986,13 +7048,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "dev": true, - "license": "MIT" - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7037,7 +7092,6 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -7431,6 +7485,16 @@ "dev": true, "license": "MIT" }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -7588,9 +7652,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7601,31 +7665,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" } }, "node_modules/escalade": { @@ -8039,13 +8104,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "dev": true, - "license": "MIT" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -8593,17 +8651,6 @@ "node": ">= 0.4" } }, - "node_modules/get-source": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", - "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", - "dev": true, - "license": "Unlicense", - "dependencies": { - "data-uri-to-buffer": "^2.0.0", - "source-map": "^0.6.1" - } - }, "node_modules/get-stream": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", @@ -9246,8 +9293,7 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", @@ -10079,6 +10125,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/koa": { "version": "2.16.3", "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.3.tgz", @@ -11240,9 +11296,9 @@ } }, "node_modules/miniflare": { - "version": "4.20250409.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250409.0.tgz", - "integrity": "sha512-Hu02dYZvFR+MyrI57O6rSrOUTofcO9EIvcodgq2SAHzAeWSJw2E0oq9lylOrcckFwPMcwxUAb/cQN1LIoCyySw==", + "version": "4.20251202.1", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20251202.1.tgz", + "integrity": "sha512-cRp2QNgnt9wpLMoNs4MOzzomyfe9UTS9sPRxIpUvxMl+mweCZ0FHpWWQvCnU7wWlfAP8VGZrHwqSsV5ERA6ahQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11251,11 +11307,12 @@ "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", + "sharp": "^0.33.5", "stoppable": "1.1.0", - "undici": "^5.28.5", - "workerd": "1.20250409.0", + "undici": "7.14.0", + "workerd": "1.20251202.0", "ws": "8.18.0", - "youch": "3.3.4", + "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { @@ -11279,16 +11336,13 @@ } }, "node_modules/miniflare/node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.14.0.tgz", + "integrity": "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==", "dev": true, "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { - "node": ">=14.0" + "node": ">=20.18.1" } }, "node_modules/miniflare/node_modules/ws": { @@ -11574,16 +11628,6 @@ "dev": true, "license": "MIT" }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "dev": true, - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -14145,13 +14189,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "dev": true, - "license": "MIT" - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -14851,13 +14888,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/printable-characters": { - "version": "1.0.42", - "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", - "dev": true, - "license": "Unlicense" - }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -16099,7 +16129,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", @@ -16139,7 +16168,6 @@ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" @@ -16154,7 +16182,6 @@ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -16444,7 +16471,6 @@ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "is-arrayish": "^0.3.1" } @@ -16663,17 +16689,6 @@ "node": "*" } }, - "node_modules/stacktracey": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", - "dev": true, - "license": "Unlicense", - "dependencies": { - "as-table": "^1.0.36", - "get-source": "^2.0.12" - } - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -17671,13 +17686,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -17729,18 +17737,14 @@ "license": "MIT" }, "node_modules/unenv": { - "version": "2.0.0-rc.15", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.15.tgz", - "integrity": "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==", + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "defu": "^6.1.4", - "exsolve": "^1.0.4", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "ufo": "^1.5.4" + "pathe": "^2.0.3" } }, "node_modules/unicode-emoji-modifier-base": { @@ -18193,12 +18197,13 @@ "license": "MIT" }, "node_modules/workerd": { - "version": "1.20250409.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250409.0.tgz", - "integrity": "sha512-hqjX9swiHvrkOI3jlH9lrZsZRRv9lddUwcMe8Ua76jnyQz+brybWznNjHu8U5oswwcrFwvky1A4CcLjcLY31gQ==", + "version": "1.20251202.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20251202.0.tgz", + "integrity": "sha512-p08YfrUMHkjCECNdT36r+6DpJIZX4kixbZ4n6GMUcLR5Gh18fakSCsiQrh72iOm4M9QHv/rM7P8YvCrUPWT5sg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "bin": { "workerd": "bin/workerd" }, @@ -18206,11 +18211,11 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20250409.0", - "@cloudflare/workerd-darwin-arm64": "1.20250409.0", - "@cloudflare/workerd-linux-64": "1.20250409.0", - "@cloudflare/workerd-linux-arm64": "1.20250409.0", - "@cloudflare/workerd-windows-64": "1.20250409.0" + "@cloudflare/workerd-darwin-64": "1.20251202.0", + "@cloudflare/workerd-darwin-arm64": "1.20251202.0", + "@cloudflare/workerd-linux-64": "1.20251202.0", + "@cloudflare/workerd-linux-arm64": "1.20251202.0", + "@cloudflare/workerd-windows-64": "1.20251202.0" } }, "node_modules/workerpool": { @@ -18221,34 +18226,33 @@ "license": "Apache-2.0" }, "node_modules/wrangler": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.10.0.tgz", - "integrity": "sha512-fTE4hZ79msEUt8+HEjl/8Q72haCyzPLu4PgrU3L81ysmjrMEdiYfUPqnvCkBUVtJvrDNdctTEimkufT1Y0ipNg==", + "version": "4.53.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.53.0.tgz", + "integrity": "sha512-/wvnHlRnlHsqaeIgGbmcEJE5NFYdTUWHCKow+U5Tv2XwQXI9vXUqBwCLAGy/BwqyS5nnycRt2kppqCzgHgyb7Q==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { - "@cloudflare/kv-asset-handler": "0.4.0", - "@cloudflare/unenv-preset": "2.3.1", + "@cloudflare/kv-asset-handler": "0.4.1", + "@cloudflare/unenv-preset": "2.7.13", "blake3-wasm": "2.1.5", - "esbuild": "0.24.2", - "miniflare": "4.20250409.0", + "esbuild": "0.27.0", + "miniflare": "4.20251202.1", "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.15", - "workerd": "1.20250409.0" + "unenv": "2.0.0-rc.24", + "workerd": "1.20251202.0" }, "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "optionalDependencies": { - "fsevents": "~2.3.2", - "sharp": "^0.33.5" + "fsevents": "~2.3.2" }, "peerDependencies": { - "@cloudflare/workers-types": "^4.20250409.0" + "@cloudflare/workers-types": "^4.20251202.0" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { @@ -18537,15 +18541,42 @@ } }, "node_modules/youch": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", - "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", "dev": true, "license": "MIT", "dependencies": { - "cookie": "^0.7.1", - "mustache": "^4.2.0", - "stacktracey": "^2.1.8" + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/youch/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/zod": { diff --git a/package.json b/package.json index 29f78f4..7a8a346 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,9 @@ "test": "c8 mocha --spec=test/**/*.test.js --ignore=test/it/**/*.test.js", "test:it": "mocha test/it/**/*.test.js", "test:all": "npm run test && npm run test:it", - "test:postdeploy": "echo 'todo'", + "test:postdeploy": "source .deployment-env && npm run test:it", "dev": "wrangler dev -e dev", - "deploy:ci": "node prepare-deploy.js && wrangler deploy -e ci -c wrangler-versioned.toml", + "deploy:ci": "./deploy-ci-version.sh ci", "deploy:prod": "node prepare-deploy.js && wrangler deploy -e production -c wrangler-versioned.toml", "deploy:stage": "node prepare-deploy.js && wrangler deploy -e stage -c wrangler-versioned.toml", "start": "wrangler dev", @@ -44,7 +44,7 @@ "semantic-release-slack-bot": "4.0.2", "sinon": "21.0.0", "tree-kill": "1.2.2", - "wrangler": "4.10.0" + "wrangler": "4.53.0" }, "lint-staged": { "*.js": "eslint", @@ -53,7 +53,7 @@ "dependencies": { "@aws-sdk/client-s3": "3.726.1", "@aws-sdk/s3-request-presigner": "^3.468.0", - "@cloudflare/workers-types": "4.20251126.0", + "@cloudflare/workers-types": "4.20251205.0", "@ssttevee/cfw-formdata-polyfill": "0.2.1", "jose": "6.0.10" } diff --git a/prepare-deploy.js b/prepare-deploy.js index 9b16498..838a643 100644 --- a/prepare-deploy.js +++ b/prepare-deploy.js @@ -23,6 +23,9 @@ try { const { version } = JSON.parse(await readFile(resolve(__dirname, 'package.json'))); toml = toml.replaceAll('@@VERSION@@', version); await writeFile(resolve(__dirname, 'wrangler-versioned.toml'), toml, 'utf-8'); + + // Export version for use in deploy scripts + console.log(version); } catch (e) { console.error(e); process.exitCode = 1; diff --git a/test/it/it-tests.js b/test/it/it-tests.js index 2f9b58b..dc98cdf 100644 --- a/test/it/it-tests.js +++ b/test/it/it-tests.js @@ -12,12 +12,14 @@ import assert from 'node:assert'; // eslint-disable-next-line func-names -export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests', function () { +export default (ctx) => describe('Integration Tests: it tests', function () { // Enable bail to stop on first failure - tests are interdependent this.bail(true); it('delete root folder should cleanup the bucket', async () => { + const { SERVER_URL, ORG, REPO } = ctx; const url = `${SERVER_URL}/source/${ORG}/${REPO}`; + console.log('url', url); const resp = await fetch(url, { method: 'DELETE', }); @@ -31,6 +33,7 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' }); it('should create a repo via HTTP request', async () => { + const { SERVER_URL, ORG, REPO } = ctx; const formData = new FormData(); const blob = new Blob(['{}'], { type: 'application/json' }); const file = new File([blob], `${REPO}.props`, { type: 'application/json' }); @@ -44,6 +47,7 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' }); it('should post an object via HTTP request', async () => { + const { SERVER_URL, ORG, REPO } = ctx; // Now create the actual page const key = 'test-folder/page1'; const ext = '.html'; @@ -91,6 +95,7 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' }); it('should list objects via HTTP request', async () => { + const { SERVER_URL, ORG, REPO } = ctx; const key = 'test-folder'; const url = `${SERVER_URL}/list/${ORG}/${REPO}/${key}`; @@ -106,6 +111,7 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' }); it('should list repos via HTTP request', async () => { + const { SERVER_URL, ORG, REPO } = ctx; const url = `${SERVER_URL}/list/${ORG}`; const resp = await fetch(url); @@ -117,6 +123,7 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' }); it('should delete an object via HTTP request', async () => { + const { SERVER_URL, ORG, REPO } = ctx; const key = 'test-folder/page2'; const ext = '.html'; @@ -132,6 +139,7 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' }); it('should deal with no config found via HTTP request', async () => { + const { SERVER_URL, ORG } = ctx; const url = `${SERVER_URL}/config/${ORG}`; const resp = await fetch(url); @@ -139,6 +147,7 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' }); it('should delete root folder', async () => { + const { SERVER_URL, ORG, REPO } = ctx; const url = `${SERVER_URL}/source/${ORG}/${REPO}`; const resp = await fetch(url, { method: 'DELETE', @@ -146,7 +155,9 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' assert.strictEqual(resp.status, 204, `Previous test should have logged out, got ${resp.status}`); }); - it('should post and get org config via HTTP request', async () => { + // TODO: setting the config works well but then we cannot edit anything without logging. + it.skip('should post and get org config via HTTP request', async () => { + const { SERVER_URL, ORG } = ctx; // First POST the config - must include CONFIG write permission const configData = JSON.stringify({ total: 2, @@ -185,7 +196,8 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' assert.strictEqual(body.data[1].value, 'test-value', `Expected test-value, got ${body.data[1].value}`); }); - it('cannot recreate root folder because of auth (previous test should setup auth)', async () => { + it.skip('cannot recreate root folder because of auth (previous test should setup auth)', async () => { + const { SERVER_URL, ORG, REPO } = ctx; const formData = new FormData(); const blob = new Blob(['{}'], { type: 'application/json' }); const file = new File([blob], `${REPO}.props`, { type: 'application/json' }); @@ -199,6 +211,7 @@ export default (SERVER_URL, ORG, REPO) => describe('Integration Tests: it tests' }); it('should logout via HTTP request', async () => { + const { SERVER_URL } = ctx; const url = `${SERVER_URL}/logout`; const resp = await fetch(url, { method: 'POST', diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index e15a473..fde6fe3 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -19,70 +19,89 @@ import itTests from './it-tests.js'; const S3_PORT = 4569; const SERVER_PORT = 8788; -const SERVER_URL = `http://localhost:${SERVER_PORT}`; +const LOCAL_SERVER_URL = `http://localhost:${SERVER_PORT}`; const S3_DIR = './test/it/bucket'; -const ORG = 'test-org'; +const LOCAL_ORG = 'test-org'; const REPO = 'test-repo'; describe('Integration Tests: smoke tests', function () { let s3rver; let devServer; + const context = { + SERVER_URL: LOCAL_SERVER_URL, + ORG: LOCAL_ORG, + REPO, + }; + 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 }); - } + if (process.env.VERSION_PREVIEW_URL) { + context.SERVER_URL = process.env.VERSION_PREVIEW_URL; + context.ORG = process.env.VERSION_PREVIEW_ORG; + } else { + // local testing, start the server - s3rver = new S3rver({ - port: S3_PORT, - address: '127.0.0.1', - directory: path.resolve(S3_DIR), - silent: true, - }); - await s3rver.run(); - - devServer = spawn('npx', [ - 'wrangler', 'dev', - '--port', SERVER_PORT.toString(), - '--env', 'it', - '--var', 'S3_DEF_URL:http://localhost:4569', - '--var', 'S3_ACCESS_KEY_ID:S3RVER', - '--var', 'S3_SECRET_ACCESS_KEY:S3RVER', - '--var', 'S3_FORCE_PATH_STYLE:true', - '--var', 'IMS_ORIGIN:http://localhost:9999', - '--var', 'AEM_ADMIN_MEDIA_API_KEY:test-key', - ], { - stdio: 'pipe', // Capture output for debugging - detached: false, // Keep in same process group for easier cleanup - }); - - // Wait for server to be ready - await new Promise((resolve, reject) => { - let started = false; - devServer.stdout.on('data', (data) => { - const str = data.toString(); - if (str.includes('Ready on http://localhost') && !started) { - started = true; - resolve(); - } + // 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', + directory: path.resolve(S3_DIR), + silent: true, }); + await s3rver.run(); + + devServer = spawn('npx', [ + 'wrangler', 'dev', + '--port', SERVER_PORT.toString(), + '--env', 'it', + '--var', 'S3_DEF_URL:http://localhost:4569', + '--var', 'S3_ACCESS_KEY_ID:S3RVER', + '--var', 'S3_SECRET_ACCESS_KEY:S3RVER', + '--var', 'S3_FORCE_PATH_STYLE:true', + '--var', 'IMS_ORIGIN:http://localhost:9999', + '--var', 'AEM_ADMIN_MEDIA_API_KEY:test-key', + ], { + stdio: 'pipe', // Capture output for debugging + detached: false, // Keep in same process group for easier cleanup + }); + + // Wait for server to be ready + await new Promise((resolve, reject) => { + let started = false; + devServer.stdout.on('data', (data) => { + const str = data.toString(); + if (str.includes('Ready on http://localhost') && !started) { + started = true; + resolve(); + } + }); + + devServer.stderr.on('data', (data) => { + console.error('[Wrangler Err]', data.toString()); + }); - devServer.stderr.on('data', (data) => { - console.error('[Wrangler Err]', data.toString()); + devServer.on('error', reject); }); + } - devServer.on('error', reject); - }); + console.log('CONTEXT', context); }); after(async function () { + if (process.env.VERSION_PREVIEW_URL) { + return; + } + this.timeout(10000); // Cleanup - forcefully kill processes if (devServer && devServer.pid) { @@ -111,5 +130,5 @@ describe('Integration Tests: smoke tests', function () { } }); - itTests(SERVER_URL, ORG, REPO); + itTests(context); }); From 629b6c7cc36eec34a02ffdf3a1df150c6ab174f2 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 8 Dec 2025 15:58:22 +0100 Subject: [PATCH 06/32] chore: posix compliant source --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7a8a346..ebf6c13 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test": "c8 mocha --spec=test/**/*.test.js --ignore=test/it/**/*.test.js", "test:it": "mocha test/it/**/*.test.js", "test:all": "npm run test && npm run test:it", - "test:postdeploy": "source .deployment-env && npm run test:it", + "test:postdeploy": ". .deployment-env && npm run test:it && rm .deployment-env", "dev": "wrangler dev -e dev", "deploy:ci": "./deploy-ci-version.sh ci", "deploy:prod": "node prepare-deploy.js && wrangler deploy -e production -c wrangler-versioned.toml", From 65bf3b3cd5f2f5135e6f3a6ef636bafce97e929d Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 8 Dec 2025 16:05:20 +0100 Subject: [PATCH 07/32] chore: multi env comp --- deploy-ci-version.sh | 15 ++++++++++++--- package.json | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/deploy-ci-version.sh b/deploy-ci-version.sh index 7b71271..e83eea3 100755 --- a/deploy-ci-version.sh +++ b/deploy-ci-version.sh @@ -44,15 +44,23 @@ echo "$OUTPUT" WORKER_VERSION_ID=$(echo "$OUTPUT" | grep "Worker Version ID:" | sed 's/.*Worker Version ID: //') VERSION_PREVIEW_URL=$(echo "$OUTPUT" | grep "Version Preview URL:" | sed 's/.*Version Preview URL: //') -# Write to a file that can be sourced +# Write to a file that can be sourced (for local use) cat > .deployment-env << EOF export WORKER_VERSION_ID="$WORKER_VERSION_ID" export VERSION_PREVIEW_URL="$VERSION_PREVIEW_URL" export VERSION_PREVIEW_ORG="ci-test-org-$BRANCH" EOF -# probably useless... -wrangler versions deploy -y -e ci --version-id $WORKER_VERSION_ID +# If running in GitHub Actions, also write to GITHUB_ENV +if [ -n "$GITHUB_ENV" ]; then + echo "WORKER_VERSION_ID=$WORKER_VERSION_ID" >> "$GITHUB_ENV" + echo "VERSION_PREVIEW_URL=$VERSION_PREVIEW_URL" >> "$GITHUB_ENV" + echo "VERSION_PREVIEW_ORG=ci-test-org-$BRANCH" >> "$GITHUB_ENV" + echo "Variables exported to GitHub Actions environment" +fi + +# Deploy the version +wrangler versions deploy -y -e ci --version-id "$WORKER_VERSION_ID" echo "" echo "Version deployment complete!" @@ -60,5 +68,6 @@ echo "----------------------------------------" echo "Deployment information:" echo "WORKER_VERSION_ID=$WORKER_VERSION_ID" echo "VERSION_PREVIEW_URL=$VERSION_PREVIEW_URL" +echo "VERSION_PREVIEW_ORG=ci-test-org-$BRANCH" echo "----------------------------------------" diff --git a/package.json b/package.json index ebf6c13..379ef90 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test": "c8 mocha --spec=test/**/*.test.js --ignore=test/it/**/*.test.js", "test:it": "mocha test/it/**/*.test.js", "test:all": "npm run test && npm run test:it", - "test:postdeploy": ". .deployment-env && npm run test:it && rm .deployment-env", + "test:postdeploy": "if [ -f .deployment-env ]; then . ./.deployment-env && rm .deployment-env; fi && npm run test:it", "dev": "wrangler dev -e dev", "deploy:ci": "./deploy-ci-version.sh ci", "deploy:prod": "node prepare-deploy.js && wrangler deploy -e production -c wrangler-versioned.toml", From 22c2af4c4579b416e99a910bf38084db650438a0 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 8 Dec 2025 16:20:04 +0100 Subject: [PATCH 08/32] chore: use anonymous --- test/it/it-tests.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/it/it-tests.js b/test/it/it-tests.js index dc98cdf..64ca0a6 100644 --- a/test/it/it-tests.js +++ b/test/it/it-tests.js @@ -155,8 +155,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.strictEqual(resp.status, 204, `Previous test should have logged out, got ${resp.status}`); }); - // TODO: setting the config works well but then we cannot edit anything without logging. - it.skip('should post and get org config via HTTP request', async () => { + it('should post and get org config via HTTP request', async () => { const { SERVER_URL, ORG } = ctx; // First POST the config - must include CONFIG write permission const configData = JSON.stringify({ @@ -164,8 +163,8 @@ export default (ctx) => describe('Integration Tests: it tests', function () { limit: 2, offset: 0, data: [ - { path: 'CONFIG', actions: 'write', groups: 'anonymous' }, - { key: 'admin.role.all', value: 'test-value' }, + { path: 'CONFIG', groups: 'anonymous', actions: 'write' }, + { path: '/+**', groups: 'anonymous', actions: 'write' }, ], ':type': 'sheet', ':sheetname': 'permissions', @@ -191,12 +190,14 @@ export default (ctx) => describe('Integration Tests: it tests', function () { 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].groups, 'anonymous', `Expected anonymous, got ${body.data[0].groups}`); 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}`); + assert.strictEqual(body.data[1].path, '/+**', `Expected /+**, got ${body.data[1].path}`); + assert.strictEqual(body.data[1].groups, 'anonymous', `Expected anonymous, got ${body.data[1].groups}`); + assert.strictEqual(body.data[1].actions, 'write', `Expected write, got ${body.data[1].actions}`); }); - it.skip('cannot recreate root folder because of auth (previous test should setup auth)', async () => { + it('cannot recreate root folder because of auth (previous test should setup auth)', async () => { const { SERVER_URL, ORG, REPO } = ctx; const formData = new FormData(); const blob = new Blob(['{}'], { type: 'application/json' }); @@ -207,7 +208,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { method: 'POST', body: formData, }); - assert.strictEqual(resp.status, 401, `Previous test should have setup auth, got ${resp.status}`); + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); }); it('should logout via HTTP request', async () => { From 6ec19b660f94c593903bbd0b2d68f86582717fe0 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 8 Dec 2025 16:25:45 +0100 Subject: [PATCH 09/32] chore: let's break it! --- src/index.js | 4 ++-- test/integration/conditional-e2e.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index a848d2b..fc8d0d1 100644 --- a/src/index.js +++ b/src/index.js @@ -13,7 +13,7 @@ import getDaCtx from './utils/daCtx.js'; import daResp from './utils/daResp.js'; import headHandler from './handlers/head.js'; -import getHandler from './handlers/get.js'; +// import getHandler from './handlers/get.js'; import postHandler from './handlers/post.js'; import deleteHandler from './handlers/delete.js'; @@ -54,7 +54,7 @@ export default { respObj = await headHandler({ env, daCtx }); break; case 'GET': - respObj = await getHandler({ env, daCtx }); + // respObj = await getHandler({ env, daCtx }); break; case 'PUT': respObj = await postHandler({ req, env, daCtx }); diff --git a/test/integration/conditional-e2e.test.js b/test/integration/conditional-e2e.test.js index f5fed26..7720a7b 100644 --- a/test/integration/conditional-e2e.test.js +++ b/test/integration/conditional-e2e.test.js @@ -58,7 +58,7 @@ describe('Conditional Headers End-to-End', () => { ); }); - it('GET with If-None-Match returns 304 with ETag header in response', async () => { + it.skip('GET with If-None-Match returns 304 with ETag header in response', async () => { const etag = '"test-etag-123"'; s3Mock .on(GetObjectCommand) @@ -156,7 +156,7 @@ describe('Conditional Headers End-to-End', () => { assert.strictEqual(resp.headers.get('Access-Control-Allow-Origin'), '*'); }); - it('versionsource GET with conditionals works correctly', async () => { + it.skip('versionsource GET with conditionals works correctly', async () => { const etag = '"version-etag"'; s3Mock .on(GetObjectCommand) From ceeb6a52e3cd67f166646e080250642470877236 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 8 Dec 2025 16:28:02 +0100 Subject: [PATCH 10/32] Revert "chore: let's break it!" This reverts commit 6ec19b660f94c593903bbd0b2d68f86582717fe0. --- src/index.js | 4 ++-- test/integration/conditional-e2e.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index fc8d0d1..a848d2b 100644 --- a/src/index.js +++ b/src/index.js @@ -13,7 +13,7 @@ import getDaCtx from './utils/daCtx.js'; import daResp from './utils/daResp.js'; import headHandler from './handlers/head.js'; -// import getHandler from './handlers/get.js'; +import getHandler from './handlers/get.js'; import postHandler from './handlers/post.js'; import deleteHandler from './handlers/delete.js'; @@ -54,7 +54,7 @@ export default { respObj = await headHandler({ env, daCtx }); break; case 'GET': - // respObj = await getHandler({ env, daCtx }); + respObj = await getHandler({ env, daCtx }); break; case 'PUT': respObj = await postHandler({ req, env, daCtx }); diff --git a/test/integration/conditional-e2e.test.js b/test/integration/conditional-e2e.test.js index 7720a7b..f5fed26 100644 --- a/test/integration/conditional-e2e.test.js +++ b/test/integration/conditional-e2e.test.js @@ -58,7 +58,7 @@ describe('Conditional Headers End-to-End', () => { ); }); - it.skip('GET with If-None-Match returns 304 with ETag header in response', async () => { + it('GET with If-None-Match returns 304 with ETag header in response', async () => { const etag = '"test-etag-123"'; s3Mock .on(GetObjectCommand) @@ -156,7 +156,7 @@ describe('Conditional Headers End-to-End', () => { assert.strictEqual(resp.headers.get('Access-Control-Allow-Origin'), '*'); }); - it.skip('versionsource GET with conditionals works correctly', async () => { + it('versionsource GET with conditionals works correctly', async () => { const etag = '"version-etag"'; s3Mock .on(GetObjectCommand) From 5dec3b68895de4daea7427e0a21aea68405712ed Mon Sep 17 00:00:00 2001 From: Chris Millar Date: Fri, 12 Dec 2025 00:28:19 -0700 Subject: [PATCH 11/32] GH-221 - Disable anonymous access * Return 412 if any user is considered anonymous * Updated tests to pass a user so existing test cases pass * Added new tests to cover 412 Resolves: GH-221 --- .dev.vars.it | 9 ++++ .gitignore | 2 + package-lock.json | 21 +------- src/index.js | 13 +++-- test/index.test.js | 57 ++++++++++++++++++++-- test/it/smoke.test.js | 108 +++++++++++++++++++++++++++++++++++++----- 6 files changed, 167 insertions(+), 43 deletions(-) create mode 100644 .dev.vars.it diff --git a/.dev.vars.it b/.dev.vars.it new file mode 100644 index 0000000..e45d536 --- /dev/null +++ b/.dev.vars.it @@ -0,0 +1,9 @@ +# Integration test environment variables +# These override .dev.vars when running with --env it + +S3_DEF_URL=http://localhost:4569 +S3_ACCESS_KEY_ID=S3RVER +S3_SECRET_ACCESS_KEY=S3RVER +S3_FORCE_PATH_STYLE=true +IMS_ORIGIN=http://localhost:9999 +AEM_ADMIN_MEDIA_API_KEY=test-key diff --git a/.gitignore b/.gitignore index ecf6a87..e6453c5 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,5 @@ dist .vscode +# Integration test - S3rver generated files +test/it/bucket/ diff --git a/package-lock.json b/package-lock.json index 1712cde..41d48d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -447,7 +447,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.726.0.tgz", "integrity": "sha512-5JzTX9jwev7+y2Jkzjz0pd1wobB5JQfPOQF3N2DrJ5Pao0/k6uRYwE4NqB0p0HlGrMTDm7xNq7OSPPIPG575Jw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -501,7 +500,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.726.1.tgz", "integrity": "sha512-qh9Q9Vu1hrM/wMBOBIaskwnE4GTFaZu26Q6WHwyWNfj7J8a40vBxpW16c2vYXHLBtwRKM1be8uRLkmDwghpiNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -1689,8 +1687,7 @@ "version": "4.20251126.0", "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20251126.0.tgz", "integrity": "sha512-DSeI1Q7JYmh5/D/tw5eZCjrKY34v69rwj63hHt60nSQW5QLwWCbj/lLtNz9f2EPa+JCACwpLXHgCXfzJ29x66w==", - "license": "MIT OR Apache-2.0", - "peer": true + "license": "MIT OR Apache-2.0" }, "node_modules/@colors/colors": { "version": "1.6.0", @@ -3040,7 +3037,6 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -3187,7 +3183,6 @@ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -5291,7 +5286,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5349,7 +5343,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7680,7 +7673,6 @@ "integrity": "sha512-sjc7Y8cUD1IlwYcTS9qPSvGjAC8Ne9LctpxKKu3x/1IC9bnOg98Zy6GxEJUfr1NojMgVPlyANXYns8oE2c1TAA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9947,7 +9939,6 @@ "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 10.16.0" } @@ -10815,7 +10806,6 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -11368,7 +11358,6 @@ "integrity": "sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" @@ -13846,7 +13835,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14636,7 +14624,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15015,7 +15002,6 @@ "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -15026,7 +15012,6 @@ "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -15647,7 +15632,6 @@ "integrity": "sha512-6qGjWccl5yoyugHt3jTgztJ9Y0JVzyH8/Voc/D8PlLat9pwxQYXz7W1Dpnq5h0/G5GCYGUaDSlYcyk3AMh5A6g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -17021,7 +17005,6 @@ "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", @@ -17750,7 +17733,6 @@ "integrity": "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.4", @@ -18215,7 +18197,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "bin": { "workerd": "bin/workerd" }, diff --git a/src/index.js b/src/index.js index a848d2b..5518374 100644 --- a/src/index.js +++ b/src/index.js @@ -39,11 +39,14 @@ export default { return daResp({ status: 500 }); } - const { authorized, key } = daCtx; - if (!authorized) { - const status = daCtx.users[0].email === 'anonymous' ? 401 : 403; - return daResp({ status }); - } + const { users, authorized, key } = daCtx; + + // Anonymous users are not permitted + const anon = users.some((user) => user.email === 'anonymous'); + if (anon) return daResp({ status: 412 }); + + if (!authorized) return daResp({ status: 403 }); + if (key?.startsWith('.da-versions')) { return daResp({ status: 404 }); } diff --git a/test/index.test.js b/test/index.test.js index eeae9e4..6266356 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -24,11 +24,17 @@ describe('fetch', () => { }); it('should return a response object for unknown', async () => { - const resp = await handler.fetch({ url: 'https://www.example.com/endpoint/repo/path/file.html', method: 'BLAH' }, {}); + const hnd = await esmock('../src/index.js', { + '../src/utils/daCtx.js': { + default: async () => ({ authorized: true, users: [{ email: 'test@example.com' }], path: '/endpoint/repo/path/file.html' }), + }, + }); + + const resp = await hnd.fetch({ url: 'https://www.example.com/endpoint/repo/path/file.html', method: 'BLAH' }, {}); assert.strictEqual(resp.status, 405); }); - it('should return 401 when not authorized and not logged in', async () => { + it('should return 412 when user is anonymous', async () => { const hnd = await esmock('../src/index.js', { '../src/utils/daCtx.js': { default: async () => ({ authorized: false, users: [{ email: 'anonymous' }] }), @@ -36,7 +42,18 @@ describe('fetch', () => { }); const resp = await hnd.fetch({ method: 'GET' }, {}); - assert.strictEqual(resp.status, 401); + assert.strictEqual(resp.status, 412); + }); + + it('should return 401 when not authorized and not logged in', async () => { + const hnd = await esmock('../src/index.js', { + '../src/utils/daCtx.js': { + default: async () => ({ authorized: false, users: [{ email: 'test@example.com' }] }), + }, + }); + + const resp = await hnd.fetch({ method: 'GET' }, {}); + assert.strictEqual(resp.status, 403); }); it('should return 403 when logged in but not authorized', async () => { @@ -51,7 +68,13 @@ describe('fetch', () => { }); it('return 404 for unknown get route', async () => { - const resp = await handler.fetch({ method: 'GET', url: 'http://www.example.com/' }, {}); + const hnd = await esmock('../src/index.js', { + '../src/utils/daCtx.js': { + default: async () => ({ authorized: true, users: [{ email: 'test@example.com' }], path: '/' }), + }, + }); + + const resp = await hnd.fetch({ method: 'GET', url: 'http://www.example.com/' }, {}); assert.strictEqual(resp.status, 404); }); @@ -70,8 +93,32 @@ describe('fetch', () => { }); describe('invalid routes', () => { + let hnd; + + before(async () => { + hnd = await esmock('../src/index.js', { + '../src/utils/daCtx.js': { + default: async (req) => { + const { pathname } = new URL(req.url); + // For invalid paths, throw the error that getDaCtx would throw + if (pathname.includes('//')) { + throw new Error('Invalid path'); + } + return { + authorized: true, + users: [{ email: 'test@example.com' }], + path: pathname, + api: 'source', + org: 'owner', + key: 'repo/path/file.html', + }; + }, + }, + }); + }); + const fetchStatus = async (path, method) => { - const resp = await handler.fetch({ method, url: `http://www.sample.com${path}` }, {}); + const resp = await hnd.fetch({ method, url: `http://www.sample.com${path}` }, {}); return resp.status; }; diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index b89709a..f3f57c6 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -15,9 +15,12 @@ import S3rver from 's3rver'; import { spawn } from 'child_process'; import path from 'path'; import kill from 'tree-kill'; +import { generateKeyPair, exportJWK, SignJWT } from 'jose'; +import { createServer } from 'http'; const S3_PORT = 4569; const SERVER_PORT = 8788; +const IMS_PORT = 9999; const SERVER_URL = `http://localhost:${SERVER_PORT}`; const S3_DIR = './test/it/bucket'; @@ -27,6 +30,9 @@ const REPO = 'test-repo'; describe('Integration Tests: smoke tests', function () { let s3rver; let devServer; + let imsServer; + let accessToken; + let publicKeyJwk; before(async function () { // Increase timeout for server startup @@ -39,6 +45,60 @@ describe('Integration Tests: smoke tests', function () { fs.rmSync(wranglerState, { recursive: true }); } + // Generate JWT token for authentication + const kid = 'test-key-id'; + const { publicKey, privateKey } = await generateKeyPair('RS256'); + publicKeyJwk = await exportJWK(publicKey); + publicKeyJwk.use = 'sig'; + publicKeyJwk.kid = kid; + publicKeyJwk.alg = 'RS256'; + + // Create JWT with timestamps in milliseconds (as IMS does) + const now = Date.now(); + accessToken = await new SignJWT({ + user_id: 'test_user', + type: 'access_token', + created_at: now, // milliseconds since epoch + expires_in: 3600000, // milliseconds (1 hour) + }) + .setProtectedHeader({ alg: 'RS256', kid }) + .sign(privateKey); + + // Start mock IMS server + imsServer = createServer((req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type'); + + // Log requests for debugging + console.log(`[IMS Mock] ${req.method} ${req.url}`); + + if (req.method === 'OPTIONS') { + res.writeHead(204); + res.end(); + } else if (req.url === '/ims/keys') { + res.writeHead(200); + res.end(JSON.stringify({ keys: [publicKeyJwk] })); + } else if (req.url === '/ims/profile/v1') { + res.writeHead(200); + res.end(JSON.stringify({ + email: 'test@example.com', + userId: 'test_user', + })); + } else if (req.url === '/ims/organizations/v5') { + res.writeHead(200); + res.end(JSON.stringify([])); + } else { + console.log(`[IMS Mock] 404 Not Found: ${req.url}`); + res.writeHead(404); + res.end(JSON.stringify({ error: 'Not found' })); + } + }); + + await new Promise((resolve) => { + imsServer.listen(IMS_PORT, '127.0.0.1', resolve); + }); + s3rver = new S3rver({ port: S3_PORT, address: '127.0.0.1', @@ -51,12 +111,7 @@ describe('Integration Tests: smoke tests', function () { 'wrangler', 'dev', '--port', SERVER_PORT.toString(), '--env', 'it', - '--var', 'S3_DEF_URL:http://localhost:4569', - '--var', 'S3_ACCESS_KEY_ID:S3RVER', - '--var', 'S3_SECRET_ACCESS_KEY:S3RVER', - '--var', 'S3_FORCE_PATH_STYLE:true', - '--var', 'IMS_ORIGIN:http://localhost:9999', - '--var', 'AEM_ADMIN_MEDIA_API_KEY:test-key', + '--log-level', 'debug', ], { stdio: 'pipe', // Capture output for debugging detached: false, // Keep in same process group for easier cleanup @@ -67,6 +122,8 @@ describe('Integration Tests: smoke tests', function () { let started = false; devServer.stdout.on('data', (data) => { const str = data.toString(); + // Always log wrangler output including errors + console.log('[Wrangler]', str.trim()); if (str.includes('Ready on http://localhost') && !started) { started = true; resolve(); @@ -108,13 +165,25 @@ describe('Integration Tests: smoke tests', function () { if (s3rver) { await s3rver.close(); } + if (imsServer) { + await new Promise((resolve) => { + imsServer.close(resolve); + }); + } }); it('should get a object via HTTP request', async () => { const pathname = 'test-folder/page1.html'; const url = `${SERVER_URL}/source/${ORG}/${REPO}/${pathname}`; - const resp = await fetch(url); + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + if (resp.status !== 200) { + const errorText = await resp.text(); + console.error('Error response:', errorText); + } assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); @@ -126,7 +195,9 @@ describe('Integration Tests: smoke tests', function () { const key = 'test-folder'; const url = `${SERVER_URL}/list/${ORG}/${REPO}/${key}`; - const resp = await fetch(url); + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); @@ -150,6 +221,7 @@ describe('Integration Tests: smoke tests', function () { const url = `${SERVER_URL}/source/${ORG}/${REPO}/${key}${ext}`; let resp = await fetch(url, { method: 'POST', + headers: { Authorization: `Bearer ${accessToken}` }, body: formData, }); @@ -162,7 +234,9 @@ describe('Integration Tests: smoke tests', function () { 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}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); @@ -174,6 +248,7 @@ describe('Integration Tests: smoke tests', function () { const url = `${SERVER_URL}/logout`; const resp = await fetch(url, { method: 'POST', + headers: { Authorization: `Bearer ${accessToken}` }, }); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); @@ -181,7 +256,9 @@ describe('Integration Tests: smoke tests', function () { it('should list repos via HTTP request', async () => { const url = `${SERVER_URL}/list/${ORG}`; - const resp = await fetch(url); + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); @@ -192,7 +269,9 @@ describe('Integration Tests: smoke tests', function () { it('should deal with no config found via HTTP request', async () => { const url = `${SERVER_URL}/config/${ORG}`; - const resp = await fetch(url); + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); assert.strictEqual(resp.status, 404, `Expected 404, got ${resp.status}`); }); @@ -204,7 +283,7 @@ describe('Integration Tests: smoke tests', function () { limit: 2, offset: 0, data: [ - { path: 'CONFIG', actions: 'write', groups: 'anonymous' }, + { path: 'CONFIG', actions: 'write', groups: 'test@example.com' }, { key: 'admin.role.all', value: 'test-value' }, ], ':type': 'sheet', @@ -217,6 +296,7 @@ describe('Integration Tests: smoke tests', function () { let url = `${SERVER_URL}/config/${ORG}`; let resp = await fetch(url, { method: 'POST', + headers: { Authorization: `Bearer ${accessToken}` }, body: formData, }); @@ -224,7 +304,9 @@ describe('Integration Tests: smoke tests', function () { // Now GET the config url = `${SERVER_URL}/config/${ORG}`; - resp = await fetch(url); + resp = await fetch(url, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); From baeaab88fed83c5f91ee441f9e3ada8dc29a489f Mon Sep 17 00:00:00 2001 From: Chris Millar Date: Fri, 12 Dec 2025 09:09:04 -0700 Subject: [PATCH 12/32] Change to 401 for anonymous users --- src/index.js | 2 +- test/index.test.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 5518374..c0f4a30 100644 --- a/src/index.js +++ b/src/index.js @@ -43,7 +43,7 @@ export default { // Anonymous users are not permitted const anon = users.some((user) => user.email === 'anonymous'); - if (anon) return daResp({ status: 412 }); + if (anon) return daResp({ status: 401 }); if (!authorized) return daResp({ status: 403 }); diff --git a/test/index.test.js b/test/index.test.js index 6266356..5558063 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -34,7 +34,7 @@ describe('fetch', () => { assert.strictEqual(resp.status, 405); }); - it('should return 412 when user is anonymous', async () => { + it('should return 401 when user is anonymous', async () => { const hnd = await esmock('../src/index.js', { '../src/utils/daCtx.js': { default: async () => ({ authorized: false, users: [{ email: 'anonymous' }] }), @@ -42,7 +42,7 @@ describe('fetch', () => { }); const resp = await hnd.fetch({ method: 'GET' }, {}); - assert.strictEqual(resp.status, 412); + assert.strictEqual(resp.status, 401); }); it('should return 401 when not authorized and not logged in', async () => { From 033682e274e134b54c18b1bad5f38984349f84a9 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Tue, 16 Dec 2025 10:59:49 +0100 Subject: [PATCH 13/32] chore: rename --- test/it/smoke.test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index 19f8399..00c5474 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -146,8 +146,9 @@ describe('Integration Tests: smoke tests', function () { this.timeout(30000); if (process.env.VERSION_PREVIEW_URL) { - context.SERVER_URL = process.env.VERSION_PREVIEW_URL; - context.ORG = process.env.VERSION_PREVIEW_ORG; + context.serverUrl = process.env.VERSION_PREVIEW_URL; + context.org = process.env.VERSION_PREVIEW_ORG; + // TODO solve IMS authentication for postdeploy tests } else { // local testing, start the server @@ -162,6 +163,8 @@ describe('Integration Tests: smoke tests', function () { await setupS3rver(); await setupDevServer(); } + + console.log('Running tests with context:', context); }); after(async function () { From 4c7a6bd848794f42ec52fb40cf5dd2b6570cdba9 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Tue, 16 Dec 2025 14:29:28 +0100 Subject: [PATCH 14/32] chore: ims cleanup --- test/it/smoke.test.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index 00c5474..4f4f1bb 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -25,6 +25,8 @@ const SERVER_PORT = 8788; const LOCAL_SERVER_URL = `http://localhost:${SERVER_PORT}`; const IMS_PORT = 9999; +const IMS_KID = 'ims'; + const S3_DIR = './test/it/bucket'; const LOCAL_ORG = 'test-org'; @@ -43,26 +45,27 @@ describe('Integration Tests: smoke tests', function () { accessToken: '', }; - const setupIMSServer = async () => { - // Generate JWT token for authentication - const kid = 'test-key-id'; + const testIMSToken = async () => { const { publicKey, privateKey } = await generateKeyPair('RS256'); publicKeyJwk = await exportJWK(publicKey); publicKeyJwk.use = 'sig'; - publicKeyJwk.kid = kid; + publicKeyJwk.kid = IMS_KID; publicKeyJwk.alg = 'RS256'; - // Create JWT with timestamps in milliseconds (as IMS does) - const now = Date.now(); - context.accessToken = await new SignJWT({ - user_id: 'test_user', + const accessToken = await new SignJWT({ + // as: 'ims-na1-stg1', type: 'access_token', - created_at: now, // milliseconds since epoch - expires_in: 3600000, // milliseconds (1 hour) + user_id: 'test_user', + created_at: String(Date.now() - 1000), + expires_in: '86400000', }) - .setProtectedHeader({ alg: 'RS256', kid }) + .setProtectedHeader({ alg: 'RS256', kid: IMS_KID }) .sign(privateKey); + return accessToken; + }; + + const setupIMSServer = async () => { // Start mock IMS server imsServer = createServer((req, res) => { res.setHeader('Content-Type', 'application/json'); @@ -159,6 +162,7 @@ describe('Integration Tests: smoke tests', function () { fs.rmSync(wranglerState, { recursive: true }); } + context.accessToken = await testIMSToken(); await setupIMSServer(); await setupS3rver(); await setupDevServer(); From b4a2c81a518c1600b21fdff83f37cfc0eacb7eec Mon Sep 17 00:00:00 2001 From: kptdobe Date: Tue, 16 Dec 2025 14:48:10 +0100 Subject: [PATCH 15/32] chore: prepare for ims stage --- test/it/smoke.test.js | 55 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index 4f4f1bb..786b6db 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -23,9 +23,17 @@ const S3_PORT = 4569; const SERVER_PORT = 8788; const LOCAL_SERVER_URL = `http://localhost:${SERVER_PORT}`; -const IMS_PORT = 9999; -const IMS_KID = 'ims'; +const IMS_LOCAL_PORT = 9999; +const IMS_LOCAL_KID = 'ims'; + +const IMS_STAGE = { + ENDPOINT: 'https://ims-na1-stg1.adobelogin.com', + CLIENT_ID: process.env.IMS_STAGE_CLIENT_ID, + CLIENT_SECRET: process.env.IMS_STAGE_CLIENT_SECRET, + ORG_ID: process.env.IMS_STAGE_ORG_ID, + SCOPES: process.env.IMS_STAGE_SCOPES, +}; const S3_DIR = './test/it/bucket'; @@ -45,11 +53,42 @@ describe('Integration Tests: smoke tests', function () { accessToken: '', }; - const testIMSToken = async () => { + const connectToIMSStage = async () => { + const postData = { + grant_type: 'client_credentials', + client_id: IMS_STAGE.CLIENT_ID, + client_secret: IMS_STAGE.CLIENT_SECRET, + org_id: IMS_STAGE.ORG_ID, + scope: IMS_STAGE.SCOPES, + }; + + const form = new FormData(); + Object.entries(postData).forEach(([k, v]) => { + form.append(k, v); + }); + + let res; + try { + res = await fetch(`${IMS_STAGE.ENDPOINT}/ims/token/v2`, { + method: 'POST', + body: form, + }); + } catch (e) { + throw new Error(`cannot send request to IMS: ${e.message}`); + } + + if (res.ok) { + const json = await res.json(); + return json.access_token; + } + throw new Error(`error response from IMS with status: ${res.status} and body: ${await res.text()}`); + }; + + const getIMSLocalToken = async () => { const { publicKey, privateKey } = await generateKeyPair('RS256'); publicKeyJwk = await exportJWK(publicKey); publicKeyJwk.use = 'sig'; - publicKeyJwk.kid = IMS_KID; + publicKeyJwk.kid = IMS_LOCAL_KID; publicKeyJwk.alg = 'RS256'; const accessToken = await new SignJWT({ @@ -59,7 +98,7 @@ describe('Integration Tests: smoke tests', function () { created_at: String(Date.now() - 1000), expires_in: '86400000', }) - .setProtectedHeader({ alg: 'RS256', kid: IMS_KID }) + .setProtectedHeader({ alg: 'RS256', kid: IMS_LOCAL_KID }) .sign(privateKey); return accessToken; @@ -98,7 +137,7 @@ describe('Integration Tests: smoke tests', function () { }); await new Promise((resolve) => { - imsServer.listen(IMS_PORT, '127.0.0.1', resolve); + imsServer.listen(IMS_LOCAL_PORT, '127.0.0.1', resolve); }); }; @@ -151,7 +190,7 @@ describe('Integration Tests: smoke tests', function () { if (process.env.VERSION_PREVIEW_URL) { context.serverUrl = process.env.VERSION_PREVIEW_URL; context.org = process.env.VERSION_PREVIEW_ORG; - // TODO solve IMS authentication for postdeploy tests + context.accessToken = await connectToIMSStage(); } else { // local testing, start the server @@ -162,7 +201,7 @@ describe('Integration Tests: smoke tests', function () { fs.rmSync(wranglerState, { recursive: true }); } - context.accessToken = await testIMSToken(); + context.accessToken = await getIMSLocalToken(); await setupIMSServer(); await setupS3rver(); await setupDevServer(); From 974d491694fbe278e912e7a30e1c525611dc7b0e Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 17 Dec 2025 11:50:38 +0100 Subject: [PATCH 16/32] chore: IMS integration --- test/it/it-tests.js | 231 ++++++++++++++++++++++++++---------------- test/it/smoke.test.js | 57 +++++++---- 2 files changed, 181 insertions(+), 107 deletions(-) diff --git a/test/it/it-tests.js b/test/it/it-tests.js index d766196..ca98859 100644 --- a/test/it/it-tests.js +++ b/test/it/it-tests.js @@ -16,12 +16,99 @@ export default (ctx) => describe('Integration Tests: it tests', function () { // Enable bail to stop on first failure - tests are interdependent this.bail(true); - it('delete root folder should cleanup the bucket', async () => { + it('should set org config via HTTP request', async function shouldSetOrgConfig() { + if (!ctx.local) { + // in stage, the config is already set and we should not overwrite it + // to preserve the setup and be able to access the content + this.skip(); + } + const { + serverUrl, org, accessToken, + } = ctx; + const configData = JSON.stringify({ + total: 2, + limit: 2, + offset: 0, + data: [ + { path: 'CONFIG', groups: 'test@example.com', actions: 'write' }, + { path: '/+**', groups: 'test@example.com', actions: 'write' }, + ], + ':type': 'sheet', + ':sheetname': 'permissions', + }); + + const formData = new FormData(); + formData.append('config', configData); + + const url = `${serverUrl}/config/${org}`; + const resp = await fetch(url, { + method: 'POST', + body: formData, + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); + }); + + it('should get org config via HTTP request', async () => { + const { + serverUrl, org, accessToken, email, + } = ctx; + const url = `${serverUrl}/config/${org}`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + 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].groups, email, `Expected user email, got ${body.data[0].groups}`); + assert.strictEqual(body.data[0].actions, 'write', `Expected write, got ${body.data[0].actions}`); + assert.strictEqual(body.data[1].path, '/+**', `Expected /+**, got ${body.data[1].path}`); + assert.strictEqual(body.data[1].groups, email, `Expected user email, got ${body.data[1].groups}`); + assert.strictEqual(body.data[1].actions, 'write', `Expected write, got ${body.data[1].actions}`); + }); + + it('not allowed to read if not authenticated', async () => { + const { + serverUrl, org, repo, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}`; + const resp = await fetch(url, { + method: 'GET', + }); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + + it('cannot delete root folder if not authenticated', async () => { + const { + serverUrl, org, repo, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}`; + const resp = await fetch(url, { + method: 'DELETE', + }); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + + it('cannot delete root folder if not authenticated', async () => { + const { + serverUrl, org, repo, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}`; + const resp = await fetch(url, { + method: 'DELETE', + }); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + + it('delete root folder to cleanup the bucket', async () => { const { serverUrl, org, repo, accessToken, } = ctx; const url = `${serverUrl}/source/${org}/${repo}`; - console.log('url', url); const resp = await fetch(url, { method: 'DELETE', headers: { Authorization: `Bearer ${accessToken}` }, @@ -54,6 +141,29 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.ok([200, 201].includes(resp.status), `Expected 200 or 201 for marker, got ${resp.status}`); }); + it('cannot post an object via HTTP request if not authenticated', async () => { + const { + serverUrl, org, repo, + } = ctx; + // Now create the actual page + const key = 'test-folder/page1'; + const ext = '.html'; + + // Create FormData with the HTML file + const formData = new FormData(); + const blob = new Blob(['

Page 1

'], { type: 'text/html' }); + const file = new File([blob], 'page1.html', { type: 'text/html' }); + formData.append('data', file); + + const url = `${serverUrl}/source/${org}/${repo}/${key}${ext}`; + const resp = await fetch(url, { + method: 'POST', + body: formData, + }); + + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + it('should post an object via HTTP request', async () => { const { serverUrl, org, repo, accessToken, @@ -108,6 +218,15 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); }); + it('cannot list objects via HTTP request if not authenticated', async () => { + const { + serverUrl, org, repo, + } = ctx; + const url = `${serverUrl}/list/${org}/${repo}`; + const resp = await fetch(url); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + it('should list objects via HTTP request', async () => { const { serverUrl, org, repo, accessToken, @@ -128,6 +247,15 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.ok(fileNames.includes('page2'), 'Should list page2'); }); + it('cannot list repos via HTTP request if not authenticated', async () => { + const { + serverUrl, org, + } = ctx; + const url = `${serverUrl}/list/${org}`; + const resp = await fetch(url); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + it('should list repos via HTTP request', async () => { const { serverUrl, org, repo, accessToken, @@ -144,6 +272,17 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.strictEqual(body[0].name, repo, `Expected ${repo}, got ${body[0].name}`); }); + it('cannot delete an object via HTTP request if not authenticated', async () => { + const { + serverUrl, org, repo, key, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}/${key}`; + const resp = await fetch(url, { + method: 'DELETE', + }); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + it('should delete an object via HTTP request', async () => { const { serverUrl, org, repo, accessToken, @@ -165,94 +304,6 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.strictEqual(resp.status, 404, `Expected 404 Not Found, got ${resp.status}`); }); - it('should deal with no config found via HTTP request', async () => { - const { - serverUrl, org, accessToken, - } = ctx; - const url = `${serverUrl}/config/${org}`; - const resp = await fetch(url, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - - assert.strictEqual(resp.status, 404, `Expected 404, got ${resp.status}`); - }); - - it('should delete root folder', async () => { - const { - serverUrl, org, repo, accessToken, - } = ctx; - const url = `${serverUrl}/source/${org}/${repo}`; - const resp = await fetch(url, { - method: 'DELETE', - headers: { Authorization: `Bearer ${accessToken}` }, - }); - assert.strictEqual(resp.status, 204, `Previous test should have logged out, got ${resp.status}`); - }); - - it('should post and get org config via HTTP request', async () => { - const { - serverUrl, org, accessToken, - } = ctx; - // First POST the config - must include CONFIG write permission - const configData = JSON.stringify({ - total: 2, - limit: 2, - offset: 0, - data: [ - { path: 'CONFIG', groups: 'test@example.com', actions: 'write' }, - { path: '/+**', groups: 'test@example.com', actions: 'write' }, - ], - ':type': 'sheet', - ':sheetname': 'permissions', - }); - - const formData = new FormData(); - formData.append('config', configData); - - let url = `${serverUrl}/config/${org}`; - let resp = await fetch(url, { - method: 'POST', - body: formData, - headers: { Authorization: `Bearer ${accessToken}` }, - }); - - assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); - - // Now GET the config - url = `${serverUrl}/config/${org}`; - resp = await fetch(url, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - - 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].groups, 'test@example.com', `Expected test@example.com, got ${body.data[0].groups}`); - assert.strictEqual(body.data[0].actions, 'write', `Expected write, got ${body.data[0].actions}`); - assert.strictEqual(body.data[1].path, '/+**', `Expected /+**, got ${body.data[1].path}`); - assert.strictEqual(body.data[1].groups, 'test@example.com', `Expected test@example.com, got ${body.data[1].groups}`); - assert.strictEqual(body.data[1].actions, 'write', `Expected write, got ${body.data[1].actions}`); - }); - - it('cannot recreate root folder because of auth (previous test should setup auth)', async () => { - const { - serverUrl, org, repo, accessToken, - } = ctx; - const formData = new FormData(); - const blob = new Blob(['{}'], { type: 'application/json' }); - const file = new File([blob], `${repo}.props`, { type: 'application/json' }); - formData.append('data', file); - - const resp = await fetch(`${serverUrl}/source/${org}/${repo}/${repo}.props`, { - method: 'POST', - body: formData, - headers: { Authorization: `Bearer ${accessToken}` }, - }); - assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); - }); - it('should logout via HTTP request', async () => { const { serverUrl, accessToken } = ctx; const url = `${serverUrl}/logout`; diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index 786b6db..c37c037 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -10,15 +10,19 @@ * governing permissions and limitations under the License. */ /* eslint-disable prefer-arrow-callback, func-names */ -import S3rver from 's3rver'; import { spawn } from 'child_process'; import path from 'path'; +import fs from 'fs'; import kill from 'tree-kill'; +import config from 'dotenv'; import { generateKeyPair, exportJWK, SignJWT } from 'jose'; import { createServer } from 'http'; +import S3rver from 's3rver'; import itTests from './it-tests.js'; +config.config(); + const S3_PORT = 4569; const SERVER_PORT = 8788; @@ -28,11 +32,10 @@ const IMS_LOCAL_PORT = 9999; const IMS_LOCAL_KID = 'ims'; const IMS_STAGE = { - ENDPOINT: 'https://ims-na1-stg1.adobelogin.com', - CLIENT_ID: process.env.IMS_STAGE_CLIENT_ID, - CLIENT_SECRET: process.env.IMS_STAGE_CLIENT_SECRET, - ORG_ID: process.env.IMS_STAGE_ORG_ID, - SCOPES: process.env.IMS_STAGE_SCOPES, + ENDPOINT: process.env.IT_IMS_STAGE_ENDPOINT, + CLIENT_ID: process.env.IT_IMS_STAGE_CLIENT_ID, + CLIENT_SECRET: process.env.IT_IMS_STAGE_CLIENT_SECRET, + SCOPES: process.env.IT_IMS_STAGE_SCOPES, }; const S3_DIR = './test/it/bucket'; @@ -53,12 +56,18 @@ describe('Integration Tests: smoke tests', function () { accessToken: '', }; + const cleanupWranglerState = () => { + const wranglerState = path.join(process.cwd(), '.wrangler/state'); + if (fs.existsSync(wranglerState)) { + fs.rmSync(wranglerState, { recursive: true }); + } + }; + const connectToIMSStage = async () => { const postData = { grant_type: 'client_credentials', client_id: IMS_STAGE.CLIENT_ID, client_secret: IMS_STAGE.CLIENT_SECRET, - org_id: IMS_STAGE.ORG_ID, scope: IMS_STAGE.SCOPES, }; @@ -69,7 +78,7 @@ describe('Integration Tests: smoke tests', function () { let res; try { - res = await fetch(`${IMS_STAGE.ENDPOINT}/ims/token/v2`, { + res = await fetch(`${IMS_STAGE.ENDPOINT}/ims/token/v3`, { method: 'POST', body: form, }); @@ -84,6 +93,17 @@ describe('Integration Tests: smoke tests', function () { throw new Error(`error response from IMS with status: ${res.status} and body: ${await res.text()}`); }; + const getIMSProfile = async (accessToken) => { + const res = await fetch(`${IMS_STAGE.ENDPOINT}/ims/profile/v1`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + if (res.ok) { + const json = await res.json(); + return json.email; + } + throw new Error(`Failed to fetch IMS profile: ${res.status}`); + }; + const getIMSLocalToken = async () => { const { publicKey, privateKey } = await generateKeyPair('RS256'); publicKeyJwk = await exportJWK(publicKey); @@ -188,20 +208,23 @@ describe('Integration Tests: smoke tests', function () { this.timeout(30000); if (process.env.VERSION_PREVIEW_URL) { + if (!IMS_STAGE.ENDPOINT || !IMS_STAGE.CLIENT_ID + || !IMS_STAGE.CLIENT_SECRET || !IMS_STAGE.SCOPES) { + throw new Error('IT_IMS_STAGE_ENDPOINT, IT_IMS_STAGE_CLIENT_ID, ' + + 'IT_IMS_STAGE_CLIENT_SECRET, and IT_IMS_STAGE_SCOPES must be set'); + } + context.serverUrl = process.env.VERSION_PREVIEW_URL; context.org = process.env.VERSION_PREVIEW_ORG; context.accessToken = await connectToIMSStage(); + context.email = await getIMSProfile(context.accessToken); + context.local = false; } else { - // local testing, start the server - - // 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 }); - } - + context.local = true; context.accessToken = await getIMSLocalToken(); + context.email = 'test@example.com'; + + cleanupWranglerState(); await setupIMSServer(); await setupS3rver(); await setupDevServer(); From 99afbdd6a691bbd54d105dabd346c0c32141c16e Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 17 Dec 2025 12:01:27 +0100 Subject: [PATCH 17/32] chore: dep --- package-lock.json | 20 +++++++++++++++++--- package.json | 1 + 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae71cd4..85d4850 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@semantic-release/git": "10.0.1", "aws-sdk-client-mock": "4.0.0", "c8": "10.0.0", + "dotenv": "17.2.3", "eslint": "9.4.0", "esmock": "2.7.3", "husky": "9.1.7", @@ -3640,6 +3641,19 @@ "npm": ">=10" } }, + "node_modules/@redocly/cli/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/@redocly/config": { "version": "0.26.4", "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.26.4.tgz", @@ -7194,9 +7208,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "dev": true, "license": "BSD-2-Clause", "engines": { diff --git a/package.json b/package.json index 32ac3c6..cba13b4 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@semantic-release/git": "10.0.1", "aws-sdk-client-mock": "4.0.0", "c8": "10.0.0", + "dotenv": "17.2.3", "eslint": "9.4.0", "esmock": "2.7.3", "husky": "9.1.7", From 5cce911126281066304d6a0e447ca53d1261435c Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 17 Dec 2025 12:04:32 +0100 Subject: [PATCH 18/32] chore: secrets --- .github/workflows/build.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 53b33b6..caabc71 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -32,6 +32,11 @@ jobs: run: npm run test - name: Run the integration tests run: npm run test:it + env: + IT_IMS_STAGE_ENDPOINT: ${{ secrets.IT_IMS_STAGE_ENDPOINT }} + IT_IMS_STAGE_CLIENT_ID: ${{ secrets.IT_IMS_STAGE_CLIENT_ID }} + IT_IMS_STAGE_CLIENT_SECRET: ${{ secrets.IT_IMS_STAGE_CLIENT_SECRET }} + IT_IMS_STAGE_SCOPES: ${{ secrets.IT_IMS_STAGE_SCOPES }} - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 From 352c5bd58307411f9fa4d0a20254d35f9915b28c Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 17 Dec 2025 12:05:10 +0100 Subject: [PATCH 19/32] chore: wrong task --- .github/workflows/build.yaml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index caabc71..9a2c3a0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -32,12 +32,6 @@ jobs: run: npm run test - name: Run the integration tests run: npm run test:it - env: - IT_IMS_STAGE_ENDPOINT: ${{ secrets.IT_IMS_STAGE_ENDPOINT }} - IT_IMS_STAGE_CLIENT_ID: ${{ secrets.IT_IMS_STAGE_CLIENT_ID }} - IT_IMS_STAGE_CLIENT_SECRET: ${{ secrets.IT_IMS_STAGE_CLIENT_SECRET }} - IT_IMS_STAGE_SCOPES: ${{ secrets.IT_IMS_STAGE_SCOPES }} - - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: @@ -69,6 +63,11 @@ jobs: - name: Post-Deployment Integration Test run: npm run test:postdeploy + env: + IT_IMS_STAGE_ENDPOINT: ${{ secrets.IT_IMS_STAGE_ENDPOINT }} + IT_IMS_STAGE_CLIENT_ID: ${{ secrets.IT_IMS_STAGE_CLIENT_ID }} + IT_IMS_STAGE_CLIENT_SECRET: ${{ secrets.IT_IMS_STAGE_CLIENT_SECRET }} + IT_IMS_STAGE_SCOPES: ${{ secrets.IT_IMS_STAGE_SCOPES }} - name: Semantic Release (Dry Run) run: npm run semantic-release-dry env: From 9f5ff0e2f4c02bd8abb31a68baa5a4014a92991e Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 17 Dec 2025 14:35:50 +0100 Subject: [PATCH 20/32] chore: move branch name inside repo name --- deploy-ci-version.sh | 6 +++--- test/it/smoke.test.js | 14 +++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/deploy-ci-version.sh b/deploy-ci-version.sh index e83eea3..1933b9a 100755 --- a/deploy-ci-version.sh +++ b/deploy-ci-version.sh @@ -48,14 +48,14 @@ VERSION_PREVIEW_URL=$(echo "$OUTPUT" | grep "Version Preview URL:" | sed 's/.*Ve cat > .deployment-env << EOF export WORKER_VERSION_ID="$WORKER_VERSION_ID" export VERSION_PREVIEW_URL="$VERSION_PREVIEW_URL" -export VERSION_PREVIEW_ORG="ci-test-org-$BRANCH" +export VERSION_PREVIEW_BRANCH="$BRANCH" EOF # If running in GitHub Actions, also write to GITHUB_ENV if [ -n "$GITHUB_ENV" ]; then echo "WORKER_VERSION_ID=$WORKER_VERSION_ID" >> "$GITHUB_ENV" echo "VERSION_PREVIEW_URL=$VERSION_PREVIEW_URL" >> "$GITHUB_ENV" - echo "VERSION_PREVIEW_ORG=ci-test-org-$BRANCH" >> "$GITHUB_ENV" + echo "VERSION_PREVIEW_BRANCH=$BRANCH" >> "$GITHUB_ENV" echo "Variables exported to GitHub Actions environment" fi @@ -68,6 +68,6 @@ echo "----------------------------------------" echo "Deployment information:" echo "WORKER_VERSION_ID=$WORKER_VERSION_ID" echo "VERSION_PREVIEW_URL=$VERSION_PREVIEW_URL" -echo "VERSION_PREVIEW_ORG=ci-test-org-$BRANCH" +echo "VERSION_PREVIEW_BRANCH=$BRANCH" echo "----------------------------------------" diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index c37c037..7ac93b0 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -40,8 +40,8 @@ const IMS_STAGE = { const S3_DIR = './test/it/bucket'; -const LOCAL_ORG = 'test-org'; -const REPO = 'test-repo'; +const IT_ORG = 'da-admin-ci-it-org'; +const IT_DEFAULT_REPO = 'test-repo'; describe('Integration Tests: smoke tests', function () { let s3rver; @@ -51,8 +51,8 @@ describe('Integration Tests: smoke tests', function () { const context = { serverUrl: LOCAL_SERVER_URL, - org: LOCAL_ORG, - repo: REPO, + org: IT_ORG, + repo: IT_DEFAULT_REPO, accessToken: '', }; @@ -215,7 +215,11 @@ describe('Integration Tests: smoke tests', function () { } context.serverUrl = process.env.VERSION_PREVIEW_URL; - context.org = process.env.VERSION_PREVIEW_ORG; + const branch = process.env.VERSION_PREVIEW_BRANCH; + if (!branch) { + throw new Error('VERSION_PREVIEW_BRANCH must be set'); + } + context.repo += `-${branch.toLowerCase().replace(/[ /_]/g, '-')}`; context.accessToken = await connectToIMSStage(); context.email = await getIMSProfile(context.accessToken); context.local = false; From 0c778781c0d75a8fdff9f53147699a8804369884 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 17 Dec 2025 14:43:53 +0100 Subject: [PATCH 21/32] chore: relax test --- test/it/it-tests.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/it/it-tests.js b/test/it/it-tests.js index ca98859..c136232 100644 --- a/test/it/it-tests.js +++ b/test/it/it-tests.js @@ -268,8 +268,10 @@ export default (ctx) => describe('Integration Tests: it tests', function () { 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}`); + assert.ok(body.length > 0, `Expected at least 1 repo, got ${body.length}`); + // need to find the current repo in the list + const repoItem = body.find((item) => item.name === repo); + assert.ok(repoItem, `Expected ${repo} to be in the list`); }); it('cannot delete an object via HTTP request if not authenticated', async () => { From ddb8538344931b08eb1cc4ddc4c0734aeccd186c Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 17 Dec 2025 14:44:09 +0100 Subject: [PATCH 22/32] chore: allow copy/paste --- deploy-ci-version.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy-ci-version.sh b/deploy-ci-version.sh index 1933b9a..39529ec 100755 --- a/deploy-ci-version.sh +++ b/deploy-ci-version.sh @@ -66,8 +66,8 @@ echo "" echo "Version deployment complete!" echo "----------------------------------------" echo "Deployment information:" -echo "WORKER_VERSION_ID=$WORKER_VERSION_ID" -echo "VERSION_PREVIEW_URL=$VERSION_PREVIEW_URL" -echo "VERSION_PREVIEW_BRANCH=$BRANCH" +echo "export WORKER_VERSION_ID=$WORKER_VERSION_ID" +echo "export VERSION_PREVIEW_URL=$VERSION_PREVIEW_URL" +echo "export VERSION_PREVIEW_BRANCH=$BRANCH" echo "----------------------------------------" From 4313575de0163239025698a8d5599302c7ccfd9a Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 17 Dec 2025 14:55:28 +0100 Subject: [PATCH 23/32] chore: create folder is more simple --- test/it/it-tests.js | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/test/it/it-tests.js b/test/it/it-tests.js index c136232..f674131 100644 --- a/test/it/it-tests.js +++ b/test/it/it-tests.js @@ -93,17 +93,6 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); }); - it('cannot delete root folder if not authenticated', async () => { - const { - serverUrl, org, repo, - } = ctx; - const url = `${serverUrl}/source/${org}/${repo}`; - const resp = await fetch(url, { - method: 'DELETE', - }); - assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); - }); - it('delete root folder to cleanup the bucket', async () => { const { serverUrl, org, repo, accessToken, @@ -128,14 +117,9 @@ export default (ctx) => describe('Integration Tests: it tests', function () { const { serverUrl, org, repo, accessToken, } = ctx; - const formData = new FormData(); - const blob = new Blob(['{}'], { type: 'application/json' }); - const file = new File([blob], `${repo}.props`, { type: 'application/json' }); - formData.append('data', file); - const resp = await fetch(`${serverUrl}/source/${org}/${repo}/${repo}.props`, { - method: 'POST', - body: formData, + const resp = await fetch(`${serverUrl}/source/${org}/${repo}`, { + method: 'PUT', headers: { Authorization: `Bearer ${accessToken}` }, }); assert.ok([200, 201].includes(resp.status), `Expected 200 or 201 for marker, got ${resp.status}`); From 94aa0f29856eb034a8e12abde43773d39eb27ac7 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 17 Dec 2025 15:03:01 +0100 Subject: [PATCH 24/32] chore: final cleanup --- test/it/it-tests.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/it/it-tests.js b/test/it/it-tests.js index f674131..8f78ba0 100644 --- a/test/it/it-tests.js +++ b/test/it/it-tests.js @@ -290,6 +290,18 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.strictEqual(resp.status, 404, `Expected 404 Not Found, got ${resp.status}`); }); + it('should do a final delete of the root folder', async () => { + const { + serverUrl, org, repo, accessToken, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}`; + const resp = await fetch(url, { + method: 'DELETE', + headers: { Authorization: `Bearer ${accessToken}` }, + }); + assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); + }); + it('should logout via HTTP request', async () => { const { serverUrl, accessToken } = ctx; const url = `${serverUrl}/logout`; From 5cb81596723f3ce301e140cc5703e7f42e26caa0 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 17 Dec 2025 15:03:50 +0100 Subject: [PATCH 25/32] chore: doc --- deploy-ci-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy-ci-version.sh b/deploy-ci-version.sh index 39529ec..eea5c2e 100755 --- a/deploy-ci-version.sh +++ b/deploy-ci-version.sh @@ -65,7 +65,7 @@ wrangler versions deploy -y -e ci --version-id "$WORKER_VERSION_ID" echo "" echo "Version deployment complete!" echo "----------------------------------------" -echo "Deployment information:" +echo "Deployment information: (copy inside a .deployment-env file to run locally)" echo "export WORKER_VERSION_ID=$WORKER_VERSION_ID" echo "export VERSION_PREVIEW_URL=$VERSION_PREVIEW_URL" echo "export VERSION_PREVIEW_BRANCH=$BRANCH" From d32804814f409d996dee99c38adf7ba1029a232a Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 22 Dec 2025 11:59:57 +0100 Subject: [PATCH 26/32] chore: doc + rename --- deploy-ci-version.sh | 14 ++--- integration-tests.md | 131 ++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- test/it/smoke.test.js | 10 ++-- 4 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 integration-tests.md diff --git a/deploy-ci-version.sh b/deploy-ci-version.sh index eea5c2e..4e97e21 100755 --- a/deploy-ci-version.sh +++ b/deploy-ci-version.sh @@ -42,20 +42,20 @@ echo "$OUTPUT" # Parse the deployment information WORKER_VERSION_ID=$(echo "$OUTPUT" | grep "Worker Version ID:" | sed 's/.*Worker Version ID: //') -VERSION_PREVIEW_URL=$(echo "$OUTPUT" | grep "Version Preview URL:" | sed 's/.*Version Preview URL: //') +WORKER_PREVIEW_URL=$(echo "$OUTPUT" | grep "Version Preview URL:" | sed 's/.*Version Preview URL: //') # Write to a file that can be sourced (for local use) cat > .deployment-env << EOF export WORKER_VERSION_ID="$WORKER_VERSION_ID" -export VERSION_PREVIEW_URL="$VERSION_PREVIEW_URL" -export VERSION_PREVIEW_BRANCH="$BRANCH" +export WORKER_PREVIEW_URL="$WORKER_PREVIEW_URL" +export WORKER_PREVIEW_BRANCH="$BRANCH" EOF # If running in GitHub Actions, also write to GITHUB_ENV if [ -n "$GITHUB_ENV" ]; then echo "WORKER_VERSION_ID=$WORKER_VERSION_ID" >> "$GITHUB_ENV" - echo "VERSION_PREVIEW_URL=$VERSION_PREVIEW_URL" >> "$GITHUB_ENV" - echo "VERSION_PREVIEW_BRANCH=$BRANCH" >> "$GITHUB_ENV" + echo "WORKER_PREVIEW_URL=$WORKER_PREVIEW_URL" >> "$GITHUB_ENV" + echo "WORKER_PREVIEW_BRANCH=$BRANCH" >> "$GITHUB_ENV" echo "Variables exported to GitHub Actions environment" fi @@ -67,7 +67,7 @@ echo "Version deployment complete!" echo "----------------------------------------" echo "Deployment information: (copy inside a .deployment-env file to run locally)" echo "export WORKER_VERSION_ID=$WORKER_VERSION_ID" -echo "export VERSION_PREVIEW_URL=$VERSION_PREVIEW_URL" -echo "export VERSION_PREVIEW_BRANCH=$BRANCH" +echo "export WORKER_PREVIEW_URL=$WORKER_PREVIEW_URL" +echo "export WORKER_PREVIEW_BRANCH=$BRANCH" echo "----------------------------------------" diff --git a/integration-tests.md b/integration-tests.md new file mode 100644 index 0000000..ff12d88 --- /dev/null +++ b/integration-tests.md @@ -0,0 +1,131 @@ +# Integration Tests + +The `da-admin` worker includes a suite of integration tests designed as "smoke tests". These tests validate: + +- **Deployment Integrity**: Ensures the worker can be successfully deployed to the Cloudflare Workers runtime. +- **Core Functionality**: Verifies critical features such as authentication, read/write operations, and permission handling function correctly end-to-end. + +## Architecture + +The test entry point is [`./test/it/smoke.test.js`](./test/it/smoke.test.js), which sets up the environment and executes the test suite defined in [`./test/it/it-tests.js`](./test/it/it-tests.js). The tests can run in two modes: + +1. **Local Mode**: Runs entirely on the local machine using mocks and local servers. +2. **Stage Mode**: Runs against a deployed version of the worker on Cloudflare (used in CI). + +### 1. Local Mode + +**Local Mode** is the default for development. It orchestrates a local environment consisting of: +- **`wrangler dev`**: Runs the `da-admin` worker locally. +- **`S3rver`**: A local S3-compatible object storage server to mock R2/S3. +- **Mock IMS Server**: A local HTTP server simulating Adobe IMS for authentication. + +**Configuration:** +- Environment variables are automatically loaded from [`./.dev.vars.it`](./.dev.vars.it). +- No manual configuration is typically required. +- **Note**: In **Local Mode**, the DA configuration is ephemeral and set up before each test run. + +**How to Run:** +```bash +npm run test:it +``` + +### 2. Stage Mode (CI/CD) + +In **Stage Mode**, tests execute against a live worker deployed to Cloudflare. This verifies the actual deployment artifacts and Cloudflare environment behavior. + +#### CI/CD Pipeline Flow + +The GitHub Actions workflow executes these tests in two phases: + +1. **Deployment**: + - `npm run deploy:ci`: Uploads a new version of the worker (tagged with the branch name) to the `ci` environment. + - Generates a `.deployment-env` file containing the `WORKER_VERSION_ID`, `WORKER_PREVIEW_URL` and `WORKER_PREVIEW_BRANCH`. + +2. **Verification**: + - `npm run test:postdeploy`: Sources the `.deployment-env` file and runs the test suite. + - When `WORKER_PREVIEW_URL` is present in the environment, [`smoke.test.js`](./test/it/smoke.test.js) switches to **Stage Mode**. + - Tests authenticates against a real IMS environment (Stage/Prod) and requests are sent to the deployed worker. + +#### Running Stage Tests Locally + +To debug CI failures or test against a deployed worker from your local machine: + +1. **Deploy the Worker**: + ```bash + npm run deploy:ci + ``` + This script will generate the `.deployment-env` file in your root directory. + +2. **Configure Credentials**: + Create a `.env` file (or set environment variables) with the required IMS credentials for the test account: + ```env + IT_IMS_STAGE_ENDPOINT=https://ims-na1.adobelogin.com + IT_IMS_STAGE_CLIENT_ID= + IT_IMS_STAGE_CLIENT_SECRET= + IT_IMS_STAGE_SCOPES=openid,AdobeID,aem.frontend.all,read_organizations,additional_info.projectedProductContext + ``` + +3. **Run the Tests**: + ```bash + # Loads the deployment vars and runs the tests + npm run test:postdeploy2 + ``` + +### Persistence & Configuration + +In **Stage Mode**, the tests rely on the `DA_CONFIG_STAGE` KV storage for permissions. This configuration is persistent. + +If the configuration is lost or needs to be reset, the expected permission model is: + +```json +{ + "total": 2, + "limit": 2, + "offset": 0, + "data": [ + { + "path": "CONFIG", + "groups": "", + "actions": "write" + }, + { + "path": "/+**", + "groups": "", + "actions": "write" + } + ], + ":type": "sheet", + ":sheetname": "permissions" +} +``` + +## IMS Configuration + +In **Stage Mode**, tests execute against the **IMS Stage** environment. + +### Prerequisites + +1. **Worker Configuration**: The `IMS_ORIGIN` secret for the `da-admin` worker (CI environment) must point to the IMS Stage endpoint. +2. **User Existence**: Test users must exist and belong to an IMS Stage organization. No specific organization permissions are required beyond basic membership. +3. **DA Configuration**: The test users must be explicitly granted permissions in the `DA_CONFIG` (as shown in the [Persistence & Configuration](#persistence--configuration) section). + +### Test Users Setup + +The integration tests use dedicated service accounts defined in the [Adobe Stage Developer Console](https://developer-stage.adobe.com/) under the `Document Authoring Stage` organization. Two distinct projects were created to simulate different user roles: + +- **Authenticated User Project**: + - **Purpose**: Simulates a user who is logged in but may not have specific permissions (used for negative testing or basic access). + - **Credentials**: Defined in CI secrets as `IT_IMS_STAGE_CLIENT_ID` / `IT_IMS_STAGE_CLIENT_SECRET`. + - **API**: Uses `Edge Delivery Service` to create OAuth Server-to-Server credentials. + +- **Authorized User Project**: + - **Purpose**: Simulates a user with full read/write permissions. + - **Credentials**: (Currently the tests primarily use one set of credentials which are authorized in the config). + - **API**: Uses `Edge Delivery Service` to create OAuth Server-to-Server credentials. + +> **Notes on Setup:** +> 1. **Multiple Projects**: Two separate projects were created because generating multiple independent credentials within a single project was not supported. +> 2. **Role Distinction**: The distinction between "authenticated" and "authorized" is managed entirely within the `DA_CONFIG` permissions sheet, not in IMS. The project naming reflects the intended use case. +> 3. **API Selection**: The `Edge Delivery Service` API was selected for convenience. Any API service can be used provided it: +> - Supports IMS connection. +> - Includes the `read_organizations` scope. diff --git a/package.json b/package.json index 110bbde..2bfcff5 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test": "c8 mocha --spec=test/**/*.test.js --ignore=test/it/**/*.test.js", "test:it": "mocha test/it/**/*.test.js", "test:all": "npm run test && npm run test:it", - "test:postdeploy": "if [ -f .deployment-env ]; then . ./.deployment-env && rm .deployment-env; fi && npm run test:it", + "test:postdeploy": "if [ -f .deployment-env ]; then . ./.deployment-env; fi && npm run test:it", "dev": "wrangler dev -e dev", "deploy:ci": "./deploy-ci-version.sh ci", "deploy:prod": "node prepare-deploy.js && wrangler deploy -e production -c wrangler-versioned.toml", diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index 7ac93b0..5858587 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -207,17 +207,17 @@ describe('Integration Tests: smoke tests', function () { // Increase timeout for server startup this.timeout(30000); - if (process.env.VERSION_PREVIEW_URL) { + if (process.env.WORKER_PREVIEW_URL) { if (!IMS_STAGE.ENDPOINT || !IMS_STAGE.CLIENT_ID || !IMS_STAGE.CLIENT_SECRET || !IMS_STAGE.SCOPES) { throw new Error('IT_IMS_STAGE_ENDPOINT, IT_IMS_STAGE_CLIENT_ID, ' + 'IT_IMS_STAGE_CLIENT_SECRET, and IT_IMS_STAGE_SCOPES must be set'); } - context.serverUrl = process.env.VERSION_PREVIEW_URL; - const branch = process.env.VERSION_PREVIEW_BRANCH; + context.serverUrl = process.env.WORKER_PREVIEW_URL; + const branch = process.env.WORKER_PREVIEW_BRANCH; if (!branch) { - throw new Error('VERSION_PREVIEW_BRANCH must be set'); + throw new Error('WORKER_PREVIEW_BRANCH must be set'); } context.repo += `-${branch.toLowerCase().replace(/[ /_]/g, '-')}`; context.accessToken = await connectToIMSStage(); @@ -238,7 +238,7 @@ describe('Integration Tests: smoke tests', function () { }); after(async function () { - if (process.env.VERSION_PREVIEW_URL) { + if (process.env.WORKER_PREVIEW_URL) { return; } From 1690876179293ec0a65c4fc8fb464690fcf24180 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 22 Dec 2025 12:15:37 +0100 Subject: [PATCH 27/32] chore: more doc --- integration-tests.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration-tests.md b/integration-tests.md index ff12d88..f77063e 100644 --- a/integration-tests.md +++ b/integration-tests.md @@ -33,6 +33,8 @@ npm run test:it In **Stage Mode**, tests execute against a live worker deployed to Cloudflare. This verifies the actual deployment artifacts and Cloudflare environment behavior. +The tests create a repository `test-repo-` under the `da-admin-ci-it-org` located in the `aem-content-stage` bucket. The config is predefined for this org and defines permissions for the test users (see below). + #### CI/CD Pipeline Flow The GitHub Actions workflow executes these tests in two phases: From 3ba3e3f2e672584150583344861eaa50abaabd63 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 22 Dec 2025 14:12:18 +0100 Subject: [PATCH 28/32] chore: doc --- integration-tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests.md b/integration-tests.md index f77063e..97ab7d1 100644 --- a/integration-tests.md +++ b/integration-tests.md @@ -113,7 +113,7 @@ In **Stage Mode**, tests execute against the **IMS Stage** environment. ### Test Users Setup -The integration tests use dedicated service accounts defined in the [Adobe Stage Developer Console](https://developer-stage.adobe.com/) under the `Document Authoring Stage` organization. Two distinct projects were created to simulate different user roles: +The integration tests use dedicated service accounts defined in the [Adobe Stage Developer Console](https://developer-stage.adobe.com/) (VPN required) under the `Document Authoring Stage` organization. Two distinct projects were created to simulate different user roles: - **Authenticated User Project**: - **Purpose**: Simulates a user who is logged in but may not have specific permissions (used for negative testing or basic access). From 624a3bc3cf17458761b258cca73b0f6eee7506af Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 22 Dec 2025 16:04:58 +0100 Subject: [PATCH 29/32] chore: multiple users tests --- integration-tests.md | 14 +- test/it/it-tests.js | 314 +++++++++++++++++++++++++++++++++--------- test/it/smoke.test.js | 101 +++++++++----- 3 files changed, 324 insertions(+), 105 deletions(-) diff --git a/integration-tests.md b/integration-tests.md index 97ab7d1..3854bd8 100644 --- a/integration-tests.md +++ b/integration-tests.md @@ -59,11 +59,13 @@ To debug CI failures or test against a deployed worker from your local machine: This script will generate the `.deployment-env` file in your root directory. 2. **Configure Credentials**: - Create a `.env` file (or set environment variables) with the required IMS credentials for the test account: + Create a `.env` file (or set environment variables) with the required IMS credentials for the test accounts: ```env IT_IMS_STAGE_ENDPOINT=https://ims-na1.adobelogin.com - IT_IMS_STAGE_CLIENT_ID= - IT_IMS_STAGE_CLIENT_SECRET= + IT_IMS_STAGE_CLIENT_ID_SUPER_USER= + IT_IMS_STAGE_CLIENT_SECRET_SUPER_USER= + IT_IMS_STAGE_CLIENT_ID_LIMITED_USER= + IT_IMS_STAGE_CLIENT_SECRET_LIMITED_USER= IT_IMS_STAGE_SCOPES=openid,AdobeID,aem.frontend.all,read_organizations,additional_info.projectedProductContext ``` @@ -87,12 +89,12 @@ If the configuration is lost or needs to be reset, the expected permission model "data": [ { "path": "CONFIG", - "groups": "", + "groups": "", "actions": "write" }, { "path": "/+**", - "groups": "", + "groups": "", "actions": "write" } ], @@ -117,7 +119,7 @@ The integration tests use dedicated service accounts defined in the [Adobe Stage - **Authenticated User Project**: - **Purpose**: Simulates a user who is logged in but may not have specific permissions (used for negative testing or basic access). - - **Credentials**: Defined in CI secrets as `IT_IMS_STAGE_CLIENT_ID` / `IT_IMS_STAGE_CLIENT_SECRET`. + - **Credentials**: Defined in CI secrets as `IT_IMS_STAGE_CLIENT_ID_SUPER_USER` / `IT_IMS_STAGE_CLIENT_SECRET_SUPER_USER`. - **API**: Uses `Edge Delivery Service` to create OAuth Server-to-Server credentials. - **Authorized User Project**: diff --git a/test/it/it-tests.js b/test/it/it-tests.js index 8f78ba0..3909df2 100644 --- a/test/it/it-tests.js +++ b/test/it/it-tests.js @@ -16,22 +16,22 @@ export default (ctx) => describe('Integration Tests: it tests', function () { // Enable bail to stop on first failure - tests are interdependent this.bail(true); - it('should set org config via HTTP request', async function shouldSetOrgConfig() { + it('should set org config', async function shouldSetOrgConfig() { if (!ctx.local) { // in stage, the config is already set and we should not overwrite it // to preserve the setup and be able to access the content this.skip(); } const { - serverUrl, org, accessToken, + serverUrl, org, superUser, } = ctx; const configData = JSON.stringify({ total: 2, limit: 2, offset: 0, data: [ - { path: 'CONFIG', groups: 'test@example.com', actions: 'write' }, - { path: '/+**', groups: 'test@example.com', actions: 'write' }, + { path: 'CONFIG', groups: superUser.email, actions: 'write' }, + { path: '/+**', groups: superUser.email, actions: 'write' }, ], ':type': 'sheet', ':sheetname': 'permissions', @@ -44,19 +44,19 @@ export default (ctx) => describe('Integration Tests: it tests', function () { const resp = await fetch(url, { method: 'POST', body: formData, - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); }); - it('should get org config via HTTP request', async () => { + it('[super user] should get org config', async () => { const { - serverUrl, org, accessToken, email, + serverUrl, org, superUser, } = ctx; const url = `${serverUrl}/config/${org}`; const resp = await fetch(url, { - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); @@ -64,68 +64,130 @@ export default (ctx) => describe('Integration Tests: it tests', function () { 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].groups, email, `Expected user email, got ${body.data[0].groups}`); + assert.strictEqual(body.data[0].groups, superUser.email, `Expected user email, got ${body.data[0].groups}`); assert.strictEqual(body.data[0].actions, 'write', `Expected write, got ${body.data[0].actions}`); assert.strictEqual(body.data[1].path, '/+**', `Expected /+**, got ${body.data[1].path}`); - assert.strictEqual(body.data[1].groups, email, `Expected user email, got ${body.data[1].groups}`); + assert.strictEqual(body.data[1].groups, superUser.email, `Expected user email, got ${body.data[1].groups}`); assert.strictEqual(body.data[1].actions, 'write', `Expected write, got ${body.data[1].actions}`); }); - it('not allowed to read if not authenticated', async () => { + it('[anonymous] cannot delete root folder', async () => { const { serverUrl, org, repo, } = ctx; const url = `${serverUrl}/source/${org}/${repo}`; const resp = await fetch(url, { - method: 'GET', + method: 'DELETE', }); assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); }); - it('cannot delete root folder if not authenticated', async () => { + it('[limited user] cannot delete root folder', async () => { const { - serverUrl, org, repo, + serverUrl, org, repo, limitedUser, } = ctx; const url = `${serverUrl}/source/${org}/${repo}`; const resp = await fetch(url, { method: 'DELETE', + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, }); - assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); }); - it('delete root folder to cleanup the bucket', async () => { + it('[super user] delete root folder to cleanup the bucket', async () => { const { - serverUrl, org, repo, accessToken, + serverUrl, org, repo, superUser, } = ctx; const url = `${serverUrl}/source/${org}/${repo}`; const resp = await fetch(url, { method: 'DELETE', - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); // validate bucket is empty const listResp = await fetch(`${serverUrl}/list/${org}/${repo}`, { - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.strictEqual(listResp.status, 200, `Expected 200 OK, got ${listResp.status}`); const listBody = await listResp.json(); assert.strictEqual(listBody.length, 0, `Expected 0 items, got ${listBody.length}`); }); - it('should create a repo via HTTP request', async () => { + it('[super user] should create a repo', async () => { const { - serverUrl, org, repo, accessToken, + serverUrl, org, repo, superUser, } = ctx; const resp = await fetch(`${serverUrl}/source/${org}/${repo}`, { method: 'PUT', - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.ok([200, 201].includes(resp.status), `Expected 200 or 201 for marker, got ${resp.status}`); }); - it('cannot post an object via HTTP request if not authenticated', async () => { + it('[anonymous] not allowed to read', async () => { + const { + serverUrl, org, repo, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}`; + const resp = await fetch(url, { + method: 'GET', + }); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + + it('[limited user] not allowed to read', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}`; + const resp = await fetch(url, { + method: 'GET', + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); + }); + + it('[anonymous] cannot list repos', async () => { + const { + serverUrl, org, + } = ctx; + const url = `${serverUrl}/list/${org}`; + const resp = await fetch(url); + assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + }); + + it('[limited user] cannot list repos', async () => { + const { + serverUrl, org, limitedUser, + } = ctx; + const url = `${serverUrl}/list/${org}`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); + }); + + it('[super user] should list repos', async () => { + const { + serverUrl, org, repo, superUser, + } = ctx; + const url = `${serverUrl}/list/${org}`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + + const body = await resp.json(); + assert.ok(body.length > 0, `Expected at least 1 repo, got ${body.length}`); + // need to find the current repo in the list + const repoItem = body.find((item) => item.name === repo); + assert.ok(repoItem, `Expected ${repo} to be in the list`); + }); + + it('[anonymous] cannot create a page', async () => { const { serverUrl, org, repo, } = ctx; @@ -148,9 +210,33 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); }); - it('should post an object via HTTP request', async () => { + it('[limited user] cannot create a page', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + // Now create the actual page + const key = 'test-folder/page1'; + const ext = '.html'; + + // Create FormData with the HTML file + const formData = new FormData(); + const blob = new Blob(['

Page 1

'], { type: 'text/html' }); + const file = new File([blob], 'page1.html', { type: 'text/html' }); + formData.append('data', file); + + const url = `${serverUrl}/source/${org}/${repo}/${key}${ext}`; + const resp = await fetch(url, { + method: 'POST', + body: formData, + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); + }); + + it('[super user] should create pages', async () => { const { - serverUrl, org, repo, accessToken, + serverUrl, org, repo, superUser, } = ctx; // Now create the actual page const key = 'test-folder/page1'; @@ -166,7 +252,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { let resp = await fetch(url, { method: 'POST', body: formData, - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); @@ -179,7 +265,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { // validate page is here (include extension in GET request) resp = await fetch(`${serverUrl}/source/${org}/${repo}/${key}${ext}`, { - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); @@ -197,68 +283,155 @@ export default (ctx) => describe('Integration Tests: it tests', function () { resp = await fetch(`${serverUrl}/source/${org}/${repo}/${key2}${ext2}`, { method: 'POST', body: formData2, - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); }); - it('cannot list objects via HTTP request if not authenticated', async () => { + it('[limited user] cannot read page1', async () => { const { - serverUrl, org, repo, + serverUrl, org, repo, limitedUser, } = ctx; - const url = `${serverUrl}/list/${org}/${repo}`; - const resp = await fetch(url); - assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); + const url = `${serverUrl}/source/${org}/${repo}/test-folder/page1.html`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); }); - it('should list objects via HTTP request', async () => { + it('[limited user] cannot read page2', async () => { const { - serverUrl, org, repo, accessToken, + serverUrl, org, repo, limitedUser, } = ctx; - const key = 'test-folder'; - - const url = `${serverUrl}/list/${org}/${repo}/${key}`; + const url = `${serverUrl}/source/${org}/${repo}/test-folder/page2.html`; const resp = await fetch(url, { - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); + }); + + it('[super user] should update the config to allow limited user to read page2', async () => { + const { + serverUrl, org, repo, superUser, limitedUser, + } = ctx; + // read config + const url = `${serverUrl}/config/${org}`; + let resp = await fetch(url, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + const body = await resp.json(); + + // add the new config data + const newConfigData = [ + ...body.data, + { path: `/${repo}/test-folder/page2.html`, groups: limitedUser.email, actions: 'read' }, + ]; + + // post the new config + const formData = new FormData(); + formData.append('config', JSON.stringify({ + total: newConfigData.length, + limit: newConfigData.length, + offset: 0, + data: newConfigData, + })); + resp = await fetch(url, { + method: 'POST', + body: formData, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); + assert.strictEqual(resp.status, 201, `Expected 201 Created, got ${resp.status}`); + }); + it('[limited user] can now read page2', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}/test-folder/page2.html`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + }); - const body = await resp.json(); + it('[super user] should remove added entries to clean up the config', async () => { + const { + serverUrl, org, repo, superUser, + } = ctx; + const url = `${serverUrl}/config/${org}`; + let resp = await fetch(url, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + let body = await resp.json(); + const newConfigData = body.data.filter((item) => item.path !== `/${repo}/test-folder/page2.html`); + const formData = new FormData(); + formData.append('config', JSON.stringify({ + total: newConfigData.length, + limit: newConfigData.length, + offset: 0, + data: newConfigData, + })); + resp = await fetch(url, { + method: 'POST', + body: formData, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 201, `Expected 201 Created, got ${resp.status}`); + resp = await fetch(url, { + headers: { Authorization: `Bearer ${superUser.accessToken}` }, + }); + 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].groups, superUser.email, `Expected user email, got ${body.data[0].groups}`); + assert.strictEqual(body.data[0].actions, 'write', `Expected write, got ${body.data[0].actions}`); + assert.strictEqual(body.data[1].path, '/+**', `Expected /+**, got ${body.data[1].path}`); + assert.strictEqual(body.data[1].groups, superUser.email, `Expected user email, got ${body.data[1].groups}`); + assert.strictEqual(body.data[1].actions, 'write', `Expected write, got ${body.data[1].actions}`); + }); - const fileNames = body.map((item) => item.name); - assert.ok(fileNames.includes('page1'), 'Should list page1'); - assert.ok(fileNames.includes('page2'), 'Should list page2'); + // TODO: currently the auth session is stored in memory, so the limited user can still read page2 + it.skip('[limited user] cannot read page2 anymore', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}/test-folder/page2.html`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); }); - it('cannot list repos via HTTP request if not authenticated', async () => { + it('[anonymous] cannot list objects', async () => { const { - serverUrl, org, + serverUrl, org, repo, } = ctx; - const url = `${serverUrl}/list/${org}`; + const url = `${serverUrl}/list/${org}/${repo}`; const resp = await fetch(url); assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); }); - it('should list repos via HTTP request', async () => { + it('[super user] should list objects', async () => { const { - serverUrl, org, repo, accessToken, + serverUrl, org, repo, superUser, } = ctx; - const url = `${serverUrl}/list/${org}`; + const key = 'test-folder'; + + const url = `${serverUrl}/list/${org}/${repo}/${key}`; const resp = await fetch(url, { - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); const body = await resp.json(); - assert.ok(body.length > 0, `Expected at least 1 repo, got ${body.length}`); - // need to find the current repo in the list - const repoItem = body.find((item) => item.name === repo); - assert.ok(repoItem, `Expected ${repo} to be in the list`); + + const fileNames = body.map((item) => item.name); + assert.ok(fileNames.includes('page1'), 'Should list page1'); + assert.ok(fileNames.includes('page2'), 'Should list page2'); }); - it('cannot delete an object via HTTP request if not authenticated', async () => { + it('[anonymous] cannot delete an object', async () => { const { serverUrl, org, repo, key, } = ctx; @@ -269,9 +442,9 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.strictEqual(resp.status, 401, `Expected 401 Unauthorized, got ${resp.status}`); }); - it('should delete an object via HTTP request', async () => { + it('[super user] should delete an object', async () => { const { - serverUrl, org, repo, accessToken, + serverUrl, org, repo, superUser, } = ctx; const key = 'test-folder/page2'; const ext = '.html'; @@ -279,35 +452,46 @@ export default (ctx) => describe('Integration Tests: it tests', function () { const url = `${serverUrl}/source/${org}/${repo}/${key}${ext}`; let resp = await fetch(url, { method: 'DELETE', - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); // validate page is not here resp = await fetch(`${serverUrl}/source/${org}/${repo}/${key}${ext}`, { - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.strictEqual(resp.status, 404, `Expected 404 Not Found, got ${resp.status}`); }); - it('should do a final delete of the root folder', async () => { + it('[super user] should do a final delete of the root folder', async () => { const { - serverUrl, org, repo, accessToken, + serverUrl, org, repo, superUser, } = ctx; const url = `${serverUrl}/source/${org}/${repo}`; const resp = await fetch(url, { method: 'DELETE', - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); }); - it('should logout via HTTP request', async () => { - const { serverUrl, accessToken } = ctx; + it('[limited user] should logout', async () => { + const { serverUrl, limitedUser } = ctx; + const url = `${serverUrl}/logout`; + const resp = await fetch(url, { + method: 'POST', + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + }); + + it('[super user] should logout', async () => { + const { serverUrl, superUser } = ctx; const url = `${serverUrl}/logout`; const resp = await fetch(url, { method: 'POST', - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); diff --git a/test/it/smoke.test.js b/test/it/smoke.test.js index 5858587..3dfac19 100644 --- a/test/it/smoke.test.js +++ b/test/it/smoke.test.js @@ -33,8 +33,10 @@ const IMS_LOCAL_KID = 'ims'; const IMS_STAGE = { ENDPOINT: process.env.IT_IMS_STAGE_ENDPOINT, - CLIENT_ID: process.env.IT_IMS_STAGE_CLIENT_ID, - CLIENT_SECRET: process.env.IT_IMS_STAGE_CLIENT_SECRET, + CLIENT_ID_SUPER_USER: process.env.IT_IMS_STAGE_CLIENT_ID_SUPER_USER, + CLIENT_SECRET_SUPER_USER: process.env.IT_IMS_STAGE_CLIENT_SECRET_SUPER_USER, + CLIENT_ID_LIMITED_USER: process.env.IT_IMS_STAGE_CLIENT_ID_LIMITED_USER, + CLIENT_SECRET_LIMITED_USER: process.env.IT_IMS_STAGE_CLIENT_SECRET_LIMITED_USER, SCOPES: process.env.IT_IMS_STAGE_SCOPES, }; @@ -63,11 +65,22 @@ describe('Integration Tests: smoke tests', function () { } }; - const connectToIMSStage = async () => { + const getIMSProfile = async (accessToken) => { + const res = await fetch(`${IMS_STAGE.ENDPOINT}/ims/profile/v1`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + if (res.ok) { + const json = await res.json(); + return json; + } + throw new Error(`Failed to fetch IMS profile: ${res.status}`); + }; + + const connectToIMS = async (clientId, clientSecret) => { const postData = { grant_type: 'client_credentials', - client_id: IMS_STAGE.CLIENT_ID, - client_secret: IMS_STAGE.CLIENT_SECRET, + client_id: clientId, + client_secret: clientSecret, scope: IMS_STAGE.SCOPES, }; @@ -88,40 +101,51 @@ describe('Integration Tests: smoke tests', function () { if (res.ok) { const json = await res.json(); - return json.access_token; + const profile = await getIMSProfile(json.access_token); + return { + accessToken: json.access_token, + email: profile.email, + userId: profile.userId, + }; } throw new Error(`error response from IMS with status: ${res.status} and body: ${await res.text()}`); }; - const getIMSProfile = async (accessToken) => { - const res = await fetch(`${IMS_STAGE.ENDPOINT}/ims/profile/v1`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - if (res.ok) { - const json = await res.json(); - return json.email; - } - throw new Error(`Failed to fetch IMS profile: ${res.status}`); - }; + /* eslint-disable max-len */ + const connectAsSuperUser = async () => connectToIMS(IMS_STAGE.CLIENT_ID_SUPER_USER, IMS_STAGE.CLIENT_SECRET_SUPER_USER); + + /* eslint-disable max-len */ + const connectAsLimitedUser = async () => connectToIMS(IMS_STAGE.CLIENT_ID_LIMITED_USER, IMS_STAGE.CLIENT_SECRET_LIMITED_USER); + + const localTokenCache = {}; + let IMSPrivateKey; - const getIMSLocalToken = async () => { - const { publicKey, privateKey } = await generateKeyPair('RS256'); + const setupIMSLocalKey = async () => { + const { privateKey, publicKey } = await generateKeyPair('RS256'); + IMSPrivateKey = privateKey; publicKeyJwk = await exportJWK(publicKey); publicKeyJwk.use = 'sig'; publicKeyJwk.kid = IMS_LOCAL_KID; publicKeyJwk.alg = 'RS256'; + }; + const getIMSLocalToken = async (userId) => { + const email = `${userId}@example.com`; const accessToken = await new SignJWT({ - // as: 'ims-na1-stg1', type: 'access_token', - user_id: 'test_user', + user_id: email, created_at: String(Date.now() - 1000), expires_in: '86400000', }) .setProtectedHeader({ alg: 'RS256', kid: IMS_LOCAL_KID }) - .sign(privateKey); + .sign(IMSPrivateKey); - return accessToken; + localTokenCache[accessToken] = { + accessToken, + email, + userId: email, + }; + return localTokenCache[accessToken]; }; const setupIMSServer = async () => { @@ -141,10 +165,16 @@ describe('Integration Tests: smoke tests', function () { res.writeHead(200); res.end(JSON.stringify({ keys: [publicKeyJwk] })); } else if (req.url === '/ims/profile/v1') { + const cachedToken = localTokenCache[req.headers.authorization.split(' ').pop()]; + if (!cachedToken) { + res.writeHead(401); + res.end(JSON.stringify({ error: 'Unauthorized' })); + return; + } res.writeHead(200); res.end(JSON.stringify({ - email: 'test@example.com', - userId: 'test_user', + email: cachedToken.email, + userId: cachedToken.userId, })); } else if (req.url === '/ims/organizations/v5') { res.writeHead(200); @@ -208,25 +238,28 @@ describe('Integration Tests: smoke tests', function () { this.timeout(30000); if (process.env.WORKER_PREVIEW_URL) { - if (!IMS_STAGE.ENDPOINT || !IMS_STAGE.CLIENT_ID - || !IMS_STAGE.CLIENT_SECRET || !IMS_STAGE.SCOPES) { - throw new Error('IT_IMS_STAGE_ENDPOINT, IT_IMS_STAGE_CLIENT_ID, ' - + 'IT_IMS_STAGE_CLIENT_SECRET, and IT_IMS_STAGE_SCOPES must be set'); + if (!IMS_STAGE.ENDPOINT + || !IMS_STAGE.CLIENT_ID_SUPER_USER + || !IMS_STAGE.CLIENT_SECRET_SUPER_USER + || !IMS_STAGE.CLIENT_ID_LIMITED_USER + || !IMS_STAGE.CLIENT_SECRET_LIMITED_USER + || !IMS_STAGE.SCOPES) { + throw new Error('IT_IMS_STAGE_ENDPOINT, IT_IMS_STAGE_CLIENT_ID_SUPER_USER, IT_IMS_STAGE_CLIENT_SECRET_SUPER_USER, IT_IMS_STAGE_CLIENT_ID_LIMITED_USER, IT_IMS_STAGE_CLIENT_SECRET_LIMITED_USER, and IT_IMS_STAGE_SCOPES must be set'); } - + context.local = false; context.serverUrl = process.env.WORKER_PREVIEW_URL; const branch = process.env.WORKER_PREVIEW_BRANCH; if (!branch) { throw new Error('WORKER_PREVIEW_BRANCH must be set'); } context.repo += `-${branch.toLowerCase().replace(/[ /_]/g, '-')}`; - context.accessToken = await connectToIMSStage(); - context.email = await getIMSProfile(context.accessToken); - context.local = false; + context.superUser = await connectAsSuperUser(); + context.limitedUser = await connectAsLimitedUser(); } else { context.local = true; - context.accessToken = await getIMSLocalToken(); - context.email = 'test@example.com'; + await setupIMSLocalKey(); + context.superUser = await getIMSLocalToken('super-user-id'); + context.limitedUser = await getIMSLocalToken('limited-user-id'); cleanupWranglerState(); await setupIMSServer(); From ddbb8a5dd01db1aed62375e41df801692d637bc9 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 22 Dec 2025 16:11:29 +0100 Subject: [PATCH 30/32] chore: provide secrets --- .github/workflows/build.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9a2c3a0..08d4131 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -65,8 +65,10 @@ jobs: run: npm run test:postdeploy env: IT_IMS_STAGE_ENDPOINT: ${{ secrets.IT_IMS_STAGE_ENDPOINT }} - IT_IMS_STAGE_CLIENT_ID: ${{ secrets.IT_IMS_STAGE_CLIENT_ID }} - IT_IMS_STAGE_CLIENT_SECRET: ${{ secrets.IT_IMS_STAGE_CLIENT_SECRET }} + IT_IMS_STAGE_CLIENT_ID_LIMITED_USER: ${{ secrets.IT_IMS_STAGE_CLIENT_ID_LIMITED_USER }} + IT_IMS_STAGE_CLIENT_SECRET_LIMITED_USER: ${{ secrets.IT_IMS_STAGE_CLIENT_SECRET_LIMITED_USER }} + IT_IMS_STAGE_CLIENT_ID_SUPER_USER: ${{ secrets.IT_IMS_STAGE_CLIENT_ID_SUPER_USER }} + IT_IMS_STAGE_CLIENT_SECRET_SUPER_USER: ${{ secrets.IT_IMS_STAGE_CLIENT_SECRET_SUPER_USER }} IT_IMS_STAGE_SCOPES: ${{ secrets.IT_IMS_STAGE_SCOPES }} - name: Semantic Release (Dry Run) run: npm run semantic-release-dry From e43b2a666be0f8bc58e1c8af20622df4308833e2 Mon Sep 17 00:00:00 2001 From: kptdobe Date: Mon, 22 Dec 2025 16:24:56 +0100 Subject: [PATCH 31/32] chore: output user --- test/it/it-tests.js | 58 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/test/it/it-tests.js b/test/it/it-tests.js index 3909df2..c650b04 100644 --- a/test/it/it-tests.js +++ b/test/it/it-tests.js @@ -16,7 +16,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { // Enable bail to stop on first failure - tests are interdependent this.bail(true); - it('should set org config', async function shouldSetOrgConfig() { + it('[super user] should set org config', async function shouldSetOrgConfig() { if (!ctx.local) { // in stage, the config is already set and we should not overwrite it // to preserve the setup and be able to access the content @@ -47,7 +47,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status} - user: ${superUser.email}`); }); it('[super user] should get org config', async () => { @@ -59,7 +59,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.email}`); const body = await resp.json(); assert.strictEqual(body.total, 2, `Expected 2, got ${body.total}`); @@ -91,10 +91,10 @@ export default (ctx) => describe('Integration Tests: it tests', function () { method: 'DELETE', headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, }); - assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); }); - it('[super user] delete root folder to cleanup the bucket', async () => { + it('[super user] should delete root folder to cleanup the bucket', async () => { const { serverUrl, org, repo, superUser, } = ctx; @@ -103,15 +103,15 @@ export default (ctx) => describe('Integration Tests: it tests', function () { method: 'DELETE', headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); + assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status} - user: ${superUser.email}`); // validate bucket is empty const listResp = await fetch(`${serverUrl}/list/${org}/${repo}`, { headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.strictEqual(listResp.status, 200, `Expected 200 OK, got ${listResp.status}`); + assert.strictEqual(listResp.status, 200, `Expected 200 OK, got ${listResp.status} - user: ${superUser.email}`); const listBody = await listResp.json(); - assert.strictEqual(listBody.length, 0, `Expected 0 items, got ${listBody.length}`); + assert.strictEqual(listBody.length, 0, `Expected 0 items, got ${listBody.length} - user: ${superUser.email}`); }); it('[super user] should create a repo', async () => { @@ -123,7 +123,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { method: 'PUT', headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.ok([200, 201].includes(resp.status), `Expected 200 or 201 for marker, got ${resp.status}`); + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201 for marker, got ${resp.status} - user: ${superUser.email}`); }); it('[anonymous] not allowed to read', async () => { @@ -146,7 +146,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { method: 'GET', headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, }); - assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); }); it('[anonymous] cannot list repos', async () => { @@ -166,7 +166,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { const resp = await fetch(url, { headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, }); - assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); }); it('[super user] should list repos', async () => { @@ -178,13 +178,13 @@ export default (ctx) => describe('Integration Tests: it tests', function () { headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.email}`); const body = await resp.json(); - assert.ok(body.length > 0, `Expected at least 1 repo, got ${body.length}`); + assert.ok(body.length > 0, `Expected at least 1 repo, got ${body.length} - user: ${superUser.email}`); // need to find the current repo in the list const repoItem = body.find((item) => item.name === repo); - assert.ok(repoItem, `Expected ${repo} to be in the list`); + assert.ok(repoItem, `Expected ${repo} to be in the list - user: ${superUser.email}`); }); it('[anonymous] cannot create a page', async () => { @@ -231,7 +231,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, }); - assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); }); it('[super user] should create pages', async () => { @@ -255,7 +255,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status} - user: ${superUser.email}`); let body = await resp.json(); assert.strictEqual(body.source.editUrl, `https://da.live/edit#/${org}/${repo}/${key}`); @@ -268,7 +268,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.email}`); body = await resp.text(); assert.strictEqual(body, '

Page 1

'); @@ -285,7 +285,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { body: formData2, headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status}`); + assert.ok([200, 201].includes(resp.status), `Expected 200 or 201, got ${resp.status} - user: ${superUser.email}`); }); it('[limited user] cannot read page1', async () => { @@ -296,7 +296,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { const resp = await fetch(url, { headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, }); - assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); }); it('[limited user] cannot read page2', async () => { @@ -307,7 +307,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { const resp = await fetch(url, { headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, }); - assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); }); it('[super user] should update the config to allow limited user to read page2', async () => { @@ -340,7 +340,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { body: formData, headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.strictEqual(resp.status, 201, `Expected 201 Created, got ${resp.status}`); + assert.strictEqual(resp.status, 201, `Expected 201 Created, got ${resp.status} - user: ${superUser.email}`); }); it('[limited user] can now read page2', async () => { @@ -376,7 +376,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { body: formData, headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.strictEqual(resp.status, 201, `Expected 201 Created, got ${resp.status}`); + assert.strictEqual(resp.status, 201, `Expected 201 Created, got ${resp.status} - user: ${superUser.email}`); resp = await fetch(url, { headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); @@ -399,7 +399,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { const resp = await fetch(url, { headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, }); - assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status}`); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); }); it('[anonymous] cannot list objects', async () => { @@ -422,7 +422,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.email}`); const body = await resp.json(); @@ -454,13 +454,13 @@ export default (ctx) => describe('Integration Tests: it tests', function () { method: 'DELETE', headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); + assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status} - user: ${superUser.email}`); // validate page is not here resp = await fetch(`${serverUrl}/source/${org}/${repo}/${key}${ext}`, { headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.strictEqual(resp.status, 404, `Expected 404 Not Found, got ${resp.status}`); + assert.strictEqual(resp.status, 404, `Expected 404 Not Found, got ${resp.status} - user: ${superUser.email}`); }); it('[super user] should do a final delete of the root folder', async () => { @@ -472,7 +472,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { method: 'DELETE', headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status}`); + assert.strictEqual(resp.status, 204, `Expected 204 No Content, got ${resp.status} - user: ${superUser.email}`); }); it('[limited user] should logout', async () => { @@ -483,7 +483,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, }); - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${limitedUser.email}`); }); it('[super user] should logout', async () => { @@ -494,6 +494,6 @@ export default (ctx) => describe('Integration Tests: it tests', function () { headers: { Authorization: `Bearer ${superUser.accessToken}` }, }); - assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); + assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.userId}`); }); }); From 0856c847f116f3cee1f7fd2f635df9afe437dabc Mon Sep 17 00:00:00 2001 From: kptdobe Date: Tue, 23 Dec 2025 15:24:36 +0100 Subject: [PATCH 32/32] chore: back to green --- test/it/it-tests.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/test/it/it-tests.js b/test/it/it-tests.js index c650b04..25170cd 100644 --- a/test/it/it-tests.js +++ b/test/it/it-tests.js @@ -62,6 +62,7 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status} - user: ${superUser.email}`); const body = await resp.json(); + // check initial config is clean 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].groups, superUser.email, `Expected user email, got ${body.data[0].groups}`); @@ -69,6 +70,8 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.strictEqual(body.data[1].path, '/+**', `Expected /+**, got ${body.data[1].path}`); assert.strictEqual(body.data[1].groups, superUser.email, `Expected user email, got ${body.data[1].groups}`); assert.strictEqual(body.data[1].actions, 'write', `Expected write, got ${body.data[1].actions}`); + assert.strictEqual(body[':type'], 'sheet', `Expected sheet, got ${body[':type']}`); + assert.strictEqual(body[':sheetname'], 'permissions', `Expected permissions, got ${body[':sheetname']}`); }); it('[anonymous] cannot delete root folder', async () => { @@ -334,6 +337,8 @@ export default (ctx) => describe('Integration Tests: it tests', function () { limit: newConfigData.length, offset: 0, data: newConfigData, + ':type': 'sheet', + ':sheetname': 'permissions', })); resp = await fetch(url, { method: 'POST', @@ -354,6 +359,17 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.strictEqual(resp.status, 200, `Expected 200 OK, got ${resp.status}`); }); + it('[limited user] still cannot read page1', async () => { + const { + serverUrl, org, repo, limitedUser, + } = ctx; + const url = `${serverUrl}/source/${org}/${repo}/test-folder/page1.html`; + const resp = await fetch(url, { + headers: { Authorization: `Bearer ${limitedUser.accessToken}` }, + }); + assert.strictEqual(resp.status, 403, `Expected 403 Unauthorized, got ${resp.status} - user: ${limitedUser.email}`); + }); + it('[super user] should remove added entries to clean up the config', async () => { const { serverUrl, org, repo, superUser, @@ -370,6 +386,8 @@ export default (ctx) => describe('Integration Tests: it tests', function () { limit: newConfigData.length, offset: 0, data: newConfigData, + ':type': 'sheet', + ':sheetname': 'permissions', })); resp = await fetch(url, { method: 'POST', @@ -388,10 +406,11 @@ export default (ctx) => describe('Integration Tests: it tests', function () { assert.strictEqual(body.data[1].path, '/+**', `Expected /+**, got ${body.data[1].path}`); assert.strictEqual(body.data[1].groups, superUser.email, `Expected user email, got ${body.data[1].groups}`); assert.strictEqual(body.data[1].actions, 'write', `Expected write, got ${body.data[1].actions}`); + assert.strictEqual(body[':type'], 'sheet', `Expected sheet, got ${body[':type']}`); + assert.strictEqual(body[':sheetname'], 'permissions', `Expected permissions, got ${body[':sheetname']}`); }); - // TODO: currently the auth session is stored in memory, so the limited user can still read page2 - it.skip('[limited user] cannot read page2 anymore', async () => { + it('[limited user] cannot read page2 anymore', async () => { const { serverUrl, org, repo, limitedUser, } = ctx;