roboservant/test/Spec.hs
Mark Wotton c61d4868cc hlint
2021-08-05 06:39:58 -05:00

171 lines
6.5 KiB
Haskell

{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DataKinds #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE TypeFamilies #-}
import qualified Breakdown
import Data.Dynamic (toDyn)
import Data.Hashable (Hashable (hash))
import Data.Maybe (isNothing)
import Data.Void (Void)
import qualified Foo
import qualified Headers
import qualified Nested
import qualified Post
import qualified Put
import qualified Product
import qualified QueryParams
import qualified Roboservant as R
import qualified Roboservant.Server as RS
import qualified Roboservant.Client as RC
import qualified Seeded
import Test.Hspec
import Test.Hspec.Core.Spec (FailureReason (Reason), ResultStatus (Failure, Success), itemExample, mapSpecItem_, resultStatus)
import qualified Valid
import Servant ( Server, Proxy(..), serve, Endpoints, HasServer )
import Servant.Client(ClientEnv, mkClientEnv, baseUrlPort, parseBaseUrl,HasClient,ClientM)
import Network.Wai(Application)
import qualified Network.Wai.Handler.Warp as Warp
import Network.HTTP.Client (newManager, defaultManagerSettings)
import Control.Monad((>=>))
main :: IO ()
main = hspec spec
fuzzBoth
:: forall a .
(R.ToReifiedApi (Endpoints a), HasServer a '[], RS.FlattenServer a, RC.ToReifiedClientApi (Endpoints a), RC.FlattenClient a,
HasClient ClientM a)
=> String -> Server a -> R.Config -> (Maybe R.Report -> IO ()) -> Spec
fuzzBoth name server config condition = do
it (name <> " via server") $
RS.fuzz @a server config >>= condition
around (withServer (serve (Proxy :: Proxy a) server)) $
it (name <> " via client") $ \(clientEnv::ClientEnv) -> do
RC.fuzz @a clientEnv config >>= condition
withServer :: Application -> ActionWith ClientEnv -> IO ()
withServer app action = Warp.testWithApplication (pure app) (genClientEnv >=> action)
where genClientEnv port = do
baseUrl <- parseBaseUrl "http://localhost"
manager <- newManager defaultManagerSettings
pure $ mkClientEnv manager (baseUrl { baseUrlPort = port })
spec :: Spec
spec = do
describe "Basic usage" $ do
describe "noError" $ do
fuzzBoth @Valid.Api "find no error in a basic app" Valid.server R.defaultConfig (`shouldSatisfy` isNothing)
fuzzBoth @Valid.RoutedApi "finds no error in a valid generic app" Valid.routedServer R.defaultConfig (`shouldSatisfy` isNothing)
fuzzBoth @Valid.Api "fails coverage check" Valid.server R.defaultConfig {R.coverageThreshold = 0.6}
(\r ->
fmap (R.failureReason . R.rsException) r
`shouldSatisfy` ( \case
Just (R.InsufficientCoverage _) -> True
_ -> False
))
describe "posted body" $
fuzzBoth @Post.Api "passes a coverage check using a posted body" Post.server R.defaultConfig {R.coverageThreshold = 0.99}
(`shouldSatisfy` isNothing)
describe "PUTted body" $
fuzzBoth @Put.Api "passes a coverage check using a posted body" Put.server R.defaultConfig {R.coverageThreshold = 0.99}
(`shouldSatisfy` isNothing)
describe "seeded" $ do
let res = Seeded.Seed 1
shouldFail $ fuzzBoth @Seeded.Api "finds an error using information passed in" Seeded.server
(R.defaultConfig {R.seed = [(toDyn res, hash res)]})
(`shouldSatisfy` isNothing)
describe "Foo" $
fuzzBoth @Foo.Api "finds an error in a basic app" Foo.server R.defaultConfig (`shouldSatisfy` serverFailure)
describe "QueryParams" $
fuzzBoth @QueryParams.Api "can handle query params" QueryParams.server R.defaultConfig { R.seed = [R.hashedDyn (12::Int)] }
(`shouldSatisfy` isNothing)
describe "BuildFrom" $ do
describe "headers (and sum types)" $
fuzzBoth @Headers.Api "should find a failure that's dependent on using header info" Headers.server R.defaultConfig
(`shouldSatisfy` serverFailure)
describe "product types" $
fuzzBoth @Product.Api "should find a failure that's dependent on creating a product" Product.server
R.defaultConfig {R.seed = [R.hashedDyn 'a', R.hashedDyn (1 :: Int)]}
(`shouldSatisfy` serverFailure)
describe "Breakdown" $ do
fuzzBoth @Breakdown.ProductApi "handles products" Breakdown.productServer R.defaultConfig
(`shouldSatisfy` serverFailure)
fuzzBoth @Breakdown.SumApi "handles sums" Breakdown.sumServer R.defaultConfig
(`shouldSatisfy` serverFailure)
describe "flattening" $
fuzzBoth @Nested.FlatApi "can handle nested apis" Nested.server R.defaultConfig {R.coverageThreshold = 0.99}
(`shouldSatisfy` isNothing)
serverFailure :: Maybe R.Report -> Bool
serverFailure = \case
Just R.Report {..} ->
let R.RoboservantException {..} = rsException
in failureReason /= R.NoPossibleMoves
_ -> False
deriving via (R.Atom Foo.Foo) instance R.Breakdown Foo.Foo
deriving via (R.Atom Foo.Foo) instance R.BuildFrom Foo.Foo
deriving via (R.Atom Headers.Foo) instance R.Breakdown Headers.Foo
deriving via (R.Atom Headers.Foo) instance R.BuildFrom Headers.Foo
deriving via (R.Atom Seeded.Seed) instance R.Breakdown Seeded.Seed
deriving via (R.Atom Seeded.Seed) instance R.BuildFrom Seeded.Seed
deriving via (R.Atom Void) instance R.BuildFrom Void
deriving via (R.Atom Post.FooPost) instance R.Breakdown Post.FooPost
deriving via (R.Atom Post.FooPost) instance R.BuildFrom Post.FooPost
deriving via (R.Atom Put.Foo) instance R.Breakdown Put.Foo
deriving via (R.Atom Put.Foo) instance R.BuildFrom Put.Foo
deriving via (R.Compound Breakdown.Foo) instance R.Breakdown Breakdown.Foo
deriving via (R.Compound Product.Foo) instance R.BuildFrom Product.Foo
deriving via (R.Compound Breakdown.SomeSum) instance R.Breakdown Breakdown.SomeSum
-- | `shouldFail` allows you to assert that a given `Spec` should contain at least one failing test.
-- this is often useful when testing tests.
shouldFail :: SpecWith a -> SpecWith a
shouldFail =
mapSpecItem_
( \i ->
i
{ itemExample = \p a cb -> do
r <- itemExample i p a cb
pure
r
{ resultStatus = case resultStatus r of
Success -> Failure Nothing (Reason "Unexpected success")
Failure _ _ -> Success
x -> x
}
}
)