atproto/packages/lexicon/tests/general.test.ts
dan 5ade78ddb3
Add fast path skipping UTF8 length counting (#2819)
* Harden UTF8 length test cases

* Harden tests to account for new fast path

* Add fast paths that skip UTF8 encoding
2024-12-10 19:45:04 +00:00

1243 lines
38 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { CID } from 'multiformats/cid'
import { LexiconDoc, Lexicons, parseLexiconDoc } from '../src/index'
import LexiconDocs from './_scaffolds/lexicons'
describe('Lexicons collection', () => {
const lex = new Lexicons(LexiconDocs)
it('Adds schemas', () => {
expect(() => lex.add(LexiconDocs[0])).toThrow()
})
it('Correctly references all definitions', () => {
expect(lex.getDef('com.example.kitchenSink')).toEqual(
LexiconDocs[0].defs.main,
)
expect(lex.getDef('lex:com.example.kitchenSink')).toEqual(
LexiconDocs[0].defs.main,
)
expect(lex.getDef('com.example.kitchenSink#main')).toEqual(
LexiconDocs[0].defs.main,
)
expect(lex.getDef('lex:com.example.kitchenSink#main')).toEqual(
LexiconDocs[0].defs.main,
)
expect(lex.getDef('com.example.kitchenSink#object')).toEqual(
LexiconDocs[0].defs.object,
)
expect(lex.getDef('lex:com.example.kitchenSink#object')).toEqual(
LexiconDocs[0].defs.object,
)
})
})
describe('General validation', () => {
const lex = new Lexicons(LexiconDocs)
it('Validates records correctly', () => {
{
const res = lex.validate('com.example.kitchenSink', {
$type: 'com.example.kitchenSink',
object: {
object: { boolean: true },
array: ['one', 'two'],
boolean: true,
integer: 123,
string: 'string',
},
array: ['one', 'two'],
boolean: true,
integer: 123,
string: 'string',
datetime: new Date().toISOString(),
atUri: 'at://did:web:example.com/com.example.test/self',
did: 'did:web:example.com',
cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
bytes: new Uint8Array([0, 1, 2, 3]),
cidLink: CID.parse(
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
),
})
expect(res.success).toBe(true)
}
{
const res = lex.validate('com.example.kitchenSink', {})
expect(res.success).toBe(false)
if (res.success) throw new Error('Asserted')
expect(res.error?.message).toBe('Record must have the property "object"')
}
})
it('Validates objects correctly', () => {
{
const res = lex.validate('com.example.kitchenSink#object', {
object: { boolean: true },
array: ['one', 'two'],
boolean: true,
integer: 123,
string: 'string',
})
expect(res.success).toBe(true)
}
{
const res = lex.validate('com.example.kitchenSink#object', {})
expect(res.success).toBe(false)
if (res.success) throw new Error('Asserted')
expect(res.error?.message).toBe('Object must have the property "object"')
}
})
it('fails when a required property is missing', () => {
const schema = {
lexicon: 1,
id: 'com.example.kitchenSink',
defs: {
test: {
type: 'object',
required: ['foo'],
properties: {},
},
},
}
expect(() => {
parseLexiconDoc(schema)
}).toThrow('Required field \\"foo\\" not defined')
})
it('fails when unknown fields are present', () => {
const schema = {
lexicon: 1,
id: 'com.example.unknownFields',
defs: {
test: {
type: 'object',
foo: 3,
},
},
}
expect(() => {
parseLexiconDoc(schema)
}).toThrow("Unrecognized key(s) in object: 'foo'")
})
it('fails lexicon parsing when uri is invalid', () => {
const schema: LexiconDoc = {
lexicon: 1,
id: 'com.example.invalidUri',
defs: {
main: {
type: 'object',
properties: {
test: { type: 'ref', ref: 'com.example.invalid#test#test' },
},
},
},
}
expect(() => {
new Lexicons([schema])
}).toThrow('Uri can only have one hash segment')
})
it('fails validation when ref uri has multiple hash segments', () => {
const schema: LexiconDoc = {
lexicon: 1,
id: 'com.example.invalidUri',
defs: {
main: {
type: 'object',
properties: {
test: { type: 'integer' },
},
},
object: {
type: 'object',
required: ['test'],
properties: {
test: {
type: 'union',
refs: ['com.example.invalidUri'],
},
},
},
},
}
const lexicons = new Lexicons([schema])
expect(() => {
lexicons.validate('com.example.invalidUri#object', {
test: {
$type: 'com.example.invalidUri#main#main',
test: 123,
},
})
}).toThrow('Uri can only have one hash segment')
})
it('union handles both implicit and explicit #main', () => {
const schemas: LexiconDoc[] = [
{
lexicon: 1,
id: 'com.example.implicitMain',
defs: {
main: {
type: 'object',
required: ['test'],
properties: {
test: { type: 'string' },
},
},
},
},
{
lexicon: 1,
id: 'com.example.testImplicitMain',
defs: {
main: {
type: 'object',
required: ['union'],
properties: {
union: {
type: 'union',
refs: ['com.example.implicitMain'],
},
},
},
},
},
{
lexicon: 1,
id: 'com.example.testExplicitMain',
defs: {
main: {
type: 'object',
required: ['union'],
properties: {
union: {
type: 'union',
refs: ['com.example.implicitMain#main'],
},
},
},
},
},
]
const lexicon = new Lexicons(schemas)
let result = lexicon.validate('com.example.testImplicitMain', {
union: {
$type: 'com.example.implicitMain',
test: 123,
},
})
expect(result.success).toBeFalsy()
expect(result['error']?.message).toBe('Object/union/test must be a string')
result = lexicon.validate('com.example.testImplicitMain', {
union: {
$type: 'com.example.implicitMain#main',
test: 123,
},
})
expect(result.success).toBeFalsy()
expect(result['error']?.message).toBe('Object/union/test must be a string')
result = lexicon.validate('com.example.testExplicitMain', {
union: {
$type: 'com.example.implicitMain',
test: 123,
},
})
expect(result.success).toBeFalsy()
expect(result['error']?.message).toBe('Object/union/test must be a string')
result = lexicon.validate('com.example.testExplicitMain', {
union: {
$type: 'com.example.implicitMain#main',
test: 123,
},
})
expect(result.success).toBeFalsy()
expect(result['error']?.message).toBe('Object/union/test must be a string')
})
})
describe('Record validation', () => {
const lex = new Lexicons(LexiconDocs)
const passingSink = {
$type: 'com.example.kitchenSink',
object: {
object: { boolean: true },
array: ['one', 'two'],
boolean: true,
integer: 123,
string: 'string',
},
array: ['one', 'two'],
boolean: true,
integer: 123,
string: 'string',
bytes: new Uint8Array([0, 1, 2, 3]),
cidLink: CID.parse(
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
),
}
it('Passes valid schemas', () => {
lex.assertValidRecord('com.example.kitchenSink', passingSink)
})
it('Fails invalid input types', () => {
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', undefined),
).toThrow('Record must be an object')
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', 1234),
).toThrow('Record must be an object')
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', 'string'),
).toThrow('Record must be an object')
})
it('Fails incorrect $type', () => {
expect(() => lex.assertValidRecord('com.example.kitchenSink', {})).toThrow(
'Record/$type must be a string',
)
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', { $type: 'foo' }),
).toThrow('Invalid $type: must be lex:com.example.kitchenSink, got foo')
})
it('Fails missing required', () => {
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', {
$type: 'com.example.kitchenSink',
array: ['one', 'two'],
boolean: true,
integer: 123,
string: 'string',
datetime: new Date().toISOString(),
atUri: 'at://did:web:example.com/com.example.test/self',
did: 'did:web:example.com',
cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
bytes: new Uint8Array([0, 1, 2, 3]),
cidLink: CID.parse(
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
),
}),
).toThrow('Record must have the property "object"')
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', {
...passingSink,
object: undefined,
}),
).toThrow('Record must have the property "object"')
})
it('Fails incorrect types', () => {
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', {
...passingSink,
object: {
...passingSink.object,
object: { boolean: '1234' },
},
}),
).toThrow('Record/object/object/boolean must be a boolean')
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', {
...passingSink,
object: true,
}),
).toThrow('Record/object must be an object')
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', {
...passingSink,
array: 1234,
}),
).toThrow('Record/array must be an array')
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', {
...passingSink,
integer: true,
}),
).toThrow('Record/integer must be an integer')
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', {
...passingSink,
string: {},
}),
).toThrow('Record/string must be a string')
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', {
...passingSink,
bytes: 1234,
}),
).toThrow('Record/bytes must be a byte array')
expect(() =>
lex.assertValidRecord('com.example.kitchenSink', {
...passingSink,
cidLink: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
}),
).toThrow('Record/cidLink must be a CID')
})
it('Handles optional properties correctly', () => {
lex.assertValidRecord('com.example.optional', {
$type: 'com.example.optional',
})
})
it('Handles default properties correctly', () => {
const result = lex.assertValidRecord('com.example.default', {
$type: 'com.example.default',
object: {},
})
expect(result).toEqual({
$type: 'com.example.default',
boolean: false,
integer: 0,
string: '',
object: {
boolean: true,
integer: 1,
string: 'x',
},
})
expect(result).not.toHaveProperty('datetime')
})
it('Handles unions correctly', () => {
lex.assertValidRecord('com.example.union', {
$type: 'com.example.union',
unionOpen: {
$type: 'com.example.kitchenSink#object',
object: { boolean: true },
array: ['one', 'two'],
boolean: true,
integer: 123,
string: 'string',
},
unionClosed: {
$type: 'com.example.kitchenSink#subobject',
boolean: true,
},
})
lex.assertValidRecord('com.example.union', {
$type: 'com.example.union',
unionOpen: {
$type: 'com.example.other',
},
unionClosed: {
$type: 'com.example.kitchenSink#subobject',
boolean: true,
},
})
expect(() =>
lex.assertValidRecord('com.example.union', {
$type: 'com.example.union',
unionOpen: {},
unionClosed: {},
}),
).toThrow(
'Record/unionOpen must be an object which includes the "$type" property',
)
expect(() =>
lex.assertValidRecord('com.example.union', {
$type: 'com.example.union',
unionOpen: {
$type: 'com.example.other',
},
unionClosed: {
$type: 'com.example.other',
boolean: true,
},
}),
).toThrow(
'Record/unionClosed $type must be one of lex:com.example.kitchenSink#object, lex:com.example.kitchenSink#subobject',
)
})
it('Handles unknowns correctly', () => {
lex.assertValidRecord('com.example.unknown', {
$type: 'com.example.unknown',
unknown: { foo: 'bar' },
})
expect(() =>
lex.assertValidRecord('com.example.unknown', {
$type: 'com.example.unknown',
}),
).toThrow('Record must have the property "unknown"')
})
it('Applies array length constraints', () => {
lex.assertValidRecord('com.example.arrayLength', {
$type: 'com.example.arrayLength',
array: [1, 2, 3],
})
expect(() =>
lex.assertValidRecord('com.example.arrayLength', {
$type: 'com.example.arrayLength',
array: [1],
}),
).toThrow('Record/array must not have fewer than 2 elements')
expect(() =>
lex.assertValidRecord('com.example.arrayLength', {
$type: 'com.example.arrayLength',
array: [1, 2, 3, 4, 5],
}),
).toThrow('Record/array must not have more than 4 elements')
})
it('Applies array item constraints', () => {
expect(() =>
lex.assertValidRecord('com.example.arrayLength', {
$type: 'com.example.arrayLength',
array: [1, '2', 3],
}),
).toThrow('Record/array/1 must be an integer')
expect(() =>
lex.assertValidRecord('com.example.arrayLength', {
$type: 'com.example.arrayLength',
array: [1, undefined, 3],
}),
).toThrow('Record/array/1 must be an integer')
})
it('Applies boolean const constraint', () => {
lex.assertValidRecord('com.example.boolConst', {
$type: 'com.example.boolConst',
boolean: false,
})
expect(() =>
lex.assertValidRecord('com.example.boolConst', {
$type: 'com.example.boolConst',
boolean: true,
}),
).toThrow('Record/boolean must be false')
})
it('Applies integer range constraint', () => {
lex.assertValidRecord('com.example.integerRange', {
$type: 'com.example.integerRange',
integer: 2,
})
expect(() =>
lex.assertValidRecord('com.example.integerRange', {
$type: 'com.example.integerRange',
integer: 1,
}),
).toThrow('Record/integer can not be less than 2')
expect(() =>
lex.assertValidRecord('com.example.integerRange', {
$type: 'com.example.integerRange',
integer: 5,
}),
).toThrow('Record/integer can not be greater than 4')
})
it('Applies integer enum constraint', () => {
lex.assertValidRecord('com.example.integerEnum', {
$type: 'com.example.integerEnum',
integer: 2,
})
expect(() =>
lex.assertValidRecord('com.example.integerEnum', {
$type: 'com.example.integerEnum',
integer: 0,
}),
).toThrow('Record/integer must be one of (1|2)')
})
it('Applies integer const constraint', () => {
lex.assertValidRecord('com.example.integerConst', {
$type: 'com.example.integerConst',
integer: 0,
})
expect(() =>
lex.assertValidRecord('com.example.integerConst', {
$type: 'com.example.integerConst',
integer: 1,
}),
).toThrow('Record/integer must be 0')
})
it('Applies integer whole-number constraint', () => {
expect(() =>
lex.assertValidRecord('com.example.integerRange', {
$type: 'com.example.integerRange',
integer: 2.5,
}),
).toThrow('Record/integer must be an integer')
})
it('Applies string length constraint', () => {
// Shorter than two UTF8 characters
expect(() =>
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: '',
}),
).toThrow('Record/string must not be shorter than 2 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: 'a',
}),
).toThrow('Record/string must not be shorter than 2 characters')
// Two to four UTF8 characters
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: 'ab',
})
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: '\u0301', // Combining acute accent (2 bytes)
})
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: 'a\u0301', // 'a' + combining acute accent (1 + 2 bytes = 3 bytes)
})
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: 'aé', // 'a' (1 byte) + 'é' (2 bytes) = 3 bytes
})
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: 'abc',
})
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: '一', // CJK character (3 bytes)
})
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: '\uD83D', // Unpaired high surrogate (3 bytes)
})
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: 'abcd',
})
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: 'éé', // 'é' + 'é' (2 + 2 bytes = 4 bytes)
})
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: 'aaé', // 1 + 1 + 2 = 4 bytes
})
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: '👋', // 4 bytes
})
expect(() =>
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: 'abcde',
}),
).toThrow('Record/string must not be longer than 4 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: 'a\u0301\u0301', // 1 + (2 * 2) = 5 bytes
}),
).toThrow('Record/string must not be longer than 4 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: '\uD83D\uD83D', // Two unpaired high surrogates (3 * 2 = 6 bytes)
}),
).toThrow('Record/string must not be longer than 4 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: 'ééé', // 2 + 2 + 2 bytes = 6 bytes
}),
).toThrow('Record/string must not be longer than 4 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: '👋a', // 4 + 1 bytes = 5 bytes
}),
).toThrow('Record/string must not be longer than 4 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: '👨👨', // 4 + 4 = 8 bytes
}),
).toThrow('Record/string must not be longer than 4 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLength', {
$type: 'com.example.stringLength',
string: '👨‍👩‍👧‍👧', // 4 emojis × 4 bytes + 3 ZWJs × 3 bytes = 25 bytes
}),
).toThrow('Record/string must not be longer than 4 characters')
})
it('Applies string length constraint (no minLength)', () => {
// Shorter than two UTF8 characters
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: '',
})
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: 'a',
})
// Two to four UTF8 characters
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: 'ab',
})
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: '\u0301', // Combining acute accent (2 bytes)
})
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: 'a\u0301', // 'a' + combining acute accent (1 + 2 bytes = 3 bytes)
})
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: 'aé', // 'a' (1 byte) + 'é' (2 bytes) = 3 bytes
})
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: 'abc',
})
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: '一', // CJK character (3 bytes)
})
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: '\uD83D', // Unpaired high surrogate (3 bytes)
})
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: 'abcd',
})
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: 'éé', // 'é' + 'é' (2 + 2 bytes = 4 bytes)
})
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: 'aaé', // 1 + 1 + 2 = 4 bytes
})
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: '👋', // 4 bytes
})
expect(() =>
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: 'abcde',
}),
).toThrow('Record/string must not be longer than 4 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: 'a\u0301\u0301', // 1 + (2 * 2) = 5 bytes
}),
).toThrow('Record/string must not be longer than 4 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: '\uD83D\uD83D', // Two unpaired high surrogates (3 * 2 = 6 bytes)
}),
).toThrow('Record/string must not be longer than 4 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: 'ééé', // 2 + 2 + 2 bytes = 6 bytes
}),
).toThrow('Record/string must not be longer than 4 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: '👋a', // 4 + 1 bytes = 5 bytes
}),
).toThrow('Record/string must not be longer than 4 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: '👨👨', // 4 + 4 = 8 bytes
}),
).toThrow('Record/string must not be longer than 4 characters')
expect(() =>
lex.assertValidRecord('com.example.stringLengthNoMinLength', {
$type: 'com.example.stringLengthNoMinLength',
string: '👨‍👩‍👧‍👧', // 4 emojis × 4 bytes + 3 ZWJs × 3 bytes = 25 bytes
}),
).toThrow('Record/string must not be longer than 4 characters')
})
it('Applies grapheme string length constraint', () => {
// Shorter than two graphemes
expect(() =>
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: '',
}),
).toThrow('Record/string must not be shorter than 2 graphemes')
expect(() =>
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: '\u0301\u0301\u0301', // Three combining acute accents
}),
).toThrow('Record/string must not be shorter than 2 graphemes')
expect(() =>
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: 'a',
}),
).toThrow('Record/string must not be shorter than 2 graphemes')
expect(() =>
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: 'a\u0301\u0301\u0301\u0301', // 'á́́́' ('a' with four combining acute accents)
}),
).toThrow('Record/string must not be shorter than 2 graphemes')
expect(() =>
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: '5\uFE0F', // '5' with emoji presentation
}),
).toThrow('Record/string must not be shorter than 2 graphemes')
expect(() =>
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: '👨‍👩‍👧‍👧',
}),
).toThrow('Record/string must not be shorter than 2 graphemes')
// Two to four graphemes
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: 'ab',
})
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: 'a\u0301b', // 'áb' with combining accent
})
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: 'a\u0301b\u0301', // 'áb́'
})
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: '😀😀',
})
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: '12👨👩👧👧',
})
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: 'abcd',
})
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: 'a\u0301b\u0301c\u0301d\u0301', // 'áb́ćd́'
})
// Longer than four graphemes
expect(() =>
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: 'abcde',
}),
).toThrow('Record/string must not be longer than 4 graphemes')
expect(() =>
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: 'a\u0301b\u0301c\u0301d\u0301e\u0301', // 'áb́ćd́é'
}),
).toThrow('Record/string must not be longer than 4 graphemes')
expect(() =>
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: '😀😀😀😀😀',
}),
).toThrow('Record/string must not be longer than 4 graphemes')
expect(() =>
lex.assertValidRecord('com.example.stringLengthGrapheme', {
$type: 'com.example.stringLengthGrapheme',
string: 'ab😀de',
}),
).toThrow('Record/string must not be longer than 4 graphemes')
})
it('Applies string enum constraint', () => {
lex.assertValidRecord('com.example.stringEnum', {
$type: 'com.example.stringEnum',
string: 'a',
})
expect(() =>
lex.assertValidRecord('com.example.stringEnum', {
$type: 'com.example.stringEnum',
string: 'c',
}),
).toThrow('Record/string must be one of (a|b)')
})
it('Applies string const constraint', () => {
lex.assertValidRecord('com.example.stringConst', {
$type: 'com.example.stringConst',
string: 'a',
})
expect(() =>
lex.assertValidRecord('com.example.stringConst', {
$type: 'com.example.stringConst',
string: 'b',
}),
).toThrow('Record/string must be a')
})
it('Applies datetime formatting constraint', () => {
for (const datetime of [
'2022-12-12T00:50:36.809Z',
'2022-12-12T00:50:36Z',
'2022-12-12T00:50:36.8Z',
'2022-12-12T00:50:36.80Z',
'2022-12-12T00:50:36+00:00',
'2022-12-12T00:50:36.8+00:00',
'2022-12-11T19:50:36-05:00',
'2022-12-11T19:50:36.8-05:00',
'2022-12-11T19:50:36.80-05:00',
'2022-12-11T19:50:36.809-05:00',
]) {
lex.assertValidRecord('com.example.datetime', {
$type: 'com.example.datetime',
datetime,
})
}
expect(() =>
lex.assertValidRecord('com.example.datetime', {
$type: 'com.example.datetime',
datetime: 'bad date',
}),
).toThrow(
'Record/datetime must be an valid atproto datetime (both RFC-3339 and ISO-8601)',
)
})
it('Applies uri formatting constraint', () => {
for (const uri of [
'https://example.com',
'https://example.com/with/path',
'https://example.com/with/path?and=query',
'at://bsky.social',
'did:example:test',
]) {
lex.assertValidRecord('com.example.uri', {
$type: 'com.example.uri',
uri,
})
}
expect(() =>
lex.assertValidRecord('com.example.uri', {
$type: 'com.example.uri',
uri: 'not a uri',
}),
).toThrow('Record/uri must be a uri')
})
it('Applies at-uri formatting constraint', () => {
lex.assertValidRecord('com.example.atUri', {
$type: 'com.example.atUri',
atUri: 'at://did:web:example.com/com.example.test/self',
})
expect(() =>
lex.assertValidRecord('com.example.atUri', {
$type: 'com.example.atUri',
atUri: 'http://not-atproto.com',
}),
).toThrow('Record/atUri must be a valid at-uri')
})
it('Applies did formatting constraint', () => {
lex.assertValidRecord('com.example.did', {
$type: 'com.example.did',
did: 'did:web:example.com',
})
lex.assertValidRecord('com.example.did', {
$type: 'com.example.did',
did: 'did:plc:12345678abcdefghijklmnop',
})
expect(() =>
lex.assertValidRecord('com.example.did', {
$type: 'com.example.did',
did: 'bad did',
}),
).toThrow('Record/did must be a valid did')
expect(() =>
lex.assertValidRecord('com.example.did', {
$type: 'com.example.did',
did: 'did:short',
}),
).toThrow('Record/did must be a valid did')
})
it('Applies handle formatting constraint', () => {
lex.assertValidRecord('com.example.handle', {
$type: 'com.example.handle',
handle: 'test.bsky.social',
})
lex.assertValidRecord('com.example.handle', {
$type: 'com.example.handle',
handle: 'bsky.test',
})
expect(() =>
lex.assertValidRecord('com.example.handle', {
$type: 'com.example.handle',
handle: 'bad handle',
}),
).toThrow('Record/handle must be a valid handle')
expect(() =>
lex.assertValidRecord('com.example.handle', {
$type: 'com.example.handle',
handle: '-bad-.test',
}),
).toThrow('Record/handle must be a valid handle')
})
it('Applies at-identifier formatting constraint', () => {
lex.assertValidRecord('com.example.atIdentifier', {
$type: 'com.example.atIdentifier',
atIdentifier: 'bsky.test',
})
lex.assertValidRecord('com.example.atIdentifier', {
$type: 'com.example.atIdentifier',
atIdentifier: 'did:plc:12345678abcdefghijklmnop',
})
expect(() =>
lex.assertValidRecord('com.example.atIdentifier', {
$type: 'com.example.atIdentifier',
atIdentifier: 'bad id',
}),
).toThrow('Record/atIdentifier must be a valid did or a handle')
expect(() =>
lex.assertValidRecord('com.example.atIdentifier', {
$type: 'com.example.atIdentifier',
atIdentifier: '-bad-.test',
}),
).toThrow('Record/atIdentifier must be a valid did or a handle')
})
it('Applies nsid formatting constraint', () => {
lex.assertValidRecord('com.example.nsid', {
$type: 'com.example.nsid',
nsid: 'com.atproto.test',
})
lex.assertValidRecord('com.example.nsid', {
$type: 'com.example.nsid',
nsid: 'app.bsky.nested.test',
})
expect(() =>
lex.assertValidRecord('com.example.nsid', {
$type: 'com.example.nsid',
nsid: 'bad nsid',
}),
).toThrow('Record/nsid must be a valid nsid')
expect(() =>
lex.assertValidRecord('com.example.nsid', {
$type: 'com.example.nsid',
nsid: 'com.bad-.foo',
}),
).toThrow('Record/nsid must be a valid nsid')
})
it('Applies cid formatting constraint', () => {
lex.assertValidRecord('com.example.cid', {
$type: 'com.example.cid',
cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
})
expect(() =>
lex.assertValidRecord('com.example.cid', {
$type: 'com.example.cid',
cid: 'abapsdofiuwrpoiasdfuaspdfoiu',
}),
).toThrow('Record/cid must be a cid string')
})
it('Applies language formatting constraint', () => {
lex.assertValidRecord('com.example.language', {
$type: 'com.example.language',
language: 'en-US-boont',
})
expect(() =>
lex.assertValidRecord('com.example.language', {
$type: 'com.example.language',
language: 'not-a-language-',
}),
).toThrow('Record/language must be a well-formed BCP 47 language tag')
})
it('Applies bytes length constraints', () => {
lex.assertValidRecord('com.example.byteLength', {
$type: 'com.example.byteLength',
bytes: new Uint8Array([1, 2, 3]),
})
expect(() =>
lex.assertValidRecord('com.example.byteLength', {
$type: 'com.example.byteLength',
bytes: new Uint8Array([1]),
}),
).toThrow('Record/bytes must not be smaller than 2 bytes')
expect(() =>
lex.assertValidRecord('com.example.byteLength', {
$type: 'com.example.byteLength',
bytes: new Uint8Array([1, 2, 3, 4, 5]),
}),
).toThrow('Record/bytes must not be larger than 4 bytes')
})
})
describe('XRPC parameter validation', () => {
const lex = new Lexicons(LexiconDocs)
it('Passes valid parameters', () => {
const queryResult = lex.assertValidXrpcParams('com.example.query', {
boolean: true,
integer: 123,
string: 'string',
array: ['x', 'y'],
})
expect(queryResult).toEqual({
boolean: true,
integer: 123,
string: 'string',
array: ['x', 'y'],
def: 0,
})
const paramResult = lex.assertValidXrpcParams('com.example.procedure', {
boolean: true,
integer: 123,
string: 'string',
array: ['x', 'y'],
def: 1,
})
expect(paramResult).toEqual({
boolean: true,
integer: 123,
string: 'string',
array: ['x', 'y'],
def: 1,
})
})
it('Handles required correctly', () => {
lex.assertValidXrpcParams('com.example.query', {
boolean: true,
integer: 123,
})
expect(() =>
lex.assertValidXrpcParams('com.example.query', {
boolean: true,
}),
).toThrow('Params must have the property "integer"')
expect(() =>
lex.assertValidXrpcParams('com.example.query', {
boolean: true,
integer: undefined,
}),
).toThrow('Params must have the property "integer"')
})
it('Validates parameter types', () => {
expect(() =>
lex.assertValidXrpcParams('com.example.query', {
boolean: 'string',
integer: 123,
string: 'string',
}),
).toThrow('boolean must be a boolean')
expect(() =>
lex.assertValidXrpcParams('com.example.query', {
boolean: true,
float: 123.45,
integer: 123,
string: 'string',
array: 'x',
}),
).toThrow('array must be an array')
})
})
describe('XRPC input validation', () => {
const lex = new Lexicons(LexiconDocs)
it('Passes valid inputs', () => {
lex.assertValidXrpcInput('com.example.procedure', {
object: { boolean: true },
array: ['one', 'two'],
boolean: true,
float: 123.45,
integer: 123,
string: 'string',
})
})
it('Validates the input', () => {
// dont need to check this extensively since it's the same logic as tested in record validation
expect(() =>
lex.assertValidXrpcInput('com.example.procedure', {
object: { boolean: 'string' },
array: ['one', 'two'],
boolean: true,
float: 123.45,
integer: 123,
string: 'string',
}),
).toThrow('Input/object/boolean must be a boolean')
expect(() => lex.assertValidXrpcInput('com.example.procedure', {})).toThrow(
'Input must have the property "object"',
)
})
})
describe('XRPC output validation', () => {
const lex = new Lexicons(LexiconDocs)
it('Passes valid outputs', () => {
lex.assertValidXrpcOutput('com.example.query', {
object: { boolean: true },
array: ['one', 'two'],
boolean: true,
float: 123.45,
integer: 123,
string: 'string',
})
lex.assertValidXrpcOutput('com.example.procedure', {
object: { boolean: true },
array: ['one', 'two'],
boolean: true,
float: 123.45,
integer: 123,
string: 'string',
})
})
it('Validates the output', () => {
// dont need to check this extensively since it's the same logic as tested in record validation
expect(() =>
lex.assertValidXrpcOutput('com.example.query', {
object: { boolean: 'string' },
array: ['one', 'two'],
boolean: true,
float: 123.45,
integer: 123,
string: 'string',
}),
).toThrow('Output/object/boolean must be a boolean')
expect(() =>
lex.assertValidXrpcOutput('com.example.procedure', {}),
).toThrow('Output must have the property "object"')
})
})