fix: BUG-012/013/007 — panel overlap, Markdown rendering, authStore tests
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

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.
This commit is contained in:
iven
2026-04-10 07:44:34 +08:00
parent 26336c3daa
commit 2c8ab47e5c
7 changed files with 94 additions and 67 deletions

View File

@@ -1,24 +1,22 @@
// ============================================================
// 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 functions + store (accessible in vi.mock factory) ──
const { mockSetToken, mockSetRefreshToken, mockLogout, _store } = vi.hoisted(() => {
const mockSetToken = vi.fn()
const mockSetRefreshToken = vi.fn()
// ── Hoisted: mock store (cookie-based auth — no JS token) ──
const { mockLogout, _store } = vi.hoisted(() => {
const mockLogout = vi.fn()
const _store = {
token: null as string | null,
refreshToken: null as string | null,
setToken: mockSetToken,
setRefreshToken: mockSetRefreshToken,
isAuthenticated: false,
logout: mockLogout,
}
return { mockSetToken, mockSetRefreshToken, mockLogout, _store }
return { mockLogout, _store }
})
vi.mock('@/stores/authStore', () => ({
@@ -38,11 +36,8 @@ const server = setupServer()
beforeEach(() => {
server.listen({ onUnhandledRequest: 'bypass' })
mockSetToken.mockClear()
mockSetRefreshToken.mockClear()
mockLogout.mockClear()
_store.token = null
_store.refreshToken = null
_store.isAuthenticated = false
})
afterEach(() => {
@@ -50,34 +45,22 @@ afterEach(() => {
})
describe('request interceptor', () => {
it('attaches Authorization header when token exists', async () => {
let capturedAuth: string | null = null
it('sends requests with credentials (cookie-based auth)', async () => {
let capturedCreds = false
server.use(
http.get('*/api/v1/test', ({ request }) => {
capturedAuth = request.headers.get('Authorization')
// Cookie-based auth: the browser sends cookies automatically.
// We verify the request was made successfully.
capturedCreds = true
return HttpResponse.json({ ok: true })
}),
)
setStoreState({ token: 'test-jwt-token' })
await request.get('/test')
setStoreState({ isAuthenticated: true })
const res = 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()
expect(res.data).toEqual({ ok: true })
expect(capturedCreds).toBe(true)
})
it('wraps non-401 errors as ApiRequestError', async () => {
@@ -116,7 +99,7 @@ describe('request interceptor', () => {
}
})
it('handles 401 with refresh token success', async () => {
it('handles 401 when authenticated — refreshes cookie and retries', async () => {
let callCount = 0
server.use(
@@ -128,26 +111,25 @@ describe('request interceptor', () => {
return HttpResponse.json({ data: 'success' })
}),
http.post('*/api/v1/auth/refresh', () => {
return HttpResponse.json({ token: 'new-jwt', refresh_token: 'new-refresh' })
// Server sets new HttpOnly cookie in response — no JS token needed
return HttpResponse.json({ ok: true })
}),
)
setStoreState({ token: 'old-jwt', refreshToken: 'old-refresh' })
setStoreState({ isAuthenticated: true })
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 () => {
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({ token: 'old-jwt', refreshToken: null })
setStoreState({ isAuthenticated: false })
try {
await request.get('/norefresh')
@@ -167,7 +149,7 @@ describe('request interceptor', () => {
}),
)
setStoreState({ token: 'old-jwt', refreshToken: 'old-refresh' })
setStoreState({ isAuthenticated: true })
try {
await request.get('/refreshfail')