From a8e9742cc3813b4eb120d900f20ebbf16e156bde Mon Sep 17 00:00:00 2001 From: HUGO LOUREIRO <149155946+Lightshadow02@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:01:52 +0200 Subject: [PATCH 1/2] fix: point jest vue transform at installed @vue/vue2-jest package The jest config referenced vue-jest which is not a dependency; the installed transformer is @vue/vue2-jest. Went unnoticed as the repo had no jest tests yet. Co-Authored-By: Claude Fable 5 Signed-off-by: HUGO LOUREIRO <149155946+Lightshadow02@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bff842b330..e396c1cee4 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ }, "transform": { "^.+\\.js$": "/node_modules/babel-jest", - ".*\\.(vue)$": "/node_modules/vue-jest" + ".*\\.(vue)$": "/node_modules/@vue/vue2-jest" }, "snapshotSerializers": [ "/node_modules/jest-serializer-vue" From a7df338d9dfe5642a09bd3174e6224826b35400e Mon Sep 17 00:00:00 2001 From: HUGO LOUREIRO <149155946+Lightshadow02@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:01:52 +0200 Subject: [PATCH 2/2] fix: align attachment frontend requests with registered OCS routes The frontend was migrated to OCS endpoints but kept the old page-route URL shape (singular /attachment/, composite type:id, POST/GET verbs) which matches no registered OCS route, so deleting, updating and restoring attachments silently failed with 404. - call /cards/{cardId}/attachments/{attachmentId} with a numeric id and pass the attachment type as a query parameter - use PUT for restore, keep POST for update and register a POST route next to PUT since PHP only parses multipart bodies for POST - default the data parameter of AttachmentOcsController::update() to an empty string as file uploads do not send it - unwrap the OCS envelope in updateAttachment() - add jest regression tests for the attachment API URLs Co-Authored-By: Claude Fable 5 Signed-off-by: HUGO LOUREIRO <149155946+Lightshadow02@users.noreply.github.com> --- appinfo/routes.php | 2 + lib/Controller/AttachmentOcsController.php | 2 +- src/services/AttachmentApi.js | 14 ++-- src/services/AttachmentApi.spec.js | 86 ++++++++++++++++++++++ 4 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 src/services/AttachmentApi.spec.js diff --git a/appinfo/routes.php b/appinfo/routes.php index 146ec8d648..41e054365d 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -160,6 +160,8 @@ ['name' => 'attachment_ocs#getAll', 'url' => '/api/v{apiVersion}/cards/{cardId}/attachments', 'verb' => 'GET'], ['name' => 'attachment_ocs#create', 'url' => '/api/v{apiVersion}/cards/{cardId}/attachment', 'verb' => 'POST'], ['name' => 'attachment_ocs#update', 'url' => '/api/v{apiVersion}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'PUT'], + // POST is needed next to PUT as file uploads (multipart/form-data) are only parsed by PHP for POST requests + ['name' => 'attachment_ocs#update', 'url' => '/api/v{apiVersion}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'POST'], ['name' => 'attachment_ocs#delete', 'url' => '/api/v{apiVersion}/cards/{cardId}/attachments/{attachmentId}', 'verb' => 'DELETE'], ['name' => 'attachment_ocs#restore', 'url' => '/api/v{apiVersion}/cards/{cardId}/attachments/{attachmentId}/restore', 'verb' => 'PUT'], diff --git a/lib/Controller/AttachmentOcsController.php b/lib/Controller/AttachmentOcsController.php index 734daef49b..3374a3c189 100644 --- a/lib/Controller/AttachmentOcsController.php +++ b/lib/Controller/AttachmentOcsController.php @@ -51,7 +51,7 @@ public function create(int $cardId, string $type, string $data = '', ?int $board } #[NoAdminRequired] - public function update(int $cardId, int $attachmentId, string $data, string $type = 'file', ?int $boardId = null): DataResponse { + public function update(int $cardId, int $attachmentId, string $data = '', string $type = 'file', ?int $boardId = null): DataResponse { $this->ensureLocalBoard($boardId); $attachment = $this->attachmentService->update($cardId, $attachmentId, $data, $type); return new DataResponse($attachment); diff --git a/src/services/AttachmentApi.js b/src/services/AttachmentApi.js index a8374d6153..5f933c8506 100644 --- a/src/services/AttachmentApi.js +++ b/src/services/AttachmentApi.js @@ -42,22 +42,25 @@ export class AttachmentApi { } async updateAttachment({ cardId, attachment, formData, boardId }) { + // POST instead of PUT as multipart/form-data bodies are only parsed by PHP for POST requests const response = await axios({ method: 'POST', - url: this.ocsUrl(`/cards/${cardId}/attachment/${attachment.type}:${attachment.id}`), + url: this.ocsUrl(`/cards/${cardId}/attachments/${attachment.id}`), params: { + type: attachment.type, boardId: boardId ?? null, }, data: formData, }) - return response.data + return response.data.ocs.data } async deleteAttachment(attachment, boardId) { await axios({ method: 'DELETE', - url: this.ocsUrl(`/cards/${attachment.cardId}/attachment/${attachment.type}:${attachment.id}`), + url: this.ocsUrl(`/cards/${attachment.cardId}/attachments/${attachment.id}`), params: { + type: attachment.type, boardId: boardId ?? null, }, }) @@ -65,9 +68,10 @@ export class AttachmentApi { async restoreAttachment(attachment, boardId) { const response = await axios({ - method: 'GET', - url: this.ocsUrl(`/cards/${attachment.cardId}/attachment/${attachment.type}:${attachment.id}/restore`), + method: 'PUT', + url: this.ocsUrl(`/cards/${attachment.cardId}/attachments/${attachment.id}/restore`), params: { + type: attachment.type, boardId: boardId ?? null, }, }) diff --git a/src/services/AttachmentApi.spec.js b/src/services/AttachmentApi.spec.js new file mode 100644 index 0000000000..4172d8629c --- /dev/null +++ b/src/services/AttachmentApi.spec.js @@ -0,0 +1,86 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import axios from '@nextcloud/axios' +import { AttachmentApi } from './AttachmentApi.js' + +jest.mock('@nextcloud/axios', () => ({ + __esModule: true, + default: jest.fn(), +})) + +jest.mock('@nextcloud/router', () => ({ + __esModule: true, + generateUrl: jest.fn((url) => `/index.php${url}`), + generateOcsUrl: jest.fn((url) => `/ocs/v2.php${url.startsWith('/') ? url : '/' + url}`), +})) + +describe('AttachmentApi', () => { + const api = new AttachmentApi() + const attachment = { id: 3, cardId: 7, type: 'deck_file' } + const boardId = 5 + + beforeEach(() => { + axios.mockReset() + axios.mockResolvedValue({ data: { ocs: { data: {} } } }) + }) + + it('fetches attachments from the registered OCS route', async () => { + await api.fetchAttachments(7, boardId) + + expect(axios).toHaveBeenCalledWith(expect.objectContaining({ + method: 'GET', + url: '/ocs/v2.php/apps/deck/api/v1.0/cards/7/attachments', + params: { boardId }, + })) + }) + + it('creates an attachment through the registered OCS route', async () => { + const formData = { fake: 'formData' } + await api.createAttachment({ cardId: 7, formData, onUploadProgress: undefined, boardId }) + + expect(axios).toHaveBeenCalledWith(expect.objectContaining({ + method: 'POST', + url: '/ocs/v2.php/apps/deck/api/v1.0/cards/7/attachment', + params: { boardId }, + data: formData, + })) + }) + + it('deletes an attachment through the registered OCS route', async () => { + await api.deleteAttachment(attachment, boardId) + + expect(axios).toHaveBeenCalledWith(expect.objectContaining({ + method: 'DELETE', + url: '/ocs/v2.php/apps/deck/api/v1.0/cards/7/attachments/3', + params: { type: 'deck_file', boardId }, + })) + }) + + it('updates an attachment through the registered OCS route', async () => { + const updated = { id: 3, cardId: 7, type: 'deck_file', data: 'file.png' } + axios.mockResolvedValue({ data: { ocs: { data: updated } } }) + const formData = { fake: 'formData' } + const result = await api.updateAttachment({ cardId: 7, attachment, formData, boardId }) + expect(result).toEqual(updated) + + expect(axios).toHaveBeenCalledWith(expect.objectContaining({ + method: 'POST', + url: '/ocs/v2.php/apps/deck/api/v1.0/cards/7/attachments/3', + params: { type: 'deck_file', boardId }, + data: formData, + })) + }) + + it('restores an attachment through the registered OCS route', async () => { + await api.restoreAttachment(attachment, boardId) + + expect(axios).toHaveBeenCalledWith(expect.objectContaining({ + method: 'PUT', + url: '/ocs/v2.php/apps/deck/api/v1.0/cards/7/attachments/3/restore', + params: { type: 'deck_file', boardId }, + })) + }) +})