From 02243eeab05822c60b7ac3291f6e7bc320062caf Mon Sep 17 00:00:00 2001 From: kaitoyama Date: Fri, 6 Dec 2024 14:07:54 +0900 Subject: [PATCH] remove router --- router/api.go | 23 - router/api_test.go | 51 - router/middleware.go | 384 ------- router/middleware_test.go | 637 ----------- router/questionnaires.go | 666 ----------- router/questionnaires_test.go | 1946 --------------------------------- router/questions.go | 158 --- router/questions_test.go | 1433 ------------------------ router/responses.go | 426 -------- router/responses_test.go | 1879 ------------------------------- router/results.go | 43 - router/results_test.go | 176 --- router/users.go | 268 ----- router/users_test.go | 951 ---------------- 14 files changed, 9041 deletions(-) delete mode 100644 router/api.go delete mode 100644 router/api_test.go delete mode 100644 router/middleware.go delete mode 100644 router/middleware_test.go delete mode 100644 router/questionnaires.go delete mode 100644 router/questionnaires_test.go delete mode 100644 router/questions.go delete mode 100644 router/questions_test.go delete mode 100644 router/responses.go delete mode 100644 router/responses_test.go delete mode 100644 router/results.go delete mode 100644 router/results_test.go delete mode 100644 router/users.go delete mode 100644 router/users_test.go diff --git a/router/api.go b/router/api.go deleted file mode 100644 index 3981794b..00000000 --- a/router/api.go +++ /dev/null @@ -1,23 +0,0 @@ -package router - -// API api全体の構造体 -type API struct { - *Middleware - *Questionnaire - *Question - *Response - *Result - *User -} - -// NewAPI APIのコンストラクタ -func NewAPI(middleware *Middleware, questionnaire *Questionnaire, question *Question, response *Response, result *Result, user *User) *API { - return &API{ - Middleware: middleware, - Questionnaire: questionnaire, - Question: question, - Response: response, - Result: result, - User: user, - } -} diff --git a/router/api_test.go b/router/api_test.go deleted file mode 100644 index 67e903db..00000000 --- a/router/api_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package router - -import ( - "errors" - "net/http" - "net/http/httptest" - "strings" - - "github.com/labstack/echo/v4" -) - -type users string -type httpMethods string -type contentTypes string - -const ( - rootPath = "/api" - userHeader = "X-Showcase-User" - userUnAuthorized = "-" - userOne users = "mazrean" - userTwo users = "ryoha" - //userThree users = "YumizSui" - methodGet httpMethods = http.MethodGet - methodPost httpMethods = http.MethodPost - methodPatch httpMethods = http.MethodPatch - methodDelete httpMethods = http.MethodDelete - typeNone contentTypes = "" - typeJSON contentTypes = echo.MIMEApplicationJSON -) - -var ( - errMock = errors.New("Mock Error") -) - -func makePath(path string) string { - return rootPath + path -} - -func createRecorder(e *echo.Echo, user users, method httpMethods, path string, contentType contentTypes, body string) *httptest.ResponseRecorder { - req := httptest.NewRequest(string(method), path, strings.NewReader(body)) - if contentType != typeNone { - req.Header.Set(echo.HeaderContentType, string(contentType)) - } - req.Header.Set(userHeader, string(user)) - - rec := httptest.NewRecorder() - - e.ServeHTTP(rec, req) - - return rec -} diff --git a/router/middleware.go b/router/middleware.go deleted file mode 100644 index c1ee10f5..00000000 --- a/router/middleware.go +++ /dev/null @@ -1,384 +0,0 @@ -package router - -import ( - "errors" - "fmt" - "net/http" - "strconv" - - "github.com/go-playground/validator/v10" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/traPtitech/anke-to/model" -) - -// Middleware Middlewareの構造体 -type Middleware struct { - model.IAdministrator - model.IRespondent - model.IQuestion - model.IQuestionnaire -} - -// NewMiddleware Middlewareのコンストラクタ -func NewMiddleware(administrator model.IAdministrator, respondent model.IRespondent, question model.IQuestion, questionnaire model.IQuestionnaire) *Middleware { - return &Middleware{ - IAdministrator: administrator, - IRespondent: respondent, - IQuestion: question, - IQuestionnaire: questionnaire, - } -} - -const ( - validatorKey = "validator" - userIDKey = "userID" - questionnaireIDKey = "questionnaireID" - responseIDKey = "responseID" - questionIDKey = "questionID" -) - -func (*Middleware) SetValidatorMiddleware(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - validate := validator.New() - c.Set(validatorKey, validate) - - return next(c) - } -} - -/* 消せないアンケートの発生を防ぐための管理者 -暫定的にハードコーディングで対応*/ -var adminUserIDs = []string{"ryoha", "xxarupakaxx", "kaitoyama", "cp20", "itzmeowww"} - -// SetUserIDMiddleware X-Showcase-UserからユーザーIDを取得しセットする -func (*Middleware) SetUserIDMiddleware(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - userID := c.Request().Header.Get("X-Showcase-User") - if userID == "" { - userID = "mds_boy" - } - - c.Set(userIDKey, userID) - - return next(c) - } -} - -// TraPMemberAuthenticate traP部員かの認証 -func (*Middleware) TraPMemberAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - // トークンを持たないユーザはアクセスできない - if userID == "-" { - c.Logger().Info("not logged in") - return echo.NewHTTPError(http.StatusUnauthorized, "You are not logged in") - } - - return next(c) - } -} - -// TrapRateLimitMiddlewareFunc traP IDベースのリクエスト制限 -func (*Middleware) TrapRateLimitMiddlewareFunc() echo.MiddlewareFunc { - config := middleware.RateLimiterConfig{ - Store: middleware.NewRateLimiterMemoryStore(5), - IdentifierExtractor: func(c echo.Context) (string, error) { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return "", echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - return userID, nil - }, - } - - return middleware.RateLimiterWithConfig(config) -} - -// QuestionnaireAdministratorAuthenticate アンケートの管理者かどうかの認証 -func (m *Middleware) QuestionnaireAdministratorAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - strQuestionnaireID := c.Param("questionnaireID") - questionnaireID, err := strconv.Atoi(strQuestionnaireID) - if err != nil { - c.Logger().Infof("failed to convert questionnaireID to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid questionnaireID:%s(error: %w)", strQuestionnaireID, err)) - } - - for _, adminID := range adminUserIDs { - if userID == adminID { - c.Set(questionnaireIDKey, questionnaireID) - - return next(c) - } - } - isAdmin, err := m.CheckQuestionnaireAdmin(c.Request().Context(), userID, questionnaireID) - if err != nil { - c.Logger().Errorf("failed to check questionnaire admin: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to check if you are administrator: %w", err)) - } - if !isAdmin { - return c.String(http.StatusForbidden, "You are not a administrator of this questionnaire.") - } - - c.Set(questionnaireIDKey, questionnaireID) - - return next(c) - } -} - -// ResponseReadAuthenticate 回答閲覧権限があるかの認証 -func (m *Middleware) ResponseReadAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - strResponseID := c.Param("responseID") - responseID, err := strconv.Atoi(strResponseID) - if err != nil { - c.Logger().Info("failed to convert responseID to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid responseID:%s(error: %w)", strResponseID, err)) - } - - // 回答者ならOK - respondent, err := m.GetRespondent(c.Request().Context(), responseID) - if errors.Is(err, model.ErrRecordNotFound) { - c.Logger().Infof("response not found: %+v", err) - return echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("response not found:%d", responseID)) - } - if err != nil { - c.Logger().Errorf("failed to check if you are a respondent: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to check if you are a respondent: %w", err)) - } - if respondent == nil { - c.Logger().Error("respondent is nil") - return echo.NewHTTPError(http.StatusInternalServerError) - } - if respondent.UserTraqid == userID { - return next(c) - } - - // 回答者以外は一時保存の回答は閲覧できない - if !respondent.SubmittedAt.Valid { - c.Logger().Info("not submitted") - - // Note: 一時保存の回答の存在もわかってはいけないので、Respondentが見つからない時と全く同じエラーを返す - return echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("response not found:%d", responseID)) - } - - // アンケートごとの回答閲覧権限チェック - responseReadPrivilegeInfo, err := m.GetResponseReadPrivilegeInfoByResponseID(c.Request().Context(), userID, responseID) - if errors.Is(err, model.ErrRecordNotFound) { - c.Logger().Infof("response not found: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid responseID: %d", responseID)) - } else if err != nil { - c.Logger().Errorf("failed to get response read privilege info: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get response read privilege info: %w", err)) - } - - haveReadPrivilege, err := checkResponseReadPrivilege(responseReadPrivilegeInfo) - if err != nil { - c.Logger().Errorf("failed to check response read privilege: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to check response read privilege: %w", err)) - } - if !haveReadPrivilege { - return c.String(http.StatusForbidden, "You do not have permission to view this response.") - } - - return next(c) - } -} - -// RespondentAuthenticate 回答者かどうかの認証 -func (m *Middleware) RespondentAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - strResponseID := c.Param("responseID") - responseID, err := strconv.Atoi(strResponseID) - if err != nil { - c.Logger().Infof("failed to convert responseID to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid responseID:%s(error: %w)", strResponseID, err)) - } - - respondent, err := m.GetRespondent(c.Request().Context(), responseID) - if errors.Is(err, model.ErrRecordNotFound) { - c.Logger().Infof("response not found: %+v", err) - return echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("response not found:%d", responseID)) - } - if err != nil { - c.Logger().Errorf("failed to check if you are a respondent: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to check if you are a respondent: %w", err)) - } - if respondent == nil { - c.Logger().Error("respondent is nil") - return echo.NewHTTPError(http.StatusInternalServerError) - } - if respondent.UserTraqid != userID { - return c.String(http.StatusForbidden, "You are not a respondent of this response.") - } - - c.Set(responseIDKey, responseID) - - return next(c) - } -} - -// QuestionAdministratorAuthenticate アンケートの管理者かどうかの認証 -func (m *Middleware) QuestionAdministratorAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - strQuestionID := c.Param("questionID") - questionID, err := strconv.Atoi(strQuestionID) - if err != nil { - c.Logger().Infof("failed to convert questionID to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid questionID:%s(error: %w)", strQuestionID, err)) - } - - for _, adminID := range adminUserIDs { - if userID == adminID { - c.Set(questionIDKey, questionID) - - return next(c) - } - } - isAdmin, err := m.CheckQuestionAdmin(c.Request().Context(), userID, questionID) - if err != nil { - c.Logger().Errorf("failed to check if you are a question administrator: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to check if you are administrator: %w", err)) - } - if !isAdmin { - return c.String(http.StatusForbidden, "You are not a administrator of this questionnaire.") - } - - c.Set(questionIDKey, questionID) - - return next(c) - } -} - -// ResultAuthenticate アンケートの回答を確認できるかの認証 -func (m *Middleware) ResultAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - strQuestionnaireID := c.Param("questionnaireID") - questionnaireID, err := strconv.Atoi(strQuestionnaireID) - if err != nil { - c.Logger().Infof("failed to convert questionnaireID to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid questionnaireID:%s(error: %w)", strQuestionnaireID, err)) - } - - responseReadPrivilegeInfo, err := m.GetResponseReadPrivilegeInfoByQuestionnaireID(c.Request().Context(), userID, questionnaireID) - if errors.Is(err, model.ErrRecordNotFound) { - c.Logger().Infof("response not found: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid responseID: %d", questionnaireID)) - } else if err != nil { - c.Logger().Errorf("failed to get responseReadPrivilegeInfo: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get response read privilege info: %w", err)) - } - - haveReadPrivilege, err := checkResponseReadPrivilege(responseReadPrivilegeInfo) - if err != nil { - c.Logger().Errorf("failed to check response read privilege: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to check response read privilege: %w", err)) - } - if !haveReadPrivilege { - return c.String(http.StatusForbidden, "You do not have permission to view this response.") - } - - return next(c) - } -} - -func checkResponseReadPrivilege(responseReadPrivilegeInfo *model.ResponseReadPrivilegeInfo) (bool, error) { - switch responseReadPrivilegeInfo.ResSharedTo { - case "administrators": - return responseReadPrivilegeInfo.IsAdministrator, nil - case "respondents": - return responseReadPrivilegeInfo.IsAdministrator || responseReadPrivilegeInfo.IsRespondent, nil - case "public": - return true, nil - } - - return false, errors.New("invalid resSharedTo") -} - -func getValidator(c echo.Context) (*validator.Validate, error) { - rowValidate := c.Get(validatorKey) - validate, ok := rowValidate.(*validator.Validate) - if !ok { - return nil, fmt.Errorf("failed to get validator") - } - - return validate, nil -} - -func getUserID(c echo.Context) (string, error) { - rowUserID := c.Get(userIDKey) - userID, ok := rowUserID.(string) - if !ok { - return "", errors.New("invalid context userID") - } - - return userID, nil -} - -func getQuestionnaireID(c echo.Context) (int, error) { - rowQuestionnaireID := c.Get(questionnaireIDKey) - questionnaireID, ok := rowQuestionnaireID.(int) - if !ok { - return 0, errors.New("invalid context questionnaireID") - } - - return questionnaireID, nil -} - -func getResponseID(c echo.Context) (int, error) { - rowResponseID := c.Get(responseIDKey) - responseID, ok := rowResponseID.(int) - if !ok { - return 0, errors.New("invalid context responseID") - } - - return responseID, nil -} - -func getQuestionID(c echo.Context) (int, error) { - rowQuestionID := c.Get(questionIDKey) - questionID, ok := rowQuestionID.(int) - if !ok { - return 0, errors.New("invalid context questionID") - } - - return questionID, nil -} diff --git a/router/middleware_test.go b/router/middleware_test.go deleted file mode 100644 index 84503255..00000000 --- a/router/middleware_test.go +++ /dev/null @@ -1,637 +0,0 @@ -package router - -import ( - "errors" - "fmt" - "net/http" - "net/http/httptest" - "strconv" - "testing" - "time" - - "github.com/golang/mock/gomock" - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" - "github.com/traPtitech/anke-to/model" - "github.com/traPtitech/anke-to/model/mock_model" - "gopkg.in/guregu/null.v4" -) - -type CallChecker struct { - IsCalled bool -} - -func (cc *CallChecker) Handler(c echo.Context) error { - cc.IsCalled = true - - return c.NoContent(http.StatusOK) -} - -func TestSetUserIDMiddleware(t *testing.T) { - t.Parallel() - - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockQuestion := mock_model.NewMockIQuestion(ctrl) - - middleware := NewMiddleware(mockAdministrator, mockRespondent, mockQuestion, mockQuestionnaire) - - type args struct { - userID string - } - type expect struct { - userID interface{} - } - type test struct { - description string - args - expect - } - - testCases := []test{ - { - description: "正常なユーザーIDなのでユーザーID取得", - args: args{ - userID: "mazrean", - }, - expect: expect{ - userID: "mazrean", - }, - }, - { - description: "ユーザーIDが空なのでmds_boy", - args: args{ - userID: "", - }, - expect: expect{ - userID: "mds_boy", - }, - }, - { - description: "ユーザーIDが-なので-", - args: args{ - userID: "-", - }, - expect: expect{ - userID: "-", - }, - }, - } - - for _, testCase := range testCases { - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - req.Header.Set("X-Showcase-User", testCase.args.userID) - - e.HTTPErrorHandler(middleware.SetUserIDMiddleware(func(c echo.Context) error { - assertion.Equal(testCase.expect.userID, c.Get(userIDKey), testCase.description, "userID") - return c.NoContent(http.StatusOK) - })(c), c) - - assertion.Equal(http.StatusOK, rec.Code, testCase.description, "status code") - } -} - -func TestTraPMemberAuthenticate(t *testing.T) { - t.Parallel() - - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockQuestion := mock_model.NewMockIQuestion(ctrl) - - middleware := NewMiddleware(mockAdministrator, mockRespondent, mockQuestion, mockQuestionnaire) - - type args struct { - userID string - } - type expect struct { - statusCode int - isCalled bool - } - type test struct { - description string - args - expect - } - - testCases := []test{ - { - description: "正常なユーザーIDなので通す", - args: args{ - userID: "mazrean", - }, - expect: expect{ - statusCode: http.StatusOK, - isCalled: true, - }, - }, - { - description: "ユーザーIDが-なので401", - args: args{ - userID: "-", - }, - expect: expect{ - statusCode: http.StatusUnauthorized, - isCalled: false, - }, - }, - } - - for _, testCase := range testCases { - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - c.Set(userIDKey, testCase.args.userID) - - callChecker := CallChecker{} - - e.HTTPErrorHandler(middleware.TraPMemberAuthenticate(callChecker.Handler)(c), c) - - assertion.Equal(testCase.expect.statusCode, rec.Code, testCase.description, "status code") - assertion.Equal(testCase.expect.isCalled, testCase.expect.statusCode == http.StatusOK, testCase.description, "isCalled") - } -} - -func TestResponseReadAuthenticate(t *testing.T) { - t.Parallel() - - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockQuestion := mock_model.NewMockIQuestion(ctrl) - - middleware := NewMiddleware(mockAdministrator, mockRespondent, mockQuestion, mockQuestionnaire) - - type args struct { - userID string - respondent *model.Respondents - GetRespondentError error - ExecutesResponseReadPrivilegeCheck bool - haveReadPrivilege bool - GetResponseReadPrivilegeInfoByResponseIDError error - checkResponseReadPrivilegeError error - } - type expect struct { - statusCode int - isCalled bool - } - type test struct { - description string - args - expect - } - - testCases := []test{ - { - description: "この回答の回答者である場合通す", - args: args{ - userID: "user1", - respondent: &model.Respondents{ - UserTraqid: "user1", - }, - }, - expect: expect{ - statusCode: http.StatusOK, - isCalled: true, - }, - }, - { - description: "GetRespondentがErrRecordNotFoundの場合404", - args: args{ - userID: "user1", - GetRespondentError: model.ErrRecordNotFound, - }, - expect: expect{ - statusCode: http.StatusNotFound, - isCalled: false, - }, - }, - { - description: "respondentがnilの場合500", - args: args{ - userID: "user1", - respondent: nil, - }, - expect: expect{ - statusCode: http.StatusInternalServerError, - isCalled: false, - }, - }, - { - description: "GetRespondentがエラー(ErrRecordNotFound以外)の場合500", - args: args{ - GetRespondentError: errors.New("error"), - }, - expect: expect{ - statusCode: http.StatusInternalServerError, - isCalled: false, - }, - }, - { - description: "responseがsubmitされていない場合404", - args: args{ - userID: "user1", - respondent: &model.Respondents{ - UserTraqid: "user2", - SubmittedAt: null.Time{}, - }, - }, - expect: expect{ - statusCode: http.StatusNotFound, - isCalled: false, - }, - }, - { - description: "この回答の回答者でなくてもsubmitされていてhaveReadPrivilegeがtrueの場合通す", - args: args{ - userID: "user1", - respondent: &model.Respondents{ - UserTraqid: "user2", - SubmittedAt: null.NewTime(time.Now(), true), - }, - ExecutesResponseReadPrivilegeCheck: true, - haveReadPrivilege: true, - }, - expect: expect{ - statusCode: http.StatusOK, - isCalled: true, - }, - }, - { - description: "この回答の回答者でなく、submitされていてhaveReadPrivilegeがfalseの場合403", - args: args{ - userID: "user1", - respondent: &model.Respondents{ - UserTraqid: "user2", - SubmittedAt: null.NewTime(time.Now(), true), - }, - ExecutesResponseReadPrivilegeCheck: true, - haveReadPrivilege: false, - }, - expect: expect{ - statusCode: http.StatusForbidden, - isCalled: false, - }, - }, - { - description: "GetResponseReadPrivilegeInfoByResponseIDがErrRecordNotFoundの場合400", - args: args{ - userID: "user1", - respondent: &model.Respondents{ - UserTraqid: "user2", - SubmittedAt: null.NewTime(time.Now(), true), - }, - ExecutesResponseReadPrivilegeCheck: true, - haveReadPrivilege: false, - GetResponseReadPrivilegeInfoByResponseIDError: model.ErrRecordNotFound, - }, - expect: expect{ - statusCode: http.StatusBadRequest, - isCalled: false, - }, - }, - { - description: "GetResponseReadPrivilegeInfoByResponseIDがエラー(ErrRecordNotFound以外)の場合500", - args: args{ - userID: "user1", - respondent: &model.Respondents{ - UserTraqid: "user2", - SubmittedAt: null.NewTime(time.Now(), true), - }, - ExecutesResponseReadPrivilegeCheck: true, - haveReadPrivilege: false, - GetResponseReadPrivilegeInfoByResponseIDError: errors.New("error"), - }, - expect: expect{ - statusCode: http.StatusInternalServerError, - isCalled: false, - }, - }, - { - description: "checkResponseReadPrivilegeがエラーの場合500", - args: args{ - userID: "user1", - respondent: &model.Respondents{ - UserTraqid: "user2", - SubmittedAt: null.NewTime(time.Now(), true), - }, - ExecutesResponseReadPrivilegeCheck: true, - haveReadPrivilege: false, - checkResponseReadPrivilegeError: errors.New("error"), - }, - expect: expect{ - statusCode: http.StatusInternalServerError, - isCalled: false, - }, - }, - } - - for _, testCase := range testCases { - responseID := 1 - - var responseReadPrivilegeInfo model.ResponseReadPrivilegeInfo - if testCase.args.checkResponseReadPrivilegeError != nil { - responseReadPrivilegeInfo = model.ResponseReadPrivilegeInfo{ - ResSharedTo: "invalid value", - } - } else if testCase.args.haveReadPrivilege { - responseReadPrivilegeInfo = model.ResponseReadPrivilegeInfo{ - ResSharedTo: "public", - } - } else { - responseReadPrivilegeInfo = model.ResponseReadPrivilegeInfo{ - ResSharedTo: "administrators", - } - } - - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - c.SetPath("/responses/:responseID") - c.SetParamNames("responseID") - c.SetParamValues(strconv.Itoa(responseID)) - c.Set(userIDKey, testCase.args.userID) - - mockRespondent. - EXPECT(). - GetRespondent(c.Request().Context(), responseID). - Return(testCase.args.respondent, testCase.args.GetRespondentError) - if testCase.args.ExecutesResponseReadPrivilegeCheck { - mockQuestionnaire. - EXPECT(). - GetResponseReadPrivilegeInfoByResponseID(c.Request().Context(), testCase.args.userID, responseID). - Return(&responseReadPrivilegeInfo, testCase.args.GetResponseReadPrivilegeInfoByResponseIDError) - } - - callChecker := CallChecker{} - - e.HTTPErrorHandler(middleware.ResponseReadAuthenticate(callChecker.Handler)(c), c) - - assertion.Equalf(testCase.expect.statusCode, rec.Code, testCase.description, "status code") - assertion.Equalf(testCase.expect.isCalled, callChecker.IsCalled, testCase.description, "isCalled") - } -} - -func TestResultAuthenticate(t *testing.T) { - t.Parallel() - - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockQuestion := mock_model.NewMockIQuestion(ctrl) - - middleware := NewMiddleware(mockAdministrator, mockRespondent, mockQuestion, mockQuestionnaire) - - type args struct { - haveReadPrivilege bool - GetResponseReadPrivilegeInfoByResponseIDError error - checkResponseReadPrivilegeError error - } - type expect struct { - statusCode int - isCalled bool - } - type test struct { - description string - args - expect - } - - testCases := []test{ - { - description: "haveReadPrivilegeがtrueの場合通す", - args: args{ - haveReadPrivilege: true, - }, - expect: expect{ - statusCode: http.StatusOK, - isCalled: true, - }, - }, - { - description: "haveReadPrivilegeがfalseの場合403", - args: args{ - haveReadPrivilege: false, - }, - expect: expect{ - statusCode: http.StatusForbidden, - isCalled: false, - }, - }, - { - description: "GetResponseReadPrivilegeInfoByResponseIDがErrRecordNotFoundの場合400", - args: args{ - haveReadPrivilege: false, - GetResponseReadPrivilegeInfoByResponseIDError: model.ErrRecordNotFound, - }, - expect: expect{ - statusCode: http.StatusBadRequest, - isCalled: false, - }, - }, - { - description: "GetResponseReadPrivilegeInfoByResponseIDがエラー(ErrRecordNotFound以外)の場合500", - args: args{ - haveReadPrivilege: false, - GetResponseReadPrivilegeInfoByResponseIDError: errors.New("error"), - }, - expect: expect{ - statusCode: http.StatusInternalServerError, - isCalled: false, - }, - }, - { - description: "checkResponseReadPrivilegeがエラーの場合500", - args: args{ - haveReadPrivilege: false, - checkResponseReadPrivilegeError: errors.New("error"), - }, - expect: expect{ - statusCode: http.StatusInternalServerError, - isCalled: false, - }, - }, - } - - for _, testCase := range testCases { - userID := "testUser" - questionnaireID := 1 - var responseReadPrivilegeInfo model.ResponseReadPrivilegeInfo - if testCase.args.checkResponseReadPrivilegeError != nil { - responseReadPrivilegeInfo = model.ResponseReadPrivilegeInfo{ - ResSharedTo: "invalid value", - } - } else if testCase.args.haveReadPrivilege { - responseReadPrivilegeInfo = model.ResponseReadPrivilegeInfo{ - ResSharedTo: "public", - } - } else { - responseReadPrivilegeInfo = model.ResponseReadPrivilegeInfo{ - ResSharedTo: "administrators", - } - } - - e := echo.New() - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/results/%d", questionnaireID), nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - c.SetPath("/results/:questionnaireID") - c.SetParamNames("questionnaireID") - c.SetParamValues(strconv.Itoa(questionnaireID)) - c.Set(userIDKey, userID) - - mockQuestionnaire. - EXPECT(). - GetResponseReadPrivilegeInfoByQuestionnaireID(c.Request().Context(), userID, questionnaireID). - Return(&responseReadPrivilegeInfo, testCase.args.GetResponseReadPrivilegeInfoByResponseIDError) - - callChecker := CallChecker{} - - e.HTTPErrorHandler(middleware.ResultAuthenticate(callChecker.Handler)(c), c) - - assertion.Equalf(testCase.expect.statusCode, rec.Code, testCase.description, "status code") - assertion.Equalf(testCase.expect.isCalled, callChecker.IsCalled, testCase.description, "isCalled") - } -} - -func TestCheckResponseReadPrivilege(t *testing.T) { - t.Parallel() - - assertion := assert.New(t) - - type args struct { - responseReadPrivilegeInfo model.ResponseReadPrivilegeInfo - } - type expect struct { - haveReadPrivilege bool - isErr bool - err error - } - type test struct { - description string - args - expect - } - - testCases := []test{ - { - description: "res_shared_toがpublic、administrators、respondentsのいずれでもない場合エラー", - args: args{ - responseReadPrivilegeInfo: model.ResponseReadPrivilegeInfo{ - ResSharedTo: "invalid value", - }, - }, - expect: expect{ - isErr: true, - }, - }, - { - description: "res_shared_toがpublicの場合true", - args: args{ - responseReadPrivilegeInfo: model.ResponseReadPrivilegeInfo{ - ResSharedTo: "public", - }, - }, - expect: expect{ - haveReadPrivilege: true, - }, - }, - { - description: "res_shared_toがadministratorsかつadministratorの場合true", - args: args{ - responseReadPrivilegeInfo: model.ResponseReadPrivilegeInfo{ - ResSharedTo: "administrators", - IsAdministrator: true, - }, - }, - expect: expect{ - haveReadPrivilege: true, - }, - }, - { - description: "res_shared_toがadministratorsかつadministratorでない場合false", - args: args{ - responseReadPrivilegeInfo: model.ResponseReadPrivilegeInfo{ - ResSharedTo: "administrators", - IsAdministrator: false, - }, - }, - }, - { - description: "res_shared_toがrespondentsかつadministratorの場合true", - args: args{ - responseReadPrivilegeInfo: model.ResponseReadPrivilegeInfo{ - ResSharedTo: "respondents", - IsAdministrator: true, - }, - }, - expect: expect{ - haveReadPrivilege: true, - }, - }, - { - description: "res_shared_toがrespondentsかつrespondentの場合true", - args: args{ - responseReadPrivilegeInfo: model.ResponseReadPrivilegeInfo{ - ResSharedTo: "respondents", - IsRespondent: true, - }, - }, - expect: expect{ - haveReadPrivilege: true, - }, - }, - { - description: "res_shared_toがrespondentsかつ、administratorでもrespondentでない場合false", - args: args{ - responseReadPrivilegeInfo: model.ResponseReadPrivilegeInfo{ - ResSharedTo: "respondents", - IsAdministrator: false, - IsRespondent: false, - }, - }, - expect: expect{ - haveReadPrivilege: false, - }, - }, - } - - for _, testCase := range testCases { - haveReadPrivilege, err := checkResponseReadPrivilege(&testCase.args.responseReadPrivilegeInfo) - - if testCase.expect.isErr { - assertion.Errorf(err, testCase.description, "error") - } else { - assertion.NoErrorf(err, testCase.description, "no error") - assertion.Equalf(testCase.expect.haveReadPrivilege, haveReadPrivilege, testCase.description, "haveReadPrivilege") - } - } -} diff --git a/router/questionnaires.go b/router/questionnaires.go deleted file mode 100644 index b33db7f4..00000000 --- a/router/questionnaires.go +++ /dev/null @@ -1,666 +0,0 @@ -package router - -import ( - "context" - "errors" - "fmt" - "net/http" - "regexp" - "strconv" - "strings" - "time" - - "github.com/labstack/echo/v4" - "gopkg.in/guregu/null.v4" - - "github.com/traPtitech/anke-to/model" - "github.com/traPtitech/anke-to/traq" -) - -// Questionnaire Questionnaireの構造体 -type Questionnaire struct { - model.IQuestionnaire - model.ITarget - model.IAdministrator - model.IQuestion - model.IOption - model.IScaleLabel - model.IValidation - model.ITransaction - traq.IWebhook -} - -const MaxTitleLength = 50 - -// NewQuestionnaire Questionnaireのコンストラクタ -func NewQuestionnaire( - questionnaire model.IQuestionnaire, - target model.ITarget, - administrator model.IAdministrator, - question model.IQuestion, - option model.IOption, - scaleLabel model.IScaleLabel, - validation model.IValidation, - transaction model.ITransaction, - webhook traq.IWebhook, -) *Questionnaire { - return &Questionnaire{ - IQuestionnaire: questionnaire, - ITarget: target, - IAdministrator: administrator, - IQuestion: question, - IOption: option, - IScaleLabel: scaleLabel, - IValidation: validation, - ITransaction: transaction, - IWebhook: webhook, - } -} - -type GetQuestionnairesQueryParam struct { - Sort string `validate:"omitempty,oneof=created_at -created_at title -title modified_at -modified_at"` - Search string `validate:"omitempty"` - Page string `validate:"omitempty,number,min=0"` - Nontargeted string `validate:"omitempty,boolean"` -} - -// GetQuestionnaires GET /questionnaires -func (q *Questionnaire) GetQuestionnaires(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - sort := c.QueryParam("sort") - search := c.QueryParam("search") - page := c.QueryParam("page") - nontargeted := c.QueryParam("nontargeted") - - p := GetQuestionnairesQueryParam{ - Sort: sort, - Search: search, - Page: page, - Nontargeted: nontargeted, - } - - validate, err := getValidator(c) - if err != nil { - c.Logger().Errorf("failed to get validator: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError) - } - - err = validate.StructCtx(c.Request().Context(), p) - if err != nil { - c.Logger().Infof("failed to validate: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - if len(page) == 0 { - page = "1" - } - pageNum, err := strconv.Atoi(page) - if err != nil { - c.Logger().Infof("failed to convert page to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("failed to convert the string query parameter 'page'(%s) to integer: %w", page, err)) - } - if pageNum <= 0 { - c.Logger().Info("page must be greater than 0") - return echo.NewHTTPError(http.StatusBadRequest, errors.New("page cannot be less than 0")) - } - - var nontargetedBool bool - if len(nontargeted) != 0 { - nontargetedBool, err = strconv.ParseBool(nontargeted) - if err != nil { - c.Logger().Infof("failed to convert nontargeted to bool: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("failed to convert the string query parameter 'nontargeted'(%s) to bool: %w", nontargeted, err)) - } - } else { - nontargetedBool = false - } - - questionnaires, pageMax, err := q.IQuestionnaire.GetQuestionnaires(c.Request().Context(), userID, sort, search, pageNum, nontargetedBool) - if err != nil { - if errors.Is(err, model.ErrTooLargePageNum) || errors.Is(err, model.ErrInvalidRegex) { - c.Logger().Infof("failed to get questionnaires: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err) - } - if errors.Is(err, model.ErrDeadlineExceeded) { - c.Logger().Errorf("failed to get questionnaires (deadline exceeded): %+v", err) - return echo.NewHTTPError(http.StatusServiceUnavailable, "deadline exceeded") - } - c.Logger().Errorf("failed to get questionnaires: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - return c.JSON(http.StatusOK, map[string]interface{}{ - "page_max": pageMax, - "questionnaires": questionnaires, - }) -} - -type PostAndEditQuestionnaireRequest struct { - Title string `json:"title" validate:"required,max=50"` - Description string `json:"description"` - ResTimeLimit null.Time `json:"res_time_limit"` - ResSharedTo string `json:"res_shared_to" validate:"required,oneof=administrators respondents public"` - Targets []string `json:"targets" validate:"dive,max=32"` - Administrators []string `json:"administrators" validate:"required,min=1,dive,max=32"` -} - -// PostQuestionnaire POST /questionnaires -func (q *Questionnaire) PostQuestionnaire(c echo.Context) error { - req := PostAndEditQuestionnaireRequest{} - - // JSONを構造体につける - err := c.Bind(&req) - if err != nil { - c.Logger().Infof("failed to bind PostAndEditQuestionnaireRequest: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest) - } - - validate, err := getValidator(c) - if err != nil { - c.Logger().Errorf("failed to get validator: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError) - } - - err = validate.StructCtx(c.Request().Context(), req) - if err != nil { - c.Logger().Infof("failed to validate: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - if req.ResTimeLimit.Valid { - isBefore := req.ResTimeLimit.ValueOrZero().Before(time.Now()) - if isBefore { - c.Logger().Infof("invalid resTimeLimit: %+v", req.ResTimeLimit) - return echo.NewHTTPError(http.StatusBadRequest, "res time limit is before now") - } - } - - var questionnaireID int - err = q.ITransaction.Do(c.Request().Context(), nil, func(ctx context.Context) error { - questionnaireID, err = q.InsertQuestionnaire(ctx, req.Title, req.Description, req.ResTimeLimit, req.ResSharedTo) - if err != nil { - c.Logger().Errorf("failed to insert a questionnaire: %+v", err) - return err - } - - err := q.InsertTargets(ctx, questionnaireID, req.Targets) - if err != nil { - c.Logger().Errorf("failed to insert targets: %+v", err) - return err - } - - err = q.InsertAdministrators(ctx, questionnaireID, req.Administrators) - if err != nil { - c.Logger().Errorf("failed to insert administrators: %+v", err) - return err - } - - message := createQuestionnaireMessage( - questionnaireID, - req.Title, - req.Description, - req.Administrators, - req.ResTimeLimit, - req.Targets, - ) - err = q.PostMessage(message) - if err != nil { - c.Logger().Errorf("failed to post message: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, "failed to post message to traQ") - } - - return nil - }) - if err != nil { - var httpError *echo.HTTPError - if errors.As(err, &httpError) { - return httpError - } - - c.Logger().Errorf("failed to create questionnaire: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, "failed to create a questionnaire") - } - - now := time.Now() - return c.JSON(http.StatusCreated, map[string]interface{}{ - "questionnaireID": questionnaireID, - "title": req.Title, - "description": req.Description, - "res_time_limit": req.ResTimeLimit, - "deleted_at": "NULL", - "created_at": now.Format(time.RFC3339), - "modified_at": now.Format(time.RFC3339), - "res_shared_to": req.ResSharedTo, - "targets": req.Targets, - "administrators": req.Administrators, - }) -} - -// GetQuestionnaire GET /questionnaires/:questionnaireID -func (q *Questionnaire) GetQuestionnaire(c echo.Context) error { - strQuestionnaireID := c.Param("questionnaireID") - questionnaireID, err := strconv.Atoi(strQuestionnaireID) - if err != nil { - c.Logger().Infof("failed to convert questionnaireID to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid questionnaireID:%s(error: %w)", strQuestionnaireID, err)) - } - - questionnaire, targets, administrators, respondents, err := q.GetQuestionnaireInfo(c.Request().Context(), questionnaireID) - if err != nil { - if errors.Is(err, model.ErrRecordNotFound) { - c.Logger().Infof("questionnaire not found: %+v", err) - return echo.NewHTTPError(http.StatusNotFound, err) - } - c.Logger().Errorf("failed to get questionnaire: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - return c.JSON(http.StatusOK, map[string]interface{}{ - "questionnaireID": questionnaire.ID, - "title": questionnaire.Title, - "description": questionnaire.Description, - "res_time_limit": questionnaire.ResTimeLimit, - "created_at": questionnaire.CreatedAt.Format(time.RFC3339), - "modified_at": questionnaire.ModifiedAt.Format(time.RFC3339), - "res_shared_to": questionnaire.ResSharedTo, - "targets": targets, - "administrators": administrators, - "respondents": respondents, - }) -} - -// PostQuestionByQuestionnaireID POST /questionnaires/:questionnaireID/questions -func (q *Questionnaire) PostQuestionByQuestionnaireID(c echo.Context) error { - strQuestionnaireID := c.Param("questionnaireID") - questionnaireID, err := strconv.Atoi(strQuestionnaireID) - if err != nil { - c.Logger().Info("failed to convert questionnaireID to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid questionnaireID:%s(error: %w)", strQuestionnaireID, err)) - } - req := PostAndEditQuestionRequest{} - if err := c.Bind(&req); err != nil { - c.Logger().Info("failed to bind PostAndEditQuestionRequest: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest) - } - - validate, err := getValidator(c) - if err != nil { - c.Logger().Errorf("failed to get validator: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError) - } - - err = validate.StructCtx(c.Request().Context(), req) - if err != nil { - c.Logger().Infof("failed to validate: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // 重複したquestionNumを持つ質問をPOSTできないように - questionNumAlreadyExists, err := q.CheckQuestionNum(c.Request().Context(), questionnaireID, req.QuestionNum) - if err != nil { - c.Logger().Errorf("failed to check questionNum: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } else if questionNumAlreadyExists { - c.Logger().Info("questionNum already exists") - return echo.NewHTTPError(http.StatusBadRequest) - } - - switch req.QuestionType { - case "Text": - // 正規表現のチェック - if _, err := regexp.Compile(req.RegexPattern); err != nil { - c.Logger().Info("invalid regex pattern: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest) - } - case "Number": - // 数字か,min<=maxになってるか - if err := q.CheckNumberValid(req.MinBound, req.MaxBound); err != nil { - c.Logger().Info("invalid number: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err) - } - } - - lastID, err := q.InsertQuestion(c.Request().Context(), questionnaireID, req.PageNum, req.QuestionNum, req.QuestionType, req.Body, req.IsRequired) - if err != nil { - c.Logger().Errorf("failed to insert question: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - switch req.QuestionType { - case "MultipleChoice", "Checkbox", "Dropdown": - for i, v := range req.Options { - if err := q.InsertOption(c.Request().Context(), lastID, i+1, v); err != nil { - c.Logger().Errorf("failed to insert option: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - } - case "LinearScale": - if err := q.InsertScaleLabel(c.Request().Context(), lastID, - model.ScaleLabels{ - ScaleLabelLeft: req.ScaleLabelLeft, - ScaleLabelRight: req.ScaleLabelRight, - ScaleMax: req.ScaleMax, - ScaleMin: req.ScaleMin, - }); err != nil { - c.Logger().Errorf("failed to insert scale label: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - case "Text", "Number": - if err := q.InsertValidation(c.Request().Context(), lastID, - model.Validations{ - RegexPattern: req.RegexPattern, - MinBound: req.MinBound, - MaxBound: req.MaxBound, - }); err != nil { - c.Logger().Errorf("failed to insert validation: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - } - - return c.JSON(http.StatusCreated, map[string]interface{}{ - "questionID": int(lastID), - "question_type": req.QuestionType, - "question_num": req.QuestionNum, - "page_num": req.PageNum, - "body": req.Body, - "is_required": req.IsRequired, - "options": req.Options, - "scale_label_right": req.ScaleLabelRight, - "scale_label_left": req.ScaleLabelLeft, - "scale_max": req.ScaleMax, - "scale_min": req.ScaleMin, - "regex_pattern": req.RegexPattern, - "min_bound": req.MinBound, - "max_bound": req.MaxBound, - }) -} - -// EditQuestionnaire PATCH /questionnaires/:questionnaireID -func (q *Questionnaire) EditQuestionnaire(c echo.Context) error { - questionnaireID, err := getQuestionnaireID(c) - if err != nil { - c.Logger().Errorf("failed to get questionnaireID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError) - } - - req := PostAndEditQuestionnaireRequest{} - - err = c.Bind(&req) - if err != nil { - c.Logger().Infof("failed to bind PostAndEditQuestionnaireRequest: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest) - } - - validate, err := getValidator(c) - if err != nil { - c.Logger().Errorf("failed to get validator: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError) - } - - err = validate.StructCtx(c.Request().Context(), req) - if err != nil { - c.Logger().Infof("failed to validate: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - err = q.ITransaction.Do(c.Request().Context(), nil, func(ctx context.Context) error { - err = q.UpdateQuestionnaire(ctx, req.Title, req.Description, req.ResTimeLimit, req.ResSharedTo, questionnaireID) - if err != nil && !errors.Is(err, model.ErrNoRecordUpdated) { - c.Logger().Errorf("failed to update questionnaire: %+v", err) - return err - } - - err = q.DeleteTargets(ctx, questionnaireID) - if err != nil { - c.Logger().Errorf("failed to delete targets: %+v", err) - return err - } - - err = q.InsertTargets(ctx, questionnaireID, req.Targets) - if err != nil { - c.Logger().Errorf("failed to insert targets: %+v", err) - return err - } - - err = q.DeleteAdministrators(ctx, questionnaireID) - if err != nil { - c.Logger().Errorf("failed to delete administrators: %+v", err) - return err - } - - err = q.InsertAdministrators(ctx, questionnaireID, req.Administrators) - if err != nil { - c.Logger().Errorf("failed to insert administrators: %+v", err) - return err - } - - return nil - }) - if err != nil { - var httpError *echo.HTTPError - if errors.As(err, &httpError) { - return httpError - } - - c.Logger().Errorf("failed to update questionnaire: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, "failed to update a questionnaire") - } - - return c.NoContent(http.StatusOK) -} - -// DeleteQuestionnaire DELETE /questionnaires/:questionnaireID -func (q *Questionnaire) DeleteQuestionnaire(c echo.Context) error { - questionnaireID, err := getQuestionnaireID(c) - if err != nil { - c.Logger().Errorf("failed to get questionnaireID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError) - } - - err = q.ITransaction.Do(c.Request().Context(), nil, func(ctx context.Context) error { - err = q.IQuestionnaire.DeleteQuestionnaire(c.Request().Context(), questionnaireID) - if err != nil { - c.Logger().Errorf("failed to delete questionnaire: %+v", err) - return err - } - - err = q.DeleteTargets(c.Request().Context(), questionnaireID) - if err != nil { - c.Logger().Errorf("failed to delete targets: %+v", err) - return err - } - - err = q.DeleteAdministrators(c.Request().Context(), questionnaireID) - if err != nil { - c.Logger().Errorf("failed to delete administrators: %+v", err) - return err - } - - return nil - }) - if err != nil { - var httpError *echo.HTTPError - if errors.As(err, &httpError) { - return httpError - } - - c.Logger().Errorf("failed to delete questionnaire: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, "failed to delete a questionnaire") - } - - return c.NoContent(http.StatusOK) -} - -// GetQuestions GET /questionnaires/:questionnaireID/questions -func (q *Questionnaire) GetQuestions(c echo.Context) error { - strQuestionnaireID := c.Param("questionnaireID") - questionnaireID, err := strconv.Atoi(strQuestionnaireID) - if err != nil { - c.Logger().Infof("failed to convert questionnaireID to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid questionnaireID:%s(error: %w)", strQuestionnaireID, err)) - } - - allquestions, err := q.IQuestion.GetQuestions(c.Request().Context(), questionnaireID) - if err != nil { - c.Logger().Errorf("failed to get questions: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - if len(allquestions) == 0 { - c.Logger().Info("no questions") - return echo.NewHTTPError(http.StatusNotFound) - } - - type questionInfo struct { - QuestionID int `json:"questionID"` - PageNum int `json:"page_num"` - QuestionNum int `json:"question_num"` - QuestionType string `json:"question_type"` - Body string `json:"body"` - IsRequired bool `json:"is_required"` - CreatedAt string `json:"created_at"` - Options []string `json:"options"` - ScaleLabelRight string `json:"scale_label_right"` - ScaleLabelLeft string `json:"scale_label_left"` - ScaleMin int `json:"scale_min"` - ScaleMax int `json:"scale_max"` - RegexPattern string `json:"regex_pattern"` - MinBound string `json:"min_bound"` - MaxBound string `json:"max_bound"` - } - var ret []questionInfo - - optionIDs := []int{} - scaleLabelIDs := []int{} - validationIDs := []int{} - for _, question := range allquestions { - switch question.Type { - case "MultipleChoice", "Checkbox", "Dropdown": - optionIDs = append(optionIDs, question.ID) - case "LinearScale": - scaleLabelIDs = append(scaleLabelIDs, question.ID) - case "Text", "Number": - validationIDs = append(validationIDs, question.ID) - } - } - - options, err := q.GetOptions(c.Request().Context(), optionIDs) - if err != nil { - c.Logger().Errorf("failed to get options: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - optionMap := make(map[int][]string, len(options)) - for _, option := range options { - optionMap[option.QuestionID] = append(optionMap[option.QuestionID], option.Body) - } - - scaleLabels, err := q.GetScaleLabels(c.Request().Context(), scaleLabelIDs) - if err != nil { - c.Logger().Errorf("failed to get scale labels: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - scaleLabelMap := make(map[int]model.ScaleLabels, len(scaleLabels)) - for _, label := range scaleLabels { - scaleLabelMap[label.QuestionID] = label - } - - validations, err := q.GetValidations(c.Request().Context(), validationIDs) - if err != nil { - c.Logger().Errorf("failed to get validations: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - validationMap := make(map[int]model.Validations, len(validations)) - for _, validation := range validations { - validationMap[validation.QuestionID] = validation - } - - for _, v := range allquestions { - options := []string{} - scalelabel := model.ScaleLabels{} - validation := model.Validations{} - switch v.Type { - case "MultipleChoice", "Checkbox", "Dropdown": - var ok bool - options, ok = optionMap[v.ID] - if !ok { - options = []string{} - } - case "LinearScale": - var ok bool - scalelabel, ok = scaleLabelMap[v.ID] - if !ok { - scalelabel = model.ScaleLabels{} - } - case "Text", "Number": - var ok bool - validation, ok = validationMap[v.ID] - if !ok { - validation = model.Validations{} - } - } - - ret = append(ret, - questionInfo{ - QuestionID: v.ID, - PageNum: v.PageNum, - QuestionNum: v.QuestionNum, - QuestionType: v.Type, - Body: v.Body, - IsRequired: v.IsRequired, - CreatedAt: v.CreatedAt.Format(time.RFC3339), - Options: options, - ScaleLabelRight: scalelabel.ScaleLabelRight, - ScaleLabelLeft: scalelabel.ScaleLabelLeft, - ScaleMin: scalelabel.ScaleMin, - ScaleMax: scalelabel.ScaleMax, - RegexPattern: validation.RegexPattern, - MinBound: validation.MinBound, - MaxBound: validation.MaxBound, - }, - ) - } - - return c.JSON(http.StatusOK, ret) -} - -func createQuestionnaireMessage(questionnaireID int, title string, description string, administrators []string, resTimeLimit null.Time, targets []string) string { - var resTimeLimitText string - if resTimeLimit.Valid { - resTimeLimitText = resTimeLimit.Time.Local().Format("2006/01/02 15:04") - } else { - resTimeLimitText = "なし" - } - - var targetsMentionText string - if len(targets) == 0 { - targetsMentionText = "なし" - } else { - targetsMentionText = "@" + strings.Join(targets, " @") - } - - return fmt.Sprintf( - `### アンケート『[%s](https://anke-to.trap.jp/questionnaires/%d)』が作成されました -#### 管理者 -%s -#### 説明 -%s -#### 回答期限 -%s -#### 対象者 -%s -#### 回答リンク -https://anke-to.trap.jp/responses/new/%d`, - title, - questionnaireID, - strings.Join(administrators, ","), - description, - resTimeLimitText, - targetsMentionText, - questionnaireID, - ) -} diff --git a/router/questionnaires_test.go b/router/questionnaires_test.go deleted file mode 100644 index 8e934895..00000000 --- a/router/questionnaires_test.go +++ /dev/null @@ -1,1946 +0,0 @@ -package router - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/http/httptest" - "strconv" - "strings" - "testing" - "time" - - "github.com/go-playground/validator/v10" - "github.com/golang/mock/gomock" - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" - "github.com/traPtitech/anke-to/model" - "github.com/traPtitech/anke-to/model/mock_model" - "github.com/traPtitech/anke-to/traq/mock_traq" - "gopkg.in/guregu/null.v4" -) - -func TestPostAndEditQuestionnaireValidate(t *testing.T) { - tests := []struct { - description string - request *PostAndEditQuestionnaireRequest - isErr bool - }{ - { - description: "旧クライアントの一般的なリクエストなのでエラーなし", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - }, - { - description: "タイトルが空なのでエラー", - request: &PostAndEditQuestionnaireRequest{ - Title: "", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - isErr: true, - }, - { - description: "タイトルが50文字なのでエラーなし", - request: &PostAndEditQuestionnaireRequest{ - Title: "アイウエオアイウエオアイウエオアイウエオアイウエオアイウエオアイウエオアイウエオアイウエオアイウエオ", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - }, - { - description: "タイトルが50文字を超えるのでエラー", - request: &PostAndEditQuestionnaireRequest{ - Title: "アイウエオアイウエオアイウエオアイウエオアイウエオアイウエオアイウエオアイウエオアイウエオアイウエオア", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - isErr: true, - }, - { - description: "descriptionが空でもエラーなし", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - }, - { - description: "resTimeLimitが設定されていてもエラーなし", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Now(), true), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - }, - { - description: "resSharedToがadministratorsでもエラーなし", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "administrators", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - }, - { - description: "resSharedToがrespondentsでもエラーなし", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "respondents", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - }, - { - description: "resSharedToがadministrators、respondents、publicのいずれでもないのでエラー", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "test", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - isErr: true, - }, - { - description: "targetがnullでもエラーなし", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: nil, - Administrators: []string{"mazrean"}, - }, - }, - { - description: "targetが32文字でもエラーなし", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{"01234567890123456789012345678901"}, - Administrators: []string{"mazrean"}, - }, - }, - { - description: "targetが32文字を超えるのでエラー", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{"012345678901234567890123456789012"}, - Administrators: []string{"mazrean"}, - }, - isErr: true, - }, - { - description: "administratorsがいないのでエラー", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{"01234567890123456789012345678901"}, - Administrators: []string{}, - }, - isErr: true, - }, - { - description: "administratorsがnullなのでエラー", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: nil, - }, - isErr: true, - }, - { - description: "administratorsが32文字でもエラーなし", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"01234567890123456789012345678901"}, - }, - }, - { - description: "administratorsが32文字を超えるのでエラー", - request: &PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"012345678901234567890123456789012"}, - }, - isErr: true, - }, - } - - for _, test := range tests { - validate := validator.New() - - t.Run(test.description, func(t *testing.T) { - err := validate.Struct(test.request) - - if test.isErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestGetQuestionnaireValidate(t *testing.T) { - t.Parallel() - - tests := []struct { - description string - request *GetQuestionnairesQueryParam - isErr bool - }{ - { - description: "一般的なQueryParameterなのでエラーなし", - request: &GetQuestionnairesQueryParam{ - Sort: "created_at", - Search: "a", - Page: "2", - Nontargeted: "true", - }, - }, - { - description: "Sortが-created_atでもエラーなし", - request: &GetQuestionnairesQueryParam{ - Sort: "-created_at", - Search: "a", - Page: "2", - Nontargeted: "true", - }, - }, - { - description: "Sortがtitleでもエラーなし", - request: &GetQuestionnairesQueryParam{ - Sort: "title", - Search: "a", - Page: "2", - Nontargeted: "true", - }, - }, - { - description: "Sortが-titleでもエラーなし", - request: &GetQuestionnairesQueryParam{ - Sort: "-title", - Search: "a", - Page: "2", - Nontargeted: "true", - }, - }, - { - description: "Sortがmodified_atでもエラーなし", - request: &GetQuestionnairesQueryParam{ - Sort: "modified_at", - Search: "a", - Page: "2", - Nontargeted: "true", - }, - }, - { - description: "Sortが-modified_atでもエラーなし", - request: &GetQuestionnairesQueryParam{ - Sort: "-modified_at", - Search: "a", - Page: "2", - Nontargeted: "true", - }, - }, - { - description: "Nontargetedをfalseにしてもエラーなし", - request: &GetQuestionnairesQueryParam{ - Sort: "created_at", - Search: "a", - Page: "2", - Nontargeted: "false", - }, - }, - { - description: "Sortを空文字にしてもエラーなし", - request: &GetQuestionnairesQueryParam{ - Sort: "", - Search: "a", - Page: "2", - Nontargeted: "true", - }, - }, - { - description: "Searchを空文字にしてもエラーなし", - request: &GetQuestionnairesQueryParam{ - Sort: "created_at", - Search: "", - Page: "2", - Nontargeted: "true", - }, - }, - { - description: "Pageを空文字にしてもエラーなし", - request: &GetQuestionnairesQueryParam{ - Sort: "created_at", - Search: "a", - Page: "", - Nontargeted: "true", - }, - }, - { - description: "Nontargetedを空文字にしてもエラーなし", - request: &GetQuestionnairesQueryParam{ - Sort: "created_at", - Search: "a", - Page: "2", - Nontargeted: "", - }, - }, - { - description: "Pageが数字ではないのでエラー", - request: &GetQuestionnairesQueryParam{ - Sort: "created_at", - Search: "a", - Page: "xx", - Nontargeted: "true", - }, - isErr: true, - }, - { - description: "Nontargetedがbool値ではないのでエラー", - request: &GetQuestionnairesQueryParam{ - Sort: "created_at", - Search: "a", - Page: "2", - Nontargeted: "arupaka", - }, - isErr: true, - }, - } - for _, test := range tests { - validate := validator.New() - - t.Run(test.description, func(t *testing.T) { - err := validate.Struct(test.request) - if test.isErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestPostQuestionnaire(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockTarget := mock_model.NewMockITarget(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - mockQuestion := mock_model.NewMockIQuestion(ctrl) - mockOption := mock_model.NewMockIOption(ctrl) - mockScaleLabel := mock_model.NewMockIScaleLabel(ctrl) - mockValidation := mock_model.NewMockIValidation(ctrl) - mockTransaction := &model.MockTransaction{} - mockWebhook := mock_traq.NewMockIWebhook(ctrl) - - questionnaire := NewQuestionnaire( - mockQuestionnaire, - mockTarget, - mockAdministrator, - mockQuestion, - mockOption, - mockScaleLabel, - mockValidation, - mockTransaction, - mockWebhook, - ) - - type expect struct { - statusCode int - } - type test struct { - description string - invalidRequest bool - request PostAndEditQuestionnaireRequest - ExecutesCreation bool - questionnaireID int - InsertQuestionnaireError error - InsertTargetsError error - InsertAdministratorsError error - PostMessageError error - expect - } - - testCases := []test{ - { - description: "リクエストの形式が誤っているので400", - invalidRequest: true, - expect: expect{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "validationで落ちるので400", - request: PostAndEditQuestionnaireRequest{}, - expect: expect{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "resTimeLimitが誤っているので400", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Now().Add(-24*time.Hour), true), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - expect: expect{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "InsertQuestionnaireがエラーなので500", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - InsertQuestionnaireError: errors.New("InsertQuestionnaireError"), - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "InsertTargetsがエラーなので500", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 1, - InsertTargetsError: errors.New("InsertTargetsError"), - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "InsertAdministratorsがエラーなので500", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 1, - InsertAdministratorsError: errors.New("InsertAdministratorsError"), - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "PostMessageがエラーなので500", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 1, - PostMessageError: errors.New("PostMessageError"), - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "一般的なリクエストなので201", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 1, - expect: expect{ - statusCode: http.StatusCreated, - }, - }, - { - description: "questionnaireIDが0でも201", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 0, - expect: expect{ - statusCode: http.StatusCreated, - }, - }, - { - description: "回答期限が設定されていてもでも201", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Now().Add(24*time.Hour), true), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 1, - expect: expect{ - statusCode: http.StatusCreated, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.description, func(t *testing.T) { - var request io.Reader - if testCase.invalidRequest { - request = strings.NewReader("test") - } else { - buf := bytes.NewBuffer(nil) - err := json.NewEncoder(buf).Encode(testCase.request) - if err != nil { - t.Errorf("failed to encode request: %v", err) - } - - request = buf - } - - e := echo.New() - req := httptest.NewRequest(http.MethodPost, "/questionnaires", request) - rec := httptest.NewRecorder() - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - c := e.NewContext(req, rec) - - c.Set(validatorKey, validator.New()) - - if testCase.ExecutesCreation { - // 時刻は完全一致しないためその対応 - var mockTimeLimit interface{} - if testCase.request.ResTimeLimit.Valid { - mockTimeLimit = gomock.Any() - } else { - mockTimeLimit = testCase.request.ResTimeLimit - } - - mockQuestionnaire. - EXPECT(). - InsertQuestionnaire( - c.Request().Context(), - testCase.request.Title, - testCase.request.Description, - mockTimeLimit, - testCase.request.ResSharedTo, - ). - Return(testCase.questionnaireID, testCase.InsertQuestionnaireError) - - if testCase.InsertQuestionnaireError == nil { - mockTarget. - EXPECT(). - InsertTargets( - c.Request().Context(), - testCase.questionnaireID, - testCase.request.Targets, - ). - Return(testCase.InsertTargetsError) - - if testCase.InsertTargetsError == nil { - mockAdministrator. - EXPECT(). - InsertAdministrators( - c.Request().Context(), - testCase.questionnaireID, - testCase.request.Administrators, - ). - Return(testCase.InsertAdministratorsError) - - if testCase.InsertAdministratorsError == nil { - mockWebhook. - EXPECT(). - PostMessage(gomock.Any()). - Return(testCase.PostMessageError) - } - } - } - } - - e.HTTPErrorHandler(questionnaire.PostQuestionnaire(c), c) - - assert.Equal(t, testCase.expect.statusCode, rec.Code, "status code") - - if testCase.expect.statusCode == http.StatusCreated { - var questionnaire map[string]interface{} - err := json.NewDecoder(rec.Body).Decode(&questionnaire) - if err != nil { - t.Errorf("failed to decode response body: %v", err) - } - - assert.Equal(t, float64(testCase.questionnaireID), questionnaire["questionnaireID"], "questionnaireID") - assert.Equal(t, testCase.request.Title, questionnaire["title"], "title") - assert.Equal(t, testCase.request.Description, questionnaire["description"], "description") - if testCase.request.ResTimeLimit.Valid { - strResTimeLimit, ok := questionnaire["res_time_limit"].(string) - assert.True(t, ok, "res_time_limit convert") - resTimeLimit, err := time.Parse(time.RFC3339, strResTimeLimit) - assert.NoError(t, err, "res_time_limit parse") - - assert.WithinDuration(t, testCase.request.ResTimeLimit.Time, resTimeLimit, 2*time.Second, "resTimeLimit") - } else { - assert.Nil(t, questionnaire["res_time_limit"], "resTimeLimit nil") - } - assert.Equal(t, testCase.request.ResSharedTo, questionnaire["res_shared_to"], "resSharedTo") - - strCreatedAt, ok := questionnaire["created_at"].(string) - assert.True(t, ok, "created_at convert") - createdAt, err := time.Parse(time.RFC3339, strCreatedAt) - assert.NoError(t, err, "created_at parse") - assert.WithinDuration(t, time.Now(), createdAt, time.Second, "created_at") - - strModifiedAt, ok := questionnaire["modified_at"].(string) - assert.True(t, ok, "modified_at convert") - modifiedAt, err := time.Parse(time.RFC3339, strModifiedAt) - assert.NoError(t, err, "modified_at parse") - assert.WithinDuration(t, time.Now(), modifiedAt, time.Second, "modified_at") - - assert.ElementsMatch(t, testCase.request.Targets, questionnaire["targets"], "targets") - assert.ElementsMatch(t, testCase.request.Administrators, questionnaire["administrators"], "administrators") - } - }) - } -} - -func TestPostQuestionByQuestionnaireID(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockTarget := mock_model.NewMockITarget(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - mockQuestion := mock_model.NewMockIQuestion(ctrl) - mockScaleLabel := mock_model.NewMockIScaleLabel(ctrl) - mockOption := mock_model.NewMockIOption(ctrl) - mockValidation := mock_model.NewMockIValidation(ctrl) - mockTransaction := &model.MockTransaction{} - mockWebhook := mock_traq.NewMockIWebhook(ctrl) - - questionnaire := NewQuestionnaire( - mockQuestionnaire, - mockTarget, - mockAdministrator, - mockQuestion, - mockOption, - mockScaleLabel, - mockValidation, - mockTransaction, - mockWebhook, - ) - - type expect struct { - statusCode int - } - type test struct { - description string - invalidRequest bool - request PostAndEditQuestionRequest - ExecutesCreation bool - ExecutesCheckQuestionNum bool - questionID int - questionnaireID string - validator string - questionNumExists bool - InsertQuestionError error - InsertOptionError error - InsertValidationError error - InsertScaleLabelError error - CheckNumberValid error - CheckQuestionNumError error - expect - } - testCases := []test{ - { - description: "一般的なリクエストなので201", - invalidRequest: false, - request: PostAndEditQuestionRequest{ - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka", "mazrean"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - }, - ExecutesCreation: true, - ExecutesCheckQuestionNum: true, - questionID: 1, - questionnaireID: "1", - expect: expect{ - statusCode: http.StatusCreated, - }, - }, - { - description: "questionIDが0でも201", - request: PostAndEditQuestionRequest{ - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka", "mazrean"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - }, - ExecutesCreation: true, - ExecutesCheckQuestionNum: true, - questionID: 0, - questionnaireID: "1", - expect: expect{ - statusCode: http.StatusCreated, - }, - }, - { - description: "questionnaireIDがstringでも201", - request: PostAndEditQuestionRequest{ - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka", "mazrean"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - }, - questionnaireID: "1", - ExecutesCreation: true, - ExecutesCheckQuestionNum: true, - questionID: 1, - expect: expect{ - statusCode: http.StatusCreated, - }, - }, - { - description: "QuestionTypeがMultipleChoiceでも201", - request: PostAndEditQuestionRequest{ - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka", "mazrean"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - }, - ExecutesCreation: true, - ExecutesCheckQuestionNum: true, - questionID: 1, - questionnaireID: "1", - expect: expect{ - statusCode: http.StatusCreated, - }, - }, - { - description: "QuestionTypeがLinearScaleでも201", - request: PostAndEditQuestionRequest{ - QuestionType: "LinearScale", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka", "mazrean"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - }, - ExecutesCreation: true, - ExecutesCheckQuestionNum: true, - questionID: 1, - questionnaireID: "1", - expect: expect{ - statusCode: http.StatusCreated, - }, - }, - { - description: "QuestionTypeがNumberでも201", - request: PostAndEditQuestionRequest{ - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka", "mazrean"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - }, - ExecutesCreation: true, - ExecutesCheckQuestionNum: true, - questionID: 1, - questionnaireID: "1", - expect: expect{ - statusCode: http.StatusCreated, - }, - }, - { - description: "QuestionTypeが存在しないものは400", - request: PostAndEditQuestionRequest{ - QuestionType: "aaa", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka", "mazrean"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - }, - InsertQuestionError: errors.New("InsertQuestionError"), - ExecutesCreation: false, - ExecutesCheckQuestionNum: false, - questionID: 1, - questionnaireID: "1", - expect: expect{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "InsertValidationがエラーで500", - request: PostAndEditQuestionRequest{ - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka", "mazrean"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - }, - InsertValidationError: errors.New("InsertValidationError"), - ExecutesCreation: true, - ExecutesCheckQuestionNum: true, - questionID: 1, - questionnaireID: "1", - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "CheckNumberValidがエラーで500", - request: PostAndEditQuestionRequest{ - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka", "mazrean"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - }, - CheckNumberValid: errors.New("CheckNumberValidError"), - ExecutesCreation: false, - ExecutesCheckQuestionNum: true, - questionID: 1, - questionnaireID: "1", - expect: expect{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "InsertQuestionがエラーで500", - request: PostAndEditQuestionRequest{ - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka", "mazrean"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - }, - InsertQuestionError: errors.New("InsertQuestionError"), - ExecutesCreation: true, - ExecutesCheckQuestionNum: true, - questionID: 1, - questionnaireID: "1", - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "InsertScaleLabelErrorがエラーで500", - request: PostAndEditQuestionRequest{ - QuestionType: "LinearScale", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka", "mazrean"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - }, - InsertScaleLabelError: errors.New("InsertScaleLabelError"), - ExecutesCreation: true, - ExecutesCheckQuestionNum: true, - questionID: 1, - questionnaireID: "1", - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "InsertOptionErrorがエラーで500", - request: PostAndEditQuestionRequest{ - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - }, - InsertOptionError: errors.New("InsertOptionError"), - ExecutesCreation: true, - ExecutesCheckQuestionNum: true, - questionID: 1, - questionnaireID: "1", - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "questionnaireIDが数値ではないので400", - request: PostAndEditQuestionRequest{}, - questionnaireID: "arupaka", - ExecutesCreation: false, - ExecutesCheckQuestionNum: false, - expect: expect{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "validatorが\"validator\"ではないので500", - request: PostAndEditQuestionRequest{}, - validator: "arupaka", - questionnaireID: "1", - ExecutesCreation: false, - ExecutesCheckQuestionNum: false, - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "正規表現が間違っているので400", - request: PostAndEditQuestionRequest{ - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"arupaka"}, - ScaleLabelRight: "arupaka", - ScaleLabelLeft: "xxarupakaxx", - ScaleMin: 1, - ScaleMax: 2, - RegexPattern: "[[", - MinBound: "0", - MaxBound: "10", - }, - InsertQuestionError: errors.New("正規表現が間違っています"), - ExecutesCreation: false, - ExecutesCheckQuestionNum: false, - expect: expect{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "リクエストの形式が異なっているので400", - invalidRequest: true, - expect: expect{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "validation(妥当性確認)で落ちるので400", - request: PostAndEditQuestionRequest{}, - expect: expect{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "CheckQuestionNumがエラーで500", - request: PostAndEditQuestionRequest{ - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - MinBound: "0", - MaxBound: "10", - }, - ExecutesCheckQuestionNum: true, - CheckQuestionNumError: errors.New("CheckQuestionNumError"), - questionID: 1, - questionnaireID: "1", - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "questionNumは重複できないので400", - request: PostAndEditQuestionRequest{ - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - MinBound: "0", - MaxBound: "10", - }, - ExecutesCheckQuestionNum: true, - questionID: 1, - questionnaireID: "1", - questionNumExists: true, - expect: expect{ - statusCode: http.StatusBadRequest, - }, - }, - } - - for _, test := range testCases { - t.Run(test.description, func(t *testing.T) { - var request io.Reader - if test.invalidRequest { - request = strings.NewReader("test") - } else { - buf := bytes.NewBuffer(nil) - err := json.NewEncoder(buf).Encode(test.request) - if err != nil { - t.Errorf("failed to encode request: %v", err) - } - - request = buf - } - - e := echo.New() - var req *http.Request - intQuestionnaireID, err := strconv.Atoi(test.questionnaireID) - if err != nil { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/questionnaires/%s/questions", test.questionnaireID), request) - } else { - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/questionnaires/%d/questions", intQuestionnaireID), request) - } - - rec := httptest.NewRecorder() - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - c := e.NewContext(req, rec) - c.SetParamNames("questionnaireID") - - c.SetParamValues(test.questionnaireID) - - c.Set(questionnaireIDKey, test.request.QuestionnaireID) - if test.validator != "" { - c.Set(test.validator, validator.New()) - } else { - c.Set(validatorKey, validator.New()) - } - - if test.ExecutesCheckQuestionNum { - mockQuestion. - EXPECT(). - CheckQuestionNum(c.Request().Context(), intQuestionnaireID, test.request.QuestionNum). - Return(test.questionNumExists, test.CheckQuestionNumError) - } - if test.ExecutesCreation { - mockQuestion. - EXPECT(). - InsertQuestion(c.Request().Context(), intQuestionnaireID, test.request.PageNum, test.request.QuestionNum, test.request.QuestionType, test.request.Body, test.request.IsRequired). - Return(test.questionID, test.InsertQuestionError) - } - if test.InsertQuestionError == nil && test.request.QuestionType == "LinearScale" { - mockScaleLabel. - EXPECT(). - InsertScaleLabel(c.Request().Context(), test.questionID, model.ScaleLabels{ - ScaleLabelRight: test.request.ScaleLabelRight, - ScaleLabelLeft: test.request.ScaleLabelLeft, - ScaleMin: test.request.ScaleMin, - ScaleMax: test.request.ScaleMax, - }). - Return(test.InsertScaleLabelError) - } - if test.InsertQuestionError == nil && (test.request.QuestionType == "MultipleChoice" || test.request.QuestionType == "Checkbox" || test.request.QuestionType == "Dropdown") { - for i, option := range test.request.Options { - mockOption. - EXPECT(). - InsertOption(c.Request().Context(), test.questionID, i+1, option). - Return(test.InsertOptionError) - } - } - if test.request.QuestionType == "Number" { - mockValidation. - EXPECT(). - CheckNumberValid(test.request.MinBound, test.request.MaxBound). - Return(test.CheckNumberValid) - } - if test.ExecutesCreation && test.InsertQuestionError == nil && test.CheckNumberValid == nil && (test.request.QuestionType == "Text" || test.request.QuestionType == "Number") { - mockValidation. - EXPECT(). - InsertValidation(c.Request().Context(), test.questionID, model.Validations{ - RegexPattern: test.request.RegexPattern, - MinBound: test.request.MinBound, - MaxBound: test.request.MaxBound, - }). - Return(test.InsertValidationError) - } - - e.HTTPErrorHandler(questionnaire.PostQuestionByQuestionnaireID(c), c) - - assert.Equal(t, test.expect.statusCode, rec.Code, "status code") - - if test.expect.statusCode == http.StatusCreated { - var question map[string]interface{} - err := json.NewDecoder(rec.Body).Decode(&question) - if err != nil { - t.Errorf("failed to decode response body: %v", err) - } - - assert.Equal(t, float64(test.questionID), question["questionID"], "questionID") - assert.Equal(t, test.request.QuestionType, question["question_type"], "question_type") - assert.Equal(t, float64(test.request.QuestionNum), question["question_num"], "question_num") - assert.Equal(t, float64(test.request.PageNum), question["page_num"], "page_num") - assert.Equal(t, test.request.Body, question["body"], "body") - assert.Equal(t, test.request.IsRequired, question["is_required"], "is_required") - assert.ElementsMatch(t, test.request.Options, question["options"], "options") - assert.Equal(t, test.request.ScaleLabelRight, question["scale_label_right"], "scale_label_right") - assert.Equal(t, test.request.ScaleLabelLeft, question["scale_label_left"], "scale_label_left") - assert.Equal(t, float64(test.request.ScaleMax), question["scale_max"], "scale_max") - assert.Equal(t, float64(test.request.ScaleMin), question["scale_min"], "scale_min") - assert.Equal(t, test.request.RegexPattern, question["regex_pattern"], "regex_pattern") - assert.Equal(t, test.request.MinBound, question["min_bound"], "min_bound") - assert.Equal(t, test.request.MaxBound, question["max_bound"], "max_bound") - - } - }) - } -} - -func TestEditQuestionnaire(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockTarget := mock_model.NewMockITarget(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - mockQuestion := mock_model.NewMockIQuestion(ctrl) - mockOption := mock_model.NewMockIOption(ctrl) - mockScaleLabel := mock_model.NewMockIScaleLabel(ctrl) - mockValidation := mock_model.NewMockIValidation(ctrl) - mockTransaction := &model.MockTransaction{} - mockWebhook := mock_traq.NewMockIWebhook(ctrl) - - questionnaire := NewQuestionnaire( - mockQuestionnaire, - mockTarget, - mockAdministrator, - mockQuestion, - mockOption, - mockScaleLabel, - mockValidation, - mockTransaction, - mockWebhook, - ) - - type expect struct { - statusCode int - } - type test struct { - description string - invalidRequest bool - request PostAndEditQuestionnaireRequest - ExecutesCreation bool - questionnaireID int - InsertQuestionnaireError error - DeleteTargetsError error - InsertTargetsError error - DeleteAdministratorsError error - InsertAdministratorsError error - PostMessageError error - expect - } - - testCases := []test{ - { - description: "リクエストの形式が誤っているので400", - invalidRequest: true, - expect: expect{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "validationで落ちるので400", - request: PostAndEditQuestionnaireRequest{}, - expect: expect{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "InsertQuestionnaireがエラーなので500", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - InsertQuestionnaireError: errors.New("InsertQuestionnaireError"), - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "DeleteTargetsがエラーなので500", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 1, - DeleteTargetsError: errors.New("DeleteTargetsError"), - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "InsertTargetsがエラーなので500", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 1, - InsertTargetsError: errors.New("InsertTargetsError"), - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "DeleteAdministratorsがエラーなので500", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 1, - DeleteAdministratorsError: errors.New("DeleteAdministratorsError"), - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "InsertAdministratorsがエラーなので500", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 1, - InsertAdministratorsError: errors.New("InsertAdministratorsError"), - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "一般的なリクエストなので200", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Time{}, false), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 1, - expect: expect{ - statusCode: http.StatusOK, - }, - }, - { - description: "resTimeLimitが現在時刻より前でも200", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Now().Add(-24*time.Hour), true), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 1, - expect: expect{ - statusCode: http.StatusOK, - }, - }, - { - description: "回答期限が設定されていてもでも200", - request: PostAndEditQuestionnaireRequest{ - Title: "第1回集会らん☆ぷろ募集アンケート", - Description: "第1回集会らん☆ぷろ参加者募集", - ResTimeLimit: null.NewTime(time.Now().Add(24*time.Hour), true), - ResSharedTo: "public", - Targets: []string{}, - Administrators: []string{"mazrean"}, - }, - ExecutesCreation: true, - questionnaireID: 1, - expect: expect{ - statusCode: http.StatusOK, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.description, func(t *testing.T) { - var request io.Reader - if testCase.invalidRequest { - request = strings.NewReader("test") - } else { - buf := bytes.NewBuffer(nil) - err := json.NewEncoder(buf).Encode(testCase.request) - if err != nil { - t.Errorf("failed to encode request: %v", err) - } - - request = buf - } - - e := echo.New() - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/questionnaires/%d", testCase.questionnaireID), request) - rec := httptest.NewRecorder() - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - c := e.NewContext(req, rec) - c.SetParamNames("questionnaireID") - c.SetParamValues(strconv.Itoa(testCase.questionnaireID)) - - c.Set(questionnaireIDKey, testCase.questionnaireID) - c.Set(validatorKey, validator.New()) - - if testCase.ExecutesCreation { - // 時刻は完全一致しないためその対応 - var mockTimeLimit interface{} - if testCase.request.ResTimeLimit.Valid { - mockTimeLimit = gomock.Any() - } else { - mockTimeLimit = testCase.request.ResTimeLimit - } - - mockQuestionnaire. - EXPECT(). - UpdateQuestionnaire( - c.Request().Context(), - testCase.request.Title, - testCase.request.Description, - mockTimeLimit, - testCase.request.ResSharedTo, - testCase.questionnaireID, - ). - Return(testCase.InsertQuestionnaireError) - - if testCase.InsertQuestionnaireError == nil { - mockTarget. - EXPECT(). - DeleteTargets( - c.Request().Context(), - testCase.questionnaireID, - ). - Return(testCase.DeleteTargetsError) - - if testCase.DeleteTargetsError == nil { - mockTarget. - EXPECT(). - InsertTargets( - c.Request().Context(), - testCase.questionnaireID, - testCase.request.Targets, - ). - Return(testCase.InsertTargetsError) - - if testCase.InsertTargetsError == nil { - mockAdministrator. - EXPECT(). - DeleteAdministrators( - c.Request().Context(), - testCase.questionnaireID, - ). - Return(testCase.DeleteAdministratorsError) - - if testCase.DeleteAdministratorsError == nil { - mockAdministrator. - EXPECT(). - InsertAdministrators( - c.Request().Context(), - testCase.questionnaireID, - testCase.request.Administrators, - ). - Return(testCase.InsertAdministratorsError) - } - } - } - } - } - - e.HTTPErrorHandler(questionnaire.EditQuestionnaire(c), c) - - assert.Equal(t, testCase.expect.statusCode, rec.Code, "status code") - }) - } -} - -func TestDeleteQuestionnaire(t *testing.T) { - t.Parallel() - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockTarget := mock_model.NewMockITarget(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - mockQuestion := mock_model.NewMockIQuestion(ctrl) - mockOption := mock_model.NewMockIOption(ctrl) - mockScaleLabel := mock_model.NewMockIScaleLabel(ctrl) - mockValidation := mock_model.NewMockIValidation(ctrl) - mockTransaction := &model.MockTransaction{} - mockWebhook := mock_traq.NewMockIWebhook(ctrl) - - questionnaire := NewQuestionnaire( - mockQuestionnaire, - mockTarget, - mockAdministrator, - mockQuestion, - mockOption, - mockScaleLabel, - mockValidation, - mockTransaction, - mockWebhook, - ) - - type expect struct { - statusCode int - } - type test struct { - description string - questionnaireID int - DeleteQuestionnaireError error - DeleteTargetsError error - DeleteAdministratorsError error - expect - } - - testCases := []test{ - { - description: "エラーなしなので200", - questionnaireID: 1, - DeleteQuestionnaireError: nil, - DeleteTargetsError: nil, - DeleteAdministratorsError: nil, - expect: expect{ - statusCode: http.StatusOK, - }, - }, - { - description: "questionnaireIDが0でも200", - questionnaireID: 0, - DeleteQuestionnaireError: nil, - DeleteTargetsError: nil, - DeleteAdministratorsError: nil, - expect: expect{ - statusCode: http.StatusOK, - }, - }, - { - description: "DeleteQuestionnaireがエラーなので500", - questionnaireID: 1, - DeleteQuestionnaireError: errors.New("error"), - DeleteTargetsError: nil, - DeleteAdministratorsError: nil, - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "DeleteTargetsがエラーなので500", - questionnaireID: 1, - DeleteQuestionnaireError: nil, - DeleteTargetsError: errors.New("error"), - DeleteAdministratorsError: nil, - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "DeleteAdministratorsがエラーなので500", - questionnaireID: 1, - DeleteQuestionnaireError: nil, - DeleteTargetsError: nil, - DeleteAdministratorsError: errors.New("error"), - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.description, func(t *testing.T) { - e := echo.New() - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/questionnaire/%d", testCase.questionnaireID), nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - c.SetPath("/questionnaires/:questionnaire_id") - c.SetParamNames("questionnaire_id") - c.SetParamValues(strconv.Itoa(testCase.questionnaireID)) - - c.Set(questionnaireIDKey, testCase.questionnaireID) - - mockQuestionnaire. - EXPECT(). - DeleteQuestionnaire( - c.Request().Context(), - testCase.questionnaireID, - ). - Return(testCase.DeleteQuestionnaireError) - - if testCase.DeleteQuestionnaireError == nil { - mockTarget. - EXPECT(). - DeleteTargets( - c.Request().Context(), - testCase.questionnaireID, - ). - Return(testCase.DeleteTargetsError) - - if testCase.DeleteTargetsError == nil { - mockAdministrator. - EXPECT(). - DeleteAdministrators( - c.Request().Context(), - testCase.questionnaireID, - ). - Return(testCase.DeleteAdministratorsError) - } - } - - e.HTTPErrorHandler(questionnaire.DeleteQuestionnaire(c), c) - - assert.Equal(t, testCase.expect.statusCode, rec.Code, "status code") - }) - } -} - -func TestCreateQuestionnaireMessage(t *testing.T) { - t.Parallel() - - type args struct { - questionnaireID int - title string - description string - administrators []string - resTimeLimit null.Time - targets []string - } - type expect struct { - message string - } - type test struct { - description string - args - expect - } - - tm, err := time.ParseInLocation("2006/01/02 15:04", "2021/10/01 09:06", time.Local) - if err != nil { - t.Errorf("failed to parse time: %v", err) - } - - testCases := []test{ - { - description: "通常の引数なので問題なし", - args: args{ - questionnaireID: 1, - title: "title", - description: "description", - administrators: []string{"administrator1"}, - resTimeLimit: null.TimeFrom(tm), - targets: []string{"target1"}, - }, - expect: expect{ - message: `### アンケート『[title](https://anke-to.trap.jp/questionnaires/1)』が作成されました -#### 管理者 -administrator1 -#### 説明 -description -#### 回答期限 -2021/10/01 09:06 -#### 対象者 -@target1 -#### 回答リンク -https://anke-to.trap.jp/responses/new/1`, - }, - }, - { - description: "questionnaireIDが0でも問題なし", - args: args{ - questionnaireID: 0, - title: "title", - description: "description", - administrators: []string{"administrator1"}, - resTimeLimit: null.TimeFrom(tm), - targets: []string{"target1"}, - }, - expect: expect{ - message: `### アンケート『[title](https://anke-to.trap.jp/questionnaires/0)』が作成されました -#### 管理者 -administrator1 -#### 説明 -description -#### 回答期限 -2021/10/01 09:06 -#### 対象者 -@target1 -#### 回答リンク -https://anke-to.trap.jp/responses/new/0`, - }, - }, - { - // 実際には発生しないけど念の為 - description: "titleが空文字でも問題なし", - args: args{ - questionnaireID: 1, - title: "", - description: "description", - administrators: []string{"administrator1"}, - resTimeLimit: null.TimeFrom(tm), - targets: []string{"target1"}, - }, - expect: expect{ - message: `### アンケート『[](https://anke-to.trap.jp/questionnaires/1)』が作成されました -#### 管理者 -administrator1 -#### 説明 -description -#### 回答期限 -2021/10/01 09:06 -#### 対象者 -@target1 -#### 回答リンク -https://anke-to.trap.jp/responses/new/1`, - }, - }, - { - description: "説明が空文字でも問題なし", - args: args{ - questionnaireID: 1, - title: "title", - description: "", - administrators: []string{"administrator1"}, - resTimeLimit: null.TimeFrom(tm), - targets: []string{"target1"}, - }, - expect: expect{ - message: `### アンケート『[title](https://anke-to.trap.jp/questionnaires/1)』が作成されました -#### 管理者 -administrator1 -#### 説明 - -#### 回答期限 -2021/10/01 09:06 -#### 対象者 -@target1 -#### 回答リンク -https://anke-to.trap.jp/responses/new/1`, - }, - }, - { - description: "administrator複数人でも問題なし", - args: args{ - questionnaireID: 1, - title: "title", - description: "description", - administrators: []string{"administrator1", "administrator2"}, - resTimeLimit: null.TimeFrom(tm), - targets: []string{"target1"}, - }, - expect: expect{ - message: `### アンケート『[title](https://anke-to.trap.jp/questionnaires/1)』が作成されました -#### 管理者 -administrator1,administrator2 -#### 説明 -description -#### 回答期限 -2021/10/01 09:06 -#### 対象者 -@target1 -#### 回答リンク -https://anke-to.trap.jp/responses/new/1`, - }, - }, - { - // 実際には発生しないけど念の為 - description: "administratorがいなくても問題なし", - args: args{ - questionnaireID: 1, - title: "title", - description: "description", - administrators: []string{}, - resTimeLimit: null.TimeFrom(tm), - targets: []string{"target1"}, - }, - expect: expect{ - message: `### アンケート『[title](https://anke-to.trap.jp/questionnaires/1)』が作成されました -#### 管理者 - -#### 説明 -description -#### 回答期限 -2021/10/01 09:06 -#### 対象者 -@target1 -#### 回答リンク -https://anke-to.trap.jp/responses/new/1`, - }, - }, - { - description: "回答期限なしでも問題なし", - args: args{ - questionnaireID: 1, - title: "title", - description: "description", - administrators: []string{"administrator1"}, - resTimeLimit: null.NewTime(time.Time{}, false), - targets: []string{"target1"}, - }, - expect: expect{ - message: `### アンケート『[title](https://anke-to.trap.jp/questionnaires/1)』が作成されました -#### 管理者 -administrator1 -#### 説明 -description -#### 回答期限 -なし -#### 対象者 -@target1 -#### 回答リンク -https://anke-to.trap.jp/responses/new/1`, - }, - }, - { - description: "対象者が複数人でも問題なし", - args: args{ - questionnaireID: 1, - title: "title", - description: "description", - administrators: []string{"administrator1"}, - resTimeLimit: null.TimeFrom(tm), - targets: []string{"target1", "target2"}, - }, - expect: expect{ - message: `### アンケート『[title](https://anke-to.trap.jp/questionnaires/1)』が作成されました -#### 管理者 -administrator1 -#### 説明 -description -#### 回答期限 -2021/10/01 09:06 -#### 対象者 -@target1 @target2 -#### 回答リンク -https://anke-to.trap.jp/responses/new/1`, - }, - }, - { - description: "対象者がいなくても問題なし", - args: args{ - questionnaireID: 1, - title: "title", - description: "description", - administrators: []string{"administrator1"}, - resTimeLimit: null.TimeFrom(tm), - targets: []string{}, - }, - expect: expect{ - message: `### アンケート『[title](https://anke-to.trap.jp/questionnaires/1)』が作成されました -#### 管理者 -administrator1 -#### 説明 -description -#### 回答期限 -2021/10/01 09:06 -#### 対象者 -なし -#### 回答リンク -https://anke-to.trap.jp/responses/new/1`, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.description, func(t *testing.T) { - message := createQuestionnaireMessage( - testCase.args.questionnaireID, - testCase.args.title, - testCase.args.description, - testCase.args.administrators, - testCase.args.resTimeLimit, - testCase.args.targets, - ) - - assert.Equal(t, testCase.expect.message, message) - }) - } -} diff --git a/router/questions.go b/router/questions.go deleted file mode 100644 index 1b446686..00000000 --- a/router/questions.go +++ /dev/null @@ -1,158 +0,0 @@ -package router - -import ( - "errors" - "fmt" - "net/http" - "regexp" - - "github.com/labstack/echo/v4" - - "github.com/traPtitech/anke-to/model" -) - -// Question Questionの構造体 -type Question struct { - model.IValidation - model.IQuestion - model.IOption - model.IScaleLabel -} - -// NewQuestion Questionのコンストラクタ -func NewQuestion(validation model.IValidation, question model.IQuestion, option model.IOption, scaleLabel model.IScaleLabel) *Question { - return &Question{ - IValidation: validation, - IQuestion: question, - IOption: option, - IScaleLabel: scaleLabel, - } -} - -type PostAndEditQuestionRequest struct { - QuestionnaireID int `json:"questionnaireID" validate:"min=0"` - QuestionType string `json:"question_type" validate:"required,oneof=Text TextArea Number MultipleChoice Checkbox LinearScale"` - QuestionNum int `json:"question_num" validate:"min=0"` - PageNum int `json:"page_num" validate:"min=0"` - Body string `json:"body" validate:"required"` - IsRequired bool `json:"is_required"` - Options []string `json:"options" validate:"required_if=QuestionType Checkbox,required_if=QuestionType MultipleChoice,dive,max=1000"` - ScaleLabelRight string `json:"scale_label_right" validate:"max=50"` - ScaleLabelLeft string `json:"scale_label_left" validate:"max=50"` - ScaleMin int `json:"scale_min"` - ScaleMax int `json:"scale_max" validate:"gtecsfield=ScaleMin"` - RegexPattern string `json:"regex_pattern"` - MinBound string `json:"min_bound" validate:"omitempty,number"` - MaxBound string `json:"max_bound" validate:"omitempty,number"` -} - -// EditQuestion PATCH /questions/:id -func (q *Question) EditQuestion(c echo.Context) error { - questionID, err := getQuestionID(c) - if err != nil { - c.Logger().Errorf("failed to get question id: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionID: %w", err)) - } - - req := PostAndEditQuestionRequest{} - - if err := c.Bind(&req); err != nil { - c.Logger().Infof("failed to bind PostAndEditQuestionRequest: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest) - } - - validate, err := getValidator(c) - if err != nil { - c.Logger().Errorf("failed to get validator: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - err = validate.Struct(req) - if err != nil { - c.Logger().Infof("validation failed: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err) - } - - switch req.QuestionType { - case "Text": - //正規表現のチェック - if _, err := regexp.Compile(req.RegexPattern); err != nil { - c.Logger().Infof("invalid regex pattern: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest) - } - case "Number": - //数字か,min<=maxになってるか - if err := q.CheckNumberValid(req.MinBound, req.MaxBound); err != nil { - c.Logger().Info("invalid number: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err) - } - } - - err = q.UpdateQuestion(c.Request().Context(), req.QuestionnaireID, req.PageNum, req.QuestionNum, req.QuestionType, req.Body, req.IsRequired, questionID) - if err != nil { - c.Logger().Errorf("failed to update question: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - switch req.QuestionType { - case "MultipleChoice", "Checkbox", "Dropdown": - if err := q.UpdateOptions(c.Request().Context(), req.Options, questionID); err != nil && !errors.Is(err, model.ErrNoRecordUpdated) { - c.Logger().Errorf("failed to update options: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - case "LinearScale": - if err := q.UpdateScaleLabel(c.Request().Context(), questionID, - model.ScaleLabels{ - ScaleLabelLeft: req.ScaleLabelLeft, - ScaleLabelRight: req.ScaleLabelRight, - ScaleMax: req.ScaleMax, - ScaleMin: req.ScaleMin, - }); err != nil && !errors.Is(err, model.ErrNoRecordUpdated) { - c.Logger().Errorf("failed to update scale label: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - case "Text", "Number": - if err := q.UpdateValidation(c.Request().Context(), questionID, - model.Validations{ - RegexPattern: req.RegexPattern, - MinBound: req.MinBound, - MaxBound: req.MaxBound, - }); err != nil && !errors.Is(err, model.ErrNoRecordUpdated) { - c.Logger().Errorf("failed to update validation: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - } - - return c.NoContent(http.StatusOK) -} - -// DeleteQuestion DELETE /questions/:id -func (q *Question) DeleteQuestion(c echo.Context) error { - questionID, err := getQuestionID(c) - if err != nil { - c.Logger().Errorf("failed to get question id: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionID: %w", err)) - } - - if err := q.IQuestion.DeleteQuestion(c.Request().Context(), questionID); err != nil { - c.Logger().Errorf("failed to delete question: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - if err := q.DeleteOptions(c.Request().Context(), questionID); err != nil { - c.Logger().Errorf("failed to delete options: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - if err := q.DeleteScaleLabel(c.Request().Context(), questionID); err != nil { - c.Logger().Errorf("failed to delete scale label: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - if err := q.DeleteValidation(c.Request().Context(), questionID); err != nil { - c.Logger().Errorf("failed to delete validation: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - return c.NoContent(http.StatusOK) -} diff --git a/router/questions_test.go b/router/questions_test.go deleted file mode 100644 index 009f695d..00000000 --- a/router/questions_test.go +++ /dev/null @@ -1,1433 +0,0 @@ -package router - -import ( - "strings" - "testing" - - "github.com/go-playground/validator/v10" - "github.com/stretchr/testify/assert" -) - -func TestPostQuestionValidate(t *testing.T) { - t.Parallel() - - tests := []struct { - description string - request *PostAndEditQuestionRequest - isErr bool - }{ - { - description: "旧クライアントの一般的なTextタイプの質問なのでエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "questionnaireIDが0でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 0, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "questionNumが0でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 0, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "questionNumが負なのでエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: -1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "pageNumが0でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 0, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "pageNumが負なのでエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: -1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "質問文が空なのでエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "isRequiredがfalseでもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: false, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "Textタイプでoptionがnullでもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: nil, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "Textタイプでoptionが1000文字以上でもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"0" + strings.Repeat("1234567890", 100)}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "TextタイプでscaleLabelRightがあってもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "TextタイプでscaleLabelLeftがあってもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "左", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "TextタイプでscaleLabelRightが50字を超えてもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "012345678901234567890123456789012345678901234567890", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "TextタイプでscaleLabelLeftが50字を超えてもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "012345678901234567890123456789012345678901234567890", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "TextタイプでscaleMinが負でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: -1, - ScaleMax: 0, - }, - }, - { - description: "TextタイプでscaleMaxが負でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: -1, - ScaleMax: -1, - }, - }, - { - description: "TextタイプでscaleMaxが正でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 1, - }, - }, - { - description: "TextタイプでscaleMinが正でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 1, - ScaleMax: 1, - }, - }, - { - description: "TextタイプでscaleMinがscaleMaxより大きいときエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 1, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "regexPatternが指定されていてもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - RegexPattern: ".*", - }, - }, - { - description: "MinBoundが設定されていてもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - MinBound: "0", - }, - }, - { - description: "MinBoundが数字でない時エラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - MinBound: "a", - }, - isErr: true, - }, - { - description: "MaxBoundが設定されていてもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - MaxBound: "0", - }, - }, - { - description: "MaxBoundが数字でない時エラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - MaxBound: "a", - }, - isErr: true, - }, - { - description: "旧クライアントの一般的なTextAreaタイプの質問なのでエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "TextArea", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "TextAreaタイプでoptionがnullでもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "TextArea", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: nil, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "TextAreaタイプでoptionが1000文字以上でもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "TextArea", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"0" + strings.Repeat("1234567890", 100)}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "TextAreaタイプでscaleLabelRightがあってもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "TextArea", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "TextAreaタイプでscaleLabelLeftがあってもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "TextArea", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "左", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "TextAreaタイプでscaleLabelRightが50字を超えてもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "TextArea", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "012345678901234567890123456789012345678901234567890", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "TextAreaタイプでscaleLabelLeftが50字を超えてもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "TextArea", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "012345678901234567890123456789012345678901234567890", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "TextAreaタイプでscaleMinが負でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "TextArea", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: -1, - ScaleMax: 0, - }, - }, - { - description: "TextAreaタイプでscaleMaxが負でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "TextArea", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: -1, - ScaleMax: -1, - }, - }, - { - description: "TextAreaタイプでscaleMaxが正でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "TextArea", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 1, - }, - }, - { - description: "TextAreaタイプでscaleMinが正でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "TextArea", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 1, - ScaleMax: 1, - }, - }, - { - description: "TextAreaタイプでscaleMinがscaleMaxより大きいときエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "TextArea", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 1, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "旧クライアントの一般的なNumberタイプの質問なのでエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "Numberタイプでoptionがnullでもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: nil, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "Numberタイプでoptionが1000文字以上でもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"0" + strings.Repeat("1234567890", 100)}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "NumberタイプでscaleLabelRightがあってもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "NumberタイプでscaleLabelLeftがあってもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "左", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "NumberタイプでscaleLabelRightが50字を超えてもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "012345678901234567890123456789012345678901234567890", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "NumberタイプでscaleLabelLeftが50字を超えてもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "012345678901234567890123456789012345678901234567890", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "NumberタイプでscaleMinが負でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: -1, - ScaleMax: 0, - }, - }, - { - description: "NumberタイプでscaleMaxが負でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: -1, - ScaleMax: -1, - }, - }, - { - description: "NumberタイプでscaleMaxが正でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 1, - }, - }, - { - description: "NumberタイプでscaleMinが正でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 1, - ScaleMax: 1, - }, - }, - { - description: "NumberタイプでscaleMinがscaleMaxより大きいときエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Number", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 1, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "旧クライアントの一般的なCheckboxタイプの質問なのでエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Checkbox", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"a", "b"}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "Checkboxタイプでoptionがnullでエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Checkbox", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: nil, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "Checkboxタイプでoptionが1000文字以上でエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Checkbox", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"0" + strings.Repeat("1234567890", 100)}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "CheckboxタイプでscaleLabelRightがあってもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Checkbox", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "CheckboxタイプでscaleLabelLeftがあってもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Checkbox", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "左", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "CheckboxタイプでscaleLabelRightが50字を超えてもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Checkbox", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "012345678901234567890123456789012345678901234567890", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "CheckboxタイプでscaleLabelLeftが50字を超えてもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Checkbox", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "012345678901234567890123456789012345678901234567890", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "CheckboxタイプでscaleMinが負でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Checkbox", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: -1, - ScaleMax: 0, - }, - }, - { - description: "CheckboxタイプでscaleMaxが負でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Checkbox", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: -1, - ScaleMax: -1, - }, - }, - { - description: "CheckboxタイプでscaleMaxが正でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Checkbox", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 1, - }, - }, - { - description: "CheckboxタイプでscaleMinが正でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Checkbox", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 1, - ScaleMax: 1, - }, - }, - { - description: "CheckboxタイプでscaleMinがscaleMaxより大きいときエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Checkbox", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 1, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "旧クライアントの一般的なMultipleChoiceタイプの質問なのでエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"a", "b"}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "MultipleChoiceタイプでoptionがnullでエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: nil, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "MultipleChoiceタイプでoptionが1000文字以上でエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"0" + strings.Repeat("1234567890", 100)}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "MultipleChoiceタイプでscaleLabelRightがあってもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "MultipleChoiceタイプでscaleLabelLeftがあってもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "左", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "MultipleChoiceタイプでscaleLabelRightが50字を超えてもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "012345678901234567890123456789012345678901234567890", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "MultipleChoiceタイプでscaleLabelLeftが50字を超えてもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "012345678901234567890123456789012345678901234567890", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "MultipleChoiceタイプでscaleMinが負でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: -1, - ScaleMax: 0, - }, - }, - { - description: "MultipleChoiceタイプでscaleMaxが負でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: -1, - ScaleMax: -1, - }, - }, - { - description: "MultipleChoiceタイプでscaleMaxが正でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 1, - }, - }, - { - description: "MultipleChoiceタイプでscaleMinが正でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 1, - ScaleMax: 1, - }, - }, - { - description: "MultipleChoiceタイプでscaleMinがscaleMaxより大きいときエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "MultipleChoice", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 1, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "旧クライアントの一般的なLinearScaleタイプの質問なのでエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "LinearScale", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "左", - ScaleMin: 1, - ScaleMax: 5, - }, - }, - { - description: "LinearScaleタイプでoptionがnullでもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "LinearScale", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: nil, - ScaleLabelRight: "右", - ScaleLabelLeft: "左", - ScaleMin: 0, - ScaleMax: 0, - }, - }, - { - description: "LinearScaleタイプでoptionが1000文字以上でもエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "LinearScale", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{"0" + strings.Repeat("1234567890", 100)}, - ScaleLabelRight: "右", - ScaleLabelLeft: "左", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "LinearScaleタイプでscaleLabelRightがなくてもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "LinearScale", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "左", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: false, - }, - { - description: "LinearScaleタイプでscaleLabelLeftがなくてもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "LinearScale", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: false, - }, - { - description: "LinearScaleタイプでscaleLabelLeft&Rightがなくてもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "LinearScale", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "", - ScaleLabelLeft: "", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: false, - }, - { - description: "LinearScaleタイプでscaleLabelRightが50字を超えていたらエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "LinearScale", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "012345678901234567890123456789012345678901234567890", - ScaleLabelLeft: "左", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "LinearScaleタイプでscaleLabelLeftが50字を超えていたらエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "LinearScale", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "012345678901234567890123456789012345678901234567890", - ScaleMin: 0, - ScaleMax: 0, - }, - isErr: true, - }, - { - description: "LinearScaleタイプでscaleMinが負でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "左", - ScaleMin: -1, - ScaleMax: 0, - }, - }, - { - description: "LinearScaleタイプでscaleMaxが負でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "左", - ScaleMin: -1, - ScaleMax: -1, - }, - }, - { - description: "LinearScaleタイプでscaleMaxが正でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "左", - ScaleMin: 0, - ScaleMax: 1, - }, - }, - { - description: "LinearScaleタイプでscaleMinが正でもエラーなし", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "左", - ScaleMin: 1, - ScaleMax: 1, - }, - }, - { - description: "LinearScaleタイプでscaleMinがscaleMaxより大きいときエラー", - request: &PostAndEditQuestionRequest{ - QuestionnaireID: 1, - QuestionType: "Text", - QuestionNum: 1, - PageNum: 1, - Body: "発表タイトル", - IsRequired: true, - Options: []string{}, - ScaleLabelRight: "右", - ScaleLabelLeft: "左", - ScaleMin: 1, - ScaleMax: 0, - }, - isErr: true, - }, - } - - for _, test := range tests { - validate := validator.New() - - t.Run(test.description, func(t *testing.T) { - err := validate.Struct(test.request) - - if test.isErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} diff --git a/router/responses.go b/router/responses.go deleted file mode 100644 index fde97cfd..00000000 --- a/router/responses.go +++ /dev/null @@ -1,426 +0,0 @@ -package router - -import ( - "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/labstack/echo/v4" - - "gopkg.in/guregu/null.v4" - - "github.com/traPtitech/anke-to/model" -) - -// Response Responseの構造体 -type Response struct { - model.IQuestionnaire - model.IValidation - model.IScaleLabel - model.IRespondent - model.IResponse -} - -// NewResponse Responseのコンストラクタ -func NewResponse(questionnaire model.IQuestionnaire, validation model.IValidation, scaleLabel model.IScaleLabel, respondent model.IRespondent, response model.IResponse) *Response { - return &Response{ - IQuestionnaire: questionnaire, - IValidation: validation, - IScaleLabel: scaleLabel, - IRespondent: respondent, - IResponse: response, - } -} - -// Responses 質問に対する回答一覧の構造体 -type Responses struct { - ID int `json:"questionnaireID" validate:"min=0"` - Temporarily bool `json:"temporarily"` - Body []model.ResponseBody `json:"body" validate:"required,dive"` -} - -// PostResponse POST /responses -func (r *Response) PostResponse(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - req := Responses{} - - if err := c.Bind(&req); err != nil { - c.Logger().Infof("failed to bind Responses: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest) - } - - validate, err := getValidator(c) - if err != nil { - c.Logger().Errorf("failed to get validator: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError) - } - - err = validate.StructCtx(c.Request().Context(), req) - if err != nil { - c.Logger().Infof("failed to validate: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - limit, err := r.GetQuestionnaireLimit(c.Request().Context(), req.ID) - if err != nil { - if errors.Is(err, model.ErrRecordNotFound) { - c.Logger().Info("questionnaire not found") - return echo.NewHTTPError(http.StatusNotFound, err) - } - c.Logger().Errorf("failed to get questionnaire limit: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - // 回答期限を過ぎた回答は許可しない - if limit.Valid && limit.Time.Before(time.Now()) { - c.Logger().Info("expired questionnaire") - return echo.NewHTTPError(http.StatusMethodNotAllowed) - } - - // validationsのパターンマッチ - questionIDs := make([]int, 0, len(req.Body)) - QuestionTypes := make(map[int]model.ResponseBody, len(req.Body)) - - for _, body := range req.Body { - questionIDs = append(questionIDs, body.QuestionID) - QuestionTypes[body.QuestionID] = body - } - - validations, err := r.GetValidations(c.Request().Context(), questionIDs) - if err != nil { - c.Logger().Errorf("failed to get validations: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - // パターンマッチしてエラーなら返す - for _, validation := range validations { - body := QuestionTypes[validation.QuestionID] - switch body.QuestionType { - case "Number": - if err := r.CheckNumberValidation(validation, body.Body.ValueOrZero()); err != nil { - if errors.Is(err, model.ErrInvalidNumber) { - c.Logger().Errorf("invalid number: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - c.Logger().Infof("invalid number: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err) - } - case "Text": - if err := r.CheckTextValidation(validation, body.Body.ValueOrZero()); err != nil { - if errors.Is(err, model.ErrTextMatching) { - c.Logger().Infof("invalid text: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err) - } - c.Logger().Errorf("invalid text: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - } - } - - scaleLabelIDs := []int{} - for _, body := range req.Body { - switch body.QuestionType { - case "LinearScale": - scaleLabelIDs = append(scaleLabelIDs, body.QuestionID) - } - } - - scaleLabels, err := r.GetScaleLabels(c.Request().Context(), scaleLabelIDs) - if err != nil { - c.Logger().Errorf("failed to get scale labels: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - scaleLabelMap := make(map[int]model.ScaleLabels, len(scaleLabels)) - for _, label := range scaleLabels { - scaleLabelMap[label.QuestionID] = label - } - - // LinearScaleのパターンマッチ - for _, body := range req.Body { - switch body.QuestionType { - case "LinearScale": - label, ok := scaleLabelMap[body.QuestionID] - if !ok { - label = model.ScaleLabels{} - } - if err := r.CheckScaleLabel(label, body.Body.ValueOrZero()); err != nil { - c.Logger().Infof("failed to check scale label: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err) - } - } - } - - var submittedAt time.Time - //一時保存のときはnull - if req.Temporarily { - submittedAt = time.Time{} - } else { - submittedAt = time.Now() - } - - responseID, err := r.InsertRespondent(c.Request().Context(), userID, req.ID, null.NewTime(submittedAt, !req.Temporarily)) - if err != nil { - c.Logger().Errorf("failed to insert respondent: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - responseMetas := make([]*model.ResponseMeta, 0, len(req.Body)) - for _, body := range req.Body { - switch body.QuestionType { - case "MultipleChoice", "Checkbox", "Dropdown": - for _, option := range body.OptionResponse { - responseMetas = append(responseMetas, &model.ResponseMeta{ - QuestionID: body.QuestionID, - Data: option, - }) - } - default: - responseMetas = append(responseMetas, &model.ResponseMeta{ - QuestionID: body.QuestionID, - Data: body.Body.ValueOrZero(), - }) - } - } - - if len(responseMetas) > 0 { - err = r.InsertResponses(c.Request().Context(), responseID, responseMetas) - if err != nil { - c.Logger().Errorf("failed to insert responses: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to insert responses: %w", err)) - } - } - - return c.JSON(http.StatusCreated, map[string]interface{}{ - "responseID": responseID, - "questionnaireID": req.ID, - "temporarily": req.Temporarily, - "submitted_at": submittedAt, - "body": req.Body, - }) -} - -// GetResponse GET /responses/:responseID -func (r *Response) GetResponse(c echo.Context) error { - strResponseID := c.Param("responseID") - responseID, err := strconv.Atoi(strResponseID) - if err != nil { - c.Logger().Infof("failed to convert responseID to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("failed to parse responseID(%s) to integer: %w", strResponseID, err)) - } - - respondentDetail, err := r.GetRespondentDetail(c.Request().Context(), responseID) - if errors.Is(err, model.ErrRecordNotFound) { - c.Logger().Infof("response not found: %+v", err) - return echo.NewHTTPError(http.StatusNotFound, "response not found") - } - if err != nil { - c.Logger().Errorf("failed to get respondent detail: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - return c.JSON(http.StatusOK, respondentDetail) -} - -// EditResponse PATCH /responses/:responseID -func (r *Response) EditResponse(c echo.Context) error { - responseID, err := getResponseID(c) - if err != nil { - c.Logger().Errorf("failed to get responseID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get responseID: %w", err)) - } - - req := Responses{} - if err := c.Bind(&req); err != nil { - c.Logger().Infof("failed to bind Responses: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest) - } - - validate, err := getValidator(c) - if err != nil { - c.Logger().Errorf("failed to get validator: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - err = validate.Struct(req) - if err != nil { - c.Logger().Infof("validation failed: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err) - } - - limit, err := r.GetQuestionnaireLimit(c.Request().Context(), req.ID) - if err != nil { - if errors.Is(err, model.ErrRecordNotFound) { - c.Logger().Infof("questionnaire not found: %+v", err) - return echo.NewHTTPError(http.StatusNotFound, err) - } - c.Logger().Errorf("failed to get questionnaire limit: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - // 回答期限を過ぎた回答は許可しない - if limit.Valid && limit.Time.Before(time.Now()) { - c.Logger().Info("expired questionnaire") - return echo.NewHTTPError(http.StatusMethodNotAllowed) - } - - // validationsのパターンマッチ - questionIDs := make([]int, 0, len(req.Body)) - QuestionTypes := make(map[int]model.ResponseBody, len(req.Body)) - - for _, body := range req.Body { - questionIDs = append(questionIDs, body.QuestionID) - QuestionTypes[body.QuestionID] = body - } - - validations, err := r.GetValidations(c.Request().Context(), questionIDs) - if err != nil { - c.Logger().Errorf("failed to get validations: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - // パターンマッチしてエラーなら返す - for _, validation := range validations { - body := QuestionTypes[validation.QuestionID] - switch body.QuestionType { - case "Number": - if err := r.CheckNumberValidation(validation, body.Body.ValueOrZero()); err != nil { - if errors.Is(err, model.ErrInvalidNumber) { - c.Logger().Errorf("invalid number: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - c.Logger().Infof("invalid number: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err) - } - case "Text": - if err := r.CheckTextValidation(validation, body.Body.ValueOrZero()); err != nil { - if errors.Is(err, model.ErrTextMatching) { - c.Logger().Infof("invalid text: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err) - } - c.Logger().Errorf("invalid text: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - } - } - - scaleLabelIDs := []int{} - for _, body := range req.Body { - switch body.QuestionType { - case "LinearScale": - scaleLabelIDs = append(scaleLabelIDs, body.QuestionID) - } - } - - scaleLabels, err := r.GetScaleLabels(c.Request().Context(), scaleLabelIDs) - if err != nil { - c.Logger().Errorf("failed to get scale labels: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - scaleLabelMap := make(map[int]model.ScaleLabels, len(scaleLabels)) - for _, label := range scaleLabels { - scaleLabelMap[label.QuestionID] = label - } - - // LinearScaleのパターンマッチ - for _, body := range req.Body { - switch body.QuestionType { - case "LinearScale": - label, ok := scaleLabelMap[body.QuestionID] - if !ok { - label = model.ScaleLabels{} - } - if err := r.CheckScaleLabel(label, body.Body.ValueOrZero()); err != nil { - c.Logger().Infof("invalid scale label: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err) - } - } - } - - if !req.Temporarily { - err := r.UpdateSubmittedAt(c.Request().Context(), responseID) - if err != nil { - c.Logger().Errorf("failed to update submitted at: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to update sbmitted_at: %w", err)) - } - } - - //全消し&追加(レコード数爆発しそう) - if err := r.IResponse.DeleteResponse(c.Request().Context(), responseID); err != nil && !errors.Is(err, model.ErrNoRecordDeleted) { - c.Logger().Errorf("failed to delete response: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - responseMetas := make([]*model.ResponseMeta, 0, len(req.Body)) - for _, body := range req.Body { - switch body.QuestionType { - case "MultipleChoice", "Checkbox", "Dropdown": - for _, option := range body.OptionResponse { - responseMetas = append(responseMetas, &model.ResponseMeta{ - QuestionID: body.QuestionID, - Data: option, - }) - } - default: - responseMetas = append(responseMetas, &model.ResponseMeta{ - QuestionID: body.QuestionID, - Data: body.Body.ValueOrZero(), - }) - } - } - - if len(responseMetas) > 0 { - err = r.InsertResponses(c.Request().Context(), responseID, responseMetas) - if err != nil { - c.Logger().Errorf("failed to insert responses: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to insert responses: %w", err)) - } - } - - return c.NoContent(http.StatusOK) -} - -// DeleteResponse DELETE /responses/:responseID -func (r *Response) DeleteResponse(c echo.Context) error { - responseID, err := getResponseID(c) - if err != nil { - c.Logger().Errorf("failed to get response id: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get responseID: %w", err)) - } - - limit, err := r.GetQuestionnaireLimitByResponseID(c.Request().Context(), responseID) - if err != nil { - if errors.Is(err, model.ErrRecordNotFound) { - c.Logger().Infof("response not found: %+v", err) - return echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("failed to find limit of responseID:%d(error: %w)", responseID, err)) - } - c.Logger().Errorf("failed to get questionnaire limit: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get limit of responseID:%d(error: %w)", responseID, err)) - } - - // 回答期限を過ぎた回答の削除は許可しない - if limit.Valid && limit.Time.Before(time.Now()) { - c.Logger().Info("expired response") - return echo.NewHTTPError(http.StatusMethodNotAllowed) - } - - err = r.DeleteRespondent(c.Request().Context(), responseID) - if err != nil { - c.Logger().Errorf("failed to delete respondent: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - err = r.IResponse.DeleteResponse(c.Request().Context(), responseID) - if err != nil && !errors.Is(err, model.ErrNoRecordDeleted) { - c.Logger().Errorf("failed to delete response: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - return c.NoContent(http.StatusOK) -} diff --git a/router/responses_test.go b/router/responses_test.go deleted file mode 100644 index 2754bfbb..00000000 --- a/router/responses_test.go +++ /dev/null @@ -1,1879 +0,0 @@ -package router - -import ( - "encoding/json" - "errors" - "fmt" - "strings" - - "github.com/go-playground/validator/v10" - - "net/http" - "net/http/httptest" - "strconv" - "testing" - "time" - - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/golang/mock/gomock" - "github.com/traPtitech/anke-to/model" - "github.com/traPtitech/anke-to/model/mock_model" - "gopkg.in/guregu/null.v4" -) - -type responseBody struct { - QuestionID int `json:"questionID" validate:"min=0"` - QuestionType string `json:"question_type" validate:"required,oneof=Text TextArea Number MultipleChoice Checkbox LinearScale"` - Body null.String `json:"response" validate:"required"` - OptionResponse []string `json:"option_response" validate:"required_if=QuestionType Checkbox,required_if=QuestionType MultipleChoice,dive,max=1000"` -} - -func TestPostResponseValidate(t *testing.T) { - t.Parallel() - - tests := []struct { - description string - request *Responses - isErr bool - }{ - { - description: "一般的なリクエストなのでエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Text", - Body: null.String{}, - OptionResponse: nil, - }, - }, - }, - }, - { - description: "IDが0でもエラーなし", - request: &Responses{ - ID: 0, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Text", - Body: null.String{}, - OptionResponse: nil, - }, - }, - }, - }, - { - description: "BodyのQuestionIDが0でもエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 0, - QuestionType: "Text", - Body: null.String{}, - OptionResponse: nil, - }, - }, - }, - }, - { - description: "ResponsesのIDが負なのでエラー", - request: &Responses{ - ID: -1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Text", - Body: null.String{}, - OptionResponse: nil, - }, - }, - }, - isErr: true, - }, - { - description: "Temporarilyがtrueでもエラーなし", - request: &Responses{ - ID: 1, - Temporarily: true, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Text", - Body: null.String{}, - OptionResponse: nil, - }, - }, - }, - isErr: false, - }, - { - description: "Bodyがnilなのでエラー", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: nil, - }, - isErr: true, - }, - { - description: "BodyのQuestionIDが負なのでエラー", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: -1, - QuestionType: "Text", - Body: null.String{}, - OptionResponse: nil, - }, - }, - }, - isErr: true, - }, - { - description: "TextタイプでoptionResponseが1000文字以上でエラー", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Text", - Body: null.String{}, - OptionResponse: []string{"0" + strings.Repeat("1234567890", 100)}, - }, - }, - }, - isErr: true, - }, - { - description: "TextタイプでoptionResponseが1000文字ピッタリはエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Text", - Body: null.String{}, - OptionResponse: []string{strings.Repeat("1234567890", 100)}, - }, - }, - }, - }, - { - description: "一般的なTextAreaタイプの回答なのでエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "TextArea", - Body: null.String{}, - OptionResponse: nil, - }, - }, - }, - }, - { - description: "TextAreaタイプでoptionResponseが1000文字以上でもエラー", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "TextArea", - Body: null.String{}, - OptionResponse: []string{"0" + strings.Repeat("1234567890", 100)}, - }, - }, - }, - isErr: true, - }, - { - description: "TextAreaタイプでoptionResponseが1000文字ピッタリはエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "TextArea", - Body: null.String{}, - OptionResponse: []string{strings.Repeat("1234567890", 100)}, - }, - }, - }, - }, - { - description: "一般的なNumberタイプの回答なのでエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Number", - Body: null.String{}, - OptionResponse: nil, - }, - }, - }, - }, - { - description: "NumberタイプでoptionResponseが1000文字以上でもエラー", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Number", - Body: null.String{}, - OptionResponse: []string{"0" + strings.Repeat("1234567890", 100)}, - }, - }, - }, - isErr: true, - }, - { - description: "NumberタイプでoptionResponseが1000文字ピッタリでエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Number", - Body: null.String{}, - OptionResponse: []string{strings.Repeat("1234567890", 100)}, - }, - }, - }, - }, - { - description: "Checkboxタイプで一般的な回答なのでエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Checkbox", - Body: null.String{}, - OptionResponse: []string{"a", "b"}, - }, - }, - }, - }, - { - description: "Checkboxタイプで選択しなくてもエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Checkbox", - Body: null.String{}, - OptionResponse: []string{}, - }, - }, - }, - }, - { - description: "CheckboxタイプでOptionResponseがnilな回答なのでエラー", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Checkbox", - Body: null.String{}, - OptionResponse: nil, - }, - }, - }, - isErr: true, - }, - { - description: "CheckboxタイプでOptionResponseが1000文字以上な回答なのでエラー", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Checkbox", - Body: null.String{}, - OptionResponse: []string{"0" + strings.Repeat("1234567890", 100)}, - }, - }, - }, - isErr: true, - }, - { - description: "CheckboxタイプでOptionResponseが1000文字ピッタリな回答なのでエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Checkbox", - Body: null.String{}, - OptionResponse: []string{strings.Repeat("1234567890", 100)}, - }, - }, - }, - }, - { - description: "MultipleChoiceタイプで一般的な回答なのでエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "MultipleChoice", - Body: null.String{}, - OptionResponse: []string{"a", "b"}, - }, - }, - }, - }, - { - description: "MultipleChoiceタイプでOptionResponseがnilな回答なのでエラー", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "MultipleChoice", - Body: null.String{}, - OptionResponse: nil, - }, - }, - }, - isErr: true, - }, - { - description: "MultipleChoiceタイプでOptionResponseが1000文字以上な回答なのでエラー", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "MultipleChoice", - Body: null.String{}, - OptionResponse: []string{"0" + strings.Repeat("1234567890", 100)}, - }, - }, - }, - isErr: true, - }, - { - description: "MultipleChoiceタイプでOptionResponseが1000文字ピッタリな回答なのでエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "MultipleChoice", - Body: null.String{}, - OptionResponse: []string{strings.Repeat("1234567890", 100)}, - }, - }, - }, - }, - { - description: "一般的なLinearScaleタイプの回答なのでエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "LinearScale", - Body: null.String{}, - OptionResponse: nil, - }, - }, - }, - }, - { - description: "LinearScaleタイプでoptionResponseが1000文字以上でもエラー", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "LinearScale", - Body: null.String{}, - OptionResponse: []string{"0" + strings.Repeat("1234567890", 100)}, - }, - }, - }, - isErr: true, - }, - { - description: "LinearScaleタイプでoptionResponseが1000文字ピッタリなのでエラーなし", - request: &Responses{ - ID: 1, - Temporarily: false, - Body: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "LinearScale", - Body: null.String{}, - OptionResponse: []string{strings.Repeat("1234567890", 100)}, - }, - }, - }, - }, - } - - for _, test := range tests { - validate := validator.New() - t.Run(test.description, func(t *testing.T) { - err := validate.Struct(test.request) - - if test.isErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestPostResponse(t *testing.T) { - type responseRequestBody struct { - QuestionnaireID int `json:"questionnaireID" validate:"min=0"` - Temporarily bool `json:"temporarily"` - Body []responseBody `json:"body" validate:"required"` - Submitted_at time.Time `json:"submitted_at"` - } - type responseResponseBody struct { - Body []responseBody `json:"body" validate:"required"` - QuestionnaireID int `json:"questionnaireID" validate:"min=0"` - ResponseID int `json:"responseID" validate:"min=0"` - Temporarily bool `json:"temporarily"` - Submitted_at time.Time `json:"submitted_at"` - } - - t.Parallel() - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - nowTime := time.Now() - - questionnaireIDSuccess := 1 - questionIDSuccess := 1 - responseIDSuccess := 1 - - questionnaireIDFailure := 0 - questionIDFailure := 0 - responseIDFailure := 0 - - questionnaireIDLimit := 2 - - validation := - model.Validations{ - QuestionID: questionIDSuccess, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - } - scalelabel := - model.ScaleLabels{ - QuestionID: questionIDSuccess, - ScaleLabelRight: "そう思わない", - ScaleLabelLeft: "そう思う", - ScaleMin: 1, - ScaleMax: 5, - } - // questionnaireIDNotFound := -1 - // questionIDNotFound := -1 - // responseIDNotFound := -1 - - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockValidation := mock_model.NewMockIValidation(ctrl) - mockScaleLabel := mock_model.NewMockIScaleLabel(ctrl) - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockResponse := mock_model.NewMockIResponse(ctrl) - - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - mockQuestion := mock_model.NewMockIQuestion(ctrl) - - r := NewResponse( - mockQuestionnaire, - mockValidation, - mockScaleLabel, - mockRespondent, - mockResponse, - ) - m := NewMiddleware( - mockAdministrator, - mockRespondent, - mockQuestion, - mockQuestionnaire, - ) - // Questionnaire - // GetQuestionnaireLimit - // success - mockQuestionnaire.EXPECT(). - GetQuestionnaireLimit(gomock.Any(), questionnaireIDSuccess). - Return(null.TimeFrom(nowTime.Add(time.Minute)), nil).AnyTimes() - // failure - mockQuestionnaire.EXPECT(). - GetQuestionnaireLimit(gomock.Any(), questionnaireIDFailure). - Return(null.NewTime(time.Time{}, false), model.ErrRecordNotFound).AnyTimes() - // limit - mockQuestionnaire.EXPECT(). - GetQuestionnaireLimit(gomock.Any(), questionnaireIDLimit). - Return(null.TimeFrom(nowTime.Add(-time.Minute)), nil).AnyTimes() - - // Validation - // GetValidations - // success - mockValidation.EXPECT(). - GetValidations(gomock.Any(), []int{questionIDSuccess}). - Return([]model.Validations{validation}, nil).AnyTimes() - // failure - mockValidation.EXPECT(). - GetValidations(gomock.Any(), []int{questionIDFailure}). - Return([]model.Validations{}, nil).AnyTimes() - // nothing - mockValidation.EXPECT(). - GetValidations(gomock.Any(), []int{}). - Return([]model.Validations{}, nil).AnyTimes() - // CheckNumberValidation - // success - mockValidation.EXPECT(). - CheckNumberValidation(validation, "success case"). - Return(nil).AnyTimes() - // ErrInvalidNumber - mockValidation.EXPECT(). - CheckNumberValidation(validation, "ErrInvalidNumber"). - Return(model.ErrInvalidNumber).AnyTimes() - // BadRequest - mockValidation.EXPECT(). - CheckNumberValidation(validation, "BadRequest"). - Return(errMock).AnyTimes() - - // CheckTextValidation - // success - mockValidation.EXPECT(). - CheckTextValidation(validation, "success case"). - Return(nil).AnyTimes() - // ErrTextMatching - mockValidation.EXPECT(). - CheckTextValidation(validation, "ErrTextMatching"). - Return(model.ErrTextMatching).AnyTimes() - // InternalServerError - mockValidation.EXPECT(). - CheckTextValidation(validation, "InternalServerError"). - Return(errMock).AnyTimes() - - // ScaleLabel - // GetScaleLabels - // success - mockScaleLabel.EXPECT(). - GetScaleLabels(gomock.Any(), []int{questionIDSuccess}). - Return([]model.ScaleLabels{scalelabel}, nil).AnyTimes() - // failure - mockScaleLabel.EXPECT(). - GetScaleLabels(gomock.Any(), []int{questionIDFailure}). - Return([]model.ScaleLabels{}, nil).AnyTimes() - // nothing - mockScaleLabel.EXPECT(). - GetScaleLabels(gomock.Any(), []int{}). - Return([]model.ScaleLabels{}, nil).AnyTimes() - - // CheckScaleLabel - // success - mockScaleLabel.EXPECT(). - CheckScaleLabel(scalelabel, "success case"). - Return(nil).AnyTimes() - // BadRequest - mockScaleLabel.EXPECT(). - CheckScaleLabel(scalelabel, "BadRequest"). - Return(errMock).AnyTimes() - - // Respondent - // InsertRespondent - // success - mockRespondent.EXPECT(). - InsertRespondent(gomock.Any(), string(userOne), questionnaireIDSuccess, gomock.Any()). - Return(responseIDSuccess, nil).AnyTimes() - // failure - mockRespondent.EXPECT(). - InsertRespondent(gomock.Any(), string(userOne), questionnaireIDFailure, gomock.Any()). - Return(responseIDFailure, nil).AnyTimes() - - // Response - // InsertResponses - // success - mockResponse.EXPECT(). - InsertResponses(gomock.Any(), responseIDSuccess, gomock.Any()). - Return(nil).AnyTimes() - // failure - mockResponse.EXPECT(). - InsertResponses(gomock.Any(), responseIDFailure, gomock.Any()). - Return(errMock).AnyTimes() - - // responseID, err := mockRespondent. - // InsertRespondent(string(userOne), 1, null.NewTime(nowTime, true)) - // assertion.Equal(1, responseID) - // assertion.NoError(err) - - type request struct { - user users - isBadRequestBody bool - requestBody responseRequestBody - } - type expect struct { - isErr bool - code int - responseID int - } - type test struct { - description string - request - expect - } - testCases := []test{ - { - description: "success", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Submitted_at: time.Now(), - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("success case"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusCreated, - responseID: responseIDSuccess, - }, - }, - { - description: "true Temporarily", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: true, - Submitted_at: time.Time{}, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("success case"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusCreated, - responseID: responseIDSuccess, - }, - }, - { - description: "null submittedat", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Submitted_at: time.Now(), - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("success case"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusCreated, - responseID: responseIDSuccess, - }, - }, - { - description: "empty body", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Submitted_at: time.Now(), - Body: []responseBody{}, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusCreated, - responseID: responseIDSuccess, - }, - }, - { - description: "questionnaire does not exist", - request: request{ - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDFailure, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusBadRequest, - }, - }, - { - description: "bad request body", - request: request{ - isBadRequestBody: true, - }, - expect: expect{ - isErr: true, - code: http.StatusBadRequest, - }, - }, - { - description: "limit exceeded", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDLimit, - Temporarily: false, - Submitted_at: time.Now(), - Body: []responseBody{}, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusMethodNotAllowed, - }, - }, - { - description: "valid number", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Submitted_at: time.Now(), - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Number", - Body: null.StringFrom("success case"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusCreated, - responseID: responseIDSuccess, - }, - }, - { - description: "invalid number", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Submitted_at: time.Now(), - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Number", - Body: null.StringFrom("ErrInvalidNumber"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusInternalServerError, - }, - }, - { - description: "BadRequest number", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Submitted_at: time.Now(), - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Number", - Body: null.StringFrom("BadRequest"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusBadRequest, - }, - }, - { - description: "valid text", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Submitted_at: time.Now(), - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("success case"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusCreated, - responseID: responseIDSuccess, - }, - }, - { - description: "text does not match", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Submitted_at: time.Now(), - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("ErrTextMatching"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusBadRequest, - }, - }, - { - description: "invalid text", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Submitted_at: time.Now(), - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("InternalServerError"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusInternalServerError, - }, - }, - { - description: "valid LinearScale", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Submitted_at: time.Now(), - Temporarily: false, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "LinearScale", - Body: null.StringFrom("success case"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusCreated, - responseID: responseIDSuccess, - }, - }, - { - description: "invalid LinearScale", - request: request{ - user: userOne, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Submitted_at: time.Now(), - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "LinearScale", - Body: null.StringFrom("BadRequest"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusBadRequest, - }, - }, - } - - e := echo.New() - e.POST("/api/responses", r.PostResponse, m.SetUserIDMiddleware, m.SetValidatorMiddleware, m.TraPMemberAuthenticate) - - for _, testCase := range testCases { - requestByte, jsonErr := json.Marshal(testCase.request.requestBody) - require.NoError(t, jsonErr) - requestStr := string(requestByte) + "\n" - - if testCase.request.isBadRequestBody { - requestStr = "badRequestBody" - } - rec := createRecorder(e, testCase.request.user, methodPost, makePath("/responses"), typeJSON, requestStr) - - assertion.Equal(testCase.expect.code, rec.Code, testCase.description, "status code") - if rec.Code < 200 || rec.Code >= 300 { - continue - } - - response := responseResponseBody{ - ResponseID: testCase.expect.responseID, - QuestionnaireID: testCase.request.requestBody.QuestionnaireID, - Temporarily: testCase.request.requestBody.Temporarily, - Body: testCase.request.requestBody.Body, - Submitted_at: testCase.request.requestBody.Submitted_at, - } - var resActual responseResponseBody - - err := json.NewDecoder(rec.Body).Decode(&resActual) - if err != nil { - t.Errorf("failed to decode response body: %v", err) - } - assertion.Equal(response.ResponseID, resActual.ResponseID, "ResponseID") - assertion.Equal(response.QuestionnaireID, resActual.QuestionnaireID, "QuestionnaireID") - assertion.Equal(response.Temporarily, response.Temporarily, "Temporarily") - assertion.Equal(response.Body, resActual.Body, "Body") - assertion.WithinDuration(response.Submitted_at, resActual.Submitted_at, time.Second*2, "submitted_at") - } -} - -func TestGetResponse(t *testing.T) { - - type responseResponseBody struct { - QuestionnaireID int `json:"questionnaireID"` - SubmittedAt null.Time `json:"submitted_at"` - ModifiedAt null.Time `json:"modified_at"` - Body []responseBody `json:"body"` - } - - t.Parallel() - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - nowTime := time.Now() - - responseIDSuccess := 1 - responseIDFailure := 0 - responseIDNotFound := -1 - - questionnaireIDSuccess := 1 - questionIDSuccess := 1 - respondentDetail := model.RespondentDetail{ - QuestionnaireID: questionnaireIDSuccess, - SubmittedAt: null.TimeFrom(nowTime), - ModifiedAt: nowTime, - Responses: []model.ResponseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("回答"), - OptionResponse: []string{}, - }, - }, - } - - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockValidation := mock_model.NewMockIValidation(ctrl) - mockScaleLabel := mock_model.NewMockIScaleLabel(ctrl) - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockResponse := mock_model.NewMockIResponse(ctrl) - - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - mockQuestion := mock_model.NewMockIQuestion(ctrl) - - r := NewResponse( - mockQuestionnaire, - mockValidation, - mockScaleLabel, - mockRespondent, - mockResponse, - ) - m := NewMiddleware( - mockAdministrator, - mockRespondent, - mockQuestion, - mockQuestionnaire, - ) - - // Respondent - // InsertRespondent - // success - mockRespondent.EXPECT(). - GetRespondentDetail(gomock.Any(), responseIDSuccess). - Return(respondentDetail, nil).AnyTimes() - // failure - mockRespondent.EXPECT(). - GetRespondentDetail(gomock.Any(), responseIDFailure). - Return(model.RespondentDetail{}, errMock).AnyTimes() - // NotFound - mockRespondent.EXPECT(). - GetRespondentDetail(gomock.Any(), responseIDNotFound). - Return(model.RespondentDetail{}, model.ErrRecordNotFound).AnyTimes() - - type request struct { - user users - responseID int - } - type expect struct { - isErr bool - code int - response responseResponseBody - } - - type test struct { - description string - request - expect - } - testCases := []test{ - { - description: "success", - request: request{ - responseID: responseIDSuccess, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - response: responseResponseBody{ - QuestionnaireID: questionnaireIDSuccess, - SubmittedAt: null.TimeFrom(nowTime), - ModifiedAt: null.TimeFrom(nowTime), - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("回答"), - OptionResponse: []string{}, - }, - }, - }, - }, - }, - { - description: "failure", - request: request{ - responseID: responseIDFailure, - }, - expect: expect{ - isErr: true, - code: http.StatusInternalServerError, - }, - }, - { - description: "NotFound", - request: request{ - responseID: responseIDNotFound, - }, - expect: expect{ - isErr: true, - code: http.StatusNotFound, - }, - }, - } - - e := echo.New() - e.GET("/api/responses/:responseID", r.GetResponse, m.SetUserIDMiddleware, m.TraPMemberAuthenticate) - - for _, testCase := range testCases { - - rec := createRecorder(e, testCase.request.user, methodGet, fmt.Sprint(rootPath, "/responses/", testCase.request.responseID), typeNone, "") - - assertion.Equal(testCase.expect.code, rec.Code, testCase.description, "status code") - if rec.Code < 200 || rec.Code >= 300 { - continue - } - - responseByte, jsonErr := json.Marshal(testCase.expect.response) - require.NoError(t, jsonErr) - responseStr := string(responseByte) + "\n" - assertion.Equal(responseStr, rec.Body.String(), testCase.description, "responseBody") - } -} - -func TestEditResponse(t *testing.T) { - type responseRequestBody struct { - QuestionnaireID int `json:"questionnaireID"` - Temporarily bool `json:"temporarily"` - Body []responseBody `json:"body"` - } - type responseResponseBody struct { - Body []responseBody `json:"body"` - QuestionnaireID int `json:"questionnaireID"` - ResponseID int `json:"responseID"` - Temporarily bool `json:"temporarily"` - } - - t.Parallel() - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - nowTime := time.Now() - - questionnaireIDSuccess := 1 - questionIDSuccess := 1 - responseIDSuccess := 1 - - questionnaireIDFailure := 0 - questionIDFailure := 0 - responseIDFailure := 0 - - questionnaireIDLimit := 2 - - validation := - model.Validations{ - QuestionID: questionIDSuccess, - RegexPattern: "^\\d*\\.\\d*$", - MinBound: "0", - MaxBound: "10", - } - scalelabel := - model.ScaleLabels{ - QuestionID: questionIDSuccess, - ScaleLabelRight: "そう思わない", - ScaleLabelLeft: "そう思う", - ScaleMin: 1, - ScaleMax: 5, - } - // questionnaireIDNotFound := -1 - // questionIDNotFound := -1 - // responseIDNotFound := -1 - - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockValidation := mock_model.NewMockIValidation(ctrl) - mockScaleLabel := mock_model.NewMockIScaleLabel(ctrl) - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockResponse := mock_model.NewMockIResponse(ctrl) - - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - mockQuestion := mock_model.NewMockIQuestion(ctrl) - - r := NewResponse( - mockQuestionnaire, - mockValidation, - mockScaleLabel, - mockRespondent, - mockResponse, - ) - m := NewMiddleware( - mockAdministrator, - mockRespondent, - mockQuestion, - mockQuestionnaire, - ) - // Questionnaire - // GetQuestionnaireLimit - // success - mockQuestionnaire.EXPECT(). - GetQuestionnaireLimit(gomock.Any(), questionnaireIDSuccess). - Return(null.TimeFrom(nowTime.Add(time.Minute)), nil).AnyTimes() - // failure - mockQuestionnaire.EXPECT(). - GetQuestionnaireLimit(gomock.Any(), questionnaireIDFailure). - Return(null.NewTime(time.Time{}, false), errMock).AnyTimes() - // limit - mockQuestionnaire.EXPECT(). - GetQuestionnaireLimit(gomock.Any(), questionnaireIDLimit). - Return(null.TimeFrom(nowTime.Add(-time.Minute)), nil).AnyTimes() - - // Validation - // GetValidations - // success - mockValidation.EXPECT(). - GetValidations(gomock.Any(), []int{questionIDSuccess}). - Return([]model.Validations{validation}, nil).AnyTimes() - // failure - mockValidation.EXPECT(). - GetValidations(gomock.Any(), []int{questionIDFailure}). - Return([]model.Validations{}, nil).AnyTimes() - // nothing - mockValidation.EXPECT(). - GetValidations(gomock.Any(), []int{}). - Return([]model.Validations{}, nil).AnyTimes() - // CheckNumberValidation - // success - mockValidation.EXPECT(). - CheckNumberValidation(validation, "success case"). - Return(nil).AnyTimes() - // ErrInvalidNumber - mockValidation.EXPECT(). - CheckNumberValidation(validation, "ErrInvalidNumber"). - Return(model.ErrInvalidNumber).AnyTimes() - // BadRequest - mockValidation.EXPECT(). - CheckNumberValidation(validation, "BadRequest"). - Return(errMock).AnyTimes() - - // CheckTextValidation - // success - mockValidation.EXPECT(). - CheckTextValidation(validation, "success case"). - Return(nil).AnyTimes() - // ErrTextMatching - mockValidation.EXPECT(). - CheckTextValidation(validation, "ErrTextMatching"). - Return(model.ErrTextMatching).AnyTimes() - // InternalServerError - mockValidation.EXPECT(). - CheckTextValidation(validation, "InternalServerError"). - Return(errMock).AnyTimes() - - // ScaleLabel - // GetScaleLabels - // success - mockScaleLabel.EXPECT(). - GetScaleLabels(gomock.Any(), []int{questionIDSuccess}). - Return([]model.ScaleLabels{scalelabel}, nil).AnyTimes() - // failure - mockScaleLabel.EXPECT(). - GetScaleLabels(gomock.Any(), []int{questionIDFailure}). - Return([]model.ScaleLabels{}, nil).AnyTimes() - // nothing - mockScaleLabel.EXPECT(). - GetScaleLabels(gomock.Any(), []int{}). - Return([]model.ScaleLabels{}, nil).AnyTimes() - - // CheckScaleLabel - // success - mockScaleLabel.EXPECT(). - CheckScaleLabel(scalelabel, "success case"). - Return(nil).AnyTimes() - // BadRequest - mockScaleLabel.EXPECT(). - CheckScaleLabel(scalelabel, "BadRequest"). - Return(errMock).AnyTimes() - - // Respondent - // InsertRespondent - // success - mockRespondent.EXPECT(). - InsertRespondent(gomock.Any(), string(userOne), questionnaireIDSuccess, gomock.Any()). - Return(responseIDSuccess, nil).AnyTimes() - // failure - mockRespondent.EXPECT(). - InsertRespondent(gomock.Any(), string(userOne), questionnaireIDFailure, gomock.Any()). - Return(responseIDFailure, nil).AnyTimes() - // UpdateSubmittedAt - // success - mockRespondent.EXPECT(). - UpdateSubmittedAt(gomock.Any(), gomock.Any()). - Return(nil).AnyTimes() - - // Response - // InsertResponses - // success - mockResponse.EXPECT(). - InsertResponses(gomock.Any(), responseIDSuccess, gomock.Any()). - Return(nil).AnyTimes() - // failure - mockResponse.EXPECT(). - InsertResponses(gomock.Any(), responseIDFailure, gomock.Any()). - Return(errMock).AnyTimes() - // DeleteResponse - // success - mockResponse.EXPECT(). - DeleteResponse(gomock.Any(), responseIDSuccess). - Return(nil).AnyTimes() - // failure - mockResponse.EXPECT(). - DeleteResponse(gomock.Any(), responseIDFailure). - Return(model.ErrNoRecordDeleted).AnyTimes() - - // responseID, err := mockRespondent. - // InsertRespondent(string(userOne), 1, null.NewTime(nowTime, true)) - // assertion.Equal(1, responseID) - // assertion.NoError(err) - - type request struct { - user users - responseID int - isBadRequestBody bool - requestBody responseRequestBody - } - type expect struct { - isErr bool - code int - } - type test struct { - description string - request - expect - } - testCases := []test{ - { - description: "success", - request: request{ - user: userOne, - responseID: responseIDSuccess, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("success case"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - }, - }, - { - description: "true Temporarily", - request: request{ - user: userOne, - responseID: responseIDSuccess, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: true, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("success case"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - }, - }, - { - description: "empty body", - request: request{ - user: userOne, - responseID: responseIDSuccess, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Body: []responseBody{}, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - }, - }, - { - description: "bad request body", - request: request{ - isBadRequestBody: true, - responseID: responseIDSuccess, - }, - expect: expect{ - isErr: true, - code: http.StatusBadRequest, - }, - }, - { - description: "limit exceeded", - request: request{ - user: userOne, - responseID: responseIDSuccess, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDLimit, - Temporarily: false, - Body: []responseBody{}, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusMethodNotAllowed, - }, - }, - { - description: "valid number", - request: request{ - user: userOne, - responseID: responseIDSuccess, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Number", - Body: null.StringFrom("success case"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - }, - }, - { - description: "invalid number", - request: request{ - user: userOne, - responseID: responseIDSuccess, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Number", - Body: null.StringFrom("ErrInvalidNumber"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusInternalServerError, - }, - }, - { - description: "BadRequest number", - request: request{ - user: userOne, - responseID: responseIDSuccess, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Number", - Body: null.StringFrom("BadRequest"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusBadRequest, - }, - }, - { - description: "valid text", - request: request{ - user: userOne, - responseID: responseIDSuccess, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("success case"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - }, - }, - { - description: "text does not match", - request: request{ - user: userOne, - responseID: responseIDSuccess, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("ErrTextMatching"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusBadRequest, - }, - }, - { - description: "invalid text", - request: request{ - user: userOne, - responseID: responseIDSuccess, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("InternalServerError"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusInternalServerError, - }, - }, - { - description: "valid LinearScale", - request: request{ - user: userOne, - responseID: responseIDSuccess, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "LinearScale", - Body: null.StringFrom("success case"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - }, - }, - { - description: "invalid LinearScale", - request: request{ - user: userOne, - responseID: responseIDSuccess, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "LinearScale", - Body: null.StringFrom("BadRequest"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusBadRequest, - }, - }, - { - description: "response does not exist", - request: request{ - user: userOne, - responseID: responseIDFailure, - requestBody: responseRequestBody{ - QuestionnaireID: questionnaireIDSuccess, - Temporarily: false, - Body: []responseBody{ - { - QuestionID: questionIDSuccess, - QuestionType: "Text", - Body: null.StringFrom("success case"), - OptionResponse: []string{}, - }, - }, - }, - }, - expect: expect{ - isErr: true, - code: http.StatusInternalServerError, //middlewareで弾くので500で良い - }, - }, - } - - e := echo.New() - e.PATCH("/api/responses/:responseID", r.EditResponse, m.SetUserIDMiddleware, m.SetValidatorMiddleware, m.TraPMemberAuthenticate, func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - responseID, err := strconv.Atoi(c.Param("responseID")) - if err != nil { - return c.JSON(http.StatusBadRequest, "responseID is not number") - } - - c.Set(responseIDKey, responseID) - return next(c) - } - }) - - for _, testCase := range testCases { - requestByte, jsonErr := json.Marshal(testCase.request.requestBody) - require.NoError(t, jsonErr) - requestStr := string(requestByte) + "\n" - - if testCase.request.isBadRequestBody { - requestStr = "badRequestBody" - } - rec := createRecorder(e, testCase.request.user, methodPatch, makePath(fmt.Sprint("/responses/", testCase.request.responseID)), typeJSON, requestStr) - - assertion.Equal(testCase.expect.code, rec.Code, testCase.description, "status code") - } -} - -func TestDeleteResponse(t *testing.T) { - t.Parallel() - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockValidation := mock_model.NewMockIValidation(ctrl) - mockScaleLabel := mock_model.NewMockIScaleLabel(ctrl) - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockResponse := mock_model.NewMockIResponse(ctrl) - - r := NewResponse( - mockQuestionnaire, - mockValidation, - mockScaleLabel, - mockRespondent, - mockResponse, - ) - - type request struct { - QuestionnaireLimit null.Time - GetQuestionnaireLimitError error - ExecutesDeletion bool - DeleteRespondentError error - DeleteResponseError error - } - type expect struct { - statusCode int - } - type test struct { - description string - request - expect - } - - testCases := []test{ - { - description: "期限が設定されていない、かつDeleteRespondentがエラーなしなので200", - request: request{ - QuestionnaireLimit: null.NewTime(time.Time{}, false), - GetQuestionnaireLimitError: nil, - ExecutesDeletion: true, - DeleteRespondentError: nil, - DeleteResponseError: nil, - }, - expect: expect{ - statusCode: http.StatusOK, - }, - }, - { - description: "期限前、かつDeleteRespondentがエラーなしなので200", - request: request{ - QuestionnaireLimit: null.NewTime(time.Now().AddDate(0, 0, 1), true), - GetQuestionnaireLimitError: nil, - ExecutesDeletion: true, - DeleteRespondentError: nil, - DeleteResponseError: nil, - }, - expect: expect{ - statusCode: http.StatusOK, - }, - }, - { - description: "期限後なので405", - request: request{ - QuestionnaireLimit: null.NewTime(time.Now().AddDate(0, 0, -1), true), - GetQuestionnaireLimitError: nil, - ExecutesDeletion: false, - DeleteRespondentError: nil, - DeleteResponseError: nil, - }, - expect: expect{ - statusCode: http.StatusMethodNotAllowed, - }, - }, - { - description: "GetQuestionnaireLimitByResponseIDがエラーRecordNotFoundを吐くので404", - request: request{ - QuestionnaireLimit: null.NewTime(time.Time{}, false), - GetQuestionnaireLimitError: model.ErrRecordNotFound, - ExecutesDeletion: false, - DeleteRespondentError: nil, - DeleteResponseError: nil, - }, - expect: expect{ - statusCode: http.StatusNotFound, - }, - }, - { - description: "GetQuestionnaireLimitByResponseIDがエラーを吐くので500", - request: request{ - QuestionnaireLimit: null.NewTime(time.Time{}, false), - GetQuestionnaireLimitError: errors.New("error"), - ExecutesDeletion: false, - DeleteRespondentError: nil, - DeleteResponseError: nil, - }, - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "DeleteRespondentがエラーを吐くので500", - request: request{ - QuestionnaireLimit: null.NewTime(time.Time{}, false), - GetQuestionnaireLimitError: nil, - ExecutesDeletion: true, - DeleteRespondentError: errors.New("error"), - DeleteResponseError: nil, - }, - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "DeleteResponseがエラーを吐くので500", - request: request{ - QuestionnaireLimit: null.NewTime(time.Time{}, false), - GetQuestionnaireLimitError: nil, - ExecutesDeletion: true, - DeleteRespondentError: nil, - DeleteResponseError: errors.New("error"), - }, - expect: expect{ - statusCode: http.StatusInternalServerError, - }, - }, - } - - for _, testCase := range testCases { - userID := "userID1" - responseID := 1 - - e := echo.New() - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/responses/%d", responseID), nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - c.SetPath("/responses/:responseID") - c.SetParamNames("responseID") - c.SetParamValues(strconv.Itoa(responseID)) - c.Set(userIDKey, userID) - c.Set(responseIDKey, responseID) - - mockQuestionnaire. - EXPECT(). - GetQuestionnaireLimitByResponseID(gomock.Any(), responseID). - Return(testCase.request.QuestionnaireLimit, testCase.request.GetQuestionnaireLimitError) - if testCase.request.ExecutesDeletion { - mockRespondent. - EXPECT(). - DeleteRespondent(gomock.Any(), responseID). - Return(testCase.request.DeleteRespondentError) - if testCase.request.DeleteRespondentError == nil { - mockResponse. - EXPECT(). - DeleteResponse(c.Request().Context(), responseID). - Return(testCase.request.DeleteResponseError) - } - } - - e.HTTPErrorHandler(r.DeleteResponse(c), c) - - assertion.Equal(testCase.expect.statusCode, rec.Code, testCase.description, "status code") - } -} diff --git a/router/results.go b/router/results.go deleted file mode 100644 index 829c20f0..00000000 --- a/router/results.go +++ /dev/null @@ -1,43 +0,0 @@ -package router - -import ( - "net/http" - "strconv" - - "github.com/labstack/echo/v4" - "github.com/traPtitech/anke-to/model" -) - -// Result Resultの構造体 -type Result struct { - model.IRespondent - model.IQuestionnaire - model.IAdministrator -} - -// NewResult Resultのコンストラクタ -func NewResult(respondent model.IRespondent, questionnaire model.IQuestionnaire, administrator model.IAdministrator) *Result { - return &Result{ - IRespondent: respondent, - IQuestionnaire: questionnaire, - IAdministrator: administrator, - } -} - -// GetResults GET /results/:questionnaireID -func (r *Result) GetResults(c echo.Context) error { - sort := c.QueryParam("sort") - questionnaireID, err := strconv.Atoi(c.Param("questionnaireID")) - if err != nil { - c.Logger().Infof("failed to convert questionnaireID to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest) - } - - respondentDetails, err := r.GetRespondentDetails(c.Request().Context(), questionnaireID, sort) - if err != nil { - c.Logger().Errorf("failed to get respondent details: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - return c.JSON(http.StatusOK, respondentDetails) -} diff --git a/router/results_test.go b/router/results_test.go deleted file mode 100644 index 5f77272e..00000000 --- a/router/results_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package router - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/golang/mock/gomock" - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" - "github.com/traPtitech/anke-to/model" - "github.com/traPtitech/anke-to/model/mock_model" - "gopkg.in/guregu/null.v4" -) - -func TestGetResults(t *testing.T) { - t.Parallel() - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - - result := NewResult(mockRespondent, mockQuestionnaire, mockAdministrator) - - type request struct { - sortParam string - questionnaireIDParam string - questionnaireIDValid bool - questionnaireID int - respondentDetails []model.RespondentDetail - getRespondentDetailsError error - } - type response struct { - statusCode int - body string - } - type test struct { - description string - request - response - } - - textResponse := []model.RespondentDetail{ - { - ResponseID: 1, - TraqID: "mazrean", - QuestionnaireID: 1, - SubmittedAt: null.NewTime(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), true), - ModifiedAt: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), - Responses: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Text", - Body: null.NewString("テスト", true), - OptionResponse: nil, - }, - }, - }, - } - sb := strings.Builder{} - err := json.NewEncoder(&sb).Encode(textResponse) - if err != nil { - t.Errorf("failed to encode text response: %v", err) - return - } - - testCases := []test{ - { - description: "questionnaireIDが数字でないので400", - request: request{ - sortParam: "traqid", - questionnaireIDParam: "abc", - }, - response: response{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "questionnaireIDが空文字なので400", - request: request{ - sortParam: "traqid", - questionnaireIDParam: "", - }, - response: response{ - statusCode: http.StatusBadRequest, - }, - }, - { - description: "GetRespondentDetailsがエラーなので500", - request: request{ - sortParam: "traqid", - questionnaireIDValid: true, - questionnaireIDParam: "1", - questionnaireID: 1, - getRespondentDetailsError: fmt.Errorf("error"), - }, - response: response{ - statusCode: http.StatusInternalServerError, - }, - }, - { - description: "respondentDetailsがnilでも200", - request: request{ - sortParam: "traqid", - questionnaireIDValid: true, - questionnaireIDParam: "1", - questionnaireID: 1, - }, - response: response{ - statusCode: http.StatusOK, - body: "null\n", - }, - }, - { - description: "respondentDetailsがそのまま帰り200", - request: request{ - sortParam: "traqid", - questionnaireIDValid: true, - questionnaireIDParam: "1", - questionnaireID: 1, - respondentDetails: []model.RespondentDetail{ - { - ResponseID: 1, - TraqID: "mazrean", - QuestionnaireID: 1, - SubmittedAt: null.NewTime(time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), true), - ModifiedAt: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), - Responses: []model.ResponseBody{ - { - QuestionID: 1, - QuestionType: "Text", - Body: null.NewString("テスト", true), - OptionResponse: nil, - }, - }, - }, - }, - }, - response: response{ - statusCode: http.StatusOK, - body: sb.String(), - }, - }, - } - - for _, testCase := range testCases { - e := echo.New() - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/results/%s?sort=%s", testCase.request.questionnaireIDParam, testCase.request.sortParam), nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - c.SetPath("/results/:questionnaireID") - c.SetParamNames("questionnaireID", "sort") - c.SetParamValues(testCase.request.questionnaireIDParam, testCase.request.sortParam) - - if testCase.request.questionnaireIDValid { - mockRespondent. - EXPECT(). - GetRespondentDetails(c.Request().Context(), testCase.request.questionnaireID, testCase.request.sortParam). - Return(testCase.request.respondentDetails, testCase.request.getRespondentDetailsError) - } - - e.HTTPErrorHandler(result.GetResults(c), c) - assertion.Equalf(testCase.response.statusCode, rec.Code, testCase.description, "statusCode") - if testCase.response.statusCode == http.StatusOK { - assertion.Equalf(testCase.response.body, rec.Body.String(), testCase.description, "body") - } - } -} diff --git a/router/users.go b/router/users.go deleted file mode 100644 index 896d0276..00000000 --- a/router/users.go +++ /dev/null @@ -1,268 +0,0 @@ -package router - -import ( - "fmt" - "net/http" - "strconv" - "time" - - "github.com/labstack/echo/v4" - "gopkg.in/guregu/null.v4" - - "github.com/traPtitech/anke-to/model" -) - -// User Userの構造体 -type User struct { - model.IRespondent - model.IQuestionnaire - model.ITarget - model.IAdministrator -} - -type UserQueryparam struct { - Sort string `validate:"omitempty,oneof=created_at -created_at title -title modified_at -modified_at"` - Answered string `validate:"omitempty,oneof=answered unanswered"` -} - -// NewUser Userのコンストラクタ -func NewUser(respondent model.IRespondent, questionnaire model.IQuestionnaire, target model.ITarget, administrator model.IAdministrator) *User { - return &User{ - IRespondent: respondent, - IQuestionnaire: questionnaire, - ITarget: target, - IAdministrator: administrator, - } -} - -// GetUsersMe GET /users/me -func (*User) GetUsersMe(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - return c.JSON(http.StatusOK, map[string]interface{}{ - "traqID": userID, - }) -} - -// GetMyResponses GET /users/me/responses -func (u *User) GetMyResponses(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - myResponses, err := u.GetRespondentInfos(c.Request().Context(), userID) - if err != nil { - c.Logger().Errorf("failed to get respondentInfos: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - return c.JSON(http.StatusOK, myResponses) -} - -// GetMyResponsesByID GET /users/me/responses/:questionnaireID -func (u *User) GetMyResponsesByID(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - questionnaireID, err := strconv.Atoi(c.Param("questionnaireID")) - if err != nil { - c.Logger().Infof("failed to convert questionnaireID to int: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest) - } - - myresponses, err := u.GetRespondentInfos(c.Request().Context(), userID, questionnaireID) - if err != nil { - c.Logger().Errorf("failed to get respondentInfos: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - return c.JSON(http.StatusOK, myresponses) -} - -// GetTargetedQuestionnaire GET /users/me/targeted -func (u *User) GetTargetedQuestionnaire(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - sort := c.QueryParam("sort") - ret, err := u.GetTargettedQuestionnaires(c.Request().Context(), userID, "", sort) - if err != nil { - c.Logger().Errorf("failed to get targetedQuestionnaires: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - return c.JSON(http.StatusOK, ret) -} - -// GetMyQuestionnaire GET /users/me/administrates -func (u *User) GetMyQuestionnaire(c echo.Context) error { - userID, err := getUserID(c) - if err != nil { - c.Logger().Errorf("failed to get userID: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) - } - - // 自分が管理者になっているアンケート一覧 - questionnaires, err := u.GetAdminQuestionnaires(c.Request().Context(), userID) - if err != nil { - c.Logger().Errorf("failed to get adminQuestionnaires: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaires: %w", err)) - } - - questionnaireIDs := make([]int, 0, len(questionnaires)) - for _, questionnaire := range questionnaires { - questionnaireIDs = append(questionnaireIDs, questionnaire.ID) - } - - targets, err := u.GetTargets(c.Request().Context(), questionnaireIDs) - if err != nil { - c.Logger().Errorf("failed to get targets: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get targets: %w", err)) - } - targetMap := map[int][]string{} - for _, target := range targets { - tgts, ok := targetMap[target.QuestionnaireID] - if !ok { - targetMap[target.QuestionnaireID] = []string{target.UserTraqid} - } else { - targetMap[target.QuestionnaireID] = append(tgts, target.UserTraqid) - } - } - - respondents, err := u.GetRespondentsUserIDs(c.Request().Context(), questionnaireIDs) - if err != nil { - c.Logger().Errorf("failed to get respondentsUserIDs: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get respondents: %w", err)) - } - respondentMap := map[int][]string{} - for _, respondent := range respondents { - rspdts, ok := respondentMap[respondent.QuestionnaireID] - if !ok { - respondentMap[respondent.QuestionnaireID] = []string{respondent.UserTraqid} - } else { - respondentMap[respondent.QuestionnaireID] = append(rspdts, respondent.UserTraqid) - } - } - - administrators, err := u.GetAdministrators(c.Request().Context(), questionnaireIDs) - if err != nil { - c.Logger().Errorf("failed to get administrators: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get administrators: %w", err)) - } - administratorMap := map[int][]string{} - for _, administrator := range administrators { - admins, ok := administratorMap[administrator.QuestionnaireID] - if !ok { - administratorMap[administrator.QuestionnaireID] = []string{administrator.UserTraqid} - } else { - administratorMap[administrator.QuestionnaireID] = append(admins, administrator.UserTraqid) - } - } - - type QuestionnaireInfo struct { - ID int `json:"questionnaireID"` - Title string `json:"title"` - Description string `json:"description"` - ResTimeLimit null.Time `json:"res_time_limit"` - CreatedAt string `json:"created_at"` - ModifiedAt string `json:"modified_at"` - ResSharedTo string `json:"res_shared_to"` - AllResponded bool `json:"all_responded"` - Targets []string `json:"targets"` - Administrators []string `json:"administrators"` - Respondents []string `json:"respondents"` - } - ret := []QuestionnaireInfo{} - - for _, questionnaire := range questionnaires { - targets, ok := targetMap[questionnaire.ID] - if !ok { - targets = []string{} - } - - administrators, ok := administratorMap[questionnaire.ID] - if !ok { - administrators = []string{} - } - - respondents, ok := respondentMap[questionnaire.ID] - if !ok { - respondents = []string{} - } - - allresponded := true - for _, t := range targets { - found := false - for _, r := range respondents { - if t == r { - found = true - break - } - } - if !found { - allresponded = false - break - } - } - - ret = append(ret, QuestionnaireInfo{ - ID: questionnaire.ID, - Title: questionnaire.Title, - Description: questionnaire.Description, - ResTimeLimit: questionnaire.ResTimeLimit, - CreatedAt: questionnaire.CreatedAt.Format(time.RFC3339), - ModifiedAt: questionnaire.ModifiedAt.Format(time.RFC3339), - ResSharedTo: questionnaire.ResSharedTo, - AllResponded: allresponded, - Targets: targets, - Administrators: administrators, - Respondents: respondents, - }) - } - - return c.JSON(http.StatusOK, ret) -} - -// GetTargettedQuestionnairesBytraQID GET /users/:traQID/targeted -func (u *User) GetTargettedQuestionnairesBytraQID(c echo.Context) error { - traQID := c.Param("traQID") - sort := c.QueryParam("sort") - answered := c.QueryParam("answered") - - p := UserQueryparam{ - Sort: sort, - Answered: answered, - } - - validate, err := getValidator(c) - if err != nil { - c.Logger().Errorf("failed to get validator: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError) - } - - err = validate.StructCtx(c.Request().Context(), p) - if err != nil { - c.Logger().Infof("failed to validate: %+v", err) - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - ret, err := u.GetTargettedQuestionnaires(c.Request().Context(), traQID, answered, sort) - if err != nil { - c.Logger().Errorf("failed to get targetted questionnaires: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, err) - } - - return c.JSON(http.StatusOK, ret) -} diff --git a/router/users_test.go b/router/users_test.go deleted file mode 100644 index 4738232d..00000000 --- a/router/users_test.go +++ /dev/null @@ -1,951 +0,0 @@ -package router - -import ( - "encoding/json" - "fmt" - "github.com/go-playground/validator/v10" - "net/http" - "testing" - "time" - - "github.com/golang/mock/gomock" - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/traPtitech/anke-to/model" - "github.com/traPtitech/anke-to/model/mock_model" - "gopkg.in/guregu/null.v4" - "gorm.io/gorm" -) - -type myResponse struct { - Title string `json:"questionnaire_title"` - Description string `json:"description"` - ResTimeLimit null.Time `json:"res_time_limit"` - ResponseID int `json:"responseID"` - QuestionnaireID int `json:"questionnaireID"` - ModifiedAt time.Time `json:"modified_at"` - SubmittedAt null.Time `json:"submitted_at"` - DeletedAt null.Time `json:"deleted_at"` -} - -type targettedQuestionnaire struct { - QuestionnaireID int `json:"questionnaireID"` - Title string `json:"title"` - Description string `json:"description"` - ResTimeLimit null.Time `json:"res_time_limit"` - DeletedAt null.Time `json:"deleted_at"` - ResSharedTo string `json:"res_shared_to"` - CreatedAt time.Time `json:"created_at"` - ModifiedAt time.Time `json:"modified_at"` - RespondedAt null.Time `json:"responded_at"` - HasResponse bool `json:"has_response"` -} - -func TestGetTargettedQuestionnairesBytraQIDValidate(t *testing.T) { - t.Parallel() - - tests := []struct { - description string - request *UserQueryparam - isErr bool - }{ - { - description: "一般的なQueryParameterなのでエラーなし", - request: &UserQueryparam{ - Sort: "created_at", - Answered: "answered", - }, - }, - { - description: "Sortが-created_atでもエラーなし", - request: &UserQueryparam{ - Sort: "-created_at", - Answered: "answered", - }, - }, - { - description: "Sortがtitleでもエラーなし", - request: &UserQueryparam{ - Sort: "title", - Answered: "answered", - }, - }, - { - description: "Sortが-titleでもエラーなし", - request: &UserQueryparam{ - Sort: "-title", - Answered: "answered", - }, - }, - { - description: "Sortがmodified_atでもエラーなし", - request: &UserQueryparam{ - Sort: "modified_at", - Answered: "answered", - }, - }, - { - description: "Sortが-modified_atでもエラーなし", - request: &UserQueryparam{ - Sort: "-modified_at", - Answered: "answered", - }, - }, - { - description: "Answeredがunansweredでもエラーなし", - request: &UserQueryparam{ - Sort: "created_at", - Answered: "unanswered", - }, - }, - { - description: "Sortが空文字でもエラーなし", - request: &UserQueryparam{ - Sort: "", - Answered: "answered", - }, - }, - { - description: "Answeredが空文字でもエラーなし", - request: &UserQueryparam{ - Sort: "created_at", - Answered: "", - }, - }, - { - description: "Sortが指定された文字列ではないためエラー", - request: &UserQueryparam{ - Sort: "sort", - Answered: "answered", - }, - isErr: true, - }, - { - description: "Answeredが指定された文字列ではないためエラー", - request: &UserQueryparam{ - Sort: "created_at", - Answered: "answer", - }, - isErr: true, - }, - } - - for _, test := range tests { - validate := validator.New() - - t.Run(test.description, func(t *testing.T) { - err := validate.Struct(test.request) - if test.isErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestGetUsersMe(t *testing.T) { - - type meResponseBody struct { - TraqID string `json:"traqID"` - } - - t.Parallel() - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockTarget := mock_model.NewMockITarget(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - - mockQuestion := mock_model.NewMockIQuestion(ctrl) - - u := NewUser( - mockRespondent, - mockQuestionnaire, - mockTarget, - mockAdministrator, - ) - m := NewMiddleware( - mockAdministrator, - mockRespondent, - mockQuestion, - mockQuestionnaire, - ) - - type request struct { - user users - } - type expect struct { - isErr bool - code int - response meResponseBody - } - - type test struct { - description string - request - expect - } - testCases := []test{ - { - description: "success", - request: request{ - user: userOne, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - response: meResponseBody{ - string(userOne), - }, - }, - }, - } - - e := echo.New() - e.GET("api/users/me", u.GetUsersMe, m.SetUserIDMiddleware, m.TraPMemberAuthenticate) - - for _, testCase := range testCases { - rec := createRecorder(e, testCase.request.user, methodGet, makePath("/users/me"), typeNone, "") - - assertion.Equal(testCase.expect.code, rec.Code, testCase.description, "status code") - if rec.Code < 200 || rec.Code >= 300 { - continue - } - - responseByte, jsonErr := json.Marshal(testCase.expect.response) - require.NoError(t, jsonErr) - responseStr := string(responseByte) + "\n" - assertion.Equal(responseStr, rec.Body.String(), testCase.description, "responseBody") - } -} - -func TestGetMyResponses(t *testing.T) { - - t.Parallel() - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - nowTime := time.Now() - - responseID1 := 1 - questionnaireID1 := 1 - responseID2 := 2 - questionnaireID2 := 2 - responseID3 := 3 - questionnaireID3 := 3 - myResponses := []myResponse{ - { - ResponseID: responseID1, - QuestionnaireID: questionnaireID1, - Title: "質問1", - Description: "質問1 description", - ResTimeLimit: null.NewTime(nowTime, false), - SubmittedAt: null.TimeFrom(nowTime), - ModifiedAt: nowTime, - }, - { - ResponseID: responseID2, - QuestionnaireID: questionnaireID2, - Title: "質問2", - Description: "質問2 description", - ResTimeLimit: null.NewTime(nowTime, false), - SubmittedAt: null.TimeFrom(nowTime), - ModifiedAt: nowTime, - }, - { - ResponseID: responseID3, - QuestionnaireID: questionnaireID3, - Title: "質問3", - Description: "質問3 description", - ResTimeLimit: null.NewTime(nowTime, false), - SubmittedAt: null.TimeFrom(nowTime), - ModifiedAt: nowTime, - }, - } - respondentInfos := []model.RespondentInfo{ - { - Title: "質問1", - Description: "質問1 description", - ResTimeLimit: null.NewTime(nowTime, false), - Respondents: model.Respondents{ - ResponseID: responseID1, - QuestionnaireID: questionnaireID1, - SubmittedAt: null.TimeFrom(nowTime), - ModifiedAt: nowTime, - }, - }, - { - Title: "質問2", - Description: "質問2 description", - ResTimeLimit: null.NewTime(nowTime, false), - Respondents: model.Respondents{ - ResponseID: responseID2, - QuestionnaireID: questionnaireID2, - SubmittedAt: null.TimeFrom(nowTime), - ModifiedAt: nowTime, - }, - }, - { - Title: "質問3", - Description: "質問3 description", - ResTimeLimit: null.NewTime(nowTime, false), - Respondents: model.Respondents{ - ResponseID: responseID3, - QuestionnaireID: questionnaireID3, - SubmittedAt: null.TimeFrom(nowTime), - ModifiedAt: nowTime, - }, - }, - } - - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockTarget := mock_model.NewMockITarget(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - - mockQuestion := mock_model.NewMockIQuestion(ctrl) - - u := NewUser( - mockRespondent, - mockQuestionnaire, - mockTarget, - mockAdministrator, - ) - m := NewMiddleware( - mockAdministrator, - mockRespondent, - mockQuestion, - mockQuestionnaire, - ) - - // Respondent - // GetRespondentInfos - // success - mockRespondent.EXPECT(). - GetRespondentInfos(gomock.Any(), string(userOne)). - Return(respondentInfos, nil).AnyTimes() - // empty - mockRespondent.EXPECT(). - GetRespondentInfos(gomock.Any(), "empty"). - Return([]model.RespondentInfo{}, nil).AnyTimes() - // failure - mockRespondent.EXPECT(). - GetRespondentInfos(gomock.Any(), "StatusInternalServerError"). - Return(nil, errMock).AnyTimes() - - type request struct { - user users - } - type expect struct { - isErr bool - code int - response []myResponse - } - - type test struct { - description string - request - expect - } - testCases := []test{ - { - description: "success", - request: request{ - user: userOne, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - response: myResponses, - }, - }, - { - description: "empty", - request: request{ - user: "empty", - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - response: []myResponse{}, - }, - }, - { - description: "StatusInternalServerError", - request: request{ - user: "StatusInternalServerError", - }, - expect: expect{ - isErr: true, - code: http.StatusInternalServerError, - }, - }, - } - - e := echo.New() - e.GET("api/users/me/responses", u.GetMyResponses, m.SetUserIDMiddleware, m.TraPMemberAuthenticate) - - for _, testCase := range testCases { - rec := createRecorder(e, testCase.request.user, methodGet, makePath("/users/me/responses"), typeNone, "") - - assertion.Equal(testCase.expect.code, rec.Code, testCase.description, "status code") - if rec.Code < 200 || rec.Code >= 300 { - continue - } - - responseByte, jsonErr := json.Marshal(testCase.expect.response) - require.NoError(t, jsonErr) - responseStr := string(responseByte) + "\n" - assertion.Equal(responseStr, rec.Body.String(), testCase.description, "responseBody") - } -} - -func TestGetMyResponsesByID(t *testing.T) { - - t.Parallel() - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - nowTime := time.Now() - - responseID1 := 1 - responseID2 := 2 - questionnaireIDSuccess := 1 - questionnaireIDNotFound := -1 - myResponses := []myResponse{ - { - ResponseID: responseID1, - QuestionnaireID: questionnaireIDSuccess, - Title: "質問1", - Description: "質問1 description", - ResTimeLimit: null.NewTime(nowTime, false), - SubmittedAt: null.TimeFrom(nowTime), - ModifiedAt: nowTime, - }, - { - ResponseID: responseID2, - QuestionnaireID: questionnaireIDSuccess, - Title: "質問2", - Description: "質問2 description", - ResTimeLimit: null.NewTime(nowTime, false), - SubmittedAt: null.TimeFrom(nowTime), - ModifiedAt: nowTime, - }, - } - respondentInfos := []model.RespondentInfo{ - { - Title: "質問1", - Description: "質問1 description", - ResTimeLimit: null.NewTime(nowTime, false), - Respondents: model.Respondents{ - ResponseID: responseID1, - QuestionnaireID: questionnaireIDSuccess, - SubmittedAt: null.TimeFrom(nowTime), - ModifiedAt: nowTime, - }, - }, - { - Title: "質問2", - Description: "質問2 description", - ResTimeLimit: null.NewTime(nowTime, false), - Respondents: model.Respondents{ - ResponseID: responseID2, - QuestionnaireID: questionnaireIDSuccess, - SubmittedAt: null.TimeFrom(nowTime), - ModifiedAt: nowTime, - }, - }, - } - - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockTarget := mock_model.NewMockITarget(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - - mockQuestion := mock_model.NewMockIQuestion(ctrl) - - u := NewUser( - mockRespondent, - mockQuestionnaire, - mockTarget, - mockAdministrator, - ) - m := NewMiddleware( - mockAdministrator, - mockRespondent, - mockQuestion, - mockQuestionnaire, - ) - - // Respondent - // GetRespondentInfos - // success - mockRespondent.EXPECT(). - GetRespondentInfos(gomock.Any(), string(userOne), questionnaireIDSuccess). - Return(respondentInfos, nil).AnyTimes() - // questionnaireIDNotFound - mockRespondent.EXPECT(). - GetRespondentInfos(gomock.Any(), string(userOne), questionnaireIDNotFound). - Return([]model.RespondentInfo{}, nil).AnyTimes() - // failure - mockRespondent.EXPECT(). - GetRespondentInfos(gomock.Any(), "StatusInternalServerError", questionnaireIDSuccess). - Return(nil, errMock).AnyTimes() - - type request struct { - user users - questionnaireID int - isBadParam bool - } - type expect struct { - isErr bool - code int - response []myResponse - } - - type test struct { - description string - request - expect - } - testCases := []test{ - { - description: "success", - request: request{ - user: userOne, - questionnaireID: questionnaireIDSuccess, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - response: myResponses, - }, - }, - { - description: "questionnaireID does not exist", - request: request{ - user: userOne, - questionnaireID: questionnaireIDNotFound, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - response: []myResponse{}, - }, - }, - { - description: "StatusInternalServerError", - request: request{ - user: "StatusInternalServerError", - questionnaireID: questionnaireIDSuccess, - }, - expect: expect{ - isErr: true, - code: http.StatusInternalServerError, - }, - }, - { - description: "badParam", - request: request{ - user: userOne, - questionnaireID: questionnaireIDSuccess, - isBadParam: true, - }, - expect: expect{ - isErr: true, - code: http.StatusBadRequest, - }, - }, - } - - e := echo.New() - e.GET("api/users/me/responses/:questionnaireID", u.GetMyResponsesByID, m.SetUserIDMiddleware, m.TraPMemberAuthenticate) - - for _, testCase := range testCases { - reqPath := fmt.Sprint(rootPath, "/users/me/responses/", testCase.request.questionnaireID) - if testCase.request.isBadParam { - reqPath = fmt.Sprint(rootPath, "/users/me/responses/", "badParam") - } - rec := createRecorder(e, testCase.request.user, methodGet, reqPath, typeNone, "") - - assertion.Equal(testCase.expect.code, rec.Code, testCase.description, "status code") - if rec.Code < 200 || rec.Code >= 300 { - continue - } - - responseByte, jsonErr := json.Marshal(testCase.expect.response) - require.NoError(t, jsonErr) - responseStr := string(responseByte) + "\n" - assertion.Equal(responseStr, rec.Body.String(), testCase.description, "responseBody") - } -} - -func TestGetTargetedQuestionnaire(t *testing.T) { - - t.Parallel() - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - nowTime := time.Now() - - questionnaireID1 := 1 - questionnaireID2 := 2 - targettedQuestionnaires := []model.TargettedQuestionnaire{ - { - Questionnaires: model.Questionnaires{ - ID: questionnaireID1, - Title: "questionnaireID1", - Description: "questionnaireID1", - ResTimeLimit: null.TimeFrom(nowTime), - DeletedAt: gorm.DeletedAt{ - Time: nowTime, - Valid: false, - }, - ResSharedTo: "public", - CreatedAt: nowTime, - ModifiedAt: nowTime, - }, - RespondedAt: null.NewTime(nowTime, false), - HasResponse: false, - }, - { - Questionnaires: model.Questionnaires{ - ID: questionnaireID2, - Title: "questionnaireID2", - Description: "questionnaireID2", - ResTimeLimit: null.TimeFrom(nowTime), - DeletedAt: gorm.DeletedAt{ - Time: nowTime, - Valid: false, - }, - ResSharedTo: "public", - CreatedAt: nowTime, - ModifiedAt: nowTime, - }, - RespondedAt: null.NewTime(nowTime, false), - HasResponse: false, - }, - } - - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockTarget := mock_model.NewMockITarget(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - - mockQuestion := mock_model.NewMockIQuestion(ctrl) - - u := NewUser( - mockRespondent, - mockQuestionnaire, - mockTarget, - mockAdministrator, - ) - m := NewMiddleware( - mockAdministrator, - mockRespondent, - mockQuestion, - mockQuestionnaire, - ) - - // Questionnaire - // GetTargettedQuestionnaires - // success - mockQuestionnaire.EXPECT(). - GetTargettedQuestionnaires(gomock.Any(), string(userOne), "", gomock.Any()). - Return(targettedQuestionnaires, nil).AnyTimes() - // empty - mockQuestionnaire.EXPECT(). - GetTargettedQuestionnaires(gomock.Any(), "empty", "", gomock.Any()). - Return([]model.TargettedQuestionnaire{}, nil).AnyTimes() - // failure - mockQuestionnaire.EXPECT(). - GetTargettedQuestionnaires(gomock.Any(), "StatusInternalServerError", "", gomock.Any()). - Return(nil, errMock).AnyTimes() - - type request struct { - user users - } - type expect struct { - isErr bool - code int - response []model.TargettedQuestionnaire - } - - type test struct { - description string - request - expect - } - testCases := []test{ - { - description: "success", - request: request{ - user: userOne, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - response: targettedQuestionnaires, - }, - }, - { - description: "empty", - request: request{ - user: "empty", - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - response: []model.TargettedQuestionnaire{}, - }, - }, - { - description: "StatusInternalServerError", - request: request{ - user: "StatusInternalServerError", - }, - expect: expect{ - isErr: true, - code: http.StatusInternalServerError, - }, - }, - } - - e := echo.New() - e.GET("api/users/me/targeted", u.GetTargetedQuestionnaire, m.SetUserIDMiddleware, m.TraPMemberAuthenticate) - - for _, testCase := range testCases { - rec := createRecorder(e, testCase.request.user, methodGet, makePath("/users/me/targeted"), typeNone, "") - - assertion.Equal(testCase.expect.code, rec.Code, testCase.description, "status code") - if rec.Code < 200 || rec.Code >= 300 { - continue - } - - responseByte, jsonErr := json.Marshal(testCase.expect.response) - require.NoError(t, jsonErr) - responseStr := string(responseByte) + "\n" - assertion.Equal(responseStr, rec.Body.String(), testCase.description, "responseBody") - } -} - -func TestGetTargettedQuestionnairesBytraQID(t *testing.T) { - - t.Parallel() - assertion := assert.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - nowTime := time.Now() - - questionnaireID1 := 1 - questionnaireID2 := 2 - targettedQuestionnaires := []model.TargettedQuestionnaire{ - { - Questionnaires: model.Questionnaires{ - ID: questionnaireID1, - Title: "questionnaireID1", - Description: "questionnaireID1", - ResTimeLimit: null.TimeFrom(nowTime), - DeletedAt: gorm.DeletedAt{ - Time: nowTime, - Valid: false, - }, - ResSharedTo: "public", - CreatedAt: nowTime, - ModifiedAt: nowTime, - }, - RespondedAt: null.NewTime(nowTime, false), - HasResponse: false, - }, - { - Questionnaires: model.Questionnaires{ - ID: questionnaireID2, - Title: "questionnaireID2", - Description: "questionnaireID2", - ResTimeLimit: null.TimeFrom(nowTime), - DeletedAt: gorm.DeletedAt{ - Time: nowTime, - Valid: false, - }, - ResSharedTo: "public", - CreatedAt: nowTime, - ModifiedAt: nowTime, - }, - RespondedAt: null.NewTime(nowTime, false), - HasResponse: false, - }, - } - - mockRespondent := mock_model.NewMockIRespondent(ctrl) - mockQuestionnaire := mock_model.NewMockIQuestionnaire(ctrl) - mockTarget := mock_model.NewMockITarget(ctrl) - mockAdministrator := mock_model.NewMockIAdministrator(ctrl) - - mockQuestion := mock_model.NewMockIQuestion(ctrl) - - u := NewUser( - mockRespondent, - mockQuestionnaire, - mockTarget, - mockAdministrator, - ) - m := NewMiddleware( - mockAdministrator, - mockRespondent, - mockQuestion, - mockQuestionnaire, - ) - - // Questionnaire - // GetTargettedQuestionnaires - // success - mockQuestionnaire.EXPECT(). - GetTargettedQuestionnaires(gomock.Any(), string(userOne), "", gomock.Any()). - Return(targettedQuestionnaires, nil).AnyTimes() - // empty - mockQuestionnaire.EXPECT(). - GetTargettedQuestionnaires(gomock.Any(), "empty", "", gomock.Any()). - Return([]model.TargettedQuestionnaire{}, nil).AnyTimes() - // failure - mockQuestionnaire.EXPECT(). - GetTargettedQuestionnaires(gomock.Any(), "StatusInternalServerError", "", gomock.Any()). - Return(nil, errMock).AnyTimes() - - type request struct { - user users - targetUser users - } - type expect struct { - isErr bool - code int - response []model.TargettedQuestionnaire - } - - type test struct { - description string - request - expect - } - testCases := []test{ - { - description: "success", - request: request{ - user: userOne, - targetUser: userOne, - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - response: targettedQuestionnaires, - }, - }, - { - description: "empty", - request: request{ - user: userOne, - targetUser: "empty", - }, - expect: expect{ - isErr: false, - code: http.StatusOK, - response: []model.TargettedQuestionnaire{}, - }, - }, - { - description: "StatusInternalServerError", - request: request{ - user: userOne, - targetUser: "StatusInternalServerError", - }, - expect: expect{ - isErr: true, - code: http.StatusInternalServerError, - }, - }, - } - - e := echo.New() - e.GET("api/users/:traQID/targeted", u.GetTargettedQuestionnairesBytraQID, m.SetUserIDMiddleware, m.SetValidatorMiddleware, m.TraPMemberAuthenticate) - - for _, testCase := range testCases { - rec := createRecorder(e, testCase.request.user, methodGet, fmt.Sprint(rootPath, "/users/", testCase.request.targetUser, "/targeted"), typeNone, "") - - assertion.Equal(testCase.expect.code, rec.Code, testCase.description, "status code") - if rec.Code < 200 || rec.Code >= 300 { - continue - } - - responseByte, jsonErr := json.Marshal(testCase.expect.response) - require.NoError(t, jsonErr) - responseStr := string(responseByte) + "\n" - assertion.Equal(responseStr, rec.Body.String(), testCase.description, "responseBody") - } -} - -// func TestGetUsersMe(t *testing.T) { -// testList := []struct { -// description string -// result meResponseBody -// expectCode int -// }{} -// fmt.Println(testList) -// } - -// func TestGetMyResponses(t *testing.T) { -// testList := []struct { -// description string -// result respondentInfos -// expectCode int -// }{} -// fmt.Println(testList) -// } - -// func TestGetMyResponsesByID(t *testing.T) { -// testList := []struct { -// description string -// questionnaireID int -// result respondentInfos -// expectCode int -// }{} -// fmt.Println(testList) -// } - -// func TestGetTargetedQuestionnaire(t *testing.T) { -// testList := []struct { -// description string -// result targettedQuestionnaire -// expectCode int -// }{} -// fmt.Println(testList) -// } - -// func TestGetMyQuestionnaire(t *testing.T) { -// testList := []struct { -// description string -// result targettedQuestionnaire -// expectCode int -// }{} -// fmt.Println(testList) -// } -// func TestGetTargettedQuestionnairesBytraQID(t *testing.T) { -// testList := []struct { -// description string -// result targettedQuestionnaire -// expectCode int -// }{} -// fmt.Println(testList) -// }