Skip to content

Commit 3653479

Browse files
rashi-gupta-08rashi-gupta-08Aamod007
authored
fix: add schema-level validation for grace parameter (0-7) (JhaSourav07#3075)
* fix: add schema-level validation for grace parameter (0-7) * fix: handle empty grace string by defaulting to 1 * chore: reorder tests and install vitest dependencies * fix test and typecheck issues --------- Co-authored-by: rashi-gupta-08 <gupta08rashi@gmail.com> Co-authored-by: Aamod Kumar <119789532+Aamod007@users.noreply.github.com>
1 parent c9602dd commit 3653479

5 files changed

Lines changed: 153 additions & 54 deletions

File tree

app/api/streak/route.test.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,18 @@ describe('GET /api/streak', () => {
116116
expect(body.details.fieldErrors.grace[0]).toBe('grace must be an integer between 0 and 7');
117117
});
118118

119+
it('returns 400 when grace exceeds max value', async () => {
120+
const response = await GET(
121+
makeRequest({
122+
user: 'octocat',
123+
grace: '999',
124+
})
125+
);
126+
expect(response.status).toBe(400);
127+
const body = await response.json();
128+
expect(body.details.fieldErrors.grace[0]).toBe('grace must be an integer between 0 and 7');
129+
});
130+
119131
it('returns 400 when days=0 is provided', async () => {
120132
const response = await GET(
121133
makeRequest({
@@ -410,17 +422,6 @@ describe('GET /api/streak', () => {
410422
expect(fetchGitHubContributions).toHaveBeenCalled();
411423
});
412424

413-
it('returns 400 when grace exceeds max value', async () => {
414-
const response = await GET(
415-
makeRequest({
416-
user: 'octocat',
417-
grace: '999',
418-
})
419-
);
420-
421-
expect(response.status).toBe(400);
422-
});
423-
424425
it('embeds the username (uppercased) in the SVG title', async () => {
425426
const response = await GET(makeRequest({ user: 'octocat' }));
426427
const body = await response.text();

lib/validations.test.ts

Lines changed: 130 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import {
66
validateGitHubUsername,
77
} from './validations';
88

9+
function parse(params: Record<string, string>) {
10+
return streakParamsSchema.parse({ user: 'octocat', ...params });
11+
}
12+
913
describe('streakParamsSchema — grace fallback behavior', () => {
1014
it('accepts "0" as a valid grace value', () => {
1115
expect(parse({ grace: '0' }).grace).toBe(0);
@@ -43,6 +47,10 @@ describe('streakParamsSchema — grace fallback behavior', () => {
4347
it('defaults to 1 when grace is omitted', () => {
4448
expect(parse({}).grace).toBe(1);
4549
});
50+
51+
it('defaults to 1 when grace is empty string', () => {
52+
expect(parse({ grace: '' }).grace).toBe(1);
53+
});
4654
});
4755

4856
describe('validateGitHubUsername', () => {
@@ -479,49 +487,144 @@ describe('streakParamsSchema', () => {
479487
});
480488
});
481489

482-
it('should succeed when username contains hyphens', () => {
483-
const result = streakParamsSchema.safeParse({
484-
user: 'valid-user',
490+
describe('streakParamsSchema — user hyphen validation', () => {
491+
it('should succeed when username contains hyphens', () => {
492+
const result = streakParamsSchema.safeParse({
493+
user: 'valid-user',
494+
});
495+
496+
expect(result.success).toBe(true);
485497
});
486498

487-
expect(result.success).toBe(true);
488-
});
499+
it('should succeed when username contains multiple hyphens', () => {
500+
const result = streakParamsSchema.safeParse({
501+
user: 'valid-user-name-123',
502+
});
489503

490-
it('should succeed when username contains multiple hyphens', () => {
491-
const result = streakParamsSchema.safeParse({
492-
user: 'valid-user-name-123',
504+
expect(result.success).toBe(true);
493505
});
494506

495-
expect(result.success).toBe(true);
496-
});
507+
it('should fail when username ends with hyphen', () => {
508+
const result = streakParamsSchema.safeParse({
509+
user: 'user-',
510+
});
497511

498-
it('should fail when username ends with hyphen', () => {
499-
const result = streakParamsSchema.safeParse({
500-
user: 'user-',
512+
expect(result.success).toBe(false);
501513
});
502514

503-
expect(result.success).toBe(false);
504-
});
515+
it('should fail when username starts with hyphen', () => {
516+
const result = streakParamsSchema.safeParse({
517+
user: '-user',
518+
});
505519

506-
it('should fail when username starts with hyphen', () => {
507-
const result = streakParamsSchema.safeParse({
508-
user: '-user',
520+
expect(result.success).toBe(false);
509521
});
510522

511-
expect(result.success).toBe(false);
523+
it('should fail when username contains consecutive hyphens', () => {
524+
const result = streakParamsSchema.safeParse({
525+
user: 'user--name',
526+
});
527+
528+
expect(result.success).toBe(false);
529+
});
512530
});
513531

514-
it('should fail when username contains consecutive hyphens', () => {
515-
const result = streakParamsSchema.safeParse({
516-
user: 'user--name',
532+
describe('streakParamsSchema — grace validation', () => {
533+
it('accepts grace=0', () => {
534+
const result = streakParamsSchema.safeParse({
535+
user: 'octocat',
536+
grace: '0',
537+
});
538+
539+
expect(result.success).toBe(true);
540+
if (result.success) {
541+
expect(result.data.grace).toBe(0);
542+
}
517543
});
518544

519-
expect(result.success).toBe(false);
520-
});
545+
it('accepts grace=7', () => {
546+
const result = streakParamsSchema.safeParse({
547+
user: 'octocat',
548+
grace: '7',
549+
});
521550

522-
function parse(params: Record<string, string>) {
523-
return streakParamsSchema.parse({ user: 'octocat', ...params });
524-
}
551+
expect(result.success).toBe(true);
552+
if (result.success) {
553+
expect(result.data.grace).toBe(7);
554+
}
555+
});
556+
557+
it('accepts grace=3', () => {
558+
const result = streakParamsSchema.safeParse({
559+
user: 'octocat',
560+
grace: '3',
561+
});
562+
563+
expect(result.success).toBe(true);
564+
if (result.success) {
565+
expect(result.data.grace).toBe(3);
566+
}
567+
});
568+
569+
it('defaults to 1 when grace is omitted', () => {
570+
const result = streakParamsSchema.safeParse({
571+
user: 'octocat',
572+
});
573+
574+
expect(result.success).toBe(true);
575+
if (result.success) {
576+
expect(result.data.grace).toBe(1);
577+
}
578+
});
579+
580+
it('rejects grace=-1', () => {
581+
const result = streakParamsSchema.safeParse({
582+
user: 'octocat',
583+
grace: '-1',
584+
});
585+
586+
expect(result.success).toBe(false);
587+
if (!result.success) {
588+
expect(result.error.issues[0]?.message).toBe('grace must be an integer between 0 and 7');
589+
}
590+
});
591+
592+
it('rejects grace=8 (exceeds max)', () => {
593+
const result = streakParamsSchema.safeParse({
594+
user: 'octocat',
595+
grace: '8',
596+
});
597+
598+
expect(result.success).toBe(false);
599+
if (!result.success) {
600+
expect(result.error.issues[0]?.message).toBe('grace must be an integer between 0 and 7');
601+
}
602+
});
603+
604+
it('rejects non-numeric grace value', () => {
605+
const result = streakParamsSchema.safeParse({
606+
user: 'octocat',
607+
grace: 'abc',
608+
});
609+
610+
expect(result.success).toBe(false);
611+
if (!result.success) {
612+
expect(result.error.issues[0]?.message).toBe('grace must be an integer between 0 and 7');
613+
}
614+
});
615+
616+
it('rejects float grace value', () => {
617+
const result = streakParamsSchema.safeParse({
618+
user: 'octocat',
619+
grace: '5.5',
620+
});
621+
622+
expect(result.success).toBe(false);
623+
if (!result.success) {
624+
expect(result.error.issues[0]?.message).toBe('grace must be an integer between 0 and 7');
625+
}
626+
});
627+
});
525628

526629
describe('streakParamsSchema — scale fallback behavior', () => {
527630
it('accepts "log" as a valid scale value', () => {

lib/validations.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,14 +280,14 @@ const baseStreakParamsSchema = z.object({
280280
.optional()
281281
.refine(
282282
(val) => {
283-
if (!val) return true;
284-
const parsed = Number(val);
285-
return !isNaN(parsed) && Number.isInteger(parsed) && parsed >= 0 && parsed <= 7;
283+
if (val === undefined || val === '') return true;
284+
return /^\d+$/.test(val) && Number(val) >= 0 && Number(val) <= 7;
286285
},
287286
{ message: 'grace must be an integer between 0 and 7' }
288287
)
289-
.transform(toGraceValue)
288+
.transform((val) => (val === undefined || val === '' ? 1 : Number(val)))
290289
.default(1),
290+
291291
mode: z.enum(['commits', 'loc']).catch('commits').default('commits'),
292292
repo: z.string().optional(),
293293
org: z

package-lock.json

Lines changed: 6 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"devDependencies": {
5353
"@emnapi/core": "^1.10.0",
5454
"@emnapi/runtime": "^1.10.0",
55+
"@testing-library/dom": "^10.4.1",
5556
"@testing-library/jest-dom": "^6.9.1",
5657
"@testing-library/react": "^16.3.2",
5758
"@testing-library/user-event": "^14.6.1",

0 commit comments

Comments
 (0)