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/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" 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 }, + })) + }) +})