Skip to content

Commit 3d728b9

Browse files
authored
Merge pull request #18 from friendsofgo/config_file
Allow to run mock server via config file
2 parents c6b1387 + b2c09ad commit 3d728b9

File tree

12 files changed

+270
-30
lines changed

12 files changed

+270
-30
lines changed

CHANGELOG.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## v0.3.3 (2019/05/11)
4+
5+
* Improve default CORS options
6+
* Allow up mock server via config file
7+
* Allow configure CORS options
8+
* Access-Control-Request-Method
9+
* Access-Control-Request-Headers
10+
* Access-Control-Allow-Origin
11+
* Access-Control-Expose-Headers
12+
* Access-Control-Allow-Credentials
13+
* Improve route_mateches unit tests
14+
315
## v0.3.2 (2019/05/08)
416

517
* Fix CORS add AccessControl allowing methods and headers
@@ -37,4 +49,4 @@
3749
* Convert headers into canonical mime type
3850
* Run server with imposter configuration
3951
* Processing and parsing imposters file
40-
* Initial version
52+
* Initial version

README.md

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ Or you can download the binary for your arch on:
2929

3030
[https://github.com/friendsofgo/killgrave/releases](https://github.com/friendsofgo/killgrave/releases)
3131

32+
### Docker
33+
34+
The application is also available through [Docker](https://hub.docker.com/r/friendsofgo/killgrave), just run:
35+
36+
```bash
37+
docker run -it --rm -p 3000:3000 -v $PWD/:/home -w /home friendsofgo/killgrave
38+
```
39+
Remember to use the [-p](https://docs.docker.com/engine/reference/run/) flag to expose the container port where the application is listening (3000 by default).
40+
41+
NOTE: If you want to use `killgrave` through Docker at the same time you use your own dockerised HTTP-based API, be careful with networking issues.
42+
3243
## Using Killgrave
3344

3445
Use `killgrave` with default flags:
@@ -39,6 +50,8 @@ $ killgrave
3950
```
4051
Or custome your server with this flags:
4152
```sh
53+
-config string
54+
path with configuration file
4255
-host string
4356
if you run your server on a different host (default "localhost")
4457
-imposters string
@@ -49,6 +62,28 @@ Or custome your server with this flags:
4962
show the version of the application
5063
```
5164
65+
Use `killgrave` with config file:
66+
67+
First of all you need create a file with a valid config, i.e:
68+
69+
```yaml
70+
#config.yml
71+
72+
imposters_path: "imposters"
73+
port: 3000
74+
host: "localhost"
75+
cors:
76+
methods: ["GET"]
77+
headers: ["Content-Type"]
78+
exposed_headers: ["Cache-Control"]
79+
origins: ["*"]
80+
allow_credentials: true
81+
```
82+
83+
The parameter `cors` is optional and his options can be empty array, the other options `imposters_path`, `port`, `host` are mandatory.
84+
85+
If you want more information about the CORS options, visit the [CORS section](#CORS).
86+
5287
## How to use
5388
5489
### Create an imposter
@@ -178,17 +213,37 @@ curl --header "Content-Type: application/json" \
178213
http://localhost:3000/gophers
179214
```
180215
181-
### Docker
216+
## CORS
182217
183-
The application is also available through [Docker](https://hub.docker.com/r/friendsofgo/killgrave), just run:
218+
If you want to use `killgrave` on your client application you must consider to configure correctly all about CORS, thus we offer the possibility to configure as you need through a config file.
184219
185-
```bash
186-
docker run -it --rm -p 3000:3000 friendsofgo/killgrave
187-
```
220+
In the CORS section of the file you can find the next options:
188221
189-
Remember to use the [-p](https://docs.docker.com/engine/reference/run/) flag to expose the container port where the application is listening (3000 by default).
222+
- **methods** (string array)
223+
224+
Represent the **Access-Control-Request-Method header**, if you don't specify it or if you do leave it as any empty array, the default values will be:
190225
191-
NOTE: If you want to use `killgrave` through Docker at the same time you use your own dockerised HTTP-based API, be careful with networking issues.
226+
`"GET", "HEAD", "POST", "PUT", "OPTIONS", "DELETE", "PATCH", "TRACE", "CONNECT"`
227+
228+
- **headers** (string array)
229+
230+
Represent the **Access-Control-Request-Headers header**, if you don't specify it or if you do leave it as any empty array, the default values will be:
231+
232+
`"X-Requested-With", "Content-Type", "Authorization"`
233+
234+
- **exposed_headers** (string array)
235+
236+
Represent the **Access-Control-Expose-Headers header**, if you don't specify it or if you do leave it as any empty array, the default values will be:
237+
238+
`"Cache-Control", "Content-Language", "Content-Type", "Expires", "Last-Modified", "Pragma"`
239+
240+
- **origins** (string array)
241+
242+
Represent the **Access-Control-Allow-Origin header**, if you don't specify or leave as empty array this options has not default value
243+
244+
- **allow_credentials** (boolean)
245+
246+
Represent the **Access-Control-Allow-Credentials header** you must indicate if true or false
192247
193248
## Features
194249
* Imposters created in json
@@ -206,6 +261,8 @@ NOTE: If you want to use `killgrave` through Docker at the same time you use you
206261
* Dynamic responses based on query params
207262
* Allow organize your imposters with structured folders
208263
* Allow write multiple imposters by file
264+
* Run mock server with predefined configuration with config yaml file
265+
* Configure your CORS server options
209266
210267
## Next Features
211268
- [ ] Proxy server

cmd/killgrave/main.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,32 @@ func main() {
2121
port := flag.Int("port", 3000, "por to run the server")
2222
imposters := flag.String("imposters", "imposters", "directory where your imposters are saved")
2323
v := flag.Bool("version", false, "show the version of the application")
24+
c := flag.String("config", "", "path with configuration file")
25+
2426
flag.Parse()
2527

2628
if *v {
2729
fmt.Printf("%s version %s\n", name, version)
2830
return
2931
}
30-
32+
var config killgrave.Config
33+
if *c != "" {
34+
killgrave.ReadConfigFile(*c, &config)
35+
} else {
36+
config = killgrave.Config{
37+
ImpostersPath: *imposters,
38+
Port: *port,
39+
Host: *host,
40+
}
41+
}
3142
r := mux.NewRouter()
3243

33-
s := killgrave.NewServer(*imposters, r)
44+
s := killgrave.NewServer(config.ImpostersPath, r)
3445
if err := s.Build(); err != nil {
3546
log.Fatal(err)
3647
}
3748

38-
httpAddr := fmt.Sprintf("%s:%d", *host, *port)
39-
log.Printf("The fake server is on tap now: http://%s:%d\n", *host, *port)
40-
log.Fatal(http.ListenAndServe(httpAddr, handlers.CORS(s.AccessControl()...)(r)))
49+
httpAddr := fmt.Sprintf("%s:%d", config.Host, config.Port)
50+
log.Printf("The fake server is on tap now: http://%s:%d\n", config.Host, config.Port)
51+
log.Fatal(http.ListenAndServe(httpAddr, handlers.CORS(s.AccessControl(config.CORS)...)(r)))
4152
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ require (
1010
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
1111
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
1212
github.com/xeipuuv/gojsonschema v1.1.0
13+
gopkg.in/yaml.v2 v2.2.2
1314
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
1717
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
1818
github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg=
1919
github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
20+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
21+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
22+
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
23+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

internal/config.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package killgrave
2+
3+
import (
4+
"io/ioutil"
5+
"os"
6+
7+
"github.com/pkg/errors"
8+
"gopkg.in/yaml.v2"
9+
)
10+
11+
// Config representation of config file yaml
12+
type Config struct {
13+
ImpostersPath string `yaml:"imposters_path"`
14+
Port int `yaml:"port"`
15+
Host string `yaml:"host"`
16+
CORS ConfigCORS `yaml:"cors"`
17+
}
18+
19+
// ConfigCORS representation of section CORS of the yaml
20+
type ConfigCORS struct {
21+
Methods []string `yaml:"methods"`
22+
Headers []string `yaml:"headers"`
23+
Origins []string `yaml:"origins"`
24+
ExposedHeaders []string `yaml:"exposed_headers"`
25+
AllowCredentials bool `yaml:"allow_credentials"`
26+
}
27+
28+
// ReadConfigFile unmarshal content of config file to Config struct
29+
func ReadConfigFile(path string, config *Config) error {
30+
configFile, err := os.Open(path)
31+
if err != nil {
32+
return errors.Wrapf(err, "error trying to read config file: %s", path)
33+
}
34+
defer configFile.Close()
35+
36+
bytes, _ := ioutil.ReadAll(configFile)
37+
if err := yaml.Unmarshal(bytes, config); err != nil {
38+
return errors.Wrapf(err, "error while unmarshall configFile file %s", path)
39+
}
40+
return nil
41+
}

internal/config_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package killgrave
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/pkg/errors"
8+
)
9+
10+
func TestReadConfigFile(t *testing.T) {
11+
tests := map[string]struct {
12+
input string
13+
expected Config
14+
err error
15+
}{
16+
"valid config file": {"test/testdata/config.yml", validConfig(), nil},
17+
"file not found": {"test/testdata/file.yml", Config{}, errors.New("error")},
18+
"wrong yaml file": {"test/testdata/wrong_config.yml", Config{}, errors.New("error")},
19+
}
20+
21+
for name, tc := range tests {
22+
t.Run(name, func(t *testing.T) {
23+
var got Config
24+
err := ReadConfigFile(tc.input, &got)
25+
26+
if err != nil && tc.err == nil {
27+
t.Fatalf("not expected any erros and got %v", err)
28+
}
29+
30+
if err == nil && tc.err != nil {
31+
t.Fatalf("expected an error and got nil")
32+
}
33+
34+
if !reflect.DeepEqual(tc.expected, got) {
35+
t.Fatalf("expected: %v, got: %v", tc.expected, got)
36+
}
37+
38+
})
39+
}
40+
}
41+
42+
func validConfig() Config {
43+
return Config{
44+
ImpostersPath: "imposters",
45+
Port: 3000,
46+
Host: "localhost",
47+
CORS: ConfigCORS{
48+
Methods: []string{"GET"},
49+
Origins: []string{"*"},
50+
Headers: []string{"Content-Type"},
51+
ExposedHeaders: []string{"Cache-Control"},
52+
AllowCredentials: true,
53+
},
54+
}
55+
}

internal/route_matchers_test.go

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import (
77
"testing"
88

99
"github.com/gorilla/mux"
10+
"github.com/pkg/errors"
1011
)
1112

1213
func TestMatcherBySchema(t *testing.T) {
1314
bodyA := ioutil.NopCloser(bytes.NewReader([]byte("{\"type\": \"gopher\"}")))
1415
bodyB := ioutil.NopCloser(bytes.NewReader([]byte("{\"type\": \"cat\"}")))
1516
emptyBody := ioutil.NopCloser(bytes.NewReader([]byte("")))
17+
wrongBody := ioutil.NopCloser(errReader(0))
1618

1719
schemaGopherFile := "test/testdata/imposters/schemas/type_gopher.json"
1820
schemaCatFile := "test/testdata/imposters/schemas/type_cat.json"
@@ -46,22 +48,22 @@ func TestMatcherBySchema(t *testing.T) {
4648
httpRequestB := &http.Request{Body: bodyB}
4749
okResponse := Response{Status: http.StatusOK}
4850

49-
var matcherData = []struct {
50-
name string
51-
fn mux.MatcherFunc
52-
req *http.Request
53-
res bool
51+
var matcherData = map[string]struct {
52+
fn mux.MatcherFunc
53+
req *http.Request
54+
res bool
5455
}{
55-
{"correct request schema", MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), httpRequestA, true},
56-
{"imposter without request schema", MatcherBySchema(Imposter{Request: requestWithoutSchema, Response: okResponse}), httpRequestA, true},
57-
{"malformatted schema file", MatcherBySchema(Imposter{Request: requestWithWrongSchema, Response: okResponse}), httpRequestA, false},
58-
{"incorrect request schema", MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), httpRequestB, false},
59-
{"non-existing schema file", MatcherBySchema(Imposter{Request: requestWithNonExistingSchema, Response: okResponse}), httpRequestB, false},
60-
{"empty body with required schema file", MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), &http.Request{Body: emptyBody}, false},
56+
"correct request schema": {MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), httpRequestA, true},
57+
"imposter without request schema": {MatcherBySchema(Imposter{Request: requestWithoutSchema, Response: okResponse}), httpRequestA, true},
58+
"malformatted schema file": {MatcherBySchema(Imposter{Request: requestWithWrongSchema, Response: okResponse}), httpRequestA, false},
59+
"incorrect request schema": {MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), httpRequestB, false},
60+
"non-existing schema file": {MatcherBySchema(Imposter{Request: requestWithNonExistingSchema, Response: okResponse}), httpRequestB, false},
61+
"empty body with required schema file": {MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), &http.Request{Body: emptyBody}, false},
62+
"invalid request body": {MatcherBySchema(Imposter{Request: requestWithSchema, Response: okResponse}), &http.Request{Body: wrongBody}, false},
6163
}
6264

63-
for _, tt := range matcherData {
64-
t.Run(tt.name, func(t *testing.T) {
65+
for name, tt := range matcherData {
66+
t.Run(name, func(t *testing.T) {
6567
res := tt.fn(tt.req, nil)
6668
if res != tt.res {
6769
t.Fatalf("error while matching by request schema - expected: %t, given: %t", tt.res, res)
@@ -70,3 +72,9 @@ func TestMatcherBySchema(t *testing.T) {
7072

7173
}
7274
}
75+
76+
type errReader int
77+
78+
func (errReader) Read(p []byte) (n int, err error) {
79+
return 0, errors.New("test error")
80+
}

internal/server.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import (
1212
"github.com/pkg/errors"
1313
)
1414

15+
var (
16+
defaultCORSMethods = []string{"GET", "HEAD", "POST", "PUT", "OPTIONS", "DELETE", "PATCH", "TRACE", "CONNECT"}
17+
defaultCORSHeaders = []string{"X-Requested-With", "Content-Type", "Authorization"}
18+
defaultCORSExposedHeaders = []string{"Cache-Control", "Content-Language", "Content-Type", "Expires", "Last-Modified", "Pragma"}
19+
)
20+
1521
// Server definition of mock server
1622
type Server struct {
1723
impostersPath string
@@ -27,9 +33,31 @@ func NewServer(p string, r *mux.Router) *Server {
2733
}
2834

2935
// AccessControl Return options to initialize the mock server with default access control
30-
func (s *Server) AccessControl() (h []handlers.CORSOption) {
31-
h = append(h, handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS", "DELETE", "PATCH", "TRACE", "CONNECT"}))
32-
h = append(h, handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "*"}))
36+
func (s *Server) AccessControl(config ConfigCORS) (h []handlers.CORSOption) {
37+
h = append(h, handlers.AllowedMethods(defaultCORSMethods))
38+
h = append(h, handlers.AllowedHeaders(defaultCORSHeaders))
39+
h = append(h, handlers.ExposedHeaders(defaultCORSExposedHeaders))
40+
41+
if len(config.Methods) > 0 {
42+
h = append(h, handlers.AllowedMethods(config.Methods))
43+
}
44+
45+
if len(config.Origins) > 0 {
46+
h = append(h, handlers.AllowedOrigins(config.Origins))
47+
}
48+
49+
if len(config.Headers) > 0 {
50+
h = append(h, handlers.AllowedHeaders(config.Headers))
51+
}
52+
53+
if len(config.ExposedHeaders) > 0 {
54+
h = append(h, handlers.ExposedHeaders(config.ExposedHeaders))
55+
}
56+
57+
if config.AllowCredentials {
58+
h = append(h, handlers.AllowCredentials())
59+
}
60+
3361
return
3462
}
3563

0 commit comments

Comments
 (0)