diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/Language/GraphQL/AST/EncoderSpec.hs | 64 | ||||
| -rw-r--r-- | tests/Language/GraphQL/AST/LexerSpec.hs | 4 | ||||
| -rw-r--r-- | tests/Language/GraphQL/AST/ParserSpec.hs | 2 | ||||
| -rw-r--r-- | tests/Language/GraphQL/ErrorSpec.hs | 2 | ||||
| -rw-r--r-- | tests/Language/GraphQL/ExecuteSpec.hs | 39 | ||||
| -rw-r--r-- | tests/Language/GraphQL/ValidateSpec.hs | 522 | ||||
| -rw-r--r-- | tests/Test/DirectiveSpec.hs | 7 | ||||
| -rw-r--r-- | tests/Test/FragmentSpec.hs | 57 | ||||
| -rw-r--r-- | tests/Test/RootOperationSpec.hs | 14 | ||||
| -rw-r--r-- | tests/Test/StarWars/Data.hs | 204 | ||||
| -rw-r--r-- | tests/Test/StarWars/QuerySpec.hs | 366 | ||||
| -rw-r--r-- | tests/Test/StarWars/Schema.hs | 154 |
12 files changed, 543 insertions, 892 deletions
diff --git a/tests/Language/GraphQL/AST/EncoderSpec.hs b/tests/Language/GraphQL/AST/EncoderSpec.hs index 5fa3706..0c7dd39 100644 --- a/tests/Language/GraphQL/AST/EncoderSpec.hs +++ b/tests/Language/GraphQL/AST/EncoderSpec.hs @@ -4,7 +4,7 @@ module Language.GraphQL.AST.EncoderSpec ( spec ) where -import Language.GraphQL.AST +import qualified Language.GraphQL.AST.Document as Full import Language.GraphQL.AST.Encoder import Test.Hspec (Spec, context, describe, it, shouldBe, shouldStartWith, shouldEndWith, shouldNotContain) import Test.QuickCheck (choose, oneof, forAll) @@ -15,52 +15,52 @@ spec :: Spec spec = do describe "value" $ do context "null value" $ do - let testNull formatter = value formatter Null `shouldBe` "null" + let testNull formatter = value formatter Full.Null `shouldBe` "null" it "minified" $ testNull minified it "pretty" $ testNull pretty context "minified" $ do it "escapes \\" $ - value minified (String "\\") `shouldBe` "\"\\\\\"" + value minified (Full.String "\\") `shouldBe` "\"\\\\\"" it "escapes double quotes" $ - value minified (String "\"") `shouldBe` "\"\\\"\"" + value minified (Full.String "\"") `shouldBe` "\"\\\"\"" it "escapes \\f" $ - value minified (String "\f") `shouldBe` "\"\\f\"" + value minified (Full.String "\f") `shouldBe` "\"\\f\"" it "escapes \\n" $ - value minified (String "\n") `shouldBe` "\"\\n\"" + value minified (Full.String "\n") `shouldBe` "\"\\n\"" it "escapes \\r" $ - value minified (String "\r") `shouldBe` "\"\\r\"" + value minified (Full.String "\r") `shouldBe` "\"\\r\"" it "escapes \\t" $ - value minified (String "\t") `shouldBe` "\"\\t\"" + value minified (Full.String "\t") `shouldBe` "\"\\t\"" it "escapes backspace" $ - value minified (String "a\bc") `shouldBe` "\"a\\bc\"" + value minified (Full.String "a\bc") `shouldBe` "\"a\\bc\"" context "escapes Unicode for chars less than 0010" $ do - it "Null" $ value minified (String "\x0000") `shouldBe` "\"\\u0000\"" - it "bell" $ value minified (String "\x0007") `shouldBe` "\"\\u0007\"" + it "Null" $ value minified (Full.String "\x0000") `shouldBe` "\"\\u0000\"" + it "bell" $ value minified (Full.String "\x0007") `shouldBe` "\"\\u0007\"" context "escapes Unicode for char less than 0020" $ do - it "DLE" $ value minified (String "\x0010") `shouldBe` "\"\\u0010\"" - it "EM" $ value minified (String "\x0019") `shouldBe` "\"\\u0019\"" + it "DLE" $ value minified (Full.String "\x0010") `shouldBe` "\"\\u0010\"" + it "EM" $ value minified (Full.String "\x0019") `shouldBe` "\"\\u0019\"" context "encodes without escape" $ do - it "space" $ value minified (String "\x0020") `shouldBe` "\" \"" - it "~" $ value minified (String "\x007E") `shouldBe` "\"~\"" + it "space" $ value minified (Full.String "\x0020") `shouldBe` "\" \"" + it "~" $ value minified (Full.String "\x007E") `shouldBe` "\"~\"" context "pretty" $ do it "uses strings for short string values" $ - value pretty (String "Short text") `shouldBe` "\"Short text\"" + value pretty (Full.String "Short text") `shouldBe` "\"Short text\"" it "uses block strings for text with new lines, with newline symbol" $ - value pretty (String "Line 1\nLine 2") + value pretty (Full.String "Line 1\nLine 2") `shouldBe` [r|""" Line 1 Line 2 """|] it "uses block strings for text with new lines, with CR symbol" $ - value pretty (String "Line 1\rLine 2") + value pretty (Full.String "Line 1\rLine 2") `shouldBe` [r|""" Line 1 Line 2 """|] it "uses block strings for text with new lines, with CR symbol followed by newline" $ - value pretty (String "Line 1\r\nLine 2") + value pretty (Full.String "Line 1\r\nLine 2") `shouldBe` [r|""" Line 1 Line 2 @@ -77,12 +77,12 @@ spec = do forAll genNotAllowedSymbol $ \x -> do let rawValue = "Short \n" <> cons x "text" - encoded = value pretty (String $ toStrict rawValue) + encoded = value pretty (Full.String $ toStrict rawValue) shouldStartWith (unpack encoded) "\"" shouldEndWith (unpack encoded) "\"" shouldNotContain (unpack encoded) "\"\"\"" - it "Hello world" $ value pretty (String "Hello,\n World!\n\nYours,\n GraphQL.") + it "Hello world" $ value pretty (Full.String "Hello,\n World!\n\nYours,\n GraphQL.") `shouldBe` [r|""" Hello, World! @@ -91,29 +91,29 @@ spec = do GraphQL. """|] - it "has only newlines" $ value pretty (String "\n") `shouldBe` [r|""" + it "has only newlines" $ value pretty (Full.String "\n") `shouldBe` [r|""" """|] it "has newlines and one symbol at the begining" $ - value pretty (String "a\n\n") `shouldBe` [r|""" + value pretty (Full.String "a\n\n") `shouldBe` [r|""" a """|] it "has newlines and one symbol at the end" $ - value pretty (String "\n\na") `shouldBe` [r|""" + value pretty (Full.String "\n\na") `shouldBe` [r|""" a """|] it "has newlines and one symbol in the middle" $ - value pretty (String "\na\n") `shouldBe` [r|""" + value pretty (Full.String "\na\n") `shouldBe` [r|""" a """|] - it "skip trailing whitespaces" $ value pretty (String " Short\ntext ") + it "skip trailing whitespaces" $ value pretty (Full.String " Short\ntext ") `shouldBe` [r|""" Short text @@ -121,11 +121,13 @@ spec = do describe "definition" $ it "indents block strings in arguments" $ - let arguments = [Argument "message" (String "line1\nline2")] - field = Field Nothing "field" arguments [] [] - operation = DefinitionOperation - $ SelectionSet (pure field) - $ Location 0 0 + let location = Full.Location 0 0 + argumentValue = Full.Node (Full.String "line1\nline2") location + arguments = [Full.Argument "message" argumentValue location] + field = Full.Field Nothing "field" arguments [] [] location + fieldSelection = pure $ Full.FieldSelection field + operation = Full.DefinitionOperation + $ Full.SelectionSet fieldSelection location in definition pretty operation `shouldBe` [r|{ field(message: """ line1 diff --git a/tests/Language/GraphQL/AST/LexerSpec.hs b/tests/Language/GraphQL/AST/LexerSpec.hs index 0b4cb31..c4fae45 100644 --- a/tests/Language/GraphQL/AST/LexerSpec.hs +++ b/tests/Language/GraphQL/AST/LexerSpec.hs @@ -75,9 +75,9 @@ spec = describe "Lexer" $ do parse dollar "" "$" `shouldParse` "$" runBetween parens `shouldSucceedOn` "()" parse spread "" "..." `shouldParse` "..." - parse colon "" ":" `shouldParse` ":" + parse colon "" `shouldSucceedOn` ":" parse equals "" "=" `shouldParse` "=" - parse at "" "@" `shouldParse` "@" + parse at "" `shouldSucceedOn` "@" runBetween brackets `shouldSucceedOn` "[]" runBetween braces `shouldSucceedOn` "{}" parse pipe "" "|" `shouldParse` "|" diff --git a/tests/Language/GraphQL/AST/ParserSpec.hs b/tests/Language/GraphQL/AST/ParserSpec.hs index f59e5a9..5c4d39e 100644 --- a/tests/Language/GraphQL/AST/ParserSpec.hs +++ b/tests/Language/GraphQL/AST/ParserSpec.hs @@ -128,7 +128,7 @@ spec = describe "Parser" $ do parse document "" `shouldSucceedOn` [r|extend schema { query: Query }|] it "parses schema extension with an operation type and directive" $ - let newDirective = Directive "newDirective" [] + let newDirective = Directive "newDirective" [] $ Location 1 15 schemaExtension = SchemaExtension $ SchemaOperationExtension [newDirective] $ OperationTypeDefinition Query "Query" :| [] diff --git a/tests/Language/GraphQL/ErrorSpec.hs b/tests/Language/GraphQL/ErrorSpec.hs index 1497c48..38d7d3a 100644 --- a/tests/Language/GraphQL/ErrorSpec.hs +++ b/tests/Language/GraphQL/ErrorSpec.hs @@ -19,6 +19,6 @@ import Test.Hspec ( Spec spec :: Spec spec = describe "singleError" $ it "constructs an error with the given message" $ - let errors'' = Seq.singleton $ Error "Message." [] + let errors'' = Seq.singleton $ Error "Message." [] [] expected = Response Aeson.Null errors'' in singleError "Message." `shouldBe` expected diff --git a/tests/Language/GraphQL/ExecuteSpec.hs b/tests/Language/GraphQL/ExecuteSpec.hs index 8fbb55b..f6e3e6f 100644 --- a/tests/Language/GraphQL/ExecuteSpec.hs +++ b/tests/Language/GraphQL/ExecuteSpec.hs @@ -3,6 +3,7 @@ obtain one at https://mozilla.org/MPL/2.0/. -} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} module Language.GraphQL.ExecuteSpec ( spec ) where @@ -10,10 +11,11 @@ module Language.GraphQL.ExecuteSpec import Control.Exception (SomeException) import Data.Aeson ((.=)) import qualified Data.Aeson as Aeson +import Data.Aeson.Types (emptyObject) import Data.Conduit import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HashMap -import Language.GraphQL.AST (Name) +import Language.GraphQL.AST (Document, Name) import Language.GraphQL.AST.Parser (document) import Language.GraphQL.Error import Language.GraphQL.Execute @@ -21,13 +23,10 @@ import Language.GraphQL.Type as Type import Language.GraphQL.Type.Out as Out import Test.Hspec (Spec, context, describe, it, shouldBe) import Text.Megaparsec (parse) +import Text.RawString.QQ (r) -schema :: Schema (Either SomeException) -schema = Schema - { query = queryType - , mutation = Nothing - , subscription = Just subscriptionType - } +philosopherSchema :: Schema (Either SomeException) +philosopherSchema = schema queryType Nothing (Just subscriptionType) mempty queryType :: Out.ObjectType (Either SomeException) queryType = Out.ObjectType "Query" Nothing [] @@ -71,9 +70,32 @@ quoteType = Out.ObjectType "Quote" Nothing [] quoteField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty +type EitherStreamOrValue = Either + (ResponseEventStream (Either SomeException) Aeson.Value) + (Response Aeson.Value) + +execute' :: Document -> Either SomeException EitherStreamOrValue +execute' = + execute philosopherSchema Nothing (mempty :: HashMap Name Aeson.Value) + spec :: Spec spec = describe "execute" $ do + it "rejects recursive fragments" $ + let sourceQuery = [r| + { + ...cyclicFragment + } + + fragment cyclicFragment on Query { + ...cyclicFragment + } + |] + expected = Response emptyObject mempty + Right (Right actual) = either (pure . parseError) execute' + $ parse document "" sourceQuery + in actual `shouldBe` expected + context "Query" $ do it "skips unknown fields" $ let data'' = Aeson.object @@ -82,7 +104,6 @@ spec = ] ] expected = Response data'' mempty - execute' = execute schema Nothing (mempty :: HashMap Name Aeson.Value) Right (Right actual) = either (pure . parseError) execute' $ parse document "" "{ philosopher { firstName surname } }" in actual `shouldBe` expected @@ -94,7 +115,6 @@ spec = ] ] expected = Response data'' mempty - execute' = execute schema Nothing (mempty :: HashMap Name Aeson.Value) Right (Right actual) = either (pure . parseError) execute' $ parse document "" "{ philosopher { firstName } philosopher { lastName } }" in actual `shouldBe` expected @@ -106,7 +126,6 @@ spec = ] ] expected = Response data'' mempty - execute' = execute schema Nothing (mempty :: HashMap Name Aeson.Value) Right (Left stream) = either (pure . parseError) execute' $ parse document "" "subscription { newQuote { quote } }" Right (Just actual) = runConduit $ stream .| await diff --git a/tests/Language/GraphQL/ValidateSpec.hs b/tests/Language/GraphQL/ValidateSpec.hs index 8f6626b..318045c 100644 --- a/tests/Language/GraphQL/ValidateSpec.hs +++ b/tests/Language/GraphQL/ValidateSpec.hs @@ -9,8 +9,7 @@ module Language.GraphQL.ValidateSpec ( spec ) where -import Data.Sequence (Seq(..)) -import qualified Data.Sequence as Seq +import Data.Foldable (toList) import qualified Data.HashMap.Strict as HashMap import Data.Text (Text) import qualified Language.GraphQL.AST as AST @@ -18,23 +17,25 @@ import Language.GraphQL.Type import qualified Language.GraphQL.Type.In as In import qualified Language.GraphQL.Type.Out as Out import Language.GraphQL.Validate -import Test.Hspec (Spec, describe, it, shouldBe) +import Test.Hspec (Spec, describe, it, shouldBe, shouldContain) import Text.Megaparsec (parse) import Text.RawString.QQ (r) -schema :: Schema IO -schema = Schema - { query = queryType - , mutation = Nothing - , subscription = Nothing - } +petSchema :: Schema IO +petSchema = schema queryType Nothing (Just subscriptionType) mempty queryType :: ObjectType IO -queryType = ObjectType "Query" Nothing [] - $ HashMap.singleton "dog" dogResolver +queryType = ObjectType "Query" Nothing [] $ HashMap.fromList + [ ("dog", dogResolver) + , ("findDog", findDogResolver) + ] where dogField = Field Nothing (Out.NamedObjectType dogType) mempty dogResolver = ValueResolver dogField $ pure Null + findDogArguments = HashMap.singleton "complex" + $ In.Argument Nothing (In.NonNullInputObjectType dogDataType) Nothing + findDogField = Field Nothing (Out.NamedObjectType dogType) findDogArguments + findDogResolver = ValueResolver findDogField $ pure Null dogCommandType :: EnumType dogCommandType = EnumType "DogCommand" Nothing $ HashMap.fromList @@ -72,6 +73,12 @@ dogType = ObjectType "Dog" Nothing [petType] $ HashMap.fromList ownerField = Field Nothing (Out.NamedObjectType humanType) mempty ownerResolver = ValueResolver ownerField $ pure Null +dogDataType :: InputObjectType +dogDataType = InputObjectType "DogData" Nothing + $ HashMap.singleton "name" nameInputField + where + nameInputField = InputField Nothing (In.NonNullScalarType string) Nothing + sentientType :: InterfaceType IO sentientType = InterfaceType "Sentient" Nothing [] $ HashMap.singleton "name" @@ -81,19 +88,28 @@ petType :: InterfaceType IO petType = InterfaceType "Pet" Nothing [] $ HashMap.singleton "name" $ Field Nothing (Out.NonNullScalarType string) mempty -{- -alienType :: ObjectType IO -alienType = ObjectType "Alien" Nothing [sentientType] $ HashMap.fromList - [ ("name", nameResolver) - , ("homePlanet", homePlanetResolver) + +subscriptionType :: ObjectType IO +subscriptionType = ObjectType "Subscription" Nothing [] $ HashMap.fromList + [ ("newMessage", newMessageResolver) + , ("disallowedSecondRootField", newMessageResolver) ] where - nameField = Field Nothing (Out.NonNullScalarType string) mempty - nameResolver = ValueResolver nameField $ pure "Name" - homePlanetField = - Field Nothing (Out.NamedScalarType string) mempty - homePlanetResolver = ValueResolver homePlanetField $ pure "Home planet" --} + newMessageField = Field Nothing (Out.NonNullObjectType messageType) mempty + newMessageResolver = ValueResolver newMessageField + $ pure $ Object HashMap.empty + +messageType :: ObjectType IO +messageType = ObjectType "Message" Nothing [] $ HashMap.fromList + [ ("sender", senderResolver) + , ("body", bodyResolver) + ] + where + senderField = Field Nothing (Out.NonNullScalarType string) mempty + senderResolver = ValueResolver senderField $ pure "Sender" + bodyField = Field Nothing (Out.NonNullScalarType string) mempty + bodyResolver = ValueResolver bodyField $ pure "Message body." + humanType :: ObjectType IO humanType = ObjectType "Human" Nothing [sentientType] $ HashMap.fromList [ ("name", nameResolver) @@ -106,45 +122,14 @@ humanType = ObjectType "Human" Nothing [sentientType] $ HashMap.fromList Field Nothing (Out.ListType $ Out.NonNullInterfaceType petType) mempty petsResolver = ValueResolver petsField $ pure $ List [] {- -catCommandType :: EnumType -catCommandType = EnumType "CatCommand" Nothing $ HashMap.fromList - [ ("JUMP", EnumValue Nothing) - ] - -catType :: ObjectType IO -catType = ObjectType "Cat" Nothing [petType] $ HashMap.fromList - [ ("name", nameResolver) - , ("nickname", nicknameResolver) - , ("doesKnowCommand", doesKnowCommandResolver) - , ("meowVolume", meowVolumeResolver) - ] - where - nameField = Field Nothing (Out.NonNullScalarType string) mempty - nameResolver = ValueResolver nameField $ pure "Name" - nicknameField = Field Nothing (Out.NamedScalarType string) mempty - nicknameResolver = ValueResolver nicknameField $ pure "Nickname" - doesKnowCommandField = Field Nothing (Out.NonNullScalarType boolean) - $ HashMap.singleton "catCommand" - $ In.Argument Nothing (In.NonNullEnumType catCommandType) Nothing - doesKnowCommandResolver = ValueResolver doesKnowCommandField - $ pure $ Boolean True - meowVolumeField = Field Nothing (Out.NamedScalarType int) mempty - meowVolumeResolver = ValueResolver meowVolumeField $ pure $ Int 2 - catOrDogType :: UnionType IO catOrDogType = UnionType "CatOrDog" Nothing [catType, dogType] - -dogOrHumanType :: UnionType IO -dogOrHumanType = UnionType "DogOrHuman" Nothing [dogType, humanType] - -humanOrAlienType :: UnionType IO -humanOrAlienType = UnionType "HumanOrAlien" Nothing [humanType, alienType] -} -validate :: Text -> Seq Error +validate :: Text -> [Error] validate queryString = case parse AST.document "" queryString of - Left _ -> Seq.empty - Right ast -> document schema specifiedRules ast + Left _ -> [] + Right ast -> toList $ document petSchema specifiedRules ast spec :: Spec spec = @@ -166,9 +151,8 @@ spec = { message = "Definition must be OperationDefinition or FragmentDefinition." , locations = [AST.Location 9 15] - , path = [] } - in validate queryString `shouldBe` Seq.singleton expected + in validate queryString `shouldContain` [expected] it "rejects multiple subscription root fields" $ let queryString = [r| @@ -182,11 +166,11 @@ spec = |] expected = Error { message = - "Subscription sub must select only one top level field." + "Subscription \"sub\" must select only one top level \ + \field." , locations = [AST.Location 2 15] - , path = [] } - in validate queryString `shouldBe` Seq.singleton expected + in validate queryString `shouldContain` [expected] it "rejects multiple subscription root fields coming from a fragment" $ let queryString = [r| @@ -204,11 +188,11 @@ spec = |] expected = Error { message = - "Subscription sub must select only one top level field." + "Subscription \"sub\" must select only one top level \ + \field." , locations = [AST.Location 2 15] - , path = [] } - in validate queryString `shouldBe` Seq.singleton expected + in validate queryString `shouldContain` [expected] it "rejects multiple anonymous operations" $ let queryString = [r| @@ -230,9 +214,8 @@ spec = { message = "This anonymous operation must be the only defined operation." , locations = [AST.Location 2 15] - , path = [] } - in validate queryString `shouldBe` Seq.singleton expected + in validate queryString `shouldBe` [expected] it "rejects operations with the same name" $ let queryString = [r| @@ -252,9 +235,8 @@ spec = { message = "There can be only one operation named \"dogOperation\"." , locations = [AST.Location 2 15, AST.Location 8 15] - , path = [] } - in validate queryString `shouldBe` Seq.singleton expected + in validate queryString `shouldBe` [expected] it "rejects fragments with the same name" $ let queryString = [r| @@ -278,6 +260,406 @@ spec = { message = "There can be only one fragment named \"fragmentOne\"." , locations = [AST.Location 8 15, AST.Location 12 15] - , path = [] } - in validate queryString `shouldBe` Seq.singleton expected + in validate queryString `shouldBe` [expected] + + it "rejects the fragment spread without a target" $ + let queryString = [r| + { + dog { + ...undefinedFragment + } + } + |] + expected = Error + { message = + "Fragment target \"undefinedFragment\" is undefined." + , locations = [AST.Location 4 19] + } + in validate queryString `shouldBe` [expected] + + it "rejects fragment spreads without an unknown target type" $ + let queryString = [r| + { + dog { + ...notOnExistingType + } + } + fragment notOnExistingType on NotInSchema { + name + } + |] + expected = Error + { message = + "Fragment \"notOnExistingType\" is specified on type \ + \\"NotInSchema\" which doesn't exist in the schema." + , locations = [AST.Location 4 19] + } + in validate queryString `shouldBe` [expected] + + it "rejects inline fragments without a target" $ + let queryString = [r| + { + ... on NotInSchema { + name + } + } + |] + expected = Error + { message = + "Inline fragment is specified on type \"NotInSchema\" \ + \which doesn't exist in the schema." + , locations = [AST.Location 3 17] + } + in validate queryString `shouldBe` [expected] + + it "rejects fragments on scalar types" $ + let queryString = [r| + { + dog { + ...fragOnScalar + } + } + fragment fragOnScalar on Int { + name + } + |] + expected = Error + { message = + "Fragment cannot condition on non composite type \ + \\"Int\"." + , locations = [AST.Location 7 15] + } + in validate queryString `shouldContain` [expected] + + it "rejects inline fragments on scalar types" $ + let queryString = [r| + { + ... on Boolean { + name + } + } + |] + expected = Error + { message = + "Fragment cannot condition on non composite type \ + \\"Boolean\"." + , locations = [AST.Location 3 17] + } + in validate queryString `shouldContain` [expected] + + it "rejects unused fragments" $ + let queryString = [r| + fragment nameFragment on Dog { # unused + name + } + + { + dog { + name + } + } + |] + expected = Error + { message = + "Fragment \"nameFragment\" is never used." + , locations = [AST.Location 2 15] + } + in validate queryString `shouldBe` [expected] + + it "rejects spreads that form cycles" $ + let queryString = [r| + { + dog { + ...nameFragment + } + } + fragment nameFragment on Dog { + name + ...barkVolumeFragment + } + fragment barkVolumeFragment on Dog { + barkVolume + ...nameFragment + } + |] + error1 = Error + { message = + "Cannot spread fragment \"barkVolumeFragment\" within \ + \itself (via barkVolumeFragment -> nameFragment -> \ + \barkVolumeFragment)." + , locations = [AST.Location 11 15] + } + error2 = Error + { message = + "Cannot spread fragment \"nameFragment\" within itself \ + \(via nameFragment -> barkVolumeFragment -> \ + \nameFragment)." + , locations = [AST.Location 7 15] + } + in validate queryString `shouldBe` [error1, error2] + + it "rejects duplicate field arguments" $ do + let queryString = [r| + { + dog { + isHousetrained(atOtherHomes: true, atOtherHomes: true) + } + } + |] + expected = Error + { message = + "There can be only one argument named \"atOtherHomes\"." + , locations = [AST.Location 4 34, AST.Location 4 54] + } + in validate queryString `shouldBe` [expected] + + it "rejects more than one directive per location" $ do + let queryString = [r| + query ($foo: Boolean = true, $bar: Boolean = false) { + dog @skip(if: $foo) @skip(if: $bar) { + name + } + } + |] + expected = Error + { message = + "There can be only one directive named \"skip\"." + , locations = [AST.Location 3 21, AST.Location 3 37] + } + in validate queryString `shouldBe` [expected] + + it "rejects duplicate variables" $ + let queryString = [r| + query houseTrainedQuery($atOtherHomes: Boolean, $atOtherHomes: Boolean) { + dog { + isHousetrained(atOtherHomes: $atOtherHomes) + } + } + |] + expected = Error + { message = + "There can be only one variable named \"atOtherHomes\"." + , locations = [AST.Location 2 39, AST.Location 2 63] + } + in validate queryString `shouldBe` [expected] + + it "rejects non-input types as variables" $ + let queryString = [r| + query takesDogBang($dog: Dog!) { + dog { + isHousetrained(atOtherHomes: $dog) + } + } + |] + expected = Error + { message = + "Variable \"$dog\" cannot be non-input type \"Dog\"." + , locations = [AST.Location 2 34] + } + in validate queryString `shouldBe` [expected] + + it "rejects undefined variables" $ + let queryString = [r| + query variableIsNotDefinedUsedInSingleFragment { + dog { + ...isHousetrainedFragment + } + } + + fragment isHousetrainedFragment on Dog { + isHousetrained(atOtherHomes: $atOtherHomes) + } + |] + expected = Error + { message = + "Variable \"$atOtherHomes\" is not defined by \ + \operation \ + \\"variableIsNotDefinedUsedInSingleFragment\"." + , locations = [AST.Location 9 46] + } + in validate queryString `shouldBe` [expected] + + it "rejects unused variables" $ + let queryString = [r| + query variableUnused($atOtherHomes: Boolean) { + dog { + isHousetrained + } + } + |] + expected = Error + { message = + "Variable \"$atOtherHomes\" is never used in operation \ + \\"variableUnused\"." + , locations = [AST.Location 2 36] + } + in validate queryString `shouldBe` [expected] + + it "rejects duplicate fields in input objects" $ + let queryString = [r| + { + findDog(complex: { name: "Fido", name: "Jack" }) { + name + } + } + |] + expected = Error + { message = + "There can be only one input field named \"name\"." + , locations = [AST.Location 3 36, AST.Location 3 50] + } + in validate queryString `shouldBe` [expected] + + it "rejects undefined fields" $ + let queryString = [r| + { + dog { + meowVolume + } + } + |] + expected = Error + { message = + "Cannot query field \"meowVolume\" on type \"Dog\"." + , locations = [AST.Location 4 19] + } + in validate queryString `shouldBe` [expected] + + it "rejects scalar fields with not empty selection set" $ + let queryString = [r| + { + dog { + barkVolume { + sinceWhen + } + } + } + |] + expected = Error + { message = + "Field \"barkVolume\" must not have a selection since \ + \type \"Int\" has no subfields." + , locations = [AST.Location 4 19] + } + in validate queryString `shouldBe` [expected] + + it "rejects field arguments missing in the type" $ + let queryString = [r| + { + dog { + doesKnowCommand(command: CLEAN_UP_HOUSE, dogCommand: SIT) + } + } + |] + expected = Error + { message = + "Unknown argument \"command\" on field \ + \\"Dog.doesKnowCommand\"." + , locations = [AST.Location 4 35] + } + in validate queryString `shouldBe` [expected] + + it "rejects directive arguments missing in the definition" $ + let queryString = [r| + { + dog { + isHousetrained(atOtherHomes: true) @include(unless: false, if: true) + } + } + |] + expected = Error + { message = + "Unknown argument \"unless\" on directive \"@include\"." + , locations = [AST.Location 4 63] + } + in validate queryString `shouldBe` [expected] + + it "rejects undefined directives" $ + let queryString = [r| + { + dog { + isHousetrained(atOtherHomes: true) @ignore(if: true) + } + } + |] + expected = Error + { message = "Unknown directive \"@ignore\"." + , locations = [AST.Location 4 54] + } + in validate queryString `shouldBe` [expected] + + it "rejects undefined input object fields" $ + let queryString = [r| + { + findDog(complex: { favoriteCookieFlavor: "Bacon", name: "Jack" }) { + name + } + } + |] + expected = Error + { message = + "Field \"favoriteCookieFlavor\" is not defined \ + \by type \"DogData\"." + , locations = [AST.Location 3 36] + } + in validate queryString `shouldBe` [expected] + + it "rejects directives in invalid locations" $ + let queryString = [r| + query @skip(if: $foo) { + dog { + name + } + } + |] + expected = Error + { message = "Directive \"@skip\" may not be used on QUERY." + , locations = [AST.Location 2 21] + } + in validate queryString `shouldBe` [expected] + + it "rejects missing required input fields" $ + let queryString = [r| + { + findDog(complex: { name: null }) { + name + } + } + |] + expected = Error + { message = + "Input field \"name\" of type \"DogData\" is required, \ + \but it was not provided." + , locations = [AST.Location 3 34] + } + in validate queryString `shouldBe` [expected] + + it "finds corresponding subscription fragment" $ + let queryString = [r| + subscription sub { + ...anotherSubscription + ...multipleSubscriptions + } + fragment multipleSubscriptions on Subscription { + newMessage { + body + } + disallowedSecondRootField { + sender + } + } + fragment anotherSubscription on Subscription { + newMessage { + body + sender + } + } + |] + expected = Error + { message = + "Subscription \"sub\" must select only one top level \ + \field." + , locations = [AST.Location 2 15] + } + in validate queryString `shouldBe` [expected] diff --git a/tests/Test/DirectiveSpec.hs b/tests/Test/DirectiveSpec.hs index e6b6cea..2d586f6 100644 --- a/tests/Test/DirectiveSpec.hs +++ b/tests/Test/DirectiveSpec.hs @@ -19,8 +19,7 @@ import Test.Hspec.GraphQL import Text.RawString.QQ (r) experimentalResolver :: Schema IO -experimentalResolver = Schema - { query = queryType, mutation = Nothing, subscription = Nothing } +experimentalResolver = schema queryType Nothing Nothing mempty where queryType = Out.ObjectType "Query" Nothing [] $ HashMap.singleton "experimentalField" @@ -72,7 +71,7 @@ spec = ...experimentalFragment @skip(if: true) } - fragment experimentalFragment on ExperimentalType { + fragment experimentalFragment on Query { experimentalField } |] @@ -83,7 +82,7 @@ spec = it "should be able to @skip an inline fragment" $ do let sourceQuery = [r| { - ... on ExperimentalType @skip(if: true) { + ... on Query @skip(if: true) { experimentalField } } diff --git a/tests/Test/FragmentSpec.hs b/tests/Test/FragmentSpec.hs index 089b721..f426e2c 100644 --- a/tests/Test/FragmentSpec.hs +++ b/tests/Test/FragmentSpec.hs @@ -46,18 +46,15 @@ inlineQuery = [r|{ }|] shirtType :: Out.ObjectType IO -shirtType = Out.ObjectType "Shirt" Nothing [] - $ HashMap.fromList - [ ("size", sizeFieldType) - , ("circumference", circumferenceFieldType) - ] +shirtType = Out.ObjectType "Shirt" Nothing [] $ HashMap.fromList + [ ("size", sizeFieldType) + ] hatType :: Out.ObjectType IO -hatType = Out.ObjectType "Hat" Nothing [] - $ HashMap.fromList - [ ("size", sizeFieldType) - , ("circumference", circumferenceFieldType) - ] +hatType = Out.ObjectType "Hat" Nothing [] $ HashMap.fromList + [ ("size", sizeFieldType) + , ("circumference", circumferenceFieldType) + ] circumferenceFieldType :: Out.Resolver IO circumferenceFieldType @@ -70,12 +67,11 @@ sizeFieldType $ pure $ snd size toSchema :: Text -> (Text, Value) -> Schema IO -toSchema t (_, resolve) = Schema - { query = queryType, mutation = Nothing, subscription = Nothing } +toSchema t (_, resolve) = schema queryType Nothing Nothing mempty where - unionMember = if t == "Hat" then hatType else shirtType + garmentType = Out.UnionType "Garment" Nothing [hatType, shirtType] typeNameField = Out.Field Nothing (Out.NamedScalarType string) mempty - garmentField = Out.Field Nothing (Out.NamedObjectType unionMember) mempty + garmentField = Out.Field Nothing (Out.NamedUnionType garmentType) mempty queryType = case t of "circumference" -> hatType @@ -111,22 +107,16 @@ spec = do it "embeds inline fragments without type" $ do let sourceQuery = [r|{ - garment { - circumference - ... { - size - } + circumference + ... { + size } }|] - resolvers = ("garment", Object $ HashMap.fromList [circumference, size]) - - actual <- graphql (toSchema "garment" resolvers) sourceQuery + actual <- graphql (toSchema "circumference" circumference) sourceQuery let expected = HashMap.singleton "data" $ Aeson.object - [ "garment" .= Aeson.object - [ "circumference" .= (60 :: Int) - , "size" .= ("L" :: Text) - ] + [ "circumference" .= (60 :: Int) + , "size" .= ("L" :: Text) ] in actual `shouldResolveTo` expected @@ -183,21 +173,6 @@ spec = do ] in actual `shouldResolveTo` expected - it "rejects recursive fragments" $ do - let expected = HashMap.singleton "data" $ Aeson.object [] - sourceQuery = [r| - { - ...circumferenceFragment - } - - fragment circumferenceFragment on Hat { - ...circumferenceFragment - } - |] - - actual <- graphql (toSchema "circumference" circumference) sourceQuery - actual `shouldResolveTo` expected - it "considers type condition" $ do let sourceQuery = [r| { diff --git a/tests/Test/RootOperationSpec.hs b/tests/Test/RootOperationSpec.hs index ea89279..1921ec9 100644 --- a/tests/Test/RootOperationSpec.hs +++ b/tests/Test/RootOperationSpec.hs @@ -23,13 +23,11 @@ hatType = Out.ObjectType "Hat" Nothing [] $ ValueResolver (Out.Field Nothing (Out.NamedScalarType int) mempty) $ pure $ Int 60 -schema :: Schema IO -schema = Schema - { query = Out.ObjectType "Query" Nothing [] hatFieldResolver - , mutation = Just $ Out.ObjectType "Mutation" Nothing [] incrementFieldResolver - , subscription = Nothing - } +garmentSchema :: Schema IO +garmentSchema = schema queryType (Just mutationType) Nothing mempty where + queryType = Out.ObjectType "Query" Nothing [] hatFieldResolver + mutationType = Out.ObjectType "Mutation" Nothing [] incrementFieldResolver garment = pure $ Object $ HashMap.fromList [ ("circumference", Int 60) ] @@ -57,7 +55,7 @@ spec = [ "circumference" .= (60 :: Int) ] ] - actual <- graphql schema querySource + actual <- graphql garmentSchema querySource actual `shouldResolveTo` expected it "chooses Mutation" $ do @@ -70,5 +68,5 @@ spec = $ object [ "incrementCircumference" .= (61 :: Int) ] - actual <- graphql schema querySource + actual <- graphql garmentSchema querySource actual `shouldResolveTo` expected diff --git a/tests/Test/StarWars/Data.hs b/tests/Test/StarWars/Data.hs deleted file mode 100644 index e3dd696..0000000 --- a/tests/Test/StarWars/Data.hs +++ /dev/null @@ -1,204 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -module Test.StarWars.Data - ( Character - , StarWarsException(..) - , appearsIn - , artoo - , getDroid - , getDroid' - , getEpisode - , getFriends - , getHero - , getHuman - , id_ - , homePlanet - , name_ - , secretBackstory - , typeName - ) where - -import Control.Monad.Catch (Exception(..), MonadThrow(..), SomeException) -import Control.Applicative (Alternative(..), liftA2) -import Data.Maybe (catMaybes) -import Data.Text (Text) -import Data.Typeable (cast) -import Language.GraphQL.Error -import Language.GraphQL.Type - --- * Data --- See https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsData.js - --- ** Characters - -type ID = Text - -data CharCommon = CharCommon - { _id_ :: ID - , _name :: Text - , _friends :: [ID] - , _appearsIn :: [Int] - } deriving (Show) - - -data Human = Human - { _humanChar :: CharCommon - , homePlanet :: Text - } - -data Droid = Droid - { _droidChar :: CharCommon - , primaryFunction :: Text - } - -type Character = Either Droid Human - -id_ :: Character -> ID -id_ (Left x) = _id_ . _droidChar $ x -id_ (Right x) = _id_ . _humanChar $ x - -name_ :: Character -> Text -name_ (Left x) = _name . _droidChar $ x -name_ (Right x) = _name . _humanChar $ x - -friends :: Character -> [ID] -friends (Left x) = _friends . _droidChar $ x -friends (Right x) = _friends . _humanChar $ x - -appearsIn :: Character -> [Int] -appearsIn (Left x) = _appearsIn . _droidChar $ x -appearsIn (Right x) = _appearsIn . _humanChar $ x - -data StarWarsException = SecretBackstory | InvalidArguments - -instance Show StarWarsException where - show SecretBackstory = "secretBackstory is secret." - show InvalidArguments = "Invalid arguments." - -instance Exception StarWarsException where - toException = toException . ResolverException - fromException e = do - ResolverException resolverException <- fromException e - cast resolverException - -secretBackstory :: Resolve (Either SomeException) -secretBackstory = throwM SecretBackstory - -typeName :: Character -> Text -typeName = either (const "Droid") (const "Human") - -luke :: Character -luke = Right luke' - -luke' :: Human -luke' = Human - { _humanChar = CharCommon - { _id_ = "1000" - , _name = "Luke Skywalker" - , _friends = ["1002","1003","2000","2001"] - , _appearsIn = [4,5,6] - } - , homePlanet = "Tatooine" - } - -vader :: Human -vader = Human - { _humanChar = CharCommon - { _id_ = "1001" - , _name = "Darth Vader" - , _friends = ["1004"] - , _appearsIn = [4,5,6] - } - , homePlanet = "Tatooine" - } - -han :: Human -han = Human - { _humanChar = CharCommon - { _id_ = "1002" - , _name = "Han Solo" - , _friends = ["1000","1003","2001" ] - , _appearsIn = [4,5,6] - } - , homePlanet = mempty - } - -leia :: Human -leia = Human - { _humanChar = CharCommon - { _id_ = "1003" - , _name = "Leia Organa" - , _friends = ["1000","1002","2000","2001"] - , _appearsIn = [4,5,6] - } - , homePlanet = "Alderaan" - } - -tarkin :: Human -tarkin = Human - { _humanChar = CharCommon - { _id_ = "1004" - , _name = "Wilhuff Tarkin" - , _friends = ["1001"] - , _appearsIn = [4] - } - , homePlanet = mempty - } - -threepio :: Droid -threepio = Droid - { _droidChar = CharCommon - { _id_ = "2000" - , _name = "C-3PO" - , _friends = ["1000","1002","1003","2001" ] - , _appearsIn = [ 4, 5, 6 ] - } - , primaryFunction = "Protocol" - } - -artoo :: Character -artoo = Left artoo' - -artoo' :: Droid -artoo' = Droid - { _droidChar = CharCommon - { _id_ = "2001" - , _name = "R2-D2" - , _friends = ["1000","1002","1003"] - , _appearsIn = [4,5,6] - } - , primaryFunction = "Astrometch" - } - --- ** Helper functions - -getHero :: Int -> Character -getHero 5 = luke -getHero _ = artoo - -getHuman :: ID -> Maybe Character -getHuman = fmap Right . getHuman' - -getHuman' :: ID -> Maybe Human -getHuman' "1000" = pure luke' -getHuman' "1001" = pure vader -getHuman' "1002" = pure han -getHuman' "1003" = pure leia -getHuman' "1004" = pure tarkin -getHuman' _ = empty - -getDroid :: ID -> Maybe Character -getDroid = fmap Left . getDroid' - -getDroid' :: ID -> Maybe Droid -getDroid' "2000" = pure threepio -getDroid' "2001" = pure artoo' -getDroid' _ = empty - -getFriends :: Character -> [Character] -getFriends char = catMaybes $ liftA2 (<|>) getDroid getHuman <$> friends char - -getEpisode :: Int -> Maybe Text -getEpisode 4 = pure "NEW_HOPE" -getEpisode 5 = pure "EMPIRE" -getEpisode 6 = pure "JEDI" -getEpisode _ = empty diff --git a/tests/Test/StarWars/QuerySpec.hs b/tests/Test/StarWars/QuerySpec.hs deleted file mode 100644 index 95b18d3..0000000 --- a/tests/Test/StarWars/QuerySpec.hs +++ /dev/null @@ -1,366 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE QuasiQuotes #-} -module Test.StarWars.QuerySpec - ( spec - ) where - -import qualified Data.Aeson as Aeson -import Data.Aeson ((.=)) -import qualified Data.HashMap.Strict as HashMap -import Data.Text (Text) -import Language.GraphQL -import Text.RawString.QQ (r) -import Test.Hspec.Expectations (Expectation, shouldBe) -import Test.Hspec (Spec, describe, it) -import Test.StarWars.Schema - --- * Test --- See https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsQueryTests.js - -spec :: Spec -spec = describe "Star Wars Query Tests" $ do - describe "Basic Queries" $ do - it "R2-D2 hero" $ testQuery - [r| query HeroNameQuery { - hero { - id - } - } - |] - $ Aeson.object - [ "data" .= Aeson.object - [ "hero" .= Aeson.object ["id" .= ("2001" :: Text)] - ] - ] - it "R2-D2 ID and friends" $ testQuery - [r| query HeroNameAndFriendsQuery { - hero { - id - name - friends { - name - } - } - } - |] - $ Aeson.object [ "data" .= Aeson.object [ - "hero" .= Aeson.object - [ "id" .= ("2001" :: Text) - , r2d2Name - , "friends" .= - [ Aeson.object [lukeName] - , Aeson.object [hanName] - , Aeson.object [leiaName] - ] - ] - ]] - - describe "Nested Queries" $ do - it "R2-D2 friends" $ testQuery - [r| query NestedQuery { - hero { - name - friends { - name - appearsIn - friends { - name - } - } - } - } - |] - $ Aeson.object [ "data" .= Aeson.object [ - "hero" .= Aeson.object [ - "name" .= ("R2-D2" :: Text) - , "friends" .= [ - Aeson.object [ - "name" .= ("Luke Skywalker" :: Text) - , "appearsIn" .= ["NEW_HOPE", "EMPIRE", "JEDI" :: Text] - , "friends" .= [ - Aeson.object [hanName] - , Aeson.object [leiaName] - , Aeson.object [c3poName] - , Aeson.object [r2d2Name] - ] - ] - , Aeson.object [ - hanName - , "appearsIn" .= ["NEW_HOPE", "EMPIRE", "JEDI" :: Text] - , "friends" .= - [ Aeson.object [lukeName] - , Aeson.object [leiaName] - , Aeson.object [r2d2Name] - ] - ] - , Aeson.object [ - leiaName - , "appearsIn" .= ["NEW_HOPE", "EMPIRE", "JEDI" :: Text] - , "friends" .= - [ Aeson.object [lukeName] - , Aeson.object [hanName] - , Aeson.object [c3poName] - , Aeson.object [r2d2Name] - ] - ] - ] - ] - ]] - it "Luke ID" $ testQuery - [r| query FetchLukeQuery { - human(id: "1000") { - name - } - } - |] - $ Aeson.object [ "data" .= Aeson.object - [ "human" .= Aeson.object [lukeName] - ]] - - it "Luke ID with variable" $ testQueryParams - (HashMap.singleton "someId" "1000") - [r| query FetchSomeIDQuery($someId: String!) { - human(id: $someId) { - name - } - } - |] - $ Aeson.object [ "data" .= Aeson.object [ - "human" .= Aeson.object [lukeName] - ]] - it "Han ID with variable" $ testQueryParams - (HashMap.singleton "someId" "1002") - [r| query FetchSomeIDQuery($someId: String!) { - human(id: $someId) { - name - } - } - |] - $ Aeson.object [ "data" .= Aeson.object [ - "human" .= Aeson.object [hanName] - ]] - it "Invalid ID" $ testQueryParams - (HashMap.singleton "id" "Not a valid ID") - [r| query humanQuery($id: String!) { - human(id: $id) { - name - } - } - |] $ Aeson.object ["data" .= Aeson.object ["human" .= Aeson.Null]] - it "Luke aliased" $ testQuery - [r| query FetchLukeAliased { - luke: human(id: "1000") { - name - } - } - |] - $ Aeson.object [ "data" .= Aeson.object [ - "luke" .= Aeson.object [lukeName] - ]] - it "R2-D2 ID and friends aliased" $ testQuery - [r| query HeroNameAndFriendsQuery { - hero { - id - name - friends { - friendName: name - } - } - } - |] - $ Aeson.object [ "data" .= Aeson.object [ - "hero" .= Aeson.object [ - "id" .= ("2001" :: Text) - , r2d2Name - , "friends" .= - [ Aeson.object ["friendName" .= ("Luke Skywalker" :: Text)] - , Aeson.object ["friendName" .= ("Han Solo" :: Text)] - , Aeson.object ["friendName" .= ("Leia Organa" :: Text)] - ] - ] - ]] - it "Luke and Leia aliased" $ testQuery - [r| query FetchLukeAndLeiaAliased { - luke: human(id: "1000") { - name - } - leia: human(id: "1003") { - name - } - } - |] - $ Aeson.object [ "data" .= Aeson.object - [ "luke" .= Aeson.object [lukeName] - , "leia" .= Aeson.object [leiaName] - ]] - - describe "Fragments for complex queries" $ do - it "Aliases to query for duplicate content" $ testQuery - [r| query DuplicateFields { - luke: human(id: "1000") { - name - homePlanet - } - leia: human(id: "1003") { - name - homePlanet - } - } - |] - $ Aeson.object [ "data" .= Aeson.object [ - "luke" .= Aeson.object [lukeName, tatooine] - , "leia" .= Aeson.object [leiaName, alderaan] - ]] - it "Fragment for duplicate content" $ testQuery - [r| query UseFragment { - luke: human(id: "1000") { - ...HumanFragment - } - leia: human(id: "1003") { - ...HumanFragment - } - } - fragment HumanFragment on Human { - name - homePlanet - } - |] - $ Aeson.object [ "data" .= Aeson.object [ - "luke" .= Aeson.object [lukeName, tatooine] - , "leia" .= Aeson.object [leiaName, alderaan] - ]] - - describe "__typename" $ do - it "R2D2 is a Droid" $ testQuery - [r| query CheckTypeOfR2 { - hero { - __typename - name - } - } - |] - $ Aeson.object ["data" .= Aeson.object [ - "hero" .= Aeson.object - [ "__typename" .= ("Droid" :: Text) - , r2d2Name - ] - ]] - it "Luke is a human" $ testQuery - [r| query CheckTypeOfLuke { - hero(episode: EMPIRE) { - __typename - name - } - } - |] - $ Aeson.object ["data" .= Aeson.object [ - "hero" .= Aeson.object - [ "__typename" .= ("Human" :: Text) - , lukeName - ] - ]] - - describe "Errors in resolvers" $ do - it "error on secretBackstory" $ testQuery - [r| - query HeroNameQuery { - hero { - name - secretBackstory - } - } - |] - $ Aeson.object - [ "data" .= Aeson.object - [ "hero" .= Aeson.object - [ "name" .= ("R2-D2" :: Text) - , "secretBackstory" .= Aeson.Null - ] - ] - , "errors" .= - [ Aeson.object - ["message" .= ("secretBackstory is secret." :: Text)] - ] - ] - it "Error in a list" $ testQuery - [r| query HeroNameQuery { - hero { - name - friends { - name - secretBackstory - } - } - } - |] - $ Aeson.object ["data" .= Aeson.object - [ "hero" .= Aeson.object - [ "name" .= ("R2-D2" :: Text) - , "friends" .= - [ Aeson.object - [ "name" .= ("Luke Skywalker" :: Text) - , "secretBackstory" .= Aeson.Null - ] - , Aeson.object - [ "name" .= ("Han Solo" :: Text) - , "secretBackstory" .= Aeson.Null - ] - , Aeson.object - [ "name" .= ("Leia Organa" :: Text) - , "secretBackstory" .= Aeson.Null - ] - ] - ] - ] - , "errors" .= - [ Aeson.object - [ "message" .= ("secretBackstory is secret." :: Text) - ] - , Aeson.object - [ "message" .= ("secretBackstory is secret." :: Text) - ] - , Aeson.object - [ "message" .= ("secretBackstory is secret." :: Text) - ] - ] - ] - it "error on secretBackstory with alias" $ testQuery - [r| query HeroNameQuery { - mainHero: hero { - name - story: secretBackstory - } - } - |] - $ Aeson.object - [ "data" .= Aeson.object - [ "mainHero" .= Aeson.object - [ "name" .= ("R2-D2" :: Text) - , "story" .= Aeson.Null - ] - ] - , "errors" .= - [ Aeson.object - [ "message" .= ("secretBackstory is secret." :: Text) - ] - ] - ] - - where - lukeName = "name" .= ("Luke Skywalker" :: Text) - leiaName = "name" .= ("Leia Organa" :: Text) - hanName = "name" .= ("Han Solo" :: Text) - r2d2Name = "name" .= ("R2-D2" :: Text) - c3poName = "name" .= ("C-3PO" :: Text) - tatooine = "homePlanet" .= ("Tatooine" :: Text) - alderaan = "homePlanet" .= ("Alderaan" :: Text) - -testQuery :: Text -> Aeson.Value -> Expectation -testQuery q expected = - let Right (Right actual) = graphql schema q - in Aeson.Object actual `shouldBe` expected - -testQueryParams :: Aeson.Object -> Text -> Aeson.Value -> Expectation -testQueryParams f q expected = - let Right (Right actual) = graphqlSubs schema Nothing f q - in Aeson.Object actual `shouldBe` expected diff --git a/tests/Test/StarWars/Schema.hs b/tests/Test/StarWars/Schema.hs deleted file mode 100644 index cecd8eb..0000000 --- a/tests/Test/StarWars/Schema.hs +++ /dev/null @@ -1,154 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -module Test.StarWars.Schema - ( schema - ) where - -import Control.Monad.Catch (MonadThrow(..), SomeException) -import Control.Monad.Trans.Reader (asks) -import qualified Data.HashMap.Strict as HashMap -import Data.Maybe (catMaybes) -import Data.Text (Text) -import Language.GraphQL.Type -import qualified Language.GraphQL.Type.In as In -import qualified Language.GraphQL.Type.Out as Out -import Test.StarWars.Data -import Prelude hiding (id) - --- See https://github.com/graphql/graphql-js/blob/master/src/__tests__/starWarsSchema.js - -schema :: Schema (Either SomeException) -schema = Schema - { query = queryType - , mutation = Nothing - , subscription = Nothing - } - where - queryType = Out.ObjectType "Query" Nothing [] $ HashMap.fromList - [ ("hero", heroFieldResolver) - , ("human", humanFieldResolver) - , ("droid", droidFieldResolver) - ] - heroField = Out.Field Nothing (Out.NamedObjectType heroObject) - $ HashMap.singleton "episode" - $ In.Argument Nothing (In.NamedEnumType episodeEnum) Nothing - heroFieldResolver = ValueResolver heroField hero - humanField = Out.Field Nothing (Out.NamedObjectType heroObject) - $ HashMap.singleton "id" - $ In.Argument Nothing (In.NonNullScalarType string) Nothing - humanFieldResolver = ValueResolver humanField human - droidField = Out.Field Nothing (Out.NamedObjectType droidObject) mempty - droidFieldResolver = ValueResolver droidField droid - -heroObject :: Out.ObjectType (Either SomeException) -heroObject = Out.ObjectType "Human" Nothing [] $ HashMap.fromList - [ ("id", idFieldType) - , ("name", nameFieldType) - , ("friends", friendsFieldType) - , ("appearsIn", appearsInField) - , ("homePlanet", homePlanetFieldType) - , ("secretBackstory", secretBackstoryFieldType) - , ("__typename", typenameFieldType) - ] - where - homePlanetFieldType - = ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty) - $ idField "homePlanet" - -droidObject :: Out.ObjectType (Either SomeException) -droidObject = Out.ObjectType "Droid" Nothing [] $ HashMap.fromList - [ ("id", idFieldType) - , ("name", nameFieldType) - , ("friends", friendsFieldType) - , ("appearsIn", appearsInField) - , ("primaryFunction", primaryFunctionFieldType) - , ("secretBackstory", secretBackstoryFieldType) - , ("__typename", typenameFieldType) - ] - where - primaryFunctionFieldType - = ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty) - $ idField "primaryFunction" - -typenameFieldType :: Resolver (Either SomeException) -typenameFieldType - = ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty) - $ idField "__typename" - -idFieldType :: Resolver (Either SomeException) -idFieldType - = ValueResolver (Out.Field Nothing (Out.NamedScalarType id) mempty) - $ idField "id" - -nameFieldType :: Resolver (Either SomeException) -nameFieldType - = ValueResolver (Out.Field Nothing (Out.NamedScalarType string) mempty) - $ idField "name" - -friendsFieldType :: Resolver (Either SomeException) -friendsFieldType - = ValueResolver (Out.Field Nothing fieldType mempty) - $ idField "friends" - where - fieldType = Out.ListType $ Out.NamedObjectType droidObject - -appearsInField :: Resolver (Either SomeException) -appearsInField - = ValueResolver (Out.Field (Just description) fieldType mempty) - $ idField "appearsIn" - where - fieldType = Out.ListType $ Out.NamedEnumType episodeEnum - description = "Which movies they appear in." - -secretBackstoryFieldType :: Resolver (Either SomeException) -secretBackstoryFieldType = ValueResolver field secretBackstory - where - field = Out.Field Nothing (Out.NamedScalarType string) mempty - -idField :: Text -> Resolve (Either SomeException) -idField f = do - v <- asks values - let (Object v') = v - pure $ v' HashMap.! f - -episodeEnum :: EnumType -episodeEnum = EnumType "Episode" (Just description) - $ HashMap.fromList [newHope, empire, jedi] - where - description = "One of the films in the Star Wars Trilogy" - newHope = ("NEW_HOPE", EnumValue $ Just "Released in 1977.") - empire = ("EMPIRE", EnumValue $ Just "Released in 1980.") - jedi = ("JEDI", EnumValue $ Just "Released in 1983.") - -hero :: Resolve (Either SomeException) -hero = do - episode <- argument "episode" - pure $ character $ case episode of - Enum "NEW_HOPE" -> getHero 4 - Enum "EMPIRE" -> getHero 5 - Enum "JEDI" -> getHero 6 - _ -> artoo - -human :: Resolve (Either SomeException) -human = do - id' <- argument "id" - case id' of - String i -> pure $ maybe Null character $ getHuman i >>= Just - _ -> throwM InvalidArguments - -droid :: Resolve (Either SomeException) -droid = do - id' <- argument "id" - case id' of - String i -> pure $ maybe Null character $ getDroid i >>= Just - _ -> throwM InvalidArguments - -character :: Character -> Value -character char = Object $ HashMap.fromList - [ ("id", String $ id_ char) - , ("name", String $ name_ char) - , ("friends", List $ character <$> getFriends char) - , ("appearsIn", List $ Enum <$> catMaybes (getEpisode <$> appearsIn char)) - , ("homePlanet", String $ either mempty homePlanet char) - , ("__typename", String $ typeName char) - ] |
