@@ -17,10 +17,22 @@ import { describe, expect, it, beforeEach, vi, afterEach } from 'vitest'
1717 * The action files were never imported by any bundle entry point, so
1818 * registerFileAction() was never called for them.
1919 * Fix: init.ts now imports both action modules as side-effects.
20+ *
21+ * Bug 3: POST /request-signature missing "file" parameter after upload.
22+ * client.stat() was called WITHOUT getDefaultPropfind() data, so the WebDAV
23+ * PROPFIND response omitted the Nextcloud-specific {owncloud}fileid property.
24+ * resultToNode() produced a Node with fileid = undefined, mapNodeToFileInfo
25+ * returned id = '', addFile() silently rejected the temp record, selectedFileId
26+ * stayed 0, and saveOrUpdateSignatureRequest sent POST without any file reference.
27+ * Fix: pass data: getDefaultPropfind() to client.stat() so fileid is always
28+ * included and the sidebar can correctly identify the uploaded file.
2029 */
2130
2231// ─── Mocks ────────────────────────────────────────────────────────────────────
2332
33+ const mockDefaultPropfind = '<propfind xmlns="DAV:"><prop><fileid/></prop></propfind>'
34+ const mockGetDefaultPropfind = vi . fn ( ( ) => mockDefaultPropfind )
35+
2436const mockStat = vi . fn ( )
2537const mockClient = { stat : mockStat }
2638const mockGetClient = vi . fn ( ( ) => mockClient )
@@ -41,6 +53,11 @@ const mockGetUploader = vi.fn(() => mockUploader)
4153
4254const mockAxiosPost = vi . fn ( ( ) => Promise . resolve ( { data : { } } ) )
4355
56+ const mockLoadState = vi . fn ( ( app : string , key : string , defaultValue ?: unknown ) => {
57+ if ( app === 'libresign' && key === 'certificate_ok' ) return true
58+ return defaultValue
59+ } )
60+
4461// ─── Module-level mocks (hoisted before imports) ─────────────────────────────
4562
4663vi . mock ( '@nextcloud/axios' , ( ) => ( {
@@ -66,8 +83,13 @@ vi.mock('@nextcloud/files', () => ({
6683 registerFileAction : mockRegisterFileAction ,
6784} ) )
6885
86+ vi . mock ( '@nextcloud/initial-state' , ( ) => ( {
87+ loadState : mockLoadState ,
88+ } ) )
89+
6990vi . mock ( '@nextcloud/files/dav' , ( ) => ( {
7091 getClient : mockGetClient ,
92+ getDefaultPropfind : mockGetDefaultPropfind ,
7193 getRootPath : mockGetRootPath ,
7294 resultToNode : mockResultToNode ,
7395 registerDavProperty : mockRegisterDavProperty ,
@@ -98,7 +120,8 @@ vi.mock('../actions/showStatusInlineAction.js', () => ({}))
98120 */
99121function captureNewMenuHandler ( ) : ( context : unknown , content : unknown ) => Promise < void > {
100122 expect ( mockAddNewFileMenuEntry ) . toHaveBeenCalledOnce ( )
101- const [ [ entry ] ] = mockAddNewFileMenuEntry . mock . calls as [ [ { handler : ( context : unknown , content : unknown ) => Promise < void > ; uploadManager ?: { upload : typeof mockUpload } } ] ]
123+ type MenuEntry = { handler : ( context : unknown , content : unknown ) => Promise < void > ; uploadManager ?: { upload : typeof mockUpload } }
124+ const entry = mockAddNewFileMenuEntry . mock . calls [ 0 ] [ 0 ] as MenuEntry
102125 // Inject the mock uploader so handler can call this.uploadManager.upload()
103126 entry . uploadManager = mockUploader
104127 return entry . handler . bind ( entry )
@@ -172,10 +195,41 @@ describe('init.ts', () => {
172195
173196 it ( 'adds a "New signature request" entry to the Files new-menu' , ( ) => {
174197 expect ( mockAddNewFileMenuEntry ) . toHaveBeenCalledOnce ( )
175- const [ entry ] = mockAddNewFileMenuEntry . mock . calls [ 0 ] as [ { id : string } ] [ ]
198+ const entry = mockAddNewFileMenuEntry . mock . calls [ 0 ] [ 0 ] as { id : string }
176199 expect ( entry . id ) . toBe ( 'libresign-request' )
177200 } )
178201
202+ /**
203+ * Regression: sidebar did not open on LibreSign tab.
204+ * The menu entry must be hidden when LibreSign's certificate is not
205+ * configured (certificate_ok = false), because isEnabled() in tab.ts also
206+ * checks certificate_ok and rejects unconfigured instances — causing the
207+ * sidebar to fall back to the default (Details) tab.
208+ */
209+ describe ( 'menu entry enabled() guard' , ( ) => {
210+ type MenuEntry = {
211+ enabled : ( context : { permissions : number } ) => boolean
212+ }
213+
214+ it ( 'is enabled when certificate_ok is true and folder has CREATE permission' , ( ) => {
215+ mockLoadState . mockReturnValue ( true )
216+ const entry = mockAddNewFileMenuEntry . mock . calls [ 0 ] [ 0 ] as MenuEntry
217+ expect ( entry . enabled ( { permissions : 4 /* CREATE */ } ) ) . toBe ( true )
218+ } )
219+
220+ it ( 'is disabled when certificate_ok is false (LibreSign not configured)' , ( ) => {
221+ mockLoadState . mockReturnValue ( false )
222+ const entry = mockAddNewFileMenuEntry . mock . calls [ 0 ] [ 0 ] as MenuEntry
223+ expect ( entry . enabled ( { permissions : 4 /* CREATE */ } ) ) . toBe ( false )
224+ } )
225+
226+ it ( 'is disabled when folder lacks CREATE permission even with certificate_ok' , ( ) => {
227+ mockLoadState . mockReturnValue ( true )
228+ const entry = mockAddNewFileMenuEntry . mock . calls [ 0 ] [ 0 ] as MenuEntry
229+ expect ( entry . enabled ( { permissions : 0 } ) ) . toBe ( false )
230+ } )
231+ } )
232+
179233 // ── Side-effect: file-action imports ─────────────────────────────────────
180234
181235 /**
@@ -213,13 +267,28 @@ describe('init.ts', () => {
213267 */
214268 it ( 'calls client.stat with getRootPath() prefix to avoid PROPFIND 404' , ( ) => {
215269 const expectedPath = `${ mockGetRootPath ( ) } ${ folderPath } /${ fileName } `
216- expect ( mockStat ) . toHaveBeenCalledWith ( expectedPath , { details : true } )
270+ expect ( mockStat ) . toHaveBeenCalledWith ( expectedPath , expect . objectContaining ( { details : true } ) )
217271 } )
218272
219273 it ( 'does NOT call client.stat with a bare path missing the root prefix' , ( ) => {
220274 const barePath = `${ folderPath } /${ fileName } `
221275 // Ensure the old (broken) path was never used
222- expect ( mockStat ) . not . toHaveBeenCalledWith ( barePath , { details : true } )
276+ expect ( mockStat ) . not . toHaveBeenCalledWith ( barePath , expect . anything ( ) )
277+ } )
278+
279+ /**
280+ * Regression: POST /request-signature missing "file" parameter.
281+ * Without getDefaultPropfind(), the WebDAV PROPFIND response omits the
282+ * Nextcloud-specific fileid property. resultToNode() then returns a Node
283+ * with fileid = undefined, which propagates as an empty fileInfo.id through
284+ * the sidebar → addFile silently rejects the record → selectedFileId = 0 →
285+ * saveOrUpdateSignatureRequest sends POST with no file reference → 422.
286+ */
287+ it ( 'calls client.stat with getDefaultPropfind() data so fileid is returned' , ( ) => {
288+ expect ( mockStat ) . toHaveBeenCalledWith (
289+ expect . any ( String ) ,
290+ expect . objectContaining ( { data : mockDefaultPropfind } ) ,
291+ )
223292 } )
224293
225294 it ( 'uploads the file before posting the OCS request' , ( ) => {
0 commit comments