packages feed

servant-auth-cookie-0.4.4: example/Test.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE RecordWildCards #-}

import Prelude ()
import Prelude.Compat
import Data.Maybe (fromMaybe)
import Data.Int (Int64)
import Data.Time.Clock (UTCTime(..))
import Control.Monad.IO.Class (liftIO)
import AuthAPI (app, authSettings, LoginForm(..), homePage, loginPage, Account(..))
import Test.Hspec (Spec, hspec, describe, it)
import Test.Hspec.Wai (WaiSession, WaiExpectation, shouldRespondWith, with, request, get)
import Text.Blaze.Renderer.Utf8 (renderMarkup)
import Servant (Proxy(..))
import Crypto.Random (drgNew)
import Servant (FormUrlEncoded, contentType)
import Servant.Server.Experimental.Auth.Cookie
import Network.HTTP.Types (methodGet, methodPost, hContentType, hCookie)
import Network.HTTP.Media.RenderHeader (renderHeader)
import Network.Wai.Test (SResponse(..))
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Lazy.Char8 as BSC8

#if MIN_VERSION_hspec_wai (0,7,0)
import Test.Hspec.Wai.Matcher (bodyEquals, ResponseMatcher(..), MatchBody(..))
#else
import Test.Hspec.Wai (matchBody)
#endif

#if MIN_VERSION_servant (0,9,0)
import Web.FormUrlEncoded (ToForm, toForm, urlEncodeForm)
#else
import Servant (ToFormUrlEncoded, mimeRender)
#endif


data SpecState = SpecState {
    ssRandomSource :: RandomSource
  , ssServerKey    :: ServerKey
  , ssAuthSettings :: AuthCookieSettings
  }

main :: IO ()
main = withState (hspec . spec) where
  withState f = do
    let ssAuthSettings = authSettings
    ssRandomSource <- mkRandomSource drgNew 1000
    ssServerKey <- mkServerKey 16 Nothing
    f $ SpecState {..}


spec :: SpecState -> Spec
spec SpecState {..} = with (return $ app ssAuthSettings ssRandomSource ssServerKey) $ do

  let formContentType = (
          hContentType
        , renderHeader $ contentType (Proxy :: Proxy FormUrlEncoded))

  describe "home page" $ do
    it "responds successfully" $ do
      get "/" `shouldRespondWith` 200 {
        matchBody = matchBody' $ renderMarkup homePage
        }

  describe "login page" $ do
    it "responds successfully" $ do
      get "/login" `shouldRespondWith` 200 {
        matchBody = matchBody' $ renderMarkup (loginPage True)
        }

    it "shows message on incorrect login" $ do
      let loginForm = encode $ LoginForm {
            lfUsername = "noname"
          , lfPassword = "noname"
          }
      let r = request methodPost "/login" [formContentType] loginForm
      r `shouldRespondWith` 200 {
        matchBody = matchBody' $ renderMarkup (loginPage False)
        }

  describe "private page" $ do
    let loginForm = encode $ LoginForm {
          lfUsername = "mr_foo"
        , lfPassword = "password1"
        }
    let loginRequest = request methodPost "/login" [formContentType] loginForm

    it "rejects requests without cookies" $ do
      let r = get "/private"
      r `shouldRespondWith` 403 { matchBody = matchBody' "No cookies" }

    it "accepts requests with proper cookies" $ do
      (SResponse {..}) <- loginRequest
      let cookieValue = fromMaybe
            (error "cookies aren't available")
            (lookup "set-cookie" simpleHeaders)

      let r = request methodGet "/private" [(hCookie, cookieValue)] ""
      r `shouldRespondWith` 200

    it "accepts requests with proper cookies (sanity check)" $ do
      (SResponse {..}) <- loginRequest

      cookieValue <- liftIO $ do
        session <- maybe
          (error "cookies aren't available")
          (decryptSession ssAuthSettings ssServerKey)
          (parseSessionResponse ssAuthSettings simpleHeaders) :: IO Account

        renderSession ssAuthSettings ssRandomSource ssServerKey session

      let r = request methodGet "/private" [(hCookie, cookieValue)] ""
      r `shouldRespondWith` 200


    it "rejects requests with incorrect MAC" $ do
      (SResponse {..}) <- loginRequest

      cookieValue <- liftIO $ do
        session <- maybe
          (error "cookies aren't available")
          (decryptSession ssAuthSettings ssServerKey)
          (parseSessionResponse ssAuthSettings simpleHeaders) :: IO Account

        sk <- mkServerKey 16 Nothing
        renderSession ssAuthSettings ssRandomSource sk session

      let r = request methodGet "/private" [(hCookie, cookieValue)] ""

      r `shouldRespondWithException` (IncorrectMAC "")


    it "rejects requests with malformed expiration time" $ do
      (SResponse {..}) <- loginRequest

      cookieValue <- liftIO $ do
        session <- maybe
          (error "cookies aren't available")
          (decryptSession ssAuthSettings ssServerKey)
          (parseSessionResponse ssAuthSettings simpleHeaders) :: IO Account

        renderSession
          ssAuthSettings { acsExpirationFormat = "%0Y%m%d" }
          ssRandomSource
          ssServerKey
          session

      let r = request methodGet "/private" [(hCookie, cookieValue)] ""
      r `shouldRespondWithException` (CannotParseExpirationTime "")


    it "rejects requests with expired cookies" $ do
      (SResponse {..}) <- loginRequest

      cookieValue <- liftIO $ do
        session <- maybe
          (error "cookies aren't available")
          (decryptSession ssAuthSettings ssServerKey)
          (parseSessionResponse ssAuthSettings simpleHeaders) :: IO Account

        renderSession
          ssAuthSettings { acsMaxAge = 0 }
          ssRandomSource
          ssServerKey
          session

      let r = request methodGet "/private" [(hCookie, cookieValue)] ""
      let dummyTime = UTCTime (toEnum 0) 0

      r `shouldRespondWithException` (CookieExpired dummyTime dummyTime)


#if MIN_VERSION_hspec_wai (0,7,0)
matchBody' :: BSL.ByteString -> MatchBody
matchBody' = bodyEquals
#else
matchBody' :: BSL.ByteString -> Maybe BSL.ByteString
matchBody' = Just
#endif

#if MIN_VERSION_servant (0,9,0)
encode :: ToForm a => a -> BSL.ByteString
encode = urlEncodeForm . toForm
#else
encode :: ToFormUrlEncoded a => a -> BSL.ByteString
encode = mimeRender (Proxy :: Proxy FormUrlEncoded)
#endif

shrinkBody :: Int64 -> SResponse -> SResponse
shrinkBody len r = r { simpleBody = BSL.take len $ simpleBody r }

shouldRespondWithException :: WaiSession SResponse -> AuthCookieException -> WaiExpectation
shouldRespondWithException req ex = do
  let exception = BSC8.pack . head . words . show $ ex
  (shrinkBody (BSC8.length exception) <$> req) `shouldRespondWith` 403 {
    matchBody = matchBody' exception
    }