From 3fd3da87f3abb65a673f2d7054f2b095c6784849 Mon Sep 17 00:00:00 2001 From: Jordan Krage Date: Wed, 1 Mar 2017 10:40:08 -0600 Subject: [PATCH] Datastore tests (#551) * common datastore tests * fix Datastore.UpdateApp * remove extra datastore tests * datastore test fixes --- Makefile | 2 +- api/datastore/bolt/bolt.go | 48 +- api/datastore/bolt/bolt_test.go | 24 + api/datastore/bolt_test.go | 286 ----------- api/datastore/internal/datastoretest/test.go | 477 +++++++++++++++++++ api/datastore/postgres/postgres.go | 302 +++++++----- api/datastore/postgres/postgres_test.go | 98 ++++ api/datastore/postgres_test.go | 323 ------------- api/models/app.go | 16 + api/models/route.go | 49 ++ 10 files changed, 852 insertions(+), 773 deletions(-) create mode 100644 api/datastore/bolt/bolt_test.go delete mode 100644 api/datastore/bolt_test.go create mode 100644 api/datastore/internal/datastoretest/test.go create mode 100644 api/datastore/postgres/postgres_test.go delete mode 100644 api/datastore/postgres_test.go diff --git a/Makefile b/Makefile index 03f2a30c4..37b567d5d 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ test: cd fn && $(MAKE) test test-datastore: - cd api/datastore && go test -v + cd api/datastore && go test -v ./... run: ./functions diff --git a/api/datastore/bolt/bolt.go b/api/datastore/bolt/bolt.go index a7428b69c..247107497 100644 --- a/api/datastore/bolt/bolt.go +++ b/api/datastore/bolt/bolt.go @@ -141,10 +141,7 @@ func (ds *BoltDatastore) UpdateApp(ctx context.Context, newapp *models.App) (*mo return err } - // Update app fields - if newapp.Config != nil { - app.Config = newapp.Config - } + app.UpdateConfig(newapp.Config) buf, err := json.Marshal(app) if err != nil { @@ -252,7 +249,7 @@ func (ds *BoltDatastore) getRouteBucketForApp(tx *bolt.Tx, appName string) (*bol func (ds *BoltDatastore) InsertRoute(ctx context.Context, route *models.Route) (*models.Route, error) { if route == nil { - return nil, models.ErrDatastoreEmptyApp + return nil, models.ErrDatastoreEmptyRoute } if route.AppName == "" { @@ -325,46 +322,15 @@ func (ds *BoltDatastore) UpdateRoute(ctx context.Context, newroute *models.Route if err != nil { return err } - // Update route fields - if newroute.Image != "" { - route.Image = newroute.Image - } - if route.Memory != 0 { - route.Memory = newroute.Memory - } - if route.Type != "" { - route.Type = newroute.Type - } - if newroute.Timeout != 0 { - route.Timeout = newroute.Timeout - } - if newroute.Format != "" { - route.Format = newroute.Format - } - if newroute.MaxConcurrency != 0 { - route.MaxConcurrency = newroute.MaxConcurrency - } - if newroute.Headers != nil { - route.Headers = newroute.Headers - } - if newroute.Config != nil { - route.Config = newroute.Config - } - if err := route.Validate(); err != nil { - return err - } + route.Update(newroute) buf, err := json.Marshal(route) if err != nil { return err } - err = b.Put(routePath, buf) - if err != nil { - return err - } - return nil + return b.Put(routePath, buf) }) if err != nil { return nil, err @@ -431,9 +397,9 @@ func (ds *BoltDatastore) GetRoute(ctx context.Context, appName, routePath string func (ds *BoltDatastore) GetRoutesByApp(ctx context.Context, appName string, filter *models.RouteFilter) ([]*models.Route, error) { res := []*models.Route{} err := ds.db.View(func(tx *bolt.Tx) error { - b, err := ds.getRouteBucketForApp(tx, appName) - if err != nil { - return err + b := tx.Bucket(ds.routesBucket).Bucket([]byte(appName)) + if b == nil { + return nil } i := 0 diff --git a/api/datastore/bolt/bolt_test.go b/api/datastore/bolt/bolt_test.go new file mode 100644 index 000000000..02854b378 --- /dev/null +++ b/api/datastore/bolt/bolt_test.go @@ -0,0 +1,24 @@ +package bolt + +import ( + "net/url" + "os" + "testing" + + "github.com/iron-io/functions/api/datastore/internal/datastoretest" +) + +const tmpBolt = "/tmp/func_test_bolt.db" + +func TestDatastore(t *testing.T) { + os.Remove(tmpBolt) + u, err := url.Parse("bolt://" + tmpBolt) + if err != nil { + t.Fatalf("failed to parse url:", err) + } + ds, err := New(u) + if err != nil { + t.Fatalf("failed to create bolt datastore:", err) + } + datastoretest.Test(t, ds) +} diff --git a/api/datastore/bolt_test.go b/api/datastore/bolt_test.go deleted file mode 100644 index 464c30b5b..000000000 --- a/api/datastore/bolt_test.go +++ /dev/null @@ -1,286 +0,0 @@ -package datastore - -import ( - "context" - "os" - "testing" - - "github.com/iron-io/functions/api/models" -) - -const tmpBolt = "/tmp/func_test_bolt.db" - -func TestBolt(t *testing.T) { - buf := setLogBuffer() - - ctx := context.Background() - - os.Remove(tmpBolt) - ds, err := New("bolt://" + tmpBolt) - if err != nil { - t.Fatalf("Error when creating datastore: %v", err) - } - - // Testing insert app - _, err = ds.InsertApp(ctx, nil) - if err != models.ErrDatastoreEmptyApp { - t.Log(buf.String()) - t.Fatalf("Test InsertApp(nil): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyApp, err) - } - - _, err = ds.InsertApp(ctx, &models.App{}) - if err != models.ErrDatastoreEmptyAppName { - t.Log(buf.String()) - t.Fatalf("Test InsertApp(nil): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) - } - - _, err = ds.InsertApp(ctx, testApp) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test InsertApp: error when Bolt was storing new app: %s", err) - } - - _, err = ds.InsertApp(ctx, testApp) - if err != models.ErrAppsAlreadyExists { - t.Log(buf.String()) - t.Fatalf("Test InsertApp duplicated: expected error `%v`, but it was `%v`", models.ErrAppsAlreadyExists, err) - } - - _, err = ds.UpdateApp(ctx, &models.App{ - Name: testApp.Name, - Config: map[string]string{ - "TEST": "1", - }, - }) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test UpdateApp: error when Bolt was updating app: %v", err) - } - - // Testing get app - _, err = ds.GetApp(ctx, "") - if err != models.ErrDatastoreEmptyAppName { - t.Log(buf.String()) - t.Fatalf("Test GetApp: expected error to be %v, but it was %s", models.ErrDatastoreEmptyAppName, err) - } - - app, err := ds.GetApp(ctx, testApp.Name) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test GetApp: unexpected error: %s", err) - } - if app.Name != testApp.Name { - t.Log(buf.String()) - t.Fatalf("Test GetApp: expected `app.Name` to be `%s` but it was `%s`", app.Name, testApp.Name) - } - - // Testing list apps - apps, err := ds.GetApps(ctx, &models.AppFilter{}) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test GetApps: unexpected error %v", err) - } - if len(apps) == 0 { - t.Fatal("Test GetApps: expected result count to be greater than 0") - } - if apps[0].Name != testApp.Name { - t.Log(buf.String()) - t.Fatalf("Test GetApps: expected `app.Name` to be `%s` but it was `%s`", app.Name, testApp.Name) - } - - // Testing app delete - err = ds.RemoveApp(ctx, "") - if err != models.ErrDatastoreEmptyAppName { - t.Log(buf.String()) - t.Fatalf("Test RemoveApp: expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) - } - - err = ds.RemoveApp(ctx, testApp.Name) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test RemoveApp: error: %s", err) - } - app, err = ds.GetApp(ctx, testApp.Name) - if err != models.ErrAppsNotFound { - t.Log(buf.String()) - t.Fatalf("Test GetApp: expected error to be `%v`, but it was `%v`", models.ErrAppsNotFound, err) - } - if app != nil { - t.Log(buf.String()) - t.Fatalf("Test RemoveApp: failed to remove the app") - } - - // Test update inexistent app - _, err = ds.UpdateApp(ctx, &models.App{ - Name: testApp.Name, - Config: map[string]string{ - "TEST": "1", - }, - }) - if err != models.ErrAppsNotFound { - t.Log(buf.String()) - t.Fatalf("Test UpdateApp inexistent: expected error to be %v, but it was %v", models.ErrAppsNotFound, err) - } - - // Insert app again to test routes - ds.InsertApp(ctx, testApp) - - // Testing insert route - _, err = ds.InsertRoute(ctx, nil) - if err == models.ErrDatastoreEmptyRoute { - t.Log(buf.String()) - t.Fatalf("Test InsertRoute(nil): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyRoute, err) - } - - _, err = ds.InsertRoute(ctx, testRoute) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test InsertRoute: error when Bolt was storing new route: %s", err) - } - - _, err = ds.InsertRoute(ctx, testRoute) - if err != models.ErrRoutesAlreadyExists { - t.Log(buf.String()) - t.Fatalf("Test InsertRoute duplicated: expected error to be `%v`, but it was `%v`", models.ErrRoutesAlreadyExists, err) - } - - _, err = ds.UpdateRoute(ctx, testRoute) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test UpdateRoute: unexpected error: %v", err) - } - - // Testing get - _, err = ds.GetRoute(ctx, "a", "") - if err != models.ErrDatastoreEmptyRoutePath { - t.Log(buf.String()) - t.Fatalf("Test GetRoute(empty route path): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyRoutePath, err) - } - - _, err = ds.GetRoute(ctx, "", "a") - if err != models.ErrDatastoreEmptyAppName { - t.Log(buf.String()) - t.Fatalf("Test GetRoute(empty app name): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) - } - - route, err := ds.GetRoute(ctx, testApp.Name, testRoute.Path) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test GetRoute: unexpected error %v", err) - } - if route.Path != testRoute.Path { - t.Log(buf.String()) - t.Fatalf("Test GetRoute: expected `route.Path` to be `%s` but it was `%s`", route.Path, testRoute.Path) - } - - // Testing list routes - routes, err := ds.GetRoutesByApp(ctx, testApp.Name, &models.RouteFilter{}) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test GetRoutes: unexpected error %v", err) - } - if len(routes) == 0 { - t.Fatal("Test GetRoutes: expected result count to be greater than 0") - } - if routes[0].Path != testRoute.Path { - t.Log(buf.String()) - t.Fatalf("Test GetRoutes: expected `app.Name` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path) - } - - // Testing list routes - routes, err = ds.GetRoutes(ctx, &models.RouteFilter{Image: testRoute.Image}) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test GetRoutes: error: %s", err) - } - if len(routes) == 0 { - t.Fatal("Test GetRoutes: expected result count to be greater than 0") - } - if routes[0].Path != testRoute.Path { - t.Log(buf.String()) - t.Fatalf("Test GetRoutes: expected `app.Name` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path) - } - - apps, err = ds.GetApps(ctx, &models.AppFilter{Name: "Tes%"}) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test GetApps(filter): unexpected error %v", err) - } - if len(apps) == 0 { - t.Fatal("Test GetApps(filter): expected result count to be greater than 0") - } - - // Testing app delete - err = ds.RemoveRoute(ctx, "", "") - if err != models.ErrDatastoreEmptyAppName { - t.Log(buf.String()) - t.Fatalf("Test RemoveRoute(empty app name): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) - } - - err = ds.RemoveRoute(ctx, "a", "") - if err != models.ErrDatastoreEmptyRoutePath { - t.Log(buf.String()) - t.Fatalf("Test RemoveRoute(empty route path): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyRoutePath, err) - } - - err = ds.RemoveRoute(ctx, testRoute.AppName, testRoute.Path) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test RemoveApp: unexpected error: %v", err) - } - - _, err = ds.UpdateRoute(ctx, &models.Route{ - AppName: testRoute.AppName, - Path: testRoute.Path, - Image: "test", - }) - if err != models.ErrRoutesNotFound { - t.Log(buf.String()) - t.Fatalf("Test UpdateRoute inexistent: expected error to be `%v`, but it was `%v`", models.ErrRoutesNotFound, err) - } - - route, err = ds.GetRoute(ctx, testRoute.AppName, testRoute.Path) - if err != models.ErrRoutesNotFound { - t.Log(buf.String()) - t.Fatalf("Test RemoveApp: failed to remove the route") - } - - // Testing Put/Get - err = ds.Put(ctx, nil, nil) - if err != models.ErrDatastoreEmptyKey { - t.Log(buf.String()) - t.Fatalf("Test Put(nil,nil): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyKey, err) - } - - err = ds.Put(ctx, []byte("test"), []byte("success")) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test Put: unexpected error: %v", err) - } - - val, err := ds.Get(ctx, []byte("test")) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test Put: unexpected error: %v", err) - } - if string(val) != "success" { - t.Log(buf.String()) - t.Fatalf("Test Get: expected value to be `%v`, but it was `%v`", "success", string(val)) - } - - err = ds.Put(ctx, []byte("test"), nil) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test Put: unexpected error: %v", err) - } - - val, err = ds.Get(ctx, []byte("test")) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test Put: unexpected error: %v", err) - } - if string(val) != "" { - t.Log(buf.String()) - t.Fatalf("Test Get: expected value to be `%v`, but it was `%v`", "", string(val)) - } -} diff --git a/api/datastore/internal/datastoretest/test.go b/api/datastore/internal/datastoretest/test.go new file mode 100644 index 000000000..df81222d4 --- /dev/null +++ b/api/datastore/internal/datastoretest/test.go @@ -0,0 +1,477 @@ +package datastoretest + +import ( + "bytes" + "context" + "log" + "testing" + + "github.com/iron-io/functions/api/models" + + "github.com/Sirupsen/logrus" + "github.com/gin-gonic/gin" + "reflect" + "net/http" +) + +func setLogBuffer() *bytes.Buffer { + var buf bytes.Buffer + buf.WriteByte('\n') + logrus.SetOutput(&buf) + gin.DefaultErrorWriter = &buf + gin.DefaultWriter = &buf + log.SetOutput(&buf) + return &buf +} + +func Test(t *testing.T, ds models.Datastore) { + buf := setLogBuffer() + + ctx := context.Background() + + t.Run("apps", func(t *testing.T) { + // Testing insert app + _, err := ds.InsertApp(ctx, nil) + if err != models.ErrDatastoreEmptyApp { + t.Log(buf.String()) + t.Fatalf("Test InsertApp(nil): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyApp, err) + } + + _, err = ds.InsertApp(ctx, &models.App{}) + if err != models.ErrDatastoreEmptyAppName { + t.Log(buf.String()) + t.Fatalf("Test InsertApp(&{}): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) + } + + inserted, err := ds.InsertApp(ctx, testApp) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test InsertApp: error when storing new app: %s", err) + } + if !reflect.DeepEqual(*inserted, *testApp) { + t.Log(buf.String()) + t.Fatalf("Test InsertApp: expected to insert:\n%v\nbut got:\n%v", testApp, inserted) + } + + _, err = ds.InsertApp(ctx, testApp) + if err != models.ErrAppsAlreadyExists { + t.Log(buf.String()) + t.Fatalf("Test InsertApp duplicated: expected error `%v`, but it was `%v`", models.ErrAppsAlreadyExists, err) + } + + { + // Set a config var + updated, err := ds.UpdateApp(ctx, + &models.App{Name: testApp.Name, Config: map[string]string{"TEST":"1"}}) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test UpdateApp: error when updating app: %v", err) + } + expected := &models.App{Name: testApp.Name, Config: map[string]string{"TEST":"1"}} + if !reflect.DeepEqual(*updated, *expected) { + t.Log(buf.String()) + t.Fatalf("Test UpdateApp: expected updated `%v` but got `%v`", expected, updated) + } + + // Set a different var (without clearing the existing) + updated, err = ds.UpdateApp(ctx, + &models.App{Name: testApp.Name, Config: map[string]string{"OTHER":"TEST"}}) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test UpdateApp: error when updating app: %v", err) + } + expected = &models.App{Name: testApp.Name, Config: map[string]string{"TEST":"1","OTHER":"TEST"}} + if !reflect.DeepEqual(*updated, *expected) { + t.Log(buf.String()) + t.Fatalf("Test UpdateApp: expected updated `%v` but got `%v`", expected, updated) + } + + // Delete a var + updated, err = ds.UpdateApp(ctx, + &models.App{Name: testApp.Name, Config: map[string]string{"TEST":""}}) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test UpdateApp: error when updating app: %v", err) + } + expected = &models.App{Name: testApp.Name, Config: map[string]string{"OTHER":"TEST"}} + if !reflect.DeepEqual(*updated, *expected) { + t.Log(buf.String()) + t.Fatalf("Test UpdateApp: expected updated `%v` but got `%v`", expected, updated) + } + } + + // Testing get app + _, err = ds.GetApp(ctx, "") + if err != models.ErrDatastoreEmptyAppName { + t.Log(buf.String()) + t.Fatalf("Test GetApp: expected error to be %v, but it was %s", models.ErrDatastoreEmptyAppName, err) + } + + app, err := ds.GetApp(ctx, testApp.Name) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test GetApp: error: %s", err) + } + if app.Name != testApp.Name { + t.Log(buf.String()) + t.Fatalf("Test GetApp: expected `app.Name` to be `%s` but it was `%s`", app.Name, testApp.Name) + } + + // Testing list apps + apps, err := ds.GetApps(ctx, &models.AppFilter{}) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test GetApps: unexpected error %v", err) + } + if len(apps) == 0 { + t.Fatal("Test GetApps: expected result count to be greater than 0") + } + if apps[0].Name != testApp.Name { + t.Log(buf.String()) + t.Fatalf("Test GetApps: expected `app.Name` to be `%s` but it was `%s`", app.Name, testApp.Name) + } + + apps, err = ds.GetApps(ctx, &models.AppFilter{Name: "Tes%"}) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test GetApps(filter): unexpected error %v", err) + } + if len(apps) == 0 { + t.Fatal("Test GetApps(filter): expected result count to be greater than 0") + } + + // Testing app delete + err = ds.RemoveApp(ctx, "") + if err != models.ErrDatastoreEmptyAppName { + t.Log(buf.String()) + t.Fatalf("Test RemoveApp: expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) + } + + err = ds.RemoveApp(ctx, testApp.Name) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test RemoveApp: error: %s", err) + } + app, err = ds.GetApp(ctx, testApp.Name) + if err != models.ErrAppsNotFound { + t.Log(buf.String()) + t.Fatalf("Test GetApp(removed): expected error `%v`, but it was `%v`", models.ErrAppsNotFound, err) + } + if app != nil { + t.Log(buf.String()) + t.Fatalf("Test RemoveApp: failed to remove the app") + } + + // Test update inexistent app + _, err = ds.UpdateApp(ctx, &models.App{ + Name: testApp.Name, + Config: map[string]string{ + "TEST": "1", + }, + }) + if err != models.ErrAppsNotFound { + t.Log(buf.String()) + t.Fatalf("Test UpdateApp(inexistent): expected error `%v`, but it was `%v`", models.ErrAppsNotFound, err) + } + }) + + t.Run("routes", func(t *testing.T) { + // Insert app again to test routes + _, err := ds.InsertApp(ctx, testApp) + if err != nil && err != models.ErrAppsAlreadyExists { + t.Log(buf.String()) + t.Fatalf("Test InsertRoute Prep: failed to insert app: ", err) + } + + // Testing insert route + { + _, err = ds.InsertRoute(ctx, nil) + if err != models.ErrDatastoreEmptyRoute { + t.Log(buf.String()) + t.Fatalf("Test InsertRoute(nil): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyRoute, err) + } + + _, err = ds.InsertRoute(ctx, &models.Route{AppName: "notreal", Path: "/test"}) + if err != models.ErrAppsNotFound { + t.Log(buf.String()) + t.Fatalf("Test InsertRoute: expected error `%v`, but it was `%v`", models.ErrAppsNotFound, err) + } + + _, err = ds.InsertRoute(ctx, testRoute) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test InsertRoute: error when storing new route: %s", err) + } + + _, err = ds.InsertRoute(ctx, testRoute) + if err != models.ErrRoutesAlreadyExists { + t.Log(buf.String()) + t.Fatalf("Test InsertRoute duplicated: expected error to be `%v`, but it was `%v`", models.ErrRoutesAlreadyExists, err) + } + } + + // Testing get + { + _, err = ds.GetRoute(ctx, "a", "") + if err != models.ErrDatastoreEmptyRoutePath { + t.Log(buf.String()) + t.Fatalf("Test GetRoute(empty route path): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyRoutePath, err) + } + + _, err = ds.GetRoute(ctx, "", "a") + if err != models.ErrDatastoreEmptyAppName { + t.Log(buf.String()) + t.Fatalf("Test GetRoute(empty app name): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) + } + + route, err := ds.GetRoute(ctx, testApp.Name, testRoute.Path) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test GetRoute: unexpected error %v", err) + } + var expected models.Route = *testRoute + if !reflect.DeepEqual(*route, expected) { + t.Log(buf.String()) + t.Fatalf("Test InsertApp: expected to insert:\n%v\nbut got:\n%v", expected, route) + } + } + + + // Testing update + { + // Update some fields, and add 3 configs and 3 headers. + updated, err := ds.UpdateRoute(ctx, &models.Route{ + AppName: testRoute.AppName, + Path: testRoute.Path, + Timeout: 100, + Config: map[string]string{ + "FIRST": "1", + "SECOND": "2", + "THIRD": "3", + }, + Headers: http.Header{ + "First": []string{"test"}, + "Second": []string{"test", "test"}, + "Third": []string{"test", "test2"}, + }, + }) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test UpdateRoute: unexpected error: %v", err) + } + expected := &models.Route{ + // unchanged + AppName: testRoute.AppName, + Path: testRoute.Path, + Image: "iron/hello", + Type: "sync", + Format: "http", + // updated + Timeout: 100, + Config: map[string]string{ + "FIRST": "1", + "SECOND": "2", + "THIRD": "3", + }, + Headers: http.Header{ + "First": []string{"test"}, + "Second": []string{"test", "test"}, + "Third": []string{"test", "test2"}, + }, + } + if !reflect.DeepEqual(*updated, *expected) { + t.Log(buf.String()) + t.Fatalf("Test UpdateRoute: expected updated `%v` but got `%v`", expected, updated) + } + + // Update a config var, remove another. Add one Header, remove another. + updated, err = ds.UpdateRoute(ctx, &models.Route{ + AppName: testRoute.AppName, + Path: testRoute.Path, + Config: map[string]string{ + "FIRST": "first", + "SECOND": "", + "THIRD": "3", + }, + Headers: http.Header{ + "First": []string{"test2"}, + "Second": nil, + }, + }) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test UpdateRoute: unexpected error: %v", err) + } + expected = &models.Route{ + // unchanged + AppName: testRoute.AppName, + Path: testRoute.Path, + Image: "iron/hello", + Type: "sync", + Format: "http", + Timeout: 100, + // updated + Config: map[string]string{ + "FIRST": "first", + "THIRD": "3", + }, + Headers: http.Header{ + "First": []string{"test", "test2"}, + "Third": []string{"test", "test2"}, + }, + } + if !reflect.DeepEqual(*updated, *expected) { + t.Log(buf.String()) + t.Fatalf("Test UpdateRoute: expected updated:\n`%v`\nbut got:\n`%v`", expected, updated) + } + } + + // Testing list routes + routes, err := ds.GetRoutesByApp(ctx, testApp.Name, &models.RouteFilter{}) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test GetRoutesByApp: unexpected error %v", err) + } + if len(routes) == 0 { + t.Fatal("Test GetRoutesByApp: expected result count to be greater than 0") + } + if routes[0] == nil { + t.Log(buf.String()) + t.Fatalf("Test GetRoutes: expected non-nil route") + } else if routes[0].Path != testRoute.Path { + t.Log(buf.String()) + t.Fatalf("Test GetRoutes: expected `app.Name` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path) + } + + routes, err = ds.GetRoutesByApp(ctx, testApp.Name, &models.RouteFilter{Image: testRoute.Image}) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test GetRoutesByApp: unexpected error %v", err) + } + if len(routes) == 0 { + t.Fatal("Test GetRoutesByApp: expected result count to be greater than 0") + } + if routes[0] == nil { + t.Log(buf.String()) + t.Fatalf("Test GetRoutes: expected non-nil route") + } else if routes[0].Path != testRoute.Path { + t.Log(buf.String()) + t.Fatalf("Test GetRoutes: expected `app.Name` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path) + } + + routes, err = ds.GetRoutesByApp(ctx, "notreal", nil) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test GetRoutesByApp: error: %s", err) + } + if len(routes) != 0 { + t.Fatalf("Test GetRoutesByApp: expected result count to be 0 but got %d", len(routes)) + } + + // Testing list routes + routes, err = ds.GetRoutes(ctx, &models.RouteFilter{Image: testRoute.Image}) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test GetRoutes: error: %s", err) + } + if len(routes) == 0 { + t.Fatal("Test GetRoutes: expected result count to be greater than 0") + } + if routes[0].Path != testRoute.Path { + t.Log(buf.String()) + t.Fatalf("Test GetRoutes: expected `app.Name` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path) + } + + // Testing route delete + err = ds.RemoveRoute(ctx, "", "") + if err != models.ErrDatastoreEmptyAppName { + t.Log(buf.String()) + t.Fatalf("Test RemoveRoute(empty app name): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) + } + + err = ds.RemoveRoute(ctx, "a", "") + if err != models.ErrDatastoreEmptyRoutePath { + t.Log(buf.String()) + t.Fatalf("Test RemoveRoute(empty route path): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyRoutePath, err) + } + + err = ds.RemoveRoute(ctx, testRoute.AppName, testRoute.Path) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test RemoveApp: unexpected error: %v", err) + } + + route, err := ds.GetRoute(ctx, testRoute.AppName, testRoute.Path) + if err != nil && err != models.ErrRoutesNotFound { + t.Log(buf.String()) + t.Fatalf("Test GetRoute: expected error `%v`, but it was `%v`", models.ErrRoutesNotFound, err) + } + if route != nil { + t.Log(buf.String()) + t.Fatalf("Test RemoveApp: failed to remove the route: %v", route) + } + + _, err = ds.UpdateRoute(ctx, &models.Route{ + AppName: testRoute.AppName, + Path: testRoute.Path, + Image: "test", + }) + if err != models.ErrRoutesNotFound { + t.Log(buf.String()) + t.Fatalf("Test UpdateRoute inexistent: expected error to be `%v`, but it was `%v`", models.ErrRoutesNotFound, err) + } + }) + + t.Run("put-get", func(t *testing.T) { + // Testing Put/Get + err := ds.Put(ctx, nil, nil) + if err != models.ErrDatastoreEmptyKey { + t.Log(buf.String()) + t.Fatalf("Test Put(nil,nil): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyKey, err) + } + + err = ds.Put(ctx, []byte("test"), []byte("success")) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test Put: unexpected error: %v", err) + } + + val, err := ds.Get(ctx, []byte("test")) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test Put: unexpected error: %v", err) + } + if string(val) != "success" { + t.Log(buf.String()) + t.Fatalf("Test Get: expected value to be `%v`, but it was `%v`", "success", string(val)) + } + + err = ds.Put(ctx, []byte("test"), nil) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test Put: unexpected error: %v", err) + } + + val, err = ds.Get(ctx, []byte("test")) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test Put: unexpected error: %v", err) + } + if string(val) != "" { + t.Log(buf.String()) + t.Fatalf("Test Get: expected value to be `%v`, but it was `%v`", "", string(val)) + } + }) +} + +var testApp = &models.App{ + Name: "Test", +} + +var testRoute = &models.Route{ + AppName: testApp.Name, + Path: "/test", + Image: "iron/hello", + Type: "sync", + Format: "http", +} \ No newline at end of file diff --git a/api/datastore/postgres/postgres.go b/api/datastore/postgres/postgres.go index e02ca0e9b..3474f3aa3 100644 --- a/api/datastore/postgres/postgres.go +++ b/api/datastore/postgres/postgres.go @@ -12,6 +12,7 @@ import ( "github.com/iron-io/functions/api/models" "github.com/lib/pq" _ "github.com/lib/pq" + "bytes" ) const routesTableCreate = ` @@ -117,39 +118,54 @@ func (ds *PostgresDatastore) InsertApp(ctx context.Context, app *models.App) (*m return app, nil } -func (ds *PostgresDatastore) UpdateApp(ctx context.Context, app *models.App) (*models.App, error) { - if app == nil { +func (ds *PostgresDatastore) UpdateApp(ctx context.Context, newapp *models.App) (*models.App, error) { + if newapp == nil { return nil, models.ErrAppsNotFound } - cbyte, err := json.Marshal(app.Config) - if err != nil { - return nil, err - } + app := &models.App{Name: newapp.Name} + err := ds.Tx(func(tx *sql.Tx) error { + row := ds.db.QueryRow("SELECT config FROM apps WHERE name=$1", app.Name) - res, err := ds.db.Exec(` - UPDATE apps SET - config = $2 - WHERE name = $1 - RETURNING *; - `, - app.Name, - string(cbyte), - ) + var config string + if err := row.Scan(&config); err != nil { + if err == sql.ErrNoRows { + return models.ErrAppsNotFound + } + return err + } + + if config != "" { + err := json.Unmarshal([]byte(config), &app.Config) + if err != nil { + return err + } + } + + app.UpdateConfig(newapp.Config) + + cbyte, err := json.Marshal(app.Config) + if err != nil { + return err + } + + res, err := ds.db.Exec(`UPDATE apps SET config = $2 WHERE name = $1;`, app.Name, string(cbyte)) + if err != nil { + return err + } + + if n, err := res.RowsAffected(); err != nil { + return err + } else if n == 0 { + return models.ErrAppsNotFound + } + return nil + }) if err != nil { return nil, err } - n, err := res.RowsAffected() - if err != nil { - return nil, err - } - - if n == 0 { - return nil, models.ErrAppsNotFound - } - return app, nil } @@ -213,9 +229,8 @@ func scanApp(scanner rowScanner, app *models.App) error { func (ds *PostgresDatastore) GetApps(ctx context.Context, filter *models.AppFilter) ([]*models.App, error) { res := []*models.App{} - filterQuery := buildFilterAppQuery(filter) - rows, err := ds.db.Query(fmt.Sprintf("SELECT DISTINCT * FROM apps %s", filterQuery)) - + filterQuery, args := buildFilterAppQuery(filter) + rows, err := ds.db.Query(fmt.Sprintf("SELECT DISTINCT * FROM apps %s", filterQuery), args...) if err != nil { return nil, err } @@ -255,7 +270,26 @@ func (ds *PostgresDatastore) InsertRoute(ctx context.Context, route *models.Rout return nil, err } - _, err = ds.db.Exec(` + err = ds.Tx(func(tx *sql.Tx) error { + r := tx.QueryRow(`SELECT 1 FROM apps WHERE name=$1`, route.AppName) + if err := r.Scan(new(int)); err != nil { + if err == sql.ErrNoRows { + return models.ErrAppsNotFound + } + return err + } + + same, err := tx.Query(`SELECT 1 FROM routes WHERE app_name=$1 AND path=$2`, + route.AppName, route.Path) + if err != nil { + return err + } + defer same.Close() + if same.Next() { + return models.ErrRoutesAlreadyExists + } + + _, err = tx.Exec(` INSERT INTO routes ( app_name, path, @@ -269,80 +303,93 @@ func (ds *PostgresDatastore) InsertRoute(ctx context.Context, route *models.Rout config ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);`, - route.AppName, - route.Path, - route.Image, - route.Format, - route.MaxConcurrency, - route.Memory, - route.Type, - route.Timeout, - string(hbyte), - string(cbyte), - ) + route.AppName, + route.Path, + route.Image, + route.Format, + route.MaxConcurrency, + route.Memory, + route.Type, + route.Timeout, + string(hbyte), + string(cbyte), + ) + return err + }) + if err != nil { - pqErr := err.(*pq.Error) - if pqErr.Code == "23505" { - return nil, models.ErrRoutesAlreadyExists - } return nil, err } return route, nil } -func (ds *PostgresDatastore) UpdateRoute(ctx context.Context, route *models.Route) (*models.Route, error) { - if route == nil { +func (ds *PostgresDatastore) UpdateRoute(ctx context.Context, newroute *models.Route) (*models.Route, error) { + if newroute == nil { return nil, models.ErrDatastoreEmptyRoute } - hbyte, err := json.Marshal(route.Headers) - if err != nil { - return nil, err - } + var route models.Route + err := ds.Tx(func(tx *sql.Tx) error { + row := ds.db.QueryRow(fmt.Sprintf("%s WHERE app_name=$1 AND path=$2", routeSelector), newroute.AppName, newroute.Path) + if err := scanRoute(row, &route); err == sql.ErrNoRows { + return models.ErrRoutesNotFound + } else if err != nil { + return err + } - cbyte, err := json.Marshal(route.Config) - if err != nil { - return nil, err - } + route.Update(newroute) - res, err := ds.db.Exec(` + hbyte, err := json.Marshal(route.Headers) + if err != nil { + return err + } + + cbyte, err := json.Marshal(route.Config) + if err != nil { + return err + } + + res, err := tx.Exec(` UPDATE routes SET image = $3, format = $4, - memory = $5, - maxc = $6, + maxc = $5, + memory = $6, type = $7, timeout = $8, headers = $9, config = $10 WHERE app_name = $1 AND path = $2;`, - route.AppName, - route.Path, - route.Image, - route.Format, - route.Memory, - route.MaxConcurrency, - route.Type, - route.Timeout, - string(hbyte), - string(cbyte), - ) + route.AppName, + route.Path, + route.Image, + route.Format, + route.MaxConcurrency, + route.Memory, + route.Type, + route.Timeout, + string(hbyte), + string(cbyte), + ) + + if err != nil { + return err + } + + if n, err := res.RowsAffected(); err != nil { + return err + } else if n == 0 { + return models.ErrRoutesNotFound + } + + return nil + }) if err != nil { return nil, err } - - n, err := res.RowsAffected() - if err != nil { - return nil, err - } - - if n == 0 { - return nil, models.ErrRoutesNotFound - } - - return route, nil + return &route, nil } func (ds *PostgresDatastore) RemoveRoute(ctx context.Context, appName, routePath string) error { @@ -384,8 +431,8 @@ func scanRoute(scanner rowScanner, route *models.Route) error { &route.Path, &route.Image, &route.Format, - &route.Memory, &route.MaxConcurrency, + &route.Memory, &route.Type, &route.Timeout, &headerStr, @@ -426,8 +473,8 @@ func (ds *PostgresDatastore) GetRoute(ctx context.Context, appName, routePath st func (ds *PostgresDatastore) GetRoutes(ctx context.Context, filter *models.RouteFilter) ([]*models.Route, error) { res := []*models.Route{} - filterQuery := buildFilterRouteQuery(filter) - rows, err := ds.db.Query(fmt.Sprintf("%s %s", routeSelector, filterQuery)) + filterQuery, args := buildFilterRouteQuery(filter) + rows, err := ds.db.Query(fmt.Sprintf("%s %s", routeSelector, filterQuery), args...) // todo: check for no rows so we don't respond with a sql 500 err if err != nil { return nil, err @@ -451,9 +498,17 @@ func (ds *PostgresDatastore) GetRoutes(ctx context.Context, filter *models.Route func (ds *PostgresDatastore) GetRoutesByApp(ctx context.Context, appName string, filter *models.RouteFilter) ([]*models.Route, error) { res := []*models.Route{} - filter.AppName = appName - filterQuery := buildFilterRouteQuery(filter) - rows, err := ds.db.Query(fmt.Sprintf("%s %s", routeSelector, filterQuery)) + + var filterQuery string + var args []interface{} + if filter == nil { + filterQuery = "WHERE app_name = $1" + args = []interface{}{appName} + } else { + filter.AppName = appName + filterQuery, args = buildFilterRouteQuery(filter) + } + rows, err := ds.db.Query(fmt.Sprintf("%s %s", routeSelector, filterQuery), args...) // todo: check for no rows so we don't respond with a sql 500 err if err != nil { return nil, err @@ -472,55 +527,44 @@ func (ds *PostgresDatastore) GetRoutesByApp(ctx context.Context, appName string, if err := rows.Err(); err != nil { return nil, err } + return res, nil } +func buildFilterAppQuery(filter *models.AppFilter) (string, []interface{}) { + if filter == nil { + return "", nil + } -func buildFilterAppQuery(filter *models.AppFilter) string { - filterQuery := "" + if filter.Name != "" { + return "WHERE name LIKE $1", []interface{}{filter.Name} + } - if filter != nil { - filterQueries := []string{} - if filter.Name != "" { - filterQueries = append(filterQueries, fmt.Sprintf("name LIKE '%s'", filter.Name)) - } + return "", nil +} - for i, field := range filterQueries { - if i == 0 { - filterQuery = fmt.Sprintf("WHERE %s ", field) +func buildFilterRouteQuery(filter *models.RouteFilter) (string, []interface{}) { + if filter == nil { + return "", nil + } + var b bytes.Buffer + var args []interface{} + + where := func(colOp, val string) { + if val != "" { + args = append(args, val) + if len(args) == 1 { + fmt.Fprintf(&b, "WHERE %s $1", colOp) } else { - filterQuery = fmt.Sprintf("%s AND %s", filterQuery, field) + fmt.Fprintf(&b, " AND %s $%d", colOp, len(args)) } } } - return filterQuery -} + where("path =", filter.Path) + where("app_name =", filter.AppName) + where("image =", filter.Image) -func buildFilterRouteQuery(filter *models.RouteFilter) string { - filterQuery := "" - - filterQueries := []string{} - if filter.Path != "" { - filterQueries = append(filterQueries, fmt.Sprintf("path = '%s'", filter.Path)) - } - - if filter.AppName != "" { - filterQueries = append(filterQueries, fmt.Sprintf("app_name = '%s'", filter.AppName)) - } - - if filter.Image != "" { - filterQueries = append(filterQueries, fmt.Sprintf("image = '%s'", filter.Image)) - } - - for i, field := range filterQueries { - if i == 0 { - filterQuery = fmt.Sprintf("WHERE %s ", field) - } else { - filterQuery = fmt.Sprintf("%s AND %s", filterQuery, field) - } - } - - return filterQuery + return b.String(), args } func (ds *PostgresDatastore) Put(ctx context.Context, key, value []byte) error { @@ -562,3 +606,17 @@ func (ds *PostgresDatastore) Get(ctx context.Context, key []byte) ([]byte, error return []byte(value), nil } + + +func (ds *PostgresDatastore) Tx(f func(*sql.Tx) error) error { + tx, err := ds.db.Begin() + if err != nil { + return err + } + err = f(tx) + if err != nil { + tx.Rollback() + return err + } + return tx.Commit() +} diff --git a/api/datastore/postgres/postgres_test.go b/api/datastore/postgres/postgres_test.go new file mode 100644 index 000000000..606cd1e19 --- /dev/null +++ b/api/datastore/postgres/postgres_test.go @@ -0,0 +1,98 @@ +package postgres + +import ( + "bytes" + "database/sql" + "fmt" + "net/url" + "os/exec" + "testing" + "time" + + "github.com/iron-io/functions/api/datastore/internal/datastoretest" +) + +const tmpPostgres = "postgres://postgres@127.0.0.1:15432/funcs?sslmode=disable" + +func preparePostgresTest(logf, fatalf func(string, ...interface{})) (func(), func()) { + fmt.Println("initializing postgres for test") + tryRun(logf, "remove old postgres container", exec.Command("docker", "rm", "-f", "iron-postgres-test")) + mustRun(fatalf, "start postgres container", exec.Command("docker", "run", "--name", "iron-postgres-test", "-p", "15432:5432", "-d", "postgres")) + + wait := 1 * time.Second + for { + db, err := sql.Open("postgres", "postgres://postgres@127.0.0.1:15432?sslmode=disable") + if err != nil { + fmt.Println("failed to connect to postgres:", err) + fmt.Println("retrying in:", wait) + time.Sleep(wait) + wait = 2 * wait + continue + } + + _, err = db.Exec(`CREATE DATABASE funcs;`) + if err != nil { + fmt.Println("failed to create database:", err) + fmt.Println("retrying in:", wait) + time.Sleep(wait) + wait = 2 * wait + continue + } + _, err = db.Exec(`GRANT ALL PRIVILEGES ON DATABASE funcs TO postgres;`) + if err == nil { + break + } + fmt.Println("failed to grant privileges:", err) + fmt.Println("retrying in:", wait) + time.Sleep(wait) + wait = 2 * wait + } + fmt.Println("postgres for test ready") + return func() { + db, err := sql.Open("postgres", tmpPostgres) + if err != nil { + fatalf("failed to connect for truncation: %s\n", err) + } + for _, table := range []string{"routes", "apps", "extras"} { + _, err = db.Exec(`TRUNCATE TABLE ` + table) + if err != nil { + fatalf("failed to truncate table %q: %s\n", table, err) + } + } + }, + func() { + tryRun(logf, "stop postgres container", exec.Command("docker", "rm", "-f", "iron-postgres-test")) + } +} + +func TestDatastore(t *testing.T) { + _, close := preparePostgresTest(t.Logf, t.Fatalf) + defer close() + + u, err := url.Parse(tmpPostgres) + if err != nil { + t.Fatalf("failed to parse url:", err) + } + ds, err := New(u) + if err != nil { + t.Fatalf("failed to create postgres datastore:", err) + } + + datastoretest.Test(t, ds) +} + +func tryRun(logf func(string, ...interface{}), desc string, cmd *exec.Cmd) { + var b bytes.Buffer + cmd.Stderr = &b + if err := cmd.Run(); err != nil { + logf("failed to %s: %s", desc, b.String()) + } +} + +func mustRun(fatalf func(string, ...interface{}), desc string, cmd *exec.Cmd) { + var b bytes.Buffer + cmd.Stderr = &b + if err := cmd.Run(); err != nil { + fatalf("failed to %s: %s", desc, b.String()) + } +} \ No newline at end of file diff --git a/api/datastore/postgres_test.go b/api/datastore/postgres_test.go deleted file mode 100644 index 0eab68bbe..000000000 --- a/api/datastore/postgres_test.go +++ /dev/null @@ -1,323 +0,0 @@ -package datastore - -import ( - "context" - "database/sql" - "fmt" - "os/exec" - "testing" - "time" - - "github.com/iron-io/functions/api/models" -) - -const tmpPostgres = "postgres://postgres@127.0.0.1:15432/funcs?sslmode=disable" - -func preparePostgresTest() func() { - fmt.Println("initializing postgres for test") - exec.Command("docker", "rm", "-f", "iron-postgres-test").Run() - exec.Command("docker", "run", "--name", "iron-postgres-test", "-p", "15432:5432", "-d", "postgres").Run() - for { - db, err := sql.Open("postgres", "postgres://postgres@127.0.0.1:15432?sslmode=disable") - if err != nil { - time.Sleep(1 * time.Second) - continue - } - - db.Exec(`CREATE DATABASE funcs;`) - _, err = db.Exec(`GRANT ALL PRIVILEGES ON DATABASE funcs TO postgres;`) - if err == nil { - break - } - time.Sleep(1 * time.Second) - } - fmt.Println("postgres for test ready") - return func() { - exec.Command("docker", "rm", "-f", "iron-postgres-test").Run() - } -} - -func TestPostgres(t *testing.T) { - close := preparePostgresTest() - defer close() - buf := setLogBuffer() - - ctx := context.Background() - - ds, err := New(tmpPostgres) - if err != nil { - t.Fatalf("Error when creating datastore: %v", err) - } - - // Testing insert app - _, err = ds.InsertApp(ctx, nil) - if err != models.ErrDatastoreEmptyApp { - t.Log(buf.String()) - t.Fatalf("Test InsertApp(nil): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyApp, err) - } - - _, err = ds.InsertApp(ctx, &models.App{}) - if err != models.ErrDatastoreEmptyAppName { - t.Log(buf.String()) - t.Fatalf("Test InsertApp(nil): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) - } - - _, err = ds.InsertApp(ctx, testApp) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test InsertApp: error when storing new app: %s", err) - } - - _, err = ds.InsertApp(ctx, testApp) - if err != models.ErrAppsAlreadyExists { - t.Log(buf.String()) - t.Fatalf("Test InsertApp duplicated: expected error `%v`, but it was `%v`", models.ErrAppsAlreadyExists, err) - } - - _, err = ds.UpdateApp(ctx, &models.App{ - Name: testApp.Name, - Config: map[string]string{ - "TEST": "1", - }, - }) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test UpdateApp: error when updating app: %v", err) - } - - // Testing get app - _, err = ds.GetApp(ctx, "") - if err != models.ErrDatastoreEmptyAppName { - t.Log(buf.String()) - t.Fatalf("Test GetApp: expected error to be %v, but it was %s", models.ErrDatastoreEmptyAppName, err) - } - - app, err := ds.GetApp(ctx, testApp.Name) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test GetApp: error: %s", err) - } - if app.Name != testApp.Name { - t.Log(buf.String()) - t.Fatalf("Test GetApp: expected `app.Name` to be `%s` but it was `%s`", app.Name, testApp.Name) - } - - // Testing list apps - apps, err := ds.GetApps(ctx, &models.AppFilter{}) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test GetApps: unexpected error %v", err) - } - if len(apps) == 0 { - t.Fatal("Test GetApps: expected result count to be greater than 0") - } - if apps[0].Name != testApp.Name { - t.Log(buf.String()) - t.Fatalf("Test GetApps: expected `app.Name` to be `%s` but it was `%s`", app.Name, testApp.Name) - } - - apps, err = ds.GetApps(ctx, &models.AppFilter{Name: "Tes%"}) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test GetApps(filter): unexpected error %v", err) - } - if len(apps) == 0 { - t.Fatal("Test GetApps(filter): expected result count to be greater than 0") - } - - // Testing app delete - err = ds.RemoveApp(ctx, "") - if err != models.ErrDatastoreEmptyAppName { - t.Log(buf.String()) - t.Fatalf("Test RemoveApp: expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) - } - - err = ds.RemoveApp(ctx, testApp.Name) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test RemoveApp: error: %s", err) - } - app, err = ds.GetApp(ctx, testApp.Name) - if err != models.ErrAppsNotFound { - t.Log(buf.String()) - t.Fatalf("Test GetApp(removed): expected error `%v`, but it was `%v`", models.ErrAppsNotFound, err) - } - if app != nil { - t.Log(buf.String()) - t.Fatalf("Test RemoveApp: failed to remove the app") - } - - // Test update inexistent app - _, err = ds.UpdateApp(ctx, &models.App{ - Name: testApp.Name, - Config: map[string]string{ - "TEST": "1", - }, - }) - if err != models.ErrAppsNotFound { - t.Log(buf.String()) - t.Fatalf("Test UpdateApp(inexistent): expected error `%v`, but it was `%v`", models.ErrAppsNotFound, err) - } - - // Insert app again to test routes - ds.InsertApp(ctx, testApp) - - // Testing insert route - _, err = ds.InsertRoute(ctx, nil) - if err != models.ErrDatastoreEmptyRoute { - t.Log(buf.String()) - t.Fatalf("Test InsertRoute(nil): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyRoute, err) - } - - _, err = ds.InsertRoute(ctx, testRoute) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test InsertRoute: error when storing new route: %s", err) - } - - _, err = ds.InsertRoute(ctx, testRoute) - if err != models.ErrRoutesAlreadyExists { - t.Log(buf.String()) - t.Fatalf("Test InsertRoute duplicated: expected error to be `%v`, but it was `%v`", models.ErrRoutesAlreadyExists, err) - } - - _, err = ds.UpdateRoute(ctx, testRoute) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test UpdateRoute: unexpected error: %v", err) - } - - // Testing get - _, err = ds.GetRoute(ctx, "a", "") - if err != models.ErrDatastoreEmptyRoutePath { - t.Log(buf.String()) - t.Fatalf("Test GetRoute(empty route path): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyRoutePath, err) - } - - _, err = ds.GetRoute(ctx, "", "a") - if err != models.ErrDatastoreEmptyAppName { - t.Log(buf.String()) - t.Fatalf("Test GetRoute(empty app name): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) - } - - route, err := ds.GetRoute(ctx, testApp.Name, testRoute.Path) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test GetRoute: unexpected error %v", err) - } - if route.Path != testRoute.Path { - t.Log(buf.String()) - t.Fatalf("Test GetRoute: expected `route.Path` to be `%s` but it was `%s`", route.Path, testRoute.Path) - } - - // Testing list routes - routes, err := ds.GetRoutesByApp(ctx, testApp.Name, &models.RouteFilter{}) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test GetRoutes: unexpected error %v", err) - } - if len(routes) == 0 { - t.Fatal("Test GetRoutes: expected result count to be greater than 0") - } - if routes[0].Path != testRoute.Path { - t.Log(buf.String()) - t.Fatalf("Test GetRoutes: expected `app.Name` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path) - } - - // Testing list routes - routes, err = ds.GetRoutes(ctx, &models.RouteFilter{Image: testRoute.Image}) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test GetRoutes: error: %s", err) - } - if len(routes) == 0 { - t.Fatal("Test GetRoutes: expected result count to be greater than 0") - } - if routes[0].Path != testRoute.Path { - t.Log(buf.String()) - t.Fatalf("Test GetRoutes: expected `app.Name` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path) - } - - // Testing app delete - err = ds.RemoveRoute(ctx, "", "") - if err != models.ErrDatastoreEmptyAppName { - t.Log(buf.String()) - t.Fatalf("Test RemoveRoute(empty app name): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) - } - - err = ds.RemoveRoute(ctx, "a", "") - if err != models.ErrDatastoreEmptyRoutePath { - t.Log(buf.String()) - t.Fatalf("Test RemoveRoute(empty route path): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyRoutePath, err) - } - - err = ds.RemoveRoute(ctx, testRoute.AppName, testRoute.Path) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test RemoveApp: unexpected error: %v", err) - } - - _, err = ds.UpdateRoute(ctx, &models.Route{ - AppName: testRoute.AppName, - Path: testRoute.Path, - Image: "test", - }) - if err != models.ErrRoutesNotFound { - t.Log(buf.String()) - t.Fatalf("Test UpdateRoute inexistent: expected error to be `%v`, but it was `%v`", models.ErrRoutesNotFound, err) - } - - route, err = ds.GetRoute(ctx, testRoute.AppName, testRoute.Path) - if err != models.ErrRoutesNotFound { - t.Log(buf.String()) - t.Fatalf("Test GetRoute: expected error `%v`, but it was `%v`", models.ErrRoutesNotFound, err) - } - if route != nil { - t.Log(buf.String()) - t.Fatalf("Test RemoveApp: failed to remove the route") - } - - // Testing Put/Get - err = ds.Put(ctx, nil, nil) - if err != models.ErrDatastoreEmptyKey { - t.Log(buf.String()) - t.Fatalf("Test Put(nil,nil): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyKey, err) - } - - err = ds.Put(ctx, []byte("test"), []byte("success")) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test Put: unexpected error: %v", err) - } - - val, err := ds.Get(ctx, []byte("test")) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test Put: unexpected error: %v", err) - } - if string(val) != "success" { - t.Log(buf.String()) - t.Fatalf("Test Get: expected value to be `%v`, but it was `%v`", "success", string(val)) - } - - err = ds.Put(ctx, []byte("test"), nil) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test Put: unexpected error: %v", err) - } - - val, err = ds.Get(ctx, []byte("test")) - if err != nil { - t.Log(buf.String()) - t.Fatalf("Test Put: unexpected error: %v", err) - } - if string(val) != "" { - t.Log(buf.String()) - t.Fatalf("Test Get: expected value to be `%v`, but it was `%v`", "", string(val)) - } - -} - -func testPostgresInsert(t *testing.T, ctx context.Context, ds models.Datastore) { - -} diff --git a/api/models/app.go b/api/models/app.go index 03505275e..db8d4e693 100644 --- a/api/models/app.go +++ b/api/models/app.go @@ -53,6 +53,22 @@ func (a *App) Validate() error { return nil } +// UpdateConfig adds entries from patch to a.Config, and removes entries with empty values. +func (a *App) UpdateConfig(patch Config) { + if patch != nil { + if a.Config == nil { + a.Config = make(Config) + } + for k, v := range patch { + if v == "" { + delete(a.Config, k) + } else { + a.Config[k] = v + } + } + } +} + type AppFilter struct { Name string } diff --git a/api/models/route.go b/api/models/route.go index d6bacead3..82eaa90b0 100644 --- a/api/models/route.go +++ b/api/models/route.go @@ -125,6 +125,55 @@ func (r *Route) Validate() error { return nil } +// Update updates fields in r with non-zero field values from new. +// 0-length slice Header values, and empty-string Config values trigger removal of map entry. +func (r *Route) Update(new *Route) { + if new.Image != "" { + r.Image = new.Image + } + if new.Memory != 0 { + r.Memory = new.Memory + } + if new.Type != "" { + r.Type = new.Type + } + if new.Timeout != 0 { + r.Timeout = new.Timeout + } + if new.Format != "" { + r.Format = new.Format + } + if new.MaxConcurrency != 0 { + r.MaxConcurrency = new.MaxConcurrency + } + if new.Headers != nil { + if r.Headers == nil { + r.Headers = make(http.Header) + } + for k, v := range new.Headers { + if len(v) == 0 { + r.Headers.Del(k) + } else { + for _, val := range v { + r.Headers.Add(k, val) + } + } + } + } + if new.Config != nil { + if r.Config == nil { + r.Config = make(Config) + } + for k, v := range new.Config { + if v == "" { + delete(r.Config, k) + } else { + r.Config[k] = v + } + } + } +} + type RouteFilter struct { Path string AppName string