diff --git a/src/__tests__/auth.test.ts b/src/__tests__/auth.test.ts new file mode 100644 index 0000000..53f1f2f --- /dev/null +++ b/src/__tests__/auth.test.ts @@ -0,0 +1,81 @@ +import { describe, test, expect, vi, beforeEach } from "vitest" + +// Mock next/cache +vi.mock("next/cache", () => ({ + revalidatePath: vi.fn(), +})) + +// Mock next/navigation +const mockRedirect = vi.fn() +vi.mock("next/navigation", () => ({ + redirect: (url: string) => { + mockRedirect(url) + throw new Error(`NEXT_REDIRECT:${url}`) + }, +})) + +// Mock Supabase server client +const mockSignInWithPassword = vi.fn() +const mockSignOut = vi.fn() + +vi.mock("@/lib/supabase/server", () => ({ + createClient: () => + Promise.resolve({ + auth: { + signInWithPassword: mockSignInWithPassword, + signOut: mockSignOut, + }, + }), +})) + +import { login, logout } from "@/app/login/actions" + +describe("login action", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + test("returns error when email or password is missing", async () => { + const formData = new FormData() + const result = await login(formData) + expect(result).toEqual({ error: "Email and password are required" }) + }) + + test("returns error on invalid credentials", async () => { + mockSignInWithPassword.mockResolvedValue({ + error: { message: "Invalid login credentials" }, + }) + + const formData = new FormData() + formData.set("email", "test@example.com") + formData.set("password", "wrong-password") + + const result = await login(formData) + expect(result).toEqual({ error: "Invalid email or password" }) + }) + + test("redirects to / on successful login", async () => { + mockSignInWithPassword.mockResolvedValue({ error: null }) + + const formData = new FormData() + formData.set("email", "test@example.com") + formData.set("password", "correct-password") + + await expect(login(formData)).rejects.toThrow("NEXT_REDIRECT:/") + expect(mockRedirect).toHaveBeenCalledWith("/") + }) +}) + +describe("logout action", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + test("signs out and redirects to /login", async () => { + mockSignOut.mockResolvedValue({ error: null }) + + await expect(logout()).rejects.toThrow("NEXT_REDIRECT:/login") + expect(mockSignOut).toHaveBeenCalled() + expect(mockRedirect).toHaveBeenCalledWith("/login") + }) +}) diff --git a/src/__tests__/middleware.test.ts b/src/__tests__/middleware.test.ts new file mode 100644 index 0000000..43170f9 --- /dev/null +++ b/src/__tests__/middleware.test.ts @@ -0,0 +1,60 @@ +import { describe, test, expect, vi, beforeEach } from "vitest" +import { NextRequest } from "next/server" + +// Mock Supabase SSR +const mockGetUser = vi.fn() +vi.mock("@supabase/ssr", () => ({ + createServerClient: () => ({ + auth: { + getUser: mockGetUser, + }, + }), +})) + +import { middleware } from "@/middleware" + +function createRequest(pathname: string) { + return new NextRequest(new URL(pathname, "http://localhost:3000")) +} + +describe("middleware", () => { + beforeEach(() => { + vi.clearAllMocks() + process.env.NEXT_PUBLIC_SUPABASE_URL = "http://localhost:54321" + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "test-anon-key" + }) + + test("redirects unauthenticated users from /settings to /login", async () => { + mockGetUser.mockResolvedValue({ data: { user: null } }) + + const response = await middleware(createRequest("/settings")) + expect(response.status).toBe(307) + expect(new URL(response.headers.get("location")!).pathname).toBe("/login") + }) + + test("allows unauthenticated users to access /", async () => { + mockGetUser.mockResolvedValue({ data: { user: null } }) + + const response = await middleware(createRequest("/")) + expect(response.status).toBe(200) + }) + + test("allows authenticated users to access /settings", async () => { + mockGetUser.mockResolvedValue({ + data: { user: { id: "user-1", email: "test@example.com" } }, + }) + + const response = await middleware(createRequest("/settings")) + expect(response.status).toBe(200) + }) + + test("redirects authenticated users away from /login", async () => { + mockGetUser.mockResolvedValue({ + data: { user: { id: "user-1", email: "test@example.com" } }, + }) + + const response = await middleware(createRequest("/login")) + expect(response.status).toBe(307) + expect(new URL(response.headers.get("location")!).pathname).toBe("/") + }) +}) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1876405..0e91410 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,19 +1,23 @@ -import type { Metadata } from "next"; -import "./globals.css"; +import type { Metadata } from "next" +import "./globals.css" +import Header from "@/components/Header" export const metadata: Metadata = { title: "Site Status", description: "Uptime monitoring for Roots of Progress", -}; +} export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode }>) { return ( -
{children} + +