Skip to content

Commit dd59a08

Browse files
committed
test: add toxiproxy in front of postgresql in withTmpDb
Having toxiproxy between PostgREST and PostgreSQL in tests opens possibilities of validating behavior of PostgREST under various network conditions: temporary network partitioning, delays etc. This change adds toxiproxy startup and configuration to withTmpDb function in withTools.nix, so that all communication goes through it in tests.
1 parent 0fc01e7 commit dd59a08

12 files changed

Lines changed: 514 additions & 49 deletions

File tree

nix/tools/withTools.nix

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
, slocat
1010
, writeText
1111
, writers
12+
, toxiproxy
1213
}:
1314
let
1415
withTmpDb =
@@ -35,7 +36,7 @@ let
3536
positionalCompletion = "_command";
3637
workingDir = "/";
3738
redirectTixFiles = false;
38-
withPath = [ postgresql ];
39+
withPath = [ postgresql toxiproxy ];
3940
withTmpDir = true;
4041
}
4142
''
@@ -55,6 +56,7 @@ let
5556
5657
export PGDATA="$tmpdir/db"
5758
export PGHOST="$tmpdir/socket"
59+
export PGPORT=6432
5860
export PGUSER
5961
export PGDATABASE
6062
export PGRST_DB_SCHEMAS
@@ -63,8 +65,17 @@ let
6365
6466
HBA_FILE="$tmpdir/pg_hba.conf"
6567
echo "local $PGDATABASE some_protected_user password" > "$HBA_FILE"
66-
echo "local $PGDATABASE all trust" >> "$HBA_FILE"
67-
echo "local replication all trust" >> "$HBA_FILE"
68+
{
69+
echo "local $PGDATABASE all trust"
70+
echo "local replication all trust"
71+
echo "host $PGDATABASE some_protected_user localhost scram-sha-256"
72+
echo "host $PGDATABASE all localhost trust"
73+
} >> "$HBA_FILE"
74+
75+
UNIX_PGHOST="$PGHOST"
76+
export TCP_PGHOST="localhost"
77+
REAL_PGPORT="$PGPORT"
78+
export TOXI_PGPORT=7432
6879
6980
log "Initializing database cluster..."
7081
# We try to make the database cluster as independent as possible from the host
@@ -81,9 +92,14 @@ let
8192
# On MacOS, it's 104 chars
8293
# See: https://serverfault.com/questions/641347/check-if-a-path-exceeds-maximum-for-unix-domain-socket
8394
84-
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\" " \
95+
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"$TCP_PGHOST\" -c port=$REAL_PGPORT -c hba_file=$HBA_FILE -k $UNIX_PGHOST -c log_statement=\"all\" " \
8596
>> "$setuplog"
8697
98+
LOG_LEVEL=error toxiproxy-server&
99+
TOXIPROXY_PID=$!
100+
sleep 1 # give the server a moment to start
101+
toxiproxy-cli create -l "$TCP_PGHOST:$TOXI_PGPORT" -u "$TCP_PGHOST:$REAL_PGPORT" pg
102+
87103
log "Creating a minimally privileged $PGUSER connection role..."
88104
createuser "$PGUSER" -U postgres --host="$tmpdir/socket" --no-createdb --no-inherit --no-superuser --no-createrole --no-replication --login
89105
@@ -94,6 +110,8 @@ let
94110
replica_slot="replica_$RANDOM"
95111
replica_dir="$tmpdir/$replica_slot"
96112
replica_host="$tmpdir/socket_$replica_slot"
113+
replica_port=6433
114+
export TOXI_REPLICA_PGPORT=7433
97115
98116
mkdir -p "$replica_host"
99117
@@ -106,15 +124,17 @@ let
106124
107125
log "Starting replica on $replica_host"
108126
109-
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" " \
127+
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"$TCP_PGHOST\" -c port=$replica_port -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" " \
110128
>> "$setuplog"
129+
toxiproxy-cli create -l $TCP_PGHOST:$TOXI_REPLICA_PGPORT -u $TCP_PGHOST:$replica_port pg_replica
111130
112131
>&2 echo "${commandName}: Replica enabled. You can connect to it with: psql 'postgres:///$PGDATABASE?host=$replica_host' -U postgres"
113132
>&2 echo "${commandName}: You can tail the replica logs with: tail -f $replica_dblog"
114133
115-
export PGREPLICAHOST="$replica_host"
134+
export PGREPLICAHOST="$TCP_PGHOST"
135+
export PGREPLICAPORT="$TOXI_REPLICA_PGPORT"
116136
export PGREPLICASLOT="$replica_slot"
117-
export PGRST_DB_URI="postgres:///$PGDATABASE?host=$PGREPLICAHOST,$PGHOST"
137+
export PGRST_DB_URI="postgres:///$PGDATABASE?host=$PGREPLICAHOST,$TCP_PGHOST&port=$PGREPLICAPORT,$TOXI_PGPORT"
118138
fi
119139
120140
# shellcheck disable=SC2317
@@ -127,6 +147,8 @@ let
127147
pg_ctl -D "$replica_dir" stop --mode=immediate >> "$setuplog"
128148
rm -rf "$replica_dir"
129149
fi
150+
kill "$TOXIPROXY_PID" || true
151+
wait "$TOXIPROXY_PID" || true
130152
}
131153
trap stop EXIT
132154
fi
@@ -140,6 +162,7 @@ let
140162
fi
141163
142164
("$_arg_command" "''${_arg_leftovers[@]}")
165+
#(PGHOST="$TCP_PGHOST" PGPORT="$TOXI_PGPORT" "$_arg_command" "''${_arg_leftovers[@]}")
143166
'';
144167

145168
# Helper script for running a command against all PostgreSQL versions.

postgrest.cabal

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,9 @@ test-suite spec
259259
Feature.Query.UpsertSpec
260260
Feature.RollbackSpec
261261
Feature.RpcPreRequestGucsSpec
262+
Feature.ToxiSpec
262263
SpecHelper
264+
Toxiproxy
263265
build-depends: base >= 4.9 && < 4.20
264266
, aeson >= 2.0.3 && < 2.3
265267
, aeson-qq >= 0.8.1 && < 0.9
@@ -290,6 +292,9 @@ test-suite spec
290292
, transformers-base >= 0.4.4 && < 0.5
291293
, wai >= 3.2.1 && < 3.3
292294
, wai-extra >= 3.0.19 && < 3.2
295+
, servant-client >= 0.20.3.0 && < 0.21
296+
, servant >= 0.20.3.0 && < 0.21
297+
, http-client >= 0.7.19 && < 0.8
293298
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
294299
-fno-spec-constr -optP-Wno-nonportable-include-path
295300
-fno-warn-missing-signatures

src/PostgREST/AppState.hs

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

test/io/conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ def dburi():
99
"Postgres database connection URI."
1010
dbname = os.environ["PGDATABASE"]
1111
host = os.environ["PGHOST"]
12+
port = os.environ["PGPORT"]
1213
user = os.environ["PGUSER"]
13-
return f"postgresql://?dbname={dbname}&host={host}&user={user}".encode()
14+
return f"postgresql://?dbname={dbname}&host={host}&port={port}&user={user}".encode()
1415

1516

1617
@pytest.fixture
@@ -19,6 +20,7 @@ def baseenv():
1920
return {
2021
"PGDATABASE": os.environ["PGDATABASE"],
2122
"PGHOST": os.environ["PGHOST"],
23+
"PGPORT": os.environ["PGPORT"],
2224
"PGUSER": os.environ["PGUSER"],
2325
}
2426

@@ -76,6 +78,7 @@ def metapostgrest():
7678
env = {
7779
"PGDATABASE": os.environ["PGDATABASE"],
7880
"PGHOST": os.environ["PGHOST"],
81+
"PGPORT": os.environ["PGPORT"],
7982
"PGUSER": role,
8083
"PGRST_DB_ANON_ROLE": role,
8184
"PGRST_DB_CONFIG": "true",

test/io/test_auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def relativeSeconds(sec):
165165

166166
def test_fail_with_invalid_password(defaultenv):
167167
"Connecting with an invalid password should fail without retries."
168-
uri = f'postgresql://?dbname={defaultenv["PGDATABASE"]}&host={defaultenv["PGHOST"]}&user=some_protected_user&password=invalid_pass'
168+
uri = f'postgresql://?dbname={defaultenv["PGDATABASE"]}&host={defaultenv["PGHOST"]}&port={defaultenv["PGPORT"]}&user=some_protected_user&password=invalid_pass'
169169
env = {**defaultenv, "PGRST_DB_URI": uri}
170170
with run(env=env, wait_for_readiness=False) as postgrest:
171171
exitCode = wait_until_exit(postgrest)

test/io/test_io.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1653,7 +1653,7 @@ def test_log_listener_connection_start(defaultenv):
16531653
# Check for the listener start message containing host and port
16541654
# Do not check if pg version is displayed properly as it is tricky to test it
16551655
assert any(
1656-
f'"{defaultenv["PGHOST"]}:5432" and listening for database notifications on the "pgrst" channel'
1656+
f'"{defaultenv["PGHOST"]}:{defaultenv["PGPORT"]}" and listening for database notifications on the "pgrst" channel'
16571657
in line
16581658
for line in output
16591659
)

test/spec/Feature/ConcurrentSpec.hs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ module Feature.ConcurrentSpec where
77
import Control.Concurrent.Async (mapConcurrently)
88
import Network.Wai (Application)
99

10-
import Control.Monad.Base
1110
import Control.Monad.Trans.Control
1211

13-
import Network.Wai.Test (Session)
1412
import Test.Hspec
1513
import Test.Hspec.Wai
16-
import Test.Hspec.Wai.Internal
1714
import Test.Hspec.Wai.JSON
1815

16+
import SpecHelper ()
17+
1918
import Protolude hiding (get)
2019

2120
spec :: SpecWith ((), Application)
@@ -34,15 +33,3 @@ raceTest :: Int -> WaiExpectation st -> WaiExpectation st
3433
raceTest times = liftBaseDiscard go
3534
where
3635
go test = void $ mapConcurrently (const test) [1..times]
37-
38-
instance MonadBaseControl IO (WaiSession st) where
39-
type StM (WaiSession st) a = StM Session a
40-
liftBaseWith f = WaiSession $
41-
liftBaseWith $ \runInBase ->
42-
f $ \k -> runInBase (unWaiSession k)
43-
restoreM = WaiSession . restoreM
44-
{-# INLINE liftBaseWith #-}
45-
{-# INLINE restoreM #-}
46-
47-
instance MonadBase IO (WaiSession st) where
48-
liftBase = liftIO

test/spec/Feature/MetricsSpec.hs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{-# LANGUAGE DataKinds #-}
22
{-# LANGUAGE FlexibleContexts #-}
33
{-# LANGUAGE MonadComprehensions #-}
4+
{-# LANGUAGE NamedFieldPuns #-}
45
{-# LANGUAGE TypeApplications #-}
56

67
module Feature.MetricsSpec where
@@ -17,11 +18,11 @@ import SpecHelper
1718
import Test.Hspec (SpecWith, describe, it)
1819
import Test.Hspec.Wai (getState)
1920

20-
spec :: SpecWith ((AppState.AppState, Metrics.MetricsState, ObsChan), Application)
21+
spec :: SpecWith (SpecState, Application)
2122
spec = describe "Server started with metrics enabled" $ do
2223
it "Should update pgrst_schema_cache_loads_total[SUCCESS]" $ do
23-
(appState, metrics, obsChan) <- getState
24-
let waitFor = waitForObs obsChan
24+
SpecState{specAppState = appState, specMetrics = metrics, specObsChan} <- getState
25+
let waitFor = waitForObs specObsChan
2526

2627
liftIO $ checkState' metrics [
2728
schemaCacheLoads "SUCCESS" (+1)
@@ -30,8 +31,8 @@ spec = describe "Server started with metrics enabled" $ do
3031
waitFor (1 * sec) "SchemaCacheLoadedObs" $ \x -> [ o | o@(SchemaCacheLoadedObs{}) <- pure x]
3132

3233
it "Should update pgrst_schema_cache_loads_total[ERROR]" $ do
33-
(appState, metrics, obsChan) <- getState
34-
let waitFor = waitForObs obsChan
34+
SpecState{specAppState = appState, specMetrics = metrics, specObsChan} <- getState
35+
let waitFor = waitForObs specObsChan
3536

3637
liftIO $ checkState' metrics [
3738
schemaCacheLoads "FAIL" (+1),

test/spec/Feature/ToxiSpec.hs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE FlexibleContexts #-}
3+
{-# LANGUAGE MonadComprehensions #-}
4+
{-# LANGUAGE NamedFieldPuns #-}
5+
module Feature.ToxiSpec where
6+
7+
import Control.Monad.Trans.Control (liftBaseDiscard)
8+
import Network.Wai (Application)
9+
import qualified PostgREST.AppState as AppState
10+
import Protolude hiding (get)
11+
import SpecHelper
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+
SpecState{specAppState, specToxiProxy} <- getState
20+
21+
-- make sure there are no open connections
22+
liftIO $ AppState.flushPool specAppState
23+
24+
liftBaseDiscard (withDisabled specToxiProxy) $ do
25+
void $ get "/items?id=eq.5"
26+
`shouldRespondWith` 503
27+
28+
void $ get "/items?id=eq.5"
29+
`shouldRespondWith` 200
30+
31+
liftBaseDiscard (withDisabled specToxiProxy) $ do
32+
void $ get "/items?id=eq.5"
33+
`shouldRespondWith` 503

test/spec/Main.hs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import qualified Feature.Query.UpdateSpec
6969
import qualified Feature.Query.UpsertSpec
7070
import qualified Feature.RollbackSpec
7171
import qualified Feature.RpcPreRequestGucsSpec
72+
import qualified Feature.ToxiSpec
7273
import PostgREST.Observation (Observation (HasqlPoolObs))
7374

7475

@@ -91,6 +92,16 @@ main = do
9192
, P.observationHandler $ (writeChan poolChan <> Metrics.observationMetrics metricsState) . HasqlPoolObs
9293
]
9394

95+
toxicPool <- P.acquire $ P.settings
96+
[ P.size 3
97+
, P.acquisitionTimeout 10
98+
, P.agingTimeout 60
99+
, P.idlenessTimeout 60
100+
, P.staticConnectionSettings (toUtf8 $ configDbUri toxicCfg)
101+
-- make sure metrics are updated and pool observations published to poolChan
102+
, P.observationHandler $ (writeChan poolChan <> Metrics.observationMetrics metricsState) . HasqlPoolObs
103+
]
104+
94105
actualPgVersion <- either (panic . show) id <$> P.use pool (queryPgVersion False)
95106

96107
-- cached schema cache so most tests run fast
@@ -109,10 +120,10 @@ main = do
109120
-- duplicate poolChan as a starting point
110121
obsChan <- dupChan poolChan
111122
stateObsChan <- newObsChan obsChan
112-
appState <- AppState.initWithPool sockets pool config loggerState metricsState (Metrics.observationMetrics metricsState <> writeChan obsChan)
123+
appState <- AppState.initWithPool sockets toxicPool config loggerState metricsState (Metrics.observationMetrics metricsState <> writeChan obsChan)
113124
AppState.putPgVersion appState actualPgVersion
114125
AppState.putSchemaCache appState (Just sCache)
115-
return ((appState, metricsState, stateObsChan), postgrest (configLogLevel config) appState (pure ()))
126+
return (SpecState appState metricsState stateObsChan testToxiProxy, postgrest (configLogLevel config) appState (pure ()))
116127

117128
-- For tests that run with the same schema cache
118129
app = initApp baseSchemaCache ()
@@ -298,8 +309,10 @@ main = do
298309
before (initApp baseSchemaCache metricsState testCfgJwtCache) $
299310
describe "Feature.Auth.JwtCacheSpec" Feature.Auth.JwtCacheSpec.spec
300311

301-
before observationsApp $
302-
describe "Feature.MetricsSpec" Feature.MetricsSpec.spec
312+
traverse_ (before observationsApp . uncurry describe) [
313+
("Feature.MetricsSpec", Feature.MetricsSpec.spec)
314+
, ("Feature.ToxiSpec", Feature.ToxiSpec.spec)
315+
]
303316

304317
where
305318
loadSCache pool conf =

0 commit comments

Comments
 (0)