From a81d7a17be74ce64c6be70fd489d2bc6940c6cdd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 10:16:36 +0000 Subject: [PATCH 1/3] Initial plan From f516b45c7d7dd006fa98bcf4cb0b2afdd858a05c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 10:43:01 +0000 Subject: [PATCH 2/3] feat: support append via PATCH /post Agent-Logs-Url: https://github.com/ether/ep_post_data/sessions/8d85ca65-9276-4233-a94b-83d25502c051 Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com> --- README.md | 4 ++ index.js | 19 ++++++--- static/tests/frontend-new/specs/smoke.spec.ts | 42 +++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d7c61f7..47d317f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ pnpm run plugins i ep_post_data ## Usage POST to `/post` to create or update a pad. Set the `X-PAD-ID` header to choose the pad name, otherwise a random ID is generated. +Use `PATCH /post` to append content to an existing pad (or create it if it does not exist). ```bash # Create a pad with a random ID @@ -18,6 +19,9 @@ curl -X POST -d @datafile.txt http://localhost:9001/post # Create or update a specific pad curl -X POST -d @datafile.txt -H 'X-PAD-ID: mypad' http://localhost:9001/post + +# Append to a specific pad (or create it if missing) +curl -X PATCH -d @datafile.txt -H 'X-PAD-ID: mypad' http://localhost:9001/post ``` ## Limits diff --git a/index.js b/index.js index 169c138..3709cdb 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,7 @@ const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomStrin const MAX_BODY_SIZE = 1024 * 1024; // 1 MB exports.registerRoute = (hookName, args, callback) => { - args.app.post('/post', (req, res) => { + const upsertPadFromRequest = (req, res, shouldAppend) => { let padId = req.headers['x-pad-id']; if (padId === undefined) { padId = randomString(8); @@ -49,15 +49,24 @@ exports.registerRoute = (hookName, args, callback) => { // Pad already exists so updating an existing pad. if (padExists) { try { - console.debug('ep_post_data: Setting text!', padId, content); - await API.setText(padId, content); - res.send('Success updating pad'); + if (shouldAppend) { + console.debug('ep_post_data: Appending text!', padId, content); + await API.appendText(padId, content); + res.send('Success appending to pad'); + } else { + console.debug('ep_post_data: Setting text!', padId, content); + await API.setText(padId, content); + res.send('Success updating pad'); + } } catch (e) { console.error('ep_post_data: Error updating pad', padId, e); res.send('Error updating pad'); } } }); - }); + }; + + args.app.post('/post', (req, res) => upsertPadFromRequest(req, res, false)); + args.app.patch('/post', (req, res) => upsertPadFromRequest(req, res, true)); callback(); }; diff --git a/static/tests/frontend-new/specs/smoke.spec.ts b/static/tests/frontend-new/specs/smoke.spec.ts index 58d0d30..1af3e19 100644 --- a/static/tests/frontend-new/specs/smoke.spec.ts +++ b/static/tests/frontend-new/specs/smoke.spec.ts @@ -5,9 +5,51 @@ test.beforeEach(async ({page}) => { await goToNewPad(page); }); +const getPadIdFromUrl = (url: string) => { + const match = /\/p\/([^/?#]+)/.exec(url); + if (!match) throw new Error(`Failed to parse pad id from URL: ${url}`); + return match[1]; +}; + test.describe('ep_post_data', () => { test('pad loads with plugin installed', async ({page}) => { const padBody = await getPadBody(page); await expect(padBody).toBeVisible(); }); + + test('PATCH /post appends text to an existing pad', async ({page}) => { + const padId = getPadIdFromUrl(page.url()); + + const postResponse = await page.request.fetch('/post', { + method: 'POST', + headers: {'X-PAD-ID': padId}, + data: 'first', + }); + expect(postResponse.ok()).toBeTruthy(); + + const patchResponse = await page.request.fetch('/post', { + method: 'PATCH', + headers: {'X-PAD-ID': padId}, + data: ' second', + }); + expect(patchResponse.ok()).toBeTruthy(); + + const txtResponse = await page.request.get(`/p/${padId}/export/txt`); + expect(txtResponse.ok()).toBeTruthy(); + expect((await txtResponse.text()).trimEnd()).toBe('first second'); + }); + + test('PATCH /post creates pad when it does not exist', async ({page}) => { + const padId = `ep-post-data-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; + const patchResponse = await page.request.fetch('/post', { + method: 'PATCH', + headers: {'X-PAD-ID': padId}, + data: 'created via patch', + }); + expect(patchResponse.ok()).toBeTruthy(); + + const txtResponse = await page.request.get(`/p/${padId}/export/txt`); + expect(txtResponse.ok()).toBeTruthy(); + expect((await txtResponse.text()).trimEnd()).toBe('created via patch'); + }); }); From a22cb0fd968956aca4e6aa0ae12b8ce0ca4c648a Mon Sep 17 00:00:00 2001 From: John McLear Date: Thu, 14 May 2026 10:47:04 +0100 Subject: [PATCH 3/3] test: use absolute URLs in post-data smoke spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- static/tests/frontend-new/specs/smoke.spec.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/static/tests/frontend-new/specs/smoke.spec.ts b/static/tests/frontend-new/specs/smoke.spec.ts index 1af3e19..54db667 100644 --- a/static/tests/frontend-new/specs/smoke.spec.ts +++ b/static/tests/frontend-new/specs/smoke.spec.ts @@ -11,6 +11,8 @@ const getPadIdFromUrl = (url: string) => { return match[1]; }; +const absoluteUrl = (pageUrl: string, path: string) => new URL(path, pageUrl).toString(); + test.describe('ep_post_data', () => { test('pad loads with plugin installed', async ({page}) => { const padBody = await getPadBody(page); @@ -18,37 +20,39 @@ test.describe('ep_post_data', () => { }); test('PATCH /post appends text to an existing pad', async ({page}) => { - const padId = getPadIdFromUrl(page.url()); + const pageUrl = page.url(); + const padId = getPadIdFromUrl(pageUrl); + const postUrl = absoluteUrl(pageUrl, '/post'); - const postResponse = await page.request.fetch('/post', { - method: 'POST', + const postResponse = await page.request.post(postUrl, { headers: {'X-PAD-ID': padId}, data: 'first', }); expect(postResponse.ok()).toBeTruthy(); - const patchResponse = await page.request.fetch('/post', { + const patchResponse = await page.request.fetch(postUrl, { method: 'PATCH', headers: {'X-PAD-ID': padId}, data: ' second', }); expect(patchResponse.ok()).toBeTruthy(); - const txtResponse = await page.request.get(`/p/${padId}/export/txt`); + const txtResponse = await page.request.get(absoluteUrl(pageUrl, `/p/${padId}/export/txt`)); expect(txtResponse.ok()).toBeTruthy(); expect((await txtResponse.text()).trimEnd()).toBe('first second'); }); test('PATCH /post creates pad when it does not exist', async ({page}) => { + const pageUrl = page.url(); const padId = `ep-post-data-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; - const patchResponse = await page.request.fetch('/post', { + const patchResponse = await page.request.fetch(absoluteUrl(pageUrl, '/post'), { method: 'PATCH', headers: {'X-PAD-ID': padId}, data: 'created via patch', }); expect(patchResponse.ok()).toBeTruthy(); - const txtResponse = await page.request.get(`/p/${padId}/export/txt`); + const txtResponse = await page.request.get(absoluteUrl(pageUrl, `/p/${padId}/export/txt`)); expect(txtResponse.ok()).toBeTruthy(); expect((await txtResponse.text()).trimEnd()).toBe('created via patch'); });