From b102d4ab1a47809f514213eb1f997d4f60893c9f Mon Sep 17 00:00:00 2001 From: Larry Sachs Date: Mon, 20 Jul 2020 22:46:49 -0700 Subject: Adds PUT api to v2/projects Add functionality to support the PUT api for v2/projects/{project-name} Also add unit tests for the PUT api Issue-ID: MULTICLOUD-1130 Change-Id: Ia271569c5f0dec3152952e64171fd5a182aaa333 Signed-off-by: Larry Sachs --- src/orchestrator/api/api.go | 1 + src/orchestrator/api/projecthandler.go | 46 +++++++++++- src/orchestrator/api/projecthandler_test.go | 95 +++++++++++++++++++++++- src/orchestrator/examples/example_module.go | 9 ++- src/orchestrator/pkg/module/project.go | 6 +- src/orchestrator/pkg/module/project_test.go | 107 +++++++++++++++++++++++++++- 6 files changed, 257 insertions(+), 7 deletions(-) (limited to 'src/orchestrator') diff --git a/src/orchestrator/api/api.go b/src/orchestrator/api/api.go index 097b9ab3..03afee28 100644 --- a/src/orchestrator/api/api.go +++ b/src/orchestrator/api/api.go @@ -56,6 +56,7 @@ func NewRouter(projectClient moduleLib.ProjectManager, client: ControllerClient, } router.HandleFunc("/projects", projHandler.createHandler).Methods("POST") + router.HandleFunc("/projects/{project-name}", projHandler.updateHandler).Methods("PUT") router.HandleFunc("/projects/{project-name}", projHandler.getHandler).Methods("GET") router.HandleFunc("/projects", projHandler.getHandler).Methods("GET") router.HandleFunc("/projects/{project-name}", projHandler.deleteHandler).Methods("DELETE") diff --git a/src/orchestrator/api/projecthandler.go b/src/orchestrator/api/projecthandler.go index 09b24096..83211b64 100644 --- a/src/orchestrator/api/projecthandler.go +++ b/src/orchestrator/api/projecthandler.go @@ -54,7 +54,7 @@ func (h projectHandler) createHandler(w http.ResponseWriter, r *http.Request) { return } - ret, err := h.client.CreateProject(p) + ret, err := h.client.CreateProject(p, false) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -69,6 +69,50 @@ func (h projectHandler) createHandler(w http.ResponseWriter, r *http.Request) { } } +// Update handles updating the Project entry in the database +func (h projectHandler) updateHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["project-name"] + + var p moduleLib.Project + + err := json.NewDecoder(r.Body).Decode(&p) + switch { + case err == io.EOF: + http.Error(w, "Empty body", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // Name is required. + if p.MetaData.Name == "" { + http.Error(w, "Missing name in PUT request", http.StatusBadRequest) + return + } + + // Name in URL should match name in body + if p.MetaData.Name != name { + http.Error(w, "Mismatched name in PUT request", http.StatusBadRequest) + return + } + + ret, err := h.client.CreateProject(p, true) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(ret) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + // Get handles GET operations on a particular Project Name // Returns a Project func (h projectHandler) getHandler(w http.ResponseWriter, r *http.Request) { diff --git a/src/orchestrator/api/projecthandler_test.go b/src/orchestrator/api/projecthandler_test.go index dae87e2b..6810099f 100644 --- a/src/orchestrator/api/projecthandler_test.go +++ b/src/orchestrator/api/projecthandler_test.go @@ -40,7 +40,7 @@ type mockProjectManager struct { Err error } -func (m *mockProjectManager) CreateProject(inp moduleLib.Project) (moduleLib.Project, error) { +func (m *mockProjectManager) CreateProject(inp moduleLib.Project, exists bool) (moduleLib.Project, error) { if m.Err != nil { return moduleLib.Project{}, m.Err } @@ -144,6 +144,99 @@ func TestProjectCreateHandler(t *testing.T) { } } +func TestProjectUpdateHandler(t *testing.T) { + testCases := []struct { + label, name string + reader io.Reader + expected moduleLib.Project + expectedCode int + projectClient *mockProjectManager + }{ + { + label: "Missing Project Name in Request Body", + name: "testProject", + reader: bytes.NewBuffer([]byte(`{ + "description":"test description" + }`)), + expectedCode: http.StatusBadRequest, + projectClient: &mockProjectManager{}, + }, + { + label: "Missing Body Failure", + name: "testProject", + expectedCode: http.StatusBadRequest, + projectClient: &mockProjectManager{}, + }, + { + label: "Mismatched Name Failure", + name: "testProject", + expectedCode: http.StatusBadRequest, + reader: bytes.NewBuffer([]byte(`{ + "metadata" : { + "name": "testProjectNameMismatch", + "description": "Test Project used for unit testing" + } + }`)), + projectClient: &mockProjectManager{}, + }, + { + label: "Update Project", + name: "testProject", + expectedCode: http.StatusOK, + reader: bytes.NewBuffer([]byte(`{ + "metadata" : { + "name": "testProject", + "description": "Test Project used for unit testing" + } + }`)), + expected: moduleLib.Project{ + MetaData: moduleLib.ProjectMetaData{ + Name: "testProject", + Description: "Test Project used for unit testing", + UserData1: "update data1", + UserData2: "update data2", + }, + }, + projectClient: &mockProjectManager{ + //Items that will be returned by the mocked Client + Items: []moduleLib.Project{ + { + MetaData: moduleLib.ProjectMetaData{ + Name: "testProject", + Description: "Test Project used for unit testing", + UserData1: "update data1", + UserData2: "update data2", + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("PUT", "/v2/projects/"+testCase.name, testCase.reader) + resp := executeRequest(request, NewRouter(testCase.projectClient, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)) + + //Check returned code + if resp.StatusCode != testCase.expectedCode { + t.Fatalf("Expected %d; Got: %d", testCase.expectedCode, resp.StatusCode) + } + + //Check returned body only if statusOK + if resp.StatusCode == http.StatusOK { + got := moduleLib.Project{} + json.NewDecoder(resp.Body).Decode(&got) + + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("updateHandler returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + func TestProjectGetHandler(t *testing.T) { testCases := []struct { diff --git a/src/orchestrator/examples/example_module.go b/src/orchestrator/examples/example_module.go index 9138b085..7edbb758 100644 --- a/src/orchestrator/examples/example_module.go +++ b/src/orchestrator/examples/example_module.go @@ -31,7 +31,14 @@ func ExampleClient_Project() { return } // Perform operations on Project Module - _, err := c.Project.CreateProject(moduleLib.Project{MetaData: moduleLib.ProjectMetaData{Name: "test", Description: "test", UserData1: "userData1", UserData2: "userData2"}}) + // POST request (exists == false) + _, err := c.Project.CreateProject(moduleLib.Project{MetaData: moduleLib.ProjectMetaData{Name: "test", Description: "test", UserData1: "userData1", UserData2: "userData2"}}, false) + if err != nil { + log.Println(err) + return + } + // PUT request (exists == true) + _, err = c.Project.CreateProject(moduleLib.Project{MetaData: moduleLib.ProjectMetaData{Name: "test", Description: "test", UserData1: "userData1", UserData2: "userData2"}}, true) if err != nil { log.Println(err) return diff --git a/src/orchestrator/pkg/module/project.go b/src/orchestrator/pkg/module/project.go index 02f6d827..e86266b9 100644 --- a/src/orchestrator/pkg/module/project.go +++ b/src/orchestrator/pkg/module/project.go @@ -55,7 +55,7 @@ func (pk ProjectKey) String() string { // ProjectManager is an interface exposes the Project functionality type ProjectManager interface { - CreateProject(pr Project) (Project, error) + CreateProject(pr Project, exists bool) (Project, error) GetProject(name string) (Project, error) DeleteProject(name string) error GetAllProjects() ([]Project, error) @@ -78,7 +78,7 @@ func NewProjectClient() *ProjectClient { } // CreateProject a new collection based on the project -func (v *ProjectClient) CreateProject(p Project) (Project, error) { +func (v *ProjectClient) CreateProject(p Project, exists bool) (Project, error) { //Construct the composite key to select the entry key := ProjectKey{ @@ -87,7 +87,7 @@ func (v *ProjectClient) CreateProject(p Project) (Project, error) { //Check if this Project already exists _, err := v.GetProject(p.MetaData.Name) - if err == nil { + if err == nil && !exists { return Project{}, pkgerrors.New("Project already exists") } diff --git a/src/orchestrator/pkg/module/project_test.go b/src/orchestrator/pkg/module/project_test.go index f6856f86..947478b4 100644 --- a/src/orchestrator/pkg/module/project_test.go +++ b/src/orchestrator/pkg/module/project_test.go @@ -62,13 +62,39 @@ func TestCreateProject(t *testing.T) { Err: pkgerrors.New("Error Creating Project"), }, }, + { + label: "Create Existing Project", + inp: Project{ + MetaData: ProjectMetaData{ + Name: "testProject", + Description: "A sample Project used for unit testing", + UserData1: "data1", + UserData2: "data2", + }, + }, + expectedError: "Project already exists", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + ProjectKey{ProjectName: "testProject"}.String(): { + "projectmetadata": []byte( + "{" + + "\"metadata\" : {" + + "\"Name\":\"testProject\"," + + "\"Description\":\"Test project for unit testing\"," + + "\"UserData1\":\"userData1\"," + + "\"UserData2\":\"userData2\"}" + + "}"), + }, + }, + }, + }, } for _, testCase := range testCases { t.Run(testCase.label, func(t *testing.T) { db.DBconn = testCase.mockdb impl := NewProjectClient() - got, err := impl.CreateProject(testCase.inp) + got, err := impl.CreateProject(testCase.inp, false) if err != nil { if testCase.expectedError == "" { t.Fatalf("Create returned an unexpected error %s", err) @@ -86,6 +112,85 @@ func TestCreateProject(t *testing.T) { } } +func TestUpdateProject(t *testing.T) { + testCases := []struct { + label string + inp Project + expectedError string + mockdb *db.MockDB + expected Project + }{ + { + label: "Update Project", + inp: Project{ + MetaData: ProjectMetaData{ + Name: "testProject", + Description: "Test project for unit testing", + UserData1: "update userData1", + UserData2: "update userData2", + }, + }, + expected: Project{ + MetaData: ProjectMetaData{ + Name: "testProject", + Description: "Test project for unit testing", + UserData1: "update userData1", + UserData2: "update userData2", + }, + }, + expectedError: "", + mockdb: &db.MockDB{ + Items: map[string]map[string][]byte{ + ProjectKey{ProjectName: "testProject"}.String(): { + "projectmetadata": []byte( + "{" + + "\"metadata\" : {" + + "\"Name\":\"testProject\"," + + "\"Description\":\"Test project for unit testing\"," + + "\"UserData1\":\"userData1\"," + + "\"UserData2\":\"userData2\"}" + + "}"), + }, + }, + }, + }, + { + label: "Failed Update Project", + inp: Project{ + MetaData: ProjectMetaData{ + Name: "unknownProject", + Description: "Unknown project for unit testing", + }, + }, + expectedError: "Creating DB Entry", + mockdb: &db.MockDB{ + Err: pkgerrors.New("Error Updating Project"), + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + db.DBconn = testCase.mockdb + impl := NewProjectClient() + got, err := impl.CreateProject(testCase.inp, true) + if err != nil { + if testCase.expectedError == "" { + t.Fatalf("Update returned an unexpected error %s", err) + } + if strings.Contains(err.Error(), testCase.expectedError) == false { + t.Fatalf("Update returned an unexpected error %s", err) + } + } else { + if reflect.DeepEqual(testCase.expected, got) == false { + t.Errorf("Update returned unexpected body: got %v;"+ + " expected %v", got, testCase.expected) + } + } + }) + } +} + func TestGetProject(t *testing.T) { testCases := []struct { -- cgit 1.2.3-korg