From d8883dba46a1380c93681a8b7a5d0eff471d3241 Mon Sep 17 00:00:00 2001 From: ChetanSenta Date: Sun, 21 Jun 2026 12:56:32 +0530 Subject: [PATCH] fix(themes): resolve case-sensitivity bug by normalizing user theme parameter lookup --- app/api/streak/route.ts | 12 +++++++----- lib/svg/themes.test.ts | 23 ++++++++++++++++++++++- lib/svg/themes.ts | 13 +++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/app/api/streak/route.ts b/app/api/streak/route.ts index bb1d66380..0b57cfe1f 100644 --- a/app/api/streak/route.ts +++ b/app/api/streak/route.ts @@ -27,7 +27,7 @@ import { generateRadarSVG } from '@/lib/svg/radar'; import { generateDoughnutSVG } from '@/lib/svg/doughnut'; import { getSecondsUntilUTCMidnight, getSecondsUntilMidnightInTimezone } from '@/utils/time'; import type { BadgeParams, RepoContribution, ExtendedContributionData } from '@/types'; -import { themes } from '@/lib/svg/themes'; +import { getNormalizedThemeKey, themes } from '@/lib/svg/themes'; import { streakParamsSchema } from '@/lib/validations'; import { sanitizeHexColor, sanitizeRadius, escapeXML } from '@/lib/svg/sanitizer'; import { getClientIp } from '@/utils/getClientIp'; @@ -174,7 +174,9 @@ export async function GET(request: Request) { | 'doughnut' | 'pie' | 'activity_graph'; - const themeName = theme || 'dark'; + + const themeKey = getNormalizedThemeKey(theme); + const themeName = themeKey === 'default' && theme ? theme : themeKey; const ip = getClientIp(request); @@ -286,8 +288,8 @@ export async function GET(request: Request) { const currentYear = new Date().getUTCFullYear(); const isHistoricalYear = !!year && Number(year) < currentYear; - const isAutoTheme = themeName === 'auto'; - const isRandomTheme = themeName === 'random'; + const isAutoTheme = themeName.toLowerCase() === 'auto'; + const isRandomTheme = themeName.toLowerCase() === 'random'; const selectedTheme = (() => { if (isAutoTheme) return themes.light; if (isRandomTheme) { @@ -296,7 +298,7 @@ export async function GET(request: Request) { const stableKey = keys[hash % keys.length]; return themes[stableKey] || themes.dark; } - return themes[theme] || themes.dark; + return themes[themeKey] || themes.dark; })(); // If 'org' is provided, we use it as the display user diff --git a/lib/svg/themes.test.ts b/lib/svg/themes.test.ts index aa5b3e3b0..d45577b45 100644 --- a/lib/svg/themes.test.ts +++ b/lib/svg/themes.test.ts @@ -4,7 +4,7 @@ // structure integrity, and AUTO_THEME pair safety checks. import { describe, it, expect } from 'vitest'; -import { themes, AUTO_THEME_LIGHT, AUTO_THEME_DARK } from './themes'; +import { themes, AUTO_THEME_LIGHT, AUTO_THEME_DARK, getNormalizedThemeKey } from './themes'; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -229,3 +229,24 @@ describe('known theme palette regression guards', () => { expect(themes['cyber-pulse'].accent).toBe('00ffee'); }); }); + +// ── Normalization Behavior Checks ───────────────────────────────────────────── + +describe('getNormalizedThemeKey', () => { + it('matches kebab-case keys when user provides all lowercase mashed inputs', () => { + expect(getNormalizedThemeKey('cyber-pulse')).toBe('cyber-pulse'); + }); + + it('matches keys when user provides screaming uppercase inputs', () => { + expect(getNormalizedThemeKey('DRACULA')).toBe('dracula'); + }); + + it('returns default fallback when theme name does not exist', () => { + expect(getNormalizedThemeKey('invalidThemeNonexistent')).toBe('default'); + }); + + it('returns default fallback gracefully when theme parameter is undefined or null', () => { + expect(getNormalizedThemeKey(undefined)).toBe('default'); + expect(getNormalizedThemeKey(null)).toBe('default'); + }); +}); diff --git a/lib/svg/themes.ts b/lib/svg/themes.ts index ecaadd831..e7970deea 100644 --- a/lib/svg/themes.ts +++ b/lib/svg/themes.ts @@ -47,3 +47,16 @@ export const themes: Record = { // viewer's OS-level light/dark setting without any JavaScript. export const AUTO_THEME_LIGHT: BadgeTheme = themes.light; export const AUTO_THEME_DARK: BadgeTheme = themes.dark; + +/** + * Resolves a theme case-insensitively by matching the normalized user input + * against the normalized theme registry keys. Returns the standard theme key. + */ +export function getNormalizedThemeKey(themeInput: string | undefined | null): string { + if (!themeInput) return 'default'; // fallback key + + const target = themeInput.toLowerCase(); + const matchedKey = Object.keys(themes).find((key) => key.toLowerCase() === target); + + return matchedKey || 'default'; +}