187 lines
5.2 KiB
TypeScript
187 lines
5.2 KiB
TypeScript
import { render, screen, waitFor } from "@testing-library/react";
|
|
import type { ReactNode } from "react";
|
|
|
|
import { useAccessStore } from "@/store/accessStore";
|
|
import { useAuthStore } from "@/store/authStore";
|
|
import { App } from "./RefineContext";
|
|
|
|
let mockSessionState: {
|
|
data: {
|
|
accessToken: string;
|
|
user: { id: string; name: string };
|
|
} | null;
|
|
status: "authenticated" | "unauthenticated";
|
|
} = {
|
|
data: {
|
|
accessToken: "expired-access-token",
|
|
user: { id: "user-1", name: "Test User" },
|
|
},
|
|
status: "authenticated",
|
|
};
|
|
|
|
jest.mock("next-auth/react", () => ({
|
|
SessionProvider: ({ children }: { children: ReactNode }) => children,
|
|
signIn: jest.fn().mockResolvedValue(undefined),
|
|
useSession: () => mockSessionState,
|
|
}));
|
|
|
|
jest.mock("next/navigation", () => ({
|
|
usePathname: () => "/network-simulation",
|
|
}));
|
|
|
|
jest.mock("@refinedev/core", () => ({
|
|
Refine: ({ children }: { children: ReactNode }) => children,
|
|
}));
|
|
|
|
jest.mock("@refinedev/kbar", () => ({
|
|
RefineKbar: () => null,
|
|
RefineKbarProvider: ({ children }: { children: ReactNode }) => children,
|
|
}));
|
|
|
|
jest.mock("@refinedev/mui", () => ({
|
|
RefineSnackbarProvider: ({ children }: { children: ReactNode }) => children,
|
|
}));
|
|
|
|
jest.mock("@refinedev/nextjs-router", () => ({}));
|
|
jest.mock("@providers/data-provider", () => ({ dataProvider: {} }));
|
|
jest.mock("@/providers/notification-provider/useAppNotificationProvider", () => ({
|
|
useAppNotificationProvider: {},
|
|
}));
|
|
jest.mock("@contexts/color-mode", () => ({
|
|
ColorModeContextProvider: ({ children }: { children: ReactNode }) => children,
|
|
}));
|
|
jest.mock("@/contexts/ProjectContext", () => ({
|
|
ProjectProvider: ({ children }: { children: ReactNode }) => children,
|
|
}));
|
|
jest.mock("@/lib/authToken", () => ({
|
|
getAccessToken: jest.fn().mockResolvedValue("expired-access-token"),
|
|
}));
|
|
|
|
describe("RefineContext access authentication", () => {
|
|
const originalFetch = global.fetch;
|
|
const originalRequest = global.Request;
|
|
|
|
beforeEach(() => {
|
|
mockSessionState = {
|
|
data: {
|
|
accessToken: "expired-access-token",
|
|
user: { id: "user-1", name: "Test User" },
|
|
},
|
|
status: "authenticated",
|
|
};
|
|
useAuthStore.setState({
|
|
accessToken: null,
|
|
sessionExpired: false,
|
|
sessionExpiryReason: null,
|
|
});
|
|
useAccessStore.setState({
|
|
context: null,
|
|
permissions: [],
|
|
loading: true,
|
|
});
|
|
global.Request = class TestRequest {} as unknown as typeof Request;
|
|
global.fetch = jest.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 401,
|
|
headers: new Headers(),
|
|
} as Response);
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
global.fetch = originalFetch;
|
|
global.Request = originalRequest;
|
|
});
|
|
|
|
it("marks the session expired when access-context rejects an expired token", async () => {
|
|
render(
|
|
<App>
|
|
<div>应用内容</div>
|
|
</App>,
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("/api/v1/access-context"),
|
|
expect.not.objectContaining({ skipAuthRedirect: true }),
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(useAuthStore.getState()).toMatchObject({
|
|
sessionExpired: true,
|
|
sessionExpiryReason: "unauthorized",
|
|
});
|
|
});
|
|
});
|
|
|
|
it("shows an authorization message when access-context rejects the account", async () => {
|
|
global.fetch = jest.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 403,
|
|
headers: new Headers(),
|
|
json: async () => ({ detail: "User authorization pending" }),
|
|
} as Response);
|
|
|
|
render(
|
|
<App>
|
|
<div>应用内容</div>
|
|
</App>,
|
|
);
|
|
|
|
expect(
|
|
await screen.findByText("账号尚未获得访问权限"),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole("button", { name: "退出登录" }),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByText("Test User")).toBeInTheDocument();
|
|
expect(screen.queryByText("应用内容")).not.toBeInTheDocument();
|
|
expect(useAuthStore.getState()).toMatchObject({
|
|
sessionExpired: false,
|
|
sessionExpiryReason: null,
|
|
});
|
|
});
|
|
|
|
it("keeps project recovery available when the selected project is denied", async () => {
|
|
global.fetch = jest.fn().mockResolvedValue({
|
|
ok: false,
|
|
status: 403,
|
|
headers: new Headers(),
|
|
json: async () => ({ detail: "No access to project" }),
|
|
} as Response);
|
|
|
|
render(
|
|
<App>
|
|
<div>应用内容</div>
|
|
</App>,
|
|
);
|
|
|
|
expect(await screen.findByText("无权访问当前项目")).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole("button", { name: "切换项目" }),
|
|
).toBeInTheDocument();
|
|
expect(screen.queryByText("应用内容")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("prioritizes an unauthenticated session over route permissions", async () => {
|
|
mockSessionState = { data: null, status: "unauthenticated" };
|
|
|
|
render(
|
|
<App>
|
|
<div>应用内容</div>
|
|
</App>,
|
|
);
|
|
|
|
expect(screen.getByText("登录状态已失效")).toBeInTheDocument();
|
|
expect(screen.queryByText("无权访问此功能")).not.toBeInTheDocument();
|
|
|
|
await waitFor(() => {
|
|
expect(useAuthStore.getState()).toMatchObject({
|
|
sessionExpired: true,
|
|
sessionExpiryReason: "unauthorized",
|
|
});
|
|
});
|
|
});
|
|
});
|