diff --git a/apps/files/src/actions/viewInFolderAction.spec.ts b/apps/files/src/actions/viewInFolderAction.spec.ts index 693a24fb1da..887ed5d47c6 100644 --- a/apps/files/src/actions/viewInFolderAction.spec.ts +++ b/apps/files/src/actions/viewInFolderAction.spec.ts @@ -55,6 +55,19 @@ describe('View in folder action enabled tests', () => { expect(action.enabled!([file], view)).toBe(true) }) + test('Disabled without permissions', () => { + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.NONE, + }) + + expect(action.enabled).toBeDefined() + expect(action.enabled!([file], view)).toBe(false) + }) + test('Disabled for non-dav ressources', () => { const file = new File({ id: 1, diff --git a/apps/files_sharing/src/actions/acceptShareAction.spec.ts b/apps/files_sharing/src/actions/acceptShareAction.spec.ts new file mode 100644 index 00000000000..507d0013e79 --- /dev/null +++ b/apps/files_sharing/src/actions/acceptShareAction.spec.ts @@ -0,0 +1,223 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ + * + * @author John Molakvoæ + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { action } from './acceptShareAction' +import { expect } from '@jest/globals' +import { File, Folder, Permission } from '@nextcloud/files' +import { FileAction } from '../../../files/src/services/FileAction' +import * as eventBus from '@nextcloud/event-bus' +import axios from '@nextcloud/axios' +import type { Navigation } from '../../../files/src/services/Navigation' +import '../main' + +const view = { + id: 'files', + name: 'Files', +} as Navigation + +const pendingShareView = { + id: 'pendingshares', + name: 'Pending shares', +} as Navigation + +describe('Accept share action conditions tests', () => { + test('Default values', () => { + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.ALL, + }) + + expect(action).toBeInstanceOf(FileAction) + expect(action.id).toBe('accept-share') + expect(action.displayName([file], pendingShareView)).toBe('Accept share') + expect(action.iconSvgInline([file], pendingShareView)).toBe('SvgMock') + expect(action.default).toBeUndefined() + expect(action.order).toBe(1) + expect(action.inline).toBeDefined() + expect(action.inline!(file, pendingShareView)).toBe(true) + }) + + test('Default values for multiple files', () => { + const file1 = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.ALL, + }) + const file2 = new File({ + id: 2, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.ALL, + }) + + expect(action.displayName([file1, file2], pendingShareView)).toBe('Accept shares') + }) +}) + +describe('Accept share action enabled tests', () => { + test('Enabled with on pending shares view', () => { + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.ALL, + }) + + expect(action.enabled).toBeDefined() + expect(action.enabled!([file], pendingShareView)).toBe(true) + }) + + test('Disabled on wrong view', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([], view)).toBe(false) + }) + + test('Disabled without nodes', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([], pendingShareView)).toBe(false) + }) +}) + +describe('Accept share action execute tests', () => { + test('Accept share action', async () => { + jest.spyOn(axios, 'post') + jest.spyOn(eventBus, 'emit') + + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 123, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const exec = await action.exec(file, pendingShareView, '/') + + expect(exec).toBe(true) + expect(axios.post).toBeCalledTimes(1) + expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/pending/123') + + expect(eventBus.emit).toBeCalledTimes(1) + expect(eventBus.emit).toBeCalledWith('files:node:deleted', file) + }) + + test('Accept remote share action', async () => { + jest.spyOn(axios, 'post') + jest.spyOn(eventBus, 'emit') + + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 123, + remote: 3, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const exec = await action.exec(file, pendingShareView, '/') + + expect(exec).toBe(true) + expect(axios.post).toBeCalledTimes(1) + expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/123') + + expect(eventBus.emit).toBeCalledTimes(1) + expect(eventBus.emit).toBeCalledWith('files:node:deleted', file) + }) + + test('Accept share action batch', async () => { + jest.spyOn(axios, 'post') + jest.spyOn(eventBus, 'emit') + + const file1 = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 123, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const file2 = new File({ + id: 2, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 456, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const exec = await action.execBatch!([file1, file2], pendingShareView, '/') + + expect(exec).toStrictEqual([true, true]) + expect(axios.post).toBeCalledTimes(2) + expect(axios.post).toHaveBeenNthCalledWith(1, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/pending/123') + expect(axios.post).toHaveBeenNthCalledWith(2, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/pending/456') + + expect(eventBus.emit).toBeCalledTimes(2) + expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1) + expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2) + }) + + test('Accept fails', async () => { + jest.spyOn(axios, 'post').mockImplementation(() => { throw new Error('Mock error') }) + + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 123, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const exec = await action.exec(file, pendingShareView, '/') + + expect(exec).toBe(false) + expect(axios.post).toBeCalledTimes(1) + expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/pending/123') + + expect(eventBus.emit).toBeCalledTimes(0) + }) +}) diff --git a/apps/files_sharing/src/actions/acceptShareAction.ts b/apps/files_sharing/src/actions/acceptShareAction.ts index 8cdc138ca81..4be69633122 100644 --- a/apps/files_sharing/src/actions/acceptShareAction.ts +++ b/apps/files_sharing/src/actions/acceptShareAction.ts @@ -22,21 +22,21 @@ import type { Node } from '@nextcloud/files' import type { Navigation } from '../../../files/src/services/Navigation' +import { emit } from '@nextcloud/event-bus' import { generateOcsUrl } from '@nextcloud/router' -import { registerFileAction, FileAction } from '@nextcloud/files' import { translatePlural as n } from '@nextcloud/l10n' import axios from '@nextcloud/axios' import CheckSvg from '@mdi/svg/svg/check.svg?raw' +import { FileAction, registerFileAction } from '../../../files/src/services/FileAction' import { pendingSharesViewId } from '../views/shares' -import { emit } from '@nextcloud/event-bus' export const action = new FileAction({ id: 'accept-share', displayName: (nodes: Node[]) => n('files_sharing', 'Accept share', 'Accept shares', nodes.length), iconSvgInline: () => CheckSvg, - enabled: (files, view) => view.id === pendingSharesViewId, + enabled: (nodes, view) => nodes.length > 0 && view.id === pendingSharesViewId, async exec(node: Node) { try { diff --git a/apps/files_sharing/src/actions/rejectShareAction.spec.ts b/apps/files_sharing/src/actions/rejectShareAction.spec.ts new file mode 100644 index 00000000000..a075b45eedb --- /dev/null +++ b/apps/files_sharing/src/actions/rejectShareAction.spec.ts @@ -0,0 +1,250 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ + * + * @author John Molakvoæ + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { action } from './rejectShareAction' +import { expect } from '@jest/globals' +import { File, Folder, Permission } from '@nextcloud/files' +import { FileAction } from '../../../files/src/services/FileAction' +import * as eventBus from '@nextcloud/event-bus' +import axios from '@nextcloud/axios' +import type { Navigation } from '../../../files/src/services/Navigation' +import '../main' + +const view = { + id: 'files', + name: 'Files', +} as Navigation + +const pendingShareView = { + id: 'pendingshares', + name: 'Pending shares', +} as Navigation + +describe('Reject share action conditions tests', () => { + test('Default values', () => { + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.ALL, + }) + + expect(action).toBeInstanceOf(FileAction) + expect(action.id).toBe('reject-share') + expect(action.displayName([file], pendingShareView)).toBe('Reject share') + expect(action.iconSvgInline([file], pendingShareView)).toBe('SvgMock') + expect(action.default).toBeUndefined() + expect(action.order).toBe(2) + expect(action.inline).toBeDefined() + expect(action.inline!(file, pendingShareView)).toBe(true) + }) + + test('Default values for multiple files', () => { + const file1 = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.ALL, + }) + const file2 = new File({ + id: 2, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.ALL, + }) + + expect(action.displayName([file1, file2], pendingShareView)).toBe('Reject shares') + }) +}) + +describe('Reject share action enabled tests', () => { + test('Enabled with on pending shares view', () => { + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.ALL, + }) + + expect(action.enabled).toBeDefined() + expect(action.enabled!([file], pendingShareView)).toBe(true) + }) + + test('Disabled on wrong view', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([], view)).toBe(false) + }) + + test('Disabled without nodes', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([], pendingShareView)).toBe(false) + }) + + test('Disabled if some nodes are remote group shares', () => { + const folder1 = new Folder({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/', + owner: 'admin', + permissions: Permission.READ, + attributes: { + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + const folder2 = new Folder({ + id: 2, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/Bar/', + owner: 'admin', + permissions: Permission.READ, + attributes: { + remote_id: 1, + share_type: window.OC.Share.SHARE_TYPE_REMOTE_GROUP, + }, + }) + + expect(action.enabled).toBeDefined() + expect(action.enabled!([folder1], pendingShareView)).toBe(true) + expect(action.enabled!([folder2], pendingShareView)).toBe(false) + expect(action.enabled!([folder1, folder2], pendingShareView)).toBe(false) + }) +}) + +describe('Reject share action execute tests', () => { + test('Reject share action', async () => { + jest.spyOn(axios, 'delete') + jest.spyOn(eventBus, 'emit') + + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 123, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const exec = await action.exec(file, pendingShareView, '/') + + expect(exec).toBe(true) + expect(axios.delete).toBeCalledTimes(1) + expect(axios.delete).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/123') + + expect(eventBus.emit).toBeCalledTimes(1) + expect(eventBus.emit).toBeCalledWith('files:node:deleted', file) + }) + + test('Reject remote share action', async () => { + jest.spyOn(axios, 'delete') + jest.spyOn(eventBus, 'emit') + + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 123, + remote: 3, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const exec = await action.exec(file, pendingShareView, '/') + + expect(exec).toBe(true) + expect(axios.delete).toBeCalledTimes(1) + expect(axios.delete).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/123') + + expect(eventBus.emit).toBeCalledTimes(1) + expect(eventBus.emit).toBeCalledWith('files:node:deleted', file) + }) + + test('Reject share action batch', async () => { + jest.spyOn(axios, 'delete') + jest.spyOn(eventBus, 'emit') + + const file1 = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 123, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const file2 = new File({ + id: 2, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 456, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const exec = await action.execBatch!([file1, file2], pendingShareView, '/') + + expect(exec).toStrictEqual([true, true]) + expect(axios.delete).toBeCalledTimes(2) + expect(axios.delete).toHaveBeenNthCalledWith(1, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/123') + expect(axios.delete).toHaveBeenNthCalledWith(2, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/456') + + expect(eventBus.emit).toBeCalledTimes(2) + expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1) + expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2) + }) + + test('Reject fails', async () => { + jest.spyOn(axios, 'delete').mockImplementation(() => { throw new Error('Mock error') }) + + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 123, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const exec = await action.exec(file, pendingShareView, '/') + + expect(exec).toBe(false) + expect(axios.delete).toBeCalledTimes(1) + expect(axios.delete).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/shares/123') + + expect(eventBus.emit).toBeCalledTimes(0) + }) +}) diff --git a/apps/files_sharing/src/actions/rejectShareAction.ts b/apps/files_sharing/src/actions/rejectShareAction.ts index 3d14896738a..44dd36abe55 100644 --- a/apps/files_sharing/src/actions/rejectShareAction.ts +++ b/apps/files_sharing/src/actions/rejectShareAction.ts @@ -24,11 +24,11 @@ import type { Navigation } from '../../../files/src/services/Navigation' import { emit } from '@nextcloud/event-bus' import { generateOcsUrl } from '@nextcloud/router' -import { registerFileAction, FileAction } from '@nextcloud/files' import { translatePlural as n } from '@nextcloud/l10n' import axios from '@nextcloud/axios' import CloseSvg from '@mdi/svg/svg/close.svg?raw' +import { FileAction, registerFileAction } from '../../../files/src/services/FileAction' import { pendingSharesViewId } from '../views/shares' export const action = new FileAction({ @@ -41,12 +41,17 @@ export const action = new FileAction({ return false } + if (nodes.length === 0) { + return false + } + // disable rejecting group shares from the pending list because they anyway // land back into that same list after rejecting them if (nodes.some(node => node.attributes.remote_id && node.attributes.share_type === window.OC.Share.SHARE_TYPE_REMOTE_GROUP)) { return false } + return true }, diff --git a/apps/files_sharing/src/actions/restoreShareAction.spec.ts b/apps/files_sharing/src/actions/restoreShareAction.spec.ts new file mode 100644 index 00000000000..8788a5cc6eb --- /dev/null +++ b/apps/files_sharing/src/actions/restoreShareAction.spec.ts @@ -0,0 +1,196 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ + * + * @author John Molakvoæ + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { action } from './restoreShareAction' +import { expect } from '@jest/globals' +import { File, Folder, Permission } from '@nextcloud/files' +import { FileAction } from '../../../files/src/services/FileAction' +import * as eventBus from '@nextcloud/event-bus' +import axios from '@nextcloud/axios' +import type { Navigation } from '../../../files/src/services/Navigation' +import '../main' + +const view = { + id: 'files', + name: 'Files', +} as Navigation + +const deletedShareView = { + id: 'deletedshares', + name: 'Deleted shares', +} as Navigation + +describe('Restore share action conditions tests', () => { + test('Default values', () => { + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.ALL, + }) + + expect(action).toBeInstanceOf(FileAction) + expect(action.id).toBe('restore-share') + expect(action.displayName([file], deletedShareView)).toBe('Restore share') + expect(action.iconSvgInline([file], deletedShareView)).toBe('SvgMock') + expect(action.default).toBeUndefined() + expect(action.order).toBe(1) + expect(action.inline).toBeDefined() + expect(action.inline!(file, deletedShareView)).toBe(true) + }) + + test('Default values for multiple files', () => { + const file1 = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.ALL, + }) + const file2 = new File({ + id: 2, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.ALL, + }) + + expect(action.displayName([file1, file2], deletedShareView)).toBe('Restore shares') + }) +}) + +describe('Restore share action enabled tests', () => { + test('Enabled with on pending shares view', () => { + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.ALL, + }) + + expect(action.enabled).toBeDefined() + expect(action.enabled!([file], deletedShareView)).toBe(true) + }) + + test('Disabled on wrong view', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([], view)).toBe(false) + }) + + test('Disabled without nodes', () => { + expect(action.enabled).toBeDefined() + expect(action.enabled!([], deletedShareView)).toBe(false) + }) +}) + +describe('Restore share action execute tests', () => { + test('Restore share action', async () => { + jest.spyOn(axios, 'post') + jest.spyOn(eventBus, 'emit') + + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 123, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const exec = await action.exec(file, deletedShareView, '/') + + expect(exec).toBe(true) + expect(axios.post).toBeCalledTimes(1) + expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/deletedshares/123') + + expect(eventBus.emit).toBeCalledTimes(1) + expect(eventBus.emit).toBeCalledWith('files:node:deleted', file) + }) + + test('Restore share action batch', async () => { + jest.spyOn(axios, 'post') + jest.spyOn(eventBus, 'emit') + + const file1 = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 123, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const file2 = new File({ + id: 2, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 456, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const exec = await action.execBatch!([file1, file2], deletedShareView, '/') + + expect(exec).toStrictEqual([true, true]) + expect(axios.post).toBeCalledTimes(2) + expect(axios.post).toHaveBeenNthCalledWith(1, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/deletedshares/123') + expect(axios.post).toHaveBeenNthCalledWith(2, 'http://localhost/ocs/v2.php/apps/files_sharing/api/v1/deletedshares/456') + + expect(eventBus.emit).toBeCalledTimes(2) + expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1) + expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2) + }) + + test('Restore fails', async () => { + jest.spyOn(axios, 'post').mockImplementation(() => { throw new Error('Mock error') }) + + const file = new File({ + id: 1, + source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt', + owner: 'admin', + mime: 'text/plain', + permissions: Permission.READ, + attributes: { + id: 123, + share_type: window.OC.Share.SHARE_TYPE_USER, + }, + }) + + const exec = await action.exec(file, deletedShareView, '/') + + expect(exec).toBe(false) + expect(axios.post).toBeCalledTimes(1) + expect(axios.post).toBeCalledWith('http://localhost/ocs/v2.php/apps/files_sharing/api/v1/deletedshares/123') + + expect(eventBus.emit).toBeCalledTimes(0) + }) +}) diff --git a/apps/files_sharing/src/actions/restoreShareAction.ts b/apps/files_sharing/src/actions/restoreShareAction.ts index 8e5a49a12b6..6c43b0cfb37 100644 --- a/apps/files_sharing/src/actions/restoreShareAction.ts +++ b/apps/files_sharing/src/actions/restoreShareAction.ts @@ -22,14 +22,14 @@ import type { Node } from '@nextcloud/files' import type { Navigation } from '../../../files/src/services/Navigation' +import { emit } from '@nextcloud/event-bus' import { generateOcsUrl } from '@nextcloud/router' -import { registerFileAction, FileAction } from '@nextcloud/files' import { translatePlural as n } from '@nextcloud/l10n' import axios from '@nextcloud/axios' import ArrowULeftTopSvg from '@mdi/svg/svg/arrow-u-left-top.svg?raw' +import { FileAction, registerFileAction } from '../../../files/src/services/FileAction' import { deletedSharesViewId } from '../views/shares' -import { emit } from '@nextcloud/event-bus' export const action = new FileAction({ id: 'restore-share', @@ -37,7 +37,7 @@ export const action = new FileAction({ iconSvgInline: () => ArrowULeftTopSvg, - enabled: (nodes, view) => view.id === deletedSharesViewId, + enabled: (nodes, view) => nodes.length > 0 && view.id === deletedSharesViewId, async exec(node: Node) { try { diff --git a/apps/files_sharing/src/main.ts b/apps/files_sharing/src/main.ts index 040dd2e17ad..8462d5b542e 100644 --- a/apps/files_sharing/src/main.ts +++ b/apps/files_sharing/src/main.ts @@ -22,6 +22,10 @@ */ // register default shares types +if (!window.OC) { + window.OC = {} +} + Object.assign(window.OC, { Share: { SHARE_TYPE_USER: 0, diff --git a/jest.config.ts b/jest.config.ts index 97a2bdf576e..0763be02678 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -23,6 +23,7 @@ import type { Config } from 'jest' // TODO: find a way to consolidate this in one place, with webpack.common.js const ignorePatterns = [ + '@buttercup/fetch', '@juliushaertl', '@mdi/svg', '@nextcloud/vue', @@ -34,6 +35,7 @@ const ignorePatterns = [ 'strip-ansi', 'tributejs', 'vue-material-design-icons', + 'webdav', ] const config: Config = {