servant-hmac-auth-0.1.5: test/Servant/Auth/HmacSpec.hs
module Servant.Auth.HmacSpec (spec) where
import Data.Text (Text)
import qualified Data.Text as Text
import Network.HTTP.Client (defaultManagerSettings, newManager)
import Network.HTTP.Types (Status, unauthorized401)
import qualified Network.Wai.Handler.Warp as Warp
import Servant (
Application,
Handler,
MimeRender,
MimeUnrender,
PlainText,
Post,
Proxy (Proxy),
ReqBody,
Server,
serve,
serveWithContext,
type (:>),
)
import Servant.Auth.Hmac (
HmacAuth,
SecretKey (SecretKey),
defaultHmacSettings,
hmacAuthServerContext,
hmacClient,
runHmacClient,
signSHA256,
)
import Servant.Client (
BaseUrl (baseUrlPort),
ClientError (..),
ResponseF (responseStatusCode),
client,
mkClientEnv,
parseBaseUrl,
runClientM,
)
import Test.Hspec (
Expectation,
Spec,
around,
context,
describe,
expectationFailure,
it,
runIO,
shouldBe,
)
newtype EchoMessage = EchoMessage
{emContent :: Text}
deriving stock (Eq, Show)
deriving (MimeRender PlainText, MimeUnrender PlainText) via Text
type EchoApi =
HmacAuth :> UnprotectedEchoApi
type UnprotectedEchoApi =
-- Echo back a reversed message.
"echo" :> ReqBody '[PlainText] EchoMessage :> Post '[PlainText] EchoMessage
echoBack :: EchoMessage -> Handler EchoMessage
echoBack (EchoMessage msg) = pure $ EchoMessage (Text.reverse msg)
unsecuredEchoServer :: Server UnprotectedEchoApi
unsecuredEchoServer = echoBack
unsecuredEchoApp :: Application
unsecuredEchoApp = serve (Proxy @UnprotectedEchoApi) unsecuredEchoServer
withUnsecuredEchoApp :: (Warp.Port -> IO ()) -> IO ()
withUnsecuredEchoApp = Warp.testWithApplication (pure unsecuredEchoApp)
securedEchoServer :: Server EchoApi
securedEchoServer = const echoBack
securedEchoApp :: SecretKey -> Application
securedEchoApp sk = serveWithContext (Proxy @EchoApi) (hmacAuthServerContext signSHA256 sk) securedEchoServer
withSecuredEchoApp :: SecretKey -> (Warp.Port -> IO ()) -> IO ()
withSecuredEchoApp sk = Warp.testWithApplication (pure $ securedEchoApp sk)
shouldBeASuccessWith :: (Show a, Eq a) => Either ClientError a -> a -> Expectation
errA `shouldBeASuccessWith` a = errA `shouldBe` Right a
shouldBeAFailureWithStatus :: (Show a) => Either ClientError a -> Status -> Expectation
Left (FailureResponse _ resp) `shouldBeAFailureWithStatus` status = responseStatusCode resp `shouldBe` status
Left (DecodeFailure _ resp) `shouldBeAFailureWithStatus` status = responseStatusCode resp `shouldBe` status
Left (UnsupportedContentType _ resp) `shouldBeAFailureWithStatus` status = responseStatusCode resp `shouldBe` status
Left (InvalidContentTypeHeader resp) `shouldBeAFailureWithStatus` status = responseStatusCode resp `shouldBe` status
Left (ConnectionError e) `shouldBeAFailureWithStatus` status = expectationFailure $ "Expected '" <> show status <> "', got this instead: " <> show e
Right a `shouldBeAFailureWithStatus` status = expectationFailure $ "Expected '" <> show status <> "', got this instead: " <> show a
spec :: Spec
spec =
describe "Hmac" $ do
let sk = SecretKey "not-so-secret-secret-key!"
let anotherSk = SecretKey "some-other-key!!"
context "with a secured server" $ do
around (withSecuredEchoApp sk) $ do
let securedEchoBack = hmacClient @UnprotectedEchoApi
let unsecuredEchoBack = client (Proxy @UnprotectedEchoApi)
baseUrl <- runIO $ parseBaseUrl "http://localhost"
manager <- runIO $ newManager defaultManagerSettings
let clientEnv port = mkClientEnv manager (baseUrl{baseUrlPort = port})
it "should have the server authenticate the client's secured request with the same secret" $ \port -> do
result <- runHmacClient (defaultHmacSettings sk) (clientEnv port) (securedEchoBack $ EchoMessage "abcdef123789")
result `shouldBeASuccessWith` EchoMessage "987321fedcba"
it "should have the server reject the client's secured request with a different secret and respond with a 401 status code" $ \port -> do
result <- runHmacClient (defaultHmacSettings anotherSk) (clientEnv port) (securedEchoBack $ EchoMessage "very sensitive message")
result `shouldBeAFailureWithStatus` unauthorized401
it "should have the server reject the client's insecured request and respond with a 401 status code" $ \port -> do
result <- runClientM (unsecuredEchoBack $ EchoMessage "very sensitive message") (clientEnv port)
result `shouldBeAFailureWithStatus` unauthorized401
context "with an unsecured server" $ do
around withUnsecuredEchoApp $ do
let securedEchoBack = hmacClient @UnprotectedEchoApi
baseUrl <- runIO $ parseBaseUrl "http://localhost"
manager <- runIO $ newManager defaultManagerSettings
let clientEnv port = mkClientEnv manager (baseUrl{baseUrlPort = port})
it "should have the server respond to the client and ignore the client request's HMAC signature" $ \port -> do
result <- runHmacClient (defaultHmacSettings sk) (clientEnv port) (securedEchoBack $ EchoMessage "abcdef123789")
result `shouldBeASuccessWith` EchoMessage "987321fedcba"