diff --git a/src/main/java/com/intuit/graphql/orchestrator/schema/type/conflict/resolver/XtextTypeConflictResolver.java b/src/main/java/com/intuit/graphql/orchestrator/schema/type/conflict/resolver/XtextTypeConflictResolver.java index e17bcce1..e8ab30ac 100644 --- a/src/main/java/com/intuit/graphql/orchestrator/schema/type/conflict/resolver/XtextTypeConflictResolver.java +++ b/src/main/java/com/intuit/graphql/orchestrator/schema/type/conflict/resolver/XtextTypeConflictResolver.java @@ -3,13 +3,16 @@ import static com.intuit.graphql.orchestrator.resolverdirective.FieldResolverDirectiveUtil.RESOLVER_ARGUMENT_INPUT_NAME; import static com.intuit.graphql.orchestrator.utils.FederationConstants.FEDERATION_EXTENDS_DIRECTIVE; import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.checkFieldsCompatibility; +import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.checkInputObjectTypeCompatibility; import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isEntity; import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isInaccessible; +import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isInputObjectType; import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isScalarType; import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.toDescriptiveString; import static com.intuit.graphql.orchestrator.utils.XtextUtils.definitionContainsDirective; import com.intuit.graphql.graphQL.EnumTypeDefinition; +import com.intuit.graphql.graphQL.InputObjectTypeDefinition; import com.intuit.graphql.graphQL.InterfaceTypeDefinition; import com.intuit.graphql.graphQL.ObjectTypeDefinition; import com.intuit.graphql.graphQL.TypeDefinition; @@ -38,6 +41,13 @@ public void resolve(final TypeDefinition conflictingType, final TypeDefinition e } private void checkSameType(final TypeDefinition conflictingType, final TypeDefinition existingType) { + boolean isInputObjectTypeComparison = isInputObjectType(conflictingType) || isInputObjectType(existingType); + if (isInputObjectTypeComparison) { + // need before checkFieldsCompatibility which supports fieldContainers. InputObjectTypeComparison does not have field containers + checkInputObjectTypeCompatibility(existingType, conflictingType); + return; + } + if (!(isSameType(conflictingType, existingType) && isScalarType(conflictingType))) { throw new TypeConflictException( String.format("Type %s is conflicting with existing type %s", toDescriptiveString(conflictingType), @@ -52,6 +62,7 @@ private void checkSharedType(final TypeDefinition conflictingType, final TypeDef boolean entityComparison = conflictingTypeisEntity && existingTypeIsEntity; boolean isInaccessibleComparison = isInaccessible(conflictingType) || isInaccessible(existingType); boolean baseExtensionComparison = definitionContainsDirective(existingType, FEDERATION_EXTENDS_DIRECTIVE) || definitionContainsDirective(conflictingType, FEDERATION_EXTENDS_DIRECTIVE); + boolean isInputObjectTypeComparison = isInputObjectType(conflictingType) || isInputObjectType(existingType); if(!isInaccessibleComparison) { if(isEntity(conflictingType) != isEntity(existingType)) { @@ -67,6 +78,13 @@ private void checkSharedType(final TypeDefinition conflictingType, final TypeDef toDescriptiveString(existingType))); } + if (isInputObjectTypeComparison) { + // need before checkFieldsCompatibility which supports fieldContainers. InputObjectTypeComparison does not have field containers + checkInputObjectTypeCompatibility(existingType, conflictingType); + return; + + } + if(!(conflictingType instanceof UnionTypeDefinition || isScalarType(conflictingType) || conflictingType instanceof EnumTypeDefinition)) { checkFieldsCompatibility(existingType, conflictingType, existingTypeIsEntity, conflictingTypeisEntity,federatedComparison); } diff --git a/src/main/java/com/intuit/graphql/orchestrator/utils/XtextTypeUtils.java b/src/main/java/com/intuit/graphql/orchestrator/utils/XtextTypeUtils.java index f77bc67d..09b1fc52 100644 --- a/src/main/java/com/intuit/graphql/orchestrator/utils/XtextTypeUtils.java +++ b/src/main/java/com/intuit/graphql/orchestrator/utils/XtextTypeUtils.java @@ -211,6 +211,58 @@ public static String toDescriptiveString(ArgumentsDefinition argumentsDefinition return StringUtils.EMPTY; } + public static void checkInputObjectTypeCompatibility(TypeDefinition existingType, TypeDefinition incomingType) { + if (!(existingType instanceof InputObjectTypeDefinition)) { + throw new TypeConflictException( + format("Type %s is conflicting with Input Type %s. Both types must be of the same type", + toDescriptiveString(existingType), + toDescriptiveString(incomingType) + ) + ); + } + + if (!(incomingType instanceof InputObjectTypeDefinition)) { + throw new TypeConflictException( + format("Type %s is conflicting with Input Type %s. Both types must be of the same type", + toDescriptiveString(incomingType), + toDescriptiveString(existingType) + ) + ); + } + + + InputObjectTypeDefinition existingTypeDefinition = (InputObjectTypeDefinition) existingType; + InputObjectTypeDefinition incomingTypeDefinition = (InputObjectTypeDefinition) incomingType; + + if (existingTypeDefinition.getInputValueDefinition().size() != incomingTypeDefinition.getInputValueDefinition().size()) { + throw new TypeConflictException( + format("Type %s is conflicting with Input Type %s. Both types must be of the same size", + toDescriptiveString(incomingType), + toDescriptiveString(existingType) + ) + ); + } + + incomingTypeDefinition.getInputValueDefinition() + .forEach(incomingInputValueDefinition -> { + boolean found = existingTypeDefinition.getInputValueDefinition() + .stream() + .anyMatch(existingTnputValueDefinition -> StringUtils.equals(incomingInputValueDefinition.getName(), + existingTnputValueDefinition.getName())); + + if (!found) { + throw new TypeConflictException( + format("Type %s is conflicting with Input Type %s. Both types much have the same InputValueDefinition", + toDescriptiveString(incomingType), + toDescriptiveString(existingType) + ) + ); + } + + }); + // + } + public static void checkFieldsCompatibility(final TypeDefinition existingTypeDefinition, final TypeDefinition conflictingTypeDefinition, boolean existingTypeIsEntity, boolean conflictingTypeisEntity, boolean federatedComparison) { List existingFieldDefinitions = getFieldDefinitions(existingTypeDefinition); @@ -291,6 +343,10 @@ public static boolean isValidInputType(NamedType namedType) { return true; } + public static boolean isInputObjectType(TypeDefinition typeDefinition) { + return typeDefinition instanceof InputObjectTypeDefinition; + } + public static boolean isEntity(final TypeDefinition type) { return definitionContainsDirective(type, FEDERATION_KEY_DIRECTIVE); } diff --git a/src/main/java/com/intuit/graphql/orchestrator/xtext/XtextResourceSetBuilder.java b/src/main/java/com/intuit/graphql/orchestrator/xtext/XtextResourceSetBuilder.java index e6ce0dc7..504ab2a0 100644 --- a/src/main/java/com/intuit/graphql/orchestrator/xtext/XtextResourceSetBuilder.java +++ b/src/main/java/com/intuit/graphql/orchestrator/xtext/XtextResourceSetBuilder.java @@ -36,7 +36,8 @@ public class XtextResourceSetBuilder { private Map files = new ConcurrentHashMap<>(); private boolean isFederatedResourceSet = false; - public static final String FEDERATION_DIRECTIVES = getFederationDirectives(); + public static final String FEDERATION_DIRECTIVES = getDirectiveDefinitions("federation_built_in_directives.graphqls"); + public static final String AUTHZPOLICY_DIRECTIVES = getDirectiveDefinitions("authzpolicy_directive_definition.graphqls"); private XtextResourceSetBuilder() { } @@ -79,13 +80,17 @@ public XtextResourceSet build() { if(isFederatedResourceSet) { String content = FEDERATION_DIRECTIVES + "\n" + StringUtils.join(files.values(), "\n"); - + content = addAuthzPolicyDirectiveDefinition(content); try { createGraphqlResourceFromString(content, "appended_federation"); } catch (IOException e) { throw new SchemaParseException("Unable to parse file: appended federation file", e); } } else { + if (isUsingAuthzPolicy(files)) { + files.put("authzpolicy_directive_definition.graphqls", AUTHZPOLICY_DIRECTIVES); + } + files.forEach((fileName, content) -> { try { createGraphqlResourceFromString(content, fileName); @@ -102,6 +107,18 @@ public XtextResourceSet build() { return graphqlResourceSet; } + private boolean isUsingAuthzPolicy(Map files) { + return files.values().stream() + .anyMatch(content -> StringUtils.contains(content, "@authzPolicy")); + } + + public String addAuthzPolicyDirectiveDefinition(String content) { + if (StringUtils.contains(content, "@authzPolicy")) { + return content + "\n" + AUTHZPOLICY_DIRECTIVES; + } + return content; + } + private XtextResource createResourceFrom(InputStream input, URI uri, Injector injector) throws IOException { XtextResource resource = (XtextResource) (injector.getInstance(IResourceFactory.class).createResource(uri)); resource.load(input, null); @@ -143,12 +160,12 @@ public static XtextResourceSet singletonSet(String fileName, String file) { .build(); } - private static String getFederationDirectives() { + private static String getDirectiveDefinitions(String file) { String directives = ""; try { directives = IOUtils.toString( XtextResourceSetBuilder.class.getClassLoader() - .getResourceAsStream( "federation_built_in_directives.graphqls"), + .getResourceAsStream( file), Charset.defaultCharset() ); } catch (IOException ex) { diff --git a/src/main/resources/authzpolicy_directive_definition.graphqls b/src/main/resources/authzpolicy_directive_definition.graphqls new file mode 100644 index 00000000..0cc21161 --- /dev/null +++ b/src/main/resources/authzpolicy_directive_definition.graphqls @@ -0,0 +1,2 @@ +directive @authzPolicy(id: String, ruleInputs:[RuleInput]!) on FIELD_DEFINITION +input RuleInput { key: String! value: [String]!} diff --git a/src/test/groovy/com/intuit/graphql/orchestrator/integration/authzpolicy/AuthzPolicyWithNestedLevelStitchingSpec.groovy b/src/test/groovy/com/intuit/graphql/orchestrator/integration/authzpolicy/AuthzPolicyWithNestedLevelStitchingSpec.groovy new file mode 100644 index 00000000..b25ee7d0 --- /dev/null +++ b/src/test/groovy/com/intuit/graphql/orchestrator/integration/authzpolicy/AuthzPolicyWithNestedLevelStitchingSpec.groovy @@ -0,0 +1,192 @@ +package com.intuit.graphql.orchestrator.integration.authzpolicy + +import com.intuit.graphql.orchestrator.ServiceProvider +import com.intuit.graphql.orchestrator.datafetcher.ServiceDataFetcher +import com.intuit.graphql.orchestrator.schema.Operation +import com.intuit.graphql.orchestrator.stitching.StitchingException +import graphql.schema.FieldCoordinates +import graphql.schema.GraphQLCodeRegistry +import graphql.schema.GraphQLSchema +import graphql.schema.StaticDataFetcher +import helpers.BaseIntegrationTestSpecification +import spock.lang.Subject + +import static com.intuit.graphql.orchestrator.TestHelper.getResourceAsString +import static graphql.Scalars.GraphQLInt + +class AuthzPolicyWithNestedLevelStitchingSpec extends BaseIntegrationTestSpecification { + + def v4osService, turboService + + def mockServiceResponse = new HashMap() + + def conflictingSchema = """ + type Query { + root: RootType + topLevelField: NestedType @authzPolicy(ruleInput: [{key:"foo",value:"a"}]) + } + + type RootType { + nestedField: NestedType + } + + type NestedType { + fieldA: String @authzPolicy(ruleInput: [{key:"foo",value:"b"}]) + fieldB: String @authzPolicy(ruleInput: [{key:"foo",value:"b"}]) + } + """ + + def conflictingSchema2 = """ + type Query { + root: RootType + } + + type RootType { + nestedField: NestedType + } + + type NestedType { + fieldC: String @authzPolicy(ruleInput: [{key:"foo",value:"b"}]) + fieldD: String @authzPolicy(ruleInput: [{key:"foo",value:"c"}]) + } + """ + ServiceProvider topLevelDeprecatedService1 + ServiceProvider topLevelDeprecatedService2 + + + @Subject + def specUnderTest + + def "Test Nested Stitching"() { + given: + v4osService = createSimpleMockService("V4OS", getResourceAsString("nested/v4os/schema.graphqls"), mockServiceResponse) + turboService = createSimpleMockService("TURBO", getResourceAsString("nested/turbo/schema.graphqls"), mockServiceResponse) + + when: + specUnderTest = createGraphQLOrchestrator([v4osService, turboService]) + + then: + GraphQLSchema result = specUnderTest.schema + + def consumer = result?.getQueryType()?.getFieldDefinition("consumer") + consumer != null + + def consumerType = result?.getQueryType()?.getFieldDefinition("consumer")?.type + consumerType != null + consumerType.name == "ConsumerType" + consumerType.description == "[V4OS,TURBO]" + + def financialProfile = consumerType?.getFieldDefinition("financialProfile") + financialProfile != null + financialProfile.type?.name == "FinancialProfileType" + + def turboExperiences = consumerType?.getFieldDefinition("turboExperiences") + turboExperiences != null + turboExperiences.type?.name == "ExperienceType" + + def financeField = consumerType?.getFieldDefinition("finance") + financeField != null + financeField.type?.name == "FinanceType" + financeField.type?.getFieldDefinition("fieldFinance")?.type == GraphQLInt + financeField.type?.getFieldDefinition("fieldTurbo")?.type == GraphQLInt + + //DataFetchers + final GraphQLCodeRegistry codeRegistry = specUnderTest.runtimeGraph.getCodeRegistry().build() + codeRegistry?.getDataFetcher(FieldCoordinates.coordinates("Query", "consumer"), consumer) instanceof StaticDataFetcher + codeRegistry?.getDataFetcher(FieldCoordinates.coordinates("ConsumerType", "finance"), financeField) instanceof StaticDataFetcher + codeRegistry?.getDataFetcher(FieldCoordinates.coordinates("ConsumerType", "financialProfile"), financeField) instanceof ServiceDataFetcher + codeRegistry?.getDataFetcher(FieldCoordinates.coordinates("ConsumerType", "turboExperiences"), turboExperiences) instanceof ServiceDataFetcher + } + + def "Nested Type Description With Namespace And Empty Description"() { + given: + def bSchema = "schema { query: Query } type Query { a: A } \"\n" +\ + " \"type A { b: B @adapter(service: 'foo') } type B {d: D}\"\n" +\ + " \"type D { field: String}\"\n" +\ + " \"directive @adapter(service:String!) on FIELD_DEFINITION" + + def bbSchema = "schema { query: Query } type Query { a: A } \"\n" +\ + " \"type A { bbc: BB } type BB {cc: String}" + + def abcSchema = "schema { query: Query } type Query { a: A } \"\n" +\ + " \"type A { bbcd: C } type C {cc: String}" + + def secondSchema = "schema { query: Query } type Query { a: A } " +\ + "type A { bbbb: BAB } type BAB {fieldBB: String}" + + def ambcSchema = "schema { query: Query } type Query { a: A } \"\n" +\ + " \"type A { bba: CDD } type CDD {ccdd: String}" + + def ttbbSchema = "schema { query: Query } type Query { a: A } \"\n" +\ + " \"type A { bbab: BBD } type BBD {cc: String}" + + def bService = createSimpleMockService("SVC_b", bSchema, mockServiceResponse) + def bbService = createSimpleMockService("SVC_bb", bbSchema, mockServiceResponse) + def abcService = createSimpleMockService("SVC_abc", abcSchema, mockServiceResponse) + def secondService = createSimpleMockService("SVC_Second", secondSchema, mockServiceResponse) + def ambcService = createSimpleMockService("AMBC", ambcSchema, mockServiceResponse) + def ttbbService = createSimpleMockService("TTBB", ttbbSchema, mockServiceResponse) + + when: + specUnderTest = createGraphQLOrchestrator([bService, bbService ,abcService,secondService, ambcService, ttbbService]) + + + then: + def aType = specUnderTest.runtimeGraph.getOperation(Operation.QUERY)?.getFieldDefinition("a")?.type + + aType.description.contains("SVC_abc") + aType.description.contains("SVC_bb") + aType.description.contains("AMBC") + aType.description.contains("TTBB") + aType.description.contains("SVC_Second") + aType.description.contains("SVC_b") + } + + def "Nested Type Description With Namespace And Description"() { + given: + String schema1 = "schema { query: Query } type Query { a: A } " +\ + "type A { b: B @adapter(service: 'foo') } type B {d: D}" +\ + "type D { field: String}" +\ + "directive @adapter(service:String!) on FIELD_DEFINITION" + + String schema2 = "schema { query: Query } type Query { a: A } " +\ + "\"description for schema2\"type A { bbc: BB } type BB {cc: String}" + + String schema3 = "schema { query: Query } type Query { a: A } " +\ + "\"description for schema3\"type A { bbcd: C } type C {cc: String}" + + String schema4 = "schema { query: Query } type Query { a: A } " +\ + "type A { bbbb: BAB } type BAB {fieldBB: String}" + + def service1 = createSimpleMockService("SVC_b", schema1, mockServiceResponse) + def service2 = createSimpleMockService("SVC_bb", schema2, mockServiceResponse) + def service3 = createSimpleMockService("SVC_abc", schema3, mockServiceResponse) + def service4 = createSimpleMockService("SVC_Second", schema4, mockServiceResponse) + + when: + specUnderTest = createGraphQLOrchestrator([service1, service2, service3, service4]) + + then: + def aType = specUnderTest?.runtimeGraph?.getOperation(Operation.QUERY)?.getFieldDefinition("a")?.type + + aType.description.contains("SVC_abc") + aType.description.contains("SVC_bb") + aType.description.contains("SVC_Second") + aType.description.contains("SVC_b") + aType.description.contains("description for schema3") + aType.description.contains("description for schema2") + } + + def "deprecated fields can not be referenced again"() { + given: + topLevelDeprecatedService1 = createSimpleMockService("test1", conflictingSchema, new HashMap()) + topLevelDeprecatedService2 = createSimpleMockService("test2", conflictingSchema2, new HashMap()) + + when: + specUnderTest = createGraphQLOrchestrator([topLevelDeprecatedService1, topLevelDeprecatedService2]) + + then: + def exception = thrown(StitchingException) + exception.message == "FORBIDDEN: Subgraphs [test2,test1] are reusing type NestedType with different field definitions." + } +} diff --git a/src/test/groovy/com/intuit/graphql/orchestrator/integration/authzpolicy/FederationSubgraphAndNonFederatoinSubgraphWithAuthzPolicySpec.groovy b/src/test/groovy/com/intuit/graphql/orchestrator/integration/authzpolicy/FederationSubgraphAndNonFederatoinSubgraphWithAuthzPolicySpec.groovy new file mode 100644 index 00000000..c18113cc --- /dev/null +++ b/src/test/groovy/com/intuit/graphql/orchestrator/integration/authzpolicy/FederationSubgraphAndNonFederatoinSubgraphWithAuthzPolicySpec.groovy @@ -0,0 +1,174 @@ +package com.intuit.graphql.orchestrator.integration.authzpolicy + +import com.google.common.collect.ImmutableMap +import com.intuit.graphql.orchestrator.ServiceProvider +import com.intuit.graphql.orchestrator.TestServiceProvider +import com.intuit.graphql.orchestrator.schema.Operation +import graphql.schema.GraphQLDirective +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLObjectType +import helpers.BaseIntegrationTestSpecification +import spock.lang.Subject + +class FederationSubgraphAndNonFederatoinSubgraphWithAuthzPolicySpec extends BaseIntegrationTestSpecification { + + @Subject + def specUnderTest + + def "Both subgraphs are using @authzPolicy"(){ + given: + def schema1 = """ + type Query { + getProvider1Val: PersonA @authzPolicy(ruleInput: [{key:"foo",value:"a"}]) + } + type PersonA { + id: ID! + name: String + } + """ + + def schema2 = """ + type Query { + getProvider2Val: PersonB @authzPolicy(ruleInput: [{key:"foo",value:"b"}]) + } + type PersonB { + id: ID! + name: String + test: String + } + """ + + def valueProvider1 = TestServiceProvider.newBuilder() + .namespace("A") + .serviceType(ServiceProvider.ServiceType.FEDERATION_SUBGRAPH) + .sdlFiles(ImmutableMap.of("schema1", schema1)) + .build() + + def valueProvider2 = TestServiceProvider.newBuilder() + .namespace("B") + .serviceType(ServiceProvider.ServiceType.GRAPHQL) + .sdlFiles(ImmutableMap.of("schema2", schema2)) + .build() + + when: + specUnderTest = createGraphQLOrchestrator([valueProvider1, valueProvider2]) + + then: + final GraphQLObjectType queryType = specUnderTest?.runtimeGraph?.getOperation(Operation.QUERY) + GraphQLFieldDefinition getProvider1ValFieldDef = queryType.getFieldDefinition("getProvider1Val") + GraphQLFieldDefinition getProvider2ValFieldDef = queryType.getFieldDefinition("getProvider2Val") + getProvider1ValFieldDef.type.name == "PersonA" + getProvider2ValFieldDef.type.name == "PersonB" + + GraphQLDirective directive1 = getProvider1ValFieldDef.getDirective("authzPolicy") + directive1.getName() == "authzPolicy" + GraphQLDirective directive2 = getProvider2ValFieldDef.getDirective("authzPolicy") + directive2.getName() == "authzPolicy" + + } + + def "Federation subgraph is using @authzPolicy"() { + given: + def schema1 = """ + type Query { + getProvider1Val: PersonA @authzPolicy(ruleInput: [{key:"foo",value:"a"}]) + } + type PersonA { + id: ID! + name: String + } + """ + + def schema2 = """ + type Query { + getProvider2Val: PersonB + } + type PersonB { + id: ID! + name: String + test: String + } + """ + + def valueProvider1 = TestServiceProvider.newBuilder() + .namespace("A") + .serviceType(ServiceProvider.ServiceType.FEDERATION_SUBGRAPH) + .sdlFiles(ImmutableMap.of("schema1", schema1)) + .build() + + def valueProvider2 = TestServiceProvider.newBuilder() + .namespace("B") + .serviceType(ServiceProvider.ServiceType.GRAPHQL) + .sdlFiles(ImmutableMap.of("schema2", schema2)) + .build() + + when: + specUnderTest = createGraphQLOrchestrator([valueProvider1, valueProvider2]) + + then: + final GraphQLObjectType queryType = specUnderTest?.runtimeGraph?.getOperation(Operation.QUERY) + GraphQLFieldDefinition getProvider1ValFieldDef = queryType.getFieldDefinition("getProvider1Val") + GraphQLFieldDefinition getProvider2ValFieldDef = queryType.getFieldDefinition("getProvider2Val") + getProvider1ValFieldDef.type.name == "PersonA" + getProvider2ValFieldDef.type.name == "PersonB" + + GraphQLDirective directive1 = getProvider1ValFieldDef.getDirective("authzPolicy") + directive1.getName() == "authzPolicy" + GraphQLDirective directive2 = getProvider2ValFieldDef.getDirective("authzPolicy") + directive2 == null + + } + + def "GraphQL subgraph is using @authzPolicy"() { + given: + def schema1 = """ + type Query { + getProvider1Val: PersonA + } + type PersonA { + id: ID! + name: String + } + """ + + def schema2 = """ + type Query { + getProvider2Val: PersonB @authzPolicy(ruleInput: [{key:"foo",value:"a"}]) + } + type PersonB { + id: ID! + name: String + test: String + } + """ + + def valueProvider1 = TestServiceProvider.newBuilder() + .namespace("A") + .serviceType(ServiceProvider.ServiceType.FEDERATION_SUBGRAPH) + .sdlFiles(ImmutableMap.of("schema1", schema1)) + .build() + + def valueProvider2 = TestServiceProvider.newBuilder() + .namespace("B") + .serviceType(ServiceProvider.ServiceType.GRAPHQL) + .sdlFiles(ImmutableMap.of("schema2", schema2)) + .build() + + when: + specUnderTest = createGraphQLOrchestrator([valueProvider1, valueProvider2]) + + then: + final GraphQLObjectType queryType = specUnderTest?.runtimeGraph?.getOperation(Operation.QUERY) + GraphQLFieldDefinition getProvider1ValFieldDef = queryType.getFieldDefinition("getProvider1Val") + GraphQLFieldDefinition getProvider2ValFieldDef = queryType.getFieldDefinition("getProvider2Val") + getProvider1ValFieldDef.type.name == "PersonA" + getProvider2ValFieldDef.type.name == "PersonB" + + GraphQLDirective directive1 = getProvider1ValFieldDef.getDirective("authzPolicy") + directive1 == null + GraphQLDirective directive2 = getProvider2ValFieldDef.getDirective("authzPolicy") + directive2.getName() == "authzPolicy" + + } + +} \ No newline at end of file diff --git a/src/test/groovy/com/intuit/graphql/orchestrator/integration/authzpolicy/FederationSubgraphWithAuthzPolicySpec.groovy b/src/test/groovy/com/intuit/graphql/orchestrator/integration/authzpolicy/FederationSubgraphWithAuthzPolicySpec.groovy new file mode 100644 index 00000000..cba0a75e --- /dev/null +++ b/src/test/groovy/com/intuit/graphql/orchestrator/integration/authzpolicy/FederationSubgraphWithAuthzPolicySpec.groovy @@ -0,0 +1,122 @@ +package com.intuit.graphql.orchestrator.integration.authzpolicy + +import com.google.common.collect.ImmutableMap +import com.intuit.graphql.orchestrator.ServiceProvider +import com.intuit.graphql.orchestrator.TestServiceProvider +import com.intuit.graphql.orchestrator.schema.Operation +import graphql.schema.GraphQLDirective +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLObjectType +import helpers.BaseIntegrationTestSpecification +import spock.lang.Subject + +class FederationSubgraphWithAuthzPolicySpec extends BaseIntegrationTestSpecification { + + @Subject + def specUnderTest + + def "Two Federation Schemas with @authzPolicy"(){ + given: + def schema1 = """ + type Query { + getProvider1Val: sharedValueType @authzPolicy(ruleInput: [{key:"foo",value:"a"}]) + } + type sharedValueType { + id: ID! + name: String + } + """ + + def schema2 = """ + type Query { + getProvider2Val: sharedValueType @authzPolicy(ruleInput: [{key:"foo",value:"b"}]) + } + type sharedValueType { + id: ID! + name: String + test: String + } + """ + + def valueProvider1 = TestServiceProvider.newBuilder() + .namespace("A") + .serviceType(ServiceProvider.ServiceType.FEDERATION_SUBGRAPH) + .sdlFiles(ImmutableMap.of("schema1", schema1)) + .build() + + def valueProvider2 = TestServiceProvider.newBuilder() + .namespace("B") + .serviceType(ServiceProvider.ServiceType.FEDERATION_SUBGRAPH) + .sdlFiles(ImmutableMap.of("schema2", schema2)) + .build() + + when: + specUnderTest = createGraphQLOrchestrator([valueProvider1, valueProvider2]) + + then: + final GraphQLObjectType queryType = specUnderTest?.runtimeGraph?.getOperation(Operation.QUERY) + GraphQLFieldDefinition getProvider1ValFieldDef = queryType.getFieldDefinition("getProvider1Val") + GraphQLFieldDefinition getProvider2ValFieldDef = queryType.getFieldDefinition("getProvider2Val") + getProvider1ValFieldDef.type.name == "sharedValueType" + getProvider2ValFieldDef.type.name == "sharedValueType" + + GraphQLDirective directive1 = getProvider1ValFieldDef.getDirective("authzPolicy") + directive1.getName() == "authzPolicy" + GraphQLDirective directive2 = getProvider2ValFieldDef.getDirective("authzPolicy") + directive2.getName() == "authzPolicy" + + } + + def "Two Federation Schemas with one using @authzPolicy"() { + given: + def schema1 = """ + type Query { + getProvider1Val: sharedValueType @authzPolicy(ruleInput: [{key:"foo",value:"a"}]) + } + type sharedValueType { + id: ID! + name: String + } + """ + + def schema2 = """ + type Query { + getProvider2Val: sharedValueType + } + type sharedValueType { + id: ID! + name: String + test: String + } + """ + + def valueProvider1 = TestServiceProvider.newBuilder() + .namespace("A") + .serviceType(ServiceProvider.ServiceType.FEDERATION_SUBGRAPH) + .sdlFiles(ImmutableMap.of("schema1", schema1)) + .build() + + def valueProvider2 = TestServiceProvider.newBuilder() + .namespace("B") + .serviceType(ServiceProvider.ServiceType.FEDERATION_SUBGRAPH) + .sdlFiles(ImmutableMap.of("schema2", schema2)) + .build() + + when: + specUnderTest = createGraphQLOrchestrator([valueProvider1, valueProvider2]) + + then: + final GraphQLObjectType queryType = specUnderTest?.runtimeGraph?.getOperation(Operation.QUERY) + GraphQLFieldDefinition getProvider1ValFieldDef = queryType.getFieldDefinition("getProvider1Val") + GraphQLFieldDefinition getProvider2ValFieldDef = queryType.getFieldDefinition("getProvider2Val") + getProvider1ValFieldDef.type.name == "sharedValueType" + getProvider2ValFieldDef.type.name == "sharedValueType" + + GraphQLDirective directive1 = getProvider1ValFieldDef.getDirective("authzPolicy") + directive1.getName() == "authzPolicy" + GraphQLDirective directive2 = getProvider2ValFieldDef.getDirective("authzPolicy") + directive2 == null + + } + +} \ No newline at end of file diff --git a/src/test/groovy/com/intuit/graphql/orchestrator/utils/XtextTypeUtilsSpec.groovy b/src/test/groovy/com/intuit/graphql/orchestrator/utils/XtextTypeUtilsSpec.groovy index 57c9258c..7dab2a76 100644 --- a/src/test/groovy/com/intuit/graphql/orchestrator/utils/XtextTypeUtilsSpec.groovy +++ b/src/test/groovy/com/intuit/graphql/orchestrator/utils/XtextTypeUtilsSpec.groovy @@ -1,6 +1,7 @@ package com.intuit.graphql.orchestrator.utils import com.intuit.graphql.graphQL.* +import com.intuit.graphql.orchestrator.schema.type.conflict.resolver.TypeConflictException import com.intuit.graphql.orchestrator.xtext.GraphQLFactoryDelegate import spock.lang.Specification @@ -437,4 +438,173 @@ class XtextTypeUtilsSpec extends Specification { then: thrown(IllegalArgumentException.class) } + + def "compare two equal InputObjectTypes Test"() { + given: + PrimitiveType stringType = GraphQLFactoryDelegate.createPrimitiveType().setType("String") + // Input Type 1 + InputValueDefinition inputValueDefinition1key = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinition1key.setName("key") + inputValueDefinition1key.setNamedType(stringType) + + InputValueDefinition inputValueDefinition1value = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinition1value.setName("value") + inputValueDefinition1value.setNamedType(stringType) + + InputObjectTypeDefinition inputObjectTypeDefinition1 = GraphQLFactoryDelegate.createInputObjectTypeDefinition() + inputObjectTypeDefinition1.setName("TestInput") + inputObjectTypeDefinition1.getInputValueDefinition().add(inputValueDefinition1key) + inputObjectTypeDefinition1.getInputValueDefinition().add(inputValueDefinition1value) + + // Input Type 2 + InputValueDefinition inputValueDefinition2key = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinition2key.setName("key") + inputValueDefinition2key.setNamedType(stringType) + + InputValueDefinition inputValueDefinition2value = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinition2value.setName("value") + inputValueDefinition2value.setNamedType(stringType) + + InputObjectTypeDefinition inputObjectTypeDefinition2 = GraphQLFactoryDelegate.createInputObjectTypeDefinition() + inputObjectTypeDefinition2.setName("TestInput") + inputObjectTypeDefinition2.getInputValueDefinition().add(inputValueDefinition2key) + inputObjectTypeDefinition2.getInputValueDefinition().add(inputValueDefinition2value) + + + when: + checkInputObjectTypeCompatibility(inputObjectTypeDefinition1, inputObjectTypeDefinition2) + + then: + noExceptionThrown() + } + + def "compare two InputObjectTypes with different InputValueDefinition Test"() { + given: + PrimitiveType stringType = GraphQLFactoryDelegate.createPrimitiveType().setType("String") + // Input Type 1 + InputValueDefinition inputValueDefinition1key = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinition1key.setName("key") + inputValueDefinition1key.setNamedType(stringType) + + InputValueDefinition inputValueDefinition1value = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinition1value.setName("value") + inputValueDefinition1value.setNamedType(stringType) + + InputObjectTypeDefinition inputObjectTypeDefinition1 = GraphQLFactoryDelegate.createInputObjectTypeDefinition() + inputObjectTypeDefinition1.setName("TestInput") + inputObjectTypeDefinition1.getInputValueDefinition().add(inputValueDefinition1key) + inputObjectTypeDefinition1.getInputValueDefinition().add(inputValueDefinition1value) + + // Input Type 2 + InputValueDefinition inputValueDefinition2key = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinition2key.setName("anotherKey") + inputValueDefinition2key.setNamedType(stringType) + + InputValueDefinition inputValueDefinition2value = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinition2value.setName("anotherValue") + inputValueDefinition2value.setNamedType(stringType) + + InputObjectTypeDefinition inputObjectTypeDefinition2 = GraphQLFactoryDelegate.createInputObjectTypeDefinition() + inputObjectTypeDefinition2.setName("TestInput") + inputObjectTypeDefinition2.getInputValueDefinition().add(inputValueDefinition2key) + inputObjectTypeDefinition2.getInputValueDefinition().add(inputValueDefinition2value) + + + when: + checkInputObjectTypeCompatibility(inputObjectTypeDefinition1, inputObjectTypeDefinition2) + + then: + def e = thrown(TypeConflictException) + e.message.contains("Both types much have the same InputValueDefinition") + } + + def "compare two InputObjectTypes with different InputValueDefinitions Size Test"() { + given: + PrimitiveType stringType = GraphQLFactoryDelegate.createPrimitiveType().setType("String") + // Input Type 1 + InputValueDefinition inputValueDefinition1key = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinition1key.setName("key") + inputValueDefinition1key.setNamedType(stringType) + + InputValueDefinition inputValueDefinition1value = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinition1value.setName("value") + inputValueDefinition1value.setNamedType(stringType) + + InputObjectTypeDefinition inputObjectTypeDefinition1 = GraphQLFactoryDelegate.createInputObjectTypeDefinition() + inputObjectTypeDefinition1.setName("TestInput") + inputObjectTypeDefinition1.getInputValueDefinition().add(inputValueDefinition1key) + inputObjectTypeDefinition1.getInputValueDefinition().add(inputValueDefinition1value) + + // Input Type 2 + InputValueDefinition inputValueDefinition2key = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinition2key.setName("anotherKey") + inputValueDefinition2key.setNamedType(stringType) + + // only one InputValueDefinition + + InputObjectTypeDefinition inputObjectTypeDefinition2 = GraphQLFactoryDelegate.createInputObjectTypeDefinition() + inputObjectTypeDefinition2.setName("TestInput") + inputObjectTypeDefinition2.getInputValueDefinition().add(inputValueDefinition2key) + + + when: + checkInputObjectTypeCompatibility(inputObjectTypeDefinition1, inputObjectTypeDefinition2) + + then: + def e = thrown(TypeConflictException) + e.message.contains("Both types must be of the same size") + } + + def "compare incoming InputObjectType to an existing non InputObjectType test"() { + given: + PrimitiveType stringType = GraphQLFactoryDelegate.createPrimitiveType().setType("String") + // Object Type + ObjectTypeDefinition objectTypeDefinition = GraphQLFactoryDelegate.createObjectTypeDefinition() + + // Input Type + InputValueDefinition inputValueDefinition2key = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinition2key.setName("anotherKey") + inputValueDefinition2key.setNamedType(stringType) + + // only one InputValueDefinition + + InputObjectTypeDefinition inputObjectTypeDefinition2 = GraphQLFactoryDelegate.createInputObjectTypeDefinition() + inputObjectTypeDefinition2.setName("TestInput") + inputObjectTypeDefinition2.getInputValueDefinition().add(inputValueDefinition2key) + + + when: + checkInputObjectTypeCompatibility(objectTypeDefinition, inputObjectTypeDefinition2) + + then: + def e = thrown(TypeConflictException) + e.message.contains("Both types must be of the same type") + } + + def "compare incoming non InputObjectType to an existing InputObjectType test"() { + given: + PrimitiveType stringType = GraphQLFactoryDelegate.createPrimitiveType().setType("String") + // Input Type + InputValueDefinition inputValueDefinitionkey = GraphQLFactoryDelegate.createInputValueDefinition() + inputValueDefinitionkey.setName("key") + inputValueDefinitionkey.setNamedType(stringType) + + // only one InputValueDefinition + + InputObjectTypeDefinition inputObjectTypeDefinition = GraphQLFactoryDelegate.createInputObjectTypeDefinition() + inputObjectTypeDefinition.setName("TestInput") + inputObjectTypeDefinition.getInputValueDefinition().add(inputValueDefinitionkey) + + // Object Type + ObjectTypeDefinition objectTypeDefinition = GraphQLFactoryDelegate.createObjectTypeDefinition() + + + + when: + checkInputObjectTypeCompatibility(inputObjectTypeDefinition, objectTypeDefinition) + + then: + def e = thrown(TypeConflictException) + e.message.contains("Both types must be of the same type") + } }