{-# LANGUAGE DataKinds #-} {-# LANGUAGE NamedFieldPuns #-} module Observation.JwtCache where import Network.Wai (Application) import Network.HTTP.Types import Test.Hspec (SpecWith, describe, it) import Test.Hspec.Wai import ObsHelper import PostgREST.Metrics (MetricsState (..)) import Protolude import Test.Hspec.Wai.JSON (json) spec :: SpecWith (SpecState, Application) spec = describe "Server started with JWT and metrics enabled" $ do it "Should not have JWT in cache" $ do expectCounters <- checkState' . specMetrics <$> getState let auth = genToken [json|{"exp": 9999999999, "role": "postgrest_test_author", "id": "jdoe1"}|] expectCounters [ requests (+ 1) , hits (+ 0) ] $ request methodGet "/authors_only" [auth] "" `shouldRespondWith` 200 it "Should have JWT in cache" $ do expectCounters <- checkState' . specMetrics <$> getState 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 expectCounters <- checkState' . specMetrics <$> getState 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 expectCounters <- checkState' . specMetrics <$> getState 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 expectCounters <- checkState' . specMetrics <$> getState 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 expectCounters <- checkState' . specMetrics <$> getState 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 expectCounters <- checkState' . specMetrics <$> getState 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"