Files
sub2api/frontend/src/views/admin/__tests__/AuthIdentityMigrationReportsView.spec.ts
2026-04-21 02:08:56 +08:00

304 lines
8.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import { defineComponent, h } from 'vue'
import AuthIdentityMigrationReportsView from '../AuthIdentityMigrationReportsView.vue'
const {
bindUserAuthIdentity,
getAuthIdentityMigrationReportSummary,
listAuthIdentityMigrationReports,
resolveAuthIdentityMigrationReport,
} = vi.hoisted(() => ({
bindUserAuthIdentity: vi.fn(),
getAuthIdentityMigrationReportSummary: vi.fn(),
listAuthIdentityMigrationReports: vi.fn(),
resolveAuthIdentityMigrationReport: vi.fn(),
}))
const { showError, showSuccess } = vi.hoisted(() => ({
showError: vi.fn(),
showSuccess: vi.fn(),
}))
vi.mock('@/api/admin', () => ({
adminAPI: {
users: {
bindUserAuthIdentity,
getAuthIdentityMigrationReportSummary,
listAuthIdentityMigrationReports,
resolveAuthIdentityMigrationReport,
},
},
}))
vi.mock('@/stores/app', () => ({
useAppStore: () => ({
showError,
showSuccess,
}),
}))
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({
locale: { value: 'en' },
t: (key: string) => key,
}),
}
})
vi.mock('@/utils/format', () => ({
formatDateTime: (value: string | null | undefined) => value ?? '',
}))
const sampleReport = {
id: 1,
report_type: 'oidc_synthetic_email_requires_manual_recovery',
report_key: 'legacy@example.invalid',
details: {
user_id: 42,
legacy_email: 'legacy@example.invalid',
provider_key: 'https://issuer.example',
provider_subject: 'subject-123',
},
created_at: '2026-04-20T01:02:03Z',
resolved_at: null,
resolved_by_user_id: null,
resolution_note: '',
}
const summaryResponse = {
total: 2,
open_total: 1,
resolved_total: 1,
by_type: {
oidc_synthetic_email_requires_manual_recovery: 2,
},
}
const listResponse = {
items: [sampleReport],
total: 1,
page: 1,
page_size: 20,
pages: 1,
}
const AppLayoutStub = defineComponent({
setup(_, { slots }) {
return () => h('div', slots.default?.())
},
})
const TablePageLayoutStub = defineComponent({
setup(_, { slots }) {
return () => h('div', [
slots.actions?.(),
slots.filters?.(),
slots.table?.(),
slots.default?.(),
slots.pagination?.(),
])
},
})
const DataTableStub = defineComponent({
props: {
columns: { type: Array, default: () => [] },
data: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
},
setup(props, { slots }) {
return () => h('div', { 'data-test': 'data-table' }, [
props.loading
? h('div', 'loading')
: (props.data as Array<Record<string, unknown>>).map((row) =>
h(
'div',
{ key: String(row.id ?? row.report_key) },
(props.columns as Array<{ key: string }>).map((column) => {
const slot = slots[`cell-${column.key}`]
return h(
'div',
{ key: column.key, [`data-test-cell`]: `${String(row.id)}-${column.key}` },
slot
? slot({ row, value: row[column.key] })
: String(row[column.key] ?? '')
)
})
)
),
])
},
})
const PaginationStub = defineComponent({
props: {
total: { type: Number, required: true },
page: { type: Number, required: true },
pageSize: { type: Number, required: true },
},
emits: ['update:page', 'update:pageSize'],
setup(props, { emit }) {
return () => h('div', { 'data-test': 'pagination' }, [
h('button', {
type: 'button',
'data-test': 'next-page',
onClick: () => emit('update:page', props.page + 1),
}, 'next'),
h('button', {
type: 'button',
'data-test': 'page-size-50',
onClick: () => emit('update:pageSize', 50),
}, '50'),
])
},
})
describe('AuthIdentityMigrationReportsView', () => {
beforeEach(() => {
getAuthIdentityMigrationReportSummary.mockReset()
listAuthIdentityMigrationReports.mockReset()
resolveAuthIdentityMigrationReport.mockReset()
bindUserAuthIdentity.mockReset()
showError.mockReset()
showSuccess.mockReset()
getAuthIdentityMigrationReportSummary.mockResolvedValue(summaryResponse)
listAuthIdentityMigrationReports.mockResolvedValue(listResponse)
resolveAuthIdentityMigrationReport.mockResolvedValue({
...sampleReport,
resolved_at: '2026-04-20T02:00:00Z',
resolved_by_user_id: 100,
resolution_note: 'resolved by admin',
})
bindUserAuthIdentity.mockResolvedValue({
identity_id: 77,
provider_type: 'oidc',
provider_key: 'https://issuer.example',
provider_subject: 'subject-123',
})
})
const mountView = () =>
mount(AuthIdentityMigrationReportsView, {
global: {
stubs: {
AppLayout: AppLayoutStub,
TablePageLayout: TablePageLayoutStub,
DataTable: DataTableStub,
Pagination: PaginationStub,
Icon: true,
},
},
})
it('loads summary and first page of reports on mount', async () => {
const wrapper = mountView()
await flushPromises()
expect(getAuthIdentityMigrationReportSummary).toHaveBeenCalledTimes(1)
expect(listAuthIdentityMigrationReports).toHaveBeenCalledWith({
page: 1,
pageSize: 20,
reportType: '',
})
expect(wrapper.get('[data-test="summary-total"]').text()).toContain('2')
expect(wrapper.get('[data-test="summary-open"]').text()).toContain('1')
expect(wrapper.get('[data-test="summary-resolved"]').text()).toContain('1')
expect(wrapper.text()).toContain('legacy@example.invalid')
})
it('reloads list when the report type filter changes', async () => {
const wrapper = mountView()
await flushPromises()
listAuthIdentityMigrationReports.mockClear()
await wrapper.get('[data-test="report-type-filter"]').setValue(
'oidc_synthetic_email_requires_manual_recovery'
)
await flushPromises()
expect(listAuthIdentityMigrationReports).toHaveBeenCalledWith({
page: 1,
pageSize: 20,
reportType: 'oidc_synthetic_email_requires_manual_recovery',
})
})
it('submits resolve note for the selected report and refreshes data', async () => {
const wrapper = mountView()
await flushPromises()
getAuthIdentityMigrationReportSummary.mockClear()
listAuthIdentityMigrationReports.mockClear()
await wrapper.get('[data-test="select-report-1"]').trigger('click')
await wrapper.get('[data-test="resolution-note"]').setValue('resolved by admin')
await wrapper.get('[data-test="resolve-submit"]').trigger('click')
await flushPromises()
expect(resolveAuthIdentityMigrationReport).toHaveBeenCalledWith(1, 'resolved by admin')
expect(showSuccess).toHaveBeenCalled()
expect(getAuthIdentityMigrationReportSummary).toHaveBeenCalledTimes(1)
expect(listAuthIdentityMigrationReports).toHaveBeenCalledWith({
page: 1,
pageSize: 20,
reportType: '',
})
})
it('pre-fills and submits remediation binding for the selected report', async () => {
const wrapper = mountView()
await flushPromises()
await wrapper.get('[data-test="select-report-1"]').trigger('click')
await flushPromises()
expect((wrapper.get('[data-test="remediation-user-id"]').element as HTMLInputElement).value).toBe('42')
expect((wrapper.get('[data-test="remediation-provider-type"]').element as HTMLInputElement).value).toBe('oidc')
expect((wrapper.get('[data-test="remediation-provider-key"]').element as HTMLInputElement).value).toBe(
'https://issuer.example'
)
expect((wrapper.get('[data-test="remediation-provider-subject"]').element as HTMLInputElement).value).toBe(
'subject-123'
)
await wrapper.get('[data-test="remediation-submit"]').trigger('click')
await flushPromises()
expect(bindUserAuthIdentity).toHaveBeenCalledWith(42, {
provider_type: 'oidc',
provider_key: 'https://issuer.example',
provider_subject: 'subject-123',
issuer: undefined,
metadata: {},
})
expect(showSuccess).toHaveBeenCalled()
})
it('keeps report type filter options available from list data when summary fails', async () => {
getAuthIdentityMigrationReportSummary.mockRejectedValueOnce(new Error('summary failed'))
listAuthIdentityMigrationReports.mockResolvedValueOnce(listResponse)
const wrapper = mountView()
await flushPromises()
const options = wrapper
.get('[data-test="report-type-filter"]')
.findAll('option')
.map((node) => node.element.value)
expect(showError).toHaveBeenCalled()
expect(options).toContain('oidc_synthetic_email_requires_manual_recovery')
})
})