Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow setting different responses per request #31 #102

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 149 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Killgrave is a simulator for HTTP-based APIs, in simple words a **Mock Server**,
* [Create an imposter using JSON Schema](#create-an-imposter-using-json-schema)
* [Create an imposter with delay](#create-an-imposter-with-delay)
* [Create an imposter with dynamic responses](#create-an-imposter-with-dynamic-responses)
* [Create an imposter with repeated responses](#create-an-imposter-with-repeated-responses)
- [Contributing](#contributing)
- [License](#license)

Expand Down Expand Up @@ -276,13 +277,15 @@ imposter example:
"method": "GET",
"endpoint": "/gophers/01D8EMQ185CA8PRGE20DKZTGSR"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"data\":{\"type\":\"gophers\",\"id\":\"01D8EMQ185CA8PRGE20DKZTGSR\",\"attributes\":{\"name\":\"Zebediah\",\"color\":\"Purples\",\"age\":55}}}"
}
"responses": [
{
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"data\":{\"type\":\"gophers\",\"id\":\"01D8EMQ185CA8PRGE20DKZTGSR\",\"attributes\":{\"name\":\"Zebediah\",\"color\":\"Purples\",\"age\":55}}}"
}
]
}
]
```
Expand All @@ -297,7 +300,7 @@ next sections.
The imposter object can be divided on two parts:

* [Request](#request)
* [Response](#response)
* [Responses](#responses)

#### Request

Expand All @@ -315,8 +318,9 @@ json schema are.
* `headers`: if you want to specify a restrictive headers, like authorization or any other, you can define a list
of headers that you request will need to match, more info: [Create an imposter with headers](#create-an-imposter-with-headers).

#### Response
#### Responses

`responses` is the list of `response` object.
On the other hand we have the `response` object, the `response` object will be in charge to generate the output that
we want, related with the `request` that was executed.

Expand Down Expand Up @@ -351,13 +355,15 @@ In the next example, we have an endpoint configured to match with any kind of [U
"method": "GET",
"endpoint": "/gophers/{_id:[\\w]{26}}"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"data\":{\"type\":\"gophers\",\"id\":\"01D8EMQ185CA8PRGE20DKZTGSR\",\"attributes\":{\"name\":\"Zebediah\",\"color\":\"Purples\",\"age\":55}}}"
}
"responses": [
{
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"data\":{\"type\":\"gophers\",\"id\":\"01D8EMQ185CA8PRGE20DKZTGSR\",\"attributes\":{\"name\":\"Zebediah\",\"color\":\"Purples\",\"age\":55}}}"
}
]
}
]
```
Expand All @@ -379,13 +385,15 @@ For example, we want an imposter that only do match if we receive an apiKey as p
"apiKey": "{_apiKey:[\\w]+}"
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"data\":{\"type\":\"gophers\",\"id\":\"01D8EMQ185CA8PRGE20DKZTGSR\",\"attributes\":{\"name\":\"Zebediah\",\"color\":\"Purples\",\"age\":55}}}"
}
"responses": [
{
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"data\":{\"type\":\"gophers\",\"id\":\"01D8EMQ185CA8PRGE20DKZTGSR\",\"attributes\":{\"name\":\"Zebediah\",\"color\":\"Purples\",\"age\":55}}}"
}
]
}
]
```
Expand All @@ -407,13 +415,15 @@ this:
"Authorization": "\\w+"
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"data\":{\"type\":\"gophers\",\"id\":\"01D8EMQ185CA8PRGE20DKZTGSR\",\"attributes\":{\"name\":\"Zebediah\",\"color\":\"Purples\",\"age\":55}}}"
}
"responses": [
{
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"data\":{\"type\":\"gophers\",\"id\":\"01D8EMQ185CA8PRGE20DKZTGSR\",\"attributes\":{\"name\":\"Zebediah\",\"color\":\"Purples\",\"age\":55}}}"
}
]
}
]
```
Expand Down Expand Up @@ -502,12 +512,14 @@ Then our `imposter` will be configure in the next way:
"Content-Type": "application/json"
}
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json"
"responses": [
{
"status": 201,
"headers": {
"Content-Type": "application/json"
}
}
}
]
}
]
````
Expand All @@ -533,13 +545,15 @@ For example, we can modify our previous POST call to add a `delay`, we want that
"Content-Type": "application/json"
}
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json"
},
"delay": "1s:5s"
}
"responses": [
{
"status": 201,
"headers": {
"Content-Type": "application/json"
},
"delay": "1s:5s"
}
]
}
]
````
Expand All @@ -565,31 +579,113 @@ json schema declared.
"Content-Type": "application/json"
}
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json"
"responses": [
{
"status": 201,
"headers": {
"Content-Type": "application/json"
}
}
}
]
},
{
"request": {
"method": "POST",
"endpoint": "/gophers"
},
"response": {
"status": 400,
"responses": [
{
"status": 400,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"errors\":\"bad request\"}"
}
]
}
]
````
So as you can see, we have first of all the imposters with the restrictive, `headers` and `json schema`, but
our last `imposter` is a simple `imposter` that it will match with any call via POST to `/gophers`.

### Create an Imposter with repeated responses

Killgrave allow repeatable/random responses, with this feature we can use one endpoint and obtain repeatable responses based on the settings provided by the user in imposter config.

There are totally two new fields which needs to be configured for this feature.
1. `request` -> `responseMode`
- `RANDOM` and `BURST` are valid values
- `RANDOM` is default if field is not provided or any other value is provided.
2. `response` -> `burst` (applicable only in `BURST` mode, ignored in)
- +ve integer are valid values.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by +ve? 🤔

Copy link
Author

@infinite-spectrum infinite-spectrum Jul 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, +ve means positive. I'll change it to be positive instead of +ve.

- applicable only in `BURST` mode, ignored in `RANDOM` mode.
- default value in `BURST` mode is 1, if the field is missing.
- 0 or -ve values will be converted to 1

For example let's consider an imposter config that will give us random response.
````json
[
{
"request": {
"method": "GET",
"endpoint": "/gophers",
"responseMode": "RANDOM"
},
"responses": [
{
"status": 201,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"errors\":\"bad request\"}"
}
"body": "Response 1"
},
{
"status": 201,
"headers": {
"Content-Type": "application/json"
},
"body": "Response 2"
}
]
}
]
````
As you can see in the above request `responseMode` is `RANDOM` and a call to /gophers will generate random responses from array of responses. So for some requests you'll get `Response 1` and for others you'll get `Response 2` randomly. In the request if `responseMode` is missing or have any other value than what is expected, then it'll act same as before (randomly).

So as you can see, we have first of all the imposters with the restrictive, `headers` and `json schema`, but
our last `imposter` is a simple `imposter` that it will match with any call via POST to `/gophers`.
Now, let's consider an imposter example which will give us repeated responses.
````json
[
{
"request": {
"method": "GET",
"endpoint": "/gophers",
"responseMode": "BURST"
},
"responses": [
{
"status": 201,
"headers": {
"Content-Type": "application/json"
},
"body": "Response 1",
"burst": 1
},
{
"status": 201,
"headers": {
"Content-Type": "application/json"
},
"body": "Response 2",
"burst": 2
}
]
}
]
````
As you can see in the above request `responseMode` is `BURST` and a call to /gophers will generate repeated responses from array of responses. So for first request you'll get `Response 1` and for next 2 requests you'll get `Response 2`. It'll repeate responses from response 1 afterwards.

For e.g. a call to /gophers above will give responses in the following order: <br>
`Response 1` -> `Response 2` -> `Response 2` -> `Response 1` -> `Response 2` -> `Response 2` ...

## Contributing
[Contributions](CONTRIBUTING.md) are more than welcome, if you are interested please follow our guidelines to help you get started.
Expand Down
5 changes: 2 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8t
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
Expand All @@ -31,7 +30,7 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
26 changes: 14 additions & 12 deletions internal/server/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,32 @@ import (
// ImposterHandler create specific handler for the received imposter
func ImposterHandler(imposter Imposter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if imposter.Delay() > 0 {
time.Sleep(imposter.Delay())
response := imposter.GetResponse()

if response.Delay() > 0 {
time.Sleep(response.Delay())
}
writeHeaders(imposter, w)
w.WriteHeader(imposter.Response.Status)
writeBody(imposter, w)
writeHeaders(response, w)
w.WriteHeader(response.Status)
writeBody(imposter, &response, w)
}
}

func writeHeaders(imposter Imposter, w http.ResponseWriter) {
if imposter.Response.Headers == nil {
func writeHeaders(response Response, w http.ResponseWriter) {
if response.Headers == nil {
return
}

for key, val := range *imposter.Response.Headers {
for key, val := range *response.Headers {
w.Header().Set(key, val)
}
}

func writeBody(imposter Imposter, w http.ResponseWriter) {
wb := []byte(imposter.Response.Body)
func writeBody(imposter Imposter, response *Response, w http.ResponseWriter) {
wb := []byte(response.Body)

if imposter.Response.BodyFile != nil {
bodyFile := imposter.CalculateFilePath(*imposter.Response.BodyFile)
if response.BodyFile != nil {
bodyFile := imposter.CalculateFilePath(*response.BodyFile)
wb = fetchBodyFromFile(bodyFile)
}
w.Write(wb)
Expand Down
8 changes: 4 additions & 4 deletions internal/server/http/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ func TestImposterHandler(t *testing.T) {
expectedBody string
statusCode int
}{
{"valid imposter with body", Imposter{Request: validRequest, Response: Response{Status: http.StatusOK, Headers: &headers, Body: body}}, body, http.StatusOK},
{"valid imposter with bodyFile", Imposter{Request: validRequest, Response: Response{Status: http.StatusOK, Headers: &headers, BodyFile: &bodyFile}}, string(expectedBodyFileData), http.StatusOK},
{"valid imposter with not exists bodyFile", Imposter{Request: validRequest, Response: Response{Status: http.StatusOK, Headers: &headers, BodyFile: &bodyFileFake}}, "", http.StatusOK},
{"valid imposter with body", Imposter{Request: validRequest, Responses: []Response{{Status: http.StatusOK, Headers: &headers, Body: body}}}, body, http.StatusOK},
{"valid imposter with bodyFile", Imposter{Request: validRequest, Responses: []Response{{Status: http.StatusOK, Headers: &headers, BodyFile: &bodyFile}}}, string(expectedBodyFileData), http.StatusOK},
{"valid imposter with not exists bodyFile", Imposter{Request: validRequest, Responses: []Response{{Status: http.StatusOK, Headers: &headers, BodyFile: &bodyFileFake}}}, "", http.StatusOK},
}

for _, tt := range dataTest {
Expand Down Expand Up @@ -90,7 +90,7 @@ func TestInvalidRequestWithSchema(t *testing.T) {
statusCode int
request []byte
}{
{"valid request no schema", Imposter{Request: Request{Method: "POST", Endpoint: "/gophers"}, Response: Response{Status: http.StatusOK, Body: "test ok"}}, http.StatusOK, validRequest},
{"valid request no schema", Imposter{Request: Request{Method: "POST", Endpoint: "/gophers"}, Responses: []Response{{Status: http.StatusOK, Body: "test ok"}}}, http.StatusOK, validRequest},
}

for _, tt := range dataTest {
Expand Down
Loading