Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],

Expand Down
2 changes: 1 addition & 1 deletion lib/Controller/AttachmentOcsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
},
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
".*\\.(vue)$": "<rootDir>/node_modules/@vue/vue2-jest"
},
"snapshotSerializers": [
"<rootDir>/node_modules/jest-serializer-vue"
Expand Down
14 changes: 9 additions & 5 deletions src/services/AttachmentApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,32 +42,36 @@ 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,
},
})
}

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,
},
})
Expand Down
86 changes: 86 additions & 0 deletions src/services/AttachmentApi.spec.js
Original file line number Diff line number Diff line change
@@ -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 },
}))
})
})