diff --git a/go-backend/db/common_test.go b/go-backend/db/common_test.go index a8b613c37..e1c6438be 100644 --- a/go-backend/db/common_test.go +++ b/go-backend/db/common_test.go @@ -6,6 +6,12 @@ import ( logger "github.com/sirupsen/logrus" "github.com/stretchr/testify/suite" "testing" + "time" +) + +var ( + now time.Time + mockedRows *sqlmock.Rows ) func InitMockDB() (s Storer, sqlConn *sqlx.DB, sqlmockInstance sqlmock.Sqlmock) { @@ -27,4 +33,6 @@ func InitMockDB() (s Storer, sqlConn *sqlx.DB, sqlmockInstance sqlmock.Sqlmock) func TestExampleTestSuite(t *testing.T) { suite.Run(t, new(OrganizationTestSuite)) suite.Run(t, new(RecognitionHi5TestSuite)) + suite.Run(t, new(ReportedRecognitionTestSuite)) + suite.Run(t, new(RecognitionModerationTestSuite)) } diff --git a/go-backend/db/db.go b/go-backend/db/db.go index 5b49f1a0a..8efcd65d5 100644 --- a/go-backend/db/db.go +++ b/go-backend/db/db.go @@ -39,4 +39,10 @@ type Storer interface { //Recognition CreateRecognitionHi5(context.Context, RecognitionHi5, int) error + + //Reported Recognition + CreateReportedRecognition(context.Context, int64, ReportedRecognition) (ReportedRecognition, error) + + //Recognition Moderation + CreateRecognitionModeration(context.Context, int64, RecognitionModeration) (RecognitionModeration, error) } diff --git a/go-backend/db/mock.go b/go-backend/db/mock.go index f9c2a9b22..813b9d63a 100644 --- a/go-backend/db/mock.go +++ b/go-backend/db/mock.go @@ -145,3 +145,13 @@ func (m *DBMockStore) UpdateUser(ctx context.Context, user User, id int) (update args := m.Called(ctx, user, id) return args.Get(0).(User), args.Error(1) } + +func (m *DBMockStore) CreateReportedRecognition(ctx context.Context, recognitionID int64, reportedRecognition ReportedRecognition) (resp ReportedRecognition, err error) { + args := m.Called(ctx, recognitionID, reportedRecognition) + return args.Get(0).(ReportedRecognition), args.Error(1) +} + +func (m *DBMockStore) CreateRecognitionModeration(ctx context.Context, recognitionID int64, recognitionModeration RecognitionModeration) (resp RecognitionModeration, err error) { + args := m.Called(ctx, recognitionID, recognitionModeration) + return args.Get(0).(RecognitionModeration), args.Error(1) +} diff --git a/go-backend/db/organization_test.go b/go-backend/db/organization_test.go index d73ebae72..b80ffc98a 100644 --- a/go-backend/db/organization_test.go +++ b/go-backend/db/organization_test.go @@ -17,8 +17,6 @@ type OrganizationTestSuite struct { sqlmock sqlmock.Sqlmock } -var mockedRows *sqlmock.Rows - var expectedOrg = Organization{ ID: 1, Name: "test organization", diff --git a/go-backend/db/recognition_moderation.go b/go-backend/db/recognition_moderation.go new file mode 100644 index 000000000..0916f8091 --- /dev/null +++ b/go-backend/db/recognition_moderation.go @@ -0,0 +1,64 @@ +package db + +import ( + "context" + "time" + + logger "github.com/sirupsen/logrus" +) + +const ( + createRecognitionModerationQuery = `INSERT INTO recognition_moderation (recognition_id, is_inappropriate, + moderator_comment, moderated_by, moderated_at, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, recognition_id, is_inappropriate, moderator_comment, moderated_by, moderated_at, created_at, updated_at` +) + +type RecognitionModeration struct { + ID int64 `db:"id" json:"id"` + RecognitionID int64 `db:"recognition_id" json:"recognition_id"` + IsInappropriate *bool `db:"is_inappropriate" json:"is_inappropriate"` + ModeratorComment string `db:"moderator_comment" json:"comment"` + ModeratedBy int64 `db:"moderated_by" json:"moderated_by"` + ModeratedAt int64 `db:"moderated_at" json:"moderated_at"` + CreatedAt time.Time `db:"created_at" json:"-"` + UpdatedAt time.Time `db:"updated_at" json:"-"` +} + +func (recognitionModeration *RecognitionModeration) Validate() (valid bool, errFields map[string]string) { + errFields = make(map[string]string) + + if recognitionModeration.IsInappropriate == nil { + errFields["is_inappropriate"] = "Can't be blank" + } + + if len(errFields) == 0 { + valid = true + } + return +} + +func (s *pgStore) CreateRecognitionModeration(ctx context.Context, recognitionID int64, recognitionModeration RecognitionModeration) (resp RecognitionModeration, err error) { + now := time.Now() + err = s.db.GetContext( + ctx, + &resp, + createRecognitionModerationQuery, + recognitionID, + recognitionModeration.IsInappropriate, + recognitionModeration.ModeratorComment, + recognitionModeration.ModeratedBy, + now.Unix(), + now, + now, + ) + if err != nil { + logger.WithFields(logger.Fields{ + "err": err.Error(), + "recognitionID": recognitionID, + "recognition_moderation_params": recognitionModeration, + }).Error("Error while creating recognition moderation") + return + } + + return +} diff --git a/go-backend/db/recognition_moderation_test.go b/go-backend/db/recognition_moderation_test.go new file mode 100644 index 000000000..d8fabfa71 --- /dev/null +++ b/go-backend/db/recognition_moderation_test.go @@ -0,0 +1,86 @@ +package db + +import ( + "context" + "github.com/DATA-DOG/go-sqlmock" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "time" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including assertion methods. +type RecognitionModerationTestSuite struct { + suite.Suite + dbStore Storer + db *sqlx.DB + sqlmock sqlmock.Sqlmock +} + +func (suite *RecognitionModerationTestSuite) SetupTest() { + dbStore, dbConn, sqlmock := InitMockDB() + suite.dbStore = dbStore + suite.db = dbConn + suite.sqlmock = sqlmock + now = time.Now() + mockedRows = suite.getMockedRows(now) +} + +func (suite *RecognitionModerationTestSuite) TearDownTest() { + suite.db.Close() +} + +func (suite *RecognitionModerationTestSuite) getMockedRows(now time.Time) (mockedRows *sqlmock.Rows) { + mockedRows = suite.sqlmock.NewRows([]string{"id", "recognition_id", "is_inappropriate", "moderator_comment", "moderated_by", "moderated_at", "created_at", "updated_at"}). + AddRow(1, 1, true, "Test Comment", 1, now.Unix(), now, now) + return +} + +func (suite *RecognitionModerationTestSuite) TestCreateRecognitionModerationSuccess() { + isInappropriate := true + expectedRecognitionModeration := RecognitionModeration{ + ID: int64(1), + RecognitionID: int64(1), + IsInappropriate: &isInappropriate, + ModeratorComment: "Test Comment", + ModeratedBy: int64(1), + ModeratedAt: now.Unix(), + CreatedAt: now, + UpdatedAt: now, + } + + suite.sqlmock.ExpectQuery("INSERT INTO recognition_moderation"). + WithArgs(1, true, "Test Comment", 1, now.Unix(), sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnRows(mockedRows) + + resp, err := suite.dbStore.CreateRecognitionModeration(context.Background(), expectedRecognitionModeration.ID, expectedRecognitionModeration) + + assert.Equal(suite.T(), expectedRecognitionModeration, resp) + assert.Nil(suite.T(), err) +} + +func (suite *RecognitionModerationTestSuite) TestCreateRecognitionModerationFailure() { + suite.db.Close() + + isInappropriate := true + expectedRecognitionModeration := RecognitionModeration{ + ID: int64(1), + RecognitionID: int64(1), + IsInappropriate: &isInappropriate, + ModeratorComment: "Test Comment", + ModeratedBy: int64(1), + ModeratedAt: now.Unix(), + CreatedAt: now, + UpdatedAt: now, + } + + suite.sqlmock.ExpectQuery("INSERT INTO recognition_moderation"). + WithArgs(1, true, "Test Comment", 1, now.Unix(), sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnRows(mockedRows) + + resp, err := suite.dbStore.CreateRecognitionModeration(context.Background(), expectedRecognitionModeration.ID, expectedRecognitionModeration) + + assert.Equal(suite.T(), RecognitionModeration{}, resp) + assert.NotNil(suite.T(), err) +} diff --git a/go-backend/db/reported_recognition.go b/go-backend/db/reported_recognition.go new file mode 100644 index 000000000..e9bb9ad3e --- /dev/null +++ b/go-backend/db/reported_recognition.go @@ -0,0 +1,86 @@ +package db + +import ( + "context" + "strings" + "time" + + logger "github.com/sirupsen/logrus" +) + +var REPORTED_RECOGNITION_TYPE = []string{"fraud", "not_relevant", "incorrect"} + +const ( + createReportedRecognitionQuery = `INSERT INTO reported_recognitions (recognition_id, type_of_reporting, + reason_for_reporting, reported_by, reported_at, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, recognition_id, type_of_reporting, reason_for_reporting, reported_by, reported_at, created_at, updated_at` +) + +type ReportedRecognition struct { + ID int64 `db:"id" json:"id"` + RecognitionID int64 `db:"recognition_id" json:"recognition_id"` + TypeOfReporting string `db:"type_of_reporting" json:"mark_as"` + ReasonForReporting string `db:"reason_for_reporting" json:"reason"` + ReportedBy int64 `db:"reported_by" json:"reported_by"` + ReportedAt int64 `db:"reported_at" json:"reported_at"` + CreatedAt time.Time `db:"created_at" json:"-"` + UpdatedAt time.Time `db:"updated_at" json:"-"` +} + +func include(slice []string, val string) bool { + for _, item := range slice { + if item == val { + return true + } + } + return false +} + +func (reportedRecognition *ReportedRecognition) Validate() (valid bool, errFields map[string]string) { + errFields = make(map[string]string) + + if reportedRecognition.TypeOfReporting == "" { + errFields["mark_as"] = "Can't be blank" + } else { + reportedRecognition.TypeOfReporting = strings.ToLower(reportedRecognition.TypeOfReporting) + ok := include(REPORTED_RECOGNITION_TYPE, reportedRecognition.TypeOfReporting) + if !ok { + errFields["mark_as"] = "Invalid reported recognition type" + } + } + + if reportedRecognition.ReasonForReporting == "" { + errFields["reason"] = "Can't be blank" + } + + if len(errFields) == 0 { + valid = true + } + return +} + +func (s *pgStore) CreateReportedRecognition(ctx context.Context, recognitionID int64, reportedRecognition ReportedRecognition) (resp ReportedRecognition, err error) { + now := time.Now() + err = s.db.GetContext( + ctx, + &resp, + createReportedRecognitionQuery, + recognitionID, + reportedRecognition.TypeOfReporting, + reportedRecognition.ReasonForReporting, + reportedRecognition.ReportedBy, + now.Unix(), + now, + now, + ) + if err != nil { + logger.WithFields(logger.Fields{ + "err": err.Error(), + "recognitionID": recognitionID, + "reported_recognition_params": reportedRecognition, + }).Error("Error while creating reported recognition") + return + } + + return +} diff --git a/go-backend/db/reported_recognition_test.go b/go-backend/db/reported_recognition_test.go new file mode 100644 index 000000000..e2e499ebf --- /dev/null +++ b/go-backend/db/reported_recognition_test.go @@ -0,0 +1,84 @@ +package db + +import ( + "context" + "github.com/DATA-DOG/go-sqlmock" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "time" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including assertion methods. +type ReportedRecognitionTestSuite struct { + suite.Suite + dbStore Storer + db *sqlx.DB + sqlmock sqlmock.Sqlmock +} + +func (suite *ReportedRecognitionTestSuite) SetupTest() { + dbStore, dbConn, sqlmock := InitMockDB() + suite.dbStore = dbStore + suite.db = dbConn + suite.sqlmock = sqlmock + now = time.Now() + mockedRows = suite.getMockedRows() +} + +func (suite *ReportedRecognitionTestSuite) TearDownTest() { + suite.db.Close() +} + +func (suite *ReportedRecognitionTestSuite) getMockedRows() (mockedRows *sqlmock.Rows) { + mockedRows = suite.sqlmock.NewRows([]string{"id", "recognition_id", "type_of_reporting", "reason_for_reporting", "reported_by", "reported_at", "created_at", "updated_at"}). + AddRow(1, 1, "fraud", "Test Reason", 1, now.Unix(), now, now) + return +} + +func (suite *ReportedRecognitionTestSuite) TestCreateReportedRecognitionSuccess() { + expectedReportedRecognition := ReportedRecognition{ + ID: int64(1), + RecognitionID: int64(1), + TypeOfReporting: "fraud", + ReasonForReporting: "Test Reason", + ReportedBy: int64(1), + ReportedAt: now.Unix(), + CreatedAt: now, + UpdatedAt: now, + } + + suite.sqlmock.ExpectQuery("INSERT INTO reported_recognitions"). + WithArgs(1, "fraud", "Test Reason", 1, now.Unix(), sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnRows(mockedRows) + + resp, err := suite.dbStore.CreateReportedRecognition(context.Background(), expectedReportedRecognition.ID, expectedReportedRecognition) + + assert.Equal(suite.T(), expectedReportedRecognition, resp) + assert.Nil(suite.T(), err) +} + +func (suite *ReportedRecognitionTestSuite) TestCreateReportedRecognitionFailure() { + suite.db.Close() + + expectedReportedRecognition := ReportedRecognition{ + ID: int64(1), + RecognitionID: int64(1), + TypeOfReporting: "fraud", + ReasonForReporting: "Test Reason", + ReportedBy: int64(1), + ReportedAt: now.Unix(), + CreatedAt: now, + UpdatedAt: now, + } + + suite.sqlmock.ExpectQuery("INSERT INTO reported_recognitions"). + WithArgs(1, "fraud", "Test Reason", 1, now.Unix(), sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnRows(mockedRows) + + resp, err := suite.dbStore.CreateReportedRecognition(context.Background(), expectedReportedRecognition.ID, expectedReportedRecognition) + + assert.Equal(suite.T(), ReportedRecognition{}, resp) + assert.NotNil(suite.T(), err) +} diff --git a/go-backend/migrations/1589893448_create_reported_recognitions.down.sql b/go-backend/migrations/1589893448_create_reported_recognitions.down.sql new file mode 100644 index 000000000..7032bcc1b --- /dev/null +++ b/go-backend/migrations/1589893448_create_reported_recognitions.down.sql @@ -0,0 +1 @@ +DROP TABLE reported_recognitions; diff --git a/go-backend/migrations/1589893448_create_reported_recognitions.up.sql b/go-backend/migrations/1589893448_create_reported_recognitions.up.sql new file mode 100644 index 000000000..91144ab5f --- /dev/null +++ b/go-backend/migrations/1589893448_create_reported_recognitions.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE reported_recognitions ( + id SERIAL PRIMARY KEY NOT NULL UNIQUE, + recognition_id integer REFERENCES recognitions(id), + type_of_reporting varchar(50), + reason_for_reporting TEXT, + reported_by INTEGER REFERENCES users(id), + reported_at BIGINT, + created_at timestamp with time zone NOT NULL default current_timestamp, + updated_at timestamp with time zone NOT NULL +); \ No newline at end of file diff --git a/go-backend/migrations/1589962840_create_recognition_moderation.down.sql b/go-backend/migrations/1589962840_create_recognition_moderation.down.sql new file mode 100644 index 000000000..9b3750eeb --- /dev/null +++ b/go-backend/migrations/1589962840_create_recognition_moderation.down.sql @@ -0,0 +1 @@ +DROP TABLE recognition_moderation; diff --git a/go-backend/migrations/1589962840_create_recognition_moderation.up.sql b/go-backend/migrations/1589962840_create_recognition_moderation.up.sql new file mode 100644 index 000000000..9ea29fe06 --- /dev/null +++ b/go-backend/migrations/1589962840_create_recognition_moderation.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE recognition_moderation ( + id SERIAL PRIMARY KEY NOT NULL UNIQUE, + recognition_id integer REFERENCES recognitions(id), + is_inappropriate BOOLEAN default FALSE, + moderator_comment TEXT, + moderated_by INTEGER REFERENCES users(id), + moderated_at BIGINT, + created_at timestamp with time zone NOT NULL default current_timestamp, + updated_at timestamp with time zone NOT NULL +); diff --git a/go-backend/service/common_http_test.go b/go-backend/service/common_http_test.go index b9af7f949..47bc1bd66 100644 --- a/go-backend/service/common_http_test.go +++ b/go-backend/service/common_http_test.go @@ -1,19 +1,27 @@ package service import ( - "github.com/gorilla/mux" - "github.com/stretchr/testify/suite" "net/http" "net/http/httptest" "strings" "testing" + + jwtmiddleware "github.com/auth0/go-jwt-middleware" + jwt "github.com/dgrijalva/jwt-go" + "github.com/gorilla/mux" + "github.com/stretchr/testify/suite" + "github.com/urfave/negroni" + "joshsoftware/peerly/config" ) func TestExampleTestSuite(t *testing.T) { + config.Load("application_test") suite.Run(t, new(UsersHandlerTestSuite)) suite.Run(t, new(OrganizationHandlerTestSuite)) suite.Run(t, new(RecognitionHi5HandlerTestSuite)) suite.Run(t, new(CoreValueHandlerTestSuite)) + suite.Run(t, new(ReportedRecognitionHandlerTestSuite)) + suite.Run(t, new(RecognitionModerationHandlerTestSuite)) } // path: is used to configure router path (eg: /users/{id}) @@ -33,3 +41,34 @@ func makeHTTPCall(method, path, requestURL, body string, handlerFunc http.Handle router.ServeHTTP(recorder, req) return } + +// path: is used to configure router path (eg: /users/{id}) +// requestURL: current request path (eg: /users/1) +func makeHTTPCallWithJWTMiddleware(method, path, requestURL, body string, handlerFunc http.HandlerFunc) (recorder *httptest.ResponseRecorder) { + // create jwt token with userID + JWTToken, _ := newJWT(1) + + // create a http request using the given parameters + req, _ := http.NewRequest(method, requestURL, strings.NewReader(body)) + req.Header.Set("Authorization", "Bearer " + JWTToken) + + // test recorder created for capturing api responses + recorder = httptest.NewRecorder() + + // create a router to serve the handler in test with the prepared request + router := mux.NewRouter() + jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{ + ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { + return config.JWTKey(), nil + }, + SigningMethod: jwt.SigningMethodHS256, + }) + router.Handle(path, negroni.New( + negroni.HandlerFunc(jwtMiddleware.HandlerWithNext), + negroni.Wrap(http.HandlerFunc(handlerFunc)), + )).Methods(method) + + // serve the request and write the response to recorder + router.ServeHTTP(recorder, req) + return +} diff --git a/go-backend/service/recognition_moderation_http.go b/go-backend/service/recognition_moderation_http.go new file mode 100644 index 000000000..a01ddce98 --- /dev/null +++ b/go-backend/service/recognition_moderation_http.go @@ -0,0 +1,73 @@ +package service + +import ( + "encoding/json" + "net/http" + "strconv" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/gorilla/mux" + logger "github.com/sirupsen/logrus" + ae "joshsoftware/peerly/apperrors" + "joshsoftware/peerly/db" +) + +func createRecognitionModerationHandler(deps Dependencies) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + parsedToken := req.Context().Value("user").(*jwt.Token) + claims := parsedToken.Claims.(jwt.MapClaims) + + userID, err := strconv.Atoi(claims["sub"].(string)) + if err != nil { + logger.Error(ae.ErrJSONParseFail, "Error parsing JSON for token response", err) + ae.JSONError(rw, http.StatusInternalServerError, err) + return + } + + vars := mux.Vars(req) + recognitionID, err := strconv.ParseInt(vars["recognition_id"], 10, 64) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while parsing recognition_id from url") + rw.WriteHeader(http.StatusBadRequest) + return + } + + var recognitionModeration db.RecognitionModeration + err = json.NewDecoder(req.Body).Decode(&recognitionModeration) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while decoding request data") + repsonse(rw, http.StatusBadRequest, errorResponse{ + Error: messageObject{ + Message: "Invalid json request body", + }, + }) + return + } + recognitionModeration.ModeratedBy = int64(userID) + + ok, errFields := recognitionModeration.Validate() + if !ok { + repsonse(rw, http.StatusBadRequest, errorResponse{ + Error: errorObject{ + Code: "invalid-recognition-moderation", + Fields: errFields, + messageObject: messageObject{"Invalid recognition moderation data"}, + }, + }) + return + } + + resp, err := deps.Store.CreateRecognitionModeration(req.Context(), recognitionID, recognitionModeration) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while creating recognition moderation") + repsonse(rw, http.StatusInternalServerError, errorResponse{ + Error: messageObject{ + Message: "Internal server error", + }, + }) + return + } + + repsonse(rw, http.StatusCreated, successResponse{Data: resp}) + }) +} diff --git a/go-backend/service/recognition_moderation_http_test.go b/go-backend/service/recognition_moderation_http_test.go new file mode 100644 index 000000000..6f4da7137 --- /dev/null +++ b/go-backend/service/recognition_moderation_http_test.go @@ -0,0 +1,112 @@ +package service + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "joshsoftware/peerly/db" +) + +type RecognitionModerationHandlerTestSuite struct { + suite.Suite + + dbMock *db.DBMockStore +} + +func (suite *RecognitionModerationHandlerTestSuite) SetupTest() { + suite.dbMock = &db.DBMockStore{} +} + +func (suite *RecognitionModerationHandlerTestSuite) TestCreateRecognitionModerationSuccess() { + now := time.Now().Unix() + isInappropriate := false + suite.dbMock.On("CreateRecognitionModeration", mock.Anything, mock.Anything, mock.Anything).Return(db.RecognitionModeration{ + ID: 1, + RecognitionID: int64(1), + IsInappropriate: &isInappropriate, + ModeratorComment: "Comment Test", + ModeratedBy: int64(1), + ModeratedAt: now, + }, nil) + + body := `{ + "is_inappropriate": false, + "comment": "Comment Test" + }` + + recorder := makeHTTPCallWithJWTMiddleware( + http.MethodPost, + "/recognitions/{recognition_id:[0-9]+}/review", + "/recognitions/1/review", + body, + createRecognitionModerationHandler(Dependencies{Store: suite.dbMock}), + ) + expectedBody := fmt.Sprintf(`{"data":{"id":1,"recognition_id":1,"is_inappropriate":false,"comment":"Comment Test","moderated_by":1,"moderated_at":%v}}`, now) + + assert.Equal(suite.T(), http.StatusCreated, recorder.Code) + assert.Equal(suite.T(), expectedBody, recorder.Body.String()) + suite.dbMock.AssertExpectations(suite.T()) +} + +func (suite *RecognitionModerationHandlerTestSuite) TestCreateRecognitionModerationWhenInvalidJSONFormat() { + body := `{ + "is_inappropriate": false + "comment": "Comment Test" + }` + + recorder := makeHTTPCallWithJWTMiddleware( + http.MethodPost, + "/recognitions/{recognition_id:[0-9]+}/review", + "/recognitions/1/review", + body, + createRecognitionModerationHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusBadRequest, recorder.Code) + assert.Equal(suite.T(), `{"error":{"message":"Invalid json request body"}}`, recorder.Body.String()) + suite.dbMock.AssertExpectations(suite.T()) +} + +func (suite *RecognitionModerationHandlerTestSuite) TestCreateRecognitionModerationWhenEmptyIsInappropriate() { + body := `{ + "comment": "Comment Test" + }` + + recorder := makeHTTPCallWithJWTMiddleware( + http.MethodPost, + "/recognitions/{recognition_id:[0-9]+}/review", + "/recognitions/1/review", + body, + createRecognitionModerationHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusBadRequest, recorder.Code) + assert.Equal(suite.T(), `{"error":{"code":"invalid-recognition-moderation","message":"Invalid recognition moderation data","fields":{"is_inappropriate":"Can't be blank"}}}`, recorder.Body.String()) + suite.dbMock.AssertExpectations(suite.T()) +} + +func (suite *RecognitionModerationHandlerTestSuite) TestCreateRecognitionModerationWhenDBFailure() { + suite.dbMock.On("CreateRecognitionModeration", mock.Anything, mock.Anything, mock.Anything).Return(db.RecognitionModeration{}, errors.New("error creating reported recognition")) + + body := `{ + "is_inappropriate": false, + "reason": "Comment Test" + }` + + recorder := makeHTTPCallWithJWTMiddleware( + http.MethodPost, + "/recognitions/{recognition_id:[0-9]+}/review", + "/recognitions/1/review", + body, + createRecognitionModerationHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusInternalServerError, recorder.Code) + assert.Equal(suite.T(), `{"error":{"message":"Internal server error"}}`, recorder.Body.String()) + suite.dbMock.AssertExpectations(suite.T()) +} diff --git a/go-backend/service/reported_recognition_http.go b/go-backend/service/reported_recognition_http.go new file mode 100644 index 000000000..d7124cee3 --- /dev/null +++ b/go-backend/service/reported_recognition_http.go @@ -0,0 +1,73 @@ +package service + +import ( + "encoding/json" + "net/http" + "strconv" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/gorilla/mux" + logger "github.com/sirupsen/logrus" + ae "joshsoftware/peerly/apperrors" + "joshsoftware/peerly/db" +) + +func createReportedRecognitionHandler(deps Dependencies) http.HandlerFunc { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + parsedToken := req.Context().Value("user").(*jwt.Token) + claims := parsedToken.Claims.(jwt.MapClaims) + + userID, err := strconv.Atoi(claims["sub"].(string)) + if err != nil { + logger.Error(ae.ErrJSONParseFail, "Error parsing JSON for token response", err) + ae.JSONError(rw, http.StatusInternalServerError, err) + return + } + + vars := mux.Vars(req) + recognitionID, err := strconv.ParseInt(vars["recognition_id"], 10, 64) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while parsing recognition_id from url") + rw.WriteHeader(http.StatusBadRequest) + return + } + + var reportedRecognition db.ReportedRecognition + err = json.NewDecoder(req.Body).Decode(&reportedRecognition) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while decoding request data") + repsonse(rw, http.StatusBadRequest, errorResponse{ + Error: messageObject{ + Message: "Invalid json request body", + }, + }) + return + } + reportedRecognition.ReportedBy = int64(userID) + + ok, errFields := reportedRecognition.Validate() + if !ok { + repsonse(rw, http.StatusBadRequest, errorResponse{ + Error: errorObject{ + Code: "invalid-reported-recognition", + Fields: errFields, + messageObject: messageObject{"Invalid reported recognition data"}, + }, + }) + return + } + + resp, err := deps.Store.CreateReportedRecognition(req.Context(), recognitionID, reportedRecognition) + if err != nil { + logger.WithField("err", err.Error()).Error("Error while creating reported recognition") + repsonse(rw, http.StatusInternalServerError, errorResponse{ + Error: messageObject{ + Message: "Internal server error", + }, + }) + return + } + + repsonse(rw, http.StatusCreated, successResponse{Data: resp}) + }) +} diff --git a/go-backend/service/reported_recognition_http_test.go b/go-backend/service/reported_recognition_http_test.go new file mode 100644 index 000000000..714cdf9be --- /dev/null +++ b/go-backend/service/reported_recognition_http_test.go @@ -0,0 +1,150 @@ +package service + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "joshsoftware/peerly/db" +) + +type ReportedRecognitionHandlerTestSuite struct { + suite.Suite + + dbMock *db.DBMockStore +} + +func (suite *ReportedRecognitionHandlerTestSuite) SetupTest() { + suite.dbMock = &db.DBMockStore{} +} + +func (suite *ReportedRecognitionHandlerTestSuite) TestCreateReportedRecognitionSuccess() { + now := time.Now().Unix() + suite.dbMock.On("CreateReportedRecognition", mock.Anything, mock.Anything, mock.Anything).Return(db.ReportedRecognition{ + ID: 1, + RecognitionID: int64(1), + TypeOfReporting: "fraud", + ReasonForReporting: "Reason Test", + ReportedBy: int64(1), + ReportedAt: now, + }, nil) + + body := `{ + "mark_as": "fraud", + "reason": "Reason Test" + }` + + recorder := makeHTTPCallWithJWTMiddleware( + http.MethodPost, + "/recognitions/{recognition_id:[0-9]+}/report", + "/recognitions/1/report", + body, + createReportedRecognitionHandler(Dependencies{Store: suite.dbMock}), + ) + expectedBody := fmt.Sprintf(`{"data":{"id":1,"recognition_id":1,"mark_as":"fraud","reason":"Reason Test","reported_by":1,"reported_at":%v}}`, now) + + assert.Equal(suite.T(), http.StatusCreated, recorder.Code) + assert.Equal(suite.T(), expectedBody, recorder.Body.String()) + suite.dbMock.AssertExpectations(suite.T()) +} + +func (suite *ReportedRecognitionHandlerTestSuite) TestCreateReportedRecognitionWhenInvalidJSONFormat() { + body := `{ + "mark_as": "fraud" + "reason": "Reason Test" + }` + + recorder := makeHTTPCallWithJWTMiddleware( + http.MethodPost, + "/recognitions/{recognition_id:[0-9]+}/report", + "/recognitions/1/report", + body, + createReportedRecognitionHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusBadRequest, recorder.Code) + assert.Equal(suite.T(), `{"error":{"message":"Invalid json request body"}}`, recorder.Body.String()) + suite.dbMock.AssertExpectations(suite.T()) +} + +func (suite *ReportedRecognitionHandlerTestSuite) TestCreateReportedRecognitionWhenEmptyReportedRecognitionType() { + body := `{ + "mark_as": "", + "reason": "Reason Test" + }` + + recorder := makeHTTPCallWithJWTMiddleware( + http.MethodPost, + "/recognitions/{recognition_id:[0-9]+}/report", + "/recognitions/1/report", + body, + createReportedRecognitionHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusBadRequest, recorder.Code) + assert.Equal(suite.T(), `{"error":{"code":"invalid-reported-recognition","message":"Invalid reported recognition data","fields":{"mark_as":"Can't be blank"}}}`, recorder.Body.String()) + suite.dbMock.AssertExpectations(suite.T()) +} + +func (suite *ReportedRecognitionHandlerTestSuite) TestCreateReportedRecognitionWhenInvalidReportedRecognitionType() { + body := `{ + "mark_as": "XYZ", + "reason": "Reason Test" + }` + + recorder := makeHTTPCallWithJWTMiddleware( + http.MethodPost, + "/recognitions/{recognition_id:[0-9]+}/report", + "/recognitions/1/report", + body, + createReportedRecognitionHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusBadRequest, recorder.Code) + assert.Equal(suite.T(), `{"error":{"code":"invalid-reported-recognition","message":"Invalid reported recognition data","fields":{"mark_as":"Invalid reported recognition type"}}}`, recorder.Body.String()) + suite.dbMock.AssertExpectations(suite.T()) +} + +func (suite *ReportedRecognitionHandlerTestSuite) TestCreateReportedRecognitionWhenEmptyReasonForReport() { + body := `{ + "mark_as": "fraud", + "reason": "" + }` + + recorder := makeHTTPCallWithJWTMiddleware( + http.MethodPost, + "/recognitions/{recognition_id:[0-9]+}/report", + "/recognitions/1/report", + body, + createReportedRecognitionHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusBadRequest, recorder.Code) + assert.Equal(suite.T(), `{"error":{"code":"invalid-reported-recognition","message":"Invalid reported recognition data","fields":{"reason":"Can't be blank"}}}`, recorder.Body.String()) + suite.dbMock.AssertExpectations(suite.T()) +} + +func (suite *ReportedRecognitionHandlerTestSuite) TestCreateReportedRecognitionWhenDBFailure() { + suite.dbMock.On("CreateReportedRecognition", mock.Anything, mock.Anything, mock.Anything).Return(db.ReportedRecognition{}, errors.New("error creating reported recognition")) + + body := `{ + "mark_as": "fraud", + "reason": "Reason Test" + }` + + recorder := makeHTTPCallWithJWTMiddleware( + http.MethodPost, + "/recognitions/{recognition_id:[0-9]+}/report", + "/recognitions/1/report", + body, + createReportedRecognitionHandler(Dependencies{Store: suite.dbMock}), + ) + + assert.Equal(suite.T(), http.StatusInternalServerError, recorder.Code) + assert.Equal(suite.T(), `{"error":{"message":"Internal server error"}}`, recorder.Body.String()) + suite.dbMock.AssertExpectations(suite.T()) +} diff --git a/go-backend/service/router.go b/go-backend/service/router.go index 7b5481c54..827e55da1 100644 --- a/go-backend/service/router.go +++ b/go-backend/service/router.go @@ -4,12 +4,11 @@ import ( "fmt" "net/http" - "joshsoftware/peerly/config" - jwtmiddleware "github.com/auth0/go-jwt-middleware" jwt "github.com/dgrijalva/jwt-go" "github.com/gorilla/mux" "github.com/urfave/negroni" + "joshsoftware/peerly/config" ) const ( @@ -40,6 +39,18 @@ func InitRouter(deps Dependencies) (router *mux.Router) { router.HandleFunc("/organisations/{organisation_id:[0-9]+}/core_values/{id:[0-9]+}", deleteCoreValueHandler(deps)).Methods(http.MethodDelete).Headers(versionHeader, v1) router.HandleFunc("/organisations/{organisation_id:[0-9]+}/core_values/{id:[0-9]+}", updateCoreValueHandler(deps)).Methods(http.MethodPut).Headers(versionHeader, v1) + //reported recognition + router.Handle("/recognitions/{recognition_id:[0-9]+}/report", negroni.New( + negroni.HandlerFunc(jwtMiddleware.HandlerWithNext), + negroni.Wrap(http.HandlerFunc(createReportedRecognitionHandler(deps))), + )).Methods(http.MethodPost).Headers(versionHeader, v1) + + //recognition moderation + router.Handle("/recognitions/{recognition_id:[0-9]+}/review", negroni.New( + negroni.HandlerFunc(jwtMiddleware.HandlerWithNext), + negroni.Wrap(http.HandlerFunc(createRecognitionModerationHandler(deps))), + )).Methods(http.MethodPost).Headers(versionHeader, v1) + //users router.HandleFunc("/users", listUsersHandler(deps)).Methods(http.MethodGet).Headers(versionHeader, v1) router.HandleFunc("/users/{id:[0-9]+}", getUserHandler(deps)).Methods(http.MethodGet).Headers(versionHeader, v1)