-
Notifications
You must be signed in to change notification settings - Fork 84
/
HTTPServerSpec.hs
160 lines (147 loc) · 6.91 KB
/
HTTPServerSpec.hs
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
160
module Hydra.API.HTTPServerSpec where
import Hydra.Prelude hiding (get)
import Test.Hydra.Prelude
import Data.Aeson (Result (Error, Success), eitherDecode, encode, fromJSON)
import Data.Aeson qualified as Aeson
import Data.Aeson.Lens (key, nth)
import Hydra.API.HTTPServer (DraftCommitTxRequest (..), DraftCommitTxResponse (..), SubmitTxRequest (..), TransactionSubmitted, httpApp)
import Hydra.API.ServerSpec (dummyChainHandle)
import Hydra.Cardano.Api (
fromLedgerPParams,
serialiseToTextEnvelope,
shelleyBasedEra,
)
import Hydra.Chain (Chain (draftCommitTx), PostTxError (..))
import Hydra.Chain.Direct.Fixture (defaultPParams)
import Hydra.JSONSchema (SchemaSelector, prop_validateJSONSchema, validateJSON, withJsonSpecifications)
import Hydra.Ledger.Cardano (Tx)
import Hydra.Logging (nullTracer)
import System.FilePath ((</>))
import System.IO.Unsafe (unsafePerformIO)
import Test.Aeson.GenericSpecs (roundtripAndGoldenSpecs)
import Test.Hspec.Wai (MatchBody (..), ResponseMatcher (matchBody), get, post, shouldRespondWith, with)
import Test.Hspec.Wai.Internal (withApplication)
import Test.QuickCheck (checkCoverage, cover, generate)
import Test.QuickCheck.Property (counterexample, forAll, property)
spec :: Spec
spec = do
parallel $ do
roundtripAndGoldenSpecs (Proxy @(ReasonablySized (DraftCommitTxResponse Tx)))
roundtripAndGoldenSpecs (Proxy @(ReasonablySized (DraftCommitTxRequest Tx)))
roundtripAndGoldenSpecs (Proxy @(ReasonablySized (SubmitTxRequest Tx)))
roundtripAndGoldenSpecs (Proxy @(ReasonablySized TransactionSubmitted))
prop "Validate /commit publish api schema" $
prop_validateJSONSchema @(DraftCommitTxRequest Tx) "api.json" $
key "components" . key "messages" . key "DraftCommitTxRequest" . key "payload"
prop "Validate /commit subscribe api schema" $
prop_validateJSONSchema @(DraftCommitTxResponse Tx) "api.json" $
key "components" . key "messages" . key "DraftCommitTxResponse" . key "payload"
prop "Validate /cardano-transaction publish api schema" $
prop_validateJSONSchema @(SubmitTxRequest Tx) "api.json" $
key "channels"
. key "/cardano-transaction"
. key "publish"
. key "message"
. key "payload"
prop "Validate /cardano-transaction subscribe api schema" $
prop_validateJSONSchema @TransactionSubmitted "api.json" $
key "channels"
. key "/cardano-transaction"
. key "subscribe"
. key "message"
. key "oneOf"
. nth 0
. key "payload"
apiServerSpec
describe "SubmitTxRequest accepted tx formats" $ do
prop "accepts json encoded transaction" $
forAll (arbitrary @Tx) $ \tx ->
let json = toJSON tx
in case fromJSON @(SubmitTxRequest Tx) json of
Success{} -> property True
Error e -> counterexample (toString $ toText e) $ property False
prop "accepts transaction encoded as TextEnvelope" $
forAll (arbitrary @Tx) $ \tx ->
let json = toJSON $ serialiseToTextEnvelope Nothing tx
in case fromJSON @(SubmitTxRequest Tx) json of
Success{} -> property True
Error e -> counterexample (toString $ toText e) $ property False
apiServerSpec :: Spec
apiServerSpec = do
describe "API should respond correctly" $ do
describe "GET /protocol-parameters" $ do
let getHeadId = pure Nothing
let webServer = httpApp nullTracer dummyChainHandle defaultPParams getHeadId
with (return webServer) $ do
it "matches schema" $
withJsonSpecifications $ \schemaDir -> do
get "/protocol-parameters"
`shouldRespondWith` 200
{ matchBody =
matchValidJSON
(schemaDir </> "api.json")
(key "components" . key "messages" . key "ProtocolParameters" . key "payload")
}
it "responds given parameters" $
get "/protocol-parameters"
`shouldRespondWith` 200
{ matchBody = matchJSON $ fromLedgerPParams shelleyBasedEra defaultPParams
}
describe "POST /commit" $ do
let getHeadId = pure $ Just (generateWith arbitrary 42)
let workingChainHandle =
dummyChainHandle
{ draftCommitTx = \_ _ _ -> do
tx <- generate $ arbitrary @Tx
pure $ Right tx
}
prop "responds on valid requests" $ \(request :: DraftCommitTxRequest Tx) ->
withApplication (httpApp nullTracer workingChainHandle defaultPParams getHeadId) $ do
post "/commit" (Aeson.encode request)
`shouldRespondWith` 200
let failingChainHandle postTxError =
dummyChainHandle
{ draftCommitTx = \_ _ _ -> pure $ Left postTxError
}
prop "handles PostTxErrors accordingly" $ \request postTxError -> do
let expectedResponse =
case postTxError of
SpendingNodeUtxoForbidden -> 400
CommittedTooMuchADAForMainnet{} -> 400
UnsupportedLegacyOutput{} -> 400
_ -> 500
let coverage = case postTxError of
SpendingNodeUtxoForbidden -> cover 1 True "SpendingNodeUtxoForbidden"
CommittedTooMuchADAForMainnet{} -> cover 1 True "CommittedTooMuchADAForMainnet"
UnsupportedLegacyOutput{} -> cover 1 True "UnsupportedLegacyOutput"
InvalidHeadId{} -> cover 1 True "InvalidHeadId"
CannotFindOwnInitial{} -> cover 1 True "CannotFindOwnInitial"
_ -> property
checkCoverage $
coverage $
withApplication (httpApp nullTracer (failingChainHandle postTxError) defaultPParams getHeadId) $ do
post "/commit" (Aeson.encode (request :: DraftCommitTxRequest Tx))
`shouldRespondWith` expectedResponse
-- * Helpers
-- | Create a 'ResponseMatcher' or 'MatchBody' from a JSON serializable value
-- (using their 'IsString' instances).
matchJSON :: (IsString s, ToJSON a) => a -> s
matchJSON = fromString . decodeUtf8 . encode
-- | Create a 'MatchBody' that validates the returned JSON response against a
-- schema. NOTE: This raises impure exceptions, so only use it in this test
-- suite.
matchValidJSON :: FilePath -> SchemaSelector -> MatchBody
matchValidJSON schemaFile selector =
MatchBody $ \_headers body ->
case eitherDecode body of
Left err -> Just $ "failed to decode body: " <> err
Right value -> validateJSONPure value
where
-- NOTE: Uses unsafePerformIO to create a pure API although we are actually
-- calling an external program to verify the schema. This is fine, because the
-- call is referentially transparent and any given invocation of schema file,
-- selector and value will always yield the same result and can be shared.
validateJSONPure value =
unsafePerformIO $ do
validateJSON schemaFile selector value
pure Nothing