Skip to content

Commit bcbed88

Browse files
test(vue3): add CrlManagement view coverage
Cover list loading, filter persistence, sort toggling and certificate revocation flows for the migrated CRL management view. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 925a5fd commit bcbed88

1 file changed

Lines changed: 303 additions & 0 deletions

File tree

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
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

Comments
 (0)