diff --git a/README.md b/README.md
index aa538fa..de2138c 100644
--- a/README.md
+++ b/README.md
@@ -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)
@@ -266,13 +267,15 @@ We use a rule-based system to match requests to imposters. Therefore, you have t
"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}}}"
+ }
+ ]
}
]
```
@@ -286,7 +289,7 @@ This a very simple example. Killgrave has more possibilities for configuring imp
The imposter object can be divided in two parts:
* [Request](#request)
-* [Response](#response)
+* [Responses](#responses)
#### Request
@@ -298,10 +301,11 @@ This part defines how Killgrave should determine whether an incoming request mat
* `params`: Restrict incoming requests by query parameters. More info can be found [here](#create-an-imposter-with-query-params). Supports regex.
* `headers`: Restrict incoming requests by HTTP header. More info can be found [here](#create-an-imposter-with-headers).
-#### Response
+#### Responses
This part defines how Killgrave should respond to the incoming request. The `response` object has the following properties:
+
* `status` (mandatory): Integer defining the HTTP status to return.
* `body` or `bodyFile`: The response body. Either a literal string (`body`) or a path to a file (`bodyFile`). `bodyFile` is especially useful in the case of large outputs.
This property is optional: if not response body should be returned it should be removed or left empty.
@@ -327,13 +331,15 @@ In the next example, we have configured an endpoint 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}}}"
+ }
+ ]
}
]
```
@@ -354,13 +360,15 @@ In this example, we have configured an imposter that only matches if we receive
"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}}}"
+ }
+ ]
}
]
```
@@ -381,13 +389,15 @@ In the next example, we have configured an imposter that uses regex to match an
"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}}}"
+ }
+ ]
}
]
```
@@ -474,12 +484,14 @@ Then our imposter will be configured as follows:
"Content-Type": "application/json"
}
},
- "response": {
- "status": 201,
- "headers": {
- "Content-Type": "application/json"
+ "responses": [
+ {
+ "status": 201,
+ "headers": {
+ "Content-Type": "application/json"
+ }
}
- }
+ ]
}
]
````
@@ -507,13 +519,15 @@ For example, we can modify our previous POST call to add a `delay` to determine
"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"
+ }
+ ]
}
]
````
@@ -537,28 +551,115 @@ In the following example, we have defined multiple imposters for the `POST /goph
"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.
+ - 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).
+
+
+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:
+`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.
diff --git a/go.sum b/go.sum
index c0f305a..6dbedd2 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
@@ -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=
diff --git a/internal/server/http/handler.go b/internal/server/http/handler.go
index 83ee85a..d753316 100644
--- a/internal/server/http/handler.go
+++ b/internal/server/http/handler.go
@@ -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)
diff --git a/internal/server/http/handler_test.go b/internal/server/http/handler_test.go
index 1a35be1..0cf0f56 100644
--- a/internal/server/http/handler_test.go
+++ b/internal/server/http/handler_test.go
@@ -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 {
@@ -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 {
diff --git a/internal/server/http/imposter.go b/internal/server/http/imposter.go
index 88c8b1a..4325ff2 100644
--- a/internal/server/http/imposter.go
+++ b/internal/server/http/imposter.go
@@ -6,6 +6,7 @@ import (
"path"
"path/filepath"
"strings"
+ "sync"
"time"
)
@@ -33,14 +34,13 @@ type ImposterConfig struct {
// Imposter define an imposter structure
type Imposter struct {
- BasePath string
- Request Request `json:"request"`
- Response Response `json:"response"`
-}
+ BasePath string
+ Request Request `json:"request"`
+ Responses []Response `json:"responses"`
-// Delay returns delay for response that user can specify in imposter config
-func (i *Imposter) Delay() time.Duration {
- return i.Response.Delay.Delay()
+ // Field for handling burst response
+ responseHandler ResponseHandler
+ sync.Mutex
}
// CalculateFilePath calculate file path based on basePath of imposter directory
@@ -48,13 +48,34 @@ func (i *Imposter) CalculateFilePath(filePath string) string {
return path.Join(i.BasePath, filePath)
}
+// GetResponse is used to get dynamic/random response.
+func (i *Imposter) GetResponse() Response {
+ i.Lock()
+ defer i.Unlock()
+ // If no response provided then returning 404 response
+ if len(i.Responses) == 0 {
+ return Response{Status: 404}
+ }
+ if i.responseHandler.scheduleMap == nil {
+ i.fillDefaults()
+ }
+ index := i.responseHandler.GetIndex()
+ return i.Responses[index]
+}
+
+// fillDefaults is used to populate default values for imposters fields.
+func (i *Imposter) fillDefaults() {
+ i.responseHandler.fillDefaults(i)
+}
+
// Request represent the structure of real request
type Request struct {
- Method string `json:"method"`
- Endpoint string `json:"endpoint"`
- SchemaFile *string `json:"schemaFile"`
- Params *map[string]string `json:"params"`
- Headers *map[string]string `json:"headers"`
+ Method string `json:"method"`
+ Endpoint string `json:"endpoint"`
+ SchemaFile *string `json:"schemaFile"`
+ Params *map[string]string `json:"params"`
+ Headers *map[string]string `json:"headers"`
+ ResponseMode string `json:"responseMode"`
}
// Response represent the structure of real response
@@ -63,7 +84,13 @@ type Response struct {
Body string `json:"body"`
BodyFile *string `json:"bodyFile" yaml:"bodyFile"`
Headers *map[string]string `json:"headers"`
- Delay ResponseDelay `json:"delay" yaml:"delay"`
+ RDelay ResponseDelay `json:"delay" yaml:"delay"`
+ Burst int `json:"burst" yaml:"burst"`
+}
+
+// Delay returns delay for response that user can specify in imposter config
+func (r *Response) Delay() time.Duration {
+ return r.RDelay.Delay()
}
func findImposters(impostersDirectory string, imposterConfigCh chan ImposterConfig) error {
diff --git a/internal/server/http/response_handler.go b/internal/server/http/response_handler.go
new file mode 100644
index 0000000..59e96c7
--- /dev/null
+++ b/internal/server/http/response_handler.go
@@ -0,0 +1,94 @@
+package http
+
+import (
+ "math/rand"
+)
+
+// ResponseMode represents random/burst mode for the response
+type ResponseMode int
+
+// ResponseHandler handles incoming request for random/burst response
+type ResponseHandler struct {
+ Mode ResponseMode // flag for random mode
+ totalResp int // total Responses available in imposters
+
+ counter int // to keep count of served requests (wrapping after totalResp)
+ currentInd int // index/key of current response in scheduleMap
+ scheduleMap []int // prefix array of repeating request
+}
+
+const (
+ // RandomMode will generate random responses
+ RandomMode ResponseMode = iota
+ // BurstMode will generate repeatable responses
+ BurstMode
+)
+
+// fillDefaults populates values based on imposter configuration
+func (rh *ResponseHandler) fillDefaults(imposter *Imposter) {
+ // Updating totalResponse length
+ rh.totalResp = len(imposter.Responses)
+
+ // Updating Response Mode
+ switch imposter.Request.ResponseMode {
+ case "BURST":
+ rh.Mode = BurstMode
+ default:
+ rh.Mode = RandomMode
+ }
+
+ // Populating state for BURST mode
+ if rh.Mode == BurstMode {
+ var scheduleMap = make([]int, len(imposter.Responses))
+ for ind, resp := range imposter.Responses {
+ value := resp.Burst
+ if value <= 0 {
+ value = 1
+ }
+ if ind != 0 {
+ scheduleMap[ind] = scheduleMap[ind-1] + value
+ } else {
+ scheduleMap[ind] = value
+ }
+ }
+
+ rh.scheduleMap = scheduleMap
+ rh.counter = 1
+ rh.currentInd = 0
+ }
+}
+
+// GetIndex is responsible for getting index for current request
+func (rh *ResponseHandler) GetIndex() int {
+ if rh.Mode == RandomMode {
+ return rh.getRandomIndex()
+ }
+ ind := rh.getBurstIndex()
+
+ return ind
+}
+
+// getRandomIndex generates random indexes in random mode
+func (rh *ResponseHandler) getRandomIndex() int {
+ return rand.Intn(rh.totalResp)
+}
+
+// getBurstIndex generates repeated index based on the config provided
+func (rh *ResponseHandler) getBurstIndex() int {
+ var index = rh.currentInd
+
+ rh.counter++ // incrementing counter for current request
+
+ // checking if it has to move to next response or not
+ if rh.scheduleMap[rh.currentInd] < rh.counter {
+ rh.currentInd++
+ }
+
+ // Wrapping logic for counter and index
+ if rh.currentInd > rh.totalResp-1 {
+ rh.currentInd = 0
+ rh.counter = 1
+ }
+
+ return index // returning current request
+}
diff --git a/internal/server/http/response_handler_test.go b/internal/server/http/response_handler_test.go
new file mode 100644
index 0000000..5671845
--- /dev/null
+++ b/internal/server/http/response_handler_test.go
@@ -0,0 +1,205 @@
+package http
+
+import (
+ "bytes"
+ "net/http"
+ "net/http/httptest"
+ "sync"
+ "testing"
+)
+
+func TestRepeatingResponse(t *testing.T) {
+ var serverData = []struct {
+ name string
+ imposter Imposter
+ expectedBodies []string
+ }{
+ {
+ "repeating response with burst",
+ Imposter{
+ Request: Request{Method: "GET", Endpoint: "/burst", ResponseMode: "BURST"},
+ Responses: []Response{{Status: 201, Body: "Response 1", Burst: 1}, {Status: 201, Body: "Response 2", Burst: 2}},
+ },
+ []string{"Response 1", "Response 2", "Response 2", "Response 1", "Response 2", "Response 2", "Response 1"},
+ },
+ {
+ "repeating response without burst", // Default value checking
+ Imposter{
+ Request: Request{Method: "GET", Endpoint: "/repeat", ResponseMode: "BURST"},
+ Responses: []Response{{Status: 201, Body: "Response 1"}, {Status: 201, Body: "Response 2"}},
+ },
+ []string{"Response 1", "Response 2", "Response 1", "Response 2", "Response 1"},
+ },
+ }
+
+ for _, tt := range serverData {
+ t.Run(tt.name, func(t *testing.T) {
+ req, err := http.NewRequest(tt.imposter.Request.Method, tt.imposter.Request.Endpoint, bytes.NewBuffer(nil))
+ if err != nil {
+ t.Fatalf("could not created request: %v", err)
+ }
+ rec := httptest.NewRecorder()
+ handler := http.HandlerFunc(ImposterHandler(tt.imposter))
+
+ for i := 0; i < len(tt.expectedBodies); i++ {
+ handler.ServeHTTP(rec, req)
+ expectedBody := tt.expectedBodies[i]
+ actualBody := rec.Body.String()
+ if expectedBody != actualBody {
+ t.Fatalf("test-%s expected body is '%s' and got '%s'", tt.name, expectedBody, actualBody)
+ }
+ rec.Body.Reset()
+ }
+ })
+ }
+}
+
+// Checking if valid responses are being generated.
+func TestRandomResponse(t *testing.T) {
+ var serverData = []struct {
+ name string
+ imposter Imposter
+ }{
+ {
+ "random responses",
+ Imposter{
+ Request: Request{Method: "GET", Endpoint: "/random", ResponseMode: "RANDOM"},
+ Responses: []Response{{Status: 201, Body: "Response 1"}, {Status: 201, Body: "Response 2"}},
+ },
+ },
+ {
+ "random responses with burst",
+ Imposter{
+ Request: Request{Method: "GET", Endpoint: "/random", ResponseMode: "RANDOM"},
+ Responses: []Response{{Status: 201, Body: "Response 1", Burst: 1}, {Status: 201, Body: "Response 2", Burst: 2}},
+ },
+ },
+ {
+ "random responses without response mode", // Default value checking
+ Imposter{
+ Request: Request{Method: "GET", Endpoint: "/random"},
+ Responses: []Response{{Status: 201, Body: "Response 1"}, {Status: 201, Body: "Response 2"}},
+ },
+ },
+ {
+ "random responses with more than 2 responses",
+ Imposter{
+ Request: Request{Method: "GET", Endpoint: "/random", ResponseMode: "BURST"},
+ Responses: []Response{{Status: 201, Body: "Response 1"}, {Status: 201, Body: "Response 2"}, {Status: 201, Body: "Response 3"}, {Status: 201, Body: "Response 4"}},
+ },
+ },
+ }
+
+ for _, tt := range serverData {
+ t.Run(tt.name, func(t *testing.T) {
+ req, err := http.NewRequest(tt.imposter.Request.Method, tt.imposter.Request.Endpoint, bytes.NewBuffer(nil))
+ if err != nil {
+ t.Fatalf("could not created request: %v", err)
+ }
+ rec := httptest.NewRecorder()
+ handler := http.HandlerFunc(ImposterHandler(tt.imposter))
+ expectedRespMap := map[string]bool{}
+ for i := 0; i < len(tt.imposter.Responses); i++ {
+ expectedRespMap[tt.imposter.Responses[i].Body] = true
+ }
+
+ for i := 0; i < 10; i++ {
+ handler.ServeHTTP(rec, req)
+ actualBody := rec.Body.String()
+
+ if _, ok := expectedRespMap[actualBody]; !ok {
+ t.Fatalf("test-%s invalid response body: '%s'", tt.name, actualBody)
+ }
+
+ rec.Body.Reset()
+ }
+ })
+ }
+}
+
+func Test404Response(t *testing.T) {
+ var serverData = []struct {
+ name string
+ imposter Imposter
+ statusCode int
+ }{
+ {
+ "no response available burst mode",
+ Imposter{
+ Request: Request{Method: "GET", Endpoint: "/burst", ResponseMode: "BURST"},
+ },
+ http.StatusNotFound,
+ },
+ {
+ "no response available random mode",
+ Imposter{
+ Request: Request{Method: "GET", Endpoint: "/random", ResponseMode: "RANDOM"},
+ },
+ http.StatusNotFound,
+ },
+ }
+
+ for _, tt := range serverData {
+ t.Run(tt.name, func(t *testing.T) {
+ req, err := http.NewRequest(tt.imposter.Request.Method, tt.imposter.Request.Endpoint, bytes.NewBuffer(nil))
+ if err != nil {
+ t.Fatalf("could not created request: %v", err)
+ }
+ rec := httptest.NewRecorder()
+ handler := http.HandlerFunc(ImposterHandler(tt.imposter))
+
+ handler.ServeHTTP(rec, req)
+ if rec.Code != tt.statusCode {
+ t.Fatalf("test-%s expected status code is '%d' and got '%d'", tt.name, tt.statusCode, rec.Code)
+ }
+ })
+ }
+}
+
+func TestConcurrentRequests(t *testing.T) {
+ var serverData = []struct {
+ name string
+ imposter Imposter
+ loopFor int
+ expectedBody string
+ }{
+ {
+ "concurrent requests using go routines",
+ Imposter{
+ Request: Request{Method: "GET", Endpoint: "/burst", ResponseMode: "BURST"},
+ Responses: []Response{{Status: 201, Body: "Response 1", Burst: 1}, {Status: 201, Body: "Response 2", Burst: 2}, {Status: 201, Body: "Response 3", Burst: 3}},
+ },
+ 12,
+ "Response 1",
+ },
+ }
+
+ for _, tt := range serverData {
+ t.Run(tt.name, func(t *testing.T) {
+ req, err := http.NewRequest(tt.imposter.Request.Method, tt.imposter.Request.Endpoint, bytes.NewBuffer(nil))
+ if err != nil {
+ t.Fatalf("could not created request: %v", err)
+ }
+ rec := httptest.NewRecorder()
+ handler := http.HandlerFunc(ImposterHandler(tt.imposter))
+
+ var wg sync.WaitGroup
+
+ for i := 0; i < tt.loopFor; i++ {
+ wg.Add(1)
+ go func(wg *sync.WaitGroup) {
+ defer wg.Done()
+ handler.ServeHTTP(rec, req)
+ }(&wg)
+ }
+
+ wg.Wait()
+ rec.Body.Reset()
+ handler.ServeHTTP(rec, req)
+ actualBody := rec.Body.String()
+ if actualBody != tt.expectedBody {
+ t.Fatalf("test-%s expected body is '%s' and got '%s'", tt.name, tt.expectedBody, actualBody)
+ }
+ })
+ }
+}
diff --git a/internal/server/http/route_matchers_test.go b/internal/server/http/route_matchers_test.go
index 1095837..f854129 100644
--- a/internal/server/http/route_matchers_test.go
+++ b/internal/server/http/route_matchers_test.go
@@ -53,13 +53,13 @@ func TestMatcherBySchema(t *testing.T) {
req *http.Request
res bool
}{
- "correct request schema": {MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), httpRequestA, true},
- "imposter without request schema": {MatcherBySchema(Imposter{Request: requestWithoutSchema, Response: okResponse}), httpRequestA, true},
- "malformatted schema file": {MatcherBySchema(Imposter{Request: requestWithWrongSchema, Response: okResponse}), httpRequestA, false},
- "incorrect request schema": {MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), httpRequestB, false},
- "non-existing schema file": {MatcherBySchema(Imposter{Request: requestWithNonExistingSchema, Response: okResponse}), httpRequestB, false},
- "empty body with required schema file": {MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), &http.Request{Body: emptyBody}, false},
- "invalid request body": {MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), &http.Request{Body: wrongBody}, false},
+ "correct request schema": {MatcherBySchema(Imposter{Request: requestWithSchema, Responses: []Response{okResponse}}), httpRequestA, true},
+ "imposter without request schema": {MatcherBySchema(Imposter{Request: requestWithoutSchema, Responses: []Response{okResponse}}), httpRequestA, true},
+ "malformatted schema file": {MatcherBySchema(Imposter{Request: requestWithWrongSchema, Responses: []Response{okResponse}}), httpRequestA, false},
+ "incorrect request schema": {MatcherBySchema(Imposter{Request: requestWithSchema, Responses: []Response{okResponse}}), httpRequestB, false},
+ "non-existing schema file": {MatcherBySchema(Imposter{Request: requestWithNonExistingSchema, Responses: []Response{okResponse}}), httpRequestB, false},
+ "empty body with required schema file": {MatcherBySchema(Imposter{Request: requestWithSchema, Responses: []Response{okResponse}}), &http.Request{Body: emptyBody}, false},
+ "invalid request body": {MatcherBySchema(Imposter{Request: requestWithSchema, Responses: []Response{okResponse}}), &http.Request{Body: wrongBody}, false},
}
for name, tt := range matcherData {
diff --git a/internal/server/http/test/testdata/imposters/create_gopher.imp.json b/internal/server/http/test/testdata/imposters/create_gopher.imp.json
index 91bd9a5..b850b29 100644
--- a/internal/server/http/test/testdata/imposters/create_gopher.imp.json
+++ b/internal/server/http/test/testdata/imposters/create_gopher.imp.json
@@ -11,13 +11,13 @@
"gopherColor": "{v:[a-z]+}"
}
},
- "response": {
+ "responses": [{
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"bodyFile": "responses/create_gopher_response.json"
- }
+ }]
},
{
"t": "random_text"
diff --git a/internal/server/http/test/testdata/imposters/test_request.imp.json b/internal/server/http/test/testdata/imposters/test_request.imp.json
index 09d9403..7f59f20 100644
--- a/internal/server/http/test/testdata/imposters/test_request.imp.json
+++ b/internal/server/http/test/testdata/imposters/test_request.imp.json
@@ -4,9 +4,9 @@
"method": "GET",
"endpoint": "/testRequest"
},
- "response": {
+ "responses": [{
"status": 200,
"body": "Handled"
- }
+ }]
}
]
diff --git a/internal/server/http/test/testdata/imposters/test_request.imp.yaml b/internal/server/http/test/testdata/imposters/test_request.imp.yaml
index 0d49b8e..90c038b 100644
--- a/internal/server/http/test/testdata/imposters/test_request.imp.yaml
+++ b/internal/server/http/test/testdata/imposters/test_request.imp.yaml
@@ -2,6 +2,6 @@
- request:
method: GET
endpoint: /yamlTestRequest
- response:
- status: 200
- body: "Yaml Handled"
\ No newline at end of file
+ responses:
+ - status: 200
+ body: "Yaml Handled"
\ No newline at end of file
diff --git a/internal/server/http/test/testdata/imposters/test_request.imp.yml b/internal/server/http/test/testdata/imposters/test_request.imp.yml
index 4f73d62..dc11ae8 100644
--- a/internal/server/http/test/testdata/imposters/test_request.imp.yml
+++ b/internal/server/http/test/testdata/imposters/test_request.imp.yml
@@ -2,20 +2,20 @@
- request:
method: GET
endpoint: /ymlTestRequest
- response:
- status: 200
- body: "Yml Handled"
- delay: "1s:5s"
+ responses:
+ - status: 200
+ body: "Yml Handled"
+ delay: "1s:5s"
- request:
method: POST
endpoint: /yamlGophers
schemaFile: "schemas/create_gopher_request.json"
headers:
"Content-Type": "application/json"
- response:
- status: 201
- headers:
- "Content-Type": "application/json"
- "X-Source": "YAML"
- bodyFile: "responses/create_gopher_response.json"
+ responses:
+ - status: 201
+ headers:
+ "Content-Type": "application/json"
+ "X-Source": "YAML"
+ bodyFile: "responses/create_gopher_response.json"
- t: "random_text"
diff --git a/internal/server/http/test/testdata/imposters_secure/test_request.imp.json b/internal/server/http/test/testdata/imposters_secure/test_request.imp.json
index 0df11d8..70cd706 100644
--- a/internal/server/http/test/testdata/imposters_secure/test_request.imp.json
+++ b/internal/server/http/test/testdata/imposters_secure/test_request.imp.json
@@ -4,9 +4,9 @@
"method": "GET",
"endpoint": "/testHTTPSRequest"
},
- "response": {
+ "responses": [{
"status": 200,
"body": "Handled"
- }
+ }]
}
]