@@ -9,6 +9,7 @@ import graphql.schema.GraphQLInputObjectType
99import graphql.schema.GraphQLObjectType
1010import graphql.schema.GraphQLSchema
1111import graphql.schema.TypeResolver
12+ import graphql.schema.idl.SchemaPrinter
1213import spock.lang.Specification
1314
1415import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition
@@ -23,6 +24,20 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
2324 return directives. find({ directive -> directive. name == " private" }) == null
2425 })
2526
27+ /**
28+ * Helper method to validate that a schema is valid by printing it and re-parsing it.
29+ * This ensures the transformation always produces a valid schema.
30+ */
31+ void assertSchemaIsValid (GraphQLSchema schema ) {
32+ def printer = new SchemaPrinter (SchemaPrinter.Options . defaultOptions()
33+ .includeDirectives(true )
34+ .includeScalarTypes(true ))
35+ def printedSchema = printer. print (schema)
36+ // Parse the printed schema to verify it's valid
37+ def reparsedSchema = TestUtil . schema(printedSchema)
38+ assert reparsedSchema != null : " Re-parsed schema should not be null"
39+ }
40+
2641 def " can remove a private field" () {
2742 given :
2843 GraphQLSchema schema = TestUtil . schema("""
@@ -49,6 +64,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
4964 then :
5065 (restrictedSchema. getType(" Account" ) as GraphQLObjectType ). getFieldDefinition(" billingStatus" ) == null
5166 restrictedSchema. getType(" BillingStatus" ) == null
67+ assertSchemaIsValid(restrictedSchema)
5268 }
5369
5470 def " can remove a type associated with a private field" () {
@@ -84,6 +100,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
84100 then :
85101 restrictedSchema. getType(" BillingStatus" ) == null
86102 restrictedSchema. getType(" SuperSecretCustomerData" ) == null
103+ assertSchemaIsValid(restrictedSchema)
87104 }
88105
89106 def " removes concrete types referenced only by interface" () {
@@ -122,6 +139,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
122139 and : " After the private field is removed, they become unreachable and are removed"
123140 restrictedSchema. getType(" BillingStatus" ) == null
124141 restrictedSchema. getType(" SuperSecretCustomerData" ) == null
142+ assertSchemaIsValid(restrictedSchema)
125143 }
126144
127145 def " interface and its implementations that have both private and public reference is retained" () {
@@ -160,6 +178,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
160178 then :
161179 restrictedSchema. getType(" SuperSecretCustomerData" ) != null
162180 restrictedSchema. getType(" BillingStatus" ) != null
181+ assertSchemaIsValid(restrictedSchema)
163182 }
164183
165184
@@ -207,6 +226,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
207226 restrictedSchema. getType(" X" ) != null
208227 restrictedSchema. getType(" Foo" ) != null
209228 restrictedSchema. getType(" X2" ) != null
229+ assertSchemaIsValid(restrictedSchema)
210230 }
211231
212232 def " union types" () {
@@ -246,6 +266,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
246266 restrictedSchema. getType(" FooOrBar" ) == null
247267 restrictedSchema. getType(" Bar" ) == null
248268 restrictedSchema. getType(" Foo" ) != null
269+ assertSchemaIsValid(restrictedSchema)
249270 }
250271
251272 def " union type with reference by private interface removed" () {
@@ -293,6 +314,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
293314 restrictedSchema. getType(" Baz" ) == null
294315 restrictedSchema. getType(" Bing" ) == null
295316 restrictedSchema. getType(" FooOrBar" ) == null
317+ assertSchemaIsValid(restrictedSchema)
296318 }
297319
298320
@@ -329,6 +351,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
329351 then :
330352 restrictedSchema. getType(" BillingStatus" ) != null
331353 restrictedSchema. getType(" SuperSecretCustomerData" ) != null
354+ assertSchemaIsValid(restrictedSchema)
332355 }
333356
334357 def " leaves interface types referenced only by concrete types" () {
@@ -400,6 +423,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
400423 then :
401424 restrictedSchema. getType(" BillingStatus" ) == null
402425 restrictedSchema. getType(" SuperSecretCustomerData" ) == null
426+ assertSchemaIsValid(restrictedSchema)
403427 }
404428
405429 def " leaves interface type if has private and public reference" () {
@@ -442,6 +466,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
442466 (restrictedSchema. getType(" Account" ) as GraphQLObjectType ). getFieldDefinition(" billingStatus" ) == null
443467 restrictedSchema. getType(" BillingStatus" ) == null
444468 restrictedSchema. getType(" SuperSecretCustomerData" ) != null
469+ assertSchemaIsValid(restrictedSchema)
445470 }
446471
447472 def " leaves concrete type if has public and private" () {
@@ -478,6 +503,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
478503 (restrictedSchema. getType(" Account" ) as GraphQLObjectType ). getFieldDefinition(" billingStatus" ) == null
479504 (restrictedSchema. getType(" Account" ) as GraphQLObjectType ). getFieldDefinition(" publicBillingStatus" ) != null
480505 restrictedSchema. getType(" BillingStatus" ) != null
506+ assertSchemaIsValid(restrictedSchema)
481507 }
482508
483509 def " removes interface type if only private reference with multiple interfaces" () {
@@ -524,6 +550,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
524550 restrictedSchema. getType(" SuperSecretCustomerData" ) == null
525551 restrictedSchema. getType(" Billable" ) == null
526552 restrictedSchema. getType(" PublicView" ) != null
553+ assertSchemaIsValid(restrictedSchema)
527554 }
528555
529556 def " primitive types are retained" () {
@@ -546,6 +573,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
546573 then :
547574 restrictedSchema. getType(" String" ) != null
548575 restrictedSchema. getType(" Boolean" ) != null
576+ assertSchemaIsValid(restrictedSchema)
549577 }
550578
551579 def " root types with different names are supported" () {
@@ -577,6 +605,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
577605
578606 then :
579607 (restrictedSchema. getType(" Account" ) as GraphQLObjectType ). getFieldDefinition(" billingStatus" ) == null
608+ assertSchemaIsValid(restrictedSchema)
580609 }
581610
582611 def " fields and types are removed from subscriptions and mutations" () {
@@ -620,6 +649,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
620649 then :
621650 restrictedSchema. getType(" Foo" ) == null
622651 restrictedSchema. getType(" Bar" ) == null
652+ assertSchemaIsValid(restrictedSchema)
623653 }
624654
625655 def " type with both private and public transitive references is retained" () {
@@ -653,6 +683,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
653683 then :
654684 (restrictedSchema. getType(" Foo" ) as GraphQLObjectType ). getFieldDefinition(" baz" ) == null
655685 restrictedSchema. getType(" Baz" ) != null
686+ assertSchemaIsValid(restrictedSchema)
656687 }
657688
658689 def " type with multiple private parent references is removed" () {
@@ -688,6 +719,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
688719 (restrictedSchema. getType(" Foo" ) as GraphQLObjectType ). getFieldDefinition(" baz" ) == null
689720 (restrictedSchema. getType(" Bar" ) as GraphQLObjectType ). getFieldDefinition(" baz" ) == null
690721 restrictedSchema. getType(" Baz" ) == null
722+ assertSchemaIsValid(restrictedSchema)
691723 }
692724
693725 def " type with multiple private grandparent references is removed" () {
@@ -722,6 +754,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
722754 restrictedSchema. getType(" Foo" ) == null
723755 restrictedSchema. getType(" Bar" ) == null
724756 restrictedSchema. getType(" Baz" ) == null
757+ assertSchemaIsValid(restrictedSchema)
725758 }
726759
727760 def " type with circular reference can be traversed" () {
@@ -746,6 +779,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
746779 then :
747780 (restrictedSchema. getType(" Foo" ) as GraphQLObjectType ). getFieldDefinition(" foo2" ) == null
748781 restrictedSchema. getType(" Foo" ) != null
782+ assertSchemaIsValid(restrictedSchema)
749783 }
750784
751785 def " input types can have private fields" () {
@@ -784,6 +818,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
784818 (restrictedSchema. getType(" FooInput" ) as GraphQLInputObjectType ). getFieldDefinition(" foo" ) == null
785819 restrictedSchema. getType(" FooInput" ) != null
786820 restrictedSchema. getType(" BarInput" ) == null
821+ assertSchemaIsValid(restrictedSchema)
787822 }
788823
789824 def " enum types can be removed" () {
@@ -831,6 +866,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
831866 restrictedSchema. getType(" FooInput" ) != null
832867 restrictedSchema. getType(" BarEnum" ) == null
833868 restrictedSchema. getType(" FooEnum" ) == null
869+ assertSchemaIsValid(restrictedSchema)
834870 }
835871
836872 def " unreferenced types can have fields removed, and the referenced types must be removed as well if they are not used" () {
@@ -868,6 +904,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
868904
869905 and : " Bing is also an additional type not reachable from roots, so it is preserved"
870906 restrictedSchema. getType(" Bing" ) != null
907+ assertSchemaIsValid(restrictedSchema)
871908 }
872909
873910 def " unreferenced types can have fields removed, and referenced type must not be removed if used elsewhere in the connected graph" () {
@@ -906,6 +943,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
906943
907944 and : " since Bing is used in the connected graph, it MUST not be removed"
908945 restrictedSchema. getType(" Bing" ) != null
946+ assertSchemaIsValid(restrictedSchema)
909947 }
910948
911949 def " unreferenced types can have fields removed, and referenced type must not be removed if used elsewhere" () {
@@ -945,6 +983,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
945983 and : " since Bing is used elsewhere (Bar.foo), it SHOULD not be removed"
946984 restrictedSchema. getType(" Bing" ) != null
947985 }
986+
948987 def " use type references - private field declared with interface type removes both concrete and interface" () {
949988 given :
950989 def query = newObject()
@@ -1041,6 +1080,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
10411080 (restrictedSchema. getType(" Account" ) as GraphQLObjectType ). getFieldDefinition(" billingStatus" ) == null
10421081 restrictedSchema. getType(" BillingStatus" ) == null
10431082 restrictedSchema. getType(" SuperSecretCustomerData" ) == null
1083+ assertSchemaIsValid(restrictedSchema)
10441084 }
10451085
10461086 def " use type references - unreferenced types are removed" () {
@@ -1074,6 +1114,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
10741114 then :
10751115 (restrictedSchema. getType(" Account" ) as GraphQLObjectType ). getFieldDefinition(" billingStatus" ) == null
10761116 restrictedSchema. getType(" BillingStatus" ) == null
1117+ assertSchemaIsValid(restrictedSchema)
10771118 }
10781119
10791120 def " before and after transformation hooks are run" () {
@@ -1148,6 +1189,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
11481189 then :
11491190 (restrictedSchema. getType(" Account" ) as GraphQLObjectType ). getFieldDefinition(" billingStatus" ) == null
11501191 restrictedSchema. getType(" BillingStatus" ) != null
1192+ assertSchemaIsValid(restrictedSchema)
11511193 }
11521194
11531195 def " handles types that become visible via types reachable by interface that implements interface" () {
@@ -1193,6 +1235,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
11931235 then :
11941236 (restrictedSchema. getType(" Account" ) as GraphQLObjectType ). getFieldDefinition(" billingStatus" ) == null
11951237 restrictedSchema. getType(" BillingStatus" ) != null
1238+ assertSchemaIsValid(restrictedSchema)
11961239 }
11971240
11981241 def " can remove a field with a directive containing enum argument" () {
@@ -1225,6 +1268,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
12251268 then :
12261269 (restrictedSchema. getType(" Account" ) as GraphQLObjectType ). getFieldDefinition(" billingStatus" ) == null
12271270 restrictedSchema. getType(" BillingStatus" ) == null
1271+ assertSchemaIsValid(restrictedSchema)
12281272 }
12291273
12301274 def " can remove a field with a directive containing type argument" () {
@@ -1256,7 +1300,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
12561300 then :
12571301 (restrictedSchema. getType(" Account" ) as GraphQLObjectType ). getFieldDefinition(" billingStatus" ) == null
12581302 restrictedSchema. getType(" BillingStatus" ) == null
1259-
1303+ assertSchemaIsValid(restrictedSchema)
12601304 }
12611305
12621306 def " remove all fields from a type which is referenced via additional types" () {
@@ -1288,6 +1332,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
12881332 GraphQLSchema restrictedSchema = visibilitySchemaTransformation. apply(patchedSchema)
12891333 then :
12901334 (restrictedSchema. getType(" Foo" ) as GraphQLObjectType ). getFieldDefinition(" toDelete" ) == null
1335+ assertSchemaIsValid(restrictedSchema)
12911336 }
12921337
12931338 def " remove field from a type which is referenced via additional types and an additional not reachable child is deleted" () {
@@ -1429,6 +1474,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
14291474 // Rental should only have id field (customer is private)
14301475 (restrictedSchema. getType(" Rental" ) as GraphQLObjectType ). getFieldDefinition(" id" ) != null
14311476 (restrictedSchema. getType(" Rental" ) as GraphQLObjectType ). getFieldDefinition(" customer" ) == null
1477+ assertSchemaIsValid(restrictedSchema)
14321478 }
14331479
14341480 def " originally unused type subgraph is fully preserved with private field removal" () {
@@ -1489,6 +1535,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
14891535
14901536 and : " PrivateOnlyType is also an additional type not reachable from roots, so it is preserved"
14911537 restrictedSchema. getType(" PrivateOnlyType" ) != null
1538+ assertSchemaIsValid(restrictedSchema)
14921539 }
14931540
14941541 def " multiple originally unused type subgraphs are all preserved" () {
@@ -1536,6 +1583,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
15361583 and : " Second unused subgraph is fully preserved"
15371584 restrictedSchema. getType(" UnusedB" ) != null
15381585 restrictedSchema. getType(" UnusedBChild" ) != null
1586+ assertSchemaIsValid(restrictedSchema)
15391587 }
15401588
15411589 def " findRootUnusedTypes considers interface implementations as reachable from roots" () {
@@ -1576,6 +1624,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
15761624
15771625 and : " TrulyUnused is an additional type not reachable from roots, so it is preserved as root unused type"
15781626 restrictedSchema. getType(" TrulyUnused" ) != null
1627+ assertSchemaIsValid(restrictedSchema)
15791628 }
15801629
15811630 def " findRootUnusedTypes ignores special introspection types starting with underscore" () {
@@ -1613,6 +1662,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
16131662 and : " Special introspection types starting with _ should be preserved"
16141663 restrictedSchema. getType(" _AppliedDirective" ) != null
16151664 restrictedSchema. getType(" _DirectiveArgument" ) != null
1665+ assertSchemaIsValid(restrictedSchema)
16161666 }
16171667
16181668 def " custom scalar types are removed when only referenced by private fields" () {
@@ -1643,6 +1693,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
16431693
16441694 and : " SecretToken scalar is only used by private field, so it should be removed"
16451695 restrictedSchema. getType(" SecretToken" ) == null
1696+ assertSchemaIsValid(restrictedSchema)
16461697 }
16471698
16481699 def " originally unused enum types are preserved" () {
@@ -1674,6 +1725,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
16741725
16751726 and : " UnusedEnum is an additional type not reachable from roots, so it is preserved"
16761727 restrictedSchema. getType(" UnusedEnum" ) != null
1728+ assertSchemaIsValid(restrictedSchema)
16771729 }
16781730
16791731 def " originally unused scalar types are preserved" () {
@@ -1697,6 +1749,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
16971749
16981750 and : " UnusedScalar is an additional type not reachable from roots, so it is preserved"
16991751 restrictedSchema. getType(" UnusedScalar" ) != null
1752+ assertSchemaIsValid(restrictedSchema)
17001753 }
17011754
17021755 def " enum and scalar types only reachable via private fields are removed" () {
@@ -1731,6 +1784,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
17311784 and : " SecretScalar and SecretEnum are only reachable via private fields, so they should be removed"
17321785 restrictedSchema. getType(" SecretScalar" ) == null
17331786 restrictedSchema. getType(" SecretEnum" ) == null
1787+ assertSchemaIsValid(restrictedSchema)
17341788 }
17351789
17361790 def " input object type only reachable via private field is removed along with nested inputs" () {
@@ -1766,6 +1820,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
17661820 and : " SecretInput and NestedSecretInput should be removed as they're only reachable via private field"
17671821 restrictedSchema. getType(" SecretInput" ) == null
17681822 restrictedSchema. getType(" NestedSecretInput" ) == null
1823+ assertSchemaIsValid(restrictedSchema)
17691824 }
17701825
17711826 def " nested input types are removed when parent input field is private" () {
@@ -1807,6 +1862,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
18071862 and : " SecretDataInput and DeepSecretInput should be removed as they're only reachable via private field"
18081863 restrictedSchema. getType(" SecretDataInput" ) == null
18091864 restrictedSchema. getType(" DeepSecretInput" ) == null
1865+ assertSchemaIsValid(restrictedSchema)
18101866 }
18111867
18121868 def " originally unused input types are preserved" () {
@@ -1836,6 +1892,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
18361892
18371893 and : " UnusedInput is an additional type not reachable from roots, so it is preserved"
18381894 restrictedSchema. getType(" UnusedInput" ) != null
1895+ assertSchemaIsValid(restrictedSchema)
18391896 }
18401897
18411898 def " input types only reachable via private input fields are removed" () {
@@ -1871,6 +1928,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
18711928
18721929 and : " PrivateRefInput should be removed as it's only reachable via private input field"
18731930 restrictedSchema. getType(" PrivateRefInput" ) == null
1931+ assertSchemaIsValid(restrictedSchema)
18741932 }
18751933
18761934 def " input type used by both public and private fields is preserved" () {
@@ -1903,6 +1961,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
19031961
19041962 and : " SharedInput should be preserved because it's still used by publicAction"
19051963 restrictedSchema. getType(" SharedInput" ) != null
1964+ assertSchemaIsValid(restrictedSchema)
19061965 }
19071966
19081967 def " input field with nested input referencing enum and scalar" () {
@@ -1948,6 +2007,7 @@ class FieldVisibilitySchemaTransformationTest extends Specification {
19482007 restrictedSchema. getType(" SecretConfigInput" ) == null
19492008 restrictedSchema. getType(" SecretToken" ) == null
19502009 restrictedSchema. getType(" SecretLevel" ) == null
2010+ assertSchemaIsValid(restrictedSchema)
19512011 }
19522012
19532013}
0 commit comments