fix: token刷新数据源统一+非空断言修复+并发竞态锁 (#1416)
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Failing after 697h43m56s

This commit was merged in pull request #1416.
This commit is contained in:
2026-08-18 13:42:32 +08:00
parent 766277142c
commit 82e05fcdbb
3 changed files with 111 additions and 53 deletions
+46 -20
View File
@@ -20,9 +20,16 @@ vi.mock("@/api/auth", () => ({
refreshAccessToken: vi.fn(),
}))
vi.mock("@/api/auth/tokenRefresh", () => ({
scheduleProactiveRefresh: vi.fn(),
cancelProactiveRefresh: vi.fn(),
executeTokenRefresh: vi.fn(),
}))
import { message } from "antd"
import { useAuthStore } from "@/store/authStore"
import { refreshAccessToken } from "@/api/auth"
import { executeTokenRefresh } from "@/api/auth/tokenRefresh"
import apiClient from "@/api/client"
// 从真实实例取出拦截器回调
@@ -264,23 +271,28 @@ describe("apiClient - 401 token refresh", () => {
expect(window.location.href).toBe("/")
})
it("refreshes token on 401 and calls setAuth", async () => {
it("refreshes token on 401 and calls executeTokenRefresh", async () => {
const mockSetAuth = vi.fn()
vi.mocked(useAuthStore.getState).mockReturnValue({
let currentAccessToken = "old-access"
vi.mocked(useAuthStore.getState).mockImplementation(() => ({
user: { id: "1", email: "test@test.com" },
accessToken: "old-access",
accessToken: currentAccessToken,
refreshToken: "old-refresh",
isAuthenticated: true,
clearAuth: vi.fn(),
setAuth: mockSetAuth,
} as any)
vi.mocked(refreshAccessToken).mockResolvedValue({
access_token: "new-access",
refresh_token: "new-refresh",
} as never)
setAuth: ((_user: any, newAccess: string, _newRefresh: string) => {
currentAccessToken = newAccess
mockSetAuth(_user, newAccess, _newRefresh)
}) as any,
}))
// Mock executeTokenRefresh to simulate successful refresh
vi.mocked(executeTokenRefresh).mockImplementation(() => {
currentAccessToken = "new-access"
mockSetAuth({ id: "1", email: "test@test.com" }, "new-access", "new-refresh")
return Promise.resolve()
})
// 拦截器重试时会调用 apiClient(config),会真的发请求,最终会 reject
// 但我们只关心刷新逻辑是否正确执行
const err = makeAxiosError(401, { detail: "Unauthorized" })
try {
@@ -289,31 +301,45 @@ describe("apiClient - 401 token refresh", () => {
// 重试会因为没有真实网络而失败,忽略
}
expect(refreshAccessToken).toHaveBeenCalledWith("old-refresh")
expect(executeTokenRefresh).toHaveBeenCalled()
expect(mockSetAuth).toHaveBeenCalled()
})
it("handles refresh failure by logging out", async () => {
const mockClearAuth = vi.fn()
vi.mocked(useAuthStore.getState).mockReturnValue({
// After executeTokenRefresh fails, it clears auth (sets accessToken to null)
// and redirects to /login. The promise resolves (doesn't reject).
let currentAccessToken: string | null = "old-access"
vi.mocked(useAuthStore.getState).mockImplementation(() => ({
user: { id: "1", email: "test@test.com" },
accessToken: "old-access",
accessToken: currentAccessToken,
refreshToken: "old-refresh",
isAuthenticated: true,
clearAuth: mockClearAuth,
isAuthenticated: currentAccessToken !== null,
clearAuth: (() => {
currentAccessToken = null
mockClearAuth()
window.location.href = "/login"
}) as any,
setAuth: vi.fn(),
} as any)
vi.mocked(refreshAccessToken).mockRejectedValue(new Error("refresh failed") as never)
}))
// Mock executeTokenRefresh: simulates failure → clears auth + redirects
vi.mocked(executeTokenRefresh).mockImplementation(() => {
currentAccessToken = null
mockClearAuth()
window.location.href = "/login"
return Promise.resolve()
})
const err = makeAxiosError(401, { detail: "Unauthorized" })
try {
await responseErrorInterceptor(err)
} catch {
// expected
// expected - rejects because accessToken is null after failed refresh
}
expect(executeTokenRefresh).toHaveBeenCalled()
expect(mockClearAuth).toHaveBeenCalled()
expect(window.location.href).toBe("/")
expect(window.location.href).toBe("/login")
})
})