packages feed

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 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 $