├── .env
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app
└── cmd
│ ├── mockify.go
│ ├── mockify_table_test.go
│ └── mockify_test.go
├── config
├── README.md
├── routes.json
└── routes.yaml
├── docker-compose.yml
└── postman
├── mockify.postman_collection.json
└── mockify.postman_environment.json
/.env:
--------------------------------------------------------------------------------
1 | MOCKIFY_PORT=8001
2 | MOCKIFY_ROUTES=/app/routes.json
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.dll
4 | *.so
5 | *.dylib
6 |
7 | # Test binary, build with `go test -c`
8 | *.test
9 |
10 | # Output of the go coverage tool, specifically when used with LiteIDE
11 | *.out
12 |
13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
14 | .glide/
15 |
16 | .idea
17 | /main
18 | *.iml
19 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1-alpine as builder
2 |
3 | RUN apk update && \
4 | apk add git && \
5 | rm -rf /var/cache/apk/*
6 |
7 | RUN go get github.com/gorilla/mux \
8 | github.com/json-iterator/go \
9 | gopkg.in/yaml.v2
10 |
11 | WORKDIR /go/src/mockify/
12 |
13 | COPY . /go/src/mockify/
14 |
15 | RUN CGO_ENABLED=0 go build -v -o mockify ./app/cmd/mockify.go
16 |
17 | FROM alpine:latest
18 |
19 | EXPOSE 8001
20 |
21 | RUN apk update && \
22 | apk add ca-certificates && \
23 | rm -rf /var/cache/apk/*
24 |
25 | WORKDIR /root/
26 |
27 | COPY config/* config/
28 | COPY --from=builder /go/src/mockify/mockify .
29 |
30 | CMD ./mockify
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Brian Moran
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Looking for contributors to help make mockify better
2 |
3 | # mockify
4 | Simple API mocking. No more waiting on backend teams to deliver services. Simply map the API call with a response and continue building great software.
5 |
6 | **Update**
7 | * Added `/list` endpoint (GET) to describe the current state of the `ResponseMapping`
8 | * Added `/add` endpoint (POST)
9 | * Added `/delete` endpoint (POST) to delete an existing mock by key (`/helloworld/foo|GET`)
10 | * Added postman collection & environment
11 | * Added functionality for `requestHeader`
12 |
13 | ## tl;dr
14 |
15 | ```
16 | docker run -it -p 0.0.0.0:8001:8001 -v ~/Desktop/routes-cart.json:/app/routes.json -e MOCKIFY_PORT=8001 brianmoran/mockify
17 | curl localhost:8001/list
18 | ```
19 |
20 | ## Getting Started
21 | These instructions will help you get started mocking your APIs with Docker.
22 |
23 | 1. Create a mapping file (JSON or YAML) anywhere you like. Mockify will check for the environment variable `MOCKIFY_ROUTES`. If the environment variable does not exist, Mockify will default to `./config/routes.yaml`
24 | * See [The Configuration Readme](https://github.com/brianmoran/mockify/tree/master/config) for more information and examples
25 | 1. _(Optional)_ Set the following environment variables `MOCKIFY_PORT` and `MOCKIFY_ROUTES`
26 | 1. Build the app inside a docker container by running `docker-compose up`. The docker container uses only 7MB of memory!
27 | 1. Start the docker container using a specific port and you can override routes.json as well
28 | ```
29 | MOCKIFY_PORT=8002 docker-compose up # set a specific port
30 | MOCKIFY_ROUTES=/app/routes-other.json docker-compose up # set a different routes file within the mockify folder as docker will copy it
31 | ```
32 | or non-dockerized
33 | ```
34 | go get github.com/gorilla/mux
35 | go get github.com/json-iterator/go
36 | go get gopkg.in/yaml.v2
37 | go build -o main ./app/cmd/mockify.go
38 | export MOCKIFY_PORT=8001
39 | export MOCKIFY_ROUTES=~/Desktop/routes.json
40 | ./main
41 | ```
42 |
43 | ### Examples
44 |
45 | Use Postman, cURL, or your own microservice to connect to the mock API
46 | ```
47 | curl http://localhost:8001/helloworld/bar
48 | {"foo":{"key1":1,"key2":true,"key3":[{"bar":true,"baz":[1,2,"3"],"foo":"foo"}]}}
49 | ```
50 |
51 | You can even mock errors
52 | ```
53 | curl http://localhost:8001/helloworld/nonexisting
54 | {"message": "Something bad happened but you knew that right?"}
55 | ```
56 |
57 | ---
58 |
59 | Here is a postman collection that includes all internal calls as well as the tests that you see in the example configuration: [https://www.getpostman.com/collections/2daab06a399baa2c8576](https://www.getpostman.com/collections/2daab06a399baa2c8576)
60 |
--------------------------------------------------------------------------------
/app/cmd/mockify.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "net/http"
9 | "os"
10 | "regexp"
11 | "strings"
12 |
13 | "gopkg.in/yaml.v2"
14 |
15 | "github.com/gorilla/mux"
16 |
17 | jsoniter "github.com/json-iterator/go"
18 | )
19 |
20 | type Route struct {
21 | Route string `yaml:"route" json:"route"`
22 | Methods []string
23 | Responses []Response `yaml:"responses" json:"responses"`
24 | }
25 |
26 | type Response struct {
27 | URI string `yaml:"uri" json:"uri"`
28 | Method string `yaml:"method" json:"method"`
29 | RequestHeader string `yaml:"requestHeader" json:"requestHeader"`
30 | RequestBody string `yaml:"requestBody" json:"requestBody"`
31 | StatusCode int `yaml:"statusCode" json:"statusCode"`
32 | Headers map[string]string `yaml:"headers" json:"headers"`
33 | Body interface{} `yaml:"body" json:"body"`
34 | }
35 |
36 | // For more how the different response mappings are used, read the README-file in the config folder
37 | var (
38 | RequestBodyResponseMappings = make(map[string]Response)
39 | RequestHeaderResponseMappings = make(map[string]Response)
40 | LowestPriorityResponseMappings = make(map[string]Response)
41 | Router = mux.NewRouter()
42 | )
43 |
44 | const (
45 | invalidHeaderChars = "[:\\s]*"
46 | )
47 |
48 | func printError(format string, v ...interface{}) {
49 | log.SetPrefix("[ERROR] ")
50 | log.Printf(format, v...)
51 | log.SetPrefix("")
52 | }
53 |
54 | func printRegisteredResponse(key string, response Response) {
55 | log.SetPrefix("[RESPONSE] ")
56 | log.Printf("[%s] -> %+v", key, response)
57 | log.SetPrefix("")
58 | }
59 |
60 | func loadRoutes(f string) []Route {
61 | log.Printf("Looking for routes in file: %s", f)
62 |
63 | routes := make([]Route, 0)
64 |
65 | yamlFile, err := ioutil.ReadFile(f)
66 | if err != nil {
67 | log.Printf("yamlFile.Get err #%v ", err)
68 | }
69 | err = yaml.Unmarshal(yamlFile, &routes)
70 | if err != nil {
71 | log.Fatalf("Unmarshal: %v", err)
72 | }
73 |
74 | return routes
75 | }
76 |
77 | func (route *Route) createResponses() {
78 | headerRegEx := regexp.MustCompile(invalidHeaderChars)
79 |
80 | for _, response := range route.Responses {
81 | upperMethod := strings.ToUpper(response.Method)
82 | upperURI := strings.ToUpper(response.URI)
83 |
84 | addToLowestPriority := true
85 |
86 | if response.RequestBody != "" {
87 | upperBody := strings.ToUpper(response.RequestBody)
88 | key := fmt.Sprintf("%s|%s|%s|BODY", upperURI, upperMethod, upperBody)
89 | printRegisteredResponse(key, response)
90 | RequestBodyResponseMappings[key] = response
91 | addToLowestPriority = false
92 | }
93 |
94 | // Add to second priority if we have a requestHeader
95 | // A route can have both a requestHeader and a requestBody, and these will be added to both mapping slices
96 | if response.RequestHeader != "" {
97 | upperHeader := strings.ToUpper(response.RequestHeader)
98 | upperHeader = strings.TrimSpace(upperHeader)
99 | upperHeader = headerRegEx.ReplaceAllString(upperHeader, "")
100 | key := fmt.Sprintf("%s|%s|%s|HEADER", upperURI, upperMethod, upperHeader)
101 | printRegisteredResponse(key, response)
102 | RequestHeaderResponseMappings[key] = response
103 | addToLowestPriority = false
104 | }
105 |
106 | if addToLowestPriority {
107 | key := fmt.Sprintf("%s|%s|LOWEST", upperURI, upperMethod)
108 | printRegisteredResponse(key, response)
109 | LowestPriorityResponseMappings[key] = response
110 | }
111 | }
112 | }
113 |
114 | func getResponse(method, uri, body string, requestHeaders http.Header) *Response {
115 | // Check if we have a match in the highest priority splice
116 | for _, response := range RequestBodyResponseMappings {
117 | if uri == response.URI && method == response.Method && strings.Contains(body, response.RequestBody) {
118 | return &response
119 | }
120 | }
121 |
122 | // Check if we have a match in the second highest priority splice
123 | for _, response := range RequestHeaderResponseMappings {
124 | if uri == response.URI && method == response.Method && response.RequestHeader != "" {
125 | suppliedMatchHeader := strings.Split(response.RequestHeader, ":")
126 | if len(suppliedMatchHeader) > 2 {
127 | log.Fatalf(`Wrongfully use of requestHeader! Should only be "key: value", you had: '%v'!`, suppliedMatchHeader)
128 | }
129 | suppliedMatchHeader[0] = strings.TrimSpace(suppliedMatchHeader[0])
130 | suppliedMatchHeader[1] = strings.TrimSpace(suppliedMatchHeader[1])
131 | found := requestHeaders.Get(suppliedMatchHeader[0])
132 | if found != "" && found == suppliedMatchHeader[1] {
133 | return &response
134 | }
135 | }
136 | }
137 |
138 | // Check if we have a match in the lowest priority splice
139 | for _, response := range LowestPriorityResponseMappings {
140 | if uri == response.URI && method == response.Method {
141 | return &response
142 | }
143 | }
144 |
145 | return nil
146 | }
147 |
148 | func (route *Route) routeHandler(w http.ResponseWriter, r *http.Request) {
149 | bodyBytes, err := ioutil.ReadAll(r.Body)
150 | if err != nil {
151 | bodyBytes = []byte("")
152 | }
153 | log.Printf("REQUEST: %+v %+v [%+v] [%+v]", r.Method, r.RequestURI, string(bodyBytes), r.Header)
154 |
155 | response := getResponse(r.Method, r.RequestURI, string(bodyBytes), r.Header)
156 | if response == nil {
157 | printError("Response not mapped for method %s and URI %s", r.Method, r.RequestURI)
158 | w.WriteHeader(http.StatusNotFound)
159 | _, _ = fmt.Fprintf(w, "404 Response not mapped for method %s and URI %s", r.Method, r.RequestURI)
160 | return
161 | }
162 |
163 | log.Printf("RESPONSE: %+v", response)
164 |
165 | //write headers
166 | for k, v := range response.Headers {
167 | w.Header().Add(k, v)
168 | }
169 |
170 | w.WriteHeader(response.StatusCode)
171 | isJson := false
172 | _, ok := response.Headers["Content-Type"]
173 | if ok && response.Headers["Content-Type"] == "application/json" {
174 | isJson = true
175 | }
176 | if !isJson {
177 | _, _ = w.Write([]byte(response.Body.(string)))
178 | } else {
179 | var jsonx = jsoniter.ConfigCompatibleWithStandardLibrary
180 | jsonB, err := jsonx.Marshal(response.Body)
181 | if err != nil {
182 | printError("Response could not be converted to JSON")
183 | w.WriteHeader(http.StatusInternalServerError)
184 | _, _ = fmt.Fprint(w, "500 Response could not be converted to JSON")
185 | return
186 | }
187 | _, _ = w.Write(jsonB)
188 | }
189 | }
190 |
191 | func listHandler(w http.ResponseWriter, _ *http.Request) {
192 | var jsonx = jsoniter.ConfigCompatibleWithStandardLibrary
193 |
194 | w.Header().Add("Content-Type", "application/json")
195 |
196 | jsonB, err := jsonx.Marshal(RequestBodyResponseMappings)
197 | if err != nil {
198 | fmt.Println("Error", err.Error())
199 | w.WriteHeader(500)
200 | printError("unable to list response mapping")
201 | _, _ = w.Write([]byte("unable to list response mapping"))
202 | }
203 | if _, err := w.Write(jsonB); err != nil {
204 | w.WriteHeader(500)
205 | printError("unable to list response mapping")
206 | _, _ = w.Write([]byte("unable to list response mapping"))
207 | }
208 | }
209 |
210 | func addHandler(w http.ResponseWriter, r *http.Request) {
211 | var errString string
212 | body, err := ioutil.ReadAll(r.Body)
213 | if err != nil {
214 | errString = "unable to parse request body"
215 | printError(errString)
216 | w.WriteHeader(500)
217 | _, _ = w.Write([]byte(errString))
218 | }
219 |
220 | route := Route{}
221 | err = json.Unmarshal(body, &route)
222 | if err != nil {
223 | errString = "unable to unmarshal body"
224 | printError(errString)
225 | w.WriteHeader(500)
226 | _, _ = w.Write([]byte(errString))
227 | }
228 |
229 | route.createResponses()
230 | Router.HandleFunc(route.Route, route.routeHandler).Methods(route.Methods...)
231 |
232 | }
233 |
234 | func deleteHandler(w http.ResponseWriter, r *http.Request) {
235 | var errString string
236 | body, err := ioutil.ReadAll(r.Body)
237 | if err != nil {
238 | errString = "unable to parse request body"
239 | printError(errString)
240 | w.WriteHeader(500)
241 | _, _ = w.Write([]byte(errString))
242 | }
243 |
244 | if body == nil {
245 | errString = "body is empty"
246 | printError(errString)
247 | w.WriteHeader(500)
248 | _, _ = w.Write([]byte(errString))
249 | }
250 | key := string(body)
251 |
252 | _, ok := RequestBodyResponseMappings[key]
253 | if !ok {
254 | log.Printf("key: %s doesn't exist", key)
255 | w.WriteHeader(200)
256 | _, _ = w.Write([]byte("nothing to delete"))
257 | return
258 | }
259 |
260 | delete(RequestBodyResponseMappings, key)
261 | w.WriteHeader(200)
262 | _, _ = w.Write([]byte("mock deleted"))
263 | }
264 |
265 | // NewMockify sets up a new instance/http server
266 | func NewMockify() {
267 | port, ok := os.LookupEnv("MOCKIFY_PORT")
268 | if !ok {
269 | port = "8001"
270 | printError("MOCKIFY_PORT not set; using default [%s]!", port)
271 | }
272 | var routes []Route
273 | routesFile, ok := os.LookupEnv("MOCKIFY_ROUTES")
274 | if !ok {
275 | log.Print("MOCKIFY_ROUTES not set.")
276 | os.Exit(1)
277 | } else {
278 | routes = loadRoutes(routesFile)
279 | }
280 |
281 | setupMockifyRouter(routes)
282 |
283 | log.Printf("Ready on port %s!", port)
284 | if err := http.ListenAndServe("0.0.0.0:"+port, Router); err != nil {
285 | log.Fatal(err)
286 | }
287 | os.Exit(6)
288 | }
289 |
290 | func setupMockifyRouter(routes []Route) {
291 | //add builtin routes
292 | Router.HandleFunc("/list", listHandler).Methods(http.MethodGet)
293 | Router.HandleFunc("/add", addHandler).Methods(http.MethodPost)
294 | Router.HandleFunc("/delete", deleteHandler).Methods(http.MethodPost)
295 |
296 | for _, route := range routes {
297 | route.createResponses()
298 | Router.HandleFunc(route.Route, route.routeHandler).Methods(route.Methods...)
299 | }
300 | }
301 |
302 | func main() {
303 | path, exist := os.LookupEnv("MOCKIFY_ROUTES")
304 | if exist {
305 | log.Printf(fmt.Sprintf("MOCKIFY_ROUTES set. [%s]", path))
306 | } else {
307 | log.Printf(fmt.Sprintf("MOCKIFY_ROUTES not set. Default ./config/routes.yaml"))
308 | _ = os.Setenv("MOCKIFY_ROUTES", "./config/routes.yaml")
309 | }
310 | NewMockify()
311 | }
312 |
--------------------------------------------------------------------------------
/app/cmd/mockify_table_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "net/http"
6 | "net/http/httptest"
7 | "testing"
8 | )
9 |
10 | type mockifyTestStructure struct {
11 | name string
12 | description string
13 | configFileName string
14 | requestBody *bytes.Buffer
15 | requestMethod string
16 | requestPath string
17 | expectedStatusCode int
18 | expectedResponseBody string
19 | expectedContentType string
20 | setup func(*http.Request, *httptest.ResponseRecorder)
21 | }
22 |
23 | type Tests struct {
24 | Tests []mockifyTestStructure
25 | T *testing.T
26 | }
27 |
28 | func TestMockify(t *testing.T) {
29 | tests := []mockifyTestStructure{
30 | {
31 | name: "RequestBody_shouldChooseCorrectRoute",
32 | description: "Verify that the correct route where the 'def' requestBody has been specified is chosen",
33 | requestBody: bytes.NewBufferString(`{"type":"def"}`),
34 | requestMethod: "POST",
35 | requestPath: "/api/mcp",
36 | expectedStatusCode: http.StatusCreated,
37 | expectedContentType: "application/json",
38 | expectedResponseBody: `{"foo":{"key1":1,"key2":true,"key3":[{"bar":true,"baz":[1,2,"3"],"foo":"foo"}]}}`,
39 | },
40 | {
41 | name: "RequestBody_requestBodyShouldHaveHigherPriorityThanRequestHeader",
42 | description: "Verify that the correct route where the 'def' requestBody has been specified is chosen, even when a requestHeader is supplied",
43 | requestBody: bytes.NewBufferString(`{"type":"def"}`),
44 | requestMethod: "POST",
45 | requestPath: "/api/mcp",
46 | expectedStatusCode: http.StatusCreated,
47 | expectedContentType: "application/json",
48 | expectedResponseBody: `{"foo":{"key1":1,"key2":true,"key3":[{"bar":true,"baz":[1,2,"3"],"foo":"foo"}]}}`,
49 | setup: func(request *http.Request, recorder *httptest.ResponseRecorder) {
50 | request.Header.Set("Authorization", "foo-bar")
51 | },
52 | },
53 | {
54 | name: "RequestBody_shouldWorkWithBothBodyAndHeader",
55 | description: "Verify that a route with both requestBody and requestHeader works",
56 | requestBody: bytes.NewBufferString(`{"type":"body-have-higher-priority-over-header"}`),
57 | requestMethod: "POST",
58 | requestPath: "/api/mcp",
59 | expectedStatusCode: http.StatusFound,
60 | expectedContentType: "application/json",
61 | expectedResponseBody: `{"win":{"key1":1,"key2":true}}`,
62 | setup: func(request *http.Request, recorder *httptest.ResponseRecorder) {
63 | request.Header.Set("Authorization", "foo-bar")
64 | },
65 | },
66 | {
67 | name: "RequestHeader_shouldChooseCorrectRoute",
68 | description: "Verify that the correct route where the 'Authorization: foo-bar' requestHeader has been specified is chosen",
69 | requestMethod: "POST",
70 | requestPath: "/api/mcp",
71 | expectedStatusCode: http.StatusFound,
72 | expectedContentType: "application/json",
73 | expectedResponseBody: `{"win":{"key1":1,"key2":true}}`,
74 | setup: func(request *http.Request, recorder *httptest.ResponseRecorder) {
75 | request.Header.Set("Authorization", "foo-bar")
76 | request.Header.Set("Foo", "Bar")
77 | },
78 | },
79 | }
80 |
81 | tt := Tests{
82 | Tests: tests,
83 | T: t,
84 | }
85 | tt.runTests()
86 | }
87 |
88 | func TestMockifyDifferentConfigFiles(t *testing.T) {
89 | tests := []mockifyTestStructure{
90 | {
91 | name: "DefaultConfigFileInYAML",
92 | description: "Test with the default YAML config file",
93 | requestMethod: "GET",
94 | requestPath: "/helloworld/foo",
95 | expectedStatusCode: 200,
96 | expectedResponseBody: `{"message":"Welcome to Mockify!"}`,
97 | expectedContentType: "application/json",
98 | },
99 | {
100 | name: "ConfigFileInJSON",
101 | description: "Test with a configuration file in JSON instead of default YAML",
102 | configFileName: "../../config/routes.json",
103 | requestMethod: "GET",
104 | requestPath: "/helloworld/foo",
105 | expectedStatusCode: 200,
106 | expectedResponseBody: `{"message":"Welcome to Mockify!"}`,
107 | expectedContentType: "application/json",
108 | },
109 | }
110 |
111 | tt := Tests{
112 | Tests: tests,
113 | T: t,
114 | }
115 | tt.runTests()
116 | }
117 |
118 | func (impl Tests) runTests() {
119 | for _, test := range impl.Tests {
120 | impl.T.Run(test.name, func(t *testing.T) {
121 | t.Log(test.description)
122 |
123 | var config []Route
124 | if test.configFileName == "" {
125 | config = loadRoutes("../../config/routes.yaml")
126 | } else {
127 | config = loadRoutes(test.configFileName)
128 | }
129 | setupMockifyRouter(config)
130 |
131 | req := httptest.NewRequest(test.requestMethod, test.requestPath, nil)
132 | if test.requestBody != nil {
133 | req = httptest.NewRequest(test.requestMethod, test.requestPath, test.requestBody)
134 | }
135 | rec := httptest.NewRecorder()
136 |
137 | if test.setup != nil {
138 | test.setup(req, rec)
139 | }
140 |
141 | Router.ServeHTTP(rec, req)
142 |
143 | gotBody := rec.Body.String()
144 | if gotBody != test.expectedResponseBody {
145 | t.Errorf(`expected body "%s"; got "%s""`, test.expectedResponseBody, gotBody)
146 | t.Fail()
147 | }
148 |
149 | gotStatusCode := rec.Result().StatusCode
150 | if gotStatusCode != test.expectedStatusCode {
151 | t.Errorf(`expected status code "%d"; got "%d"`, test.expectedStatusCode, gotStatusCode)
152 | t.Fail()
153 | }
154 |
155 | gotContentType := rec.Header().Get("Content-Type")
156 | if gotContentType != test.expectedContentType {
157 | t.Errorf(`expected content type "%s"; got "%s"`, test.expectedContentType, gotContentType)
158 | t.Fail()
159 | }
160 | })
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/app/cmd/mockify_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestLoadRoutes(t *testing.T) {
8 | routes := loadRoutes("../../config/routes.yaml")
9 | if len(routes) == 0 {
10 | t.Error("at least 1 route is required")
11 | t.Fail()
12 | } else {
13 | for _, route := range routes {
14 | if route.Route == "" {
15 | t.Error("missing a route")
16 | t.Fail()
17 | }
18 | if len(route.Methods) == 0 {
19 | t.Error("route needs at least 1 supported request method")
20 | t.Fail()
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/config/README.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | ## Explanation
4 |
5 | The configuration file is either YAML or JSON, and contains an array of route objects that Mockify will respond to.
6 |
7 | Each route must have 3 fields:
8 |
9 | - `route` [`string`]: the URI with any variables portions of it defined by `{}`
10 | - `methods` [`array`]: REST method(s) that this route will provide responses for. Must contain at least one
11 | - `responses` [`array`]: an array of responses that each have 6 fields:
12 | - `uri` [`string`]: the URI that this reponses is used for (no variable parts here)
13 | - `method` [`string`]: REST method for the response
14 | - _(Optional)_ `requestBody` [`string`]: If any part of the request body matches this string, that response will be used (only used if it has a value, can't be an empty string. Have the highest matching priority)
15 | - _(Optional)_ `requestHeader` [`string`]: Must be in the format `Key: Value` (i.e. `Authorization: foo-bar`). If `Key` is found in the request, and `Key`'s value is `Value` that response will be used (only used if it has a value, can't be an empty string. Have the second highest matching priority)
16 | - `statusCode` [`integer`]: Response status code
17 | - `headers` [`object`]: Response headers
18 | - `body` [`object`]: Response body
19 | - If `headers` contains `"Content-Type": "application/json"` the response body will be converted to JSON, otherwise the response is sent as a string but the the content-type provided so and `application/xml` can be interpreted correctly.
20 |
21 | ### Matching priorities
22 |
23 | There are three different priorities:
24 |
25 | 1. The routes with the `requestBody` has the highest matching priority. This means that they will be chosen first as the response when Mockify is called
26 | 1. The routes with the `requestHeader` has the second highest matching priority. This means that they will be chosen if a route with the highest priority hasn't been found
27 | 1. The routes without `requestBody` nor `requestHeader` has the lowest matching priority
28 |
29 | ## Examples
30 |
31 | See the configuration files in this folder.
32 |
--------------------------------------------------------------------------------
/config/routes.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "route": "/helloworld/{key}",
4 | "methods": ["GET"],
5 | "responses": [
6 | {
7 | "uri": "/helloworld/foo",
8 | "method": "GET",
9 | "statusCode": 200,
10 | "headers": {
11 | "Content-Type": "application/json"
12 | },
13 | "body": {
14 | "message": "Welcome to Mockify!"
15 | }
16 | },
17 | {
18 | "uri": "/helloworld/bar",
19 | "method": "GET",
20 | "statusCode": 200,
21 | "headers": {
22 | "Content-Type": "application/json"
23 | },
24 | "body": {
25 | "foo": {
26 | "key1": 1,
27 | "key2": true,
28 | "key3": [
29 | {
30 | "foo": "foo",
31 | "bar": true,
32 | "baz": [1, 2, "3"]
33 | }
34 | ]
35 | }
36 | }
37 | }
38 | ]
39 | },
40 | {
41 | "route": "/api/mcp",
42 | "methods": ["POST"],
43 | "responses": [
44 | {
45 | "uri": "/api/mcp",
46 | "method": "POST",
47 | "requestBody": "abc",
48 | "statusCode": 200,
49 | "headers": {
50 | "Content-Type": "application/xml"
51 | },
52 | "body": {
53 | "message": "ToveJaniReminderDon't forget me this weekend!"
54 | }
55 | },
56 | {
57 | "uri": "/api/mcp",
58 | "method": "POST",
59 | "requestBody": "def",
60 | "statusCode": 201,
61 | "headers": {
62 | "Content-Type": "application/json"
63 | },
64 | "body": {
65 | "foo": {
66 | "key1": 1,
67 | "key2": true,
68 | "key3": [
69 | {
70 | "foo": "foo",
71 | "bar": true,
72 | "baz": [1, 2, "3"]
73 | }
74 | ]
75 | }
76 | }
77 | },
78 | {
79 | "uri": "/api/mcp",
80 | "method": "POST",
81 | "requestBody": "body-have-higher-priority-over-header",
82 | "requestHeader": "Authorization: foo-bar",
83 | "statusCode": 302,
84 | "headers": {
85 | "Content-Type": "application/json"
86 | },
87 | "body": {
88 | "win": {
89 | "key1": 1,
90 | "key2": true
91 | }
92 | }
93 | }
94 | ]
95 | }
96 | ]
97 |
--------------------------------------------------------------------------------
/config/routes.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | - route: "/helloworld/{key}"
3 | methods:
4 | - GET
5 | responses:
6 | - uri: "/helloworld/foo"
7 | method: GET
8 | statusCode: 200
9 | headers:
10 | Content-Type: application/json
11 | body:
12 | message: Welcome to Mockify!
13 | - uri: "/helloworld/bar"
14 | method: GET
15 | statusCode: 200
16 | headers:
17 | Content-Type: application/json
18 | body:
19 | foo:
20 | key1: 1
21 | key2: true
22 | key3:
23 | - foo: foo
24 | bar: true
25 | baz:
26 | - 1
27 | - 2
28 | - '3'
29 | - route: "/api/mcp"
30 | methods:
31 | - POST
32 | responses:
33 | - uri: "/api/mcp"
34 | method: POST
35 | requestBody: abc
36 | statusCode: 200
37 | headers:
38 | Content-Type: application/xml
39 | body:
40 | message: ToveJaniReminderDon't
41 | forget me this weekend!
42 | - uri: "/api/mcp"
43 | method: POST
44 | requestBody: def
45 | statusCode: 201
46 | headers:
47 | Content-Type: application/json
48 | body:
49 | foo:
50 | key1: 1
51 | key2: true
52 | key3:
53 | - foo: foo
54 | bar: true
55 | baz:
56 | - 1
57 | - 2
58 | - '3'
59 | - uri: "/api/mcp"
60 | method: POST
61 | requestBody: "body-have-higher-priority-over-header"
62 | requestHeader: "Authorization: foo-bar"
63 | statusCode: 302
64 | headers:
65 | Content-Type: application/json
66 | body:
67 | win:
68 | key1: 1
69 | key2: true
70 |
71 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | mockify:
4 | build: .
5 | env_file:
6 | - .env
7 | environment:
8 | - MOCKIFY_PORT=8001
9 | - MOCKIFY_ROUTES=./config/routes.yaml
10 | volumes:
11 | - ./app:/app
12 | ports:
13 | - "${MOCKIFY_PORT}:${MOCKIFY_PORT}"
14 |
--------------------------------------------------------------------------------
/postman/mockify.postman_collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "_postman_id": "7d7ccf60-26db-4670-95c4-0d4d51ad2191",
4 | "name": "mockify",
5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
6 | },
7 | "item": [
8 | {
9 | "name": "/helloworld/foo",
10 | "request": {
11 | "method": "GET",
12 | "header": [],
13 | "body": {
14 | "mode": "raw",
15 | "raw": ""
16 | },
17 | "url": {
18 | "raw": "{{url}}/helloworld/foo",
19 | "host": [
20 | "{{url}}"
21 | ],
22 | "path": [
23 | "helloworld",
24 | "foo"
25 | ]
26 | }
27 | },
28 | "response": []
29 | },
30 | {
31 | "name": "/list",
32 | "request": {
33 | "method": "GET",
34 | "header": [],
35 | "body": {
36 | "mode": "raw",
37 | "raw": ""
38 | },
39 | "url": {
40 | "raw": "{{url}}/list",
41 | "host": [
42 | "{{url}}"
43 | ],
44 | "path": [
45 | "list"
46 | ]
47 | }
48 | },
49 | "response": []
50 | },
51 | {
52 | "name": "/add",
53 | "request": {
54 | "method": "POST",
55 | "header": [
56 | {
57 | "key": "Content-Type",
58 | "name": "Content-Type",
59 | "value": "application/json",
60 | "type": "text"
61 | }
62 | ],
63 | "body": {
64 | "mode": "raw",
65 | "raw": "{\n\t\"route\": \"/foo/{id}\",\n\t\"methods\": [\"get\"],\n\t\"responses\": [\n\t\t{\n\t\t\t\"uri\": \"/foo/1\",\n\t\t\t\"method\": \"get\",\n\t\t\t\"statusCode\": 200,\n\t\t\t\"headers\": {\"Content-Type\": \"application/json\"},\n\t\t\t\"body\": {\n\t\t\t\t\"foo\": \"bar\"\n\t\t\t}\n\t\t}\t\n\t]\n}"
66 | },
67 | "url": {
68 | "raw": "{{url}}/add",
69 | "host": [
70 | "{{url}}"
71 | ],
72 | "path": [
73 | "add"
74 | ]
75 | }
76 | },
77 | "response": []
78 | }
79 | ]
80 | }
--------------------------------------------------------------------------------
/postman/mockify.postman_environment.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "a85a1239-f631-4b0c-bc62-aa739af7c149",
3 | "name": "mockify",
4 | "values": [
5 | {
6 | "key": "port",
7 | "value": "8001",
8 | "description": "",
9 | "enabled": true
10 | },
11 | {
12 | "key": "host",
13 | "value": "localhost",
14 | "type": "text",
15 | "description": "",
16 | "enabled": true
17 | },
18 | {
19 | "key": "url",
20 | "value": "{{host}}:{{port}}",
21 | "type": "text",
22 | "description": "",
23 | "enabled": true
24 | }
25 | ],
26 | "_postman_variable_scope": "environment",
27 | "_postman_exported_at": "2018-12-31T02:48:36.307Z",
28 | "_postman_exported_using": "Postman/6.6.1"
29 | }
--------------------------------------------------------------------------------