graphql-engine/server/tests-hspec/Test/DirectivesSpec.hs
Samir Talwar eab4f75212 An ErrorMessage type, to encapsulate.
This introduces an `ErrorMessage` newtype which wraps `Text` in a manner which is designed to be easy to construct, and difficult to deconstruct.

It provides functionality similar to `Data.Text.Extended`, but designed _only_ for error messages. Error messages are constructed through `fromString`, concatenation, or the `toErrorValue` function, which is designed to be overridden for all meaningful domain types that might show up in an error message. Notably, there are not and should never be instances of `ToErrorValue` for `String`, `Text`, `Int`, etc. This is so that we correctly represent the value in a way that is specific to its type. For example, all `Name` values (from the _graphql-parser-hs_ library) are single-quoted now; no exceptions.

I have mostly had to add `instance ToErrorValue` for various backend types (and also add newtypes where necessary). Some of these are not strictly necessary for this changeset, as I had bigger aspirations when I started. These aspirations have been tempered by trying and failing twice.

As such, in this changeset, I have started by introducing this type to the `parseError` and `parseErrorWith` functions. In the future, I would like to extend this to the `QErr` record and the various `throwError` functions, but this is a much larger task and should probably be done in stages.

For now, `toErrorMessage` and `fromErrorMessage` are provided for conversion to and from `Text`, but the intent is to stop exporting these once all error messages are converted to the new type.

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5018
GitOrigin-RevId: 84b37e238992e4312255a87ca44f41af65e2d89a
2022-07-18 20:27:06 +00:00

335 lines
8.4 KiB
Haskell

{-# LANGUAGE QuasiQuotes #-}
-- | Test directives.
module Test.DirectivesSpec (spec) where
import Data.Aeson (Value (..), object, (.=))
import Harness.Backend.BigQuery qualified as Bigquery
import Harness.Backend.Citus qualified as Citus
import Harness.Backend.Mysql as Mysql
import Harness.Backend.Postgres qualified as Postgres
import Harness.Backend.Sqlserver qualified as Sqlserver
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.Quoter.Graphql (graphql)
import Harness.Quoter.Yaml
import Harness.Test.Context qualified as Context
import Harness.Test.Schema (Table (..), table)
import Harness.Test.Schema qualified as Schema
import Harness.TestEnvironment (TestEnvironment)
import Harness.Yaml
import Test.Hspec
import Prelude
--------------------------------------------------------------------------------
-- Preamble
spec :: SpecWith TestEnvironment
spec =
Context.run
[ Context.Context
{ name = Context.Backend Context.MySQL,
mkLocalTestEnvironment = Context.noLocalTestEnvironment,
setup = Mysql.setup schema,
teardown = Mysql.teardown schema,
customOptions = Nothing
},
Context.Context
{ name = Context.Backend Context.Postgres,
mkLocalTestEnvironment = Context.noLocalTestEnvironment,
setup = Postgres.setup schema,
teardown = Postgres.teardown schema,
customOptions = Nothing
},
Context.Context
{ name = Context.Backend Context.Citus,
mkLocalTestEnvironment = Context.noLocalTestEnvironment,
setup = Citus.setup schema,
teardown = Citus.teardown schema,
customOptions = Nothing
},
Context.Context
{ name = Context.Backend Context.SQLServer,
mkLocalTestEnvironment = Context.noLocalTestEnvironment,
setup = Sqlserver.setup schema,
teardown = Sqlserver.teardown schema,
customOptions = Nothing
},
Context.Context
{ name = Context.Backend Context.BigQuery,
mkLocalTestEnvironment = Context.noLocalTestEnvironment,
setup = Bigquery.setup schema,
teardown = Bigquery.teardown schema,
customOptions =
Just $
Context.Options
{ stringifyNumbers = True
}
}
]
tests
--------------------------------------------------------------------------------
-- Schema
schema :: [Schema.Table]
schema = [author]
author :: Schema.Table
author =
(table "author")
{ tableColumns =
[ Schema.column "id" Schema.TInt,
Schema.column "name" Schema.TStr
],
tablePrimaryKey = ["id"],
tableData =
[ [Schema.VInt 1, Schema.VStr "Author 1"],
[Schema.VInt 2, Schema.VStr "Author 2"]
]
}
--------------------------------------------------------------------------------
-- Tests
data QueryParams = QueryParams
{ includeId :: Bool,
skipId :: Bool
}
query :: QueryParams -> Value
query QueryParams {includeId, skipId} =
[graphql|
query author_with_both {
hasura_author {
id @include(if: #{includeId}) @skip(if: #{skipId})
name
}
}
|]
tests :: Context.Options -> SpecWith TestEnvironment
tests opts = do
it "Skip id field conditionally" \testEnvironment ->
shouldReturnOneOfYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
(query QueryParams {includeId = False, skipId = False})
)
(combinationsObjectUsingValue authorResponse authorNames)
it "Skip id field conditionally, includeId=true" \testEnvironment ->
shouldReturnOneOfYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
(query QueryParams {includeId = True, skipId = False})
)
(combinationsObjectUsingValue authorResponse authorNameIds)
it "Skip id field conditionally, skipId=true" \testEnvironment ->
shouldReturnOneOfYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
(query QueryParams {includeId = False, skipId = True})
)
(combinationsObjectUsingValue authorResponse authorNames)
it "Skip id field conditionally, skipId=true, includeId=true" \testEnvironment ->
shouldReturnOneOfYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
(query QueryParams {includeId = True, skipId = True})
)
(combinationsObjectUsingValue authorResponse authorNames)
it "Author with skip id" \testEnvironment ->
shouldReturnOneOfYaml
opts
( GraphqlEngine.postGraphqlYaml
testEnvironment
[yaml|
query: |
query author_with_skip($skipId: Boolean!, $skipName: Boolean!) {
hasura_author {
id @skip(if: $skipId)
name @skip(if: $skipName)
}
}
variables:
skipId: true
skipName: false
|]
)
(combinationsObjectUsingValue authorResponse authorNames)
it "Author with skip name" \testEnvironment ->
shouldReturnOneOfYaml
opts
( GraphqlEngine.postGraphqlYaml
testEnvironment
[yaml|
query: |
query author_with_skip($skipId: Boolean!, $skipName: Boolean!) {
hasura_author {
id @skip(if: $skipId)
name @skip(if: $skipName)
}
}
variables:
skipId: false
skipName: true
|]
)
(combinationsObjectUsingValue authorResponse authorIds)
it "Rejects unknown directives" \testEnvironment ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphqlYaml
testEnvironment
[yaml|
query: |
query {
hasura_author {
id @exclude(if: true)
name
}
}
|]
)
[yaml|
errors:
- extensions:
path: $.selectionSet.hasura_author.selectionSet
code: validation-failed
message: |-
directive 'exclude' is not defined in the schema
|]
it "Rejects duplicate directives" \testEnvironment ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphqlYaml
testEnvironment
[yaml|
query: |
query {
hasura_author {
id @include(if: true) @include(if: true)
name
}
}
|]
)
[yaml|
errors:
- extensions:
path: $.selectionSet.hasura_author.selectionSet
code: validation-failed
message: |-
the following directives are used more than once: ['include']
|]
it "Rejects directives on wrong element" \testEnvironment ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphqlYaml
testEnvironment
[yaml|
query: |
query @include(if: true) {
hasura_author {
id
name
}
}
|]
)
[yaml|
errors:
- extensions:
path: $
code: validation-failed
message: |-
directive 'include' is not allowed on a query
|]
it "works with includeId as True includeName as False" \testEnvironment ->
shouldReturnOneOfYaml
opts
( GraphqlEngine.postGraphqlWithPair
testEnvironment
[graphql|
query author_with_include($includeId: Boolean!, $includeName: Boolean!) {
hasura_author {
id @include(if: $includeId)
name @include(if: $includeName)
}
}
|]
["variables" .= object ["includeId" .= True, "includeName" .= False]]
)
(combinationsObjectUsingValue authorResponse authorIds)
it "works with includeId as False includeName as True" \testEnvironment ->
shouldReturnOneOfYaml
opts
( GraphqlEngine.postGraphqlWithPair
testEnvironment
[graphql|
query author_with_include($includeId: Boolean!, $includeName: Boolean!) {
hasura_author {
id @include(if: $includeId)
name @include(if: $includeName)
}
}
|]
["variables" .= object ["includeId" .= False, "includeName" .= True]]
)
(combinationsObjectUsingValue authorResponse authorNames)
authorNames :: [Value]
authorNames =
[ object
[ "name"
.= String "Author 1"
],
object
[ "name"
.= String "Author 2"
]
]
authorIds :: [Value]
authorIds =
[ object
[ "id"
.= (1 :: Int)
],
object
[ "id"
.= (2 :: Int)
]
]
authorNameIds :: [Value]
authorNameIds =
[ object
[ "name"
.= String "Author 1",
"id" .= (1 :: Int)
],
object
[ "name"
.= String "Author 2",
"id" .= (2 :: Int)
]
]
authorResponse :: Value -> Value
authorResponse authors =
[yaml|
data:
hasura_author: *authors
|]