diff --git a/appapi.yaml b/appapi.yaml index 9e85e2dfec012bc03ebd8e50b69ad42d22a83fc3..827826114d18d848a015079dd491d98bfe02b997 100755 --- a/appapi.yaml +++ b/appapi.yaml @@ -648,6 +648,34 @@ paths: description: User is denied to answer that roundquestion, either because that roundquestion has already been answered by that user or the user is not participant of the related duel. '404': description: A roundquestion with that id does not exist. + '/duel/report': + post: + summary: Report a mistake in a question. + description: Endpoint to report a mistake in a question, its answers or its explaination. + parameters: + - name: question + description: ID of the falty question. + in: query + required: true + schema: + type: integer + format: uint32 + requestBody: + description: Object with the contents of the report. + content: + application/json: + schema: + type: object + required: + - text + properties: + text: + type: string + responses: + '201': + description: Report has been submitted. + '404': + description: There is no question with the given id. '/friend': get: summary: "Get a list of your friends." diff --git a/endpoint/duel/endpoint.go b/endpoint/duel/endpoint.go index a29f6790abdf8e3ab76d7b58b9e72581e9494166..afa101d83cf02f3fe218a900a70fa7f37146337d 100644 --- a/endpoint/duel/endpoint.go +++ b/endpoint/duel/endpoint.go @@ -45,6 +45,8 @@ func SetupDuelRoutes(group *gin.RouterGroup, repository DuelQuery, titleReposito //Endpoint to answer a questino of a round group.POST("/answer", postAnswer(repository, titleRepository, sessionRepository)) + + group.POST("/report", postReport(repository)) } func getDuel(repository DuelQuery) gin.HandlerFunc { @@ -410,6 +412,36 @@ func postAnswer(repository DuelQuery, titleRepository title.TitleQuery, sessionR } } +func postReport(repository DuelQuery) gin.HandlerFunc { + return func(ctx *gin.Context) { + var req struct { + QuestionID uint32 `form:"question" binding:"required_without=Text"` + Text string `json:"text" binding:"required_without=QuestionID"` + } + + if err := ctx.BindQuery(&req); err != nil { + ctx.AbortWithError(http.StatusBadRequest, err) + return + } + + if err := ctx.BindJSON(&req); err != nil { + ctx.AbortWithError(http.StatusBadRequest, err) + return + } + + if err := repository.InsertReport(req.QuestionID, req.Text); err == ErrQuestionNotFound { + ctx.Status(http.StatusNotFound) + return + } else if err != nil { + raven.CaptureError(err, nil) + ctx.AbortWithError(http.StatusInternalServerError, err) + return + } + + ctx.Status(http.StatusCreated) + } +} + func notifyUser(nt notifications.NotificationType, sessionRepository refreshjwt.RefreshJWTQuery, userID uint32, message *messaging.Notification, data map[string]string) error { // Fetch user token token, err := sessionRepository.GetLatestToken(userID) diff --git a/endpoint/duel/query.go b/endpoint/duel/query.go index 2021245971e4409723d5d14c820674a290ff419a..a34e8fd7a9eb8572ed74fd0c8d4dc93f30980e32 100644 --- a/endpoint/duel/query.go +++ b/endpoint/duel/query.go @@ -226,6 +226,9 @@ var ErrInvalidAnswer = errors.New("given answer was not of expected form") // ErrRoundQuestionNotFound is returned if no roundquestion was returned by a parameterized query var ErrRoundQuestionNotFound = errors.New("roundquestion not found") +// ErrQuestionNotFound is returned if a referenced question does not exist. +var ErrQuestionNotFound = errors.New("question not found") + var ErrBadRequest = errors.New("The request was either formatted incorrectly or it was not allowed at the currect state of the database.") const TotalRounds = 2 @@ -274,6 +277,7 @@ type DuelQuery interface { InsertTextUserAnswer(roundID, questionID, userID uint32, answer string) (*UserAwards, error) IsPlayerOfRound(userID, roundID uint32) (bool, error) CanAnswerQuestion(userID, roundID, questionID uint32) (bool, error) + InsertReport(questionID uint32, text string) error } // MySQLDuelQuery provides functionality to create, get and update duels in a MySQL database. @@ -1466,3 +1470,24 @@ func (*MySQLDuelQuery) CanAnswerQuestion(userID, roundID, questionID uint32) (bo return count == 0, nil } + +// InsertReport creates a report with the given questionID and text. ErrQuestionNotFound will +// be returned when the referenced questionID does not belong to any question. +func (*MySQLDuelQuery) InsertReport(questionID uint32, text string) error { + db, err := dbhandler.GetDBConnection() + if err != nil { + return err + } + + _, err = db.Exec("INSERT INTO `report` (`question`, `test`) VALUES (?, ?)", questionID, text) + if err != nil { + if merr := err.(*mysql.MySQLError); merr.Number == 1452 { + // Question does not exist + return ErrQuestionNotFound + } + + return err + } + + return nil +} diff --git a/resources/data/testdata.sql b/resources/data/testdata.sql index 389d69dcad99371080de0791266ee8a9ae08c981..bb1182027e2f665a035edcbac98a14361ad3cd6c 100644 --- a/resources/data/testdata.sql +++ b/resources/data/testdata.sql @@ -12,6 +12,7 @@ TRUNCATE TABLE `maintainer`; TRUNCATE TABLE `user`; TRUNCATE TABLE `user_title`; TRUNCATE TABLE `question`; +TRUNCATE TABLE `report`; TRUNCATE TABLE `duel_player`; TRUNCATE TABLE `duel`; TRUNCATE TABLE `roundquestion`; diff --git a/test/duel_test.go b/test/duel_test.go index 68b21a67c03902df0f2793315c8a85eafeafc93d..cd16ade3704df395cabc79b2f3049028f8123bdf 100644 --- a/test/duel_test.go +++ b/test/duel_test.go @@ -162,6 +162,9 @@ func (m *mockDuel) IsPlayerOfRound(userID, roundID uint32) (bool, error) { func (m *mockDuel) CanAnswerQuestion(userID, roundID, questionID uint32) (bool, error) { return false, errors.New("query not mocked") } +func (m *mockDuel) InsertReport(questionID uint32, text string) error { + return errors.New("query not mocked") +} func newMockDuel() mockDuel { var mock mockDuel diff --git a/test/e2e/duelendpoint_test.go b/test/e2e/duelendpoint_test.go index e77d0eecc32825ea4b2193b4bd011d8847a55b25..6e2931d55f8ab63f7f586685e398901f3555ddc8 100644 --- a/test/e2e/duelendpoint_test.go +++ b/test/e2e/duelendpoint_test.go @@ -352,6 +352,69 @@ func getDuelPool403(router *gin.Engine) func(t *testing.T) { } } +func postReport201(router *gin.Engine) func(t *testing.T) { + return func(t *testing.T) { + env := new(test.TestEnv) + env.Router = router + env.Login("user1", "Password1", t) + + res := httptest.NewRecorder() + req, _ := env.NewParameterRequest("POST", "/duel/report", map[string]string{"question": "3"}, []byte("{\"text\":\"my report\"}")) + + router.ServeHTTP(res, req) + + assert.Equal(t, 201, res.Code) + db, err := dbhandler.GetDBConnection() + if err != nil { + t.Skip("skipping postReport201 since unable to fetch database instance") + } + + row := db.QueryRow("SELECT `question`, `test` FROM `report`") + + var questionID uint32 + var text string + + if err := row.Scan(&questionID, &text); err != nil { + t.Error("failed to query database result") + } + + assert.EqualValues(t, 3, questionID) + assert.Equal(t, "my report", text) + } +} + +func postReport404(router *gin.Engine) func(t *testing.T) { + return func(t *testing.T) { + env := new(test.TestEnv) + env.Router = router + env.Login("user1", "Password1", t) + + res := httptest.NewRecorder() + req, _ := env.NewParameterRequest("POST", "/duel/report", map[string]string{"question": "123"}, []byte("{\"text\":\"no report\"}")) + + router.ServeHTTP(res, req) + + assert.Equal(t, 404, res.Code) + db, err := dbhandler.GetDBConnection() + if err != nil { + t.Skip("skipping postReport201 since unable to fetch database instance") + } + + row := db.QueryRow("SELECT COUNT(`idreport`), `question`, `test` FROM `report`") + + var count, questionID uint32 + var text string + + if err := row.Scan(&count, &questionID, &text); err != nil { + t.Error("failed to query database result") + } + + assert.EqualValues(t, 1, count) + assert.NotEqual(t, uint32(123), questionID) + assert.NotEqual(t, "no report", text) + } +} + func Test_getDuelByID(t *testing.T) { env := E2EEnvironment{} @@ -437,3 +500,16 @@ func Test_setPool(t *testing.T) { t.Run("setPool200", testPatchPool200(router)) } + +func Test_postReport(t *testing.T) { + + env := E2EEnvironment{} + router, setuperr := env.SetupEnvironment() + if setuperr != nil { + t.Fail() + t.Skip("Skipping E2E getDuel tests. Environment setup failed.", setuperr.Error()) + } + + t.Run("postReport201", postReport201(router)) + t.Run("postReport404", postReport404(router)) +}