1+ /*
2+ * SPDX-FileCopyrightText: 2026 LibreSign contributors
3+ * SPDX-License-Identifier: AGPL-3.0-or-later
4+ */
5+
6+ import { beforeEach , describe , expect , it , vi } from 'vitest'
7+ import { flushPromises , mount } from '@vue/test-utils'
8+
9+ import IdDocsValidation from '../../../views/Documents/IdDocsValidation.vue'
10+ import { FILE_STATUS } from '../../../constants.js'
11+
12+ const axiosGetMock = vi . fn ( )
13+ const axiosDeleteMock = vi . fn ( )
14+ const showErrorMock = vi . fn ( )
15+ const openDocumentMock = vi . fn ( )
16+ const routerPushMock = vi . fn ( )
17+ const userConfigUpdateMock = vi . fn ( )
18+
19+ const userConfigStore = {
20+ id_docs_filters : {
21+ owner : '' ,
22+ status : null ,
23+ } ,
24+ id_docs_sort : {
25+ sortBy : 'owner' ,
26+ sortOrder : 'DESC' ,
27+ } ,
28+ update : vi . fn ( ( ...args : unknown [ ] ) => userConfigUpdateMock ( ...args ) ) ,
29+ }
30+
31+ vi . mock ( '@nextcloud/l10n' , ( ) => ( {
32+ t : vi . fn ( ( _app : string , text : string ) => text ) ,
33+ } ) )
34+
35+ vi . mock ( '@nextcloud/axios' , ( ) => ( {
36+ default : {
37+ get : vi . fn ( ( ...args : unknown [ ] ) => axiosGetMock ( ...args ) ) ,
38+ delete : vi . fn ( ( ...args : unknown [ ] ) => axiosDeleteMock ( ...args ) ) ,
39+ } ,
40+ } ) )
41+
42+ vi . mock ( '@nextcloud/dialogs' , ( ) => ( {
43+ showError : vi . fn ( ( ...args : unknown [ ] ) => showErrorMock ( ...args ) ) ,
44+ } ) )
45+
46+ vi . mock ( '@nextcloud/router' , ( ) => ( {
47+ generateOcsUrl : vi . fn ( ( path : string , params ?: Record < string , string | number > ) => {
48+ let resolvedPath = path
49+ for ( const [ key , value ] of Object . entries ( params || { } ) ) {
50+ resolvedPath = resolvedPath . replace ( `{${ key } }` , String ( value ) )
51+ }
52+ return `/ocs/v2.php${ resolvedPath } `
53+ } ) ,
54+ } ) )
55+
56+ vi . mock ( 'vue-router' , ( ) => ( {
57+ useRouter : vi . fn ( ( ) => ( {
58+ push : routerPushMock ,
59+ } ) ) ,
60+ } ) )
61+
62+ vi . mock ( '../../../store/userconfig.js' , ( ) => ( {
63+ useUserConfigStore : vi . fn ( ( ) => userConfigStore ) ,
64+ } ) )
65+
66+ vi . mock ( '../../../utils/viewer.js' , ( ) => ( {
67+ openDocument : vi . fn ( ( ...args : unknown [ ] ) => openDocumentMock ( ...args ) ) ,
68+ } ) )
69+
70+ vi . mock ( '@nextcloud/vue/components/NcActions' , ( ) => ( {
71+ default : { name : 'NcActions' , template : '<div class="nc-actions-stub"><slot /><slot name="icon" /></div>' } ,
72+ } ) )
73+
74+ vi . mock ( '@nextcloud/vue/components/NcActionButton' , ( ) => ( {
75+ default : {
76+ name : 'NcActionButton' ,
77+ emits : [ 'click' , 'update:modelValue' ] ,
78+ template : '<button class="nc-action-button-stub" @click="$emit(\'click\')"><slot /><slot name="icon" /></button>' ,
79+ } ,
80+ } ) )
81+
82+ vi . mock ( '@nextcloud/vue/components/NcActionInput' , ( ) => ( {
83+ default : {
84+ name : 'NcActionInput' ,
85+ props : [ 'modelValue' , 'label' ] ,
86+ emits : [ 'update:modelValue' ] ,
87+ template : '<input class="nc-action-input-stub" />' ,
88+ } ,
89+ } ) )
90+
91+ vi . mock ( '@nextcloud/vue/components/NcActionSeparator' , ( ) => ( {
92+ default : { name : 'NcActionSeparator' , template : '<hr class="nc-action-separator-stub" />' } ,
93+ } ) )
94+
95+ vi . mock ( '@nextcloud/vue/components/NcAvatar' , ( ) => ( {
96+ default : { name : 'NcAvatar' , template : '<div class="nc-avatar-stub" />' } ,
97+ } ) )
98+
99+ vi . mock ( '@nextcloud/vue/components/NcEmptyContent' , ( ) => ( {
100+ default : { name : 'NcEmptyContent' , template : '<div class="nc-empty-content-stub"><slot /><slot name="icon" /></div>' } ,
101+ } ) )
102+
103+ vi . mock ( '@nextcloud/vue/components/NcLoadingIcon' , ( ) => ( {
104+ default : { name : 'NcLoadingIcon' , template : '<span class="nc-loading-icon-stub" />' } ,
105+ } ) )
106+
107+ vi . mock ( '@nextcloud/vue/components/NcIconSvgWrapper' , ( ) => ( {
108+ default : { name : 'NcIconSvgWrapper' , template : '<i class="nc-icon-stub" />' } ,
109+ } ) )
110+
111+ describe ( 'IdDocsValidation.vue' , ( ) => {
112+ const signedDoc = {
113+ uuid : 'doc-1' ,
114+ account : {
115+ userId : 'alice' ,
116+ displayName : 'Alice' ,
117+ } ,
118+ file_type : {
119+ type : 'passport' ,
120+ name : 'Passport' ,
121+ } ,
122+ file : {
123+ uuid : 'file-1' ,
124+ status : FILE_STATUS . SIGNED ,
125+ statusText : 'Signed' ,
126+ name : 'alice-passport.pdf' ,
127+ file : {
128+ nodeId : 10 ,
129+ url : '/files/alice-passport.pdf' ,
130+ } ,
131+ signers : [ { uid : 'approver' , displayName : 'Approver' , sign_date : '2026-03-06T12:00:00Z' } ] ,
132+ } ,
133+ }
134+
135+ const pendingDoc = {
136+ uuid : 'doc-2' ,
137+ account : {
138+ userId : 'bob' ,
139+ displayName : 'Bob' ,
140+ } ,
141+ file_type : {
142+ type : 'driver-license' ,
143+ name : 'Driver License' ,
144+ } ,
145+ file : {
146+ uuid : 'file-2' ,
147+ status : FILE_STATUS . ABLE_TO_SIGN ,
148+ statusText : 'Pending' ,
149+ name : 'bob-license.pdf' ,
150+ file : {
151+ nodeId : 11 ,
152+ url : '/files/bob-license.pdf' ,
153+ } ,
154+ signers : [ ] ,
155+ } ,
156+ }
157+
158+ const createWrapper = ( ) => mount ( IdDocsValidation )
159+
160+ beforeEach ( ( ) => {
161+ axiosGetMock . mockReset ( )
162+ axiosDeleteMock . mockReset ( )
163+ showErrorMock . mockReset ( )
164+ openDocumentMock . mockReset ( )
165+ routerPushMock . mockReset ( )
166+ userConfigUpdateMock . mockReset ( )
167+ userConfigStore . update . mockClear ( )
168+ userConfigStore . id_docs_filters = { owner : '' , status : null }
169+ userConfigStore . id_docs_sort = { sortBy : 'owner' , sortOrder : 'DESC' }
170+
171+ axiosGetMock . mockResolvedValue ( {
172+ data : {
173+ ocs : {
174+ data : {
175+ data : [ signedDoc , pendingDoc ] ,
176+ total : 2 ,
177+ } ,
178+ } ,
179+ } ,
180+ } )
181+
182+ axiosDeleteMock . mockResolvedValue ( {
183+ data : {
184+ ocs : {
185+ data : {
186+ success : true ,
187+ } ,
188+ } ,
189+ } ,
190+ } )
191+ } )
192+
193+ it ( 'loads documents on mount using saved sort' , async ( ) => {
194+ const wrapper = createWrapper ( )
195+ await flushPromises ( )
196+
197+ expect ( axiosGetMock ) . toHaveBeenCalledWith ( '/ocs/v2.php/apps/libresign/api/v1/id-docs/approval/list' , {
198+ params : {
199+ page : 1 ,
200+ length : 50 ,
201+ sortBy : 'owner' ,
202+ sortOrder : 'DESC' ,
203+ } ,
204+ } )
205+ expect ( wrapper . vm . documentList ) . toHaveLength ( 2 )
206+ expect ( wrapper . vm . hasMore ) . toBe ( false )
207+ } )
208+
209+ it ( 'filters by owner and status and persists filter changes' , async ( ) => {
210+ vi . useFakeTimers ( )
211+ const wrapper = createWrapper ( )
212+ await flushPromises ( )
213+
214+ wrapper . vm . filters . owner = 'bob'
215+ wrapper . vm . setStatusFilter ( 'pending' , true )
216+ vi . runAllTimers ( )
217+ await flushPromises ( )
218+
219+ expect ( wrapper . vm . hasActiveFilters ) . toBe ( true )
220+ expect ( wrapper . vm . activeFilterCount ) . toBe ( 2 )
221+ expect ( wrapper . vm . filteredDocuments ) . toEqual ( [ pendingDoc ] )
222+ expect ( userConfigUpdateMock ) . toHaveBeenCalledWith ( 'id_docs_filters' , {
223+ owner : 'bob' ,
224+ status : {
225+ value : 'pending' ,
226+ label : 'Pending' ,
227+ } ,
228+ } )
229+
230+ vi . useRealTimers ( )
231+ } )
232+
233+ it ( 'toggles sort direction and then clears the sort for the same column' , async ( ) => {
234+ const wrapper = createWrapper ( )
235+ await flushPromises ( )
236+
237+ await wrapper . vm . sortColumn ( 'owner' )
238+ expect ( wrapper . vm . sortOrder ) . toBe ( 'ASC' )
239+
240+ await wrapper . vm . sortColumn ( 'owner' )
241+ expect ( wrapper . vm . sortBy ) . toBeNull ( )
242+ expect ( wrapper . vm . sortOrder ) . toBeNull ( )
243+ expect ( userConfigUpdateMock ) . toHaveBeenCalledWith ( 'id_docs_sort' , {
244+ sortBy : null ,
245+ sortOrder : null ,
246+ } )
247+ } )
248+
249+ it ( 'routes to approve and validation pages using document uuid' , async ( ) => {
250+ const wrapper = createWrapper ( )
251+ await flushPromises ( )
252+
253+ wrapper . vm . openApprove ( pendingDoc )
254+ wrapper . vm . openValidationURL ( signedDoc )
255+
256+ expect ( routerPushMock ) . toHaveBeenNthCalledWith ( 1 , {
257+ name : 'IdDocsApprove' ,
258+ params : { uuid : 'file-2' } ,
259+ query : { idDocApproval : 'true' } ,
260+ } )
261+ expect ( routerPushMock ) . toHaveBeenNthCalledWith ( 2 , {
262+ name : 'ValidationFile' ,
263+ params : { uuid : 'file-1' } ,
264+ } )
265+ } )
266+
267+ it ( 'opens the file in the viewer and reports missing urls' , async ( ) => {
268+ const wrapper = createWrapper ( )
269+ await flushPromises ( )
270+
271+ wrapper . vm . openFile ( signedDoc )
272+ wrapper . vm . openFile ( { file : { file : { nodeId : 12 } , name : 'missing.pdf' } } )
273+
274+ expect ( openDocumentMock ) . toHaveBeenCalledWith ( {
275+ fileUrl : '/files/alice-passport.pdf' ,
276+ filename : 'alice-passport.pdf' ,
277+ nodeId : 10 ,
278+ } )
279+ expect ( showErrorMock ) . toHaveBeenCalledWith ( 'File not found' )
280+ } )
281+
282+ it ( 'deletes a document and reloads the list' , async ( ) => {
283+ const wrapper = createWrapper ( )
284+ await flushPromises ( )
285+
286+ await wrapper . vm . deleteDocument ( signedDoc )
287+ await flushPromises ( )
288+
289+ expect ( axiosDeleteMock ) . toHaveBeenCalledWith ( '/ocs/v2.php/apps/libresign/api/v1/id-docs/10' )
290+ expect ( axiosGetMock ) . toHaveBeenCalledTimes ( 2 )
291+ } )
292+ } )
0 commit comments