Skip to content

Commit

Permalink
Basic Dhall support (#11)
Browse files Browse the repository at this point in the history
* basic dhall support

* add linux modifier to archive name, for consistency

* dhall in docs

* readme updates

* bump version
  • Loading branch information
aviaviavi authored Oct 3, 2020
1 parent af1911a commit 3921500
Show file tree
Hide file tree
Showing 10 changed files with 434 additions and 45 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ script: |
ln -s curl-runnings-${version}.tar.gz curl-runnings.tar.gz &&
mkdir -p $HOME/cr-release &&
cd $HOME/cr-release &&
ln -s $HOME/.local/bin/curl-runnings-${version}.tar.gz curl-runnings-${version}.tar.gz
ln -s $HOME/.local/bin/curl-runnings-${version}.tar.gz curl-runnings-${version}-linux.tar.gz
cache:
directories:
- $HOME/.stack
Expand Down
58 changes: 31 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,19 @@

_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 that can encode simple but powerful matchers
against responses.
A common form of black-box API testing boils down to simply making requests to
an endpoint and verifying properties of the response. curl-runnings aims to make
writing tests like this fast and easy.

curl-runnings is a framework for writing declarative tests for your APIs in a
fashion equivalent to performing `curl`s and verifying the responses. Write your
tests quickly and correctly with a straight-forward specification in
[Dhall](https://dhall-lang.org/), yaml, or json that can encode simple but
powerful matchers against responses.

Alternatively, you can use the curl-runnings library to write your tests in
Haskell (though a Haskell setup is absolutely not required to use this tool).

### Why?

This library came out of a pain-point my coworkers and I were running into
during development: Writing integration tests for our APIs was generally
annoying. They were time consuming to write especially considering how basic
they were, and we are a small startup where developer time is in short supply.
Over time, we found ourselves sometimes just writing bash scripts that would
`curl` our various endpoints and check the output with very basic matchers.
These tests were fast to write, but quickly became difficult to maintain as
complexity was added. Not only did maintenance become challenging, but the whole
system was very error prone and confidence in the tests overall was decreasing.
At the end of the day, we needed to just curl some endpoints and verify the
output looks sane, and do this quickly and correctly. This is precisely the goal
of curl-runnings.

Now you can write your tests just as data in a yaml or json file,
and curl-runnings will take care of the rest!

While yaml/json is the current way to write curl-runnings tests, this project is
being built in a way that should lend itself well to an embedded domain specific
language, which is a future goal for the project. curl-runnings specs in Dhall
is also being developed and may fulfill the same needs.

### Installing

Expand All @@ -55,10 +38,31 @@ Alternatively, you can compile from source with stack.

Curl runnings tests are just data! A test spec is an object containing an array
of `cases`, where each item represents a single curl and set of assertions about
the response. Write your tests specs in a yaml or json file. Note: the legacy
the response. Write your tests specs in a Dhall, yaml or json file. Note: the legacy
format of a top level array of test cases is still supported, but may not be in
future releases.

```dhall
let JSON = https://prelude.dhall-lang.org/JSON/package.dhall

let CurlRunnings = ./dhall/curl-runnings.dhall

in CurlRunnings.hydrateCase
CurlRunnings.Case::{
, expectData = Some
( CurlRunnings.ExpectData.Exactly
( JSON.object
[ { mapKey = "okay", mapValue = JSON.bool True },
{ mapKey = "message", mapValue = JSON.string "a message" }]
)
)
, expectStatus = 200
, name = "test 1"
, requestMethod = CurlRunnings.HttpMethod.GET
, url = "http://your-endpoing.com/status"
}
```


```yaml
---
Expand Down
240 changes: 240 additions & 0 deletions dhall/curl-runnings.dhall
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
let JSON = https://prelude.dhall-lang.org/JSON/package.dhall

let List/map = https://prelude.dhall-lang.org/List/map

let Optional/map = https://prelude.dhall-lang.org/Optional/map

let Map = https://prelude.dhall-lang.org/Map/Type

let HttpMethod
: Type
= < GET | POST | PUT | PATCH | DELETE >

let PartialMatcher =
< KeyMatch : Text
| ValueMatch : JSON.Type
| KeyValueMatch : { key : Text, value : JSON.Type }
>

let ExpectData =
< Exactly : JSON.Type
| Contains : List PartialMatcher
| NotContains : List PartialMatcher
| MixedContains :
{ contains : List PartialMatcher, notContains : List PartialMatcher }
>

let KeyValMatchHydrated =
{ keyMatch : Optional PartialMatcher
, valueMatch : Optional PartialMatcher
, keyValueMatch : Optional PartialMatcher
}

let ExpectHeaders =
< HeaderString : Text
| HeaderKeyVal : { key : Optional Text, value : Optional Text }
>

let hydrateContains =
λ(containsMatcher : PartialMatcher)
merge
{ KeyMatch =
λ(k : Text)
{ keyMatch = Some (PartialMatcher.KeyMatch k)
, valueMatch = None PartialMatcher
, keyValueMatch = None PartialMatcher
}
, ValueMatch =
λ(v : JSON.Type)
{ keyMatch = None PartialMatcher
, valueMatch = Some (PartialMatcher.ValueMatch v)
, keyValueMatch = None PartialMatcher
}
, KeyValueMatch =
λ(args : { key : Text, value : JSON.Type })
{ keyMatch = None PartialMatcher
, valueMatch = None PartialMatcher
, keyValueMatch = Some
( PartialMatcher.KeyValueMatch
{ key = args.key, value = args.value }
)
}
}
containsMatcher

let ExpectResponseHydrated =
{ exactly : Optional ExpectData
, contains : Optional (List KeyValMatchHydrated)
, notContains : Optional (List KeyValMatchHydrated)
}

let hydrateExpectData =
λ(matcher : ExpectData)
merge
{ Exactly =
λ(j : JSON.Type)
{ exactly = Some (ExpectData.Exactly j)
, contains = None (List KeyValMatchHydrated)
, notContains = None (List KeyValMatchHydrated)
}
, Contains =
λ(ms : List PartialMatcher)
{ exactly = None ExpectData
, contains = Some
( List/map
PartialMatcher
KeyValMatchHydrated
hydrateContains
ms
)
, notContains = None (List KeyValMatchHydrated)
}
, NotContains =
λ(ms : List PartialMatcher)
{ exactly = None ExpectData
, contains = None (List KeyValMatchHydrated)
, notContains = Some
( List/map
PartialMatcher
KeyValMatchHydrated
hydrateContains
ms
)
}
, MixedContains =
λ ( args
: { contains : List PartialMatcher
, notContains : List PartialMatcher
}
)
{ exactly = None ExpectData
, contains = Some
( List/map
PartialMatcher
KeyValMatchHydrated
hydrateContains
args.contains
)
, notContains = Some
( List/map
PartialMatcher
KeyValMatchHydrated
hydrateContains
args.notContains
)
}
}
matcher

let BodyType = < json | urlencoded >

let RequestData = < JSON : JSON.Type | UrlEncoded : Map Text Text >

let RequestDataHydrated = { bodyType : BodyType, content : RequestData }

let hydrateRquestData =
λ(reqData : RequestData)
merge
{ JSON =
λ(json : JSON.Type)
{ bodyType = BodyType.json, content = RequestData.JSON json }
, UrlEncoded =
λ(encoded : Map Text Text)
{ bodyType = BodyType.urlencoded
, content = RequestData.UrlEncoded encoded
}
}
reqData

let makeQueryParams =
λ(params : Map Text Text)
JSON.object
( List/map
{ mapKey : Text, mapValue : Text }
{ mapKey : Text, mapValue : JSON.Type }
( λ(args : { mapKey : Text, mapValue : Text })
{ mapKey = args.mapKey, mapValue = JSON.string args.mapValue }
)
params
)

let QueryParams = List { mapKey : Text, mapValue : JSON.Type }

let Case =
{ Type =
{ name : Text
, url : Text
, requestMethod : HttpMethod
, queryParameters : Map Text Text
, expectData : Optional ExpectData
, expectStatus : Natural
, headers : Optional Text
, expectHeaders : Optional (List ExpectHeaders)
, allowedRedirects : Natural
, requestData : Optional RequestData
}
, default =
{ expectData = None ExpectData
, headers = None Text
, expectHeaders = None (List ExpectHeaders)
, allowedRedirects = 10
, queryParameters = [] : Map Text Text
, requestData = None RequestData
}
}

let HydratedCase =
{ Type =
{ name : Text
, url : Text
, requestMethod : HttpMethod
, queryParameters : JSON.Type
, expectData : Optional ExpectResponseHydrated
, expectStatus : Natural
, headers : Optional Text
, expectHeaders : Optional (List ExpectHeaders)
, allowedRedirects : Natural
, requestData : Optional RequestDataHydrated
}
, default =
{ expectData = None ExpectResponseHydrated
, headers = None Text
, expectHeaders = None (List ExpectHeaders)
, allowedRedirects = 10
, queryParameters = JSON.null
, requestData = None RequestDataHydrated
}
}

let hydrateCase =
λ(c : Case.Type)
c
{ queryParameters = makeQueryParams c.queryParameters
, expectData =
Optional/map
ExpectData
ExpectResponseHydrated
hydrateExpectData
c.expectData
, requestData =
Optional/map
RequestData
RequestDataHydrated
hydrateRquestData
c.requestData
}

let hydrateCases =
λ(cases : List Case.Type)
List/map Case.Type HydratedCase.Type hydrateCase cases

in { Case
, HydratedCase
, hydrateCase
, hydrateCases
, HttpMethod
, ExpectData
, PartialMatcher
, ExpectHeaders
, RequestData
}
Loading

0 comments on commit 3921500

Please sign in to comment.