diff --git a/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/RuntimeAPICommonUtil.java b/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/RuntimeAPICommonUtil.java index c383a740b3..03f0433593 100644 --- a/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/RuntimeAPICommonUtil.java +++ b/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/RuntimeAPICommonUtil.java @@ -63,9 +63,8 @@ public static APIDefinitionValidationResponse validateOpenAPIDefinition(String t new String(inputByteArray, StandardCharsets.UTF_8), returnContent); } else { - throw new APIManagementException("Unsupported extension type of file: " + - fileName, - ExceptionCodes.UNSUPPORTED_GRAPHQL_FILE_EXTENSION); + OASParserUtil.addErrorToValidationResponse(validationResponse, + "Invalid definition file type provided."); } } return validationResponse; diff --git a/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/definitions/OASParserUtil.java b/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/definitions/OASParserUtil.java index 3a582a6dc8..66eae511c8 100644 --- a/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/definitions/OASParserUtil.java +++ b/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/definitions/OASParserUtil.java @@ -636,27 +636,33 @@ public static APIDefinitionValidationResponse validateGraphQLSchema(String apiDe boolean returnGraphQLSchemaContent) { APIDefinitionValidationResponse validationResponse = new APIDefinitionValidationResponse(); ArrayList errors = new ArrayList<>(); - if (apiDefinition == "") { + + if (apiDefinition.isBlank()) { validationResponse.setValid(false); - errors.add(new ErrorItem("Invalid API Definition", "API Definition is empty", - 400, 400)); + errors.add(ExceptionCodes.GRAPHQL_SCHEMA_CANNOT_BE_NULL); validationResponse.setErrorItems(errors); - return validationResponse; } SchemaParser schemaParser = new SchemaParser(); - TypeDefinitionRegistry typeRegistry = schemaParser.parse(apiDefinition); - GraphQLSchema graphQLSchema = UnExecutableSchemaGenerator.makeUnExecutableSchema(typeRegistry); - SchemaValidator schemaValidation = new SchemaValidator(); - Set validationErrors = schemaValidation.validateSchema(graphQLSchema); - - if (validationErrors.toArray().length > 0) { + Set validationErrors = new HashSet<>(); + try { + TypeDefinitionRegistry typeRegistry = schemaParser.parse(apiDefinition); + GraphQLSchema graphQLSchema = UnExecutableSchemaGenerator.makeUnExecutableSchema(typeRegistry); + SchemaValidator schemaValidation = new SchemaValidator(); + validationErrors = schemaValidation.validateSchema(graphQLSchema); + if (validationErrors.toArray().length > 0) { + validationResponse.setValid(false); + errors.add(ExceptionCodes.API_NOT_GRAPHQL); + validationResponse.setErrorItems(errors); + } else { + validationResponse.setValid(true); + validationResponse.setContent(apiDefinition); + } + } catch (Exception e) { + OASParserUtil.addErrorToValidationResponse(validationResponse, e.getMessage()); validationResponse.setValid(false); errors.add(new ErrorItem("API Definition Validation Error", "API Definition is invalid", 400, 400)); validationResponse.setErrorItems(errors); - } else { - validationResponse.setValid(true); - validationResponse.setContent(apiDefinition); } return validationResponse; diff --git a/test/cucumber-tests/src/test/resources/artifacts/apk-confs/graphql_conf.apk-conf b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/graphql_conf.apk-conf new file mode 100644 index 0000000000..b8bb40ea33 --- /dev/null +++ b/test/cucumber-tests/src/test/resources/artifacts/apk-confs/graphql_conf.apk-conf @@ -0,0 +1,56 @@ +--- +name: "" +basePath: "/" +version: "" +type: "GRAPHQL" +defaultVersion: false +subscriptionValidation: false +operations: +- target: "hero" + verb: "QUERY" + secured: true + scopes: [] +- target: "reviews" + verb: "QUERY" + secured: true + scopes: [] +- target: "search" + verb: "QUERY" + secured: true + scopes: [] +- target: "character" + verb: "QUERY" + secured: true + scopes: [] +- target: "droid" + verb: "QUERY" + secured: true + scopes: [] +- target: "human" + verb: "QUERY" + secured: true + scopes: [] +- target: "allHumans" + verb: "QUERY" + secured: true + scopes: [] +- target: "allDroids" + verb: "QUERY" + secured: true + scopes: [] +- target: "allCharacters" + verb: "QUERY" + secured: true + scopes: [] +- target: "starship" + verb: "QUERY" + secured: true + scopes: [] +- target: "createReview" + verb: "MUTATION" + secured: true + scopes: [] +- target: "reviewAdded" + verb: "SUBSCRIPTION" + secured: true + scopes: [] diff --git a/test/cucumber-tests/src/test/resources/artifacts/definitions/graphql_sample_api.graphql b/test/cucumber-tests/src/test/resources/artifacts/definitions/graphql_sample_api.graphql new file mode 100644 index 0000000000..d8273eab32 --- /dev/null +++ b/test/cucumber-tests/src/test/resources/artifacts/definitions/graphql_sample_api.graphql @@ -0,0 +1,199 @@ +schema { + query: Query + mutation: Mutation + subscription: Subscription +} + +# The query type, represents all of the entry points into our object graph +type Query { + hero(episode: Episode): Character + reviews(episode: Episode!): [Review] + search(text: String): [SearchResult] + character(id: ID!): Character + droid(id: ID!): Droid + human(id: ID!): Human + allHumans(first: Int): [Human] + allDroids(first: Int): [Droid] + allCharacters(first: Int): [Character] + starship(id: ID!): Starship +} + +# The mutation type, represents all updates we can make to our data +type Mutation { + createReview(episode: Episode, review: ReviewInput!): Review +} + +# The subscription type, represents all subscriptions we can make to our data +type Subscription { + reviewAdded(episode: Episode): Review +} + +# The episodes in the Star Wars trilogy +enum Episode { + # Star Wars Episode IV: A New Hope, released in 1977. + NEWHOPE + + # Star Wars Episode V: The Empire Strikes Back, released in 1980. + EMPIRE + + # Star Wars Episode VI: Return of the Jedi, released in 1983. + JEDI + + # Star Wars Episode III: Revenge of the Sith, released in 2005 + SITH +} + +# A character from the Star Wars universe +interface Character { + # The ID of the character + id: ID! + + # The name of the character + name: String! + + # The friends of the character, or an empty list if they have none + friends: [Character] + + # The friends of the character exposed as a connection with edges + friendsConnection(first: Int, after: ID): FriendsConnection! + + # The movies this character appears in + appearsIn: [Episode]! +} + +# Units of height +enum LengthUnit { + # The standard unit around the world + METER + + # Primarily used in the United States + FOOT +} + +# A humanoid creature from the Star Wars universe +type Human implements Character { + # The ID of the human + id: ID! + + # What this human calls themselves + name: String! + + # The home planet of the human, or null if unknown + homePlanet: String + + # Height in the preferred unit, default is meters + height(unit: LengthUnit = METER): Float + + # Mass in kilograms, or null if unknown + mass: Float + + # This human's friends, or an empty list if they have none + friends: [Character] + + # The friends of the human exposed as a connection with edges + friendsConnection(first: Int, after: ID): FriendsConnection! + + # The movies this human appears in + appearsIn: [Episode]! + + # A list of starships this person has piloted, or an empty list if none + starships: [Starship] +} + +# An autonomous mechanical character in the Star Wars universe +type Droid implements Character { + # The ID of the droid + id: ID! + + # What others call this droid + name: String! + + # This droid's friends, or an empty list if they have none + friends: [Character] + + # The friends of the droid exposed as a connection with edges + friendsConnection(first: Int, after: ID): FriendsConnection! + + # The movies this droid appears in + appearsIn: [Episode]! + + # This droid's primary function + primaryFunction: String +} + +# A connection object for a character's friends +type FriendsConnection { + # The total number of friends + totalCount: Int + + # The edges for each of the character's friends. + edges: [FriendsEdge] + + # A list of the friends, as a convenience when edges are not needed. + friends: [Character] + + # Information for paginating this connection + pageInfo: PageInfo! +} + +# An edge object for a character's friends +type FriendsEdge { + # A cursor used for pagination + cursor: ID! + + # The character represented by this friendship edge + node: Character +} + +# Information for paginating this connection +type PageInfo { + startCursor: ID + endCursor: ID + hasNextPage: Boolean! +} + +# Represents a review for a movie +type Review { + # The movie + episode: Episode + + # The number of stars this review gave, 1-5 + stars: Int! + + # Comment about the movie + commentary: String +} + +# The input object sent when someone is creating a new review +input ReviewInput { + # 0-5 stars + stars: Int! + + # Comment about the movie, optional + commentary: String + + # Favorite color, optional + favorite_color: ColorInput +} + +# The input object sent when passing in a color +input ColorInput { + red: Int! + green: Int! + blue: Int! +} + +type Starship { + # The ID of the starship + id: ID! + + # The name of the starship + name: String! + + # Length of the starship, along the longest axis + length(unit: LengthUnit = METER): Float + + coordinates: [[Float!]!] +} + +union SearchResult = Human | Droid | Starship diff --git a/test/cucumber-tests/src/test/resources/artifacts/definitions/invalid_graphql_api.graphql b/test/cucumber-tests/src/test/resources/artifacts/definitions/invalid_graphql_api.graphql new file mode 100644 index 0000000000..b6af211a85 --- /dev/null +++ b/test/cucumber-tests/src/test/resources/artifacts/definitions/invalid_graphql_api.graphql @@ -0,0 +1,185 @@ +schema { + query: Query + mutation: Mutation + subscription: Subscription +} + +# The mutation type, represents all updates we can make to our data +type Mutation { + createReview(episode: Episode, review: ReviewInput!): Review +} + +# The subscription type, represents all subscriptions we can make to our data +type Subscription { + reviewAdded(episode: Episode): Review +} + +# The episodes in the Star Wars trilogy +enum Episode { + # Star Wars Episode IV: A New Hope, released in 1977. + NEWHOPE + + # Star Wars Episode V: The Empire Strikes Back, released in 1980. + EMPIRE + + # Star Wars Episode VI: Return of the Jedi, released in 1983. + JEDI + + # Star Wars Episode III: Revenge of the Sith, released in 2005 + SITH +} + +# A character from the Star Wars universe +interface Character { + # The ID of the character + id: ID! + + # The name of the character + name: String! + + # The friends of the character, or an empty list if they have none + friends: [Character] + + # The friends of the character exposed as a connection with edges + friendsConnection(first: Int, after: ID): FriendsConnection! + + # The movies this character appears in + appearsIn: [Episode]! +} + +# Units of height +enum LengthUnit { + # The standard unit around the world + METER + + # Primarily used in the United States + FOOT +} + +# A humanoid creature from the Star Wars universe +type Human implements Character { + # The ID of the human + id: ID! + + # What this human calls themselves + name: String! + + # The home planet of the human, or null if unknown + homePlanet: String + + # Height in the preferred unit, default is meters + height(unit: LengthUnit = METER): Float + + # Mass in kilograms, or null if unknown + mass: Float + + # This human's friends, or an empty list if they have none + friends: [Character] + + # The friends of the human exposed as a connection with edges + friendsConnection(first: Int, after: ID): FriendsConnection! + + # The movies this human appears in + appearsIn: [Episode]! + + # A list of starships this person has piloted, or an empty list if none + starships: [Starship] +} + +# An autonomous mechanical character in the Star Wars universe +type Droid implements Character { + # The ID of the droid + id: ID! + + # What others call this droid + name: String! + + # This droid's friends, or an empty list if they have none + friends: [Character] + + # The friends of the droid exposed as a connection with edges + friendsConnection(first: Int, after: ID): FriendsConnection! + + # The movies this droid appears in + appearsIn: [Episode]! + + # This droid's primary function + primaryFunction: String +} + +# A connection object for a character's friends +type FriendsConnection { + # The total number of friends + totalCount: Int + + # The edges for each of the character's friends. + edges: [FriendsEdge] + + # A list of the friends, as a convenience when edges are not needed. + friends: [Character] + + # Information for paginating this connection + pageInfo: PageInfo! +} + +# An edge object for a character's friends +type FriendsEdge { + # A cursor used for pagination + cursor: ID! + + # The character represented by this friendship edge + node: Character +} + +# Information for paginating this connection +type PageInfo { + startCursor: ID + endCursor: ID + hasNextPage: Boolean! +} + +# Represents a review for a movie +type Review { + # The movie + episode: Episode + + # The number of stars this review gave, 1-5 + stars: Int! + + # Comment about the movie + commentary: String +} + +# The input object sent when someone is creating a new review +input ReviewInput { + # 0-5 stars + stars: Int! + + # Comment about the movie, optional + commentary: String + + # Favorite color, optional + favorite_color: ColorInput +} + +# The input object sent when passing in a color +input ColorInput { + red: Int! + green: Int! + blue: Int! +} + +type Starship { + # The ID of the starship + id: ID! + + # The name of the starship + name: String! + + # Length of the starship, along the longest axis + length(unit: LengthUnit = METER): Float + + coordinates: [[Float!]!] +} + +union SearchResult = Human | Droid | Starship diff --git a/test/cucumber-tests/src/test/resources/tests/api/GraphQL.feature b/test/cucumber-tests/src/test/resources/tests/api/GraphQL.feature new file mode 100644 index 0000000000..e1930538d0 --- /dev/null +++ b/test/cucumber-tests/src/test/resources/tests/api/GraphQL.feature @@ -0,0 +1,13 @@ +Feature: Generating APK conf for GraphQL API + Scenario: Generating APK conf using a valid GraphQL API definition + Given The system is ready + When I use the definition file "artifacts/definitions/graphql_sample_api.graphql" in resources + And generate the APK conf file for a "GRAPHQL" API + Then the response status code should be 200 + And the response body should be "artifacts/apk-confs/graphql_conf.apk-conf" in resources + + Scenario: Generating APK conf using an invalid GraphQL API definition + Given The system is ready + When I use the definition file "artifacts/definitions/invalid_graphql_api.graphql" in resources + And generate the APK conf file for a "GRAPHQL" API + Then the response status code should be 400