/**
* Unit tests for lib/reserved-names.js
*
* Tests name validation, reserved names, and edge cases.
*/
import test from 'brittle'
import { RESERVED_NAMES, NAME_REGEX, validateName } from '../../lib/reserved-names.js'
// ========================================
// Reserved Names Set
// ========================================
test('RESERVED_NAMES - contains all system route prefixes', (t) => {
const required = ['api', 'admin', 'profile', 'search', 'login', 'deploy', 'merge', 'groups', 'join']
for (const name of required) {
t.ok(RESERVED_NAMES.has(name), `"${name}" is reserved`)
}
})
test('RESERVED_NAMES - contains future route prefixes', (t) => {
const future = ['static', 'assets', 'settings', 'help', 'new', 'explore', 'notifications']
for (const name of future) {
t.ok(RESERVED_NAMES.has(name), `"${name}" is reserved`)
}
})
test('RESERVED_NAMES - contains separator characters', (t) => {
t.ok(RESERVED_NAMES.has('-'), '"-" is reserved')
t.ok(RESERVED_NAMES.has('_'), '"_" is reserved')
})
// ========================================
// NAME_REGEX
// ========================================
test('NAME_REGEX - accepts valid names', (t) => {
const valid = ['a', 'abc', 'my-repo', 'repo123', 'a1b2c3', 'x']
for (const name of valid) {
t.ok(NAME_REGEX.test(name), `"${name}" should match`)
}
})
test('NAME_REGEX - rejects names with leading/trailing hyphens', (t) => {
t.not(NAME_REGEX.test('-repo'), '"-repo" should not match')
t.not(NAME_REGEX.test('repo-'), '"repo-" should not match')
t.not(NAME_REGEX.test('-'), '"-" alone should not match (length 1)')
})
test('NAME_REGEX - rejects names with uppercase', (t) => {
t.not(NAME_REGEX.test('MyRepo'), '"MyRepo" should not match')
t.not(NAME_REGEX.test('REPO'), '"REPO" should not match')
})
test('NAME_REGEX - rejects names with special characters', (t) => {
t.not(NAME_REGEX.test('repo.name'), '"repo.name" should not match')
t.not(NAME_REGEX.test('repo_name'), '"repo_name" should not match')
t.not(NAME_REGEX.test('repo/name'), '"repo/name" should not match')
t.not(NAME_REGEX.test('repo name'), '"repo name" should not match')
})
// ========================================
// validateName
// ========================================
test('validateName - accepts valid names', (t) => {
t.ok(validateName('my-repo').valid, '"my-repo" is valid')
t.ok(validateName('repo123').valid, '"repo123" is valid')
t.ok(validateName('a').valid, 'single char "a" is valid')
t.ok(validateName('x1').valid, '"x1" is valid')
})
test('validateName - rejects empty/null/undefined', (t) => {
t.not(validateName('').valid, 'empty string rejected')
t.not(validateName(null).valid, 'null rejected')
t.not(validateName(undefined).valid, 'undefined rejected')
})
test('validateName - rejects names over 64 chars', (t) => {
const long = 'a'.repeat(65)
const result = validateName(long)
t.not(result.valid, '65-char name rejected')
t.ok(result.error.includes('1-64'), 'error mentions length limit')
})
test('validateName - accepts exactly 64 chars', (t) => {
const name = 'a'.repeat(64)
t.ok(validateName(name).valid, '64-char name accepted')
})
test('validateName - rejects consecutive hyphens', (t) => {
const result = validateName('my--repo')
t.not(result.valid, '"my--repo" rejected')
t.ok(result.error.includes('consecutive'), 'error mentions consecutive hyphens')
})
test('validateName - rejects all reserved names', (t) => {
for (const name of RESERVED_NAMES) {
// Skip '-' and '_' since they fail regex before reaching reserved check
if (name === '-' || name === '_') continue
const result = validateName(name)
t.not(result.valid, `"${name}" should be rejected as reserved`)
t.ok(result.error.includes('reserved'), `"${name}" error mentions reserved`)
}
})
test('validateName - rejects path traversal attempts', (t) => {
// These fail the regex (contain . / or uppercase)
t.not(validateName('../etc').valid, '"../etc" rejected')
t.not(validateName('../../passwd').valid, '"../../passwd" rejected')
t.not(validateName('.git').valid, '".git" rejected')
t.not(validateName('a/b').valid, '"a/b" rejected')
})