Add authentication infrastructure for protected routes: - withAuth() wrapper for API route handlers (src/lib/auth-middleware.ts) - Next.js middleware for page protection (src/middleware.ts) withAuth() loads auth from cookies, validates session, and passes user context to handlers. Returns 401 for unauthenticated requests. Page middleware redirects unauthenticated users to /login, while allowing public routes (/login), API routes (handled separately), and static assets through. Tests: 18 new tests (6 for withAuth, 12 for page middleware) Total test count: 60 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
135 lines
4.2 KiB
TypeScript
135 lines
4.2 KiB
TypeScript
// ABOUTME: Unit tests for Next.js middleware page protection.
|
|
// ABOUTME: Tests that protected routes redirect to login when unauthenticated.
|
|
import type { NextRequest } from "next/server";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { config, middleware } from "./middleware";
|
|
|
|
describe("middleware", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
function createMockRequest(url: string, cookieValue?: string): NextRequest {
|
|
const request = {
|
|
nextUrl: new URL(url, "http://localhost:3000"),
|
|
url: `http://localhost:3000${url}`,
|
|
cookies: {
|
|
get: vi
|
|
.fn()
|
|
.mockReturnValue(
|
|
cookieValue ? { name: "pb_auth", value: cookieValue } : undefined,
|
|
),
|
|
},
|
|
} as unknown as NextRequest;
|
|
return request;
|
|
}
|
|
|
|
describe("protected routes", () => {
|
|
it("redirects to /login when pb_auth cookie is missing", async () => {
|
|
const request = createMockRequest("/");
|
|
const response = await middleware(request);
|
|
|
|
expect(response.status).toBe(307);
|
|
expect(response.headers.get("location")).toBe(
|
|
"http://localhost:3000/login",
|
|
);
|
|
});
|
|
|
|
it("redirects to /login when pb_auth cookie is empty", async () => {
|
|
const request = createMockRequest("/", "");
|
|
const response = await middleware(request);
|
|
|
|
expect(response.status).toBe(307);
|
|
expect(response.headers.get("location")).toBe(
|
|
"http://localhost:3000/login",
|
|
);
|
|
});
|
|
|
|
it("allows access when pb_auth cookie is present", async () => {
|
|
const request = createMockRequest("/", "valid-token");
|
|
const response = await middleware(request);
|
|
|
|
expect(response.status).toBe(200);
|
|
});
|
|
|
|
it("redirects /settings to /login when unauthenticated", async () => {
|
|
const request = createMockRequest("/settings");
|
|
const response = await middleware(request);
|
|
|
|
expect(response.status).toBe(307);
|
|
expect(response.headers.get("location")).toBe(
|
|
"http://localhost:3000/login",
|
|
);
|
|
});
|
|
|
|
it("redirects /calendar to /login when unauthenticated", async () => {
|
|
const request = createMockRequest("/calendar");
|
|
const response = await middleware(request);
|
|
|
|
expect(response.status).toBe(307);
|
|
expect(response.headers.get("location")).toBe(
|
|
"http://localhost:3000/login",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("public routes", () => {
|
|
it("allows /login without authentication", async () => {
|
|
const request = createMockRequest("/login");
|
|
const response = await middleware(request);
|
|
|
|
expect(response.status).toBe(200);
|
|
});
|
|
|
|
it("allows /login even with authentication cookie", async () => {
|
|
const request = createMockRequest("/login", "valid-token");
|
|
const response = await middleware(request);
|
|
|
|
// Could redirect authenticated users away from login, but for now just allow
|
|
expect(response.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe("API routes", () => {
|
|
it("skips middleware for /api routes (handled by route-level auth)", async () => {
|
|
const request = createMockRequest("/api/user");
|
|
const response = await middleware(request);
|
|
|
|
// API routes are not processed by this middleware
|
|
expect(response.status).toBe(200);
|
|
});
|
|
|
|
it("skips middleware for /api/calendar ICS endpoint", async () => {
|
|
const request = createMockRequest("/api/calendar/user123/token.ics");
|
|
const response = await middleware(request);
|
|
|
|
expect(response.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe("static assets", () => {
|
|
it("skips middleware for _next static assets", async () => {
|
|
const request = createMockRequest("/_next/static/chunk.js");
|
|
const response = await middleware(request);
|
|
|
|
expect(response.status).toBe(200);
|
|
});
|
|
|
|
it("skips middleware for favicon", async () => {
|
|
const request = createMockRequest("/favicon.ico");
|
|
const response = await middleware(request);
|
|
|
|
expect(response.status).toBe(200);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("middleware config", () => {
|
|
it("exports a matcher configuration", () => {
|
|
expect(config).toBeDefined();
|
|
expect(config.matcher).toBeDefined();
|
|
expect(Array.isArray(config.matcher)).toBe(true);
|
|
});
|
|
});
|