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>
This commit is contained in:
committed by
Steve Chavez
parent
796339172c
commit
12ef63370b
@@ -1,146 +0,0 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE ExistentialQuantification #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE ImpredicativeTypes #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
module Feature.Auth.JwtCacheSpec
|
||||
|
||||
where
|
||||
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Network.HTTP.Types
|
||||
import Test.Hspec (SpecWith, describe, it)
|
||||
import Test.Hspec.Wai
|
||||
|
||||
import PostgREST.Metrics (MetricsState (..))
|
||||
import Protolude
|
||||
import SpecHelper
|
||||
import Test.Hspec.Wai.JSON (json)
|
||||
|
||||
spec :: SpecWith (MetricsState, Application)
|
||||
spec = describe "Server started with JWT and metrics enabled" $ do
|
||||
it "Should not have JWT in cache" $ do
|
||||
let auth = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe1"}|]
|
||||
|
||||
expectCounters
|
||||
[
|
||||
requests (+ 1)
|
||||
, hits (+ 0)
|
||||
] $
|
||||
|
||||
request methodGet "/authors_only" [auth] ""
|
||||
|
||||
it "Should have JWT in cache" $ do
|
||||
let auth = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe2"}|]
|
||||
|
||||
expectCounters
|
||||
[
|
||||
requests (+ 2)
|
||||
, hits (+ 1)
|
||||
] $
|
||||
|
||||
request methodGet "/authors_only" [auth] "" `shouldRespondWith` 200
|
||||
*> request methodGet "/authors_only" [auth] "" `shouldRespondWith` 200
|
||||
|
||||
it "Should not cache invalid JWTs" $ do
|
||||
let auth = authHeaderJWT "some random bytes"
|
||||
|
||||
expectCounters
|
||||
[
|
||||
requests (+ 2)
|
||||
, hits (+ 0)
|
||||
] $
|
||||
|
||||
request methodGet "/authors_only" [auth] "" `shouldRespondWith` 401
|
||||
*> request methodGet "/authors_only" [auth] "" `shouldRespondWith` 401
|
||||
|
||||
it "Should cache expired JWTs" $ do
|
||||
let auth = genToken [json|{"exp": 1, "role": "postgrest_test_author", "id": "jdoe2"}|]
|
||||
|
||||
expectCounters
|
||||
[
|
||||
requests (+ 2)
|
||||
, hits (+ 1)
|
||||
] $
|
||||
|
||||
request methodGet "/authors_only" [auth] "" `shouldRespondWith` 401
|
||||
*> request methodGet "/authors_only" [auth] "" `shouldRespondWith` 401
|
||||
|
||||
it "Should evict entries from the JWT cache (jwt cache max is 2)" $ do
|
||||
let jwt1 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe3"}|]
|
||||
jwt2 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe4"}|]
|
||||
jwt3 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe5"}|]
|
||||
|
||||
expectCounters
|
||||
[
|
||||
requests (+ 6)
|
||||
, hits (+ 0)
|
||||
, evictions (+ 4)
|
||||
] $
|
||||
|
||||
request methodGet "/authors_only" [jwt1] ""
|
||||
*> request methodGet "/authors_only" [jwt2] ""
|
||||
*> request methodGet "/authors_only" [jwt3] ""
|
||||
*> request methodGet "/authors_only" [jwt1] ""
|
||||
*> request methodGet "/authors_only" [jwt2] ""
|
||||
*> request methodGet "/authors_only" [jwt3] ""
|
||||
|
||||
it "Should not evict entries from the JWT cache in FIFO order" $ do
|
||||
let jwt1 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe6"}|]
|
||||
jwt2 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe7"}|]
|
||||
jwt3 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe8"}|]
|
||||
|
||||
expectCounters
|
||||
[
|
||||
requests (+ 6)
|
||||
, hits (+ 3)
|
||||
, evictions (+ 1)
|
||||
] $
|
||||
|
||||
request methodGet "/authors_only" [jwt1] ""
|
||||
*> request methodGet "/authors_only" [jwt2] ""
|
||||
-- this one should hit the cache
|
||||
*> request methodGet "/authors_only" [jwt1] ""
|
||||
-- this one should trigger eviction of jwt2 (not FIFO)
|
||||
*> request methodGet "/authors_only" [jwt3] ""
|
||||
-- these two should hit the cache
|
||||
*> request methodGet "/authors_only" [jwt1] ""
|
||||
*> request methodGet "/authors_only" [jwt3] ""
|
||||
|
||||
-- This one makes sure we test the scenario when finger
|
||||
-- has to move through the whole list first and pass the head
|
||||
-- The test case was added based on coverage report
|
||||
-- showing this scenario was not covered by previous tests
|
||||
it "Should evict entries even though all were hit" $ do
|
||||
let jwt1 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe9"}|]
|
||||
jwt2 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe10"}|]
|
||||
jwt3 = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe11"}|]
|
||||
|
||||
expectCounters
|
||||
[
|
||||
requests (+ 7)
|
||||
, hits (+ 4)
|
||||
, evictions (+ 1)
|
||||
] $
|
||||
|
||||
request methodGet "/authors_only" [jwt1] ""
|
||||
*> request methodGet "/authors_only" [jwt2] ""
|
||||
-- these two should hit the cache
|
||||
*> request methodGet "/authors_only" [jwt1] ""
|
||||
*> request methodGet "/authors_only" [jwt2] ""
|
||||
-- this one should trigger eviction of jwt1
|
||||
*> request methodGet "/authors_only" [jwt3] ""
|
||||
-- these two should hit the cache
|
||||
*> request methodGet "/authors_only" [jwt2] ""
|
||||
*> request methodGet "/authors_only" [jwt3] ""
|
||||
|
||||
where
|
||||
genToken = authHeaderJWT . generateJWT
|
||||
requests = expectCounter @"jwtCacheRequests"
|
||||
hits = expectCounter @"jwtCacheHits"
|
||||
evictions = expectCounter @"jwtCacheEvictions"
|
||||
expectCounters = checkState
|
||||
@@ -23,7 +23,6 @@ import qualified Feature.Auth.AsymmetricJwtSpec
|
||||
import qualified Feature.Auth.AudienceJwtSecretSpec
|
||||
import qualified Feature.Auth.AuthSpec
|
||||
import qualified Feature.Auth.BinaryJwtSecretSpec
|
||||
import qualified Feature.Auth.JwtCacheSpec
|
||||
import qualified Feature.Auth.NoAnonSpec
|
||||
import qualified Feature.Auth.NoJwtSecretSpec
|
||||
import qualified Feature.ConcurrentSpec
|
||||
@@ -274,9 +273,6 @@ main = do
|
||||
before pgSafeUpdateApp $
|
||||
describe "Feature.Query.PgSafeUpdateSpec.spec" Feature.Query.PgSafeUpdateSpec.spec
|
||||
|
||||
before (initApp baseSchemaCache metricsState testCfgJwtCache) $
|
||||
describe "Feature.Auth.JwtCacheSpec" Feature.Auth.JwtCacheSpec.spec
|
||||
|
||||
where
|
||||
loadSCache pool conf =
|
||||
either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ querySchemaCache conf)
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE ExistentialQuantification #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
module SpecHelper where
|
||||
|
||||
import Control.Lens ((^?))
|
||||
@@ -42,10 +35,8 @@ import PostgREST.Config (AppConfig (..),
|
||||
OpenAPIMode (..),
|
||||
Verbosity (..), parseSecret)
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
||||
import Prometheus (Counter, getCounter)
|
||||
import Protolude hiding (get, toS)
|
||||
import Protolude.Conv (toS)
|
||||
import Test.Hspec.Expectations.Contrib (annotate)
|
||||
|
||||
filterAndMatchCT :: BS.ByteString -> MatchHeader
|
||||
filterAndMatchCT val = MatchHeader $ \headers _ ->
|
||||
@@ -215,14 +206,6 @@ testCfgBinaryJWT =
|
||||
, configJWKS = rightToMaybe $ parseSecret generateSecret
|
||||
}
|
||||
|
||||
testCfgJwtCache :: AppConfig
|
||||
testCfgJwtCache =
|
||||
baseCfg {
|
||||
configJwtSecret = Just generateSecret
|
||||
, configJWKS = rightToMaybe $ parseSecret generateSecret
|
||||
, configJwtCacheMaxEntries = 2
|
||||
}
|
||||
|
||||
testCfgAudienceJWT :: AppConfig
|
||||
testCfgAudienceJWT =
|
||||
baseCfg {
|
||||
@@ -355,29 +338,3 @@ getInsertDataForTiobePlsTable rows =
|
||||
|
||||
readFixtureFile :: FilePath -> BL.ByteString
|
||||
readFixtureFile file = unsafePerformIO $ BL.readFile $ "test/spec/fixtures/" <> file
|
||||
|
||||
-- state check helpers
|
||||
|
||||
data StateCheck st m = forall a. StateCheck (st -> (String, m a)) (a -> a -> Expectation)
|
||||
|
||||
stateCheck :: (Show a, Eq a) => (c -> m a) -> (st -> (String, c)) -> (a -> a) -> StateCheck st m
|
||||
stateCheck extractValue extractComponent expect = StateCheck (second extractValue . extractComponent) (flip shouldBe . expect)
|
||||
|
||||
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
|
||||
expectField extractValue = stateCheck extractValue ((symbolVal (Proxy @s),) . getField @s)
|
||||
|
||||
checkState :: (Traversable t) => t (StateCheck st (WaiSession st)) -> WaiSession st b -> WaiSession st ()
|
||||
checkState checks act = getState >>= flip (`checkState'` checks) act
|
||||
|
||||
checkState' :: (Traversable t, MonadIO m) => st -> t (StateCheck st m) -> m b -> m ()
|
||||
checkState' initialState checks act = do
|
||||
expectations <- traverse (\(StateCheck g expect) -> let (msg, m) = g initialState in m >>= createExpectation msg m . expect) checks
|
||||
void act
|
||||
sequenceA_ expectations
|
||||
where
|
||||
createExpectation msg metrics expect = pure $ metrics >>= liftIO . annotate msg . expect
|
||||
|
||||
expectCounter :: forall s st m. (KnownSymbol s, HasField s st Counter, MonadIO m) => (Int -> Int) -> StateCheck st m
|
||||
expectCounter = expectField @s intCounter
|
||||
where
|
||||
intCounter = ((round @Double @Int) <$>) . getCounter
|
||||
|
||||
Reference in New Issue
Block a user