Add spec compliance improvements: seed switch alert, calendar emojis, period indicator, IP logging
Some checks failed
CI / quality (push) Failing after 28s
Deploy / deploy (push) Successful in 2m38s

- NutritionPanel: Display seed switch alert on day 15 per dashboard spec
- MonthView: Add phase emojis to legend (🩸🌱🌸🌙🌑) per calendar spec
- DayCell: Show period indicator (🩸) for days 1-3 per calendar spec
- Auth middleware: Log client IP from x-forwarded-for/x-real-ip per observability spec
- Updated NutritionGuidance type to include seedSwitchAlert field
- /api/today now returns seedSwitchAlert in nutrition response

Test coverage: 1005 tests (15 new tests added)
- nutrition-panel.test.tsx: +4 tests
- month-view.test.tsx: +1 test
- day-cell.test.tsx: +5 tests
- auth-middleware.test.ts: +3 tests
- today/route.test.ts: +2 tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-12 23:33:14 +00:00
parent d613417e47
commit eeeece17bf
12 changed files with 293 additions and 48 deletions

View File

@@ -79,6 +79,16 @@ describe("withAuth", () => {
get: vi.fn(),
};
// Helper to create mock request with headers
const createMockRequest = (
headers: Record<string, string | null> = {},
): NextRequest =>
({
headers: {
get: vi.fn((name: string) => headers[name] ?? null),
},
}) as unknown as NextRequest;
beforeEach(() => {
vi.clearAllMocks();
mockCookies.mockResolvedValue(mockCookieStore);
@@ -91,7 +101,7 @@ describe("withAuth", () => {
const handler = vi.fn();
const wrappedHandler = withAuth(handler);
const mockRequest = {} as NextRequest;
const mockRequest = createMockRequest();
const response = await wrappedHandler(mockRequest);
expect(response.status).toBe(401);
@@ -109,7 +119,7 @@ describe("withAuth", () => {
.mockResolvedValue(NextResponse.json({ data: "success" }));
const wrappedHandler = withAuth(handler);
const mockRequest = {} as NextRequest;
const mockRequest = createMockRequest();
const response = await wrappedHandler(mockRequest);
expect(response.status).toBe(200);
@@ -128,7 +138,7 @@ describe("withAuth", () => {
const handler = vi.fn().mockResolvedValue(NextResponse.json({}));
const wrappedHandler = withAuth(handler);
await wrappedHandler({} as NextRequest);
await wrappedHandler(createMockRequest());
expect(mockCreatePocketBaseClient).toHaveBeenCalled();
expect(mockCookies).toHaveBeenCalled();
@@ -146,7 +156,7 @@ describe("withAuth", () => {
const handler = vi.fn();
const wrappedHandler = withAuth(handler);
const response = await wrappedHandler({} as NextRequest);
const response = await wrappedHandler(createMockRequest());
expect(response.status).toBe(401);
expect(handler).not.toHaveBeenCalled();
@@ -159,7 +169,7 @@ describe("withAuth", () => {
const handler = vi.fn().mockResolvedValue(NextResponse.json({}));
const wrappedHandler = withAuth(handler);
const mockRequest = {} as NextRequest;
const mockRequest = createMockRequest();
const mockParams = { id: "123" };
await wrappedHandler(mockRequest, { params: mockParams });
@@ -176,7 +186,7 @@ describe("withAuth", () => {
const handler = vi.fn().mockRejectedValue(new Error("Handler error"));
const wrappedHandler = withAuth(handler);
const response = await wrappedHandler({} as NextRequest);
const response = await wrappedHandler(createMockRequest());
expect(response.status).toBe(500);
const body = await response.json();
@@ -196,7 +206,7 @@ describe("withAuth", () => {
const handler = vi.fn();
const wrappedHandler = withAuth(handler);
await wrappedHandler({} as NextRequest);
await wrappedHandler(createMockRequest());
expect(mockLogger.warn).toHaveBeenCalledWith(
expect.objectContaining({ reason: "not_authenticated" }),
@@ -204,6 +214,76 @@ describe("withAuth", () => {
);
});
it("logs auth failure with IP address from x-forwarded-for header", async () => {
mockIsAuthenticated.mockReturnValue(false);
const handler = vi.fn();
const wrappedHandler = withAuth(handler);
const mockRequest = {
headers: {
get: vi.fn((name: string) =>
name === "x-forwarded-for" ? "192.168.1.100" : null,
),
},
} as unknown as NextRequest;
await wrappedHandler(mockRequest);
expect(mockLogger.warn).toHaveBeenCalledWith(
expect.objectContaining({
reason: "not_authenticated",
ip: "192.168.1.100",
}),
expect.stringContaining("Auth failure"),
);
});
it("logs auth failure with IP address from x-real-ip header when x-forwarded-for not present", async () => {
mockIsAuthenticated.mockReturnValue(false);
const handler = vi.fn();
const wrappedHandler = withAuth(handler);
const mockRequest = {
headers: {
get: vi.fn((name: string) =>
name === "x-real-ip" ? "10.0.0.1" : null,
),
},
} as unknown as NextRequest;
await wrappedHandler(mockRequest);
expect(mockLogger.warn).toHaveBeenCalledWith(
expect.objectContaining({
reason: "not_authenticated",
ip: "10.0.0.1",
}),
expect.stringContaining("Auth failure"),
);
});
it("logs auth failure with unknown IP when no IP headers present", async () => {
mockIsAuthenticated.mockReturnValue(false);
const handler = vi.fn();
const wrappedHandler = withAuth(handler);
const mockRequest = {
headers: {
get: vi.fn(() => null),
},
} as unknown as NextRequest;
await wrappedHandler(mockRequest);
expect(mockLogger.warn).toHaveBeenCalledWith(
expect.objectContaining({ reason: "not_authenticated", ip: "unknown" }),
expect.stringContaining("Auth failure"),
);
});
it("logs auth failure when getCurrentUser returns null", async () => {
mockIsAuthenticated.mockReturnValue(true);
mockGetCurrentUser.mockReturnValue(null);
@@ -211,7 +291,7 @@ describe("withAuth", () => {
const handler = vi.fn();
const wrappedHandler = withAuth(handler);
await wrappedHandler({} as NextRequest);
await wrappedHandler(createMockRequest());
expect(mockLogger.warn).toHaveBeenCalledWith(
expect.objectContaining({ reason: "user_not_found" }),
@@ -227,7 +307,7 @@ describe("withAuth", () => {
const handler = vi.fn().mockRejectedValue(testError);
const wrappedHandler = withAuth(handler);
await wrappedHandler({} as NextRequest);
await wrappedHandler(createMockRequest());
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({