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
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:
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user