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>
This commit is contained in:
179
admin-v2/tests/services/request.test.ts
Normal file
179
admin-v2/tests/services/request.test.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
// ============================================================
|
||||
// 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()
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user