mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-10-05 06:18:04 +03:00
GDW Agent Test Suite [GDW-79]
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4175 GitOrigin-RevId: d37d7d131597af6b9cca6bd773c8dbbce8719ca5
This commit is contained in:
parent
e9436c5d97
commit
38c41b2ae6
@ -1,6 +1,7 @@
|
||||
active-repositories: hackage.haskell.org:merge
|
||||
constraints: any.Cabal ==3.2.1.0,
|
||||
Cabal -bundled-binary-generic,
|
||||
any.Diff ==0.4.1,
|
||||
any.HTTP ==4000.3.16,
|
||||
HTTP -conduit10 -mtl1 +network-uri -warn-as-error -warp-tests,
|
||||
any.HUnit ==1.6.2.0,
|
||||
@ -43,7 +44,7 @@ constraints: any.Cabal ==3.2.1.0,
|
||||
any.authenticate-oauth ==1.7,
|
||||
any.auto-update ==0.1.6,
|
||||
any.autodocodec ==0.0.1.0,
|
||||
any.autodocodec-openapi3 ==0.1.0.0,
|
||||
any.autodocodec-openapi3 ==0.2.0.0,
|
||||
any.barbies ==2.0.3.1,
|
||||
any.base ==4.14.3.0,
|
||||
any.base-compat ==0.11.2,
|
||||
@ -181,11 +182,13 @@ constraints: any.Cabal ==3.2.1.0,
|
||||
any.hpc ==0.6.1.0,
|
||||
any.hsc2hs ==0.68.8,
|
||||
hsc2hs -in-ghc-tree,
|
||||
any.hscolour ==1.24.4,
|
||||
any.hspec ==2.9.4,
|
||||
any.hspec-core ==2.9.4,
|
||||
any.hspec-discover ==2.9.4,
|
||||
any.hspec-expectations ==0.8.2,
|
||||
any.hspec-expectations-lifted ==0.10.0,
|
||||
any.hspec-expectations-pretty-diff ==0.7.2.6,
|
||||
any.hspec-hedgehog ==0.0.1.2,
|
||||
any.hspec-wai ==0.11.0,
|
||||
any.hspec-wai-json ==0.11.0,
|
||||
@ -264,6 +267,7 @@ constraints: any.Cabal ==3.2.1.0,
|
||||
any.network-info ==0.2.1,
|
||||
any.network-ip ==0.3.0.3,
|
||||
any.network-uri ==2.6.4.1,
|
||||
any.nicify-lib ==1.0.1,
|
||||
any.odbc ==0.2.6,
|
||||
any.old-locale ==1.0.0.7,
|
||||
any.old-time ==1.1.0.3,
|
||||
@ -410,6 +414,7 @@ constraints: any.Cabal ==3.2.1.0,
|
||||
any.type-hint ==0.1,
|
||||
any.typed-process ==0.2.8.0,
|
||||
any.unbounded-delays ==0.1.1.1,
|
||||
any.unicode-show ==0.1.1.0,
|
||||
any.unix ==2.7.2.2,
|
||||
any.unix-compat ==0.5.4,
|
||||
unix-compat -old-time,
|
||||
@ -464,4 +469,4 @@ constraints: any.Cabal ==3.2.1.0,
|
||||
yaml +no-examples +no-exe,
|
||||
any.zlib ==0.6.2.3,
|
||||
zlib -bundled-c-zlib -non-blocking-ffi -pkg-config
|
||||
index-state: hackage.haskell.org 2022-02-16T22:54:12Z
|
||||
index-state: hackage.haskell.org 2022-04-06T04:57:40Z
|
||||
|
@ -1103,3 +1103,48 @@ test-suite tests-hspec
|
||||
Test.WhereSpec
|
||||
Test.RunSQLSpec
|
||||
Test.InsertCheckPermissionSpec
|
||||
|
||||
test-suite tests-gdw-api
|
||||
import: common-all, common-exe
|
||||
type: exitcode-stdio-1.0
|
||||
build-depends:
|
||||
, aeson
|
||||
, autodocodec
|
||||
, autodocodec-openapi3
|
||||
, base
|
||||
, bytestring
|
||||
, deepseq
|
||||
, file-embed
|
||||
, gdw-api
|
||||
, hashable
|
||||
, hspec
|
||||
, hspec-core
|
||||
, hspec-expectations-pretty-diff
|
||||
, http-client
|
||||
, lens
|
||||
, lens-aeson
|
||||
, mtl
|
||||
, network-uri
|
||||
, openapi3
|
||||
, optparse-applicative
|
||||
, scientific
|
||||
, servant
|
||||
, servant-client
|
||||
, servant-client-core
|
||||
, servant-openapi3
|
||||
, text
|
||||
, unordered-containers
|
||||
, vector
|
||||
hs-source-dirs: tests-gdw-api
|
||||
-- Turning off optimizations is intentional; tests aren't
|
||||
-- performance sensitive and waiting for compilation is a problem.
|
||||
ghc-options: -Wall -O0 -threaded
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Command
|
||||
, Paths_graphql_engine
|
||||
, Test.Data
|
||||
, Test.QuerySpec
|
||||
, Test.QuerySpec.BasicSpec
|
||||
, Test.QuerySpec.RelationshipsSpec
|
||||
, Test.SchemaSpec
|
||||
|
@ -5,6 +5,8 @@ module Hasura.Backends.DataWrapper.API
|
||||
SchemaApi,
|
||||
QueryApi,
|
||||
openApiSchema,
|
||||
Routes (..),
|
||||
apiClient,
|
||||
)
|
||||
where
|
||||
|
||||
@ -13,6 +15,7 @@ import Data.OpenApi (OpenApi)
|
||||
import Hasura.Backends.DataWrapper.API.V0.API as V0
|
||||
import Servant.API
|
||||
import Servant.API.Generic
|
||||
import Servant.Client (Client, ClientM, client)
|
||||
import Servant.OpenApi
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@ -42,3 +45,7 @@ type Api = SchemaApi :<|> QueryApi
|
||||
-- | Provide an OpenApi 3.0 schema for the API
|
||||
openApiSchema :: OpenApi
|
||||
openApiSchema = toOpenApi (Proxy :: Proxy Api)
|
||||
|
||||
apiClient :: Client ClientM (NamedRoutes Routes)
|
||||
apiClient =
|
||||
client (Proxy @(NamedRoutes Routes))
|
||||
|
@ -62,7 +62,7 @@ instance HasCodec Query where
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
data RelField = RelField
|
||||
{ fieldMapping :: M.HashMap PrimaryKey ForeignKey,
|
||||
{ columnMapping :: M.HashMap PrimaryKey ForeignKey,
|
||||
query :: Query
|
||||
}
|
||||
deriving stock (Eq, Ord, Show, Generic, Data)
|
||||
@ -70,7 +70,7 @@ data RelField = RelField
|
||||
instance HasObjectCodec RelField where
|
||||
objectCodec =
|
||||
RelField
|
||||
<$> requiredField "field_mapping" "Mapping from local fields to remote fields" .= fieldMapping
|
||||
<$> requiredField "column_mapping" "Mapping from local fields to remote fields" .= columnMapping
|
||||
<*> requiredField "query" "Relationship query" .= query
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
@ -48,7 +48,7 @@ instance HasCodec TableInfo where
|
||||
codec =
|
||||
object "TableInfo" $
|
||||
TableInfo
|
||||
<$> requiredField "table_name" "The name of the table" .= dtiName
|
||||
<$> requiredField "name" "The name of the table" .= dtiName
|
||||
<*> requiredField "columns" "The columns of the table" .= dtiColumns
|
||||
<*> optionalFieldOrNull "primary_key" "The primary key of the table" .= dtiPrimaryKey
|
||||
<*> optionalFieldOrNull "description" "Description of the table" .= dtiDescription
|
||||
|
@ -38,7 +38,7 @@ spec = do
|
||||
(RelationshipField $ RelField fieldMapping query)
|
||||
[aesonQQ|
|
||||
{ "type": "relationship",
|
||||
"field_mapping": {"id": "my_foreign_id"},
|
||||
"column_mapping": {"id": "my_foreign_id"},
|
||||
"query": {"fields": {}, "from": "my_table_name"}
|
||||
}
|
||||
|]
|
||||
|
@ -24,7 +24,7 @@ spec = do
|
||||
testToFromJSONToSchema
|
||||
(TableInfo (TableName "my_table_name") [] Nothing Nothing)
|
||||
[aesonQQ|
|
||||
{ "table_name": "my_table_name",
|
||||
{ "name": "my_table_name",
|
||||
"columns": []
|
||||
}
|
||||
|]
|
||||
@ -37,7 +37,7 @@ spec = do
|
||||
(Just "my description")
|
||||
)
|
||||
[aesonQQ|
|
||||
{ "table_name": "my_table_name",
|
||||
{ "name": "my_table_name",
|
||||
"columns": [{"name": "id", "type": "string", "nullable": false}],
|
||||
"primary_key": "id",
|
||||
"description": "my description"
|
||||
|
197
server/tests-gdw-api/Command.hs
Normal file
197
server/tests-gdw-api/Command.hs
Normal file
@ -0,0 +1,197 @@
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Command
|
||||
( Command (..),
|
||||
TestOptions (..),
|
||||
AgentCapabilities (..),
|
||||
parseCommandLine,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Arrow (left)
|
||||
import Control.Lens (contains, modifying, use, (^.), _2)
|
||||
import Control.Lens.TH (makeLenses)
|
||||
import Control.Monad (when)
|
||||
import Control.Monad.State (State, runState)
|
||||
import Data.HashSet (HashSet)
|
||||
import Data.HashSet qualified as HashSet
|
||||
import Data.Text (Text)
|
||||
import Data.Text qualified as Text
|
||||
import Data.Version (showVersion)
|
||||
import Hasura.Backends.DataWrapper.API qualified as API
|
||||
import Options.Applicative
|
||||
import Paths_graphql_engine qualified as PackageInfo
|
||||
import Servant.Client (BaseUrl, parseBaseUrl)
|
||||
import Prelude
|
||||
|
||||
data Command
|
||||
= Test TestOptions
|
||||
| ExportOpenAPISpec
|
||||
|
||||
data TestOptions = TestOptions
|
||||
{ _toAgentBaseUrl :: BaseUrl,
|
||||
_toAgentCapabilities :: AgentCapabilities,
|
||||
_toParallelDegree :: Maybe Int,
|
||||
_toMatch :: Maybe String,
|
||||
_toSkip :: Maybe String
|
||||
}
|
||||
|
||||
data AgentCapabilities
|
||||
= AutoDetect
|
||||
| Explicit API.Capabilities
|
||||
|
||||
data CapabilitiesState = CapabilitiesState
|
||||
{ _csRemainingCapabilities :: HashSet Text,
|
||||
_csCapabilitiesEnquired :: HashSet Text
|
||||
}
|
||||
|
||||
$(makeLenses ''CapabilitiesState)
|
||||
|
||||
parseCommandLine :: IO Command
|
||||
parseCommandLine =
|
||||
execParser $
|
||||
info
|
||||
(helper <*> version <*> commandParser)
|
||||
( fullDesc
|
||||
<> header "Hasura GraphQL Data Wrapper Agent Test Utility"
|
||||
)
|
||||
|
||||
version :: Parser (a -> a)
|
||||
version =
|
||||
infoOption
|
||||
displayText
|
||||
( long "version"
|
||||
<> short 'v'
|
||||
<> help "Prints the version of the application and quits"
|
||||
<> hidden
|
||||
)
|
||||
where
|
||||
displayText = "Version " <> showVersion PackageInfo.version
|
||||
|
||||
commandParser :: Parser Command
|
||||
commandParser =
|
||||
subparser
|
||||
(testCommand <> exportOpenApiSpecCommand)
|
||||
where
|
||||
testCommand =
|
||||
command
|
||||
"test"
|
||||
( info
|
||||
(helper <*> testCommandParser)
|
||||
(progDesc "Executes a suite of tests against an agent to ensure its correct function")
|
||||
)
|
||||
exportOpenApiSpecCommand =
|
||||
command
|
||||
"export-openapi-spec"
|
||||
( info
|
||||
(helper <*> pure ExportOpenAPISpec)
|
||||
(progDesc "Exports the OpenAPI specification of the GDW API agents must implement")
|
||||
)
|
||||
|
||||
testOptionsParser :: Parser TestOptions
|
||||
testOptionsParser =
|
||||
TestOptions
|
||||
<$> option
|
||||
baseUrl
|
||||
( long "agent-base-url"
|
||||
<> short 'u'
|
||||
<> metavar "URL"
|
||||
<> help "The base URL of the GDW agent to be tested"
|
||||
)
|
||||
<*> agentCapabilitiesParser
|
||||
<*> optional
|
||||
( option
|
||||
positiveNonZeroInt
|
||||
( long "jobs"
|
||||
<> short 'j'
|
||||
<> metavar "INT"
|
||||
<> help "Run at most N parallelizable tests simultaneously (default: number of available processors)"
|
||||
)
|
||||
)
|
||||
<*> optional
|
||||
( option
|
||||
auto
|
||||
( long "match"
|
||||
<> short 'm'
|
||||
<> metavar "PATTERN"
|
||||
<> help "Only run tests that match given PATTERN"
|
||||
)
|
||||
)
|
||||
<*> optional
|
||||
( option
|
||||
auto
|
||||
( long "skip"
|
||||
<> short 's'
|
||||
<> metavar "PATTERN"
|
||||
<> help "Skip tests that match given PATTERN"
|
||||
)
|
||||
)
|
||||
|
||||
testCommandParser :: Parser Command
|
||||
testCommandParser = Test <$> testOptionsParser
|
||||
|
||||
baseUrl :: ReadM BaseUrl
|
||||
baseUrl = eitherReader $ left show . parseBaseUrl
|
||||
|
||||
positiveNonZeroInt :: ReadM Int
|
||||
positiveNonZeroInt =
|
||||
auto >>= \int ->
|
||||
if int <= 0 then readerError "Must be a positive, non-zero integer" else pure int
|
||||
|
||||
agentCapabilitiesParser :: Parser AgentCapabilities
|
||||
agentCapabilitiesParser =
|
||||
option
|
||||
agentCapabilities
|
||||
( long "capabilities"
|
||||
<> short 'c'
|
||||
<> metavar "CAPABILITIES"
|
||||
<> value AutoDetect
|
||||
<> help (Text.unpack helpText)
|
||||
)
|
||||
where
|
||||
helpText =
|
||||
"The capabilities that the agent has, to determine what tests to run. By default, they will be autodetected. The valid capabilities are: " <> allCapabilitiesText
|
||||
allCapabilitiesText =
|
||||
"[autodetect | none | " <> Text.intercalate "," (HashSet.toList allPossibleCapabilities) <> "]"
|
||||
|
||||
agentCapabilities :: ReadM AgentCapabilities
|
||||
agentCapabilities =
|
||||
str >>= \text -> do
|
||||
let capabilities = HashSet.fromList $ Text.strip <$> Text.split (== ',') text
|
||||
if HashSet.member "autodetect" capabilities
|
||||
then
|
||||
if HashSet.size capabilities == 1
|
||||
then pure AutoDetect
|
||||
else readerError "You can either autodetect capabilities or specify them manually, not both"
|
||||
else
|
||||
if HashSet.member "none" capabilities
|
||||
then
|
||||
if HashSet.size capabilities == 1
|
||||
then pure . Explicit . fst $ readCapabilities mempty
|
||||
else readerError "You cannot specify other capabilities when specifying none"
|
||||
else Explicit <$> readExplicitCapabilities capabilities
|
||||
where
|
||||
readExplicitCapabilities :: HashSet Text -> ReadM API.Capabilities
|
||||
readExplicitCapabilities providedCapabilities =
|
||||
let (capabilities, CapabilitiesState {..}) = readCapabilities providedCapabilities
|
||||
in if _csRemainingCapabilities /= mempty
|
||||
then readerError . Text.unpack $ "Unknown capabilities: " <> Text.intercalate "," (HashSet.toList _csRemainingCapabilities)
|
||||
else pure capabilities
|
||||
|
||||
readCapabilities :: HashSet Text -> (API.Capabilities, CapabilitiesState)
|
||||
readCapabilities providedCapabilities =
|
||||
flip runState (CapabilitiesState providedCapabilities mempty) $
|
||||
API.Capabilities
|
||||
<$> readCapability "relationships"
|
||||
|
||||
readCapability :: Text -> State CapabilitiesState Bool
|
||||
readCapability capability = do
|
||||
modifying csCapabilitiesEnquired $ HashSet.insert capability
|
||||
hasCapability <- use $ csRemainingCapabilities . contains capability
|
||||
when hasCapability $
|
||||
modifying csRemainingCapabilities $ HashSet.delete capability
|
||||
pure hasCapability
|
||||
|
||||
allPossibleCapabilities :: HashSet Text
|
||||
allPossibleCapabilities =
|
||||
readCapabilities mempty ^. _2 . csCapabilitiesEnquired
|
60
server/tests-gdw-api/Main.hs
Normal file
60
server/tests-gdw-api/Main.hs
Normal file
@ -0,0 +1,60 @@
|
||||
module Main (main) where
|
||||
|
||||
import Command (AgentCapabilities (..), Command (..), TestOptions (..), parseCommandLine)
|
||||
import Control.Exception (throwIO)
|
||||
import Control.Monad ((>=>))
|
||||
import Data.Aeson.Text (encodeToLazyText)
|
||||
import Data.Proxy (Proxy (..))
|
||||
import Data.Text.Lazy.IO qualified as Text
|
||||
import Hasura.Backends.DataWrapper.API (Routes (..), apiClient, openApiSchema)
|
||||
import Hasura.Backends.DataWrapper.API qualified as API
|
||||
import Network.HTTP.Client (defaultManagerSettings, newManager)
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client, ClientError, hoistClient, mkClientEnv, runClientM, (//))
|
||||
import Test.Hspec (Spec)
|
||||
import Test.Hspec.Core.Runner (runSpec)
|
||||
import Test.Hspec.Core.Util (filterPredicate)
|
||||
import Test.Hspec.Runner (Config (..), defaultConfig, evaluateSummary)
|
||||
import Test.QuerySpec qualified
|
||||
import Test.SchemaSpec qualified
|
||||
import Prelude
|
||||
|
||||
tests :: Client IO (NamedRoutes Routes) -> API.Capabilities -> Spec
|
||||
tests api capabilities = do
|
||||
Test.SchemaSpec.spec api capabilities
|
||||
Test.QuerySpec.spec api capabilities
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
command <- parseCommandLine
|
||||
case command of
|
||||
Test testOptions -> do
|
||||
api <- mkIOApiClient testOptions
|
||||
agentCapabilities <- getAgentCapabilities api (_toAgentCapabilities testOptions)
|
||||
runSpec (tests api agentCapabilities) (applyTestConfig defaultConfig testOptions) >>= evaluateSummary
|
||||
ExportOpenAPISpec ->
|
||||
Text.putStrLn $ encodeToLazyText openApiSchema
|
||||
|
||||
pure ()
|
||||
|
||||
mkIOApiClient :: TestOptions -> IO (Client IO (NamedRoutes Routes))
|
||||
mkIOApiClient TestOptions {..} = do
|
||||
manager <- newManager defaultManagerSettings
|
||||
let clientEnv = mkClientEnv manager _toAgentBaseUrl
|
||||
pure $ hoistClient (Proxy @(NamedRoutes Routes)) (flip runClientM clientEnv >=> throwClientError) apiClient
|
||||
|
||||
throwClientError :: Either ClientError a -> IO a
|
||||
throwClientError = either throwIO pure
|
||||
|
||||
getAgentCapabilities :: Client IO (NamedRoutes Routes) -> AgentCapabilities -> IO API.Capabilities
|
||||
getAgentCapabilities api = \case
|
||||
AutoDetect -> fmap API.srCapabilities $ api // _schema
|
||||
Explicit capabilities -> pure capabilities
|
||||
|
||||
applyTestConfig :: Config -> TestOptions -> Config
|
||||
applyTestConfig config TestOptions {..} =
|
||||
config
|
||||
{ configConcurrentJobs = _toParallelDegree,
|
||||
configFilterPredicate = filterPredicate <$> _toMatch,
|
||||
configSkipPredicate = filterPredicate <$> _toSkip
|
||||
}
|
31
server/tests-gdw-api/README.md
Normal file
31
server/tests-gdw-api/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
# GraphQL Data Wrappers Agent Tests
|
||||
This test suite provides a set of tests that is able to test any GDW agent that contains the Chinook data set to ensure the agent is behaving as expected. The test executable is designed to be distributable to customers building GDW agents, but is also useful to ensure Hasura's own agents are working correctly.
|
||||
|
||||
Not all tests will be appropriate for all agents. Agents self-describe their capabilities and only the tests appropriate for those capabilities will be run.
|
||||
|
||||
The executable also has the ability to export the OpenAPI spec of the GDW agent API so that customers can use that to ensure their agent complies with the API format.
|
||||
|
||||
## How to Use
|
||||
First, start your GDW agent and ensure it is populated with the Chinook data set. For example, you could start the Reference Agent by following the instructions in [its README](../../gdw-agents/reference/README.md).
|
||||
|
||||
To run the tests against the agent (for example), you must specify the agent's URL on the command line:
|
||||
|
||||
```
|
||||
cabal run test:tests-gdw-api -- test -u "http://localhost:8100"
|
||||
```
|
||||
|
||||
By default, the test suite will discover what capabilities the agent exposes by querying it. Otherwise, the user can use command line flags to specify which capabilities their agent has to ensure that it exposes the expected capabilities and that the test suite only runs the tests that correspond to those capabilities.
|
||||
|
||||
To set the agent's available the capabilities use `-c` and comma separate them:
|
||||
|
||||
```
|
||||
> cabal run test:tests-gdw-api -- test -u "http://localhost:8100" -c relationships
|
||||
```
|
||||
|
||||
If `-c` is omitted, the default value is `autodetect`. If you have no capabilities, you can specify `none`.
|
||||
|
||||
To export the OpenAPI spec, you can run this command, and the spec will be written to stdout.
|
||||
|
||||
```
|
||||
> cabal run test:tests-gdw-api -- export-openapi-spec
|
||||
```
|
90
server/tests-gdw-api/Test/Data.hs
Normal file
90
server/tests-gdw-api/Test/Data.hs
Normal file
@ -0,0 +1,90 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Test.Data
|
||||
( Artist (..),
|
||||
Album (..),
|
||||
schemaTables,
|
||||
artists,
|
||||
artistsAsJson,
|
||||
artistsAsJsonById,
|
||||
albums,
|
||||
albumsAsJson,
|
||||
sortBy,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Lens (ix, (^?))
|
||||
import Data.Aeson (FromJSON (..), Object, eitherDecodeStrict, withObject, (.:))
|
||||
import Data.Aeson.Lens (_Number)
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.FileEmbed (embedFile, makeRelativeToProject)
|
||||
import Data.HashMap.Strict (HashMap)
|
||||
import Data.HashMap.Strict qualified as HashMap
|
||||
import Data.Hashable (Hashable)
|
||||
import Data.List (sortOn)
|
||||
import Data.Maybe (mapMaybe)
|
||||
import Data.Scientific (Scientific)
|
||||
import Data.Text (Text)
|
||||
import GHC.Generics (Generic)
|
||||
import Hasura.Backends.DataWrapper.API (TableInfo (..))
|
||||
import Prelude
|
||||
|
||||
data Artist = Artist
|
||||
{ _artistId :: Int,
|
||||
_artistName :: Text
|
||||
}
|
||||
deriving stock (Eq, Show, Ord, Generic)
|
||||
deriving anyclass (Hashable)
|
||||
|
||||
instance FromJSON Artist where
|
||||
parseJSON = withObject "Artist" $ \obj ->
|
||||
Artist
|
||||
<$> obj .: "id"
|
||||
<*> obj .: "name"
|
||||
|
||||
data Album = Album
|
||||
{ _albumId :: Int,
|
||||
_albumTitle :: Text,
|
||||
_albumArtistId :: Int
|
||||
}
|
||||
deriving stock (Eq, Show, Ord, Generic)
|
||||
deriving anyclass (Hashable)
|
||||
|
||||
instance FromJSON Album where
|
||||
parseJSON = withObject "Album" $ \obj ->
|
||||
Album
|
||||
<$> obj .: "id"
|
||||
<*> obj .: "title"
|
||||
<*> obj .: "artist_id"
|
||||
|
||||
schemaBS :: ByteString
|
||||
schemaBS = $(makeRelativeToProject "tests-gdw-api/Test/Data/schema-tables.json" >>= embedFile)
|
||||
|
||||
artistsBS :: ByteString
|
||||
artistsBS = $(makeRelativeToProject "tests-gdw-api/Test/Data/artists.json" >>= embedFile)
|
||||
|
||||
albumsBS :: ByteString
|
||||
albumsBS = $(makeRelativeToProject "tests-gdw-api/Test/Data/albums.json" >>= embedFile)
|
||||
|
||||
schemaTables :: [TableInfo]
|
||||
schemaTables = sortOn dtiName . either error id . eitherDecodeStrict $ schemaBS
|
||||
|
||||
artists :: [Artist]
|
||||
artists = sortOn _artistId . either error id . eitherDecodeStrict $ artistsBS
|
||||
|
||||
artistsAsJson :: [Object]
|
||||
artistsAsJson = sortBy "id" . either error id . eitherDecodeStrict $ artistsBS
|
||||
|
||||
artistsAsJsonById :: HashMap Scientific Object
|
||||
artistsAsJsonById =
|
||||
HashMap.fromList $ mapMaybe (\artist -> (,artist) <$> artist ^? ix "id" . _Number) artistsAsJson
|
||||
|
||||
albums :: [Album]
|
||||
albums = sortOn _albumId . either error id . eitherDecodeStrict $ albumsBS
|
||||
|
||||
albumsAsJson :: [Object]
|
||||
albumsAsJson = sortBy "id" . either error id . eitherDecodeStrict $ albumsBS
|
||||
|
||||
sortBy :: Text -> [Object] -> [Object]
|
||||
sortBy propName = sortOn (^? ix propName)
|
1702
server/tests-gdw-api/Test/Data/albums.json
Normal file
1702
server/tests-gdw-api/Test/Data/albums.json
Normal file
File diff suppressed because it is too large
Load Diff
1102
server/tests-gdw-api/Test/Data/artists.json
Normal file
1102
server/tests-gdw-api/Test/Data/artists.json
Normal file
File diff suppressed because it is too large
Load Diff
46
server/tests-gdw-api/Test/Data/schema-tables.json
Normal file
46
server/tests-gdw-api/Test/Data/schema-tables.json
Normal file
@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"name": "artists",
|
||||
"primary_key": "id",
|
||||
"description": "Collection of artists of music",
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "number",
|
||||
"nullable": false,
|
||||
"description": "Artist primary key identifier"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"nullable": false,
|
||||
"description": "The name of the artist"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "albums",
|
||||
"primary_key": "id",
|
||||
"description": "Collection of music albums created by artists",
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"type": "number",
|
||||
"nullable": false,
|
||||
"description": "Album primary key identifier"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"nullable": false,
|
||||
"description": "The title of the album"
|
||||
},
|
||||
{
|
||||
"name": "artist_id",
|
||||
"type": "number",
|
||||
"nullable": false,
|
||||
"description": "The ID of the artist that created this album"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
17
server/tests-gdw-api/Test/QuerySpec.hs
Normal file
17
server/tests-gdw-api/Test/QuerySpec.hs
Normal file
@ -0,0 +1,17 @@
|
||||
module Test.QuerySpec (spec) where
|
||||
|
||||
import Control.Monad (when)
|
||||
import Hasura.Backends.DataWrapper.API (Capabilities (..), Routes (..))
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client)
|
||||
import Test.Hspec
|
||||
import Test.QuerySpec.BasicSpec qualified
|
||||
import Test.QuerySpec.RelationshipsSpec qualified
|
||||
import Prelude
|
||||
|
||||
spec :: Client IO (NamedRoutes Routes) -> Capabilities -> Spec
|
||||
spec api Capabilities {..} = do
|
||||
describe "query API" do
|
||||
Test.QuerySpec.BasicSpec.spec api
|
||||
when (dcRelationships) $
|
||||
Test.QuerySpec.RelationshipsSpec.spec api
|
218
server/tests-gdw-api/Test/QuerySpec/BasicSpec.hs
Normal file
218
server/tests-gdw-api/Test/QuerySpec/BasicSpec.hs
Normal file
@ -0,0 +1,218 @@
|
||||
module Test.QuerySpec.BasicSpec (spec) where
|
||||
|
||||
import Autodocodec.Extended (ValueWrapper (..), ValueWrapper2 (..), ValueWrapper3 (ValueWrapper3))
|
||||
import Control.Lens (ix, (^?))
|
||||
import Data.Aeson.Lens (AsNumber (_Number), AsPrimitive (_String))
|
||||
import Data.HashMap.Strict qualified as HashMap
|
||||
import Data.List (sortOn)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Ord (Down (..))
|
||||
import Data.Text (Text)
|
||||
import Hasura.Backends.DataWrapper.API
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client, (//))
|
||||
import Test.Data qualified as Data
|
||||
import Test.Hspec (Spec, describe, it)
|
||||
import Test.Hspec.Expectations.Pretty (shouldBe)
|
||||
import Prelude
|
||||
|
||||
spec :: Client IO (NamedRoutes Routes) -> Spec
|
||||
spec api = describe "Basic Queries" $ do
|
||||
describe "Column Fields" $ do
|
||||
it "can query for a list of artists" $ do
|
||||
let query = artistsQuery
|
||||
receivedArtists <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let expectedArtists = Data.artistsAsJson
|
||||
receivedArtists `shouldBe` expectedArtists
|
||||
|
||||
it "can query for a list of albums with a subset of columns" $ do
|
||||
let fields = HashMap.fromList [("artist_id", columnField "artist_id"), ("title", columnField "title")]
|
||||
let query = albumsQuery {fields}
|
||||
receivedAlbums <- fmap (Data.sortBy "title" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let filterToRequiredProperties =
|
||||
HashMap.filterWithKey (\propName _value -> propName == "artist_id" || propName == "title")
|
||||
|
||||
let expectedAlbums = Data.sortBy "title" $ filterToRequiredProperties <$> Data.albumsAsJson
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "can project columns into fields with different names" $ do
|
||||
let fields = HashMap.fromList [("artist_id", columnField "id"), ("artist_name", columnField "name")]
|
||||
let query = artistsQuery {fields}
|
||||
receivedArtists <- fmap (Data.sortBy "artist_id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let renameProperties =
|
||||
HashMap.mapKeys
|
||||
( \case
|
||||
"id" -> "artist_id"
|
||||
"name" -> "artist_name"
|
||||
other -> other
|
||||
)
|
||||
|
||||
let expectedArtists = Data.sortBy "artist_id" $ renameProperties <$> Data.artistsAsJson
|
||||
receivedArtists `shouldBe` expectedArtists
|
||||
|
||||
describe "Limit & Offset" $ do
|
||||
it "can use limit and offset to paginate results" $ do
|
||||
let allQuery = artistsQuery
|
||||
let page1Query = artistsQuery {limit = Just 10, offset = Just 0}
|
||||
let page2Query = artistsQuery {limit = Just 10, offset = Just 10}
|
||||
|
||||
allArtists <- fmap getQueryResponse $ api // _query $ allQuery
|
||||
page1Artists <- fmap getQueryResponse $ api // _query $ page1Query
|
||||
page2Artists <- fmap getQueryResponse $ api // _query $ page2Query
|
||||
|
||||
page1Artists `shouldBe` take 10 allArtists
|
||||
page2Artists `shouldBe` take 10 (drop 10 allArtists)
|
||||
|
||||
describe "Order By" $ do
|
||||
it "can use order by to order results in ascending order" $ do
|
||||
let orderBy = OrderBy (ColumnName "title") Ascending :| []
|
||||
let query = albumsQuery {orderBy = Just orderBy}
|
||||
receivedAlbums <- fmap getQueryResponse $ api // _query $ query
|
||||
|
||||
let expectedAlbums = sortOn (^? ix "title") Data.albumsAsJson
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "can use order by to order results in descending order" $ do
|
||||
let orderBy = OrderBy (ColumnName "title") Descending :| []
|
||||
let query = albumsQuery {orderBy = Just orderBy}
|
||||
receivedAlbums <- fmap getQueryResponse $ api // _query $ query
|
||||
|
||||
let expectedAlbums = sortOn (Down . (^? ix "title")) Data.albumsAsJson
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "can use multiple order bys to order results" $ do
|
||||
let orderBy = OrderBy (ColumnName "artist_id") Ascending :| [OrderBy (ColumnName "title") Descending]
|
||||
let query = albumsQuery {orderBy = Just orderBy}
|
||||
receivedAlbums <- fmap getQueryResponse $ api // _query $ query
|
||||
|
||||
let expectedAlbums =
|
||||
sortOn (\album -> (album ^? ix "artist_id", Down (album ^? ix "title"))) Data.albumsAsJson
|
||||
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
describe "Where" $ do
|
||||
it "can filter using an equality expression" $ do
|
||||
let where' = Equal (ValueWrapper2 (Column (ValueWrapper (ColumnName "id"))) (Literal (ValueWrapper (Number 2))))
|
||||
let query = albumsQuery {where_ = Just where'}
|
||||
receivedAlbums <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let expectedAlbums =
|
||||
filter ((== Just 2) . (^? ix "id" . _Number)) Data.albumsAsJson
|
||||
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "can filter using an inequality expression" $ do
|
||||
let where' = NotEqual (ValueWrapper2 (Column (ValueWrapper (ColumnName "id"))) (Literal (ValueWrapper (Number 2))))
|
||||
let query = albumsQuery {where_ = Just where'}
|
||||
receivedAlbums <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let expectedAlbums =
|
||||
filter ((/= Just 2) . (^? ix "id" . _Number)) Data.albumsAsJson
|
||||
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "can filter using an in expression" $ do
|
||||
let where' = In (ValueWrapper2 (Column (ValueWrapper (ColumnName "id"))) [Number 2, Number 3])
|
||||
let query = albumsQuery {where_ = Just where'}
|
||||
receivedAlbums <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let expectedAlbums =
|
||||
filter (flip elem [Just 2, Just 3] . (^? ix "id" . _Number)) Data.albumsAsJson
|
||||
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "can negate an in expression filter using a not expression" $ do
|
||||
let where' = Not (ValueWrapper (In (ValueWrapper2 (Column (ValueWrapper (ColumnName "id"))) [Number 2, Number 3])))
|
||||
let query = albumsQuery {where_ = Just where'}
|
||||
receivedAlbums <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let expectedAlbums =
|
||||
filter (flip notElem [Just 2, Just 3] . (^? ix "id" . _Number)) Data.albumsAsJson
|
||||
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "can combine filters using an and expression" $ do
|
||||
let where1 = Equal (ValueWrapper2 (Column (ValueWrapper (ColumnName "artist_id"))) (Literal (ValueWrapper (Number 58))))
|
||||
let where2 = Equal (ValueWrapper2 (Column (ValueWrapper (ColumnName "title"))) (Literal (ValueWrapper (String "Stormbringer"))))
|
||||
let where' = And (ValueWrapper [where1, where2])
|
||||
let query = albumsQuery {where_ = Just where'}
|
||||
receivedAlbums <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let expectedAlbums =
|
||||
filter
|
||||
( \album ->
|
||||
(album ^? ix "artist_id" . _Number == Just 58) && (album ^? ix "title" . _String == Just "Stormbringer")
|
||||
)
|
||||
Data.albumsAsJson
|
||||
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "can combine filters using an or expression" $ do
|
||||
let where1 = Equal (ValueWrapper2 (Column (ValueWrapper (ColumnName "id"))) (Literal (ValueWrapper (Number 2))))
|
||||
let where2 = Equal (ValueWrapper2 (Column (ValueWrapper (ColumnName "id"))) (Literal (ValueWrapper (Number 3))))
|
||||
let where' = Or (ValueWrapper [where1, where2])
|
||||
let query = albumsQuery {where_ = Just where'}
|
||||
receivedAlbums <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let expectedAlbums =
|
||||
filter (flip elem [Just 2, Just 3] . (^? ix "id" . _Number)) Data.albumsAsJson
|
||||
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "can filter by applying the greater than operator" $ do
|
||||
let where' = ApplyOperator (ValueWrapper3 GreaterThan (Column (ValueWrapper (ColumnName "id"))) (Literal (ValueWrapper (Number 300))))
|
||||
let query = albumsQuery {where_ = Just where'}
|
||||
receivedAlbums <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let expectedAlbums =
|
||||
filter ((> Just 300) . (^? ix "id" . _Number)) Data.albumsAsJson
|
||||
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "can filter by applying the greater than or equal operator" $ do
|
||||
let where' = ApplyOperator (ValueWrapper3 GreaterThanOrEqual (Column (ValueWrapper (ColumnName "id"))) (Literal (ValueWrapper (Number 300))))
|
||||
let query = albumsQuery {where_ = Just where'}
|
||||
receivedAlbums <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let expectedAlbums =
|
||||
filter ((>= Just 300) . (^? ix "id" . _Number)) Data.albumsAsJson
|
||||
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "can filter by applying the less than operator" $ do
|
||||
let where' = ApplyOperator (ValueWrapper3 LessThan (Column (ValueWrapper (ColumnName "id"))) (Literal (ValueWrapper (Number 100))))
|
||||
let query = albumsQuery {where_ = Just where'}
|
||||
receivedAlbums <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let expectedAlbums =
|
||||
filter ((< Just 100) . (^? ix "id" . _Number)) Data.albumsAsJson
|
||||
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "can filter by applying the less than or equal operator" $ do
|
||||
let where' = ApplyOperator (ValueWrapper3 LessThanOrEqual (Column (ValueWrapper (ColumnName "id"))) (Literal (ValueWrapper (Number 100))))
|
||||
let query = albumsQuery {where_ = Just where'}
|
||||
receivedAlbums <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let expectedAlbums =
|
||||
filter ((<= Just 100) . (^? ix "id" . _Number)) Data.albumsAsJson
|
||||
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
artistsQuery :: Query
|
||||
artistsQuery =
|
||||
let fields = HashMap.fromList [("id", columnField "id"), ("name", columnField "name")]
|
||||
tableName = TableName "artists"
|
||||
in Query fields tableName Nothing Nothing Nothing Nothing
|
||||
|
||||
albumsQuery :: Query
|
||||
albumsQuery =
|
||||
let fields = HashMap.fromList [("id", columnField "id"), ("artist_id", columnField "artist_id"), ("title", columnField "title")]
|
||||
tableName = TableName "albums"
|
||||
in Query fields tableName Nothing Nothing Nothing Nothing
|
||||
|
||||
columnField :: Text -> Field
|
||||
columnField = ColumnField . ValueWrapper . ColumnName
|
93
server/tests-gdw-api/Test/QuerySpec/RelationshipsSpec.hs
Normal file
93
server/tests-gdw-api/Test/QuerySpec/RelationshipsSpec.hs
Normal file
@ -0,0 +1,93 @@
|
||||
module Test.QuerySpec.RelationshipsSpec (spec) where
|
||||
|
||||
import Autodocodec.Extended (ValueWrapper (..))
|
||||
import Control.Lens (ix, (^?))
|
||||
import Data.Aeson (Object, Value (..))
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Aeson.Lens (_Number)
|
||||
import Data.HashMap.Strict qualified as HashMap
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Text (Text)
|
||||
import Data.Vector qualified as Vector
|
||||
import Hasura.Backends.DataWrapper.API
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client, (//))
|
||||
import Test.Data qualified as Data
|
||||
import Test.Hspec (Spec, describe, it)
|
||||
import Test.Hspec.Expectations.Pretty (shouldBe)
|
||||
import Prelude
|
||||
|
||||
spec :: Client IO (NamedRoutes Routes) -> Spec
|
||||
spec api = describe "Relationship Queries" $ do
|
||||
it "perform a many to one query by joining artist to albums" $ do
|
||||
let query = albumsWithArtistQuery id
|
||||
receivedAlbums <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let joinInArtist (album :: Object) =
|
||||
let artist = (album ^? ix "artist_id" . _Number) >>= \artistId -> Data.artistsAsJsonById ^? ix artistId
|
||||
artistPropVal = maybe J.Null (Array . Vector.singleton . Object) artist
|
||||
in HashMap.insert "artist" artistPropVal album
|
||||
let removeArtistId = HashMap.delete "artist_id"
|
||||
|
||||
let expectedAlbums = (removeArtistId . joinInArtist) <$> Data.albumsAsJson
|
||||
receivedAlbums `shouldBe` expectedAlbums
|
||||
|
||||
it "perform a one to many query by joining albums to artists" $ do
|
||||
let query = artistsWithAlbumsQuery id
|
||||
receivedArtists <- fmap (Data.sortBy "id" . getQueryResponse) $ api // _query $ query
|
||||
|
||||
let joinInAlbums (artist :: Object) =
|
||||
let artistId = artist ^? ix "id" . _Number
|
||||
albums = maybe [] (\artistId' -> filter (\album -> album ^? ix "artist_id" . _Number == Just artistId') Data.albumsAsJson) artistId
|
||||
albums' = Object . HashMap.delete "artist_id" <$> albums
|
||||
in HashMap.insert "albums" (Array . Vector.fromList $ albums') artist
|
||||
|
||||
let expectedAlbums = joinInAlbums <$> Data.artistsAsJson
|
||||
receivedArtists `shouldBe` expectedAlbums
|
||||
|
||||
albumsWithArtistQuery :: (Query -> Query) -> Query
|
||||
albumsWithArtistQuery modifySubquery =
|
||||
let joinFieldMapping =
|
||||
HashMap.fromList
|
||||
[ (PrimaryKey $ ColumnName "artist_id", ForeignKey $ ColumnName "id")
|
||||
]
|
||||
artistsSubquery = modifySubquery artistsQuery
|
||||
fields =
|
||||
HashMap.fromList
|
||||
[ ("id", columnField "id"),
|
||||
("title", columnField "title"),
|
||||
("artist", RelationshipField $ RelField joinFieldMapping artistsSubquery)
|
||||
]
|
||||
in albumsQuery {fields}
|
||||
|
||||
artistsWithAlbumsQuery :: (Query -> Query) -> Query
|
||||
artistsWithAlbumsQuery modifySubquery =
|
||||
let joinFieldMapping =
|
||||
HashMap.fromList
|
||||
[ (PrimaryKey $ ColumnName "id", ForeignKey $ ColumnName "artist_id")
|
||||
]
|
||||
albumFields = HashMap.fromList [("id", columnField "id"), ("title", columnField "title")]
|
||||
albumsSort = OrderBy (ColumnName "id") Ascending :| []
|
||||
albumsSubquery = modifySubquery (albumsQuery {fields = albumFields, orderBy = Just albumsSort})
|
||||
fields =
|
||||
HashMap.fromList
|
||||
[ ("id", columnField "id"),
|
||||
("name", columnField "name"),
|
||||
("albums", RelationshipField $ RelField joinFieldMapping albumsSubquery)
|
||||
]
|
||||
in artistsQuery {fields}
|
||||
|
||||
artistsQuery :: Query
|
||||
artistsQuery =
|
||||
let fields = HashMap.fromList [("id", columnField "id"), ("name", columnField "name")]
|
||||
tableName = TableName "artists"
|
||||
in Query fields tableName Nothing Nothing Nothing Nothing
|
||||
|
||||
albumsQuery :: Query
|
||||
albumsQuery =
|
||||
let fields = HashMap.fromList [("id", columnField "id"), ("artist_id", columnField "artist_id"), ("title", columnField "title")]
|
||||
tableName = TableName "albums"
|
||||
in Query fields tableName Nothing Nothing Nothing Nothing
|
||||
|
||||
columnField :: Text -> Field
|
||||
columnField = ColumnField . ValueWrapper . ColumnName
|
20
server/tests-gdw-api/Test/SchemaSpec.hs
Normal file
20
server/tests-gdw-api/Test/SchemaSpec.hs
Normal file
@ -0,0 +1,20 @@
|
||||
module Test.SchemaSpec (spec) where
|
||||
|
||||
import Data.List (sortOn)
|
||||
import Hasura.Backends.DataWrapper.API (Capabilities, Routes (..), SchemaResponse (..), TableInfo (..))
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client, (//))
|
||||
import Test.Data qualified as Data
|
||||
import Test.Hspec (Spec, describe, it)
|
||||
import Test.Hspec.Expectations.Pretty (shouldBe)
|
||||
import Prelude
|
||||
|
||||
spec :: Client IO (NamedRoutes Routes) -> Capabilities -> Spec
|
||||
spec api expectedCapabilities = describe "schema API" $ do
|
||||
it "returns the expected capabilities" $ do
|
||||
capabilities <- fmap srCapabilities $ api // _schema
|
||||
capabilities `shouldBe` expectedCapabilities
|
||||
|
||||
it "returns Chinook schema" $ do
|
||||
tables <- fmap (sortOn dtiName . srTables) $ api // _schema
|
||||
tables `shouldBe` Data.schemaTables
|
Loading…
Reference in New Issue
Block a user