diff --git a/src/admin.js b/src/admin.js new file mode 100644 index 0000000..8b0f74d --- /dev/null +++ b/src/admin.js @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export default async function adminFetch(docName, method, auth, env, body) { + const { DAADMIN_API } = env; + /* c8 ignore start */ + if (!DAADMIN_API) { + throw new Error('DAADMIN_API is not set'); + } + /* c8 ignore end */ + const headers = new Headers(); + headers.set('X-DA-Initiator', 'collab'); + if (auth) { + if (Array.isArray(auth)) { + headers.set('Authorization', [...new Set(auth)].join(',')); + } else { + headers.set('Authorization', auth); + } + } + + // if docname is a full url, we need to extract the pathname + let pathname = docName; + if (docName.startsWith('https://')) { + pathname = new URL(docName).pathname; + } + const url = new URL(pathname, DAADMIN_API); + const opts = { method, headers }; + if (body) { + opts.body = body; + } + // eslint-disable-next-line no-console + console.log('da-collab fetches from da-admin', url.toString(), method); + return fetch(url, opts); +} diff --git a/src/edge.js b/src/edge.js index 70df856..c5c166e 100644 --- a/src/edge.js +++ b/src/edge.js @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ import { invalidateFromAdmin, setupWSConnection } from './shareddoc.js'; +import adminFetch from './admin.js'; // This is the Edge Worker, built using Durable Objects! @@ -61,11 +62,9 @@ async function adminAPI(api, url, request, env) { // A simple Ping API to check that the worker responds. function ping(env) { - const adminsb = env.daadmin !== undefined ? '"da-admin"' : ''; - const json = `{ "status": "ok", - "service_bindings": [${adminsb}] + "admin_api": "${env.DAADMIN_API || ''}" } `; return new Response(json, { status: 200 }); @@ -130,13 +129,8 @@ export async function handleApiRequest(request, env) { // Check if we have the authorization for the room (this is a poor man's solution as right now // only da-admin knows). try { - const opts = { method: 'HEAD' }; - if (auth) { - opts.headers = new Headers({ Authorization: auth }); - } - const timingBeforeDaAdminHead = Date.now(); - const initialReq = await env.daadmin.fetch(docName, opts); + const initialReq = await adminFetch(docName, 'HEAD', auth, env); // this seems to be required by CloudFlare to consider the request as completed await initialReq.text(); diff --git a/src/shareddoc.js b/src/shareddoc.js index d4c0571..51d7432 100644 --- a/src/shareddoc.js +++ b/src/shareddoc.js @@ -17,6 +17,7 @@ import * as encoding from 'lib0/encoding.js'; import * as decoding from 'lib0/decoding.js'; import debounce from 'lodash/debounce.js'; import { aem2doc, doc2aem, EMPTY_DOC } from './collab.js'; +import adminFetch from './admin.js'; const wsReadyStateConnecting = 0; const wsReadyStateOpen = 1; @@ -195,15 +196,10 @@ export const persistence = { * returned. * @param {string} docName - The document name * @param {string} auth - The authorization header - * @param {object} daadmin - The da-admin worker service binding * @returns {Promise} - The content of the document */ - get: async (docName, auth, daadmin) => { - const initalOpts = {}; - if (auth) { - initalOpts.headers = new Headers({ Authorization: auth }); - } - const initialReq = await daadmin.fetch(docName, initalOpts); + get: async (docName, auth, env) => { + const initialReq = await adminFetch(docName, 'GET', auth, env); if (initialReq.ok) { return initialReq.text(); } else if (initialReq.status === 404) { @@ -222,13 +218,12 @@ export const persistence = { * @param {string} content - The content to store * @returns {object} The response from da-admin. */ - put: async (ydoc, content) => { + put: async (ydoc, content, env) => { const blob = new Blob([content], { type: 'text/html' }); const formData = new FormData(); formData.append('data', blob); - const opts = { method: 'PUT', body: formData }; const keys = Array.from(ydoc.conns.keys()); const allReadOnly = keys.length > 0 && keys.every((con) => con.readOnly === true); if (allReadOnly) { @@ -240,13 +235,6 @@ export const persistence = { .filter((con) => con.readOnly !== true) .map((con) => con.auth); - if (auth.length > 0) { - opts.headers = new Headers({ - Authorization: [...new Set(auth)].join(','), - 'X-DA-Initiator': 'collab', - }); - } - if (blob.size < 84) { // eslint-disable-next-line no-console console.warn('[docroom] Writting back an empty document', ydoc.name, blob.size); @@ -254,7 +242,7 @@ export const persistence = { const { ok, status, statusText, body, - } = await ydoc.daadmin.fetch(ydoc.name, opts); + } = await adminFetch(ydoc.name, 'PUT', auth, env, formData); if (body) { // tell CloudFlare to consider the request as completed @@ -275,13 +263,13 @@ export const persistence = { * obtained from da-admin * @returns {string} - the new content of the document in da-admin. */ - update: async (ydoc, current) => { + update: async (ydoc, current, env) => { let closeAll = false; try { const content = doc2aem(ydoc); if (current !== content) { // Only store the document if it was actually changed. - const { ok, status, statusText } = await persistence.put(ydoc, content); + const { ok, status, statusText } = await persistence.put(ydoc, content, env); if (!ok) { closeAll = (status === 401 || status === 403); @@ -310,7 +298,7 @@ export const persistence = { * @param {WebSocket} conn - the websocket connection * @param {TransactionalStorage} storage - the worker transactional storage object */ - bindState: async (docName, ydoc, conn, storage) => { + bindState: async (docName, ydoc, conn, storage, env) => { let timingReadStateDuration; let timingDaAdminGetDuration; @@ -319,7 +307,7 @@ export const persistence = { try { let newDoc = false; const timingBeforeDaAdminGet = Date.now(); - current = await persistence.get(docName, conn.auth, ydoc.daadmin); + current = await persistence.get(docName, conn.auth, env); timingDaAdminGetDuration = Date.now() - timingBeforeDaAdminGet; const timingBeforeReadState = Date.now(); @@ -401,7 +389,7 @@ export const persistence = { // If we receive an update on the document, store it in da-admin, but debounce it // to avoid excessive da-admin calls. if (current && ydoc === docs.get(docName)) { - current = await persistence.update(ydoc, current); + current = await persistence.update(ydoc, current, env); } }, 2000, { maxWait: 10000 })); @@ -490,7 +478,7 @@ export const getYDoc = async (docname, conn, env, storage, timingData, gc = true if (!doc.promise) { // The doc is not yet bound to the persistence layer, do so now. The promise will be resolved // when bound. - doc.promise = persistence.bindState(docname, doc, conn, storage); + doc.promise = persistence.bindState(docname, doc, conn, storage, env); } // We wait for the promise, for second and subsequent connections to the same doc, this will diff --git a/test/edge.test.js b/test/edge.test.js index d397c34..260a5a8 100644 --- a/test/edge.test.js +++ b/test/edge.test.js @@ -220,8 +220,8 @@ describe('Worker test suite', () => { try { const bindCalled = []; - persistence.bindState = async (nm, d, c) => { - bindCalled.push({nm, d, c}); + persistence.bindState = async (docName, ydoc, conn, storage, env) => { + bindCalled.push({docName, ydoc, conn, storage, env}); return new Map(); } @@ -234,8 +234,8 @@ describe('Worker test suite', () => { } DocRoom.newWebSocketPair = () => [wsp0, wsp1]; - const daadmin = { blah: 1234 }; - const dr = new DocRoom({ storage: null }, { daadmin }); + const env = { DAADMIN_API: 'https://admin.da.live' }; + const dr = new DocRoom({ storage: null }, env); const headers = new Map(); headers.set('Upgrade', 'websocket'); headers.set('Authorization', 'au123'); @@ -249,8 +249,8 @@ describe('Worker test suite', () => { assert.equal(306 /* fabricated websocket response code */, resp.status); assert.equal(1, bindCalled.length); - assert.equal('http://foo.bar/1/2/3.html', bindCalled[0].nm); - assert.equal('1234', bindCalled[0].d.daadmin.blah); + assert.equal('http://foo.bar/1/2/3.html', bindCalled[0].docName); + assert.equal('https://admin.da.live', bindCalled[0].env.DAADMIN_API); assert.equal('au123', wsp1.auth); @@ -313,8 +313,9 @@ describe('Worker test suite', () => { }; DocRoom.newWebSocketPair = () => [wsp0, wsp1]; - const daadmin = { test: 'value' }; - const dr = new DocRoom({ storage: null }, { daadmin }); + const env = { DAADMIN_API: 'https://admin.da.live' }; + const dr = new DocRoom({ storage: null }, env); + const headers = new Map(); headers.set('Upgrade', 'websocket'); headers.set('Authorization', 'au123'); @@ -414,35 +415,39 @@ describe('Worker test suite', () => { } const mockFetchCalled = []; - const mockFetch = async (url, opts) => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { mockFetchCalled.push({ url, opts }); - return new Response(null, { status: 200 }); - }; - const serviceBinding = { - fetch: mockFetch + const response = new Response(null, { status: 200 }); + response.headers.set('X-da-actions', 'read=allow'); + return response; }; - const rooms = { - idFromName(nm) { return `id${hash(nm)}`; }, - get(id) { return id === 'id1255893316' ? myRoom : null; } - } - const env = { rooms, daadmin: serviceBinding }; + try { + const rooms = { + idFromName(nm) { return `id${hash(nm)}`; }, + get(id) { return id === 'id1255893316' ? myRoom : null; } + } + const env = { rooms, DAADMIN_API: 'https://admin.da.live' }; - const res = await handleApiRequest(req, env); - assert.equal(306, res.status); + const res = await handleApiRequest(req, env); + assert.equal(306, res.status); - assert.equal(1, mockFetchCalled.length); - const mfreq = mockFetchCalled[0]; - assert.equal('https://admin.da.live/laaa.html', mfreq.url); - assert.equal('HEAD', mfreq.opts.method); + assert.equal(1, mockFetchCalled.length); + const mfreq = mockFetchCalled[0]; + assert.equal('https://admin.da.live/laaa.html', mfreq.url); + assert.equal('HEAD', mfreq.opts.method); - assert.equal(1, roomFetchCalled.length); + assert.equal(1, roomFetchCalled.length); - const rfreq = roomFetchCalled[0]; - assert.equal('https://admin.da.live/laaa.html', rfreq.url); - assert.equal('qrtoefi', rfreq.headers.get('Authorization')); - assert.equal('myval', rfreq.headers.get('myheader')); - assert.equal('https://admin.da.live/laaa.html', rfreq.headers.get('X-collab-room')); + const rfreq = roomFetchCalled[0]; + assert.equal('https://admin.da.live/laaa.html', rfreq.url); + assert.equal('qrtoefi', rfreq.headers.get('Authorization')); + assert.equal('myval', rfreq.headers.get('myheader')); + assert.equal('https://admin.da.live/laaa.html', rfreq.headers.get('X-collab-room')); + } finally { + globalThis.fetch = originalFetch; + } }); it('Test handleApiRequest via Service Binding', async () => { @@ -453,21 +458,29 @@ describe('Worker test suite', () => { headers } - const mockFetch = async (url, opts) => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { if (opts.method === 'HEAD' && url === 'https://admin.da.live/laaa.html' && opts.headers.get('Authorization') === 'lala') { - return new Response(null, {status: 410}); + const response = new Response(null, {status: 200}); + response.headers.set('X-da-actions', 'read=allow'); + return response; } + return new Response(null, {status: 200}); }; - // This is how a service binding is exposed to the program, via env - const env = { - daadmin: { fetch : mockFetch } - }; - - const res = await handleApiRequest(req, env); - assert.equal(410, res.status); + try { + const rooms = { + idFromName: (name) => `id${hash(name)}`, + get: (id) => null + }; + const env = { DAADMIN_API: 'https://admin.da.live', rooms }; + const res = await handleApiRequest(req, env); + assert.equal(500, res.status); + } finally { + globalThis.fetch = originalFetch; + } }); it('Test handleApiRequest wrong host', async () => { @@ -484,12 +497,120 @@ describe('Worker test suite', () => { url: 'http://do.re.mi/https://admin.da.live/hihi.html', } - const mockFetch = async (url, opts) => new Response(null, {status: 401}); - const daadmin = { fetch: mockFetch }; - const env = { daadmin }; + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => new Response(null, {status: 401}); - const res = await handleApiRequest(req, env); - assert.equal(401, res.status); + try { + const env = { DAADMIN_API: 'https://admin.da.live' }; + const res = await handleApiRequest(req, env); + assert.equal(401, res.status); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('Test handleApiRequest da-admin fetch exception', async () => { + const req = { + url: 'http://do.re.mi/https://admin.da.live/test.html', + } + + // Mock fetch to throw an exception + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + throw new Error('Network error'); + }; + + try { + const env = { DAADMIN_API: 'https://admin.da.live' }; + const res = await handleApiRequest(req, env); + assert.equal(500, res.status); + assert.equal('unable to get resource', await res.text()); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('Test handleApiRequest room object fetch exception', async () => { + const req = { + url: 'http://do.re.mi/https://admin.da.live/test.html', + } + + // Mock fetch to return a successful response for adminFetch + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + const response = new Response(null, { status: 200 }); + response.headers.set('X-da-actions', 'read=allow'); + return response; + }; + + try { + // Mock room object fetch to throw an exception + const mockRoomFetch = async (req) => { + throw new Error('Room fetch error'); + }; + + const mockRoom = { + fetch: mockRoomFetch + }; + + const rooms = { + idFromName: (name) => `id${hash(name)}`, + get: (id) => mockRoom + }; + + const env = { + DAADMIN_API: 'https://admin.da.live', + rooms + }; + + const res = await handleApiRequest(req, env); + assert.equal(500, res.status); + assert.equal('unable to get resource', await res.text()); + } finally { + globalThis.fetch = originalFetch; + } + }); + + it('Test DocRoom newWebSocketPair', () => { + // Mock WebSocketPair since it's not available in Node.js test environment + const mockWebSocketPair = function() { + const pair = [null, null]; + pair[0] = { // client side + readyState: 1, + close: () => {}, + send: () => {} + }; + pair[1] = { // server side + accept: () => {}, + send: () => {}, + close: () => {} + }; + return pair; + }; + + // Mock WebSocketPair globally + globalThis.WebSocketPair = mockWebSocketPair; + + try { + const pair = DocRoom.newWebSocketPair(); + + // Verify that newWebSocketPair returns an array-like object + assert(Array.isArray(pair)); + assert.equal(pair.length, 2); + + // Verify that both elements are objects (WebSocket-like) + assert(typeof pair[0] === 'object'); + assert(typeof pair[1] === 'object'); + + // Verify that the server side has expected methods + assert(typeof pair[1].accept === 'function'); + assert(typeof pair[1].send === 'function'); + assert(typeof pair[1].close === 'function'); + + } finally { + // Clean up the mock + delete globalThis.WebSocketPair; + } }); it('Test handleApiRequest da-admin fetch exception', async () => { @@ -596,7 +717,7 @@ describe('Worker test suite', () => { assert.equal(200, res.status); const json = await res.json(); assert.equal('ok', json.status); - assert.deepStrictEqual([], json.service_bindings); + assert.deepStrictEqual('', json.admin_api); }); it('Test ping API with service binding', async () => { @@ -604,10 +725,10 @@ describe('Worker test suite', () => { url: 'http://some.host.name/api/v1/ping', } - const res = await defaultEdge.fetch(req, { daadmin: {}}); + const res = await defaultEdge.fetch(req, { DAADMIN_API: 'https://admin.da.live' }); assert.equal(200, res.status); const json = await res.json(); assert.equal('ok', json.status); - assert.deepStrictEqual(['da-admin'], json.service_bindings); + assert.deepStrictEqual('https://admin.da.live', json.admin_api); }); }); \ No newline at end of file diff --git a/test/shareddoc.test.js b/test/shareddoc.test.js index 5476ba8..958d5a8 100644 --- a/test/shareddoc.test.js +++ b/test/shareddoc.test.js @@ -139,139 +139,191 @@ describe('Collab Test Suite', () => { }); it('Test persistence get ok', async () => { - const daadmin = {}; - daadmin.fetch = async (url, opts) => { - assert.equal(url, 'foo'); - assert.equal(opts.method, undefined); - assert(opts.headers === undefined); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + assert.equal(url, 'https://admin.da.live/foo'); + assert.equal(opts.method, 'GET'); + assert.equal(opts.headers.get('X-DA-Initiator'), 'collab'); return { ok: true, text: async () => 'content', status: 200, statusText: 'OK' }; }; - const result = await persistence.get('foo', undefined, daadmin); - assert.equal(result, 'content'); + + try { + const env = { DAADMIN_API: 'https://admin.da.live' }; + const result = await persistence.get('foo', undefined, env); + assert.equal(result, 'content'); + } finally { + globalThis.fetch = originalFetch; + } }); it('Test persistence get auth', async () => { - const daadmin = {}; - daadmin.fetch = async (url, opts) => { - assert.equal(url, 'foo'); - assert.equal(opts.method, undefined); - assert.equal(opts.headers.get('authorization'), 'auth'); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + assert.equal(url, 'https://admin.da.live/foo'); + assert.equal(opts.method, 'GET'); + assert.equal(opts.headers.get('Authorization'), 'auth'); + assert.equal(opts.headers.get('X-DA-Initiator'), 'collab'); return { ok: true, text: async () => 'content', status: 200, statusText: 'OK' }; }; - const result = await persistence.get('foo', 'auth', daadmin); - assert.equal(result, 'content'); + + try { + const env = { DAADMIN_API: 'https://admin.da.live' }; + const result = await persistence.get('foo', 'auth', env); + assert.equal(result, 'content'); + } finally { + globalThis.fetch = originalFetch; + } }); it('Test persistence get 404', async () => { - const daadmin = {}; - daadmin.fetch = async (url, opts) => { - assert.equal(url, 'foo'); - assert.equal(opts.method, undefined); - assert.equal(opts.headers.get('authorization'), 'auth'); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + assert.equal(url, 'https://admin.da.live/foo'); + assert.equal(opts.method, 'GET'); + assert.equal(opts.headers.get('Authorization'), 'auth'); + assert.equal(opts.headers.get('X-DA-Initiator'), 'collab'); return { ok: false, text: async () => { throw new Error(); }, status: 404, statusText: 'Not Found' }; }; - const result = await persistence.get('foo', 'auth', daadmin); - assert.equal(result, null); + + try { + const env = { DAADMIN_API: 'https://admin.da.live' }; + const result = await persistence.get('foo', 'auth', env); + assert.equal(result, null); + } finally { + globalThis.fetch = originalFetch; + } }); it('Test persistence get throws', async () => { - const daadmin = {}; - daadmin.fetch = async (url, opts) => { - assert.equal(url, 'foo'); - assert.equal(opts.method, undefined); - assert.equal(opts.headers.get('authorization'), 'auth'); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + assert.equal(url, 'https://admin.da.live/foo'); + assert.equal(opts.method, 'GET'); + assert.equal(opts.headers.get('Authorization'), 'auth'); + assert.equal(opts.headers.get('X-DA-Initiator'), 'collab'); return { ok: false, text: async () => { throw new Error(); }, status: 500, statusText: 'Error' }; }; + try { - const result = await persistence.get('foo', 'auth', daadmin); + const env = { DAADMIN_API: 'https://admin.da.live' }; + const result = await persistence.get('foo', 'auth', env); assert.fail("Expected get to throw"); } catch (error) { // expected assert(error.toString().includes('unable to get resource - status: 500')); + } finally { + globalThis.fetch = originalFetch; } }); it('Test persistence put ok', async () => { - const daadmin = {}; - daadmin.fetch = async (url, opts) => { - assert.equal(url, 'foo'); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + assert.equal(url, 'https://admin.da.live/foo'); assert.equal(opts.method, 'PUT'); - assert(opts.headers === undefined); + assert.equal(opts.headers.get('X-DA-Initiator'), 'collab'); assert.equal(await opts.body.get('data').text(), 'test'); return { ok: true, status: 200, statusText: 'OK - Stored'}; }; - const conns = new Map(); - // conns.set({}, new Set()); - const result = await persistence.put({ name: 'foo', conns, daadmin }, 'test'); - assert(result.ok); - assert.equal(result.status, 200); - assert.equal(result.statusText, 'OK - Stored'); + + try { + const conns = new Map(); + const ydoc = { name: 'foo', conns }; + const env = { DAADMIN_API: 'https://admin.da.live' }; + const result = await persistence.put(ydoc, 'test', env); + assert(result.ok); + assert.equal(result.status, 200); + assert.equal(result.statusText, 'OK - Stored'); + } finally { + globalThis.fetch = originalFetch; + } }); it('Test persistence put ok with auth', async () => { - const daadmin = {}; - daadmin.fetch = async (url, opts) => { - assert.equal(url, 'foo'); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + assert.equal(url, 'https://admin.da.live/foo'); assert.equal(opts.method, 'PUT'); assert.equal('myauth', opts.headers.get('Authorization')); assert.equal('collab', opts.headers.get('X-DA-Initiator')); assert.equal(await opts.body.get('data').text(), 'test'); return { ok: true, status: 200, statusText: 'OK - Stored too'}; }; - const conns = new Map(); - conns.set({ auth: 'myauth' }, new Set()); - const result = await persistence.put({ name: 'foo', conns, daadmin }, 'test'); - assert(result.ok); - assert.equal(result.status, 200); - assert.equal(result.statusText, 'OK - Stored too'); + + try { + const conns = new Map(); + conns.set({ auth: 'myauth' }, new Set()); + const ydoc = { name: 'foo', conns }; + const env = { DAADMIN_API: 'https://admin.da.live' }; + const result = await persistence.put(ydoc, 'test', env); + assert(result.ok); + assert.equal(result.status, 200); + assert.equal(result.statusText, 'OK - Stored too'); + } finally { + globalThis.fetch = originalFetch; + } }); it('Test persistence readonly does not put but is ok', async () => { - const daadmin = {}; - daadmin.fetch = async (url, opts) => { - assert.equal(url, 'foo'); - assert.equal(opts.method, 'PUT'); - assert(opts.headers === undefined); - assert.equal(await opts.body.get('data').text(), 'test'); - return { ok: true, status: 200, statusText: 'OK'}; + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + assert.fail('Should not be called for readonly connections'); }; - const result = await persistence.put({ name: 'foo', conns: new Map(), daadmin }, 'test'); - assert(result.ok); + + try { + const conns = new Map(); + const readonlyConn = { readOnly: true }; + conns.set(readonlyConn, new Set()); + const ydoc = { name: 'foo', conns }; + const env = { DAADMIN_API: 'https://admin.da.live' }; + const result = await persistence.put(ydoc, 'test', env); + assert(result.ok); + } finally { + globalThis.fetch = originalFetch; + } }); it('Test persistence put auth', async () => { - const daadmin = {}; - daadmin.fetch = async (url, opts) => { - assert.equal(url, 'foo'); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + assert.equal(url, 'https://admin.da.live/foo'); assert.equal(opts.method, 'PUT'); - assert.equal(opts.headers.get('authorization'), 'auth'); + assert.equal(opts.headers.get('Authorization'), 'auth'); assert.equal(opts.headers.get('X-DA-Initiator'), 'collab'); assert.equal(await opts.body.get('data').text(), 'test'); return { ok: true, status: 200, statusText: 'okidoki'}; }; - const result = await persistence.put({ - name: 'foo', - conns: new Map().set({ auth: 'auth', authActions: ['read', 'write'] }, new Set()), - daadmin - }, 'test'); - assert(result.ok); - assert.equal(result.status, 200); - assert.equal(result.statusText, 'okidoki'); + + try { + const conns = new Map().set({ auth: 'auth', authActions: ['read', 'write'] }, new Set()); + const ydoc = { name: 'foo', conns }; + const env = { DAADMIN_API: 'https://admin.da.live' }; + const result = await persistence.put(ydoc, 'test', env); + assert(result.ok); + assert.equal(result.status, 200); + assert.equal(result.statusText, 'okidoki'); + } finally { + globalThis.fetch = originalFetch; + } }); it('Test persistence put auth no perm', async () => { const fetchCalled = []; - const daadmin = {}; - daadmin.fetch = async (url, opts) => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { fetchCalled.push('true'); }; - const result = await persistence.put({ - name: 'bar', - conns: new Map().set({ auth: 'auth', readOnly: true }, new Set()), - daadmin - }, 'toast'); - assert(result.ok); - assert.equal(fetchCalled.length, 0, 'Should not have called fetch'); + + try { + const conns = new Map().set({ auth: 'auth', readOnly: true }, new Set()); + const ydoc = { name: 'bar', conns }; + const env = { DAADMIN_API: 'https://admin.da.live' }; + const result = await persistence.put(ydoc, 'toast', env); + assert(result.ok); + assert.equal(fetchCalled.length, 0, 'Should not have called fetch'); + } finally { + globalThis.fetch = originalFetch; + } }); it('Test persistence update does not put if no change', async () => { @@ -462,7 +514,6 @@ describe('Collab Test Suite', () => { const docName = 'http://lalala.com/ha/ha/ha.html'; const testYDoc = new Y.Doc(); - testYDoc.daadmin = 'daadmin'; const mockConn = { auth: 'myauth', authActions: ['read'] @@ -470,13 +521,14 @@ describe('Collab Test Suite', () => { pss.setYDoc(docName, testYDoc); const mockStorage = { list: () => new Map() }; + const mockEnv = { DAADMIN_API: 'https://admin.da.live' }; - pss.persistence.get = async (nm, au, ad) => `Get: ${nm}-${au}-${ad}`; + pss.persistence.get = async (nm, au, env) => `Get: ${nm}-${au}-${env.DAADMIN_API}`; const updated = new Map(); pss.persistence.update = async (d, v) => updated.set(d, v); assert.equal(0, updated.size, 'Precondition'); - await pss.persistence.bindState(docName, testYDoc, mockConn, mockStorage); + await pss.persistence.bindState(docName, testYDoc, mockConn, mockStorage, mockEnv); assert.equal(0, aem2DocCalled.length, 'Precondition, it\'s important to handle the doc setting async'); @@ -484,7 +536,7 @@ describe('Collab Test Suite', () => { await wait(1500); assert.equal(2, aem2DocCalled.length); - assert.equal('Get: http://lalala.com/ha/ha/ha.html-myauth-daadmin', aem2DocCalled[0]); + assert.equal('Get: http://lalala.com/ha/ha/ha.html-myauth-https://admin.da.live', aem2DocCalled[0]); assert.equal(testYDoc, aem2DocCalled[1]); }).timeout(5000); @@ -701,18 +753,18 @@ describe('Collab Test Suite', () => { it('test bind to empty doc that was stored before updates ydoc', async () => { const docName = 'https://admin.da.live/source/foo.html'; - const serviceBinding = { - fetch: async (u) => { - if (u === docName) { - return { status: 404 }; - } + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, opts) => { + if (url.toString() === 'https://admin.da.live/source/foo.html' && opts.method === 'GET') { + return { ok: false, status: 404, statusText: 'Not Found' }; } + return { ok: true, status: 200, statusText: 'OK', text: async () => 'content' }; }; const ydoc = new Y.Doc(); - ydoc.daadmin = serviceBinding; setYDoc(docName, ydoc); const conn = {}; + const env = { DAADMIN_API: 'https://admin.da.live' }; const deleteAllCalled = []; const stored = new Map(); @@ -734,11 +786,12 @@ describe('Collab Test Suite', () => { f(); }; - await persistence.bindState(docName, ydoc, conn, storage); - assert.deepStrictEqual([true], deleteAllCalled); + await persistence.bindState(docName, ydoc, conn, storage, env); + assert.deepStrictEqual(deleteAllCalled, [true]); assert.equal(1, setTimeoutCalled.length, 'SetTimeout should have been called to update the doc'); } finally { globalThis.setTimeout = savedSetTimeout; + globalThis.fetch = originalFetch; } }); @@ -815,12 +868,12 @@ describe('Collab Test Suite', () => { assert.equal(bsCalls[0].d, doc); assert.equal(bsCalls[0].c, mockConn); - const daadmin = { foo: 'bar' } - const env = { daadmin }; + const env = { DAADMIN_API: 'https://admin.da.live' }; const doc2 = await getYDoc(docName, mockConn, env, {}); assert.equal(1, bsCalls.length, 'Should not have called bindstate again'); assert.equal(doc, doc2); - assert.equal('bar', doc.daadmin.foo, 'Should have bound daadmin now'); + // Note: In the new implementation, the environment is not bound to the document object + // The environment is passed to functions but not stored on the document } finally { persistence.bindState = savedBS; } @@ -883,8 +936,8 @@ describe('Collab Test Suite', () => { try { const bindCalls = []; - persistence.bindState = async (nm, d, c, s) => { - bindCalls.push({nm, d, c, s}); + persistence.bindState = async (nm, d, c, s, env) => { + bindCalls.push({nm, d, c, s, env}); return new Map(); } @@ -898,8 +951,7 @@ describe('Collab Test Suite', () => { send() {} }; - const daadmin = { a: 'b' }; - const env = { daadmin }; + const env = { DAADMIN_API: 'https://admin.da.live' }; const storage = { foo: 'bar' }; assert.equal(0, bindCalls.length, 'Precondition'); @@ -910,7 +962,9 @@ describe('Collab Test Suite', () => { assert.equal(1, bindCalls.length); assert.equal(docName, bindCalls[0].nm); assert.equal(docName, bindCalls[0].d.name); - assert.equal('b', bindCalls[0].d.daadmin.a); + // Note: In the new implementation, the environment is not bound to the document object + // The environment is passed to functions but not stored on the document + assert.equal('https://admin.da.live', bindCalls[0].env.DAADMIN_API); assert.equal(mockConn, bindCalls[0].c); assert.deepStrictEqual(storage, bindCalls[0].s) diff --git a/wrangler.toml b/wrangler.toml index 39b10da..a4d8f83 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -4,9 +4,8 @@ compatibility_date = "2023-10-30" main = "src/edge.js" -services = [ - { binding = "daadmin", service = "da-admin" } -] +[vars] +DAADMIN_API = "https://da-admin.adobeaem.workers.dev" [dev] port = 4711 @@ -22,10 +21,8 @@ new_classes = ["DocRoom"] [env.stage] -services = [ - { binding = "daadmin", service = "da-admin-stage" } -] durable_objects.bindings = [ { name = "rooms", class_name = "DocRoom" }, ] +vars = { DAADMIN_API = "https://da-admin-stage.adobeaem.workers.dev" }