servant-prometheus-1.0.0: test/Prometheus/ServantSpec.hs
module Prometheus.ServantSpec (spec) where
import Data.Aeson (FromJSON, ToJSON)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import Network.HTTP.Client (defaultManagerSettings, newManager)
import Network.Wai (Application)
import Network.Wai.Handler.Warp (Port, withApplication)
import Prometheus qualified as P
import Servant
( Capture
, Delete
, Get
, JSON
, NoContent (..)
, Post
, Proxy (..)
, QueryParam
, ReqBody
, Server
, serve
, type (:<|>) (..)
, type (:>)
)
import Servant.Client
( BaseUrl (..)
, ClientError
, ClientM
, Scheme (..)
, client
, mkClientEnv
, runClientM
)
import Servant.Test.ComprehensiveAPI (comprehensiveAPI)
import Test.Hspec (Spec, describe, it)
import Test.Hspec.Expectations.Pretty (shouldBe)
import Prometheus.Servant (Metrics (..), defaultMetrics, prometheusMiddleware)
import Prometheus.Servant.Internal (Endpoint (..), HasEndpoint (..))
-- * Spec
spec :: Spec
spec = describe "servant-prometheus" $ do
let getEp :<|> postEp :<|> deleteEp = client testApi
it "collects number of request" $
withApp $ \port -> do
mgr <- newManager defaultManagerSettings
let runFn :: ClientM a -> IO (Either ClientError a)
runFn fn = runClientM fn (mkClientEnv mgr (BaseUrl Http "localhost" port ""))
_ <- runFn $ getEp "name" Nothing
_ <- runFn $ postEp (Greet "hi")
_ <- runFn $ deleteEp "blah"
let Metrics{..} = defaultMetrics
latencies <- P.getVectorWith mLatency P.getHistogram
map fst latencies
`shouldBe` [ ("/greet", "POST", "200")
, ("/greet/:greetid", "DELETE", "200")
, ("/hello/:name", "GET", "200")
]
map (sum . map snd . Map.toList . snd) latencies
`shouldBe` [1, 1, 1]
it "is comprehensive" $ do
let !_typeLevelTest = prometheusMiddleware defaultMetrics comprehensiveAPI
True `shouldBe` True
it "enumerates the parts of an API correctly" $
enumerateEndpoints testApi
`shouldBe` [ Endpoint ["hello", ":name"] "GET"
, Endpoint ["greet"] "POST"
, Endpoint ["greet", ":greetid"] "DELETE"
]
-- * Example
-- | A greet message data type
newtype Greet = Greet {_msg :: Text}
deriving stock (Generic, Show)
instance FromJSON Greet
instance ToJSON Greet
-- | API specification
type TestApi =
-- GET /hello/:name?capital={true, false} returns a Greet as JSON
"hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get '[JSON] Greet
-- POST /greet with a Greet as JSON in the request body,
-- returns a Greet as JSON
:<|> "greet" :> ReqBody '[JSON] Greet :> Post '[JSON] Greet
-- DELETE /greet/:greetid
:<|> "greet" :> Capture "greetid" Text :> Delete '[JSON] NoContent
testApi :: Proxy TestApi
testApi = Proxy
-- | Server-side handlers.
--
-- There's one handler per endpoint, which, just like in the type
-- that represents the API, are glued together using :<|>.
--
-- Each handler runs in the 'EitherT (Int, String) IO' monad.
server :: Server TestApi
server = helloH :<|> postGreetH :<|> deleteGreetH
where
helloH name Nothing = helloH name (Just False)
helloH name (Just False) = pure . Greet $ "Hello, " <> name
helloH name (Just True) = pure . Greet . T.toUpper $ "Hello, " <> name
postGreetH = pure
deleteGreetH _ = pure NoContent
-- | Turn the server into a WAI app. 'serve' is provided by servant,
-- more precisely by the Servant.Server module.
test :: Application
test = serve testApi server
withApp :: (Port -> IO a) -> IO a
withApp =
withApplication (pure $ prometheusMiddleware defaultMetrics testApi test)