Skip to content

Commit 22084e8

Browse files
test: add regression tests for init.ts Files integration
Covers two bugs fixed in the previous commit: 1. PROPFIND 404: asserts that client.stat() is called with the getRootPath() prefix (/files/{uid}/...) and never with a bare path. 2. Missing context-menu actions: asserts that both openInLibreSignAction and showStatusInlineAction side-effect modules are imported so registerFileAction() runs for each of them. Also covers: DAV property registration, new-menu entry presence, upload-before-post ordering, OCS endpoint, and sidebar opening. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 578df9a commit 22084e8

1 file changed

Lines changed: 243 additions & 0 deletions

File tree

src/tests/init.spec.ts

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 LibreSign contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { describe, expect, it, beforeEach, vi, afterEach } from 'vitest'
7+
8+
/**
9+
* Regression tests for src/init.ts
10+
*
11+
* Bug 1: PROPFIND 404 on file upload via "New signature request" menu.
12+
* client.stat() was called with only the bare file path (e.g. "/folder/test.pdf"),
13+
* which resolves to /remote.php/dav/folder/test.pdf → 404.
14+
* Fix: path must be prefixed with getRootPath() (e.g. "/files/uid/folder/test.pdf").
15+
*
16+
* Bug 2: "Open in LibreSign" action was missing from Files context-menu.
17+
* The action files were never imported by any bundle entry point, so
18+
* registerFileAction() was never called for them.
19+
* Fix: init.ts now imports both action modules as side-effects.
20+
*/
21+
22+
// ─── Mocks ────────────────────────────────────────────────────────────────────
23+
24+
const mockStat = vi.fn()
25+
const mockClient = { stat: mockStat }
26+
const mockGetClient = vi.fn(() => mockClient)
27+
const mockGetRootPath = vi.fn(() => '/files/testuser')
28+
const mockResultToNode = vi.fn((data: unknown) => data)
29+
const mockRegisterDavProperty = vi.fn()
30+
31+
const mockSidebarOpen = vi.fn()
32+
const mockSidebarSetActiveTab = vi.fn()
33+
const mockSidebar = { open: mockSidebarOpen, setActiveTab: mockSidebarSetActiveTab }
34+
const mockGetSidebar = vi.fn(() => mockSidebar)
35+
const mockAddNewFileMenuEntry = vi.fn()
36+
const mockRegisterFileAction = vi.fn()
37+
38+
const mockUpload = vi.fn(() => Promise.resolve())
39+
const mockUploader = { upload: mockUpload }
40+
const mockGetUploader = vi.fn(() => mockUploader)
41+
42+
const mockAxiosPost = vi.fn(() => Promise.resolve({ data: {} }))
43+
44+
// ─── Module-level mocks (hoisted before imports) ─────────────────────────────
45+
46+
vi.mock('@nextcloud/axios', () => ({
47+
default: { post: mockAxiosPost },
48+
}))
49+
50+
vi.mock('@nextcloud/router', () => ({
51+
generateOcsUrl: vi.fn((path: string) => `https://localhost${path}`),
52+
}))
53+
54+
vi.mock('@nextcloud/l10n', () => ({
55+
t: vi.fn((_app: string, text: string) => text),
56+
}))
57+
58+
vi.mock('@nextcloud/upload', () => ({
59+
getUploader: mockGetUploader,
60+
}))
61+
62+
vi.mock('@nextcloud/files', () => ({
63+
addNewFileMenuEntry: mockAddNewFileMenuEntry,
64+
getSidebar: mockGetSidebar,
65+
Permission: { CREATE: 4 },
66+
registerFileAction: mockRegisterFileAction,
67+
}))
68+
69+
vi.mock('@nextcloud/files/dav', () => ({
70+
getClient: mockGetClient,
71+
getRootPath: mockGetRootPath,
72+
resultToNode: mockResultToNode,
73+
registerDavProperty: mockRegisterDavProperty,
74+
}))
75+
76+
// Stub the SVG imports so they don't blow up in the test environment
77+
vi.mock('../../img/app-colored.svg?raw', () => ({ default: '<svg/>' }))
78+
vi.mock('../../img/app-dark.svg?raw', () => ({ default: '<svg/>' }))
79+
80+
vi.mock('../helpers/useIsDarkTheme', () => ({
81+
useIsDarkTheme: vi.fn(() => false),
82+
}))
83+
84+
vi.mock('../logger', () => ({
85+
default: { debug: vi.fn(), error: vi.fn(), warn: vi.fn(), info: vi.fn() },
86+
}))
87+
88+
// Stub the action side-effect modules so they don't pull in unrelated deps,
89+
// but still allow us to assert they were imported (tested separately below).
90+
vi.mock('../actions/openInLibreSignAction.js', () => ({}))
91+
vi.mock('../actions/showStatusInlineAction.js', () => ({}))
92+
93+
// ─── Helpers ──────────────────────────────────────────────────────────────────
94+
95+
/**
96+
* Extract the handler that was registered via addNewFileMenuEntry so tests
97+
* can call it directly without simulating a real click.
98+
*/
99+
function captureNewMenuHandler(): (context: unknown, content: unknown) => Promise<void> {
100+
expect(mockAddNewFileMenuEntry).toHaveBeenCalledOnce()
101+
const [[entry]] = mockAddNewFileMenuEntry.mock.calls as [[{ handler: (context: unknown, content: unknown) => Promise<void>; uploadManager?: { upload: typeof mockUpload } }]]
102+
// Inject the mock uploader so handler can call this.uploadManager.upload()
103+
entry.uploadManager = mockUploader
104+
return entry.handler.bind(entry)
105+
}
106+
107+
/**
108+
* Intercept the <input type="file"> that the handler creates (it is never
109+
* appended to the DOM so document.querySelector cannot find it).
110+
* Returns an async trigger: call it after invoking the handler to simulate
111+
* the user picking a file.
112+
*/
113+
function setupFileInputInterception(fileName: string, mimeType = 'application/pdf') {
114+
let capturedInput: HTMLInputElement | null = null
115+
const originalCreate = document.createElement.bind(document)
116+
117+
vi.spyOn(document, 'createElement').mockImplementation((tag: string, ...args: unknown[]) => {
118+
const el = originalCreate(tag, ...(args as [ElementCreationOptions?]))
119+
if (tag === 'input') {
120+
capturedInput = el as HTMLInputElement
121+
}
122+
return el
123+
})
124+
125+
return async function triggerChange() {
126+
expect(capturedInput, 'handler must have called document.createElement("input")').not.toBeNull()
127+
128+
const file = new File(['%PDF-1.4'], fileName, { type: mimeType })
129+
Object.defineProperty(capturedInput, 'files', { value: [file], configurable: true })
130+
capturedInput!.dispatchEvent(new Event('change'))
131+
132+
await vi.waitFor(() => expect(mockStat).toHaveBeenCalled(), { timeout: 2000 })
133+
134+
vi.restoreAllMocks()
135+
}
136+
}
137+
138+
// ─── Tests ────────────────────────────────────────────────────────────────────
139+
140+
describe('init.ts', () => {
141+
beforeEach(async () => {
142+
vi.clearAllMocks()
143+
144+
// Reset stat mock to return valid data for resultToNode
145+
mockStat.mockResolvedValue({ data: { filename: '/files/testuser/Documents/test.pdf' } })
146+
147+
// Import init.ts – all side-effects run here
148+
await import('../init')
149+
})
150+
151+
afterEach(() => {
152+
vi.resetModules()
153+
})
154+
155+
// ── Side-effect: DAV properties ─────────────────────────────────────────
156+
157+
it('registers the libresign-signature-status DAV property', () => {
158+
expect(mockRegisterDavProperty).toHaveBeenCalledWith(
159+
'nc:libresign-signature-status',
160+
{ nc: 'http://nextcloud.org/ns' },
161+
)
162+
})
163+
164+
it('registers the libresign-signed-node-id DAV property', () => {
165+
expect(mockRegisterDavProperty).toHaveBeenCalledWith(
166+
'nc:libresign-signed-node-id',
167+
{ nc: 'http://nextcloud.org/ns' },
168+
)
169+
})
170+
171+
// ── Side-effect: new-file-menu entry ─────────────────────────────────────
172+
173+
it('adds a "New signature request" entry to the Files new-menu', () => {
174+
expect(mockAddNewFileMenuEntry).toHaveBeenCalledOnce()
175+
const [entry] = mockAddNewFileMenuEntry.mock.calls[0] as [{ id: string }][]
176+
expect(entry.id).toBe('libresign-request')
177+
})
178+
179+
// ── Side-effect: file-action imports ─────────────────────────────────────
180+
181+
/**
182+
* Regression: missing context-menu actions.
183+
* Both action modules must be imported so their registerFileAction() side-effect runs.
184+
*/
185+
it('imports openInLibreSignAction side-effect module', async () => {
186+
const mod = await import('../actions/openInLibreSignAction.js')
187+
expect(mod).toBeDefined()
188+
})
189+
190+
it('imports showStatusInlineAction side-effect module', async () => {
191+
const mod = await import('../actions/showStatusInlineAction.js')
192+
expect(mod).toBeDefined()
193+
})
194+
195+
// ── Upload handler: DAV stat path ─────────────────────────────────────────
196+
197+
describe('new-menu handler', () => {
198+
const folderPath = '/Documents'
199+
const fileName = 'contract.pdf'
200+
const context = { path: folderPath, permissions: 4 /* CREATE */ }
201+
202+
beforeEach(async () => {
203+
const triggerChange = setupFileInputInterception(fileName)
204+
const handler = captureNewMenuHandler()
205+
await handler(context, [])
206+
await triggerChange()
207+
})
208+
209+
/**
210+
* Regression: PROPFIND 404.
211+
* client.stat() must include the WebDAV root path prefix (/files/{uid}),
212+
* otherwise the request resolves to /remote.php/dav/<filename> → 404.
213+
*/
214+
it('calls client.stat with getRootPath() prefix to avoid PROPFIND 404', () => {
215+
const expectedPath = `${mockGetRootPath()}${folderPath}/${fileName}`
216+
expect(mockStat).toHaveBeenCalledWith(expectedPath, { details: true })
217+
})
218+
219+
it('does NOT call client.stat with a bare path missing the root prefix', () => {
220+
const barePath = `${folderPath}/${fileName}`
221+
// Ensure the old (broken) path was never used
222+
expect(mockStat).not.toHaveBeenCalledWith(barePath, { details: true })
223+
})
224+
225+
it('uploads the file before posting the OCS request', () => {
226+
const uploadCallOrder = mockUpload.mock.invocationCallOrder[0]
227+
const postCallOrder = mockAxiosPost.mock.invocationCallOrder[0]
228+
expect(uploadCallOrder).toBeLessThan(postCallOrder)
229+
})
230+
231+
it('posts to the LibreSign OCS file endpoint', () => {
232+
expect(mockAxiosPost).toHaveBeenCalledWith(
233+
expect.stringContaining('/apps/libresign/api/v1/file'),
234+
expect.objectContaining({ name: fileName }),
235+
)
236+
})
237+
238+
it('opens the sidebar to the LibreSign tab after upload', () => {
239+
expect(mockSidebarOpen).toHaveBeenCalledOnce()
240+
expect(mockSidebarSetActiveTab).toHaveBeenCalledWith('libresign')
241+
})
242+
})
243+
})

0 commit comments

Comments
 (0)