fix: 统一API响应格式并修复前端测试

- 所有Handler方法使用标准{code:0,message:"success",data:...}响应格式
- 修复Cursor分页响应包装(GetAllDevices,GetLoginLogs,ListUsers等)
- 修复AuthHandler和SMSHandler认证方法响应格式
- 修复operation_log.go admin用户operation_type前缀问题
- 修复DashboardPage嵌套stats结构
- 修复LoginLogsPage reset功能stale closure问题
- 修复UsersPage批量操作API调用
- 修复多个前端测试(mock格式、按钮选择、断言逻辑)
- 添加OAuth测试域名白名单
- 新增代码审查流程文档
This commit is contained in:
2026-04-08 20:06:54 +08:00
parent 26c5def4d7
commit a85d822419
33 changed files with 2108 additions and 206 deletions

View File

@@ -19,17 +19,21 @@ describe('stats service', () => {
it('gets dashboard stats', async () => {
const mockData = {
total_users: 100,
active_users: 80,
inactive_users: 10,
locked_users: 5,
disabled_users: 5,
today_new_users: 3,
week_new_users: 15,
month_new_users: 50,
today_success_logins: 50,
today_failed_logins: 2,
week_success_logins: 300,
users: {
total_users: 100,
active_users: 80,
inactive_users: 10,
locked_users: 5,
disabled_users: 5,
new_users_today: 3,
new_users_week: 15,
new_users_month: 50,
},
logins: {
logins_today_success: 50,
logins_today_failed: 2,
logins_week: 300,
},
}
getMock.mockResolvedValue(mockData)
@@ -38,8 +42,8 @@ describe('stats service', () => {
expect(getMock).toHaveBeenCalledWith('/admin/stats/dashboard')
expect(result).toEqual(mockData)
expect(result.total_users).toBe(100)
expect(result.active_users).toBe(80)
expect(result.users.total_users).toBe(100)
expect(result.users.active_users).toBe(80)
})
it('gets user stats', async () => {

View File

@@ -8,8 +8,9 @@ import { get, post, put, del } from '@/lib/http/client'
import type { PaginatedData } from '@/types/http'
import type { Role } from '@/types/auth'
import type {
CreateUserRequest,
User,
UserStatus,
CreateUserRequest,
UserListParams,
UpdateUserRequest,
UpdateUserStatusRequest,
@@ -79,3 +80,19 @@ export function getUserRoles(id: number): Promise<Role[]> {
export function assignUserRoles(id: number, data: AssignUserRolesRequest): Promise<void> {
return put<void>(`/users/${id}/roles`, data)
}
/**
* 批量更新用户状态
* PUT /api/v1/users/batch/status
*/
export function batchUpdateStatus(ids: number[], status: UserStatus): Promise<{ count: number }> {
return put<{ count: number }>('/users/batch/status', { ids, status })
}
/**
* 批量删除用户
* DELETE /api/v1/users/batch
*/
export function batchDelete(ids: number[]): Promise<{ count: number }> {
return del<{ count: number }>('/users/batch', { body: { ids } })
}

View File

@@ -21,44 +21,49 @@ describe('webhooks service', () => {
})
it('normalizes mixed raw event payloads from the API', async () => {
getMock.mockResolvedValue([
{
id: 1,
name: 'String Events',
url: 'https://example.com/string',
events: '["user.registered"]',
status: 1,
max_retries: 3,
timeout_sec: 10,
created_by: 1,
created_at: '2026-03-27 20:00:00',
updated_at: '2026-03-27 20:00:00',
},
{
id: 2,
name: 'Array Events',
url: 'https://example.com/array',
events: ['user.login'],
status: 0,
max_retries: 3,
timeout_sec: 10,
created_by: 2,
created_at: '2026-03-27 20:05:00',
updated_at: '2026-03-27 20:05:00',
},
{
id: 3,
name: 'Invalid Events',
url: 'https://example.com/invalid',
events: 'not-json',
status: 1,
max_retries: 3,
timeout_sec: 10,
created_by: 3,
created_at: '2026-03-27 20:10:00',
updated_at: '2026-03-27 20:10:00',
},
])
getMock.mockResolvedValue({
data: [
{
id: 1,
name: 'String Events',
url: 'https://example.com/string',
events: '["user.registered"]',
status: 1,
max_retries: 3,
timeout_sec: 10,
created_by: 1,
created_at: '2026-03-27 20:00:00',
updated_at: '2026-03-27 20:00:00',
},
{
id: 2,
name: 'Array Events',
url: 'https://example.com/array',
events: ['user.login'],
status: 0,
max_retries: 3,
timeout_sec: 10,
created_by: 2,
created_at: '2026-03-27 20:05:00',
updated_at: '2026-03-27 20:05:00',
},
{
id: 3,
name: 'Invalid Events',
url: 'https://example.com/invalid',
events: 'not-json',
status: 1,
max_retries: 3,
timeout_sec: 10,
created_by: 3,
created_at: '2026-03-27 20:10:00',
updated_at: '2026-03-27 20:10:00',
},
],
total: 3,
page: 1,
page_size: 20,
})
const { listWebhooks } = await import('./webhooks')
const result = await listWebhooks({ keyword: 'ignored' })