Skip to content

Commit e41d033

Browse files
committed
test(refactor): Add Toxiproxy Hspec test infrastructure
1 parent f2d919f commit e41d033

7 files changed

Lines changed: 459 additions & 16 deletions

File tree

nix/tools/tests.nix

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ let
4242
}
4343
''
4444
${withTools.withPg} -f test/observability/fixtures/load.sql \
45-
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability -- "''${_arg_leftovers[@]}"
45+
${withTools.withToxiproxyPgProxy} \
46+
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability -- "''${_arg_leftovers[@]}"
4647
'';
4748

4849
testDoctests =

postgrest.cabal

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,22 +311,32 @@ test-suite observability
311311
Observation.JwtCache
312312
Observation.MetricsSpec
313313
Observation.SchemaCacheSpec
314+
Observation.ToxiSpec
315+
Toxiproxy
314316
build-depends: base >= 4.9 && < 4.22
317+
, aeson >= 2.0.3 && < 2.3
315318
, base64-bytestring >= 1 && < 1.3
316319
, bytestring >= 0.10.8 && < 0.13
320+
, containers >= 0.5.7 && < 0.8
317321
, hasql-pool >= 1.0.1 && <= 1.3.0.4
318322
, hasql-transaction >= 1.0.1 && <= 1.2.1
319323
, hspec >= 2.3 && < 2.12
320324
, hspec-expectations >= 0.8.4 && < 0.9
321325
, hspec-wai >= 0.10 && < 0.12
322326
, hspec-wai-json >= 0.10 && < 0.12
327+
, http-client >= 0.7.19 && < 0.8
323328
, http-types >= 0.12.3 && < 0.13
324329
, jose-jwt >= 0.9.6 && < 0.11
330+
, monad-control >= 1.0.1 && < 1.1
325331
, postgrest
326332
, prometheus-client >= 1.1.1 && < 1.2.0
327333
, protolude >= 0.3.1 && < 0.4
334+
, servant-client >= 0.20.3.0 && < 0.21
335+
, servant >= 0.20.3.0 && < 0.21
328336
, text >= 1.2.2 && < 2.2
337+
, transformers-base >= 0.4.4 && < 0.5
329338
, wai >= 3.2.1 && < 3.3
339+
, wai-extra >= 3.1.8 && < 3.2
330340
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
331341
-fno-spec-constr -optP-Wno-nonportable-include-path
332342
-fwrite-ide-info

src/PostgREST/AppState.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
module PostgREST.AppState
77
( AppState
88
, destroy
9+
, flushPool
910
, getConfig
1011
, getSchemaCache
1112
, getMainThreadId

test/observability/Main.hs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ import PostgREST.SchemaCache (querySchemaCache)
1818
import qualified Observation.JwtCache
1919
import qualified Observation.MetricsSpec
2020

21+
import qualified Data.Text as T
2122
import qualified Observation.SchemaCacheSpec
23+
import qualified Observation.ToxiSpec
2224
import ObsHelper
2325
import PostgREST.Observation (Observation (HasqlPoolObs))
2426
import Protolude hiding (toList, toS)
27+
import qualified System.Environment as System
2528
import Test.Hspec
2629

2730
main :: IO ()
@@ -34,40 +37,47 @@ main = do
3437
-- this means we have another thread running for the entire duration of the spec but this shouldn't be a problem since Haskell green threads are lightweight
3538
void $ forkIO $ forever $ readChan poolChan
3639
metricsState <- Metrics.init (configDbPoolSize testCfg)
40+
toxiProxyName <- T.pack <$> System.getEnv "TOXI_PROXY_NAME"
41+
toxiPgPort <- T.pack <$> System.getEnv "TOXI_PGPORT"
42+
pgPort <- T.pack <$> System.getEnv "PGPORT"
43+
let toxiCfg = testCfg { configDbUri = "postgresql://localhost:" <> toxiPgPort }
3744
pool <- P.acquire $ P.settings
3845
[ P.size 3
3946
, P.acquisitionTimeout 10
4047
, P.agingTimeout 60
4148
, P.idlenessTimeout 60
42-
, P.staticConnectionSettings $ toConnectionSettings identity testCfg
49+
, P.staticConnectionSettings $ toConnectionSettings identity toxiCfg
4350
-- make sure metrics are updated and pool observations published to poolChan
4451
, P.observationHandler $ (writeChan poolChan <> Metrics.observationMetrics metricsState) . HasqlPoolObs
4552
]
4653

4754
actualPgVersion <- either (panic . show) id <$> P.use pool queryPgVersion
4855

4956
-- cached schema cache so most tests run fast
50-
baseSchemaCache <- loadSCache pool testCfg
57+
baseSchemaCache <- loadSCache pool toxiCfg
5158
loggerState <- Logger.init
5259

5360
let
54-
initApp sCache config = do
61+
initApp sCache configure =
62+
let config = configure toxiCfg in do
5563
-- duplicate poolChan as a starting point
5664
obsChan <- dupChan poolChan
5765
stateObsChan <- newObsChan obsChan
5866
appState <- AppState.initWithPool pool config loggerState metricsState (Metrics.observationMetrics metricsState <> writeChan obsChan)
5967
AppState.putPgVersion appState actualPgVersion
6068
AppState.putSchemaCache appState (Just sCache)
61-
return (SpecState appState metricsState stateObsChan, postgrest (configLogLevel config) appState (pure ()))
69+
return (SpecState appState metricsState stateObsChan $ testToxiProxy toxiProxyName toxiPgPort pgPort, postgrest (configLogLevel config) appState (pure ()))
6270

6371
-- Run all test modules
6472
hspec $ do
6573
before (initApp baseSchemaCache testCfgJwtCache) $
6674
describe "Observation.JwtCacheObs" Observation.JwtCache.spec
67-
before (initApp baseSchemaCache testCfg) $
68-
describe "Feature.MetricsSpec" Observation.MetricsSpec.spec
69-
before (initApp baseSchemaCache testCfg) $
70-
describe "Feature.SchemaCacheSpec" Observation.SchemaCacheSpec.spec
75+
76+
traverse_ (before (initApp baseSchemaCache identity) . uncurry describe) [
77+
("Observation.MetricsSpec", Observation.MetricsSpec.spec)
78+
, ("Observation.SchemaCacheSpec", Observation.SchemaCacheSpec.spec)
79+
, ("Observation.ToxiSpec", Observation.ToxiSpec.spec)
80+
]
7181

7282
where
7383
loadSCache pool conf =

test/observability/ObsHelper.hs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
{-# LANGUAGE FlexibleContexts #-}
55
{-# LANGUAGE FlexibleInstances #-}
66
{-# LANGUAGE LambdaCase #-}
7+
{-# LANGUAGE MultiParamTypeClasses #-}
78
{-# LANGUAGE RankNTypes #-}
89
{-# LANGUAGE ScopedTypeVariables #-}
910
{-# LANGUAGE TupleSections #-}
1011
{-# LANGUAGE TypeApplications #-}
12+
{-# LANGUAGE TypeFamilies #-}
1113
{-# LANGUAGE TypeOperators #-}
14+
{-# LANGUAGE UndecidableInstances #-}
15+
{-# OPTIONS_GHC -Wno-orphans #-}
1216
module ObsHelper where
1317

18+
import Control.Monad.Base (MonadBase (liftBase))
19+
import Control.Monad.Trans.Control
1420
import qualified Data.ByteString as BS
1521
import qualified Data.ByteString.Base64 as B64
1622
import qualified Data.ByteString.Lazy as BL
@@ -22,6 +28,7 @@ import qualified Jose.Jwa as JWT
2228
import qualified Jose.Jws as JWT
2329
import qualified Jose.Jwt as JWT
2430
import Network.HTTP.Types
31+
import Network.Wai.Test
2532
import qualified PostgREST.AppState as AppState
2633
import PostgREST.Config (AppConfig (..),
2734
JSPathExp (..),
@@ -36,6 +43,13 @@ import Protolude hiding (get, toS)
3643
import System.Timeout (timeout)
3744
import Test.Hspec
3845
import Test.Hspec.Expectations.Contrib (annotate)
46+
import Test.Hspec.Wai.Internal
47+
import qualified Toxiproxy
48+
import Toxiproxy (proxyEnabled,
49+
proxyListen,
50+
proxyName,
51+
proxyToxics,
52+
proxyUpstream)
3953

4054
-- helpers used to produce observation diagnostics in waitForObs
4155
-- Implementing the Show instance for Observation is hard due to having many different parameters so instead we use generic programming (`conName`) to obtain the constructor name as `Text`
@@ -52,10 +66,23 @@ instance (HasConstructor x, HasConstructor y) => HasConstructor (x :+: y) where
5266
instance Constructor c => HasConstructor (C1 c f) where
5367
genericConstrName = T.pack . conName
5468

69+
instance MonadBaseControl IO (WaiSession st) where
70+
type StM (WaiSession st) a = StM Session a
71+
liftBaseWith f = WaiSession $
72+
liftBaseWith $ \runInBase ->
73+
f $ \k -> runInBase (unWaiSession k)
74+
restoreM = WaiSession . restoreM
75+
{-# INLINE liftBaseWith #-}
76+
{-# INLINE restoreM #-}
77+
78+
instance MonadBase IO (WaiSession st) where
79+
liftBase = liftIO
80+
5581
data SpecState = SpecState {
56-
specAppState :: AppState.AppState,
57-
specMetrics :: Metrics.MetricsState,
58-
specObsChan :: ObsChan
82+
specAppState :: AppState.AppState,
83+
specMetrics :: Metrics.MetricsState,
84+
specObsChan :: ObsChan,
85+
specToxiProxy :: Toxiproxy.Proxy
5986
}
6087

6188
data StateCheck st m = forall a. StateCheck (st -> (String, m a)) (a -> a -> Expectation)
@@ -74,7 +101,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
74101
, configClientErrorVerbosity = Verbose
75102
, configDbAggregates = False
76103
, configDbAnonRole = Just "postgrest_test_anonymous"
77-
, configDbChannel = mempty
104+
, configDbChannel = "pgrst"
78105
, configDbChannelEnabled = True
79106
, configDbExtraSearchPath = []
80107
, configDbHoistedTxSettings = ["default_transaction_isolation","plan_filter.statement_cost_limit","statement_timeout"]
@@ -126,14 +153,27 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
126153
testCfg :: AppConfig
127154
testCfg = baseCfg
128155

129-
testCfgJwtCache :: AppConfig
130-
testCfgJwtCache =
131-
baseCfg {
156+
testCfgJwtCache :: AppConfig -> AppConfig
157+
testCfgJwtCache base =
158+
base {
132159
configJwtSecret = Just generateSecret
133160
, configJWKS = rightToMaybe $ parseSecret generateSecret
134161
, configJwtCacheMaxEntries = 2
135162
}
136163

164+
testToxiProxy :: Text -> Text -> Text -> Toxiproxy.Proxy
165+
testToxiProxy name proxyPort pgPort = Toxiproxy.Proxy {
166+
proxyName = Toxiproxy.ProxyName name,
167+
proxyEnabled = True,
168+
proxyToxics = mempty,
169+
-- we don't create proxies
170+
-- as they are already created
171+
-- but we have to be careful not to override
172+
-- the values
173+
proxyListen = "localhost:" <> proxyPort,
174+
proxyUpstream = "localhost:" <> pgPort
175+
}
176+
137177
authHeader :: BS.ByteString -> BS.ByteString -> Header
138178
authHeader typ creds =
139179
(hAuthorization, typ <> " " <> creds)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE FlexibleContexts #-}
3+
{-# LANGUAGE MonadComprehensions #-}
4+
{-# LANGUAGE NamedFieldPuns #-}
5+
module Observation.ToxiSpec where
6+
7+
import Control.Monad.Trans.Control (liftBaseDiscard)
8+
import Network.Wai (Application)
9+
import ObsHelper
10+
import qualified PostgREST.AppState as AppState
11+
import Protolude hiding (get)
12+
import Test.Hspec (SpecWith, describe, it)
13+
import Test.Hspec.Wai
14+
import Toxiproxy (withDisabled)
15+
16+
spec :: SpecWith (SpecState, Application)
17+
spec = describe "Tests using Toxiproxy" $ do
18+
it "Should return 503 on temporary database server unavailability" $ do
19+
pendingWith "TODO fix"
20+
SpecState{specAppState, specToxiProxy} <- getState
21+
22+
-- make sure there are no open connections
23+
liftIO $ AppState.flushPool specAppState
24+
25+
liftBaseDiscard (withDisabled specToxiProxy) $ do
26+
void $ get "/items?id=eq.5"
27+
`shouldRespondWith` 503
28+
29+
void $ get "/items?id=eq.5"
30+
`shouldRespondWith` 200
31+
32+
liftBaseDiscard (withDisabled specToxiProxy) $ do
33+
void $ get "/items?id=eq.5"
34+
`shouldRespondWith` 503

0 commit comments

Comments
 (0)