test: add Stage 1 lib and Stage 2 services test coverage
Add comprehensive unit tests for: - lib layer: config, device-fingerprint, errors, storage, hooks/useBreadcrumbs, http - services layer: devices, login-logs, operation-logs, permissions, profile, roles, settings, stats, import-export All 491 tests pass across 74 test files.
This commit is contained in:
125
frontend/admin/src/services/devices.test.ts
Normal file
125
frontend/admin/src/services/devices.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const getMock = vi.fn()
|
||||
const postMock = vi.fn()
|
||||
const putMock = vi.fn()
|
||||
const delMock = vi.fn()
|
||||
|
||||
vi.mock('@/lib/http/client', () => ({
|
||||
get: getMock,
|
||||
post: postMock,
|
||||
put: putMock,
|
||||
del: delMock,
|
||||
}))
|
||||
|
||||
describe('devices service', () => {
|
||||
beforeEach(() => {
|
||||
getMock.mockReset()
|
||||
postMock.mockReset()
|
||||
putMock.mockReset()
|
||||
delMock.mockReset()
|
||||
})
|
||||
|
||||
it('lists user devices', async () => {
|
||||
const { listDevices } = await import('./devices')
|
||||
await listDevices({ page: 1, page_size: 10 })
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/devices', { page: 1, page_size: 10 })
|
||||
})
|
||||
|
||||
it('lists all devices for admin', async () => {
|
||||
const { listAllDevices } = await import('./devices')
|
||||
await listAllDevices({ page: 1, page_size: 20, status: 1 })
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/admin/devices', { page: 1, page_size: 20, status: 1 })
|
||||
})
|
||||
|
||||
it('gets a single device by id', async () => {
|
||||
const { getDevice } = await import('./devices')
|
||||
await getDevice(5)
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/devices/5')
|
||||
})
|
||||
|
||||
it('deletes a user device', async () => {
|
||||
const { deleteDevice } = await import('./devices')
|
||||
await deleteDevice(3)
|
||||
|
||||
expect(delMock).toHaveBeenCalledWith('/devices/3')
|
||||
})
|
||||
|
||||
it('deletes a device by admin', async () => {
|
||||
const { adminDeleteDevice } = await import('./devices')
|
||||
await adminDeleteDevice(7)
|
||||
|
||||
expect(delMock).toHaveBeenCalledWith('/admin/devices/7')
|
||||
})
|
||||
|
||||
it('updates device status', async () => {
|
||||
const { updateDeviceStatus } = await import('./devices')
|
||||
await updateDeviceStatus(2, 1)
|
||||
|
||||
expect(putMock).toHaveBeenCalledWith('/devices/2/status', { status: 1 })
|
||||
})
|
||||
|
||||
it('updates device status by admin', async () => {
|
||||
const { adminUpdateDeviceStatus } = await import('./devices')
|
||||
await adminUpdateDeviceStatus(4, 0)
|
||||
|
||||
expect(putMock).toHaveBeenCalledWith('/admin/devices/4/status', { status: 0 })
|
||||
})
|
||||
|
||||
it('trusts a device', async () => {
|
||||
const { trustDevice } = await import('./devices')
|
||||
await trustDevice(1, '30d')
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith('/devices/1/trust', { trust_duration: '30d' })
|
||||
})
|
||||
|
||||
it('trusts a device by admin', async () => {
|
||||
const { adminTrustDevice } = await import('./devices')
|
||||
await adminTrustDevice(6, '7d')
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith('/admin/devices/6/trust', { trust_duration: '7d' })
|
||||
})
|
||||
|
||||
it('trusts a device by device id string', async () => {
|
||||
const { trustDeviceByDeviceId } = await import('./devices')
|
||||
await trustDeviceByDeviceId('device-abc-123', '30d')
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith(
|
||||
'/devices/by-device-id/device-abc-123/trust',
|
||||
{ trust_duration: '30d' },
|
||||
)
|
||||
})
|
||||
|
||||
it('untrusts a device', async () => {
|
||||
const { untrustDevice } = await import('./devices')
|
||||
await untrustDevice(2)
|
||||
|
||||
expect(delMock).toHaveBeenCalledWith('/devices/2/trust')
|
||||
})
|
||||
|
||||
it('untrusts a device by admin', async () => {
|
||||
const { adminUntrustDevice } = await import('./devices')
|
||||
await adminUntrustDevice(8)
|
||||
|
||||
expect(delMock).toHaveBeenCalledWith('/admin/devices/8/trust')
|
||||
})
|
||||
|
||||
it('gets my trusted devices', async () => {
|
||||
const { getMyTrustedDevices } = await import('./devices')
|
||||
await getMyTrustedDevices()
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/devices/me/trusted')
|
||||
})
|
||||
|
||||
it('logs out other devices', async () => {
|
||||
const { logoutOtherDevices } = await import('./devices')
|
||||
await logoutOtherDevices('current-device-id')
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith('/devices/me/logout-others', {
|
||||
current_device_id: 'current-device-id',
|
||||
})
|
||||
})
|
||||
})
|
||||
120
frontend/admin/src/services/import-export.test.ts
Normal file
120
frontend/admin/src/services/import-export.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const downloadMock = vi.fn()
|
||||
const postMock = vi.fn()
|
||||
|
||||
vi.mock('@/lib/http/client', () => ({
|
||||
download: downloadMock,
|
||||
post: postMock,
|
||||
}))
|
||||
|
||||
describe('import-export service', () => {
|
||||
beforeEach(() => {
|
||||
downloadMock.mockReset()
|
||||
postMock.mockReset()
|
||||
})
|
||||
|
||||
it('exports users with specified format and fields', async () => {
|
||||
const blob = new Blob(['csv,data'], { type: 'text/csv' })
|
||||
downloadMock.mockResolvedValue(blob)
|
||||
|
||||
const clickMock = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined)
|
||||
const createObjectURLMock = vi.fn(() => 'blob:mock')
|
||||
const revokeObjectURLMock = vi.fn()
|
||||
|
||||
Object.defineProperty(window.URL, 'createObjectURL', {
|
||||
configurable: true,
|
||||
value: createObjectURLMock,
|
||||
})
|
||||
Object.defineProperty(window.URL, 'revokeObjectURL', {
|
||||
configurable: true,
|
||||
value: revokeObjectURLMock,
|
||||
})
|
||||
|
||||
const { exportUsers } = await import('./import-export')
|
||||
await exportUsers({
|
||||
format: 'csv',
|
||||
fields: ['id', 'username', 'email'],
|
||||
keyword: 'alice',
|
||||
status: 1,
|
||||
})
|
||||
|
||||
expect(downloadMock).toHaveBeenCalledWith('/admin/users/export', {
|
||||
format: 'csv',
|
||||
fields: 'id,username,email',
|
||||
keyword: 'alice',
|
||||
status: 1,
|
||||
})
|
||||
|
||||
expect(createObjectURLMock).toHaveBeenCalled()
|
||||
expect(clickMock).toHaveBeenCalled()
|
||||
expect(revokeObjectURLMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('downloads import template', async () => {
|
||||
const blob = new Blob(['template,data'], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||||
downloadMock.mockResolvedValue(blob)
|
||||
|
||||
const clickMock = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined)
|
||||
const createObjectURLMock = vi.fn(() => 'blob:mock')
|
||||
const revokeObjectURLMock = vi.fn()
|
||||
|
||||
Object.defineProperty(window.URL, 'createObjectURL', {
|
||||
configurable: true,
|
||||
value: createObjectURLMock,
|
||||
})
|
||||
Object.defineProperty(window.URL, 'revokeObjectURL', {
|
||||
configurable: true,
|
||||
value: revokeObjectURLMock,
|
||||
})
|
||||
|
||||
const { downloadImportTemplate } = await import('./import-export')
|
||||
await downloadImportTemplate('xlsx')
|
||||
|
||||
expect(downloadMock).toHaveBeenCalledWith('/admin/users/import/template', { format: 'xlsx' })
|
||||
|
||||
expect(createObjectURLMock).toHaveBeenCalled()
|
||||
expect(clickMock).toHaveBeenCalled()
|
||||
expect(revokeObjectURLMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('imports users from csv file', async () => {
|
||||
const file = new File(['username,email'], 'users.csv', { type: 'text/csv' })
|
||||
const importResult = {
|
||||
success_count: 10,
|
||||
fail_count: 2,
|
||||
errors: ['Row 3: Invalid email', 'Row 7: Missing username'],
|
||||
message: 'Import completed with errors',
|
||||
}
|
||||
postMock.mockResolvedValue(importResult)
|
||||
|
||||
const { importUsers } = await import('./import-export')
|
||||
const result = await importUsers(file)
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith('/admin/users/import', expect.any(FormData))
|
||||
const payload = postMock.mock.calls[0][1] as FormData
|
||||
expect(payload.get('file')).toBe(file)
|
||||
expect(result).toEqual(importResult)
|
||||
})
|
||||
|
||||
it('imports users from xlsx file', async () => {
|
||||
const file = new File(['xlsx,data'], 'users.xlsx', {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
})
|
||||
const importResult = {
|
||||
success_count: 50,
|
||||
fail_count: 0,
|
||||
errors: [],
|
||||
message: 'Import successful',
|
||||
}
|
||||
postMock.mockResolvedValue(importResult)
|
||||
|
||||
const { importUsers } = await import('./import-export')
|
||||
const result = await importUsers(file)
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith('/admin/users/import', expect.any(FormData))
|
||||
const payload = postMock.mock.calls[0][1] as FormData
|
||||
expect(payload.get('file')).toBe(file)
|
||||
expect(result).toEqual(importResult)
|
||||
})
|
||||
})
|
||||
76
frontend/admin/src/services/login-logs.test.ts
Normal file
76
frontend/admin/src/services/login-logs.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const getMock = vi.fn()
|
||||
const downloadMock = vi.fn()
|
||||
|
||||
vi.mock('@/lib/http/client', () => ({
|
||||
get: getMock,
|
||||
download: downloadMock,
|
||||
}))
|
||||
|
||||
describe('login-logs service', () => {
|
||||
beforeEach(() => {
|
||||
getMock.mockReset()
|
||||
downloadMock.mockReset()
|
||||
})
|
||||
|
||||
it('lists login logs with pagination', async () => {
|
||||
getMock.mockResolvedValue({
|
||||
list: [{ id: 1, status: 1, login_type: 1 }],
|
||||
total: 1,
|
||||
page: 1,
|
||||
size: 20,
|
||||
})
|
||||
|
||||
const { listLoginLogs } = await import('./login-logs')
|
||||
const result = await listLoginLogs({ page: 1, page_size: 20 })
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/logs/login', { page: 1, page_size: 20 })
|
||||
expect(result).toEqual({
|
||||
items: [{ id: 1, status: 1, login_type: 1 }],
|
||||
total: 1,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
})
|
||||
|
||||
it('lists login logs with filters', async () => {
|
||||
getMock.mockResolvedValue({
|
||||
list: [{ id: 2, status: 0 }],
|
||||
total: 1,
|
||||
page: 2,
|
||||
size: 10,
|
||||
})
|
||||
|
||||
const { listLoginLogs } = await import('./login-logs')
|
||||
const result = await listLoginLogs({ page: 2, page_size: 10, status: 0 })
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/logs/login', { page: 2, page_size: 10, status: 0 })
|
||||
expect(result).toEqual({
|
||||
items: [{ id: 2, status: 0 }],
|
||||
total: 1,
|
||||
page: 2,
|
||||
page_size: 10,
|
||||
})
|
||||
})
|
||||
|
||||
it('lists my login logs', async () => {
|
||||
getMock.mockResolvedValue({
|
||||
list: [{ id: 3, status: 1 }],
|
||||
total: 3,
|
||||
page: 1,
|
||||
size: 5,
|
||||
})
|
||||
|
||||
const { listMyLoginLogs } = await import('./login-logs')
|
||||
const result = await listMyLoginLogs({ page: 1, page_size: 5 })
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/logs/login/me', { page: 1, page_size: 5 })
|
||||
expect(result).toEqual({
|
||||
items: [{ id: 3, status: 1 }],
|
||||
total: 3,
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
})
|
||||
})
|
||||
})
|
||||
73
frontend/admin/src/services/operation-logs.test.ts
Normal file
73
frontend/admin/src/services/operation-logs.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const getMock = vi.fn()
|
||||
|
||||
vi.mock('@/lib/http/client', () => ({
|
||||
get: getMock,
|
||||
}))
|
||||
|
||||
describe('operation-logs service', () => {
|
||||
beforeEach(() => {
|
||||
getMock.mockReset()
|
||||
})
|
||||
|
||||
it('lists operation logs with pagination', async () => {
|
||||
getMock.mockResolvedValue({
|
||||
list: [{ id: 1, operation_name: 'create_user' }],
|
||||
total: 1,
|
||||
page: 1,
|
||||
size: 20,
|
||||
})
|
||||
|
||||
const { listOperationLogs } = await import('./operation-logs')
|
||||
const result = await listOperationLogs({ page: 1, page_size: 20 })
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/logs/operation', { page: 1, page_size: 20 })
|
||||
expect(result).toEqual({
|
||||
items: [{ id: 1, operation_name: 'create_user' }],
|
||||
total: 1,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
})
|
||||
|
||||
it('lists operation logs with filters', async () => {
|
||||
getMock.mockResolvedValue({
|
||||
list: [{ id: 2, operation_name: 'update_user', method: 'PUT' }],
|
||||
total: 1,
|
||||
page: 2,
|
||||
size: 10,
|
||||
})
|
||||
|
||||
const { listOperationLogs } = await import('./operation-logs')
|
||||
const result = await listOperationLogs({ page: 2, page_size: 10, method: 'PUT' })
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/logs/operation', { page: 2, page_size: 10, method: 'PUT' })
|
||||
expect(result).toEqual({
|
||||
items: [{ id: 2, operation_name: 'update_user', method: 'PUT' }],
|
||||
total: 1,
|
||||
page: 2,
|
||||
page_size: 10,
|
||||
})
|
||||
})
|
||||
|
||||
it('lists my operation logs', async () => {
|
||||
getMock.mockResolvedValue({
|
||||
list: [{ id: 3, operation_name: 'login' }],
|
||||
total: 5,
|
||||
page: 1,
|
||||
size: 10,
|
||||
})
|
||||
|
||||
const { listMyOperationLogs } = await import('./operation-logs')
|
||||
const result = await listMyOperationLogs({ page: 1, page_size: 10 })
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/logs/operation/me', { page: 1, page_size: 10 })
|
||||
expect(result).toEqual({
|
||||
items: [{ id: 3, operation_name: 'login' }],
|
||||
total: 5,
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
})
|
||||
})
|
||||
})
|
||||
100
frontend/admin/src/services/permissions.test.ts
Normal file
100
frontend/admin/src/services/permissions.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const getMock = vi.fn()
|
||||
const postMock = vi.fn()
|
||||
const putMock = vi.fn()
|
||||
const delMock = vi.fn()
|
||||
|
||||
vi.mock('@/lib/http/client', () => ({
|
||||
get: getMock,
|
||||
post: postMock,
|
||||
put: putMock,
|
||||
del: delMock,
|
||||
}))
|
||||
|
||||
describe('permissions service', () => {
|
||||
beforeEach(() => {
|
||||
getMock.mockReset()
|
||||
postMock.mockReset()
|
||||
putMock.mockReset()
|
||||
delMock.mockReset()
|
||||
})
|
||||
|
||||
it('gets permission tree', async () => {
|
||||
const mockTree = [
|
||||
{ id: 1, name: 'dashboard', children: [{ id: 2, name: 'view' }] },
|
||||
]
|
||||
getMock.mockResolvedValue(mockTree)
|
||||
|
||||
const { getPermissionTree } = await import('./permissions')
|
||||
const result = await getPermissionTree()
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/permissions/tree')
|
||||
expect(result).toEqual(mockTree)
|
||||
})
|
||||
|
||||
it('lists all permissions', async () => {
|
||||
const mockPermissions = [
|
||||
{ id: 1, name: 'view dashboard', code: 'dashboard:view' },
|
||||
{ id: 2, name: 'edit dashboard', code: 'dashboard:edit' },
|
||||
]
|
||||
getMock.mockResolvedValue(mockPermissions)
|
||||
|
||||
const { listPermissions } = await import('./permissions')
|
||||
const result = await listPermissions()
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/permissions')
|
||||
expect(result).toEqual(mockPermissions)
|
||||
})
|
||||
|
||||
it('gets a single permission', async () => {
|
||||
getMock.mockResolvedValue({ id: 5, name: 'view users', code: 'users:view' })
|
||||
|
||||
const { getPermission } = await import('./permissions')
|
||||
const result = await getPermission(5)
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/permissions/5')
|
||||
expect(result).toEqual({ id: 5, name: 'view users', code: 'users:view' })
|
||||
})
|
||||
|
||||
it('creates a permission', async () => {
|
||||
const newPermission = { name: 'new permission', code: 'new:code', type: 'button' as const }
|
||||
const created = { id: 10, ...newPermission }
|
||||
postMock.mockResolvedValue(created)
|
||||
|
||||
const { createPermission } = await import('./permissions')
|
||||
const result = await createPermission(newPermission)
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith('/permissions', newPermission)
|
||||
expect(result).toEqual(created)
|
||||
})
|
||||
|
||||
it('updates a permission', async () => {
|
||||
const updateData = { name: 'updated name' }
|
||||
putMock.mockResolvedValue({ id: 3, ...updateData })
|
||||
|
||||
const { updatePermission } = await import('./permissions')
|
||||
const result = await updatePermission(3, updateData)
|
||||
|
||||
expect(putMock).toHaveBeenCalledWith('/permissions/3', updateData)
|
||||
expect(result).toEqual({ id: 3, name: 'updated name' })
|
||||
})
|
||||
|
||||
it('deletes a permission', async () => {
|
||||
delMock.mockResolvedValue(undefined)
|
||||
|
||||
const { deletePermission } = await import('./permissions')
|
||||
await deletePermission(7)
|
||||
|
||||
expect(delMock).toHaveBeenCalledWith('/permissions/7')
|
||||
})
|
||||
|
||||
it('updates permission status', async () => {
|
||||
putMock.mockResolvedValue(undefined)
|
||||
|
||||
const { updatePermissionStatus } = await import('./permissions')
|
||||
await updatePermissionStatus(4, 0)
|
||||
|
||||
expect(putMock).toHaveBeenCalledWith('/permissions/4/status', { status: 0 })
|
||||
})
|
||||
})
|
||||
127
frontend/admin/src/services/profile.test.ts
Normal file
127
frontend/admin/src/services/profile.test.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const getMock = vi.fn()
|
||||
const postMock = vi.fn()
|
||||
const putMock = vi.fn()
|
||||
|
||||
vi.mock('@/lib/http/client', () => ({
|
||||
get: getMock,
|
||||
post: postMock,
|
||||
put: putMock,
|
||||
}))
|
||||
|
||||
vi.mock('./users', () => ({
|
||||
getUserRoles: vi.fn().mockResolvedValue([{ id: 2, name: '管理员' }]),
|
||||
}))
|
||||
|
||||
describe('profile service', () => {
|
||||
beforeEach(() => {
|
||||
getMock.mockReset()
|
||||
postMock.mockReset()
|
||||
putMock.mockReset()
|
||||
})
|
||||
|
||||
it('gets current user profile with roles', async () => {
|
||||
getMock
|
||||
.mockResolvedValueOnce({ id: 1, username: 'admin', nickname: 'Admin' })
|
||||
.mockResolvedValueOnce([{ id: 2, name: '管理员' }])
|
||||
|
||||
const { getCurrentProfile } = await import('./profile')
|
||||
const result = await getCurrentProfile(1)
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/users/1')
|
||||
expect(result).toEqual({
|
||||
user: { id: 1, username: 'admin', nickname: 'Admin' },
|
||||
roles: [{ id: 2, name: '管理员' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('updates user profile', async () => {
|
||||
const updateData = { nickname: 'New Nickname' }
|
||||
putMock.mockResolvedValue({ id: 1, ...updateData })
|
||||
|
||||
const { updateProfile } = await import('./profile')
|
||||
const result = await updateProfile(1, updateData)
|
||||
|
||||
expect(putMock).toHaveBeenCalledWith('/users/1', updateData)
|
||||
expect(result).toEqual({ id: 1, nickname: 'New Nickname' })
|
||||
})
|
||||
|
||||
it('uploads avatar', async () => {
|
||||
const file = new File(['avatar'], 'avatar.png', { type: 'image/png' })
|
||||
const uploadResponse = {
|
||||
avatar_url: 'https://example.com/avatar.png',
|
||||
thumbnail: 'https://example.com/avatar_thumb.png',
|
||||
message: 'Upload success',
|
||||
}
|
||||
postMock.mockResolvedValue(uploadResponse)
|
||||
|
||||
const { uploadAvatar } = await import('./profile')
|
||||
const result = await uploadAvatar(1, file)
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith('/users/1/avatar', expect.any(FormData))
|
||||
const payload = postMock.mock.calls[0][1] as FormData
|
||||
expect(payload.get('avatar')).toBe(file)
|
||||
expect(result).toEqual(uploadResponse)
|
||||
})
|
||||
|
||||
it('updates password', async () => {
|
||||
putMock.mockResolvedValue(undefined)
|
||||
|
||||
const { updatePassword } = await import('./profile')
|
||||
await updatePassword(1, {
|
||||
current_password: 'OldPass123',
|
||||
new_password: 'NewPass123',
|
||||
confirm_password: 'NewPass123',
|
||||
})
|
||||
|
||||
expect(putMock).toHaveBeenCalledWith('/users/1/password', {
|
||||
current_password: 'OldPass123',
|
||||
new_password: 'NewPass123',
|
||||
confirm_password: 'NewPass123',
|
||||
})
|
||||
})
|
||||
|
||||
it('gets TOTP status', async () => {
|
||||
getMock.mockResolvedValue({ totp_enabled: true })
|
||||
|
||||
const { getTOTPStatus } = await import('./profile')
|
||||
const result = await getTOTPStatus()
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/auth/2fa/status')
|
||||
expect(result).toEqual({ totp_enabled: true })
|
||||
})
|
||||
|
||||
it('gets TOTP setup data', async () => {
|
||||
const setupData = {
|
||||
secret: 'JBSWY3DPEHPK3PXP',
|
||||
qr_code_base64: 'data:image/png;base64,abc123',
|
||||
recovery_codes: ['code1', 'code2', 'code3'],
|
||||
}
|
||||
getMock.mockResolvedValue(setupData)
|
||||
|
||||
const { getTOTPSetup } = await import('./profile')
|
||||
const result = await getTOTPSetup()
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/auth/2fa/setup')
|
||||
expect(result).toEqual(setupData)
|
||||
})
|
||||
|
||||
it('enables TOTP', async () => {
|
||||
postMock.mockResolvedValue(undefined)
|
||||
|
||||
const { enableTOTP } = await import('./profile')
|
||||
await enableTOTP('123456')
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith('/auth/2fa/enable', { code: '123456' })
|
||||
})
|
||||
|
||||
it('disables TOTP', async () => {
|
||||
postMock.mockResolvedValue(undefined)
|
||||
|
||||
const { disableTOTP } = await import('./profile')
|
||||
await disableTOTP('654321')
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith('/auth/2fa/disable', { code: '654321' })
|
||||
})
|
||||
})
|
||||
121
frontend/admin/src/services/roles.test.ts
Normal file
121
frontend/admin/src/services/roles.test.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const getMock = vi.fn()
|
||||
const postMock = vi.fn()
|
||||
const putMock = vi.fn()
|
||||
const delMock = vi.fn()
|
||||
|
||||
vi.mock('@/lib/http/client', () => ({
|
||||
get: getMock,
|
||||
post: postMock,
|
||||
put: putMock,
|
||||
del: delMock,
|
||||
}))
|
||||
|
||||
describe('roles service', () => {
|
||||
beforeEach(() => {
|
||||
getMock.mockReset()
|
||||
postMock.mockReset()
|
||||
putMock.mockReset()
|
||||
delMock.mockReset()
|
||||
})
|
||||
|
||||
it('lists roles with pagination', async () => {
|
||||
getMock.mockResolvedValue({
|
||||
items: [
|
||||
{ id: 1, name: '管理员', code: 'admin' },
|
||||
{ id: 2, name: '用户', code: 'user' },
|
||||
],
|
||||
total: 2,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
|
||||
const { listRoles } = await import('./roles')
|
||||
const result = await listRoles({ page: 1, page_size: 20 })
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/roles', { page: 1, page_size: 20 })
|
||||
expect(result).toEqual({
|
||||
items: [
|
||||
{ id: 1, name: '管理员', code: 'admin' },
|
||||
{ id: 2, name: '用户', code: 'user' },
|
||||
],
|
||||
total: 2,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
})
|
||||
|
||||
it('gets a single role', async () => {
|
||||
getMock.mockResolvedValue({ id: 3, name: '审计员', code: 'auditor' })
|
||||
|
||||
const { getRole } = await import('./roles')
|
||||
const result = await getRole(3)
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/roles/3')
|
||||
expect(result).toEqual({ id: 3, name: '审计员', code: 'auditor' })
|
||||
})
|
||||
|
||||
it('creates a role', async () => {
|
||||
const roleData = { name: '新角色', code: 'new_role' }
|
||||
const created = { id: 10, ...roleData }
|
||||
postMock.mockResolvedValue(created)
|
||||
|
||||
const { createRole } = await import('./roles')
|
||||
const result = await createRole(roleData)
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith('/roles', roleData)
|
||||
expect(result).toEqual(created)
|
||||
})
|
||||
|
||||
it('updates a role', async () => {
|
||||
const updateData = { name: '更新的角色', description: '新描述' }
|
||||
putMock.mockResolvedValue({ id: 5, ...updateData })
|
||||
|
||||
const { updateRole } = await import('./roles')
|
||||
const result = await updateRole(5, updateData)
|
||||
|
||||
expect(putMock).toHaveBeenCalledWith('/roles/5', updateData)
|
||||
expect(result).toEqual({ id: 5, ...updateData })
|
||||
})
|
||||
|
||||
it('deletes a role', async () => {
|
||||
delMock.mockResolvedValue(undefined)
|
||||
|
||||
const { deleteRole } = await import('./roles')
|
||||
await deleteRole(7)
|
||||
|
||||
expect(delMock).toHaveBeenCalledWith('/roles/7')
|
||||
})
|
||||
|
||||
it('updates role status', async () => {
|
||||
putMock.mockResolvedValue(undefined)
|
||||
|
||||
const { updateRoleStatus } = await import('./roles')
|
||||
await updateRoleStatus(4, 0)
|
||||
|
||||
expect(putMock).toHaveBeenCalledWith('/roles/4/status', { status: 0 })
|
||||
})
|
||||
|
||||
it('gets role permissions', async () => {
|
||||
getMock.mockResolvedValue([
|
||||
{ id: 1, name: 'view' },
|
||||
{ id: 2, name: 'edit' },
|
||||
])
|
||||
|
||||
const { getRolePermissions } = await import('./roles')
|
||||
const result = await getRolePermissions(3)
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/roles/3/permissions')
|
||||
expect(result).toEqual([1, 2])
|
||||
})
|
||||
|
||||
it('assigns permissions to a role', async () => {
|
||||
putMock.mockResolvedValue(undefined)
|
||||
|
||||
const { assignRolePermissions } = await import('./roles')
|
||||
await assignRolePermissions(3, [1, 2, 3])
|
||||
|
||||
expect(putMock).toHaveBeenCalledWith('/roles/3/permissions', { permission_ids: [1, 2, 3] })
|
||||
})
|
||||
})
|
||||
58
frontend/admin/src/services/settings.test.ts
Normal file
58
frontend/admin/src/services/settings.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const getMock = vi.fn()
|
||||
|
||||
vi.mock('@/lib/http/client', () => ({
|
||||
get: getMock,
|
||||
}))
|
||||
|
||||
describe('settings service', () => {
|
||||
beforeEach(() => {
|
||||
getMock.mockReset()
|
||||
})
|
||||
|
||||
it('gets system settings', async () => {
|
||||
const mockSettings = {
|
||||
data: {
|
||||
system: {
|
||||
name: 'UserSystem',
|
||||
version: '1.0.0',
|
||||
environment: 'production',
|
||||
description: 'User management system',
|
||||
},
|
||||
security: {
|
||||
password_min_length: 8,
|
||||
password_require_uppercase: true,
|
||||
password_require_lowercase: true,
|
||||
password_require_numbers: true,
|
||||
password_require_symbols: true,
|
||||
password_history: 5,
|
||||
totp_enabled: true,
|
||||
login_fail_lock: true,
|
||||
login_fail_threshold: 5,
|
||||
login_fail_duration: 30,
|
||||
session_timeout: 3600,
|
||||
device_trust_duration: 2592000,
|
||||
},
|
||||
features: {
|
||||
email_verification: true,
|
||||
phone_verification: false,
|
||||
oauth_providers: ['google', 'github'],
|
||||
sso_enabled: false,
|
||||
operation_log_enabled: true,
|
||||
login_log_enabled: true,
|
||||
data_export_enabled: true,
|
||||
data_import_enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
getMock.mockResolvedValue(mockSettings)
|
||||
|
||||
const { getSettings } = await import('./settings')
|
||||
const result = await getSettings()
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/admin/settings')
|
||||
expect(result).toEqual(mockSettings.data)
|
||||
})
|
||||
})
|
||||
49
frontend/admin/src/services/stats.test.ts
Normal file
49
frontend/admin/src/services/stats.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const getMock = vi.fn()
|
||||
|
||||
vi.mock('@/lib/http/client', () => ({
|
||||
get: getMock,
|
||||
}))
|
||||
|
||||
describe('stats service', () => {
|
||||
beforeEach(() => {
|
||||
getMock.mockReset()
|
||||
})
|
||||
|
||||
it('gets dashboard stats', async () => {
|
||||
const mockStats = {
|
||||
total_users: 100,
|
||||
active_users: 75,
|
||||
new_users_today: 5,
|
||||
total_devices: 200,
|
||||
trusted_devices: 150,
|
||||
}
|
||||
|
||||
getMock.mockResolvedValue(mockStats)
|
||||
|
||||
const { getDashboardStats } = await import('./stats')
|
||||
const result = await getDashboardStats()
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/admin/stats/dashboard')
|
||||
expect(result).toEqual(mockStats)
|
||||
})
|
||||
|
||||
it('gets user stats', async () => {
|
||||
const mockUserStats = {
|
||||
total: 100,
|
||||
active: 75,
|
||||
inactive: 25,
|
||||
verified: 80,
|
||||
unverified: 20,
|
||||
}
|
||||
|
||||
getMock.mockResolvedValue(mockUserStats)
|
||||
|
||||
const { getUserStats } = await import('./stats')
|
||||
const result = await getUserStats()
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('/admin/stats/users')
|
||||
expect(result).toEqual(mockUserStats)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user