Skip to content

Commit 2d48d58

Browse files
committed
nix(test): add test suite for observability tests
- Create separate test suite for observability tests - Create wrapper script `postgrest-test-observability` - Add to CI and `postgrest-check` - Move JWT cache tests under observability tests Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
1 parent c448712 commit 2d48d58

14 files changed

Lines changed: 320 additions & 64 deletions

File tree

.github/workflows/test.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,18 @@ jobs:
8383
uses: ./.github/actions/setup-nix
8484
with:
8585
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
86-
tools: tests.testSpec.bin tests.testIO.bin tests.testBigSchema.bin withTools.pg-${{ matrix.pgVersion }}.bin cabalTools.update.bin
86+
tools: tests.testSpec.bin tests.testObservability.bin tests.testIO.bin tests.testBigSchema.bin withTools.pg-${{ matrix.pgVersion }}.bin cabalTools.update.bin
8787

8888
- run: postgrest-cabal-update
8989

9090
- name: Run spec tests
9191
if: always()
9292
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-spec
9393

94+
- name: Run observability tests
95+
if: always()
96+
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-observability
97+
9498
- name: Run IO tests
9599
if: always()
96100
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-io -vv

nix/tools/devTools.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ let
8282
}
8383
''
8484
${tests}/bin/postgrest-test-spec
85+
${tests}/bin/postgrest-test-observability
8586
${tests}/bin/postgrest-test-doctests
8687
${tests}/bin/postgrest-test-io
8788
${tests}/bin/postgrest-test-big-schema

nix/tools/tests.nix

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ let
3232
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec -- "''${_arg_leftovers[@]}"
3333
'';
3434

35+
testObservability =
36+
checkedShellScript
37+
{
38+
name = "postgrest-test-observability";
39+
docs = "Run the Haskell observability test suite.";
40+
args = [ "ARG_LEFTOVERS([hspec arguments])" ];
41+
workingDir = "/";
42+
withEnv = postgrest.env;
43+
}
44+
''
45+
${withTools.withPg} -f test/observability/fixtures/load.sql \
46+
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability -- "''${_arg_leftovers[@]}"
47+
'';
48+
3549
testDoctests =
3650
checkedShellScript
3751
{
@@ -155,7 +169,7 @@ let
155169
rm -rf coverage/*
156170
157171
# build once before running all the tests
158-
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec
172+
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec test:observability
159173
160174
(
161175
trap 'echo Found dead code: Check file list above.' ERR ;
@@ -179,11 +193,16 @@ let
179193
${withTools.withPg} -f test/spec/fixtures/load.sql \
180194
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec
181195
196+
HPCTIXFILE="$tmpdir"/observability.tix \
197+
${withTools.withPg} -f test/observability/fixtures/load.sql \
198+
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability
199+
182200
# Note: No coverage for doctests, as doctests leverage GHCi and GHCi does not support hpc
183201
184202
# collect all the tix files
185203
${ghc}/bin/hpc sum --union --exclude=Paths_postgrest --output="$tmpdir"/tests.tix \
186-
"$tmpdir"/io*.tix "$tmpdir"/big_schema*.tix "$tmpdir"/replica*.tix "$tmpdir"/spec.tix
204+
"$tmpdir"/io*.tix "$tmpdir"/big_schema*.tix "$tmpdir"/replica*.tix "$tmpdir"/spec.tix \
205+
"$tmpdir"/observability.tix
187206
188207
# prepare the overlay
189208
${ghc}/bin/hpc overlay --output="$tmpdir"/overlay.tix test/coverage.overlay
@@ -250,6 +269,7 @@ buildToolbox
250269
tools = {
251270
inherit
252271
testSpec
272+
testObservability
253273
testDoctests
254274
testSpecIdempotence
255275
testIO

postgrest.cabal

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ test-suite spec
213213
Feature.Auth.AudienceJwtSecretSpec
214214
Feature.Auth.AuthSpec
215215
Feature.Auth.BinaryJwtSecretSpec
216-
Feature.Auth.JwtCacheSpec
217216
Feature.Auth.NoAnonSpec
218217
Feature.Auth.NoJwtSecretSpec
219218
Feature.ConcurrentSpec
@@ -296,6 +295,37 @@ test-suite spec
296295
-- https://github.com/PostgREST/postgrest/issues/387
297296
-with-rtsopts=-K33K
298297

298+
test-suite observability
299+
type: exitcode-stdio-1.0
300+
default-language: Haskell2010
301+
default-extensions: OverloadedStrings
302+
QuasiQuotes
303+
NoImplicitPrelude
304+
hs-source-dirs: test/observability
305+
main-is: Main.hs
306+
other-modules: ObsHelper
307+
Observation.JwtCache
308+
build-depends: base >= 4.9 && < 4.20
309+
, base64-bytestring >= 1 && < 1.3
310+
, bytestring >= 0.10.8 && < 0.13
311+
, hasql-pool >= 1.0.1 && < 1.1
312+
, hasql-transaction >= 1.0.1 && < 1.2
313+
, hspec >= 2.3 && < 2.12
314+
, hspec-expectations >= 0.8.4 && < 0.9
315+
, hspec-wai >= 0.10 && < 0.12
316+
, hspec-wai-json >= 0.10 && < 0.12
317+
, http-types >= 0.12.3 && < 0.13
318+
, jose-jwt >= 0.9.6 && < 0.11
319+
, postgrest
320+
, prometheus-client >= 1.1.1 && < 1.2.0
321+
, protolude >= 0.3.1 && < 0.4
322+
, wai >= 3.2.1 && < 3.3
323+
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
324+
-fno-spec-constr -optP-Wno-nonportable-include-path
325+
-fwrite-ide-info
326+
-- https://github.com/PostgREST/postgrest/issues/387
327+
-with-rtsopts=-K33K
328+
299329
test-suite doctests
300330
type: exitcode-stdio-1.0
301331
default-language: Haskell2010

test/observability/Main.hs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
module Main where
2+
3+
import qualified Hasql.Pool as P
4+
import qualified Hasql.Pool.Config as P
5+
import qualified Hasql.Transaction.Sessions as HT
6+
7+
import Data.Function (id)
8+
9+
import PostgREST.App (postgrest)
10+
import qualified PostgREST.AppState as AppState
11+
import PostgREST.Config (AppConfig (..))
12+
import PostgREST.Config.Database (queryPgVersion)
13+
import qualified PostgREST.Logger as Logger
14+
import qualified PostgREST.Metrics as Metrics
15+
import PostgREST.SchemaCache (querySchemaCache)
16+
17+
import qualified Observation.JwtCache
18+
19+
import ObsHelper
20+
import Protolude hiding (toList, toS)
21+
import Test.Hspec
22+
23+
main :: IO ()
24+
main = do
25+
pool <- P.acquire $ P.settings
26+
[ P.size 3
27+
, P.acquisitionTimeout 10
28+
, P.agingTimeout 60
29+
, P.idlenessTimeout 60
30+
, P.staticConnectionSettings (toUtf8 $ configDbUri testCfg)
31+
]
32+
33+
actualPgVersion <- either (panic . show) id <$> P.use pool (queryPgVersion False)
34+
35+
-- cached schema cache so most tests run fast
36+
baseSchemaCache <- loadSCache pool testCfg
37+
loggerState <- Logger.init
38+
metricsState <- Metrics.init (configDbPoolSize testCfg)
39+
40+
let
41+
initApp sCache st config = do
42+
appState <- AppState.initWithPool pool config loggerState metricsState (Metrics.observationMetrics metricsState)
43+
AppState.putPgVersion appState actualPgVersion
44+
AppState.putSchemaCache appState (Just sCache)
45+
return (st, postgrest (configLogLevel config) appState (pure ()))
46+
47+
-- Run all test modules
48+
hspec $ do
49+
before (initApp baseSchemaCache metricsState testCfgJwtCache) $
50+
describe "Observation.JwtCacheObs" Observation.JwtCache.spec
51+
52+
where
53+
loadSCache pool conf =
54+
either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ querySchemaCache conf)

test/observability/ObsHelper.hs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
{-# LANGUAGE AllowAmbiguousTypes #-}
2+
{-# LANGUAGE ExistentialQuantification #-}
3+
{-# LANGUAGE FlexibleContexts #-}
4+
{-# LANGUAGE ScopedTypeVariables #-}
5+
{-# LANGUAGE TupleSections #-}
6+
{-# LANGUAGE TypeApplications #-}
7+
module ObsHelper where
8+
9+
import qualified Data.ByteString.Base64 as B64 (decodeLenient)
10+
import qualified Data.ByteString.Char8 as BS
11+
import qualified Data.ByteString.Lazy as BL
12+
import qualified Jose.Jwa as JWT
13+
import qualified Jose.Jws as JWT
14+
import qualified Jose.Jwt as JWT
15+
16+
import PostgREST.Config (AppConfig (..),
17+
JSPathExp (..),
18+
LogLevel (..),
19+
OpenAPIMode (..),
20+
Verbosity (..), parseSecret)
21+
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
22+
23+
import Data.List.NonEmpty (fromList)
24+
import Data.String (String)
25+
import Network.HTTP.Types
26+
import Prometheus (Counter, getCounter)
27+
import Protolude hiding (get, toS)
28+
import Test.Hspec
29+
import Test.Hspec.Expectations.Contrib (annotate)
30+
import Test.Hspec.Wai
31+
32+
33+
baseCfg :: AppConfig
34+
baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
35+
AppConfig {
36+
configAppSettings = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ]
37+
, configClientErrorVerbosity = Verbose
38+
, configDbAggregates = False
39+
, configDbAnonRole = Just "postgrest_test_anonymous"
40+
, configDbChannel = mempty
41+
, configDbChannelEnabled = True
42+
, configDbExtraSearchPath = []
43+
, configDbHoistedTxSettings = ["default_transaction_isolation","plan_filter.statement_cost_limit","statement_timeout"]
44+
, configDbMaxRows = Nothing
45+
, configDbPlanEnabled = False
46+
, configDbPoolSize = 10
47+
, configDbPoolAcquisitionTimeout = 10
48+
, configDbPoolMaxLifetime = 1800
49+
, configDbPoolMaxIdletime = 600
50+
, configDbPoolAutomaticRecovery = True
51+
, configDbPreRequest = Just $ QualifiedIdentifier "test" "switch_role"
52+
, configDbPreparedStatements = True
53+
, configDbRootSpec = Nothing
54+
, configDbSchemas = fromList ["test"]
55+
, configDbConfig = False
56+
, configDbPreConfig = Nothing
57+
, configDbUri = "postgresql://"
58+
, configFilePath = Nothing
59+
, configJWKS = rightToMaybe $ parseSecret secret
60+
, configJwtAudience = Nothing
61+
, configJwtRoleClaimKey = [JSPKey "role"]
62+
, configJwtSecret = Just secret
63+
, configJwtSecretIsBase64 = False
64+
, configJwtCacheMaxEntries = 10
65+
, configLogLevel = LogCrit
66+
, configLogQuery = False
67+
, configOpenApiMode = OAFollowPriv
68+
, configOpenApiSecurityActive = False
69+
, configOpenApiServerProxyUri = Nothing
70+
, configServerCorsAllowedOrigins = Nothing
71+
, configServerHost = "localhost"
72+
, configServerPort = 3000
73+
, configServerTraceHeader = Nothing
74+
, configServerUnixSocket = Nothing
75+
, configServerUnixSocketMode = 432
76+
, configDbTxAllowOverride = True
77+
, configDbTxRollbackAll = True
78+
, configAdminServerHost = "localhost"
79+
, configAdminServerPort = Nothing
80+
, configRoleSettings = mempty
81+
, configRoleIsoLvl = mempty
82+
, configInternalSCQuerySleep = Nothing
83+
, configInternalSCLoadSleep = Nothing
84+
, configInternalSCRelLoadSleep = Nothing
85+
, configServerTimingEnabled = True
86+
}
87+
88+
testCfg :: AppConfig
89+
testCfg = baseCfg
90+
91+
testCfgJwtCache :: AppConfig
92+
testCfgJwtCache =
93+
baseCfg {
94+
configJwtSecret = Just generateSecret
95+
, configJWKS = rightToMaybe $ parseSecret generateSecret
96+
, configJwtCacheMaxEntries = 2
97+
}
98+
99+
authHeader :: BS.ByteString -> BS.ByteString -> Header
100+
authHeader typ creds =
101+
(hAuthorization, typ <> " " <> creds)
102+
103+
authHeaderJWT :: BS.ByteString -> Header
104+
authHeaderJWT = authHeader "Bearer"
105+
106+
generateSecret :: ByteString
107+
generateSecret = B64.decodeLenient "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU="
108+
109+
generateJWT :: BL.ByteString -> ByteString
110+
generateJWT claims =
111+
either mempty JWT.unJwt $ JWT.hmacEncode JWT.HS256 generateSecret (BL.toStrict claims)
112+
113+
-- state check helpers
114+
115+
data StateCheck st m = forall a. StateCheck (st -> (String, m a)) (a -> a -> Expectation)
116+
117+
stateCheck :: (Show a, Eq a) => (c -> m a) -> (st -> (String, c)) -> (a -> a) -> StateCheck st m
118+
stateCheck extractValue extractComponent expect = StateCheck (second extractValue . extractComponent) (flip shouldBe . expect)
119+
120+
expectField :: forall s st a c m. (KnownSymbol s, Show a, Eq a, HasField s st c) => (c -> m a) -> (a -> a) -> StateCheck st m
121+
expectField extractValue = stateCheck extractValue ((symbolVal (Proxy @s),) . getField @s)
122+
123+
checkState :: (Traversable t) => t (StateCheck st (WaiSession st)) -> WaiSession st b -> WaiSession st ()
124+
checkState checks act = getState >>= flip (`checkState'` checks) act
125+
126+
checkState' :: (Traversable t, MonadIO m) => st -> t (StateCheck st m) -> m b -> m ()
127+
checkState' initialState checks act = do
128+
expectations <- traverse (\(StateCheck g expect) -> let (msg, m) = g initialState in m >>= createExpectation msg m . expect) checks
129+
void act
130+
sequenceA_ expectations
131+
where
132+
createExpectation msg metrics expect = pure $ metrics >>= liftIO . annotate msg . expect
133+
134+
expectCounter :: forall s st m. (KnownSymbol s, HasField s st Counter, MonadIO m) => (Int -> Int) -> StateCheck st m
135+
expectCounter = expectField @s intCounter
136+
where
137+
intCounter = ((round @Double @Int) <$>) . getCounter

test/spec/Feature/Auth/JwtCacheSpec.hs renamed to test/observability/Observation/JwtCache.hs

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
1-
{-# LANGUAGE AllowAmbiguousTypes #-}
2-
{-# LANGUAGE DataKinds #-}
3-
{-# LANGUAGE ExistentialQuantification #-}
4-
{-# LANGUAGE FlexibleContexts #-}
5-
{-# LANGUAGE ImpredicativeTypes #-}
6-
{-# LANGUAGE OverloadedStrings #-}
7-
{-# LANGUAGE ScopedTypeVariables #-}
8-
{-# LANGUAGE TypeApplications #-}
9-
module Feature.Auth.JwtCacheSpec
10-
11-
where
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE TypeApplications #-}
3+
module Observation.JwtCache where
124

135
import Network.Wai (Application)
146

157
import Network.HTTP.Types
168
import Test.Hspec (SpecWith, describe, it)
179
import Test.Hspec.Wai
1810

11+
import ObsHelper
1912
import PostgREST.Metrics (MetricsState (..))
2013
import Protolude
21-
import SpecHelper
2214
import Test.Hspec.Wai.JSON (json)
2315

2416
spec :: SpecWith (MetricsState, Application)
@@ -32,7 +24,7 @@ spec = describe "Server started with JWT and metrics enabled" $ do
3224
, hits (+ 0)
3325
] $
3426

35-
request methodGet "/authors_only" [auth] ""
27+
request methodGet "/authors_only" [auth] "" `shouldRespondWith` 200
3628

3729
it "Should have JWT in cache" $ do
3830
let auth = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe2"}|]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Suppress NOTICE: ... messages
2+
SET client_min_messages TO warning;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Loads all fixtures for the PostgREST observability tests
2+
3+
\set ON_ERROR_STOP on
4+
5+
\ir database.sql
6+
\ir roles.sql
7+
\ir schema.sql
8+
\ir privileges.sql
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- Schema test objects
2+
SET search_path = test, pg_catalog;
3+
4+
GRANT USAGE ON SCHEMA test TO postgrest_test_anonymous;
5+
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA test TO postgrest_test_anonymous;
6+
REVOKE ALL PRIVILEGES ON TABLE authors_only FROM postgrest_test_anonymous;
7+
8+
GRANT USAGE ON SCHEMA test TO postgrest_test_author;
9+
GRANT ALL ON TABLE authors_only TO postgrest_test_author;

0 commit comments

Comments
 (0)