diff --git a/tests/aliases.test.ts b/tests/aliases.test.ts new file mode 100644 index 0000000..6a6238e --- /dev/null +++ b/tests/aliases.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest'; +import { ALIAS_MAX_LENGTH, normalizeAlias, validateAlias } from '../worker/lib/aliases'; + +describe('alias normalization', () => { + it('lowercases aliases', () => { + expect(normalizeAlias('Claude')).toBe('claude'); + }); + + it('trims aliases', () => { + expect(normalizeAlias(' claude ')).toBe('claude'); + }); + + it('keeps hyphen and underscore', () => { + expect(normalizeAlias('my_link-1')).toBe('my_link-1'); + }); +}); + +describe('alias validation', () => { + it('rejects empty string with exact error', () => { + expect(validateAlias('')).toEqual({ ok: false, error: 'Alias is required' }); + expect(validateAlias(' ')).toEqual({ ok: false, error: 'Alias is required' }); + }); + + it('rejects path separators with exact error', () => { + expect(validateAlias('a/b')).toEqual({ + ok: false, + error: 'Alias may contain only letters, numbers, underscore, and hyphen', + }); + }); + + it('rejects aliases starting with hyphen or underscore', () => { + expect(validateAlias('-alias')).toEqual({ + ok: false, + error: 'Alias may contain only letters, numbers, underscore, and hyphen', + }); + expect(validateAlias('_alias')).toEqual({ + ok: false, + error: 'Alias may contain only letters, numbers, underscore, and hyphen', + }); + }); + + it('accepts length 100 and rejects length 101', () => { + const validAlias = 'a'.repeat(ALIAS_MAX_LENGTH); + const tooLongAlias = 'a'.repeat(ALIAS_MAX_LENGTH + 1); + + expect(validateAlias(validAlias)).toEqual({ ok: true, value: validAlias }); + expect(validateAlias(tooLongAlias)).toEqual({ + ok: false, + error: 'Alias must be 100 characters or fewer', + }); + }); +}); diff --git a/worker/lib/aliases.ts b/worker/lib/aliases.ts new file mode 100644 index 0000000..d025c6a --- /dev/null +++ b/worker/lib/aliases.ts @@ -0,0 +1,38 @@ +export const ALIAS_MAX_LENGTH = 100; + +const ALIAS_INVALID_ERROR = 'Alias may contain only letters, numbers, underscore, and hyphen'; +const ALIAS_PATTERN = /^[a-z0-9][a-z0-9_-]{0,99}$/; + +type AliasValidationSuccess = { + ok: true; + value: string; +}; + +type AliasValidationFailure = { + ok: false; + error: string; +}; + +export type ValidationResult = AliasValidationSuccess | AliasValidationFailure; + +export function normalizeAlias(input: string): string { + return input.trim().replace(/[A-Z]/g, (character) => character.toLowerCase()); +} + +export function validateAlias(input: string): ValidationResult { + const value = normalizeAlias(input); + + if (value.length === 0) { + return { ok: false, error: 'Alias is required' }; + } + + if (value.length > ALIAS_MAX_LENGTH) { + return { ok: false, error: `Alias must be ${ALIAS_MAX_LENGTH} characters or fewer` }; + } + + if (!ALIAS_PATTERN.test(value)) { + return { ok: false, error: ALIAS_INVALID_ERROR }; + } + + return { ok: true, value }; +}