mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
Merge pull request #349 from fnproject/pagination
add pagination to all list endpoints
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/fnproject/fn/api/models"
|
||||
@@ -10,13 +11,24 @@ import (
|
||||
func (s *Server) handleAppList(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
filter := &models.AppFilter{}
|
||||
var filter models.AppFilter
|
||||
filter.Cursor, filter.PerPage = pageParams(c, true)
|
||||
|
||||
apps, err := s.Datastore.GetApps(ctx, filter)
|
||||
apps, err := s.Datastore.GetApps(ctx, &filter)
|
||||
if err != nil {
|
||||
handleErrorResponse(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, appsResponse{"Successfully listed applications", apps})
|
||||
var nextCursor string
|
||||
if len(apps) > 0 && len(apps) == filter.PerPage {
|
||||
last := []byte(apps[len(apps)-1].Name)
|
||||
nextCursor = base64.RawURLEncoding.EncodeToString(last)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, appsResponse{
|
||||
Message: "Successfully listed applications",
|
||||
NextCursor: nextCursor,
|
||||
Apps: apps,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -120,17 +122,36 @@ func TestAppList(t *testing.T) {
|
||||
|
||||
rnr, cancel := testRunner(t)
|
||||
defer cancel()
|
||||
ds := datastore.NewMock()
|
||||
ds := datastore.NewMockInit(
|
||||
[]*models.App{
|
||||
{Name: "myapp"},
|
||||
{Name: "myapp2"},
|
||||
{Name: "myapp3"},
|
||||
},
|
||||
nil, // no routes
|
||||
nil, // no calls
|
||||
)
|
||||
fnl := logs.NewMock()
|
||||
srv := testServer(ds, &mqs.Mock{}, fnl, rnr)
|
||||
|
||||
a1b := base64.RawURLEncoding.EncodeToString([]byte("myapp"))
|
||||
a2b := base64.RawURLEncoding.EncodeToString([]byte("myapp2"))
|
||||
a3b := base64.RawURLEncoding.EncodeToString([]byte("myapp3"))
|
||||
|
||||
for i, test := range []struct {
|
||||
path string
|
||||
body string
|
||||
expectedCode int
|
||||
expectedError error
|
||||
expectedLen int
|
||||
nextCursor string
|
||||
}{
|
||||
{"/v1/apps", "", http.StatusOK, nil},
|
||||
{"/v1/apps?per_page", "", http.StatusOK, nil, 3, ""},
|
||||
{"/v1/apps?per_page=1", "", http.StatusOK, nil, 1, a1b},
|
||||
{"/v1/apps?per_page=1&cursor=" + a1b, "", http.StatusOK, nil, 1, a2b},
|
||||
{"/v1/apps?per_page=1&cursor=" + a2b, "", http.StatusOK, nil, 1, a3b},
|
||||
{"/v1/apps?per_page=100&cursor=" + a2b, "", http.StatusOK, nil, 1, ""}, // cursor is empty if per_page > len(results)
|
||||
{"/v1/apps?per_page=1&cursor=" + a3b, "", http.StatusOK, nil, 0, ""}, // cursor could point to empty page
|
||||
} {
|
||||
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
|
||||
|
||||
@@ -148,6 +169,20 @@ func TestAppList(t *testing.T) {
|
||||
t.Errorf("Test %d: Expected error message to have `%s`",
|
||||
i, test.expectedError.Error())
|
||||
}
|
||||
} else {
|
||||
// normal path
|
||||
|
||||
var resp appsResponse
|
||||
err := json.NewDecoder(rec.Body).Decode(&resp)
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: Expected response body to be a valid json object. err: %v", i, err)
|
||||
}
|
||||
if len(resp.Apps) != test.expectedLen {
|
||||
t.Errorf("Test %d: Expected apps length to be %d, but got %d", i, test.expectedLen, len(resp.Apps))
|
||||
}
|
||||
if resp.NextCursor != test.nextCursor {
|
||||
t.Errorf("Test %d: Expected next_cursor to be %s, but got %s", i, test.nextCursor, resp.NextCursor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +18,5 @@ func (s *Server) handleCallGet(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, fnCallResponse{"Successfully loaded call", callObj})
|
||||
c.JSON(http.StatusOK, callResponse{"Successfully loaded call", callObj})
|
||||
}
|
||||
|
||||
@@ -2,45 +2,80 @@ package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fnproject/fn/api"
|
||||
"github.com/fnproject/fn/api/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-openapi/strfmt"
|
||||
)
|
||||
|
||||
func (s *Server) handleCallList(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
name, ok := c.Get(api.AppName)
|
||||
appName, conv := name.(string)
|
||||
if ok && conv && appName == "" {
|
||||
handleErrorResponse(c, models.ErrRoutesValidationMissingAppName)
|
||||
return
|
||||
}
|
||||
appName := c.MustGet(api.AppName).(string)
|
||||
|
||||
filter := models.CallFilter{AppName: appName, Path: c.Query(api.CRoute)}
|
||||
// TODO api.CRoute needs to be escaped probably, since it has '/' a lot
|
||||
filter := models.CallFilter{AppName: appName, Path: c.Query("path")}
|
||||
filter.Cursor, filter.PerPage = pageParams(c, false) // ids are url safe
|
||||
|
||||
calls, err := s.Datastore.GetCalls(ctx, &filter)
|
||||
var err error
|
||||
filter.FromTime, filter.ToTime, err = timeParams(c)
|
||||
if err != nil {
|
||||
handleErrorResponse(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(calls) == 0 {
|
||||
_, err = s.Datastore.GetApp(c, appName)
|
||||
if err != nil {
|
||||
handleErrorResponse(c, err)
|
||||
return
|
||||
}
|
||||
calls, err := s.Datastore.GetCalls(ctx, &filter)
|
||||
|
||||
if filter.Path != "" {
|
||||
_, err = s.Datastore.GetRoute(c, appName, filter.Path)
|
||||
if err != nil {
|
||||
handleErrorResponse(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(calls) == 0 {
|
||||
// TODO this should be done in front of this handler to even get here...
|
||||
_, err = s.Datastore.GetApp(c, appName)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, fnCallsResponse{"Successfully listed calls", calls})
|
||||
if err != nil {
|
||||
handleErrorResponse(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
var nextCursor string
|
||||
if len(calls) > 0 && len(calls) == filter.PerPage {
|
||||
nextCursor = calls[len(calls)-1].ID
|
||||
// don't base64, IDs are url safe
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, callsResponse{
|
||||
Message: "Successfully listed calls",
|
||||
NextCursor: nextCursor,
|
||||
Calls: calls,
|
||||
})
|
||||
}
|
||||
|
||||
// "" gets parsed to a zero time, which is fine (ignored in query)
|
||||
func timeParams(c *gin.Context) (fromTime, toTime strfmt.DateTime, err error) {
|
||||
fromStr := c.Query("from_time")
|
||||
toStr := c.Query("to_time")
|
||||
var ok bool
|
||||
if fromStr != "" {
|
||||
fromTime, ok = strToTime(fromStr)
|
||||
if !ok {
|
||||
return fromTime, toTime, models.ErrInvalidFromTime
|
||||
}
|
||||
}
|
||||
if toStr != "" {
|
||||
toTime, ok = strToTime(toStr)
|
||||
if !ok {
|
||||
return fromTime, toTime, models.ErrInvalidToTime
|
||||
}
|
||||
}
|
||||
return fromTime, toTime, nil
|
||||
}
|
||||
|
||||
func strToTime(str string) (strfmt.DateTime, bool) {
|
||||
sec, err := strconv.ParseInt(str, 10, 64)
|
||||
if err != nil {
|
||||
return strfmt.DateTime(time.Time{}), false
|
||||
}
|
||||
return strfmt.DateTime(time.Unix(sec, 0)), true
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (s *Server) handleCallLogGet(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, fnCallLogResponse{"Successfully loaded call", callObj})
|
||||
c.JSON(http.StatusOK, callLogResponse{"Successfully loaded call", callObj})
|
||||
}
|
||||
|
||||
func (s *Server) handleCallLogDelete(c *gin.Context) {
|
||||
|
||||
190
api/server/calls_test.go
Normal file
190
api/server/calls_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fnproject/fn/api/datastore"
|
||||
"github.com/fnproject/fn/api/id"
|
||||
"github.com/fnproject/fn/api/logs"
|
||||
"github.com/fnproject/fn/api/models"
|
||||
"github.com/fnproject/fn/api/mqs"
|
||||
"github.com/go-openapi/strfmt"
|
||||
)
|
||||
|
||||
func TestCallGet(t *testing.T) {
|
||||
buf := setLogBuffer()
|
||||
|
||||
call := &models.Call{
|
||||
ID: id.New().String(),
|
||||
AppName: "myapp",
|
||||
Path: "/thisisatest",
|
||||
Image: "fnproject/hello",
|
||||
// Delay: 0,
|
||||
Type: "sync",
|
||||
Format: "default",
|
||||
// Payload: TODO,
|
||||
Priority: new(int32), // TODO this is crucial, apparently
|
||||
Timeout: 30,
|
||||
IdleTimeout: 30,
|
||||
Memory: 256,
|
||||
BaseEnv: map[string]string{"YO": "DAWG"},
|
||||
EnvVars: map[string]string{"YO": "DAWG"},
|
||||
CreatedAt: strfmt.DateTime(time.Now()),
|
||||
URL: "http://localhost:8080/r/myapp/thisisatest",
|
||||
Method: "GET",
|
||||
}
|
||||
|
||||
rnr, cancel := testRunner(t)
|
||||
defer cancel()
|
||||
ds := datastore.NewMockInit(
|
||||
[]*models.App{
|
||||
{Name: call.AppName},
|
||||
},
|
||||
nil,
|
||||
[]*models.Call{call},
|
||||
)
|
||||
fnl := logs.NewMock()
|
||||
srv := testServer(ds, &mqs.Mock{}, fnl, rnr)
|
||||
|
||||
for i, test := range []struct {
|
||||
path string
|
||||
body string
|
||||
expectedCode int
|
||||
expectedError error
|
||||
}{
|
||||
{"/v1/apps//calls/" + call.ID, "", http.StatusBadRequest, models.ErrMissingAppName},
|
||||
{"/v1/apps/nodawg/calls/" + call.ID, "", http.StatusNotFound, models.ErrCallNotFound}, // TODO a little weird
|
||||
{"/v1/apps/myapp/calls/" + call.ID[:3], "", http.StatusNotFound, models.ErrCallNotFound},
|
||||
{"/v1/apps/myapp/calls/" + call.ID, "", http.StatusOK, nil},
|
||||
} {
|
||||
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
|
||||
|
||||
if rec.Code != test.expectedCode {
|
||||
t.Log(buf.String())
|
||||
t.Errorf("Test %d: Expected status code to be %d but was %d",
|
||||
i, test.expectedCode, rec.Code)
|
||||
}
|
||||
|
||||
if test.expectedError != nil {
|
||||
resp := getErrorResponse(t, rec)
|
||||
|
||||
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
|
||||
t.Log(buf.String())
|
||||
t.Errorf("Test %d: Expected error message to have `%s`",
|
||||
i, test.expectedError.Error())
|
||||
}
|
||||
}
|
||||
// TODO json parse the body and assert fields
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallList(t *testing.T) {
|
||||
buf := setLogBuffer()
|
||||
|
||||
call := &models.Call{
|
||||
ID: id.New().String(),
|
||||
AppName: "myapp",
|
||||
Path: "/thisisatest",
|
||||
Image: "fnproject/hello",
|
||||
// Delay: 0,
|
||||
Type: "sync",
|
||||
Format: "default",
|
||||
// Payload: TODO,
|
||||
Priority: new(int32), // TODO this is crucial, apparently
|
||||
Timeout: 30,
|
||||
IdleTimeout: 30,
|
||||
Memory: 256,
|
||||
BaseEnv: map[string]string{"YO": "DAWG"},
|
||||
EnvVars: map[string]string{"YO": "DAWG"},
|
||||
CreatedAt: strfmt.DateTime(time.Now()),
|
||||
URL: "http://localhost:8080/r/myapp/thisisatest",
|
||||
Method: "GET",
|
||||
}
|
||||
c2 := *call
|
||||
c3 := *call
|
||||
c2.ID = id.New().String()
|
||||
c2.CreatedAt = strfmt.DateTime(time.Now().Add(100 * time.Second))
|
||||
c2.Path = "test2"
|
||||
c3.ID = id.New().String()
|
||||
c3.CreatedAt = strfmt.DateTime(time.Now().Add(200 * time.Second))
|
||||
c3.Path = "/test3"
|
||||
|
||||
rnr, cancel := testRunner(t)
|
||||
defer cancel()
|
||||
ds := datastore.NewMockInit(
|
||||
[]*models.App{
|
||||
{Name: call.AppName},
|
||||
},
|
||||
nil,
|
||||
[]*models.Call{call, &c2, &c3},
|
||||
)
|
||||
fnl := logs.NewMock()
|
||||
srv := testServer(ds, &mqs.Mock{}, fnl, rnr)
|
||||
|
||||
// add / sub 1 second b/c unix time will lop off millis and mess up our comparisons
|
||||
rangeTest := fmt.Sprintf("from_time=%d&to_time=%d",
|
||||
time.Time(call.CreatedAt).Add(1*time.Second).Unix(),
|
||||
time.Time(c3.CreatedAt).Add(-1*time.Second).Unix(),
|
||||
)
|
||||
|
||||
for i, test := range []struct {
|
||||
path string
|
||||
body string
|
||||
expectedCode int
|
||||
expectedError error
|
||||
expectedLen int
|
||||
nextCursor string
|
||||
}{
|
||||
{"/v1/apps//calls", "", http.StatusBadRequest, models.ErrMissingAppName, 0, ""},
|
||||
{"/v1/apps/nodawg/calls", "", http.StatusNotFound, models.ErrAppsNotFound, 0, ""},
|
||||
{"/v1/apps/myapp/calls", "", http.StatusOK, nil, 3, ""},
|
||||
{"/v1/apps/myapp/calls?per_page=1", "", http.StatusOK, nil, 1, c3.ID},
|
||||
{"/v1/apps/myapp/calls?per_page=1&cursor=" + c3.ID, "", http.StatusOK, nil, 1, c2.ID},
|
||||
{"/v1/apps/myapp/calls?per_page=1&cursor=" + c2.ID, "", http.StatusOK, nil, 1, call.ID},
|
||||
{"/v1/apps/myapp/calls?per_page=100&cursor=" + c2.ID, "", http.StatusOK, nil, 1, ""}, // cursor is empty if per_page > len(results)
|
||||
{"/v1/apps/myapp/calls?per_page=1&cursor=" + call.ID, "", http.StatusOK, nil, 0, ""}, // cursor could point to empty page
|
||||
{"/v1/apps/myapp/calls?" + rangeTest, "", http.StatusOK, nil, 1, ""},
|
||||
{"/v1/apps/myapp/calls?from_time=xyz", "", http.StatusBadRequest, models.ErrInvalidFromTime, 0, ""},
|
||||
{"/v1/apps/myapp/calls?to_time=xyz", "", http.StatusBadRequest, models.ErrInvalidToTime, 0, ""},
|
||||
|
||||
// TODO path isn't url safe w/ '/', so this is weird. hack in for tests
|
||||
{"/v1/apps/myapp/calls?path=test2", "", http.StatusOK, nil, 1, ""},
|
||||
} {
|
||||
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
|
||||
|
||||
if rec.Code != test.expectedCode {
|
||||
t.Log(buf.String())
|
||||
t.Errorf("Test %d: Expected status code to be %d but was %d",
|
||||
i, test.expectedCode, rec.Code)
|
||||
}
|
||||
|
||||
if test.expectedError != nil {
|
||||
resp := getErrorResponse(t, rec)
|
||||
|
||||
if resp.Error == nil || !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
|
||||
t.Log(buf.String())
|
||||
t.Errorf("Test %d: Expected error message to have `%s`, got: `%s`",
|
||||
i, test.expectedError.Error(), resp.Error)
|
||||
}
|
||||
} else {
|
||||
// normal path
|
||||
|
||||
var resp callsResponse
|
||||
err := json.NewDecoder(rec.Body).Decode(&resp)
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: Expected response body to be a valid json object. err: %v", i, err)
|
||||
}
|
||||
if len(resp.Calls) != test.expectedLen {
|
||||
t.Fatalf("Test %d: Expected apps length to be %d, but got %d", i, test.expectedLen, len(resp.Calls))
|
||||
}
|
||||
if resp.NextCursor != test.nextCursor {
|
||||
t.Errorf("Test %d: Expected next_cursor to be %s, but got %s", i, test.nextCursor, resp.NextCursor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func bindRoute(c *gin.Context, method string, wroute *models.RouteWrapper) error
|
||||
}
|
||||
if method == http.MethodPost {
|
||||
if wroute.Route.Path == "" {
|
||||
return models.ErrRoutesValidationMissingPath
|
||||
return models.ErrMissingPath
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/fnproject/fn/api"
|
||||
@@ -11,24 +12,20 @@ import (
|
||||
func (s *Server) handleRouteList(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
filter := &models.RouteFilter{}
|
||||
appName := c.MustGet(api.AppName).(string)
|
||||
|
||||
if img := c.Query("image"); img != "" {
|
||||
filter.Image = img
|
||||
}
|
||||
var filter models.RouteFilter
|
||||
filter.Image = c.Query("image")
|
||||
// filter.PathPrefix = c.Query("path_prefix") TODO not hooked up
|
||||
filter.Cursor, filter.PerPage = pageParams(c, true)
|
||||
|
||||
var routes []*models.Route
|
||||
var err error
|
||||
appName, exists := c.Get(api.AppName)
|
||||
name, ok := appName.(string)
|
||||
if exists && ok && name != "" {
|
||||
routes, err = s.Datastore.GetRoutesByApp(ctx, name, filter)
|
||||
// if there are no routes for the app, check if the app exists to return 404 if it does not
|
||||
if len(routes) == 0 {
|
||||
_, err = s.Datastore.GetApp(ctx, name)
|
||||
}
|
||||
} else {
|
||||
routes, err = s.Datastore.GetRoutes(ctx, filter)
|
||||
routes, err := s.Datastore.GetRoutesByApp(ctx, appName, &filter)
|
||||
|
||||
// if there are no routes for the app, check if the app exists to return
|
||||
// 404 if it does not
|
||||
// TODO this should be done in front of this handler to even get here...
|
||||
if err == nil && len(routes) == 0 {
|
||||
_, err = s.Datastore.GetApp(ctx, appName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -36,5 +33,15 @@ func (s *Server) handleRouteList(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, routesResponse{"Successfully listed routes", routes})
|
||||
var nextCursor string
|
||||
if len(routes) > 0 && len(routes) == filter.PerPage {
|
||||
last := []byte(routes[len(routes)-1].Path)
|
||||
nextCursor = base64.RawURLEncoding.EncodeToString(last)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, routesResponse{
|
||||
Message: "Successfully listed routes",
|
||||
NextCursor: nextCursor,
|
||||
Routes: routes,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -59,10 +61,10 @@ func TestRouteCreate(t *testing.T) {
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", ``, http.StatusBadRequest, models.ErrInvalidJSON},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "path": "/myroute", "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { } }`, http.StatusBadRequest, models.ErrRoutesValidationMissingPath},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesValidationMissingImage},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/hello", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesValidationMissingPath},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/hello", "path": "myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesValidationInvalidPath},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { } }`, http.StatusBadRequest, models.ErrMissingPath},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrMissingImage},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/hello", "type": "sync" } }`, http.StatusBadRequest, models.ErrMissingPath},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/hello", "path": "myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrInvalidPath},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/$/routes", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrAppsValidationInvalidName},
|
||||
{datastore.NewMockInit(nil,
|
||||
[]*models.Route{
|
||||
@@ -87,13 +89,13 @@ func TestRoutePut(t *testing.T) {
|
||||
// errors (NOTE: this route doesn't exist yet)
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "path": "/myroute", "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesValidationMissingImage},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesValidationMissingImage},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "type": "sync" } }`, http.StatusBadRequest, models.ErrMissingImage},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrMissingImage},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "myroute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "diffRoute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/$/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrAppsValidationInvalidName},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrRoutesValidationInvalidType},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "format": "invalid-format", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesValidationInvalidFormat},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrInvalidType},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "format": "invalid-format", "type": "sync" } }`, http.StatusBadRequest, models.ErrInvalidFormat},
|
||||
|
||||
// success
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusOK, nil},
|
||||
@@ -149,18 +151,53 @@ func TestRouteList(t *testing.T) {
|
||||
rnr, cancel := testRunner(t)
|
||||
defer cancel()
|
||||
|
||||
ds := datastore.NewMock()
|
||||
ds := datastore.NewMockInit(
|
||||
[]*models.App{
|
||||
{Name: "myapp"},
|
||||
},
|
||||
[]*models.Route{
|
||||
{
|
||||
AppName: "myapp",
|
||||
Path: "/myroute",
|
||||
},
|
||||
{
|
||||
AppName: "myapp",
|
||||
Path: "/myroute1",
|
||||
},
|
||||
{
|
||||
AppName: "myapp",
|
||||
Path: "/myroute2",
|
||||
Image: "fnproject/hello",
|
||||
},
|
||||
},
|
||||
nil, // no calls
|
||||
)
|
||||
fnl := logs.NewMock()
|
||||
|
||||
r1b := base64.RawURLEncoding.EncodeToString([]byte("/myroute"))
|
||||
r2b := base64.RawURLEncoding.EncodeToString([]byte("/myroute1"))
|
||||
r3b := base64.RawURLEncoding.EncodeToString([]byte("/myroute2"))
|
||||
|
||||
srv := testServer(ds, &mqs.Mock{}, fnl, rnr)
|
||||
|
||||
for i, test := range []struct {
|
||||
path string
|
||||
body string
|
||||
path string
|
||||
body string
|
||||
|
||||
expectedCode int
|
||||
expectedError error
|
||||
expectedLen int
|
||||
nextCursor string
|
||||
}{
|
||||
{"/v1/apps/a/routes", "", http.StatusNotFound, models.ErrAppsNotFound},
|
||||
{"/v1/apps//routes", "", http.StatusBadRequest, models.ErrMissingAppName, 0, ""},
|
||||
{"/v1/apps/a/routes", "", http.StatusNotFound, models.ErrAppsNotFound, 0, ""},
|
||||
{"/v1/apps/myapp/routes", "", http.StatusOK, nil, 3, ""},
|
||||
{"/v1/apps/myapp/routes?per_page=1", "", http.StatusOK, nil, 1, r1b},
|
||||
{"/v1/apps/myapp/routes?per_page=1&cursor=" + r1b, "", http.StatusOK, nil, 1, r2b},
|
||||
{"/v1/apps/myapp/routes?per_page=1&cursor=" + r2b, "", http.StatusOK, nil, 1, r3b},
|
||||
{"/v1/apps/myapp/routes?per_page=100&cursor=" + r2b, "", http.StatusOK, nil, 1, ""}, // cursor is empty if per_page > len(results)
|
||||
{"/v1/apps/myapp/routes?per_page=1&cursor=" + r3b, "", http.StatusOK, nil, 0, ""}, // cursor could point to empty page
|
||||
{"/v1/apps/myapp/routes?image=fnproject/hello", "", http.StatusOK, nil, 1, ""},
|
||||
} {
|
||||
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
|
||||
|
||||
@@ -178,6 +215,20 @@ func TestRouteList(t *testing.T) {
|
||||
t.Errorf("Test %d: Expected error message to have `%s`",
|
||||
i, test.expectedError.Error())
|
||||
}
|
||||
} else {
|
||||
// normal path
|
||||
|
||||
var resp routesResponse
|
||||
err := json.NewDecoder(rec.Body).Decode(&resp)
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: Expected response body to be a valid json object. err: %v", i, err)
|
||||
}
|
||||
if len(resp.Routes) != test.expectedLen {
|
||||
t.Errorf("Test %d: Expected route length to be %d, but got %d", i, test.expectedLen, len(resp.Routes))
|
||||
}
|
||||
if resp.NextCursor != test.nextCursor {
|
||||
t.Errorf("Test %d: Expected next_cursor to be %s, but got %s", i, test.nextCursor, resp.NextCursor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,8 +279,8 @@ func TestRouteUpdate(t *testing.T) {
|
||||
// errors
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", ``, http.StatusBadRequest, models.ErrInvalidJSON},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{}`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrRoutesValidationInvalidType},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "format": "invalid-format" } }`, http.StatusBadRequest, models.ErrRoutesValidationInvalidFormat},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrInvalidType},
|
||||
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "format": "invalid-format" } }`, http.StatusBadRequest, models.ErrInvalidFormat},
|
||||
|
||||
// success
|
||||
{datastore.NewMockInit(nil,
|
||||
|
||||
@@ -3,12 +3,14 @@ package server
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/fnproject/fn/api"
|
||||
"github.com/fnproject/fn/api/agent"
|
||||
@@ -210,6 +212,16 @@ func loggerWrap(c *gin.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func appWrap(c *gin.Context) {
|
||||
appName := c.GetString(api.AppName)
|
||||
if appName == "" {
|
||||
handleErrorResponse(c, models.ErrMissingAppName)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func (s *Server) handleRunnerRequest(c *gin.Context) {
|
||||
s.handleRequest(c)
|
||||
}
|
||||
@@ -269,18 +281,20 @@ func (s *Server) bindHandlers(ctx context.Context) {
|
||||
engine.GET("/version", handleVersion)
|
||||
engine.GET("/stats", s.handleStats)
|
||||
|
||||
v1 := engine.Group("/v1")
|
||||
v1.Use(s.middlewareWrapperFunc(ctx))
|
||||
{
|
||||
v1 := engine.Group("/v1")
|
||||
v1.Use(s.middlewareWrapperFunc(ctx))
|
||||
v1.GET("/apps", s.handleAppList)
|
||||
v1.POST("/apps", s.handleAppCreate)
|
||||
|
||||
v1.GET("/apps/:app", s.handleAppGet)
|
||||
v1.PATCH("/apps/:app", s.handleAppUpdate)
|
||||
v1.DELETE("/apps/:app", s.handleAppDelete)
|
||||
|
||||
apps := v1.Group("/apps/:app")
|
||||
{
|
||||
apps := v1.Group("/apps/:app")
|
||||
apps.Use(appWrap)
|
||||
|
||||
apps.GET("", s.handleAppGet)
|
||||
apps.PATCH("", s.handleAppUpdate)
|
||||
apps.DELETE("", s.handleAppDelete)
|
||||
|
||||
apps.GET("/routes", s.handleRouteList)
|
||||
apps.POST("/routes", s.handleRoutesPostPutPatch)
|
||||
apps.GET("/routes/*route", s.handleRouteGet)
|
||||
@@ -293,12 +307,15 @@ func (s *Server) bindHandlers(ctx context.Context) {
|
||||
apps.GET("/calls/:call", s.handleCallGet)
|
||||
apps.GET("/calls/:call/log", s.handleCallLogGet)
|
||||
apps.DELETE("/calls/:call/log", s.handleCallLogDelete)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
engine.Any("/r/:app", s.handleRunnerRequest)
|
||||
engine.Any("/r/:app/*route", s.handleRunnerRequest)
|
||||
{
|
||||
runner := engine.Group("/r")
|
||||
runner.Use(appWrap)
|
||||
runner.Any("/:app", s.handleRunnerRequest)
|
||||
runner.Any("/:app/*route", s.handleRunnerRequest)
|
||||
}
|
||||
|
||||
engine.NoRoute(func(c *gin.Context) {
|
||||
logrus.Debugln("not found", c.Request.URL.Path)
|
||||
@@ -306,14 +323,34 @@ func (s *Server) bindHandlers(ctx context.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// returns the unescaped ?cursor and ?perPage values
|
||||
// pageParams clamps 0 < ?perPage <= 100 and defaults to 30 if 0
|
||||
// ignores parsing errors and falls back to defaults.
|
||||
func pageParams(c *gin.Context, base64d bool) (cursor string, perPage int) {
|
||||
cursor = c.Query("cursor")
|
||||
if base64d {
|
||||
cbytes, _ := base64.RawURLEncoding.DecodeString(cursor)
|
||||
cursor = string(cbytes)
|
||||
}
|
||||
|
||||
perPage, _ = strconv.Atoi(c.Query("per_page"))
|
||||
if perPage > 100 {
|
||||
perPage = 100
|
||||
} else if perPage <= 0 {
|
||||
perPage = 30
|
||||
}
|
||||
return cursor, perPage
|
||||
}
|
||||
|
||||
type appResponse struct {
|
||||
Message string `json:"message"`
|
||||
App *models.App `json:"app"`
|
||||
}
|
||||
|
||||
type appsResponse struct {
|
||||
Message string `json:"message"`
|
||||
Apps []*models.App `json:"apps"`
|
||||
Message string `json:"message"`
|
||||
NextCursor string `json:"next_cursor"`
|
||||
Apps []*models.App `json:"apps"`
|
||||
}
|
||||
|
||||
type routeResponse struct {
|
||||
@@ -322,21 +359,23 @@ type routeResponse struct {
|
||||
}
|
||||
|
||||
type routesResponse struct {
|
||||
Message string `json:"message"`
|
||||
Routes []*models.Route `json:"routes"`
|
||||
Message string `json:"message"`
|
||||
NextCursor string `json:"next_cursor"`
|
||||
Routes []*models.Route `json:"routes"`
|
||||
}
|
||||
|
||||
type fnCallResponse struct {
|
||||
type callResponse struct {
|
||||
Message string `json:"message"`
|
||||
Call *models.Call `json:"call"`
|
||||
}
|
||||
|
||||
type fnCallsResponse struct {
|
||||
Message string `json:"message"`
|
||||
Calls []*models.Call `json:"calls"`
|
||||
type callsResponse struct {
|
||||
Message string `json:"message"`
|
||||
NextCursor string `json:"next_cursor"`
|
||||
Calls []*models.Call `json:"calls"`
|
||||
}
|
||||
|
||||
type fnCallLogResponse struct {
|
||||
type callLogResponse struct {
|
||||
Message string `json:"message"`
|
||||
Log *models.CallLog `json:"log"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user