An ATProto Lexicon validator for Gleam.
at main 16 kB view raw
1import gleam/json 2import gleeunit 3import gleeunit/should 4import honk/validation/context 5import honk/validation/primary/params 6 7pub fn main() { 8 gleeunit.main() 9} 10 11// Test valid params with boolean property 12pub fn valid_params_boolean_test() { 13 let schema = 14 json.object([ 15 #("type", json.string("params")), 16 #( 17 "properties", 18 json.object([ 19 #( 20 "isPublic", 21 json.object([ 22 #("type", json.string("boolean")), 23 #("description", json.string("Whether the item is public")), 24 ]), 25 ), 26 ]), 27 ), 28 ]) 29 30 let ctx = context.builder() |> context.build() 31 case ctx { 32 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok 33 Error(_) -> should.fail() 34 } 35} 36 37// Test valid params with multiple property types 38pub fn valid_params_multiple_types_test() { 39 let schema = 40 json.object([ 41 #("type", json.string("params")), 42 #( 43 "properties", 44 json.object([ 45 #( 46 "limit", 47 json.object([ 48 #("type", json.string("integer")), 49 #("minimum", json.int(1)), 50 #("maximum", json.int(100)), 51 ]), 52 ), 53 #( 54 "cursor", 55 json.object([ 56 #("type", json.string("string")), 57 #("description", json.string("Pagination cursor")), 58 ]), 59 ), 60 #("includeReplies", json.object([#("type", json.string("boolean"))])), 61 ]), 62 ), 63 ]) 64 65 let ctx = context.builder() |> context.build() 66 case ctx { 67 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok 68 Error(_) -> should.fail() 69 } 70} 71 72// Test valid params with array property 73pub fn valid_params_with_array_test() { 74 let schema = 75 json.object([ 76 #("type", json.string("params")), 77 #( 78 "properties", 79 json.object([ 80 #( 81 "tags", 82 json.object([ 83 #("type", json.string("array")), 84 #( 85 "items", 86 json.object([ 87 #("type", json.string("string")), 88 #("maxLength", json.int(50)), 89 ]), 90 ), 91 ]), 92 ), 93 ]), 94 ), 95 ]) 96 97 let ctx = context.builder() |> context.build() 98 case ctx { 99 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok 100 Error(_) -> should.fail() 101 } 102} 103 104// Test valid params with required fields 105pub fn valid_params_with_required_test() { 106 let schema = 107 json.object([ 108 #("type", json.string("params")), 109 #( 110 "properties", 111 json.object([ 112 #( 113 "repo", 114 json.object([ 115 #("type", json.string("string")), 116 #("format", json.string("at-identifier")), 117 ]), 118 ), 119 #( 120 "collection", 121 json.object([ 122 #("type", json.string("string")), 123 #("format", json.string("nsid")), 124 ]), 125 ), 126 ]), 127 ), 128 #("required", json.array([json.string("repo")], fn(x) { x })), 129 ]) 130 131 let ctx = context.builder() |> context.build() 132 case ctx { 133 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok 134 Error(_) -> should.fail() 135 } 136} 137 138// Test valid params with unknown type 139pub fn valid_params_with_unknown_test() { 140 let schema = 141 json.object([ 142 #("type", json.string("params")), 143 #( 144 "properties", 145 json.object([ 146 #("metadata", json.object([#("type", json.string("unknown"))])), 147 ]), 148 ), 149 ]) 150 151 let ctx = context.builder() |> context.build() 152 case ctx { 153 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok 154 Error(_) -> should.fail() 155 } 156} 157 158// Test invalid: params with object property (not allowed) 159pub fn invalid_params_object_property_test() { 160 let schema = 161 json.object([ 162 #("type", json.string("params")), 163 #( 164 "properties", 165 json.object([ 166 #( 167 "filter", 168 json.object([ 169 #("type", json.string("object")), 170 #("properties", json.object([])), 171 ]), 172 ), 173 ]), 174 ), 175 ]) 176 177 let assert Ok(c) = context.builder() |> context.build() 178 params.validate_schema(schema, c) |> should.be_error 179} 180 181// Test invalid: params with blob property (not allowed) 182pub fn invalid_params_blob_property_test() { 183 let schema = 184 json.object([ 185 #("type", json.string("params")), 186 #( 187 "properties", 188 json.object([ 189 #( 190 "avatar", 191 json.object([ 192 #("type", json.string("blob")), 193 #("accept", json.array([json.string("image/*")], fn(x) { x })), 194 ]), 195 ), 196 ]), 197 ), 198 ]) 199 200 let assert Ok(c) = context.builder() |> context.build() 201 params.validate_schema(schema, c) |> should.be_error 202} 203 204// Test invalid: required field not in properties 205pub fn invalid_params_required_not_in_properties_test() { 206 let schema = 207 json.object([ 208 #("type", json.string("params")), 209 #( 210 "properties", 211 json.object([ 212 #("limit", json.object([#("type", json.string("integer"))])), 213 ]), 214 ), 215 #("required", json.array([json.string("cursor")], fn(x) { x })), 216 ]) 217 218 let assert Ok(c) = context.builder() |> context.build() 219 params.validate_schema(schema, c) |> should.be_error 220} 221 222// Test invalid: empty property name 223pub fn invalid_params_empty_property_name_test() { 224 let schema = 225 json.object([ 226 #("type", json.string("params")), 227 #( 228 "properties", 229 json.object([ 230 #("", json.object([#("type", json.string("string"))])), 231 ]), 232 ), 233 ]) 234 235 let assert Ok(c) = context.builder() |> context.build() 236 params.validate_schema(schema, c) |> should.be_error 237} 238 239// Test invalid: array with object items (not allowed) 240pub fn invalid_params_array_of_objects_test() { 241 let schema = 242 json.object([ 243 #("type", json.string("params")), 244 #( 245 "properties", 246 json.object([ 247 #( 248 "filters", 249 json.object([ 250 #("type", json.string("array")), 251 #( 252 "items", 253 json.object([ 254 #("type", json.string("object")), 255 #("properties", json.object([])), 256 ]), 257 ), 258 ]), 259 ), 260 ]), 261 ), 262 ]) 263 264 let assert Ok(c) = context.builder() |> context.build() 265 params.validate_schema(schema, c) |> should.be_error 266} 267 268// Test invalid: wrong type (not "params") 269pub fn invalid_params_wrong_type_test() { 270 let schema = 271 json.object([ 272 #("type", json.string("object")), 273 #("properties", json.object([])), 274 ]) 275 276 let assert Ok(c) = context.builder() |> context.build() 277 params.validate_schema(schema, c) |> should.be_error 278} 279 280// Test valid: array of integers 281pub fn valid_params_array_of_integers_test() { 282 let schema = 283 json.object([ 284 #("type", json.string("params")), 285 #( 286 "properties", 287 json.object([ 288 #( 289 "ids", 290 json.object([ 291 #("type", json.string("array")), 292 #( 293 "items", 294 json.object([ 295 #("type", json.string("integer")), 296 #("minimum", json.int(1)), 297 ]), 298 ), 299 ]), 300 ), 301 ]), 302 ), 303 ]) 304 305 let ctx = context.builder() |> context.build() 306 case ctx { 307 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok 308 Error(_) -> should.fail() 309 } 310} 311 312// Test valid: array of unknown 313pub fn valid_params_array_of_unknown_test() { 314 let schema = 315 json.object([ 316 #("type", json.string("params")), 317 #( 318 "properties", 319 json.object([ 320 #( 321 "data", 322 json.object([ 323 #("type", json.string("array")), 324 #("items", json.object([#("type", json.string("unknown"))])), 325 ]), 326 ), 327 ]), 328 ), 329 ]) 330 331 let ctx = context.builder() |> context.build() 332 case ctx { 333 Ok(c) -> params.validate_schema(schema, c) |> should.be_ok 334 Error(_) -> should.fail() 335 } 336} 337 338// ==================== DATA VALIDATION TESTS ==================== 339 340// Test valid data with required parameters 341pub fn valid_data_with_required_params_test() { 342 let schema = 343 json.object([ 344 #("type", json.string("params")), 345 #( 346 "properties", 347 json.object([ 348 #("repo", json.object([#("type", json.string("string"))])), 349 #("limit", json.object([#("type", json.string("integer"))])), 350 ]), 351 ), 352 #( 353 "required", 354 json.array([json.string("repo"), json.string("limit")], fn(x) { x }), 355 ), 356 ]) 357 358 let data = 359 json.object([ 360 #("repo", json.string("alice.bsky.social")), 361 #("limit", json.int(50)), 362 ]) 363 364 let assert Ok(c) = context.builder() |> context.build() 365 params.validate_data(data, schema, c) |> should.be_ok 366} 367 368// Test valid data with optional parameters 369pub fn valid_data_with_optional_params_test() { 370 let schema = 371 json.object([ 372 #("type", json.string("params")), 373 #( 374 "properties", 375 json.object([ 376 #("repo", json.object([#("type", json.string("string"))])), 377 #("cursor", json.object([#("type", json.string("string"))])), 378 ]), 379 ), 380 #("required", json.array([json.string("repo")], fn(x) { x })), 381 ]) 382 383 // Data has required param but not optional cursor 384 let data = json.object([#("repo", json.string("alice.bsky.social"))]) 385 386 let assert Ok(c) = context.builder() |> context.build() 387 params.validate_data(data, schema, c) |> should.be_ok 388} 389 390// Test valid data with all parameter types 391pub fn valid_data_all_types_test() { 392 let schema = 393 json.object([ 394 #("type", json.string("params")), 395 #( 396 "properties", 397 json.object([ 398 #("name", json.object([#("type", json.string("string"))])), 399 #("count", json.object([#("type", json.string("integer"))])), 400 #("enabled", json.object([#("type", json.string("boolean"))])), 401 #("metadata", json.object([#("type", json.string("unknown"))])), 402 ]), 403 ), 404 ]) 405 406 let data = 407 json.object([ 408 #("name", json.string("test")), 409 #("count", json.int(42)), 410 #("enabled", json.bool(True)), 411 #("metadata", json.object([#("key", json.string("value"))])), 412 ]) 413 414 let assert Ok(c) = context.builder() |> context.build() 415 params.validate_data(data, schema, c) |> should.be_ok 416} 417 418// Test valid data with array parameter 419pub fn valid_data_with_array_test() { 420 let schema = 421 json.object([ 422 #("type", json.string("params")), 423 #( 424 "properties", 425 json.object([ 426 #( 427 "tags", 428 json.object([ 429 #("type", json.string("array")), 430 #("items", json.object([#("type", json.string("string"))])), 431 ]), 432 ), 433 ]), 434 ), 435 ]) 436 437 let data = 438 json.object([ 439 #( 440 "tags", 441 json.array([json.string("foo"), json.string("bar")], fn(x) { x }), 442 ), 443 ]) 444 445 let assert Ok(c) = context.builder() |> context.build() 446 params.validate_data(data, schema, c) |> should.be_ok 447} 448 449// Test invalid data: missing required parameter 450pub fn invalid_data_missing_required_test() { 451 let schema = 452 json.object([ 453 #("type", json.string("params")), 454 #( 455 "properties", 456 json.object([ 457 #("repo", json.object([#("type", json.string("string"))])), 458 #("limit", json.object([#("type", json.string("integer"))])), 459 ]), 460 ), 461 #("required", json.array([json.string("repo")], fn(x) { x })), 462 ]) 463 464 // Data is missing required "repo" parameter 465 let data = json.object([#("limit", json.int(50))]) 466 467 let assert Ok(c) = context.builder() |> context.build() 468 params.validate_data(data, schema, c) |> should.be_error 469} 470 471// Test invalid data: wrong type for parameter 472pub fn invalid_data_wrong_type_test() { 473 let schema = 474 json.object([ 475 #("type", json.string("params")), 476 #( 477 "properties", 478 json.object([ 479 #("limit", json.object([#("type", json.string("integer"))])), 480 ]), 481 ), 482 ]) 483 484 // limit should be integer but is string 485 let data = json.object([#("limit", json.string("not a number"))]) 486 487 let assert Ok(c) = context.builder() |> context.build() 488 params.validate_data(data, schema, c) |> should.be_error 489} 490 491// Test invalid data: string exceeds maxLength 492pub fn invalid_data_string_too_long_test() { 493 let schema = 494 json.object([ 495 #("type", json.string("params")), 496 #( 497 "properties", 498 json.object([ 499 #( 500 "name", 501 json.object([ 502 #("type", json.string("string")), 503 #("maxLength", json.int(5)), 504 ]), 505 ), 506 ]), 507 ), 508 ]) 509 510 // name is longer than maxLength of 5 511 let data = json.object([#("name", json.string("toolongname"))]) 512 513 let assert Ok(c) = context.builder() |> context.build() 514 params.validate_data(data, schema, c) |> should.be_error 515} 516 517// Test invalid data: integer below minimum 518pub fn invalid_data_integer_below_minimum_test() { 519 let schema = 520 json.object([ 521 #("type", json.string("params")), 522 #( 523 "properties", 524 json.object([ 525 #( 526 "count", 527 json.object([ 528 #("type", json.string("integer")), 529 #("minimum", json.int(1)), 530 ]), 531 ), 532 ]), 533 ), 534 ]) 535 536 // count is below minimum of 1 537 let data = json.object([#("count", json.int(0))]) 538 539 let assert Ok(c) = context.builder() |> context.build() 540 params.validate_data(data, schema, c) |> should.be_error 541} 542 543// Test invalid data: array with wrong item type 544pub fn invalid_data_array_wrong_item_type_test() { 545 let schema = 546 json.object([ 547 #("type", json.string("params")), 548 #( 549 "properties", 550 json.object([ 551 #( 552 "ids", 553 json.object([ 554 #("type", json.string("array")), 555 #("items", json.object([#("type", json.string("integer"))])), 556 ]), 557 ), 558 ]), 559 ), 560 ]) 561 562 // Array contains strings instead of integers 563 let data = 564 json.object([ 565 #( 566 "ids", 567 json.array([json.string("one"), json.string("two")], fn(x) { x }), 568 ), 569 ]) 570 571 let assert Ok(c) = context.builder() |> context.build() 572 params.validate_data(data, schema, c) |> should.be_error 573} 574 575// Test valid data with no properties defined (empty schema) 576pub fn valid_data_empty_schema_test() { 577 let schema = json.object([#("type", json.string("params"))]) 578 579 let data = json.object([]) 580 581 let assert Ok(c) = context.builder() |> context.build() 582 params.validate_data(data, schema, c) |> should.be_ok 583} 584 585// Test valid data allows unknown parameters not in schema 586pub fn valid_data_unknown_parameters_allowed_test() { 587 let schema = 588 json.object([ 589 #("type", json.string("params")), 590 #( 591 "properties", 592 json.object([ 593 #("repo", json.object([#("type", json.string("string"))])), 594 ]), 595 ), 596 ]) 597 598 // Data has "extra" parameter not in schema 599 let data = 600 json.object([ 601 #("repo", json.string("alice.bsky.social")), 602 #("extra", json.string("allowed")), 603 ]) 604 605 let assert Ok(c) = context.builder() |> context.build() 606 params.validate_data(data, schema, c) |> should.be_ok 607}