import { encodeShortId, decodeShortId, deserializeShortId, serializeShortId, encodeDomainQualifiedShortId, decodeDomainQualifiedShortId, isDomainQualifiedShortId } from '../short-id.js' test('encodes the worked example from docs/handles-and-shortids.md', () => { const token = encodeShortId([0, 7, 42]) expect(token).toBe('~DyU') }) test('kind ids are scoped per arity: id 0 means item (arity 2) or category (arity 1)', () => { expect(deserializeShortId(decodeShortId(encodeShortId([0, 7, 42])))).toEqual({ kind: 'item', owner_identity_id: 7, item_local_id: 42 }) expect(deserializeShortId(decodeShortId(encodeShortId([0, 5])))).toEqual({ kind: 'category', category_id: 5 }) }) test('every kind round-trips through serialize/encode/decode/deserialize', () => { const cases = [ {kind: 'item', owner_identity_id: 7, item_local_id: 42}, {kind: 'category', category_id: 5}, {kind: 'group_item', owner_group_id: 5, item_local_id: 42}, {kind: 'group', group_id: 11}, {kind: 'storage_location', owner_identity_id: 3, storage_location_id: 1000}, {kind: 'group_storage_location', owner_group_id: 5, storage_location_id: 1000}, {kind: 'file', file_id: 123}, {kind: 'workflow', owner_identity_id: 2, workflow_id: 9}, ] for (const named of cases) { const token = encodeShortId(serializeShortId(named)) expect(deserializeShortId(decodeShortId(token))).toEqual(named) } }) test('a non-last field may be 0 - only the last field is restricted', () => { const named = {kind: 'item', owner_identity_id: 0, item_local_id: 42} const token = encodeShortId(serializeShortId(named)) expect(deserializeShortId(decodeShortId(token))).toEqual(named) }) test('rejects a 0 last-field value, for a single-field kind', () => { expect(() => encodeShortId(serializeShortId({kind: 'category', category_id: 0}))).toThrow() }) test('rejects a 0 last-field value, for a multi-field kind', () => { expect(() => encodeShortId(serializeShortId({kind: 'item', owner_identity_id: 7, item_local_id: 0}))).toThrow() }) test('decoding never over-reads: a single-chunk field that exactly exhausts padding is not ' + 'mistaken for a second field', () => { // category_id 5 -> 7 bits + 5 zero padding bits, identical to a genuine one-chunk zero field - the case the last-field-nonzero rule disambiguates. const token = encodeShortId([0, 5]) expect(token).toBe('~Cg') expect(decodeShortId(token)).toEqual([0, 5]) }) test('round-trips small and large nonzero values, in both last and non-last field position', () => { const values = [1, 7, 42, 100, 10000, 100000, 123456789] for (const value of values) { expect(decodeShortId(encodeShortId([0, value]))).toEqual([0, value]) expect(decodeShortId(encodeShortId([0, value, 1]))).toEqual([0, value, 1]) } }) test('tokens are URL-safe: only unreserved characters, plus the leading ~', () => { const token = encodeShortId([0, 123456, 987654]) expect(token[0]).toBe('~') expect(token.slice(1)).toMatch(/^[A-Za-z0-9\-_]+$/) }) test('kinds at escaped ids (3+) round-trip through the escape tag, not just the direct 0-2 range', () => { const token = encodeShortId(serializeShortId({kind: 'workflow', owner_identity_id: 2, workflow_id: 9})) expect(deserializeShortId(decodeShortId(token))).toEqual({ kind: 'workflow', owner_identity_id: 2, workflow_id: 9 }) }) test('serializeShortId then deserializeShortId round-trips through encode/decode', () => { const token = encodeShortId(serializeShortId({kind: 'item', owner_identity_id: 7, item_local_id: 42})) expect(token).toBe('~DyU') expect(deserializeShortId(decodeShortId(token))).toEqual({ kind: 'item', owner_identity_id: 7, item_local_id: 42 }) }) test('rejects a token missing the leading ~', () => { expect(() => decodeShortId('DyU')).toThrow() }) test('rejects an unknown kind index', () => { expect(() => encodeShortId([99, 1])).toThrow() }) test('rejects an arity with no registered schema at that kind index', () => { // kind 0 is registered for arity 2 (item) and arity 1 (category), but not arity 3. expect(() => encodeShortId([0, 7, 8, 9])).toThrow() }) test('rejects an unknown kind name', () => { expect(() => serializeShortId({kind: 'not_a_kind'})).toThrow() }) test('rejects a missing field', () => { expect(() => serializeShortId({kind: 'item', owner_identity_id: 7})).toThrow() }) test('rejects a negative field value', () => { expect(() => encodeShortId([0, -1, 42])).toThrow() }) test('encodes a Domain-Qualified Short ID as ":"', () => { expect(encodeDomainQualifiedShortId('toolsheddomain.tld', [0, 7, 42])).toBe('toolsheddomain.tld:~DyU') }) test('decodes a Domain-Qualified Short ID back into its domain and ints', () => { const decoded = decodeDomainQualifiedShortId('toolsheddomain.tld:~DyU') expect(decoded.domain).toBe('toolsheddomain.tld') expect(decoded.token).toBe('~DyU') expect(decoded.ints).toEqual([0, 7, 42]) }) test('round-trips a Domain-Qualified Short ID through encode/decode', () => { const named = {kind: 'storage_location', owner_identity_id: 3, storage_location_id: 1000} const encoded = encodeDomainQualifiedShortId('example.com', serializeShortId(named)) const {domain, ints} = decodeDomainQualifiedShortId(encoded) expect(domain).toBe('example.com') expect(deserializeShortId(ints)).toEqual(named) }) test('rejects a Domain-Qualified Short ID missing the "~" after the domain', () => { expect(() => decodeDomainQualifiedShortId('toolsheddomain.tldDyU')).toThrow() expect(() => decodeDomainQualifiedShortId('~DyU')).toThrow() }) test('isDomainQualifiedShortId tells a ":~token" apart from a bare token', () => { expect(isDomainQualifiedShortId('toolsheddomain.tld:~DyU')).toBe(true) expect(isDomainQualifiedShortId('~DyU')).toBe(false) expect(isDomainQualifiedShortId('alice@example.com:i42')).toBe(false) expect(isDomainQualifiedShortId('not a short id at all')).toBe(false) })