Skip to content
Merged
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
18 changes: 5 additions & 13 deletions e2e/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ test("[API E2E] /api/metrics/streak returns 401 without a session", async ({
expect([401, 302, 403]).toContain(res.status());
});

test("[API E2E] /api/metrics/contributions returns 200 with valid session cookie", async ({
page,
test("[API E2E] /api/metrics/contributions accepts valid session cookie", async ({
request,
}) => {
const sessionToken = await buildSessionCookie();
Expand Down Expand Up @@ -117,20 +116,13 @@ test("[API E2E] /api/goals POST without session returns 401 or 403", async ({
});

test("[API E2E] /api/metrics/contributions with days param returns valid JSON when authenticated", async ({
page,
request,
}) => {
const sessionToken = await buildSessionCookie();

await page.context().addCookies([
{
name: "next-auth.session-token",
value: sessionToken,
domain: "127.0.0.1",
path: "/",
httpOnly: true,
sameSite: "Lax",
secure: false,
expires: Math.floor(Date.now() / 1000) + 60 * 60,
const res = await request.get("/api/metrics/contributions?days=30", {
headers: {
Cookie: `next-auth.session-token=${sessionToken}`,
},
]);

Expand Down
1 change: 1 addition & 0 deletions e2e/dashboard-widgets.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ function mockMetricResponse(url) {
longest: 9,
lastCommitDate: "2026-05-18",
totalActiveDays: 12,
freezeDates: [],
};
}
if (url.includes("/api/metrics/weekly-summary")) {
Expand Down
23 changes: 9 additions & 14 deletions e2e/dashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test";
import { encode } from "next-auth/jwt";
import { scrollToWidget } from "./helpers/dashboard-mocks";

/**
* dashboard.spec.ts
Expand Down Expand Up @@ -141,7 +142,7 @@ async function injectMockSession(page: import("@playwright/test").Page) {
await page.route("**/api/streak/freeze**", (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify({ freezes: [] }),
body: JSON.stringify({ hasFreeze: false, freezeDate: null }),
})
);

Expand Down Expand Up @@ -316,35 +317,29 @@ test("[Dashboard E2E] dashboard heading is visible after mock login", async ({
});

test("[Dashboard E2E] Commits widget renders", async ({ page }) => {
await page.goto("/dashboard", { waitUntil: "load" });
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
await expect(
page.getByRole("heading", { name: "Your Commits" })
).toBeVisible({ timeout: 10_000 });
await scrollToWidget(page, "Your Commits");
});

test("[Dashboard E2E] PR Analytics widget renders", async ({ page }) => {
await page.goto("/dashboard", { waitUntil: "load" });
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
await expect(
page.getByRole("heading", { name: "PR Analytics" })
).toBeVisible({ timeout: 10_000 });
await scrollToWidget(page, "PR Analytics");
});

test("[Dashboard E2E] Goals widget renders with mocked goal", async ({
page,
}) => {
await page.goto("/dashboard", { waitUntil: "load" });
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
await expect(
page.getByRole("heading", { name: "Goals", exact: true })
).toBeVisible({ timeout: 10_000 });
await scrollToWidget(page, "Goals");
await expect(page.getByText("Make 10 commits")).toBeVisible({
timeout: 10_000,
});
Expand Down Expand Up @@ -388,7 +383,7 @@ test("[Dashboard E2E] no uncaught console errors on dashboard load", async ({
});

test("[Dashboard E2E] weekly summary widget renders", async ({ page }) => {
await page.goto("/dashboard", { waitUntil: "load" });
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
Expand Down
74 changes: 17 additions & 57 deletions e2e/goals.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { expect, test } from "@playwright/test";
import { encode } from "next-auth/jwt";
import {
installDashboardApiMocks,
scrollToWidget,
} from "./helpers/dashboard-mocks";

/**
* goals.spec.ts
Expand Down Expand Up @@ -79,43 +83,15 @@ async function setupGoalsMocks(page: import("@playwright/test").Page) {
})
);

// Stub remaining metric routes so the page loads without errors.
const stubs = [
"**/api/metrics/contributions**",
"**/api/metrics/streak**",
"**/api/streak/freeze**",
"**/api/metrics/prs**",
"**/api/metrics/pr-breakdown**",
"**/api/metrics/pr-review-trend**",
"**/api/metrics/issues**",
"**/api/metrics/languages**",
"**/api/metrics/weekly-summary**",
"**/api/ai-insights**",
"**/api/metrics/repos**",
"**/api/metrics/pinned-repos**",
"**/api/metrics/compare**",
"**/api/metrics/repo-health**",
"**/api/metrics/ci**",
"**/api/user/github-accounts**",
"**/api/integrations/jira**",
"**/api/metrics/activity**",
"**/api/metrics/commit-time**",
"**/api/metrics/personal-records**",
"**/api/metrics/discussions**",
"**/api/metrics/inactive-repos**",
"**/api/local-coding/stats**",
"**/api/metrics/coding-time**",
"**/api/metrics/coding-activity-insights**",
"**/api/wakatime**",
"**/api/metrics/productive-hours**",
"**/api/user/pinned-repos/details**",
"**/api/metrics/repo-explorer**",
];
for (const pattern of stubs) {
await page.route(pattern, (route) =>
route.fulfill({ contentType: "application/json", body: JSON.stringify({}) })
);
}
await installDashboardApiMocks(page);
}

async function openGoalsWidget(page: import("@playwright/test").Page) {
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
await scrollToWidget(page, "Goals");
}

test("[Goals E2E] goals widget renders on dashboard", async ({ page }) => {
Expand All @@ -132,13 +108,7 @@ test("[Goals E2E] goals widget renders on dashboard", async ({ page }) => {
});
});

await page.goto("/dashboard", { waitUntil: "load" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
await expect(
page.getByRole("heading", { name: "Goals", exact: true })
).toBeVisible({ timeout: 10_000 });
await openGoalsWidget(page);
});

test("[Goals E2E] creating a goal sends POST /api/goals with correct payload", async ({
Expand All @@ -164,10 +134,7 @@ test("[Goals E2E] creating a goal sends POST /api/goals with correct payload", a
});
});

await page.goto("/dashboard", { waitUntil: "load" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
await openGoalsWidget(page);

// Scroll the goal form into view and wait for it to be interactive.
const titleInput = page.getByLabel("Goal title");
Expand Down Expand Up @@ -231,12 +198,8 @@ test("[Goals E2E] newly created goal appears in the goals list", async ({
});
});

await page.goto("/dashboard", { waitUntil: "load" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
await openGoalsWidget(page);

// Existing goal should be present.
await expect(page.getByText("Existing Goal")).toBeVisible({ timeout: 10_000 });

// Scroll goal form into view and wait for it to be interactive.
Expand Down Expand Up @@ -303,10 +266,7 @@ test("[Goals E2E] deleting a goal removes it from the list", async ({
return route.fulfill({ status: 204, body: "" });
});

await page.goto("/dashboard", { waitUntil: "load" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
await openGoalsWidget(page);
await expect(page.getByText("Goal to Delete")).toBeVisible({ timeout: 10_000 });

// Click the delete button (trash icon) next to this goal.
Expand Down
9 changes: 8 additions & 1 deletion e2e/notifications.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ function mockMetricResponse(url) {
longest: 9,
lastCommitDate: "2026-05-18",
totalActiveDays: 12,
freezeDates: [],
};
if (url.includes("/api/streak/freeze"))
return { hasFreeze: false, freezeDate: null };
if (url.includes("/api/metrics/weekly-summary"))
return {
commits: { current: 10, previous: 7, delta: 3, trend: "up" },
Expand Down Expand Up @@ -99,7 +102,11 @@ function mockMetricResponse(url) {
timezone: "UTC",
};
if (url.includes("/api/metrics/contributions"))
return { data: { "2026-05-16": 3, "2026-05-17": 5, "2026-05-18": 2 } };
return {
days: 365,
total: 10,
data: { "2026-05-16": 3, "2026-05-17": 5, "2026-05-18": 2 },
};
if (url.includes("/api/metrics/productive-hours"))
return { grid: [], peak: null, total: 0, days: 0, timezone: "UTC" };
if (url.includes("/api/user/pinned-repos/details"))
Expand Down
50 changes: 27 additions & 23 deletions e2e/streak.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { expect, test } from "@playwright/test";
import { encode } from "next-auth/jwt";

/**
* streak.spec.ts
* Covers: streak widget shows numeric values; freeze button is present.
*/
import {
installDashboardApiMocks,
scrollToWidget,
streakSection,
} from "./helpers/dashboard-mocks";

const AUTH_SECRET =
process.env.NEXTAUTH_SECRET ?? "test-nextauth-secret-for-playwright-tests";
Expand Down Expand Up @@ -163,20 +163,18 @@ test.beforeEach(async ({ page }) => {
test("[Streak E2E] streak widget section is rendered on dashboard", async ({
page,
}) => {
await page.goto("/dashboard", { waitUntil: "load" });
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
// The streak section may use "Streak", "Current Streak", or similar heading.
await expect(
page.getByRole("heading", { name: /streak/i }).first()
).toBeVisible({ timeout: 10_000 });

await scrollToWidget(page, "Commit Streaks");
});

test("[Streak E2E] streak widget shows the mocked current streak value", async ({
page,
}) => {
await page.goto("/dashboard", { waitUntil: "load" });
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
Expand All @@ -195,19 +193,25 @@ test("[Streak E2E] streak widget shows the mocked current streak value", async (
test("[Streak E2E] streak widget shows the mocked longest streak value", async ({
page,
}) => {
await page.goto("/dashboard", { waitUntil: "load" });
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });

// The mock returns longest: 21.
await expect(page.getByText(/21/).first()).toBeVisible({ timeout: 10_000 });
const section = streakSection(page);
await section.scrollIntoViewIfNeeded();
await expect(section.getByText("Longest Streak")).toBeVisible({
timeout: 15_000,
});
await expect(section.getByText("21", { exact: true })).toBeVisible({
timeout: 10_000,
});
});

test("[Streak E2E] freeze button is present in the streak widget", async ({
page,
}) => {
await page.goto("/dashboard", { waitUntil: "load" });
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });
Expand Down Expand Up @@ -240,19 +244,19 @@ test("[Streak E2E] streak freeze API is called when freeze button is clicked", a
});
});

await page.goto("/dashboard", { waitUntil: "load" });
await page.goto("/dashboard", { waitUntil: "domcontentloaded" });
await expect(
page.getByRole("heading", { name: "Dashboard", exact: true })
).toBeVisible({ timeout: 30_000 });

const freezeBtn = page
.getByRole("button", { name: /freeze|protect/i })
.first();
await expect(freezeBtn).toBeVisible({ timeout: 10_000 });
await freezeBtn.click();
const freezeButton = streakSection(page).getByRole("button", {
name: "Freeze Streak",
});
await freezeButton.scrollIntoViewIfNeeded();
await expect(freezeButton).toBeVisible({ timeout: 15_000 });
await freezeButton.click();

// Give the network request time to fire.
await expect
.poll(() => freezeRequests.length, { timeout: 8_000 })
.toBeGreaterThan(0);
});
});
6 changes: 3 additions & 3 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,17 @@ const RATE_LIMIT_CONFIG = {
/**
* Maximum allowed API metrics requests for authenticated users in the window.
*/
AUTHENTICATED_LIMIT: isDev ? 5000 : 60,
AUTHENTICATED_LIMIT: isRelaxedRateLimit ? 5000 : 60,

/**
* Maximum allowed API metrics requests for anonymous users in the window.
*/
ANONYMOUS_LIMIT: isDev ? 1000 : 10,
ANONYMOUS_LIMIT: isRelaxedRateLimit ? 1000 : 10,

/**
* Maximum allowed sign-in attempts for authentication routes in the window.
*/
AUTH_LIMIT: isDev ? 1000 : AUTH_LIMIT,
AUTH_LIMIT: isRelaxedRateLimit ? 1000 : AUTH_LIMIT,
} as const;

const memoryBuckets = new Map<string, number[]>();
Expand Down
Loading