private/apigen,cmd/apigentest: add tests for generated API code
This change implements a unit test for ensuring proper processing of requests and responses by generated API code. Additionally, this change requires API handlers to explicitly receive Monkit scopes rather than assuming that `mon` will always exist in the generated API code's namespace. Change-Id: Iea56f139f9dad0050b7d09ea765189280c3466f2
This commit is contained in:
parent
26bed35f33
commit
1f0638719e
116
private/apigen/example/api.gen.go
Normal file
116
private/apigen/example/api.gen.go
Normal file
@ -0,0 +1,116 @@
|
||||
// AUTOGENERATED BY private/apigen
|
||||
// DO NOT EDIT.
|
||||
|
||||
package example
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spacemonkeygo/monkit/v3"
|
||||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/private/api"
|
||||
)
|
||||
|
||||
const dateLayout = "2006-01-02T15:04:05.999Z"
|
||||
|
||||
var ErrTestapiAPI = errs.Class("example testapi api")
|
||||
|
||||
type TestAPIService interface {
|
||||
GenTestAPI(context.Context, uuid.UUID, time.Time, string, struct{ Content string }) (*struct {
|
||||
ID uuid.UUID
|
||||
Date time.Time
|
||||
PathParam string
|
||||
Body string
|
||||
}, api.HTTPError)
|
||||
}
|
||||
|
||||
// TestAPIHandler is an api handler that exposes all testapi related functionality.
|
||||
type TestAPIHandler struct {
|
||||
log *zap.Logger
|
||||
mon *monkit.Scope
|
||||
service TestAPIService
|
||||
auth api.Auth
|
||||
}
|
||||
|
||||
func NewTestAPI(log *zap.Logger, mon *monkit.Scope, service TestAPIService, router *mux.Router, auth api.Auth) *TestAPIHandler {
|
||||
handler := &TestAPIHandler{
|
||||
log: log,
|
||||
mon: mon,
|
||||
service: service,
|
||||
auth: auth,
|
||||
}
|
||||
|
||||
testapiRouter := router.PathPrefix("/api/v0/testapi").Subrouter()
|
||||
testapiRouter.HandleFunc("/{path}", handler.handleGenTestAPI).Methods("POST")
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func (h *TestAPIHandler) handleGenTestAPI(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
|
||||
if err != nil {
|
||||
h.auth.RemoveAuthCookie(w)
|
||||
api.ServeError(h.log, w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
idParam := r.URL.Query().Get("id")
|
||||
if idParam == "" {
|
||||
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("parameter 'id' can't be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.FromString(idParam)
|
||||
if err != nil {
|
||||
api.ServeError(h.log, w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
dateParam := r.URL.Query().Get("date")
|
||||
if dateParam == "" {
|
||||
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("parameter 'date' can't be empty"))
|
||||
return
|
||||
}
|
||||
|
||||
date, err := time.Parse(dateLayout, dateParam)
|
||||
if err != nil {
|
||||
api.ServeError(h.log, w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
path, ok := mux.Vars(r)["path"]
|
||||
if !ok {
|
||||
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("missing path route param"))
|
||||
return
|
||||
}
|
||||
|
||||
payload := struct{ Content string }{}
|
||||
if err = json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
api.ServeError(h.log, w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
retVal, httpErr := h.service.GenTestAPI(ctx, id, date, path, payload)
|
||||
if httpErr.Err != nil {
|
||||
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(retVal)
|
||||
if err != nil {
|
||||
h.log.Debug("failed to write json GenTestAPI response", zap.Error(ErrTestapiAPI.Wrap(err)))
|
||||
}
|
||||
}
|
41
private/apigen/example/gen.go
Normal file
41
private/apigen/example/gen.go
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2022 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
//go:generate go run gen.go
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/private/apigen"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a := &apigen.API{PackageName: "example"}
|
||||
|
||||
g := a.Group("TestAPI", "testapi")
|
||||
|
||||
g.Post("/{path}", &apigen.Endpoint{
|
||||
MethodName: "GenTestAPI",
|
||||
Response: struct {
|
||||
ID uuid.UUID
|
||||
Date time.Time
|
||||
PathParam string
|
||||
Body string
|
||||
}{},
|
||||
Request: struct{ Content string }{},
|
||||
QueryParams: []apigen.Param{
|
||||
apigen.NewParam("id", uuid.UUID{}),
|
||||
apigen.NewParam("date", time.Time{}),
|
||||
},
|
||||
PathParams: []apigen.Param{
|
||||
apigen.NewParam("path", ""),
|
||||
},
|
||||
})
|
||||
|
||||
a.MustWriteGo("api.gen.go")
|
||||
}
|
@ -19,6 +19,9 @@ import (
|
||||
"storj.io/common/uuid"
|
||||
)
|
||||
|
||||
// DateFormat is the layout of dates passed into and out of the API.
|
||||
const DateFormat = "2006-01-02T15:04:05.999Z"
|
||||
|
||||
// MustWriteGo writes generated Go code into a file.
|
||||
func (a *API) MustWriteGo(path string) {
|
||||
generated, err := a.generateGo()
|
||||
@ -56,7 +59,7 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
|
||||
i := func(paths ...string) {
|
||||
for _, path := range paths {
|
||||
if getPackageName(path) == a.PackageName {
|
||||
if path == "" || getPackageName(path) == a.PackageName {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -89,9 +92,6 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
p("const dateLayout = \"2006-01-02T15:04:05.000Z\"")
|
||||
p("")
|
||||
|
||||
for _, group := range a.EndpointGroups {
|
||||
i("github.com/zeebo/errs")
|
||||
p("var Err%sAPI = errs.Class(\"%s %s api\")", cases.Title(language.Und).String(group.Prefix), a.PackageName, group.Prefix)
|
||||
@ -131,10 +131,11 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
}
|
||||
|
||||
for _, group := range a.EndpointGroups {
|
||||
i("go.uber.org/zap")
|
||||
i("go.uber.org/zap", "github.com/spacemonkeygo/monkit/v3")
|
||||
p("// %sHandler is an api handler that exposes all %s related functionality.", group.Name, group.Prefix)
|
||||
p("type %sHandler struct {", group.Name)
|
||||
p("log *zap.Logger")
|
||||
p("mon *monkit.Scope")
|
||||
p("service %sService", group.Name)
|
||||
p("auth api.Auth")
|
||||
p("}")
|
||||
@ -144,13 +145,14 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
for _, group := range a.EndpointGroups {
|
||||
i("github.com/gorilla/mux")
|
||||
p(
|
||||
"func New%s(log *zap.Logger, service %sService, router *mux.Router, auth api.Auth) *%sHandler {",
|
||||
"func New%s(log *zap.Logger, mon *monkit.Scope, service %sService, router *mux.Router, auth api.Auth) *%sHandler {",
|
||||
group.Name,
|
||||
group.Name,
|
||||
group.Name,
|
||||
)
|
||||
p("handler := &%sHandler{", group.Name)
|
||||
p("log: log,")
|
||||
p("mon: mon,")
|
||||
p("service: service,")
|
||||
p("auth: auth,")
|
||||
p("}")
|
||||
@ -174,7 +176,7 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
p("func (h *%sHandler) %s(w http.ResponseWriter, r *http.Request) {", group.Name, handlerName)
|
||||
p("ctx := r.Context()")
|
||||
p("var err error")
|
||||
p("defer mon.Task()(&ctx)(&err)")
|
||||
p("defer h.mon.Task()(&ctx)(&err)")
|
||||
p("")
|
||||
|
||||
p("w.Header().Set(\"Content-Type\", \"application/json\")")
|
||||
@ -257,6 +259,11 @@ func (a *API) generateGo() ([]byte, error) {
|
||||
p(")")
|
||||
p("")
|
||||
|
||||
if _, ok := imports.All["time"]; ok {
|
||||
p("const dateLayout = \"%s\"", DateFormat)
|
||||
p("")
|
||||
}
|
||||
|
||||
result += fileBody
|
||||
|
||||
output, err := format.Source([]byte(result))
|
||||
|
128
private/apigen/gogen_test.go
Normal file
128
private/apigen/gogen_test.go
Normal file
@ -0,0 +1,128 @@
|
||||
// Copyright (C) 2022 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package apigen_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spacemonkeygo/monkit/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
"storj.io/common/testcontext"
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/private/api"
|
||||
"storj.io/storj/private/apigen"
|
||||
"storj.io/storj/private/apigen/example"
|
||||
)
|
||||
|
||||
type (
|
||||
auth struct{}
|
||||
service struct{}
|
||||
response = struct {
|
||||
ID uuid.UUID
|
||||
Date time.Time
|
||||
PathParam string
|
||||
Body string
|
||||
}
|
||||
)
|
||||
|
||||
func (a auth) IsAuthenticated(ctx context.Context, r *http.Request, isCookieAuth, isKeyAuth bool) (context.Context, error) {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (a auth) RemoveAuthCookie(w http.ResponseWriter) {}
|
||||
|
||||
func (s service) GenTestAPI(ctx context.Context, id uuid.UUID, date time.Time, pathParam string, body struct{ Content string }) (*response, api.HTTPError) {
|
||||
return &response{
|
||||
ID: id,
|
||||
Date: date,
|
||||
PathParam: pathParam,
|
||||
Body: body.Content,
|
||||
}, api.HTTPError{}
|
||||
}
|
||||
|
||||
func send(ctx context.Context, method string, url string, body interface{}) ([]byte, error) {
|
||||
var bodyReader io.Reader = http.NoBody
|
||||
if body != nil {
|
||||
bodyJSON, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyReader = bytes.NewBuffer(bodyJSON)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
func TestAPIServer(t *testing.T) {
|
||||
ctx := testcontext.NewWithTimeout(t, 5*time.Second)
|
||||
defer ctx.Cleanup()
|
||||
|
||||
router := mux.NewRouter()
|
||||
example.NewTestAPI(zaptest.NewLogger(t), monkit.Package(), service{}, router, auth{})
|
||||
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
|
||||
id, err := uuid.New()
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := response{
|
||||
ID: id,
|
||||
Date: time.Now(),
|
||||
PathParam: "foo",
|
||||
Body: "bar",
|
||||
}
|
||||
|
||||
resp, err := send(ctx, http.MethodPost,
|
||||
fmt.Sprintf("%s/api/v0/testapi/%s?id=%s&date=%s",
|
||||
server.URL,
|
||||
expected.PathParam,
|
||||
url.QueryEscape(expected.ID.String()),
|
||||
url.QueryEscape(expected.Date.Format(apigen.DateFormat)),
|
||||
), struct{ Content string }{expected.Body},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
var actual map[string]string
|
||||
require.NoError(t, json.Unmarshal(resp, &actual))
|
||||
|
||||
for _, key := range []string{"ID", "Date", "PathParam", "Body"} {
|
||||
require.Contains(t, actual, key)
|
||||
}
|
||||
require.Equal(t, expected.ID.String(), actual["ID"])
|
||||
require.Equal(t, expected.Date.Format(apigen.DateFormat), actual["Date"])
|
||||
require.Equal(t, expected.Body, actual["Body"])
|
||||
}
|
@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spacemonkeygo/monkit/v3"
|
||||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@ -19,7 +20,7 @@ import (
|
||||
"storj.io/storj/satellite/console"
|
||||
)
|
||||
|
||||
const dateLayout = "2006-01-02T15:04:05.000Z"
|
||||
const dateLayout = "2006-01-02T15:04:05.999Z"
|
||||
|
||||
var ErrProjectsAPI = errs.Class("consoleapi projects api")
|
||||
var ErrApikeysAPI = errs.Class("consoleapi apikeys api")
|
||||
@ -45,6 +46,7 @@ type UserManagementService interface {
|
||||
// ProjectManagementHandler is an api handler that exposes all projects related functionality.
|
||||
type ProjectManagementHandler struct {
|
||||
log *zap.Logger
|
||||
mon *monkit.Scope
|
||||
service ProjectManagementService
|
||||
auth api.Auth
|
||||
}
|
||||
@ -52,6 +54,7 @@ type ProjectManagementHandler struct {
|
||||
// APIKeyManagementHandler is an api handler that exposes all apikeys related functionality.
|
||||
type APIKeyManagementHandler struct {
|
||||
log *zap.Logger
|
||||
mon *monkit.Scope
|
||||
service APIKeyManagementService
|
||||
auth api.Auth
|
||||
}
|
||||
@ -59,13 +62,15 @@ type APIKeyManagementHandler struct {
|
||||
// UserManagementHandler is an api handler that exposes all users related functionality.
|
||||
type UserManagementHandler struct {
|
||||
log *zap.Logger
|
||||
mon *monkit.Scope
|
||||
service UserManagementService
|
||||
auth api.Auth
|
||||
}
|
||||
|
||||
func NewProjectManagement(log *zap.Logger, service ProjectManagementService, router *mux.Router, auth api.Auth) *ProjectManagementHandler {
|
||||
func NewProjectManagement(log *zap.Logger, mon *monkit.Scope, service ProjectManagementService, router *mux.Router, auth api.Auth) *ProjectManagementHandler {
|
||||
handler := &ProjectManagementHandler{
|
||||
log: log,
|
||||
mon: mon,
|
||||
service: service,
|
||||
auth: auth,
|
||||
}
|
||||
@ -81,9 +86,10 @@ func NewProjectManagement(log *zap.Logger, service ProjectManagementService, rou
|
||||
return handler
|
||||
}
|
||||
|
||||
func NewAPIKeyManagement(log *zap.Logger, service APIKeyManagementService, router *mux.Router, auth api.Auth) *APIKeyManagementHandler {
|
||||
func NewAPIKeyManagement(log *zap.Logger, mon *monkit.Scope, service APIKeyManagementService, router *mux.Router, auth api.Auth) *APIKeyManagementHandler {
|
||||
handler := &APIKeyManagementHandler{
|
||||
log: log,
|
||||
mon: mon,
|
||||
service: service,
|
||||
auth: auth,
|
||||
}
|
||||
@ -94,9 +100,10 @@ func NewAPIKeyManagement(log *zap.Logger, service APIKeyManagementService, route
|
||||
return handler
|
||||
}
|
||||
|
||||
func NewUserManagement(log *zap.Logger, service UserManagementService, router *mux.Router, auth api.Auth) *UserManagementHandler {
|
||||
func NewUserManagement(log *zap.Logger, mon *monkit.Scope, service UserManagementService, router *mux.Router, auth api.Auth) *UserManagementHandler {
|
||||
handler := &UserManagementHandler{
|
||||
log: log,
|
||||
mon: mon,
|
||||
service: service,
|
||||
auth: auth,
|
||||
}
|
||||
@ -110,7 +117,7 @@ func NewUserManagement(log *zap.Logger, service UserManagementService, router *m
|
||||
func (h *ProjectManagementHandler) handleGenCreateProject(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@ -142,7 +149,7 @@ func (h *ProjectManagementHandler) handleGenCreateProject(w http.ResponseWriter,
|
||||
func (h *ProjectManagementHandler) handleGenUpdateProject(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@ -186,7 +193,7 @@ func (h *ProjectManagementHandler) handleGenUpdateProject(w http.ResponseWriter,
|
||||
func (h *ProjectManagementHandler) handleGenDeleteProject(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@ -218,7 +225,7 @@ func (h *ProjectManagementHandler) handleGenDeleteProject(w http.ResponseWriter,
|
||||
func (h *ProjectManagementHandler) handleGenGetUsersProjects(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@ -244,7 +251,7 @@ func (h *ProjectManagementHandler) handleGenGetUsersProjects(w http.ResponseWrit
|
||||
func (h *ProjectManagementHandler) handleGenGetSingleBucketUsageRollup(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@ -312,7 +319,7 @@ func (h *ProjectManagementHandler) handleGenGetSingleBucketUsageRollup(w http.Re
|
||||
func (h *ProjectManagementHandler) handleGenGetBucketUsageRollups(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@ -374,7 +381,7 @@ func (h *ProjectManagementHandler) handleGenGetBucketUsageRollups(w http.Respons
|
||||
func (h *APIKeyManagementHandler) handleGenCreateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@ -406,7 +413,7 @@ func (h *APIKeyManagementHandler) handleGenCreateAPIKey(w http.ResponseWriter, r
|
||||
func (h *UserManagementHandler) handleGenGetUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
defer h.mon.Task()(&ctx)(&err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
|
@ -245,9 +245,9 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
|
||||
router.Use(newTraceRequestMiddleware(logger, router))
|
||||
|
||||
if server.config.GeneratedAPIEnabled {
|
||||
consoleapi.NewProjectManagement(logger, server.service, router, &apiAuth{&server})
|
||||
consoleapi.NewAPIKeyManagement(logger, server.service, router, &apiAuth{&server})
|
||||
consoleapi.NewUserManagement(logger, server.service, router, &apiAuth{&server})
|
||||
consoleapi.NewProjectManagement(logger, mon, server.service, router, &apiAuth{&server})
|
||||
consoleapi.NewAPIKeyManagement(logger, mon, server.service, router, &apiAuth{&server})
|
||||
consoleapi.NewUserManagement(logger, mon, server.service, router, &apiAuth{&server})
|
||||
}
|
||||
|
||||
router.HandleFunc("/registrationToken/", server.createRegistrationTokenHandler)
|
||||
|
Loading…
Reference in New Issue
Block a user