Spock-core-0.11.0.0: test/Web/Spock/FrameworkSpecHelper.hs
{-# LANGUAGE OverloadedStrings #-}
module Web.Spock.FrameworkSpecHelper where
import Test.Hspec
import Test.Hspec.Wai
import Data.Monoid
import Data.Word
import Network.HTTP.Types.Header
import Network.HTTP.Types.Method
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Lazy.Char8 as BSLC
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Network.Wai as Wai
sizeLimitSpec :: (Word64 -> IO Wai.Application) -> Spec
sizeLimitSpec app =
with (app maxSize) $
describe "Request size limit" $
do it "allows small enough requests the way" $
do post "/size" okBs `shouldRespondWith` matcher 200 okBs
post "/size" okBs2 `shouldRespondWith` matcher 200 okBs2
it "denys large requests the way" $
post "/size" tooLongBs `shouldRespondWith` 413
where
matcher s b =
ResponseMatcher
{ matchStatus = s
, matchBody = Just b
, matchHeaders = []
}
maxSize = 1024
okBs = BSLC.replicate (fromIntegral maxSize - 50) 'i'
okBs2 = BSLC.replicate (fromIntegral maxSize) 'j'
tooLongBs = BSLC.replicate (fromIntegral maxSize + 100) 'k'
frameworkSpec :: IO Wai.Application -> Spec
frameworkSpec app =
with app $
do routingSpec
actionSpec
headerTest
cookieTest
routingSpec :: SpecWith Wai.Application
routingSpec =
describe "Routing Framework" $
do it "allows root actions" $
get "/" `shouldRespondWith` "root" { matchStatus = 200 }
it "routes different HTTP-verbs to different actions" $
do verbTest get "GET"
verbTest (`post` "") "POST"
verbTest (`put` "") "PUT"
verbTest delete "DELETE"
verbTest (`patch` "") "PATCH"
verbTestGp get "GETPOST"
verbTestGp (`post` "") "GETPOST"
it "can extract params from routes" $
get "/param-test/42" `shouldRespondWith` "int42" { matchStatus = 200 }
it "can handle multiple matching routes" $
get "/param-test/static" `shouldRespondWith` "static" { matchStatus = 200 }
it "ignores trailing slashes" $
get "/param-test/static/" `shouldRespondWith` "static" { matchStatus = 200 }
it "works with subcomponents" $
do get "/subcomponent/foo" `shouldRespondWith` "foo" { matchStatus = 200 }
get "/subcomponent/subcomponent2/bar" `shouldRespondWith` "bar" { matchStatus = 200 }
it "allows the definition of a fallback handler" $
get "/askldjas/aklsdj" `shouldRespondWith` "askldjas/aklsdj" { matchStatus = 200 }
it "allows the definition of a fallback handler for custom verb" $
request "MYVERB" "/askldjas/aklsdj" [] "" `shouldRespondWith` "askldjas/aklsdj" { matchStatus = 200 }
it "detected the preferred format" $
request "GET" "/preferred-format" [("Accept", "text/html,application/xml;q=0.9,image/webp,*/*;q=0.8")] "" `shouldRespondWith` "html" { matchStatus = 200 }
it "/test-slash and test-noslash are the same thing" $
do get "/test-slash" `shouldRespondWith` "ok" { matchStatus = 200 }
get "test-slash" `shouldRespondWith` "ok" { matchStatus = 200 }
get "/test-noslash" `shouldRespondWith` "ok" { matchStatus = 200 }
get "test-noslash" `shouldRespondWith` "ok" { matchStatus = 200 }
it "allows custom verbs" $
request "NOTIFY" "/notify/itnotifies" [] "" `shouldRespondWith` "itnotifies" { matchStatus = 200 }
where
verbTestGp verb verbVerbose =
verb "/verb-test-gp" `shouldRespondWith` (verbVerbose { matchStatus = 200 })
verbTest verb verbVerbose =
verb "/verb-test" `shouldRespondWith` (verbVerbose { matchStatus = 200 })
errorHandlerSpec :: IO Wai.Application -> Spec
errorHandlerSpec app =
with app $ describe "Error Handler" $
do it "handles non-existing routes correctly" $
do get "/non/existing/route" `shouldRespondWith` "NOT FOUND" { matchStatus = 404 }
post "/non/existing/route" "" `shouldRespondWith` "NOT FOUND" { matchStatus = 404 }
put "/non/existing/route" "" `shouldRespondWith` "NOT FOUND" { matchStatus = 404 }
patch "/non/existing/route" "" `shouldRespondWith` "NOT FOUND" { matchStatus = 404 }
it "handles server errors correctly" $
get "/failing/route" `shouldRespondWith` "SERVER ERROR" { matchStatus = 500 }
it "does not interfere with user emitted errors" $
get "/user/error" `shouldRespondWith` "UNAUTHORIZED" { matchStatus = 403 }
actionSpec :: SpecWith Wai.Application
actionSpec =
describe "Action Framework" $
do it "handles auth correctly" $
do request methodGet "/auth/user/pass" [mkAuthHeader "user" "pass"] "" `shouldRespondWith` "ok" { matchStatus = 200 }
request methodGet "/auth/user/pass" [mkAuthHeader "user" ""] "" `shouldRespondWith` "err" { matchStatus = 401 }
request methodGet "/auth/user/pass" [mkAuthHeader "" ""] "" `shouldRespondWith` "err" { matchStatus = 401 }
request methodGet "/auth/user/pass" [mkAuthHeader "asd" "asd"] "" `shouldRespondWith` "err" { matchStatus = 401 }
request methodGet "/auth/user/pass" [] "" `shouldRespondWith` "Authentication required. " { matchStatus = 401 }
where
mkAuthHeader :: BS.ByteString -> BS.ByteString -> Header
mkAuthHeader user pass =
("Authorization", "Basic " <> (B64.encode $ user <> ":" <> pass))
cookieTest :: SpecWith Wai.Application
cookieTest =
describe "Cookies" $
do it "sets single cookies correctly" $
get "/cookie/single" `shouldRespondWith`
"set"
{ matchStatus = 200
, matchHeaders =
[ matchCookie "single" "test"
]
}
it "sets multiple cookies correctly" $
get "/cookie/multiple" `shouldRespondWith`
"set"
{ matchStatus = 200
, matchHeaders =
[ matchCookie "multiple1" "test1"
, matchCookie "multiple2" "test2"
]
}
headerTest :: SpecWith Wai.Application
headerTest =
describe "Headers" $
do it "supports custom headers" $
get "/set-header" `shouldRespondWith`
"ok"
{ matchStatus = 200
, matchHeaders =
[ "X-FooBar" <:> "Baz"
]
}
it "supports multi headers" $
get "/set-multi-header" `shouldRespondWith`
"ok"
{ matchStatus = 200
, matchHeaders =
[ "Content-Language" <:> "de"
, "Content-Language" <:> "en"
]
}
matchCookie :: T.Text -> T.Text -> MatchHeader
matchCookie name val =
MatchHeader $ \headers ->
let relevantHeaders = filter (\h -> fst h == "Set-Cookie") headers
loop [] =
Just ("No cookie named " ++ T.unpack name ++ " with value "
++ T.unpack val ++ " found")
loop (x:xs) =
let (cname, cval) = T.breakOn "=" $ fst $ T.breakOn ";" $ T.decodeUtf8 $ snd x
in if cname == name && cval == "=" <> val
then Nothing
else loop xs
in loop relevantHeaders