Files
zclaw_openfang/admin-v2/tests/services/request.test.ts
iven ee51d5abcd feat(admin-v2): add ProTable search, scenarios/quick_commands form, tests, remove quota_reset_interval
- Enable ProTable search on Accounts (username/email), Models (model_id/alias),
  Providers (display_name/name) with hideInSearch for non-searchable columns
- Add scenarios (Select tags) and quick_commands (Form.List) to AgentTemplates
  create form, plus service type updates
- Remove unused quota_reset_interval from ProviderKey model, key_pool SQL,
  handlers, and frontend types; add migration + bump schema to v11
- Add Vitest config, test setup, request interceptor tests (7 cases),
  authStore tests (8 cases) — all 15 passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 11:13:16 +08:00

180 lines
5.1 KiB
TypeScript

// ============================================================
// request.ts 拦截器测试
// ============================================================
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
// ── Hoisted: mock functions + store (accessible in vi.mock factory) ──
const { mockSetToken, mockSetRefreshToken, mockLogout, _store } = vi.hoisted(() => {
const mockSetToken = vi.fn()
const mockSetRefreshToken = vi.fn()
const mockLogout = vi.fn()
const _store = {
token: null as string | null,
refreshToken: null as string | null,
setToken: mockSetToken,
setRefreshToken: mockSetRefreshToken,
logout: mockLogout,
}
return { mockSetToken, mockSetRefreshToken, mockLogout, _store }
})
vi.mock('@/stores/authStore', () => ({
useAuthStore: {
getState: () => _store,
},
}))
import request, { ApiRequestError } from '@/services/request'
function setStoreState(overrides: Partial<typeof _store>) {
Object.assign(_store, overrides)
}
// ── MSW server ──────────────────────────────────────────────
const server = setupServer()
beforeEach(() => {
server.listen({ onUnhandledRequest: 'bypass' })
mockSetToken.mockClear()
mockSetRefreshToken.mockClear()
mockLogout.mockClear()
_store.token = null
_store.refreshToken = null
})
afterEach(() => {
server.close()
})
describe('request interceptor', () => {
it('attaches Authorization header when token exists', async () => {
let capturedAuth: string | null = null
server.use(
http.get('*/api/v1/test', ({ request }) => {
capturedAuth = request.headers.get('Authorization')
return HttpResponse.json({ ok: true })
}),
)
setStoreState({ token: 'test-jwt-token' })
await request.get('/test')
expect(capturedAuth).toBe('Bearer test-jwt-token')
})
it('does not attach Authorization header when no token', async () => {
let capturedAuth: string | null = null
server.use(
http.get('*/api/v1/test', ({ request }) => {
capturedAuth = request.headers.get('Authorization')
return HttpResponse.json({ ok: true })
}),
)
setStoreState({ token: null })
await request.get('/test')
expect(capturedAuth).toBeNull()
})
it('wraps non-401 errors as ApiRequestError', async () => {
server.use(
http.get('*/api/v1/test', () => {
return HttpResponse.json(
{ error: 'not_found', message: 'Resource not found' },
{ status: 404 },
)
}),
)
try {
await request.get('/test')
expect.fail('Should have thrown')
} catch (err) {
expect(err).toBeInstanceOf(ApiRequestError)
expect((err as ApiRequestError).status).toBe(404)
expect((err as ApiRequestError).body.message).toBe('Resource not found')
}
})
it('wraps network errors as ApiRequestError with status 0', async () => {
server.use(
http.get('*/api/v1/test', () => {
return HttpResponse.error()
}),
)
try {
await request.get('/test')
expect.fail('Should have thrown')
} catch (err) {
expect(err).toBeInstanceOf(ApiRequestError)
expect((err as ApiRequestError).status).toBe(0)
}
})
it('handles 401 with refresh token success', async () => {
let callCount = 0
server.use(
http.get('*/api/v1/protected', () => {
callCount++
if (callCount === 1) {
return HttpResponse.json({ error: 'unauthorized' }, { status: 401 })
}
return HttpResponse.json({ data: 'success' })
}),
http.post('*/api/v1/auth/refresh', () => {
return HttpResponse.json({ token: 'new-jwt', refresh_token: 'new-refresh' })
}),
)
setStoreState({ token: 'old-jwt', refreshToken: 'old-refresh' })
const res = await request.get('/protected')
expect(res.data).toEqual({ data: 'success' })
expect(mockSetToken).toHaveBeenCalledWith('new-jwt')
expect(mockSetRefreshToken).toHaveBeenCalledWith('new-refresh')
})
it('handles 401 with no refresh token — calls logout immediately', async () => {
server.use(
http.get('*/api/v1/norefresh', () => {
return HttpResponse.json({ error: 'unauthorized' }, { status: 401 })
}),
)
setStoreState({ token: 'old-jwt', refreshToken: null })
try {
await request.get('/norefresh')
expect.fail('Should have thrown')
} catch {
expect(mockLogout).toHaveBeenCalled()
}
})
it('handles 401 with refresh failure — calls logout', async () => {
server.use(
http.get('*/api/v1/refreshfail', () => {
return HttpResponse.json({ error: 'unauthorized' }, { status: 401 })
}),
http.post('*/api/v1/auth/refresh', () => {
return HttpResponse.json({ error: 'invalid' }, { status: 401 })
}),
)
setStoreState({ token: 'old-jwt', refreshToken: 'old-refresh' })
try {
await request.get('/refreshfail')
expect.fail('Should have thrown')
} catch {
expect(mockLogout).toHaveBeenCalled()
}
})
})