Implement auth middleware for API routes (P0.2)
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>
This commit is contained in:
164
src/lib/auth-middleware.test.ts
Normal file
164
src/lib/auth-middleware.test.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
// ABOUTME: Unit tests for API route authentication middleware.
|
||||
// ABOUTME: Tests withAuth wrapper for protected route handlers.
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { withAuth } from "./auth-middleware";
|
||||
|
||||
// Mock the pocketbase module
|
||||
vi.mock("./pocketbase", () => ({
|
||||
createPocketBaseClient: vi.fn(),
|
||||
loadAuthFromCookies: vi.fn(),
|
||||
isAuthenticated: vi.fn(),
|
||||
getCurrentUser: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock next/headers
|
||||
vi.mock("next/headers", () => ({
|
||||
cookies: vi.fn(),
|
||||
}));
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import {
|
||||
createPocketBaseClient,
|
||||
getCurrentUser,
|
||||
isAuthenticated,
|
||||
loadAuthFromCookies,
|
||||
} from "./pocketbase";
|
||||
|
||||
const mockCookies = cookies as ReturnType<typeof vi.fn>;
|
||||
const mockCreatePocketBaseClient = createPocketBaseClient as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const mockLoadAuthFromCookies = loadAuthFromCookies as ReturnType<typeof vi.fn>;
|
||||
const mockIsAuthenticated = isAuthenticated as ReturnType<typeof vi.fn>;
|
||||
const mockGetCurrentUser = getCurrentUser as ReturnType<typeof vi.fn>;
|
||||
|
||||
describe("withAuth", () => {
|
||||
const mockUser = {
|
||||
id: "user123",
|
||||
email: "test@example.com",
|
||||
garminConnected: false,
|
||||
garminOauth1Token: "",
|
||||
garminOauth2Token: "",
|
||||
garminTokenExpiresAt: new Date(),
|
||||
calendarToken: "cal-token",
|
||||
lastPeriodDate: new Date("2025-01-01"),
|
||||
cycleLength: 31,
|
||||
notificationTime: "07:00",
|
||||
timezone: "UTC",
|
||||
activeOverrides: [],
|
||||
created: new Date(),
|
||||
updated: new Date(),
|
||||
};
|
||||
|
||||
const mockPbClient = {
|
||||
authStore: {
|
||||
isValid: true,
|
||||
model: mockUser,
|
||||
},
|
||||
};
|
||||
|
||||
const mockCookieStore = {
|
||||
get: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockCookies.mockResolvedValue(mockCookieStore);
|
||||
mockCreatePocketBaseClient.mockReturnValue(mockPbClient);
|
||||
});
|
||||
|
||||
it("returns 401 when not authenticated", async () => {
|
||||
mockIsAuthenticated.mockReturnValue(false);
|
||||
|
||||
const handler = vi.fn();
|
||||
const wrappedHandler = withAuth(handler);
|
||||
|
||||
const mockRequest = {} as NextRequest;
|
||||
const response = await wrappedHandler(mockRequest);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
const body = await response.json();
|
||||
expect(body.error).toBe("Unauthorized");
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls handler with user when authenticated", async () => {
|
||||
mockIsAuthenticated.mockReturnValue(true);
|
||||
mockGetCurrentUser.mockReturnValue(mockUser);
|
||||
|
||||
const handler = vi
|
||||
.fn()
|
||||
.mockResolvedValue(NextResponse.json({ data: "success" }));
|
||||
const wrappedHandler = withAuth(handler);
|
||||
|
||||
const mockRequest = {} as NextRequest;
|
||||
const response = await wrappedHandler(mockRequest);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(handler).toHaveBeenCalledWith(mockRequest, mockUser, undefined);
|
||||
});
|
||||
|
||||
it("loads auth from cookies before checking authentication", async () => {
|
||||
mockIsAuthenticated.mockReturnValue(true);
|
||||
mockGetCurrentUser.mockReturnValue(mockUser);
|
||||
|
||||
const handler = vi.fn().mockResolvedValue(NextResponse.json({}));
|
||||
const wrappedHandler = withAuth(handler);
|
||||
|
||||
await wrappedHandler({} as NextRequest);
|
||||
|
||||
expect(mockCreatePocketBaseClient).toHaveBeenCalled();
|
||||
expect(mockCookies).toHaveBeenCalled();
|
||||
expect(mockLoadAuthFromCookies).toHaveBeenCalledWith(
|
||||
mockPbClient,
|
||||
mockCookieStore,
|
||||
);
|
||||
expect(mockIsAuthenticated).toHaveBeenCalledWith(mockPbClient);
|
||||
});
|
||||
|
||||
it("returns 401 when getCurrentUser returns null", async () => {
|
||||
mockIsAuthenticated.mockReturnValue(true);
|
||||
mockGetCurrentUser.mockReturnValue(null);
|
||||
|
||||
const handler = vi.fn();
|
||||
const wrappedHandler = withAuth(handler);
|
||||
|
||||
const response = await wrappedHandler({} as NextRequest);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes route params to handler when provided", async () => {
|
||||
mockIsAuthenticated.mockReturnValue(true);
|
||||
mockGetCurrentUser.mockReturnValue(mockUser);
|
||||
|
||||
const handler = vi.fn().mockResolvedValue(NextResponse.json({}));
|
||||
const wrappedHandler = withAuth(handler);
|
||||
|
||||
const mockRequest = {} as NextRequest;
|
||||
const mockParams = { id: "123" };
|
||||
|
||||
await wrappedHandler(mockRequest, { params: mockParams });
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(mockRequest, mockUser, {
|
||||
params: mockParams,
|
||||
});
|
||||
});
|
||||
|
||||
it("handles handler errors gracefully", async () => {
|
||||
mockIsAuthenticated.mockReturnValue(true);
|
||||
mockGetCurrentUser.mockReturnValue(mockUser);
|
||||
|
||||
const handler = vi.fn().mockRejectedValue(new Error("Handler error"));
|
||||
const wrappedHandler = withAuth(handler);
|
||||
|
||||
const response = await wrappedHandler({} as NextRequest);
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
const body = await response.json();
|
||||
expect(body.error).toBe("Internal server error");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user