Skip to content

Commit 36f066a

Browse files
authored
Merge pull request #12 from friendsofgo/matching_by_request_schema
Matching by request schema and calculate files directories
2 parents 3d30ee6 + bc924cd commit 36f066a

File tree

16 files changed

+204
-116
lines changed

16 files changed

+204
-116
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ imposters
1515
schemas
1616

1717
!test/testdata/imposters/
18-
!test/testdata/schemas/
18+
!test/testdata/imposters/schemas/

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,10 @@
1414
* Create an official docker image for the application
1515
* Update README.md with how to use the application with docker
1616
* Allow write headers for the response
17+
18+
## v0.2.1 (2019/04/25)
19+
20+
* Allow imposter's matching by request schema
21+
* Dynamic responses based on regex endpoint or request schema
22+
* Calculate files directory(body and schema) based on imposters path
23+
* Update REAMDE.md with resolved features and new future features

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
[![CircleCI](https://circleci.com/gh/friendsofgo/killgrave/tree/master.svg?style=svg)](https://circleci.com/gh/friendsofgo/killgrave/tree/master)
2+
[![Version](https://img.shields.io/github/release/friendsofgo/killgrave.svg?style=flat-square)](https://github.com/friendsofgo/killgrave/releases/latest)
23
[![codecov](https://codecov.io/gh/friendsofgo/killgrave/branch/master/graph/badge.svg)](https://codecov.io/gh/friendsofgo/killgrave)
34
[![Go Report Card](https://goreportcard.com/badge/github.com/friendsofgo/killgrave)](https://goreportcard.com/report/github.com/friendsofgo/killgrave)
45
[![GoDoc](https://godoc.org/graphql.co/graphql?status.svg)](https://godoc.org/github.com/friendsofgo/killgrave)
5-
[![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)
6+
[![FriendsOfGo](https://img.shields.io/badge/powered%20by-Friends%20of%20Go-73D7E2.svg)](https://friendsofgo.tech)
67

78
<p align="center">
89
<img src="https://res.cloudinary.com/fogo/image/upload/c_scale,w_350/v1555701634/fogo/projects/gopher-killgrave.png" alt="Golang Killgrave"/>
@@ -181,10 +182,14 @@ NOTE: If you want to use `killgrave` through Docker at the same time you use you
181182
* Write bodies in line
182183
* Regex for using on endpoint urls
183184
* Allow write headers on response
185+
* Allow imposter's matching by request schema
186+
* Dynamic responses based on regex endpoint or request schema
184187
185188
## Next Features
189+
- [ ] Dynamic responses based on headers
190+
- [ ] Dynamic responses based on query params
191+
- [ ] Allow write multiples imposters by file
186192
- [ ] Proxy server
187-
- [ ] Dynamic responses and error responses
188193
- [ ] Record proxy server
189194
- [ ] Better documentation with examples of each feature
190195

handler.go

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,74 +2,29 @@ package killgrave
22

33
import (
44
"fmt"
5-
"io"
65
"io/ioutil"
76
"log"
87
"net/http"
98
"net/textproto"
109
"os"
1110
"strings"
12-
13-
"github.com/pkg/errors"
14-
"github.com/xeipuuv/gojsonschema"
1511
)
1612

1713
// ImposterHandler create specific handler for the received imposter
1814
func ImposterHandler(imposter Imposter) http.HandlerFunc {
1915
return func(w http.ResponseWriter, r *http.Request) {
20-
if err := validateSchema(imposter, r.Body); err != nil {
21-
w.WriteHeader(http.StatusBadRequest)
22-
w.Write([]byte(err.Error()))
23-
return
24-
}
25-
2616
if err := validateHeaders(imposter, r.Header); err != nil {
2717
w.WriteHeader(http.StatusBadRequest)
2818
w.Write([]byte(err.Error()))
2919
return
3020
}
3121

32-
3322
writeHeaders(imposter, w)
3423
w.WriteHeader(imposter.Response.Status)
3524
writeBody(imposter, w)
3625
}
3726
}
3827

39-
func validateSchema(imposter Imposter, bodyRequest io.ReadCloser) error {
40-
if imposter.Request.SchemaFile == nil {
41-
return nil
42-
}
43-
44-
schemaFile := *imposter.Request.SchemaFile
45-
if _, err := os.Stat(schemaFile); os.IsNotExist(err) {
46-
return errors.Wrapf(err, "the schema file %s not found", schemaFile)
47-
}
48-
49-
b, err := ioutil.ReadAll(bodyRequest)
50-
if err != nil {
51-
return errors.Wrapf(err, "impossible read the request body")
52-
}
53-
54-
dir, _ := os.Getwd()
55-
schemaFilePath := "file://" + dir + "/" + schemaFile
56-
schema := gojsonschema.NewReferenceLoader(schemaFilePath)
57-
document := gojsonschema.NewStringLoader(string(b))
58-
59-
res, err := gojsonschema.Validate(schema, document)
60-
if err != nil {
61-
return errors.Wrap(err, "error validating the json schema")
62-
}
63-
64-
if !res.Valid() {
65-
for _, desc := range res.Errors() {
66-
return errors.New(desc.String())
67-
}
68-
}
69-
70-
return nil
71-
}
72-
7328
func validateHeaders(imposter Imposter, header http.Header) error {
7429
if imposter.Request.Headers == nil {
7530
return nil
@@ -117,7 +72,8 @@ func writeBody(imposter Imposter, w http.ResponseWriter) {
11772
wb := []byte(imposter.Response.Body)
11873

11974
if imposter.Response.BodyFile != nil {
120-
wb = fetchBodyFromFile(*imposter.Response.BodyFile)
75+
bodyFile := imposter.CalculateFilePath(*imposter.Response.BodyFile)
76+
wb = fetchBodyFromFile(bodyFile)
12177
}
12278
w.Write(wb)
12379
}

handler_test.go

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ func TestImposterHandler(t *testing.T) {
2323
var headers = make(http.Header)
2424
headers.Add("Content-Type", "application/json")
2525

26-
schemaFile := "test/testdata/schemas/create_gopher_request.json"
27-
bodyFile := "test/testdata/responses/create_gopher_response.json"
28-
bodyFileFake := "test/testdata/responses/create_gopher_response_fail.json"
26+
schemaFile := "test/testdata/imposters/schemas/create_gopher_request.json"
27+
bodyFile := "test/testdata/imposters/responses/create_gopher_response.json"
28+
bodyFileFake := "test/testdata/imposters/responses/create_gopher_response_fail.json"
2929
body := `{"test":true}`
3030

3131
validRequest := Request{
@@ -75,15 +75,6 @@ func TestImposterHandler(t *testing.T) {
7575
}
7676

7777
func TestInvalidRequestWithSchema(t *testing.T) {
78-
wrongRequest := []byte(`{
79-
"data": {
80-
"type": "gophers",
81-
"attributes": {
82-
"name": "Zebediah",
83-
"color": "Purple"
84-
}
85-
}
86-
}`)
8778
validRequest := []byte(`{
8879
"data": {
8980
"type": "gophers",
@@ -93,23 +84,18 @@ func TestInvalidRequestWithSchema(t *testing.T) {
9384
}
9485
}
9586
}`)
96-
notExistFile := "failSchema"
97-
wrongSchema := "test/testdata/schemas/create_gopher_request_fail.json"
98-
validSchema := "test/testdata/schemas/create_gopher_request.json"
9987

10088
var dataTest = []struct {
10189
name string
10290
imposter Imposter
10391
statusCode int
10492
request []byte
10593
}{
106-
{"schema file not found", Imposter{Request: Request{Method: "POST", Endpoint: "/gophers", SchemaFile: &notExistFile}}, http.StatusBadRequest, validRequest},
107-
{"wrong schema", Imposter{Request: Request{Method: "POST", Endpoint: "/gophers", SchemaFile: &wrongSchema}}, http.StatusBadRequest, validRequest},
108-
{"request invalid", Imposter{Request: Request{Method: "POST", Endpoint: "/gophers", SchemaFile: &validSchema}}, http.StatusBadRequest, wrongRequest},
10994
{"valid request no schema", Imposter{Request: Request{Method: "POST", Endpoint: "/gophers"}, Response: Response{Status: http.StatusOK, Body: "test ok"}}, http.StatusOK, validRequest},
11095
}
11196

11297
for _, tt := range dataTest {
98+
11399
t.Run(tt.name, func(t *testing.T) {
114100
req, err := http.NewRequest("POST", "/gophers", bytes.NewBuffer(tt.request))
115101
if err != nil {
@@ -137,7 +123,7 @@ func TestInvalidHeaders(t *testing.T) {
137123
}
138124
}
139125
}`)
140-
schemaFile := "test/testdata/schemas/create_gopher_request.json"
126+
schemaFile := "test/testdata/imposters/schemas/create_gopher_request.json"
141127
var expectedHeaders = make(http.Header)
142128
expectedHeaders.Add("Content-Type", "application/json")
143129
expectedHeaders.Add("Authorization", "Bearer gopher")

imposter.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
package killgrave
22

3-
import "net/http"
3+
import (
4+
"net/http"
5+
"path"
6+
)
47

58
// Imposter define an imposter structure
69
type Imposter struct {
10+
BasePath string
711
Request Request `json:"request"`
812
Response Response `json:"response"`
913
}
1014

15+
// CalculateFilePath calculate file path based on basePath of imposter directory
16+
func (i *Imposter) CalculateFilePath(filePath string) string {
17+
return path.Join(i.BasePath, filePath)
18+
}
19+
1120
// Request represent the structure of real request
1221
type Request struct {
1322
Method string `json:"method"`
@@ -18,8 +27,8 @@ type Request struct {
1827

1928
// Response represent the structure of real response
2029
type Response struct {
21-
Status int `json:"status"`
22-
Body string `json:"body"`
23-
BodyFile *string `json:"bodyFile"`
24-
Headers *http.Header `json:"headers"`
30+
Status int `json:"status"`
31+
Body string `json:"body"`
32+
BodyFile *string `json:"bodyFile"`
33+
Headers *http.Header `json:"headers"`
2534
}

route_matchers.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package killgrave
2+
3+
import (
4+
"bytes"
5+
"github.com/gorilla/mux"
6+
"github.com/pkg/errors"
7+
"github.com/xeipuuv/gojsonschema"
8+
"io/ioutil"
9+
"log"
10+
"net/http"
11+
"os"
12+
)
13+
14+
// MatcherBySchema check if the request matching with the schema file
15+
func MatcherBySchema(imposter Imposter) mux.MatcherFunc {
16+
return func(req *http.Request, rm *mux.RouteMatch) bool {
17+
err := validateSchema(imposter, req)
18+
19+
// TODO: inject the logger
20+
if err != nil {
21+
log.Println(err)
22+
return false
23+
}
24+
return true
25+
}
26+
}
27+
28+
func validateSchema(imposter Imposter, req *http.Request) error {
29+
if imposter.Request.SchemaFile == nil {
30+
return nil
31+
}
32+
33+
var b []byte
34+
35+
defer func() {
36+
req.Body.Close()
37+
req.Body = ioutil.NopCloser(bytes.NewBuffer(b))
38+
}()
39+
40+
schemaFile := imposter.CalculateFilePath(*imposter.Request.SchemaFile)
41+
if _, err := os.Stat(schemaFile); os.IsNotExist(err) {
42+
return errors.Wrapf(err, "the schema file %s not found", schemaFile)
43+
}
44+
45+
b, err := ioutil.ReadAll(req.Body)
46+
if err != nil {
47+
return errors.Wrapf(err, "impossible read the request body")
48+
}
49+
50+
dir, _ := os.Getwd()
51+
schemaFilePath := "file://" + dir + "/" + schemaFile
52+
schema := gojsonschema.NewReferenceLoader(schemaFilePath)
53+
document := gojsonschema.NewStringLoader(string(b))
54+
55+
res, err := gojsonschema.Validate(schema, document)
56+
if err != nil {
57+
return errors.Wrap(err, "error validating the json schema")
58+
}
59+
60+
if !res.Valid() {
61+
for _, desc := range res.Errors() {
62+
return errors.New(desc.String())
63+
}
64+
}
65+
66+
return nil
67+
}

route_matchers_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package killgrave
2+
3+
import (
4+
"bytes"
5+
"github.com/gorilla/mux"
6+
"io/ioutil"
7+
"net/http"
8+
"testing"
9+
)
10+
11+
func TestMatcherBySchema(t *testing.T) {
12+
bodyA := ioutil.NopCloser(bytes.NewReader([]byte("{\"type\": \"gopher\"}")))
13+
bodyB := ioutil.NopCloser(bytes.NewReader([]byte("{\"type\": \"cat\"}")))
14+
15+
schemaGopherFile := "test/testdata/imposters/schemas/type_gopher.json"
16+
schemaCatFile := "test/testdata/imposters/schemas/type_cat.json"
17+
schemeFailFile := "test/testdata/imposters/schemas/type_gopher_fail.json"
18+
19+
requestWithoutSchema := Request{
20+
Method: "POST",
21+
Endpoint: "/login",
22+
SchemaFile: nil,
23+
}
24+
25+
requestWithSchema := Request{
26+
Method: "POST",
27+
Endpoint: "/login",
28+
SchemaFile: &schemaGopherFile,
29+
}
30+
31+
requestWithNonExistingSchema := Request{
32+
Method: "POST",
33+
Endpoint: "/login",
34+
SchemaFile: &schemaCatFile,
35+
}
36+
37+
requestWithWrongSchema := Request{
38+
Method: "POST",
39+
Endpoint: "/login",
40+
SchemaFile: &schemeFailFile,
41+
}
42+
43+
okResponse := Response{Status: http.StatusOK}
44+
45+
var matcherData = []struct {
46+
name string
47+
fn mux.MatcherFunc
48+
req *http.Request
49+
res bool
50+
}{
51+
{"imposter without request schema", MatcherBySchema(Imposter{Request: requestWithoutSchema, Response: okResponse}), &http.Request{Body: bodyA}, true},
52+
{"correct request schema", MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), &http.Request{Body: bodyA}, true},
53+
{"incorrect request schema", MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), &http.Request{Body: bodyB}, false},
54+
{"non-existing schema file", MatcherBySchema(Imposter{Request: requestWithNonExistingSchema, Response: okResponse}), &http.Request{Body: bodyB}, false},
55+
{"malformatted schema file", MatcherBySchema(Imposter{Request: requestWithWrongSchema, Response: okResponse}), &http.Request{Body: bodyB}, false},
56+
}
57+
58+
for _, tt := range matcherData {
59+
t.Run(tt.name, func(t *testing.T) {
60+
res := tt.fn(tt.req, nil)
61+
if res != tt.res {
62+
t.Fatalf("error while matching by request schema - expected: %t, given: %t", tt.res, res)
63+
}
64+
})
65+
66+
}
67+
}

server.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ func (s *Server) buildImposters() error {
4040
files, _ := ioutil.ReadDir(s.impostersPath)
4141

4242
for _, f := range files {
43+
if f.IsDir() {
44+
continue
45+
}
46+
4347
var imposter Imposter
4448
if err := s.buildImposter(f.Name(), &imposter); err != nil {
4549
return err
@@ -48,7 +52,9 @@ func (s *Server) buildImposters() error {
4852
if imposter.Request.Endpoint == "" {
4953
continue
5054
}
51-
s.router.HandleFunc(imposter.Request.Endpoint, ImposterHandler(imposter)).Methods(imposter.Request.Method)
55+
s.router.HandleFunc(imposter.Request.Endpoint, ImposterHandler(imposter)).
56+
Methods(imposter.Request.Method).
57+
MatcherFunc(MatcherBySchema(imposter))
5258
}
5359

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

0 commit comments

Comments
 (0)