馃寠 A GraphQL implementation in Gleam
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}