diff --git a/controller/adapter.go b/controller/adapter.go index 44794d72..b385a31a 100644 --- a/controller/adapter.go +++ b/controller/adapter.go @@ -3,6 +3,7 @@ package controller import ( "strconv" + "github.com/google/uuid" "github.com/labstack/echo/v4" "github.com/traPtitech/anke-to/model" "github.com/traPtitech/anke-to/openapi" @@ -64,10 +65,10 @@ func convertResSharedTo(resSharedTo string) openapi.ResShareType { } -func createUsersAndGroups(users []string, groups []string) openapi.UsersAndGroups { +func createUsersAndGroups(users []string, groups uuid.UUIDs) openapi.UsersAndGroups { res := openapi.UsersAndGroups{ Users: users, - Groups: groups, + Groups: groups.Strings(), } return res } @@ -147,14 +148,14 @@ func convertRespondents(respondents []model.Respondents) []string { return res } -func questionnaire2QuestionnaireDetail(questionnaires model.Questionnaires, adminUsers []string, adminGroups []string, targetUsers []string, targetGroups []string, respondents []string) openapi.QuestionnaireDetail { +func questionnaire2QuestionnaireDetail(questionnaires model.Questionnaires, adminUsers []string, adminGroups []uuid.UUID, targetUsers []string, targetGroups []uuid.UUID, respondents []string) openapi.QuestionnaireDetail { res := openapi.QuestionnaireDetail{ Admins: createUsersAndGroups(adminUsers, adminGroups), CreatedAt: questionnaires.CreatedAt, Description: questionnaires.Description, // IsAllowingMultipleResponses: questionnaires.IsAllowingMultipleResponses, - // IsAnonymous: questionnaires.IsAnonymous, - // IsPublished: questionnaires.IsPublished, + IsAnonymous: questionnaires.IsAnonymous, + IsPublished: questionnaires.IsPublished, ModifiedAt: questionnaires.ModifiedAt, QuestionnaireId: questionnaires.ID, Questions: convertQuestions(questionnaires.Questions), @@ -264,6 +265,12 @@ func respondentDetail2Response(ctx echo.Context, respondentDetail model.Responde oResponseBodies = append(oResponseBodies, oResponseBody) } + isAnonymous, err := model.NewQuestionnaire().GetResponseIsAnonymousByQuestionnaireID(ctx.Request().Context(), respondentDetail.QuestionnaireID) + if err != nil { + ctx.Logger().Errorf("failed to get response is anonymous: %+v", err) + return openapi.Response{}, err + } + res := openapi.Response{ Body: oResponseBodies, IsDraft: respondentDetail.SubmittedAt.Valid, @@ -272,6 +279,7 @@ func respondentDetail2Response(ctx echo.Context, respondentDetail model.Responde Respondent: &respondentDetail.TraqID, ResponseId: respondentDetail.ResponseID, SubmittedAt: respondentDetail.SubmittedAt.Time, + IsAnonymous: &isAnonymous, } return res, nil diff --git a/controller/questionnaire.go b/controller/questionnaire.go index 89b09db7..99e0ccaf 100644 --- a/controller/questionnaire.go +++ b/controller/questionnaire.go @@ -33,8 +33,28 @@ type Questionnaire struct { Response } -func NewQuestionnaire() *Questionnaire { - return &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, + } } const MaxTitleLength = 50 @@ -112,7 +132,7 @@ func (q Questionnaire) PostQuestionnaire(c echo.Context, userID string, params o questionnaireID := 0 err := q.ITransaction.Do(c.Request().Context(), nil, func(ctx context.Context) error { - questionnaireID, err := q.InsertQuestionnaire(ctx, params.Title, params.Description, responseDueDateTime, convertResponseViewableBy(params.ResponseViewableBy), params.IsPublished) + questionnaireID, err := q.InsertQuestionnaire(ctx, params.Title, params.Description, responseDueDateTime, convertResponseViewableBy(params.ResponseViewableBy), params.IsPublished, params.IsAnonymous) if err != nil { c.Logger().Errorf("failed to insert questionnaire: %+v", err) return err @@ -191,6 +211,12 @@ func (q Questionnaire) PostQuestionnaire(c echo.Context, userID string, params o return err } + Jq.PushReminder(questionnaireID, params.ResponseDueDateTime) + if err != nil { + c.Logger().Errorf("failed to push reminder: %+v", err) + return err + } + return nil }) if err != nil { @@ -316,13 +342,24 @@ func (q Questionnaire) GetQuestionnaire(ctx echo.Context, questionnaireID int) ( } func (q Questionnaire) EditQuestionnaire(c echo.Context, questionnaireID int, params openapi.EditQuestionnaireJSONRequestBody) error { + // unable to change the questionnaire from anoymous to non-anonymous + isAnonymous, err := q.GetResponseIsAnonymousByQuestionnaireID(c.Request().Context(), questionnaireID) + if err != nil { + c.Logger().Errorf("failed to get anonymous info: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get anonymous info") + } + if isAnonymous && !params.IsAnonymous { + c.Logger().Info("unable to change the questionnaire from anoymous to non-anonymous") + return echo.NewHTTPError(http.StatusBadRequest, "unable to change the questionnaire from anoymous to non-anonymous") + } + responseDueDateTime := null.Time{} if params.ResponseDueDateTime != nil { responseDueDateTime.Valid = true responseDueDateTime.Time = *params.ResponseDueDateTime } - err := q.ITransaction.Do(c.Request().Context(), nil, func(ctx context.Context) error { - err := q.UpdateQuestionnaire(ctx, params.Title, params.Description, responseDueDateTime, string(params.ResponseViewableBy), questionnaireID, params.IsPublished) + err = q.ITransaction.Do(c.Request().Context(), nil, func(ctx context.Context) error { + err := q.UpdateQuestionnaire(ctx, params.Title, params.Description, responseDueDateTime, string(params.ResponseViewableBy), questionnaireID, params.IsPublished, params.IsAnonymous) if err != nil && !errors.Is(err, model.ErrNoRecordUpdated) { c.Logger().Errorf("failed to update questionnaire: %+v", err) return err @@ -405,6 +442,17 @@ func (q Questionnaire) EditQuestionnaire(c echo.Context, questionnaireID int, pa } } + err = Jq.DeleteReminder(questionnaireID) + if err != nil { + c.Logger().Errorf("failed to delete reminder: %+v", err) + return err + } + err = Jq.PushReminder(questionnaireID, params.ResponseDueDateTime) + if err != nil { + c.Logger().Errorf("failed to push reminder: %+v", err) + return err + } + return nil }) if err != nil { @@ -543,6 +591,12 @@ func (q Questionnaire) DeleteQuestionnaire(c echo.Context, questionnaireID int) } } + err = Jq.DeleteReminder(questionnaireID) + if err != nil { + c.Logger().Errorf("failed to delete reminder: %+v", err) + return err + } + return nil }) if err != nil { @@ -558,12 +612,48 @@ func (q Questionnaire) DeleteQuestionnaire(c echo.Context, questionnaireID int) } func (q Questionnaire) GetQuestionnaireMyRemindStatus(c echo.Context, questionnaireID int) (bool, error) { - // todo: check remind status - return false, nil + status, err := Jq.CheckRemindStatus(questionnaireID) + if err != nil { + c.Logger().Errorf("failed to check remind status: %+v", err) + return false, echo.NewHTTPError(http.StatusInternalServerError, "failed to check remind status") + } + + return status, nil } -func (q Questionnaire) EditQuestionnaireMyRemindStatus(c echo.Context, questionnaireID int) error { - // todo: edit remind status +func (q Questionnaire) EditQuestionnaireMyRemindStatus(c echo.Context, questionnaireID int, isRemindEnabled bool) error { + if isRemindEnabled { + status, err := Jq.CheckRemindStatus(questionnaireID) + if err != nil { + c.Logger().Errorf("failed to check remind status: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to check remind status") + } + if status { + return nil + } + + questionnaire, _, _, _, _, _, err := q.GetQuestionnaireInfo(c.Request().Context(), questionnaireID) + if err != nil { + if errors.Is(err, model.ErrRecordNotFound) { + c.Logger().Info("questionnaire not found") + return echo.NewHTTPError(http.StatusNotFound, "questionnaire not found") + } + c.Logger().Errorf("failed to get questionnaire: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get questionnaire") + } + + err = Jq.PushReminder(questionnaireID, &questionnaire.ResTimeLimit.Time) + if err != nil { + c.Logger().Errorf("failed to push reminder: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to push reminder") + } + } else { + err := Jq.DeleteReminder(questionnaireID) + if err != nil { + c.Logger().Errorf("failed to delete reminder: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to delete reminder") + } + } return nil } @@ -716,6 +806,8 @@ func (q Questionnaire) PostQuestionnaireResponse(c echo.Context, questionnaireID } } + isAnonymous, err := q.GetResponseIsAnonymousByQuestionnaireID(c.Request().Context(), questionnaireID) + res = openapi.Response{ QuestionnaireId: questionnaireID, ResponseId: resopnseID, @@ -723,6 +815,7 @@ func (q Questionnaire) PostQuestionnaireResponse(c echo.Context, questionnaireID SubmittedAt: submittedAt, ModifiedAt: modifiedAt, IsDraft: params.IsDraft, + IsAnonymous: &isAnonymous, Body: params.Body, } @@ -765,3 +858,32 @@ https://anke-to.trap.jp/responses/new/%d`, questionnaireID, ) } + +func createReminderMessage(questionnaireID int, title string, description string, administrators []string, resTimeLimit time.Time, targets []string, leftTimeText string) string { + resTimeLimitText := resTimeLimit.Local().Format("2006/01/02 15:04") + targetsMentionText := "@" + strings.Join(targets, " @") + + return fmt.Sprintf( + `### アンケート『[%s](https://anke-to.trap.jp/questionnaires/%d)』の回答期限が迫っています! +==残り%sです!== +#### 管理者 +%s +#### 説明 +%s +#### 回答期限 +%s +#### 対象者 +%s +#### 回答リンク +https://anke-to.trap.jp/responses/new/%d +`, + title, + questionnaireID, + leftTimeText, + strings.Join(administrators, ","), + description, + resTimeLimitText, + targetsMentionText, + questionnaireID, + ) +} diff --git a/controller/reminder.go b/controller/reminder.go new file mode 100644 index 00000000..3da0139f --- /dev/null +++ b/controller/reminder.go @@ -0,0 +1,158 @@ +package controller + +import ( + "context" + "slices" + "sort" + "sync" + "time" + + "github.com/traPtitech/anke-to/model" + "github.com/traPtitech/anke-to/traq" + "golang.org/x/sync/semaphore" +) + +type Job struct { + Timestamp time.Time + QuestionnaireID int + Action func() +} + +type JobQueue struct { + jobs []*Job + mu sync.Mutex +} + +var ( + sem = semaphore.NewWeighted(1) + Jq = &JobQueue{} + Wg = &sync.WaitGroup{} + reminderTimingMinutes = []int{5, 30, 60, 1440, 10080} + reminderTimingStrings = []string{"5分", "30分", "1時間", "1日", "1週間"} +) + +func (jq *JobQueue) Push(job *Job) { + jq.mu.Lock() + defer jq.mu.Unlock() + jq.jobs = append(jq.jobs, job) + sort.Slice(jq.jobs, func(i, j int) bool { + return jq.jobs[i].Timestamp.Before(jq.jobs[j].Timestamp) + }) +} + +func (jq *JobQueue) Pop() *Job { + jq.mu.Lock() + defer jq.mu.Unlock() + if len(jq.jobs) == 0 { + return nil + } + job := jq.jobs[0] + jq.jobs = jq.jobs[1:] + return job +} + +func (jq *JobQueue) PushReminder(questionnaireID int, limit *time.Time) error { + + for i, timing := range reminderTimingMinutes { + remindTimeStamp := limit.Add(-time.Duration(timing) * time.Minute) + if remindTimeStamp.Before(time.Now()) { + Jq.Push(&Job{ + Timestamp: remindTimeStamp, + QuestionnaireID: questionnaireID, + Action: func() { + reminderAction(questionnaireID, reminderTimingStrings[i]) + }, + }) + } + } + + return nil +} + +func (jq *JobQueue) DeleteReminder(questionnaireID int) error { + jq.mu.Lock() + defer jq.mu.Unlock() + if len(jq.jobs) == 1 && jq.jobs[0].QuestionnaireID == questionnaireID { + jq.jobs = []*Job{} + } + for i, job := range jq.jobs { + if job.QuestionnaireID == questionnaireID { + jq.jobs = append(jq.jobs[:i], jq.jobs[i+1:]...) + } + } + + return nil +} + +func (jq *JobQueue) CheckRemindStatus(questionnaireID int) (bool, error) { + jq.mu.Lock() + defer jq.mu.Unlock() + for _, job := range jq.jobs { + if job.QuestionnaireID == questionnaireID { + return true, nil + } + } + return false, nil +} + +func reminderAction(questionnaireID int, leftTimeText string) error { + ctx := context.Background() + q := model.Questionnaire{} + questionnaire, _, _, administrators, _, respondants, err := q.GetQuestionnaireInfo(ctx, questionnaireID) + if err != nil { + return err + } + + var reminderTargets []string + for _, target := range questionnaire.Targets { + if target.IsCanceled { + continue + } + if slices.Contains(respondants, target.UserTraqid) { + continue + } + reminderTargets = append(reminderTargets, target.UserTraqid) + } + + reminderMessage := createReminderMessage(questionnaireID, questionnaire.Title, questionnaire.Description, administrators, questionnaire.ResTimeLimit.Time, reminderTargets, leftTimeText) + wh := traq.NewWebhook() + err = wh.PostMessage(reminderMessage) + if err != nil { + return err + } + + return nil +} + +func ReminderWorker() { + for { + job := Jq.Pop() + if job == nil { + time.Sleep(1 * time.Minute) + continue + } + + if time.Until(job.Timestamp) > 0 { + time.Sleep(time.Until(job.Timestamp)) + } + + Wg.Add(1) + go func() { + defer Wg.Done() + job.Action() + }() + } +} + +func ReminderInit() { + questionnaires, err := model.NewQuestionnaire().GetQuestionnairesInfoForReminder(context.Background()) + if err != nil { + panic(err) + } + for _, questionnaire := range questionnaires { + err := Jq.PushReminder(questionnaire.ID, &questionnaire.ResTimeLimit.Time) + if err != nil { + panic(err) + } + } +} \ No newline at end of file diff --git a/controller/response.go b/controller/response.go index 346f548e..933db74c 100644 --- a/controller/response.go +++ b/controller/response.go @@ -22,8 +22,24 @@ type Response struct { model.IScaleLabel } -func NewResponse() *Response { - return &Response{} +func NewResponse( + questionnaire model.IQuestionnaire, + respondent model.IRespondent, + response model.IResponse, + target model.ITarget, + question model.IQuestion, + validation model.IValidation, + scaleLabel model.IScaleLabel, +) *Response { + return &Response{ + IQuestionnaire: questionnaire, + IRespondent: respondent, + IResponse: response, + ITarget: target, + IQuestion: question, + IValidation: validation, + IScaleLabel: scaleLabel, + } } func (r Response) GetMyResponses(ctx echo.Context, params openapi.GetMyResponsesParams, userID string) (openapi.ResponsesWithQuestionnaireInfo, error) { @@ -79,6 +95,7 @@ func (r Response) GetMyResponses(ctx echo.Context, params openapi.GetMyResponses Respondent: &userID, ResponseId: response.ResponseId, SubmittedAt: response.SubmittedAt, + IsAnonymous: response.IsAnonymous, } res = append(res, tmp) } @@ -244,6 +261,6 @@ func (r Response) EditResponse(ctx echo.Context, responseID openapi.ResponseIDIn return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to insert responses: %w", err)) } } - + return nil -} \ No newline at end of file +} diff --git a/docs/db_schema.md b/docs/db_schema.md index fee0ed48..7d1f4523 100644 --- a/docs/db_schema.md +++ b/docs/db_schema.md @@ -48,9 +48,10 @@ | res_time_limit | timestamp | YES | | _NULL_ | | 回答の締切日時 (締切がない場合は NULL) | | deleted_at | timestamp | YES | | _NULL_ | | アンケートが削除された日時 (削除されていない場合は NULL) | | res_shared_to | char(30) | NO | | administrators | | アンケートの結果を, 運営は見られる ("administrators"), 回答済みの人は見られる ("respondents") 誰でも見られる ("public") | +| is_anonymous | boolean | NO | | false | | アンケートが匿名解答かどうか | | created_at | timestamp | NO | | CURRENT_TIMESTAMP | | アンケートが作成された日時 | | modified_at | timestamp | NO | | CURRENT_TIMESTAMP | | アンケートが更新された日時 | -| is_published | bookean | NO | | false | | アンケートが公開かどうか +| is_published | boolean | NO | | false | | アンケートが公開かどうか | ### respondents @@ -108,3 +109,4 @@ | ---------------- | -------- | ---- | --- | ------- | ----- | -------- | | questionnaire_id | int(11) | NO | PRI | _NULL_ | | user_traqid | char(32) | NO | PRI | _NULL_ | +| is_canceled | boolean | NO | | false | | アンケートの対象者がキャンセルしたかどうか | diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index d3658844..7bbea625 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -91,7 +91,7 @@ paths: # TODO 変数の命名を確認する operationId: editQuestionnaire tags: - questionnaire - description: アンケートの情報を変更します。 + description: アンケートの情報を変更します。匿名のアンケートを非匿名アンケートに変更することができません。 parameters: - $ref: "#/components/parameters/questionnaireIDInPath" requestBody: @@ -105,6 +105,8 @@ paths: # TODO 変数の命名を確認する description: 正常にアンケートを変更できました。 "400": description: アンケートのIDが無効です + "405": + description: 匿名のアンケートを非匿名アンケートに変更することができません "500": description: 正常にアンケートを変更できませんでした delete: @@ -672,6 +674,8 @@ components: example: 1 question_id: type: integer + description: | + 質問を追加する場合はnull。 example: 1 created_at: type: string @@ -679,22 +683,18 @@ components: example: 2020-01-01T00:00:00+09:00 required: - questionnaire_id - - question_id - created_at QuestionBase: type: object properties: - title: - type: string - description: + body: type: string is_required: type: boolean description: | 回答必須かどうか required: - - title - - description + - body - is_required QuestionSettingsByType: oneOf: diff --git a/go.mod b/go.mod index 1ff36d4d..a17726c7 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.10.0 - golang.org/x/sync v0.7.0 + golang.org/x/sync v0.8.0 golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect golang.org/x/tools v0.21.0 // indirect diff --git a/go.sum b/go.sum index 2c369dc4..0c26be87 100644 --- a/go.sum +++ b/go.sum @@ -575,6 +575,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/handler/handler.go b/handler/handler.go index f620ece3..aa6ddd82 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -1,3 +1,17 @@ package handler -type Handler struct{} +import "github.com/traPtitech/anke-to/controller" + +type Handler struct { + Questionnaire *controller.Questionnaire + Response *controller.Response +} + +func NewHandler(questionnaire *controller.Questionnaire, + response *controller.Response, +) *Handler { + return &Handler{ + Questionnaire: questionnaire, + Response: response, + } +} diff --git a/handler/middleware.go b/handler/middleware.go index 02f8ee51..b7e0167f 100644 --- a/handler/middleware.go +++ b/handler/middleware.go @@ -41,7 +41,7 @@ const ( var adminUserIDs = []string{"ryoha", "xxarupakaxx", "kaitoyama", "cp20", "itzmeowww"} // SetUserIDMiddleware X-Showcase-UserからユーザーIDを取得しセットする -func SetUserIDMiddleware(next echo.HandlerFunc) echo.HandlerFunc { +func (*Middleware) SetUserIDMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { userID := c.Request().Header.Get("X-Showcase-User") if userID == "" { @@ -55,7 +55,7 @@ func SetUserIDMiddleware(next echo.HandlerFunc) echo.HandlerFunc { } // TraPMemberAuthenticate traP部員かの認証 -func TraPMemberAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { +func (*Middleware) TraPMemberAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { userID, err := getUserID(c) if err != nil { @@ -74,7 +74,7 @@ func TraPMemberAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { } // TrapRateLimitMiddlewareFunc traP IDベースのリクエスト制限 -func TrapRateLimitMiddlewareFunc() echo.MiddlewareFunc { +func (*Middleware) TrapRateLimitMiddlewareFunc() echo.MiddlewareFunc { config := middleware.RateLimiterConfig{ Store: middleware.NewRateLimiterMemoryStore(5), IdentifierExtractor: func(c echo.Context) (string, error) { @@ -92,9 +92,8 @@ func TrapRateLimitMiddlewareFunc() echo.MiddlewareFunc { } // QuestionnaireReadAuthenticate アンケートの閲覧権限があるかの認証 -func QuestionnaireReadAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { +func (m *Middleware) QuestionnaireReadAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - m := NewMiddleware() userID, err := getUserID(c) if err != nil { @@ -148,9 +147,8 @@ func QuestionnaireReadAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { } // QuestionnaireAdministratorAuthenticate アンケートの管理者かどうかの認証 -func QuestionnaireAdministratorAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { +func (m *Middleware) QuestionnaireAdministratorAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - m := NewMiddleware() userID, err := getUserID(c) if err != nil { @@ -188,9 +186,8 @@ func QuestionnaireAdministratorAuthenticate(next echo.HandlerFunc) echo.HandlerF } // ResponseReadAuthenticate 回答閲覧権限があるかの認証 -func ResponseReadAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { +func (m *Middleware) ResponseReadAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - m := NewMiddleware() userID, err := getUserID(c) if err != nil { @@ -255,9 +252,8 @@ func ResponseReadAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { } // RespondentAuthenticate 回答者かどうかの認証 -func RespondentAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { +func (m *Middleware) RespondentAuthenticate(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - m := NewMiddleware() userID, err := getUserID(c) if err != nil { diff --git a/handler/questionnaire.go b/handler/questionnaire.go index 15f7bcf7..1719d207 100644 --- a/handler/questionnaire.go +++ b/handler/questionnaire.go @@ -6,7 +6,6 @@ import ( "net/http" "github.com/labstack/echo/v4" - "github.com/traPtitech/anke-to/controller" "github.com/traPtitech/anke-to/model" "github.com/traPtitech/anke-to/openapi" ) @@ -14,14 +13,13 @@ import ( // (GET /questionnaires) func (h Handler) GetQuestionnaires(ctx echo.Context, params openapi.GetQuestionnairesParams) error { res := openapi.QuestionnaireList{} - q := controller.NewQuestionnaire() userID, err := getUserID(ctx) if err != nil { ctx.Logger().Errorf("failed to get userID: %+v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) } - res, err = q.GetQuestionnaires(ctx, userID, params) + res, err = h.Questionnaire.GetQuestionnaires(ctx, userID, params) if err != nil { ctx.Logger().Errorf("failed to get questionnaires: %+v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaires: %w", err)) @@ -50,14 +48,13 @@ func (h Handler) PostQuestionnaire(ctx echo.Context) error { } res := openapi.QuestionnaireDetail{} - q := controller.NewQuestionnaire() userID, err := getUserID(ctx) if err != nil { ctx.Logger().Errorf("failed to get userID: %+v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) } - res, err = q.PostQuestionnaire(ctx, userID, params) + res, err = h.Questionnaire.PostQuestionnaire(ctx, userID, params) if err != nil { ctx.Logger().Errorf("failed to post questionnaire: %+v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to post questionnaire: %w", err)) @@ -69,8 +66,7 @@ func (h Handler) PostQuestionnaire(ctx echo.Context) error { // (GET /questionnaires/{questionnaireID}) func (h Handler) GetQuestionnaire(ctx echo.Context, questionnaireID openapi.QuestionnaireIDInPath) error { res := openapi.QuestionnaireDetail{} - q := controller.NewQuestionnaire() - res, err := q.GetQuestionnaire(ctx, questionnaireID) + res, err := h.Questionnaire.GetQuestionnaire(ctx, questionnaireID) if err != nil { if errors.Is(err, model.ErrRecordNotFound) { return echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("questionnaire not found: %w", err)) @@ -89,11 +85,10 @@ func (h Handler) EditQuestionnaire(ctx echo.Context, questionnaireID openapi.Que return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("failed to bind request body: %w", err)) } - q := controller.NewQuestionnaire() - err := q.EditQuestionnaire(ctx, questionnaireID, params) + err := h.Questionnaire.EditQuestionnaire(ctx, questionnaireID, params) if err != nil { ctx.Logger().Errorf("failed to edit questionnaire: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to edit questionnaire: %w", err)) + return err } return ctx.NoContent(200) @@ -101,8 +96,7 @@ func (h Handler) EditQuestionnaire(ctx echo.Context, questionnaireID openapi.Que // (DELETE /questionnaires/{questionnaireID}) func (h Handler) DeleteQuestionnaire(ctx echo.Context, questionnaireID openapi.QuestionnaireIDInPath) error { - q := controller.NewQuestionnaire() - err := q.DeleteQuestionnaire(ctx, questionnaireID) + err := h.Questionnaire.DeleteQuestionnaire(ctx, questionnaireID) if err != nil { ctx.Logger().Errorf("failed to delete questionnaire: %+v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to delete questionnaire: %w", err)) @@ -114,11 +108,10 @@ func (h Handler) DeleteQuestionnaire(ctx echo.Context, questionnaireID openapi.Q // (GET /questionnaires/{questionnaireID}/myRemindStatus) func (h Handler) GetQuestionnaireMyRemindStatus(ctx echo.Context, questionnaireID openapi.QuestionnaireIDInPath) error { res := openapi.QuestionnaireIsRemindEnabled{} - q := controller.NewQuestionnaire() - status, err := q.GetQuestionnaireMyRemindStatus(ctx, questionnaireID) + status, err := h.Questionnaire.GetQuestionnaireMyRemindStatus(ctx, questionnaireID) if err != nil { ctx.Logger().Errorf("failed to get questionnaire my remind status: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire my remind status: %w", err)) + return err } res.IsRemindEnabled = status @@ -127,11 +120,16 @@ func (h Handler) GetQuestionnaireMyRemindStatus(ctx echo.Context, questionnaireI // (PATCH /questionnaires/{questionnaireID}/myRemindStatus) func (h Handler) EditQuestionnaireMyRemindStatus(ctx echo.Context, questionnaireID openapi.QuestionnaireIDInPath) error { - q := controller.NewQuestionnaire() - err := q.EditQuestionnaireMyRemindStatus(ctx, questionnaireID) + params := openapi.EditQuestionnaireMyRemindStatusJSONRequestBody{} + if err := ctx.Bind(¶ms); err != nil { + ctx.Logger().Errorf("failed to bind request body: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("failed to bind request body: %w", err)) + } + + err := h.Questionnaire.EditQuestionnaireMyRemindStatus(ctx, questionnaireID, params.IsRemindEnabled) if err != nil { ctx.Logger().Errorf("failed to edit questionnaire my remind status: %+v", err) - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to edit questionnaire my remind status: %w", err)) + return err } return ctx.NoContent(200) } @@ -143,8 +141,7 @@ func (h Handler) GetQuestionnaireResponses(ctx echo.Context, questionnaireID ope ctx.Logger().Errorf("failed to get userID: %+v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) } - q := controller.NewQuestionnaire() - res, err := q.GetQuestionnaireResponses(ctx, questionnaireID, params, userID) + res, err := h.Questionnaire.GetQuestionnaireResponses(ctx, questionnaireID, params, userID) if err != nil { ctx.Logger().Errorf("failed to get questionnaire responses: %+v", err) return err @@ -179,8 +176,7 @@ func (h Handler) PostQuestionnaireResponse(ctx echo.Context, questionnaireID ope return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("failed to validate request body: %w", err)) } - q := controller.NewQuestionnaire() - res, err = q.PostQuestionnaireResponse(ctx, questionnaireID, params, userID) + res, err = h.Questionnaire.PostQuestionnaireResponse(ctx, questionnaireID, params, userID) if err != nil { ctx.Logger().Errorf("failed to post questionnaire response: %+v", err) return err diff --git a/handler/response.go b/handler/response.go index 7db80a56..5bf97c57 100644 --- a/handler/response.go +++ b/handler/response.go @@ -5,7 +5,6 @@ import ( "net/http" "github.com/labstack/echo/v4" - "github.com/traPtitech/anke-to/controller" "github.com/traPtitech/anke-to/openapi" ) @@ -18,8 +17,7 @@ func (h Handler) GetMyResponses(ctx echo.Context, params openapi.GetMyResponsesP return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) } - r := controller.NewResponse() - res, err = r.GetMyResponses(ctx, params, userID) + res, err = h.Response.GetMyResponses(ctx, params, userID) if err != nil { ctx.Logger().Errorf("failed to get my responses: %+v", err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get my responses: %w", err)) @@ -35,8 +33,7 @@ func (h Handler) DeleteResponse(ctx echo.Context, responseID openapi.ResponseIDI return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get userID: %w", err)) } - r := controller.NewResponse() - err = r.DeleteResponse(ctx, responseID, userID) + err = h.Response.DeleteResponse(ctx, responseID, userID) if err != nil { ctx.Logger().Errorf("failed to delete response: %+v", err) return err @@ -49,8 +46,7 @@ func (h Handler) DeleteResponse(ctx echo.Context, responseID openapi.ResponseIDI func (h Handler) GetResponse(ctx echo.Context, responseID openapi.ResponseIDInPath) error { res := openapi.Response{} - r := controller.NewResponse() - res, err := r.GetResponse(ctx, responseID) + res, err := h.Response.GetResponse(ctx, responseID) if err != nil { ctx.Logger().Errorf("failed to get response: %+v", err) return err @@ -78,11 +74,10 @@ func (h Handler) EditResponse(ctx echo.Context, responseID openapi.ResponseIDInP return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("failed to validate request body: %w", err)) } - r := controller.NewResponse() - err = r.EditResponse(ctx, responseID, req) + err = h.Response.EditResponse(ctx, responseID, req) if err != nil { ctx.Logger().Errorf("failed to edit response: %+v", err) - return err + return err } return ctx.NoContent(200) diff --git a/main.go b/main.go index adec2505..2aad8ef1 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,7 @@ import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" oapiMiddleware "github.com/oapi-codegen/echo-middleware" - "github.com/traPtitech/anke-to/handler" + "github.com/traPtitech/anke-to/controller" "github.com/traPtitech/anke-to/model" "github.com/traPtitech/anke-to/openapi" @@ -57,32 +57,53 @@ func main() { panic("no PORT") } - e := echo.New() - swagger, err := openapi.GetSwagger() - if err != nil { - panic(err) - } - e.Use(oapiMiddleware.OapiRequestValidator(swagger)) - e.Use(handler.SetUserIDMiddleware) - e.Use(middleware.Logger()) - e.Use(middleware.Recover()) - - mws := NewMiddlewareSwitcher() - mws.AddGroupConfig("", handler.TraPMemberAuthenticate) - - mws.AddRouteConfig("/questionnaires", http.MethodGet, handler.TrapRateLimitMiddlewareFunc()) - mws.AddRouteConfig("/questionnaires/:questionnaireID", http.MethodGet, handler.QuestionnaireReadAuthenticate) - mws.AddRouteConfig("/questionnaires/:questionnaireID", http.MethodPatch, handler.QuestionnaireAdministratorAuthenticate) - mws.AddRouteConfig("/questionnaires/:questionnaireID", http.MethodDelete, handler.QuestionnaireAdministratorAuthenticate) - - mws.AddRouteConfig("/responses/:responseID", http.MethodGet, handler.ResponseReadAuthenticate) - mws.AddRouteConfig("/responses/:responseID", http.MethodPatch, handler.RespondentAuthenticate) - mws.AddRouteConfig("/responses/:responseID", http.MethodDelete, handler.RespondentAuthenticate) - - openapi.RegisterHandlers(e, handler.Handler{}) - - e.Use(mws.ApplyMiddlewares) - e.Logger.Fatal(e.Start(port)) + controller.Wg.Add(1) + go func() { + e := echo.New() + swagger, err := openapi.GetSwagger() + if err != nil { + panic(err) + } + api := InjectAPIServer() + e.Use(oapiMiddleware.OapiRequestValidator(swagger)) + e.Use(api.SetUserIDMiddleware) + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + mws := NewMiddlewareSwitcher() + mws.AddGroupConfig("", api.TraPMemberAuthenticate) + + mws.AddRouteConfig("/questionnaires", http.MethodGet, api.TrapRateLimitMiddlewareFunc()) + mws.AddRouteConfig("/questionnaires/:questionnaireID", http.MethodGet, api.QuestionnaireReadAuthenticate) + mws.AddRouteConfig("/questionnaires/:questionnaireID", http.MethodPatch, api.QuestionnaireAdministratorAuthenticate) + mws.AddRouteConfig("/questionnaires/:questionnaireID", http.MethodDelete, api.QuestionnaireAdministratorAuthenticate) + + mws.AddRouteConfig("/responses/:responseID", http.MethodGet, api.ResponseReadAuthenticate) + mws.AddRouteConfig("/responses/:responseID", http.MethodPatch, api.RespondentAuthenticate) + mws.AddRouteConfig("/responses/:responseID", http.MethodDelete, api.RespondentAuthenticate) + + handlerApi := InjectHandler() + openapi.RegisterHandlers(e, handlerApi) + + e.Use(mws.ApplyMiddlewares) + e.Logger.Fatal(e.Start(port)) + + controller.Wg.Done() + }() + + controller.Wg.Add(1) + go func() { + controller.ReminderInit() + controller.Wg.Done() + }() + + controller.Wg.Add(1) + go func() { + controller.ReminderWorker() + controller.Wg.Done() + }() + + controller.Wg.Wait() // SetRouting(port) } diff --git a/model/administratorGroups_impl.go b/model/administratorGroups_impl.go index a4953d43..ccf97e6b 100644 --- a/model/administratorGroups_impl.go +++ b/model/administratorGroups_impl.go @@ -12,7 +12,7 @@ type AdministratorGroup struct{} // NewAdministratorGroup AdministratorGroupRepositoryのコンストラクタ func NewAdministratorGroup() *AdministratorGroup { - return &AdministratorGroup{} + return new(AdministratorGroup) } type AdministratorGroups struct { diff --git a/model/questionnaires.go b/model/questionnaires.go index c9f04c87..0ed2780b 100644 --- a/model/questionnaires.go +++ b/model/questionnaires.go @@ -5,20 +5,23 @@ package model import ( "context" + "github.com/google/uuid" "gopkg.in/guregu/null.v4" ) // IQuestionnaire QuestionnaireのRepository type IQuestionnaire interface { - InsertQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, isPublished bool) (int, error) - UpdateQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, questionnaireID int, isPublished bool) error + InsertQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, isPublished bool, isAnonymous bool) (int, error) + UpdateQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, questionnaireID int, isPublished bool, isAnonymous bool) error DeleteQuestionnaire(ctx context.Context, questionnaireID int) error GetQuestionnaires(ctx context.Context, userID string, sort string, search string, pageNum int, onlyTargetingMe bool, onlyAdministratedByMe bool) ([]QuestionnaireInfo, int, error) GetAdminQuestionnaires(ctx context.Context, userID string) ([]Questionnaires, error) - GetQuestionnaireInfo(ctx context.Context, questionnaireID int) (*Questionnaires, []string, []string, []string, []string, []string, error) + GetQuestionnaireInfo(ctx context.Context, questionnaireID int) (*Questionnaires, []string, []uuid.UUID, []string, []uuid.UUID, []string, error) GetTargettedQuestionnaires(ctx context.Context, userID string, answered string, sort string) ([]TargettedQuestionnaire, error) GetQuestionnaireLimit(ctx context.Context, questionnaireID int) (null.Time, error) GetQuestionnaireLimitByResponseID(ctx context.Context, responseID int) (null.Time, error) GetResponseReadPrivilegeInfoByResponseID(ctx context.Context, userID string, responseID int) (*ResponseReadPrivilegeInfo, error) GetResponseReadPrivilegeInfoByQuestionnaireID(ctx context.Context, userID string, questionnaireID int) (*ResponseReadPrivilegeInfo, error) + GetResponseIsAnonymousByQuestionnaireID(ctx context.Context, questionnaireID int) (bool, error) + GetQuestionnairesInfoForReminder(ctx context.Context) ([]Questionnaires, error) } diff --git a/model/questionnaires_impl.go b/model/questionnaires_impl.go index 25e20cb9..f8fa8c3d 100755 --- a/model/questionnaires_impl.go +++ b/model/questionnaires_impl.go @@ -28,6 +28,7 @@ type Questionnaires struct { ResTimeLimit null.Time `json:"res_time_limit,omitempty" gorm:"type:TIMESTAMP NULL;default:NULL;"` DeletedAt gorm.DeletedAt `json:"-" gorm:"type:TIMESTAMP NULL;default:NULL;"` ResSharedTo string `json:"res_shared_to" gorm:"type:char(30);size:30;not null;default:administrators"` + IsAnonymous bool `json:"is_anonymous" gorm:"type:boolean;not null;default:false"` CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;not null;default:CURRENT_TIMESTAMP"` ModifiedAt time.Time `json:"modified_at" gorm:"type:timestamp;not null;default:CURRENT_TIMESTAMP"` Administrators []Administrators `json:"-" gorm:"foreignKey:QuestionnaireID"` @@ -82,7 +83,7 @@ type ResponseReadPrivilegeInfo struct { } // InsertQuestionnaire アンケートの追加 -func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, isPublished bool) (int, error) { +func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, isPublished bool, isAnonymous bool) (int, error) { db, err := getTx(ctx) if err != nil { return 0, fmt.Errorf("failed to get tx: %w", err) @@ -95,6 +96,7 @@ func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, des Description: description, ResSharedTo: resSharedTo, IsPublished: isPublished, + IsAnonymous: isAnonymous, } } else { questionnaire = Questionnaires{ @@ -103,6 +105,7 @@ func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, des ResTimeLimit: resTimeLimit, ResSharedTo: resSharedTo, IsPublished: isPublished, + IsAnonymous: isAnonymous, } } @@ -115,7 +118,7 @@ func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, des } // UpdateQuestionnaire アンケートの更新 -func (*Questionnaire) UpdateQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, questionnaireID int, isPublished bool) error { +func (*Questionnaire) UpdateQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, questionnaireID int, isPublished bool, isAnonymous bool) error { db, err := getTx(ctx) if err != nil { return fmt.Errorf("failed to get tx: %w", err) @@ -129,6 +132,7 @@ func (*Questionnaire) UpdateQuestionnaire(ctx context.Context, title string, des ResTimeLimit: resTimeLimit, ResSharedTo: resSharedTo, IsPublished: isPublished, + IsAnonymous: isAnonymous, } } else { questionnaire = map[string]interface{}{ @@ -137,6 +141,7 @@ func (*Questionnaire) UpdateQuestionnaire(ctx context.Context, title string, des "res_time_limit": gorm.Expr("NULL"), "res_shared_to": resSharedTo, "is_published": isPublished, + "is_anonymous": isAnonymous, } } @@ -296,6 +301,7 @@ func (*Questionnaire) GetQuestionnaireInfo(ctx context.Context, questionnaireID err = db. Where("questionnaires.id = ?", questionnaireID). + Preload("Targets"). First(&questionnaire).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil, nil, nil, nil, ErrRecordNotFound @@ -378,6 +384,24 @@ func (*Questionnaire) GetTargettedQuestionnaires(ctx context.Context, userID str return questionnaires, nil } +// GetQuestionnairesInfoForReminder 回答期限が7日以内のアンケートの詳細情報の取得 +func (*Questionnaire) GetQuestionnairesInfoForReminder(ctx context.Context) ([]Questionnaires, error) { + db, err := getTx(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get tx: %w", err) + } + + questionnaires := []Questionnaires{} + err = db. + Where("res_time_limit > ? AND res_time_limit < ?", time.Now(), time.Now().AddDate(0, 0, 7)). + Find(&questionnaires).Error + if err != nil { + return nil, fmt.Errorf("failed to get the questionnaires: %w", err) + } + + return questionnaires, nil +} + // GetQuestionnaireLimit アンケートの回答期限の取得 func (*Questionnaire) GetQuestionnaireLimit(ctx context.Context, questionnaireID int) (null.Time, error) { db, err := getTx(ctx) @@ -476,6 +500,28 @@ func (*Questionnaire) GetResponseReadPrivilegeInfoByQuestionnaireID(ctx context. return &responseReadPrivilegeInfo, nil } +func (*Questionnaire) GetResponseIsAnonymousByQuestionnaireID(ctx context.Context, questionnaireID int) (bool, error) { + db, err := getTx(ctx) + if err != nil { + return true, fmt.Errorf("failed to get tx: %w", err) + } + + var isAnonymous bool + err = db. + Table("questionnaires"). + Where("questionnaires.id = ?", questionnaireID). + Select("questionnaires.is_anonymous"). + Take(&isAnonymous).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return true, ErrRecordNotFound + } + if err != nil { + return true, fmt.Errorf("failed to get is_anonymous: %w", err) + } + + return isAnonymous, nil +} + func setQuestionnairesOrder(query *gorm.DB, sort string) (*gorm.DB, error) { switch sort { case "created_at": diff --git a/model/questionnaires_test.go b/model/questionnaires_test.go index a3a32a87..d828e7f8 100644 --- a/model/questionnaires_test.go +++ b/model/questionnaires_test.go @@ -369,6 +369,7 @@ func insertQuestionnaireTest(t *testing.T) { resTimeLimit null.Time resSharedTo string isPublished bool + isAnonymous bool } type expect struct { isErr bool @@ -390,6 +391,7 @@ func insertQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, }, { @@ -400,6 +402,7 @@ func insertQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, }, { @@ -410,6 +413,7 @@ func insertQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "respondents", isPublished: true, + isAnonymous: false, }, }, { @@ -420,6 +424,7 @@ func insertQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "administrators", isPublished: true, + isAnonymous: false, }, }, { @@ -430,6 +435,7 @@ func insertQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, }, { @@ -440,6 +446,7 @@ func insertQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, expect: expect{ isErr: true, @@ -453,6 +460,7 @@ func insertQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, }, { @@ -463,6 +471,7 @@ func insertQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, expect: expect{ isErr: true, @@ -476,6 +485,18 @@ func insertQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: false, + isAnonymous: false, + }, + }, + { + description: "anonymous questionnaire", + args: args{ + title: "第1回集会らん☆ぷろ募集アンケート", + description: "第1回集会らん☆ぷろ参加者募集", + resTimeLimit: null.NewTime(time.Time{}, false), + resSharedTo: "public", + isPublished: true, + isAnonymous: true, }, }, } @@ -483,7 +504,7 @@ func insertQuestionnaireTest(t *testing.T) { for _, testCase := range testCases { ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, testCase.args.title, testCase.args.description, testCase.args.resTimeLimit, testCase.args.resSharedTo, testCase.args.isPublished) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, testCase.args.title, testCase.args.description, testCase.args.resTimeLimit, testCase.args.resSharedTo, testCase.args.isPublished, testCase.args.isAnonymous) if !testCase.expect.isErr { assertion.NoError(err, testCase.description, "no error") @@ -525,6 +546,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit null.Time resSharedTo string isPublished bool + isAnonymous bool } type expect struct { isErr bool @@ -547,6 +569,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", @@ -554,6 +577,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "respondents", isPublished: true, + isAnonymous: false, }, }, { @@ -564,6 +588,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, after: args{ title: "第2回集会らん☆ぷろ募集アンケート", @@ -571,6 +596,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, }, { @@ -581,6 +607,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", @@ -588,6 +615,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, }, { @@ -598,6 +626,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", @@ -605,6 +634,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "respondents", isPublished: true, + isAnonymous: false, }, }, { @@ -615,6 +645,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, after: args{ title: "第2回集会らん☆ぷろ募集アンケート", @@ -622,6 +653,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, }, { @@ -632,6 +664,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", @@ -639,6 +672,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, }, { @@ -649,6 +683,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", @@ -656,6 +691,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, }, { @@ -666,6 +702,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", @@ -673,6 +710,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Now().Add(time.Minute), true), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, }, { @@ -683,6 +721,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", @@ -690,6 +729,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, }, { @@ -700,6 +740,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: false, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", @@ -707,6 +748,26 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, + }, + }, + { + description: "update is_anonymous(false->true)", + before: args{ + title: "第1回集会らん☆ぷろ募集アンケート", + description: "第1回集会らん☆ぷろ参加者募集", + resTimeLimit: null.NewTime(time.Time{}, true), + resSharedTo: "public", + isPublished: true, + isAnonymous: false, + }, + after: args{ + title: "第1回集会らん☆ぷろ募集アンケート", + description: "第1回集会らん☆ぷろ参加者募集", + resTimeLimit: null.NewTime(time.Time{}, true), + resSharedTo: "public", + isPublished: true, + isAnonymous: true, }, }, } @@ -732,7 +793,7 @@ func updateQuestionnaireTest(t *testing.T) { createdAt := questionnaire.CreatedAt questionnaireID := questionnaire.ID after := &testCase.after - err = questionnaireImpl.UpdateQuestionnaire(ctx, after.title, after.description, after.resTimeLimit, after.resSharedTo, questionnaireID, after.isPublished) + err = questionnaireImpl.UpdateQuestionnaire(ctx, after.title, after.description, after.resTimeLimit, after.resSharedTo, questionnaireID, after.isPublished, false) if !testCase.expect.isErr { assertion.NoError(err, testCase.description, "no error") @@ -786,6 +847,7 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, { title: "第1回集会らん☆ぷろ募集アンケート", @@ -793,13 +855,14 @@ func updateQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, } for _, arg := range invalidTestCases { ctx := context.Background() - err := questionnaireImpl.UpdateQuestionnaire(ctx, arg.title, arg.description, arg.resTimeLimit, arg.resSharedTo, invalidQuestionnaireID, arg.isPublished) + err := questionnaireImpl.UpdateQuestionnaire(ctx, arg.title, arg.description, arg.resTimeLimit, arg.resSharedTo, invalidQuestionnaireID, arg.isPublished, arg.isAnonymous) if !errors.Is(err, ErrNoRecordUpdated) { if err == nil { t.Errorf("Succeeded with invalid questionnaireID") @@ -821,6 +884,7 @@ func deleteQuestionnaireTest(t *testing.T) { resTimeLimit null.Time resSharedTo string isPublished bool + isAnonymous bool } type expect struct { isErr bool @@ -839,6 +903,7 @@ func deleteQuestionnaireTest(t *testing.T) { resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", isPublished: true, + isAnonymous: false, }, }, } @@ -852,6 +917,7 @@ func deleteQuestionnaireTest(t *testing.T) { ResTimeLimit: testCase.args.resTimeLimit, ResSharedTo: testCase.args.resSharedTo, IsPublished: testCase.isPublished, + IsAnonymous: testCase.args.isAnonymous, } err := db. Session(&gorm.Session{NewDB: true}). diff --git a/model/respondents_impl.go b/model/respondents_impl.go index 2cbf4aaf..233dbcfc 100755 --- a/model/respondents_impl.go +++ b/model/respondents_impl.go @@ -291,16 +291,25 @@ func (*Respondent) GetRespondentDetails(ctx context.Context, questionnaireID int responseIDs = append(responseIDs, respondent.ResponseID) } + isAnonymous, err := NewQuestionnaire().GetResponseIsAnonymousByQuestionnaireID(ctx, questionnaireID) + respondentDetails := make([]RespondentDetail, 0, len(respondents)) respondentDetailMap := make(map[int]*RespondentDetail, len(respondents)) for i, respondent := range respondents { - respondentDetails = append(respondentDetails, RespondentDetail{ + r := RespondentDetail{ ResponseID: respondent.ResponseID, - TraqID: respondent.UserTraqid, QuestionnaireID: questionnaireID, SubmittedAt: respondent.SubmittedAt, ModifiedAt: respondent.ModifiedAt, - }) + } + + if !isAnonymous { + r.TraqID = respondent.UserTraqid + } else { + r.TraqID = "" + } + + respondentDetails = append(respondentDetails, r) respondentDetailMap[respondent.ResponseID] = &respondentDetails[i] } @@ -387,18 +396,23 @@ func (*Respondent) GetRespondentsUserIDs(ctx context.Context, questionnaireIDs [ } // GetMyResponses 自分のすべての回答を取得 -func (*Respondent) GetMyResponseIDs(ctx context.Context, userID string) ([]int, error) { +func (*Respondent) GetMyResponseIDs(ctx context.Context, sort string, userID string) ([]int, error) { db, err := getTx(ctx) if err != nil { return nil, fmt.Errorf("failed to get transaction: %w", err) } responsesID := []int{} - err = db. - Model(&Respondents{}). + query := db.Model(&Respondents{}). Where("user_traqid = ?", userID). - Select("response_id"). - Find(&responsesID).Error + Select("response_id") + + query, _, err = setRespondentsOrder(query, sort) + if err != nil { + return nil, fmt.Errorf("failed to set respondents order: %w", err) + } + + err = query.Find(&responsesID).Error if err != nil { return nil, fmt.Errorf("failed to get responsesID: %w", err) } diff --git a/model/respondents_test.go b/model/respondents_test.go index a9f353bc..41078f11 100644 --- a/model/respondents_test.go +++ b/model/respondents_test.go @@ -19,7 +19,7 @@ func TestInsertRespondent(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -129,7 +129,7 @@ func TestUpdateSubmittedAt(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -203,7 +203,7 @@ func TestDeleteRespondent(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -391,9 +391,9 @@ func TestGetRespondentInfos(t *testing.T) { args expect } - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第2回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第2回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) - questionnaireID2, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第2回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID2, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第2回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) questionnaire := Questionnaires{} @@ -523,7 +523,7 @@ func TestGetRespondentDetail(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false) require.NoError(t, err) questionnaire := Questionnaires{} @@ -620,7 +620,7 @@ func TestGetRespondentDetails(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false) require.NoError(t, err) questionnaire := Questionnaires{} @@ -868,7 +868,7 @@ func TestGetRespondentDetails(t *testing.T) { } for _, testCase := range testCases { - respondentDetails, err := respondentImpl.GetRespondentDetails(ctx, testCase.args.questionnaireID, testCase.args.sort) + respondentDetails, err := respondentImpl.GetRespondentDetails(ctx, testCase.args.questionnaireID, testCase.args.sort, false, "") if !testCase.expect.isErr { assertion.NoError(err, testCase.description, "no error") } else if testCase.expect.err != nil { @@ -909,7 +909,7 @@ func TestGetRespondentsUserIDs(t *testing.T) { } questionnaireIDs := make([]int, 0, 3) for i := 0; i < 3; i++ { - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) questionnaireIDs = append(questionnaireIDs, questionnaireID) } @@ -997,7 +997,7 @@ func TestGetMyResponseIDs(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false) require.NoError(t, err) respondents := []Respondents{ @@ -1025,6 +1025,7 @@ func TestGetMyResponseIDs(t *testing.T) { } type args struct { + sort string userID string } type expect struct { @@ -1042,6 +1043,7 @@ func TestGetMyResponseIDs(t *testing.T) { { description: "valid user with one resonse", args: args{ + sort: "submitted_at", userID: userOne, }, expect: expect{ @@ -1051,6 +1053,7 @@ func TestGetMyResponseIDs(t *testing.T) { { description: "valid user with multiple responses", args: args{ + sort: "submitted_at", userID: userTwo, }, expect: expect{ @@ -1060,6 +1063,7 @@ func TestGetMyResponseIDs(t *testing.T) { { description: "valid user with no response", args: args{ + sort: "submitted_at", userID: userThree, }, expect: expect{ @@ -1069,7 +1073,7 @@ func TestGetMyResponseIDs(t *testing.T) { } for _, testCase := range testCases { - MyResponseIDs, err := respondentImpl.GetMyResponseIDs(ctx, testCase.args.userID) + MyResponseIDs, err := respondentImpl.GetMyResponseIDs(ctx, testCase.args.sort, testCase.args.userID) if !testCase.expect.isErr { assertion.NoError(err, testCase.description, "no error") @@ -1092,7 +1096,7 @@ func TestTestCheckRespondent(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) diff --git a/model/responses_test.go b/model/responses_test.go index ec0b5b1d..cd6793f3 100644 --- a/model/responses_test.go +++ b/model/responses_test.go @@ -19,7 +19,7 @@ func TestInsertResponses(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -142,7 +142,7 @@ func TestDeleteResponse(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) diff --git a/model/scale_labels_test.go b/model/scale_labels_test.go index ad5ea98e..12e12b11 100644 --- a/model/scale_labels_test.go +++ b/model/scale_labels_test.go @@ -20,7 +20,7 @@ func TestInsertScaleLabel(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -163,7 +163,7 @@ func TestUpdateScaleLabel(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -283,7 +283,7 @@ func TestDeleteScaleLabel(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -371,7 +371,7 @@ func TestGetScaleLabels(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -489,7 +489,7 @@ func TestCheckScaleLabel(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) diff --git a/model/targets.go b/model/targets.go index a1bb5506..e6d4cc17 100644 --- a/model/targets.go +++ b/model/targets.go @@ -10,4 +10,5 @@ type ITarget interface { DeleteTargets(ctx context.Context, questionnaireID int) error GetTargets(ctx context.Context, questionnaireIDs []int) ([]Targets, error) IsTargetingMe(ctx context.Context, quesionnairID int, userID string) (bool, error) + CancelTargets(ctx context.Context, questionnaireID int, targets []string) error } diff --git a/model/targets_impl.go b/model/targets_impl.go index 6e046217..65196014 100644 --- a/model/targets_impl.go +++ b/model/targets_impl.go @@ -17,6 +17,7 @@ func NewTarget() *Target { type Targets struct { QuestionnaireID int `gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` UserTraqid string `gorm:"type:varchar(32);size:32;not null;primaryKey"` + IsCanceled bool `gorm:"type:tinyint(1);not null;default:0"` } // InsertTargets アンケートの対象を追加 @@ -35,6 +36,7 @@ func (*Target) InsertTargets(ctx context.Context, questionnaireID int, targets [ dbTargets = append(dbTargets, Targets{ QuestionnaireID: questionnaireID, UserTraqid: target, + IsCanceled: false, }) } @@ -101,3 +103,21 @@ func (*Target) IsTargetingMe(ctx context.Context, questionnairID int, userID str } return false, nil } + +// CancelTargets アンケートの対象をキャンセル(削除しない) +func (*Target) CancelTargets(ctx context.Context, questionnaireID int, targets []string) error { + db, err := getTx(ctx) + if err != nil { + return fmt.Errorf("failed to get transaction: %w", err) + } + + err = db. + Model(&Targets{}). + Where("questionnaire_id = ? AND user_traqid IN (?)", questionnaireID, targets). + Update("is_canceled", true).Error + if err != nil { + return fmt.Errorf("failed to cancel targets: %w", err) + } + + return nil +} \ No newline at end of file diff --git a/model/targets_test.go b/model/targets_test.go index 79adf7c6..b525eb6e 100644 --- a/model/targets_test.go +++ b/model/targets_test.go @@ -318,7 +318,7 @@ func TestIsTargetingMe(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false) require.NoError(t, err) err = targetImpl.InsertTargets(ctx, questionnaireID, []string{userOne}) @@ -376,3 +376,119 @@ func TestIsTargetingMe(t *testing.T) { assertion.Equal(testCase.expect.isTargeted, isTargeted, testCase.description, "isTargeted") } } + +func TestCancelTargets(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + type test struct { + description string + beforeValidTargets []string + beforeInvalidTargets []string + afterValidTargets []string + afterInvalidTargets []string + argCancelTargets []string + isErr bool + err error + } + + testCases := []test{ + { + description: "キャンセルするtargetが1人でエラーなし", + beforeValidTargets: []string{"a"}, + beforeInvalidTargets: []string{}, + afterValidTargets: []string{}, + afterInvalidTargets: []string{"a"}, + argCancelTargets: []string{"a"}, + }, + { + description: "キャンセルするtargetが複数でエラーなし", + beforeValidTargets: []string{"a", "b"}, + beforeInvalidTargets: []string{}, + afterValidTargets: []string{}, + afterInvalidTargets: []string{"a", "b"}, + argCancelTargets: []string{"a", "b"}, + }, + { + description: "キャンセルするtargetがないときエラーなし", + beforeValidTargets: []string{"a"}, + beforeInvalidTargets: []string{}, + afterValidTargets: []string{"a"}, + afterInvalidTargets: []string{}, + argCancelTargets: []string{}, + }, + { + description: "キャンセルするtargetが見つからないときエラー", + beforeValidTargets: []string{"a"}, + beforeInvalidTargets: []string{}, + afterValidTargets: []string{"a"}, + afterInvalidTargets: []string{}, + argCancelTargets: []string{"b"}, + isErr: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + targets := make([]Targets, 0, len(testCase.beforeValidTargets)+len(testCase.beforeInvalidTargets)) + for _, target := range testCase.beforeValidTargets { + targets = append(targets, Targets{ + UserTraqid: target, + IsCanceled: false, + }) + } + for _, target := range testCase.beforeInvalidTargets { + targets = append(targets, Targets{ + UserTraqid: target, + IsCanceled: true, + }) + } + questionnaire := Questionnaires{ + Targets: targets, + } + err := db. + Session(&gorm.Session{}). + Create(&questionnaire).Error + if err != nil { + t.Errorf("failed to create questionnaire: %v", err) + } + + err = targetImpl.CancelTargets(ctx, questionnaire.ID, testCase.argCancelTargets) + if err != nil { + if !testCase.isErr { + t.Errorf("unexpected error: %v", err) + } else if !errors.Is(err, testCase.err) { + t.Errorf("invalid error: expected: %+v, actual: %+v", testCase.err, err) + } + return + } + + afterTargets := make([]Targets, 0, len(testCase.afterValidTargets)+len(testCase.afterInvalidTargets)) + for _, afterTarget := range testCase.afterInvalidTargets { + afterTargets = append(afterTargets, Targets{ + UserTraqid: afterTarget, + IsCanceled: false, + }) + } + for _, afterTarget := range testCase.afterValidTargets { + afterTargets = append(afterTargets, Targets{ + UserTraqid: afterTarget, + IsCanceled: true, + }) + } + + actualTargets := make([]Targets, 0, len(testCase.afterValidTargets)+len(testCase.afterInvalidTargets)) + err = db. + Session(&gorm.Session{}). + Model(&Targets{}). + Where("questionnaire_id = ?", questionnaire.ID). + Find(&actualTargets).Error + if err != nil { + t.Errorf("failed to get targets: %v", err) + } + + assert.ElementsMatchf(t, afterTargets, actualTargets, "targets") + }) + } +} diff --git a/model/v3.go b/model/v3.go index 1935453a..2bf827b9 100644 --- a/model/v3.go +++ b/model/v3.go @@ -10,9 +10,12 @@ import ( func v3() *gormigrate.Migration { return &gormigrate.Migration{ - ID: "v3", + ID: "3", Migrate: func(tx *gorm.DB) error { - if err := tx.AutoMigrate(&Targets{}); err != nil { + if err := tx.AutoMigrate(&v3Targets{}); err != nil { + return err + } + if err := tx.AutoMigrate(&v3Questionnaires{}); err != nil { return err } return nil @@ -20,6 +23,16 @@ func v3() *gormigrate.Migration { } } +type v3Targets struct { + QuestionnaireID int `gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` + UserTraqid string `gorm:"type:varchar(32);size:32;not null;primaryKey"` + IsCanceled bool `gorm:"type:tinyint(1);not null;default:0"` +} + +func (*v3Targets) TableName() string { + return "targets" +} + type v3Questionnaires struct { ID int `json:"questionnaireID" gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` Title string `json:"title" gorm:"type:char(50);size:50;not null"` @@ -27,11 +40,11 @@ type v3Questionnaires struct { ResTimeLimit null.Time `json:"res_time_limit,omitempty" gorm:"type:TIMESTAMP NULL;default:NULL;"` DeletedAt gorm.DeletedAt `json:"-" gorm:"type:TIMESTAMP NULL;default:NULL;"` ResSharedTo string `json:"res_shared_to" gorm:"type:char(30);size:30;not null;default:administrators"` + IsAnonymous bool `json:"is_anonymous" gorm:"type:boolean;not null;default:false"` CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;not null;default:CURRENT_TIMESTAMP"` ModifiedAt time.Time `json:"modified_at" gorm:"type:timestamp;not null;default:CURRENT_TIMESTAMP"` Administrators []Administrators `json:"-" gorm:"foreignKey:QuestionnaireID"` Targets []Targets `json:"-" gorm:"foreignKey:QuestionnaireID"` - TargetGroups []TargetGroups `json:"-" gorm:"foreignKey:QuestionnaireID"` Questions []Questions `json:"-" gorm:"foreignKey:QuestionnaireID"` Respondents []Respondents `json:"-" gorm:"foreignKey:QuestionnaireID"` IsPublished bool `json:"is_published" gorm:"type:boolean;default:false"` @@ -39,4 +52,4 @@ type v3Questionnaires struct { func (*v3Questionnaires) TableName() string { return "questionnaires" -} \ No newline at end of file +} diff --git a/model/validations_test.go b/model/validations_test.go index 54ab6548..c59e1adf 100644 --- a/model/validations_test.go +++ b/model/validations_test.go @@ -20,7 +20,7 @@ func TestInsertValidation(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -212,7 +212,7 @@ func TestUpdateValidation(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -360,7 +360,7 @@ func TestDeleteValidation(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -458,7 +458,7 @@ func TestGetValidations(t *testing.T) { assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) diff --git a/openapi/spec.go b/openapi/spec.go index 603ecd54..88e3e723 100644 --- a/openapi/spec.go +++ b/openapi/spec.go @@ -17,75 +17,74 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - - "H4sIAAAAAAAC/+xc628TyZb/V6ze/QDaDjaBK931t0BGV5GGu0DC7gcSWR27kvRcu9t0twEvipRq8wjE", - "QMQjTAhDCJOBAEPCDCyb4fm/bKUd+9P+C6uqflX1ux0bZkYrIRR316k6dc6vzjl16lRf4IpypSpLQNJU", - "Ln+BqwqKUAEaUMgvWSrXh0oVURJVTRE0UDpSPwZGpBM1oNTx+xJQi4pY1URZ4vJc+8pzY/4ygs3dzbXd", - "xcvtuUsIvkDwOYI/IvgEwYvk74tI1xHcJP8+GzeXjE/3Mvs0pQb285lQQn3BotJ1Y/EF0iF5vozgbwg+", - "sTuZEsoq2I/mdNS4ghp3kf4MNV6gxjyCW+QVmtPHJY7nRMzsGTIHnpOECuDywTPleE4tzoCKgOeq1au4", - "4aQsl4EgcbOzPCE6Vj8J1KosqfFy2TRWHu6+vBM4c0+bnfc/GetLfZ2ty3iSaY4JyjTQRGk6if6R/hk1", - "PiD9V9RoEI5ClBkkiKS0/RQNNdk42VSF6XCB7Hy8ixr3yXS2d1c2EVzI7Gs9eG5s3t/99Azr+tEbYxFz", - "dZButj+EMzxUEDuipIFpoBB2ztSAigeXBFEBI8Mj0nFBm/EzhvTHqPEa6b/gQRvzI8OuOKqYwBnT0x/H", - "cwo4UxMVUOLyWGEx7Cj20gjlxIR7OANuD92OPSorWriGtp8g+Lrz6HJm387HB635xda9n1rLOoLN1tIr", - "BO8heDEzzqm1yYqoaaBUELRxjs94mho31812A96GGMz6OkZe4wWCm63vr+ChxjlN1MogoEFn+brZYMBp", - "0Vp501p6FchWRS6JU6IzmKelyxXTLhMGL1VWNAZe/6yAKS7P/VPW9RBZ862aPUkJdwzLHktcBYJSnAmV", - "tVcY6w923zwOY4Z0FYR2VVNEadocb++aLSpASKBXttmfVquUNmdtGhIH/E2Ra1Xyl6iBiuqXNmmQOXXK", - "WsjgvFCplgGXP8h79eY8EBRFqOPffwfnTlhmBncslMv/NsXlT0ezalMcEVTAzfLJGo8CDVt19UjdnOUE", - "OzoxculZIGQOH1VFrgJFEwGRkm0/WdlF9UpLwyesWdoGnqZ6n7Dn4vj0vJeVSblUT8yF3c0RTBSgM1Et", - "lBRhSsP9OMo2jXKAk6R5dih5k6MJh0Ke/A4UNdz7V4KDT3vusmfmyQ3mBnMDuYMDuYNjuVye/PuX3L/m", - "czmO56ZkpYLbcyVBAwOaWME+27cGbM0VxBLTtbteHFfmceoJKEJQ4pCzw/P0NE0cMdL0AYlZ+Rf8cxPV", - "gjt8sL83Pl/qPJpHcAHBZwheRnCBmA0vdHjTpAZbf3qKZjOeGYtlJApmHhyQfQ9IATqbfgyc11KDDxN9", - "KxPJpSP8e60ySfCRjmxUlKbL4OiMLBbTr5RjtbImVrsmHy0KZcvsxvSceuFjzQVwxyJXrvptcYx78gDN", - "7sEPp6BJWSrqajKUetlJVITzhbNCuQaC4l6eq4hS+OvZRGybauqKa0vDgUyXhUlQDhR5gilFEMdMmFaf", - "25YeNJkymYXTnXC8S+/ropOYq64m4hi6ACUDadrc53WHPcccds2YY0wTMCdZiyyatwDbkg8L9Aqa5UKA", - "VKtgnXgIJ/gYV8Z2FOW2KCuRnB2LoMdsOBYjIRdm+14z4VmgSXmhyXrMkr3EErJCmveBBXtBpWCDkPSQ", - "FXeT1N3+aoxEeEkDDkIyTAWDqQjtzc9wDQwLGhjDMXxXHfy7CM4Jk2VwpJ6OfkQdkmSpXpFramrCclk+", - "J0rTttmxWUnd0fHaZFlUZ0ApHaGZOlWHpBLJpqusJSVNjprbjaGAldGvDZcHtvSGJw6zw+xuJ3Ir5DK8", - "+/PPB42Vh6ixhhqvUWMRNT50Vi7vfLiP4FOSvb6K9Nv/8/1lBP8b6dcRfLq7/K69tkGSQKsIXtx59w7p", - "t4yF1c7KZfLwE4LLmURkUEdwHRPoTdT48L8fYKw46FkkkIcmiOUuV/HIcDo0pdrOe8CViuyYlUwb0nqS", - "xAnP4Nh56ZJ94BbVzSkVKGpE1oftbMK/1EaGw81+r7IJsYgZkabkL2f192y8e4OgFJaWPm2ajZdmhIn3", - "6VpUC4LVulCxmhcUur03aT5HTEizvX6ldfeVsfLQPrJ8iuB1pC/4Ujapk39R/CSZvOMYAydLv/Uknpqf", - "jcXr9nT2PAtnoAQ8u640iOcq/TbymA7BpnHp587SAoJ3sXV3TkT3PB2XhwTTOQkqolT6RsKRTfCUFNKi", - "ANwmwQfFL4wt7NfwFBrPUeMhOTR5jRpXEWy2Hlw1rv1GT42c5z4hx6Rv8f+waWx9av+yRsoFnmK/p18j", - "7ddtLd+jyw6sw9Y5iOBtBDeNuXUEm1hANnETwVd+PjpzcOfzmiVxfcE+Oo6XqUcICQRLG4IgsWr2+0IF", - "7On0fc94YTiJndm3ohoQ61WFaVCoCOcDlurifHtjHodJ9ql46+6rsHOlsDx5emdNyEZrlYqg1GNTLQ73", - "vmFjxUF5Cp9QqEO9vkbA9DixDAe5VB/nthUvlGqggBkpaGIQTM212Xqw2llexCuaLEbreFO/1YE3EP63", - "ivSrjOvBCL6J/ydLUKqVy3T9BNMpbnrPXqc9lWAyMVG7vnApnbUaFSbrCc7iRmcEBTjn7LQiAzuM1aiN", - "8i8Txf//3v2PsXf3hKJfe9sllMsFe38TFBzZVV/YELiBQNOyBdvzCH52bEQGe7YM0m+1P9/BhNjw/ID0", - "plVnB7cypDCMaZHZx3T78nvjgbnTpqIJuOXpeX8Sh8pzM4JaqNTdM3RvGL7QWtkmZs8ZF0dJXERXCnXw", - "H2Rxk3TlSNs14IXJuhVtdOFlmFn6OeU9Kg7YwPrSST6LKjjPY3fSQ1LJKmTBttzsOS2h9+DZ6oW3+Yg1", - "vWP2iTY7Deeg25dDsvJGbOrHygyx+4NYfZiDBLHI+Jj4fcjm7n8tth4+QPotPtOBC8bSWwS32k8WCJc4", - "TM7sG7ckMs7t5zOeNUn2mL72VC5jnNufaT9/hX2/rvv6leqyBMY5a6lZSWtL/mxKhLca4zm7grWeBZRl", - "0NUz3TvG6G1prG3oWwxIyyYO92OKcGakxFF1lIlKVegCyL4GsDRXnmH5uOg2tgDLgQExSEwtVOLiEJoq", - "UWGIlyBRUQhNlLAghCZJVQxCE6YsBGHGdItA6MdHBBWMWEDyG3lJPWc+j8lOWg1DTBwzWNjRqW8s6pC4", - "y6FGTQzHDxUC9oRD9amGhvUFHbjduvawrT8mxxhmnuQKajSQvoX03xDc7Fy6bszfI7Y5bLbe8gbKgERu", - "ukPl4AVTjwpvkkKagpOPlZ4U06RhxF5Efk56X8DSFV8pITUu+afSg9KVNKxb6zeQjR4Vq+yVHad02xe8", - "2Z5y984GgpuqrGg4ftvY7Kw9pMInjwcd8Py2CywH7D9oF8szxej+qgGeOz+Axxk4KyiSUMHW4DQ3ag8w", - "pA2NHuV4+sHwN+QJCZWH3D+tx+6mccjzmzSgpfIfojbjO4sa0UAlucbcWICPOUmzjrmSR4uYIKj0KMLM", - "q6kruIOOIJ3eAuWTeohwKQeMHQ5UW5xoDjKSNYGL9I/u7sOLXupAn6dva/QFuU6uw4Sf83PPqLVibp9g", - "NEU4kfHerOCK1cFcUIxvHhon1aEb53sV5dn5+oKXaed5VP/uhrtm85X+xNsk5e0h/QuEXD4q1hRRq4/i", - "nqyAo1oti0XBrs6YKsvnzOc1bUZWxP8kb47KJeB7eEopc3luRtOqaj6bPXNAU4Tqge+qWaEqZs8eysq4", - "8WDWJjFv5clVO8UrlLAcy3i4DP4lStMZBahyTSkCPI1ziqgBtwlBYJ1thPUh/wMkZIQ0ZUwJeW5e47Ht", - "UlGWNKFI3KZ1N0hThOMcz9WYMaZFbaY2eaAoV7L4vSZqoDiTFaR/gAFNxnyx2LReZIaOjziLzfv0LFBU", - "s/XBA7kDuQFZUA/hnuQqkISqyOW5Q/g5DhgFbYYIMes/y5kGgZmyGwjOW+kBuNr6YW3n/Vuk32q9myMV", - "McuDuZ33b3fe/7SzvUCMiDeRgRrPcdjamEf6LfP6qFNug+Z0jjCpEEzglcn9DWgnWM545sp0iDNxm2Tp", - "S2thjp9uztypS0BAX0hN0Dzkbm9CytC74diJMXUGg7mcDUIr+0Ctzux3qrlEk11T8x8uEqCzyGi9/NHY", - "3kbwha1V8xjpk1UrNaf7sWDuXOxULoWCWZ47bPIfjb7GJePRLwhuGh8fGx9uItjcvfuKnFldM/vCHf0l", - "qCMvL/qtcPZXkH6b/MTzMHs85O9x9MS3mJHN1fZas7Wsd5ZuI9g8pOLJvb2EmYar9oG4vrP9HsEXrZc/", - "tp/cbK9t7N78hGDTuLFqrDwisyfZ0mnVV/1DnFZVVgPWpXOh0T8z85Jl5Co7LqvsMrPu/gJVs5MwPQGS", - "7+7fLOt5NKUGZn1APtgfIFuFdVFQDhemF9z2c7NOZNVP+HuCuG8SHohH4G+W97qK7AXPnfVZk5cy0EAS", - "royr1zrL65HwHCadeQGazg0EX9QPM5uJ8WBz78VDqH79VnBkGGv14hqpvnmaRqVw0x6+2ZUy+WAf7x/G", - "QmFXHrvPmvoqdiHMxe1V64dzhxMVpblHo5a+++Hoor2QoBVn0kFn/Wpr5U0kdL4piX3ETu/dWShs4jxa", - "ShtjSe6L2Jg0fPTWbWQrdbPScVQTtFr4FqTLYsaujNcxlqU/minz1o7+SWwaq+72xktj836/LVv3qOvC", - "7vURdn22goGI24s57Jfx+xqw7LP9ZKSaMLIzLm1YFV1m0VKQlWQr+d0i0JNOcQOW77N3raUrxst7eEdv", - "VkDbW5QkhtbNt/cI7PH5lKDvVSVMw/i/QddXm07VJf4pDLiDtR5Z7OiMiDOaPxHin4VzD2f35R3j5la7", - "8ZFwwdxn6MAbxo331OcFm/Y2zNz9J86yUN8D/D0beKZA58smbNhxw4DvVXBMcsZp38+czJ5WzeHBwZhC", - "fth0K/fhKtIhW7wftwK9soPNPeaEHASQKD7ODzkf32Rqip2PdaY+IjhW795/BLqBvVrzVMe5IafC/uKc", - "9LbfkWlUqj0QIQHfUA1QSwK0ONXHXqBccD81GZkydCMTNlMYkibs2qj6vp2ZMjno45NRRlLj4ojbtjLB", - "xuVQAjnhTUpr45lpLuzreraubHN3OFGU6DBlWx9yO7Xpsz5h9iz3ly7tWYA0Iw2bq8SuURqSFY0IkINM", - "Uv9h+AUdbNrIktJCUDwZBd7O0q/tJ0+7B28MEnsfjzLICUkeuNhJlgzoNXh6HxOmCwiTmc2gnf6XNJvW", - "+FHI+0PYzC9nKuk6IAJPpgLotFmaY9ffTGAsqkA5a2OZZaeqyKVa0fqSJFsiY1W30MU4AVWtZbkolBna", - "fDZLHs7Iqpb/a+6vOZNywpnLhcDPbpO+PZ/D5mYnZv8vAAD//69Dqo+7XwAA", + "H4sIAAAAAAAC/+xc628TyZb/V6ze/QDaDjaBK931t0BGV5GGu0Bg9wOJrI5dSXqu3W2624AXRUq1eQRi", + "SMQjTAhDCJOBAEPCDCyb4fm/bKUd+9P+C6uqflV3V78cG2ZGKyEUd9epOnXOr845depUX+SKcqUqS0DS", + "VC5/kasKilABGlDIL1kq14dKFVESVU0RNFA6Uj8GRqQTNaDU8fsSUIuKWNVEWeLyXPvqc2PuCoLN3c21", + "3cUr7dnLCL5A8DmCPyL4BMFL5O9LSNcR3CT/PhsLS8ane5l9mlID+/lMKKE+b1HpurH4AumQPF9G8DcE", + "n9idTAplFexHszpqXEWNu0h/hhovUGMOwS3yCs3qYxLHcyJm9iyZA89JQgVwefZMOZ5Ti9OgIuC5avUq", + "bjghy2UgSNzMDE+IjtVPArUqS2q8XDaNlYe7L+8wZ+5rs/P+J2N9qa+zdRlPMs1TgjIFNFGaSqJ/pH9G", + "jQ9I/xU1GoSjEGWyBJGUtp+ioSYbJ5uqMBUukJ2Pd1HjPpnO9u7KJoLzmX2tB8+Nzfu7n55hXT96Yyxi", + "rg7SzfaHcIaHYrEjShqYAgph52wNqHhwSRAVMDI8Ih0XtOkgY0h/jBqvkf4LHrQxNzLsiqOKCZwxff1x", + "PKeAszVRASUujxUWw45iL41QTky4hzPg9tDt2KOyooVraPsJgq87j65k9u18fNCaW2zd+6m1rCPYbC29", + "QvAegpcyY5xam6iImgZKBUEb4/iMr6mxsG62G/A3xGDW1zHyGi8Q3Gx9fxUPNcZpolYGjAad5RtmgwGn", + "RWvlTWvpFZOtilwSJ0VnMF9LlytPu0wYvFRZ0Tzw+mcFTHJ57p+yrofImm/V7ElKuKew7LHEVSAoxelQ", + "WfuFsf5g983jMGZIVyy0q5oiSlPmeHvXbFEBQgK9epv9abVKaXPGpiFxwN8UuVYlf4kaqKhBaZMGmdOn", + "rYUMLgiVahlw+YO8X2/OA0FRhDr+/Xdw/oRlZnDHQrn8b5Nc/kw0qzbFEUEF3AyfrPEo0LBVV4/UzVmO", + "e0cnRi49C4TM4aOqyFWgaCIgUrLtp1d2Ub3S0ggIa4a2gWeo3sftuTg+Pe9nZUIu1RNzYXdzBBMxdCaq", + "hZIiTGq4H0fZplFmOEmaZ4eSNzkadyjkie9AUcO9fyU4BLTnLnvPPLnB3GBuIHdwIHfwVC6XJ//+Jfev", + "+VyO47lJWang9lxJ0MCAJlawzw6sAVtzBbHEiKNePzfuLiD9VvvzR+P6Ixze6PNOrCDVymU7gGGsM8cF", + "+oIBa6QoihB0OeQ8LRATcR65h0IuMH1RLbgjsUMC4/PlzqM5BOcRfIbgFQTnyYxj0EVG9PYfBTAfAsiO", + "B6SAm01/ClzQUsMOE30rE4GkI/x7rTJBNJyObFSUpsrg6LQsFtOvkWO1siZWuyYfLQply+DG9Jx6yWPN", + "MbjzIlGuBq1wjGPyIcvuIQgn1qQsFXU1GUq93klUhAuFc0K5BlgRL89VRCn89Uwitk01dcW1pWEm02Vh", + "ApSZIk8wpQjimAnT6nPb0oMmU6Zn4XQnHP/S+7roJOaqq4k4ho6hZCBNmTu87rDnmMOuGXOMaQLmJGuR", + "RfPGsC35sBCvoFkuBEi1CtaJj3CcZ2xhWP7W7CjKbVFWIjk7FkGP2XAsRkIuzPa9ZsK3QJPyQpP1mCV7", + "iSVkhTTvAwv2gkrBBiHpISvu9qi7ndUpvGdOHHAQkmEqnkxFaG97hmtgWNDAKRy9d9XBv4vgvDBRBkfq", + "6ehH1CFJluoVuaamJiyX5fOiNGWbHZuV1B0dr02URXUalNIRmklTdUgqkTy66rWkpMlRc/swxFgZ/dpq", + "+WBLb2DiMEujKMBvyfvSZXj3558PGisPUWMNNV6jxiJqfOisXNn5cB/BpyRvfQ3pt//n+ysI/jfSbyD4", + "dHf5XXttg6R/VhG8tPPuHdJvGfOrnZUr5OEnBJczicigjuA6JtCbqPHhfz/AWHHQs0ggD00Qy12u4pHh", + "dGhKtZH3gSsV2TErjTak9SR9E567sTPSJfuoLaqb0ypQ1Ih8j7ez8eBSGxkON/u9ygfEImZEmpS/nNXf", + "s/HuDYJSWFr6nGkmXpoRJj6ga1EtCFbrQsVqXlDo9v50+SwxIc32+tXW3VfGykP7sPIpgjeQPh/IxKRO", + "+0Xxk2TyjmNkTpZ+68snNT8bizfs6ex5Fs5ACXh2XSmL5yr9NvKADsGmcfnnztI8gnexdXfOQvc8HZeH", + "BNM5CSqiVPpGwpENe0oKaVEAbhP2EfELY+uTmddEjeeo8ZAcl7xGjWsINlsPrhnXf6OnRk5yn5AD0rf4", + "f9g0tj61f1kjhQJPsd/Tr5P267aW79EFB1bqdBYieBvBTWN2HcEmFpBN3ETwVZCPzizc+bxmSVyft3Ou", + "8TL1CSGBYGlDwBKrZr8vVMCezt33jBcPJ7Ez+1ZUGbFeVZgChYpwgbFUF+faG3M4TLLPw1t3X6XNdKd3", + "1oRstFapCEo9NtXicB8YNlYclKcICIU6zutrBEyPE8swy6UGOLeteKFUAwXMSEETWTA112brwWpneRGv", + "aLIYrYNN/VYH3kT43yrSr3lcD0bwAv6fLEFy+kFVTng6xU3vBc5GeiHBZGKidn3hUjpnNSpM1BOcwo1O", + "CwpwTthpRTI7jNWojfIvE8X//979j7F394WiX3vbJZTLBXt/wwqO7HovbAjcQKBp2YLtOQQ/OzYigz1b", + "hhym3sGE2PD8gPSmVWEHtzKkJMzTIrPP0+3L740H5k6biibglq/n/UkcKs9NC2qhUndPz/1h+HxrZZuY", + "PWdcHCVxEV0p1JE/y+Im6cqRtmvACxN1K9rowst4ZhnklPepmLGBDaSTAhZVcJ7H7qSHpJJVwoJtudlz", + "WkLfDO1eeJuPWNNrGsPANDT7cSCHZOWNvKkfKzPk3R/E6sMchMWix8fE70M2d/9rsfXwAdJv8ZkOnDeW", + "3iK41X4yT7jEYXJm35glkTFuP5/xrUmyxwy0p3IZY9z+TPv5K+z7dT3Qr1SXJTDGWUvNSlpb8vemRHir", + "MZ6zK1jrGaMgg66b6d4xRm9LY21D32JAWjZxuD+lCGdHShxVQZkgVcR7aiT7GsDSXPmG5eOi29jSKwcG", + "xCB5qqASF4fQVIkKQ/wEiYpCaKKEBSE0SapiEJowZSGIZ0y3CIR+fERQwYgFpKCRl9Tz5vOY7KTVMMTE", + "eQYLOzoNjEUdEnc51KiJ4fihQsCecKg+1dB4fUEHbreuP2zrj8kxhpknuYoaDaRvIf03BDc7l28Yc/eI", + "bQ6brb+8gTIgkZvuUDn4wdSjwpukkKbgFGClJ8U0aRixF1GQk94XsHTFV0pIjUnBqfSgdCUN69b6ZbLR", + "o2KVvbLjFG0HgjfbU+7e2UBwU5UVDcdvG5udtYdU+OTzoAO+32bkyFu16z4Xy3vK0INVAzx3YQCPM3BO", + "UCShgq3BGW7UHmBIGxo9yvH0g+FvyBMSKg+5f1qP3U3jkO83aUBL5T9EbTpwFjWigUpyjbmxAB9zkmYd", + "cyWPFjEBq/QowsyrqWu3WUeQTm9M+aQeIlzKjLHDgWqLE81Cj2RN4CL9o7v78KOXOtDn6XsafUGuk+sw", + "4ef83DNqrZg7IBhNEU5k/HcquGJ1MMeK8c1D46Q6dON8v6J8O99A8DLlPI/q391w12y+0p94m6S8PWRw", + "gZBrR8WaImr1UdyTFXBUq2WxKNjVGZNl+bz5vKZNy4r4n+TNUbkEAg9PK2Uuz01rWlXNZ7NnD2iKUD3w", + "XTUrVMXsuUNZGTcezNok5n08uWqneIUSlmMZD5fBv0RpKqMAVa4pRYCncV4RNeA2IQisexthfcj/AAkZ", + "IU09poQ8Ny/w2HapKEuaUCRu07oVpCnCcY7nap4xpkRtujZxoChXsvi9JmqgOJ0VpH+AAU3GfHmxab3I", + "DB0fcRab/+k5oKhm64MHcgdyA7KgHsI9yVUgCVWRy3OH8HMcMAraNBFiNniWMwWYmbKbCM5Z6QG42vph", + "bef9W6Tfar2bJRUxy4O5nfdvd97/tLM9T4yIP5GBGs9x2NqYQ/ot8+KoU26DZnWOMKkQTOCVyf0NaCe8", + "nPGey9IhzsRtkqWvq4U5frq55zZdAgL6KmqC5iG3ehNSht4Kx07MU2cwmMvZILSyD9TqzH6nmks02QW1", + "4OEiAboXGa2XPxrb2wi+sLVqHiN9smqlZvUgFsydi53KpVAww3OHTf6j0de4bDz6BcFN4+Nj48MCgs3d", + "u6/ImdV1sy/c0V9YHfl50W+Fs7+C9NvkJ56H2eOhYI+jJ77FjGyuttearWW9s3QbweYhFU/u7WXMNFy1", + "D8T1ne33CL5ovfyx/WShvbaxu/AJwaZxc9VYeURmT7KlU2qg+oc4raqsMtalc5UxODPzemXkKjsuq95l", + "Zt36BapmJ2F6AqTArb8Zr+fRlBqYCQD5YH+AbBXWRUE5XJh+cNvPzTqR1SDh7wnigUn4IB6Bvxne7yqy", + "F3231WdMXspAA0m4Mq5d7yyvR8JzmHTmB2g6N8C+oh9mNhPjwebej4dQ/Qat4Mgw1uqlNVJ98zSNSuGm", + "PXyzK2XybB8fHMZCYVceu8+a+ip2IczF7VXrh3OHExWluUejlr774eiivZCgFafTQWf9WmvlTSR0vimJ", + "fcRO791ZKGziPFpKG2NJ7ovYmDR89NZtZCt1s9JxVBO0WvgWpMtixq6M1zEvS380U+avHf2T2DSvutsb", + "L43N+/22bN2jrgu710fY9dkKMhG3F3PYL+P3NWDZZ/vpkWrCyM64vGFVdJlFSywr6a3kd4tATzrFDVi+", + "z961lq4aL+/hHb1ZAW1vUZIYWjff3iOwx+dTWF+qSpiGCX59rq82napL/FMYcAdrPbLY0RkRZ7RgIiQ4", + "C+cezu7LO8bCVrvxkXDhuc/QgTeNm++pDws27W2YuftPnGWhvgT4ezbwngKdL5uw8Y4bBny/gmOSM077", + "fuZk9rRqDg8OxhTyw6ZbuQ9XkQ69xftxK9AvO9jcY07IQQCJ4uP8kPPZTU9NsfOZztRHBMfq3fsPphvY", + "qzVPdZwbciocLM5Jb/sdmUal2pkIYXw9laGWBGhxqo/9QLnofmQyMmXoRibeTGFImrBroxr4ambK5GCA", + "T48ykhoXR9y2lWEbl0MJ5IQ3Ka2NZ6a5sK/r2bqyzd3hRFGiw5Rtfcjt1GbA+oTZs9xfurRnDGlGGjZX", + "iV2jNCQrGhEgs0xS/2H4BR1s2siS0gIrnowCb2fp1/aTp92DNwaJvY9HPcgJSR642EmWDOg1eHofE6YL", + "CJOZTdZO/0uaTWv8KOT9IWzmlzOVdB0QgaenAuiMWZpj19+MYyyqQDlnY9nLTlWRS7Ui+eEvkbGqW+hi", + "HEZVa1kuCmUPbT6bJQ+nZVXL/zX315xJOe7M5SLzg9ukb9+HsLmZ8Zn/CwAA///nbjGLtV8AAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/openapi/types.go b/openapi/types.go index ab4f9a46..f033c075 100644 --- a/openapi/types.go +++ b/openapi/types.go @@ -137,11 +137,10 @@ type Groups = []string // NewQuestion defines model for NewQuestion. type NewQuestion struct { - Description string `json:"description"` + Body string `json:"body"` // IsRequired 回答必須かどうか - IsRequired bool `json:"is_required"` - Title string `json:"title"` + IsRequired bool `json:"is_required"` union json.RawMessage } @@ -177,24 +176,24 @@ type NewResponse struct { // Question defines model for Question. type Question struct { - CreatedAt time.Time `json:"created_at"` - Description string `json:"description"` + Body string `json:"body"` + CreatedAt time.Time `json:"created_at"` // IsRequired 回答必須かどうか - IsRequired bool `json:"is_required"` - QuestionId int `json:"question_id"` - QuestionnaireId int `json:"questionnaire_id"` - Title string `json:"title"` + IsRequired bool `json:"is_required"` + + // QuestionId 質問を追加する場合はnull。 + QuestionId *int `json:"question_id,omitempty"` + QuestionnaireId int `json:"questionnaire_id"` union json.RawMessage } // QuestionBase defines model for QuestionBase. type QuestionBase struct { - Description string `json:"description"` + Body string `json:"body"` // IsRequired 回答必須かどうか - IsRequired bool `json:"is_required"` - Title string `json:"title"` + IsRequired bool `json:"is_required"` } // QuestionSettingsByType defines model for QuestionSettingsByType. @@ -862,9 +861,9 @@ func (t NewQuestion) MarshalJSON() ([]byte, error) { } } - object["description"], err = json.Marshal(t.Description) + object["body"], err = json.Marshal(t.Body) if err != nil { - return nil, fmt.Errorf("error marshaling 'description': %w", err) + return nil, fmt.Errorf("error marshaling 'body': %w", err) } object["is_required"], err = json.Marshal(t.IsRequired) @@ -872,11 +871,6 @@ func (t NewQuestion) MarshalJSON() ([]byte, error) { return nil, fmt.Errorf("error marshaling 'is_required': %w", err) } - object["title"], err = json.Marshal(t.Title) - if err != nil { - return nil, fmt.Errorf("error marshaling 'title': %w", err) - } - b, err = json.Marshal(object) return b, err } @@ -892,10 +886,10 @@ func (t *NewQuestion) UnmarshalJSON(b []byte) error { return err } - if raw, found := object["description"]; found { - err = json.Unmarshal(raw, &t.Description) + if raw, found := object["body"]; found { + err = json.Unmarshal(raw, &t.Body) if err != nil { - return fmt.Errorf("error reading 'description': %w", err) + return fmt.Errorf("error reading 'body': %w", err) } } @@ -906,13 +900,6 @@ func (t *NewQuestion) UnmarshalJSON(b []byte) error { } } - if raw, found := object["title"]; found { - err = json.Unmarshal(raw, &t.Title) - if err != nil { - return fmt.Errorf("error reading 'title': %w", err) - } - } - return err } @@ -1085,14 +1072,14 @@ func (t Question) MarshalJSON() ([]byte, error) { } } - object["created_at"], err = json.Marshal(t.CreatedAt) + object["body"], err = json.Marshal(t.Body) if err != nil { - return nil, fmt.Errorf("error marshaling 'created_at': %w", err) + return nil, fmt.Errorf("error marshaling 'body': %w", err) } - object["description"], err = json.Marshal(t.Description) + object["created_at"], err = json.Marshal(t.CreatedAt) if err != nil { - return nil, fmt.Errorf("error marshaling 'description': %w", err) + return nil, fmt.Errorf("error marshaling 'created_at': %w", err) } object["is_required"], err = json.Marshal(t.IsRequired) @@ -1100,9 +1087,11 @@ func (t Question) MarshalJSON() ([]byte, error) { return nil, fmt.Errorf("error marshaling 'is_required': %w", err) } - object["question_id"], err = json.Marshal(t.QuestionId) - if err != nil { - return nil, fmt.Errorf("error marshaling 'question_id': %w", err) + if t.QuestionId != nil { + object["question_id"], err = json.Marshal(t.QuestionId) + if err != nil { + return nil, fmt.Errorf("error marshaling 'question_id': %w", err) + } } object["questionnaire_id"], err = json.Marshal(t.QuestionnaireId) @@ -1110,11 +1099,6 @@ func (t Question) MarshalJSON() ([]byte, error) { return nil, fmt.Errorf("error marshaling 'questionnaire_id': %w", err) } - object["title"], err = json.Marshal(t.Title) - if err != nil { - return nil, fmt.Errorf("error marshaling 'title': %w", err) - } - b, err = json.Marshal(object) return b, err } @@ -1130,17 +1114,17 @@ func (t *Question) UnmarshalJSON(b []byte) error { return err } - if raw, found := object["created_at"]; found { - err = json.Unmarshal(raw, &t.CreatedAt) + if raw, found := object["body"]; found { + err = json.Unmarshal(raw, &t.Body) if err != nil { - return fmt.Errorf("error reading 'created_at': %w", err) + return fmt.Errorf("error reading 'body': %w", err) } } - if raw, found := object["description"]; found { - err = json.Unmarshal(raw, &t.Description) + if raw, found := object["created_at"]; found { + err = json.Unmarshal(raw, &t.CreatedAt) if err != nil { - return fmt.Errorf("error reading 'description': %w", err) + return fmt.Errorf("error reading 'created_at': %w", err) } } @@ -1165,13 +1149,6 @@ func (t *Question) UnmarshalJSON(b []byte) error { } } - if raw, found := object["title"]; found { - err = json.Unmarshal(raw, &t.Title) - if err != nil { - return fmt.Errorf("error reading 'title': %w", err) - } - } - return err } diff --git a/router.go b/router.go index 389a4d19..ab4860d6 100644 --- a/router.go +++ b/router.go @@ -1,83 +1,83 @@ package main -import ( - "github.com/labstack/echo-contrib/prometheus" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" -) +// import ( +// "github.com/labstack/echo-contrib/prometheus" +// "github.com/labstack/echo/v4" +// "github.com/labstack/echo/v4/middleware" +// ) -// SetRouting ルーティングの設定 -func SetRouting(port string) { - e := echo.New() +// // SetRouting ルーティングの設定 +// func SetRouting(port string) { +// e := echo.New() - // Middleware - e.Use(middleware.Recover()) - e.Use(middleware.Logger()) - p := prometheus.NewPrometheus("echo", nil) - p.Use(e) +// // Middleware +// e.Use(middleware.Recover()) +// e.Use(middleware.Logger()) +// p := prometheus.NewPrometheus("echo", nil) +// p.Use(e) - api := InjectAPIServer() +// api := InjectAPIServer() - // Static Files - e.Static("/", "client/dist") - e.Static("/js", "client/dist/js") - e.Static("/img", "client/dist/img") - e.Static("/fonts", "client/dist/fonts") - e.Static("/css", "client/dist/css") +// // Static Files +// e.Static("/", "client/dist") +// e.Static("/js", "client/dist/js") +// e.Static("/img", "client/dist/img") +// e.Static("/fonts", "client/dist/fonts") +// e.Static("/css", "client/dist/css") - e.File("/app.js", "client/dist/app.js") - e.File("/favicon.ico", "client/dist/favicon.ico") - e.File("*", "client/dist/index.html") +// e.File("/app.js", "client/dist/app.js") +// e.File("/favicon.ico", "client/dist/favicon.ico") +// e.File("*", "client/dist/index.html") - echoAPI := e.Group("/api", api.SetValidatorMiddleware, api.SetUserIDMiddleware, api.TraPMemberAuthenticate) - { - apiQuestionnnaires := echoAPI.Group("/questionnaires") - { - apiQuestionnnaires.GET("", api.GetQuestionnaires, api.TrapRateLimitMiddlewareFunc()) - apiQuestionnnaires.POST("", api.PostQuestionnaire) - apiQuestionnnaires.GET("/:questionnaireID", api.GetQuestionnaire) - apiQuestionnnaires.PATCH("/:questionnaireID", api.EditQuestionnaire, api.QuestionnaireAdministratorAuthenticate) - apiQuestionnnaires.DELETE("/:questionnaireID", api.DeleteQuestionnaire, api.QuestionnaireAdministratorAuthenticate) - apiQuestionnnaires.GET("/:questionnaireID/questions", api.GetQuestions) - apiQuestionnnaires.POST("/:questionnaireID/questions", api.PostQuestionByQuestionnaireID) - } +// echoAPI := e.Group("/api", api.SetValidatorMiddleware, api.SetUserIDMiddleware, api.TraPMemberAuthenticate) +// { +// apiQuestionnnaires := echoAPI.Group("/questionnaires") +// { +// apiQuestionnnaires.GET("", api.GetQuestionnaires, api.TrapRateLimitMiddlewareFunc()) +// apiQuestionnnaires.POST("", api.PostQuestionnaire) +// apiQuestionnnaires.GET("/:questionnaireID", api.GetQuestionnaire) +// apiQuestionnnaires.PATCH("/:questionnaireID", api.EditQuestionnaire, api.QuestionnaireAdministratorAuthenticate) +// apiQuestionnnaires.DELETE("/:questionnaireID", api.DeleteQuestionnaire, api.QuestionnaireAdministratorAuthenticate) +// apiQuestionnnaires.GET("/:questionnaireID/questions", api.GetQuestions) +// apiQuestionnnaires.POST("/:questionnaireID/questions", api.PostQuestionByQuestionnaireID) +// } - apiQuestions := echoAPI.Group("/questions") - { - apiQuestions.PATCH("/:questionID", api.EditQuestion, api.QuestionAdministratorAuthenticate) - apiQuestions.DELETE("/:questionID", api.DeleteQuestion, api.QuestionAdministratorAuthenticate) - } +// apiQuestions := echoAPI.Group("/questions") +// { +// apiQuestions.PATCH("/:questionID", api.EditQuestion, api.QuestionAdministratorAuthenticate) +// apiQuestions.DELETE("/:questionID", api.DeleteQuestion, api.QuestionAdministratorAuthenticate) +// } - apiResponses := echoAPI.Group("/responses") - { - apiResponses.POST("", api.PostResponse) - apiResponses.GET("/:responseID", api.GetResponse, api.ResponseReadAuthenticate) - apiResponses.PATCH("/:responseID", api.EditResponse, api.RespondentAuthenticate) - apiResponses.DELETE("/:responseID", api.DeleteResponse, api.RespondentAuthenticate) - } +// apiResponses := echoAPI.Group("/responses") +// { +// apiResponses.POST("", api.PostResponse) +// apiResponses.GET("/:responseID", api.GetResponse, api.ResponseReadAuthenticate) +// apiResponses.PATCH("/:responseID", api.EditResponse, api.RespondentAuthenticate) +// apiResponses.DELETE("/:responseID", api.DeleteResponse, api.RespondentAuthenticate) +// } - apiUsers := echoAPI.Group("/users") - { - /* - TODO - apiUsers.GET("") - */ - apiUsersMe := apiUsers.Group("/me") - { - apiUsersMe.GET("", api.GetUsersMe) - apiUsersMe.GET("/responses", api.GetMyResponses) - apiUsersMe.GET("/responses/:questionnaireID", api.GetMyResponsesByID) - apiUsersMe.GET("/targeted", api.GetTargetedQuestionnaire) - apiUsersMe.GET("/administrates", api.GetMyQuestionnaire) - } - apiUsers.GET("/:traQID/targeted", api.GetTargettedQuestionnairesBytraQID) - } +// apiUsers := echoAPI.Group("/users") +// { +// /* +// TODO +// apiUsers.GET("") +// */ +// apiUsersMe := apiUsers.Group("/me") +// { +// apiUsersMe.GET("", api.GetUsersMe) +// apiUsersMe.GET("/responses", api.GetMyResponses) +// apiUsersMe.GET("/responses/:questionnaireID", api.GetMyResponsesByID) +// apiUsersMe.GET("/targeted", api.GetTargetedQuestionnaire) +// apiUsersMe.GET("/administrates", api.GetMyQuestionnaire) +// } +// apiUsers.GET("/:traQID/targeted", api.GetTargettedQuestionnairesBytraQID) +// } - apiResults := echoAPI.Group("/results") - { - apiResults.GET("/:questionnaireID", api.GetResults, api.ResultAuthenticate) - } - } +// apiResults := echoAPI.Group("/results") +// { +// apiResults.GET("/:questionnaireID", api.GetResults, api.ResultAuthenticate) +// } +// } - e.Logger.Fatal(e.Start(port)) -} +// e.Logger.Fatal(e.Start(port)) +// } 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) -// } diff --git a/wire.go b/wire.go index c32e951f..c3e060c6 100644 --- a/wire.go +++ b/wire.go @@ -5,8 +5,9 @@ package main import ( "github.com/google/wire" + "github.com/traPtitech/anke-to/controller" + "github.com/traPtitech/anke-to/handler" "github.com/traPtitech/anke-to/model" - "github.com/traPtitech/anke-to/router" "github.com/traPtitech/anke-to/traq" ) @@ -25,15 +26,11 @@ var ( webhookBind = wire.Bind(new(traq.IWebhook), new(*traq.Webhook)) ) -func InjectAPIServer() *router.API { +func InjectHandler() *handler.Handler { wire.Build( - router.NewAPI, - router.NewMiddleware, - router.NewQuestionnaire, - router.NewQuestion, - router.NewResponse, - router.NewResult, - router.NewUser, + handler.NewHandler, + controller.NewResponse, + controller.NewQuestionnaire, model.NewAdministrator, model.NewOption, model.NewQuestionnaire, @@ -60,3 +57,36 @@ func InjectAPIServer() *router.API { return nil } + +func InjectAPIServer() *handler.Middleware { + wire.Build( + // handler.NewHandler, + handler.NewMiddleware, + // controller.NewResponse, + // controller.NewQuestionnaire, + // model.NewAdministrator, + // model.NewOption, + // model.NewQuestionnaire, + // model.NewQuestion, + // model.NewRespondent, + // model.NewResponse, + // model.NewScaleLabel, + // model.NewTarget, + // model.NewValidation, + // model.NewTransaction, + // traq.NewWebhook, + // administratorBind, + // optionBind, + // questionnaireBind, + // questionBind, + // respondentBind, + // responseBind, + // scaleLabelBind, + // targetBind, + // validationBind, + // transactionBind, + // webhookBind, + ) + + return nil +} diff --git a/wire_gen.go b/wire_gen.go index bede2c48..b25ae41a 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run github.com/google/wire/cmd/wire +//go:generate go run -mod=mod github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject @@ -8,8 +8,9 @@ package main import ( "github.com/google/wire" + "github.com/traPtitech/anke-to/controller" + "github.com/traPtitech/anke-to/handler" "github.com/traPtitech/anke-to/model" - "github.com/traPtitech/anke-to/router" "github.com/traPtitech/anke-to/traq" ) @@ -19,26 +20,27 @@ import ( // Injectors from wire.go: -func InjectAPIServer() *router.API { - administrator := model.NewAdministrator() - respondent := model.NewRespondent() - question := model.NewQuestion() +func InjectHandler() *handler.Handler { questionnaire := model.NewQuestionnaire() - middleware := router.NewMiddleware(administrator, respondent, question, questionnaire) target := model.NewTarget() + administrator := model.NewAdministrator() + question := model.NewQuestion() option := model.NewOption() scaleLabel := model.NewScaleLabel() validation := model.NewValidation() transaction := model.NewTransaction() webhook := traq.NewWebhook() - routerQuestionnaire := router.NewQuestionnaire(questionnaire, target, administrator, question, option, scaleLabel, validation, transaction, webhook) - routerQuestion := router.NewQuestion(validation, question, option, scaleLabel) + controllerQuestionnaire := controller.NewQuestionnaire(questionnaire, target, administrator, question, option, scaleLabel, validation, transaction, webhook) + respondent := model.NewRespondent() response := model.NewResponse() - routerResponse := router.NewResponse(questionnaire, validation, scaleLabel, respondent, response) - result := router.NewResult(respondent, questionnaire, administrator) - user := router.NewUser(respondent, questionnaire, target, administrator) - api := router.NewAPI(middleware, routerQuestionnaire, routerQuestion, routerResponse, result, user) - return api + controllerResponse := controller.NewResponse(questionnaire, respondent, response, target, question, validation, scaleLabel) + handlerHandler := handler.NewHandler(controllerQuestionnaire, controllerResponse) + return handlerHandler +} + +func InjectAPIServer() *handler.Middleware { + middleware := handler.NewMiddleware() + return middleware } // wire.go: