Skip to content

Commit 36b1a32

Browse files
authored
Adds support for URLEncoded request bodies on the requestData field (#52)
* Adds support for URLEncoded request bodies on the requestData field To set a URLEncoded body set bodyType to urlencoded: requestData: bodyType: urlencoded content: a: b c: d e: f To set a JSON body set bodyType to json: requestData: bodyType: json content: a: b c: d e: f In either case the body is formed from the value of the content key. The change is backwards compatible so that the following: requestData: a: b c: d e: A will send a JSON body formed from the value of the requestData key. * Adds queryParameters to example JSON specification. * Adds `requestData` enhancements to JSON and YAML example specs. * Moves KeyValuePairs type into its own module * Adds tests for decoding KeyValuePairs * Moves Payload to its own module and adds documentation. * Adds tests for Payload
1 parent 5353210 commit 36b1a32

File tree

9 files changed

+287
-52
lines changed

9 files changed

+287
-52
lines changed

curl-runnings.cabal

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ cabal-version: 1.12
44
--
55
-- see: https://github.com/sol/hpack
66
--
7-
-- hash: d7e8fd8b9ac832b7f655ad0afb08922e6d81f002810f33a8785f57504b9e38f3
7+
-- hash: 47e14a53d64cd8b70caa137bf6d6d5d667b4b75064f6cebc3ed82c11072d6258
88

99
name: curl-runnings
1010
version: 0.11.1
@@ -37,6 +37,8 @@ library
3737
Testing.CurlRunnings.Internal
3838
Testing.CurlRunnings.Internal.Parser
3939
Testing.CurlRunnings.Internal.Headers
40+
Testing.CurlRunnings.Internal.KeyValuePairs
41+
Testing.CurlRunnings.Internal.Payload
4042
other-modules:
4143
Paths_curl_runnings
4244
hs-source-dirs:
@@ -93,10 +95,13 @@ test-suite curl-runnings-test
9395
test
9496
ghc-options: -threaded -rtsopts -with-rtsopts=-N
9597
build-depends:
96-
base >=4.0 && <5
98+
aeson >=1.2.4.0
99+
, base >=4.0 && <5
100+
, bytestring >=0.10.8.2
97101
, curl-runnings
98102
, directory >=1.3.0.2
99103
, hspec >=2.4.4
100104
, hspec-expectations >=0.8.2
105+
, raw-strings-qq >=1.1
101106
, text >=1.2.2.2
102107
default-language: Haskell2010

examples/example-spec.json

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"name": "test 1",
44
"url": "http://your-endpoint.com/status",
55
"requestMethod": "GET",
6+
"queryParameters": {
7+
"key": "value"
8+
},
69
"expectData": {
710
"exactly": {
811
"okay": true,
@@ -26,6 +29,22 @@
2629
},
2730
{
2831
"name": "test 3",
32+
"url": "http://your-endpoint.com/path",
33+
"requestMethod": "POST",
34+
"expectStatus": [
35+
200,
36+
201
37+
],
38+
"requestData": {
39+
"bodyType": "urlencoded",
40+
"content": {
41+
"hello": "there",
42+
"num": 1
43+
}
44+
}
45+
},
46+
{
47+
"name": "test 4",
2948
"url": "http://your-url.com/other/path",
3049
"requestMethod": "GET",
3150
"expectData": {
@@ -44,7 +63,7 @@
4463
"expectStatus": 200
4564
},
4665
{
47-
"name": "test 4",
66+
"name": "test 5",
4867
"url": "http://your-url.com/other/path",
4968
"requestMethod": "GET",
5069
"expectData": {
@@ -63,7 +82,7 @@
6382
"expectStatus": 200
6483
},
6584
{
66-
"name": "test 5",
85+
"name": "test 6",
6786
"url": "http://your-url.com/other/path",
6887
"requestMethod": "GET",
6988
"expectData": {
@@ -84,15 +103,15 @@
84103
"expectStatus": 200
85104
},
86105
{
87-
"name": "test 6",
106+
"name": "test 7",
88107
"url": "http://your-url.com/other/path",
89108
"requestMethod": "GET",
90109
"headers": "Content-Type: application/json",
91110
"expectStatus": 200,
92111
"expectHeaders": "Content-Type: application/json; Hello: world"
93112
},
94113
{
95-
"name": "test 7",
114+
"name": "test 8",
96115
"url": "http://your-url.com/other/path",
97116
"requestMethod": "GET",
98117
"headers": "Content-Type: application/json",
@@ -104,7 +123,7 @@
104123
]
105124
},
106125
{
107-
"name": "test 8",
126+
"name": "test 9",
108127
"url": "http://your-url.com/other/path",
109128
"requestMethod": "GET",
110129
"headers": "Content-Type: application/json",

examples/example-spec.yaml

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ cases:
3434
num: 1
3535

3636
- name: test 3
37+
url: http://your-endpoint.com/path
38+
requestMethod: POST
39+
expectStatus:
40+
- 200
41+
- 201
42+
# [Optional] Data to send with the request.
43+
requestData:
44+
# `bodyType` specifies the type of request payload. Possible values
45+
# are `urlencoded` | `json`.
46+
bodyType: urlencoded
47+
content:
48+
# This object specifies the data to send with the request.
49+
hello: there
50+
num: 1
51+
52+
- name: test 4
3753
url: http://your-url.com/other/path
3854
requestMethod: GET
3955
expectData:
@@ -54,7 +70,7 @@ cases:
5470
- keyMatch: okay
5571
expectStatus: 200
5672

57-
- name: test 4
73+
- name: test 5
5874
url: http://your-url.com/other/path
5975
requestMethod: GET
6076
expectData:
@@ -70,7 +86,7 @@ cases:
7086
- keyMatch: error
7187
expectStatus: 200
7288

73-
- name: test 5
89+
- name: test 6
7490
url: http://your-url.com/other/path
7591
requestMethod: GET
7692
expectData:
@@ -83,7 +99,7 @@ cases:
8399
- valueMatch: false
84100
expectStatus: 200
85101

86-
- name: test 6
102+
- name: test 7
87103
url: http://your-url.com/other/path
88104
requestMethod: GET
89105
# Specify the headers you want to sent, just like the -H flag in a curl command
@@ -94,7 +110,7 @@ cases:
94110
# Header strings again match the -H syntax from curl
95111
expectHeaders: "Content-Type: application/json; Hello: world"
96112

97-
- name: test 7
113+
- name: test 8
98114
url: http://your-url.com/other/path
99115
requestMethod: GET
100116
headers: "Content-Type: application/json"
@@ -104,7 +120,7 @@ cases:
104120
-
105121
key: "Key-With-Val-We-Dont-Care-About"
106122

107-
- name: test 8
123+
- name: test 9
108124
url: http://your-url.com/other/path
109125
requestMethod: GET
110126
headers: "Content-Type: application/json"
@@ -114,4 +130,3 @@ cases:
114130
- "Hello: world"
115131
-
116132
value: "Value-With-Key-We-Dont-Care-About"
117-

package.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ library:
2525
- Testing.CurlRunnings.Internal
2626
- Testing.CurlRunnings.Internal.Parser
2727
- Testing.CurlRunnings.Internal.Headers
28+
- Testing.CurlRunnings.Internal.KeyValuePairs
29+
- Testing.CurlRunnings.Internal.Payload
2830
dependencies:
2931
- aeson >=1.2.4.0
3032
- bytestring >=0.10.8.2
@@ -75,8 +77,11 @@ tests:
7577
- -rtsopts
7678
- -with-rtsopts=-N
7779
dependencies:
80+
- bytestring >=0.10.8.2
7881
- curl-runnings
7982
- directory >=1.3.0.2
83+
- aeson >=1.2.4.0
8084
- hspec >= 2.4.4
8185
- hspec-expectations >=0.8.2
86+
- raw-strings-qq >= 1.1
8287
- text >=1.2.2.2

src/Testing/CurlRunnings.hs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,16 @@ noVerifyTlsSettings =
7171
}
7272

7373
-- | Fetch existing query parameters from the request and append those specfied in the queryParameters field.
74-
appendQueryParameters :: [QueryParameter] -> Request -> Request
74+
appendQueryParameters :: [KeyValuePair] -> Request -> Request
7575
appendQueryParameters newParams r = setQueryString (existing ++ newQuery) r where
7676
existing = NT.parseQuery $ queryString r
77-
newQuery = NT.simpleQueryToQuery $ fmap (\(QueryParameter k v) -> (T.encodeUtf8 k, T.encodeUtf8 v)) newParams
77+
newQuery = NT.simpleQueryToQuery $ fmap (\(KeyValuePair k v) -> (T.encodeUtf8 k, T.encodeUtf8 v)) newParams
78+
79+
setPayload :: Maybe Payload -> Request -> Request
80+
setPayload Nothing = id
81+
setPayload (Just (JSON v)) = setRequestBodyJSON v
82+
setPayload (Just (URLEncoded (KeyValuePairs xs))) = setRequestBodyURLEncoded $ kvpairs xs where
83+
kvpairs = fmap (\(KeyValuePair k v) -> (T.encodeUtf8 k, T.encodeUtf8 v))
7884

7985
-- | Run a single test case, and returns the result. IO is needed here since this method is responsible
8086
-- for actually curling the test case endpoint and parsing the result.
@@ -83,34 +89,33 @@ runCase state@(CurlRunningsState _ _ _ tlsCheckType) curlCase = do
8389
let eInterpolatedUrl = interpolateQueryString state $ url curlCase
8490
eInterpolatedHeaders =
8591
interpolateHeaders state $ fromMaybe (HeaderSet []) (headers curlCase)
86-
eInterpolatedQueryParams = interpolateViaJSON state $ fromMaybe (QueryParameters []) (queryParameters curlCase)
92+
eInterpolatedQueryParams = interpolateViaJSON state $ fromMaybe (KeyValuePairs []) (queryParameters curlCase)
8793
case (eInterpolatedUrl, eInterpolatedHeaders, eInterpolatedQueryParams) of
8894
(Left err, _, _) ->
8995
return $ CaseFail curlCase Nothing Nothing [QueryFailure curlCase err] 0
9096
(_, Left err, _) ->
9197
return $ CaseFail curlCase Nothing Nothing [QueryFailure curlCase err] 0
9298
(_, _, Left err) ->
9399
return $ CaseFail curlCase Nothing Nothing [QueryFailure curlCase err] 0
94-
(Right interpolatedUrl, Right interpolatedHeaders, Right (QueryParameters interpolatedQueryParams)) ->
95-
case sequence $ runReplacements state <$> requestData curlCase of
100+
(Right interpolatedUrl, Right interpolatedHeaders, Right (KeyValuePairs interpolatedQueryParams)) ->
101+
case sequence $ interpolateViaJSON state <$> requestData curlCase of
96102
Left l ->
97103
return $ CaseFail curlCase Nothing Nothing [QueryFailure curlCase l] 0
98-
Right replacedJSON -> do
104+
Right interpolatedData -> do
99105
initReq <- parseRequest $ T.unpack interpolatedUrl
100106
manager <- newManager noVerifyTlsManagerSettings
101107

102108
let !request =
103-
setRequestBodyJSON (fromMaybe emptyObject replacedJSON) .
104-
setRequestHeaders (toHTTPHeaders interpolatedHeaders) .
105-
appendQueryParameters interpolatedQueryParams .
106-
(if tlsCheckType == DoTLSCheck then id else (setRequestManager manager)) $
107-
initReq {method = B8S.pack . show $ requestMethod curlCase}
108-
109+
setPayload interpolatedData .
110+
setRequestHeaders (toHTTPHeaders interpolatedHeaders) .
111+
appendQueryParameters interpolatedQueryParams .
112+
(if tlsCheckType == DoTLSCheck then id else (setRequestManager manager)) $
113+
initReq {method = B8S.pack . show $ requestMethod curlCase}
109114
logger state DEBUG (pShow request)
110115
logger
111116
state
112117
DEBUG
113-
("Request body: " <> (pShow $ fromMaybe emptyObject replacedJSON))
118+
("Request body: " <> (pShow $ fromMaybe (JSON emptyObject) interpolatedData))
114119
start <- nowMillis
115120
response <- httpBS request
116121
stop <- nowMillis
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
-- | A module defining the KeyValuePairs type. This type may be used to
4+
-- represent a structure in a specification that is a collection of
5+
-- key-vaue pairs. For example query parameters and URLEncoded request
6+
-- bodies.
7+
--
8+
-- The module provides FromJSON and ToJSON instances for KeyValuePairs.
9+
-- Valid KeyValuePairs JSON is a single JSON object where all values
10+
-- are either String, Scienfific or Bool.
11+
12+
module Testing.CurlRunnings.Internal.KeyValuePairs
13+
( KeyValuePairs (..)
14+
, KeyValuePair (..)
15+
) where
16+
17+
import Data.Aeson
18+
import Data.Aeson.Types
19+
import qualified Data.ByteString.Lazy as LBS
20+
import Data.HashMap.Strict as H
21+
import qualified Data.Text as T
22+
import Data.Text.Encoding as T
23+
24+
-- | A container for a list of key-value pairs
25+
newtype KeyValuePairs = KeyValuePairs [KeyValuePair] deriving Show
26+
27+
-- | A representation of a single key-value pair
28+
data KeyValuePair = KeyValuePair T.Text T.Text deriving Show
29+
30+
instance ToJSON KeyValuePairs where
31+
toJSON (KeyValuePairs qs) =
32+
object (fmap (\(KeyValuePair k v) -> k .= toJSON v) qs)
33+
34+
instance FromJSON KeyValuePairs where
35+
parseJSON = withObject "keyValuePairs" parseKeyValuePairs where
36+
parseKeyValuePairs o = KeyValuePairs <$> traverse parseKeyValuePair (H.toList o)
37+
parseKeyValuePair (t, v) = KeyValuePair t <$> parseSingleValueType v
38+
39+
parseSingleValueType :: Value -> Parser T.Text
40+
parseSingleValueType (Bool b) = parseToText b
41+
parseSingleValueType (String t) = return t
42+
parseSingleValueType (Number n) = parseToText n
43+
parseSingleValueType invalid = typeMismatch "KeyValuePairs" invalid
44+
45+
parseToText :: (ToJSON a) => a -> Parser T.Text
46+
parseToText = return . T.decodeUtf8 . LBS.toStrict . encode

0 commit comments

Comments
 (0)