graphql-engine/server/tests-hspec/Harness/Backend/DataConnector.hs
Solomon baf196daa6 [GDW-111] Mockable GDC Agent for integration testing
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4802
GitOrigin-RevId: 36cdfcff4a864598a4d10e746ecdf93dae2b4c02
2022-06-25 19:06:54 +00:00

168 lines
5.9 KiB
Haskell

{-# LANGUAGE QuasiQuotes #-}
-- | Data Connector helpers.
module Harness.Backend.DataConnector
( -- * Reference Agent
setupFixture,
teardown,
defaultBackendConfig,
-- * Mock Agent
MockConfig (..),
MockAgentEnvironment (..),
TestCase (..),
mockBackendConfig,
chinookMock,
runMockedTest,
mkLocalTestEnvironmentMock,
setupMock,
teardownMock,
)
where
--------------------------------------------------------------------------------
import Control.Concurrent (ThreadId, forkIO, killThread)
import Data.Aeson qualified as Aeson
import Data.IORef qualified as I
import Harness.Backend.DataConnector.MockAgent
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.Quoter.Yaml (shouldReturnYaml, yaml)
import Harness.Test.Context (BackendType (DataConnector), Options, defaultBackendTypeString)
import Harness.TestEnvironment (TestEnvironment)
import Hasura.Backends.DataConnector.API qualified as API
import Hasura.Prelude
import Test.Hspec (shouldBe)
--------------------------------------------------------------------------------
defaultBackendConfig :: Aeson.Value
defaultBackendConfig =
let backendType = defaultBackendTypeString $ DataConnector
in [yaml|
dataconnector:
*backendType:
uri: "http://127.0.0.1:65005/"
|]
mockBackendConfig :: Aeson.Value
mockBackendConfig =
let backendType = defaultBackendTypeString $ DataConnector
in [yaml|
dataconnector:
*backendType:
uri: "http://127.0.0.1:65006/"
|]
--------------------------------------------------------------------------------
-- Chinook Agent
-- | Setup the schema given source metadata and backend config.
setupFixture :: Aeson.Value -> Aeson.Value -> (TestEnvironment, ()) -> IO ()
setupFixture sourceMetadata backendConfig (testEnvironment, _) = do
-- Clear and reconfigure the metadata
GraphqlEngine.setSource testEnvironment sourceMetadata (Just backendConfig)
-- | Teardown the schema and tracking in the most expected way.
teardown :: (TestEnvironment, ()) -> IO ()
teardown (testEnvironment, _) = do
GraphqlEngine.clearMetadata testEnvironment
--------------------------------------------------------------------------------
-- Mock Agent
--
-- Current Design:
--
-- The Mock Agent receives at startup a 'I.IORef MockConfig' and an
-- empty 'I.IORef (Maybe API.Query)'.
--
-- The 'MockConfig' contains static responses for all the Agent
-- endpoints. When the agent handlers are called, they read from the
-- 'I.IORef MockConfig' and return the value revelevant to the handler.
--
-- In the case of the Query Handler, before returning the mock value,
-- the handler writes the incoming 'API.Query' value to the 'I.IORef
-- (Maybe API.Query)'.
--
-- The two 'I.IORef' values are constructed when we build the local
-- test environment in 'mkLocalTestEnvironmentMock' and call
-- 'runMockServer'. We return '(I.IORef MockConfig, I.IORef (Maybe
-- API.Query), ThreadId)' so that we can use the 'I.IORef' values in
-- test setups and the 'ThreadId' in teardown (to kill the agent
-- thread).
--
-- NOTE: In the current design we use the same agent and the same
-- 'I.IORef's for all tests. This is safe because the tests are run
-- sequentially. Parallelizing the test suite would break the testing
-- setup for 'DataConnector'.
--
-- If a parallelization refactor occurs, we will need to construct a
-- mock agent and corresponding 'I.IORef's for each individual
-- test. To make this work we would likely need to use hspec hooks on
-- the individual tests to spawn and destroy a mock agent and
-- associated 'I.IORef's.
data MockAgentEnvironment = MockAgentEnvironment
{ maeConfig :: I.IORef MockConfig,
maeQuery :: I.IORef (Maybe API.QueryRequest),
maeThreadId :: ThreadId
}
-- | Create the 'I.IORef's and launch the servant mock agent.
mkLocalTestEnvironmentMock :: TestEnvironment -> IO MockAgentEnvironment
mkLocalTestEnvironmentMock _ = do
maeConfig <- I.newIORef chinookMock
maeQuery <- I.newIORef Nothing
maeThreadId <- forkIO $ runMockServer maeConfig maeQuery
pure $ MockAgentEnvironment {..}
-- | Load the agent schema into HGE.
setupMock :: Aeson.Value -> Aeson.Value -> (TestEnvironment, MockAgentEnvironment) -> IO ()
setupMock sourceMetadata backendConfig (testEnvironment, _mockAgentEnvironment) = do
-- Clear and reconfigure the metadata
GraphqlEngine.setSource testEnvironment sourceMetadata (Just backendConfig)
-- | Teardown the schema and kill the servant mock agent.
teardownMock :: (TestEnvironment, MockAgentEnvironment) -> IO ()
teardownMock (testEnvironment, MockAgentEnvironment {..}) = do
GraphqlEngine.clearMetadata testEnvironment
killThread maeThreadId
-- | Mock Agent test case input.
data TestCase = TestCase
{ -- | The Mock configuration for the agent
_given :: MockConfig,
-- | The Graphql Query to test
_whenRequest :: Aeson.Value,
-- | The expected HGE 'API.Query' value to be provided to the
-- agent. A @Nothing@ value indicates that the 'API.Query'
-- assertion should be skipped.
_whenQuery :: Maybe API.QueryRequest,
-- | The expected GQL response and outgoing HGE 'API.Query'
_then :: Aeson.Value
}
-- | Test runner for the Mock Agent. 'runMockedTest' sets the mocked
-- value in the agent, fires a GQL request, then asserts on the
-- expected response and 'API.Query' value.
runMockedTest :: Options -> TestCase -> (TestEnvironment, MockAgentEnvironment) -> IO ()
runMockedTest opts TestCase {..} (testEnvironment, MockAgentEnvironment {..}) = do
-- Set the Agent with the 'MockConfig'
I.writeIORef maeConfig _given
-- Execute the GQL Query and assert on the result
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
_whenRequest
)
_then
-- Read the logged 'API.QueryRequest' from the Agent
query <- I.readIORef maeQuery
I.writeIORef maeQuery Nothing
-- Assert that the 'API.QueryRequest' was constructed how we expected.
onJust _whenQuery ((query `shouldBe`) . Just)