curl-runnings 0.2.0 → 0.3.0
raw patch · 6 files changed
+227/−51 lines, 6 filesdep +case-insensitivedep +http-typesdep +vectorPVP ok
version bump matches the API change (PVP)
Dependencies added: case-insensitive, http-types, vector
API changes (from Hackage documentation)
+ Testing.CurlRunnings.Types: Header :: Text -> Text -> Header
+ Testing.CurlRunnings.Types: HeaderFailure :: CurlCase -> HeaderMatcher -> Headers -> AssertionFailure
+ Testing.CurlRunnings.Types: HeaderMatcher :: [PartialHeaderMatcher] -> HeaderMatcher
+ Testing.CurlRunnings.Types: HeaderSet :: [Header] -> Headers
+ Testing.CurlRunnings.Types: PartialHeaderMatcher :: (Maybe Text) -> (Maybe Text) -> PartialHeaderMatcher
+ Testing.CurlRunnings.Types: [expectHeaders] :: CurlCase -> Maybe HeaderMatcher
+ Testing.CurlRunnings.Types: [headers] :: CurlCase -> Maybe Headers
+ Testing.CurlRunnings.Types: data Header
+ Testing.CurlRunnings.Types: data HeaderMatcher
+ Testing.CurlRunnings.Types: data Headers
+ Testing.CurlRunnings.Types: data PartialHeaderMatcher
+ Testing.CurlRunnings.Types: instance Data.Aeson.Types.FromJSON.FromJSON Testing.CurlRunnings.Types.HeaderMatcher
+ Testing.CurlRunnings.Types: instance Data.Aeson.Types.FromJSON.FromJSON Testing.CurlRunnings.Types.Headers
+ Testing.CurlRunnings.Types: instance Data.Aeson.Types.ToJSON.ToJSON Testing.CurlRunnings.Types.Header
+ Testing.CurlRunnings.Types: instance Data.Aeson.Types.ToJSON.ToJSON Testing.CurlRunnings.Types.HeaderMatcher
+ Testing.CurlRunnings.Types: instance Data.Aeson.Types.ToJSON.ToJSON Testing.CurlRunnings.Types.Headers
+ Testing.CurlRunnings.Types: instance Data.Aeson.Types.ToJSON.ToJSON Testing.CurlRunnings.Types.PartialHeaderMatcher
+ Testing.CurlRunnings.Types: instance GHC.Generics.Generic Testing.CurlRunnings.Types.Header
+ Testing.CurlRunnings.Types: instance GHC.Generics.Generic Testing.CurlRunnings.Types.HeaderMatcher
+ Testing.CurlRunnings.Types: instance GHC.Generics.Generic Testing.CurlRunnings.Types.Headers
+ Testing.CurlRunnings.Types: instance GHC.Generics.Generic Testing.CurlRunnings.Types.PartialHeaderMatcher
+ Testing.CurlRunnings.Types: instance GHC.Show.Show Testing.CurlRunnings.Types.Header
+ Testing.CurlRunnings.Types: instance GHC.Show.Show Testing.CurlRunnings.Types.HeaderMatcher
+ Testing.CurlRunnings.Types: instance GHC.Show.Show Testing.CurlRunnings.Types.Headers
+ Testing.CurlRunnings.Types: instance GHC.Show.Show Testing.CurlRunnings.Types.PartialHeaderMatcher
- Testing.CurlRunnings.Types: CurlCase :: String -> String -> HttpMethod -> Maybe Value -> Maybe JsonMatcher -> StatusCodeMatcher -> CurlCase
+ Testing.CurlRunnings.Types: CurlCase :: String -> String -> HttpMethod -> Maybe Value -> Maybe Headers -> Maybe JsonMatcher -> StatusCodeMatcher -> Maybe HeaderMatcher -> CurlCase
Files
- curl-runnings.cabal +5/−2
- examples/example-spec.json +28/−14
- examples/example-spec.yaml +31/−14
- src/Testing/CurlRunnings.hs +59/−10
- src/Testing/CurlRunnings/Types.hs +103/−7
- test/Spec.hs +1/−4
curl-runnings.cabal view
@@ -2,10 +2,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 5ba21bafee5c2e642cec1c0c7bff81b86f715d883cc524542a61d787f7aca778+-- hash: 9530ddd7493ff79858e3c01c7064eb8119932f625cdb1fa3f5cb93e3b3786189 name: curl-runnings-version: 0.2.0+version: 0.3.0 synopsis: A framework for declaratively writing curl based API tests description: Please see the README on Github at <https://github.com/aviaviavi/curl-runnings#readme> category: Testing@@ -37,12 +37,15 @@ , aeson-pretty >=0.8.5 , base >=4.7 && <5 , bytestring >=0.10.8.2+ , case-insensitive >=0.2.1 , directory >=1.3.0.2 , hspec >=2.4.4 , hspec-expectations >=0.8.2 , http-conduit >=2.2.4+ , http-types >=0.9.1 , text >=1.2.2.2 , unordered-containers >=0.2.8.0+ , vector >=0.12.0 , yaml >=0.8.28 exposed-modules: Testing.CurlRunnings
examples/example-spec.json view
@@ -28,19 +28,33 @@ "name": "test 3", "url": "http://your-url.com/other/path", "requestMethod": "GET",- "expectData": {- "contains": [- {- "keyValueMatch": {- "key": "okay",- "value": true- }- },- {- "valueMatch": true- }- ]- },- "expectStatus": 200+ "headers": "Content-Type: application/json",+ "expectStatus": 200,+ "expectHeaders": "Content-Type: application/json; Hello: world"+ },+ {+ "name": "test 4",+ "url": "http://your-url.com/other/path",+ "requestMethod": "GET",+ "headers": "Content-Type: application/json",+ "expectStatus": 200,+ "expectHeaders": [+ {+ "key": "Key-With-Val-We-Dont-Care-About"+ }+ ]+ },+ {+ "name": "test 5",+ "url": "http://your-url.com/other/path",+ "requestMethod": "GET",+ "headers": "Content-Type: application/json",+ "expectStatus": 200,+ "expectHeaders": [+ "Hello: world",+ {+ "value": "Value-With-Key-We-Dont-Care-About"+ }+ ] } ]
examples/example-spec.yaml view
@@ -1,5 +1,6 @@ --- # The top level of the file is an array of test cases+ - name: test 1 # required url: http://your-endpoint.com/status # required requestMethod: GET # required@@ -16,6 +17,7 @@ # [Required] Assertions about the returned status code. Pass in # an acceptable code or list of codes expectStatus: 200+ - name: test 2 url: http://your-endpoint.com/path requestMethod: POST@@ -26,21 +28,36 @@ requestData: hello: there num: 1+ - name: test 3 url: http://your-url.com/other/path requestMethod: GET- expectData:- # In the `contains` case of data validation, a list of matchers is specified. Currently,- # possible types are `valueMatch` | `keyValueMatch`.- contains:- # `keyValueMatch` looks for the key/value pair anywhere in the payload- # here, {'okay': true} must be somewhere in the return payload- - keyValueMatch:- key: okay- value: true- # `valueMatch` searches for a value anywhere in the payload (note: _not_ a key).- # Here, we look for the value `true` anywhere in the payload.- # This can be useful for matching against values where you don't know the key ahead of time,- # or for values in a top level array.- - valueMatch: true+ # Specify the headers you want to sent, just like the -H flag in a curl command+ # IE "key: value; key: value; ..."+ headers: "Content-Type: application/json" expectStatus: 200+ # The response must constain at least these headers exactly.+ # Header strings again match the -H syntax from curl+ expectHeaders: "Content-Type: application/json; Hello: world"++- name: test 4+ url: http://your-url.com/other/path+ requestMethod: GET+ headers: "Content-Type: application/json"+ expectStatus: 200+ # You can also specify a key and/or value to look for in the headers+ expectHeaders: + -+ key: "Key-With-Val-We-Dont-Care-About"++- name: test 5+ url: http://your-url.com/other/path+ requestMethod: GET+ headers: "Content-Type: application/json"+ expectStatus: 200+ # Specify a mix of full or partial header matches in a list like so:+ expectHeaders: + - "Hello: world"+ -+ value: "Value-With-Key-We-Dont-Care-About"+
src/Testing/CurlRunnings.hs view
@@ -14,12 +14,14 @@ import Data.Aeson import qualified Data.ByteString.Char8 as B8S import qualified Data.ByteString.Lazy as B+import qualified Data.CaseInsensitive as CI import qualified Data.HashMap.Strict as H import Data.Maybe-import qualified Data.Text as T+import qualified Data.Text as T import qualified Data.Yaml as Y import Network.HTTP.Conduit import Network.HTTP.Simple+import qualified Network.HTTP.Types.Header as HTTP import Testing.CurlRunnings.Types import Text.Printf @@ -40,20 +42,50 @@ runCase :: CurlCase -> IO CaseResult runCase curlCase = do initReq <- parseRequest $ url curlCase- response <- httpBS . setRequestBodyJSON (requestData curlCase) $ initReq {method = B8S.pack . show $ requestMethod curlCase}+ response <-+ httpBS .+ (setRequestHeaders+ (toHTTPHeaders $ fromMaybe (HeaderSet []) (headers curlCase))) .+ setRequestBodyJSON (requestData curlCase) $+ initReq {method = B8S.pack . show $ requestMethod curlCase} returnVal <- (return . decode . B.fromStrict $ getResponseBody response) :: IO (Maybe Value) let returnCode = getResponseStatusCode response+ receivedHeaders = responseHeaders response assertionErrors = map fromJust $ filter isJust- [checkBody curlCase returnVal, checkCode curlCase returnCode]+ [ checkBody curlCase returnVal+ , checkCode curlCase returnCode+ , checkHeaders curlCase receivedHeaders+ ] return $ case assertionErrors of [] -> CasePass curlCase failures -> CaseFail curlCase failures +checkHeaders :: CurlCase -> [HTTP.Header] -> Maybe AssertionFailure+checkHeaders (CurlCase _ _ _ _ _ _ _ Nothing) _ = Nothing+checkHeaders curlCase@(CurlCase _ _ _ _ _ _ _ (Just matcher@(HeaderMatcher m))) l =+ let receivedHeaders = fromHTTPHeaders l+ notFound = filter (not . headerIn receivedHeaders) m+ in+ if null $ notFound then+ Nothing+ else+ Just $ HeaderFailure curlCase matcher receivedHeaders++-- | Does this header contain our matcher?+headerMatches :: PartialHeaderMatcher -> Header -> Bool+headerMatches (PartialHeaderMatcher mk mv) (Header k v) =+ (maybe True (== k) mk) && (maybe True (== v) mv)++-- | Does any of these headers contain our matcher?+headerIn :: Headers -> PartialHeaderMatcher -> Bool+headerIn (HeaderSet received) headerMatcher =+ any (headerMatches headerMatcher) received+ safeLast :: [a] -> Maybe a safeLast [] = Nothing safeLast x = Just $ last x@@ -80,18 +112,18 @@ -- | Check if the retrieved value fail's the case's assertion checkBody :: CurlCase -> Maybe Value -> Maybe AssertionFailure -- | We are looking for an exact payload match, and we have a payload to check-checkBody curlCase@(CurlCase _ _ _ _ (Just matcher@(Exactly expectedValue)) _) (Just receivedBody)+checkBody curlCase@(CurlCase _ _ _ _ _ (Just matcher@(Exactly expectedValue)) _ _) (Just receivedBody) | expectedValue /= receivedBody = Just $ DataFailure curlCase matcher (Just receivedBody) | otherwise = Nothing -- | We are checking a list of expected subvalues, and we have a payload to check-checkBody curlCase@(CurlCase _ _ _ _ (Just matcher@(Contains expectedSubvalues)) _) (Just receivedBody)+checkBody curlCase@(CurlCase _ _ _ _ _ (Just matcher@(Contains expectedSubvalues)) _ _) (Just receivedBody) | jsonContainsAll receivedBody expectedSubvalues = Nothing | otherwise = Just $ DataFailure curlCase matcher (Just receivedBody) -- | We expected a body but didn't get one-checkBody curlCase@(CurlCase _ _ _ _ (Just anything) _) Nothing = Just $ DataFailure curlCase anything Nothing+checkBody curlCase@(CurlCase _ _ _ _ _ (Just anything) _ _) Nothing = Just $ DataFailure curlCase anything Nothing -- | No assertions on the body-checkBody (CurlCase _ _ _ _ Nothing _) _ = Nothing+checkBody (CurlCase _ _ _ _ _ Nothing _ _) _ = Nothing -- | Does the json value contain all of these sub-values? jsonContainsAll :: Value -> [JsonSubExpr] -> Bool@@ -104,7 +136,7 @@ -- | Does the json value contain the given key value pair? containsKeyVal :: Value -> T.Text -> Value -> Bool containsKeyVal jsonValue key val = case jsonValue of- Object o -> isJust $ H.lookup key o+ Object o -> H.lookup key o == Just val _ -> False -- | Fully traverse the json and return a list of all the values@@ -121,9 +153,26 @@ -- | Verify the returned http status code is ok, construct the right failure -- type if needed checkCode :: CurlCase -> Int -> Maybe AssertionFailure-checkCode curlCase@(CurlCase _ _ _ _ _ (ExactCode expectedCode)) receivedCode+checkCode curlCase@(CurlCase _ _ _ _ _ _ (ExactCode expectedCode) _) receivedCode | expectedCode /= receivedCode = Just $ StatusFailure curlCase receivedCode | otherwise = Nothing-checkCode curlCase@(CurlCase _ _ _ _ _ (AnyCodeIn l)) receivedCode+checkCode curlCase@(CurlCase _ _ _ _ _ _ (AnyCodeIn l) _) receivedCode | receivedCode `notElem` l = Just $ StatusFailure curlCase receivedCode | otherwise = Nothing++-- | Utility conversion from HTTP headers to CurlRunnings headers.+fromHTTPHeaders :: HTTP.ResponseHeaders -> Headers+fromHTTPHeaders rh = HeaderSet $ map fromHTTPHeader rh++-- | Utility conversion from an HTTP header to a CurlRunnings header.+fromHTTPHeader :: HTTP.Header -> Header+fromHTTPHeader (a, b) =+ Header (T.pack . B8S.unpack $ CI.original a) (T.pack $ B8S.unpack b)++-- | Utility conversion from an HTTP header to a CurlRunnings header.+toHTTPHeader :: Header -> HTTP.Header+toHTTPHeader (Header a b) = (CI.mk . B8S.pack $ T.unpack a, B8S.pack $ T.unpack b)++-- | Utility conversion from CurlRunnings headers to HTTP headers.+toHTTPHeaders :: Headers -> HTTP.RequestHeaders+toHTTPHeaders (HeaderSet h) = map toHTTPHeader h
src/Testing/CurlRunnings/Types.hs view
@@ -1,27 +1,37 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-} -- | Data types for curl-runnings tests+ module Testing.CurlRunnings.Types- ( CurlSuite(..)+ ( AssertionFailure(..)+ , CaseResult(..)+ , CurlSuite(..) , CurlCase(..)+ , Header(..)+ , HeaderMatcher(..)+ , Headers(..) , HttpMethod(..) , JsonMatcher(..) , JsonSubExpr(..)+ , PartialHeaderMatcher(..) , StatusCodeMatcher(..)- , AssertionFailure(..)- , CaseResult(..)- , isPassing+ , isFailing+ , isPassing+ ) where import Data.Aeson import Data.Aeson.Encode.Pretty import Data.Aeson.Types import qualified Data.ByteString.Lazy.Char8 as B8+import Data.Either import qualified Data.HashMap.Strict as H+import Data.List import Data.Maybe import qualified Data.Text as T+import qualified Data.Vector as V import GHC.Generics import Testing.CurlRunnings.Internal import Text.Printf@@ -55,6 +65,80 @@ | isJust $ H.lookup "contains" v = Contains <$> v .: "contains" parseJSON invalid = typeMismatch "JsonMatcher" invalid +-- | A representation of a single header+data Header =+ Header T.Text+ T.Text+ deriving (Show, Generic)++instance ToJSON Header++-- | Simple container for a list of headers, useful for a vehicle for defining a+-- fromJSON+data Headers =+ HeaderSet [Header]+ deriving (Show, Generic)++instance ToJSON Headers++-- | Specify a key, value, or both to match against in the returned headers of a+-- response.+data PartialHeaderMatcher =+ PartialHeaderMatcher (Maybe T.Text)+ (Maybe T.Text)+ deriving (Show, Generic)+instance ToJSON PartialHeaderMatcher++-- | Collection of matchers to run against a single curl response+data HeaderMatcher =+ HeaderMatcher [PartialHeaderMatcher]+ deriving (Show, Generic)++instance ToJSON HeaderMatcher++parseHeader :: T.Text -> Either T.Text Header+parseHeader str =+ case map T.strip $ T.splitOn ":" str of+ [key, val] -> Right $ Header key val+ anythingElse -> Left . T.pack $ "bad header found: " ++ (show anythingElse)++parseHeaders :: T.Text -> Either T.Text Headers+parseHeaders str =+ let _headers = filter (/= "") $ T.splitOn ";" str+ parses = map parseHeader _headers+ in case find isLeft parses of+ Just (Left failure) -> Left failure+ _ ->+ Right . HeaderSet $+ map+ (fromRight $+ error+ "Internal error parsing headers, this is a bug in curl runnings :(")+ parses++instance FromJSON Headers where+ parseJSON a@(String v) =+ case parseHeaders v of+ Right h -> return h+ Left failure -> typeMismatch ("Header failure: " ++ T.unpack failure) a+ parseJSON invalid = typeMismatch "Header" invalid++instance FromJSON HeaderMatcher where+ parseJSON o@(String v) =+ either+ (\s -> typeMismatch ("HeaderMatcher: " ++ T.unpack s) o)+ (\(HeaderSet parsed) ->+ return . HeaderMatcher $+ map+ (\(Header key val) -> PartialHeaderMatcher (Just key) (Just val))+ parsed)+ (parseHeaders v)+ parseJSON (Object v) = do+ partial <- PartialHeaderMatcher <$> v .:? "key" <*> v .:? "value"+ return $ HeaderMatcher [partial]+ parseJSON (Array v) = mconcat . V.toList $ V.map parseJSON v+ parseJSON invalid = typeMismatch "HeaderMatcher" invalid+ -- | A matcher for a subvalue of a json payload data JsonSubExpr -- | Assert some value anywhere in the json has a value equal to a given@@ -94,10 +178,12 @@ data CurlCase = CurlCase { name :: String -- ^ The name of the test case , url :: String -- ^ The target url to test- , requestMethod :: HttpMethod -- ^ Berb to use for the request+ , requestMethod :: HttpMethod -- ^ Verb to use for the request , requestData :: Maybe Value -- ^ Payload to send with the request, if any+ , headers :: Maybe Headers -- ^ Headers to send with the request, if any , expectData :: Maybe JsonMatcher -- ^ The assertions to make on the response payload, if any , expectStatus :: StatusCodeMatcher -- ^ Assertion about the status code returned by the target+ , expectHeaders :: Maybe HeaderMatcher -- ^ Assertions to make about the response headers, if any } deriving (Show, Generic) instance FromJSON CurlCase@@ -117,6 +203,10 @@ -- | The status code we got back was wrong | StatusFailure CurlCase Int+ -- | The headers we got back were wrong+ | HeaderFailure CurlCase+ HeaderMatcher+ Headers -- | Something else | UnexpectedFailure @@ -149,6 +239,12 @@ (url curlCase) (B8.unpack (encodePretty expectedVals)) (B8.unpack (encodePretty receivedVal))+ show (HeaderFailure curlCase expected receivedHeaders) =+ printf+ "Headers from %s didn't contain expected headers. Expected headers: %s. Received headers: %s"+ (url curlCase)+ (show expected)+ (show receivedHeaders) show UnexpectedFailure = "Unexpected Error D:" -- | A type representing the result of a single curl, and all associated
test/Spec.hs view
@@ -1,12 +1,9 @@ module Main where +import Data.Either import System.Directory import Test.Hspec import Testing.CurlRunnings--isRight :: Either a b -> Bool-isRight (Right _) = True-isRight _ = False main :: IO () main = hspec $