144 lines
6 KiB
JavaScript
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)
|
|
})
|