diff --git a/apps/web/src/api/ai/analysis.test.ts b/apps/web/src/api/ai/analysis.test.ts new file mode 100644 index 0000000..f2ec95c --- /dev/null +++ b/apps/web/src/api/ai/analysis.test.ts @@ -0,0 +1,127 @@ +/** + * AI 模块 API 契约测试(analysis + prompts + suggestions + usage) + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { analysisApi } from './analysis' +import { promptApi } from './prompts' +import { suggestionApi } from './suggestions' +import { usageApi } from './usage' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('analysisApi', () => { + const fakeRes = { data: { data: {} } } + + it('list 应调用 GET /ai/analysis/history 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await analysisApi.list({ patient_id: 'p-001', analysis_type: 'lab-report', page: 1, page_size: 10 }) + + expect(mockGet).toHaveBeenCalledWith('/ai/analysis/history', { + params: { patient_id: 'p-001', analysis_type: 'lab-report', page: 1, page_size: 10 }, + }) + }) + + it('get 应调用 GET /ai/analysis/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await analysisApi.get('ana-001') + + expect(mockGet).toHaveBeenCalledWith('/ai/analysis/ana-001') + }) +}) + +describe('promptApi', () => { + const fakeRes = { data: { data: {} } } + + it('list 应调用 GET /ai/prompts 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await promptApi.list({ category: 'analysis', page: 1, page_size: 10 }) + + expect(mockGet).toHaveBeenCalledWith('/ai/prompts', { + params: { category: 'analysis', page: 1, page_size: 10 }, + }) + }) + + it('create 应调用 POST /ai/prompts 并传递请求体', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '化验解读', system_prompt: '你是专业医生', user_prompt_template: '解读: {report}', model_config: {}, category: 'analysis' } + await promptApi.create(req) + + expect(mockPost).toHaveBeenCalledWith('/ai/prompts', req) + }) + + it('activate 应调用 POST /ai/prompts/:id/activate', async () => { + mockPost.mockResolvedValue(fakeRes) + await promptApi.activate('prompt-001') + + expect(mockPost).toHaveBeenCalledWith('/ai/prompts/prompt-001/activate') + }) + + it('rollback 应调用 POST /ai/prompts/:id/rollback', async () => { + mockPost.mockResolvedValue(fakeRes) + await promptApi.rollback('prompt-001') + + expect(mockPost).toHaveBeenCalledWith('/ai/prompts/prompt-001/rollback') + }) +}) + +describe('suggestionApi', () => { + const fakeRes = { data: { data: {} } } + + it('list 应调用 GET /ai/suggestions 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await suggestionApi.list({ analysis_id: 'ana-001', status: 'pending' }) + + expect(mockGet).toHaveBeenCalledWith('/ai/suggestions', { + params: { analysis_id: 'ana-001', status: 'pending' }, + }) + }) + + it('approve 应调用 POST /ai/suggestions/:id/approve 并传递 action', async () => { + mockPost.mockResolvedValue(fakeRes) + await suggestionApi.approve('sug-001', 'approve') + + expect(mockPost).toHaveBeenCalledWith('/ai/suggestions/sug-001/approve', { action: 'approve' }) + }) + + it('getComparison 应调用 GET /ai/suggestions/:id/comparison', async () => { + mockGet.mockResolvedValue(fakeRes) + await suggestionApi.getComparison('sug-001') + + expect(mockGet).toHaveBeenCalledWith('/ai/suggestions/sug-001/comparison') + }) +}) + +describe('usageApi', () => { + const fakeRes = { data: { data: {} } } + + it('overview 应调用 GET /ai/usage/overview', async () => { + mockGet.mockResolvedValue(fakeRes) + await usageApi.overview() + + expect(mockGet).toHaveBeenCalledWith('/ai/usage/overview') + }) + + it('byType 应调用 GET /ai/usage/by-type', async () => { + mockGet.mockResolvedValue(fakeRes) + await usageApi.byType() + + expect(mockGet).toHaveBeenCalledWith('/ai/usage/by-type') + }) +}) diff --git a/apps/web/src/api/auditLogs.test.ts b/apps/web/src/api/auditLogs.test.ts new file mode 100644 index 0000000..1a71084 --- /dev/null +++ b/apps/web/src/api/auditLogs.test.ts @@ -0,0 +1,51 @@ +/** + * auditLogs API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('./client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import * as auditLogsApi from './auditLogs' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('auditLogs API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listAuditLogs 应调用 GET /audit-logs 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await auditLogsApi.listAuditLogs({ resource_type: 'user', user_id: 'u-001', page: 1, page_size: 10 }) + + expect(mockGet).toHaveBeenCalledWith('/audit-logs', { + params: expect.objectContaining({ + resource_type: 'user', + user_id: 'u-001', + page: 1, + page_size: 10, + }), + }) + }) + + it('listAuditLogs 默认应传 page=1 page_size=20', async () => { + mockGet.mockResolvedValue(fakeRes) + await auditLogsApi.listAuditLogs() + + expect(mockGet).toHaveBeenCalledWith('/audit-logs', { + params: expect.objectContaining({ page: 1, page_size: 20 }), + }) + }) +}) diff --git a/apps/web/src/api/auth.test.ts b/apps/web/src/api/auth.test.ts new file mode 100644 index 0000000..5a68920 --- /dev/null +++ b/apps/web/src/api/auth.test.ts @@ -0,0 +1,55 @@ +/** + * auth API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('./client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import * as authApi from './auth' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('auth API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('login 应调用 POST /auth/login 并传递用户名密码', async () => { + mockPost.mockResolvedValue(fakeRes) + await authApi.login({ username: 'admin', password: '123456' }) + + expect(mockPost).toHaveBeenCalledWith('/auth/login', { + username: 'admin', + password: '123456', + }) + }) + + it('logout 应调用 POST /auth/logout', async () => { + mockPost.mockResolvedValue(undefined) + await authApi.logout() + + expect(mockPost).toHaveBeenCalledWith('/auth/logout') + }) + + it('changePassword 应调用 POST /auth/change-password', async () => { + mockPost.mockResolvedValue(undefined) + await authApi.changePassword('oldPass', 'newPass') + + expect(mockPost).toHaveBeenCalledWith('/auth/change-password', { + current_password: 'oldPass', + new_password: 'newPass', + }) + }) +}) diff --git a/apps/web/src/api/config-modules.test.ts b/apps/web/src/api/config-modules.test.ts new file mode 100644 index 0000000..f051af7 --- /dev/null +++ b/apps/web/src/api/config-modules.test.ts @@ -0,0 +1,197 @@ +/** + * config-modules API 契约测试(menus + settings + languages + numberingRules + themes) + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('./client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import * as menusApi from './menus' +import * as settingsApi from './settings' +import * as languagesApi from './languages' +import * as numberingApi from './numberingRules' +import * as themesApi from './themes' + +beforeEach(() => { + vi.clearAllMocks() +}) + +// ============================================================ +// menus +// ============================================================ +describe('menus API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('getMenus 应调用 GET /config/menus', async () => { + mockGet.mockResolvedValue(fakeRes) + await menusApi.getMenus() + + expect(mockGet).toHaveBeenCalledWith('/config/menus') + }) + + it('getMenusForUser 应调用 GET /menus/user', async () => { + mockGet.mockResolvedValue(fakeRes) + await menusApi.getMenusForUser() + + expect(mockGet).toHaveBeenCalledWith('/menus/user') + }) + + it('batchSaveMenus 应调用 PUT /config/menus 并传递 menus 数组', async () => { + mockPut.mockResolvedValue(undefined) + const menus = [{ title: '仪表盘', path: '/dashboard' }] + await menusApi.batchSaveMenus(menus) + + expect(mockPut).toHaveBeenCalledWith('/config/menus', { menus }) + }) + + it('createMenu 应调用 POST /config/menus', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { title: '新菜单', path: '/new', sort_order: 10 } + await menusApi.createMenu(req) + + expect(mockPost).toHaveBeenCalledWith('/config/menus', req) + }) + + it('deleteMenu 应调用 DELETE /config/menus/:id 并在 body 传递 version', async () => { + mockDelete.mockResolvedValue(undefined) + await menusApi.deleteMenu('menu-001', 3) + + expect(mockDelete).toHaveBeenCalledWith('/config/menus/menu-001', { data: { version: 3 } }) + }) +}) + +// ============================================================ +// settings +// ============================================================ +describe('settings API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('getSetting 应调用 GET /config/settings/:key 并传递 scope 参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await settingsApi.getSetting('site.name', 'global', 'org-001') + + expect(mockGet).toHaveBeenCalledWith('/config/settings/site.name', { + params: { scope: 'global', scope_id: 'org-001' }, + }) + }) + + it('updateSetting 应调用 PUT /config/settings/:key', async () => { + mockPut.mockResolvedValue(fakeRes) + await settingsApi.updateSetting('site.name', '新名称', 1) + + expect(mockPut).toHaveBeenCalledWith('/config/settings/site.name', { + setting_value: '新名称', + version: 1, + }) + }) + + it('deleteSetting 应调用 DELETE /config/settings/:key', async () => { + mockDelete.mockResolvedValue(undefined) + await settingsApi.deleteSetting('site.name', 2) + + expect(mockDelete).toHaveBeenCalledWith('/config/settings/site.name', { data: { version: 2 } }) + }) +}) + +// ============================================================ +// languages +// ============================================================ +describe('languages API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listLanguages 应调用 GET /config/languages', async () => { + mockGet.mockResolvedValue(fakeRes) + await languagesApi.listLanguages() + + expect(mockGet).toHaveBeenCalledWith('/config/languages') + }) + + it('updateLanguage 应调用 PUT /config/languages/:code', async () => { + mockPut.mockResolvedValue(fakeRes) + await languagesApi.updateLanguage('zh-CN', { is_active: true, name: '简体中文' }) + + expect(mockPut).toHaveBeenCalledWith('/config/languages/zh-CN', { + is_active: true, + name: '简体中文', + }) + }) +}) + +// ============================================================ +// numberingRules +// ============================================================ +describe('numberingRules API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listNumberingRules 应调用 GET /config/numbering-rules', async () => { + mockGet.mockResolvedValue(fakeRes) + await numberingApi.listNumberingRules(1, 10) + + expect(mockGet).toHaveBeenCalledWith('/config/numbering-rules', { + params: { page: 1, page_size: 10 }, + }) + }) + + it('createNumberingRule 应调用 POST /config/numbering-rules', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '患者编号', code: 'patient', prefix: 'P', seq_length: 6 } + await numberingApi.createNumberingRule(req) + + expect(mockPost).toHaveBeenCalledWith('/config/numbering-rules', req) + }) + + it('updateNumberingRule 应调用 PUT /config/numbering-rules/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { prefix: 'HMS', version: 1 } + await numberingApi.updateNumberingRule('nr-001', req) + + expect(mockPut).toHaveBeenCalledWith('/config/numbering-rules/nr-001', req) + }) + + it('generateNumber 应调用 POST /config/numbering-rules/:id/generate', async () => { + mockPost.mockResolvedValue(fakeRes) + await numberingApi.generateNumber('nr-001') + + expect(mockPost).toHaveBeenCalledWith('/config/numbering-rules/nr-001/generate') + }) + + it('deleteNumberingRule 应调用 DELETE /config/numbering-rules/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await numberingApi.deleteNumberingRule('nr-001', 1) + + expect(mockDelete).toHaveBeenCalledWith('/config/numbering-rules/nr-001', { data: { version: 1 } }) + }) +}) + +// ============================================================ +// themes +// ============================================================ +describe('themes API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('getTheme 应调用 GET /config/themes', async () => { + mockGet.mockResolvedValue(fakeRes) + await themesApi.getTheme() + + expect(mockGet).toHaveBeenCalledWith('/config/themes') + }) + + it('updateTheme 应调用 PUT /config/themes', async () => { + mockPut.mockResolvedValue(fakeRes) + const theme = { primary_color: '#1890ff', brand_name: 'HMS' } + await themesApi.updateTheme(theme) + + expect(mockPut).toHaveBeenCalledWith('/config/themes', theme) + }) +}) diff --git a/apps/web/src/api/dictionaries.test.ts b/apps/web/src/api/dictionaries.test.ts new file mode 100644 index 0000000..a0000a6 --- /dev/null +++ b/apps/web/src/api/dictionaries.test.ts @@ -0,0 +1,96 @@ +/** + * dictionaries API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('./client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import * as dictApi from './dictionaries' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('dictionaries API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listDictionaries 应调用 GET /config/dictionaries 并传递分页参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await dictApi.listDictionaries(1, 10) + + expect(mockGet).toHaveBeenCalledWith('/config/dictionaries', { + params: { page: 1, page_size: 10 }, + }) + }) + + it('createDictionary 应调用 POST /config/dictionaries', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '性别', code: 'gender', description: '性别字典' } + await dictApi.createDictionary(req) + + expect(mockPost).toHaveBeenCalledWith('/config/dictionaries', req) + }) + + it('updateDictionary 应调用 PUT /config/dictionaries/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { name: '性别(更新)', version: 1 } + await dictApi.updateDictionary('dict-001', req) + + expect(mockPut).toHaveBeenCalledWith('/config/dictionaries/dict-001', req) + }) + + it('deleteDictionary 应调用 DELETE /config/dictionaries/:id 并在 body 传递 version', async () => { + mockDelete.mockResolvedValue(undefined) + await dictApi.deleteDictionary('dict-001', 2) + + expect(mockDelete).toHaveBeenCalledWith('/config/dictionaries/dict-001', { + data: { version: 2 }, + }) + }) + + it('listItemsByCode 应调用 GET /config/dictionaries/items 并传递 code 参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await dictApi.listItemsByCode('gender') + + expect(mockGet).toHaveBeenCalledWith('/config/dictionaries/items', { + params: { code: 'gender' }, + }) + }) + + it('createDictionaryItem 应调用 POST /config/dictionaries/:id/items', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { label: '男', value: 'male', sort_order: 1 } + await dictApi.createDictionaryItem('dict-001', req) + + expect(mockPost).toHaveBeenCalledWith('/config/dictionaries/dict-001/items', req) + }) + + it('updateDictionaryItem 应调用 PUT /config/dictionaries/:dictId/items/:itemId', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { label: '女', version: 1 } + await dictApi.updateDictionaryItem('dict-001', 'item-001', req) + + expect(mockPut).toHaveBeenCalledWith('/config/dictionaries/dict-001/items/item-001', req) + }) + + it('deleteDictionaryItem 应调用 DELETE /config/dictionaries/:dictId/items/:itemId', async () => { + mockDelete.mockResolvedValue(undefined) + await dictApi.deleteDictionaryItem('dict-001', 'item-001', 1) + + expect(mockDelete).toHaveBeenCalledWith('/config/dictionaries/dict-001/items/item-001', { + data: { version: 1 }, + }) + }) +}) diff --git a/apps/web/src/api/health/alerts.test.ts b/apps/web/src/api/health/alerts.test.ts new file mode 100644 index 0000000..716bbfc --- /dev/null +++ b/apps/web/src/api/health/alerts.test.ts @@ -0,0 +1,100 @@ +/** + * alerts API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { alertApi, alertRuleApi } from './alerts' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('alertApi', () => { + const fakeRes = { data: { data: {} } } + + it('list 应调用 GET /health/alerts 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await alertApi.list({ patient_id: 'p-001', status: 'active', page: 1, page_size: 20 }) + + expect(mockGet).toHaveBeenCalledWith('/health/alerts', { + params: { patient_id: 'p-001', status: 'active', page: 1, page_size: 20 }, + }) + }) + + it('acknowledge 应调用 PUT /health/alerts/:id/acknowledge 并传递 version', async () => { + mockPut.mockResolvedValue(fakeRes) + await alertApi.acknowledge('a-001', 2) + + expect(mockPut).toHaveBeenCalledWith('/health/alerts/a-001/acknowledge', { version: 2 }) + }) + + it('dismiss 应调用 PUT /health/alerts/:id/dismiss', async () => { + mockPut.mockResolvedValue(fakeRes) + await alertApi.dismiss('a-001', 1) + + expect(mockPut).toHaveBeenCalledWith('/health/alerts/a-001/dismiss', { version: 1 }) + }) + + it('resolve 应调用 PUT /health/alerts/:id/resolve', async () => { + mockPut.mockResolvedValue(fakeRes) + await alertApi.resolve('a-001', 3) + + expect(mockPut).toHaveBeenCalledWith('/health/alerts/a-001/resolve', { version: 3 }) + }) +}) + +describe('alertRuleApi', () => { + const fakeRes = { data: { data: {} } } + + it('list 应调用 GET /health/alert-rules 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await alertRuleApi.list({ device_type: 'blood_pressure', page: 1, page_size: 10 }) + + expect(mockGet).toHaveBeenCalledWith('/health/alert-rules', { + params: { device_type: 'blood_pressure', page: 1, page_size: 10 }, + }) + }) + + it('create 应调用 POST /health/alert-rules 并传递请求体', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { + name: '血压偏高告警', + device_type: 'blood_pressure', + condition_type: 'threshold', + condition_params: { field: 'systolic', operator: '>', value: 140 }, + severity: 'high', + } + await alertRuleApi.create(req) + + expect(mockPost).toHaveBeenCalledWith('/health/alert-rules', req) + }) + + it('update 应调用 PUT /health/alert-rules/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { severity: 'critical', version: 1 } + await alertRuleApi.update('rule-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/alert-rules/rule-001', req) + }) + + it('deactivate 应调用 PUT /health/alert-rules/:id/deactivate', async () => { + mockPut.mockResolvedValue(fakeRes) + await alertRuleApi.deactivate('rule-001', 2) + + expect(mockPut).toHaveBeenCalledWith('/health/alert-rules/rule-001/deactivate', { version: 2 }) + }) +}) diff --git a/apps/web/src/api/health/appointments.test.ts b/apps/web/src/api/health/appointments.test.ts new file mode 100644 index 0000000..49fc27d --- /dev/null +++ b/apps/web/src/api/health/appointments.test.ts @@ -0,0 +1,106 @@ +/** + * appointments API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { appointmentApi } from './appointments' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('appointmentApi', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('list 应调用 GET /health/appointments 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await appointmentApi.list({ page: 1, page_size: 20, status: 'confirmed', doctor_id: 'd-001' }) + + expect(mockGet).toHaveBeenCalledWith('/health/appointments', { + params: { page: 1, page_size: 20, status: 'confirmed', doctor_id: 'd-001' }, + }) + }) + + it('get 应调用 GET /health/appointments/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await appointmentApi.get('appt-001') + + expect(mockGet).toHaveBeenCalledWith('/health/appointments/appt-001') + }) + + it('create 应调用 POST /health/appointments 并传递请求体', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { + patient_id: 'p-001', + doctor_id: 'd-001', + appointment_date: '2026-05-10', + start_time: '09:00', + end_time: '09:30', + } + await appointmentApi.create(req) + + expect(mockPost).toHaveBeenCalledWith('/health/appointments', req) + }) + + it('updateStatus 应调用 PUT /health/appointments/:id/status', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { status: 'cancelled', cancel_reason: '时间冲突', version: 2 } + await appointmentApi.updateStatus('appt-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/appointments/appt-001/status', req) + }) + + it('listSchedules 应调用 GET /health/doctor-schedules 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await appointmentApi.listSchedules({ doctor_id: 'd-001', date: '2026-05-10' }) + + expect(mockGet).toHaveBeenCalledWith('/health/doctor-schedules', { + params: { doctor_id: 'd-001', date: '2026-05-10' }, + }) + }) + + it('createSchedule 应调用 POST /health/doctor-schedules', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { + doctor_id: 'd-001', + schedule_date: '2026-05-10', + start_time: '08:00', + end_time: '12:00', + max_appointments: 10, + } + await appointmentApi.createSchedule(req) + + expect(mockPost).toHaveBeenCalledWith('/health/doctor-schedules', req) + }) + + it('updateSchedule 应调用 PUT /health/doctor-schedules/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { max_appointments: 15, version: 1 } + await appointmentApi.updateSchedule('sch-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/doctor-schedules/sch-001', req) + }) + + it('calendar 应调用 GET /health/doctor-schedules/calendar', async () => { + mockGet.mockResolvedValue(fakeRes) + await appointmentApi.calendar({ start_date: '2026-05-01', end_date: '2026-05-31', doctor_id: 'd-001' }) + + expect(mockGet).toHaveBeenCalledWith('/health/doctor-schedules/calendar', { + params: { start_date: '2026-05-01', end_date: '2026-05-31', doctor_id: 'd-001' }, + }) + }) +}) diff --git a/apps/web/src/api/health/articles.test.ts b/apps/web/src/api/health/articles.test.ts new file mode 100644 index 0000000..e9d4c35 --- /dev/null +++ b/apps/web/src/api/health/articles.test.ts @@ -0,0 +1,173 @@ +/** + * articles API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { articleApi, articleCategoryApi, articleTagApi } from './articles' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('articleApi', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('list 应调用 GET /health/articles 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await articleApi.list({ page: 1, page_size: 10, status: 'published', category_id: 'cat-001' }) + + expect(mockGet).toHaveBeenCalledWith('/health/articles', { + params: { page: 1, page_size: 10, status: 'published', category_id: 'cat-001' }, + }) + }) + + it('get 应调用 GET /health/articles/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await articleApi.get('art-001') + + expect(mockGet).toHaveBeenCalledWith('/health/articles/art-001') + }) + + it('create 应调用 POST /health/articles', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { title: '健康饮食指南', content: '正文内容', content_type: 'markdown' } + await articleApi.create(req) + + expect(mockPost).toHaveBeenCalledWith('/health/articles', req) + }) + + it('update 应调用 PUT /health/articles/:id 并传递请求体', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { title: '健康饮食指南(修订)', version: 1 } + await articleApi.update('art-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/articles/art-001', req) + }) + + it('delete 应调用 DELETE /health/articles/:id', async () => { + mockDelete.mockResolvedValue({ data: { success: true, data: null } }) + await articleApi.delete('art-001') + + expect(mockDelete).toHaveBeenCalledWith('/health/articles/art-001') + }) + + it('submit 应调用 POST /health/articles/:id/submit 并传递 version', async () => { + mockPost.mockResolvedValue(fakeRes) + await articleApi.submit('art-001', 2) + + expect(mockPost).toHaveBeenCalledWith('/health/articles/art-001/submit', { version: 2 }) + }) + + it('approve 应调用 POST /health/articles/:id/approve', async () => { + mockPost.mockResolvedValue(fakeRes) + await articleApi.approve('art-001', 2) + + expect(mockPost).toHaveBeenCalledWith('/health/articles/art-001/approve', { version: 2 }) + }) + + it('reject 应调用 POST /health/articles/:id/reject 并传递 review_note', async () => { + mockPost.mockResolvedValue(fakeRes) + await articleApi.reject('art-001', 2, '内容需要修改') + + expect(mockPost).toHaveBeenCalledWith('/health/articles/art-001/reject', { + version: 2, + review_note: '内容需要修改', + }) + }) + + it('unpublish 应调用 POST /health/articles/:id/unpublish', async () => { + mockPost.mockResolvedValue(fakeRes) + await articleApi.unpublish('art-001', 3) + + expect(mockPost).toHaveBeenCalledWith('/health/articles/art-001/unpublish', { version: 3 }) + }) + + it('view 应调用 POST /health/articles/:id/view', async () => { + mockPost.mockResolvedValue(fakeRes) + await articleApi.view('art-001') + + expect(mockPost).toHaveBeenCalledWith('/health/articles/art-001/view') + }) +}) + +describe('articleCategoryApi', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('list 应调用 GET /health/article-categories', async () => { + mockGet.mockResolvedValue(fakeRes) + await articleCategoryApi.list() + + expect(mockGet).toHaveBeenCalledWith('/health/article-categories') + }) + + it('create 应调用 POST /health/article-categories', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '营养健康', sort_order: 1 } + await articleCategoryApi.create(req) + + expect(mockPost).toHaveBeenCalledWith('/health/article-categories', req) + }) + + it('update 应调用 PUT /health/article-categories/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { name: '营养健康(更新)' } + await articleCategoryApi.update('cat-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/article-categories/cat-001', req) + }) + + it('delete 应调用 DELETE /health/article-categories/:id', async () => { + mockDelete.mockResolvedValue({ data: { success: true, data: null } }) + await articleCategoryApi.delete('cat-001') + + expect(mockDelete).toHaveBeenCalledWith('/health/article-categories/cat-001') + }) +}) + +describe('articleTagApi', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('list 应调用 GET /health/article-tags', async () => { + mockGet.mockResolvedValue(fakeRes) + await articleTagApi.list() + + expect(mockGet).toHaveBeenCalledWith('/health/article-tags') + }) + + it('create 应调用 POST /health/article-tags', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '高血压', color: '#ff0000' } + await articleTagApi.create(req) + + expect(mockPost).toHaveBeenCalledWith('/health/article-tags', req) + }) + + it('update 应调用 PUT /health/article-tags/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { name: '高血压管理', version: 1 } + await articleTagApi.update('tag-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/article-tags/tag-001', req) + }) + + it('delete 应调用 DELETE /health/article-tags/:id 并在 body 传递 version', async () => { + mockDelete.mockResolvedValue({ data: { success: true, data: null } }) + await articleTagApi.delete('tag-001', 2) + + expect(mockDelete).toHaveBeenCalledWith('/health/article-tags/tag-001', { data: { version: 2 } }) + }) +}) diff --git a/apps/web/src/api/health/consultations.test.ts b/apps/web/src/api/health/consultations.test.ts new file mode 100644 index 0000000..2936f7f --- /dev/null +++ b/apps/web/src/api/health/consultations.test.ts @@ -0,0 +1,76 @@ +/** + * consultations API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { consultationApi } from './consultations' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('consultationApi', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listSessions 应调用 GET /health/consultation-sessions 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await consultationApi.listSessions({ page: 1, page_size: 20, status: 'active', patient_id: 'p-001' }) + + expect(mockGet).toHaveBeenCalledWith('/health/consultation-sessions', { + params: { page: 1, page_size: 20, status: 'active', patient_id: 'p-001' }, + }) + }) + + it('createSession 应调用 POST /health/consultation-sessions', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { patient_id: 'p-001', doctor_id: 'd-001', consultation_type: 'online' } + await consultationApi.createSession(req) + + expect(mockPost).toHaveBeenCalledWith('/health/consultation-sessions', req) + }) + + it('getSession 应调用 GET /health/consultation-sessions/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await consultationApi.getSession('sess-001') + + expect(mockGet).toHaveBeenCalledWith('/health/consultation-sessions/sess-001') + }) + + it('closeSession 应调用 PUT /health/consultation-sessions/:id/close', async () => { + mockPut.mockResolvedValue(fakeRes) + await consultationApi.closeSession('sess-001', { version: 1 }) + + expect(mockPut).toHaveBeenCalledWith('/health/consultation-sessions/sess-001/close', { version: 1 }) + }) + + it('listMessages 应调用 GET /health/consultation-sessions/:id/messages 并传递分页参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await consultationApi.listMessages('sess-001', { page: 2, page_size: 50, after_id: 'msg-100' }) + + expect(mockGet).toHaveBeenCalledWith('/health/consultation-sessions/sess-001/messages', { + params: { page: 2, page_size: 50, after_id: 'msg-100' }, + }) + }) + + it('createMessage 应调用 POST /health/consultation-messages', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { session_id: 'sess-001', content_type: 'text', content: '你好' } + await consultationApi.createMessage(req) + + expect(mockPost).toHaveBeenCalledWith('/health/consultation-messages', req) + }) +}) diff --git a/apps/web/src/api/health/dashboard.test.ts b/apps/web/src/api/health/dashboard.test.ts new file mode 100644 index 0000000..a58b82d --- /dev/null +++ b/apps/web/src/api/health/dashboard.test.ts @@ -0,0 +1,105 @@ +/** + * dashboard + actionInbox API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { dashboardApi } from './dashboard' +import { actionInboxApi } from './actionInbox' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('dashboardApi', () => { + const fakeRes = { data: { data: {} } } + + it('getSystemHealth 应调用 GET /health/admin/system-health', async () => { + mockGet.mockResolvedValue(fakeRes) + await dashboardApi.getSystemHealth() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/system-health') + }) + + it('getUserActivity 应调用 GET /health/admin/user-activity', async () => { + mockGet.mockResolvedValue(fakeRes) + await dashboardApi.getUserActivity() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/user-activity') + }) + + it('getModuleStatus 应调用 GET /health/admin/modules', async () => { + mockGet.mockResolvedValue(fakeRes) + await dashboardApi.getModuleStatus() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/modules') + }) + + it('getPointsRecentActivity 应调用 GET /health/points/recent-activity', async () => { + mockGet.mockResolvedValue(fakeRes) + await dashboardApi.getPointsRecentActivity() + + expect(mockGet).toHaveBeenCalledWith('/health/points/recent-activity') + }) + + it('getArticleStats 应调用 GET /health/articles/stats', async () => { + mockGet.mockResolvedValue(fakeRes) + await dashboardApi.getArticleStats() + + expect(mockGet).toHaveBeenCalledWith('/health/articles/stats') + }) +}) + +describe('actionInboxApi', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('list 应调用 GET /health/action-inbox 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await actionInboxApi.list({ status: 'pending', type: 'alert', page: 1, page_size: 20 }) + + expect(mockGet).toHaveBeenCalledWith('/health/action-inbox', { + params: { status: 'pending', type: 'alert', page: 1, page_size: 20 }, + }) + }) + + it('getThread 应调用 GET /health/action-inbox/:sourceRef/thread', async () => { + mockGet.mockResolvedValue(fakeRes) + await actionInboxApi.getThread('ref-001') + + expect(mockGet).toHaveBeenCalledWith('/health/action-inbox/ref-001/thread') + }) + + it('getThread 应对特殊字符 URL 编码', async () => { + mockGet.mockResolvedValue(fakeRes) + await actionInboxApi.getThread('ref/with:special') + + expect(mockGet).toHaveBeenCalledWith('/health/action-inbox/ref%2Fwith%3Aspecial/thread') + }) + + it('stats 应调用 GET /health/action-inbox/stats', async () => { + mockGet.mockResolvedValue(fakeRes) + await actionInboxApi.stats() + + expect(mockGet).toHaveBeenCalledWith('/health/action-inbox/stats') + }) + + it('team 应调用 GET /health/action-inbox/team', async () => { + mockGet.mockResolvedValue(fakeRes) + await actionInboxApi.team() + + expect(mockGet).toHaveBeenCalledWith('/health/action-inbox/team') + }) +}) diff --git a/apps/web/src/api/health/deviceReadings.test.ts b/apps/web/src/api/health/deviceReadings.test.ts new file mode 100644 index 0000000..ae0b191 --- /dev/null +++ b/apps/web/src/api/health/deviceReadings.test.ts @@ -0,0 +1,82 @@ +/** + * deviceReadings + devices API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { deviceReadingApi } from './deviceReadings' +import { deviceApi } from './devices' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('deviceReadingApi', () => { + const fakeRes = { data: { data: {} } } + + it('batchCreate 应调用 POST /health/patients/:id/device-readings/batch', async () => { + mockPost.mockResolvedValue(fakeRes) + const data = { + device_id: 'dev-001', + readings: [ + { device_type: 'blood_pressure', values: { systolic: 130, diastolic: 85 }, measured_at: '2026-05-03T08:00:00Z' }, + ], + } + await deviceReadingApi.batchCreate('p-001', data) + + expect(mockPost).toHaveBeenCalledWith('/health/patients/p-001/device-readings/batch', data) + }) + + it('query 应调用 GET /health/patients/:id/device-readings 并剥离 patient_id', async () => { + mockGet.mockResolvedValue(fakeRes) + await deviceReadingApi.query({ patient_id: 'p-001', device_type: 'blood_pressure', hours: 24 }) + + expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/device-readings', { + params: { device_type: 'blood_pressure', hours: 24 }, + }) + }) + + it('queryHourly 应调用 GET /health/patients/:id/device-readings/hourly', async () => { + mockGet.mockResolvedValue(fakeRes) + await deviceReadingApi.queryHourly({ patient_id: 'p-001', device_type: 'blood_pressure', days: 7 }) + + expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/device-readings/hourly', { + params: { device_type: 'blood_pressure', days: 7 }, + }) + }) +}) + +describe('deviceApi', () => { + const fakeRes = { data: { data: {} } } + + it('listDevices 应调用 GET /health/devices 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await deviceApi.listDevices({ patient_id: 'p-001', device_type: 'blood_pressure', page: 1, page_size: 10 }) + + expect(mockGet).toHaveBeenCalledWith('/health/devices', { + params: { patient_id: 'p-001', device_type: 'blood_pressure', page: 1, page_size: 10 }, + }) + }) + + it('unbindDevice 应调用 DELETE /health/devices/:id 并在 body 传递 version', async () => { + mockDelete.mockResolvedValue(fakeRes) + await deviceApi.unbindDevice('dev-001', 2) + + expect(mockDelete).toHaveBeenCalledWith('/health/devices/dev-001', { + data: { version: 2 }, + }) + }) +}) diff --git a/apps/web/src/api/health/doctors.test.ts b/apps/web/src/api/health/doctors.test.ts new file mode 100644 index 0000000..aabdb7b --- /dev/null +++ b/apps/web/src/api/health/doctors.test.ts @@ -0,0 +1,67 @@ +/** + * doctors API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { doctorApi } from './doctors' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('doctorApi', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('list 应调用 GET /health/doctors 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await doctorApi.list({ page: 1, page_size: 10, search: '王', department: '内科' }) + + expect(mockGet).toHaveBeenCalledWith('/health/doctors', { + params: { page: 1, page_size: 10, search: '王', department: '内科' }, + }) + }) + + it('get 应调用 GET /health/doctors/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await doctorApi.get('d-001') + + expect(mockGet).toHaveBeenCalledWith('/health/doctors/d-001') + }) + + it('create 应调用 POST /health/doctors 并传递请求体', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '王医生', department: '内科', title: '主任医师' } + await doctorApi.create(req) + + expect(mockPost).toHaveBeenCalledWith('/health/doctors', req) + }) + + it('update 应调用 PUT /health/doctors/:id 并传递请求体含 version', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { title: '副主任医师', version: 1 } + await doctorApi.update('d-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/doctors/d-001', req) + }) + + it('delete 应调用 DELETE /health/doctors/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await doctorApi.delete('d-001') + + expect(mockDelete).toHaveBeenCalledWith('/health/doctors/d-001') + }) +}) diff --git a/apps/web/src/api/health/followUp.test.ts b/apps/web/src/api/health/followUp.test.ts new file mode 100644 index 0000000..3460c3d --- /dev/null +++ b/apps/web/src/api/health/followUp.test.ts @@ -0,0 +1,97 @@ +/** + * followUp API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { followUpApi } from './followUp' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('followUpApi - Tasks', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listTasks 应调用 GET /health/follow-up-tasks 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await followUpApi.listTasks({ page: 1, page_size: 20, patient_id: 'p-001', status: 'pending' }) + + expect(mockGet).toHaveBeenCalledWith('/health/follow-up-tasks', { + params: { page: 1, page_size: 20, patient_id: 'p-001', status: 'pending' }, + }) + }) + + it('getTask 应调用 GET /health/follow-up-tasks/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await followUpApi.getTask('task-001') + + expect(mockGet).toHaveBeenCalledWith('/health/follow-up-tasks/task-001') + }) + + it('createTask 应调用 POST /health/follow-up-tasks', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { patient_id: 'p-001', follow_up_type: 'phone', planned_date: '2026-05-10' } + await followUpApi.createTask(req) + + expect(mockPost).toHaveBeenCalledWith('/health/follow-up-tasks', req) + }) + + it('updateTask 应调用 PUT /health/follow-up-tasks/:id 并传递 version', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { status: 'completed', version: 1 } + await followUpApi.updateTask('task-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/follow-up-tasks/task-001', req) + }) + + it('deleteTask 应调用 DELETE /health/follow-up-tasks/:id 并在 body 传递 version', async () => { + mockDelete.mockResolvedValue(undefined) + await followUpApi.deleteTask('task-001', 2) + + expect(mockDelete).toHaveBeenCalledWith('/health/follow-up-tasks/task-001', { + data: { version: 2 }, + }) + }) +}) + +describe('followUpApi - Records', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listRecords 应调用 GET /health/follow-up-records 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await followUpApi.listRecords({ page: 1, page_size: 10, task_id: 'task-001' }) + + expect(mockGet).toHaveBeenCalledWith('/health/follow-up-records', { + params: { page: 1, page_size: 10, task_id: 'task-001' }, + }) + }) + + it('createRecord 应调用 POST /health/follow-up-tasks/:taskId/records 并注入 task_id', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { + executed_date: '2026-05-10', + result: '已完成', + patient_condition: '良好', + } + await followUpApi.createRecord('task-001', req) + + expect(mockPost).toHaveBeenCalledWith('/health/follow-up-tasks/task-001/records', { + ...req, + task_id: 'task-001', + }) + }) +}) diff --git a/apps/web/src/api/health/followUpTemplates.test.ts b/apps/web/src/api/health/followUpTemplates.test.ts new file mode 100644 index 0000000..637fb65 --- /dev/null +++ b/apps/web/src/api/health/followUpTemplates.test.ts @@ -0,0 +1,75 @@ +/** + * followUpTemplates API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { followUpTemplateApi } from './followUpTemplates' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('followUpTemplateApi', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('list 应调用 GET /health/follow-up-templates 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await followUpTemplateApi.list({ page: 1, page_size: 10, follow_up_type: 'phone', status: 'active' }) + + expect(mockGet).toHaveBeenCalledWith('/health/follow-up-templates', { + params: { page: 1, page_size: 10, follow_up_type: 'phone', status: 'active' }, + }) + }) + + it('get 应调用 GET /health/follow-up-templates/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await followUpTemplateApi.get('tpl-001') + + expect(mockGet).toHaveBeenCalledWith('/health/follow-up-templates/tpl-001') + }) + + it('create 应调用 POST /health/follow-up-templates 并传递请求体', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { + name: '电话随访模板', + follow_up_type: 'phone', + fields: [ + { label: '患者状态', field_key: 'patient_status', field_type: 'select', required: true, options: '良好,一般,较差' }, + ], + } + await followUpTemplateApi.create(req) + + expect(mockPost).toHaveBeenCalledWith('/health/follow-up-templates', req) + }) + + it('update 应调用 PUT /health/follow-up-templates/:id 并传递请求体', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { name: '更新后模板', status: 'active', version: 1 } + await followUpTemplateApi.update('tpl-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/follow-up-templates/tpl-001', req) + }) + + it('delete 应调用 DELETE /health/follow-up-templates/:id 并在 body 传递 version', async () => { + mockDelete.mockResolvedValue(undefined) + await followUpTemplateApi.delete('tpl-001', 2) + + expect(mockDelete).toHaveBeenCalledWith('/health/follow-up-templates/tpl-001', { + data: { version: 2 }, + }) + }) +}) diff --git a/apps/web/src/api/health/healthData.test.ts b/apps/web/src/api/health/healthData.test.ts new file mode 100644 index 0000000..1f9862e --- /dev/null +++ b/apps/web/src/api/health/healthData.test.ts @@ -0,0 +1,135 @@ +/** + * healthData API 契约测试(体征/化验报告/健康记录/趋势/日常监测) + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { healthDataApi } from './healthData' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('healthDataApi - Vital Signs', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listVitalSigns 应调用 GET /health/patients/:id/vital-signs 并传递分页', async () => { + mockGet.mockResolvedValue(fakeRes) + await healthDataApi.listVitalSigns('p-001', { page: 1, page_size: 10 }) + + expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/vital-signs', { + params: { page: 1, page_size: 10 }, + }) + }) + + it('createVitalSigns 应调用 POST /health/patients/:id/vital-signs', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { record_date: '2026-05-03', systolic_bp_morning: 120, diastolic_bp_morning: 80 } + await healthDataApi.createVitalSigns('p-001', req) + + expect(mockPost).toHaveBeenCalledWith('/health/patients/p-001/vital-signs', req) + }) + + it('updateVitalSigns 应调用 PUT /health/patients/:pid/vital-signs/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { systolic_bp_morning: 125, version: 1 } + await healthDataApi.updateVitalSigns('p-001', 'vs-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/patients/p-001/vital-signs/vs-001', req) + }) + + it('deleteVitalSigns 应调用 DELETE /health/patients/:pid/vital-signs/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await healthDataApi.deleteVitalSigns('p-001', 'vs-001') + + expect(mockDelete).toHaveBeenCalledWith('/health/patients/p-001/vital-signs/vs-001') + }) +}) + +describe('healthDataApi - Lab Reports', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listLabReports 应调用 GET /health/patients/:id/lab-reports', async () => { + mockGet.mockResolvedValue(fakeRes) + await healthDataApi.listLabReports('p-001', { page: 1, page_size: 10 }) + + expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/lab-reports', { + params: { page: 1, page_size: 10 }, + }) + }) + + it('createLabReport 应调用 POST /health/patients/:id/lab-reports', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { report_date: '2026-05-03', report_type: 'blood_test' } + await healthDataApi.createLabReport('p-001', req) + + expect(mockPost).toHaveBeenCalledWith('/health/patients/p-001/lab-reports', req) + }) + + it('reviewLabReport 应调用 PUT /health/patients/:pid/lab-reports/:rid/review', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { version: 1, doctor_notes: '指标正常' } + await healthDataApi.reviewLabReport('p-001', 'lr-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/patients/p-001/lab-reports/lr-001/review', req) + }) + + it('deleteLabReport 应调用 DELETE /health/patients/:pid/lab-reports/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await healthDataApi.deleteLabReport('p-001', 'lr-001') + + expect(mockDelete).toHaveBeenCalledWith('/health/patients/p-001/lab-reports/lr-001') + }) +}) + +describe('healthDataApi - Health Records', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listHealthRecords 应调用 GET /health/patients/:id/health-records', async () => { + mockGet.mockResolvedValue(fakeRes) + await healthDataApi.listHealthRecords('p-001', { page: 1, page_size: 10 }) + + expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/health-records', { + params: { page: 1, page_size: 10 }, + }) + }) + + it('createHealthRecord 应调用 POST /health/patients/:id/health-records', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { record_type: 'checkup', record_date: '2026-05-03', content: '体检结果正常' } + await healthDataApi.createHealthRecord('p-001', req) + + expect(mockPost).toHaveBeenCalledWith('/health/patients/p-001/health-records', req) + }) +}) + +describe('healthDataApi - Trends', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listTrends 应调用 GET /health/patients/:id/trends', async () => { + mockGet.mockResolvedValue(fakeRes) + await healthDataApi.listTrends('p-001') + + expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/trends') + }) + + it('getIndicatorTimeseries 应调用 GET /health/patients/:id/trends/:indicator 并编码', async () => { + mockGet.mockResolvedValue(fakeRes) + await healthDataApi.getIndicatorTimeseries('p-001', 'blood_pressure/systolic') + + expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/trends/blood_pressure%2Fsystolic') + }) +}) diff --git a/apps/web/src/api/health/patients.test.ts b/apps/web/src/api/health/patients.test.ts new file mode 100644 index 0000000..9654cc1 --- /dev/null +++ b/apps/web/src/api/health/patients.test.ts @@ -0,0 +1,126 @@ +/** + * patients API 契约测试 + * + * 验证 patientApi 各函数调用正确的 HTTP 方法、URL 路径和参数序列化。 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { patientApi } from './patients' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('patientApi', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('list 应调用 GET /health/patients 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await patientApi.list({ page: 1, page_size: 20, search: '张三', status: 'active' }) + + expect(mockGet).toHaveBeenCalledWith('/health/patients', { + params: { page: 1, page_size: 20, search: '张三', status: 'active' }, + }) + }) + + it('list 应支持 tag_id 过滤参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await patientApi.list({ tag_id: 'tag-001' }) + + expect(mockGet).toHaveBeenCalledWith('/health/patients', { + params: { tag_id: 'tag-001' }, + }) + }) + + it('get 应调用 GET /health/patients/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await patientApi.get('p-001') + + expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001') + }) + + it('create 应调用 POST /health/patients 并传递请求体', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '李四', gender: 'male', birth_date: '1990-01-01' } + await patientApi.create(req) + + expect(mockPost).toHaveBeenCalledWith('/health/patients', req) + }) + + it('update 应调用 PUT /health/patients/:id 并传递请求体含 version', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { name: '李四改', version: 2 } + await patientApi.update('p-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/patients/p-001', req) + }) + + it('delete 应调用 DELETE /health/patients/:id 并在 body 中传递 version', async () => { + mockDelete.mockResolvedValue(undefined) + await patientApi.delete('p-001', 3) + + expect(mockDelete).toHaveBeenCalledWith('/health/patients/p-001', { + data: { version: 3 }, + }) + }) + + it('manageTags 应调用 POST /health/patients/:id/tags 并传递 tag_ids', async () => { + mockPost.mockResolvedValue(undefined) + await patientApi.manageTags('p-001', ['tag-1', 'tag-2']) + + expect(mockPost).toHaveBeenCalledWith('/health/patients/p-001/tags', { + tag_ids: ['tag-1', 'tag-2'], + }) + }) + + it('listFamilyMembers 应调用 GET /health/patients/:id/family-members', async () => { + mockGet.mockResolvedValue(fakeRes) + await patientApi.listFamilyMembers('p-001') + + expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/family-members') + }) + + it('createFamilyMember 应调用 POST /health/patients/:id/family-members', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '家属A', relationship: 'spouse', phone: '13800138000' } + await patientApi.createFamilyMember('p-001', req) + + expect(mockPost).toHaveBeenCalledWith('/health/patients/p-001/family-members', req) + }) + + it('updateFamilyMember 应调用 PUT /health/patients/:pid/family-members/:mid', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { name: '家属A改', version: 1 } + await patientApi.updateFamilyMember('p-001', 'fm-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/patients/p-001/family-members/fm-001', req) + }) + + it('deleteFamilyMember 应调用 DELETE /health/patients/:pid/family-members/:mid', async () => { + mockDelete.mockResolvedValue(undefined) + await patientApi.deleteFamilyMember('p-001', 'fm-001') + + expect(mockDelete).toHaveBeenCalledWith('/health/patients/p-001/family-members/fm-001') + }) + + it('listTags 应调用 GET /health/patient-tags', async () => { + mockGet.mockResolvedValue(fakeRes) + await patientApi.listTags() + + expect(mockGet).toHaveBeenCalledWith('/health/patient-tags') + }) +}) diff --git a/apps/web/src/api/health/points.test.ts b/apps/web/src/api/health/points.test.ts new file mode 100644 index 0000000..a898c99 --- /dev/null +++ b/apps/web/src/api/health/points.test.ts @@ -0,0 +1,230 @@ +/** + * points API 契约测试(完整覆盖 pointsApi + pointsAdminApi) + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('../client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import { pointsApi, pointsAdminApi } from './points' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('pointsAdminApi', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('getPatientAccount 应调用 GET /health/admin/points/patients/:id/account', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsAdminApi.getPatientAccount('p-001') + + expect(mockGet).toHaveBeenCalledWith('/health/admin/points/patients/p-001/account') + }) + + it('listPatientTransactions 应调用 GET 并传递分页参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsAdminApi.listPatientTransactions('p-001', { page: 2, page_size: 15 }) + + expect(mockGet).toHaveBeenCalledWith('/health/admin/points/patients/p-001/transactions', { + params: { page: 2, page_size: 15 }, + }) + }) +}) + +describe('pointsApi - Rules', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listRules 应调用 GET /health/admin/points/rules', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsApi.listRules() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/points/rules') + }) + + it('createRule 应调用 POST /health/admin/points/rules', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { event_type: 'daily_checkin', name: '每日签到', points_value: 10, daily_cap: 1 } + await pointsApi.createRule(req) + + expect(mockPost).toHaveBeenCalledWith('/health/admin/points/rules', req) + }) + + it('updateRule 应调用 PUT /health/admin/points/rules/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { points_value: 20, version: 1 } + await pointsApi.updateRule('rule-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/admin/points/rules/rule-001', { + data: req, + version: req.version, + }) + }) + + it('deleteRule 应调用 DELETE /health/admin/points/rules/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await pointsApi.deleteRule('rule-001', 2) + + expect(mockDelete).toHaveBeenCalledWith('/health/admin/points/rules/rule-001', { + data: { version: 2 }, + }) + }) +}) + +describe('pointsApi - Products', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listProducts 应调用 GET /health/points/products', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsApi.listProducts() + + expect(mockGet).toHaveBeenCalledWith('/health/points/products', { params: undefined }) + }) + + it('createProduct 应调用 POST /health/admin/points/products', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '体检优惠券', product_type: 'service', points_cost: 500, stock: 100 } + await pointsApi.createProduct(req) + + expect(mockPost).toHaveBeenCalledWith('/health/admin/points/products', req) + }) + + it('updateProduct 应调用 PUT /health/admin/points/products/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { points_cost: 600, version: 1 } + await pointsApi.updateProduct('prod-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/admin/points/products/prod-001', { + data: req, + version: req.version, + }) + }) + + it('deleteProduct 应调用 DELETE /health/admin/points/products/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await pointsApi.deleteProduct('prod-001', 1) + + expect(mockDelete).toHaveBeenCalledWith('/health/admin/points/products/prod-001', { + data: { version: 1 }, + }) + }) +}) + +describe('pointsApi - Orders', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listOrders 应调用 GET /health/admin/points/orders', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsApi.listOrders() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/points/orders', { params: undefined }) + }) + + it('verifyOrder 应调用 POST /health/points/verify', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { qr_code: 'QR-123456' } + await pointsApi.verifyOrder(req) + + expect(mockPost).toHaveBeenCalledWith('/health/points/verify', req) + }) +}) + +describe('pointsApi - Offline Events', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listOfflineEvents 应调用 GET /health/admin/offline-events', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsApi.listOfflineEvents() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/offline-events', { params: undefined }) + }) + + it('createOfflineEvent 应调用 POST /health/admin/offline-events', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { title: '健康讲座', event_date: '2026-05-20', points_reward: 50 } + await pointsApi.createOfflineEvent(req) + + expect(mockPost).toHaveBeenCalledWith('/health/admin/offline-events', req) + }) + + it('updateOfflineEvent 应调用 PUT /health/admin/offline-events/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { title: '健康讲座(更新)', version: 1 } + await pointsApi.updateOfflineEvent('evt-001', req) + + expect(mockPut).toHaveBeenCalledWith('/health/admin/offline-events/evt-001', req) + }) + + it('deleteOfflineEvent 应调用 DELETE /health/admin/offline-events/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await pointsApi.deleteOfflineEvent('evt-001', 1) + + expect(mockDelete).toHaveBeenCalledWith('/health/admin/offline-events/evt-001', { + data: { version: 1 }, + }) + }) +}) + +describe('pointsApi - Statistics', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('getStatistics 应调用 GET /health/admin/points/statistics', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsApi.getStatistics() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/points/statistics') + }) + + it('getPatientStats 应调用 GET /health/admin/statistics/patients', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsApi.getPatientStats() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/statistics/patients') + }) + + it('getConsultationStats 应调用 GET /health/admin/statistics/consultations', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsApi.getConsultationStats() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/statistics/consultations') + }) + + it('getFollowUpStats 应调用 GET /health/admin/statistics/follow-ups', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsApi.getFollowUpStats() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/statistics/follow-ups') + }) + + it('getHealthDataStats 应调用 GET /health/admin/statistics/health-data', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsApi.getHealthDataStats() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/statistics/health-data') + }) + + it('getDialysisStats 应调用 GET /health/admin/statistics/dialysis', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsApi.getDialysisStats() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/statistics/dialysis') + }) + + it('getPersonalStats 应调用 GET /health/admin/statistics/personal-stats', async () => { + mockGet.mockResolvedValue(fakeRes) + await pointsApi.getPersonalStats() + + expect(mockGet).toHaveBeenCalledWith('/health/admin/statistics/personal-stats') + }) +}) diff --git a/apps/web/src/api/messages.test.ts b/apps/web/src/api/messages.test.ts new file mode 100644 index 0000000..b8ed9cd --- /dev/null +++ b/apps/web/src/api/messages.test.ts @@ -0,0 +1,100 @@ +/** + * messages + messageTemplates API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('./client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import * as messagesApi from './messages' +import * as templateApi from './messageTemplates' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('messages API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listMessages 应调用 GET /messages 并传递查询参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await messagesApi.listMessages({ page: 2, page_size: 10, is_read: false, priority: 'high' }) + + expect(mockGet).toHaveBeenCalledWith('/messages', { + params: expect.objectContaining({ + page: 2, + page_size: 10, + is_read: false, + priority: 'high', + }), + }) + }) + + it('getUnreadCount 应调用 GET /messages/unread-count', async () => { + mockGet.mockResolvedValue(fakeRes) + await messagesApi.getUnreadCount() + + expect(mockGet).toHaveBeenCalledWith('/messages/unread-count') + }) + + it('markRead 应调用 PUT /messages/:id/read', async () => { + mockPut.mockResolvedValue({ data: { success: true } }) + await messagesApi.markRead('msg-001') + + expect(mockPut).toHaveBeenCalledWith('/messages/msg-001/read') + }) + + it('markAllRead 应调用 PUT /messages/read-all', async () => { + mockPut.mockResolvedValue({ data: { success: true } }) + await messagesApi.markAllRead() + + expect(mockPut).toHaveBeenCalledWith('/messages/read-all') + }) + + it('deleteMessage 应调用 DELETE /messages/:id', async () => { + mockDelete.mockResolvedValue({ data: { success: true } }) + await messagesApi.deleteMessage('msg-001') + + expect(mockDelete).toHaveBeenCalledWith('/messages/msg-001') + }) + + it('sendMessage 应调用 POST /messages 并传递请求体', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { title: '通知', body: '内容', recipient_id: 'u-001' } + await messagesApi.sendMessage(req) + + expect(mockPost).toHaveBeenCalledWith('/messages', req) + }) +}) + +describe('messageTemplates API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listTemplates 应调用 GET /message-templates 并传递分页参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await templateApi.listTemplates(1, 10) + + expect(mockGet).toHaveBeenCalledWith('/message-templates', { + params: { page: 1, page_size: 10 }, + }) + }) + + it('createTemplate 应调用 POST /message-templates', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '预约提醒', code: 'appointment_reminder', title_template: '预约提醒', body_template: '您有预约' } + await templateApi.createTemplate(req) + + expect(mockPost).toHaveBeenCalledWith('/message-templates', req) + }) +}) diff --git a/apps/web/src/api/orgs.test.ts b/apps/web/src/api/orgs.test.ts new file mode 100644 index 0000000..d5718d7 --- /dev/null +++ b/apps/web/src/api/orgs.test.ts @@ -0,0 +1,126 @@ +/** + * orgs API 契约测试(组织/部门/岗位) + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('./client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import * as orgsApi from './orgs' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('organizations API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listOrgTree 应调用 GET /organizations', async () => { + mockGet.mockResolvedValue(fakeRes) + await orgsApi.listOrgTree() + + expect(mockGet).toHaveBeenCalledWith('/organizations') + }) + + it('createOrg 应调用 POST /organizations', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '总公司', code: 'HQ' } + await orgsApi.createOrg(req) + + expect(mockPost).toHaveBeenCalledWith('/organizations', req) + }) + + it('updateOrg 应调用 PUT /organizations/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { name: '改名', version: 1 } + await orgsApi.updateOrg('org-001', req) + + expect(mockPut).toHaveBeenCalledWith('/organizations/org-001', req) + }) + + it('deleteOrg 应调用 DELETE /organizations/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await orgsApi.deleteOrg('org-001') + + expect(mockDelete).toHaveBeenCalledWith('/organizations/org-001') + }) +}) + +describe('departments API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listDeptTree 应调用 GET /organizations/:orgId/departments', async () => { + mockGet.mockResolvedValue(fakeRes) + await orgsApi.listDeptTree('org-001') + + expect(mockGet).toHaveBeenCalledWith('/organizations/org-001/departments') + }) + + it('createDept 应调用 POST /organizations/:orgId/departments', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '内科', code: 'NK' } + await orgsApi.createDept('org-001', req) + + expect(mockPost).toHaveBeenCalledWith('/organizations/org-001/departments', req) + }) + + it('updateDept 应调用 PUT /departments/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { name: '内科(更新)', version: 1 } + await orgsApi.updateDept('dept-001', req) + + expect(mockPut).toHaveBeenCalledWith('/departments/dept-001', req) + }) + + it('deleteDept 应调用 DELETE /departments/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await orgsApi.deleteDept('dept-001') + + expect(mockDelete).toHaveBeenCalledWith('/departments/dept-001') + }) +}) + +describe('positions API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listPositions 应调用 GET /departments/:deptId/positions', async () => { + mockGet.mockResolvedValue(fakeRes) + await orgsApi.listPositions('dept-001') + + expect(mockGet).toHaveBeenCalledWith('/departments/dept-001/positions') + }) + + it('createPosition 应调用 POST /departments/:deptId/positions', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '主治医师', code: 'ZYS' } + await orgsApi.createPosition('dept-001', req) + + expect(mockPost).toHaveBeenCalledWith('/departments/dept-001/positions', req) + }) + + it('updatePosition 应调用 PUT /positions/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { name: '主任医师', version: 1 } + await orgsApi.updatePosition('pos-001', req) + + expect(mockPut).toHaveBeenCalledWith('/positions/pos-001', req) + }) + + it('deletePosition 应调用 DELETE /positions/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await orgsApi.deletePosition('pos-001') + + expect(mockDelete).toHaveBeenCalledWith('/positions/pos-001') + }) +}) diff --git a/apps/web/src/api/pluginData.test.ts b/apps/web/src/api/pluginData.test.ts new file mode 100644 index 0000000..378cefc --- /dev/null +++ b/apps/web/src/api/pluginData.test.ts @@ -0,0 +1,131 @@ +/** + * pluginData API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockPatch = vi.fn() +const mockDelete = vi.fn() + +vi.mock('./client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + patch: (...args: unknown[]) => mockPatch(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import * as pluginDataApi from './pluginData' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('pluginData CRUD', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listPluginData 应调用 GET /plugins/:pid/:entity 并传递分页和过滤参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await pluginDataApi.listPluginData('crm', 'customer', 1, 20, { + filter: { status: 'active' }, + search: '张', + sort_by: 'name', + sort_order: 'asc', + }) + + expect(mockGet).toHaveBeenCalledWith('/plugins/crm/customer', { + params: expect.objectContaining({ + page: '1', + page_size: '20', + search: '张', + sort_by: 'name', + sort_order: 'asc', + }), + }) + }) + + it('getPluginData 应调用 GET /plugins/:pid/:entity/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await pluginDataApi.getPluginData('crm', 'customer', 'rec-001') + + expect(mockGet).toHaveBeenCalledWith('/plugins/crm/customer/rec-001') + }) + + it('createPluginData 应调用 POST /plugins/:pid/:entity 并包裹 data', async () => { + mockPost.mockResolvedValue(fakeRes) + const recordData = { name: '客户A', phone: '13800138000' } + await pluginDataApi.createPluginData('crm', 'customer', recordData) + + expect(mockPost).toHaveBeenCalledWith('/plugins/crm/customer', { data: recordData }) + }) + + it('updatePluginData 应调用 PUT /plugins/:pid/:entity/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const recordData = { name: '客户A(更新)' } + await pluginDataApi.updatePluginData('crm', 'customer', 'rec-001', recordData, 2) + + expect(mockPut).toHaveBeenCalledWith('/plugins/crm/customer/rec-001', { + data: recordData, + version: 2, + }) + }) + + it('deletePluginData 应调用 DELETE /plugins/:pid/:entity/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await pluginDataApi.deletePluginData('crm', 'customer', 'rec-001') + + expect(mockDelete).toHaveBeenCalledWith('/plugins/crm/customer/rec-001') + }) +}) + +describe('pluginData 高级查询', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('countPluginData 应调用 GET /plugins/:pid/:entity/count', async () => { + mockGet.mockResolvedValue(fakeRes) + await pluginDataApi.countPluginData('crm', 'customer', { filter: { status: 'active' } }) + + expect(mockGet).toHaveBeenCalledWith('/plugins/crm/customer/count', { + params: expect.objectContaining({ + filter: '{"status":"active"}', + }), + }) + }) + + it('aggregatePluginData 应调用 GET /plugins/:pid/:entity/aggregate', async () => { + mockGet.mockResolvedValue(fakeRes) + await pluginDataApi.aggregatePluginData('crm', 'customer', 'status') + + expect(mockGet).toHaveBeenCalledWith('/plugins/crm/customer/aggregate', { + params: { group_by: 'status' }, + }) + }) + + it('batchPluginData 应调用 POST /plugins/:pid/:entity/batch', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { action: 'delete', ids: ['rec-1', 'rec-2'] } + await pluginDataApi.batchPluginData('crm', 'customer', req) + + expect(mockPost).toHaveBeenCalledWith('/plugins/crm/customer/batch', req) + }) + + it('resolveRefLabels 应调用 POST /plugins/:pid/:entity/resolve-labels', async () => { + mockPost.mockResolvedValue(fakeRes) + const fields = { customer_tag_id: ['tag-1', 'tag-2'] } + await pluginDataApi.resolveRefLabels('crm', 'customer', fields) + + expect(mockPost).toHaveBeenCalledWith('/plugins/crm/customer/resolve-labels', { fields }) + }) + + it('importPluginData 应调用 POST /plugins/:pid/:entity/import', async () => { + mockPost.mockResolvedValue(fakeRes) + const rows = [{ name: '客户A' }, { name: '客户B' }] + await pluginDataApi.importPluginData('crm', 'customer', rows) + + expect(mockPost).toHaveBeenCalledWith('/plugins/crm/customer/import', { rows }) + }) +}) diff --git a/apps/web/src/api/plugins.test.ts b/apps/web/src/api/plugins.test.ts new file mode 100644 index 0000000..cd5b6e8 --- /dev/null +++ b/apps/web/src/api/plugins.test.ts @@ -0,0 +1,132 @@ +/** + * plugins API 契约测试(插件管理 + 市场) + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('./client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import * as pluginsApi from './plugins' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('plugins management API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listPlugins 应调用 GET /admin/plugins 并传递分页和状态', async () => { + mockGet.mockResolvedValue(fakeRes) + await pluginsApi.listPlugins(1, 10, 'enabled') + + expect(mockGet).toHaveBeenCalledWith('/admin/plugins', { + params: { page: 1, page_size: 10, status: 'enabled' }, + }) + }) + + it('getPlugin 应调用 GET /admin/plugins/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await pluginsApi.getPlugin('plug-001') + + expect(mockGet).toHaveBeenCalledWith('/admin/plugins/plug-001') + }) + + it('installPlugin 应调用 POST /admin/plugins/:id/install', async () => { + mockPost.mockResolvedValue(fakeRes) + await pluginsApi.installPlugin('plug-001') + + expect(mockPost).toHaveBeenCalledWith('/admin/plugins/plug-001/install') + }) + + it('enablePlugin 应调用 POST /admin/plugins/:id/enable', async () => { + mockPost.mockResolvedValue(fakeRes) + await pluginsApi.enablePlugin('plug-001') + + expect(mockPost).toHaveBeenCalledWith('/admin/plugins/plug-001/enable') + }) + + it('disablePlugin 应调用 POST /admin/plugins/:id/disable', async () => { + mockPost.mockResolvedValue(fakeRes) + await pluginsApi.disablePlugin('plug-001') + + expect(mockPost).toHaveBeenCalledWith('/admin/plugins/plug-001/disable') + }) + + it('purgePlugin 应调用 DELETE /admin/plugins/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await pluginsApi.purgePlugin('plug-001') + + expect(mockDelete).toHaveBeenCalledWith('/admin/plugins/plug-001') + }) + + it('getPluginHealth 应调用 GET /admin/plugins/:id/health', async () => { + mockGet.mockResolvedValue(fakeRes) + await pluginsApi.getPluginHealth('plug-001') + + expect(mockGet).toHaveBeenCalledWith('/admin/plugins/plug-001/health') + }) + + it('updatePluginConfig 应调用 PUT /admin/plugins/:id/config', async () => { + mockPut.mockResolvedValue(fakeRes) + const config = { theme: 'dark' } + await pluginsApi.updatePluginConfig('plug-001', config, 1) + + expect(mockPut).toHaveBeenCalledWith('/admin/plugins/plug-001/config', { + config, + version: 1, + }) + }) + + it('getPluginSchema 应调用 GET /admin/plugins/:id/schema', async () => { + mockGet.mockResolvedValue(fakeRes) + await pluginsApi.getPluginSchema('plug-001') + + expect(mockGet).toHaveBeenCalledWith('/admin/plugins/plug-001/schema') + }) +}) + +describe('plugin market API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listMarketEntries 应调用 GET /market/entries', async () => { + mockGet.mockResolvedValue(fakeRes) + await pluginsApi.listMarketEntries({ page: 1, page_size: 10, category: 'crm' }) + + expect(mockGet).toHaveBeenCalledWith('/market/entries', { + params: { page: 1, page_size: 10, category: 'crm' }, + }) + }) + + it('getMarketEntry 应调用 GET /market/entries/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await pluginsApi.getMarketEntry('mkt-001') + + expect(mockGet).toHaveBeenCalledWith('/market/entries/mkt-001') + }) + + it('installFromMarket 应调用 POST /market/entries/:id/install', async () => { + mockPost.mockResolvedValue(fakeRes) + await pluginsApi.installFromMarket('mkt-001') + + expect(mockPost).toHaveBeenCalledWith('/market/entries/mkt-001/install') + }) + + it('submitMarketReview 应调用 POST /market/entries/:id/reviews', async () => { + mockPost.mockResolvedValue(fakeRes) + const review = { rating: 5, review_text: '很好用' } + await pluginsApi.submitMarketReview('mkt-001', review) + + expect(mockPost).toHaveBeenCalledWith('/market/entries/mkt-001/reviews', review) + }) +}) diff --git a/apps/web/src/api/roles.test.ts b/apps/web/src/api/roles.test.ts new file mode 100644 index 0000000..ce58021 --- /dev/null +++ b/apps/web/src/api/roles.test.ts @@ -0,0 +1,86 @@ +/** + * roles API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('./client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import * as rolesApi from './roles' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('roles API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listRoles 应调用 GET /roles 并传递分页参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await rolesApi.listRoles(1, 10) + + expect(mockGet).toHaveBeenCalledWith('/roles', { params: { page: 1, page_size: 10 } }) + }) + + it('getRole 应调用 GET /roles/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await rolesApi.getRole('r-001') + + expect(mockGet).toHaveBeenCalledWith('/roles/r-001') + }) + + it('createRole 应调用 POST /roles', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '医生', code: 'doctor', description: '医生角色' } + await rolesApi.createRole(req) + + expect(mockPost).toHaveBeenCalledWith('/roles', req) + }) + + it('updateRole 应调用 PUT /roles/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { name: '高级医生', version: 1 } + await rolesApi.updateRole('r-001', req) + + expect(mockPut).toHaveBeenCalledWith('/roles/r-001', req) + }) + + it('deleteRole 应调用 DELETE /roles/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await rolesApi.deleteRole('r-001') + + expect(mockDelete).toHaveBeenCalledWith('/roles/r-001') + }) + + it('assignPermissions 应调用 POST /roles/:id/permissions', async () => { + mockPost.mockResolvedValue(undefined) + await rolesApi.assignPermissions('r-001', ['p-1', 'p-2']) + + expect(mockPost).toHaveBeenCalledWith('/roles/r-001/permissions', { permission_ids: ['p-1', 'p-2'] }) + }) + + it('getRolePermissions 应调用 GET /roles/:id/permissions', async () => { + mockGet.mockResolvedValue(fakeRes) + await rolesApi.getRolePermissions('r-001') + + expect(mockGet).toHaveBeenCalledWith('/roles/r-001/permissions') + }) + + it('listPermissions 应调用 GET /permissions', async () => { + mockGet.mockResolvedValue(fakeRes) + await rolesApi.listPermissions() + + expect(mockGet).toHaveBeenCalledWith('/permissions') + }) +}) diff --git a/apps/web/src/api/users.test.ts b/apps/web/src/api/users.test.ts new file mode 100644 index 0000000..31f86b1 --- /dev/null +++ b/apps/web/src/api/users.test.ts @@ -0,0 +1,83 @@ +/** + * users API 契约测试 + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('./client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import * as usersApi from './users' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('users API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listUsers 应调用 GET /users 并传递分页和搜索参数', async () => { + mockGet.mockResolvedValue(fakeRes) + await usersApi.listUsers(2, 10, '张') + + expect(mockGet).toHaveBeenCalledWith('/users', { + params: { page: 2, page_size: 10, search: '张' }, + }) + }) + + it('listUsers 空搜索时应传 search 为 undefined', async () => { + mockGet.mockResolvedValue(fakeRes) + await usersApi.listUsers(1, 20, '') + + expect(mockGet).toHaveBeenCalledWith('/users', { + params: { page: 1, page_size: 20, search: undefined }, + }) + }) + + it('getUser 应调用 GET /users/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await usersApi.getUser('u-001') + + expect(mockGet).toHaveBeenCalledWith('/users/u-001') + }) + + it('createUser 应调用 POST /users', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { username: 'newuser', password: 'pass123', display_name: '新用户' } + await usersApi.createUser(req) + + expect(mockPost).toHaveBeenCalledWith('/users', req) + }) + + it('updateUser 应调用 PUT /users/:id', async () => { + mockPut.mockResolvedValue(fakeRes) + const req = { display_name: '改名', version: 1 } + await usersApi.updateUser('u-001', req) + + expect(mockPut).toHaveBeenCalledWith('/users/u-001', req) + }) + + it('deleteUser 应调用 DELETE /users/:id', async () => { + mockDelete.mockResolvedValue(undefined) + await usersApi.deleteUser('u-001') + + expect(mockDelete).toHaveBeenCalledWith('/users/u-001') + }) + + it('assignRoles 应调用 POST /users/:id/roles', async () => { + mockPost.mockResolvedValue(undefined) + await usersApi.assignRoles('u-001', ['role-1', 'role-2']) + + expect(mockPost).toHaveBeenCalledWith('/users/u-001/roles', { role_ids: ['role-1', 'role-2'] }) + }) +}) diff --git a/apps/web/src/api/workflow.test.ts b/apps/web/src/api/workflow.test.ts new file mode 100644 index 0000000..d2c262a --- /dev/null +++ b/apps/web/src/api/workflow.test.ts @@ -0,0 +1,141 @@ +/** + * workflow API 契约测试(definitions + instances + tasks) + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockGet = vi.fn() +const mockPost = vi.fn() +const mockPut = vi.fn() +const mockDelete = vi.fn() + +vi.mock('./client', () => ({ + default: { + get: (...args: unknown[]) => mockGet(...args), + post: (...args: unknown[]) => mockPost(...args), + put: (...args: unknown[]) => mockPut(...args), + delete: (...args: unknown[]) => mockDelete(...args), + }, +})) + +import * as defApi from './workflowDefinitions' +import * as instApi from './workflowInstances' +import * as taskApi from './workflowTasks' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('workflowDefinitions API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listProcessDefinitions 应调用 GET /workflow/definitions', async () => { + mockGet.mockResolvedValue(fakeRes) + await defApi.listProcessDefinitions(1, 10) + + expect(mockGet).toHaveBeenCalledWith('/workflow/definitions', { + params: { page: 1, page_size: 10 }, + }) + }) + + it('getProcessDefinition 应调用 GET /workflow/definitions/:id', async () => { + mockGet.mockResolvedValue(fakeRes) + await defApi.getProcessDefinition('wf-001') + + expect(mockGet).toHaveBeenCalledWith('/workflow/definitions/wf-001') + }) + + it('createProcessDefinition 应调用 POST /workflow/definitions', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { name: '审批流程', key: 'approval', nodes: [], edges: [] } + await defApi.createProcessDefinition(req) + + expect(mockPost).toHaveBeenCalledWith('/workflow/definitions', req) + }) + + it('publishProcessDefinition 应调用 POST /workflow/definitions/:id/publish', async () => { + mockPost.mockResolvedValue(fakeRes) + await defApi.publishProcessDefinition('wf-001') + + expect(mockPost).toHaveBeenCalledWith('/workflow/definitions/wf-001/publish') + }) +}) + +describe('workflowInstances API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('startInstance 应调用 POST /workflow/instances', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { definition_id: 'wf-001', business_key: 'BIZ-001' } + await instApi.startInstance(req) + + expect(mockPost).toHaveBeenCalledWith('/workflow/instances', req) + }) + + it('listInstances 应调用 GET /workflow/instances 并传递分页', async () => { + mockGet.mockResolvedValue(fakeRes) + await instApi.listInstances(1, 10) + + expect(mockGet).toHaveBeenCalledWith('/workflow/instances', { + params: { page: 1, page_size: 10 }, + }) + }) + + it('suspendInstance 应调用 POST /workflow/instances/:id/suspend', async () => { + mockPost.mockResolvedValue(fakeRes) + await instApi.suspendInstance('inst-001') + + expect(mockPost).toHaveBeenCalledWith('/workflow/instances/inst-001/suspend') + }) + + it('resumeInstance 应调用 POST /workflow/instances/:id/resume', async () => { + mockPost.mockResolvedValue(fakeRes) + await instApi.resumeInstance('inst-001') + + expect(mockPost).toHaveBeenCalledWith('/workflow/instances/inst-001/resume') + }) + + it('terminateInstance 应调用 POST /workflow/instances/:id/terminate', async () => { + mockPost.mockResolvedValue(fakeRes) + await instApi.terminateInstance('inst-001') + + expect(mockPost).toHaveBeenCalledWith('/workflow/instances/inst-001/terminate') + }) +}) + +describe('workflowTasks API', () => { + const fakeRes = { data: { success: true, data: {} } } + + it('listPendingTasks 应调用 GET /workflow/tasks/pending', async () => { + mockGet.mockResolvedValue(fakeRes) + await taskApi.listPendingTasks(1, 10) + + expect(mockGet).toHaveBeenCalledWith('/workflow/tasks/pending', { + params: { page: 1, page_size: 10 }, + }) + }) + + it('listCompletedTasks 应调用 GET /workflow/tasks/completed', async () => { + mockGet.mockResolvedValue(fakeRes) + await taskApi.listCompletedTasks(1, 10) + + expect(mockGet).toHaveBeenCalledWith('/workflow/tasks/completed', { + params: { page: 1, page_size: 10 }, + }) + }) + + it('completeTask 应调用 POST /workflow/tasks/:id/complete', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { outcome: 'approved', form_data: { comment: '同意' } } + await taskApi.completeTask('task-001', req) + + expect(mockPost).toHaveBeenCalledWith('/workflow/tasks/task-001/complete', req) + }) + + it('delegateTask 应调用 POST /workflow/tasks/:id/delegate', async () => { + mockPost.mockResolvedValue(fakeRes) + const req = { delegate_to: 'u-002' } + await taskApi.delegateTask('task-001', req) + + expect(mockPost).toHaveBeenCalledWith('/workflow/tasks/task-001/delegate', req) + }) +})