diff --git a/app/api/streak/route.ts b/app/api/streak/route.ts index 029ced14..8e22a56d 100644 --- a/app/api/streak/route.ts +++ b/app/api/streak/route.ts @@ -16,6 +16,7 @@ import { getSecondsUntilUTCMidnight, getSecondsUntilMidnightInTimezone } from '@ import type { BadgeParams } from '@/types'; import { themes } from '@/lib/svg/themes'; import { streakParamsSchema } from '@/lib/validations'; +import { sanitizeHexColor } from '@/lib/svg/sanitizer'; const SVG_CSP_HEADER = "default-src 'none'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src https://fonts.gstatic.com;"; @@ -299,15 +300,15 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo message.toLowerCase().includes('validation') || message.toLowerCase().includes('strictly for organizations'); - const errBg = `#${(parseResult.success && parseResult.data.bg) || '0d1117'}`; - const errAccent = `#${ + const errBg = `#${sanitizeHexColor(parseResult.success ? parseResult.data.bg : undefined, '0d1117')}`; + const errAccentRaw = (parseResult.success && (Array.isArray(parseResult.data.accent) ? parseResult.data.accent[parseResult.data.accent.length - 1] : parseResult.data.accent)) || - '58a6ff' - }`; - const errText = `#${(parseResult.success && parseResult.data.text) || 'c9d1d9'}`; + undefined; + const errAccent = `#${sanitizeHexColor(errAccentRaw, '58a6ff')}`; + const errText = `#${sanitizeHexColor(parseResult.success ? parseResult.data.text : undefined, 'c9d1d9')}`; const errRadius = parseResult.success ? (() => { const r = Number(parseResult.data.radius); diff --git a/app/components/navbar.test.tsx b/app/components/navbar.test.tsx index c0cd7399..ee97e6b8 100644 --- a/app/components/navbar.test.tsx +++ b/app/components/navbar.test.tsx @@ -58,9 +58,17 @@ vi.mock('lucide-react', () => ({ describe('Navbar mobile menu', () => { beforeEach(() => { + Object.defineProperty(window, 'localStorage', { + value: { + getItem: vi.fn(), + setItem: vi.fn(), + clear: vi.fn(), + }, + writable: true, + }); window.innerWidth = 500; mockMatchMedia(false); - window.localStorage.clear(); + window.localStorage?.clear(); document.documentElement.className = ''; }); @@ -106,8 +114,16 @@ describe('Navbar mobile menu', () => { describe('Navbar responsive breakpoints', () => { beforeEach(() => { + Object.defineProperty(window, 'localStorage', { + value: { + getItem: vi.fn(), + setItem: vi.fn(), + clear: vi.fn(), + }, + writable: true, + }); window.innerWidth = 500; - window.localStorage.clear(); + window.localStorage?.clear(); document.documentElement.className = ''; }); diff --git a/app/page.test.tsx b/app/page.test.tsx index 596d1f31..f42cd922 100644 --- a/app/page.test.tsx +++ b/app/page.test.tsx @@ -29,6 +29,10 @@ vi.mock('next/link', () => ({ ), })); +vi.mock('@/utils/tracking', () => ({ + trackUser: vi.fn(), +})); + // Mock GSAP so FeatureCards don't break in JSDOM vi.mock('gsap', () => { const tween = { kill: vi.fn() }; diff --git a/package-lock.json b/package-lock.json index 97945129..1caa1839 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1810,6 +1810,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1825,6 +1826,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1840,6 +1842,10 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1855,6 +1861,10 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1870,6 +1880,10 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1885,6 +1899,10 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1900,6 +1918,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -2131,6 +2150,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2297,6 +2319,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2314,6 +2339,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2331,6 +2359,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2348,6 +2379,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2364,6 +2398,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2381,6 +2418,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2647,6 +2687,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3486,6 +3529,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4086,9 +4132,9 @@ "license": "MIT" }, "node_modules/ast-v8-to-istanbul": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.2.tgz", - "integrity": "sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.3.tgz", + "integrity": "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==", "dev": true, "license": "MIT", "dependencies": { @@ -4205,9 +4251,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.32", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", - "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "version": "2.10.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -5043,9 +5089,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.362", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.362.tgz", - "integrity": "sha512-PUY2DrLvkjkUuWqq+KPL2iWshrJsZOcIojzRQ7eXFacc9dWga7MGMJAa15VbiejSZB1PAXaRLAiKgruHP8LB1w==", + "version": "1.5.364", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", + "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", "dev": true, "license": "ISC" }, @@ -5057,9 +5103,9 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", - "integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==", + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", + "integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -5518,9 +5564,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.13.0.tgz", + "integrity": "sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6407,9 +6453,9 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -7186,9 +7232,9 @@ } }, "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -7476,6 +7522,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7496,6 +7545,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7516,6 +7568,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7536,6 +7591,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8121,9 +8179,9 @@ } }, "node_modules/mongoose": { - "version": "9.6.2", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.6.2.tgz", - "integrity": "sha512-7m8HntjkoRnwEmuPC0kdlwcZXJOQf4twumFj+PNzg/anqqZE2Er7hQslqyzy07mP3JcFjoTSgH5765PyqOXsxw==", + "version": "9.6.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.6.3.tgz", + "integrity": "sha512-vI6dTTlQnfMCyyQ5TrvhG0bCRs4dq5e1uFNPtOOWsOhn0fSg8AoIHjfyyCYr8aybyvPs845dRHGxsC3w/fHcBA==", "license": "MIT", "dependencies": { "kareem": "3.3.0", @@ -9240,6 +9298,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -10069,9 +10130,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz", - "integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { @@ -10079,9 +10140,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -10137,22 +10198,22 @@ } }, "node_modules/tldts": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.0.tgz", - "integrity": "sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", + "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.0" + "tldts-core": "^7.4.2" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.0.tgz", - "integrity": "sha512-/mb9kRld+x1sIMXxWNOAp5m6C+D4GrAORWlJkOJ5dElvxdN1eutz/o7qHLp9gFvDF4Y3/L2xeScoxz6AbEo8rQ==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", + "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", "dev": true, "license": "MIT" }, @@ -10251,9 +10312,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", - "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", "dependencies": { @@ -10354,18 +10415,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" }, "engines": { "node": ">= 0.4" diff --git a/vitest.config.ts b/vitest.config.ts index cdc257e3..f9c606f6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ test: { environment: 'jsdom', globals: true, + testTimeout: 20000, include: ['**/*.test.ts', '**/*.test.tsx'], exclude: ['node_modules', '.next'], coverage: {