From 70e4df8bcec3fb9c2781ac7edf90d10e3dd9149d Mon Sep 17 00:00:00 2001 From: AbhishekMauryaGEEK Date: Sat, 6 Jun 2026 00:27:21 +0530 Subject: [PATCH] fix: enforce rate limits in maintainer actions --- src/app/actions/maintainer.test.ts | 39 ++++++++++++++++++++++++++++++ src/app/actions/maintainer.ts | 24 +++++++++++++++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/app/actions/maintainer.test.ts b/src/app/actions/maintainer.test.ts index 477cbb91..39b2109a 100644 --- a/src/app/actions/maintainer.test.ts +++ b/src/app/actions/maintainer.test.ts @@ -6,6 +6,10 @@ import { getCommunityLinks, upsertCommunityLink, deleteCommunityLink, + getRepoHealthOverview, + getStaleIssues, + getTopContributors, + getFlaggedAccounts, } from './maintainer'; import * as detect from '@/lib/maintainer/detect'; import * as rateLimitLib from '@/lib/rate-limit'; @@ -286,4 +290,39 @@ describe('maintainer actions', () => { if (!res.ok) expect(res.error.code).toBe('not_found'); }); }); + it('getRepoHealthOverview returns rate_limited when rate limit exceeded', async () => { + vi.mocked(rateLimitLib.rateLimit).mockResolvedValue({ ok: false } as never); + + const res = await getRepoHealthOverview(); + + expect(res.ok).toBe(false); + if (!res.ok) expect(res.error.code).toBe('rate_limited'); + }); + + it('getStaleIssues returns rate_limited when rate limit exceeded', async () => { + vi.mocked(rateLimitLib.rateLimit).mockResolvedValue({ ok: false } as never); + + const res = await getStaleIssues(); + + expect(res.ok).toBe(false); + if (!res.ok) expect(res.error.code).toBe('rate_limited'); + }); + + it('getTopContributors returns rate_limited when rate limit exceeded', async () => { + vi.mocked(rateLimitLib.rateLimit).mockResolvedValue({ ok: false } as never); + + const res = await getTopContributors(); + + expect(res.ok).toBe(false); + if (!res.ok) expect(res.error.code).toBe('rate_limited'); + }); + + it('getFlaggedAccounts returns rate_limited when rate limit exceeded', async () => { + vi.mocked(rateLimitLib.rateLimit).mockResolvedValue({ ok: false } as never); + + const res = await getFlaggedAccounts(); + + expect(res.ok).toBe(false); + if (!res.ok) expect(res.error.code).toBe('rate_limited'); + }); }); diff --git a/src/app/actions/maintainer.ts b/src/app/actions/maintainer.ts index 93e44551..f07bedf4 100644 --- a/src/app/actions/maintainer.ts +++ b/src/app/actions/maintainer.ts @@ -633,13 +633,17 @@ export async function getRepoHealthOverview(): Promise> return err('not_authenticated', 'sign in first'); } - await rateLimit({ + const limited = await rateLimit({ namespace: 'maintainer', key: user.id, limit: 30, windowSec: 60, }); + if (!limited.ok) { + return err('rate_limited', 'slow down', true); + } + if (!(await isUserMaintainer(user.id))) { return err('not_authorised', 'not a maintainer'); } @@ -722,13 +726,17 @@ export async function getStaleIssues(): Promise> { return err('not_authenticated', 'sign in first'); } - await rateLimit({ + const limited = await rateLimit({ namespace: 'maintainer', key: user.id, limit: 30, windowSec: 60, }); + if (!limited.ok) { + return err('rate_limited', 'slow down', true); + } + if (!(await isUserMaintainer(user.id))) { return err('not_authorised', 'not a maintainer'); } @@ -804,13 +812,17 @@ export async function getTopContributors(): Promise> { return err('not_authenticated', 'sign in first'); } - await rateLimit({ + const limited = await rateLimit({ namespace: 'maintainer', key: user.id, limit: 30, windowSec: 60, }); + if (!limited.ok) { + return err('rate_limited', 'slow down', true); + } + if (!(await isUserMaintainer(user.id))) { return err('not_authorised', 'not a maintainer'); } @@ -1096,13 +1108,17 @@ export async function getFlaggedAccounts(): Promise> return err('not_authenticated', 'sign in first'); } - await rateLimit({ + const limited = await rateLimit({ namespace: 'maintainer', key: user.id, limit: 30, windowSec: 60, }); + if (!limited.ok) { + return err('rate_limited', 'slow down', true); + } + if (!(await isUserMaintainer(user.id))) { return err('not_authorised', 'not a maintainer'); }