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

Support for sequences of responses per imposter #167

Merged
merged 1 commit into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading