Skip to content

Commit

Permalink
feature/generate-router-from-config-file
Browse files Browse the repository at this point in the history
  • Loading branch information
anthonysyk committed Jan 6, 2023
1 parent 2717f0f commit 03c338e
Show file tree
Hide file tree
Showing 14 changed files with 439 additions and 56 deletions.
87 changes: 63 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,45 +10,84 @@ version: 1.0
paths:
/health:
GET:
responses:
200:
body: |
{"status":"OK"}
/recommended-movies:
200:
body: |-
{"status":"OK"}
/top-movies:
GET:
responses:
200:
filepath: "payload/top-movies.json"
200:
filepath: "payload/top-movies.json"
/movies/{id}:
GET:
responses:
200:
filepath: "payload/movies/{id}.json"
/movies/{id}/actors/{id}:
200:
filepath: "payload/movies/{id}.json"
/movies/{moviesId}/actors/{actorsId}:
GET:
responses:
200:
filepath: "payload/movies/{id}/actors/{id}.json"
/popular-actors:
200:
filepath: "payload/movies/{moviesId}/actors/{actorsId}.json"
/movies/suggest:
GET:
responses:
200:
filepath: "payload/top-actors.json"
200:
body: |-
{
"title": "The Godfather",
"description": "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.",
"duration": 175,
"actors": [
"Marlon Brando",
"Al Pacino",
"James Caan"
]
}
/top-actors:
GET:
200:
filepath: "payload/top-actors.json"
/genres:
GET:
responses:
200:
body: |
["Action","Adventure","Animation","Comedy","Crime","Drama","Family","Fantasy","Music","Science Fiction","Thriller"]
200:
body: |-
["Action","Adventure","Animation","Comedy","Crime","Drama","Family","Fantasy","Music","Science Fiction","Thriller"]
POST:
200:
body: |-
{"message":"new genre added"}
DELETE:
200:
body: |-
{"message":"genre deleted"}
```
You can either set data as :
- a filepath
- an inline string
### Usage
```go
//go:embed routes.yml
var RoutesYAML string

func main() {
router, err := http_server_mock.GenerateRouter(RoutesYAML)
if err != nil {
// handle error
}
http.ListenAndServe(":8080", router)
}
```

### Use Cases

- To run a mock http server for developing, testing or debugging with mocked data
- To mock an API you don't have access to
- To mock response payloads and match a specific environment
- To implement integration tests
- To implement integration tests

### Troubleshoot

Warnings can be raised if :
- Path parameters definitions must be unique per URL
- Good : `/movies/{moviesId}/actors/{actorsId}`
- Bad : `/movies/{id}/actors/{id}`
- Routes must be defined only once (no duplicate)
17 changes: 6 additions & 11 deletions conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package http_server_mock
import (
"errors"
"gopkg.in/yaml.v3"
"os"
)

func GetRoutes(config string) (Routes, error) {
Expand All @@ -19,12 +18,12 @@ func GetRoutes(config string) (Routes, error) {
for _, mt := range methodTails {
statusTails := getStatus(mt.Tail)
for _, st := range statusTails {
filepath, body, bodyErr := getBody(st.Tail)
filepath, body, bodyErr := getFilepathOrInlineBody(st.Tail)
routes = append(routes, Route{
URL: ut.URL,
Method: mt.Method,
StatusCode: st.Status,
Body: body,
InlineBody: body,
Filepath: filepath,
Errors: []error{bodyErr},
})
Expand Down Expand Up @@ -85,20 +84,16 @@ func getStatus(m map[interface{}]interface{}) []StatusTail {
return statusTails
}

func getBody(m map[string]interface{}) (string, []byte, error) {
func getFilepathOrInlineBody(m map[string]interface{}) (string, []byte, error) {
if len(m) > 1 {
return "", nil, errors.New("cannot set both body and filepath for a specific status code")
}
if body, ok := m["body"]; ok && body != "" {
if body, ok := m["body"]; ok && body != "" && body != nil {
return "", []byte(body.(string)), nil
}
if filepath, ok := m["filepath"]; ok && filepath != "" {
if filepath, ok := m["filepath"]; ok && filepath != "" && filepath != nil {
fp := filepath.(string)
content, err := os.ReadFile(fp)
if err != nil {
return fp, nil, err
}
return fp, content, nil
return fp, nil, nil
}

return "", nil, errors.New("no body found")
Expand Down
139 changes: 129 additions & 10 deletions example/conf/conf_test.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,142 @@
package conf

import (
_ "embed"
"fmt"
"github.com/anthonysyk/http-server-mock"
"github.com/anthonysyk/http-server-mock/pkg/routehelper"
"github.com/anthonysyk/http-server-mock/pkg/stdout"
"github.com/stretchr/testify/assert"
"io"
"net/http"
"strings"
"testing"

_ "embed"
)

//go:embed routes.yml
var RoutesYAML string

func TestModel(t *testing.T) {
func TestRoutes(t *testing.T) {
routes, err := http_server_mock.GetRoutes(RoutesYAML)
assert.NoError(t, err)
assert.Len(t, routes, 8)
}

// run server
// do some calls
// catch output stdout
// verify output is ok
func TestRouter(t *testing.T) {
router, err := http_server_mock.GenerateRouter(RoutesYAML)
assert.NoError(t, err)
output := stdout.Record(func() { routehelper.PrintRoutes(router) })
expectedOutput := `[GET] /movies/{id} - http-server-mock.handler.func1
[GET] /movies/{moviesId}/actors/{actorsId} - http-server-mock.handler.func1
[GET] /top-actors - http-server-mock.handler.func1
[DELETE] /genres - http-server-mock.handler.func1
[GET] /genres - http-server-mock.handler.func1
[POST] /genres - http-server-mock.handler.func1
[GET] /health - http-server-mock.handler.func1
[GET] /top-movies - http-server-mock.handler.func1
`
assert.ElementsMatch(t, strings.Split(expectedOutput, "\n"), strings.Split(output, "\n"))
}

//go:embed routes.yml
var RoutesYAML string

func TestServer(t *testing.T) {
router, err := http_server_mock.GenerateRouter(RoutesYAML)
assert.NoError(t, err)
go http.ListenAndServe(":8080", router)

client := &http.Client{}

tests := []struct {
url string
method string
expectedStatusCode int
expectedResponse string
}{
{
url: "/health",
method: http.MethodGet,
expectedStatusCode: 200,
expectedResponse: `{"status":"OK"}`,
},
{
url: "/genres",
method: http.MethodGet,
expectedStatusCode: 200,
expectedResponse: `["Action","Adventure","Animation","Comedy","Crime","Drama","Family","Fantasy","Music","Science Fiction","Thriller"]`,
},
{
url: "/genres",
method: http.MethodPost,
expectedStatusCode: 200,
expectedResponse: `{"message":"new genre added"}`,
},
{
url: "/genres",
method: http.MethodDelete,
expectedStatusCode: 200,
expectedResponse: `{"message":"genre deleted"}`,
},
{
url: "/top-movies",
method: http.MethodGet,
expectedStatusCode: 200,
expectedResponse: TopMoviesPayload,
},
{
url: "/top-actors",
method: http.MethodGet,
expectedStatusCode: 200,
expectedResponse: TopActorsPayload,
},
{
url: "/movies/399566",
method: http.MethodGet,
expectedStatusCode: 200,
expectedResponse: Movie399566Payload,
},
{
url: "/movies/399566/actors/15556",
method: http.MethodGet,
expectedStatusCode: 200,
expectedResponse: Movie399566Actor15556Payload,
},
{
url: "/movies/suggest",
method: http.MethodGet,
expectedStatusCode: 200,
expectedResponse: MovieSuggestPayload,
},
{
url: routehelper.ReplaceQueryParamWithID("/movies/{id}"),
method: http.MethodGet,
expectedStatusCode: 404,
},
{
url: routehelper.ReplaceQueryParamWithID("/movies/{id}/actors/{id}"),
method: http.MethodGet,
expectedStatusCode: 404,
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("%s%s", test.method, test.url), func(t *testing.T) {
url := fmt.Sprintf("%s%s", "http://localhost:8080", test.url)
req, reqErr := http.NewRequest(test.method, url, nil)
if reqErr != nil {
t.Fatal(reqErr)
}
res, resErr := client.Do(req)
defer res.Body.Close()
if resErr != nil {
t.Fatal(resErr)
}
body, bodyErr := io.ReadAll(res.Body)
if bodyErr != nil {
t.Fatal(bodyErr)
}

assert.Equal(t, test.expectedStatusCode, res.StatusCode)
assert.Equal(t, test.expectedResponse, string(body))
})
}

}
20 changes: 20 additions & 0 deletions example/conf/payload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package conf

import (
_ "embed"
)

//go:embed payload/top-movies.json
var TopMoviesPayload string

//go:embed payload/top-actors.json
var TopActorsPayload string

//go:embed payload/movies/399566.json
var Movie399566Payload string

//go:embed payload/movies/399566/actors/15556.json
var Movie399566Actor15556Payload string

//go:embed payload/movies/suggest.json
var MovieSuggestPayload string
10 changes: 10 additions & 0 deletions example/conf/payload/movies/suggest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"title": "The Godfather",
"description": "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.",
"duration": 175,
"actors": [
"Marlon Brando",
"Al Pacino",
"James Caan"
]
}
Loading

0 comments on commit 03c338e

Please sign in to comment.