Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transform Main.hs into Tests #1

Merged
merged 4 commits into from
Jul 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions servant-client/servant-client.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ test-suite spec
, kan-extensions
, servant-client
, servant-client-core
, sop-core
, stm
, text
, transformers
Expand Down
51 changes: 46 additions & 5 deletions servant-client/test/Servant/ClientTestUtils.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
Expand All @@ -26,10 +27,14 @@ import Control.Concurrent
import Control.Monad.Error.Class
(throwError)
import Data.Aeson
import qualified Data.ByteString.Lazy as LazyByteString
import Data.Char
(chr, isPrint)
import Data.Monoid ()
import Data.Proxy
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
import GHC.Generics
(Generic)
import qualified Network.HTTP.Client as C
Expand All @@ -47,8 +52,11 @@ import Servant.API
((:<|>) ((:<|>)), (:>), AuthProtect, BasicAuth,
BasicAuthData (..), Capture, CaptureAll,
DeleteNoContent, EmptyAPI, FormUrlEncoded, Get, Header,
Headers, JSON, NoContent (NoContent), Post, QueryFlag,
QueryParam, QueryParams, Raw, ReqBody, addHeader)
Headers, JSON, MimeRender(mimeRender),
MimeUnrender(mimeUnrender), NoContent (NoContent), PlainText,
Post, QueryFlag, QueryParam, QueryParams, Raw, ReqBody,
StdMethod(GET), Union, UVerb, WithStatus(WithStatus),
addHeader)
import Servant.Client
import qualified Servant.Client.Core.Auth as Auth
import Servant.Server
Expand All @@ -63,7 +71,7 @@ _ = client comprehensiveAPIWithoutStreaming
data Person = Person
{ _name :: String
, _age :: Integer
} deriving (Eq, Show, Generic)
} deriving (Eq, Show, Read, Generic)

instance ToJSON Person
instance FromJSON Person
Expand All @@ -74,6 +82,22 @@ instance FromForm Person
instance Arbitrary Person where
arbitrary = Person <$> arbitrary <*> arbitrary

instance MimeRender PlainText Person where
mimeRender _ = LazyByteString.fromStrict . encodeUtf8 . Text.pack . show

instance MimeRender ctype a => MimeRender ctype (WithStatus _status a) where
mimeRender contentTypeProxy (WithStatus a) = mimeRender contentTypeProxy a

instance MimeUnrender ctype a => MimeUnrender ctype (WithStatus _status a) where
mimeUnrender contentTypeProxy input =
WithStatus <$> mimeUnrender contentTypeProxy input

instance MimeUnrender PlainText Person where
mimeUnrender _ =
-- This does not handle any errors, but it should be fine for tests
Right . read . Text.unpack . decodeUtf8 . LazyByteString.toStrict


alice :: Person
alice = Person "Alice" 42

Expand Down Expand Up @@ -105,6 +129,12 @@ type Api =
:<|> "deleteContentType" :> DeleteNoContent
:<|> "redirectWithCookie" :> Raw
:<|> "empty" :> EmptyAPI
:<|> "uverb-success-or-redirect" :>
Capture "bool" Bool :>
UVerb 'GET '[PlainText] '[WithStatus 200 Person,
WithStatus 301 Text]
:<|> "uverb-get-created" :> UVerb 'GET '[PlainText] '[WithStatus 201 Person]


api :: Proxy Api
api = Proxy
Expand All @@ -126,6 +156,10 @@ getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
getRespHeaders :: ClientM (Headers TestHeaders Bool)
getDeleteContentType :: ClientM NoContent
getRedirectWithCookie :: HTTP.Method -> ClientM Response
uverbGetSuccessOrRedirect :: Bool
-> ClientM (Union '[WithStatus 200 Person,
WithStatus 301 Text])
uverbGetCreated :: ClientM (Union '[WithStatus 201 Person])

getRoot
:<|> getGet
Expand All @@ -143,7 +177,9 @@ getRoot
:<|> getRespHeaders
:<|> getDeleteContentType
:<|> getRedirectWithCookie
:<|> EmptyClient = client api
:<|> EmptyClient
:<|> uverbGetSuccessOrRedirect
:<|> uverbGetCreated = client api

server :: Application
server = serve api (
Expand All @@ -166,7 +202,12 @@ server = serve api (
:<|> (return $ addHeader 1729 $ addHeader "eg2" True)
:<|> return NoContent
:<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.found302 [("Location", "testlocation"), ("Set-Cookie", "testcookie=test")] "")
:<|> emptyServer)
:<|> emptyServer
:<|> (\shouldRedirect -> if shouldRedirect
then respond (WithStatus @301 ("redirecting" :: Text))
else respond (WithStatus @200 alice ))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
else respond (WithStatus @200 alice ))
else respond (WithStatus @200 alice))

:<|> respond (WithStatus @201 carol)
)

type FailApi =
"get" :> Raw
Expand Down
49 changes: 48 additions & 1 deletion servant-client/test/Servant/SuccessSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,24 @@ import Control.Concurrent.STM.TVar
(newTVar, readTVar)
import Data.Foldable
(forM_, toList)
import Data.Functor.Identity
(Identity(Identity))
import Data.Maybe
(listToMaybe)
import Data.Monoid ()
import Data.SOP.NS
(NS(Z))
import qualified Network.HTTP.Client as C
import qualified Network.HTTP.Types as HTTP
import Network.HTTP.Types.Status
(status301)
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.HUnit
import Test.QuickCheck

import Servant.API
(NoContent (NoContent), getHeaders)
(NoContent (NoContent), WithStatus(WithStatus), getHeaders)
import Servant.Client
import qualified Servant.Client.Core.Request as Req
import Servant.Client.Internal.HttpClient (defaultMakeClientRequest)
Expand Down Expand Up @@ -151,3 +157,44 @@ successSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
result <- left show <$> runClient (getMultiple cap num flag body) baseUrl
return $
result === Right (cap, num, flag, body)

context "With a route that can either return success or redirect" $ do
it "Redirects when appropriate" $ \(_, baseUrl) -> do
eitherResponse <- runClient (uverbGetSuccessOrRedirect True) baseUrl

-- This is what we would actually want, since the 301 is part of the
-- declared api:
-- case eitherResponse of
-- Left clientError -> fail $ show clientError
-- Right response ->
-- case response of
-- Z (Identity (WithStatus person :: WithStatus 200 Person)) ->
-- fail "Expected to be redirected"
-- S (last) ->
-- let Identity (WithStatus message :: WithStatus 301 Text)
-- = unZ last
-- in message `shouldBe` "redirecting"
Comment on lines +169 to +176
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-- Right response ->
-- case response of
-- Z (Identity (WithStatus person :: WithStatus 200 Person)) ->
-- fail "Expected to be redirected"
-- S (last) ->
-- let Identity (WithStatus message :: WithStatus 301 Text)
-- = unZ last
-- in message `shouldBe` "redirecting"
-- Right (Z (Identity (WithStatus person :: WithStatus 200 Person)) ->
-- fail "Expected to be redirected"
-- Right (S last) ->
-- let Identity (WithStatus message :: WithStatus 301 Text) = unZ last
-- in message `shouldBe` "redirecting"

(you could collapse the nexted cases in the actual test as well if you like, but i agree, that code shouldn't make it out of the uverb-branch.)

--
-- But since servant-client interprets the 301 as an error, this is the
-- behaviour we actually have
case eitherResponse of
Left clientError ->
case clientError of
FailureResponse _request response -> do
responseStatusCode response `shouldBe` status301
responseBody response `shouldBe` "redirecting"
r ->
fail $ "Expected FailureResponse, got: " <> show r
Right r ->
fail $ "Expected a ClientError, got: " <> show r

it "Returns a proper response when appropriate" $ \(_, baseUrl) -> do
Right response <- runClient (uverbGetSuccessOrRedirect False) baseUrl
let Z (Identity (WithStatus person :: WithStatus 200 Person)) = response
person `shouldBe` alice

context "with a route that uses uverb but only has a single response" $
it "returns the expected response" $ \(_, baseUrl) -> do
Right response <- runClient (uverbGetCreated) baseUrl
let Z (Identity (WithStatus person :: WithStatus 201 Person)) = response
person `shouldBe` carol
99 changes: 84 additions & 15 deletions servant-server/test/Servant/ServerSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# OPTIONS_GHC -freduction-depth=100 #-}

module Servant.ServerSpec where
Expand Down Expand Up @@ -49,14 +48,16 @@ import Network.Wai.Test
import Servant.API
((:<|>) (..), (:>), AuthProtect, BasicAuth,
BasicAuthData (BasicAuthData), Capture, Capture', CaptureAll,
Delete, EmptyAPI, Get, Header, Headers, HttpVersion,
IsSecure (..), JSON, Lenient, NoContent (..), NoContentVerb,
NoFraming, OctetStream, Patch, PlainText, Post, Put,
QueryFlag, QueryParam, QueryParams, Raw, RemoteHost, ReqBody,
SourceIO, StdMethod (..), Stream, Strict, Verb, addHeader)
Delete, EmptyAPI, Get, HasStatus(StatusOf), Header, Headers,
HttpVersion, IsSecure (..), JSON, Lenient, NoContent (..),
NoContentVerb, NoFraming, OctetStream, Patch, PlainText, Post,
Put, QueryFlag, QueryParam, QueryParams, Raw, RemoteHost,
ReqBody, SourceIO, StdMethod (..), Stream, Strict, Union,
UVerb, Verb, addHeader)
import Servant.Server
(Context ((:.), EmptyContext), Handler, Server, Tagged (..),
emptyServer, err401, err403, err404, serve, serveWithContext)
emptyServer, err401, err403, err404, respond, serve,
serveWithContext)
import Servant.Test.ComprehensiveAPI
import qualified Servant.Types.SourceT as S
import Test.Hspec
Expand Down Expand Up @@ -87,6 +88,7 @@ comprehensiveApiContext = NamedContext EmptyContext :. EmptyContext
spec :: Spec
spec = do
verbSpec
uverbSpec
captureSpec
captureAllSpec
queryParamSpec
Expand Down Expand Up @@ -253,8 +255,8 @@ captureSpec = do

with (return (serve
(Proxy :: Proxy (Capture "captured" String :> Raw))
(\ "captured" -> Tagged $ \request_ respond ->
respond $ responseLBS ok200 [] (cs $ show $ pathInfo request_)))) $ do
(\ "captured" -> Tagged $ \request_ sendResponse ->
sendResponse $ responseLBS ok200 [] (cs $ show $ pathInfo request_)))) $ do
it "strips the captured path snippet from pathInfo" $ do
get "/captured/foo" `shouldRespondWith` (fromString (show ["foo" :: String]))

Expand Down Expand Up @@ -305,8 +307,8 @@ captureAllSpec = do

with (return (serve
(Proxy :: Proxy (CaptureAll "segments" String :> Raw))
(\ _captured -> Tagged $ \request_ respond ->
respond $ responseLBS ok200 [] (cs $ show $ pathInfo request_)))) $ do
(\ _captured -> Tagged $ \request_ sendResponse ->
sendResponse $ responseLBS ok200 [] (cs $ show $ pathInfo request_)))) $ do
it "consumes everything from pathInfo" $ do
get "/captured/foo/bar/baz" `shouldRespondWith` (fromString (show ([] :: [Int])))

Expand Down Expand Up @@ -544,8 +546,8 @@ rawApi :: Proxy RawApi
rawApi = Proxy

rawApplication :: Show a => (Request -> a) -> Tagged m Application
rawApplication f = Tagged $ \request_ respond ->
respond $ responseLBS ok200 []
rawApplication f = Tagged $ \request_ sendResponse ->
sendResponse $ responseLBS ok200 []
(cs $ show $ f request_)

rawSpec :: Spec
Expand Down Expand Up @@ -706,7 +708,7 @@ basicAuthApi = Proxy
basicAuthServer :: Server BasicAuthAPI
basicAuthServer =
const (return jerry) :<|>
(Tagged $ \ _ respond -> respond $ responseLBS imATeapot418 [] "")
(Tagged $ \ _ sendResponse -> sendResponse $ responseLBS imATeapot418 [] "")

basicAuthContext :: Context '[ BasicAuthCheck () ]
basicAuthContext =
Expand Down Expand Up @@ -751,7 +753,7 @@ genAuthApi = Proxy

genAuthServer :: Server GenAuthAPI
genAuthServer = const (return tweety)
:<|> (Tagged $ \ _ respond -> respond $ responseLBS imATeapot418 [] "")
:<|> (Tagged $ \ _ sendResponse -> sendResponse $ responseLBS imATeapot418 [] "")

type instance AuthServerData (AuthProtect "auth") = ()

Expand Down Expand Up @@ -781,6 +783,73 @@ genAuthSpec = do
it "plays nice with subsequent Raw endpoints" $ do
get "/foo" `shouldRespondWith` 418

-- }}}
------------------------------------------------------------------------------
-- * UVerb {{{
------------------------------------------------------------------------------

newtype PersonResponse = PersonResponse Person
deriving Generic
instance ToJSON PersonResponse
instance HasStatus PersonResponse where
type StatusOf PersonResponse = 200

newtype RedirectResponse = RedirectResponse String
deriving Generic
instance ToJSON RedirectResponse
instance HasStatus RedirectResponse where
type StatusOf RedirectResponse = 301

newtype AnimalResponse = AnimalResponse Animal
deriving Generic
instance ToJSON AnimalResponse
instance HasStatus AnimalResponse where
type StatusOf AnimalResponse = 203


type UVerbApi
= "person" :> Capture "shouldRedirect" Bool :> UVerb 'GET '[JSON] '[PersonResponse, RedirectResponse]
:<|> "animal" :> UVerb 'GET '[JSON] '[AnimalResponse]

uverbSpec :: Spec
uverbSpec = describe "Servant.API.UVerb " $ do
let
joe = Person "joe" 42
mouse = Animal "Mouse" 7

personHandler
:: Bool
-> Handler (Union '[PersonResponse
,RedirectResponse])
personHandler True = respond $ RedirectResponse "over there!"
personHandler False = respond $ PersonResponse joe

animalHandler = respond $ AnimalResponse mouse

server :: Server UVerbApi
server = personHandler :<|> animalHandler

with (pure $ serve (Proxy :: Proxy UVerbApi) server) $ do
context "A route returning either 301/String or 200/Person" $ do
context "when requesting the person" $ do
let theRequest = THW.get "/person/false"
it "returns status 200" $
theRequest `shouldRespondWith` 200
it "returns a person" $ do
response <- theRequest
liftIO $ decode' (simpleBody response) `shouldBe` Just joe
context "requesting the redirect" $
it "returns a message and status 301" $
THW.get "/person/true"
`shouldRespondWith` "\"over there!\"" {matchStatus = 301}
context "a route with a single response type" $ do
let theRequest = THW.get "/animal"
it "should return the defined status code" $
theRequest `shouldRespondWith` 203
it "should return the expected response" $ do
response <- theRequest
liftIO $ decode' (simpleBody response) `shouldBe` Just mouse

-- }}}
------------------------------------------------------------------------------
-- * Test data types {{{
Expand Down