You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

300 lines
9.2 KiB

import { describe, expect, it } from 'vitest';
import {
code,
codeToLangPath,
errCode,
isDescendantOf,
isEqualOrDescendantOf,
isModuleSeed,
isValidCode,
leaf,
moduleOf,
moduleSeed,
parent,
parts,
sub,
validateCode,
type ErrCode
} from '../index.ts';
import {
CodeFormatError,
ERRS_VALIDATION_REASON_CONSECUTIVE_SEPS,
ERRS_VALIDATION_REASON_DUPLICATE_MODULE_SEP,
ERRS_VALIDATION_REASON_FIRST_CHAR,
ERRS_VALIDATION_REASON_INCOMPLETE_MODULE_SEP,
ERRS_VALIDATION_REASON_INVALID_CHAR,
ERRS_VALIDATION_REASON_LAST_CHAR,
ERRS_VALIDATION_REASON_LENGTH,
ERRS_VALIDATION_REASON_MISSING_MODULE_SEP,
ERRS_VALIDATION_REASON_PATH_BEFORE_MODULE
} from '../index.ts';
describe('code() — validation', () => {
it('accepts the minimum valid code', () => {
expect(code('m::e')).toBe('m::e');
});
it('accepts a flat module + path', () => {
expect(code('auth::login')).toBe('auth::login');
});
it('accepts a deep hierarchy', () => {
expect(code('auth::user.login.failed')).toBe('auth::user.login.failed');
});
it('accepts segments with internal underscores', () => {
expect(code('app::not_found')).toBe('app::not_found');
expect(code('app::section_one.sub_two')).toBe('app::section_one.sub_two');
});
it('accepts digits in segments', () => {
expect(code('a1::b2.c3_d4')).toBe('a1::b2.c3_d4');
});
it('rejects values shorter than the minimum length', () => {
expect(() => code('')).toThrow(CodeFormatError);
expect(() => code('m')).toThrow(CodeFormatError);
expect(() => code('m::')).toThrow(CodeFormatError);
});
it('rejects empty module', () => {
expect(() => code('::e')).toThrow(CodeFormatError);
});
it('rejects missing module separator', () => {
expect(validateCode('login.failed')).toBe(ERRS_VALIDATION_REASON_PATH_BEFORE_MODULE);
expect(validateCode('loginfailedz')).toBe(ERRS_VALIDATION_REASON_MISSING_MODULE_SEP);
});
it('rejects duplicate module separators', () => {
expect(validateCode('a::b::c')).toBe(ERRS_VALIDATION_REASON_DUPLICATE_MODULE_SEP);
});
it('rejects single-colon as incomplete module separator', () => {
expect(validateCode('a:b.c')).toBe(ERRS_VALIDATION_REASON_INCOMPLETE_MODULE_SEP);
});
it('rejects path separator before module separator', () => {
expect(validateCode('a.b::c')).toBe(ERRS_VALIDATION_REASON_PATH_BEFORE_MODULE);
});
it('rejects consecutive separators', () => {
expect(validateCode('a::b..c')).toBe(ERRS_VALIDATION_REASON_CONSECUTIVE_SEPS);
expect(validateCode('a::b._c')).toBe(ERRS_VALIDATION_REASON_CONSECUTIVE_SEPS);
expect(validateCode('a::_b')).toBe(ERRS_VALIDATION_REASON_CONSECUTIVE_SEPS);
expect(validateCode('a::b__c')).toBe(ERRS_VALIDATION_REASON_CONSECUTIVE_SEPS);
});
it('rejects uppercase characters', () => {
expect(validateCode('A::e')).toBe(ERRS_VALIDATION_REASON_FIRST_CHAR);
expect(validateCode('a::bCd')).toBe(ERRS_VALIDATION_REASON_INVALID_CHAR);
});
it('rejects hyphens and slashes', () => {
expect(validateCode('a::b-c')).toBe(ERRS_VALIDATION_REASON_INVALID_CHAR);
expect(validateCode('a::b/c')).toBe(ERRS_VALIDATION_REASON_INVALID_CHAR);
});
it('rejects values starting or ending with a separator', () => {
expect(validateCode('.a::b')).toBe(ERRS_VALIDATION_REASON_FIRST_CHAR);
expect(validateCode('a::b.')).toBe(ERRS_VALIDATION_REASON_LAST_CHAR);
expect(validateCode('_a::b')).toBe(ERRS_VALIDATION_REASON_FIRST_CHAR);
expect(validateCode('a::b_')).toBe(ERRS_VALIDATION_REASON_LAST_CHAR);
});
it('rejects too short with the LENGTH reason', () => {
expect(validateCode('m::')).toBe(ERRS_VALIDATION_REASON_LENGTH);
});
it('CodeFormatError carries the invalid value and reason', () => {
try {
code('A::e');
expect.fail('expected throw');
} catch (err) {
expect(err).toBeInstanceOf(CodeFormatError);
expect((err as CodeFormatError).invalidValue).toBe('A::e');
expect((err as CodeFormatError).reason).toBe(ERRS_VALIDATION_REASON_FIRST_CHAR);
}
});
});
describe('isValidCode()', () => {
it('returns true for valid codes', () => {
expect(isValidCode('auth::login')).toBe(true);
});
it('returns false for invalid codes (no throw)', () => {
expect(isValidCode('A::e')).toBe(false);
expect(isValidCode('')).toBe(false);
});
});
describe('moduleOf()', () => {
it('returns the module portion before "::"', () => {
expect(moduleOf(code('auth::login.failed'))).toBe('auth');
expect(moduleOf(code('m::e'))).toBe('m');
});
});
describe('leaf()', () => {
it('returns the last segment', () => {
expect(leaf(code('auth::user.login.failed'))).toBe('failed');
});
it('returns the first segment after "::" when there is no path', () => {
expect(leaf(code('auth::failed'))).toBe('failed');
});
});
describe('parent()', () => {
it('returns the parent code by stripping the last segment', () => {
expect(parent(code('a::b.c.d'))).toBe('a::b.c');
});
it('returns undefined when the code has no path', () => {
expect(parent(code('a::b'))).toBeUndefined();
});
});
describe('sub()', () => {
it('appends a segment to a parent code', () => {
expect(sub(code('a::b'), 'c')).toBe('a::b.c');
expect(sub(code('a::b.c'), 'd')).toBe('a::b.c.d');
});
it('throws CodeFormatError on invalid segment', () => {
const root = code('a::b');
expect(() => sub(root, '')).toThrow(CodeFormatError);
expect(() => sub(root, 'C')).toThrow(CodeFormatError);
expect(() => sub(root, 'c-d')).toThrow(CodeFormatError);
expect(() => sub(root, '_c')).toThrow(CodeFormatError);
expect(() => sub(root, 'c_')).toThrow(CodeFormatError);
});
});
describe('parts()', () => {
it('splits into module + segments', () => {
expect(parts(code('auth::user.login.failed'))).toEqual([
'auth',
'user',
'login',
'failed'
]);
expect(parts(code('m::e'))).toEqual(['m', 'e']);
});
});
describe('isDescendantOf()', () => {
it('returns true for a strict descendant', () => {
expect(isDescendantOf(code('a::b.c'), code('a::b'))).toBe(true);
expect(isDescendantOf(code('a::b.c.d'), code('a::b'))).toBe(true);
});
it('returns false for identity', () => {
expect(isDescendantOf(code('a::b'), code('a::b'))).toBe(false);
});
it('returns false for an unrelated code', () => {
expect(isDescendantOf(code('a::b'), code('a::c'))).toBe(false);
});
it('respects segment boundaries (no false positives on shared prefix)', () => {
// 'a::b_extra' is NOT a descendant of 'a::b' — the underscore is
// inside the same segment.
const child = code('a::b_extra');
const ancestor = code('a::b');
expect(isDescendantOf(child, ancestor)).toBe(false);
});
});
describe('isEqualOrDescendantOf()', () => {
it('returns true for identity', () => {
expect(isEqualOrDescendantOf(code('a::b'), code('a::b'))).toBe(true);
});
it('returns true for a descendant', () => {
expect(isEqualOrDescendantOf(code('a::b.c'), code('a::b'))).toBe(true);
});
it('returns false for an unrelated code', () => {
expect(isEqualOrDescendantOf(code('a::b'), code('a::c'))).toBe(false);
});
});
describe('codeToLangPath()', () => {
it('replaces the module separator with a dot', () => {
expect(codeToLangPath(code('buss::listener.failed'))).toBe('buss.listener.failed');
expect(codeToLangPath(code('m::e'))).toBe('m.e');
});
it('only replaces the module separator (not subsequent dots)', () => {
const c: ErrCode = code('a::b.c');
expect(codeToLangPath(c)).toBe('a.b.c');
});
});
describe('moduleSeed()', () => {
it('produces a seed of the form "<module>::"', () => {
expect(moduleSeed('buss')).toBe('buss::');
expect(moduleSeed('sess')).toBe('sess::');
});
it('throws on invalid module names', () => {
expect(() => moduleSeed('')).toThrow();
expect(() => moduleSeed('Buss')).toThrow();
expect(() => moduleSeed('a-b')).toThrow();
expect(() => moduleSeed('_buss')).toThrow();
});
it('accepts internal underscores', () => {
expect(moduleSeed('libs_aapp')).toBe('libs_aapp::');
});
});
describe('isModuleSeed()', () => {
it('returns true for valid seeds', () => {
expect(isModuleSeed('buss::')).toBe(true);
expect(isModuleSeed(moduleSeed('sess'))).toBe(true);
});
it('returns false for full ErrCodes', () => {
expect(isModuleSeed('buss::disposed')).toBe(false);
expect(isModuleSeed(code('buss::disposed'))).toBe(false);
});
it('returns false for malformed strings', () => {
expect(isModuleSeed('buss')).toBe(false);
expect(isModuleSeed('::')).toBe(false);
expect(isModuleSeed('Buss::')).toBe(false);
});
});
describe('errCode()', () => {
it('appends segment to a seed with no separator', () => {
const seed = moduleSeed('buss');
expect(errCode(seed, 'disposed')).toBe('buss::disposed');
});
it('appends segment to an existing code with a dot', () => {
const seed = moduleSeed('buss');
const listener = errCode(seed, 'listener');
expect(errCode(listener, 'failed')).toBe('buss::listener.failed');
});
it('chains naturally for deeper hierarchies', () => {
const seed = moduleSeed('auth');
const user = errCode(seed, 'user');
const login = errCode(user, 'login');
expect(errCode(login, 'failed')).toBe('auth::user.login.failed');
});
it('throws on invalid segment', () => {
const seed = moduleSeed('buss');
expect(() => errCode(seed, '')).toThrow();
expect(() => errCode(seed, 'A')).toThrow();
expect(() => errCode(seed, 'a-b')).toThrow();
expect(() => errCode(seed, '_a')).toThrow();
});
});

Powered by TurnKey Linux.