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 +15/−61
- app/Main.hs +0/−20
- curl-runnings.cabal +21/−14
- examples/example-spec.json +46/−0
- examples/example-spec.yaml +46/−0
- examples/test.json +75/−0
- src/Testing/CurlRunnings.hs +19/−3
- src/Testing/CurlRunnings/Types.hs +39/−23
- test/Spec.hs +25/−1
README.md view
@@ -2,10 +2,14 @@ [](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++