Skip to content

Commit fef6257

Browse files
authored
test(api validation): Veried query validation boundaries for grace params (JhaSourav07#2184)
## Description - Added schema-level `refine` to `grace` field in `lib/validations.ts` — rejects values outside `[0, 7]` (integers only) with error message `'grace must be an integer between 0 and 7'` instead of silently clamping - Added test in `app/api/streak/route.test.ts`: `grace=-1` returns 400 with the correct field error - Updated existing `grace=999` test to expect 400 (was 200 under old clamping behavior) Fixes JhaSourav07#1454 ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have started the repo. - [x] I have made sure that i have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [ ] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
2 parents ed2bdb7 + ab67539 commit fef6257

3 files changed

Lines changed: 44 additions & 23 deletions

File tree

app/api/streak/route.test.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ describe('GET /api/streak', () => {
103103
});
104104

105105
describe('parameter validation', () => {
106+
it('returns 400 when grace=-1 is provided', async () => {
107+
const response = await GET(
108+
makeRequest({
109+
user: 'octocat',
110+
grace: '-1',
111+
})
112+
);
113+
expect(response.status).toBe(400);
114+
const body = await response.json();
115+
expect(body.details.fieldErrors.grace[0]).toBe('grace must be an integer between 0 and 7');
116+
});
117+
106118
it('returns 400 Bad Request when ?layout= is set to an unsupported format', async () => {
107119
const response = await GET(
108120
makeRequest({
@@ -305,18 +317,15 @@ describe('GET /api/streak', () => {
305317
expect(fetchGitHubContributions).toHaveBeenCalled();
306318
});
307319

308-
it('returns valid SVG when grace exceeds max value', async () => {
320+
it('returns 400 when grace exceeds max value', async () => {
309321
const response = await GET(
310322
makeRequest({
311323
user: 'octocat',
312324
grace: '999',
313325
})
314326
);
315327

316-
expect(response.status).toBe(200);
317-
318-
const body = await response.text();
319-
expect(body).toContain('<svg');
328+
expect(response.status).toBe(400);
320329
});
321330

322331
it('embeds the username (uppercased) in the SVG title', async () => {

lib/validations.test.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,29 @@ describe('streakParamsSchema — grace fallback behavior', () => {
1010
expect(parse({ grace: '7' }).grace).toBe(7);
1111
});
1212

13-
it('clamps "8" to 7', () => {
14-
expect(parse({ grace: '8' }).grace).toBe(7);
13+
it('rejects "8" as out-of-range', () => {
14+
const result = streakParamsSchema.safeParse({ user: 'octocat', grace: '8' });
15+
expect(result.success).toBe(false);
1516
});
1617

17-
it('clamps "-1" to 0', () => {
18-
expect(parse({ grace: '-1' }).grace).toBe(0);
18+
it('rejects "-1" and returns correct error message', () => {
19+
const result = streakParamsSchema.safeParse({ user: 'octocat', grace: '-1' });
20+
expect(result.success).toBe(false);
21+
if (!result.success) {
22+
expect(result.error.flatten().fieldErrors.grace?.[0]).toBe(
23+
'grace must be an integer between 0 and 7'
24+
);
25+
}
1926
});
2027

21-
it('falls back or clamps a negative non-integer grace input safely', () => {
22-
const result = streakParamsSchema.safeParse({
23-
user: 'octocat',
24-
grace: '-1.5',
25-
});
26-
27-
expect(result.success).toBe(true);
28-
if (result.success) {
29-
expect(result.data.grace).toBe(0);
30-
}
28+
it('rejects a negative non-integer grace input', () => {
29+
const result = streakParamsSchema.safeParse({ user: 'octocat', grace: '-1.5' });
30+
expect(result.success).toBe(false);
3131
});
3232

33-
it('falls back to 1 for non-numeric grace value', () => {
34-
expect(parse({ grace: 'abc' }).grace).toBe(1);
33+
it('rejects non-numeric grace value', () => {
34+
const result = streakParamsSchema.safeParse({ user: 'octocat', grace: 'abc' });
35+
expect(result.success).toBe(false);
3536
});
3637

3738
it('defaults to 1 when grace is omitted', () => {

lib/validations.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,18 @@ const baseStreakParamsSchema = z.object({
223223
delta_format: z.enum(['percent', 'absolute', 'both']).catch('percent').default('percent'),
224224
width: dimensionParam('width', 100, 1200),
225225
height: dimensionParam('height', 80, 800),
226-
grace: z.string().optional().transform(toGraceValue).default(1),
227-
opacity: z.string().optional().transform(toOpacityValue).default(1.0),
226+
grace: z
227+
.string()
228+
.optional()
229+
.refine(
230+
(val) => {
231+
if (val === undefined) return true;
232+
const parsed = Number(val);
233+
return !isNaN(parsed) && Number.isInteger(parsed) && parsed >= 0 && parsed <= 7;
234+
},
235+
{ message: 'grace must be an integer between 0 and 7' }
236+
)
237+
.transform((val) => (val === undefined ? 1 : Number(val))),
228238
mode: z.enum(['commits', 'loc']).catch('commits').default('commits'),
229239
repo: z.string().optional(),
230240
org: z
@@ -271,6 +281,7 @@ const baseStreakParamsSchema = z.object({
271281
.transform((val) => val === 'true' || val === '1'),
272282
// Glow effect — on by default. Accepts 'true'/'1' (true) or 'false' (false).
273283
glow: z.string().optional().transform(toBooleanFlag).default(true),
284+
opacity: z.string().optional().transform(toOpacityValue),
274285
entrance: z.enum(['rise', 'fade', 'slide', 'none']).catch('rise').default('rise'),
275286

276287
// Output format: 'svg' (default) or 'json' for programmatic access.

0 commit comments

Comments
 (0)