Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
{-# 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