toolshed/frontend/src/tests/short-id.js
2026-09-02 22:01:05 +02:00

144 lines
6 KiB
JavaScript

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 "<domain>:<token>"', () => {
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 "<domain>:~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)
})