馃寠 A GraphQL implementation in Gleam
at main 25 kB view raw
1/// Tests for GraphQL Introspection 2/// 3/// Comprehensive tests for introspection queries 4import gleam/list 5import gleam/option.{None} 6import gleam/string 7import gleeunit/should 8import swell/executor 9import swell/schema 10import swell/value 11 12// Helper to create a simple test schema 13fn test_schema() -> schema.Schema { 14 let query_type = 15 schema.object_type("Query", "Root query type", [ 16 schema.field("hello", schema.string_type(), "Hello field", fn(_ctx) { 17 Ok(value.String("world")) 18 }), 19 schema.field("number", schema.int_type(), "Number field", fn(_ctx) { 20 Ok(value.Int(42)) 21 }), 22 ]) 23 24 schema.schema(query_type, None) 25} 26 27/// Test: Multiple scalar fields on __schema 28/// This test verifies that all requested fields on __schema are returned 29pub fn schema_multiple_fields_test() { 30 let schema = test_schema() 31 let query = 32 "{ __schema { queryType { name } mutationType { name } subscriptionType { name } } }" 33 34 let result = executor.execute(query, schema, schema.context(None)) 35 36 should.be_ok(result) 37 |> fn(response) { 38 case response { 39 executor.Response(data: value.Object(fields), errors: []) -> { 40 // Check that we have __schema field 41 case list.key_find(fields, "__schema") { 42 Ok(value.Object(schema_fields)) -> { 43 // Check for all three fields 44 let has_query_type = case 45 list.key_find(schema_fields, "queryType") 46 { 47 Ok(value.Object(_)) -> True 48 _ -> False 49 } 50 let has_mutation_type = case 51 list.key_find(schema_fields, "mutationType") 52 { 53 Ok(value.Null) -> True 54 // Should be null 55 _ -> False 56 } 57 let has_subscription_type = case 58 list.key_find(schema_fields, "subscriptionType") 59 { 60 Ok(value.Null) -> True 61 // Should be null 62 _ -> False 63 } 64 has_query_type && has_mutation_type && has_subscription_type 65 } 66 _ -> False 67 } 68 } 69 _ -> False 70 } 71 } 72 |> should.be_true 73} 74 75/// Test: types field with other fields 76/// Verifies that the types array is returned along with other fields 77pub fn schema_types_with_other_fields_test() { 78 let schema = test_schema() 79 let query = "{ __schema { queryType { name } types { name } } }" 80 81 let result = executor.execute(query, schema, schema.context(None)) 82 83 should.be_ok(result) 84 |> fn(response) { 85 case response { 86 executor.Response(data: value.Object(fields), errors: []) -> { 87 case list.key_find(fields, "__schema") { 88 Ok(value.Object(schema_fields)) -> { 89 // Check for both fields 90 let has_query_type = case 91 list.key_find(schema_fields, "queryType") 92 { 93 Ok(value.Object(qt_fields)) -> { 94 case list.key_find(qt_fields, "name") { 95 Ok(value.String("Query")) -> True 96 _ -> False 97 } 98 } 99 _ -> False 100 } 101 let has_types = case list.key_find(schema_fields, "types") { 102 Ok(value.List(types)) -> { 103 // Should have 6 types: Query + 5 scalars 104 list.length(types) == 6 105 } 106 _ -> False 107 } 108 has_query_type && has_types 109 } 110 _ -> False 111 } 112 } 113 _ -> False 114 } 115 } 116 |> should.be_true 117} 118 119/// Test: All __schema top-level fields 120/// Verifies that a query with all possible __schema fields returns all of them 121pub fn schema_all_fields_test() { 122 let schema = test_schema() 123 let query = 124 "{ __schema { queryType { name } mutationType { name } subscriptionType { name } types { name } directives { name } } }" 125 126 let result = executor.execute(query, schema, schema.context(None)) 127 128 should.be_ok(result) 129 |> fn(response) { 130 case response { 131 executor.Response(data: value.Object(fields), errors: []) -> { 132 case list.key_find(fields, "__schema") { 133 Ok(value.Object(schema_fields)) -> { 134 // Check for all five fields 135 let field_count = list.length(schema_fields) 136 // Should have exactly 5 fields 137 field_count == 5 138 } 139 _ -> False 140 } 141 } 142 _ -> False 143 } 144 } 145 |> should.be_true 146} 147 148/// Test: Field order doesn't matter 149/// Verifies that field order in the query doesn't affect results 150pub fn schema_field_order_test() { 151 let schema = test_schema() 152 let query1 = "{ __schema { types { name } queryType { name } } }" 153 let query2 = "{ __schema { queryType { name } types { name } } }" 154 155 let result1 = executor.execute(query1, schema, schema.context(None)) 156 let result2 = executor.execute(query2, schema, schema.context(None)) 157 158 // Both should succeed 159 should.be_ok(result1) 160 should.be_ok(result2) 161 162 // Both should have the same fields 163 case result1, result2 { 164 Ok(executor.Response(data: value.Object(fields1), errors: [])), 165 Ok(executor.Response(data: value.Object(fields2), errors: [])) 166 -> { 167 case 168 list.key_find(fields1, "__schema"), 169 list.key_find(fields2, "__schema") 170 { 171 Ok(value.Object(schema_fields1)), Ok(value.Object(schema_fields2)) -> { 172 let count1 = list.length(schema_fields1) 173 let count2 = list.length(schema_fields2) 174 // Both should have 2 fields 175 count1 == 2 && count2 == 2 176 } 177 _, _ -> False 178 } 179 } 180 _, _ -> False 181 } 182 |> should.be_true 183} 184 185/// Test: Nested introspection on types 186/// Verifies that nested field selections work correctly 187pub fn schema_types_nested_fields_test() { 188 let schema = test_schema() 189 let query = "{ __schema { types { name kind fields { name } } } }" 190 191 let result = executor.execute(query, schema, schema.context(None)) 192 193 should.be_ok(result) 194 |> fn(response) { 195 case response { 196 executor.Response(data: value.Object(fields), errors: []) -> { 197 case list.key_find(fields, "__schema") { 198 Ok(value.Object(schema_fields)) -> { 199 case list.key_find(schema_fields, "types") { 200 Ok(value.List(types)) -> { 201 // Check that each type has name, kind, and fields 202 list.all(types, fn(type_val) { 203 case type_val { 204 value.Object(type_fields) -> { 205 let has_name = case list.key_find(type_fields, "name") { 206 Ok(_) -> True 207 _ -> False 208 } 209 let has_kind = case list.key_find(type_fields, "kind") { 210 Ok(_) -> True 211 _ -> False 212 } 213 let has_fields = case 214 list.key_find(type_fields, "fields") 215 { 216 Ok(_) -> True 217 // Can be null or list 218 _ -> False 219 } 220 has_name && has_kind && has_fields 221 } 222 _ -> False 223 } 224 }) 225 } 226 _ -> False 227 } 228 } 229 _ -> False 230 } 231 } 232 _ -> False 233 } 234 } 235 |> should.be_true 236} 237 238/// Test: Empty nested selections on null fields 239/// Verifies that querying nested fields on null values doesn't cause errors 240pub fn schema_null_field_with_deep_nesting_test() { 241 let schema = test_schema() 242 let query = "{ __schema { mutationType { name fields { name } } } }" 243 244 let result = executor.execute(query, schema, schema.context(None)) 245 246 should.be_ok(result) 247 |> fn(response) { 248 case response { 249 executor.Response(data: value.Object(fields), errors: []) -> { 250 case list.key_find(fields, "__schema") { 251 Ok(value.Object(schema_fields)) -> { 252 case list.key_find(schema_fields, "mutationType") { 253 Ok(value.Null) -> True 254 // Should be null, not error 255 _ -> False 256 } 257 } 258 _ -> False 259 } 260 } 261 _ -> False 262 } 263 } 264 |> should.be_true 265} 266 267/// Test: Inline fragments in introspection 268/// Verifies that inline fragments work correctly in introspection queries (like GraphiQL uses) 269pub fn schema_inline_fragment_test() { 270 let schema = test_schema() 271 let query = "{ __schema { types { ... on __Type { kind name } } } }" 272 273 let result = executor.execute(query, schema, schema.context(None)) 274 275 should.be_ok(result) 276 |> fn(response) { 277 case response { 278 executor.Response(data: value.Object(fields), errors: []) -> { 279 case list.key_find(fields, "__schema") { 280 Ok(value.Object(schema_fields)) -> { 281 case list.key_find(schema_fields, "types") { 282 Ok(value.List(types)) -> { 283 // Should have 6 types with kind and name fields 284 list.length(types) == 6 285 && list.all(types, fn(type_val) { 286 case type_val { 287 value.Object(type_fields) -> { 288 let has_kind = case list.key_find(type_fields, "kind") { 289 Ok(value.String(_)) -> True 290 _ -> False 291 } 292 let has_name = case list.key_find(type_fields, "name") { 293 Ok(value.String(_)) -> True 294 _ -> False 295 } 296 has_kind && has_name 297 } 298 _ -> False 299 } 300 }) 301 } 302 _ -> False 303 } 304 } 305 _ -> False 306 } 307 } 308 _ -> False 309 } 310 } 311 |> should.be_true 312} 313 314/// Test: Basic __type query 315/// Verifies that __type(name: "TypeName") returns the correct type 316pub fn type_basic_query_test() { 317 let schema = test_schema() 318 let query = "{ __type(name: \"Query\") { name kind } }" 319 320 let result = executor.execute(query, schema, schema.context(None)) 321 322 should.be_ok(result) 323 |> fn(response) { 324 case response { 325 executor.Response(data: value.Object(fields), errors: []) -> { 326 case list.key_find(fields, "__type") { 327 Ok(value.Object(type_fields)) -> { 328 // Check name and kind 329 let has_correct_name = case list.key_find(type_fields, "name") { 330 Ok(value.String("Query")) -> True 331 _ -> False 332 } 333 let has_correct_kind = case list.key_find(type_fields, "kind") { 334 Ok(value.String("OBJECT")) -> True 335 _ -> False 336 } 337 has_correct_name && has_correct_kind 338 } 339 _ -> False 340 } 341 } 342 _ -> False 343 } 344 } 345 |> should.be_true 346} 347 348/// Test: __type query with nested fields 349/// Verifies that nested selections work correctly on __type 350pub fn type_nested_fields_test() { 351 let schema = test_schema() 352 let query = 353 "{ __type(name: \"Query\") { name kind fields { name type { name kind } } } }" 354 355 let result = executor.execute(query, schema, schema.context(None)) 356 357 should.be_ok(result) 358 |> fn(response) { 359 case response { 360 executor.Response(data: value.Object(fields), errors: []) -> { 361 case list.key_find(fields, "__type") { 362 Ok(value.Object(type_fields)) -> { 363 // Check that fields exists and is a list 364 case list.key_find(type_fields, "fields") { 365 Ok(value.List(field_list)) -> { 366 // Should have 2 fields (hello and number) 367 list.length(field_list) == 2 368 && list.all(field_list, fn(field_val) { 369 case field_val { 370 value.Object(field_fields) -> { 371 let has_name = case list.key_find(field_fields, "name") { 372 Ok(value.String(_)) -> True 373 _ -> False 374 } 375 let has_type = case list.key_find(field_fields, "type") { 376 Ok(value.Object(_)) -> True 377 _ -> False 378 } 379 has_name && has_type 380 } 381 _ -> False 382 } 383 }) 384 } 385 _ -> False 386 } 387 } 388 _ -> False 389 } 390 } 391 _ -> False 392 } 393 } 394 |> should.be_true 395} 396 397/// Test: __type query for scalar types 398/// Verifies that __type works for built-in scalar types 399pub fn type_scalar_query_test() { 400 let schema = test_schema() 401 let query = "{ __type(name: \"String\") { name kind } }" 402 403 let result = executor.execute(query, schema, schema.context(None)) 404 405 should.be_ok(result) 406 |> fn(response) { 407 case response { 408 executor.Response(data: value.Object(fields), errors: []) -> { 409 case list.key_find(fields, "__type") { 410 Ok(value.Object(type_fields)) -> { 411 // Check name and kind 412 let has_correct_name = case list.key_find(type_fields, "name") { 413 Ok(value.String("String")) -> True 414 _ -> False 415 } 416 let has_correct_kind = case list.key_find(type_fields, "kind") { 417 Ok(value.String("SCALAR")) -> True 418 _ -> False 419 } 420 has_correct_name && has_correct_kind 421 } 422 _ -> False 423 } 424 } 425 _ -> False 426 } 427 } 428 |> should.be_true 429} 430 431/// Test: __type query for non-existent type 432/// Verifies that __type returns null for types that don't exist 433pub fn type_not_found_test() { 434 let schema = test_schema() 435 let query = "{ __type(name: \"NonExistentType\") { name kind } }" 436 437 let result = executor.execute(query, schema, schema.context(None)) 438 439 should.be_ok(result) 440 |> fn(response) { 441 case response { 442 executor.Response(data: value.Object(fields), errors: []) -> { 443 case list.key_find(fields, "__type") { 444 Ok(value.Null) -> True 445 _ -> False 446 } 447 } 448 _ -> False 449 } 450 } 451 |> should.be_true 452} 453 454/// Test: __type query without name argument 455/// Verifies that __type returns an error when name argument is missing 456pub fn type_missing_argument_test() { 457 let schema = test_schema() 458 let query = "{ __type { name kind } }" 459 460 let result = executor.execute(query, schema, schema.context(None)) 461 462 should.be_ok(result) 463 |> fn(response) { 464 case response { 465 executor.Response(data: value.Object(fields), errors: errors) -> { 466 // Should have __type field as null 467 let has_null_type = case list.key_find(fields, "__type") { 468 Ok(value.Null) -> True 469 _ -> False 470 } 471 // Should have an error 472 let has_error = errors != [] 473 has_null_type && has_error 474 } 475 _ -> False 476 } 477 } 478 |> should.be_true 479} 480 481/// Test: Combined __type and __schema query 482/// Verifies that __type and __schema can be queried together 483pub fn type_and_schema_combined_test() { 484 let schema = test_schema() 485 let query = 486 "{ __schema { queryType { name } } __type(name: \"String\") { name kind } }" 487 488 let result = executor.execute(query, schema, schema.context(None)) 489 490 should.be_ok(result) 491 |> fn(response) { 492 case response { 493 executor.Response(data: value.Object(fields), errors: []) -> { 494 let has_schema = case list.key_find(fields, "__schema") { 495 Ok(value.Object(_)) -> True 496 _ -> False 497 } 498 let has_type = case list.key_find(fields, "__type") { 499 Ok(value.Object(_)) -> True 500 _ -> False 501 } 502 has_schema && has_type 503 } 504 _ -> False 505 } 506 } 507 |> should.be_true 508} 509 510/// Test: Deep introspection queries complete without hanging 511/// This test verifies that the cycle detection prevents infinite loops 512/// by successfully completing a deeply nested introspection query 513pub fn deep_introspection_test() { 514 let schema = test_schema() 515 516 // Query with deep nesting including ofType chains 517 // Without cycle detection, this could cause infinite loops 518 let query = 519 "{ __schema { types { name kind fields { name type { name kind ofType { name kind ofType { name } } } } } } }" 520 521 let result = executor.execute(query, schema, schema.context(None)) 522 523 // The key test: should complete without hanging 524 should.be_ok(result) 525 |> fn(response) { 526 case response { 527 executor.Response(data: value.Object(fields), errors: _errors) -> { 528 // Should have __schema field with types 529 case list.key_find(fields, "__schema") { 530 Ok(value.Object(schema_fields)) -> { 531 case list.key_find(schema_fields, "types") { 532 Ok(value.List(types)) -> types != [] 533 _ -> False 534 } 535 } 536 _ -> False 537 } 538 } 539 _ -> False 540 } 541 } 542 |> should.be_true 543} 544 545/// Test: Fragment spreads work in introspection queries 546/// Verifies that fragment spreads like those used by GraphiQL work correctly 547pub fn introspection_fragment_spread_test() { 548 // Create a schema with an ENUM type 549 let sort_enum = 550 schema.enum_type("SortDirection", "Sort direction", [ 551 schema.enum_value("ASC", "Ascending"), 552 schema.enum_value("DESC", "Descending"), 553 ]) 554 555 let query_type = 556 schema.object_type("Query", "Root query", [ 557 schema.field("items", schema.list_type(schema.string_type()), "", fn(_) { 558 Ok(value.List([value.String("a"), value.String("b")])) 559 }), 560 schema.field("sort", sort_enum, "", fn(_) { Ok(value.String("ASC")) }), 561 ]) 562 563 let test_schema = schema.schema(query_type, None) 564 565 // Use a fragment spread like GraphiQL does 566 let query = 567 " 568 query IntrospectionQuery { 569 __schema { 570 types { 571 ...FullType 572 } 573 } 574 } 575 576 fragment FullType on __Type { 577 kind 578 name 579 enumValues(includeDeprecated: true) { 580 name 581 description 582 } 583 } 584 " 585 586 let result = executor.execute(query, test_schema, schema.context(None)) 587 588 should.be_ok(result) 589 |> fn(response) { 590 case response { 591 executor.Response(data: value.Object(fields), errors: _) -> { 592 case list.key_find(fields, "__schema") { 593 Ok(value.Object(schema_fields)) -> { 594 case list.key_find(schema_fields, "types") { 595 Ok(value.List(types)) -> { 596 // Find the SortDirection enum 597 let enum_type = 598 list.find(types, fn(t) { 599 case t { 600 value.Object(type_fields) -> { 601 case list.key_find(type_fields, "name") { 602 Ok(value.String("SortDirection")) -> True 603 _ -> False 604 } 605 } 606 _ -> False 607 } 608 }) 609 610 case enum_type { 611 Ok(value.Object(type_fields)) -> { 612 // Should have kind field from fragment 613 let has_kind = case list.key_find(type_fields, "kind") { 614 Ok(value.String("ENUM")) -> True 615 _ -> False 616 } 617 618 // Should have enumValues field from fragment 619 let has_enum_values = case 620 list.key_find(type_fields, "enumValues") 621 { 622 Ok(value.List(values)) -> list.length(values) == 2 623 _ -> False 624 } 625 626 has_kind && has_enum_values 627 } 628 _ -> False 629 } 630 } 631 _ -> False 632 } 633 } 634 _ -> False 635 } 636 } 637 _ -> False 638 } 639 } 640 |> should.be_true 641} 642 643/// Test: Simple fragment on __type 644pub fn simple_type_fragment_test() { 645 let schema = test_schema() 646 647 let query = 648 "{ __type(name: \"Query\") { ...TypeFrag } } fragment TypeFrag on __Type { name kind }" 649 650 let result = executor.execute(query, schema, schema.context(None)) 651 652 should.be_ok(result) 653 |> fn(response) { 654 case response { 655 executor.Response(data: value.Object(fields), errors: _) -> { 656 case list.key_find(fields, "__type") { 657 Ok(value.Object(type_fields)) -> { 658 // Check if we got an error about fragment not found 659 case list.key_find(type_fields, "__FRAGMENT_ERROR") { 660 Ok(value.String(msg)) -> { 661 // Fragment wasn't found 662 panic as msg 663 } 664 _ -> { 665 // No error, check if we have actual fields 666 type_fields != [] 667 } 668 } 669 } 670 _ -> False 671 } 672 } 673 _ -> False 674 } 675 } 676 |> should.be_true 677} 678 679/// Test: Introspection types are returned in alphabetical order 680/// Verifies that __schema.types are sorted alphabetically by name 681pub fn schema_types_alphabetical_order_test() { 682 let schema = test_schema() 683 let query = "{ __schema { types { name } } }" 684 685 let result = executor.execute(query, schema, schema.context(None)) 686 687 should.be_ok(result) 688 |> fn(response) { 689 case response { 690 executor.Response(data: value.Object(fields), errors: []) -> { 691 case list.key_find(fields, "__schema") { 692 Ok(value.Object(schema_fields)) -> { 693 case list.key_find(schema_fields, "types") { 694 Ok(value.List(types)) -> { 695 // Extract type names 696 let names = 697 list.filter_map(types, fn(type_val) { 698 case type_val { 699 value.Object(type_fields) -> { 700 case list.key_find(type_fields, "name") { 701 Ok(value.String(name)) -> Ok(name) 702 _ -> Error(Nil) 703 } 704 } 705 _ -> Error(Nil) 706 } 707 }) 708 709 // Check that names are sorted alphabetically 710 // Expected order: Boolean, Float, ID, Int, Query, String 711 let sorted_names = list.sort(names, string.compare) 712 names == sorted_names 713 } 714 _ -> False 715 } 716 } 717 _ -> False 718 } 719 } 720 _ -> False 721 } 722 } 723 |> should.be_true 724} 725 726/// Test: Union type introspection returns possibleTypes 727/// Verifies that introspecting a union type correctly returns all possible types 728pub fn union_type_possible_types_test() { 729 // Create object types that will be part of the union 730 let post_type = 731 schema.object_type("Post", "A blog post", [ 732 schema.field("title", schema.string_type(), "Post title", fn(_ctx) { 733 Ok(value.String("test")) 734 }), 735 ]) 736 737 let comment_type = 738 schema.object_type("Comment", "A comment", [ 739 schema.field("text", schema.string_type(), "Comment text", fn(_ctx) { 740 Ok(value.String("test")) 741 }), 742 ]) 743 744 // Type resolver for the union 745 let type_resolver = fn(_ctx: schema.Context) -> Result(String, String) { 746 Ok("Post") 747 } 748 749 // Create union type 750 let search_result_union = 751 schema.union_type( 752 "SearchResult", 753 "A search result", 754 [post_type, comment_type], 755 type_resolver, 756 ) 757 758 // Create query type that uses the union 759 let query_type = 760 schema.object_type("Query", "Root query type", [ 761 schema.field( 762 "search", 763 schema.list_type(search_result_union), 764 "Search results", 765 fn(_ctx) { Ok(value.List([])) }, 766 ), 767 ]) 768 769 let test_schema = schema.schema(query_type, None) 770 771 // Query for union type's possibleTypes 772 let query = 773 "{ __type(name: \"SearchResult\") { name kind possibleTypes { name } } }" 774 775 let result = executor.execute(query, test_schema, schema.context(None)) 776 777 should.be_ok(result) 778 |> fn(response) { 779 case response { 780 executor.Response(data: value.Object(fields), errors: []) -> { 781 case list.key_find(fields, "__type") { 782 Ok(value.Object(type_fields)) -> { 783 // Check it's a UNION 784 let is_union = case list.key_find(type_fields, "kind") { 785 Ok(value.String("UNION")) -> True 786 _ -> False 787 } 788 789 // Check possibleTypes contains both Post and Comment 790 let has_possible_types = case 791 list.key_find(type_fields, "possibleTypes") 792 { 793 Ok(value.List(possible_types)) -> { 794 let names = 795 list.filter_map(possible_types, fn(pt) { 796 case pt { 797 value.Object(pt_fields) -> { 798 case list.key_find(pt_fields, "name") { 799 Ok(value.String(name)) -> Ok(name) 800 _ -> Error(Nil) 801 } 802 } 803 _ -> Error(Nil) 804 } 805 }) 806 807 // Should have exactly 2 possible types: Comment and Post 808 list.length(names) == 2 809 && list.contains(names, "Post") 810 && list.contains(names, "Comment") 811 } 812 _ -> False 813 } 814 815 is_union && has_possible_types 816 } 817 _ -> False 818 } 819 } 820 _ -> False 821 } 822 } 823 |> should.be_true 824}