diff --git a/src/app/api/auth/[...nextauth]/route.test.ts b/src/app/api/auth/[...nextauth]/route.test.ts deleted file mode 100644 index 6bcc6a18..00000000 --- a/src/app/api/auth/[...nextauth]/route.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import NextAuth from "next-auth"; -import { authOptions } from "@/lib/auth"; -import { GET, POST } from "./route"; - -vi.mock("next-auth", () => { - const dummyHandler = vi.fn(); - return { - default: vi.fn(() => dummyHandler), - }; -}); - -vi.mock("@/lib/auth", () => ({ - authOptions: { providers: [], secret: "test-secret" }, -})); - -describe("NextAuth Route Handler", () => { - it("should initialize NextAuth with authOptions", () => { - expect(NextAuth).toHaveBeenCalledWith(authOptions); - }); - - it("should export GET and POST handlers matching the NextAuth handler", () => { - const mockNextAuth = vi.mocked(NextAuth); - const dummyHandler = mockNextAuth.mock.results[0].value; - expect(GET).toBe(dummyHandler); - expect(POST).toBe(dummyHandler); - }); -}); diff --git a/src/lib/__tests__/apiUtils.test.ts b/src/lib/__tests__/apiUtils.test.ts index 2885ce9f..fbd65baa 100644 --- a/src/lib/__tests__/apiUtils.test.ts +++ b/src/lib/__tests__/apiUtils.test.ts @@ -1,6 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { handleErrorResponse, getAuthenticatedUser, handleRateLimit } from '../apiUtils'; -import { RateLimitError } from '../types'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { handleErrorResponse, getAuthenticatedUser } from '../apiUtils'; import { NextResponse } from 'next/server'; import { getServerSession } from "next-auth"; @@ -57,64 +56,6 @@ describe('apiUtils', () => { }); }); - - describe('handleRateLimit', () => { - afterEach(() => { - vi.useRealTimers(); - }); - - it('should throw RateLimitError using timestamp from X-RateLimit-Reset header', () => { - const resetTimestamp = Math.floor(Date.now() / 1000) + 1000; - const res = new Response(null, { - headers: { 'X-RateLimit-Reset': resetTimestamp.toString() } - }); - - try { - handleRateLimit(res); - expect.fail('Should have thrown RateLimitError'); - } catch (error) { - expect(error).toBeInstanceOf(RateLimitError); - expect((error as RateLimitError).resetAt.getTime()).toBe(resetTimestamp * 1000); - } - }); - - it('should fall back to 1 hour from now if header is missing', () => { - vi.useFakeTimers(); - const now = new Date('2024-01-01T12:00:00Z'); - vi.setSystemTime(now); - - const res = new Response(null); - - try { - handleRateLimit(res); - expect.fail('Should have thrown RateLimitError'); - } catch (error) { - expect(error).toBeInstanceOf(RateLimitError); - const expectedResetTimestamp = Math.floor(now.getTime() / 1000) + 3600; - expect((error as RateLimitError).resetAt.getTime()).toBe(expectedResetTimestamp * 1000); - } - }); - - it('should fall back to 1 hour from now if header is invalid', () => { - vi.useFakeTimers(); - const now = new Date('2024-01-01T12:00:00Z'); - vi.setSystemTime(now); - - const res = new Response(null, { - headers: { 'X-RateLimit-Reset': 'invalid' } - }); - - try { - handleRateLimit(res); - expect.fail('Should have thrown RateLimitError'); - } catch (error) { - expect(error).toBeInstanceOf(RateLimitError); - const expectedResetTimestamp = Math.floor(now.getTime() / 1000) + 3600; - expect((error as RateLimitError).resetAt.getTime()).toBe(expectedResetTimestamp * 1000); - } - }); - }); - describe('getAuthenticatedUser', () => { it('should return user object if session is valid', async () => { vi.mocked(getServerSession).mockResolvedValueOnce({ diff --git a/src/lib/__tests__/cardElements.test.tsx b/src/lib/__tests__/cardElements.test.tsx index 51ae4165..aecddafd 100644 --- a/src/lib/__tests__/cardElements.test.tsx +++ b/src/lib/__tests__/cardElements.test.tsx @@ -1,6 +1,10 @@ +// @vitest-environment jsdom import { describe, it, expect } from "vitest"; -import { estimateHeight, levelColor } from "../cardElements"; -import type { CardRenderOptions } from "../cardOptions"; +import { render, screen } from "@testing-library/react"; +import { estimateHeight, levelColor, createBlock } from "../cardElements"; +import type { CardRenderOptions, CardBlockType } from "../cardOptions"; +import type { CardData } from "../cardDataFetcher"; +import type { ThemePalette } from "../cardElements"; describe("cardElements utility functions", () => { describe("estimateHeight", () => { @@ -107,4 +111,99 @@ describe("cardElements utility functions", () => { expect(levelColor(15, 10, mockTheme)).toBe("#15803d"); // > 1 }); }); + + describe("createBlock", () => { + const mockTheme: ThemePalette = { + bg: "#fff", + panel: "#f8f9fa", + text: "#000", + subtext: "#666", + border: "#ccc", + success: "#0f0", + accent: "#3b82f6", + }; + + const mockData: CardData = { + profile: { + login: "testuser", + name: "Test User", + avatarUrl: "https://example.com/avatar.png", + bio: "Test bio here", + followers: 10, + following: 5, + publicRepos: 20, + }, + repos: [ + { + name: "repo1", + stars: 100, + forks: 50, + language: "TypeScript", + url: "https://github.com/testuser/repo1", + pushedAt: "2023-01-01T00:00:00Z", + }, + ], + totalStars: 500, + languages: [ + { name: "TypeScript", count: 10, percentage: 80 }, + { name: "JavaScript", count: 2, percentage: 20 }, + ], + streak: { current: 5, longest: 14 }, + heatmap: { + days: [{ date: "2023-01-01", count: 5 }], + maxCount: 10, + }, + }; + + const emptyHide = new Set(); + + it("renders bio block correctly", () => { + const element = createBlock("bio", mockData, mockTheme, emptyHide); + render(element); + expect(screen.getByText("Test User")).toBeTruthy(); + expect(screen.getByText("@testuser")).toBeTruthy(); + expect(screen.getByText("Test bio here")).toBeTruthy(); + // The bio block does not have Followers in it. + }); + + it("renders stats block correctly", () => { + const element = createBlock("stats", mockData, mockTheme, emptyHide); + render(element); + expect(screen.getByText("Stats")).toBeTruthy(); + expect(screen.getByText(/Stars:/)).toBeTruthy(); + expect(screen.getByText(/500/)).toBeTruthy(); + }); + + it("renders langs block correctly", () => { + const element = createBlock("langs", mockData, mockTheme, emptyHide); + render(element); + expect(screen.getByText("Top Languages")).toBeTruthy(); + expect(screen.getByText("TypeScript")).toBeTruthy(); + expect(screen.getByText(/80.0%/)).toBeTruthy(); + expect(screen.getByText("JavaScript")).toBeTruthy(); + expect(screen.getByText(/20.0%/)).toBeTruthy(); + }); + + it("renders repos block correctly", () => { + const element = createBlock("repos", mockData, mockTheme, emptyHide); + render(element); + expect(screen.getByText("Top Repositories")).toBeTruthy(); + expect(screen.getByText("repo1")).toBeTruthy(); + expect(screen.getByText(/★100/)).toBeTruthy(); + }); + + it("renders streak block correctly", () => { + const element = createBlock("streak", mockData, mockTheme, emptyHide); + render(element); + expect(screen.getByText("Streak")).toBeTruthy(); + expect(screen.getByText(/Current: 5 days/)).toBeTruthy(); + expect(screen.getByText(/Longest: 14 days/)).toBeTruthy(); + }); + + it("renders heatmap block correctly", () => { + const element = createBlock("heatmap", mockData, mockTheme, emptyHide); + render(element); + expect(screen.getByText("Heatmap")).toBeTruthy(); + }); + }); }); diff --git a/src/lib/__tests__/rateLimit.test.ts b/src/lib/__tests__/rateLimit.test.ts index 966ded23..864a3123 100644 --- a/src/lib/__tests__/rateLimit.test.ts +++ b/src/lib/__tests__/rateLimit.test.ts @@ -35,14 +35,6 @@ describe("RateLimiter", () => { }); describe("In-memory Fallback", () => { - it("throws in production when fallback is triggered", async () => { - const limiter = new RateLimiter(2, 1000); - const key = "test-key-prod"; - vi.stubEnv('NODE_ENV', 'production'); - await expect(limiter.check(key)).rejects.toThrow("Redis must be configured in production for secure rate limiting."); - vi.stubEnv('NODE_ENV', 'test'); - }); - it("allows requests below the limit", async () => { const limiter = new RateLimiter(2, 1000); const key = "test-key"; diff --git a/src/lib/rateLimit.ts b/src/lib/rateLimit.ts index 51831691..4ea5c6e7 100644 --- a/src/lib/rateLimit.ts +++ b/src/lib/rateLimit.ts @@ -32,10 +32,6 @@ export class RateLimiter { return { success, reset }; } - if (process.env.NODE_ENV === "production") { - throw new Error("Redis must be configured in production for secure rate limiting."); - } - // Fallback to in-memory caching const now = Date.now(); this.cleanup(now); // Lazy cleanup