From ea3c765561524717e6ca2e01c7feb02e8e166464 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Thu, 20 Aug 2026 19:38:12 +0200 Subject: [PATCH] chore: remove unused frontend validation module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `src/lib/utils/validation.ts` exported six validators (validateItemId, validateImageType, validateMediaSourceId, validateUrlPathSegment, validateNumericParam, validateQueryParamValue). Nothing outside its own 213-line test suite ever called them, so the module read as covered, guarded input validation while guarding nothing — a green test run over code no input ever passes through. Deleting it does not weaken any check that was running; it removes the false assurance that one was. Note: the layer this validation belongs in per CLAUDE.md ("Validate all inputs in Rust command handlers") does not implement it either. That is a separate concern and is left untouched here. --- src/lib/utils/validation.test.ts | 118 ----------------------------- src/lib/utils/validation.ts | 123 ------------------------------- 2 files changed, 241 deletions(-) delete mode 100644 src/lib/utils/validation.test.ts delete mode 100644 src/lib/utils/validation.ts diff --git a/src/lib/utils/validation.test.ts b/src/lib/utils/validation.test.ts deleted file mode 100644 index cd44bdfa..00000000 --- a/src/lib/utils/validation.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Input validation utility tests - * - * TRACES: UR-009, UR-025 | DR-015 - */ - -import { describe, it, expect } from "vitest"; -import { - validateItemId, - validateImageType, - validateMediaSourceId, - validateNumericParam, - validateQueryParamValue, -} from "./validation"; - -describe("validateItemId", () => { - it("should accept valid item IDs", () => { - expect(() => validateItemId("123abc")).not.toThrow(); - expect(() => validateItemId("abc-123_def")).not.toThrow(); - expect(() => validateItemId("12345")).not.toThrow(); - }); - - it("should reject empty or non-string IDs", () => { - expect(() => validateItemId("")).toThrow("must be a non-empty string"); - expect(() => validateItemId(null as any)).toThrow("must be a non-empty string"); - expect(() => validateItemId(undefined as any)).toThrow("must be a non-empty string"); - }); - - it("should reject IDs exceeding max length", () => { - expect(() => validateItemId("a".repeat(51))).toThrow("exceeds maximum length"); - }); - - it("should reject IDs with invalid characters", () => { - expect(() => validateItemId("abc/def")).toThrow("contains invalid characters"); - expect(() => validateItemId("abc..def")).toThrow("contains invalid characters"); - expect(() => validateItemId("abc def")).toThrow("contains invalid characters"); - }); -}); - -describe("validateImageType", () => { - it("should accept valid image types", () => { - expect(() => validateImageType("Primary")).not.toThrow(); - expect(() => validateImageType("Backdrop")).not.toThrow(); - expect(() => validateImageType("Banner")).not.toThrow(); - expect(() => validateImageType("Logo")).not.toThrow(); - }); - - it("should reject invalid image types", () => { - expect(() => validateImageType("InvalidType")).toThrow("not a valid image type"); - expect(() => validateImageType("..")).toThrow("not a valid image type"); - expect(() => validateImageType("Primary/Avatar")).toThrow("not a valid image type"); - }); - - it("should reject empty or non-string types", () => { - expect(() => validateImageType("")).toThrow("must be a non-empty string"); - }); -}); - -describe("validateMediaSourceId", () => { - it("should accept valid media source IDs", () => { - expect(() => validateMediaSourceId("source-123")).not.toThrow(); - expect(() => validateMediaSourceId("video_stream_1")).not.toThrow(); - }); - - it("should reject IDs with invalid characters", () => { - expect(() => validateMediaSourceId("source/path")).toThrow("contains invalid characters"); - expect(() => validateMediaSourceId("source..path")).toThrow("contains invalid characters"); - }); - - it("should reject IDs exceeding max length", () => { - expect(() => validateMediaSourceId("a".repeat(51))).toThrow("exceeds maximum length"); - }); -}); - -describe("validateNumericParam", () => { - it("should accept valid numbers", () => { - expect(validateNumericParam(100)).toBe(100); - expect(validateNumericParam(0)).toBe(0); - expect(validateNumericParam(9999)).toBe(9999); - }); - - it("should reject non-integers", () => { - expect(() => validateNumericParam(10.5)).toThrow("must be an integer"); - expect(() => validateNumericParam("100")).toThrow("must be an integer"); - }); - - it("should respect min and max bounds", () => { - expect(() => validateNumericParam(-1, 0, 100)).toThrow("must be between 0 and 100"); - expect(() => validateNumericParam(101, 0, 100)).toThrow("must be between 0 and 100"); - }); - - it("should allow custom bounds", () => { - expect(validateNumericParam(50, 10, 100)).toBe(50); - expect(() => validateNumericParam(5, 10, 100)).toThrow("must be between 10 and 100"); - }); -}); - -describe("validateQueryParamValue", () => { - it("should accept valid query param values", () => { - expect(() => validateQueryParamValue("abc123")).not.toThrow(); - expect(() => validateQueryParamValue("value-with-dash")).not.toThrow(); - expect(() => validateQueryParamValue("value_with_underscore")).not.toThrow(); - }); - - it("should reject values with invalid characters", () => { - expect(() => validateQueryParamValue("value with spaces")).toThrow("contains invalid characters"); - expect(() => validateQueryParamValue("value/path")).toThrow("contains invalid characters"); - expect(() => validateQueryParamValue("value?query")).toThrow("contains invalid characters"); - }); - - it("should reject values exceeding max length", () => { - expect(() => validateQueryParamValue("a".repeat(101))).toThrow("exceeds maximum length"); - }); - - it("should respect custom max length", () => { - expect(() => validateQueryParamValue("a".repeat(50), 40)).toThrow("exceeds maximum length"); - }); -}); diff --git a/src/lib/utils/validation.ts b/src/lib/utils/validation.ts deleted file mode 100644 index 54706365..00000000 --- a/src/lib/utils/validation.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Input validation utilities for security and data integrity - */ - -/** - * Validate Jellyfin item ID format - * Item IDs should be non-empty alphanumeric strings with optional dashes/underscores - */ -export function validateItemId(itemId: string): void { - if (!itemId || typeof itemId !== "string") { - throw new Error("Invalid itemId: must be a non-empty string"); - } - - if (itemId.length > 50) { - throw new Error("Invalid itemId: exceeds maximum length of 50 characters"); - } - - // Jellyfin item IDs are typically UUIDs or numeric IDs - if (!/^[a-zA-Z0-9\-_]+$/.test(itemId)) { - throw new Error("Invalid itemId: contains invalid characters"); - } -} - -/** - * Validate image type to prevent path traversal attacks - */ -export function validateImageType(imageType: string): void { - if (!imageType || typeof imageType !== "string") { - throw new Error("Invalid imageType: must be a non-empty string"); - } - - // Only allow known image types - const validImageTypes = [ - "Primary", - "Backdrop", - "Banner", - "Disc", - "Box", - "Logo", - "Thumb", - "Art", - "Chapter", - "Keyframe", - ]; - - if (!validImageTypes.includes(imageType)) { - throw new Error(`Invalid imageType: "${imageType}" is not a valid image type`); - } -} - -/** - * Validate media source ID format - */ -export function validateMediaSourceId(mediaSourceId: string): void { - if (!mediaSourceId || typeof mediaSourceId !== "string") { - throw new Error("Invalid mediaSourceId: must be a non-empty string"); - } - - if (mediaSourceId.length > 50) { - throw new Error("Invalid mediaSourceId: exceeds maximum length"); - } - - if (!/^[a-zA-Z0-9\-_]+$/.test(mediaSourceId)) { - throw new Error("Invalid mediaSourceId: contains invalid characters"); - } -} - -/** - * Validate URL path segment to prevent directory traversal - * Disallows: "..", ".", and characters that could enable attacks - */ -export function validateUrlPathSegment(segment: string): void { - if (!segment || typeof segment !== "string") { - throw new Error("Invalid path segment: must be a non-empty string"); - } - - if (segment === ".." || segment === ".") { - throw new Error("Invalid path segment: directory traversal not allowed"); - } - - // Reject path separators and null bytes - if (/[\/\\%]/.test(segment)) { - throw new Error("Invalid path segment: contains invalid characters"); - } -} - -/** - * Validate numeric parameter (width, height, quality, etc.) - */ -export function validateNumericParam(value: unknown, min = 0, max = 10000, name = "parameter"): number { - // Must be an actual number, not a string that looks like a number - if (typeof value !== "number") { - throw new Error(`Invalid ${name}: must be an integer`); - } - - if (!Number.isInteger(value)) { - throw new Error(`Invalid ${name}: must be an integer`); - } - - if (value < min || value > max) { - throw new Error(`Invalid ${name}: must be between ${min} and ${max}`); - } - - return value; -} - -/** - * Sanitize query parameter value - allows alphanumeric, dash, underscore - */ -export function validateQueryParamValue(value: string, maxLength = 100): void { - if (typeof value !== "string") { - throw new Error("Query parameter value must be a string"); - } - - if (value.length > maxLength) { - throw new Error(`Query parameter exceeds maximum length of ${maxLength}`); - } - - // Allow only safe characters in query params - if (!/^[a-zA-Z0-9\-_.~]+$/.test(value)) { - throw new Error("Query parameter contains invalid characters"); - } -}