From e3651e34f030a6fc6de1b0519311b45cb5922439 Mon Sep 17 00:00:00 2001 From: Sonja Happ Date: Fri, 6 Sep 2019 15:10:25 +0200 Subject: [PATCH] - revise testing of widget enpoints - add validators for widgets - revise implementation of widget endpoints - clean up testdata, serializers and response - improve documentation for swaggo --- common/responses.go | 26 -- common/serializers.go | 41 --- common/testdata.go | 77 +----- doc/api/responses.go | 8 + routes/widget/widget_endpoints.go | 153 ++++++----- routes/widget/widget_methods.go | 6 +- routes/widget/widget_middleware.go | 10 +- routes/widget/widget_test.go | 418 ++++++++++++++++++++++++++--- routes/widget/widget_validators.go | 109 ++++++++ 9 files changed, 594 insertions(+), 254 deletions(-) create mode 100644 routes/widget/widget_validators.go diff --git a/common/responses.go b/common/responses.go index 64f7e77..0f3e284 100644 --- a/common/responses.go +++ b/common/responses.go @@ -1,23 +1,5 @@ package common -import "github.com/jinzhu/gorm/dialects/postgres" - -type WidgetResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Width uint `json:"width"` - Height uint `json:"height"` - MinWidth uint `json:"minWidth"` - MinHeight uint `json:"minHeight"` - X int `json:"x"` - Y int `json:"y"` - Z int `json:"z"` - DashboardID uint `json:"dashboardID"` - IsLocked bool `json:"isLocked"` - CustomProperties postgres.Jsonb `json:"customProperties"` -} - type FileResponse struct { Name string `json:"name"` ID uint `json:"id"` @@ -52,14 +34,6 @@ type ResponseMsgSignal struct { Signal SignalResponse `json:"signal"` } -type ResponseMsgWidgets struct { - Widgets []WidgetResponse `json:"widgets"` -} - -type ResponseMsgWidget struct { - Widget WidgetResponse `json:"widget"` -} - type ResponseMsgFiles struct { Files []FileResponse `json:"files"` } diff --git a/common/serializers.go b/common/serializers.go index 051a064..e143a5b 100644 --- a/common/serializers.go +++ b/common/serializers.go @@ -4,47 +4,6 @@ import ( "github.com/gin-gonic/gin" ) -// Widget/s Serializers - -type WidgetsSerializer struct { - Ctx *gin.Context - Widgets []Widget -} - -func (self *WidgetsSerializer) Response() []WidgetResponse { - response := []WidgetResponse{} - for _, widget := range self.Widgets { - serializer := WidgetSerializer{self.Ctx, widget} - response = append(response, serializer.Response()) - } - return response -} - -type WidgetSerializer struct { - Ctx *gin.Context - Widget -} - -func (self *WidgetSerializer) Response() WidgetResponse { - - response := WidgetResponse{ - ID: self.ID, - Name: self.Name, - Type: self.Type, - Width: self.Width, - Height: self.Height, - MinWidth: self.MinWidth, - MinHeight: self.MinHeight, - X: self.X, - Y: self.Y, - Z: self.Z, - DashboardID: self.DashboardID, - IsLocked: self.IsLocked, - CustomProperties: self.CustomProperties, - } - return response -} - // File/s Serializers type FilesSerializerNoAssoc struct { diff --git a/common/testdata.go b/common/testdata.go index 37aedc4..4f93ebe 100644 --- a/common/testdata.go +++ b/common/testdata.go @@ -265,7 +265,6 @@ var FileD = File{ // Widgets var customPropertiesA = json.RawMessage(`{"property1" : "testValue1A", "property2" : "testValue2A", "property3" : 42}`) var customPropertiesB = json.RawMessage(`{"property1" : "testValue1B", "property2" : "testValue2B", "property3" : 43}`) -var customPropertiesC = json.RawMessage(`{"property1" : "testValue1C", "property2" : "testValue2C", "property3" : 44}`) var WidgetA = Widget{ Name: "Widget_A", @@ -281,21 +280,6 @@ var WidgetA = Widget{ CustomProperties: postgres.Jsonb{customPropertiesA}, } -var WidgetA_response = WidgetResponse{ - ID: 1, - Name: WidgetA.Name, - Type: WidgetA.Type, - Width: WidgetA.Width, - Height: WidgetA.Height, - MinWidth: WidgetA.MinWidth, - MinHeight: WidgetA.MinHeight, - X: WidgetA.X, - Y: WidgetA.Y, - Z: WidgetA.Z, - IsLocked: WidgetA.IsLocked, - CustomProperties: WidgetA.CustomProperties, -} - var WidgetB = Widget{ Name: "Widget_B", Type: "slider", @@ -305,66 +289,7 @@ var WidgetB = Widget{ MinWidth: 50, X: 100, Y: -40, - Z: 0, + Z: -1, IsLocked: false, CustomProperties: postgres.Jsonb{customPropertiesB}, } - -var WidgetB_response = WidgetResponse{ - ID: 2, - Name: WidgetB.Name, - Type: WidgetB.Type, - Width: WidgetB.Width, - Height: WidgetB.Height, - MinWidth: WidgetB.MinWidth, - MinHeight: WidgetB.MinHeight, - X: WidgetB.X, - Y: WidgetB.Y, - Z: WidgetB.Z, - IsLocked: WidgetB.IsLocked, - CustomProperties: WidgetB.CustomProperties, -} - -var WidgetC = Widget{ - Name: "Widget_C", - Type: "bargraph", - Height: 30, - Width: 100, - MinHeight: 20, - MinWidth: 50, - X: 11, - Y: 12, - Z: 13, - IsLocked: false, - CustomProperties: postgres.Jsonb{customPropertiesC}, -} - -var WidgetC_response = WidgetResponse{ - ID: 3, - Name: WidgetC.Name, - Type: WidgetC.Type, - Width: WidgetC.Width, - Height: WidgetC.Height, - MinWidth: WidgetC.MinWidth, - MinHeight: WidgetC.MinHeight, - X: WidgetC.X, - Y: WidgetC.Y, - Z: WidgetC.Z, - IsLocked: WidgetC.IsLocked, - CustomProperties: WidgetC.CustomProperties, -} - -var WidgetCUpdated_response = WidgetResponse{ - ID: 3, - Name: "Widget_CUpdated", - Type: WidgetC.Type, - Height: 35, - Width: 110, - MinHeight: WidgetC.MinHeight, - MinWidth: WidgetC.MinWidth, - X: WidgetC.X, - Y: WidgetC.Y, - Z: WidgetC.Z, - IsLocked: WidgetC.IsLocked, - CustomProperties: WidgetC.CustomProperties, -} diff --git a/doc/api/responses.go b/doc/api/responses.go index b64e4fa..3a0c40c 100644 --- a/doc/api/responses.go +++ b/doc/api/responses.go @@ -56,3 +56,11 @@ type ResponseDashboards struct { type ResponseDashboard struct { dashboard common.Dashboard } + +type ResponseWidgets struct { + widgets []common.Widget +} + +type ResponseWidget struct { + widget common.Widget +} diff --git a/routes/widget/widget_endpoints.go b/routes/widget/widget_endpoints.go index 8bd9afe..77a51c7 100644 --- a/routes/widget/widget_endpoints.go +++ b/routes/widget/widget_endpoints.go @@ -1,6 +1,7 @@ package widget import ( + "fmt" "net/http" "github.com/gin-gonic/gin" @@ -22,11 +23,10 @@ func RegisterWidgetEndpoints(r *gin.RouterGroup) { // @ID getWidgets // @Produce json // @Tags widgets -// @Success 200 {array} common.WidgetResponse "Array of widgets to which belong to dashboard" -// @Failure 401 "Unauthorized Access" -// @Failure 403 "Access forbidden." -// @Failure 404 "Not found" -// @Failure 500 "Internal server error" +// @Success 200 {object} docs.ResponseWidgets "Widgets to which belong to dashboard" +// @Failure 404 {object} docs.ResponseError "Not found" +// @Failure 422 {object} docs.ResponseError "Unprocessable entity" +// @Failure 500 {object} docs.ResponseError "Internal server error" // @Param dashboardID query int true "Dashboard ID" // @Router /widgets [get] func getWidgets(c *gin.Context) { @@ -43,9 +43,8 @@ func getWidgets(c *gin.Context) { return } - serializer := common.WidgetsSerializer{c, widgets} c.JSON(http.StatusOK, gin.H{ - "widgets": serializer.Response(), + "widgets": widgets, }) } @@ -55,51 +54,51 @@ func getWidgets(c *gin.Context) { // @Accept json // @Produce json // @Tags widgets -// @Param inputWidget body common.ResponseMsgWidget true "Widget to be added incl. ID of dashboard" -// @Success 200 "OK." -// @Failure 401 "Unauthorized Access" -// @Failure 403 "Access forbidden." -// @Failure 404 "Not found" -// @Failure 500 "Internal server error" +// @Param inputWidget body widget.validNewWidget true "Widget to be added incl. ID of dashboard" +// @Success 200 {object} docs.ResponseWidget "Widget that was added" +// @Failure 400 {object} docs.ResponseError "Bad request" +// @Failure 404 {object} docs.ResponseError "Not found" +// @Failure 422 {object} docs.ResponseError "Unprocessable entity" +// @Failure 500 {object} docs.ResponseError "Internal server error" // @Router /widgets [post] func addWidget(c *gin.Context) { - var newWidgetData common.ResponseMsgWidget - err := c.BindJSON(&newWidgetData) - if err != nil { - errormsg := "Bad request. Error binding form data to JSON: " + err.Error() + var req addWidgetRequest + if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ - "error": errormsg, + "success": false, + "message": fmt.Sprintf("%v", err), }) return } - var newWidget Widget - newWidget.Name = newWidgetData.Widget.Name - newWidget.Type = newWidgetData.Widget.Type - newWidget.Height = newWidgetData.Widget.Height - newWidget.Width = newWidgetData.Widget.Width - newWidget.MinHeight = newWidgetData.Widget.MinHeight - newWidget.MinWidth = newWidgetData.Widget.MinWidth - newWidget.X = newWidgetData.Widget.X - newWidget.Y = newWidgetData.Widget.Y - newWidget.Z = newWidgetData.Widget.Z - newWidget.CustomProperties = newWidgetData.Widget.CustomProperties - newWidget.IsLocked = newWidgetData.Widget.IsLocked - newWidget.DashboardID = newWidgetData.Widget.DashboardID + // Validate the request + if err := req.validate(); err != nil { + c.JSON(http.StatusUnprocessableEntity, gin.H{ + "success": false, + "message": fmt.Sprintf("%v", err), + }) + return + } + // Create the new widget from the request + newWidget := req.createWidget() + + // Check if user is allowed to modify selected dashboard (scenario) ok, _ := dashboard.CheckPermissions(c, common.Create, "body", int(newWidget.DashboardID)) if !ok { return } - err = newWidget.addToDashboard() - - if common.ProvideErrorResponse(c, err) == false { - c.JSON(http.StatusOK, gin.H{ - "message": "OK.", - }) + err := newWidget.addToDashboard() + if err != nil { + common.ProvideErrorResponse(c, err) + return } + + c.JSON(http.StatusOK, gin.H{ + "widget": newWidget.Widget, + }) } // updateWidget godoc @@ -108,37 +107,60 @@ func addWidget(c *gin.Context) { // @Tags widgets // @Accept json // @Produce json -// @Param inputWidget body common.ResponseMsgWidget true "Widget to be updated" -// @Success 200 "OK." -// @Failure 401 "Unauthorized Access" -// @Failure 403 "Access forbidden." -// @Failure 404 "Not found" -// @Failure 500 "Internal server error" +// @Param inputWidget body widget.validUpdatedWidget true "Widget to be updated" +// @Success 200 {object} docs.ResponseWidget "Widget that was updated" +// @Failure 400 {object} docs.ResponseError "Bad request" +// @Failure 404 {object} docs.ResponseError "Not found" +// @Failure 422 {object} docs.ResponseError "Unprocessable entity" +// @Failure 500 {object} docs.ResponseError "Internal server error" // @Param widgetID path int true "Widget ID" // @Router /widgets/{widgetID} [put] func updateWidget(c *gin.Context) { - ok, w := CheckPermissions(c, common.Update, -1) + ok, oldWidget := CheckPermissions(c, common.Update, -1) if !ok { return } - var modifiedWidget common.ResponseMsgWidget - err := c.BindJSON(&modifiedWidget) - if err != nil { - errormsg := "Bad request. Error binding form data to JSON: " + err.Error() + var req updateWidgetRequest + if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ - "error": errormsg, + "success": false, + "message": fmt.Sprintf("%v", err), }) return } - err = w.update(modifiedWidget.Widget) - if common.ProvideErrorResponse(c, err) == false { - c.JSON(http.StatusOK, gin.H{ - "message": "OK.", + // Validate the request + if err := req.validate(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": fmt.Sprintf("%v", err), }) + return } + + // Create the updatedScenario from oldScenario + updatedWidget, err := req.updatedWidget(oldWidget) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": fmt.Sprintf("%v", err), + }) + return + } + + // Update the widget in the DB + err = oldWidget.update(updatedWidget) + if err != nil { + common.ProvideErrorResponse(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "widget": updatedWidget.Widget, + }) + } // getWidget godoc @@ -146,11 +168,11 @@ func updateWidget(c *gin.Context) { // @ID getWidget // @Tags widgets // @Produce json -// @Success 200 {object} common.WidgetResponse "Requested widget." -// @Failure 401 "Unauthorized Access" -// @Failure 403 "Access forbidden." -// @Failure 404 "Not found" -// @Failure 500 "Internal server error" +// @Success 200 {object} docs.ResponseWidget "Widget that was requested" +// @Failure 400 {object} docs.ResponseError "Bad request" +// @Failure 404 {object} docs.ResponseError "Not found" +// @Failure 422 {object} docs.ResponseError "Unprocessable entity" +// @Failure 500 {object} docs.ResponseError "Internal server error" // @Param widgetID path int true "Widget ID" // @Router /widgets/{widgetID} [get] func getWidget(c *gin.Context) { @@ -160,9 +182,8 @@ func getWidget(c *gin.Context) { return } - serializer := common.WidgetSerializer{c, w.Widget} c.JSON(http.StatusOK, gin.H{ - "widget": serializer.Response(), + "widget": w.Widget, }) } @@ -171,11 +192,11 @@ func getWidget(c *gin.Context) { // @ID deleteWidget // @Tags widgets // @Produce json -// @Success 200 "OK." -// @Failure 401 "Unauthorized Access" -// @Failure 403 "Access forbidden." -// @Failure 404 "Not found" -// @Failure 500 "Internal server error" +// @Success 200 {object} docs.ResponseWidget "Widget that was deleted" +// @Failure 400 {object} docs.ResponseError "Bad request" +// @Failure 404 {object} docs.ResponseError "Not found" +// @Failure 422 {object} docs.ResponseError "Unprocessable entity" +// @Failure 500 {object} docs.ResponseError "Internal server error" // @Param widgetID path int true "Widget ID" // @Router /widgets/{widgetID} [delete] func deleteWidget(c *gin.Context) { @@ -191,6 +212,6 @@ func deleteWidget(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{ - "message": "OK.", + "widget": w.Widget, }) } diff --git a/routes/widget/widget_methods.go b/routes/widget/widget_methods.go index d87e46a..1381c70 100644 --- a/routes/widget/widget_methods.go +++ b/routes/widget/widget_methods.go @@ -1,8 +1,6 @@ package widget import ( - "fmt" - "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/common" "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/routes/dashboard" ) @@ -21,7 +19,7 @@ func (w *Widget) ByID(id uint) error { db := common.GetDB() err := db.Find(w, id).Error if err != nil { - return fmt.Errorf("Widget with id=%v does not exist", id) + return err } return nil } @@ -46,7 +44,7 @@ func (w *Widget) addToDashboard() error { return err } -func (w *Widget) update(modifiedWidget common.WidgetResponse) error { +func (w *Widget) update(modifiedWidget Widget) error { db := common.GetDB() err := db.Model(w).Updates(map[string]interface{}{ diff --git a/routes/widget/widget_middleware.go b/routes/widget/widget_middleware.go index c6d3f06..8735349 100644 --- a/routes/widget/widget_middleware.go +++ b/routes/widget/widget_middleware.go @@ -17,7 +17,10 @@ func CheckPermissions(c *gin.Context, operation common.CRUD, widgetIDBody int) ( err := common.ValidateRole(c, common.ModelWidget, operation) if err != nil { - c.JSON(http.StatusUnprocessableEntity, "Access denied (role validation failed).") + c.JSON(http.StatusUnprocessableEntity, gin.H{ + "success": false, + "message": fmt.Sprintf("Access denied (role validation failed): %v", err), + }) return false, w } @@ -25,9 +28,10 @@ func CheckPermissions(c *gin.Context, operation common.CRUD, widgetIDBody int) ( if widgetIDBody < 0 { widgetID, err = strconv.Atoi(c.Param("widgetID")) if err != nil { - errormsg := fmt.Sprintf("Bad request. No or incorrect format of widgetID path parameter") + c.JSON(http.StatusBadRequest, gin.H{ - "error": errormsg, + "success": false, + "message": fmt.Sprintf("Bad request. No or incorrect format of widgetID path parameter"), }) return false, w } diff --git a/routes/widget/widget_test.go b/routes/widget/widget_test.go index c88dd6b..b165d74 100644 --- a/routes/widget/widget_test.go +++ b/routes/widget/widget_test.go @@ -1,65 +1,407 @@ package widget import ( - "encoding/json" - "testing" - - "github.com/gin-gonic/gin" - + "fmt" "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/common" + "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/routes/dashboard" + "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/routes/scenario" "git.rwth-aachen.de/acs/public/villas/villasweb-backend-go/routes/user" + "github.com/gin-gonic/gin" + "github.com/jinzhu/gorm" + "github.com/jinzhu/gorm/dialects/postgres" + "github.com/stretchr/testify/assert" + "os" + "testing" ) -// Test /widgets endpoints -func TestWidgetEndpoints(t *testing.T) { +var router *gin.Engine +var db *gorm.DB - var token string +type WidgetRequest struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Width uint `json:"width,omitempty"` + Height uint `json:"height,omitempty"` + MinWidth uint `json:"minWidth,omitempty"` + MinHeight uint `json:"minHeight,omitempty"` + X int `json:"x,omitempty"` + Y int `json:"y,omitempty"` + Z int `json:"z,omitempty"` + DashboardID uint `json:"dashboardID,omitempty"` + IsLocked bool `json:"isLocked,omitempty"` + CustomProperties postgres.Jsonb `json:"customProperties,omitempty"` +} - var myWidgets = []common.WidgetResponse{common.WidgetA_response, common.WidgetB_response} - var msgWidgets = common.ResponseMsgWidgets{Widgets: myWidgets} - var msgWdg = common.ResponseMsgWidget{Widget: common.WidgetC_response} - var msgWdgupdated = common.ResponseMsgWidget{Widget: common.WidgetCUpdated_response} +type DashboardRequest struct { + Name string `json:"name,omitempty"` + Grid int `json:"grid,omitempty"` + ScenarioID uint `json:"scenarioID,omitempty"` +} - db := common.DummyInitDB() +type ScenarioRequest struct { + Name string `json:"name,omitempty"` + Running bool `json:"running,omitempty"` + StartParameters postgres.Jsonb `json:"startParameters,omitempty"` +} + +func addScenarioAndDashboard(token string) (scenarioID uint, dashboardID uint) { + + // POST $newScenario + newScenario := ScenarioRequest{ + Name: common.ScenarioA.Name, + Running: common.ScenarioA.Running, + StartParameters: common.ScenarioA.StartParameters, + } + _, resp, _ := common.NewTestEndpoint(router, token, + "/api/scenarios", "POST", common.KeyModels{"scenario": newScenario}) + + // Read newScenario's ID from the response + newScenarioID, _ := common.GetResponseID(resp) + + // test POST dashboards/ $newDashboard + newDashboard := DashboardRequest{ + Name: common.DashboardA.Name, + Grid: common.DashboardA.Grid, + ScenarioID: uint(newScenarioID), + } + _, resp, _ = common.NewTestEndpoint(router, token, + "/api/dashboards", "POST", common.KeyModels{"dashboard": newDashboard}) + + // Read newDashboard's ID from the response + newDashboardID, _ := common.GetResponseID(resp) + + return uint(newScenarioID), uint(newDashboardID) +} + +func TestMain(m *testing.M) { + + db = common.DummyInitDB() defer db.Close() - common.DummyPopulateDB(db) - router := gin.Default() + router = gin.Default() api := router.Group("/api") - // All endpoints require authentication except when someone wants to - // login (POST /authenticate) user.RegisterAuthenticate(api.Group("/authenticate")) - api.Use(user.Authentication(true)) - RegisterWidgetEndpoints(api.Group("/widgets")) + // scenario endpoints required here to first add a scenario to the DB + // that can be associated with a new dashboard + scenario.RegisterScenarioEndpoints(api.Group("/scenarios")) + // dashboard endpoints required here to first add a dashboard to the DB + // that can be associated with a new widget + dashboard.RegisterDashboardEndpoints(api.Group("/dashboards")) - credjson, _ := json.Marshal(common.CredUser) - msgOKjson, _ := json.Marshal(common.MsgOK) - msgWidgetsjson, _ := json.Marshal(msgWidgets) - msgWdgjson, _ := json.Marshal(msgWdg) - msgWdgupdatedjson, _ := json.Marshal(msgWdgupdated) + os.Exit(m.Run()) +} - token = common.AuthenticateForTest(t, router, "/api/authenticate", "POST", credjson, 200) +func TestAddWidget(t *testing.T) { + common.DropTables(db) + common.MigrateModels(db) + common.DummyAddOnlyUserTableWithAdminAndUsersDB(db) - // test GET widgets - common.TestEndpoint(t, router, token, "/api/widgets?dashboardID=1", "GET", nil, 200, msgWidgetsjson) + // authenticate as normal user + token, err := common.NewAuthenticateForTest(router, + "/api/authenticate", "POST", common.UserACredentials) + assert.NoError(t, err) - // test POST widgets - common.TestEndpoint(t, router, token, "/api/widgets", "POST", msgWdgjson, 200, msgOKjson) + _, dashboardID := addScenarioAndDashboard(token) - // test GET widgets/:widgetID to check if previous POST worked correctly - common.TestEndpoint(t, router, token, "/api/widgets/3", "GET", nil, 200, msgWdgjson) + // test POST widgets/ $newWidget + newWidget := WidgetRequest{ + Name: common.WidgetA.Name, + Type: common.WidgetA.Type, + Width: common.WidgetA.Width, + Height: common.WidgetA.Height, + MinWidth: common.WidgetA.MinWidth, + MinHeight: common.WidgetA.MinHeight, + X: common.WidgetA.X, + Y: common.WidgetA.Y, + Z: common.WidgetA.Z, + IsLocked: common.WidgetA.IsLocked, + CustomProperties: common.WidgetA.CustomProperties, + DashboardID: dashboardID, + } + code, resp, err := common.NewTestEndpoint(router, token, + "/api/widgets", "POST", common.KeyModels{"widget": newWidget}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) - // test PUT widgets/:widgetID - common.TestEndpoint(t, router, token, "/api/widgets/3", "PUT", msgWdgupdatedjson, 200, msgOKjson) - common.TestEndpoint(t, router, token, "/api/widgets/3", "GET", nil, 200, msgWdgupdatedjson) + // Compare POST's response with the newWidget + err = common.CompareResponse(resp, common.KeyModels{"widget": newWidget}) + assert.NoError(t, err) - // test DELETE widgets/:widgetID - common.TestEndpoint(t, router, token, "/api/widgets/3", "DELETE", nil, 200, msgOKjson) - common.TestEndpoint(t, router, token, "/api/widgets?dashboardID=1", "GET", nil, 200, msgWidgetsjson) + // Read newWidget's ID from the response + newWidgetID, err := common.GetResponseID(resp) + assert.NoError(t, err) - // TODO add testing for other return codes + // Get the newWidget + code, resp, err = common.NewTestEndpoint(router, token, + fmt.Sprintf("/api/widgets/%v", newWidgetID), "GET", nil) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Compare GET's response with the newWidget + err = common.CompareResponse(resp, common.KeyModels{"widget": newWidget}) + assert.NoError(t, err) + + // try to POST a malformed widget + // Required fields are missing + malformedNewWidget := WidgetRequest{ + Name: "ThisIsAMalformedDashboard", + } + // this should NOT work and return a unprocessable entity 442 status code + code, resp, err = common.NewTestEndpoint(router, token, + "/api/widgets", "POST", common.KeyModels{"widget": malformedNewWidget}) + assert.NoError(t, err) + assert.Equalf(t, 422, code, "Response body: \n%v\n", resp) +} + +func TestUpdateWidget(t *testing.T) { + common.DropTables(db) + common.MigrateModels(db) + common.DummyAddOnlyUserTableWithAdminAndUsersDB(db) + + // authenticate as normal user + token, err := common.NewAuthenticateForTest(router, + "/api/authenticate", "POST", common.UserACredentials) + assert.NoError(t, err) + + _, dashboardID := addScenarioAndDashboard(token) + + // test POST widgets/ $newWidget + newWidget := WidgetRequest{ + Name: common.WidgetA.Name, + Type: common.WidgetA.Type, + Width: common.WidgetA.Width, + Height: common.WidgetA.Height, + MinWidth: common.WidgetA.MinWidth, + MinHeight: common.WidgetA.MinHeight, + X: common.WidgetA.X, + Y: common.WidgetA.Y, + Z: common.WidgetA.Z, + IsLocked: common.WidgetA.IsLocked, + CustomProperties: common.WidgetA.CustomProperties, + DashboardID: dashboardID, + } + code, resp, err := common.NewTestEndpoint(router, token, + "/api/widgets", "POST", common.KeyModels{"widget": newWidget}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Read newWidget's ID from the response + newWidgetID, err := common.GetResponseID(resp) + assert.NoError(t, err) + + updatedWidget := WidgetRequest{ + Name: common.WidgetB.Name, + Type: common.WidgetB.Type, + Width: common.WidgetB.Width, + Height: common.WidgetB.Height, + MinWidth: common.WidgetB.MinWidth, + MinHeight: common.WidgetB.MinHeight, + CustomProperties: common.WidgetA.CustomProperties, + } + + code, resp, err = common.NewTestEndpoint(router, token, + fmt.Sprintf("/api/widgets/%v", newWidgetID), "PUT", common.KeyModels{"widget": updatedWidget}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Compare PUT's response with the updatedWidget + err = common.CompareResponse(resp, common.KeyModels{"widget": updatedWidget}) + assert.NoError(t, err) + + // Get the updatedWidget + code, resp, err = common.NewTestEndpoint(router, token, + fmt.Sprintf("/api/widgets/%v", newWidgetID), "GET", nil) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Compare GET's response with the updatedWidget + err = common.CompareResponse(resp, common.KeyModels{"widget": updatedWidget}) + assert.NoError(t, err) + + // try to update a widget that does not exist (should return not found 404 status code) + code, resp, err = common.NewTestEndpoint(router, token, + fmt.Sprintf("/api/widgets/%v", newWidgetID+1), "PUT", common.KeyModels{"widget": updatedWidget}) + assert.NoError(t, err) + assert.Equalf(t, 404, code, "Response body: \n%v\n", resp) } + +func TestDeleteWidget(t *testing.T) { + common.DropTables(db) + common.MigrateModels(db) + common.DummyAddOnlyUserTableWithAdminAndUsersDB(db) + + // authenticate as normal user + token, err := common.NewAuthenticateForTest(router, + "/api/authenticate", "POST", common.UserACredentials) + assert.NoError(t, err) + + _, dashboardID := addScenarioAndDashboard(token) + + // test POST widgets/ $newWidget + newWidget := WidgetRequest{ + Name: common.WidgetA.Name, + Type: common.WidgetA.Type, + Width: common.WidgetA.Width, + Height: common.WidgetA.Height, + MinWidth: common.WidgetA.MinWidth, + MinHeight: common.WidgetA.MinHeight, + X: common.WidgetA.X, + Y: common.WidgetA.Y, + Z: common.WidgetA.Z, + IsLocked: common.WidgetA.IsLocked, + CustomProperties: common.WidgetA.CustomProperties, + DashboardID: dashboardID, + } + code, resp, err := common.NewTestEndpoint(router, token, + "/api/widgets", "POST", common.KeyModels{"widget": newWidget}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Read newWidget's ID from the response + newWidgetID, err := common.GetResponseID(resp) + assert.NoError(t, err) + + // Count the number of all the widgets returned for dashboard + initialNumber, err := common.LengthOfResponse(router, token, + fmt.Sprintf("/api/widgets?dashboardID=%v", dashboardID), "GET", nil) + assert.NoError(t, err) + + // Delete the added newWidget + code, resp, err = common.NewTestEndpoint(router, token, + fmt.Sprintf("/api/widgets/%v", newWidgetID), "DELETE", nil) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Compare DELETE's response with the newWidget + err = common.CompareResponse(resp, common.KeyModels{"widget": newWidget}) + assert.NoError(t, err) + + // Again count the number of all the widgets returned for dashboard + finalNumber, err := common.LengthOfResponse(router, token, + fmt.Sprintf("/api/widgets?dashboardID=%v", dashboardID), "GET", nil) + assert.NoError(t, err) + + assert.Equal(t, initialNumber-1, finalNumber) +} + +func TestGetAllWidgetsOfDashboard(t *testing.T) { + common.DropTables(db) + common.MigrateModels(db) + common.DummyAddOnlyUserTableWithAdminAndUsersDB(db) + + // authenticate as normal user + token, err := common.NewAuthenticateForTest(router, + "/api/authenticate", "POST", common.UserACredentials) + assert.NoError(t, err) + + _, dashboardID := addScenarioAndDashboard(token) + + // Count the number of all the widgets returned for dashboard + initialNumber, err := common.LengthOfResponse(router, token, + fmt.Sprintf("/api/widgets?dashboardID=%v", dashboardID), "GET", nil) + assert.NoError(t, err) + + // test POST widgets/ $newWidget + newWidgetA := WidgetRequest{ + Name: common.WidgetA.Name, + Type: common.WidgetA.Type, + Width: common.WidgetA.Width, + Height: common.WidgetA.Height, + MinWidth: common.WidgetA.MinWidth, + MinHeight: common.WidgetA.MinHeight, + X: common.WidgetA.X, + Y: common.WidgetA.Y, + Z: common.WidgetA.Z, + IsLocked: common.WidgetA.IsLocked, + CustomProperties: common.WidgetA.CustomProperties, + DashboardID: dashboardID, + } + code, resp, err := common.NewTestEndpoint(router, token, + "/api/widgets", "POST", common.KeyModels{"widget": newWidgetA}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + newWidgetB := WidgetRequest{ + Name: common.WidgetB.Name, + Type: common.WidgetB.Type, + Width: common.WidgetB.Width, + Height: common.WidgetB.Height, + MinWidth: common.WidgetB.MinWidth, + MinHeight: common.WidgetB.MinHeight, + X: common.WidgetB.X, + Y: common.WidgetB.Y, + Z: common.WidgetB.Z, + IsLocked: common.WidgetB.IsLocked, + CustomProperties: common.WidgetB.CustomProperties, + DashboardID: dashboardID, + } + code, resp, err = common.NewTestEndpoint(router, token, + "/api/widgets", "POST", common.KeyModels{"widget": newWidgetB}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Again count the number of all the widgets returned for dashboard + finalNumber, err := common.LengthOfResponse(router, token, + fmt.Sprintf("/api/widgets?dashboardID=%v", dashboardID), "GET", nil) + assert.NoError(t, err) + + assert.Equal(t, initialNumber+2, finalNumber) +} + +//// Test /widgets endpoints +//func TestWidgetEndpoints(t *testing.T) { +// +// var token string +// +// var myWidgets = []common.WidgetResponse{common.WidgetA_response, common.WidgetB_response} +// var msgWidgets = common.ResponseMsgWidgets{Widgets: myWidgets} +// var msgWdg = common.ResponseMsgWidget{Widget: common.WidgetC_response} +// var msgWdgupdated = common.ResponseMsgWidget{Widget: common.WidgetCUpdated_response} +// +// db := common.DummyInitDB() +// defer db.Close() +// common.DummyPopulateDB(db) +// +// router := gin.Default() +// api := router.Group("/api") +// +// // All endpoints require authentication except when someone wants to +// // login (POST /authenticate) +// user.RegisterAuthenticate(api.Group("/authenticate")) +// +// api.Use(user.Authentication(true)) +// +// RegisterWidgetEndpoints(api.Group("/widgets")) +// +// credjson, _ := json.Marshal(common.CredUser) +// msgOKjson, _ := json.Marshal(common.MsgOK) +// msgWidgetsjson, _ := json.Marshal(msgWidgets) +// msgWdgjson, _ := json.Marshal(msgWdg) +// msgWdgupdatedjson, _ := json.Marshal(msgWdgupdated) +// +// token = common.AuthenticateForTest(t, router, "/api/authenticate", "POST", credjson, 200) +// +// // test GET widgets +// common.TestEndpoint(t, router, token, "/api/widgets?dashboardID=1", "GET", nil, 200, msgWidgetsjson) +// +// // test POST widgets +// common.TestEndpoint(t, router, token, "/api/widgets", "POST", msgWdgjson, 200, msgOKjson) +// +// // test GET widgets/:widgetID to check if previous POST worked correctly +// common.TestEndpoint(t, router, token, "/api/widgets/3", "GET", nil, 200, msgWdgjson) +// +// // test PUT widgets/:widgetID +// common.TestEndpoint(t, router, token, "/api/widgets/3", "PUT", msgWdgupdatedjson, 200, msgOKjson) +// common.TestEndpoint(t, router, token, "/api/widgets/3", "GET", nil, 200, msgWdgupdatedjson) +// +// // test DELETE widgets/:widgetID +// common.TestEndpoint(t, router, token, "/api/widgets/3", "DELETE", nil, 200, msgOKjson) +// common.TestEndpoint(t, router, token, "/api/widgets?dashboardID=1", "GET", nil, 200, msgWidgetsjson) +// +// // TODO add testing for other return codes +// +//} diff --git a/routes/widget/widget_validators.go b/routes/widget/widget_validators.go new file mode 100644 index 0000000..21c3ccd --- /dev/null +++ b/routes/widget/widget_validators.go @@ -0,0 +1,109 @@ +package widget + +import ( + "encoding/json" + "github.com/jinzhu/gorm/dialects/postgres" + "github.com/nsf/jsondiff" + "gopkg.in/go-playground/validator.v9" +) + +var validate *validator.Validate + +type validNewWidget struct { + Name string `form:"name" validate:"required"` + Type string `form:"type" validate:"required"` + Width uint `form:"width" validate:"required"` + Height uint `form:"height" validate:"required"` + MinWidth uint `form:"minWidth" validate:"omitempty"` + MinHeight uint `form:"minHeight" validate:"omitempty"` + X int `form:"x" validate:"omitempty"` + Y int `form:"y" validate:"omitempty"` + Z int `form:"z" validate:"omitempty"` + DashboardID uint `form:"dashboardID" validate:"required"` + IsLocked bool `form:"isLocked" validate:"omitempty"` + CustomProperties postgres.Jsonb `form:"customProperties" validate:"omitempty"` +} + +type validUpdatedWidget struct { + Name string `form:"name" validate:"omitempty"` + Type string `form:"type" validate:"omitempty"` + Width uint `form:"width" validate:"omitempty"` + Height uint `form:"height" validate:"omitempty"` + MinWidth uint `form:"minWidth" validate:"omitempty"` + MinHeight uint `form:"minHeight" validate:"omitempty"` + X int `form:"x" validate:"omitempty"` + Y int `form:"y" validate:"omitempty"` + Z int `form:"z" validate:"omitempty"` + IsLocked bool `form:"isLocked" validate:"omitempty"` + CustomProperties postgres.Jsonb `form:"customProperties" validate:"omitempty"` +} + +type addWidgetRequest struct { + validNewWidget `json:"widget"` +} + +type updateWidgetRequest struct { + validUpdatedWidget `json:"widget"` +} + +func (r *addWidgetRequest) validate() error { + validate = validator.New() + errs := validate.Struct(r) + return errs +} + +func (r *validUpdatedWidget) validate() error { + validate = validator.New() + errs := validate.Struct(r) + return errs +} + +func (r *addWidgetRequest) createWidget() Widget { + var s Widget + + s.Name = r.Name + s.Type = r.Type + s.Width = r.Width + s.Height = r.Height + s.MinWidth = r.MinWidth + s.MinHeight = r.MinHeight + s.X = r.X + s.Y = r.Y + s.Z = r.Z + s.IsLocked = r.IsLocked + s.CustomProperties = r.CustomProperties + s.DashboardID = r.DashboardID + return s +} + +func (r *updateWidgetRequest) updatedWidget(oldWidget Widget) (Widget, error) { + // Use the old Widget as a basis for the updated Widget `s` + s := oldWidget + + if r.Name != "" { + s.Name = r.Name + } + + s.Type = r.Type + s.Width = r.Width + s.Height = r.Height + s.MinWidth = r.MinWidth + s.MinHeight = r.MinHeight + s.X = r.X + s.Y = r.Y + s.Z = r.Z + s.IsLocked = r.IsLocked + + // only update custom props if not empty + var emptyJson postgres.Jsonb + // Serialize empty json and params + emptyJson_ser, _ := json.Marshal(emptyJson) + customprops_ser, _ := json.Marshal(r.CustomProperties) + opts := jsondiff.DefaultConsoleOptions() + diff, _ := jsondiff.Compare(emptyJson_ser, customprops_ser, &opts) + if diff.String() != "FullMatch" { + s.CustomProperties = r.CustomProperties + } + + return s, nil +}