1
0
forked from baron/baron-sso

userfront e2e 전체 테스트

This commit is contained in:
2026-05-29 08:19:34 +09:00
parent dc16958804
commit da01f63c54
22 changed files with 1439 additions and 103 deletions

View File

@@ -0,0 +1,72 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest";
import LoginPage from "./LoginPage";
const mockSigninRedirect = vi.fn();
const mockUseAuth = vi.fn();
vi.mock("react-oidc-context", () => ({
useAuth: () => mockUseAuth(),
}));
function renderLoginPage(initialEntry: string) {
return render(
<MemoryRouter initialEntries={[initialEntry]}>
<LoginPage />
</MemoryRouter>,
);
}
describe("LoginPage", () => {
beforeEach(() => {
Object.defineProperty(window, "crypto", {
configurable: true,
value: {},
});
Object.defineProperty(window, "isSecureContext", {
configurable: true,
value: false,
});
mockSigninRedirect.mockReset();
mockUseAuth.mockReturnValue({
activeNavigator: undefined,
error: undefined,
isAuthenticated: false,
isLoading: false,
signinRedirect: mockSigninRedirect,
});
});
it("shows an actionable error instead of starting PKCE when WebCrypto is unavailable", async () => {
renderLoginPage("/login?returnTo=%2F");
await userEvent.click(screen.getByRole("button", { name: /SSO 계정으로 로그인/i }));
expect(mockSigninRedirect).not.toHaveBeenCalled();
expect(screen.getByRole("alert")).toHaveTextContent(
/SSO 로그인을 시작할 수 없습니다/,
);
});
it("preserves the returnTo query when starting SSO manually", async () => {
Object.defineProperty(window, "crypto", {
configurable: true,
value: { subtle: {} },
});
Object.defineProperty(window, "isSecureContext", {
configurable: true,
value: true,
});
renderLoginPage("/login?returnTo=%2Fusers%3Fpage%3D2");
await userEvent.click(screen.getByRole("button", { name: /SSO 계정으로 로그인/i }));
expect(mockSigninRedirect).toHaveBeenCalledWith({
state: {
returnTo: "/users?page=2",
},
});
});
});

View File

@@ -1,5 +1,5 @@
import { ExternalLink, LogIn, ShieldHalf } from "lucide-react";
import { useEffect, useRef } from "react";
import { AlertTriangle, ExternalLink, LogIn, ShieldHalf } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useAuth } from "react-oidc-context";
import { useNavigate, useSearchParams } from "react-router-dom";
import { Button } from "../../components/ui/button";
@@ -10,15 +10,36 @@ import {
CardHeader,
CardTitle,
} from "../../components/ui/card";
import { canStartBrowserPkceLogin } from "../../lib/authConfig";
import { debugLog } from "../../lib/debugLog";
const insecurePkceMessage =
"이 주소에서는 브라우저 보안 정책 때문에 SSO 로그인을 시작할 수 없습니다. HTTPS 또는 localhost로 접속하거나, 내부망/host.docker.internal 개발 접속은 Chrome의 insecure-origin secure context 옵션에 실제 auth UI origin(예: http://host.docker.internal:5000)을 정확히 등록해 주세요.";
function isPkceSetupFailure(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return /Crypto\.subtle|WebCrypto|PKCE|secure context|subtle/i.test(message);
}
function LoginPage() {
const auth = useAuth();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const autoStartedRef = useRef(false);
const [loginError, setLoginError] = useState<string | null>(null);
const returnTo = searchParams.get("returnTo") || "/";
const shouldAutoLogin = searchParams.get("auto") === "1";
const authErrorMessage = useMemo(() => {
const message = auth.error?.message;
if (!message) {
return null;
}
if (message.includes("Crypto.subtle")) {
return insecurePkceMessage;
}
return message;
}, [auth.error?.message]);
const visibleLoginError = loginError || authErrorMessage;
useEffect(() => {
debugLog("[LoginPage] Auth state check:", {
@@ -42,21 +63,46 @@ function LoginPage() {
if (autoStartedRef.current || auth.isLoading || auth.activeNavigator) {
return;
}
if (!canStartBrowserPkceLogin()) {
setLoginError(insecurePkceMessage);
return;
}
autoStartedRef.current = true;
void auth.signinRedirect({
state: {
returnTo,
},
});
void auth
.signinRedirect({
state: {
returnTo,
},
})
.catch((error) => {
if (isPkceSetupFailure(error)) {
setLoginError(insecurePkceMessage);
return;
}
console.error("Auto login redirect failed", error);
});
}, [auth, auth.activeNavigator, auth.isLoading, returnTo, shouldAutoLogin]);
const handleSSOLogin = () => {
void auth.signinRedirect({
state: {
returnTo: "/",
},
});
const handleSSOLogin = async () => {
try {
setLoginError(null);
if (!canStartBrowserPkceLogin()) {
setLoginError(insecurePkceMessage);
return;
}
await auth.signinRedirect({
state: {
returnTo,
},
});
} catch (error) {
if (isPkceSetupFailure(error)) {
setLoginError(insecurePkceMessage);
return;
}
console.error("Redirect login failed", error);
}
};
return (
@@ -85,11 +131,7 @@ function LoginPage() {
variant="ghost"
className="p-0 h-auto text-destructive underline mt-2 hover:bg-transparent"
onClick={() => {
void auth.signinRedirect({
state: {
returnTo,
},
});
void handleSSOLogin();
}}
>
@@ -127,6 +169,16 @@ function LoginPage() {
)}
</Button>
{visibleLoginError ? (
<div
role="alert"
className="flex gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm leading-5 text-destructive"
>
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{visibleLoginError}</span>
</div>
) : null}
<p className="mt-6 text-xs text-center text-muted-foreground leading-relaxed">
15 .
<br />