diff --git a/Makefile b/Makefile index 946f3bd76..bb23b82af 100644 --- a/Makefile +++ b/Makefile @@ -14,15 +14,18 @@ build-docker: docker build -t iron/functions:latest . test: - go test -v $(shell glide nv | grep -v examples | grep -v tool | grep -v fnctl) + go test -v $(shell go list ./... | grep -v vendor | grep -v examples | grep -v tool | grep -v fnctl) cd fnctl && $(MAKE) test +test-datastore: + cd api/datastore && go test -v + test-docker: docker run -ti --privileged --rm -e LOG_LEVEL=debug \ -v /var/run/docker.sock:/var/run/docker.sock \ -v $(DIR):/go/src/github.com/iron-io/functions \ -w /go/src/github.com/iron-io/functions iron/go:dev go test \ - -v $(shell glide nv | grep -v examples | grep -v tool | grep -v fnctl) + -v $(shell go list ./... | grep -v vendor | grep -v examples | grep -v tool | grep -v fnctl | grep -v datastore) run: ./functions diff --git a/api/datastore/bolt/bolt.go b/api/datastore/bolt/bolt.go index 0b6b32d80..df171339d 100644 --- a/api/datastore/bolt/bolt.go +++ b/api/datastore/bolt/bolt.go @@ -72,18 +72,31 @@ func New(url *url.URL) (models.Datastore, error) { return ds, nil } -func (ds *BoltDatastore) StoreApp(app *models.App) (*models.App, error) { +func (ds *BoltDatastore) InsertApp(app *models.App) (*models.App, error) { if app == nil { return nil, models.ErrDatastoreEmptyApp } + if app.Name == "" { + return nil, models.ErrDatastoreEmptyAppName + } + + appname := []byte(app.Name) + err := ds.db.Update(func(tx *bolt.Tx) error { bIm := tx.Bucket(ds.appsBucket) + + v := bIm.Get(appname) + if v != nil { + return models.ErrAppsAlreadyExists + } + buf, err := json.Marshal(app) if err != nil { return err } - err = bIm.Put([]byte(app.Name), buf) + + err = bIm.Put(appname, buf) if err != nil { return err } @@ -94,6 +107,62 @@ func (ds *BoltDatastore) StoreApp(app *models.App) (*models.App, error) { } return nil }) + + return app, err +} + +func (ds *BoltDatastore) UpdateApp(newapp *models.App) (*models.App, error) { + if newapp == nil { + return nil, models.ErrDatastoreEmptyApp + } + + if newapp.Name == "" { + return nil, models.ErrDatastoreEmptyAppName + } + + var app *models.App + appname := []byte(newapp.Name) + + err := ds.db.Update(func(tx *bolt.Tx) error { + bIm := tx.Bucket(ds.appsBucket) + + v := bIm.Get(appname) + if v == nil { + return models.ErrAppsNotFound + } + + err := json.Unmarshal(v, &app) + if err != nil { + return err + } + + // Update app fields + if newapp.Config != nil { + if app.Config == nil { + app.Config = map[string]string{} + } + for k, v := range newapp.Config { + app.Config[k] = v + } + } + + buf, err := json.Marshal(app) + if err != nil { + return err + } + + err = bIm.Put(appname, buf) + if err != nil { + return err + } + bjParent := tx.Bucket(ds.routesBucket) + _, err = bjParent.CreateBucketIfNotExists([]byte(app.Name)) + if err != nil { + return err + } + return nil + }) + return app, err } @@ -181,23 +250,114 @@ func (ds *BoltDatastore) getRouteBucketForApp(tx *bolt.Tx, appName string) (*bol return b, nil } -func (ds *BoltDatastore) StoreRoute(route *models.Route) (*models.Route, error) { +func (ds *BoltDatastore) InsertRoute(route *models.Route) (*models.Route, error) { if route == nil { return nil, models.ErrDatastoreEmptyApp } + if route.AppName == "" { + return nil, models.ErrDatastoreEmptyAppName + } + + if route.Path == "" { + return nil, models.ErrDatastoreEmptyRoutePath + } + + routePath := []byte(route.Path) + err := ds.db.Update(func(tx *bolt.Tx) error { b, err := ds.getRouteBucketForApp(tx, route.AppName) if err != nil { return err } + v := b.Get(routePath) + if v != nil { + return models.ErrRoutesAlreadyExists + } + buf, err := json.Marshal(route) if err != nil { return err } - err = b.Put([]byte(route.Path), buf) + err = b.Put(routePath, buf) + if err != nil { + return err + } + return nil + }) + if err != nil { + return nil, err + } + return route, nil +} + +func (ds *BoltDatastore) UpdateRoute(newroute *models.Route) (*models.Route, error) { + if newroute == nil { + return nil, models.ErrDatastoreEmptyRoute + } + + if newroute.AppName == "" { + return nil, models.ErrDatastoreEmptyAppName + } + + if newroute.Path == "" { + return nil, models.ErrDatastoreEmptyRoutePath + } + + routePath := []byte(newroute.Path) + + var route *models.Route + + err := ds.db.Update(func(tx *bolt.Tx) error { + b, err := ds.getRouteBucketForApp(tx, newroute.AppName) + if err != nil { + return err + } + + v := b.Get(routePath) + if v == nil { + return models.ErrRoutesNotFound + } + + err = json.Unmarshal(v, &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.Headers != nil { + if route.Config == nil { + route.Config = map[string]string{} + } + for k, v := range newroute.Headers { + route.Headers[k] = v + } + } + if newroute.Config != nil { + if route.Config == nil { + route.Config = map[string]string{} + } + for k, v := range newroute.Config { + route.Config[k] = v + } + } + + buf, err := json.Marshal(route) + if err != nil { + return err + } + + err = b.Put(routePath, buf) if err != nil { return err } diff --git a/api/datastore/bolt_test.go b/api/datastore/bolt_test.go index 284e087ff..9afcd51f9 100644 --- a/api/datastore/bolt_test.go +++ b/api/datastore/bolt_test.go @@ -21,15 +21,16 @@ func setLogBuffer() *bytes.Buffer { return &buf } +const tmpBolt = "/tmp/func_test_bolt.db" + func TestBolt(t *testing.T) { buf := setLogBuffer() - const tmpBolt = "/tmp/func_test_bolt.db" + os.Remove(tmpBolt) ds, err := New("bolt://" + tmpBolt) if err != nil { - t.Fatal("Error when creating datastore: %s", err) + t.Fatalf("Error when creating datastore: %v", err) } - defer os.Remove(tmpBolt) testApp := &models.App{ Name: "Test", @@ -41,24 +42,47 @@ func TestBolt(t *testing.T) { Image: "iron/hello", } - // Testing store app - _, err = ds.StoreApp(nil) - if err == nil { + // Testing insert app + _, err = ds.InsertApp(nil) + if err != models.ErrDatastoreEmptyApp { t.Log(buf.String()) - t.Fatalf("Test StoreApp: expected error when using nil app", err) + t.Fatalf("Test InsertApp(nil): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyApp, err) } - _, err = ds.StoreApp(testApp) + _, err = ds.InsertApp(&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(testApp) if err != nil { t.Log(buf.String()) - t.Fatalf("Test StoreApp: error when Bolt was storing new app: %s", err) + t.Fatalf("Test InsertApp: error when Bolt was storing new app: %s", err) + } + + _, err = ds.InsertApp(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(&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("") - if err == nil { + if err != models.ErrDatastoreEmptyAppName { t.Log(buf.String()) - t.Fatalf("Test GetApp: expected error when using empty app name", err) + t.Fatalf("Test GetApp: expected error to be %v, but it was %s", models.ErrDatastoreEmptyAppName, err) } app, err := ds.GetApp(testApp.Name) @@ -75,7 +99,7 @@ func TestBolt(t *testing.T) { apps, err := ds.GetApps(&models.AppFilter{}) if err != nil { t.Log(buf.String()) - t.Fatalf("Test GetApps: error: %s", err) + t.Fatalf("Test GetApps: unexpected error %v", err) } if len(apps) == 0 { t.Fatal("Test GetApps: expected result count to be greater than 0") @@ -87,9 +111,9 @@ func TestBolt(t *testing.T) { // Testing app delete err = ds.RemoveApp("") - if err == nil { + if err != models.ErrDatastoreEmptyAppName { t.Log(buf.String()) - t.Fatalf("Test RemoveApp: expected error when using empty app name", err) + t.Fatalf("Test RemoveApp: expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) } err = ds.RemoveApp(testApp.Name) @@ -107,39 +131,63 @@ func TestBolt(t *testing.T) { t.Fatalf("Test RemoveApp: failed to remove the app") } - // Store app again to test routes - ds.StoreApp(testApp) - - // Testing store route - _, err = ds.StoreRoute(nil) - if err == nil { + // Test update inexistent app + _, err = ds.UpdateApp(&models.App{ + Name: testApp.Name, + Config: map[string]string{ + "TEST": "1", + }, + }) + if err != models.ErrAppsNotFound { t.Log(buf.String()) - t.Fatalf("Test StoreRoute: expected error when using nil route", err) + t.Fatalf("Test UpdateApp inexistent: expected error to be %v, but it was %v", models.ErrAppsNotFound, err) } - _, err = ds.StoreRoute(testRoute) + // Insert app again to test routes + ds.InsertApp(testApp) + + // Testing insert route + _, err = ds.InsertRoute(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(testRoute) if err != nil { t.Log(buf.String()) - t.Fatalf("Test StoreReoute: error when Bolt was storing new route: %s", err) + t.Fatalf("Test InsertRoute: error when Bolt was storing new route: %s", err) + } + + _, err = ds.InsertRoute(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(testRoute) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test UpdateRoute: unexpected error: %v", err) } // Testing get _, err = ds.GetRoute("a", "") - if err == nil { + if err != models.ErrDatastoreEmptyRoutePath { t.Log(buf.String()) - t.Fatalf("Test GetRoute: expected error when using empty route name", err) + t.Fatalf("Test GetRoute(empty route path): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyRoutePath, err) } _, err = ds.GetRoute("", "a") - if err == nil { + if err != models.ErrDatastoreEmptyAppName { t.Log(buf.String()) - t.Fatalf("Test GetRoute: expected error when using empty app name", err) + t.Fatalf("Test GetRoute(empty app name): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) } route, err := ds.GetRoute(testApp.Name, testRoute.Path) if err != nil { t.Log(buf.String()) - t.Fatalf("Test GetRoute: error: %s", err) + t.Fatalf("Test GetRoute: unexpected error %v", err) } if route.Path != testRoute.Path { t.Log(buf.String()) @@ -150,7 +198,7 @@ func TestBolt(t *testing.T) { routes, err := ds.GetRoutesByApp(testApp.Name, &models.RouteFilter{}) if err != nil { t.Log(buf.String()) - t.Fatalf("Test GetRoutes: error: %s", err) + t.Fatalf("Test GetRoutes: unexpected error %v", err) } if len(routes) == 0 { t.Fatal("Test GetRoutes: expected result count to be greater than 0") @@ -176,21 +224,31 @@ func TestBolt(t *testing.T) { // Testing app delete err = ds.RemoveRoute("", "") - if err == nil { + if err != models.ErrDatastoreEmptyAppName { t.Log(buf.String()) - t.Fatalf("Test RemoveRoute: expected error when using empty app name", err) + t.Fatalf("Test RemoveRoute(empty app name): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyAppName, err) } err = ds.RemoveRoute("a", "") - if err == nil { + if err != models.ErrDatastoreEmptyRoutePath { t.Log(buf.String()) - t.Fatalf("Test RemoveRoute: expected error when using empty route name", err) + t.Fatalf("Test RemoveRoute(empty route path): expected error `%v`, but it was `%v`", models.ErrDatastoreEmptyRoutePath, err) } err = ds.RemoveRoute(testRoute.AppName, testRoute.Path) if err != nil { t.Log(buf.String()) - t.Fatalf("Test RemoveApp: error: %s", err) + t.Fatalf("Test RemoveApp: unexpected error: %v", err) + } + + _, err = ds.UpdateRoute(&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(testRoute.AppName, testRoute.Path) diff --git a/api/datastore/mock.go b/api/datastore/mock.go index c8d9f03a7..10a9d128d 100644 --- a/api/datastore/mock.go +++ b/api/datastore/mock.go @@ -27,7 +27,12 @@ func (m *Mock) GetApps(appFilter *models.AppFilter) ([]*models.App, error) { return m.FakeApps, nil } -func (m *Mock) StoreApp(app *models.App) (*models.App, error) { +func (m *Mock) InsertApp(app *models.App) (*models.App, error) { + // TODO: improve this mock method + return m.FakeApp, nil +} + +func (m *Mock) UpdateApp(app *models.App) (*models.App, error) { // TODO: improve this mock method return m.FakeApp, nil } @@ -69,7 +74,12 @@ func (m *Mock) GetRoutesByApp(appName string, routeFilter *models.RouteFilter) ( return routes, nil } -func (m *Mock) StoreRoute(route *models.Route) (*models.Route, error) { +func (m *Mock) InsertRoute(route *models.Route) (*models.Route, error) { + // TODO: improve this mock method + return m.FakeRoute, nil +} + +func (m *Mock) UpdateRoute(route *models.Route) (*models.Route, error) { // TODO: improve this mock method return m.FakeRoute, nil } diff --git a/api/datastore/postgres/postgres.go b/api/datastore/postgres/postgres.go index e51da8d1b..373169f3a 100644 --- a/api/datastore/postgres/postgres.go +++ b/api/datastore/postgres/postgres.go @@ -8,6 +8,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/iron-io/functions/api/models" + "github.com/lib/pq" _ "github.com/lib/pq" ) @@ -75,17 +76,56 @@ func New(url *url.URL) (models.Datastore, error) { return pg, nil } -func (ds *PostgresDatastore) StoreApp(app *models.App) (*models.App, error) { +func (ds *PostgresDatastore) InsertApp(app *models.App) (*models.App, error) { + var cbyte []byte + var err error + + if app == nil { + return nil, models.ErrDatastoreEmptyApp + } + + if app.Name == "" { + return nil, models.ErrDatastoreEmptyAppName + } + + if app.Config != nil { + cbyte, err = json.Marshal(app.Config) + if err != nil { + return nil, err + } + } + + _, err = ds.db.Exec(`INSERT INTO apps (name, config) VALUES ($1, $2);`, + app.Name, + string(cbyte), + ) + + if err != nil { + pqErr := err.(*pq.Error) + if pqErr.Code == "23505" { + return nil, models.ErrAppsAlreadyExists + } + return nil, err + } + + return app, nil +} + +func (ds *PostgresDatastore) UpdateApp(app *models.App) (*models.App, error) { + if app == nil { + return nil, models.ErrAppsNotFound + } + cbyte, err := json.Marshal(app.Config) if err != nil { return nil, err } - _, err = ds.db.Exec(` - INSERT INTO apps (name, config) - VALUES ($1, $2) - ON CONFLICT (name) DO UPDATE SET - config = $2; + res, err := ds.db.Exec(` + UPDATE apps SET + config = $2 + WHERE name = $1 + RETURNING *; `, app.Name, string(cbyte), @@ -95,10 +135,23 @@ func (ds *PostgresDatastore) StoreApp(app *models.App) (*models.App, error) { return nil, err } + n, err := res.RowsAffected() + if err != nil { + return nil, err + } + + if n == 0 { + return nil, models.ErrAppsNotFound + } + return app, nil } func (ds *PostgresDatastore) RemoveApp(appName string) error { + if appName == "" { + return models.ErrDatastoreEmptyAppName + } + _, err := ds.db.Exec(` DELETE FROM apps WHERE name = $1 @@ -112,6 +165,10 @@ func (ds *PostgresDatastore) RemoveApp(appName string) error { } func (ds *PostgresDatastore) GetApp(name string) (*models.App, error) { + if name == "" { + return nil, models.ErrDatastoreEmptyAppName + } + row := ds.db.QueryRow("SELECT name, config FROM apps WHERE name=$1", name) var resName string @@ -125,6 +182,9 @@ func (ds *PostgresDatastore) GetApp(name string) (*models.App, error) { json.Unmarshal([]byte(config), &res.Config) if err != nil { + if err == sql.ErrNoRows { + return nil, models.ErrAppsNotFound + } return nil, err } @@ -132,10 +192,19 @@ func (ds *PostgresDatastore) GetApp(name string) (*models.App, error) { } func scanApp(scanner rowScanner, app *models.App) error { + var configStr string + err := scanner.Scan( &app.Name, + &configStr, ) + if configStr == "" { + return models.ErrAppsNotFound + } + + json.Unmarshal([]byte(configStr), &app.Config) + return err } @@ -156,18 +225,25 @@ func (ds *PostgresDatastore) GetApps(filter *models.AppFilter) ([]*models.App, e err := scanApp(rows, &app) if err != nil { - return nil, err + if err == sql.ErrNoRows { + return res, nil + } + return res, err } res = append(res, &app) } if err := rows.Err(); err != nil { - return nil, err + return res, err } return res, nil } -func (ds *PostgresDatastore) StoreRoute(route *models.Route) (*models.Route, error) { +func (ds *PostgresDatastore) InsertRoute(route *models.Route) (*models.Route, error) { + if route == nil { + return nil, models.ErrDatastoreEmptyRoute + } + hbyte, err := json.Marshal(route.Headers) if err != nil { return nil, err @@ -187,14 +263,47 @@ func (ds *PostgresDatastore) StoreRoute(route *models.Route) (*models.Route, err headers, config ) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT (app_name, path) DO UPDATE SET - path = $2, + VALUES ($1, $2, $3, $4, $5, $6);`, + route.AppName, + route.Path, + route.Image, + route.Memory, + string(hbyte), + string(cbyte), + ) + + 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(route *models.Route) (*models.Route, error) { + if route == nil { + return nil, models.ErrDatastoreEmptyRoute + } + + hbyte, err := json.Marshal(route.Headers) + if err != nil { + return nil, err + } + + cbyte, err := json.Marshal(route.Config) + if err != nil { + return nil, err + } + + res, err := ds.db.Exec(` + UPDATE routes SET image = $3, memory = $4, headers = $5, - config = $6; - `, + config = $6 + WHERE app_name = $1 AND path = $2;`, route.AppName, route.Path, route.Image, @@ -206,11 +315,29 @@ func (ds *PostgresDatastore) StoreRoute(route *models.Route) (*models.Route, err 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 } func (ds *PostgresDatastore) RemoveRoute(appName, routePath string) error { - _, err := ds.db.Exec(` + if appName == "" { + return models.ErrDatastoreEmptyAppName + } + + if routePath == "" { + return models.ErrDatastoreEmptyRoutePath + } + + res, err := ds.db.Exec(` DELETE FROM routes WHERE path = $1 AND app_name = $2 `, routePath, appName) @@ -218,6 +345,16 @@ func (ds *PostgresDatastore) RemoveRoute(appName, routePath string) error { if err != nil { return err } + + n, err := res.RowsAffected() + if err != nil { + return err + } + + if n == 0 { + return models.ErrRoutesRemoving + } + return nil } @@ -244,10 +381,18 @@ func scanRoute(scanner rowScanner, route *models.Route) error { return err } -func getRoute(qr rowQuerier, routePath string) (*models.Route, error) { +func (ds *PostgresDatastore) GetRoute(appName, routePath string) (*models.Route, error) { + if appName == "" { + return nil, models.ErrDatastoreEmptyAppName + } + + if routePath == "" { + return nil, models.ErrDatastoreEmptyRoutePath + } + var route models.Route - row := qr.QueryRow(fmt.Sprintf("%s WHERE path=$1", routeSelector), routePath) + row := ds.db.QueryRow(fmt.Sprintf("%s WHERE app_name=$1 AND path=$2", routeSelector), appName, routePath) err := scanRoute(row, &route) if err == sql.ErrNoRows { @@ -258,10 +403,6 @@ func getRoute(qr rowQuerier, routePath string) (*models.Route, error) { return &route, nil } -func (ds *PostgresDatastore) GetRoute(appName, routePath string) (*models.Route, error) { - return getRoute(ds.db, routePath) -} - func (ds *PostgresDatastore) GetRoutes(filter *models.RouteFilter) ([]*models.Route, error) { res := []*models.Route{} filterQuery := buildFilterQuery(filter) diff --git a/api/datastore/postgres_test.go b/api/datastore/postgres_test.go new file mode 100644 index 000000000..806fd14e8 --- /dev/null +++ b/api/datastore/postgres_test.go @@ -0,0 +1,277 @@ +package datastore + +import ( + "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() + + ds, err := New(tmpPostgres) + if err != nil { + t.Fatalf("Error when creating datastore: %v", err) + } + + testApp := &models.App{ + Name: "Test", + } + + testRoute := &models.Route{ + AppName: testApp.Name, + Path: "/test", + Image: "iron/hello", + } + + // Testing insert app + _, err = ds.InsertApp(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(&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(testApp) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test InsertApp: error when storing new app: %s", err) + } + + _, err = ds.InsertApp(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(&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("") + 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(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(&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("") + 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(testApp.Name) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test RemoveApp: error: %s", err) + } + app, err = ds.GetApp(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(&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(testApp) + + // Testing insert route + _, err = ds.InsertRoute(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(testRoute) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test InsertRoute: error when storing new route: %s", err) + } + + _, err = ds.InsertRoute(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(testRoute) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test UpdateRoute: unexpected error: %v", err) + } + + // Testing get + _, err = ds.GetRoute("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("", "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(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(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(&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("", "") + 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("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(testRoute.AppName, testRoute.Path) + if err != nil { + t.Log(buf.String()) + t.Fatalf("Test RemoveApp: unexpected error: %v", err) + } + + _, err = ds.UpdateRoute(&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(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") + } +} diff --git a/api/models/app.go b/api/models/app.go index 83780ce54..30f7f48c0 100644 --- a/api/models/app.go +++ b/api/models/app.go @@ -13,6 +13,7 @@ var ( ErrAppsRemoving = errors.New("Could not remove app from datastore") ErrAppsGet = errors.New("Could not get app from datastore") ErrAppsList = errors.New("Could not list apps from datastore") + ErrAppsAlreadyExists = errors.New("App already exists") ErrAppsNotFound = errors.New("App not found") ErrAppsNothingToUpdate = errors.New("Nothing to update") ErrAppsMissingNew = errors.New("Missing new application") diff --git a/api/models/datastore.go b/api/models/datastore.go index e603109c6..d73cfcf7e 100644 --- a/api/models/datastore.go +++ b/api/models/datastore.go @@ -5,13 +5,15 @@ import "errors" type Datastore interface { GetApp(appName string) (*App, error) GetApps(*AppFilter) ([]*App, error) - StoreApp(*App) (*App, error) + InsertApp(app *App) (*App, error) + UpdateApp(app *App) (*App, error) RemoveApp(appName string) error GetRoute(appName, routePath string) (*Route, error) GetRoutes(*RouteFilter) (routes []*Route, err error) GetRoutesByApp(string, *RouteFilter) (routes []*Route, err error) - StoreRoute(*Route) (*Route, error) + InsertRoute(route *Route) (*Route, error) + UpdateRoute(route *Route) (*Route, error) RemoveRoute(appName, routePath string) error // The following provide a generic key value store for arbitrary data, can be used by extensions to store extra data diff --git a/api/models/route.go b/api/models/route.go index 5ec7073ac..42b4a9289 100644 --- a/api/models/route.go +++ b/api/models/route.go @@ -11,14 +11,15 @@ import ( ) var ( - ErrRoutesCreate = errors.New("Could not create route") - ErrRoutesUpdate = errors.New("Could not update route") - ErrRoutesRemoving = errors.New("Could not remove route from datastore") - ErrRoutesGet = errors.New("Could not get route from datastore") - ErrRoutesList = errors.New("Could not list routes from datastore") - ErrRoutesNotFound = errors.New("Route not found") - ErrRoutesMissingNew = errors.New("Missing new route") - ErrInvalidPayload = errors.New("Invalid payload") + ErrRoutesCreate = errors.New("Could not create route") + ErrRoutesUpdate = errors.New("Could not update route") + ErrRoutesRemoving = errors.New("Could not remove route from datastore") + ErrRoutesGet = errors.New("Could not get route from datastore") + ErrRoutesList = errors.New("Could not list routes from datastore") + ErrRoutesAlreadyExists = errors.New("Route already exists") + ErrRoutesNotFound = errors.New("Route not found") + ErrRoutesMissingNew = errors.New("Missing new route") + ErrInvalidPayload = errors.New("Invalid payload") ) type Routes []*Route diff --git a/api/server/apps_create.go b/api/server/apps_create.go index 804aee564..e12c3b378 100644 --- a/api/server/apps_create.go +++ b/api/server/apps_create.go @@ -41,7 +41,7 @@ func handleAppCreate(c *gin.Context) { return } - _, err = Api.Datastore.StoreApp(wapp.App) + _, err = Api.Datastore.InsertApp(wapp.App) if err != nil { log.WithError(err).Errorln(models.ErrAppsCreate) c.JSON(http.StatusInternalServerError, simpleError(models.ErrAppsCreate)) diff --git a/api/server/apps_test.go b/api/server/apps_test.go index 790295b3d..729134c72 100644 --- a/api/server/apps_test.go +++ b/api/server/apps_test.go @@ -26,28 +26,29 @@ func setLogBuffer() *bytes.Buffer { func TestAppCreate(t *testing.T) { buf := setLogBuffer() - New(&datastore.Mock{}, &mqs.Mock{}, testRunner(t)) - s := New(&datastore.Mock{}, &mqs.Mock{}, testRunner(t)) - router := testRouter(s) for i, test := range []struct { + mock *datastore.Mock path string body string expectedCode int expectedError error }{ // errors - {"/v1/apps", ``, http.StatusBadRequest, models.ErrInvalidJSON}, - {"/v1/apps", `{}`, http.StatusBadRequest, models.ErrAppsMissingNew}, - {"/v1/apps", `{ "name": "Test" }`, http.StatusBadRequest, models.ErrAppsMissingNew}, - {"/v1/apps", `{ "app": { "name": "" } }`, http.StatusInternalServerError, models.ErrAppsValidationMissingName}, - {"/v1/apps", `{ "app": { "name": "1234567890123456789012345678901" } }`, http.StatusInternalServerError, models.ErrAppsValidationTooLongName}, - {"/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusInternalServerError, models.ErrAppsValidationInvalidName}, - {"/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusInternalServerError, models.ErrAppsValidationInvalidName}, + {&datastore.Mock{}, "/v1/apps", ``, http.StatusBadRequest, models.ErrInvalidJSON}, + {&datastore.Mock{}, "/v1/apps", `{}`, http.StatusBadRequest, models.ErrAppsMissingNew}, + {&datastore.Mock{}, "/v1/apps", `{ "name": "Test" }`, http.StatusBadRequest, models.ErrAppsMissingNew}, + {&datastore.Mock{}, "/v1/apps", `{ "app": { "name": "" } }`, http.StatusInternalServerError, models.ErrAppsValidationMissingName}, + {&datastore.Mock{}, "/v1/apps", `{ "app": { "name": "1234567890123456789012345678901" } }`, http.StatusInternalServerError, models.ErrAppsValidationTooLongName}, + {&datastore.Mock{}, "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusInternalServerError, models.ErrAppsValidationInvalidName}, + {&datastore.Mock{}, "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusInternalServerError, models.ErrAppsValidationInvalidName}, // success - {"/v1/apps", `{ "app": { "name": "teste" } }`, http.StatusCreated, nil}, + {&datastore.Mock{}, "/v1/apps", `{ "app": { "name": "teste" } }`, http.StatusCreated, nil}, } { + s := New(test.mock, &mqs.Mock{}, testRunner(t)) + router := testRouter(s) + body := bytes.NewBuffer([]byte(test.body)) _, rec := routerRequest(t, router, "POST", test.path, body) @@ -171,21 +172,27 @@ func TestAppGet(t *testing.T) { func TestAppUpdate(t *testing.T) { buf := setLogBuffer() - s := New(&datastore.Mock{}, &mqs.Mock{}, testRunner(t)) - router := testRouter(s) for i, test := range []struct { + mock *datastore.Mock path string body string expectedCode int expectedError error }{ // errors - {"/v1/apps/myapp", ``, http.StatusBadRequest, models.ErrInvalidJSON}, + {&datastore.Mock{}, "/v1/apps/myapp", ``, http.StatusBadRequest, models.ErrInvalidJSON}, // success - {"/v1/apps/myapp", `{ "app": { "config": { "test": "1" } } }`, http.StatusOK, nil}, + {&datastore.Mock{ + FakeApp: &models.App{ + Name: "myapp", + }, + }, "/v1/apps/myapp", `{ "app": { "config": { "test": "1" } } }`, http.StatusOK, nil}, } { + s := New(test.mock, &mqs.Mock{}, testRunner(t)) + router := testRouter(s) + body := bytes.NewBuffer([]byte(test.body)) _, rec := routerRequest(t, router, "PUT", test.path, body) diff --git a/api/server/apps_update.go b/api/server/apps_update.go index 4b58775f4..6863a79c7 100644 --- a/api/server/apps_update.go +++ b/api/server/apps_update.go @@ -28,10 +28,10 @@ func handleAppUpdate(c *gin.Context) { return } - app, err := Api.Datastore.StoreApp(wapp.App) + app, err := Api.Datastore.UpdateApp(wapp.App) if err != nil { - log.WithError(err).Debug(models.ErrAppsCreate) - c.JSON(http.StatusInternalServerError, simpleError(models.ErrAppsCreate)) + log.WithError(err).Debug(models.ErrAppsUpdate) + c.JSON(http.StatusInternalServerError, simpleError(models.ErrAppsUpdate)) return } diff --git a/api/server/routes_create.go b/api/server/routes_create.go index 01d1c3036..abdb69772 100644 --- a/api/server/routes_create.go +++ b/api/server/routes_create.go @@ -53,6 +53,7 @@ func handleRouteCreate(c *gin.Context) { } if app == nil { + // Create a new application and add the route to that new application newapp := &models.App{Name: wroute.Route.AppName} if err := newapp.Validate(); err != nil { log.Error(err) @@ -60,7 +61,7 @@ func handleRouteCreate(c *gin.Context) { return } - app, err = Api.Datastore.StoreApp(newapp) + app, err = Api.Datastore.InsertApp(newapp) if err != nil { log.WithError(err).Error(models.ErrAppsCreate) c.JSON(http.StatusInternalServerError, simpleError(models.ErrAppsCreate)) @@ -68,7 +69,7 @@ func handleRouteCreate(c *gin.Context) { } } - _, err = Api.Datastore.StoreRoute(wroute.Route) + _, err = Api.Datastore.InsertRoute(wroute.Route) if err != nil { log.WithError(err).Error(models.ErrRoutesCreate) c.JSON(http.StatusInternalServerError, simpleError(models.ErrRoutesCreate)) diff --git a/api/server/routes_test.go b/api/server/routes_test.go index 52cd8ae50..ed35d8bfe 100644 --- a/api/server/routes_test.go +++ b/api/server/routes_test.go @@ -13,27 +13,29 @@ import ( func TestRouteCreate(t *testing.T) { buf := setLogBuffer() - s := New(&datastore.Mock{}, &mqs.Mock{}, testRunner(t)) - router := testRouter(s) for i, test := range []struct { + mock *datastore.Mock path string body string expectedCode int expectedError error }{ // errors - {"/v1/apps/a/routes", ``, http.StatusBadRequest, models.ErrInvalidJSON}, - {"/v1/apps/a/routes", `{ }`, http.StatusBadRequest, models.ErrRoutesMissingNew}, - {"/v1/apps/a/routes", `{ "path": "/myroute" }`, http.StatusBadRequest, models.ErrRoutesMissingNew}, - {"/v1/apps/a/routes", `{ "route": { } }`, http.StatusInternalServerError, models.ErrRoutesValidationMissingImage}, - {"/v1/apps/a/routes", `{ "route": { "image": "iron/hello" } }`, http.StatusInternalServerError, models.ErrRoutesValidationMissingPath}, - {"/v1/apps/a/routes", `{ "route": { "image": "iron/hello", "path": "myroute" } }`, http.StatusInternalServerError, models.ErrRoutesValidationInvalidPath}, - {"/v1/apps/$/routes", `{ "route": { "image": "iron/hello", "path": "/myroute" } }`, http.StatusInternalServerError, models.ErrAppsValidationInvalidName}, + {&datastore.Mock{}, "/v1/apps/a/routes", ``, http.StatusBadRequest, models.ErrInvalidJSON}, + {&datastore.Mock{}, "/v1/apps/a/routes", `{ }`, http.StatusBadRequest, models.ErrRoutesMissingNew}, + {&datastore.Mock{}, "/v1/apps/a/routes", `{ "path": "/myroute" }`, http.StatusBadRequest, models.ErrRoutesMissingNew}, + {&datastore.Mock{}, "/v1/apps/a/routes", `{ "route": { } }`, http.StatusInternalServerError, models.ErrRoutesValidationMissingImage}, + {&datastore.Mock{}, "/v1/apps/a/routes", `{ "route": { "image": "iron/hello" } }`, http.StatusInternalServerError, models.ErrRoutesValidationMissingPath}, + {&datastore.Mock{}, "/v1/apps/a/routes", `{ "route": { "image": "iron/hello", "path": "myroute" } }`, http.StatusInternalServerError, models.ErrRoutesValidationInvalidPath}, + {&datastore.Mock{}, "/v1/apps/$/routes", `{ "route": { "image": "iron/hello", "path": "/myroute" } }`, http.StatusInternalServerError, models.ErrAppsValidationInvalidName}, // success - {"/v1/apps/a/routes", `{ "route": { "name": "myroute", "image": "iron/hello", "path": "/myroute" } }`, http.StatusCreated, nil}, + {&datastore.Mock{}, "/v1/apps/a/routes", `{ "route": { "image": "iron/hello", "path": "/myroute" } }`, http.StatusCreated, nil}, } { + s := New(test.mock, &mqs.Mock{}, testRunner(t)) + router := testRouter(s) + body := bytes.NewBuffer([]byte(test.body)) _, rec := routerRequest(t, router, "POST", test.path, body) diff --git a/api/server/routes_update.go b/api/server/routes_update.go index 93b0e23db..02a99a424 100644 --- a/api/server/routes_update.go +++ b/api/server/routes_update.go @@ -37,10 +37,10 @@ func handleRouteUpdate(c *gin.Context) { return } - _, err = Api.Datastore.StoreRoute(wroute.Route) + _, err = Api.Datastore.UpdateRoute(wroute.Route) if err != nil { - log.WithError(err).Debug(models.ErrAppsCreate) - c.JSON(http.StatusInternalServerError, simpleError(models.ErrAppsCreate)) + log.WithError(err).Debug(models.ErrRoutesUpdate) + c.JSON(http.StatusInternalServerError, simpleError(models.ErrRoutesUpdate)) return } diff --git a/api/server/runner_test.go b/api/server/runner_test.go index 16b90e438..dda59cdf9 100644 --- a/api/server/runner_test.go +++ b/api/server/runner_test.go @@ -2,7 +2,6 @@ package server import ( "bytes" - "fmt" "net/http" "strings" "testing" @@ -92,7 +91,6 @@ func TestRouteRunnerPost(t *testing.T) { resp := getErrorResponse(t, rec) respMsg := resp.Error.Message expMsg := test.expectedError.Error() - fmt.Println(respMsg == expMsg) if respMsg != expMsg && !strings.Contains(respMsg, expMsg) { t.Log(buf.String()) t.Errorf("Test %d: Expected error message to have `%s`", @@ -165,7 +163,6 @@ func TestMatchRoute(t *testing.T) { for j, param := range test.expectedParams { if params[j].Key != param.Key || params[j].Value != param.Value { t.Log(buf.String()) - fmt.Println(params[j]) t.Errorf("Test %d: expected param %d, key = %s, value = %s", i, j, param.Key, param.Value) } } diff --git a/api/server/server.go b/api/server/server.go index 19635367a..fb0ab1d6c 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -3,6 +3,7 @@ package server import ( "context" "encoding/json" + "errors" "io/ioutil" "net/http" "path" @@ -185,6 +186,8 @@ func (s *Server) bindHandlers() { engine.NoRoute(handleSpecial) } +var ErrInternalServerError = errors.New("Something unexpected happened on the server") + func simpleError(err error) *models.Error { return &models.Error{&models.ErrorBody{Message: err.Error()}} } diff --git a/api/server/server_test.go b/api/server/server_test.go index f7d17a30c..2d27f18c0 100644 --- a/api/server/server_test.go +++ b/api/server/server_test.go @@ -72,6 +72,7 @@ func getErrorResponse(t *testing.T, rec *httptest.ResponseRecorder) models.Error } func prepareBolt(t *testing.T) (models.Datastore, func()) { + os.Remove(tmpBolt) ds, err := datastore.New("bolt://" + tmpBolt) if err != nil { t.Fatal("Error when creating datastore: %s", err) diff --git a/circle.yml b/circle.yml index 103c97c4d..f191c3da5 100644 --- a/circle.yml +++ b/circle.yml @@ -31,6 +31,8 @@ test: override: - make test-docker: pwd: $GO_PROJECT + - make test-datastore: + pwd: $GO_PROJECT deployment: release: