feat(frontend): login page — POST /api/session + error UI
This commit is contained in:
48
frontend/src/pages/LoginPage.test.tsx
Normal file
48
frontend/src/pages/LoginPage.test.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { renderWithProviders } from "@/testing/renderWithProviders";
|
||||
import { mswServer, http, HttpResponse } from "@/testing/msw";
|
||||
import LoginPage from "./LoginPage";
|
||||
|
||||
describe("LoginPage", () => {
|
||||
it("posts /api/session and redirects to /workflows on success", async () => {
|
||||
mswServer.use(
|
||||
http.post("/api/session", async ({ request }) => {
|
||||
const body = await request.json() as { password: string };
|
||||
if (body.password === "hunter2") return new HttpResponse(null, { status: 204 });
|
||||
return new HttpResponse(null, { status: 401 });
|
||||
}),
|
||||
http.get("/api/session/me", () =>
|
||||
HttpResponse.json({ authenticated: true, user: "admin" })),
|
||||
);
|
||||
|
||||
renderWithProviders(
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/workflows" element={<div>Workflows here</div>} />
|
||||
</Routes>,
|
||||
{ route: "/login" },
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/password/i), "hunter2");
|
||||
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Workflows here")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows an error when the API returns 401", async () => {
|
||||
mswServer.use(
|
||||
http.post("/api/session", () => new HttpResponse(null, { status: 401 })),
|
||||
);
|
||||
renderWithProviders(<LoginPage />, { route: "/login" });
|
||||
await userEvent.type(screen.getByLabelText(/password/i), "wrong");
|
||||
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/invalid password/i)).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,51 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { UnauthorizedError } from "@/api/client";
|
||||
import { useLogin } from "@/auth/session";
|
||||
|
||||
export default function LoginPage() {
|
||||
return <div className="p-8">Login stub — replaced Task 8.</div>;
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const login = useLogin();
|
||||
const nav = useNavigate();
|
||||
|
||||
async function onSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
try {
|
||||
await login.mutateAsync(password);
|
||||
nav("/workflows", { replace: true });
|
||||
} catch (err) {
|
||||
setError(err instanceof UnauthorizedError ? "Invalid password" : "Login failed");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-slate-950">
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="w-80 rounded-lg border border-slate-800 bg-slate-900 p-6 space-y-4"
|
||||
>
|
||||
<h1 className="text-lg font-semibold text-brand-500">VolcanicCore</h1>
|
||||
<label className="block text-sm space-y-1">
|
||||
<span className="text-slate-300">Password</span>
|
||||
<input
|
||||
type="password"
|
||||
autoFocus
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full rounded bg-slate-950 border border-slate-800 px-3 py-2 outline-none focus:border-brand-500"
|
||||
/>
|
||||
</label>
|
||||
{error && <p className="text-red-400 text-sm" role="alert">{error}</p>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={login.isPending}
|
||||
className="w-full rounded bg-brand-600 hover:bg-brand-700 disabled:opacity-50 py-2 text-sm font-medium"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user