An ATProto Lexicon validator for Gleam.
at main 9.6 kB view raw
1import gleam/json 2import gleeunit 3import gleeunit/should 4import honk/validation/context 5import honk/validation/field 6import honk/validation/field/reference 7 8pub fn main() { 9 gleeunit.main() 10} 11 12// ========== SCHEMA VALIDATION TESTS ========== 13 14pub fn valid_local_reference_schema_test() { 15 let schema = 16 json.object([#("type", json.string("ref")), #("ref", json.string("#post"))]) 17 18 let assert Ok(ctx) = context.builder() |> context.build 19 20 reference.validate_schema(schema, ctx) |> should.be_ok 21} 22 23pub fn valid_global_reference_schema_test() { 24 let schema = 25 json.object([ 26 #("type", json.string("ref")), 27 #("ref", json.string("com.atproto.repo.strongRef#main")), 28 ]) 29 30 let assert Ok(ctx) = context.builder() |> context.build 31 32 reference.validate_schema(schema, ctx) |> should.be_ok 33} 34 35pub fn valid_global_main_reference_schema_test() { 36 let schema = 37 json.object([ 38 #("type", json.string("ref")), 39 #("ref", json.string("com.atproto.repo.strongRef")), 40 ]) 41 42 let assert Ok(ctx) = context.builder() |> context.build 43 44 reference.validate_schema(schema, ctx) |> should.be_ok 45} 46 47pub fn invalid_empty_ref_test() { 48 let schema = 49 json.object([#("type", json.string("ref")), #("ref", json.string(""))]) 50 51 let assert Ok(ctx) = context.builder() |> context.build 52 53 reference.validate_schema(schema, ctx) |> should.be_error 54} 55 56pub fn invalid_missing_ref_field_test() { 57 let schema = json.object([#("type", json.string("ref"))]) 58 59 let assert Ok(ctx) = context.builder() |> context.build 60 61 reference.validate_schema(schema, ctx) |> should.be_error 62} 63 64pub fn invalid_local_ref_no_def_name_test() { 65 let schema = 66 json.object([#("type", json.string("ref")), #("ref", json.string("#"))]) 67 68 let assert Ok(ctx) = context.builder() |> context.build 69 70 reference.validate_schema(schema, ctx) |> should.be_error 71} 72 73pub fn invalid_global_ref_empty_nsid_test() { 74 // Test that a global reference must have an NSID before the # 75 // The reference "com.example#main" is valid, but starting with just # makes it local 76 // This test actually verifies that "#" alone (empty def name) is invalid 77 let schema = 78 json.object([#("type", json.string("ref")), #("ref", json.string("#"))]) 79 80 let assert Ok(ctx) = context.builder() |> context.build 81 82 reference.validate_schema(schema, ctx) |> should.be_error 83} 84 85pub fn invalid_global_ref_empty_def_test() { 86 let schema = 87 json.object([ 88 #("type", json.string("ref")), 89 #("ref", json.string("com.example.lexicon#")), 90 ]) 91 92 let assert Ok(ctx) = context.builder() |> context.build 93 94 reference.validate_schema(schema, ctx) |> should.be_error 95} 96 97pub fn invalid_multiple_hash_test() { 98 let schema = 99 json.object([ 100 #("type", json.string("ref")), 101 #("ref", json.string("com.example#foo#bar")), 102 ]) 103 104 let assert Ok(ctx) = context.builder() |> context.build 105 106 reference.validate_schema(schema, ctx) |> should.be_error 107} 108 109// ========== DATA VALIDATION TESTS ========== 110 111pub fn valid_reference_to_string_test() { 112 // Create a simple lexicon with a string definition 113 let defs = 114 json.object([ 115 #( 116 "post", 117 json.object([ 118 #("type", json.string("string")), 119 #("maxLength", json.int(280)), 120 ]), 121 ), 122 ]) 123 124 let lexicon = 125 json.object([ 126 #("lexicon", json.int(1)), 127 #("id", json.string("app.bsky.feed.post")), 128 #("defs", defs), 129 ]) 130 131 let assert Ok(builder) = 132 context.builder() 133 |> context.with_validator(field.dispatch_data_validation) 134 |> context.with_lexicons([lexicon]) 135 136 let assert Ok(ctx) = context.build(builder) 137 let ctx = context.with_current_lexicon(ctx, "app.bsky.feed.post") 138 139 let ref_schema = 140 json.object([#("type", json.string("ref")), #("ref", json.string("#post"))]) 141 142 let data = json.string("Hello, world!") 143 144 reference.validate_data(data, ref_schema, ctx) 145 |> should.be_ok 146} 147 148pub fn valid_reference_to_object_test() { 149 // Create a lexicon with an object definition 150 let defs = 151 json.object([ 152 #( 153 "user", 154 json.object([ 155 #("type", json.string("object")), 156 #( 157 "properties", 158 json.object([ 159 #( 160 "name", 161 json.object([ 162 #("type", json.string("string")), 163 #("required", json.bool(True)), 164 ]), 165 ), 166 ]), 167 ), 168 ]), 169 ), 170 ]) 171 172 let lexicon = 173 json.object([ 174 #("lexicon", json.int(1)), 175 #("id", json.string("app.test.schema")), 176 #("defs", defs), 177 ]) 178 179 let assert Ok(builder) = 180 context.builder() 181 |> context.with_validator(field.dispatch_data_validation) 182 |> context.with_lexicons([lexicon]) 183 184 let assert Ok(ctx) = context.build(builder) 185 let ctx = context.with_current_lexicon(ctx, "app.test.schema") 186 187 let ref_schema = 188 json.object([#("type", json.string("ref")), #("ref", json.string("#user"))]) 189 190 let data = json.object([#("name", json.string("Alice"))]) 191 192 reference.validate_data(data, ref_schema, ctx) 193 |> should.be_ok 194} 195 196pub fn invalid_reference_not_found_test() { 197 let defs = json.object([]) 198 199 let lexicon = 200 json.object([ 201 #("lexicon", json.int(1)), 202 #("id", json.string("app.test.schema")), 203 #("defs", defs), 204 ]) 205 206 let assert Ok(builder) = 207 context.builder() 208 |> context.with_validator(field.dispatch_data_validation) 209 |> context.with_lexicons([lexicon]) 210 211 let assert Ok(ctx) = context.build(builder) 212 let ctx = context.with_current_lexicon(ctx, "app.test.schema") 213 214 let ref_schema = 215 json.object([ 216 #("type", json.string("ref")), 217 #("ref", json.string("#nonexistent")), 218 ]) 219 220 let data = json.string("test") 221 222 reference.validate_data(data, ref_schema, ctx) 223 |> should.be_error 224} 225 226pub fn circular_reference_detection_test() { 227 // Create lexicon with circular reference: A -> B -> A 228 let defs = 229 json.object([ 230 #( 231 "refA", 232 json.object([ 233 #("type", json.string("ref")), 234 #("ref", json.string("#refB")), 235 ]), 236 ), 237 #( 238 "refB", 239 json.object([ 240 #("type", json.string("ref")), 241 #("ref", json.string("#refA")), 242 ]), 243 ), 244 ]) 245 246 let lexicon = 247 json.object([ 248 #("lexicon", json.int(1)), 249 #("id", json.string("app.test.circular")), 250 #("defs", defs), 251 ]) 252 253 let assert Ok(builder) = 254 context.builder() 255 |> context.with_validator(field.dispatch_data_validation) 256 |> context.with_lexicons([lexicon]) 257 258 let assert Ok(ctx) = context.build(builder) 259 let ctx = context.with_current_lexicon(ctx, "app.test.circular") 260 261 let ref_schema = 262 json.object([#("type", json.string("ref")), #("ref", json.string("#refA"))]) 263 264 let data = json.string("test") 265 266 // Should detect the circular reference and return an error 267 reference.validate_data(data, ref_schema, ctx) 268 |> should.be_error 269} 270 271pub fn nested_reference_chain_test() { 272 // Create lexicon with nested references: A -> B -> string 273 let defs = 274 json.object([ 275 #( 276 "refA", 277 json.object([ 278 #("type", json.string("ref")), 279 #("ref", json.string("#refB")), 280 ]), 281 ), 282 #( 283 "refB", 284 json.object([ 285 #("type", json.string("ref")), 286 #("ref", json.string("#actualString")), 287 ]), 288 ), 289 #("actualString", json.object([#("type", json.string("string"))])), 290 ]) 291 292 let lexicon = 293 json.object([ 294 #("lexicon", json.int(1)), 295 #("id", json.string("app.test.nested")), 296 #("defs", defs), 297 ]) 298 299 let assert Ok(builder) = 300 context.builder() 301 |> context.with_validator(field.dispatch_data_validation) 302 |> context.with_lexicons([lexicon]) 303 304 let assert Ok(ctx) = context.build(builder) 305 let ctx = context.with_current_lexicon(ctx, "app.test.nested") 306 307 let ref_schema = 308 json.object([#("type", json.string("ref")), #("ref", json.string("#refA"))]) 309 310 let data = json.string("Hello!") 311 312 reference.validate_data(data, ref_schema, ctx) 313 |> should.be_ok 314} 315 316pub fn cross_lexicon_reference_test() { 317 // Create two lexicons where one references the other 318 let lex1_defs = 319 json.object([ 320 #( 321 "userRef", 322 json.object([ 323 #("type", json.string("ref")), 324 #("ref", json.string("app.test.types#user")), 325 ]), 326 ), 327 ]) 328 329 let lex2_defs = 330 json.object([ 331 #( 332 "user", 333 json.object([ 334 #("type", json.string("object")), 335 #( 336 "properties", 337 json.object([ 338 #( 339 "id", 340 json.object([ 341 #("type", json.string("string")), 342 #("required", json.bool(True)), 343 ]), 344 ), 345 ]), 346 ), 347 ]), 348 ), 349 ]) 350 351 let lex1 = 352 json.object([ 353 #("lexicon", json.int(1)), 354 #("id", json.string("app.test.schema")), 355 #("defs", lex1_defs), 356 ]) 357 358 let lex2 = 359 json.object([ 360 #("lexicon", json.int(1)), 361 #("id", json.string("app.test.types")), 362 #("defs", lex2_defs), 363 ]) 364 365 let assert Ok(builder) = 366 context.builder() 367 |> context.with_validator(field.dispatch_data_validation) 368 |> context.with_lexicons([lex1, lex2]) 369 370 let assert Ok(ctx) = context.build(builder) 371 let ctx = context.with_current_lexicon(ctx, "app.test.schema") 372 373 let ref_schema = 374 json.object([ 375 #("type", json.string("ref")), 376 #("ref", json.string("#userRef")), 377 ]) 378 379 let data = json.object([#("id", json.string("user123"))]) 380 381 reference.validate_data(data, ref_schema, ctx) 382 |> should.be_ok 383}