Skip to content
Snippets Groups Projects
SpecStuff.hs 3.69 KiB
Newer Older
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module SpecStuff where

import Test.Tasty
  ( TestTree
  , testGroup
  )
import Test.Tasty.Wai
  ( assertBody
  , assertStatus
  , assertStatus'
  , get
  , post
  , testWai
  )
import Test.Tasty.QuickCheck
  ( testProperty
  )
import PaymentServer.Persistence
  ( Voucher
  )
import PaymentServer.Redemption
  ( RedemptionAPI
  , Result(Failed, Succeeded)
  )
import PaymentServer.Issuer
  ( BlindedToken
  )
import Data.ByteString
  ( ByteString
  )
import Text.Printf
  ( printf
  )
import Data.Aeson
 ( decode
 , encode
 )
import Servant
  ( Application
  , Proxy(Proxy)
  , serve
  )
import Test.Hspec
  ( Spec
  , parallel
  , describe
  , it
  , runIO
  )
import Network.HTTP.Types
  ( Header
  )
import Test.Hspec.Wai
  ( ResponseMatcher(matchBody, matchHeaders)
  , (<:>)
  , WaiExpectation
  , Body
  , MatchBody(MatchBody)
  , with
  , shouldRespondWith
  , liftIO
  )
import Test.QuickCheck
  ( ioProperty
  )
import Test.Hspec.Wai.QuickCheck
  ( Testable(toProperty)
  , WaiProperty(unWaiProperty)
  , property
  )
import Test.QuickCheck.Instances.Text ()
import Util.Spec
  ( wrongMethodNotAllowed
  , nonJSONUnsupportedMediaType
  , wrongJSONInvalidRequest
  )
import Util.WAI
  ( postJSON
  )
import PaymentServer.Issuer
  ( BlindedToken
  , ChallengeBypass(ChallengeBypass)
  , Issuer
  , trivialIssue
  )
import PaymentServer.Redemption
  ( RedemptionAPI
  , Redeem(Redeem)
  , Result(Failed, Succeeded)
  , redemptionServer
  )
import PaymentServer.Persistence
  ( RedeemError(NotPaid)
  , Voucher
  , VoucherDatabase(payForVoucher, redeemVoucher)
  , memory
  )

redemptionAPI :: Proxy RedemptionAPI
redemptionAPI = Proxy

app :: VoucherDatabase d => Issuer -> d -> Application
app issue = serve redemptionAPI . redemptionServer issue

path = "/"

propertyRedeem :: ByteString -> Voucher -> [BlindedToken] -> ResponseMatcher -> WaiExpectation
propertyRedeem path voucher tokens matcher =
  postJSON path (encode $ Redeem voucher tokens) `shouldRespondWith` matcher

-- | A VoucherDatabaseTestDouble has a VoucherDatabase instance which provides
-- a number of different behaviors which are useful to be able to directly
-- test against.
data VoucherDatabaseTestDouble
  -- | A RefuseRedemption database always refuses redemption with a given error.
  = RefuseRedemption RedeemError
  -- | A PermitRedemption database always permits redemption.
  | PermitRedemption
  deriving (Show)

instance VoucherDatabase VoucherDatabaseTestDouble where
  payForVoucher _ voucher = return ()
  redeemVoucher (RefuseRedemption err) _ _ = return $ Left err
  redeemVoucher PermitRedemption _ _ = return $ Right ()

test_foo :: TestTree
test_foo = testGroup "Foo" redemption

redemption :: [TestTree]
redemption =
  [ testProperty "fails when the voucher is not paid" $
    \(voucher :: Voucher) (tokens :: [BlindedToken]) ->
      propertyRedeem path voucher tokens 400
      { matchBody = matchJSONBody Failed
      -- major/minor, fine.  charset=utf-8... okay.  but really this is
      -- overspecified by encoding the exact byte sequence.  I'd rather
      -- assert semantic equality.
      , matchHeaders = ["Content-Type" <:> "application/json;charset=utf-8"]
      }
  ]

matchJSONBody :: Result -> MatchBody
matchJSONBody expected =
  let
    bodyMatcher :: [Header] -> Body -> Maybe String
    bodyMatcher headers actualBody =
      case decode actualBody of
        Nothing ->
          Just $ "failed to decode body as value of expected type: " ++ show actualBody
        Just actual ->
          if actual == expected then
            Nothing
          else
            Just $ "decoded body does not equal expected value: " ++ show actual ++ show expected
  in
    MatchBody bodyMatcher