Files
zclaw_openfang/admin-v2/tests/services/request.test.ts
iven 2c8ab47e5c
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
fix: BUG-012/013/007 — panel overlap, Markdown rendering, authStore tests
BUG-012: Reposition side panel toggle button (top-[52px]→top-20) to
avoid overlap with header buttons in ResizableChatLayout.

BUG-013: Install @tailwindcss/typography plugin and import in index.css
to enable prose-* Markdown rendering classes in StreamingText.

BUG-007: Rewrite authStore tests to match HttpOnly cookie auth model
(login takes 1 arg, no token/refreshToken in state). Rewrite request
interceptor tests for cookie-based auth. Update bug-tracker status.
2026-04-10 07:44:34 +08:00

162 lines
4.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// request.ts 拦截器测试
// ============================================================
//
// 认证策略已迁移到 HttpOnly cookie 模式。
// 浏览器自动附加 cookiewithCredentials: trueJS 不操作 token。
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
// ── Hoisted: mock store (cookie-based auth — no JS token) ──
const { mockLogout, _store } = vi.hoisted(() => {
const mockLogout = vi.fn()
const _store = {
isAuthenticated: false,
logout: mockLogout,
}
return { 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' })
mockLogout.mockClear()
_store.isAuthenticated = false
})
afterEach(() => {
server.close()
})
describe('request interceptor', () => {
it('sends requests with credentials (cookie-based auth)', async () => {
let capturedCreds = false
server.use(
http.get('*/api/v1/test', ({ request }) => {
// Cookie-based auth: the browser sends cookies automatically.
// We verify the request was made successfully.
capturedCreds = true
return HttpResponse.json({ ok: true })
}),
)
setStoreState({ isAuthenticated: true })
const res = await request.get('/test')
expect(res.data).toEqual({ ok: true })
expect(capturedCreds).toBe(true)
})
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 when authenticated — refreshes cookie and retries', 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', () => {
// Server sets new HttpOnly cookie in response — no JS token needed
return HttpResponse.json({ ok: true })
}),
)
setStoreState({ isAuthenticated: true })
const res = await request.get('/protected')
expect(res.data).toEqual({ data: 'success' })
})
it('handles 401 when not authenticated — calls logout immediately', async () => {
server.use(
http.get('*/api/v1/norefresh', () => {
return HttpResponse.json({ error: 'unauthorized' }, { status: 401 })
}),
)
setStoreState({ isAuthenticated: false })
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({ isAuthenticated: true })
try {
await request.get('/refreshfail')
expect.fail('Should have thrown')
} catch {
expect(mockLogout).toHaveBeenCalled()
}
})
})