Skip to content

Commit 6df8bdf

Browse files
committed
fix(files): always ask for confirmation if trashbin app is disabled
Signed-off-by: skjnldsv <[email protected]> Signed-off-by: nextcloud-command <[email protected]> Signed-off-by: nextcloud-command <[email protected]>
1 parent e61807c commit 6df8bdf

13 files changed

+377
-135
lines changed
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: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
import { action } from './deleteAction'
2323
import { expect } from '@jest/globals'
2424
import { File, Folder, Permission, View, FileAction } from '@nextcloud/files'
25-
import eventBus from '@nextcloud/event-bus'
25+
import * as capabilities from '@nextcloud/capabilities'
2626
import axios from '@nextcloud/axios'
27+
import eventBus from '@nextcloud/event-bus'
2728

2829
import logger from '../logger'
2930

@@ -111,6 +112,16 @@ describe('Delete action conditions tests', () => {
111112
expect(action.displayName([file], trashbinView)).toBe('Delete permanently')
112113
})
113114

115+
test('Trashbin disabled displayName', () => {
116+
jest.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
117+
return {
118+
files: {},
119+
}
120+
})
121+
expect(action.displayName([file], view)).toBe('Delete permanently')
122+
expect(capabilities.getCapabilities).toBeCalledTimes(1)
123+
})
124+
114125
test('Shared root node displayName', () => {
115126
expect(action.displayName([file2], view)).toBe('Leave this share')
116127
expect(action.displayName([folder2], view)).toBe('Leave this share')
@@ -181,6 +192,9 @@ describe('Delete action enabled tests', () => {
181192
})
182193

183194
describe('Delete action execute tests', () => {
195+
afterEach(() => {
196+
jest.restoreAllMocks()
197+
})
184198
test('Delete action', async () => {
185199
jest.spyOn(axios, 'delete')
186200
jest.spyOn(eventBus, 'emit')
@@ -241,9 +255,123 @@ describe('Delete action execute tests', () => {
241255
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
242256
})
243257

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

248376
const file = new File({
249377
id: 1,
@@ -262,4 +390,35 @@ describe('Delete action execute tests', () => {
262390
expect(eventBus.emit).toBeCalledTimes(0)
263391
expect(logger.error).toBeCalledTimes(1)
264392
})
393+
394+
test('Delete is cancelled', async () => {
395+
jest.spyOn(axios, 'delete')
396+
jest.spyOn(eventBus, 'emit')
397+
jest.spyOn(capabilities, 'getCapabilities').mockImplementation(() => {
398+
return {
399+
files: {},
400+
}
401+
})
402+
403+
// Emulate the confirmation dialog to always confirm
404+
const confirmMock = jest.fn().mockImplementation((a, b, c, resolve) => resolve(false))
405+
window.OC = { dialogs: { confirmDestructive: confirmMock } }
406+
407+
const file1 = new File({
408+
id: 1,
409+
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
410+
owner: 'test',
411+
mime: 'text/plain',
412+
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
413+
})
414+
415+
const exec = await action.execBatch!([file1], view, '/')
416+
417+
expect(confirmMock).toBeCalledTimes(1)
418+
419+
expect(exec).toStrictEqual([null])
420+
expect(axios.delete).toBeCalledTimes(0)
421+
422+
expect(eventBus.emit).toBeCalledTimes(0)
423+
})
265424
})

0 commit comments

Comments
 (0)