Files
fn-serverless/api/server/fns_test.go
James Jeffrey d336035678 Add annotation to trigger on create if endpoints are enabled (#1177)
* Add annotations for creation of triggers and fns along with the test for them fixes #1178

* Log errors and still return created resource for annotation failures
2018-08-21 10:26:36 +01:00

434 lines
14 KiB
Go

package server
import (
"bytes"
"encoding/base64"
"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"
)
type funcTestCase struct {
ds models.Datastore
logDB models.LogStore
method string
path string
body string
expectedCode int
expectedError error
}
func (test *funcTestCase) run(t *testing.T, i int, buf *bytes.Buffer) {
rnr, cancel := testRunner(t)
srv := testServer(test.ds, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
body := bytes.NewBuffer([]byte(test.body))
_, rec := routerRequest(t, srv.Router, test.method, test.path, body)
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Log(rec.Body.String())
t.Fatalf("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 == nil {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`, but it was nil",
i, test.expectedError)
} else if resp.Message != test.expectedError.Error() {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`, but it was `%s`",
i, test.expectedError, resp.Message)
}
}
if test.expectedCode == http.StatusOK {
var fn models.Fn
err := json.NewDecoder(rec.Body).Decode(&fn)
if err != nil {
t.Log(buf.String())
t.Errorf("Test %d: error decoding body for 'ok' json, it was a lie: %v", i, err)
}
if test.method == http.MethodPut {
// IsZero() doesn't really work, this ensures it's not unset as long as we're not in 1970
if time.Time(fn.CreatedAt).Before(time.Now().Add(-1 * time.Hour)) {
t.Log(buf.String())
t.Errorf("Test %d: expected created_at to be set on func, it wasn't: %s", i, fn.CreatedAt)
}
if time.Time(fn.UpdatedAt).Before(time.Now().Add(-1 * time.Hour)) {
t.Log(buf.String())
t.Errorf("Test %d: expected updated_at to be set on func, it wasn't: %s", i, fn.UpdatedAt)
}
if fn.ID == "" {
t.Log(buf.String())
t.Errorf("Test %d: expected id to be non-empty, it was empty: %v", i, fn)
}
}
}
cancel()
buf.Reset()
}
func TestFnCreate(t *testing.T) {
buf := setLogBuffer()
a := &models.App{Name: "a", ID: "aid"}
ds := datastore.NewMockInit([]*models.App{a})
ls := logs.NewMock()
for i, test := range []funcTestCase{
// errors
{ds, ls, http.MethodPost, "/v2/fns", ``, http.StatusBadRequest, models.ErrInvalidJSON},
{ds, ls, http.MethodPost, "/v2/fns", `{ }`, http.StatusBadRequest, models.ErrFnsMissingAppID},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s" }`, a.ID), http.StatusBadRequest, models.ErrFnsMissingName},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "a" }`, a.ID), http.StatusBadRequest, models.ErrFnsMissingImage},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": " ", "image": "fnproject/fn-test-utils" }`, a.ID), http.StatusBadRequest, models.ErrFnsInvalidName},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "a", "image": "fnproject/fn-test-utils", "format": "wazzup" }`, a.ID), http.StatusBadRequest, models.ErrFnsInvalidFormat},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "a", "image": "fnproject/fn-test-utils", "timeout": 3601 }`, a.ID), http.StatusBadRequest, models.ErrFnsInvalidTimeout},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "a", "image": "fnproject/fn-test-utils", "idle_timeout": 3601 }`, a.ID), http.StatusBadRequest, models.ErrFnsInvalidIdleTimeout},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "a", "image": "fnproject/fn-test-utils", "memory": 100000000000000 }`, a.ID), http.StatusBadRequest, models.ErrInvalidMemory},
// success create & update
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "myfunc", "image": "fnproject/fn-test-utils" }`, a.ID), http.StatusOK, nil},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "myfunc", "image": "fnproject/fn-test-utils" }`, a.ID), http.StatusConflict, models.ErrFnsExists},
} {
test.run(t, i, buf)
}
}
func TestFnUpdate(t *testing.T) {
buf := setLogBuffer()
a := &models.App{Name: "a", ID: "app_id"}
f := &models.Fn{ID: "fn_id", Name: "f", AppID: a.ID}
f.SetDefaults()
ds := datastore.NewMockInit([]*models.App{a}, []*models.Fn{f})
ls := logs.NewMock()
for i, test := range []funcTestCase{
{ds, ls, http.MethodPut, "/v2/fns/missing", `{ }`, http.StatusNotFound, models.ErrFnsNotFound},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "id": "nottheid" }`, http.StatusBadRequest, models.ErrFnsIDMismatch},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "image": "fnproject/test" }`, http.StatusOK, nil},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "format": "http" }`, http.StatusOK, nil},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "memory": 1000 }`, http.StatusOK, nil},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "timeout": 10 }`, http.StatusOK, nil},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "idle_timeout": 10 }`, http.StatusOK, nil},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "config": {"k":"v"} }`, http.StatusOK, nil},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "annotations": {"k":"v"} }`, http.StatusOK, nil},
// test that partial update fails w/ same errors as create
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "format": "wazzup" }`, http.StatusBadRequest, models.ErrFnsInvalidFormat},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "timeout": 3601 }`, http.StatusBadRequest, models.ErrFnsInvalidTimeout},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "idle_timeout": 3601 }`, http.StatusBadRequest, models.ErrFnsInvalidIdleTimeout},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "memory": 100000000000000 }`, http.StatusBadRequest, models.ErrInvalidMemory},
} {
test.run(t, i, buf)
}
}
func TestFnDelete(t *testing.T) {
buf := setLogBuffer()
a := &models.App{Name: "a", ID: "appid"}
f := &models.Fn{ID: "fn_id", Name: "myfunc", AppID: a.ID}
f.SetDefaults()
commonDS := datastore.NewMockInit([]*models.App{a}, []*models.Fn{f})
for i, test := range []struct {
ds models.Datastore
logDB models.LogStore
path string
body string
expectedCode int
expectedError error
}{
{commonDS, logs.NewMock(), "/v2/fns/missing", "", http.StatusNotFound, models.ErrFnsNotFound},
{commonDS, logs.NewMock(), fmt.Sprintf("/v2/fns/%s", f.ID), "", http.StatusNoContent, nil},
} {
rnr, cancel := testRunner(t)
srv := testServer(test.ds, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
_, rec := routerRequest(t, srv.Router, "DELETE", test.path, nil)
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Log(rec.Body.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.Message, test.expectedError.Error()) {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
}
}
cancel()
}
}
func TestFnList(t *testing.T) {
buf := setLogBuffer()
rnr, cancel := testRunner(t)
defer cancel()
// ids are sortable, need to test cursoring works as expected
r1b := id.New().String()
r2b := id.New().String()
r3b := id.New().String()
r4b := id.New().String()
fn1 := "myfunc1"
fn2 := "myfunc2"
fn3 := "myfunc3"
fn4 := "myfunc3"
app1 := &models.App{Name: "myapp1", ID: "app_id1"}
app2 := &models.App{Name: "myapp2", ID: "app_id2"}
ds := datastore.NewMockInit(
[]*models.App{app1, app2},
[]*models.Fn{
{
ID: r1b,
Name: fn1,
AppID: app1.ID,
Image: "fnproject/fn-test-utils",
},
{
ID: r2b,
Name: fn2,
AppID: app1.ID,
Image: "fnproject/fn-test-utils",
},
{
ID: r3b,
Name: fn3,
AppID: app1.ID,
Image: "fnproject/yo",
},
{
ID: r4b,
Name: fn4,
AppID: app2.ID,
Image: "fnproject/foo",
},
},
)
fnl := logs.NewMock()
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
fn1b := base64.RawURLEncoding.EncodeToString([]byte(fn1))
fn2b := base64.RawURLEncoding.EncodeToString([]byte(fn2))
fn3b := base64.RawURLEncoding.EncodeToString([]byte(fn3))
for i, test := range []struct {
path string
body string
expectedCode int
expectedError error
expectedLen int
nextCursor string
}{
{"/v2/fns", "", http.StatusBadRequest, models.ErrFnsMissingAppID, 0, ""},
{fmt.Sprintf("/v2/fns?app_id=%s", app1.ID), "", http.StatusOK, nil, 3, ""},
{fmt.Sprintf("/v2/fns?app_id=%s&per_page=1", app1.ID), "", http.StatusOK, nil, 1, fn1b},
{fmt.Sprintf("/v2/fns?app_id=%s&per_page=1&cursor=%s", app1.ID, fn1b), "", http.StatusOK, nil, 1, fn2b},
{fmt.Sprintf("/v2/fns?app_id=%s&per_page=1&cursor=%s", app1.ID, fn2b), "", http.StatusOK, nil, 1, fn3b},
{fmt.Sprintf("/v2/fns?app_id=%s&per_page=100&cursor=%s", app1.ID, fn3b), "", http.StatusOK, nil, 0, ""}, // cursor is empty if per_page > len(results)
{fmt.Sprintf("/v2/fns?app_id=%s&per_page=1&cursor=%s", app1.ID, fn3b), "", http.StatusOK, nil, 0, ""}, // cursor could point to empty page
} {
_, 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.Message, test.expectedError.Error()) {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
}
} else {
// normal path
var resp models.FnList
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.Items) != test.expectedLen {
t.Errorf("Test %d: Expected fns length to be %d, but got %d", i, test.expectedLen, len(resp.Items))
}
if resp.NextCursor != test.nextCursor {
t.Errorf("Test %d: Expected next_cursor to be %s, but got %s", i, test.nextCursor, resp.NextCursor)
}
}
}
}
func TestFnGet(t *testing.T) {
buf := setLogBuffer()
rnr, cancel := testRunner(t)
defer cancel()
app := &models.App{Name: "myapp", ID: "appid"}
ds := datastore.NewMockInit(
[]*models.App{app},
[]*models.Fn{
{
ID: "myfnId",
Name: "myfunc",
AppID: "appid",
Image: "fnproject/fn-test-utils",
},
})
fnl := logs.NewMock()
nilFn := new(models.Fn)
expectedFn := &models.Fn{
ID: "myfnId",
Name: "myfunc",
Image: "fnproject/fn-test-utils"}
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
for i, test := range []struct {
path string
body string
expectedCode int
expectedError error
desiredFn *models.Fn
}{
{"/v2/fns/missing", "", http.StatusNotFound, models.ErrFnsNotFound, nilFn},
{"/v2/fns/myfnId", "", http.StatusOK, nil, expectedFn},
} {
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Fatalf("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.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.Message)
}
}
if !test.desiredFn.Equals(nilFn) {
var fn models.Fn
err := json.NewDecoder(rec.Body).Decode(&fn)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if test.desiredFn.Equals(&fn) {
t.Errorf("Test %d: Expected fn [%v] got [%v]", i, test.desiredFn, fn)
}
}
}
}
func TestFnInvokeEndpointAnnotations(t *testing.T) {
a := &models.App{ID: "app_id", Name: "myapp"}
fn := &models.Fn{AppID: a.ID, Name: "fnname", Image: "fnproject/image"}
commonDS := datastore.NewMockInit([]*models.App{a})
srv := testServer(commonDS, &mqs.Mock{}, logs.NewMock(), nil, ServerTypeAPI)
body, err := json.Marshal(fn)
if err != nil {
t.Fatalf("Failed to marshal json to create fn %s", err)
}
_, createFN := routerRequest(t, srv.Router, "POST", "/v2/fns", bytes.NewReader(body))
if createFN.Code != http.StatusOK {
t.Fatalf("expected code %d != 200 %s", createFN.Code, createFN.Body.String())
}
var fnCreate models.Fn
err = json.NewDecoder(createFN.Body).Decode(&fnCreate)
if err != nil {
t.Fatalf("Invalid json from server %s", err)
}
const fnEndpoint = "fnproject.io/fn/invokeEndpoint"
v, err := fnCreate.Annotations.GetString(fnEndpoint)
if err != nil {
t.Errorf("failed to get fn %s", err)
}
if v != "http://127.0.0.1:8080/invoke/"+fnCreate.ID {
t.Errorf("unexpected fn val %s", v)
}
_, rec := routerRequest(t, srv.Router, "GET", "/v2/fns/"+fnCreate.ID, bytes.NewBuffer([]byte("")))
if rec.Code != http.StatusOK {
t.Fatalf("expected code %d != 200", rec.Code)
}
var fnGet models.Fn
err = json.NewDecoder(rec.Body).Decode(&fnGet)
if err != nil {
t.Fatalf("Invalid json from server %s", err)
}
v, err = fnGet.Annotations.GetString(fnEndpoint)
if err != nil {
t.Fatalf("failed to get fn %s", err)
}
if v != "http://127.0.0.1:8080/invoke/"+fnCreate.ID {
t.Errorf("unexpected fn val %s", v)
}
_, rec = routerRequest(t, srv.Router, "GET", fmt.Sprintf("/v2/fns?app_id=%s", a.ID), nil)
if rec.Code != http.StatusOK {
t.Fatalf("expected code %d != 200", rec.Code)
}
var resp models.FnList
err = json.NewDecoder(rec.Body).Decode(&resp)
if err != nil {
t.Fatalf("Invalid json from server %s : %s", err, string(rec.Body.Bytes()))
}
if len(resp.Items) != 1 {
t.Fatalf("Unexpected fn list result, %v", resp)
}
v, err = resp.Items[0].Annotations.GetString(fnEndpoint)
if v != "http://127.0.0.1:8080/invoke/"+fnCreate.ID {
t.Errorf("unexpected fn val %s", v)
}
}