1 {-# LANGUAGE OverloadedStrings #-}
 
   2 {-# LANGUAGE TypeApplications #-}
 
   3 module Hspec.Router.Error where
 
   4 import Control.Monad (Monad(..))
 
   6 import Data.Either (Either(..))
 
   7 import Data.Maybe (Maybe(..))
 
   8 import Data.Function (($), (.))
 
  10 import Text.Show (Show(..))
 
  11 import Text.Read (readMaybe)
 
  16 import Test.Tasty.Hspec
 
  17 import Data.Semigroup (Semigroup(..))
 
  18 import qualified Data.ByteString.Lazy as BSL
 
  19 import qualified Data.Text as Text
 
  20 import qualified Data.Text.Encoding as Text
 
  21 import qualified Data.Text.Lazy as TL
 
  22 import qualified Data.Text.Lazy.Encoding as TL
 
  23 import qualified Network.HTTP.Types as HTTP
 
  24 import qualified Network.Wai.Handler.Warp as Warp
 
  30   <.> query @Int "param"
 
  31   <.> endpoint @Int @PlainText HTTP.methodPost
 
  32 rtr = runRouter api $ route_good
 
  34         route_good i qry (RouterEndpointArg respond) =
 
  35                 RouterResponse $ \_req res -> do
 
  36                         res $ respond status200 [] i
 
  38 srv = Warp.run 8080 rtr
 
  39 instance MimeSerialize PlainText Int where
 
  40         mimeSerialize _mt = BSL.fromStrict . Text.encodeUtf8 . Text.pack . show
 
  41 instance MimeUnserialize PlainText Int where
 
  42         mimeUnserialize _mt s =
 
  43                 case readMaybe $ TL.unpack $ TL.decodeUtf8 s of
 
  45                  _ -> Left "cannot parse Int"
 
  48         testSpec "Error order" $
 
  49                 with (return rtr) $ do
 
  50                         it "has 404 as its highest priority error" $ do
 
  51                                 request badMethod badURI [badAuth, badContentType, badAccept] badBody
 
  52                                 `shouldRespondWith` 404
 
  53                         it "has 405 as its second highest priority error" $ do
 
  54                                 request badMethod badParam [badAuth, badContentType, badAccept] badBody
 
  55                                 `shouldRespondWith` 405
 
  56                         it "has 401 as its third highest priority error (auth)" $ do
 
  57                                 request goodMethod badParam [badAuth, badContentType, badAccept] badBody
 
  58                                 `shouldRespondWith` 401
 
  59                         it "has 406 as its fourth highest priority error" $ do
 
  60                                 request goodMethod badParam [goodAuth, badContentType, badAccept] badBody
 
  61                                 `shouldRespondWith` 406
 
  64 badContentType  = (HTTP.hContentType, "application/json")
 
  65 badAccept       = (HTTP.hAccept, "text/plain")
 
  66 badMethod       = HTTP.methodGet
 
  69 badAuth         = (HTTP.hAuthorization, "Basic foofoofoo")
 
  70 goodContentType = (HTTP.hContentType, "text/plain")
 
  71 goodAccept      = (HTTP.hAccept, "text/plain")
 
  72 goodMethod      = HTTP.methodPost
 
  74 goodURI         = goodPath<>"?param=2"
 
  75 badParam        = goodPath<>"?param=foo"
 
  76 goodBody        = {-encode-} (42::Int)
 
  77 -- username:password = servant:server
 
  78 goodAuth        = (HTTP.hAuthorization, "Basic c2VydmFudDpzZXJ2ZXI=")
 
  82 {-# LANGUAGE DataKinds             #-}
 
  83 {-# LANGUAGE MultiParamTypeClasses #-}
 
  84 {-# LANGUAGE OverloadedStrings     #-}
 
  85 {-# LANGUAGE TypeFamilies          #-}
 
  86 {-# LANGUAGE TypeOperators         #-}
 
  87 {-# OPTIONS_GHC -fno-warn-orphans #-}
 
  88 module Servant.Server.ErrorSpec (spec) where
 
  94 import qualified Data.ByteString.Char8      as BC
 
  95 import qualified Data.ByteString.Lazy.Char8 as BCL
 
  99 import           Network.HTTP.Types
 
 100                  (hAccept, hAuthorization, hContentType, methodGet, methodPost,
 
 105 import           Test.Hspec.Wai
 
 110 spec = describe "HTTP Errors" $ do
 
 116 -- * Auth machinery (reused throughout)
 
 118 -- | 'BasicAuthCheck' holds the handler we'll use to verify a username and password.
 
 119 errorOrderAuthCheck :: BasicAuthCheck ()
 
 120 errorOrderAuthCheck =
 
 121   let check (BasicAuthData username password) =
 
 122         if username == "servant" && password == "server"
 
 123         then return (Authorized ())
 
 124         else return Unauthorized
 
 125   in BasicAuthCheck check
 
 127 ------------------------------------------------------------------------------
 
 130 type ErrorOrderApi = "home"
 
 131                   :> BasicAuth "error-realm" ()
 
 132                   :> ReqBody '[JSON] Int
 
 134                   :> QueryParam "param" Int
 
 137 errorOrderApi :: Proxy ErrorOrderApi
 
 138 errorOrderApi = Proxy
 
 140 errorOrderServer :: Server ErrorOrderApi
 
 141 errorOrderServer = \_ _ _ _ -> throwError err402
 
 143 -- On error priorities:
 
 147 -- 404, 405, 401, 415, 400, 406, 402
 
 149 -- but we changed this to
 
 151 -- 404, 405, 401, 406, 415, 400, 402
 
 155 -- This change is due to the body check being irreversible (to support
 
 156 -- streaming). Any check done after the body check has to be made fatal,
 
 157 -- breaking modularity. We've therefore moved the accept check before
 
 158 -- the body check, to allow it being recoverable and modular, and this
 
 159 -- goes along with promoting the error priority of 406.
 
 160 errorOrderSpec :: Spec
 
 162   describe "HTTP error order" $
 
 163     with (return $ serveWithContext errorOrderApi
 
 164                    (errorOrderAuthCheck :. EmptyContext)
 
 167   let badContentType  = (hContentType, "text/plain")
 
 168       badAccept       = (hAccept, "text/plain")
 
 169       badMethod       = methodGet
 
 170       badUrl          = "nonexistent"
 
 172       badAuth         = (hAuthorization, "Basic foofoofoo")
 
 173       goodContentType = (hContentType, "application/json")
 
 174       goodAccept      = (hAccept, "application/json")
 
 175       goodMethod      = methodPost
 
 176       goodUrl         = "home/2?param=55"
 
 177       badParams       = goodUrl <> "?param=foo"
 
 178       goodBody        = encode (5 :: Int)
 
 179       -- username:password = servant:server
 
 180       goodAuth        = (hAuthorization, "Basic c2VydmFudDpzZXJ2ZXI=")
 
 182   it "has 404 as its highest priority error" $ do
 
 183     request badMethod badUrl [badAuth, badContentType, badAccept] badBody
 
 184       `shouldRespondWith` 404
 
 186   it "has 405 as its second highest priority error" $ do
 
 187     request badMethod badParams [badAuth, badContentType, badAccept] badBody
 
 188       `shouldRespondWith` 405
 
 190   it "has 401 as its third highest priority error (auth)" $ do
 
 191     request goodMethod badParams [badAuth, badContentType, badAccept] badBody
 
 192       `shouldRespondWith` 401
 
 194   it "has 406 as its fourth highest priority error" $ do
 
 195     request goodMethod badParams [goodAuth, badContentType, badAccept] badBody
 
 196       `shouldRespondWith` 406
 
 198   it "has 415 as its fifth highest priority error" $ do
 
 199     request goodMethod badParams [goodAuth, badContentType, goodAccept] badBody
 
 200       `shouldRespondWith` 415
 
 202   it "has 400 as its sixth highest priority error" $ do
 
 203     badParamsRes <- request goodMethod badParams [goodAuth, goodContentType, goodAccept] goodBody
 
 204     badBodyRes <- request goodMethod goodUrl [goodAuth, goodContentType, goodAccept] badBody
 
 206     -- Both bad body and bad params result in 400
 
 207     return badParamsRes `shouldRespondWith` 400
 
 208     return badBodyRes `shouldRespondWith` 400
 
 210     -- Param check should occur before body checks
 
 211     both <- request goodMethod badParams [goodAuth, goodContentType, goodAccept ] badBody
 
 212     when (both /= badParamsRes) $ liftIO $
 
 213         expectationFailure $ "badParams + badBody /= badParams: " ++ show both ++ ", " ++ show badParamsRes
 
 214     when (both == badBodyRes) $ liftIO $
 
 215         expectationFailure $ "badParams + badBody == badBody: " ++ show both
 
 217   it "has handler-level errors as last priority" $ do
 
 218     request goodMethod goodUrl [goodAuth, goodContentType, goodAccept] goodBody
 
 219       `shouldRespondWith` 402
 
 221 type PrioErrorsApi = ReqBody '[JSON] Integer :> "foo" :> Get '[JSON] Integer
 
 223 prioErrorsApi :: Proxy PrioErrorsApi
 
 224 prioErrorsApi = Proxy
 
 226 -- Check whether matching continues even if a 'ReqBody' or similar construct
 
 227 -- is encountered early in a path. We don't want to see a complaint about the
 
 228 -- request body unless the path actually matches.
 
 229 prioErrorsSpec :: Spec
 
 230 prioErrorsSpec = describe "PrioErrors" $ do
 
 232   with (return $ serve prioErrorsApi server) $ do
 
 233     let check (mdescr, method) path (cdescr, ctype, body) resp =
 
 235             Test.Hspec.Wai.request method path [(hContentType, ctype)] body
 
 236               `shouldRespondWith` resp
 
 238             fulldescr = "returns " ++ show (matchStatus resp) ++ " on " ++ mdescr
 
 239                      ++ " " ++ BC.unpack path ++ " (" ++ cdescr ++ ")"
 
 241         get' = ("GET", methodGet)
 
 242         put' = ("PUT", methodPut)
 
 244         txt   = ("text"        , "text/plain;charset=utf8"      , "42"        )
 
 245         ijson = ("invalid json", "application/json;charset=utf8", "invalid"   )
 
 246         vjson = ("valid json"  , "application/json;charset=utf8", encode (5 :: Int))
 
 248     check get' "/"    txt   404
 
 249     check get' "/bar" txt   404
 
 250     check get' "/foo" txt   415
 
 251     check put' "/"    txt   404
 
 252     check put' "/bar" txt   404
 
 253     check put' "/foo" txt   405
 
 254     check get' "/"    ijson 404
 
 255     check get' "/bar" ijson 404
 
 256     check get' "/foo" ijson 400
 
 257     check put' "/"    ijson 404
 
 258     check put' "/bar" ijson 404
 
 259     check put' "/foo" ijson 405
 
 260     check get' "/"    vjson 404
 
 261     check get' "/bar" vjson 404
 
 262     check get' "/foo" vjson 200
 
 263     check put' "/"    vjson 404
 
 264     check put' "/bar" vjson 404
 
 265     check put' "/foo" vjson 405
 
 268 ------------------------------------------------------------------------------
 
 272      = "a" :> ReqBody '[JSON] Int      :> Post '[JSON] Int                -- err402
 
 273   :<|> "a" :> ReqBody '[PlainText] Int :> Post '[JSON] Int                -- 1
 
 274   :<|> "a" :> ReqBody '[JSON] Int      :> Post '[PlainText] Int           -- 2
 
 275   :<|> "a" :> ReqBody '[JSON] String   :> Post '[JSON] Int                -- 3
 
 276   :<|> "a" :> ReqBody '[JSON] Int      :> Get  '[JSON] Int                -- 4
 
 277   :<|> "a" :> BasicAuth "bar-realm" ()
 
 278            :> ReqBody '[JSON] Int      :> Get  '[PlainText] Int           -- 5
 
 279   :<|> "a" :> ReqBody '[JSON] Int      :> Get  '[PlainText] Int           -- 6
 
 281   :<|>        ReqBody '[JSON] Int      :> Get  '[JSON] Int                -- 7
 
 282   :<|>        ReqBody '[JSON] Int      :> Post '[JSON] Int                -- 8
 
 284 errorRetryApi :: Proxy ErrorRetryApi
 
 285 errorRetryApi = Proxy
 
 287 errorRetryServer :: Server ErrorRetryApi
 
 289      = (\_ -> throwError err402)
 
 290   :<|> (\_ -> return 1)
 
 291   :<|> (\_ -> return 2)
 
 292   :<|> (\_ -> return 3)
 
 293   :<|> (\_ -> return 4)
 
 294   :<|> (\_ _ -> return 5)
 
 295   :<|> (\_ -> return 6)
 
 296   :<|> (\_ -> return 7)
 
 297   :<|> (\_ -> return 8)
 
 299 errorRetrySpec :: Spec
 
 301   describe "Handler search" $
 
 302     with (return $ serveWithContext errorRetryApi
 
 303                          (errorOrderAuthCheck :. EmptyContext)
 
 307   let jsonCT      = (hContentType, "application/json")
 
 308       jsonAccept  = (hAccept, "application/json")
 
 309       jsonBody    = encode (1797 :: Int)
 
 311   it "should continue when URLs don't match" $ do
 
 312     request methodPost "" [jsonCT, jsonAccept] jsonBody
 
 313      `shouldRespondWith` 200 { matchBody = mkBody $ encode (8 :: Int) }
 
 315   it "should continue when methods don't match" $ do
 
 316     request methodGet "a" [jsonCT, jsonAccept] jsonBody
 
 317      `shouldRespondWith` 200 { matchBody = mkBody $ encode (4 :: Int) }
 
 319     mkBody b = MatchBody $ \_ b' ->
 
 322         else Just "body not correct\n"
 
 325 ------------------------------------------------------------------------------
 
 326 -- * Error Choice {{{
 
 329      = "path0" :> Get '[JSON] Int                                     -- 0
 
 330   :<|> "path1" :> Post '[JSON] Int                                    -- 1
 
 331   :<|> "path2" :> Post '[PlainText] Int                               -- 2
 
 332   :<|> "path3" :> ReqBody '[JSON] Int :> Post '[PlainText] Int        -- 3
 
 333   :<|> "path4" :> (ReqBody '[PlainText] Int :> Post '[PlainText] Int  -- 4
 
 334              :<|>  ReqBody '[PlainText] Int :> Post '[JSON] Int)      -- 5
 
 335   :<|> "path5" :> (ReqBody '[JSON] Int      :> Post '[PlainText] Int  -- 6
 
 336              :<|>  ReqBody '[PlainText] Int :> Post '[PlainText] Int) -- 7
 
 338 errorChoiceApi :: Proxy ErrorChoiceApi
 
 339 errorChoiceApi = Proxy
 
 341 errorChoiceServer :: Server ErrorChoiceApi
 
 342 errorChoiceServer = return 0
 
 345                :<|> (\_ -> return 3)
 
 346                :<|> ((\_ -> return 4) :<|> (\_ -> return 5))
 
 347                :<|> ((\_ -> return 6) :<|> (\_ -> return 7))
 
 350 errorChoiceSpec :: Spec
 
 351 errorChoiceSpec = describe "Multiple handlers return errors"
 
 352                 $ with (return $ serve errorChoiceApi errorChoiceServer) $ do
 
 354   it "should respond with 404 if no path matches" $ do
 
 355     request methodGet "" [] "" `shouldRespondWith` 404
 
 357   it "should respond with 405 if a path but not method matches" $ do
 
 358     request methodGet "path2" [] "" `shouldRespondWith` 405
 
 360   it "should respond with the corresponding error if path and method match" $ do
 
 361     request methodPost "path3" [(hContentType, "text/plain;charset=utf-8")] ""
 
 362       `shouldRespondWith` 415
 
 363     request methodPost "path3" [(hContentType, "application/json")] ""
 
 364       `shouldRespondWith` 400
 
 365     request methodPost "path4" [(hContentType, "text/plain;charset=utf-8"),
 
 366                                 (hAccept, "blah")] "5"
 
 367       `shouldRespondWith` 406
 
 368   it "should respond with 415 only if none of the subservers supports the request's content type" $ do
 
 369     request methodPost "path5" [(hContentType, "text/plain;charset=utf-8")] "1"
 
 370       `shouldRespondWith` 200
 
 371     request methodPost "path5" [(hContentType, "application/json")] "1"
 
 372       `shouldRespondWith` 200
 
 373     request methodPost "path5" [(hContentType, "application/not-supported")] ""
 
 374       `shouldRespondWith` 415
 
 378 ------------------------------------------------------------------------------
 
 381 instance MimeUnrender PlainText Int where
 
 382     mimeUnrender _ x = maybe (Left "no parse") Right (readMay $ BCL.unpack x)
 
 384 instance MimeRender PlainText Int where
 
 385     mimeRender _ = BCL.pack . show