A plain JavaScript validator for AT Protocol lexicon schemas
at main 222 lines 7.6 kB view raw
1import { describe, test, expect } from 'vitest'; 2import { defineLexicon, validateLexicons, validateRecord } from './lexicon.js'; 3import { atprotoValidateLexicons, atprotoValidateRecord } from './test/atproto-oracle.js'; 4 5// Schema inputs 6import * as schemaInputs from './test/inputs/schema/index.js'; 7 8// Data inputs 9import * as dataInputs from './test/inputs/data/index.js'; 10 11// Format inputs 12import * as formatInputs from './test/inputs/format/index.js'; 13 14// E2E inputs 15import * as e2eInputs from './test/inputs/e2e/index.js'; 16 17// Collect all schema inputs 18const allSchemaInputs = [ 19 ...schemaInputs.stringSchemaInputs, 20 ...schemaInputs.integerSchemaInputs, 21 ...schemaInputs.booleanSchemaInputs, 22 ...schemaInputs.objectSchemaInputs, 23 ...schemaInputs.arraySchemaInputs, 24 ...schemaInputs.unionSchemaInputs, 25 ...schemaInputs.refSchemaInputs, 26 ...schemaInputs.blobSchemaInputs, 27 ...schemaInputs.bytesSchemaInputs, 28 ...schemaInputs.tokenSchemaInputs, 29 ...schemaInputs.unknownSchemaInputs, 30 ...schemaInputs.recordSchemaInputs, 31 ...schemaInputs.paramsSchemaInputs, 32 ...schemaInputs.querySchemaInputs, 33 ...schemaInputs.procedureSchemaInputs, 34 ...schemaInputs.subscriptionSchemaInputs, 35]; 36 37// Collect all data inputs (record types only - validateRecord doesn't handle query/procedure/params) 38const allDataInputs = [ 39 ...dataInputs.stringDataInputs, 40 ...dataInputs.integerDataInputs, 41 ...dataInputs.objectDataInputs, 42 ...dataInputs.arrayDataInputs, 43 ...dataInputs.blobDataInputs, 44 ...dataInputs.bytesDataInputs, 45 ...dataInputs.tokenDataInputs, 46 ...dataInputs.unknownDataInputs, 47 ...dataInputs.unionDataInputs, 48 ...dataInputs.cidLinkDataInputs, 49]; 50 51// Collect all format inputs 52const allFormatInputs = [ 53 ...formatInputs.datetimeFormatInputs, 54 ...formatInputs.handleFormatInputs, 55 ...formatInputs.didFormatInputs, 56 ...formatInputs.uriFormatInputs, 57 ...formatInputs.atUriFormatInputs, 58 ...formatInputs.cidFormatInputs, 59 ...formatInputs.tidFormatInputs, 60 ...formatInputs.nsidFormatInputs, 61 ...formatInputs.languageFormatInputs, 62 ...formatInputs.recordKeyFormatInputs, 63]; 64 65// Collect all e2e inputs 66const allE2eInputs = [ 67 ...e2eInputs.bskyPostInputs, 68 ...e2eInputs.bskyProfileInputs, 69 ...e2eInputs.integrationInputs, 70]; 71 72// Helper: determine expected result from test name 73// Tests with 'valid' in name should pass, 'invalid' should fail 74function expectedValid(name) { 75 if (name.includes('-valid-')) return true; 76 if (name.includes('-invalid-')) return false; 77 throw new Error(`Test name must contain '-valid-' or '-invalid-': ${name}`); 78} 79 80// ========================================================================== 81// SPEC-BASED TESTS 82// ========================================================================== 83 84describe('schema validation', () => { 85 for (const input of allSchemaInputs) { 86 test(input.name, () => { 87 const lexicons = input.additionalLexicons 88 ? [input.lexicon, ...input.additionalLexicons] 89 : [input.lexicon]; 90 const result = validateLexicons(lexicons); 91 const isValid = result === null; 92 expect(isValid).toBe(expectedValid(input.name)); 93 }); 94 } 95}); 96 97describe('data validation', () => { 98 for (const input of allDataInputs) { 99 test(input.name, () => { 100 const result = validateRecord(input.lexicons, input.collection, input.record); 101 const isValid = result === null; 102 expect(isValid).toBe(expectedValid(input.name)); 103 }); 104 } 105}); 106 107describe('format validation', () => { 108 for (const input of allFormatInputs) { 109 test(input.name, () => { 110 const result = validateRecord(input.lexicons, input.collection, input.record); 111 const isValid = result === null; 112 expect(isValid).toBe(expectedValid(input.name)); 113 }); 114 } 115}); 116 117describe('e2e validation', () => { 118 for (const input of allE2eInputs) { 119 test(input.name, () => { 120 const result = validateRecord(input.lexicons, input.collection, input.record); 121 const isValid = result === null; 122 expect(isValid).toBe(expectedValid(input.name)); 123 }); 124 } 125}); 126 127// Ref data validation - validates data against resolved ref schemas 128describe('ref data validation', () => { 129 for (const input of dataInputs.refDataInputs) { 130 test(input.name, () => { 131 const result = validateRecord(input.lexicons, input.collection, input.record); 132 const isValid = result === null; 133 expect(isValid).toBe(expectedValid(input.name)); 134 }); 135 } 136}); 137 138// Union ref data validation - validates union data against resolved ref schemas 139describe('union ref data validation', () => { 140 for (const input of dataInputs.unionRefDataInputs) { 141 test(input.name, () => { 142 const result = validateRecord(input.lexicons, input.collection, input.record); 143 const isValid = result === null; 144 expect(isValid).toBe(expectedValid(input.name)); 145 }); 146 } 147}); 148 149// ========================================================================== 150// @ATPROTO/LEXICON COMPARISON (informational - differences are expected) 151// See docs/oracle-differences.md for details on behavioral differences. 152// ========================================================================== 153 154describe.skip('@atproto/lexicon comparison', () => { 155 describe('schema validation', () => { 156 for (const input of allSchemaInputs) { 157 test(input.name, () => { 158 const lexicons = input.additionalLexicons 159 ? [input.lexicon, ...input.additionalLexicons] 160 : [input.lexicon]; 161 const atprotoResult = atprotoValidateLexicons(lexicons); 162 const ourResult = validateLexicons(lexicons); 163 164 const atprotoValid = atprotoResult === null; 165 const ourValid = ourResult === null; 166 expect(ourValid).toBe(atprotoValid); 167 }); 168 } 169 }); 170 171 describe('data validation', () => { 172 for (const input of allDataInputs) { 173 test(input.name, () => { 174 const atprotoResult = atprotoValidateRecord(input.lexicons, input.collection, input.record); 175 const ourResult = validateRecord(input.lexicons, input.collection, input.record); 176 177 const atprotoValid = atprotoResult === null; 178 const ourValid = ourResult === null; 179 expect(ourValid).toBe(atprotoValid); 180 }); 181 } 182 }); 183 184 describe('format validation', () => { 185 for (const input of allFormatInputs) { 186 test(input.name, () => { 187 const atprotoResult = atprotoValidateRecord(input.lexicons, input.collection, input.record); 188 const ourResult = validateRecord(input.lexicons, input.collection, input.record); 189 190 const atprotoValid = atprotoResult === null; 191 const ourValid = ourResult === null; 192 expect(ourValid).toBe(atprotoValid); 193 }); 194 } 195 }); 196 197 describe('e2e validation', () => { 198 for (const input of allE2eInputs) { 199 test(input.name, () => { 200 const atprotoResult = atprotoValidateRecord(input.lexicons, input.collection, input.record); 201 const ourResult = validateRecord(input.lexicons, input.collection, input.record); 202 203 const atprotoValid = atprotoResult === null; 204 const ourValid = ourResult === null; 205 expect(ourValid).toBe(atprotoValid); 206 }); 207 } 208 }); 209 210 describe('ref data validation', () => { 211 for (const input of dataInputs.refDataInputs) { 212 test(input.name, () => { 213 const atprotoResult = atprotoValidateRecord(input.lexicons, input.collection, input.record); 214 const ourResult = validateRecord(input.lexicons, input.collection, input.record); 215 216 const atprotoValid = atprotoResult === null; 217 const ourValid = ourResult === null; 218 expect(ourValid).toBe(atprotoValid); 219 }); 220 } 221 }); 222});