Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eff59c5a7e | ||
|
|
d98fa359ae |
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
@@ -1,4 +1,5 @@
|
|||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { signIn } from "next-auth/react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { useAccessStore } from "@/store/accessStore";
|
import { useAccessStore } from "@/store/accessStore";
|
||||||
@@ -25,6 +26,8 @@ let mockSessionState: {
|
|||||||
};
|
};
|
||||||
let mockPathname = "/network-simulation";
|
let mockPathname = "/network-simulation";
|
||||||
let mockProjectProviderBlocks = false;
|
let mockProjectProviderBlocks = false;
|
||||||
|
let capturedLogin: (() => Promise<unknown>) | null = null;
|
||||||
|
const replaceMock = jest.fn();
|
||||||
|
|
||||||
jest.mock("next-auth/react", () => ({
|
jest.mock("next-auth/react", () => ({
|
||||||
SessionProvider: ({ children }: { children: ReactNode }) => children,
|
SessionProvider: ({ children }: { children: ReactNode }) => children,
|
||||||
@@ -34,10 +37,20 @@ jest.mock("next-auth/react", () => ({
|
|||||||
|
|
||||||
jest.mock("next/navigation", () => ({
|
jest.mock("next/navigation", () => ({
|
||||||
usePathname: () => mockPathname,
|
usePathname: () => mockPathname,
|
||||||
|
useRouter: () => ({ replace: replaceMock }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("@refinedev/core", () => ({
|
jest.mock("@refinedev/core", () => ({
|
||||||
Refine: ({ children }: { children: ReactNode }) => children,
|
Refine: ({
|
||||||
|
children,
|
||||||
|
authProvider,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
authProvider: { login: (params: Record<string, never>) => Promise<unknown> };
|
||||||
|
}) => {
|
||||||
|
capturedLogin = () => authProvider.login({});
|
||||||
|
return children;
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("@refinedev/kbar", () => ({
|
jest.mock("@refinedev/kbar", () => ({
|
||||||
@@ -78,6 +91,9 @@ describe("RefineContext access authentication", () => {
|
|||||||
clearIntentionalLogout();
|
clearIntentionalLogout();
|
||||||
mockPathname = "/network-simulation";
|
mockPathname = "/network-simulation";
|
||||||
mockProjectProviderBlocks = false;
|
mockProjectProviderBlocks = false;
|
||||||
|
capturedLogin = null;
|
||||||
|
replaceMock.mockReset();
|
||||||
|
jest.mocked(signIn).mockClear();
|
||||||
mockSessionState = {
|
mockSessionState = {
|
||||||
data: {
|
data: {
|
||||||
accessToken: "expired-access-token",
|
accessToken: "expired-access-token",
|
||||||
@@ -217,6 +233,25 @@ describe("RefineContext access authentication", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("starts login with a safe post-authentication callback from the login route", async () => {
|
||||||
|
mockPathname = "/login";
|
||||||
|
mockSessionState = { data: null, status: "unauthenticated" };
|
||||||
|
|
||||||
|
render(
|
||||||
|
<App>
|
||||||
|
<div>登录页面</div>
|
||||||
|
</App>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(capturedLogin).not.toBeNull();
|
||||||
|
await capturedLogin!();
|
||||||
|
|
||||||
|
expect(signIn).toHaveBeenCalledWith("keycloak", {
|
||||||
|
callbackUrl: "/",
|
||||||
|
redirect: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("does not treat returning to a protected route after logout as session expiry", async () => {
|
it("does not treat returning to a protected route after logout as session expiry", async () => {
|
||||||
markIntentionalLogout();
|
markIntentionalLogout();
|
||||||
mockSessionState = { data: null, status: "unauthenticated" };
|
mockSessionState = { data: null, status: "unauthenticated" };
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export const App = (props: React.PropsWithChildren<AppProps>) => {
|
|||||||
const authProvider: AuthProvider = {
|
const authProvider: AuthProvider = {
|
||||||
login: async () => {
|
login: async () => {
|
||||||
signIn("keycloak", {
|
signIn("keycloak", {
|
||||||
callbackUrl: to ? to.toString() : "/",
|
callbackUrl: to && to !== "/login" ? to.toString() : "/",
|
||||||
redirect: true,
|
redirect: true,
|
||||||
});
|
});
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ import { useAccessStore } from "@/store/accessStore";
|
|||||||
import { useAuthStore } from "@/store/authStore";
|
import { useAuthStore } from "@/store/authStore";
|
||||||
import { RoutePermissionGuard } from "./RoutePermissionGuard";
|
import { RoutePermissionGuard } from "./RoutePermissionGuard";
|
||||||
|
|
||||||
|
const replaceMock = jest.fn();
|
||||||
|
let mockPathname = "/network-simulation";
|
||||||
|
|
||||||
jest.mock("next/navigation", () => ({
|
jest.mock("next/navigation", () => ({
|
||||||
usePathname: () => "/network-simulation",
|
usePathname: () => mockPathname,
|
||||||
|
useRouter: () => ({ replace: replaceMock }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("@/components/header", () => ({
|
jest.mock("@/components/header", () => ({
|
||||||
@@ -16,6 +20,8 @@ jest.mock("@/components/header", () => ({
|
|||||||
|
|
||||||
describe("RoutePermissionGuard", () => {
|
describe("RoutePermissionGuard", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
replaceMock.mockReset();
|
||||||
|
mockPathname = "/network-simulation";
|
||||||
useAccessStore.setState({
|
useAccessStore.setState({
|
||||||
context: null,
|
context: null,
|
||||||
permissions: [],
|
permissions: [],
|
||||||
@@ -86,7 +92,7 @@ describe("RoutePermissionGuard", () => {
|
|||||||
expect(screen.getByTestId("access-header")).toHaveTextContent("project");
|
expect(screen.getByTestId("access-header")).toHaveTextContent("project");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("checks authentication before the expired state effect runs", () => {
|
it("redirects an unauthenticated protected route to login", () => {
|
||||||
render(
|
render(
|
||||||
<RoutePermissionGuard authenticated={false}>
|
<RoutePermissionGuard authenticated={false}>
|
||||||
<div>受保护内容</div>
|
<div>受保护内容</div>
|
||||||
@@ -95,5 +101,20 @@ describe("RoutePermissionGuard", () => {
|
|||||||
|
|
||||||
expect(screen.getByText("登录状态已失效")).toBeInTheDocument();
|
expect(screen.getByText("登录状态已失效")).toBeInTheDocument();
|
||||||
expect(screen.queryByText("无权访问此功能")).not.toBeInTheDocument();
|
expect(screen.queryByText("无权访问此功能")).not.toBeInTheDocument();
|
||||||
|
expect(replaceMock).toHaveBeenCalledWith("/login");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows the login route to render while unauthenticated", () => {
|
||||||
|
mockPathname = "/login";
|
||||||
|
|
||||||
|
render(
|
||||||
|
<RoutePermissionGuard authenticated={false}>
|
||||||
|
<div>登录页面</div>
|
||||||
|
</RoutePermissionGuard>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("登录页面")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("登录状态已失效")).not.toBeInTheDocument();
|
||||||
|
expect(replaceMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import type { ReactNode } from "react";
|
import { type ReactNode, useEffect } from "react";
|
||||||
|
|
||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import { permissionForPath } from "@/lib/permissions";
|
import { permissionForPath } from "@/lib/permissions";
|
||||||
@@ -26,10 +26,22 @@ export const RoutePermissionGuard = ({
|
|||||||
accessDeniedReason?: "account" | "project" | null;
|
accessDeniedReason?: "account" | "project" | null;
|
||||||
}) => {
|
}) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
const permissions = useAccessStore((state) => state.permissions);
|
const permissions = useAccessStore((state) => state.permissions);
|
||||||
const loading = useAccessStore((state) => state.loading);
|
const loading = useAccessStore((state) => state.loading);
|
||||||
const sessionExpired = useAuthStore((state) => state.sessionExpired);
|
const sessionExpired = useAuthStore((state) => state.sessionExpired);
|
||||||
const requiredPermission = permissionForPath(pathname);
|
const requiredPermission = permissionForPath(pathname);
|
||||||
|
const isLoginRoute = pathname === "/login";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authenticated && !isLoginRoute) {
|
||||||
|
router.replace("/login");
|
||||||
|
}
|
||||||
|
}, [authenticated, isLoginRoute, router]);
|
||||||
|
|
||||||
|
if (isLoginRoute) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
if (!authenticated || sessionExpired) {
|
if (!authenticated || sessionExpired) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user