Implement GET /api/user endpoint (P0.4)
Add authenticated user profile retrieval endpoint using withAuth wrapper. Returns user profile with safe fields, excluding encrypted tokens. Changes: - Implement GET handler in src/app/api/user/route.ts - Add 4 tests for auth, response shape, sensitive field exclusion - Add path alias resolution to vitest.config.ts for @/* imports - Update IMPLEMENTATION_PLAN.md to mark P0.4 complete Response includes: id, email, garminConnected, cycleLength, lastPeriodDate, notificationTime, timezone, activeOverrides 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
101
src/app/api/user/route.test.ts
Normal file
101
src/app/api/user/route.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// ABOUTME: Unit tests for user profile API route.
|
||||
// ABOUTME: Tests GET /api/user for profile retrieval with authentication.
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { User } from "@/types";
|
||||
|
||||
// Module-level variable to control mock user in tests
|
||||
let currentMockUser: User | null = null;
|
||||
|
||||
// Mock the auth-middleware module
|
||||
vi.mock("@/lib/auth-middleware", () => ({
|
||||
withAuth: vi.fn((handler) => {
|
||||
return async (request: NextRequest) => {
|
||||
if (!currentMockUser) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
return handler(request, currentMockUser);
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
import { GET } from "./route";
|
||||
|
||||
describe("GET /api/user", () => {
|
||||
const mockUser: User = {
|
||||
id: "user123",
|
||||
email: "test@example.com",
|
||||
garminConnected: true,
|
||||
garminOauth1Token: "encrypted-token-1",
|
||||
garminOauth2Token: "encrypted-token-2",
|
||||
garminTokenExpiresAt: new Date("2025-06-01"),
|
||||
calendarToken: "cal-secret-token",
|
||||
lastPeriodDate: new Date("2025-01-15"),
|
||||
cycleLength: 28,
|
||||
notificationTime: "07:30",
|
||||
timezone: "America/New_York",
|
||||
activeOverrides: ["flare"],
|
||||
created: new Date("2024-01-01"),
|
||||
updated: new Date("2025-01-10"),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
currentMockUser = null;
|
||||
});
|
||||
|
||||
it("returns user profile when authenticated", async () => {
|
||||
currentMockUser = mockUser;
|
||||
|
||||
const mockRequest = {} as NextRequest;
|
||||
const response = await GET(mockRequest);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
|
||||
// Should include these fields
|
||||
expect(body.id).toBe("user123");
|
||||
expect(body.email).toBe("test@example.com");
|
||||
expect(body.garminConnected).toBe(true);
|
||||
expect(body.cycleLength).toBe(28);
|
||||
expect(body.lastPeriodDate).toBe("2025-01-15");
|
||||
expect(body.notificationTime).toBe("07:30");
|
||||
expect(body.timezone).toBe("America/New_York");
|
||||
});
|
||||
|
||||
it("does not expose sensitive token fields", async () => {
|
||||
currentMockUser = mockUser;
|
||||
|
||||
const mockRequest = {} as NextRequest;
|
||||
const response = await GET(mockRequest);
|
||||
const body = await response.json();
|
||||
|
||||
// Should NOT include encrypted tokens
|
||||
expect(body.garminOauth1Token).toBeUndefined();
|
||||
expect(body.garminOauth2Token).toBeUndefined();
|
||||
expect(body.calendarToken).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includes activeOverrides array", async () => {
|
||||
currentMockUser = mockUser;
|
||||
|
||||
const mockRequest = {} as NextRequest;
|
||||
const response = await GET(mockRequest);
|
||||
const body = await response.json();
|
||||
|
||||
expect(body.activeOverrides).toEqual(["flare"]);
|
||||
});
|
||||
|
||||
it("returns 401 when not authenticated", async () => {
|
||||
currentMockUser = null;
|
||||
|
||||
const mockRequest = {} as NextRequest;
|
||||
const response = await GET(mockRequest);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
const body = await response.json();
|
||||
expect(body.error).toBe("Unauthorized");
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,30 @@
|
||||
// ABOUTME: Handles GET for profile retrieval and PATCH for updates.
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
// TODO: Implement user profile retrieval
|
||||
return NextResponse.json({ message: "Not implemented" }, { status: 501 });
|
||||
}
|
||||
import { withAuth } from "@/lib/auth-middleware";
|
||||
|
||||
/**
|
||||
* GET /api/user
|
||||
* Returns the authenticated user's profile.
|
||||
* Excludes sensitive fields like encrypted tokens.
|
||||
*/
|
||||
export const GET = withAuth(async (_request, user) => {
|
||||
// Format date for consistent API response
|
||||
const lastPeriodDate = user.lastPeriodDate
|
||||
? user.lastPeriodDate.toISOString().split("T")[0]
|
||||
: null;
|
||||
|
||||
return NextResponse.json({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
garminConnected: user.garminConnected,
|
||||
cycleLength: user.cycleLength,
|
||||
lastPeriodDate,
|
||||
notificationTime: user.notificationTime,
|
||||
timezone: user.timezone,
|
||||
activeOverrides: user.activeOverrides,
|
||||
});
|
||||
});
|
||||
|
||||
export async function PATCH() {
|
||||
// TODO: Implement user profile update
|
||||
|
||||
Reference in New Issue
Block a user