From c00cb406ebaaad2441bd0ec370f37ccbe8b8b9a1 Mon Sep 17 00:00:00 2001 From: Sonja Happ Date: Tue, 8 Feb 2022 17:21:46 +0100 Subject: [PATCH] fix bugs in scenario deletion, improve testing of scenario deletion --- .../component-configuration/config_methods.go | 47 ++- routes/scenario/scenario_endpoints.go | 4 +- routes/scenario/scenario_methods.go | 48 ++- routes/scenario/scenario_test.go | 362 +++++++++++++++++- 4 files changed, 418 insertions(+), 43 deletions(-) diff --git a/routes/component-configuration/config_methods.go b/routes/component-configuration/config_methods.go index 743cede..324b635 100644 --- a/routes/component-configuration/config_methods.go +++ b/routes/component-configuration/config_methods.go @@ -22,6 +22,7 @@ package component_configuration import ( + "github.com/jinzhu/gorm" "log" "git.rwth-aachen.de/acs/public/villas/web-backend-go/database" @@ -126,12 +127,6 @@ func (m *ComponentConfiguration) delete() error { return err } - var ic database.InfrastructureComponent - err = db.Find(&ic, m.ICID).Error - if err != nil { - return err - } - // remove association between ComponentConfiguration and Scenario log.Println("DELETE ASSOCIATION to scenario ", so.ID, "(name="+so.Name+")") err = db.Model(&so).Association("ComponentConfigurations").Delete(m).Error @@ -139,13 +134,6 @@ func (m *ComponentConfiguration) delete() error { return err } - // remove association between Infrastructure component and config - log.Println("DELETE ASSOCIATION to IC ", ic.ID, "(name="+ic.Name+")") - err = db.Model(&ic).Association("ComponentConfigurations").Delete(m).Error - if err != nil { - return err - } - // Get Signals of InputMapping and delete them var InputMappingSignals []database.Signal err = db.Model(m).Related(&InputMappingSignals, "InputMapping").Error @@ -174,19 +162,36 @@ func (m *ComponentConfiguration) delete() error { } } + var ic database.InfrastructureComponent + err = db.Find(&ic, m.ICID).Error + if err == nil { + // remove association between Infrastructure component and config + log.Println("DELETE ASSOCIATION to IC ", ic.ID, "(name="+ic.Name+")") + err = db.Model(&ic).Association("ComponentConfigurations").Delete(m).Error + if err != nil { + return err + } + + // if IC has state gone and there is no component configuration associated with it: delete IC + no_configs := db.Model(&ic).Association("ComponentConfigurations").Count() + if no_configs == 0 && ic.State == "gone" { + log.Println("DELETE IC with state gone, last component config deleted", ic.UUID) + err = db.Delete(&ic).Error + return err + } + } else { + if err == gorm.ErrRecordNotFound { + log.Printf("SKIPPING IC association removal, IC with id=%v not found\n", m.ICID) + } else { + return err + } + } + // delete component configuration err = db.Delete(m).Error if err != nil { return err } - // if IC has state gone and there is no component configuration associated with it: delete IC - no_configs := db.Model(&ic).Association("ComponentConfigurations").Count() - if no_configs == 0 && ic.State == "gone" { - log.Println("DELETE IC with state gone, last component config deleted", ic.UUID) - err = db.Delete(&ic).Error - return err - } - return nil } diff --git a/routes/scenario/scenario_endpoints.go b/routes/scenario/scenario_endpoints.go index 1e6b3e9..a40c432 100644 --- a/routes/scenario/scenario_endpoints.go +++ b/routes/scenario/scenario_endpoints.go @@ -242,7 +242,9 @@ func deleteScenario(c *gin.Context) { if len(errs) > 0 { var errorString = "DB errors:" for _, e := range errs { - errorString += ", " + e.Error() + if e != nil { + errorString += ", " + e.Error() + } } helper.InternalServerError(c, errorString) return diff --git a/routes/scenario/scenario_methods.go b/routes/scenario/scenario_methods.go index 1197689..a5fa460 100644 --- a/routes/scenario/scenario_methods.go +++ b/routes/scenario/scenario_methods.go @@ -210,18 +210,6 @@ func (s *Scenario) delete() []error { } for _, config := range configs { - var ic database.InfrastructureComponent - err = db.Find(&ic, config.ICID).Error - if err != nil { - errs = append(errs, err) - } - - // remove association between Infrastructure component and config - log.Println("DELETE ASSOCIATION to IC ", ic.ID, "(name="+ic.Name+")") - err = db.Model(&ic).Association("ComponentConfigurations").Delete(&config).Error - if err != nil { - errs = append(errs, err) - } // Get Signals of InputMapping and delete them var InputMappingSignals []database.Signal @@ -251,20 +239,40 @@ func (s *Scenario) delete() []error { } } + var ic database.InfrastructureComponent + err = db.Find(&ic, config.ICID).Error + if err == nil { + // remove association between Infrastructure component and config + log.Println("DELETE ASSOCIATION to IC ", ic.ID, "(name="+ic.Name+")") + err = db.Model(&ic).Association("ComponentConfigurations").Delete(&config).Error + if err != nil { + errs = append(errs, err) + } + + // if IC has state gone and there is no component configuration associated with it: delete IC + no_configs := db.Model(&ic).Association("ComponentConfigurations").Count() + if no_configs == 0 && ic.State == "gone" { + log.Println("DELETE IC with state gone, last component config deleted", ic.UUID) + err = db.Delete(&ic).Error + if err != nil { + errs = append(errs, err) + } + + } + } else { + if err == gorm.ErrRecordNotFound { + log.Printf("SKIPPING IC association removal, IC with id=%v not found\n", config.ICID) + } else { + errs = append(errs, err) + } + } + // delete component configuration log.Println("DELETE component config ", config.ID, "(name="+config.Name+")") err = db.Delete(&config).Error if err != nil { errs = append(errs, err) } - - // if IC has state gone and there is no component configuration associated with it: delete IC - no_configs := db.Model(&ic).Association("ComponentConfigurations").Count() - if no_configs == 0 && ic.State == "gone" { - log.Println("DELETE IC with state gone, last component config deleted", ic.UUID) - err = db.Delete(&ic).Error - errs = append(errs, err) - } } // delete scenario from all users and vice versa diff --git a/routes/scenario/scenario_test.go b/routes/scenario/scenario_test.go index 73fe009..8f9cb35 100644 --- a/routes/scenario/scenario_test.go +++ b/routes/scenario/scenario_test.go @@ -22,8 +22,22 @@ package scenario import ( + "bytes" "encoding/json" "fmt" + component_configuration "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/component-configuration" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/dashboard" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/file" + infrastructure_component "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/infrastructure-component" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/result" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/signal" + "git.rwth-aachen.de/acs/public/villas/web-backend-go/routes/widget" + "io" + "io/ioutil" + "log" + "mime/multipart" + "net/http" + "net/http/httptest" "os" "testing" @@ -88,6 +102,13 @@ func TestMain(m *testing.M) { // user endpoints required to set user to inactive user.RegisterUserEndpoints(api.Group("/users")) + file.RegisterFileEndpoints(api.Group("/files")) + component_configuration.RegisterComponentConfigurationEndpoints(api.Group("/configs")) + signal.RegisterSignalEndpoints(api.Group("/signals")) + dashboard.RegisterDashboardEndpoints(api.Group("/dashboards")) + widget.RegisterWidgetEndpoints(api.Group("/widgets")) + result.RegisterResultEndpoints(api.Group("/results")) + infrastructure_component.RegisterICEndpoints(api.Group("/ic")) RegisterScenarioEndpoints(api.Group("/scenarios")) os.Exit(m.Run()) @@ -414,8 +435,14 @@ func TestDeleteScenario(t *testing.T) { database.MigrateModels() assert.NoError(t, database.AddTestUsers()) + // authenticate as admin user to add ICs + token, err := helper.AuthenticateForTest(router, database.AdminCredentials) + assert.NoError(t, err) + + ic1ID, ic2ID := addICs(t, token) + // authenticate as normal user - token, err := helper.AuthenticateForTest(router, database.UserACredentials) + token, err = helper.AuthenticateForTest(router, database.UserACredentials) assert.NoError(t, err) // test POST scenarios/ $newScenario @@ -429,16 +456,22 @@ func TestDeleteScenario(t *testing.T) { assert.NoError(t, err) // add file to the scenario + fileID := addFile(t, token, newScenarioID) // add result to the scenario + resultID := addResult(t, token, newScenarioID) // add dashboard to the scenario + dashboardID := addDashboard(t, token, newScenarioID) // add widget to the dashboard + widgetID := addWidget(t, token, dashboardID) // add component config to the scenario + componentConfig1ID, componentConfig2ID := addComponentConfigs(t, token, newScenarioID, ic1ID, ic2ID) // add signal to the component config + signalInID, signalOutID := addSignals(t, token, componentConfig1ID) // add guest user to new scenario code, resp, err = helper.TestEndpoint(router, token, @@ -482,6 +515,10 @@ func TestDeleteScenario(t *testing.T) { assert.NoError(t, err) assert.Equal(t, finalNumber, initialNumber-1) + + // TODO check if dashboard, result, file, etc still exists (use API) + // TODO make sure everything is properly deleted + log.Println(fileID, resultID, dashboardID, widgetID, componentConfig1ID, componentConfig2ID, signalInID, signalOutID, ic1ID, ic2ID) } func TestAddUserToScenario(t *testing.T) { @@ -726,3 +763,326 @@ func TestRemoveUserFromScenario(t *testing.T) { assert.Equalf(t, 404, code, "Response body: \n%v\n", resp) } + +func addFile(t *testing.T, token string, scenarioID int) int { + // create a testfile.txt in local folder + c1 := []byte("This is my testfile\n") + err := ioutil.WriteFile("testfile.txt", c1, 0644) + assert.NoError(t, err) + + // test POST files + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + fileWriter, err := bodyWriter.CreateFormFile("file", "testuploadfile.txt") + assert.NoError(t, err, "writing to buffer") + + // open file handle + fh, err := os.Open("testfile.txt") + assert.NoError(t, err, "opening file") + defer fh.Close() + + // io copy + _, err = io.Copy(fileWriter, fh) + assert.NoError(t, err, "IO copy") + + contentType := bodyWriter.FormDataContentType() + bodyWriter.Close() + + // Create the request + w := httptest.NewRecorder() + req, err := http.NewRequest("POST", fmt.Sprintf("/api/v2/files?scenarioID=%v", scenarioID), bodyBuf) + assert.NoError(t, err, "create request") + + req.Header.Set("Content-Type", contentType) + req.Header.Add("Authorization", "Bearer "+token) + router.ServeHTTP(w, req) + + assert.Equalf(t, 200, w.Code, "Response body: \n%v\n", w.Body) + + newFileID, err := helper.GetResponseID(w.Body) + assert.NoError(t, err) + + return newFileID +} + +func addResult(t *testing.T, token string, scenarioID int) int { + + type ResultRequest struct { + Description string `json:"description,omitempty"` + ScenarioID uint `json:"scenarioID,omitempty"` + ConfigSnapshots postgres.Jsonb `json:"configSnapshots,omitempty"` + } + + var newResult = ResultRequest{ + Description: "This is a test result.", + } + + configSnapshot1 := json.RawMessage(`{"configs": [ {"Name" : "conf1", "scenarioID" : 1}, {"Name" : "conf2", "scenarioID" : 1}]}`) + confSnapshots := postgres.Jsonb{ + RawMessage: configSnapshot1, + } + newResult.ScenarioID = uint(scenarioID) + newResult.ConfigSnapshots = confSnapshots + + code, resp, err := helper.TestEndpoint(router, token, + "/api/v2/results", "POST", helper.KeyModels{"result": newResult}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Read newResults's ID from the response + newResultID, err := helper.GetResponseID(resp) + assert.NoError(t, err) + + return newResultID +} + +func addDashboard(t *testing.T, token string, scenarioID int) int { + + type DashboardRequest struct { + Name string `json:"name,omitempty"` + Grid int `json:"grid,omitempty"` + Height int `json:"height,omitempty"` + ScenarioID uint `json:"scenarioID,omitempty"` + } + + var newDashboard = DashboardRequest{ + Name: "Dashboard_A", + Grid: 15, + } + + // test POST dashboards/ $newDashboad + newDashboard.ScenarioID = uint(scenarioID) + code, resp, err := helper.TestEndpoint(router, token, + "/api/v2/dashboards", "POST", helper.KeyModels{"dashboard": newDashboard}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Read newDashboard's ID from the response + newDashboardID, err := helper.GetResponseID(resp) + assert.NoError(t, err) + + return newDashboardID +} + +func addWidget(t *testing.T, token string, dashboardID int) int { + + 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"` + SignalIDs []int64 `json:"signalIDs,omitempty"` + } + + var newWidget = WidgetRequest{ + Name: "My label", + Type: "Label", + Width: 100, + Height: 50, + MinWidth: 40, + MinHeight: 80, + X: 10, + Y: 10, + Z: 200, + IsLocked: false, + CustomProperties: postgres.Jsonb{RawMessage: json.RawMessage(`{"textSize" : "20", "fontColor" : "#4287f5", "fontColor_opacity": 1}`)}, + SignalIDs: []int64{}, + } + + newWidget.DashboardID = uint(dashboardID) + + code, resp, err := helper.TestEndpoint(router, token, + "/api/v2/widgets", "POST", helper.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 := helper.GetResponseID(resp) + assert.NoError(t, err) + + return newWidgetID +} + +func addComponentConfigs(t *testing.T, token string, scenarioID int, ic1ID int, ic2ID int) (int, int) { + type ConfigRequest struct { + Name string `json:"name,omitempty"` + ScenarioID uint `json:"scenarioID,omitempty"` + ICID uint `json:"icID,omitempty"` + StartParameters postgres.Jsonb `json:"startParameters,omitempty"` + FileIDs []int64 `json:"fileIDs,omitempty"` + } + + var newConfig1 = ConfigRequest{ + Name: "Example for Signal generator", + StartParameters: postgres.Jsonb{RawMessage: json.RawMessage(`{"parameter1" : "testValue1A", "parameter2" : "testValue2A", "parameter3" : 42}`)}, + FileIDs: []int64{}, + } + + var newConfig2 = ConfigRequest{ + Name: "Config example 2", + StartParameters: postgres.Jsonb{RawMessage: json.RawMessage(`{"parameter1" : "testValue1A"}`)}, + FileIDs: []int64{}, + } + + newConfig1.ScenarioID = uint(scenarioID) + newConfig1.ICID = uint(ic1ID) + newConfig2.ScenarioID = uint(scenarioID) + newConfig2.ICID = uint(ic2ID) + + code, resp, err := helper.TestEndpoint(router, token, + "/api/v2/configs", "POST", helper.KeyModels{"config": newConfig1}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Read newConfig's ID from the response + newConfig1ID, err := helper.GetResponseID(resp) + assert.NoError(t, err) + + code, resp, err = helper.TestEndpoint(router, token, + "/api/v2/configs", "POST", helper.KeyModels{"config": newConfig2}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Read newConfig's ID from the response + newConfig2ID, err := helper.GetResponseID(resp) + assert.NoError(t, err) + + return newConfig1ID, newConfig2ID +} + +func addSignals(t *testing.T, token string, componentConfigID int) (int, int) { + + type SignalRequest struct { + Name string `json:"name,omitempty"` + Unit string `json:"unit,omitempty"` + Index *uint `json:"index,omitempty"` + Direction string `json:"direction,omitempty"` + ScalingFactor float32 `json:"scalingFactor,omitempty"` + ConfigID uint `json:"configID,omitempty"` + } + + var signalIndex0 uint = 0 + + var newSignalOut = SignalRequest{ + Name: "outSignal_A", + Unit: "V", + Direction: "out", + Index: &signalIndex0, + } + + var newSignalIn = SignalRequest{ + Name: "inSignal_A", + Unit: "V", + Direction: "in", + Index: &signalIndex0, + } + + newSignalOut.ConfigID = uint(componentConfigID) + newSignalIn.ConfigID = uint(componentConfigID) + + code, resp, err := helper.TestEndpoint(router, token, + "/api/v2/signals", "POST", helper.KeyModels{"signal": newSignalOut}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Read newSignal's ID from the response + newSignalIDOut, err := helper.GetResponseID(resp) + assert.NoError(t, err) + + code, resp, err = helper.TestEndpoint(router, token, + "/api/v2/signals", "POST", helper.KeyModels{"signal": newSignalIn}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Read newSignal's ID from the response + newSignalIDIn, err := helper.GetResponseID(resp) + assert.NoError(t, err) + + return newSignalIDIn, newSignalIDOut + +} + +func addICs(t *testing.T, token string) (int, int) { + + type ICRequest struct { + UUID string `json:"uuid,omitempty"` + WebsocketURL string `json:"websocketurl,omitempty"` + APIURL string `json:"apiurl,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Category string `json:"category,omitempty"` + State string `json:"state,omitempty"` + Location string `json:"location,omitempty"` + Description string `json:"description,omitempty"` + StartParameterSchema postgres.Jsonb `json:"startparameterschema,omitempty"` + CreateParameterSchema postgres.Jsonb `json:"createparameterschema,omitempty"` + ManagedExternally *bool `json:"managedexternally"` + Manager string `json:"manager,omitempty"` + } + + var newIC1 = ICRequest{ + UUID: "7be0322d-354e-431e-84bd-ae4c9633138b", + WebsocketURL: "https://villas.k8s.eonerc.rwth-aachen.de/ws/ws_sig", + APIURL: "https://villas.k8s.eonerc.rwth-aachen.de/ws/api/v2", + Type: "villas-node", + Name: "ACS Demo Signals", + Category: "gateway", + State: "idle", + Location: "k8s", + Description: "A signal generator for testing purposes", + StartParameterSchema: postgres.Jsonb{RawMessage: json.RawMessage(`{"startprop1" : "a nice prop"}`)}, + CreateParameterSchema: postgres.Jsonb{RawMessage: json.RawMessage(`{"createprop1" : "a really nice prop"}`)}, + ManagedExternally: newFalse(), + Manager: "7be0322d-354e-431e-84bd-ae4c9633beef", + } + + // IC with state gone + var newIC2 = ICRequest{ + UUID: "4854af30-325f-44a5-ad59-b67b2597de68", + WebsocketURL: "xxx.yyy.zzz.aaa", + APIURL: "https://villas.k8s.eonerc.rwth-aachen.de/ws/api/v2", + Type: "dpsim", + Name: "Test DPsim Simulator", + Category: "simulator", + State: "gone", + Location: "k8s", + Description: "This is a test description", + StartParameterSchema: postgres.Jsonb{RawMessage: json.RawMessage(`{"startprop1" : "a nice prop"}`)}, + CreateParameterSchema: postgres.Jsonb{RawMessage: json.RawMessage(`{"createprop1" : "a really nice prop"}`)}, + ManagedExternally: newFalse(), + Manager: "4854af30-325f-44a5-ad59-b67b2597de99", + } + + code, resp, err := helper.TestEndpoint(router, token, + "/api/v2/ic", "POST", helper.KeyModels{"ic": newIC1}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Read newIC's ID from the response + newIC1ID, err := helper.GetResponseID(resp) + assert.NoError(t, err) + + code, resp, err = helper.TestEndpoint(router, token, + "/api/v2/ic", "POST", helper.KeyModels{"ic": newIC2}) + assert.NoError(t, err) + assert.Equalf(t, 200, code, "Response body: \n%v\n", resp) + + // Read newIC's ID from the response + newIC2ID, err := helper.GetResponseID(resp) + assert.NoError(t, err) + + return newIC1ID, newIC2ID +} + +func newFalse() *bool { + b := false + return &b +}