|
| 1 | +/** |
| 2 | + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors |
| 3 | + * SPDX-License-Identifier: GPL-3.0-or-later |
| 4 | + */ |
| 5 | +import type { INode } from '@nextcloud/files' |
| 6 | +import type { SharingShare } from '../types/api.ts' |
| 7 | +import type { Share } from './share.ts' |
| 8 | + |
| 9 | +import { beforeEach, describe, expect, it, vi } from 'vitest' |
| 10 | +import { SOURCE_TYPE_NODE } from '../constants.ts' |
| 11 | +import { createShare, getShare, searchRecipients } from './share.ts' |
| 12 | +import * as client from './sharing.ts' |
| 13 | + |
| 14 | +vi.mock('./sharing.ts', () => ({ |
| 15 | + createShare: vi.fn(), |
| 16 | + getShare: vi.fn(), |
| 17 | + addShareSource: vi.fn(), |
| 18 | + removeShareSource: vi.fn(), |
| 19 | + addShareRecipient: vi.fn(), |
| 20 | + removeShareRecipient: vi.fn(), |
| 21 | + updateShareRecipientSecret: vi.fn(), |
| 22 | + updateShareProperty: vi.fn(), |
| 23 | + updateSharePermission: vi.fn(), |
| 24 | + selectSharePermissionPreset: vi.fn(), |
| 25 | + updateShareState: vi.fn(), |
| 26 | + searchRecipients: vi.fn(), |
| 27 | + deleteShare: vi.fn(), |
| 28 | +})) |
| 29 | + |
| 30 | +const mocked = vi.mocked(client) |
| 31 | + |
| 32 | +/** |
| 33 | + * Build a minimal share schema for testing. |
| 34 | + * |
| 35 | + * @param overrides Fields to override on the default schema |
| 36 | + */ |
| 37 | +function share(overrides: Partial<SharingShare> = {}): SharingShare { |
| 38 | + return { |
| 39 | + id: 'abc', |
| 40 | + owner: { user_id: 'alice', instance: null, display_name: 'Alice', icon: { svg: '' } }, |
| 41 | + last_updated: 0, |
| 42 | + state: 'draft', |
| 43 | + sources: [], |
| 44 | + recipients: [], |
| 45 | + properties: [], |
| 46 | + permissions: [], |
| 47 | + permission_preset: null, |
| 48 | + ...overrides, |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +/** |
| 53 | + * Obtain a Share instance wrapping the given schema, via the public factory. |
| 54 | + * |
| 55 | + * @param data The schema the instance should wrap |
| 56 | + */ |
| 57 | +async function makeShare(data: SharingShare = share()): Promise<Share> { |
| 58 | + mocked.getShare.mockResolvedValueOnce(data) |
| 59 | + return getShare(data.id) |
| 60 | +} |
| 61 | + |
| 62 | +beforeEach(() => { |
| 63 | + vi.clearAllMocks() |
| 64 | +}) |
| 65 | + |
| 66 | +describe('Share', () => { |
| 67 | + it('createShare() wraps the created draft', async () => { |
| 68 | + mocked.createShare.mockResolvedValue(share({ id: 'new' })) |
| 69 | + const instance = await createShare() |
| 70 | + expect(mocked.createShare).toHaveBeenCalledOnce() |
| 71 | + expect(instance.id).toBe('new') |
| 72 | + expect(instance.state).toBe('draft') |
| 73 | + }) |
| 74 | + |
| 75 | + it('getShare() fetches an existing share', async () => { |
| 76 | + mocked.getShare.mockResolvedValue(share({ id: 'x' })) |
| 77 | + const instance = await getShare('x', 'secret', { password: 'p' }) |
| 78 | + expect(mocked.getShare).toHaveBeenCalledWith('x', 'secret', { password: 'p' }) |
| 79 | + expect(instance.id).toBe('x') |
| 80 | + }) |
| 81 | + |
| 82 | + it('exposes the schema through getters', async () => { |
| 83 | + const data = share({ |
| 84 | + state: 'active', |
| 85 | + permission_preset: 'preset-a', |
| 86 | + sources: [{ class: 'S', value: '1', display_name: 'file', icon: null }], |
| 87 | + }) |
| 88 | + const instance = await makeShare(data) |
| 89 | + expect(instance.data).toBe(data) |
| 90 | + expect(instance.state).toBe('active') |
| 91 | + expect(instance.permissionPreset).toBe('preset-a') |
| 92 | + expect(instance.sources).toHaveLength(1) |
| 93 | + }) |
| 94 | + |
| 95 | + it('mutations pass the id to the client and re-sync the data', async () => { |
| 96 | + const instance = await makeShare(share({ id: 'abc' })) |
| 97 | + const updated = share({ id: 'abc', state: 'active' }) |
| 98 | + mocked.updateShareState.mockResolvedValue(updated) |
| 99 | + |
| 100 | + const result = await instance.setState('active') |
| 101 | + |
| 102 | + expect(mocked.updateShareState).toHaveBeenCalledWith('abc', 'active') |
| 103 | + expect(result).toBe(instance) // returns this for chaining |
| 104 | + expect(instance.data).toBe(updated) // internal data replaced |
| 105 | + expect(instance.state).toBe('active') |
| 106 | + }) |
| 107 | + |
| 108 | + it('setProperty forwards the class and value', async () => { |
| 109 | + const instance = await makeShare() |
| 110 | + mocked.updateShareProperty.mockResolvedValue(share()) |
| 111 | + await instance.setProperty('P', 'v') |
| 112 | + expect(mocked.updateShareProperty).toHaveBeenCalledWith('abc', 'P', 'v') |
| 113 | + }) |
| 114 | + |
| 115 | + it('setPermission forwards the class and enabled flag', async () => { |
| 116 | + const instance = await makeShare() |
| 117 | + mocked.updateSharePermission.mockResolvedValue(share()) |
| 118 | + await instance.setPermission('C', false) |
| 119 | + expect(mocked.updateSharePermission).toHaveBeenCalledWith('abc', 'C', false) |
| 120 | + }) |
| 121 | + |
| 122 | + it('addSource forwards the class and value', async () => { |
| 123 | + const instance = await makeShare() |
| 124 | + mocked.addShareSource.mockResolvedValue(share()) |
| 125 | + await instance.addSource('S', '1') |
| 126 | + expect(mocked.addShareSource).toHaveBeenCalledWith('abc', 'S', '1') |
| 127 | + }) |
| 128 | + |
| 129 | + it('addNode maps the node to the node source type', async () => { |
| 130 | + const instance = await makeShare() |
| 131 | + mocked.addShareSource.mockResolvedValue(share()) |
| 132 | + await instance.addNode({ fileid: 42 } as unknown as INode) |
| 133 | + expect(mocked.addShareSource).toHaveBeenCalledWith('abc', SOURCE_TYPE_NODE, '42') |
| 134 | + }) |
| 135 | + |
| 136 | + it('removeSource forwards the class and value', async () => { |
| 137 | + const instance = await makeShare() |
| 138 | + mocked.removeShareSource.mockResolvedValue(share()) |
| 139 | + await instance.removeSource('S', '1') |
| 140 | + expect(mocked.removeShareSource).toHaveBeenCalledWith('abc', 'S', '1') |
| 141 | + }) |
| 142 | + |
| 143 | + it('addRecipient forwards class, value and instance', async () => { |
| 144 | + const instance = await makeShare() |
| 145 | + mocked.addShareRecipient.mockResolvedValue(share()) |
| 146 | + await instance.addRecipient('R', 'bob', 'https://remote.example') |
| 147 | + expect(mocked.addShareRecipient).toHaveBeenCalledWith('abc', 'R', 'bob', 'https://remote.example') |
| 148 | + }) |
| 149 | + |
| 150 | + it('removeRecipient forwards class, value and instance', async () => { |
| 151 | + const instance = await makeShare() |
| 152 | + mocked.removeShareRecipient.mockResolvedValue(share()) |
| 153 | + await instance.removeRecipient('R', 'bob') |
| 154 | + expect(mocked.removeShareRecipient).toHaveBeenCalledWith('abc', 'R', 'bob', undefined) |
| 155 | + }) |
| 156 | + |
| 157 | + it('setRecipientSecret forwards the secret', async () => { |
| 158 | + const instance = await makeShare() |
| 159 | + mocked.updateShareRecipientSecret.mockResolvedValue(share()) |
| 160 | + await instance.setRecipientSecret('R', 'bob', 'sEcret') |
| 161 | + expect(mocked.updateShareRecipientSecret).toHaveBeenCalledWith('abc', 'R', 'bob', 'sEcret', undefined) |
| 162 | + }) |
| 163 | + |
| 164 | + it('selectPreset forwards the preset class', async () => { |
| 165 | + const instance = await makeShare() |
| 166 | + mocked.selectSharePermissionPreset.mockResolvedValue(share()) |
| 167 | + await instance.selectPreset('preset-a') |
| 168 | + expect(mocked.selectSharePermissionPreset).toHaveBeenCalledWith('abc', 'preset-a') |
| 169 | + }) |
| 170 | + |
| 171 | + it('refresh re-fetches the share by id', async () => { |
| 172 | + const instance = await makeShare(share({ id: 'abc' })) |
| 173 | + const fresh = share({ id: 'abc', state: 'active' }) |
| 174 | + mocked.getShare.mockResolvedValueOnce(fresh) |
| 175 | + await instance.refresh() |
| 176 | + expect(mocked.getShare).toHaveBeenLastCalledWith('abc') |
| 177 | + expect(instance.data).toBe(fresh) |
| 178 | + }) |
| 179 | + |
| 180 | + it('activate() sets the state to active', async () => { |
| 181 | + const instance = await makeShare() |
| 182 | + mocked.updateShareState.mockResolvedValue(share({ state: 'active' })) |
| 183 | + await instance.activate() |
| 184 | + expect(mocked.updateShareState).toHaveBeenCalledWith('abc', 'active') |
| 185 | + }) |
| 186 | + |
| 187 | + it('delete() removes the share and returns nothing', async () => { |
| 188 | + const instance = await makeShare() |
| 189 | + mocked.deleteShare.mockResolvedValue() |
| 190 | + await expect(instance.delete()).resolves.toBeUndefined() |
| 191 | + expect(mocked.deleteShare).toHaveBeenCalledWith('abc') |
| 192 | + }) |
| 193 | + |
| 194 | + it('searchRecipients delegates to the client', async () => { |
| 195 | + mocked.searchRecipients.mockResolvedValue([]) |
| 196 | + await searchRecipients('bob', 'UserType', 5, 2) |
| 197 | + expect(mocked.searchRecipients).toHaveBeenCalledWith('bob', 'UserType', 5, 2) |
| 198 | + }) |
| 199 | +}) |
0 commit comments