From bc68115ce1cd64a9359d1842c717177a890c18e6 Mon Sep 17 00:00:00 2001 From: Deepak Date: Sat, 9 Jan 2021 23:39:03 -0600 Subject: [PATCH 1/5] adds update action method --- models/action.go | 5 +++++ models/err_model_test.go | 4 ++++ models/models.go | 1 + models/models_test.go | 6 ++++++ routes/route_model_test.go | 13 +++++++++++++ store/postgres.go | 22 ++++++++++++++++++++++ store/postgres_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 89 insertions(+) diff --git a/models/action.go b/models/action.go index 8b71cb1..5f17bb1 100644 --- a/models/action.go +++ b/models/action.go @@ -31,3 +31,8 @@ func (m *Model) Action(id int) (*Action, error) { func (m *Model) AddAction(action *Action) (int, error) { return m.InsertAction(action) } + +// SaveAction saves and updates an action. +func (m *Model) SaveAction(action *Action) error { + return m.UpdateAction(action) +} diff --git a/models/err_model_test.go b/models/err_model_test.go index b800fd1..27c357a 100644 --- a/models/err_model_test.go +++ b/models/err_model_test.go @@ -16,6 +16,10 @@ func (e *errorStore) InsertAction(action *models.Action) (int, error) { return 0, e.error } +func (e *errorStore) UpdateAction(action *models.Action) error { + return e.error +} + func (e *errorStore) SelectPlans() ([]*models.Plan, error) { return nil, e.error } diff --git a/models/models.go b/models/models.go index 86e99c0..d25a1ac 100644 --- a/models/models.go +++ b/models/models.go @@ -10,6 +10,7 @@ type Store interface { SelectActions() ([]*Action, error) SelectActionByID(id int) (*Action, error) InsertAction(action *Action) (int, error) + UpdateAction(action *Action) error SelectPlans() ([]*Plan, error) SelectPlanByID(id int) (*Plan, error) InsertPlan(plan *Plan) (int, error) diff --git a/models/models_test.go b/models/models_test.go index ed832e6..7cb62e8 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -23,6 +23,10 @@ func (ms *multiStore) InsertAction(action *models.Action) (int, error) { return int(action.ActionID), nil } +func (ms *multiStore) UpdateAction(action *models.Action) error { + return nil +} + func (ms *multiStore) SelectPlans() ([]*models.Plan, error) { return ms.plans, nil } @@ -65,6 +69,8 @@ func TestModelActions(t *testing.T) { assert.Nil(err) assert.EqualValues(3, actionID) + err = m.SaveAction(a1) + assert.Nil(err) } func TestModelPlanMethods(t *testing.T) { diff --git a/routes/route_model_test.go b/routes/route_model_test.go index a177dfb..d0d98f8 100644 --- a/routes/route_model_test.go +++ b/routes/route_model_test.go @@ -34,6 +34,10 @@ func (ms *multiStore) InsertAction(action *models.Action) (int, error) { return int(action.ActionID), nil } +func (ms *multiStore) UpdateAction(action *models.Action) error { + return nil +} + func (ms *multiStore) SelectPlans() ([]*models.Plan, error) { return ms.plans, nil } @@ -87,6 +91,10 @@ func (e *errorStore) InsertAction(action *models.Action) (int, error) { return 0, e.error } +func (e *errorStore) UpdateAction(action *models.Action) error { + return e.error +} + func (e *errorStore) SelectPlans() ([]*models.Plan, error) { return nil, e.error } @@ -123,10 +131,15 @@ func (e *onlyCreateStore) SelectActions() ([]*models.Action, error) { func (e *onlyCreateStore) SelectActionByID(id int) (*models.Action, error) { return nil, e.error } + func (e *onlyCreateStore) InsertAction(action *models.Action) (int, error) { return int(action.ActionID), nil } +func (e *onlyCreateStore) UpdateAction(action *models.Action) error { + return nil +} + func (e *onlyCreateStore) SelectPlans() ([]*models.Plan, error) { return nil, e.error } diff --git a/store/postgres.go b/store/postgres.go index ae041de..a443f40 100644 --- a/store/postgres.go +++ b/store/postgres.go @@ -75,6 +75,28 @@ func (store *postgresStore) InsertAction(action *models.Action) (int, error) { return id, nil } +func (store *postgresStore) UpdateAction(action *models.Action) error { + query := `UPDATE actions SET + action_description = :action_description, + estimated_chunks = :estimated_chunks, + completed_chunks = :completed_chunks, + completed_on = :completed_on, + plan_id = :plan_id + WHERE action_id = :action_id` + tx := store.db.MustBegin() + _, err := store.db.NamedExec(query, action) + if err != nil { + tx.Rollback() + return err + } + err = tx.Commit() + if err != nil { + return err + } + return nil + +} + func (store *postgresStore) SelectPlans() ([]*models.Plan, error) { plans := make([]*models.Plan, 0) err := store.db.Select(&plans, "SELECT plan_id, plan_date FROM plans") diff --git a/store/postgres_test.go b/store/postgres_test.go index 823beb6..e45d38f 100644 --- a/store/postgres_test.go +++ b/store/postgres_test.go @@ -496,3 +496,41 @@ func TestInsertActionCommitErr(t *testing.T) { } } + +func TestUpdateAction(t *testing.T) { + // setup + assert := assert.New(t) + + str, mock := getDbMock(t) + completedOn, _ := time.Parse("2006-01-02", "2021-01-01") + action := &models.Action{ + CompletedOn: &completedOn, + EstimatedChunks: 3, + CompletedChunks: 6, + PlanID: 5, + ActionDescription: "testing", + ActionID: 2, + } + + mock.ExpectBegin() + mock.ExpectExec(` + UPDATE actions SET + action_description = \$1, + estimated_chunks = \$2, + completed_chunks = \$3, + completed_on = \$4, + plan_id = \$5 + WHERE action_id = \$6`). + WithArgs("testing", 3, 6, completedOn, 5, 2). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + // function under test + err := str.UpdateAction(action) + // check results + assert.Nil(err) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %s", err) + } + +} -- 2.47.2 From 395231e1bfa61b33989990726f9639dc2de918e8 Mon Sep 17 00:00:00 2001 From: Deepak Date: Sat, 9 Jan 2021 23:53:16 -0600 Subject: [PATCH 2/5] adds test for errors in update --- store/postgres_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/store/postgres_test.go b/store/postgres_test.go index e45d38f..e24e1bb 100644 --- a/store/postgres_test.go +++ b/store/postgres_test.go @@ -534,3 +534,67 @@ func TestUpdateAction(t *testing.T) { } } + + +func TestUpdateActionErr(t *testing.T) { + // setup + assert := assert.New(t) + + str, mock := getDbMock(t) + completedOn, _ := time.Parse("2006-01-02", "2021-01-01") + action := &models.Action{ + CompletedOn: &completedOn, + EstimatedChunks: 3, + CompletedChunks: 6, + PlanID: 5, + ActionDescription: "testing", + ActionID: 2, + } + + mock.ExpectBegin() + mock.ExpectExec("UPDATE actions"). + WithArgs("testing", 3, 6, completedOn, 5, 2). + WillReturnError(fmt.Errorf("example error")) + mock.ExpectRollback() + + // function under test + err := str.UpdateAction(action) + // check results + assert.NotNil(err) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %s", err) + } + +} + +func TestUpdateActionCommitErr(t *testing.T) { + // setup + assert := assert.New(t) + + str, mock := getDbMock(t) + completedOn, _ := time.Parse("2006-01-02", "2021-01-01") + action := &models.Action{ + CompletedOn: &completedOn, + EstimatedChunks: 3, + CompletedChunks: 6, + PlanID: 5, + ActionDescription: "testing", + ActionID: 2, + } + + + mock.ExpectBegin() + mock.ExpectExec("UPDATE actions"). + WithArgs("testing", 3, 6, completedOn, 5, 2). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit().WillReturnError(fmt.Errorf("another error example")) + + // function under test + err := str.UpdateAction(action) + // check results + assert.NotNil(err) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unfulfilled expectations: %s", err) + } + +} -- 2.47.2 From 0faef20441f1081c60c07c12cb9eda631d97f984 Mon Sep 17 00:00:00 2001 From: Deepak Date: Sat, 9 Jan 2021 23:54:52 -0600 Subject: [PATCH 3/5] fmt changes --- store/postgres_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/store/postgres_test.go b/store/postgres_test.go index e24e1bb..4fe3371 100644 --- a/store/postgres_test.go +++ b/store/postgres_test.go @@ -535,7 +535,6 @@ func TestUpdateAction(t *testing.T) { } - func TestUpdateActionErr(t *testing.T) { // setup assert := assert.New(t) @@ -548,7 +547,7 @@ func TestUpdateActionErr(t *testing.T) { CompletedChunks: 6, PlanID: 5, ActionDescription: "testing", - ActionID: 2, + ActionID: 2, } mock.ExpectBegin() @@ -579,10 +578,9 @@ func TestUpdateActionCommitErr(t *testing.T) { CompletedChunks: 6, PlanID: 5, ActionDescription: "testing", - ActionID: 2, + ActionID: 2, } - mock.ExpectBegin() mock.ExpectExec("UPDATE actions"). WithArgs("testing", 3, 6, completedOn, 5, 2). -- 2.47.2 From a5528be4568ee7313a2f51dd20d4e597b833ba4e Mon Sep 17 00:00:00 2001 From: Deepak Date: Sun, 10 Jan 2021 10:38:03 -0600 Subject: [PATCH 4/5] Adding new func --- routes/actions.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/routes/actions.go b/routes/actions.go index 466f38a..c5f07e8 100644 --- a/routes/actions.go +++ b/routes/actions.go @@ -124,3 +124,52 @@ func postActionFunc(m *models.Model) http.HandlerFunc { } } + +func putActionFunc(m *models.Model) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + r.Body = http.MaxBytesReader(w, r.Body, 1024) + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + var a models.Action + err := dec.Decode(&a) + if err != nil { + badRequestError(w, err) + return + } + err = dec.Decode(&struct{}{}) + if err != io.EOF { + badRequestError(w, err) + return + } + + action := &models.Action{ + ActionDescription: a.ActionDescription, + EstimatedChunks: a.EstimatedChunks, + CompletedChunks: a.CompletedChunks, + CompletedOn: a.CompletedOn, + PlanID: a.PlanID, + } + id, err := m.AddAction(action) + if err != nil { + serverError(w, err) + return + } + action, err = m.Action(id) + if err != nil { + serverError(w, err) + return + } + + response := &createActionResponse{ + CreatedAction: action, + ID: int64(id), + } + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + serverError(w, err) + } + + } +} -- 2.47.2 From 86a4a28aeec4b5d5a62b6dce075cc26dcc21c680 Mon Sep 17 00:00:00 2001 From: Deepak Date: Sun, 10 Jan 2021 11:27:32 -0600 Subject: [PATCH 5/5] adds put action route --- routes/actions.go | 23 +++- routes/put_action_test.go | 236 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 routes/put_action_test.go diff --git a/routes/actions.go b/routes/actions.go index c5f07e8..fb24749 100644 --- a/routes/actions.go +++ b/routes/actions.go @@ -14,6 +14,7 @@ func newActionRouter(m *models.Model) http.Handler { router.Get("/", getActionsFunc(m)) router.Post("/", postActionFunc(m)) router.Get("/{actionid}", getActionByIDFunc(m)) + router.Put("/{actionid}", putActionFunc(m)) return router } @@ -125,14 +126,23 @@ func postActionFunc(m *models.Model) http.HandlerFunc { } } +type updateActionResponse struct { + UpdatedAction *models.Action `json:"updated_action"` + ID int64 `json:"id"` +} + func putActionFunc(m *models.Model) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - + id, err := strconv.Atoi(chi.URLParam(r, "actionid")) + if err != nil { + notFoundHandler(w, r) + return + } r.Body = http.MaxBytesReader(w, r.Body, 1024) dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() var a models.Action - err := dec.Decode(&a) + err = dec.Decode(&a) if err != nil { badRequestError(w, err) return @@ -149,8 +159,9 @@ func putActionFunc(m *models.Model) http.HandlerFunc { CompletedChunks: a.CompletedChunks, CompletedOn: a.CompletedOn, PlanID: a.PlanID, + ActionID: int64(id), } - id, err := m.AddAction(action) + err = m.SaveAction(action) if err != nil { serverError(w, err) return @@ -161,11 +172,11 @@ func putActionFunc(m *models.Model) http.HandlerFunc { return } - response := &createActionResponse{ - CreatedAction: action, + response := &updateActionResponse{ + UpdatedAction: action, ID: int64(id), } - w.WriteHeader(http.StatusCreated) + w.WriteHeader(http.StatusOK) w.Header().Add("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(response); err != nil { serverError(w, err) diff --git a/routes/put_action_test.go b/routes/put_action_test.go new file mode 100644 index 0000000..5dc03b4 --- /dev/null +++ b/routes/put_action_test.go @@ -0,0 +1,236 @@ +package routes_test + +import ( + "bytes" + "encoding/json" + "gitea.deepak.science/deepak/gogmagog/models" + "gitea.deepak.science/deepak/gogmagog/routes" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestPureJSONPutAction(t *testing.T) { + // set up + assert := assert.New(t) + compOn, _ := time.Parse("2006-01-02", "2021-01-01") + a := &models.Action{ + PlanID: 5, + CompletedOn: &compOn, + EstimatedChunks: 3, + CompletedChunks: 2, + ActionDescription: "here's an action", + } + m := getModel([]*models.Plan{}, []*models.Action{a}) + router := routes.NewRouter(m) + data := []byte(`{ + "action_description": "here's an action", + "estimated_chunks": 3, + "completed_chunks": 2, + "completed_on": "2021-01-01T00:00:00Z", + "plan_id": 5 + }`) + req, _ := http.NewRequest("PUT", "/actions/0", bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + // function under test + router.ServeHTTP(rr, req) + + // check results + status := rr.Code + assert.Equal(http.StatusOK, status) + // We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp. + expected := `{ + "updated_action": { + "action_description": "here's an action", + "estimated_chunks": 3, + "completed_chunks": 2, + "completed_on": "2021-01-01T00:00:00Z", + "plan_id": 5, + "action_id": 0 + }, + "id": 0 + }` + assert.JSONEq(expected, rr.Body.String()) + contentType := rr.Header().Get("Content-Type") + assert.Equal("application/json", contentType) +} + +func TestExtraFieldActionPutJSON(t *testing.T) { + // set up + assert := assert.New(t) + planDate, _ := time.Parse("2006-01-02", "2021-01-01") + p := &models.Plan{PlanID: 6, PlanDate: &planDate} + m := getModel([]*models.Plan{p}, []*models.Action{}) + router := routes.NewRouter(m) + data := []byte(`{ + "completed_on": "2021-01-01T00:00:00Z", + "plan_id": 5, + "sabotage": "omg" + }`) + req, _ := http.NewRequest("PUT", "/actions/1", bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + // function under test + router.ServeHTTP(rr, req) + + // check results + status := rr.Code + assert.Equal(http.StatusBadRequest, status) + // We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp. + expected := `Bad Request` + assert.Equal(expected, strings.TrimSpace(rr.Body.String())) +} +func TestEmptyBodyActionPut(t *testing.T) { + // set up + assert := assert.New(t) + planDate, _ := time.Parse("2006-01-02", "2021-01-01") + p := &models.Plan{PlanID: 6, PlanDate: &planDate} + m := getModel([]*models.Plan{p}, []*models.Action{}) + router := routes.NewRouter(m) + data := []byte(``) + req, _ := http.NewRequest("PUT", "/actions/1", bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + // function under test + router.ServeHTTP(rr, req) + + // check results + status := rr.Code + assert.Equal(http.StatusBadRequest, status) + // We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp. + expected := `Bad Request` + assert.Equal(expected, strings.TrimSpace(rr.Body.String())) +} + +func TestTwoBodyActionPut(t *testing.T) { + // set up + assert := assert.New(t) + planDate, _ := time.Parse("2006-01-02", "2021-01-01") + p := &models.Plan{PlanID: 6, PlanDate: &planDate} + m := getModel([]*models.Plan{p}, []*models.Action{}) + router := routes.NewRouter(m) + data := []byte(`{ + "plan_id": 5 + }, { + "plan_id": 6 + }`) + req, _ := http.NewRequest("PUT", "/actions/1", bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + // function under test + router.ServeHTTP(rr, req) + + // check results + status := rr.Code + assert.Equal(http.StatusBadRequest, status) + // We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp. + expected := `Bad Request` + assert.Equal(expected, strings.TrimSpace(rr.Body.String())) +} + +func TestBadActionIDPut(t *testing.T) { + // set up + assert := assert.New(t) + planDate, _ := time.Parse("2006-01-02", "2021-01-01") + p := &models.Plan{PlanID: 6, PlanDate: &planDate} + m := getModel([]*models.Plan{p}, []*models.Action{}) + router := routes.NewRouter(m) + data := []byte(`{ + "plan_id": 5 + }, { + "plan_id": 6 + }`) + req, _ := http.NewRequest("PUT", "/actions/text", bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + // function under test + router.ServeHTTP(rr, req) + + // check results + status := rr.Code + assert.Equal(http.StatusNotFound, status) + // We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp. + expected := `Not Found` + assert.Equal(expected, strings.TrimSpace(rr.Body.String())) +} + +func TestErrorUpdateAction(t *testing.T) { + // set up + assert := assert.New(t) + + m := getErrorModel("Model always errors") + + router := routes.NewRouter(m) + a := &models.Action{PlanID: 6} + data, _ := json.Marshal(a) + req, _ := http.NewRequest("PUT", "/actions/1", bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + + // function under test + router.ServeHTTP(rr, req) + + // check results + status := rr.Code + assert.Equal(http.StatusInternalServerError, status) + // We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp. + expected := `Internal Server Error` + assert.Equal(expected, strings.TrimSpace(rr.Body.String())) +} + +func TestErrorOnRetrieveUpdateAction(t *testing.T) { + // set up + assert := assert.New(t) + + m := getErrorOnGetModel("Model always errors") + + router := routes.NewRouter(m) + a := &models.Action{PlanID: 6} + data, _ := json.Marshal(a) + req, _ := http.NewRequest("PUT", "/actions/1", bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + rr := httptest.NewRecorder() + + // function under test + router.ServeHTTP(rr, req) + + // check results + status := rr.Code + assert.Equal(http.StatusInternalServerError, status) + // We pass in the date as a time.time so it makes sense that it comes back with a midnight timestamp. + expected := `Internal Server Error` + assert.Equal(expected, strings.TrimSpace(rr.Body.String())) +} + +func TestErrorWriterUpdateAction(t *testing.T) { + // set up + assert := assert.New(t) + + a := &models.Action{PlanID: 6} + m := getModel([]*models.Plan{}, []*models.Action{a}) + + router := routes.NewRouter(m) + data, _ := json.Marshal(a) + req, _ := http.NewRequest("PUT", "/actions/1", bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + + rr := NewBadWriter() + + // function under test + router.ServeHTTP(rr, req) + + // check results + status := rr.Code + assert.Equal(http.StatusInternalServerError, status) + +} -- 2.47.2