2021-05-24 23:12:53 +03:00
|
|
|
module Hasura.GraphQL.Schema.RemoteTest (spec) where
|
|
|
|
|
|
|
|
import Hasura.Prelude
|
|
|
|
|
|
|
|
import qualified Data.Aeson as J
|
|
|
|
import qualified Data.ByteString.Lazy as LBS
|
|
|
|
import qualified Data.HashMap.Strict as M
|
|
|
|
import qualified Data.Text as T
|
|
|
|
import qualified Language.GraphQL.Draft.Parser as G
|
|
|
|
import qualified Language.GraphQL.Draft.Syntax as G
|
|
|
|
import qualified Network.URI as N
|
|
|
|
|
2021-08-05 00:23:33 +03:00
|
|
|
import Control.Lens (Prism', _Right, prism', to, (^..))
|
2021-05-24 23:12:53 +03:00
|
|
|
import Data.Text.Extended
|
|
|
|
import Data.Text.RawString
|
|
|
|
import Test.Hspec
|
|
|
|
|
|
|
|
import qualified Hasura.GraphQL.Parser.Internal.Parser as P
|
|
|
|
|
|
|
|
import Hasura.Base.Error
|
|
|
|
import Hasura.GraphQL.Execute.Inline
|
2021-08-05 00:23:33 +03:00
|
|
|
import Hasura.GraphQL.Execute.Remote (resolveRemoteVariable, runVariableCache)
|
2021-05-24 23:12:53 +03:00
|
|
|
import Hasura.GraphQL.Execute.Resolve
|
|
|
|
import Hasura.GraphQL.Parser.Monad
|
|
|
|
import Hasura.GraphQL.Parser.Schema
|
|
|
|
import Hasura.GraphQL.Parser.TestUtils
|
2021-07-30 14:33:06 +03:00
|
|
|
import Hasura.GraphQL.RemoteServer (identityCustomizer)
|
2021-05-24 23:12:53 +03:00
|
|
|
import Hasura.GraphQL.Schema.Remote
|
|
|
|
import Hasura.RQL.Types.RemoteSchema
|
|
|
|
import Hasura.RQL.Types.SchemaCache
|
|
|
|
import Hasura.Session
|
|
|
|
|
|
|
|
|
|
|
|
-- test tools
|
|
|
|
|
|
|
|
runError :: Monad m => ExceptT QErr m a -> m a
|
|
|
|
runError = runExceptT >=> (`onLeft` (error . T.unpack . qeError))
|
|
|
|
|
|
|
|
|
|
|
|
mkTestRemoteSchema :: Text -> RemoteSchemaIntrospection
|
|
|
|
mkTestRemoteSchema schema = RemoteSchemaIntrospection $ runIdentity $ runError $ do
|
|
|
|
G.SchemaDocument types <- G.parseSchemaDocument schema `onLeft` throw500
|
|
|
|
pure $ flip mapMaybe types \case
|
|
|
|
G.TypeSystemDefinitionSchema _ -> Nothing
|
|
|
|
G.TypeSystemDefinitionType td -> Just $ case fmap toRemoteInputValue td of
|
|
|
|
G.TypeDefinitionScalar std -> G.TypeDefinitionScalar std
|
|
|
|
G.TypeDefinitionObject otd -> G.TypeDefinitionObject otd
|
|
|
|
G.TypeDefinitionUnion utd -> G.TypeDefinitionUnion utd
|
|
|
|
G.TypeDefinitionEnum etd -> G.TypeDefinitionEnum etd
|
|
|
|
G.TypeDefinitionInputObject itd -> G.TypeDefinitionInputObject itd
|
|
|
|
G.TypeDefinitionInterface itd -> G.TypeDefinitionInterface $ G.InterfaceTypeDefinition
|
|
|
|
{ G._itdDescription = G._itdDescription itd
|
|
|
|
, G._itdName = G._itdName itd
|
|
|
|
, G._itdDirectives = G._itdDirectives itd
|
|
|
|
, G._itdFieldsDefinition = G._itdFieldsDefinition itd
|
|
|
|
, G._itdPossibleTypes = []
|
|
|
|
}
|
|
|
|
where
|
|
|
|
toRemoteInputValue ivd = RemoteSchemaInputValueDefinition
|
|
|
|
{ _rsitdDefinition = ivd
|
|
|
|
, _rsitdPresetArgument = choice $ G._ivdDirectives ivd <&> \dir -> do
|
|
|
|
guard $ G._dName dir == $$(G.litName "preset")
|
|
|
|
value <- M.lookup $$(G.litName "value") $ G._dArguments dir
|
|
|
|
Just $ case value of
|
|
|
|
G.VString "x-hasura-test" -> G.VVariable $
|
|
|
|
SessionPresetVariable (mkSessionVariable "x-hasura-test") $$(G.litName "String") SessionArgumentPresetScalar
|
|
|
|
_ -> absurd <$> value
|
|
|
|
}
|
|
|
|
|
|
|
|
mkTestExecutableDocument :: Text -> ([G.VariableDefinition], G.SelectionSet G.NoFragments G.Name)
|
|
|
|
mkTestExecutableDocument t = runIdentity $ runError $ do
|
|
|
|
G.ExecutableDocument execDoc <- G.parseExecutableDoc t `onLeft` throw500
|
|
|
|
case execDoc of
|
|
|
|
[G.ExecutableDefinitionOperation op] -> case op of
|
|
|
|
G.OperationDefinitionUnTyped selSet -> ([],) <$> inlineSelectionSet [] selSet
|
|
|
|
G.OperationDefinitionTyped opDef -> do
|
|
|
|
unless (G._todType opDef == G.OperationTypeQuery) $
|
|
|
|
throw500 "only queries for now"
|
|
|
|
resSelSet <- inlineSelectionSet [] $ G._todSelectionSet opDef
|
|
|
|
pure (G._todVariableDefinitions opDef, resSelSet)
|
|
|
|
_ -> throw500 "must have only one query in the document"
|
|
|
|
|
|
|
|
mkTestVariableValues :: LBS.ByteString -> M.HashMap G.Name J.Value
|
|
|
|
mkTestVariableValues vars = runIdentity $ runError $ do
|
|
|
|
value <- J.eitherDecode vars `onLeft` (throw500 . T.pack)
|
|
|
|
case value of
|
|
|
|
J.Object vs -> M.fromList <$> for (M.toList vs) \(name, val) -> do
|
|
|
|
gname <- G.mkName name `onNothing` throw500 ("wrong Name: " <>> name)
|
|
|
|
pure (gname, val)
|
|
|
|
_ -> throw500 "variables must be an object"
|
|
|
|
|
|
|
|
|
|
|
|
buildQueryParsers
|
|
|
|
:: RemoteSchemaIntrospection
|
|
|
|
-> IO (P.FieldParser TestMonad (G.Field G.NoFragments RemoteSchemaVariable))
|
|
|
|
buildQueryParsers introspection = do
|
|
|
|
let introResult = IntrospectionResult introspection $$(G.litName "Query") Nothing Nothing
|
|
|
|
(query, _, _) <- runError
|
|
|
|
$ runSchemaT
|
2021-06-10 12:56:27 +03:00
|
|
|
$ buildRemoteParser introResult
|
2021-07-30 14:33:06 +03:00
|
|
|
$ RemoteSchemaInfo (ValidatedRemoteSchemaDef N.nullURI [] False 60 Nothing) identityCustomizer
|
|
|
|
pure $ head query <&> \(RemoteFieldG _ _ abstractField) ->
|
|
|
|
case abstractField of
|
|
|
|
RRFRealField f -> f
|
|
|
|
RRFNamespaceField _ ->
|
|
|
|
error "buildQueryParsers: unexpected RRFNamespaceField"
|
|
|
|
-- Shouldn't happen if we're using identityCustomizer
|
|
|
|
-- TODO: add some tests for remote schema customization
|
2021-05-24 23:12:53 +03:00
|
|
|
|
|
|
|
|
|
|
|
runQueryParser
|
2021-08-05 00:23:33 +03:00
|
|
|
:: P.FieldParser TestMonad any
|
2021-05-24 23:12:53 +03:00
|
|
|
-> ([G.VariableDefinition], G.SelectionSet G.NoFragments G.Name)
|
|
|
|
-> M.HashMap G.Name J.Value
|
2021-08-05 00:23:33 +03:00
|
|
|
-> any
|
|
|
|
runQueryParser parser (varDefs, selSet) vars = runIdentity . runError $ do
|
2021-05-24 23:12:53 +03:00
|
|
|
(_, resolvedSelSet) <- resolveVariables varDefs vars [] selSet
|
|
|
|
field <- case resolvedSelSet of
|
|
|
|
[G.SelectionField f] -> pure f
|
|
|
|
_ -> error "expecting only one field in the query"
|
|
|
|
runTest (P.fParser parser field) `onLeft` throw500
|
|
|
|
|
|
|
|
run
|
2021-08-05 00:23:33 +03:00
|
|
|
:: Text -- ^ schema
|
|
|
|
-> Text -- ^ query
|
|
|
|
-> LBS.ByteString -- ^ variables
|
2021-05-24 23:12:53 +03:00
|
|
|
-> IO (G.Field G.NoFragments RemoteSchemaVariable)
|
2021-08-05 00:23:33 +03:00
|
|
|
run schema query variables = do
|
|
|
|
parser <- buildQueryParsers $ mkTestRemoteSchema schema
|
2021-05-24 23:12:53 +03:00
|
|
|
pure $ runQueryParser
|
|
|
|
parser
|
2021-08-05 00:23:33 +03:00
|
|
|
(mkTestExecutableDocument query)
|
|
|
|
(mkTestVariableValues variables)
|
|
|
|
|
2021-05-24 23:12:53 +03:00
|
|
|
|
|
|
|
|
|
|
|
-- actual test
|
|
|
|
|
|
|
|
spec :: Spec
|
|
|
|
spec = do
|
|
|
|
testNoVarExpansionIfNoPreset
|
|
|
|
testNoVarExpansionIfNoPresetUnlessTopLevelOptionalField
|
|
|
|
testPartialVarExpansionIfPreset
|
2021-08-05 00:23:33 +03:00
|
|
|
testVariableSubstitutionCollision
|
2021-05-24 23:12:53 +03:00
|
|
|
|
|
|
|
testNoVarExpansionIfNoPreset :: Spec
|
|
|
|
testNoVarExpansionIfNoPreset = it "variables aren't expanded if there's no preset" $ do
|
|
|
|
field <- run
|
|
|
|
-- schema
|
|
|
|
[raw|
|
|
|
|
scalar Int
|
|
|
|
|
|
|
|
input A {
|
|
|
|
b: B
|
|
|
|
}
|
|
|
|
|
|
|
|
input B {
|
|
|
|
c: C
|
|
|
|
}
|
|
|
|
|
|
|
|
input C {
|
|
|
|
i: Int
|
|
|
|
}
|
|
|
|
|
|
|
|
type Query {
|
|
|
|
test(a: A!): Int
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
-- query
|
|
|
|
[raw|
|
|
|
|
query($a: A!) {
|
|
|
|
test(a: $a)
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
-- variables
|
|
|
|
[raw|
|
|
|
|
{
|
|
|
|
"a": {
|
|
|
|
"b": {
|
|
|
|
"c": {
|
|
|
|
"i": 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
let arg = head $ M.toList $ G._fArguments field
|
|
|
|
arg `shouldBe`
|
|
|
|
( $$(G.litName "a")
|
|
|
|
-- the parser did not create a new JSON variable, and forwarded the query variable unmodified
|
|
|
|
, G.VVariable $ QueryVariable $ Variable
|
|
|
|
(VIRequired $$(G.litName "a"))
|
|
|
|
(G.TypeNamed (G.Nullability False) $$(G.litName "A"))
|
|
|
|
(JSONValue $ J.Object $ M.fromList [("b", J.Object $ M.fromList [("c", J.Object $ M.fromList [("i", J.Number 0)])])])
|
|
|
|
)
|
|
|
|
|
|
|
|
testNoVarExpansionIfNoPresetUnlessTopLevelOptionalField :: Spec
|
|
|
|
testNoVarExpansionIfNoPresetUnlessTopLevelOptionalField = it "unless fieldOptional peels the variable first" $ do
|
|
|
|
field <- run
|
|
|
|
-- schema
|
|
|
|
[raw|
|
|
|
|
scalar Int
|
|
|
|
|
|
|
|
input A {
|
|
|
|
b: B
|
|
|
|
}
|
|
|
|
|
|
|
|
input B {
|
|
|
|
c: C
|
|
|
|
}
|
|
|
|
|
|
|
|
input C {
|
|
|
|
i: Int
|
|
|
|
}
|
|
|
|
|
|
|
|
type Query {
|
|
|
|
test(a: A): Int
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
-- query
|
|
|
|
[raw|
|
|
|
|
query($a: A) {
|
|
|
|
test(a: $a)
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
-- variables
|
|
|
|
[raw|
|
|
|
|
{
|
|
|
|
"a": {
|
|
|
|
"b": {
|
|
|
|
"c": {
|
|
|
|
"i": 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
let arg = head $ M.toList $ G._fArguments field
|
|
|
|
arg `shouldBe`
|
|
|
|
( $$(G.litName "a")
|
|
|
|
-- fieldOptional has peeled the variable; all we see is a JSON blob, and in doubt
|
|
|
|
-- we repackage it as a newly minted JSON variable
|
|
|
|
, G.VVariable $ RemoteJSONValue
|
|
|
|
(G.TypeNamed (G.Nullability True) $$(G.litName "A"))
|
|
|
|
(J.Object $ M.fromList [("b", J.Object $ M.fromList [("c", J.Object $ M.fromList [("i", J.Number 0)])])])
|
|
|
|
)
|
|
|
|
|
|
|
|
testPartialVarExpansionIfPreset :: Spec
|
|
|
|
testPartialVarExpansionIfPreset = it "presets cause partial var expansion" $ do
|
|
|
|
field <- run
|
|
|
|
-- schema
|
|
|
|
[raw|
|
|
|
|
scalar Int
|
|
|
|
|
|
|
|
input A {
|
|
|
|
x: Int @preset(value: 0)
|
|
|
|
b: B
|
|
|
|
}
|
|
|
|
|
|
|
|
input B {
|
|
|
|
c: C
|
|
|
|
}
|
|
|
|
|
|
|
|
input C {
|
|
|
|
i: Int
|
|
|
|
}
|
|
|
|
|
|
|
|
type Query {
|
|
|
|
test(a: A!): Int
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
-- query
|
|
|
|
[raw|
|
|
|
|
query($a: A!) {
|
|
|
|
test(a: $a)
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
-- variables
|
|
|
|
[raw|
|
|
|
|
{
|
|
|
|
"a": {
|
|
|
|
"b": {
|
|
|
|
"c": {
|
|
|
|
"i": 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
let arg = head $ M.toList $ G._fArguments field
|
|
|
|
arg `shouldBe`
|
|
|
|
( $$(G.litName "a")
|
|
|
|
-- the preset has caused partial variable expansion, only up to where it's needed
|
|
|
|
, G.VObject $ M.fromList
|
|
|
|
[ ( $$(G.litName "x")
|
|
|
|
, G.VInt 0
|
|
|
|
)
|
|
|
|
, ( $$(G.litName "b")
|
|
|
|
, G.VVariable $ RemoteJSONValue
|
|
|
|
(G.TypeNamed (G.Nullability True) $$(G.litName "B"))
|
|
|
|
(J.Object $ M.fromList [("c", J.Object $ M.fromList [("i", J.Number 0)])])
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
2021-08-05 00:23:33 +03:00
|
|
|
|
|
|
|
|
|
|
|
-- | Regression test for https://github.com/hasura/graphql-engine/issues/7170
|
|
|
|
testVariableSubstitutionCollision :: Spec
|
|
|
|
testVariableSubstitutionCollision = it "ensures that remote variables are de-duplicated by type and value, not just by value" $ do
|
|
|
|
field <- run schema query variables
|
|
|
|
let
|
|
|
|
dummyUserInfo =
|
|
|
|
UserInfo
|
|
|
|
adminRoleName
|
|
|
|
(mempty @SessionVariables)
|
|
|
|
BOFADisallowed
|
|
|
|
eField <-
|
|
|
|
runExceptT
|
|
|
|
. runVariableCache
|
|
|
|
. traverse (resolveRemoteVariable dummyUserInfo)
|
|
|
|
$ field
|
|
|
|
let
|
|
|
|
variableNames =
|
|
|
|
eField ^.. _Right . to G._fArguments . traverse . _VVariable . to vInfo . to getName . to G.unName
|
|
|
|
variableNames `shouldBe` ["hasura_json_var_1", "hasura_json_var_2"]
|
|
|
|
where
|
|
|
|
-- A schema whose values are representable as collections of JSON values.
|
|
|
|
schema :: Text
|
|
|
|
schema = [raw|
|
|
|
|
scalar Int
|
|
|
|
scalar String
|
|
|
|
|
|
|
|
type Query {
|
|
|
|
test(a: [Int], b: [String]): Int
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
-- A query against values from 'schema' using JSON variable substitution.
|
|
|
|
query :: Text
|
|
|
|
query = [raw|
|
|
|
|
query($a: [Int], $b: [String]) {
|
|
|
|
test(a: $a, b: $b)
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
-- Two identical JSON variables to substitute; 'schema' and 'query' declare
|
|
|
|
-- that these variables should have different types despite both being
|
|
|
|
-- empty collections.
|
|
|
|
variables :: LBS.ByteString
|
|
|
|
variables = [raw|
|
|
|
|
{
|
|
|
|
"a": [],
|
|
|
|
"b": []
|
|
|
|
}
|
|
|
|
|]
|
|
|
|
|
|
|
|
-- | Convenience function to focus on a 'G.VVariable' when pulling test values
|
|
|
|
-- out in 'testVariableSubstitutionCollision'.
|
|
|
|
_VVariable :: Prism' (G.Value var) var
|
|
|
|
_VVariable = prism' upcast downcast
|
|
|
|
where
|
|
|
|
upcast = G.VVariable
|
|
|
|
downcast = \case
|
|
|
|
G.VVariable var -> Just var
|
|
|
|
_ -> Nothing
|