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 CrlManagement from '../../../views/CrlManagement/CrlManagement.vue'
10+
11+ const axiosGetMock = vi . fn ( )
12+ const axiosPostMock = vi . fn ( )
13+ const showErrorMock = vi . fn ( )
14+ const showSuccessMock = vi . fn ( )
15+ const userConfigUpdateMock = vi . fn ( )
16+
17+ const userConfigStore = {
18+ crl_filters : {
19+ serialNumber : '' ,
20+ status : null ,
21+ owner : '' ,
22+ } ,
23+ crl_sort : {
24+ sortBy : 'revoked_at' ,
25+ sortOrder : 'DESC' ,
26+ } ,
27+ update : vi . fn ( ( ...args : unknown [ ] ) => userConfigUpdateMock ( ...args ) ) ,
28+ }
29+
30+ vi . mock ( '@nextcloud/l10n' , ( ) => ( {
31+ t : vi . fn ( ( _app : string , text : string ) => text ) ,
32+ } ) )
33+
34+ vi . mock ( '@nextcloud/axios' , ( ) => ( {
35+ default : {
36+ get : vi . fn ( ( ...args : unknown [ ] ) => axiosGetMock ( ...args ) ) ,
37+ post : vi . fn ( ( ...args : unknown [ ] ) => axiosPostMock ( ...args ) ) ,
38+ } ,
39+ } ) )
40+
41+ vi . mock ( '@nextcloud/router' , ( ) => ( {
42+ generateOcsUrl : vi . fn ( ( path : string , params ?: Record < string , string > ) => {
43+ if ( ! params ) {
44+ return `/ocs/v2.php${ path } `
45+ }
46+ return `/ocs/v2.php${ path . replace ( '{apiVersion}' , params . apiVersion ) } `
47+ } ) ,
48+ } ) )
49+
50+ vi . mock ( '@nextcloud/dialogs' , ( ) => ( {
51+ showError : vi . fn ( ( ...args : unknown [ ] ) => showErrorMock ( ...args ) ) ,
52+ showSuccess : vi . fn ( ( ...args : unknown [ ] ) => showSuccessMock ( ...args ) ) ,
53+ } ) )
54+
55+ vi . mock ( '../../../store/userconfig.js' , ( ) => ( {
56+ useUserConfigStore : vi . fn ( ( ) => userConfigStore ) ,
57+ } ) )
58+
59+ vi . mock ( '@nextcloud/vue/components/NcActions' , ( ) => ( {
60+ default : { name : 'NcActions' , template : '<div class="nc-actions-stub"><slot /><slot name="icon" /></div>' } ,
61+ } ) )
62+
63+ vi . mock ( '@nextcloud/vue/components/NcIconSvgWrapper' , ( ) => ( {
64+ default : { name : 'NcIconSvgWrapper' , template : '<i class="nc-icon-stub" />' } ,
65+ } ) )
66+
67+ vi . mock ( '@nextcloud/vue/components/NcActionButton' , ( ) => ( {
68+ default : {
69+ name : 'NcActionButton' ,
70+ emits : [ 'click' , 'update:modelValue' ] ,
71+ template : '<button class="nc-action-button-stub" @click="$emit(\'click\')"><slot /><slot name="icon" /></button>' ,
72+ } ,
73+ } ) )
74+
75+ vi . mock ( '@nextcloud/vue/components/NcActionInput' , ( ) => ( {
76+ default : {
77+ name : 'NcActionInput' ,
78+ props : [ 'modelValue' , 'label' ] ,
79+ emits : [ 'update:modelValue' ] ,
80+ template : '<input class="nc-action-input-stub" />' ,
81+ } ,
82+ } ) )
83+
84+ vi . mock ( '@nextcloud/vue/components/NcActionSeparator' , ( ) => ( {
85+ default : { name : 'NcActionSeparator' , template : '<hr class="nc-action-separator-stub" />' } ,
86+ } ) )
87+
88+ vi . mock ( '@nextcloud/vue/components/NcAppContent' , ( ) => ( {
89+ default : { name : 'NcAppContent' , template : '<div class="nc-app-content-stub"><slot /></div>' } ,
90+ } ) )
91+
92+ vi . mock ( '@nextcloud/vue/components/NcAvatar' , ( ) => ( {
93+ default : { name : 'NcAvatar' , template : '<div class="nc-avatar-stub" />' } ,
94+ } ) )
95+
96+ vi . mock ( '@nextcloud/vue/components/NcButton' , ( ) => ( {
97+ default : {
98+ name : 'NcButton' ,
99+ emits : [ 'click' ] ,
100+ template : '<button class="nc-button-stub" @click="$emit(\'click\')"><slot /><slot name="icon" /></button>' ,
101+ } ,
102+ } ) )
103+
104+ vi . mock ( '@nextcloud/vue/components/NcDialog' , ( ) => ( {
105+ default : {
106+ name : 'NcDialog' ,
107+ emits : [ 'update:open' ] ,
108+ template : '<div class="nc-dialog-stub"><slot /></div>' ,
109+ } ,
110+ } ) )
111+
112+ vi . mock ( '@nextcloud/vue/components/NcEmptyContent' , ( ) => ( {
113+ default : { name : 'NcEmptyContent' , template : '<div class="nc-empty-content-stub"><slot /><slot name="icon" /></div>' } ,
114+ } ) )
115+
116+ vi . mock ( '@nextcloud/vue/components/NcLoadingIcon' , ( ) => ( {
117+ default : { name : 'NcLoadingIcon' , template : '<span class="nc-loading-icon-stub" />' } ,
118+ } ) )
119+
120+ vi . mock ( '@nextcloud/vue/components/NcNoteCard' , ( ) => ( {
121+ default : { name : 'NcNoteCard' , template : '<div class="nc-note-card-stub"><slot /></div>' } ,
122+ } ) )
123+
124+ vi . mock ( '@nextcloud/vue/components/NcSelect' , ( ) => ( {
125+ default : {
126+ name : 'NcSelect' ,
127+ props : [ 'modelValue' , 'options' ] ,
128+ emits : [ 'update:modelValue' ] ,
129+ template : '<div class="nc-select-stub" />' ,
130+ } ,
131+ } ) )
132+
133+ vi . mock ( '@nextcloud/vue/components/NcTextArea' , ( ) => ( {
134+ default : {
135+ name : 'NcTextArea' ,
136+ props : [ 'modelValue' ] ,
137+ emits : [ 'update:modelValue' ] ,
138+ template : '<textarea class="nc-text-area-stub" />' ,
139+ } ,
140+ } ) )
141+
142+ vi . mock ( '@nextcloud/vue/components/NcTextField' , ( ) => ( {
143+ default : {
144+ name : 'NcTextField' ,
145+ props : [ 'modelValue' ] ,
146+ emits : [ 'update:modelValue' ] ,
147+ template : '<input class="nc-text-field-stub" />' ,
148+ } ,
149+ } ) )
150+
151+ describe ( 'CrlManagement.vue' , ( ) => {
152+ const sampleEntry = {
153+ serial_number : 'ABC123' ,
154+ owner : 'Alice' ,
155+ status : 'issued' ,
156+ engine : 'openssl' ,
157+ certificate_type : 'leaf' ,
158+ issued_at : '2026-03-01T10:00:00Z' ,
159+ valid_to : '2026-12-31T10:00:00Z' ,
160+ revoked_at : null ,
161+ reason_code : 0 ,
162+ comment : '' ,
163+ }
164+
165+ const createWrapper = ( ) => mount ( CrlManagement )
166+
167+ beforeEach ( ( ) => {
168+ axiosGetMock . mockReset ( )
169+ axiosPostMock . mockReset ( )
170+ showErrorMock . mockReset ( )
171+ showSuccessMock . mockReset ( )
172+ userConfigUpdateMock . mockReset ( )
173+ userConfigStore . update . mockClear ( )
174+ userConfigStore . crl_filters = { serialNumber : '' , status : null , owner : '' }
175+ userConfigStore . crl_sort = { sortBy : 'revoked_at' , sortOrder : 'DESC' }
176+
177+ axiosGetMock . mockResolvedValue ( {
178+ data : {
179+ ocs : {
180+ data : {
181+ data : [ sampleEntry ] ,
182+ total : 1 ,
183+ } ,
184+ } ,
185+ } ,
186+ } )
187+
188+ axiosPostMock . mockResolvedValue ( {
189+ data : {
190+ ocs : {
191+ data : {
192+ success : true ,
193+ } ,
194+ } ,
195+ } ,
196+ } )
197+ } )
198+
199+ it ( 'loads CRL entries on mount using saved filters and sort' , async ( ) => {
200+ const wrapper = createWrapper ( )
201+ await flushPromises ( )
202+
203+ expect ( axiosGetMock ) . toHaveBeenCalledWith ( '/ocs/v2.php/apps/libresign/api/v1/crl/list' , {
204+ params : {
205+ page : 1 ,
206+ length : 50 ,
207+ sortBy : 'revoked_at' ,
208+ sortOrder : 'DESC' ,
209+ } ,
210+ } )
211+ expect ( wrapper . vm . entries ) . toHaveLength ( 1 )
212+ expect ( wrapper . vm . hasMore ) . toBe ( false )
213+ } )
214+
215+ it ( 'computes active filters and persists filter updates' , async ( ) => {
216+ vi . useFakeTimers ( )
217+ const wrapper = createWrapper ( )
218+ await flushPromises ( )
219+
220+ wrapper . vm . filters . serialNumber = 'XYZ'
221+ wrapper . vm . filters . owner = 'Bob'
222+ wrapper . vm . onFilterChange ( )
223+ vi . runAllTimers ( )
224+ await flushPromises ( )
225+
226+ expect ( wrapper . vm . hasActiveFilters ) . toBe ( true )
227+ expect ( wrapper . vm . activeFilterCount ) . toBe ( 2 )
228+ expect ( userConfigUpdateMock ) . toHaveBeenCalledWith ( 'crl_filters' , {
229+ serialNumber : 'XYZ' ,
230+ status : null ,
231+ owner : 'Bob' ,
232+ } )
233+ vi . useRealTimers ( )
234+ } )
235+
236+ it ( 'toggles sort direction and then clears sort for the same column' , async ( ) => {
237+ const wrapper = createWrapper ( )
238+ await flushPromises ( )
239+
240+ wrapper . vm . sortBy = 'owner'
241+ wrapper . vm . sortOrder = 'DESC'
242+ await wrapper . vm . sortColumn ( 'owner' )
243+
244+ expect ( wrapper . vm . sortOrder ) . toBe ( 'ASC' )
245+
246+ await wrapper . vm . sortColumn ( 'owner' )
247+
248+ expect ( wrapper . vm . sortBy ) . toBeNull ( )
249+ expect ( wrapper . vm . sortOrder ) . toBeNull ( )
250+ expect ( userConfigUpdateMock ) . toHaveBeenCalledWith ( 'crl_sort' , {
251+ sortBy : null ,
252+ sortOrder : null ,
253+ } )
254+ } )
255+
256+ it ( 'opens the CA warning dialog before revoking a root certificate' , async ( ) => {
257+ const wrapper = createWrapper ( )
258+ await flushPromises ( )
259+
260+ wrapper . vm . openRevokeDialog ( { ...sampleEntry , certificate_type : 'root' } )
261+
262+ expect ( wrapper . vm . caWarningDialog . open ) . toBe ( true )
263+ expect ( wrapper . vm . caWarningDialog . typeLabel ) . toBe ( 'ROOT' )
264+
265+ wrapper . vm . proceedToRevokeDialog ( )
266+
267+ expect ( wrapper . vm . caWarningDialog . open ) . toBe ( false )
268+ expect ( wrapper . vm . revokeDialog . open ) . toBe ( true )
269+ expect ( wrapper . vm . revokeDialog . reasonCode ) . toEqual ( wrapper . vm . reasonCodeOptions [ 0 ] )
270+ } )
271+
272+ it ( 'revokes a certificate and refreshes the list on success' , async ( ) => {
273+ const wrapper = createWrapper ( )
274+ await flushPromises ( )
275+ axiosGetMock . mockClear ( )
276+
277+ wrapper . vm . revokeDialog . open = true
278+ wrapper . vm . revokeDialog . entry = sampleEntry
279+ wrapper . vm . revokeDialog . reasonCode = wrapper . vm . reasonCodeOptions [ 1 ]
280+ wrapper . vm . revokeDialog . reasonText = 'Compromised'
281+
282+ await wrapper . vm . confirmRevoke ( )
283+ await flushPromises ( )
284+
285+ expect ( axiosPostMock ) . toHaveBeenCalledWith ( '/ocs/v2.php/apps/libresign/api/v1/crl/revoke' , {
286+ serialNumber : 'ABC123' ,
287+ reasonCode : 1 ,
288+ reasonText : 'Compromised' ,
289+ } )
290+ expect ( showSuccessMock ) . toHaveBeenCalledWith ( 'Certificate revoked successfully' )
291+ expect ( axiosGetMock ) . toHaveBeenCalledTimes ( 1 )
292+ expect ( wrapper . vm . revokeDialog . open ) . toBe ( false )
293+ } )
294+
295+ it ( 'formats missing dates and maps unknown reason codes safely' , async ( ) => {
296+ const wrapper = createWrapper ( )
297+ await flushPromises ( )
298+
299+ expect ( wrapper . vm . formatDate ( null ) ) . toBe ( '-' )
300+ expect ( wrapper . vm . getReasonText ( 999 ) ) . toBe ( 'Unknown' )
301+ expect ( wrapper . vm . getCertificateTypeLabel ( 'intermediate' ) ) . toBe ( 'Intermediate Certificate (CA)' )
302+ } )
303+ } )
0 commit comments