chore: remove unused frontend validation module
`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.
This commit is contained in:
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user