Skip to content

Commit

Permalink
Merge pull request #12 from friendsofgo/matching_by_request_schema
Browse files Browse the repository at this point in the history
Matching by request schema and calculate files directories
  • Loading branch information
aperezg committed Apr 25, 2019
2 parents 3d30ee6 + bc924cd commit 36f066a
Show file tree
Hide file tree
Showing 16 changed files with 204 additions and 116 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Expand Up @@ -15,4 +15,4 @@ imposters
schemas

!test/testdata/imposters/
!test/testdata/schemas/
!test/testdata/imposters/schemas/
7 changes: 7 additions & 0 deletions CHANGELOG.md
Expand Up @@ -14,3 +14,10 @@
* Create an official docker image for the application
* Update README.md with how to use the application with docker
* Allow write headers for the response

## v0.2.1 (2019/04/25)

* Allow imposter's matching by request schema
* Dynamic responses based on regex endpoint or request schema
* Calculate files directory(body and schema) based on imposters path
* Update REAMDE.md with resolved features and new future features
9 changes: 7 additions & 2 deletions README.md
@@ -1,8 +1,9 @@
[![CircleCI](https://circleci.com/gh/friendsofgo/killgrave/tree/master.svg?style=svg)](https://circleci.com/gh/friendsofgo/killgrave/tree/master)
[![Version](https://img.shields.io/github/release/friendsofgo/killgrave.svg?style=flat-square)](https://github.com/friendsofgo/killgrave/releases/latest)
[![codecov](https://codecov.io/gh/friendsofgo/killgrave/branch/master/graph/badge.svg)](https://codecov.io/gh/friendsofgo/killgrave)
[![Go Report Card](https://goreportcard.com/badge/github.com/friendsofgo/killgrave)](https://goreportcard.com/report/github.com/friendsofgo/killgrave)
[![GoDoc](https://godoc.org/graphql.co/graphql?status.svg)](https://godoc.org/github.com/friendsofgo/killgrave)
[![FriendsOfGo](https://img.shields.io/badge/powered%20by-Friends%20of%20Go-73D7E2.svg)](https://img.shields.io/badge/powered%20by-Friends%20of%20Go-73D7E2.svg)
[![FriendsOfGo](https://img.shields.io/badge/powered%20by-Friends%20of%20Go-73D7E2.svg)](https://friendsofgo.tech)

<p align="center">
<img src="https://res.cloudinary.com/fogo/image/upload/c_scale,w_350/v1555701634/fogo/projects/gopher-killgrave.png" alt="Golang Killgrave"/>
Expand Down Expand Up @@ -181,10 +182,14 @@ NOTE: If you want to use `killgrave` through Docker at the same time you use you
* Write bodies in line
* Regex for using on endpoint urls
* Allow write headers on response
* Allow imposter's matching by request schema
* Dynamic responses based on regex endpoint or request schema
## Next Features
- [ ] Dynamic responses based on headers
- [ ] Dynamic responses based on query params
- [ ] Allow write multiples imposters by file
- [ ] Proxy server
- [ ] Dynamic responses and error responses
- [ ] Record proxy server
- [ ] Better documentation with examples of each feature
Expand Down
48 changes: 2 additions & 46 deletions handler.go
Expand Up @@ -2,74 +2,29 @@ package killgrave

import (
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/textproto"
"os"
"strings"

"github.com/pkg/errors"
"github.com/xeipuuv/gojsonschema"
)

// ImposterHandler create specific handler for the received imposter
func ImposterHandler(imposter Imposter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := validateSchema(imposter, r.Body); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}

if err := validateHeaders(imposter, r.Header); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}


writeHeaders(imposter, w)
w.WriteHeader(imposter.Response.Status)
writeBody(imposter, w)
}
}

func validateSchema(imposter Imposter, bodyRequest io.ReadCloser) error {
if imposter.Request.SchemaFile == nil {
return nil
}

schemaFile := *imposter.Request.SchemaFile
if _, err := os.Stat(schemaFile); os.IsNotExist(err) {
return errors.Wrapf(err, "the schema file %s not found", schemaFile)
}

b, err := ioutil.ReadAll(bodyRequest)
if err != nil {
return errors.Wrapf(err, "impossible read the request body")
}

dir, _ := os.Getwd()
schemaFilePath := "file://" + dir + "/" + schemaFile
schema := gojsonschema.NewReferenceLoader(schemaFilePath)
document := gojsonschema.NewStringLoader(string(b))

res, err := gojsonschema.Validate(schema, document)
if err != nil {
return errors.Wrap(err, "error validating the json schema")
}

if !res.Valid() {
for _, desc := range res.Errors() {
return errors.New(desc.String())
}
}

return nil
}

func validateHeaders(imposter Imposter, header http.Header) error {
if imposter.Request.Headers == nil {
return nil
Expand Down Expand Up @@ -117,7 +72,8 @@ func writeBody(imposter Imposter, w http.ResponseWriter) {
wb := []byte(imposter.Response.Body)

if imposter.Response.BodyFile != nil {
wb = fetchBodyFromFile(*imposter.Response.BodyFile)
bodyFile := imposter.CalculateFilePath(*imposter.Response.BodyFile)
wb = fetchBodyFromFile(bodyFile)
}
w.Write(wb)
}
Expand Down
24 changes: 5 additions & 19 deletions handler_test.go
Expand Up @@ -23,9 +23,9 @@ func TestImposterHandler(t *testing.T) {
var headers = make(http.Header)
headers.Add("Content-Type", "application/json")

schemaFile := "test/testdata/schemas/create_gopher_request.json"
bodyFile := "test/testdata/responses/create_gopher_response.json"
bodyFileFake := "test/testdata/responses/create_gopher_response_fail.json"
schemaFile := "test/testdata/imposters/schemas/create_gopher_request.json"
bodyFile := "test/testdata/imposters/responses/create_gopher_response.json"
bodyFileFake := "test/testdata/imposters/responses/create_gopher_response_fail.json"
body := `{"test":true}`

validRequest := Request{
Expand Down Expand Up @@ -75,15 +75,6 @@ func TestImposterHandler(t *testing.T) {
}

func TestInvalidRequestWithSchema(t *testing.T) {
wrongRequest := []byte(`{
"data": {
"type": "gophers",
"attributes": {
"name": "Zebediah",
"color": "Purple"
}
}
}`)
validRequest := []byte(`{
"data": {
"type": "gophers",
Expand All @@ -93,23 +84,18 @@ func TestInvalidRequestWithSchema(t *testing.T) {
}
}
}`)
notExistFile := "failSchema"
wrongSchema := "test/testdata/schemas/create_gopher_request_fail.json"
validSchema := "test/testdata/schemas/create_gopher_request.json"

var dataTest = []struct {
name string
imposter Imposter
statusCode int
request []byte
}{
{"schema file not found", Imposter{Request: Request{Method: "POST", Endpoint: "/gophers", SchemaFile: &notExistFile}}, http.StatusBadRequest, validRequest},
{"wrong schema", Imposter{Request: Request{Method: "POST", Endpoint: "/gophers", SchemaFile: &wrongSchema}}, http.StatusBadRequest, validRequest},
{"request invalid", Imposter{Request: Request{Method: "POST", Endpoint: "/gophers", SchemaFile: &validSchema}}, http.StatusBadRequest, wrongRequest},
{"valid request no schema", Imposter{Request: Request{Method: "POST", Endpoint: "/gophers"}, Response: Response{Status: http.StatusOK, Body: "test ok"}}, http.StatusOK, validRequest},
}

for _, tt := range dataTest {

t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("POST", "/gophers", bytes.NewBuffer(tt.request))
if err != nil {
Expand Down Expand Up @@ -137,7 +123,7 @@ func TestInvalidHeaders(t *testing.T) {
}
}
}`)
schemaFile := "test/testdata/schemas/create_gopher_request.json"
schemaFile := "test/testdata/imposters/schemas/create_gopher_request.json"
var expectedHeaders = make(http.Header)
expectedHeaders.Add("Content-Type", "application/json")
expectedHeaders.Add("Authorization", "Bearer gopher")
Expand Down
19 changes: 14 additions & 5 deletions imposter.go
@@ -1,13 +1,22 @@
package killgrave

import "net/http"
import (
"net/http"
"path"
)

// Imposter define an imposter structure
type Imposter struct {
BasePath string
Request Request `json:"request"`
Response Response `json:"response"`
}

// CalculateFilePath calculate file path based on basePath of imposter directory
func (i *Imposter) CalculateFilePath(filePath string) string {
return path.Join(i.BasePath, filePath)
}

// Request represent the structure of real request
type Request struct {
Method string `json:"method"`
Expand All @@ -18,8 +27,8 @@ type Request struct {

// Response represent the structure of real response
type Response struct {
Status int `json:"status"`
Body string `json:"body"`
BodyFile *string `json:"bodyFile"`
Headers *http.Header `json:"headers"`
Status int `json:"status"`
Body string `json:"body"`
BodyFile *string `json:"bodyFile"`
Headers *http.Header `json:"headers"`
}
67 changes: 67 additions & 0 deletions route_matchers.go
@@ -0,0 +1,67 @@
package killgrave

import (
"bytes"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/xeipuuv/gojsonschema"
"io/ioutil"
"log"
"net/http"
"os"
)

// MatcherBySchema check if the request matching with the schema file
func MatcherBySchema(imposter Imposter) mux.MatcherFunc {
return func(req *http.Request, rm *mux.RouteMatch) bool {
err := validateSchema(imposter, req)

// TODO: inject the logger
if err != nil {
log.Println(err)
return false
}
return true
}
}

func validateSchema(imposter Imposter, req *http.Request) error {
if imposter.Request.SchemaFile == nil {
return nil
}

var b []byte

defer func() {
req.Body.Close()
req.Body = ioutil.NopCloser(bytes.NewBuffer(b))
}()

schemaFile := imposter.CalculateFilePath(*imposter.Request.SchemaFile)
if _, err := os.Stat(schemaFile); os.IsNotExist(err) {
return errors.Wrapf(err, "the schema file %s not found", schemaFile)
}

b, err := ioutil.ReadAll(req.Body)
if err != nil {
return errors.Wrapf(err, "impossible read the request body")
}

dir, _ := os.Getwd()
schemaFilePath := "file://" + dir + "/" + schemaFile
schema := gojsonschema.NewReferenceLoader(schemaFilePath)
document := gojsonschema.NewStringLoader(string(b))

res, err := gojsonschema.Validate(schema, document)
if err != nil {
return errors.Wrap(err, "error validating the json schema")
}

if !res.Valid() {
for _, desc := range res.Errors() {
return errors.New(desc.String())
}
}

return nil
}
67 changes: 67 additions & 0 deletions route_matchers_test.go
@@ -0,0 +1,67 @@
package killgrave

import (
"bytes"
"github.com/gorilla/mux"
"io/ioutil"
"net/http"
"testing"
)

func TestMatcherBySchema(t *testing.T) {
bodyA := ioutil.NopCloser(bytes.NewReader([]byte("{\"type\": \"gopher\"}")))
bodyB := ioutil.NopCloser(bytes.NewReader([]byte("{\"type\": \"cat\"}")))

schemaGopherFile := "test/testdata/imposters/schemas/type_gopher.json"
schemaCatFile := "test/testdata/imposters/schemas/type_cat.json"
schemeFailFile := "test/testdata/imposters/schemas/type_gopher_fail.json"

requestWithoutSchema := Request{
Method: "POST",
Endpoint: "/login",
SchemaFile: nil,
}

requestWithSchema := Request{
Method: "POST",
Endpoint: "/login",
SchemaFile: &schemaGopherFile,
}

requestWithNonExistingSchema := Request{
Method: "POST",
Endpoint: "/login",
SchemaFile: &schemaCatFile,
}

requestWithWrongSchema := Request{
Method: "POST",
Endpoint: "/login",
SchemaFile: &schemeFailFile,
}

okResponse := Response{Status: http.StatusOK}

var matcherData = []struct {
name string
fn mux.MatcherFunc
req *http.Request
res bool
}{
{"imposter without request schema", MatcherBySchema(Imposter{Request: requestWithoutSchema, Response: okResponse}), &http.Request{Body: bodyA}, true},
{"correct request schema", MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), &http.Request{Body: bodyA}, true},
{"incorrect request schema", MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), &http.Request{Body: bodyB}, false},
{"non-existing schema file", MatcherBySchema(Imposter{Request: requestWithNonExistingSchema, Response: okResponse}), &http.Request{Body: bodyB}, false},
{"malformatted schema file", MatcherBySchema(Imposter{Request: requestWithWrongSchema, Response: okResponse}), &http.Request{Body: bodyB}, false},
}

for _, tt := range matcherData {
t.Run(tt.name, func(t *testing.T) {
res := tt.fn(tt.req, nil)
if res != tt.res {
t.Fatalf("error while matching by request schema - expected: %t, given: %t", tt.res, res)
}
})

}
}
10 changes: 9 additions & 1 deletion server.go
Expand Up @@ -40,6 +40,10 @@ func (s *Server) buildImposters() error {
files, _ := ioutil.ReadDir(s.impostersPath)

for _, f := range files {
if f.IsDir() {
continue
}

var imposter Imposter
if err := s.buildImposter(f.Name(), &imposter); err != nil {
return err
Expand All @@ -48,7 +52,9 @@ func (s *Server) buildImposters() error {
if imposter.Request.Endpoint == "" {
continue
}
s.router.HandleFunc(imposter.Request.Endpoint, ImposterHandler(imposter)).Methods(imposter.Request.Method)
s.router.HandleFunc(imposter.Request.Endpoint, ImposterHandler(imposter)).
Methods(imposter.Request.Method).
MatcherFunc(MatcherBySchema(imposter))
}

return nil
Expand All @@ -63,5 +69,7 @@ func (s *Server) buildImposter(imposterFileName string, imposter *Imposter) erro
if err := json.Unmarshal(bytes, imposter); err != nil {
return malformattedImposterError(fmt.Sprintf("error while unmarshall imposter file %s", f))
}
imposter.BasePath = s.impostersPath

return nil
}

0 comments on commit 36f066a

Please sign in to comment.