Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 0 additions & 28 deletions src/app/api/auth/[...nextauth]/route.test.ts

This file was deleted.

63 changes: 2 additions & 61 deletions src/lib/__tests__/apiUtils.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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({
Expand Down
103 changes: 101 additions & 2 deletions src/lib/__tests__/cardElements.test.tsx
Original file line number Diff line number Diff line change
@@ -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";

Check warning on line 5 in src/lib/__tests__/cardElements.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

'CardBlockType' is defined but never used
import type { CardData } from "../cardDataFetcher";
import type { ThemePalette } from "../cardElements";

describe("cardElements utility functions", () => {
describe("estimateHeight", () => {
Expand Down Expand Up @@ -107,4 +111,99 @@
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<string>();

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();
});
});
});
8 changes: 0 additions & 8 deletions src/lib/__tests__/rateLimit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 0 additions & 4 deletions src/lib/rateLimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading