Skip to content

Commit 05b77c6

Browse files
author
kali
committed
fix(api): validate monthly badge dimensions
1 parent 3187139 commit 05b77c6

3 files changed

Lines changed: 52 additions & 4 deletions

File tree

app/api/streak/route.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,25 @@ describe('GET /api/streak', () => {
9090

9191
expect(fetchGitHubContributions).not.toHaveBeenCalled();
9292
});
93+
94+
it('returns 400 for invalid monthly badge dimensions', async () => {
95+
const invalidDimensionParams: Array<Record<string, string>> = [
96+
{ width: 'abc' },
97+
{ width: '-50' },
98+
{ width: '1201' },
99+
{ height: 'abc' },
100+
{ height: '0' },
101+
{ height: '801' },
102+
];
103+
104+
for (const params of invalidDimensionParams) {
105+
const response = await GET(makeRequest({ user: 'octocat', view: 'monthly', ...params }));
106+
107+
expect(response.status).toBe(400);
108+
}
109+
110+
expect(fetchGitHubContributions).not.toHaveBeenCalled();
111+
});
93112
});
94113

95114
describe('successful response', () => {
@@ -607,6 +626,18 @@ describe('GET /api/streak', () => {
607626
expect(body).toContain('COMMITS THIS MONTH');
608627
});
609628

629+
it('uses valid custom width and height in monthly SVG output', async () => {
630+
const response = await GET(
631+
makeRequest({ user: 'octocat', view: 'monthly', width: '400', height: '200' })
632+
);
633+
const body = await response.text();
634+
635+
expect(response.status).toBe(200);
636+
expect(body).toContain('width="400"');
637+
expect(body).toContain('height="200"');
638+
expect(body).toContain('viewBox="0 0 400 200"');
639+
});
640+
610641
it('defaults to default view when an unknown view is given', async () => {
611642
const response = await GET(makeRequest({ user: 'octocat', view: 'invalid' }));
612643

app/api/streak/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ export async function GET(request: Request) {
129129
lang,
130130
view,
131131
delta_format,
132-
width: width ? parseInt(width, 10) : undefined,
133-
height: height ? parseInt(height, 10) : undefined,
132+
width,
133+
height,
134134
size,
135135
grace,
136136
};

lib/validations.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
import { z } from 'zod';
22
import { sanitizeHexColor, sanitizeSpeed, sanitizeRadius, sanitizeFont } from './svg/sanitizer';
33

4+
function dimensionParam(name: string, min: number, max: number) {
5+
return z
6+
.string()
7+
.optional()
8+
.refine(
9+
(val) => {
10+
if (val === undefined) return true;
11+
if (!/^\d+$/.test(val)) return false;
12+
13+
const parsed = Number(val);
14+
return parsed >= min && parsed <= max;
15+
},
16+
{ message: `${name} must be an integer between ${min} and ${max}` }
17+
)
18+
.transform((val) => (val === undefined ? undefined : Number(val)));
19+
}
20+
421
export const streakParamsSchema = z.object({
522
// Required — missing user surfaces as "Missing" to match existing tests
623
user: z
@@ -83,8 +100,8 @@ export const streakParamsSchema = z.object({
83100
view: z.enum(['default', 'monthly']).catch('default').default('default'),
84101
// Invalid delta formats fall back to percentage mode.
85102
delta_format: z.enum(['percent', 'absolute', 'both']).catch('percent').default('percent'),
86-
width: z.string().optional(),
87-
height: z.string().optional(),
103+
width: dimensionParam('width', 100, 1200),
104+
height: dimensionParam('height', 80, 800),
88105
grace: z
89106
.string()
90107
.optional()

0 commit comments

Comments
 (0)