diff options
Diffstat (limited to 'core/src/tests')
-rw-r--r-- | core/src/tests/.eslintrc.js | 22 | ||||
-rw-r--r-- | core/src/tests/OC/requesttoken.spec.js | 73 | ||||
-rw-r--r-- | core/src/tests/OC/requesttoken.spec.ts | 147 | ||||
-rw-r--r-- | core/src/tests/OC/session-heartbeat.spec.ts | 123 | ||||
-rw-r--r-- | core/src/tests/components/ContactsMenu/Contact.spec.js | 27 | ||||
-rw-r--r-- | core/src/tests/views/ContactsMenu.spec.js | 53 |
6 files changed, 299 insertions, 146 deletions
diff --git a/core/src/tests/.eslintrc.js b/core/src/tests/.eslintrc.js index b44ea2c697d..598fc5c28b4 100644 --- a/core/src/tests/.eslintrc.js +++ b/core/src/tests/.eslintrc.js @@ -1,25 +1,7 @@ /** - * @copyright Copyright (c) 2016 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @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 <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ - module.exports = { globals: { jsdom: true, diff --git a/core/src/tests/OC/requesttoken.spec.js b/core/src/tests/OC/requesttoken.spec.js deleted file mode 100644 index 57a3d4de3c1..00000000000 --- a/core/src/tests/OC/requesttoken.spec.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author François Freitag <mail@franek.fr> - * - * @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 <http://www.gnu.org/licenses/>. - * - */ - -import { subscribe, unsubscribe } from '@nextcloud/event-bus' - -import { manageToken, setToken } from '../../OC/requesttoken.js' - -describe('request token', () => { - - let emit - let manager - const token = 'abc123' - - beforeEach(() => { - emit = jest.fn() - const head = window.document.getElementsByTagName('head')[0] - head.setAttribute('data-requesttoken', token) - - manager = manageToken(window.document, emit) - }) - - test('reads the token from the document', () => { - expect(manager.getToken()).toBe('abc123') - }) - - test('remembers the updated token', () => { - manager.setToken('bca321') - - expect(manager.getToken()).toBe('bca321') - }) - - describe('@nextcloud/auth integration', () => { - let listener - - beforeEach(() => { - listener = jest.fn() - - subscribe('csrf-token-update', listener) - }) - - afterEach(() => { - unsubscribe('csrf-token-update', listener) - }) - - test('fires off an event for @nextcloud/auth', () => { - setToken('123') - - expect(listener).toHaveBeenCalledWith({ token: '123' }) - }) - }) - -}) diff --git a/core/src/tests/OC/requesttoken.spec.ts b/core/src/tests/OC/requesttoken.spec.ts new file mode 100644 index 00000000000..8f92dbed153 --- /dev/null +++ b/core/src/tests/OC/requesttoken.spec.ts @@ -0,0 +1,147 @@ +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { setupServer } from 'msw/node' +import { http, HttpResponse } from 'msw' +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { fetchRequestToken, getRequestToken, setRequestToken } from '../../OC/requesttoken.ts' + +const eventbus = vi.hoisted(() => ({ emit: vi.fn() })) +vi.mock('@nextcloud/event-bus', () => eventbus) + +const server = setupServer() + +describe('getRequestToken', () => { + it('can read the token from DOM', () => { + mockToken('tokenmock-123') + expect(getRequestToken()).toBe('tokenmock-123') + }) + + it('can handle missing token', () => { + mockToken(undefined) + expect(getRequestToken()).toBeUndefined() + }) +}) + +describe('setRequestToken', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + it('does emit an event on change', () => { + setRequestToken('new-token') + expect(eventbus.emit).toBeCalledTimes(1) + expect(eventbus.emit).toBeCalledWith('csrf-token-update', { token: 'new-token' }) + }) + + it('does set the new token to the DOM', () => { + setRequestToken('new-token') + expect(document.head.dataset.requesttoken).toBe('new-token') + }) + + it('does remember the new token', () => { + mockToken('old-token') + setRequestToken('new-token') + expect(getRequestToken()).toBe('new-token') + }) + + it('throws if the token is not a string', () => { + // @ts-expect-error mocking + expect(() => setRequestToken(123)).toThrowError('Invalid CSRF token given') + }) + + it('throws if the token is not valid', () => { + expect(() => setRequestToken('')).toThrowError('Invalid CSRF token given') + }) + + it('does not emit an event if the token is not valid', () => { + expect(() => setRequestToken('')).toThrowError('Invalid CSRF token given') + expect(eventbus.emit).not.toBeCalled() + }) +}) + +describe('fetchRequestToken', () => { + const successfullCsrf = http.get('/index.php/csrftoken', () => { + return HttpResponse.json({ token: 'new-token' }) + }) + const forbiddenCsrf = http.get('/index.php/csrftoken', () => { + return HttpResponse.json([], { status: 403 }) + }) + const serverErrorCsrf = http.get('/index.php/csrftoken', () => { + return HttpResponse.json([], { status: 500 }) + }) + const networkErrorCsrf = http.get('/index.php/csrftoken', () => { + return new HttpResponse(null, { type: 'error' }) + }) + + beforeAll(() => { + server.listen() + }) + + beforeEach(() => { + vi.resetAllMocks() + }) + + it('correctly parses response', async () => { + server.use(successfullCsrf) + + mockToken('oldToken') + const token = await fetchRequestToken() + expect(token).toBe('new-token') + }) + + it('sets the token', async () => { + server.use(successfullCsrf) + + mockToken('oldToken') + await fetchRequestToken() + expect(getRequestToken()).toBe('new-token') + }) + + it('does emit an event', async () => { + server.use(successfullCsrf) + + await fetchRequestToken() + expect(eventbus.emit).toHaveBeenCalledOnce() + expect(eventbus.emit).toBeCalledWith('csrf-token-update', { token: 'new-token' }) + }) + + it('handles 403 error due to invalid cookies', async () => { + server.use(forbiddenCsrf) + + mockToken('oldToken') + await expect(() => fetchRequestToken()).rejects.toThrowError('Could not fetch CSRF token from API') + expect(getRequestToken()).toBe('oldToken') + }) + + it('handles server error', async () => { + server.use(serverErrorCsrf) + + mockToken('oldToken') + await expect(() => fetchRequestToken()).rejects.toThrowError('Could not fetch CSRF token from API') + expect(getRequestToken()).toBe('oldToken') + }) + + it('handles network error', async () => { + server.use(networkErrorCsrf) + + mockToken('oldToken') + await expect(() => fetchRequestToken()).rejects.toThrow() + expect(getRequestToken()).toBe('oldToken') + }) +}) + +/** + * Mock the request token directly so we can test reading it. + * + * @param token - The CSRF token to mock + */ +function mockToken(token?: string) { + if (token === undefined) { + delete document.head.dataset.requesttoken + } else { + document.head.dataset.requesttoken = token + } +} diff --git a/core/src/tests/OC/session-heartbeat.spec.ts b/core/src/tests/OC/session-heartbeat.spec.ts new file mode 100644 index 00000000000..61b82d92887 --- /dev/null +++ b/core/src/tests/OC/session-heartbeat.spec.ts @@ -0,0 +1,123 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const requestToken = vi.hoisted(() => ({ + fetchRequestToken: vi.fn<() => Promise<string>>(), + setRequestToken: vi.fn<(token: string) => void>(), +})) +vi.mock('../../OC/requesttoken.ts', () => requestToken) + +const initialState = vi.hoisted(() => ({ loadState: vi.fn() })) +vi.mock('@nextcloud/initial-state', () => initialState) + +describe('Session heartbeat', () => { + beforeAll(() => { + vi.useFakeTimers() + }) + + beforeEach(() => { + vi.clearAllTimers() + vi.resetModules() + vi.resetAllMocks() + }) + + it('sends heartbeat half the session lifetime when heartbeat enabled', async () => { + initialState.loadState.mockImplementationOnce(() => ({ + session_keepalive: true, + session_lifetime: 300, + })) + + const { initSessionHeartBeat } = await import('../../session-heartbeat.ts') + initSessionHeartBeat() + + // initial state loaded + expect(initialState.loadState).toBeCalledWith('core', 'config', {}) + + // less than half, still nothing + await vi.advanceTimersByTimeAsync(100 * 1000) + expect(requestToken.fetchRequestToken).not.toBeCalled() + + // reach past half, one call + await vi.advanceTimersByTimeAsync(60 * 1000) + expect(requestToken.fetchRequestToken).toBeCalledTimes(1) + + // almost there to the next, still one + await vi.advanceTimersByTimeAsync(135 * 1000) + expect(requestToken.fetchRequestToken).toBeCalledTimes(1) + + // past it, second call + await vi.advanceTimersByTimeAsync(5 * 1000) + expect(requestToken.fetchRequestToken).toBeCalledTimes(2) + }) + + it('does not send heartbeat when heartbeat disabled', async () => { + initialState.loadState.mockImplementationOnce(() => ({ + session_keepalive: false, + session_lifetime: 300, + })) + + const { initSessionHeartBeat } = await import('../../session-heartbeat.ts') + initSessionHeartBeat() + + // initial state loaded + expect(initialState.loadState).toBeCalledWith('core', 'config', {}) + + // less than half, still nothing + await vi.advanceTimersByTimeAsync(100 * 1000) + expect(requestToken.fetchRequestToken).not.toBeCalled() + + // more than one, still nothing + await vi.advanceTimersByTimeAsync(300 * 1000) + expect(requestToken.fetchRequestToken).not.toBeCalled() + }) + + it('limit heartbeat to at least one minute', async () => { + initialState.loadState.mockImplementationOnce(() => ({ + session_keepalive: true, + session_lifetime: 55, + })) + + const { initSessionHeartBeat } = await import('../../session-heartbeat.ts') + initSessionHeartBeat() + + // initial state loaded + expect(initialState.loadState).toBeCalledWith('core', 'config', {}) + + // 30 / 55 seconds + await vi.advanceTimersByTimeAsync(30 * 1000) + expect(requestToken.fetchRequestToken).not.toBeCalled() + + // 59 / 55 seconds should not be called except it does not limit + await vi.advanceTimersByTimeAsync(29 * 1000) + expect(requestToken.fetchRequestToken).not.toBeCalled() + + // now one minute has passed + await vi.advanceTimersByTimeAsync(1000) + expect(requestToken.fetchRequestToken).toHaveBeenCalledOnce() + }) + + it('limit heartbeat to at least one minute', async () => { + initialState.loadState.mockImplementationOnce(() => ({ + session_keepalive: true, + session_lifetime: 50 * 60 * 60, + })) + + const { initSessionHeartBeat } = await import('../../session-heartbeat.ts') + initSessionHeartBeat() + + // initial state loaded + expect(initialState.loadState).toBeCalledWith('core', 'config', {}) + + // 23 hours + await vi.advanceTimersByTimeAsync(23 * 60 * 60 * 1000) + expect(requestToken.fetchRequestToken).not.toBeCalled() + + // one day - it should be called now + await vi.advanceTimersByTimeAsync(60 * 60 * 1000) + expect(requestToken.fetchRequestToken).toHaveBeenCalledOnce() + }) +}) diff --git a/core/src/tests/components/ContactsMenu/Contact.spec.js b/core/src/tests/components/ContactsMenu/Contact.spec.js index bdf0238e5f9..e83f75bfd15 100644 --- a/core/src/tests/components/ContactsMenu/Contact.spec.js +++ b/core/src/tests/components/ContactsMenu/Contact.spec.js @@ -1,24 +1,9 @@ /** - * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * 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 <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ +import { describe, expect, it } from 'vitest' import { shallowMount } from '@vue/test-utils' import Contact from '../../../components/ContactsMenu/Contact.vue' @@ -33,19 +18,19 @@ describe('Contact', function() { topAction: { title: 'Mail', icon: 'icon-mail', - hyperlink: 'mailto:deboraoliver%40centrexin.com' + hyperlink: 'mailto:deboraoliver%40centrexin.com', }, emailAddresses: [], actions: [ { title: 'Mail', icon: 'icon-mail', - hyperlink: 'mailto:mathisholland%40virxo.com' + hyperlink: 'mailto:mathisholland%40virxo.com', }, { title: 'Details', icon: 'icon-info', - hyperlink: 'https://localhost/index.php/apps/contacts' + hyperlink: 'https://localhost/index.php/apps/contacts', }, ], lastMessage: '', diff --git a/core/src/tests/views/ContactsMenu.spec.js b/core/src/tests/views/ContactsMenu.spec.js index 6b438a4998e..084c3215e47 100644 --- a/core/src/tests/views/ContactsMenu.spec.js +++ b/core/src/tests/views/ContactsMenu.spec.js @@ -1,31 +1,20 @@ /** - * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * 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 <http://www.gnu.org/licenses/>. + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ -import axios from '@nextcloud/axios' import { mount, shallowMount } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' import ContactsMenu from '../../views/ContactsMenu.vue' -jest.mock('@nextcloud/axios', () => ({ - post: jest.fn(), +const axios = vi.hoisted(() => ({ + post: vi.fn(), +})) +vi.mock('@nextcloud/axios', () => ({ default: axios })) + +vi.mock('@nextcloud/auth', () => ({ + getCurrentUser: () => ({ uid: 'user', isAdmin: false, displayName: 'User' }), })) describe('ContactsMenu', function() { @@ -55,7 +44,7 @@ describe('ContactsMenu', function() { it('shows error view when contacts can not be loaded', async () => { const view = mount(ContactsMenu) axios.post.mockResolvedValue({}) - jest.spyOn(console, 'error').mockImplementation(() => {}) + vi.spyOn(console, 'error').mockImplementation(() => {}) try { await view.vm.handleOpen() @@ -72,7 +61,7 @@ describe('ContactsMenu', function() { it('shows text when there are no contacts', async () => { const view = mount(ContactsMenu) - axios.post.mockResolvedValue({ + axios.post.mockResolvedValueOnce({ data: { contacts: [], contactsAppEnabled: false, @@ -98,19 +87,19 @@ describe('ContactsMenu', function() { topAction: { title: 'Mail', icon: 'icon-mail', - hyperlink: 'mailto:deboraoliver%40centrexin.com' + hyperlink: 'mailto:deboraoliver%40centrexin.com', }, actions: [ { title: 'Mail', icon: 'icon-mail', - hyperlink: 'mailto:mathisholland%40virxo.com' + hyperlink: 'mailto:mathisholland%40virxo.com', }, { title: 'Details', icon: 'icon-info', - hyperlink: 'https://localhost/index.php/apps/contacts' - } + hyperlink: 'https://localhost/index.php/apps/contacts', + }, ], lastMessage: '', emailAddresses: [], @@ -121,23 +110,23 @@ describe('ContactsMenu', function() { topAction: { title: 'Mail', icon: 'icon-mail', - hyperlink: 'mailto:ceciliasoto%40essensia.com' + hyperlink: 'mailto:ceciliasoto%40essensia.com', }, actions: [ { title: 'Mail', icon: 'icon-mail', - hyperlink: 'mailto:pearliesellers%40inventure.com' + hyperlink: 'mailto:pearliesellers%40inventure.com', }, { title: 'Details', icon: 'icon-info', - hyperlink: 'https://localhost/index.php/apps/contacts' - } + hyperlink: 'https://localhost/index.php/apps/contacts', + }, ], lastMessage: 'cu', emailAddresses: [], - } + }, ], contactsAppEnabled: true, }, |