mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
Vast commit, includes: * Introduces the Trigger domain entity. * Introduces the Fns domain entity. * V2 of the API for interacting with the new entities in swaggerv2.yml * Adds v2 end points for Apps to support PUT updates. * Rewrites the datastore level tests into a new pattern. * V2 routes use entity ID over name as the path parameter.
335 lines
12 KiB
Go
335 lines
12 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/fnproject/fn/api/agent"
|
|
"github.com/fnproject/fn/api/datastore"
|
|
"github.com/fnproject/fn/api/datastore/sql"
|
|
_ "github.com/fnproject/fn/api/datastore/sql/sqlite"
|
|
"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/gin-gonic/gin"
|
|
)
|
|
|
|
var tmpDatastoreTests = "/tmp/func_test_datastore.db"
|
|
|
|
func testServer(ds models.Datastore, mq models.MessageQueue, logDB models.LogStore, rnr agent.Agent, nodeType ServerNodeType) *Server {
|
|
return New(context.Background(),
|
|
WithLogLevel(getEnv(EnvLogLevel, DefaultLogLevel)),
|
|
WithDatastore(ds),
|
|
WithMQ(mq),
|
|
WithLogstore(logDB),
|
|
WithAgent(rnr),
|
|
WithType(nodeType),
|
|
)
|
|
}
|
|
|
|
func createRequest(t *testing.T, method, path string, body io.Reader) *http.Request {
|
|
|
|
bodyLen := int64(0)
|
|
|
|
// HACK: derive content-length since protocol/http does not add content-length
|
|
// if it's not present.
|
|
if body != nil {
|
|
buf := &bytes.Buffer{}
|
|
nRead, err := io.Copy(buf, body)
|
|
if err != nil {
|
|
t.Fatalf("Test: Could not copy %s request body to %s: %v", method, path, err)
|
|
}
|
|
|
|
bodyLen = nRead
|
|
body = buf
|
|
}
|
|
|
|
req, err := http.NewRequest(method, "http://127.0.0.1:8080"+path, body)
|
|
if err != nil {
|
|
t.Fatalf("Test: Could not create %s request to %s: %v", method, path, err)
|
|
}
|
|
|
|
if body != nil {
|
|
req.ContentLength = bodyLen
|
|
req.Header.Set("Content-Length", strconv.FormatInt(bodyLen, 10))
|
|
}
|
|
|
|
return req
|
|
}
|
|
|
|
func routerRequest(t *testing.T, router *gin.Engine, method, path string, body io.Reader) (*http.Request, *httptest.ResponseRecorder) {
|
|
req := createRequest(t, method, path, body)
|
|
return routerRequest2(t, router, req)
|
|
}
|
|
|
|
func routerRequest2(_ *testing.T, router *gin.Engine, req *http.Request) (*http.Request, *httptest.ResponseRecorder) {
|
|
rec := httptest.NewRecorder()
|
|
rec.Body = new(bytes.Buffer)
|
|
router.ServeHTTP(rec, req)
|
|
return req, rec
|
|
}
|
|
|
|
func newRouterRequest(t *testing.T, method, path string, body io.Reader) (*http.Request, *httptest.ResponseRecorder) {
|
|
req := createRequest(t, method, path, body)
|
|
rec := httptest.NewRecorder()
|
|
rec.Body = new(bytes.Buffer)
|
|
return req, rec
|
|
}
|
|
|
|
func getV1ErrorResponse(t *testing.T, rec *httptest.ResponseRecorder) *models.ErrorWrapper {
|
|
var err models.ErrorWrapper
|
|
decodeErr := json.NewDecoder(rec.Body).Decode(&err)
|
|
if decodeErr != nil {
|
|
t.Error("Test: Expected not empty response body")
|
|
}
|
|
return &err
|
|
}
|
|
|
|
func getErrorResponse(t *testing.T, rec *httptest.ResponseRecorder) *models.Error {
|
|
var err models.Error
|
|
decodeErr := json.NewDecoder(rec.Body).Decode(&err)
|
|
if decodeErr != nil {
|
|
t.Error("Test: Expected not empty response body")
|
|
}
|
|
return &err
|
|
}
|
|
|
|
func prepareDB(ctx context.Context, t *testing.T) (models.Datastore, models.LogStore, func()) {
|
|
os.Remove(tmpDatastoreTests)
|
|
uri, err := url.Parse("sqlite3://" + tmpDatastoreTests)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ss, err := sql.New(ctx, uri)
|
|
if err != nil {
|
|
t.Fatalf("Error when creating datastore: %s", err)
|
|
}
|
|
logDB := logs.Wrap(ss)
|
|
ds := datastore.Wrap(ss)
|
|
|
|
return ds, logDB, func() {
|
|
os.Remove(tmpDatastoreTests)
|
|
}
|
|
}
|
|
|
|
func TestFullStack(t *testing.T) {
|
|
ctx := context.Background()
|
|
buf := setLogBuffer()
|
|
ds, logDB, close := prepareDB(ctx, t)
|
|
defer close()
|
|
|
|
rnr, rnrcancel := testRunner(t, ds)
|
|
defer rnrcancel()
|
|
|
|
srv := testServer(ds, &mqs.Mock{}, logDB, rnr, ServerTypeFull)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
body string
|
|
expectedCode int
|
|
expectedCacheSize int // TODO kill me
|
|
}{
|
|
{"create my app", "POST", "/v1/apps", `{ "app": { "name": "myapp" } }`, http.StatusOK, 0},
|
|
{"list apps", "GET", "/v1/apps", ``, http.StatusOK, 0},
|
|
{"get app", "GET", "/v1/apps/myapp", ``, http.StatusOK, 0},
|
|
// NOTE: cache is lazy, loads when a request comes in for the route, not when added
|
|
{"add myroute", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute", "path": "/myroute", "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusOK, 0},
|
|
{"add myroute2", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute2", "path": "/myroute2", "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusOK, 0},
|
|
{"get myroute", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK, 0},
|
|
{"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 0},
|
|
{"get all routes", "GET", "/v1/apps/myapp/routes", ``, http.StatusOK, 0},
|
|
{"execute myroute", "POST", "/r/myapp/myroute", `{ "echoContent": "Teste" }`, http.StatusOK, 1},
|
|
{"execute myroute2", "POST", "/r/myapp/myroute2", `{"sleepTime": 0, "isDebug": true, "isCrash": true}`, http.StatusBadGateway, 2},
|
|
{"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 2},
|
|
{"delete myroute", "DELETE", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK, 1},
|
|
{"delete myroute2", "DELETE", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 0},
|
|
{"delete app (success)", "DELETE", "/v1/apps/myapp", ``, http.StatusOK, 0},
|
|
{"get deleted app", "GET", "/v1/apps/myapp", ``, http.StatusNotFound, 0},
|
|
{"get deleteds route on deleted app", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusNotFound, 0},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, rec := routerRequest(t, srv.Router, test.method, test.path, bytes.NewBuffer([]byte(test.body)))
|
|
|
|
if rec.Code != test.expectedCode {
|
|
t.Log(buf.String())
|
|
t.Log(rec.Body.String())
|
|
t.Errorf("Test \"%s\": Expected status code to be %d but was %d",
|
|
test.name, test.expectedCode, rec.Code)
|
|
}
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func TestRunnerNode(t *testing.T) {
|
|
ctx := context.Background()
|
|
buf := setLogBuffer()
|
|
ds, logDB, close := prepareDB(ctx, t)
|
|
defer close()
|
|
|
|
rnr, rnrcancel := testRunner(t, ds)
|
|
defer rnrcancel()
|
|
|
|
// Add route with an API server using the same DB
|
|
{
|
|
apiServer := testServer(ds, &mqs.Mock{}, logDB, nil, ServerTypeAPI)
|
|
_, rec := routerRequest(t, apiServer.Router, "POST", "/v1/apps/myapp/routes", bytes.NewBuffer([]byte(`{ "route": { "name": "myroute", "path": "/myroute", "image": "fnproject/fn-test-utils", "type": "sync" } }`)))
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("Expected status code 200 when creating sync route, but got %d", rec.Code)
|
|
}
|
|
_, rec = routerRequest(t, apiServer.Router, "POST", "/v1/apps/myapp/routes", bytes.NewBuffer([]byte(`{ "route": { "name": "myasyncroute", "path": "/myasyncroute", "image": "fnproject/fn-test-utils", "type": "async" } }`)))
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("Expected status code 200 when creating async route, but got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
srv := testServer(ds, &mqs.Mock{}, logDB, rnr, ServerTypeRunner)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
body string
|
|
expectedCode int
|
|
expectedCacheSize int // TODO kill me
|
|
}{
|
|
// Support sync and async API calls
|
|
{"execute sync route succeeds", "POST", "/r/myapp/myroute", `{ "echoContent": "Teste" }`, http.StatusOK, 1},
|
|
{"execute async route succeeds", "POST", "/r/myapp/myasyncroute", `{ "echoContent": "Teste" }`, http.StatusAccepted, 1},
|
|
|
|
// All other API functions should not be available on runner nodes
|
|
{"create app not found", "POST", "/v1/apps", `{ "app": { "name": "myapp" } }`, http.StatusBadRequest, 0},
|
|
{"list apps not found", "GET", "/v1/apps", ``, http.StatusBadRequest, 0},
|
|
{"get app not found", "GET", "/v1/apps/myapp", ``, http.StatusBadRequest, 0},
|
|
|
|
{"add route not found", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute", "path": "/myroute", "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusBadRequest, 0},
|
|
{"get route not found", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusBadRequest, 0},
|
|
{"get all routes not found", "GET", "/v1/apps/myapp/routes", ``, http.StatusBadRequest, 0},
|
|
{"delete app not found", "DELETE", "/v1/apps/myapp", ``, http.StatusBadRequest, 0},
|
|
} {
|
|
_, rec := routerRequest(t, srv.Router, test.method, test.path, bytes.NewBuffer([]byte(test.body)))
|
|
|
|
if rec.Code != test.expectedCode {
|
|
t.Log(buf.String())
|
|
t.Errorf("Test \"%s\": Expected status code to be %d but was %d",
|
|
test.name, test.expectedCode, rec.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestApiNode(t *testing.T) {
|
|
ctx := context.Background()
|
|
buf := setLogBuffer()
|
|
ds, logDB, close := prepareDB(ctx, t)
|
|
defer close()
|
|
|
|
srv := testServer(ds, &mqs.Mock{}, logDB, nil, ServerTypeAPI)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
body string
|
|
expectedCode int
|
|
expectedCacheSize int // TODO kill me
|
|
}{
|
|
// All routes should be supported
|
|
{"create my app", "POST", "/v1/apps", `{ "app": { "name": "myapp" } }`, http.StatusOK, 0},
|
|
{"list apps", "GET", "/v1/apps", ``, http.StatusOK, 0},
|
|
{"get app", "GET", "/v1/apps/myapp", ``, http.StatusOK, 0},
|
|
|
|
{"add myroute", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute", "path": "/myroute", "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusOK, 0},
|
|
{"add myroute2", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute2", "path": "/myroute2", "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusOK, 0},
|
|
{"add myasyncroute", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myasyncroute", "path": "/myasyncroute", "image": "fnproject/fn-test-utils", "type": "async" } }`, http.StatusOK, 0},
|
|
{"get myroute", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK, 0},
|
|
{"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 0},
|
|
{"get all routes", "GET", "/v1/apps/myapp/routes", ``, http.StatusOK, 0},
|
|
|
|
// Don't support calling sync or async
|
|
{"execute myroute", "POST", "/r/myapp/myroute", `{ "echoContent": "Teste" }`, http.StatusBadRequest, 1},
|
|
{"execute myroute2", "POST", "/r/myapp/myroute2", `{ "echoContent": "Teste" }`, http.StatusBadRequest, 2},
|
|
{"execute myasyncroute", "POST", "/r/myapp/myasyncroute", `{ "echoContent": "Teste" }`, http.StatusBadRequest, 1},
|
|
|
|
{"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 2},
|
|
{"delete myroute", "DELETE", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK, 1},
|
|
{"delete myroute2", "DELETE", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 0},
|
|
{"delete app (success)", "DELETE", "/v1/apps/myapp", ``, http.StatusOK, 0},
|
|
{"get deleted app", "GET", "/v1/apps/myapp", ``, http.StatusNotFound, 0},
|
|
{"get deleted route on deleted app", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusNotFound, 0},
|
|
} {
|
|
_, rec := routerRequest(t, srv.Router, test.method, test.path, bytes.NewBuffer([]byte(test.body)))
|
|
if rec.Code != test.expectedCode {
|
|
t.Log(buf.String())
|
|
t.Errorf("Test \"%s\": Expected status code to be %d but was %d",
|
|
test.name, test.expectedCode, rec.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHybridEndpoints(t *testing.T) {
|
|
buf := setLogBuffer()
|
|
app := &models.App{ID: "app_id", Name: "myapp"}
|
|
ds := datastore.NewMockInit(
|
|
[]*models.App{app},
|
|
[]*models.Route{{
|
|
AppID: app.ID,
|
|
Path: "yodawg",
|
|
}},
|
|
)
|
|
|
|
logDB := logs.NewMock()
|
|
|
|
srv := testServer(ds, &mqs.Mock{}, logDB, nil /* TODO */, ServerTypeAPI)
|
|
|
|
newCallBody := func() string {
|
|
call := &models.Call{
|
|
AppID: app.ID,
|
|
ID: id.New().String(),
|
|
Path: "yodawg",
|
|
// TODO ?
|
|
}
|
|
var b bytes.Buffer
|
|
json.NewEncoder(&b).Encode(&call)
|
|
return b.String()
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
body string
|
|
expectedCode int
|
|
}{
|
|
// TODO change all these tests to just do an async task in normal order once plumbing is done
|
|
|
|
{"post async call", "PUT", "/v1/runner/async", newCallBody(), http.StatusOK},
|
|
|
|
// TODO this one only works if it's not the same as the first since update isn't hooked up
|
|
{"finish call", "POST", "/v1/runner/finish", newCallBody(), http.StatusOK},
|
|
|
|
// TODO these won't work until update works and the agent gets shut off
|
|
//{"get async call", "GET", "/v1/runner/async", "", http.StatusOK},
|
|
//{"start call", "POST", "/v1/runner/start", "TODO", http.StatusOK},
|
|
} {
|
|
_, rec := routerRequest(t, srv.Router, test.method, test.path, strings.NewReader(test.body))
|
|
|
|
if rec.Code != test.expectedCode {
|
|
t.Log(buf.String())
|
|
t.Errorf("Test \"%s\": Expected status code to be %d but was %d",
|
|
test.name, test.expectedCode, rec.Code)
|
|
}
|
|
}
|
|
}
|