Skip to content

Commit

Permalink
Merge branch 'main' into remove-afero
Browse files Browse the repository at this point in the history
  • Loading branch information
joanlopez committed Jun 13, 2024
2 parents 866e7f8 + 392da86 commit a066757
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 43 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ jobs:
steps:
- uses: actions/checkout@v3

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to DockerHub
uses: docker/login-action@v3
with:
Expand All @@ -53,5 +59,6 @@ jobs:
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ FROM golang:1.21-alpine AS build

LABEL MAINTAINER = 'Friends of Go ([email protected])'

ARG TARGETOS=linux
ARG TARGETARCH=amd64

RUN apk add --update git
RUN apk add ca-certificates
WORKDIR /go/src/github.com/friendsofgo/killgrave
COPY . .
RUN go mod tidy && TAG=$(git describe --tags --abbrev=0) \
&& LDFLAGS=$(echo "-s -w -X github.com/friendsofgo/killgrave/internal/app/cmd._version="docker-$TAG) \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o /go/bin/killgrave -ldflags "$LDFLAGS" cmd/killgrave/main.go
&& CGO_ENABLED=0 GOOS="${TARGETOS}" GOARCH="${TARGETARCH}" go build -a -installsuffix cgo -o /go/bin/killgrave -ldflags "$LDFLAGS" cmd/killgrave/main.go

# Building image with the binary
FROM scratch
Expand Down
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,8 @@ Killgrave is a simulator for HTTP-based APIs, in simple words a **Mock Server**,
![Github actions](https://github.com/friendsofgo/killgrave/actions/workflows/main.yaml/badge.svg?branch=main)
[![Version](https://img.shields.io/github/release/friendsofgo/killgrave.svg?style=flat-square)](https://github.com/friendsofgo/killgrave/releases/latest)
[![Go Report Card](https://goreportcard.com/badge/github.com/friendsofgo/killgrave)](https://goreportcard.com/report/github.com/friendsofgo/killgrave)
[![Total alerts](https://img.shields.io/lgtm/alerts/g/friendsofgo/killgrave.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/friendsofgo/killgrave/alerts/)
[![FriendsOfGo](https://img.shields.io/badge/powered%20by-Friends%20of%20Go-73D7E2.svg)](https://friendsofgo.tech)

<p>
<a href="https://www.buymeacoffee.com/friendsofgo" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: 100px !important;" ></a>
</p>

# Table of Content
- [Overview](#overview)
- [Concepts](#concepts)
Expand Down Expand Up @@ -91,13 +86,13 @@ $ brew install friendsofgo/tap/killgrave
The application is also available through [Docker](https://hub.docker.com/r/friendsofgo/killgrave).

```bash
docker run -it --rm -p 3000:3000 -v $PWD/:/home -w /home friendsofgo/killgrave -host 0.0.0.0
docker run -it --rm -p 3000:3000 -v $PWD/:/home -w /home friendsofgo/killgrave --host 0.0.0.0
```

`-p 3000:3000` [publishes](https://docs.docker.com/engine/reference/run/#expose-incoming-ports) port 3000 (Killgrave's default port) inside the
container to port 3000 on the host machine.

`-host 0.0.0.0` is necessary to allow Killgrave to listen and respond to requests from outside the container (the default,
`--host 0.0.0.0` is necessary to allow Killgrave to listen and respond to requests from outside the container (the default,
`localhost`, will not capture requests from the host network).

### Compile by yourself
Expand Down
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())
})
}
80 changes: 69 additions & 11 deletions internal/server/http/imposter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package http
import (
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"time"

"gopkg.in/yaml.v2"
)
Expand Down Expand Up @@ -37,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 @@ -71,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 {
path string
fs fs.FS
Expand Down Expand Up @@ -117,8 +173,10 @@ func (i ImposterFs) FindImposters(impostersCh chan []Imposter) error {
}

func (i ImposterFs) unmarshalImposters(imposterConfig ImposterConfig) ([]Imposter, error) {
// TODO: Error handling?
bytes, _ := fs.ReadFile(i.fs, imposterConfig.FilePath)
imposterFile, _ := i.fs.Open(imposterConfig.FilePath)
defer imposterFile.Close()

bytes, _ := io.ReadAll(imposterFile)

var parseError error
var imposters []Imposter
Expand Down
Loading

0 comments on commit a066757

Please sign in to comment.