Skip to content

Commit

Permalink
Support for sequences of responses per imposter
Browse files Browse the repository at this point in the history
  • Loading branch information
joanlopez committed Apr 11, 2024
1 parent f95b1f4 commit cc54512
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 33 deletions.
27 changes: 14 additions & 13 deletions internal/server/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,33 @@ import (
)

// ImposterHandler create specific handler for the received imposter
func ImposterHandler(imposter Imposter) http.HandlerFunc {
func ImposterHandler(i Imposter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if imposter.Delay() > 0 {
time.Sleep(imposter.Delay())
res := i.NextResponse()
if res.Delay.Delay() > 0 {
time.Sleep(res.Delay.Delay())
}
writeHeaders(imposter, w)
w.WriteHeader(imposter.Response.Status)
writeBody(imposter, w)
writeHeaders(res, w)
w.WriteHeader(res.Status)
writeBody(i, res, w)
}
}

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

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

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

if imposter.Response.BodyFile != nil {
bodyFile := imposter.CalculateFilePath(*imposter.Response.BodyFile)
if r.BodyFile != nil {
bodyFile := i.CalculateFilePath(*r.BodyFile)
wb = fetchBodyFromFile(bodyFile)
}
w.Write(wb)
Expand Down
71 changes: 65 additions & 6 deletions internal/server/http/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestImposterHandler(t *testing.T) {
Expand Down Expand Up @@ -47,9 +48,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, Response: Responses{{Status: http.StatusOK, Headers: &headers, Body: body}}}, body, http.StatusOK},
{"valid imposter with bodyFile", Imposter{Request: validRequest, Response: Responses{{Status: http.StatusOK, Headers: &headers, BodyFile: &bodyFile}}}, string(expectedBodyFileData), http.StatusOK},
{"valid imposter with not exists bodyFile", Imposter{Request: validRequest, Response: Responses{{Status: http.StatusOK, Headers: &headers, BodyFile: &bodyFileFake}}}, "", http.StatusOK},
}

for _, tt := range dataTest {
Expand All @@ -58,7 +59,7 @@ func TestImposterHandler(t *testing.T) {
assert.NoError(t, err)

rec := httptest.NewRecorder()
handler := http.HandlerFunc(ImposterHandler(tt.imposter))
handler := ImposterHandler(tt.imposter)

handler.ServeHTTP(rec, req)
assert.Equal(t, rec.Code, tt.statusCode)
Expand All @@ -85,7 +86,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"}, Response: Responses{{Status: http.StatusOK, Body: "test ok"}}}, http.StatusOK, validRequest},
}

for _, tt := range dataTest {
Expand All @@ -94,11 +95,69 @@ func TestInvalidRequestWithSchema(t *testing.T) {
req, err := http.NewRequest("POST", "/gophers", bytes.NewBuffer(tt.request))
assert.Nil(t, err)
rec := httptest.NewRecorder()
handler := http.HandlerFunc(ImposterHandler(tt.imposter))
handler := ImposterHandler(tt.imposter)

handler.ServeHTTP(rec, req)

assert.Equal(t, tt.statusCode, rec.Code)
})
}
}

func TestImposterHandler_MultipleRequests(t *testing.T) {
req, err := http.NewRequest("POST", "/gophers", bytes.NewBuffer([]byte(`{
"data": {
"type": "gophers",
"attributes": {
"name": "Zebediah",
"color": "Purple"
}
}
}`)))
require.NoError(t, err)

t.Run("created then conflict", func(t *testing.T) {
imp := Imposter{
Request: Request{Method: "POST", Endpoint: "/gophers"},
Response: Responses{
{Status: http.StatusCreated, Body: "Created"},
{Status: http.StatusConflict, Body: "Conflict"},
},
}

handler := ImposterHandler(imp)

// First request
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusCreated, rec.Code)
assert.Equal(t, "Created", rec.Body.String())

// Second request
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusConflict, rec.Code)
assert.Equal(t, "Conflict", rec.Body.String())
})

t.Run("idempotent", func(t *testing.T) {
handler := ImposterHandler(Imposter{
Request: Request{Method: "POST", Endpoint: "/gophers"},
Response: Responses{
{Status: http.StatusAccepted, Body: "Accepted"},
},
})

// First request
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusAccepted, rec.Code)
assert.Equal(t, "Accepted", rec.Body.String())

// Second request
rec = httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusAccepted, rec.Code)
assert.Equal(t, "Accepted", rec.Body.String())
})
}
77 changes: 66 additions & 11 deletions internal/server/http/imposter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ package http
import (
"encoding/json"
"fmt"
"io/ioutil"
"io"
"os"
"path"
"path/filepath"
"strings"
"time"

"github.com/spf13/afero"
"gopkg.in/yaml.v2"
Expand Down Expand Up @@ -38,18 +37,22 @@ type ImposterConfig struct {

// Imposter define an imposter structure
type Imposter struct {
BasePath string `json:"-" yaml:"-"`
Path string `json:"-" yaml:"-"`
Request Request `json:"request"`
Response Response `json:"response"`
BasePath string `json:"-" yaml:"-"`
Path string `json:"-" yaml:"-"`
Request Request `json:"request"`
Response Responses `json:"response"`
resIdx int
}

// Delay returns delay for response that user can specify in imposter config
func (i *Imposter) Delay() time.Duration {
return i.Response.Delay.Delay()
// NextResponse returns the imposter's response.
// If there are multiple responses, it will return them sequentially.
func (i *Imposter) NextResponse() Response {
r := i.Response[i.resIdx]
i.resIdx = (i.resIdx + 1) % len(i.Response)
return r
}

// CalculateFilePath calculate file path based on basePath of imposter directory
// CalculateFilePath calculate file path based on basePath of imposter's directory
func (i *Imposter) CalculateFilePath(filePath string) string {
return path.Join(i.BasePath, filePath)
}
Expand All @@ -72,6 +75,58 @@ type Response struct {
Delay ResponseDelay `json:"delay" yaml:"delay"`
}

// Responses is a wrapper for Response, to allow the use of either a single
// response or an array of responses, while keeping backwards compatibility.
type Responses []Response

func (rr *Responses) MarshalJSON() ([]byte, error) {
if len(*rr) == 1 {
return json.Marshal((*rr)[0])
}
return json.Marshal(*rr)
}

func (rr *Responses) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
*rr = nil
return nil
}

if data[0] == '[' {
return json.Unmarshal(data, (*[]Response)(rr))
}

var r Response
if err := json.Unmarshal(data, &r); err != nil {
return err
}

*rr = Responses{r}
return nil
}

func (rr *Responses) MarshalYAML() (interface{}, error) {
if len(*rr) == 1 {
return (*rr)[0], nil
}
return *rr, nil
}

func (rr *Responses) UnmarshalYAML(unmarshal func(interface{}) error) error {
var r Response
if err := unmarshal(&r); err == nil {
*rr = Responses{r}
return nil
}

var tmp []Response
if err := unmarshal(&tmp); err != nil {
return err
}
*rr = tmp
return nil
}

type ImposterFs struct {
fs afero.Fs
}
Expand Down Expand Up @@ -114,7 +169,7 @@ func (i ImposterFs) unmarshalImposters(imposterConfig ImposterConfig) ([]Imposte
imposterFile, _ := i.fs.Open(imposterConfig.FilePath)
defer imposterFile.Close()

bytes, _ := ioutil.ReadAll(imposterFile)
bytes, _ := io.ReadAll(imposterFile)

var parseError error
var imposters []Imposter
Expand Down
Loading

0 comments on commit cc54512

Please sign in to comment.