{-# 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
}