This commit is contained in:
j3d1 2026-08-22 23:29:02 +02:00
parent 3299c97392
commit de7d9426be
10 changed files with 259 additions and 99 deletions

View file

@ -5,23 +5,60 @@ test('encodes the worked example from docs/handles-and-shortids.md', () => {
expect(token).toBe('~DyU')
})
test('round-trips every kind as a flat [kindIndex, ...fieldValues] list', () => {
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 = [
[0, 7, 42],
[1, 3, 1000],
[2, 0],
{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: 'file', file_id: 123},
{kind: 'workflow', owner_identity_id: 2, workflow_id: 9},
]
for (const ints of cases) {
const token = encodeShortId(ints)
expect(decodeShortId(token)).toEqual(ints)
for (const named of cases) {
const token = encodeShortId(serializeShortId(named))
expect(deserializeShortId(decodeShortId(token))).toEqual(named)
}
})
test('round-trips small and large values, including zero', () => {
const values = [0, 1, 7, 42, 100, 10000, 100000, 123456789]
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 encodes as kind-tag(2 bits) + one 5-bit chunk = 7 bits, padded with exactly
// 5 zero bits - the same 5 bits as a genuine one-chunk field of value 0. This is the concrete
// case the last-field-nonzero rule exists to disambiguate.
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) {
const token = encodeShortId([2, value])
expect(decodeShortId(token)).toEqual([2, value])
expect(decodeShortId(encodeShortId([0, value]))).toEqual([0, value])
expect(decodeShortId(encodeShortId([0, value, 1]))).toEqual([0, value, 1])
}
})
@ -31,30 +68,11 @@ test('tokens are URL-safe: only unreserved characters, plus the leading ~', () =
expect(token.slice(1)).toMatch(/^[A-Za-z0-9\-_]+$/)
})
test('deserializeShortId names a decoded list into a flat shape, serializeShortId reverses it', () => {
const cases = [
[[0, 7, 42], {kind: 'item', owner_identity_id: 7, item_local_id: 42}],
[[1, 3, 1000], {kind: 'storage_location', owner_identity_id: 3, storage_location_id: 1000}],
[[2, 0], {kind: 'category', category_id: 0}],
[[3, 2, 9], {kind: 'workflow', owner_identity_id: 2, workflow_id: 9}],
[[4, 11], {kind: 'group', group_id: 11}],
[[5, 123], {kind: 'file', file_id: 123}],
]
for (const [ints, named] of cases) {
expect(deserializeShortId(ints)).toEqual(named)
expect(serializeShortId(named)).toEqual(ints)
}
})
test('kinds 3+ round-trip through the escape tag, not just the direct 0-2 range', () => {
for (const named of [
{kind: 'workflow', owner_identity_id: 2, workflow_id: 9},
{kind: 'group', group_id: 11},
{kind: 'file', file_id: 123},
]) {
const token = encodeShortId(serializeShortId(named))
expect(deserializeShortId(decodeShortId(token))).toEqual(named)
}
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', () => {
@ -75,30 +93,19 @@ test('rejects an unknown kind index', () => {
expect(() => encodeShortId([99, 1])).toThrow()
})
test('deserializeShortId is keyed by (kind, fields.length), not kind alone', () => {
// kind=1 with the arity storage_location actually has (2 fields) is fine...
expect(deserializeShortId([1, 3, 1000])).toEqual({
kind: 'storage_location',
owner_identity_id: 3,
storage_location_id: 1000
})
// ...but kind=1 with a different arity (1 field) isn't a registered schema at all, and must
// not be silently misread as a truncated storage_location.
expect(() => deserializeShortId([1, 359])).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 field count mismatch', () => {
expect(() => encodeShortId([0, 7])).toThrow()
})
test('rejects a missing field', () => {
expect(() => serializeShortId({kind: 'item', owner_identity_id: 7})).toThrow()
})
test('rejects a negative field value', () => {
expect(() => encodeShortId([2, -1])).toThrow()
expect(() => encodeShortId([0, -1, 42])).toThrow()
})