packages feed

curl-runnings 0.1.0 → 0.2.0

raw patch · 9 files changed

+286/−122 lines, 9 filesdep +directorydep +hspecdep +hspec-expectationsdep ~aesondep ~aeson-prettydep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: directory, hspec, hspec-expectations

Dependency ranges changed: aeson, aeson-pretty, base, bytestring, cmdargs, http-conduit, text, unordered-containers, yaml

API changes (from Hackage documentation)

+ Testing.CurlRunnings: decodeFile :: FilePath -> IO (Either String CurlSuite)

Files

README.md view
@@ -2,10 +2,14 @@  [![Build Status](https://travis-ci.org/aviaviavi/curl-runnings.svg?branch=master)](https://travis-ci.org/aviaviavi/curl-runnings) -Feel the rhythm! Feel the rhyme! Get on up, it's testing time! curl-runnings!+_Feel the rhythm! Feel the rhyme! Get on up, it's testing time! curl-runnings!_  curl-runnings is a framework for writing declarative, curl based tests for your APIs.  +Write your tests quickly and correctly with a straight-forward specification in+yaml or json. A DSL for writing your tests is on the way! Alternatively, you can+use the curl-runnings library to write your tests directly in Haskell.+ ### Why?  When writing curl based smoke/integration tests for APIs using bash and `curl`@@ -27,68 +31,16 @@  There are few options to install: -- clone this repo and run `stack install`-- download the releases -- (soon) - `cabal install curl-runnings`+- download the releases from the github [releases page](https://github.com/aviaviavi/curl-runnings/releases)+- `stack install curl-runnings`+- `cabal install curl-runnings`+- build from source with `stack`  ### Writing a test specification -A test spec is a top level list of test cases, each item represents a single curl and set of assertions about the response:--```yaml-----# the top level of the file is an array of test cases-- name: test 1 # required-  url: http://your-endpoint.com # required-  requestMethod: GET # required-  # specify the json payload we expect here, if any-  expectData: # optional-    # `tag` refers to the type of matcher we want to use-    # valid tags are `Exactly` | `Contains`-    tag: Exactly-    # check for exactly this payload-    contents:-      okay: true-      msg: ''-  expectStatus: # requried-    # `tag` refers to the type of matcher we want to use-    # valid tags are `ExactCode` (contents :: number) | `AnyCodeIn` (contents :: [number])-    tag: ExactCode-    contents: 200-- name: test 2-  url: http://your-endpoint.com/path-  requestMethod: POST-  expectStatus:-    # Any code listed in `contents` is valid-    tag: AnyCodeIn-    contents:-    - 200-    - 201-  # json data to send with the request-  requestData:-    hello: there-    num: 1-- name: test 3-  url: http://your-url.com/other/path-  requestMethod: GET-  expectData:-    # apply a list of matchers to the returned json payload-    tag: Contains-    contents:-    # valid tags are `ValueMatch` | `KeyValueMatch`-    # find 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.-    - tag: ValueMatch-      contents: true-    # `KeyValueMatch` looks for the key/value pair anywhere in the payload-    # here, {'okay': true} must be somewhere in the return payload-    - tag: KeyValueMatch-      matchKey: okay-      matchValue: true-  expectStatus:-    tag: ExactCode-    contents: 200-```+For now, you write your tests specs in a yaml or json file. See /examples to get+started. A test spec is a top level array of test cases, each item represents a+single curl and set of assertions about the response.  ### Running @@ -107,8 +59,9 @@ $ curl-runnings --help ``` +### Roadmap -### Future work+Contributions in any form are welcome and encouraged. Don't be shy! :D  - [x] Json specifications for tests - [x] Yaml specifications for tests@@ -119,3 +72,4 @@   - [ ] timeouts   - [ ] retry logic   - [ ] ability to configure alerts+  
app/Main.hs view
@@ -3,12 +3,8 @@  module Main where -import           Data.Aeson-import qualified Data.ByteString.Char8           as B8S-import qualified Data.ByteString.Lazy            as B import qualified Data.Text                       as T import           Data.Version                    (showVersion)-import qualified Data.Yaml                       as Y import           Paths_curl_runnings             (version) import           System.Console.CmdArgs import           System.Environment@@ -30,22 +26,6 @@   summary ("curl-runnings " ++ showVersion version) &=   program "curl-runnings" &=   help "Use the --file or -f flag to specify an intput file spec to run"---- | decode a json or yaml file into a suite object-decodeFile :: FilePath -> IO (Either String CurlSuite)-decodeFile specPath =-  case last $ T.splitOn "." (T.pack specPath) of-    "json" ->-      eitherDecode' <$> B.readFile specPath :: IO (Either String CurlSuite)-    "yaml" ->-      Y.decodeEither <$> B8S.readFile specPath :: IO (Either String CurlSuite)-    "yml" ->-      Y.decodeEither <$> B8S.readFile specPath :: IO (Either String CurlSuite)-    _ ->-      return . Left $-      printf-        "Invalid spec path %s"-        (T.pack specPath)  main :: IO () main = do
curl-runnings.cabal view
@@ -2,10 +2,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: d8d3fa8a875ec9e27b6db4c1ad13dcc50d7354bbbc647ae07ff08a91b0f4c79b+-- hash: 5ba21bafee5c2e642cec1c0c7bff81b86f715d883cc524542a61d787f7aca778  name:           curl-runnings-version:        0.1.0+version:        0.2.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@@ -20,6 +20,9 @@ cabal-version:  >= 1.10  extra-source-files:+    examples/example-spec.json+    examples/example-spec.yaml+    examples/test.json     README.md  source-repository head@@ -30,13 +33,17 @@   hs-source-dirs:       src   build-depends:-      aeson >=1.2.4.0 && <1.3-    , aeson-pretty >=0.8.5 && <0.9+      aeson >=1.2.4.0+    , aeson-pretty >=0.8.5     , base >=4.7 && <5-    , bytestring >=0.10.8.2 && <0.11-    , http-conduit >=2.2.4 && <2.3-    , text >=1.2.2.2 && <1.3-    , unordered-containers >=0.2.8.0 && <0.3+    , bytestring >=0.10.8.2+    , directory >=1.3.0.2+    , hspec >=2.4.4+    , hspec-expectations >=0.8.2+    , http-conduit >=2.2.4+    , text >=1.2.2.2+    , unordered-containers >=0.2.8.0+    , yaml >=0.8.28   exposed-modules:       Testing.CurlRunnings       Testing.CurlRunnings.Types@@ -51,13 +58,10 @@       app   ghc-options: -threaded -rtsopts -with-rtsopts=-N   build-depends:-      aeson >=1.2.4.0 && <1.3-    , base >=4.7 && <5-    , bytestring >=0.10.8.2 && <0.11-    , cmdargs >=0.10.20 && <0.11+      base >=4.7+    , cmdargs >=0.10.20     , curl-runnings-    , text >=1.2.2.2 && <1.3-    , yaml >=0.8.28 && <0.9+    , text >=1.2.2.2   other-modules:       Paths_curl_runnings   default-language: Haskell2010@@ -71,6 +75,9 @@   build-depends:       base >=4.7 && <5     , curl-runnings+    , directory >=1.3.0.2+    , hspec >=2.4.4+    , hspec-expectations >=0.8.2   other-modules:       Paths_curl_runnings   default-language: Haskell2010
+ examples/example-spec.json view
@@ -0,0 +1,46 @@+[+  {+    "name": "test 1",+    "url": "http://your-endpoint.com/status",+    "requestMethod": "GET",+    "expectData": {+      "exactly": {+        "okay": true,+        "msg": "a message"+      }+    },+    "expectStatus": 200+  },+  {+    "name": "test 2",+    "url": "http://your-endpoint.com/path",+    "requestMethod": "POST",+    "expectStatus": [+      200,+      201+    ],+    "requestData": {+      "hello": "there",+      "num": 1+    }+  },+  {+    "name": "test 3",+    "url": "http://your-url.com/other/path",+    "requestMethod": "GET",+    "expectData": {+      "contains": [+        {+          "keyValueMatch": {+            "key": "okay",+            "value": true+          }+        },+        {+          "valueMatch": true+        }+      ]+    },+    "expectStatus": 200+  }+]
+ examples/example-spec.yaml view
@@ -0,0 +1,46 @@+---+# 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+  # [Optional] Specify the json payload we expect here+  # The 1 key in this block should be either:+  # exactly | contains+  expectData:+    # The 1 key in this object specifies the matcher we want+    # to use to test the returned payload. In this case, we+    # require the payload is exactly what we specify.+    exactly:+      okay: true+      msg: 'a message'+  # [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+  expectStatus:+  - 200+  - 201+  # [Optional] json data to send with the request+  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+  expectStatus: 200
+ examples/test.json view
@@ -0,0 +1,75 @@+[+  {+    "name": "test 1",+    "url": "http://sd.ddpay.io/status",+    "requestMethod": "GET",+    "expectData": {+        "tag": "Exactly",+        "contents": {+            "okay": true,+            "msg": ""+        }+    },+    "expectStatus": {+        "tag": "ExactCode",+        "contents": 200+    },+    "requestData": null+  },++  {+    "name": "test 2",+    "url": "http://sd.ddpay.io/status",+    "requestMethod": "GET",+    "expectData": {+        "tag": "Contains",+        "contents": [+          {+            "tag": "ValueMatch",+            "contents": true+          },+          {+            "tag": "ValueMatch",+            "contents": ""+          },+          {+            "tag": "KeyValueMatch",+            "matchKey": "msg",+            "matchValue": ""+          },+          {+            "tag": "KeyValueMatch",+            "matchKey": "okay",+            "matchValue": true+          }+                      ]+    },+    "expectStatus": {+      "tag": "AnyCodeIn",+      "contents": [200, 300]+    },+    "requestData": null+  },++  {+    "name": "test 4",+    "url": "http://sd.ddpay.io/v1/DevOnlyGetAllCustomerIds",+    "requestMethod": "POST",+    "expectStatus": {+      "tag": "ExactCode",+      "contents": 400+    },+    "requestData": {}+  },+  {+    "name": "test 5",+    "url": "http://sd.ddpay.io/v1/DevOnlyGetAllCustomerIds",+    "requestMethod": "POST",+    "expectData": null,+    "expectStatus": {+      "tag": "AnyCodeIn",+      "contents": [401]+    },+    "requestData": {"api_token": "asdf"}+  }+]
src/Testing/CurlRunnings.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}+ -- | curl-runnings is a framework for writing declaratively writing curl based tests for your API's. -- Write your test specifications with yaml or json, and you're done! --@@ -5,6 +7,7 @@     (       runCase     , runSuite+    , decodeFile     ) where  import           Control.Monad@@ -13,11 +16,25 @@ import qualified Data.ByteString.Lazy       as B 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           Testing.CurlRunnings.Types+import           Text.Printf +-- | decode a json or yaml file into a suite object+decodeFile :: FilePath -> IO (Either String CurlSuite)+decodeFile specPath =+  case last $ T.splitOn "." (T.pack specPath) of+    "json" ->+      eitherDecode' <$> B.readFile specPath :: IO (Either String CurlSuite)+    "yaml" ->+      Y.decodeEither <$> B8S.readFile specPath :: IO (Either String CurlSuite)+    "yml" ->+      Y.decodeEither <$> B8S.readFile specPath :: IO (Either String CurlSuite)+    _ -> return . Left $ printf "Invalid spec path %s" (T.pack specPath)+ -- | Run a single test case, and returns the result. IO is needed here since this method is responsible -- for actually curling the test case endpoint and parsing the result. runCase :: CurlCase -> IO CaseResult@@ -79,11 +96,10 @@ -- | Does the json value contain all of these sub-values? jsonContainsAll :: Value -> [JsonSubExpr] -> Bool jsonContainsAll jsonValue =-  all (\match -> case match  of+  all $ \match -> case match  of           ValueMatch subval -> subval `elem` traverseValue jsonValue           KeyValueMatch key subval ->             containsKeyVal jsonValue key subval-      )  -- | Does the json value contain the given key value pair? containsKeyVal :: Value -> T.Text -> Value -> Bool
src/Testing/CurlRunnings/Types.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveGeneric     #-} {-# LANGUAGE OverloadedStrings #-}  -- | Data types for curl-runnings tests@@ -15,13 +15,16 @@   , isFailing   ) where -import Data.Aeson-import Data.Aeson.Encode.Pretty-import qualified Data.ByteString.Lazy.Char8 as B8-import qualified Data.Text as T-import GHC.Generics-import Text.Printf-import Testing.CurlRunnings.Internal+import           Data.Aeson+import           Data.Aeson.Encode.Pretty+import           Data.Aeson.Types+import qualified Data.ByteString.Lazy.Char8    as B8+import qualified Data.HashMap.Strict           as H+import           Data.Maybe+import qualified Data.Text                     as T+import           GHC.Generics+import           Testing.CurlRunnings.Internal+import           Text.Printf  -- | A basic enum for supported HTTP verbs data HttpMethod@@ -42,13 +45,16 @@   = Exactly Value   -- | A list of matchers to make assertions about some subset of the response.   | Contains [JsonSubExpr]-   deriving (Show, Generic) -instance FromJSON JsonMatcher- instance ToJSON JsonMatcher +instance FromJSON JsonMatcher where+  parseJSON (Object v)+    | isJust $ H.lookup "exactly" v = Exactly <$> v .: "exactly"+    | isJust $ H.lookup "contains" v = Contains <$> v .: "contains"+  parseJSON invalid = typeMismatch "JsonMatcher" 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@@ -56,12 +62,19 @@   --  top level array. It's also useful if you don't know the key ahead of time.   = ValueMatch Value   -- | Assert the key value pair can be found somewhere the json.-  | KeyValueMatch { matchKey :: T.Text+  | KeyValueMatch { matchKey   :: T.Text                   , matchValue :: Value }   deriving (Show, Generic) -instance FromJSON JsonSubExpr-+instance FromJSON JsonSubExpr where+  parseJSON (Object v)+    | isJust $ H.lookup "keyValueMatch" v =+      let toParse = fromJust $ H.lookup "keyValueMatch" v+      in case toParse of+           Object o -> KeyValueMatch <$> o .: "key" <*> o .: "value"+           _        -> typeMismatch "JsonSubExpr" toParse+    | isJust $ H.lookup "valueMatch" v = ValueMatch <$> v .: "valueMatch"+  parseJSON invalid = typeMismatch "JsonSubExpr" invalid instance ToJSON JsonSubExpr  -- | Check the status code of a response. You can specify one or many valid codes.@@ -70,18 +83,21 @@   | AnyCodeIn [Int]   deriving (Show, Generic) -instance FromJSON StatusCodeMatcher- instance ToJSON StatusCodeMatcher +instance FromJSON StatusCodeMatcher where+  parseJSON obj@(Number _) = ExactCode <$> parseJSON obj+  parseJSON obj@(Array _)  = AnyCodeIn <$> parseJSON obj+  parseJSON invalid        = typeMismatch "StatusCodeMatcher" invalid+ -- | A single curl test case, the basic foundation of a curl-runnings test. data CurlCase = CurlCase-  { name :: String -- ^ The name of the test case-  , url :: String -- ^ The target url to test+  { name          :: String -- ^ The name of the test case+  , url           :: String -- ^ The target url to test   , requestMethod :: HttpMethod -- ^ Berb to use for the request-  , requestData :: Maybe Value -- ^ Payload 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+  , requestData   :: Maybe Value -- ^ Payload 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   } deriving (Show, Generic)  instance FromJSON CurlCase@@ -162,10 +178,10 @@  -- | Simple predicate that checks if the result is passing isPassing :: CaseResult -> Bool-isPassing (CasePass _) = True+isPassing (CasePass _)   = True isPassing (CaseFail _ _) = False  -- | Simple predicate that checks if the result is failing isFailing :: CaseResult -> Bool-isFailing (CasePass _) = False+isFailing (CasePass _)   = False isFailing (CaseFail _ _) = True
test/Spec.hs view
@@ -1,2 +1,26 @@+module Main where++import           System.Directory+import           Test.Hspec+import           Testing.CurlRunnings++isRight :: Either a b -> Bool+isRight (Right _) = True+isRight _         = False+ main :: IO ()-main = putStrLn "Test suite not yet implemented"+main = hspec $+  describe "Curl Runnings" $ do+  it "should provide valid example yaml specs" $+    testValidSpec "/examples/example-spec.yaml"++  it "should provide valid example json specs" $+    testValidSpec "/examples/example-spec.json"++testValidSpec :: String -> IO ()+testValidSpec file = do+  currentDirectory <- getCurrentDirectory+  spec <- decodeFile (currentDirectory ++ file)+  spec `shouldSatisfy` isRight++