Skip to content

Commit 8b4731e

Browse files
committed
fix(files): always ask for confirmation if trashbin app is disabled
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
1 parent 36130ac commit 8b4731e

5 files changed

Lines changed: 367 additions & 122 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
import type { Capabilities } from '../../apps/files/src/types'
6+
7+
export const getCapabilities = (): Capabilities => {
8+
return {
9+
files: {
10+
bigfilechunking: true,
11+
blacklisted_files: [],
12+
forbidden_filename_basenames: [],
13+
forbidden_filename_characters: [],
14+
forbidden_filename_extensions: [],
15+
forbidden_filenames: [],
16+
undelete: true,
17+
version_deletion: true,
18+
version_labeling: true,
19+
versioning: true,
20+
},
21+
}
22+
}

apps/files/src/actions/deleteAction.spec.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { action } from './deleteAction'
66
import { expect } from '@jest/globals'
77
import { File, Folder, Permission, View, FileAction } from '@nextcloud/files'
88
import axios from '@nextcloud/axios'
9+
import * as capabilities from '@nextcloud/capabilities'
910
import eventBus from '@nextcloud/event-bus'
1011

1112
import logger from '../logger'
@@ -94,6 +95,16 @@ describe('Delete action conditions tests', () => {
9495
expect(action.displayName([file], trashbinView)).toBe('Delete permanently')
9596
})
9697

98+
test('Trashbin disabled displayName', () => {
99+
jest.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
100+
return {
101+
files: {},
102+
}
103+
})
104+
expect(action.displayName([file], view)).toBe('Delete permanently')
105+
expect(capabilities.getCapabilities).toBeCalledTimes(1)
106+
})
107+
97108
test('Shared root node displayName', () => {
98109
expect(action.displayName([file2], view)).toBe('Leave this share')
99110
expect(action.displayName([folder2], view)).toBe('Leave this share')
@@ -164,6 +175,9 @@ describe('Delete action enabled tests', () => {
164175
})
165176

166177
describe('Delete action execute tests', () => {
178+
afterEach(() => {
179+
jest.restoreAllMocks()
180+
})
167181
test('Delete action', async () => {
168182
jest.spyOn(axios, 'delete')
169183
jest.spyOn(eventBus, 'emit')
@@ -225,9 +239,125 @@ describe('Delete action execute tests', () => {
225239
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
226240
})
227241

242+
test('Delete action batch large set', async () => {
243+
jest.spyOn(axios, 'delete')
244+
jest.spyOn(eventBus, 'emit')
245+
246+
// Emulate the confirmation dialog to always confirm
247+
const confirmMock = jest.fn().mockImplementation((a, b, c, resolve) => resolve(true))
248+
// @ts-expect-error We only mock what needed
249+
window.OC = { dialogs: { confirmDestructive: confirmMock } }
250+
251+
const file1 = new File({
252+
id: 1,
253+
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
254+
owner: 'test',
255+
mime: 'text/plain',
256+
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
257+
})
258+
259+
const file2 = new File({
260+
id: 2,
261+
source: 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt',
262+
owner: 'test',
263+
mime: 'text/plain',
264+
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
265+
})
266+
267+
const file3 = new File({
268+
id: 3,
269+
source: 'https://cloud.domain.com/remote.php/dav/files/test/baz.txt',
270+
owner: 'test',
271+
mime: 'text/plain',
272+
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
273+
})
274+
275+
const file4 = new File({
276+
id: 4,
277+
source: 'https://cloud.domain.com/remote.php/dav/files/test/qux.txt',
278+
owner: 'test',
279+
mime: 'text/plain',
280+
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
281+
})
282+
283+
const file5 = new File({
284+
id: 5,
285+
source: 'https://cloud.domain.com/remote.php/dav/files/test/quux.txt',
286+
owner: 'test',
287+
mime: 'text/plain',
288+
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
289+
})
290+
291+
const exec = await action.execBatch!([file1, file2, file3, file4, file5], view, '/')
292+
293+
// Enough nodes to trigger a confirmation dialog
294+
expect(confirmMock).toBeCalledTimes(1)
295+
296+
expect(exec).toStrictEqual([true, true, true, true, true])
297+
expect(axios.delete).toBeCalledTimes(5)
298+
expect(axios.delete).toHaveBeenNthCalledWith(1, 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt')
299+
expect(axios.delete).toHaveBeenNthCalledWith(2, 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt')
300+
expect(axios.delete).toHaveBeenNthCalledWith(3, 'https://cloud.domain.com/remote.php/dav/files/test/baz.txt')
301+
expect(axios.delete).toHaveBeenNthCalledWith(4, 'https://cloud.domain.com/remote.php/dav/files/test/qux.txt')
302+
expect(axios.delete).toHaveBeenNthCalledWith(5, 'https://cloud.domain.com/remote.php/dav/files/test/quux.txt')
303+
304+
expect(eventBus.emit).toBeCalledTimes(5)
305+
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1)
306+
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
307+
expect(eventBus.emit).toHaveBeenNthCalledWith(3, 'files:node:deleted', file3)
308+
expect(eventBus.emit).toHaveBeenNthCalledWith(4, 'files:node:deleted', file4)
309+
expect(eventBus.emit).toHaveBeenNthCalledWith(5, 'files:node:deleted', file5)
310+
})
311+
312+
test('Delete action batch trashbin disabled', async () => {
313+
jest.spyOn(axios, 'delete')
314+
jest.spyOn(eventBus, 'emit')
315+
jest.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
316+
return {
317+
files: {},
318+
}
319+
})
320+
321+
// Emulate the confirmation dialog to always confirm
322+
const confirmMock = jest.fn().mockImplementation((a, b, c, resolve) => resolve(true))
323+
// @ts-expect-error We only mock what needed
324+
window.OC = { dialogs: { confirmDestructive: confirmMock } }
325+
326+
const file1 = new File({
327+
id: 1,
328+
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
329+
owner: 'test',
330+
mime: 'text/plain',
331+
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
332+
})
333+
334+
const file2 = new File({
335+
id: 2,
336+
source: 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt',
337+
owner: 'test',
338+
mime: 'text/plain',
339+
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
340+
})
341+
342+
const exec = await action.execBatch!([file1, file2], view, '/')
343+
344+
// Will trigger a confirmation dialog because trashbin app is disabled
345+
expect(confirmMock).toBeCalledTimes(1)
346+
347+
expect(exec).toStrictEqual([true, true])
348+
expect(axios.delete).toBeCalledTimes(2)
349+
expect(axios.delete).toHaveBeenNthCalledWith(1, 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt')
350+
expect(axios.delete).toHaveBeenNthCalledWith(2, 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt')
351+
352+
expect(eventBus.emit).toBeCalledTimes(2)
353+
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1)
354+
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
355+
})
356+
228357
test('Delete fails', async () => {
229358
jest.spyOn(axios, 'delete').mockImplementation(() => { throw new Error('Mock error') })
230359
jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
360+
jest.spyOn(eventBus, 'emit')
231361

232362
const file = new File({
233363
id: 1,
@@ -246,4 +376,36 @@ describe('Delete action execute tests', () => {
246376
expect(eventBus.emit).toBeCalledTimes(0)
247377
expect(logger.error).toBeCalledTimes(1)
248378
})
379+
380+
test('Delete is cancelled', async () => {
381+
jest.spyOn(axios, 'delete')
382+
jest.spyOn(eventBus, 'emit')
383+
jest.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
384+
return {
385+
files: {},
386+
}
387+
})
388+
389+
// Emulate the confirmation dialog to always confirm
390+
const confirmMock = jest.fn().mockImplementation((a, b, c, resolve) => resolve(false))
391+
// @ts-expect-error We only mock what needed
392+
window.OC = { dialogs: { confirmDestructive: confirmMock } }
393+
394+
const file1 = new File({
395+
id: 1,
396+
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
397+
owner: 'test',
398+
mime: 'text/plain',
399+
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
400+
})
401+
402+
const exec = await action.execBatch!([file1], view, '/')
403+
404+
expect(confirmMock).toBeCalledTimes(1)
405+
406+
expect(exec).toStrictEqual([null])
407+
expect(axios.delete).toBeCalledTimes(0)
408+
409+
expect(eventBus.emit).toBeCalledTimes(0)
410+
})
249411
})

0 commit comments

Comments
 (0)