├── .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 | } --------------------------------------------------------------------------------