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:
Jeremy Wharton 2022-06-15 21:07:38 -05:00 committed by Storj Robot
parent 26bed35f33
commit 1f0638719e
6 changed files with 321 additions and 22 deletions

View 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)))
}
}

View 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")
}

View File

@ -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))

View 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"])
}

View File

@ -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")

View File

@ -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)