2020-07-15 19:15:31 +02:00
|
|
|
{- This Source Code Form is subject to the terms of the Mozilla Public License,
|
|
|
|
v. 2.0. If a copy of the MPL was not distributed with this file, You can
|
|
|
|
obtain one at https://mozilla.org/MPL/2.0/. -}
|
|
|
|
|
2020-06-10 11:42:00 +02:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2020-09-11 08:03:49 +02:00
|
|
|
{-# LANGUAGE QuasiQuotes #-}
|
2020-06-10 11:42:00 +02:00
|
|
|
module Language.GraphQL.ExecuteSpec
|
|
|
|
( spec
|
|
|
|
) where
|
|
|
|
|
2020-07-17 07:05:03 +02:00
|
|
|
import Control.Exception (SomeException)
|
2020-06-10 11:42:00 +02:00
|
|
|
import Data.Aeson ((.=))
|
|
|
|
import qualified Data.Aeson as Aeson
|
2020-09-11 08:03:49 +02:00
|
|
|
import Data.Aeson.Types (emptyObject)
|
2020-07-15 19:15:31 +02:00
|
|
|
import Data.Conduit
|
2020-06-10 11:42:00 +02:00
|
|
|
import Data.HashMap.Strict (HashMap)
|
|
|
|
import qualified Data.HashMap.Strict as HashMap
|
2021-05-12 06:51:59 +02:00
|
|
|
import Language.GraphQL.AST (Document, Location(..), Name)
|
2020-06-10 11:42:00 +02:00
|
|
|
import Language.GraphQL.AST.Parser (document)
|
|
|
|
import Language.GraphQL.Error
|
|
|
|
import Language.GraphQL.Execute
|
2020-06-13 07:20:19 +02:00
|
|
|
import Language.GraphQL.Type as Type
|
2020-06-10 11:42:00 +02:00
|
|
|
import Language.GraphQL.Type.Out as Out
|
2020-07-15 19:15:31 +02:00
|
|
|
import Test.Hspec (Spec, context, describe, it, shouldBe)
|
2020-06-10 11:42:00 +02:00
|
|
|
import Text.Megaparsec (parse)
|
2020-09-11 08:03:49 +02:00
|
|
|
import Text.RawString.QQ (r)
|
2020-06-10 11:42:00 +02:00
|
|
|
|
2020-09-28 07:06:15 +02:00
|
|
|
philosopherSchema :: Schema (Either SomeException)
|
2020-10-07 05:24:51 +02:00
|
|
|
philosopherSchema = schema queryType Nothing (Just subscriptionType) mempty
|
2020-06-10 11:42:00 +02:00
|
|
|
|
2020-07-17 07:05:03 +02:00
|
|
|
queryType :: Out.ObjectType (Either SomeException)
|
2020-06-10 11:42:00 +02:00
|
|
|
queryType = Out.ObjectType "Query" Nothing []
|
2021-05-09 12:34:39 +02:00
|
|
|
$ HashMap.singleton "philosopher"
|
2020-07-14 19:37:56 +02:00
|
|
|
$ ValueResolver philosopherField
|
|
|
|
$ pure $ Type.Object mempty
|
2020-06-10 11:42:00 +02:00
|
|
|
where
|
2020-07-14 19:37:56 +02:00
|
|
|
philosopherField =
|
|
|
|
Out.Field Nothing (Out.NonNullObjectType philosopherType) HashMap.empty
|
2020-06-12 07:58:08 +02:00
|
|
|
|
2021-05-12 06:51:59 +02:00
|
|
|
musicType :: Out.ObjectType (Either SomeException)
|
|
|
|
musicType = Out.ObjectType "Music" Nothing []
|
|
|
|
$ HashMap.fromList resolvers
|
|
|
|
where
|
|
|
|
resolvers =
|
|
|
|
[ ("instrument", ValueResolver instrumentField instrumentResolver)
|
|
|
|
]
|
|
|
|
instrumentResolver = pure $ Type.String "piano"
|
|
|
|
instrumentField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
|
|
|
|
|
|
|
|
poetryType :: Out.ObjectType (Either SomeException)
|
|
|
|
poetryType = Out.ObjectType "Poetry" Nothing []
|
|
|
|
$ HashMap.fromList resolvers
|
|
|
|
where
|
|
|
|
resolvers =
|
|
|
|
[ ("genre", ValueResolver genreField genreResolver)
|
|
|
|
]
|
|
|
|
genreResolver = pure $ Type.String "Futurism"
|
|
|
|
genreField = Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
|
|
|
|
|
|
|
|
interestType :: Out.UnionType (Either SomeException)
|
|
|
|
interestType = Out.UnionType "Interest" Nothing [musicType, poetryType]
|
|
|
|
|
2020-07-17 07:05:03 +02:00
|
|
|
philosopherType :: Out.ObjectType (Either SomeException)
|
2020-06-12 07:58:08 +02:00
|
|
|
philosopherType = Out.ObjectType "Philosopher" Nothing []
|
|
|
|
$ HashMap.fromList resolvers
|
|
|
|
where
|
|
|
|
resolvers =
|
2020-07-14 19:37:56 +02:00
|
|
|
[ ("firstName", ValueResolver firstNameField firstNameResolver)
|
|
|
|
, ("lastName", ValueResolver lastNameField lastNameResolver)
|
2021-05-09 12:34:39 +02:00
|
|
|
, ("school", ValueResolver schoolField schoolResolver)
|
2021-05-12 06:51:59 +02:00
|
|
|
, ("interest", ValueResolver interestField interestResolver)
|
2020-06-12 07:58:08 +02:00
|
|
|
]
|
2020-07-14 19:37:56 +02:00
|
|
|
firstNameField =
|
|
|
|
Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
|
|
|
|
firstNameResolver = pure $ Type.String "Friedrich"
|
2020-06-29 13:14:23 +02:00
|
|
|
lastNameField
|
|
|
|
= Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
|
2020-07-14 19:37:56 +02:00
|
|
|
lastNameResolver = pure $ Type.String "Nietzsche"
|
2021-05-09 12:34:39 +02:00
|
|
|
schoolField
|
|
|
|
= Out.Field Nothing (Out.NonNullEnumType schoolType) HashMap.empty
|
|
|
|
schoolResolver = pure $ Type.Enum "EXISTENTIALISM"
|
2021-05-12 06:51:59 +02:00
|
|
|
interestField
|
|
|
|
= Out.Field Nothing (Out.NonNullUnionType interestType) HashMap.empty
|
|
|
|
interestResolver = pure
|
|
|
|
$ Type.Object
|
|
|
|
$ HashMap.fromList [("instrument", "piano")]
|
2020-06-10 11:42:00 +02:00
|
|
|
|
2020-07-17 07:05:03 +02:00
|
|
|
subscriptionType :: Out.ObjectType (Either SomeException)
|
2020-07-15 19:15:31 +02:00
|
|
|
subscriptionType = Out.ObjectType "Subscription" Nothing []
|
|
|
|
$ HashMap.singleton "newQuote"
|
|
|
|
$ EventStreamResolver quoteField (pure $ Type.Object mempty)
|
|
|
|
$ pure $ yield $ Type.Object mempty
|
|
|
|
where
|
|
|
|
quoteField =
|
|
|
|
Out.Field Nothing (Out.NonNullObjectType quoteType) HashMap.empty
|
|
|
|
|
2020-07-17 07:05:03 +02:00
|
|
|
quoteType :: Out.ObjectType (Either SomeException)
|
2020-07-15 19:15:31 +02:00
|
|
|
quoteType = Out.ObjectType "Quote" Nothing []
|
|
|
|
$ HashMap.singleton "quote"
|
|
|
|
$ ValueResolver quoteField
|
|
|
|
$ pure "Naturam expelles furca, tamen usque recurret."
|
|
|
|
where
|
|
|
|
quoteField =
|
|
|
|
Out.Field Nothing (Out.NonNullScalarType string) HashMap.empty
|
|
|
|
|
2021-05-09 12:34:39 +02:00
|
|
|
schoolType :: EnumType
|
|
|
|
schoolType = EnumType "School" Nothing $ HashMap.fromList
|
|
|
|
[ ("NOMINALISM", EnumValue Nothing)
|
|
|
|
, ("REALISM", EnumValue Nothing)
|
|
|
|
, ("IDEALISM", EnumValue Nothing)
|
|
|
|
]
|
|
|
|
|
2020-09-11 08:03:49 +02:00
|
|
|
type EitherStreamOrValue = Either
|
|
|
|
(ResponseEventStream (Either SomeException) Aeson.Value)
|
|
|
|
(Response Aeson.Value)
|
|
|
|
|
|
|
|
execute' :: Document -> Either SomeException EitherStreamOrValue
|
2020-09-28 07:06:15 +02:00
|
|
|
execute' =
|
|
|
|
execute philosopherSchema Nothing (mempty :: HashMap Name Aeson.Value)
|
2020-09-11 08:03:49 +02:00
|
|
|
|
2020-06-10 11:42:00 +02:00
|
|
|
spec :: Spec
|
|
|
|
spec =
|
2020-06-12 07:58:08 +02:00
|
|
|
describe "execute" $ do
|
2020-09-11 08:03:49 +02:00
|
|
|
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
|
|
|
|
|
2020-07-15 19:15:31 +02:00
|
|
|
context "Query" $ do
|
|
|
|
it "skips unknown fields" $
|
|
|
|
let data'' = Aeson.object
|
|
|
|
[ "philosopher" .= Aeson.object
|
|
|
|
[ "firstName" .= ("Friedrich" :: String)
|
|
|
|
]
|
|
|
|
]
|
|
|
|
expected = Response data'' mempty
|
2020-07-17 07:05:03 +02:00
|
|
|
Right (Right actual) = either (pure . parseError) execute'
|
2020-07-15 19:15:31 +02:00
|
|
|
$ parse document "" "{ philosopher { firstName surname } }"
|
|
|
|
in actual `shouldBe` expected
|
|
|
|
it "merges selections" $
|
|
|
|
let data'' = Aeson.object
|
|
|
|
[ "philosopher" .= Aeson.object
|
|
|
|
[ "firstName" .= ("Friedrich" :: String)
|
|
|
|
, "lastName" .= ("Nietzsche" :: String)
|
|
|
|
]
|
2020-06-12 07:58:08 +02:00
|
|
|
]
|
2020-07-15 19:15:31 +02:00
|
|
|
expected = Response data'' mempty
|
2020-07-17 07:05:03 +02:00
|
|
|
Right (Right actual) = either (pure . parseError) execute'
|
2020-07-15 19:15:31 +02:00
|
|
|
$ parse document "" "{ philosopher { firstName } philosopher { lastName } }"
|
|
|
|
in actual `shouldBe` expected
|
2021-05-09 12:34:39 +02:00
|
|
|
|
|
|
|
it "errors on invalid output enum values" $
|
|
|
|
let data'' = Aeson.object
|
|
|
|
[ "philosopher" .= Aeson.object
|
|
|
|
[ "school" .= Aeson.Null
|
|
|
|
]
|
|
|
|
]
|
|
|
|
executionErrors = pure $ Error
|
|
|
|
{ message = "Enum value completion failed."
|
2021-05-12 06:51:59 +02:00
|
|
|
, locations = [Location 1 17]
|
2021-05-09 12:34:39 +02:00
|
|
|
, path = []
|
|
|
|
}
|
|
|
|
expected = Response data'' executionErrors
|
|
|
|
Right (Right actual) = either (pure . parseError) execute'
|
|
|
|
$ parse document "" "{ philosopher { school } }"
|
|
|
|
in actual `shouldBe` expected
|
|
|
|
|
2021-05-12 06:51:59 +02:00
|
|
|
it "gives location information for invalid interfaces" $
|
|
|
|
let data'' = Aeson.object
|
|
|
|
[ "philosopher" .= Aeson.object
|
|
|
|
[ "interest" .= Aeson.Null
|
|
|
|
]
|
|
|
|
]
|
|
|
|
executionErrors = pure $ Error
|
|
|
|
{ message = "Union value completion failed."
|
|
|
|
, locations = [Location 1 17]
|
|
|
|
, path = []
|
|
|
|
}
|
|
|
|
expected = Response data'' executionErrors
|
|
|
|
Right (Right actual) = either (pure . parseError) execute'
|
|
|
|
$ parse document "" "{ philosopher { interest } }"
|
|
|
|
in actual `shouldBe` expected
|
|
|
|
|
2020-07-15 19:15:31 +02:00
|
|
|
context "Subscription" $
|
|
|
|
it "subscribes" $
|
|
|
|
let data'' = Aeson.object
|
|
|
|
[ "newQuote" .= Aeson.object
|
|
|
|
[ "quote" .= ("Naturam expelles furca, tamen usque recurret." :: String)
|
|
|
|
]
|
2020-06-12 07:58:08 +02:00
|
|
|
]
|
2020-07-15 19:15:31 +02:00
|
|
|
expected = Response data'' mempty
|
2020-07-17 07:05:03 +02:00
|
|
|
Right (Left stream) = either (pure . parseError) execute'
|
2020-07-15 19:15:31 +02:00
|
|
|
$ parse document "" "subscription { newQuote { quote } }"
|
2020-07-17 07:05:03 +02:00
|
|
|
Right (Just actual) = runConduit $ stream .| await
|
2020-07-15 19:15:31 +02:00
|
|
|
in actual `shouldBe` expected
|