20260819173221
This commit is contained in:
1 parent
1ffc8e1ac9
commit
a0a4273f4c
326 files changed
+301646
-2314
No files matched your search
@@ -0,0 +1,115 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
|
||||
// api.js 只依赖 http.js 的 post,mock 掉后断言每个接口的请求路径与参数
|
||||
vi.mock('@/services/http', () => ({
|
||||
post: vi.fn(),
|
||||
}))
|
||||
|
||||
import { post } from '@/services/http'
|
||||
import {
|
||||
loginApi,
|
||||
loadDataApi,
|
||||
loadDataBySqlApi,
|
||||
pageDataApi,
|
||||
saveObjectApi,
|
||||
nextIdApi,
|
||||
nextCodeApi,
|
||||
describeApi,
|
||||
} from '@/services/api'
|
||||
|
||||
beforeEach(() => {
|
||||
post.mockReset()
|
||||
})
|
||||
|
||||
describe('services/api', () => {
|
||||
it('loginApi:POST /auth/login,参数 orgid/userid/password', () => {
|
||||
loginApi('ORG1', 'admin', 'secret')
|
||||
expect(post).toHaveBeenCalledWith('/auth/login', {
|
||||
orgid: 'ORG1',
|
||||
userid: 'admin',
|
||||
password: 'secret',
|
||||
})
|
||||
})
|
||||
|
||||
it('loadDataApi:POST /data/loaddata,携带全部查询参数', () => {
|
||||
loadDataApi('s_module', 'b_canuse = 1', 'b_id ASC', ['b_id', 'b_code'])
|
||||
expect(post).toHaveBeenCalledWith('/data/loaddata', {
|
||||
view_name: 's_module',
|
||||
search_condition: 'b_canuse = 1',
|
||||
order_by: 'b_id ASC',
|
||||
search_columns: ['b_id', 'b_code'],
|
||||
})
|
||||
})
|
||||
|
||||
it('loadDataApi:省略可选参数时传 undefined', () => {
|
||||
loadDataApi('s_module')
|
||||
expect(post).toHaveBeenCalledWith('/data/loaddata', {
|
||||
view_name: 's_module',
|
||||
search_condition: undefined,
|
||||
order_by: undefined,
|
||||
search_columns: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('loadDataBySqlApi:POST /data/loaddatabysql,携带 sql', () => {
|
||||
loadDataBySqlApi('select * from t')
|
||||
expect(post).toHaveBeenCalledWith('/data/loaddatabysql', { sql: 'select * from t' })
|
||||
})
|
||||
|
||||
it('pageDataApi:使用默认分页 1/20', () => {
|
||||
pageDataApi({ viewName: 's_module' })
|
||||
expect(post).toHaveBeenCalledWith('/data/page', {
|
||||
view_name: 's_module',
|
||||
order_by: undefined,
|
||||
search_columns: undefined,
|
||||
search_condition: undefined,
|
||||
page_no: 1,
|
||||
page_size: 20,
|
||||
})
|
||||
})
|
||||
|
||||
it('pageDataApi:自定义分页与条件', () => {
|
||||
pageDataApi({
|
||||
viewName: 'ocean_booking',
|
||||
orderBy: 'b_id DESC',
|
||||
searchColumns: ['b_id'],
|
||||
searchCondition: 'b_org_id = 1',
|
||||
pageNo: 3,
|
||||
pageSize: 50,
|
||||
})
|
||||
expect(post).toHaveBeenCalledWith('/data/page', {
|
||||
view_name: 'ocean_booking',
|
||||
order_by: 'b_id DESC',
|
||||
search_columns: ['b_id'],
|
||||
search_condition: 'b_org_id = 1',
|
||||
page_no: 3,
|
||||
page_size: 50,
|
||||
})
|
||||
})
|
||||
|
||||
it('saveObjectApi:POST /data/saveobjt,透传请求数组', () => {
|
||||
const requests = [{ table: 's_module', data: { b_id: 1 } }]
|
||||
saveObjectApi(requests)
|
||||
expect(post).toHaveBeenCalledWith('/data/saveobjt', requests)
|
||||
})
|
||||
|
||||
it('nextIdApi:默认 count=1', () => {
|
||||
nextIdApi()
|
||||
expect(post).toHaveBeenCalledWith('/data/nextid', { count: 1 })
|
||||
})
|
||||
|
||||
it('nextIdApi:自定义 count', () => {
|
||||
nextIdApi(5)
|
||||
expect(post).toHaveBeenCalledWith('/data/nextid', { count: 5 })
|
||||
})
|
||||
|
||||
it('nextCodeApi:moduleId 转字符串并携带 count', () => {
|
||||
nextCodeApi(42, 3)
|
||||
expect(post).toHaveBeenCalledWith('/data/nextcode', { moduleId: '42', count: 3 })
|
||||
})
|
||||
|
||||
it('describeApi:POST /data/describe,携带表名', () => {
|
||||
describeApi('ocean_booking')
|
||||
expect(post).toHaveBeenCalledWith('/data/describe', { table: 'ocean_booking' })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,200 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { createTestPinia } from '../helpers/app.js'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
// http 模块持有模块级状态(redirectingToLogin),每个用例前重置模块并动态 import,
|
||||
// 保证跳转去重标志不跨用例泄漏。
|
||||
async function loadHttp() {
|
||||
vi.resetModules()
|
||||
const { default: http } = await import('@/services/http')
|
||||
return http
|
||||
}
|
||||
|
||||
function stubLocation() {
|
||||
// jsdom 的 location.assign 会抛 "Not implemented",替换为 spy
|
||||
const assign = vi.fn()
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value: { ...window.location, assign, pathname: '/dashboard' },
|
||||
})
|
||||
return assign
|
||||
}
|
||||
|
||||
const requestFulfilled = (http) => http.interceptors.request.handlers[0].fulfilled
|
||||
const responseFulfilled = (http) => http.interceptors.response.handlers[0].fulfilled
|
||||
const responseRejected = (http) => http.interceptors.response.handlers[0].rejected
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('http 请求拦截器', () => {
|
||||
it('无 token 时不注入 Authorization', async () => {
|
||||
const http = await loadHttp()
|
||||
createTestPinia()
|
||||
const config = { headers: {} }
|
||||
const result = requestFulfilled(http)(config)
|
||||
expect(result.headers.Authorization).toBeUndefined()
|
||||
})
|
||||
|
||||
it('有 token 时注入 Bearer 头', async () => {
|
||||
const http = await loadHttp()
|
||||
createTestPinia()
|
||||
useAuthStore().setSession({ token: 'tk-123' }, 'ORG')
|
||||
const config = { headers: {} }
|
||||
const result = requestFulfilled(http)(config)
|
||||
expect(result.headers.Authorization).toBe('Bearer tk-123')
|
||||
})
|
||||
})
|
||||
|
||||
describe('http 响应拦截器 · 成功分支', () => {
|
||||
it('code === 0:标记 success 并原样返回', async () => {
|
||||
const http = await loadHttp()
|
||||
const res = { data: { code: 0, data: [1, 2] } }
|
||||
const result = responseFulfilled(http)(res)
|
||||
expect(result.data.success).toBe(true)
|
||||
expect(result).toBe(res)
|
||||
})
|
||||
|
||||
it('无 code 字段:按成功透传并包装 ApiResult', async () => {
|
||||
const http = await loadHttp()
|
||||
const payload = { id: 1, name: 'x' }
|
||||
const result = responseFulfilled(http)({ data: payload })
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
code: 0,
|
||||
message: '',
|
||||
data: payload,
|
||||
})
|
||||
})
|
||||
|
||||
it('业务错误码(非认证):抛 Error 并携带 code/data/success', async () => {
|
||||
const http = await loadHttp()
|
||||
const res = { data: { code: 500, message: '服务器开小差了', data: { a: 1 } } }
|
||||
let caught
|
||||
try {
|
||||
responseFulfilled(http)(res)
|
||||
} catch (e) {
|
||||
caught = e
|
||||
}
|
||||
expect(caught).toBeInstanceOf(Error)
|
||||
expect(caught.message).toBe('服务器开小差了')
|
||||
expect(caught.code).toBe(500)
|
||||
expect(caught.data).toEqual({ a: 1 })
|
||||
expect(caught.success).toBe(false)
|
||||
})
|
||||
|
||||
it('业务错误码无 message:使用默认文案', async () => {
|
||||
const http = await loadHttp()
|
||||
let caught
|
||||
try {
|
||||
responseFulfilled(http)({ data: { code: 1001 } })
|
||||
} catch (e) {
|
||||
caught = e
|
||||
}
|
||||
expect(caught.message).toBe('请求失败')
|
||||
})
|
||||
})
|
||||
|
||||
describe('http 响应拦截器 · 认证失效分支', () => {
|
||||
it('401:清空会话并跳转 /login?reason=expired', async () => {
|
||||
const http = await loadHttp()
|
||||
createTestPinia()
|
||||
useAuthStore().setSession({ token: 't', user: { id: 1 } }, 'ORG')
|
||||
const assign = stubLocation()
|
||||
|
||||
let caught
|
||||
try {
|
||||
responseRejected(http)({ response: { status: 401, data: { code: 401 } } })
|
||||
} catch (e) {
|
||||
caught = e
|
||||
}
|
||||
|
||||
expect(useAuthStore().isAuthenticated).toBe(false)
|
||||
expect(assign).toHaveBeenCalledTimes(1)
|
||||
const url = assign.mock.calls[0][0]
|
||||
expect(url.startsWith('/login?')).toBe(true)
|
||||
expect(url).toContain('reason=expired')
|
||||
expect(caught.code).toBe(401)
|
||||
})
|
||||
|
||||
it('40101:跳转 /login?reason=session-replaced', async () => {
|
||||
const http = await loadHttp()
|
||||
createTestPinia()
|
||||
useAuthStore().setSession({ token: 't', user: { id: 1 } }, 'ORG')
|
||||
const assign = stubLocation()
|
||||
|
||||
try {
|
||||
responseRejected(http)({ response: { status: 200, data: { code: 40101 } } })
|
||||
} catch {
|
||||
// 预期抛错
|
||||
}
|
||||
|
||||
const url = assign.mock.calls[0][0]
|
||||
expect(url).toContain('reason=session-replaced')
|
||||
})
|
||||
|
||||
it('已在 /login 页面时不重复跳转(不抛导航错误)', async () => {
|
||||
const http = await loadHttp()
|
||||
createTestPinia()
|
||||
const assign = vi.fn()
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value: { ...window.location, assign, pathname: '/login' },
|
||||
})
|
||||
|
||||
let caught
|
||||
try {
|
||||
responseRejected(http)({ response: { status: 401, data: { code: 401 } } })
|
||||
} catch (e) {
|
||||
caught = e
|
||||
}
|
||||
expect(assign).not.toHaveBeenCalled()
|
||||
expect(caught.code).toBe(401)
|
||||
})
|
||||
})
|
||||
|
||||
describe('http 响应拦截器 · 网络异常分支', () => {
|
||||
it('超时(ECONNABORTED):抛"请求超时"', async () => {
|
||||
const http = await loadHttp()
|
||||
stubLocation()
|
||||
let caught
|
||||
try {
|
||||
responseRejected(http)({ code: 'ECONNABORTED' })
|
||||
} catch (e) {
|
||||
caught = e
|
||||
}
|
||||
expect(caught.message).toBe('请求超时,请稍后重试')
|
||||
expect(caught.code).toBe(-1)
|
||||
})
|
||||
|
||||
it('其他网络错误:抛"网络异常"', async () => {
|
||||
const http = await loadHttp()
|
||||
stubLocation()
|
||||
let caught
|
||||
try {
|
||||
responseRejected(http)({ message: 'Network Error' })
|
||||
} catch (e) {
|
||||
caught = e
|
||||
}
|
||||
expect(caught.message).toBe('网络异常,请检查服务是否正常')
|
||||
expect(caught.code).toBe(-1)
|
||||
})
|
||||
|
||||
it('HTTP 状态错误且带后端 message:优先使用后端文案', async () => {
|
||||
const http = await loadHttp()
|
||||
stubLocation()
|
||||
let caught
|
||||
try {
|
||||
responseRejected(http)({
|
||||
response: { status: 500, data: { code: 500, message: '后端错误信息' } },
|
||||
})
|
||||
} catch (e) {
|
||||
caught = e
|
||||
}
|
||||
expect(caught.message).toBe('后端错误信息')
|
||||
expect(caught.code).toBe(500)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user