diff --git a/controller/adapter.go b/controller/adapter.go new file mode 100644 index 00000000..cd173d10 --- /dev/null +++ b/controller/adapter.go @@ -0,0 +1,352 @@ +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" + "gopkg.in/guregu/null.v4" +) + +func questionnaireInfo2questionnaireSummary(questionnaireInfo model.QuestionnaireInfo, allResponded bool, hasMyDraft bool, hasMyResponse bool, respondedDateTimeByMe null.Time) *openapi.QuestionnaireSummary { + res := openapi.QuestionnaireSummary{ + AllResponded: allResponded, + CreatedAt: questionnaireInfo.CreatedAt, + Description: questionnaireInfo.Description, + HasMyDraft: hasMyDraft, + HasMyResponse: hasMyResponse, + IsDuplicateAnswerAllowed: questionnaireInfo.IsDuplicateAnswerAllowed, + // IsAnonymous: questionnaireInfo.IsAnonymous, + IsPublished: questionnaireInfo.IsPublished, + IsTargetingMe: questionnaireInfo.IsTargeted, + ModifiedAt: questionnaireInfo.ModifiedAt, + QuestionnaireId: questionnaireInfo.ID, + Title: questionnaireInfo.Title, + } + if respondedDateTimeByMe.Valid { + res.RespondedDateTimeByMe = &respondedDateTimeByMe.Time + } else { + res.RespondedDateTimeByMe = nil + } + if questionnaireInfo.ResTimeLimit.Valid { + res.ResponseDueDateTime = &questionnaireInfo.ResTimeLimit.Time + } else { + res.ResponseDueDateTime = nil + } + return &res +} + +func convertResponseViewableBy(resShareType openapi.ResShareType) string { + switch resShareType { + case "admins": + return "administrators" + case "respondents": + return "respondents" + case "anyone": + return "public" + default: + return "administrators" + } +} + +func convertResSharedTo(resSharedTo string) openapi.ResShareType { + switch resSharedTo { + case "administrators": + return "admins" + case "respondents": + return "respondents" + case "public": + return "anyone" + default: + return "admins" + } + +} + +func createUsersAndGroups(users []string, groups uuid.UUIDs) openapi.UsersAndGroups { + res := openapi.UsersAndGroups{ + Users: users, + Groups: groups.Strings(), + } + return res +} + +func convertOptions(options []model.Options) openapi.QuestionSettingsSingleChoice { + res := openapi.QuestionSettingsSingleChoice{} + for _, option := range options { + res.Options = append(res.Options, option.Body) + } + return res +} + +func convertQuestions(questions []model.Questions) []openapi.Question { + res := []openapi.Question{} + for _, question := range questions { + q := openapi.Question{ + CreatedAt: question.CreatedAt, + // Description: question.Description, + IsRequired: question.IsRequired, + QuestionId: question.ID, + QuestionnaireId: question.QuestionnaireID, + Title: question.Body, + } + switch question.Type { + case "Text": + q.FromQuestionSettingsText( + openapi.QuestionSettingsText{ + QuestionType: "Text", + }, + ) + case "TextArea": + q.FromQuestionSettingsText( + openapi.QuestionSettingsText{ + QuestionType: "TextLong", + }, + ) + case "Number": + q.FromQuestionSettingsNumber( + openapi.QuestionSettingsNumber{ + QuestionType: "Number", + }, + ) + case "Radio": + q.FromQuestionSettingsSingleChoice( + openapi.QuestionSettingsSingleChoice{ + QuestionType: "Radio", + Options: convertOptions(question.Options).Options, + }, + ) + case "MultipleChoice": + q.FromQuestionSettingsMultipleChoice( + openapi.QuestionSettingsMultipleChoice{ + QuestionType: "MultipleChoice", + Options: convertOptions(question.Options).Options, + }, + ) + case "LinearScale": + q.FromQuestionSettingsScale( + openapi.QuestionSettingsScale{ + QuestionType: "LinearScale", + MinLabel: &question.ScaleLabels[0].ScaleLabelLeft, + MaxLabel: &question.ScaleLabels[0].ScaleLabelRight, + MinValue: question.ScaleLabels[0].ScaleMin, + MaxValue: question.ScaleLabels[0].ScaleMax, + }, + ) + } + } + return res +} + +func convertRespondents(respondents []model.Respondents) []string { + res := []string{} + for _, respondent := range respondents { + res = append(res, respondent.UserTraqid) + } + return res +} + +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, + IsDuplicateAnswerAllowed: questionnaires.IsDuplicateAnswerAllowed, + IsAnonymous: questionnaires.IsAnonymous, + IsPublished: questionnaires.IsPublished, + ModifiedAt: questionnaires.ModifiedAt, + QuestionnaireId: questionnaires.ID, + Questions: convertQuestions(questionnaires.Questions), + Respondents: respondents, + ResponseDueDateTime: &questionnaires.ResTimeLimit.Time, + ResponseViewableBy: convertResSharedTo(questionnaires.ResSharedTo), + Targets: createUsersAndGroups(targetUsers, targetGroups), + Title: questionnaires.Title, + } + return res +} + +func respondentDetail2Response(ctx echo.Context, respondentDetail model.RespondentDetail) (openapi.Response, error) { + oResponseBodies := []openapi.ResponseBody{} + for j, r := range respondentDetail.Responses { + oResponseBody := openapi.ResponseBody{} + switch r.QuestionType { + case "Text": + if r.Body.Valid { + oResponseBody.FromResponseBodyText( + openapi.ResponseBodyText{ + Answer: r.Body.String, + QuestionType: "Text", + }, + ) + } + case "TextArea": + if r.Body.Valid { + oResponseBody.FromResponseBodyText( + openapi.ResponseBodyText{ + Answer: r.Body.String, + QuestionType: "TextLong", + }, + ) + } + case "Number": + if r.Body.Valid { + answer, err := strconv.ParseFloat(r.Body.String, 32) + if err != nil { + ctx.Logger().Errorf("failed to convert string to float: %+v", err) + return openapi.Response{}, err + } + oResponseBody.FromResponseBodyNumber( + openapi.ResponseBodyNumber{ + Answer: float32(answer), + QuestionType: "Number", + }, + ) + } + case "MultipleChoice": + if r.Body.Valid { + answer := []int{} + questionnaire, _, _, _, _, _, err := model.NewQuestionnaire().GetQuestionnaireInfo(ctx.Request().Context(), r.QuestionID) + if err != nil { + ctx.Logger().Errorf("failed to get questionnaire info: %+v", err) + return openapi.Response{}, err + } + for _, a := range r.OptionResponse { + for i, o := range questionnaire.Questions[j].Options { + if a == o.Body { + answer = append(answer, i) + } + } + } + oResponseBody.FromResponseBodyMultipleChoice( + openapi.ResponseBodyMultipleChoice{ + Answer: answer, + QuestionType: "MultipleChoice", + }, + ) + } + case "Checkbox": + if r.Body.Valid { + questionnaire, _, _, _, _, _, err := model.NewQuestionnaire().GetQuestionnaireInfo(ctx.Request().Context(), r.QuestionID) + if err != nil { + ctx.Logger().Errorf("failed to get questionnaire info: %+v", err) + return openapi.Response{}, err + } + for _, a := range r.OptionResponse { + for i, o := range questionnaire.Questions[j].Options { + if a == o.Body { + oResponseBody.FromResponseBodySingleChoice( + openapi.ResponseBodySingleChoice{ + Answer: i, + QuestionType: "SingleChoice", + }, + ) + } + } + } + } + case "LinearScale": + if r.Body.Valid { + answer, err := strconv.Atoi(r.Body.String) + if err != nil { + ctx.Logger().Errorf("failed to convert string to int: %+v", err) + return openapi.Response{}, err + } + oResponseBody.FromResponseBodyScale( + openapi.ResponseBodyScale{ + Answer: answer, + QuestionType: "LinearScale", + }, + ) + } + } + 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, + ModifiedAt: respondentDetail.ModifiedAt, + QuestionnaireId: respondentDetail.QuestionnaireID, + Respondent: &respondentDetail.TraqID, + ResponseId: respondentDetail.ResponseID, + SubmittedAt: respondentDetail.SubmittedAt.Time, + IsAnonymous: &isAnonymous, + } + + return res, nil +} + +func responseBody2ResponseMetas(body []openapi.ResponseBody, questions []model.Questions) ([]*model.ResponseMeta, error) { + res := []*model.ResponseMeta{} + + for i, b := range body { + switch questions[i].Type { + case "Text": + bText, err := b.AsResponseBodyText() + if err != nil { + return nil, err + } + res = append(res, &model.ResponseMeta{ + QuestionID: questions[i].ID, + Data: bText.Answer, + }) + case "TextLong": + bTextLong, err := b.AsResponseBodyTextLong() + if err != nil { + return nil, err + } + res = append(res, &model.ResponseMeta{ + QuestionID: questions[i].ID, + Data: bTextLong.Answer, + }) + case "Number": + bNumber, err := b.AsResponseBodyNumber() + if err != nil { + return nil, err + } + res = append(res, &model.ResponseMeta{ + QuestionID: questions[i].ID, + Data: strconv.FormatFloat(float64(bNumber.Answer), 'f', -1, 32), + }) + case "SingleChoice": + bSingleChoice, err := b.AsResponseBodySingleChoice() + if err != nil { + return nil, err + } + res = append(res, &model.ResponseMeta{ + QuestionID: questions[i].ID, + Data: strconv.FormatInt(int64(bSingleChoice.Answer), 10), + }) + case "MultipleChoice": + bMultipleChoice, err := b.AsResponseBodyMultipleChoice() + if err != nil { + return nil, err + } + for _, a := range bMultipleChoice.Answer { + res = append(res, &model.ResponseMeta{ + QuestionID: questions[i].ID, + Data: strconv.FormatInt(int64(a), 10), + }) + } + case "LinearScale": + bScale, err := b.AsResponseBodyScale() + if err != nil { + return nil, err + } + res = append(res, &model.ResponseMeta{ + QuestionID: questions[i].ID, + Data: strconv.FormatInt(int64(bScale.Answer), 10), + }) + } + } + return res, nil +} diff --git a/controller/questionnaire.go b/controller/questionnaire.go new file mode 100644 index 00000000..efe111d9 --- /dev/null +++ b/controller/questionnaire.go @@ -0,0 +1,889 @@ +package controller + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/labstack/echo/v4" + "github.com/traPtitech/anke-to/model" + "github.com/traPtitech/anke-to/openapi" + "github.com/traPtitech/anke-to/traq" + "gopkg.in/guregu/null.v4" +) + +// Questionnaire Questionnaireの構造体 +type Questionnaire struct { + model.IQuestionnaire + model.ITarget + model.ITargetGroup + model.IAdministrator + model.IAdministratorGroup + model.IQuestion + model.IOption + model.IScaleLabel + model.IValidation + model.ITransaction + traq.IWebhook + Response +} + +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 + +func (q Questionnaire) GetQuestionnaires(ctx echo.Context, userID string, params openapi.GetQuestionnairesParams) (openapi.QuestionnaireList, error) { + res := openapi.QuestionnaireList{} + sort := string(*params.Sort) + search := string(*params.Search) + pageNum := int(*params.Page) + if pageNum < 1 { + pageNum = 1 + } + + questionnaireList, pageMax, err := q.IQuestionnaire.GetQuestionnaires(ctx.Request().Context(), userID, sort, search, pageNum, *params.OnlyTargetingMe, *params.OnlyAdministratedByMe) + if err != nil { + return res, err + } + + for _, questionnaire := range questionnaireList { + targets, err := q.ITarget.GetTargets(ctx.Request().Context(), []int{questionnaire.ID}) + if err != nil { + return res, err + } + allRespondend := false + if len(targets) == 0 { + allRespondend = true + } else { + respondents, err := q.IRespondent.GetRespondentsUserIDs(ctx.Request().Context(), []int{questionnaire.ID}) + if err != nil { + return res, err + } + allRespondend = isAllTargetsReponded(targets, respondents) + } + + hasMyDraft := false + hasMyResponse := false + respondendDateTimeByMe := null.Time{} + + myRespondents, err := q.GetRespondentInfos(ctx.Request().Context(), userID, questionnaire.ID) + if err != nil { + return res, err + } + for _, respondent := range myRespondents { + if !respondent.SubmittedAt.Valid { + hasMyDraft = true + } + if respondent.SubmittedAt.Valid { + if !respondendDateTimeByMe.Valid { + respondendDateTimeByMe = respondent.SubmittedAt + } + hasMyResponse = true + } + } + + res.PageMax = pageMax + res.Questionnaires = append(res.Questionnaires, *questionnaireInfo2questionnaireSummary(questionnaire, allRespondend, hasMyDraft, hasMyResponse, respondendDateTimeByMe)) + } + return res, nil +} + +func (q Questionnaire) PostQuestionnaire(c echo.Context, userID string, params openapi.PostQuestionnaireJSONRequestBody) (openapi.QuestionnaireDetail, error) { + responseDueDateTime := null.Time{} + if params.ResponseDueDateTime != nil { + responseDueDateTime.Valid = true + responseDueDateTime.Time = *params.ResponseDueDateTime + } + if responseDueDateTime.Valid { + isBefore := responseDueDateTime.ValueOrZero().Before(time.Now()) + if isBefore { + c.Logger().Infof("invalid resTimeLimit: %+v", responseDueDateTime) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusBadRequest, "invalid resTimeLimit") + } + } + + 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, params.IsAnonymous, params.IsDuplicateAnswerAllowed) + if err != nil { + c.Logger().Errorf("failed to insert questionnaire: %+v", err) + return err + } + allTargetUsers, err := rollOutUsersAndGroups(params.Targets.Users, params.Targets.Groups) + if err != nil { + c.Logger().Errorf("failed to roll out users and groups: %+v", err) + return err + } + targetGroupNames, err := uuid2GroupNames(params.Targets.Groups) + if err != nil { + c.Logger().Errorf("failed to get group names: %+v", err) + return err + } + err = q.InsertTargets(ctx, questionnaireID, allTargetUsers) + if err != nil { + c.Logger().Errorf("failed to insert targets: %+v", err) + return err + } + err = q.InsertTargetGroups(ctx, questionnaireID, params.Targets.Groups) + if err != nil { + c.Logger().Errorf("failed to insert target groups: %+v", err) + return err + } + allAdminUsers, err := rollOutUsersAndGroups(params.Admins.Users, params.Admins.Groups) + if err != nil { + c.Logger().Errorf("failed to roll out administrators: %+v", err) + return err + } + adminGroupNames, err := uuid2GroupNames(params.Admins.Groups) + if err != nil { + c.Logger().Errorf("failed to get group names: %+v", err) + return err + } + err = q.InsertAdministrators(ctx, questionnaireID, allAdminUsers) + if err != nil { + c.Logger().Errorf("failed to insert administrators: %+v", err) + return err + } + err = q.InsertAdministratorGroups(ctx, questionnaireID, params.Admins.Groups) + if err != nil { + c.Logger().Errorf("failed to insert administrator groups: %+v", err) + return err + } + for questoinNum, question := range params.Questions { + b, err := question.MarshalJSON() + if err != nil { + c.Logger().Errorf("failed to marshal new question: %+v", err) + return err + } + var questionParsed map[string]interface{} + err = json.Unmarshal([]byte(b), &questionParsed) + if err != nil { + c.Logger().Errorf("failed to unmarshal new question: %+v", err) + return err + } + questionType := questionParsed["question_type"].(string) + _, err = q.InsertQuestion(ctx, questionnaireID, 1, questoinNum+1, questionType, question.Body, question.IsRequired) + if err != nil { + c.Logger().Errorf("failed to insert question: %+v", err) + return err + } + } + + message := createQuestionnaireMessage( + questionnaireID, + params.Title, + params.Description, + append(allAdminUsers, adminGroupNames...), + responseDueDateTime, + append(allTargetUsers, targetGroupNames...), + ) + err = q.PostMessage(message) + if err != nil { + c.Logger().Errorf("failed to post message: %+v", err) + 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 { + c.Logger().Errorf("failed to create a questionnaire: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to create a questionnaire") + } + + // insert validations + questions, err := q.IQuestion.GetQuestions(c.Request().Context(), questionnaireID) + for i, question := range questions { + switch question.Type { + case "SingleChoice": + b, err := params.Questions[i].AsQuestionSettingsSingleChoice() + if err != nil { + c.Logger().Errorf("failed to get question settings: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to get question settings") + } + for i, v := range b.Options { + err := q.IOption.InsertOption(c.Request().Context(), question.ID, i+1, v) + if err != nil { + c.Logger().Errorf("failed to insert option: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to insert option") + } + } + case "MultipleChoice": + b, err := params.Questions[i].AsQuestionSettingsMultipleChoice() + if err != nil { + c.Logger().Errorf("failed to get question settings: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to get question settings") + } + for i, v := range b.Options { + err := q.IOption.InsertOption(c.Request().Context(), question.ID, i+1, v) + if err != nil { + c.Logger().Errorf("failed to insert option: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to insert option") + } + } + case "Scale": + b, err := params.Questions[i].AsQuestionSettingsScale() + if err != nil { + c.Logger().Errorf("failed to get question settings: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to get question settings") + } + err = q.IScaleLabel.InsertScaleLabel(c.Request().Context(), question.ID, + model.ScaleLabels{ + ScaleLabelLeft: *b.MinLabel, + ScaleLabelRight: *b.MaxLabel, + ScaleMax: b.MaxValue, + ScaleMin: b.MinValue, + }) + if err != nil { + c.Logger().Errorf("failed to insert scale label: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to insert scale label") + } + case "Text": + b, err := params.Questions[i].AsQuestionSettingsText() + if err != nil { + c.Logger().Errorf("failed to get question settings: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to get question settings") + } + err = q.IValidation.InsertValidation(c.Request().Context(), question.ID, + model.Validations{ + RegexPattern: ".{," + strconv.Itoa(*b.MaxLength) + "}", + }) + if err != nil { + c.Logger().Errorf("failed to insert validation: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to insert validation") + } + case "TextLong": + b, err := params.Questions[i].AsQuestionSettingsTextLong() + if err != nil { + c.Logger().Errorf("failed to get question settings: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to get question settings") + } + err = q.IValidation.InsertValidation(c.Request().Context(), question.ID, + model.Validations{ + RegexPattern: ".{," + fmt.Sprintf("%.0f", *b.MaxLength) + "}", + }) + if err != nil { + c.Logger().Errorf("failed to insert validation: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to insert validation") + } + case "Number": + b, err := params.Questions[i].AsQuestionSettingsNumber() + if err != nil { + c.Logger().Errorf("failed to get question settings: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to get question settings") + } + // 数字かどうか,min<=maxになっているかどうか + err = q.IValidation.CheckNumberValid(strconv.Itoa(*b.MinValue), strconv.Itoa(*b.MaxValue)) + if err != nil { + c.Logger().Errorf("invalid number: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusBadRequest, "invalid number") + } + err = q.IValidation.InsertValidation(c.Request().Context(), question.ID, + model.Validations{ + MinBound: strconv.Itoa(*b.MinValue), + MaxBound: strconv.Itoa(*b.MaxValue), + }) + if err != nil { + c.Logger().Errorf("failed to insert validation: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to insert validation") + } + } + } + + questionnaireInfo, targets, targetGroups, admins, adminGroups, respondents, err := q.GetQuestionnaireInfo(c.Request().Context(), questionnaireID) + if err != nil { + c.Logger().Errorf("failed to get questionnaire info: %+v", err) + return openapi.QuestionnaireDetail{}, echo.NewHTTPError(http.StatusInternalServerError, "failed to get questionnaire info") + } + + questionnaireDetail := questionnaire2QuestionnaireDetail(*questionnaireInfo, admins, adminGroups, targets, targetGroups, respondents) + return questionnaireDetail, nil +} +func (q Questionnaire) GetQuestionnaire(ctx echo.Context, questionnaireID int) (openapi.QuestionnaireDetail, error) { + questionnaireInfo, targets, targetGroups, admins, adminGroups, respondents, err := q.GetQuestionnaireInfo(ctx.Request().Context(), questionnaireID) + if err != nil { + return openapi.QuestionnaireDetail{}, err + } + questionnaireDetail := questionnaire2QuestionnaireDetail(*questionnaireInfo, admins, adminGroups, targets, targetGroups, respondents) + return questionnaireDetail, nil +} + +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, params.IsAnonymous, params.IsDuplicateAnswerAllowed) + 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.DeleteTargetGroups(ctx, questionnaireID) + if err != nil { + c.Logger().Errorf("failed to delete target groups: %+v", err) + return err + } + allTargetUsers, err := rollOutUsersAndGroups(params.Targets.Users, params.Targets.Groups) + if err != nil { + c.Logger().Errorf("failed to roll out users and groups: %+v", err) + return err + } + err = q.InsertTargets(ctx, questionnaireID, allTargetUsers) + if err != nil { + c.Logger().Errorf("failed to insert targets: %+v", err) + return err + } + err = q.InsertTargetGroups(ctx, questionnaireID, params.Targets.Groups) + if err != nil { + c.Logger().Errorf("failed to insert target groups: %+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.DeleteAdministratorGroups(ctx, questionnaireID) + if err != nil { + c.Logger().Errorf("failed to delete administrator groups: %+v", err) + return err + } + allAdminUsers, err := rollOutUsersAndGroups(params.Admins.Users, params.Admins.Groups) + if err != nil { + c.Logger().Errorf("failed to roll out administrators: %+v", err) + return err + } + err = q.InsertAdministrators(ctx, questionnaireID, allAdminUsers) + if err != nil { + c.Logger().Errorf("failed to insert administrators: %+v", err) + return err + } + err = q.InsertAdministratorGroups(ctx, questionnaireID, params.Admins.Groups) + if err != nil { + c.Logger().Errorf("failed to insert administrator groups: %+v", err) + return err + } + for questoinNum, question := range params.Questions { + b, err := question.MarshalJSON() + if err != nil { + c.Logger().Errorf("failed to marshal new question: %+v", err) + return err + } + var questionParsed map[string]interface{} + err = json.Unmarshal([]byte(b), &questionParsed) + if err != nil { + c.Logger().Errorf("failed to unmarshal new question: %+v", err) + return err + } + questionType := questionParsed["question_type"].(string) + if question.QuestionId == nil { + _, err = q.InsertQuestion(ctx, questionnaireID, 1, questoinNum+1, questionType, question.Body, question.IsRequired) + if err != nil { + c.Logger().Errorf("failed to insert question: %+v", err) + return err + } + } else { + err = q.UpdateQuestion(ctx, questionnaireID, 1, questoinNum+1, questionType, question.Body, question.IsRequired, *question.QuestionId) + if err != nil { + c.Logger().Errorf("failed to update question: %+v", err) + return err + } + } + } + + 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 { + c.Logger().Errorf("failed to update a questionnaire: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to update a questionnaire") + } + + // update validations + questions, err := q.IQuestion.GetQuestions(c.Request().Context(), questionnaireID) + for i, question := range questions { + switch question.Type { + case "SingleChoice": + b, err := params.Questions[i].AsQuestionSettingsSingleChoice() + if err != nil { + c.Logger().Errorf("failed to get question settings: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get question settings") + } + err = q.IOption.UpdateOptions(c.Request().Context(), b.Options, question.ID) + if err != nil && !errors.Is(err, model.ErrNoRecordUpdated) { + c.Logger().Errorf("failed to update options: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to update options") + } + case "MultipleChoice": + b, err := params.Questions[i].AsQuestionSettingsMultipleChoice() + if err != nil { + c.Logger().Errorf("failed to get question settings: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get question settings") + } + err = q.IOption.UpdateOptions(c.Request().Context(), b.Options, question.ID) + if err != nil && !errors.Is(err, model.ErrNoRecordUpdated) { + c.Logger().Errorf("failed to update options: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to update options") + } + case "Scale": + b, err := params.Questions[i].AsQuestionSettingsScale() + if err != nil { + c.Logger().Errorf("failed to get question settings: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get question settings") + } + err = q.IScaleLabel.UpdateScaleLabel(c.Request().Context(), question.ID, + model.ScaleLabels{ + ScaleLabelLeft: *b.MinLabel, + ScaleLabelRight: *b.MaxLabel, + ScaleMax: b.MaxValue, + ScaleMin: b.MinValue, + }) + if err != nil && !errors.Is(err, model.ErrNoRecordUpdated) { + c.Logger().Errorf("failed to insert scale label: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to insert scale label") + } + case "Text": + b, err := params.Questions[i].AsQuestionSettingsText() + if err != nil { + c.Logger().Errorf("failed to get question settings: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get question settings") + } + err = q.IValidation.UpdateValidation(c.Request().Context(), question.ID, + model.Validations{ + RegexPattern: ".{," + strconv.Itoa(*b.MaxLength) + "}", + }) + if err != nil && !errors.Is(err, model.ErrNoRecordUpdated) { + c.Logger().Errorf("failed to insert validation: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to insert validation") + } + case "TextLong": + b, err := params.Questions[i].AsQuestionSettingsTextLong() + if err != nil { + c.Logger().Errorf("failed to get question settings: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get question settings") + } + err = q.IValidation.UpdateValidation(c.Request().Context(), question.ID, + model.Validations{ + RegexPattern: ".{," + fmt.Sprintf("%.0f", *b.MaxLength) + "}", + }) + if err != nil && !errors.Is(err, model.ErrNoRecordUpdated) { + c.Logger().Errorf("failed to insert validation: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to insert validation") + } + case "Number": + b, err := params.Questions[i].AsQuestionSettingsNumber() + if err != nil { + c.Logger().Errorf("failed to get question settings: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get question settings") + } + // 数字かどうか,min<=maxになっているかどうか + err = q.IValidation.CheckNumberValid(strconv.Itoa(*b.MinValue), strconv.Itoa(*b.MaxValue)) + if err != nil { + c.Logger().Errorf("invalid number: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, "invalid number") + } + err = q.IValidation.UpdateValidation(c.Request().Context(), question.ID, + model.Validations{ + MinBound: strconv.Itoa(*b.MinValue), + MaxBound: strconv.Itoa(*b.MaxValue), + }) + if err != nil && !errors.Is(err, model.ErrNoRecordUpdated) { + c.Logger().Errorf("failed to insert validation: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to insert validation") + } + } + } + + return nil +} + +func (q Questionnaire) DeleteQuestionnaire(c echo.Context, questionnaireID int) error { + 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 + } + + questions, err := q.GetQuestions(c.Request().Context(), questionnaireID) + if err != nil { + c.Logger().Errorf("failed to get questions: %+v", err) + return err + } + for _, question := range questions { + err = q.DeleteQuestion(c.Request().Context(), question.ID) + if err != nil { + c.Logger().Errorf("failed to delete administrators: %+v", err) + return err + } + } + + err = Jq.DeleteReminder(questionnaireID) + if err != nil { + c.Logger().Errorf("failed to delete reminder: %+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 nil +} + +func (q Questionnaire) GetQuestionnaireMyRemindStatus(c echo.Context, questionnaireID int) (bool, error) { + 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, 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 +} + +func (q Questionnaire) GetQuestionnaireResponses(c echo.Context, questionnaireID int, params openapi.GetQuestionnaireResponsesParams, userID string) (openapi.Responses, error) { + res := []openapi.Response{} + respondentDetails, err := q.GetRespondentDetails(c.Request().Context(), questionnaireID, string(*params.Sort), *params.OnlyMyResponse, userID) + if err != nil { + if errors.Is(err, model.ErrRecordNotFound) { + return res, echo.NewHTTPError(http.StatusNotFound, "respondent not found") + } + c.Logger().Errorf("failed to get respondent details: %+v", err) + return res, echo.NewHTTPError(http.StatusInternalServerError, "failed to get respondent details") + } + + for _, respondentDetail := range respondentDetails { + response, err := respondentDetail2Response(c, respondentDetail) + if err != nil { + c.Logger().Errorf("failed to convert respondent detail to response: %+v", err) + return res, echo.NewHTTPError(http.StatusInternalServerError, "failed to convert respondent detail to response") + } + res = append(res, response) + } + + return res, nil +} + +func (q Questionnaire) PostQuestionnaireResponse(c echo.Context, questionnaireID int, params openapi.PostQuestionnaireResponseJSONRequestBody, userID string) (openapi.Response, error) { + res := openapi.Response{} + + limit, err := q.GetQuestionnaireLimit(c.Request().Context(), questionnaireID) + if err != nil { + if errors.Is(err, model.ErrRecordNotFound) { + c.Logger().Info("questionnaire not found") + return res, echo.NewHTTPError(http.StatusNotFound, err) + } + c.Logger().Errorf("failed to get questionnaire limit: %+v", err) + return res, echo.NewHTTPError(http.StatusInternalServerError, err) + } + + // 回答期限を過ぎていたらエラー + if limit.Valid && limit.Time.Before(time.Now()) { + c.Logger().Info("expired questionnaire") + return res, echo.NewHTTPError(http.StatusUnprocessableEntity, err) + } + + questions, err := q.IQuestion.GetQuestions(c.Request().Context(), questionnaireID) + if err != nil { + c.Logger().Errorf("failed to get questions: %+v", err) + return res, echo.NewHTTPError(http.StatusInternalServerError, err) + } + + responseMetas, err := responseBody2ResponseMetas(params.Body, questions) + if err != nil { + c.Logger().Errorf("failed to convert response body to response metas: %+v", err) + return res, echo.NewHTTPError(http.StatusInternalServerError, err) + } + + // validationでチェック + questionIDs := make([]int, len(questions)) + questionTypes := make(map[int]string, len(questions)) + for i, question := range questions { + questionIDs[i] = question.ID + questionTypes[question.ID] = question.Type + } + + validations, err := q.IValidation.GetValidations(c.Request().Context(), questionIDs) + if err != nil { + c.Logger().Errorf("failed to get validations: %+v", err) + return res, echo.NewHTTPError(http.StatusInternalServerError, err) + } + + for i, validation := range validations { + switch questionTypes[validation.QuestionID] { + case "Text", "TextLong": + err := q.IValidation.CheckTextValidation(validation, responseMetas[i].Data) + if err != nil { + if errors.Is(err, model.ErrTextMatching) { + c.Logger().Errorf("invalid text: %+v", err) + return res, echo.NewHTTPError(http.StatusBadRequest, err) + } + c.Logger().Errorf("invalid text: %+v", err) + return res, echo.NewHTTPError(http.StatusBadRequest, err) + } + case "Number": + err := q.IValidation.CheckNumberValidation(validation, responseMetas[i].Data) + if err != nil { + if errors.Is(err, model.ErrInvalidNumber) { + c.Logger().Errorf("invalid number: %+v", err) + return res, echo.NewHTTPError(http.StatusBadRequest, err) + } + c.Logger().Errorf("invalid number: %+v", err) + return res, echo.NewHTTPError(http.StatusBadRequest, err) + } + } + } + + // scaleのvalidation + scaleLabelIDs := []int{} + for _, question := range questions { + if question.Type == "Scale" { + scaleLabelIDs = append(scaleLabelIDs, question.ID) + } + } + + scaleLabels, err := q.IScaleLabel.GetScaleLabels(c.Request().Context(), scaleLabelIDs) + if err != nil { + c.Logger().Errorf("failed to get scale labels: %+v", err) + return res, echo.NewHTTPError(http.StatusInternalServerError, err) + } + scaleLabelMap := make(map[int]model.ScaleLabels, len(scaleLabels)) + for _, scaleLabel := range scaleLabels { + scaleLabelMap[scaleLabel.QuestionID] = scaleLabel + } + + for i, question := range questions { + if question.Type == "Scale" { + label, ok := scaleLabelMap[question.ID] + if !ok { + label = model.ScaleLabels{} + } + err := q.IScaleLabel.CheckScaleLabel(label, responseMetas[i].Data) + if err != nil { + c.Logger().Errorf("invalid scale: %+v", err) + return res, echo.NewHTTPError(http.StatusBadRequest, err) + } + } + } + + var submittedAt, modifiedAt time.Time + //一時保存のときはnull + if params.IsDraft { + submittedAt = time.Time{} + modifiedAt = time.Time{} + } else { + submittedAt = time.Now() + modifiedAt = time.Now() + } + + resopnseID, err := q.InsertRespondent(c.Request().Context(), userID, questionnaireID, null.NewTime(submittedAt, !params.IsDraft)) + if err != nil { + c.Logger().Errorf("failed to insert respondant: %+v", err) + return res, echo.NewHTTPError(http.StatusInternalServerError, err) + } + + if len(responseMetas) > 0 { + err = q.InsertResponses(c.Request().Context(), resopnseID, responseMetas) + if err != nil { + c.Logger().Errorf("failed to insert responses: %+v", err) + return res, echo.NewHTTPError(http.StatusInternalServerError, err) + } + } + + isAnonymous, err := q.GetResponseIsAnonymousByQuestionnaireID(c.Request().Context(), questionnaireID) + + res = openapi.Response{ + QuestionnaireId: questionnaireID, + ResponseId: resopnseID, + Respondent: &userID, + SubmittedAt: submittedAt, + ModifiedAt: modifiedAt, + IsDraft: params.IsDraft, + IsAnonymous: &isAnonymous, + Body: params.Body, + } + + return res, nil +} + +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, + ) +} + +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 new file mode 100644 index 00000000..933db74c --- /dev/null +++ b/controller/response.go @@ -0,0 +1,266 @@ +package controller + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/labstack/echo/v4" + "github.com/traPtitech/anke-to/model" + "github.com/traPtitech/anke-to/openapi" +) + +// Response Responseの構造体 +type Response struct { + model.IQuestionnaire + model.IRespondent + model.IResponse + model.ITarget + model.IQuestion + model.IValidation + model.IScaleLabel +} + +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) { + res := openapi.ResponsesWithQuestionnaireInfo{} + + sort := string(*params.Sort) + responsesID := []int{} + responsesID, err := r.IRespondent.GetMyResponseIDs(ctx.Request().Context(), sort, userID) + if err != nil { + ctx.Logger().Errorf("failed to get my responses ID: %+v", err) + return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire responses: %w", err)) + } + + for _, responseID := range responsesID { + responseDetail, err := r.IRespondent.GetRespondentDetail(ctx.Request().Context(), responseID) + if err != nil { + ctx.Logger().Errorf("failed to get respondent detail: %+v", err) + return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get respondent detail: %w", err)) + } + + questionnaire, _, _, _, _, _, err := r.IQuestionnaire.GetQuestionnaireInfo(ctx.Request().Context(), responseDetail.QuestionnaireID) + if err != nil { + ctx.Logger().Errorf("failed to get questionnaire info: %+v", err) + return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire info: %w", err)) + } + + isTargetingMe, err := r.ITarget.IsTargetingMe(ctx.Request().Context(), responseDetail.QuestionnaireID, userID) + if err != nil { + ctx.Logger().Errorf("failed to get target info: %+v", err) + return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get target info: %w", err)) + } + + questionnaireInfo := openapi.QuestionnaireInfo{ + CreatedAt: questionnaire.CreatedAt, + IsTargetingMe: isTargetingMe, + ModifiedAt: questionnaire.ModifiedAt, + ResponseDueDateTime: &questionnaire.ResTimeLimit.Time, + Title: questionnaire.Title, + } + + response, err := respondentDetail2Response(ctx, responseDetail) + if err != nil { + ctx.Logger().Errorf("failed to convert respondent detail into response: %+v", err) + return openapi.ResponsesWithQuestionnaireInfo{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to convert respondent detail into response: %w", err)) + } + + tmp := openapi.ResponseWithQuestionnaireInfoItem{ + Body: response.Body, + IsDraft: response.IsDraft, + ModifiedAt: response.ModifiedAt, + QuestionnaireId: response.QuestionnaireId, + QuestionnaireInfo: &questionnaireInfo, + Respondent: &userID, + ResponseId: response.ResponseId, + SubmittedAt: response.SubmittedAt, + IsAnonymous: response.IsAnonymous, + } + res = append(res, tmp) + } + + return res, nil +} + +func (r Response) GetResponse(ctx echo.Context, responseID openapi.ResponseIDInPath) (openapi.Response, error) { + responseDetail, err := r.IRespondent.GetRespondentDetail(ctx.Request().Context(), responseID) + if err != nil { + if errors.Is(err, model.ErrRecordNotFound) { + ctx.Logger().Errorf("failed to find response by response ID: %+v", err) + return openapi.Response{}, echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("failed to find response by response ID: %w", err)) + } + ctx.Logger().Errorf("failed to get respondent detail: %+v", err) + return openapi.Response{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get respondent detail: %w", err)) + } + + res, err := respondentDetail2Response(ctx, responseDetail) + if err != nil { + ctx.Logger().Errorf("failed to convert respondent detail into response: %+v", err) + return openapi.Response{}, echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to convert respondent detail into response: %w", err)) + } + + return res, nil +} + +func (r Response) DeleteResponse(ctx echo.Context, responseID openapi.ResponseIDInPath, userID string) error { + limit, err := r.IQuestionnaire.GetQuestionnaireLimitByResponseID(ctx.Request().Context(), responseID) + if err != nil { + if errors.Is(err, model.ErrRecordNotFound) { + ctx.Logger().Errorf("failed to find response by response ID: %+v", err) + return echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("failed to find response by response ID: %w", err)) + } + ctx.Logger().Errorf("failed to get questionnaire limit by response ID: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire limit by response ID: %w", err)) + } + if limit.Valid && limit.Time.Before(time.Now()) { + ctx.Logger().Errorf("unable delete the expired response") + return echo.NewHTTPError(http.StatusMethodNotAllowed, fmt.Errorf("unable delete the expired response")) + } + + err = r.IRespondent.DeleteRespondent(ctx.Request().Context(), responseID) + if err != nil { + ctx.Logger().Errorf("failed to delete respondent: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to delete respondent: %w", err)) + } + + err = r.IResponse.DeleteResponse(ctx.Request().Context(), responseID) + if err != nil { + ctx.Logger().Errorf("failed to delete response: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to delete response: %w", err)) + } + + return nil +} + +func (r Response) EditResponse(ctx echo.Context, responseID openapi.ResponseIDInPath, req openapi.EditResponseJSONRequestBody) error { + limit, err := r.IQuestionnaire.GetQuestionnaireLimitByResponseID(ctx.Request().Context(), responseID) + if err != nil { + if errors.Is(err, model.ErrRecordNotFound) { + ctx.Logger().Infof("failed to find response by response ID: %+v", err) + return echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("failed to find response by response ID: %w", err)) + } + ctx.Logger().Errorf("failed to get questionnaire limit by response ID: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire limit by response ID: %w", err)) + } + + if limit.Valid && limit.Time.Before(time.Now()) { + ctx.Logger().Info("unable to edit the expired response") + return echo.NewHTTPError(http.StatusMethodNotAllowed, fmt.Errorf("unable edit the expired response")) + } + + err = r.IResponse.DeleteResponse(ctx.Request().Context(), responseID) + if err != nil { + ctx.Logger().Errorf("failed to delete response: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to delete response: %w", err)) + } + + questions, err := r.IQuestion.GetQuestions(ctx.Request().Context(), req.QuestionnaireId) + + responseMetas, err := responseBody2ResponseMetas(req.Body, questions) + if err != nil { + ctx.Logger().Errorf("failed to convert response body into response metas: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to convert response body into response metas: %w", err)) + } + + // validationでチェック + questionIDs := make([]int, len(questions)) + questionTypes := make(map[int]string, len(questions)) + for i, question := range questions { + questionIDs[i] = question.ID + questionTypes[question.ID] = question.Type + } + + validations, err := r.IValidation.GetValidations(ctx.Request().Context(), questionIDs) + if err != nil { + ctx.Logger().Errorf("failed to get validations: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get validations: %w", err)) + } + + for i, validation := range validations { + switch questionTypes[validation.QuestionID] { + case "Text", "TextLong": + err := r.IValidation.CheckTextValidation(validation, responseMetas[i].Data) + if err != nil { + if errors.Is(err, model.ErrTextMatching) { + ctx.Logger().Errorf("invalid text: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid text: %w", err)) + } + ctx.Logger().Errorf("invalid text: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid text: %w", err)) + } + case "Number": + err := r.IValidation.CheckNumberValidation(validation, responseMetas[i].Data) + if err != nil { + if errors.Is(err, model.ErrInvalidNumber) { + ctx.Logger().Errorf("invalid number: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid number: %w", err)) + } + ctx.Logger().Errorf("invalid number: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid number: %w", err)) + } + } + } + + // scaleのvalidation + scaleLabelIDs := []int{} + for _, question := range questions { + if question.Type == "Scale" { + scaleLabelIDs = append(scaleLabelIDs, question.ID) + } + } + + scaleLabels, err := r.IScaleLabel.GetScaleLabels(ctx.Request().Context(), scaleLabelIDs) + if err != nil { + ctx.Logger().Errorf("failed to get scale labels: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get scale labels: %w", err)) + } + scaleLabelMap := make(map[int]model.ScaleLabels, len(scaleLabels)) + for _, scaleLabel := range scaleLabels { + scaleLabelMap[scaleLabel.QuestionID] = scaleLabel + } + + for i, question := range questions { + if question.Type == "Scale" { + label, ok := scaleLabelMap[question.ID] + if !ok { + label = model.ScaleLabels{} + } + err := r.IScaleLabel.CheckScaleLabel(label, responseMetas[i].Data) + if err != nil { + ctx.Logger().Errorf("invalid scale: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid scale: %w", err)) + } + } + } + + if len(responseMetas) > 0 { + err = r.IResponse.InsertResponses(ctx.Request().Context(), responseID, responseMetas) + if err != nil { + ctx.Logger().Errorf("failed to insert responses: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to insert responses: %w", err)) + } + } + + return nil +} diff --git a/controller/utils.go b/controller/utils.go new file mode 100644 index 00000000..fe9df181 --- /dev/null +++ b/controller/utils.go @@ -0,0 +1,87 @@ +package controller + +import ( + "context" + "slices" + + mapset "github.com/deckarep/golang-set/v2" + "github.com/google/uuid" + "github.com/traPtitech/anke-to/model" + "github.com/traPtitech/anke-to/traq" +) + +func isAllTargetsReponded(targets []model.Targets, respondents []model.Respondents) bool { + respondentsString := []string{} + for _, respondent := range respondents { + respondentsString = append(respondentsString, respondent.UserTraqid) + } + + for _, target := range targets { + if !slices.Contains(respondentsString, target.UserTraqid) { + return false + } + } + return true +} + +func minimizeUsersAndGroups(users []string, groups []uuid.UUID) ([]string, []uuid.UUID, error) { + ctx := context.Background() + client := traq.NewTraqAPIClient() + userSet := mapset.NewSet[string]() + for _, user := range users { + userSet.Add(user) + } + groupUserSet := mapset.NewSet[string]() + for _, group := range groups { + members, err := client.GetGroupMembers(ctx, group.String()) + if err != nil { + return nil, nil, err + } + for _, member := range members { + memberTraqID, err := client.GetUserTraqID(ctx, member.Id) + if err != nil { + return nil, nil, err + } + groupUserSet.Add(memberTraqID) + } + } + userSet = userSet.Difference(groupUserSet) + return userSet.ToSlice(), groups, nil +} + +func rollOutUsersAndGroups(users []string, groups []string) ([]string, error) { + ctx := context.Background() + client := traq.NewTraqAPIClient() + userSet := mapset.NewSet[string]() + for _, user := range users { + userSet.Add(user) + } + for _, group := range groups { + members, err := client.GetGroupMembers(ctx, group) + if err != nil { + return nil, err + } + for _, member := range members { + memberTraqID, err := client.GetUserTraqID(ctx, member.Id) + if err != nil { + return nil, err + } + userSet.Add(memberTraqID) + } + } + return userSet.ToSlice(), nil +} + +func uuid2GroupNames(users []string) ([]string, error) { + ctx := context.Background() + client := traq.NewTraqAPIClient() + groupNames := []string{} + for _, user := range users { + groupName, err := client.GetGroupName(ctx, user) + if err != nil { + return nil, err + } + groupNames = append(groupNames, groupName) + } + return groupNames, nil +} diff --git a/docs/db_schema.md b/docs/db_schema.md index 45f30206..7d1f4523 100644 --- a/docs/db_schema.md +++ b/docs/db_schema.md @@ -48,8 +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 | boolean | NO | | false | | アンケートが公開かどうか | ### respondents @@ -107,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 919406ed..35dc86b0 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: @@ -455,7 +457,7 @@ components: - $ref: "#/components/schemas/QuestionnaireResponseDueDateTime" - $ref: "#/components/schemas/QuestionnaireResponseViewableBy" - $ref: "#/components/schemas/QuestionnaireIsAnonymous" - - $ref: "#/components/schemas/QuestionnaireIsAllowingMultipleResponses" + - $ref: "#/components/schemas/QuestionnaireIsDuplicateAnswerAllowed" - $ref: "#/components/schemas/QuestionnaireIsPublished" - $ref: "#/components/schemas/QuestionnaireTargetsAndAdmins" NewQuestionnaire: @@ -494,7 +496,7 @@ components: - $ref: "#/components/schemas/QuestionnaireResponseDueDateTime" - $ref: "#/components/schemas/QuestionnaireResponseViewableBy" - $ref: "#/components/schemas/QuestionnaireIsAnonymous" - - $ref: "#/components/schemas/QuestionnaireIsAllowingMultipleResponses" + - $ref: "#/components/schemas/QuestionnaireIsDuplicateAnswerAllowed" - $ref: "#/components/schemas/QuestionnaireIsPublished" - $ref: "#/components/schemas/QuestionnaireIsTargetingMe" - $ref: "#/components/schemas/QuestionnaireCreatedAt" @@ -583,16 +585,16 @@ components: 匿名回答かどうか required: - is_anonymous - QuestionnaireIsAllowingMultipleResponses: + QuestionnaireIsDuplicateAnswerAllowed: type: object properties: - is_allowing_multiple_responses: + is_duplicate_answer_allowed: type: boolean example: true description: | 一人が複数回回答できるかどうか required: - - is_allowing_multiple_responses + - is_duplicate_answer_allowed QuestionnaireIsPublished: type: object properties: @@ -667,34 +669,32 @@ components: - $ref: "#/components/schemas/QuestionBase" - $ref: "#/components/schemas/QuestionSettingsByType" - properties: + questionnaire_id: + type: integer + example: 1 question_id: type: integer + description: | + 質問を追加する場合はnull。 example: 1 created_at: type: string format: date-time example: 2020-01-01T00:00:00+09:00 required: - - question_id + - questionnaire_id - created_at QuestionBase: type: object properties: - questionnaire_id: - type: integer - example: 1 - title: - type: string - description: + body: type: string is_required: type: boolean description: | 回答必須かどうか required: - - questionnaire_id - - title - - description + - body - is_required QuestionSettingsByType: oneOf: diff --git a/go.mod b/go.mod index 5358018d..a17726c7 100644 --- a/go.mod +++ b/go.mod @@ -1,46 +1,46 @@ module github.com/traPtitech/anke-to -go 1.20 +go 1.22.2 require ( github.com/golang/mock v1.6.0 - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/google/subcommands v1.2.0 // indirect github.com/google/wire v0.5.0 - github.com/labstack/echo/v4 v4.11.1 - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/stretchr/testify v1.8.1 - golang.org/x/crypto v0.11.0 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.12.0 // indirect - golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f - golang.org/x/sync v0.1.0 - golang.org/x/sys v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect - golang.org/x/tools v0.6.0 // indirect + github.com/labstack/echo/v4 v4.12.0 + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.23.0 // indirect + 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.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 google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.27.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-playground/validator/v10 v10.10.1 + github.com/go-playground/validator/v10 v10.14.1 github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/jinzhu/inflection v1.0.0 // indirect - github.com/labstack/gommon v0.4.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/time v0.5.0 // indirect ) require ( - github.com/go-playground/locales v0.14.0 // indirect - github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/labstack/echo-contrib v0.12.0 - github.com/leodido/go-urn v1.2.1 // indirect + github.com/leodido/go-urn v1.2.4 // indirect ) require ( @@ -65,4 +65,35 @@ require ( gorm.io/plugin/prometheus v0.0.0-20210820101226-2a49866f83ee ) -require github.com/go-gormigrate/gormigrate/v2 v2.1.1 // indirect +require ( + github.com/deepmap/oapi-codegen v1.16.2 + github.com/go-gormigrate/gormigrate/v2 v2.1.1 +) + +require ( + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.3.0 // indirect + github.com/traPtitech/go-traq v0.0.0-20240420012203-0152d96098b0 // indirect +) + +require ( + github.com/getkin/kin-openapi v0.124.0 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/swag v0.22.8 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/invopop/yaml v0.2.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oapi-codegen/echo-middleware v1.0.1 + github.com/perimeterx/marshmallow v1.1.5 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gofrs/uuid v4.4.0+incompatible + github.com/google/uuid v1.5.0 // indirect + github.com/oapi-codegen/runtime v1.1.1 // indirect +) diff --git a/go.sum b/go.sum index 83e0cc26..0c26be87 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/Shopify/sarama v1.30.0/go.mod h1:zujlQQx1kzHsh4jfV1USnptCQrHAEZ2Hk8fTKCulPVs= github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -44,12 +45,15 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/casbin/casbin/v2 v2.40.6/go.mod h1:sEL80qBYTbd+BPeL4iyvwYzFT3qwLaESq5aFKVLbLfA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -67,6 +71,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deepmap/oapi-codegen v1.16.2 h1:xGHx0dNqYfy9gE8a7AVgVM8Sd5oF9SEgePzP+UPAUXI= +github.com/deepmap/oapi-codegen v1.16.2/go.mod h1:rdYoEA2GE+riuZ91DvpmBX9hJbQpuY9wchXpfQ3n+ho= github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -81,6 +89,12 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/getkin/kin-openapi v0.118.0 h1:z43njxPmJ7TaPpMSCQb7PN0dEYno4tyBPQcrFdHoLuM= +github.com/getkin/kin-openapi v0.118.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc= +github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= +github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -93,20 +107,43 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= +github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.10.1 h1:uA0+amWMiglNZKZ9FJRKUAe9U3RX91eVn1JYXMWt7ig= github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= +github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= @@ -141,6 +178,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -169,12 +208,17 @@ github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3 github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= @@ -183,6 +227,10 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= @@ -197,6 +245,8 @@ github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -204,6 +254,7 @@ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= @@ -225,12 +276,24 @@ github.com/labstack/echo-contrib v0.12.0/go.mod h1:kR62TbwsBgmpV2HVab5iQRsQtLuhP github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k= github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= +github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= +github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -243,6 +306,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -250,11 +315,19 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/echo-middleware v1.0.1 h1:edYGScq1phCcuDoz9AqA9eHX+tEI1LNL5PL1lkkQh1k= +github.com/oapi-codegen/echo-middleware v1.0.1/go.mod h1:DBQKRn+D/vfXOFbaX5GRwFttoJY64JH6yu+pdt7wU3o= +github.com/oapi-codegen/oapi-codegen/v2 v2.3.0 h1:rICjNsHbPP1LttefanBPnwsSwl09SqhCO7Ee623qR84= +github.com/oapi-codegen/oapi-codegen/v2 v2.3.0/go.mod h1:4k+cJeSq5ntkwlcpQSxLxICCxQzCL772o30PxdibRt4= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= @@ -264,6 +337,10 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ= +github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw= +github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -306,6 +383,7 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -320,8 +398,16 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/traPtitech/go-traq v0.0.0-20240420012203-0152d96098b0 h1:qXV2Edt1yRQulqcNv7G9mg+dmkJPX9XXOGSHLuZk+Os= +github.com/traPtitech/go-traq v0.0.0-20240420012203-0152d96098b0/go.mod h1:iac/zAWmOCe6eGomX2U8EeOAM1kwrm5TM19gRMOxJrU= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= @@ -358,6 +444,15 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -396,6 +491,10 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -439,6 +538,16 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -447,6 +556,8 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -461,6 +572,11 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 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= @@ -523,6 +639,15 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -540,12 +665,20 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -594,6 +727,10 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -686,6 +823,10 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -704,9 +845,11 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.2.3 h1:cZqzlOfg5Kf1VIdLC1D9hT6Cy9BgxhExLj/2tIgUe7Y= diff --git a/handler/handler.go b/handler/handler.go new file mode 100644 index 00000000..aa6ddd82 --- /dev/null +++ b/handler/handler.go @@ -0,0 +1,17 @@ +package handler + +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/router/middleware.go b/handler/middleware.go similarity index 74% rename from router/middleware.go rename to handler/middleware.go index c1ee10f5..b7e0167f 100644 --- a/router/middleware.go +++ b/handler/middleware.go @@ -1,4 +1,4 @@ -package router +package handler import ( "errors" @@ -21,13 +21,8 @@ type Middleware struct { } // 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, - } +func NewMiddleware() *Middleware { + return &Middleware{} } const ( @@ -38,17 +33,11 @@ const ( 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を取得しセットする @@ -102,9 +91,65 @@ func (*Middleware) TrapRateLimitMiddlewareFunc() echo.MiddlewareFunc { return middleware.RateLimiterWithConfig(config) } +// QuestionnaireReadAuthenticate アンケートの閲覧権限があるかの認証 +func (m *Middleware) QuestionnaireReadAuthenticate(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)) + } + + // 管理者ならOK + 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 { + c.Set(questionnaireIDKey, questionnaireID) + return next(c) + } + + // 公開されたらOK + questionnaire, _, _, _, _, _, err := m.GetQuestionnaireInfo(c.Request().Context(), questionnaireID) + if errors.Is(err, model.ErrRecordNotFound) { + c.Logger().Infof("questionnaire not found: %+v", err) + return echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("questionnaire not found:%d", questionnaireID)) + } + if err != nil { + c.Logger().Errorf("failed to get questionnaire read privilege info: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire read privilege info: %w", err)) + } + if !questionnaire.IsPublished { + return c.String(http.StatusForbidden, "The questionnaire is not published.") + } + + c.Set(questionnaireIDKey, questionnaireID) + + return next(c) + } +} + // 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) @@ -143,6 +188,7 @@ func (m *Middleware) QuestionnaireAdministratorAuthenticate(next echo.HandlerFun // 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) @@ -208,6 +254,7 @@ func (m *Middleware) ResponseReadAuthenticate(next echo.HandlerFunc) echo.Handle // 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) @@ -244,82 +291,6 @@ func (m *Middleware) RespondentAuthenticate(next echo.HandlerFunc) echo.HandlerF } } -// 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": @@ -333,6 +304,7 @@ func checkResponseReadPrivilege(responseReadPrivilegeInfo *model.ResponseReadPri return false, errors.New("invalid resSharedTo") } +// getValidator Validatorを設定する func getValidator(c echo.Context) (*validator.Validate, error) { rowValidate := c.Get(validatorKey) validate, ok := rowValidate.(*validator.Validate) @@ -343,6 +315,7 @@ func getValidator(c echo.Context) (*validator.Validate, error) { return validate, nil } +// getUserID ユーザーIDを取得する func getUserID(c echo.Context) (string, error) { rowUserID := c.Get(userIDKey) userID, ok := rowUserID.(string) @@ -352,33 +325,3 @@ func getUserID(c echo.Context) (string, error) { 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/handler/questionnaire.go b/handler/questionnaire.go new file mode 100644 index 00000000..1719d207 --- /dev/null +++ b/handler/questionnaire.go @@ -0,0 +1,186 @@ +package handler + +import ( + "errors" + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/traPtitech/anke-to/model" + "github.com/traPtitech/anke-to/openapi" +) + +// (GET /questionnaires) +func (h Handler) GetQuestionnaires(ctx echo.Context, params openapi.GetQuestionnairesParams) error { + res := openapi.QuestionnaireList{} + 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 = 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)) + } + + return ctx.JSON(200, res) +} + +// (POST /questionnaires) +func (h Handler) PostQuestionnaire(ctx echo.Context) error { + params := openapi.PostQuestionnaireJSONRequestBody{} + 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)) + } + validate, err := getValidator(ctx) + if err != nil { + ctx.Logger().Errorf("failed to get validator: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get validator: %w", err)) + } + + err = validate.StructCtx(ctx.Request().Context(), params) + if err != nil { + ctx.Logger().Errorf("failed to validate request body: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("failed to validate request body: %w", err)) + } + + res := openapi.QuestionnaireDetail{} + 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 = 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)) + } + + return ctx.JSON(201, res) +} + +// (GET /questionnaires/{questionnaireID}) +func (h Handler) GetQuestionnaire(ctx echo.Context, questionnaireID openapi.QuestionnaireIDInPath) error { + res := openapi.QuestionnaireDetail{} + 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)) + } + ctx.Logger().Errorf("failed to get questionnaire: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get questionnaire: %w", err)) + } + return ctx.JSON(200, res) +} + +// (PATCH /questionnaires/{questionnaireID}) +func (h Handler) EditQuestionnaire(ctx echo.Context, questionnaireID openapi.QuestionnaireIDInPath) error { + params := openapi.EditQuestionnaireJSONRequestBody{} + 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.EditQuestionnaire(ctx, questionnaireID, params) + if err != nil { + ctx.Logger().Errorf("failed to edit questionnaire: %+v", err) + return err + } + + return ctx.NoContent(200) +} + +// (DELETE /questionnaires/{questionnaireID}) +func (h Handler) DeleteQuestionnaire(ctx echo.Context, questionnaireID openapi.QuestionnaireIDInPath) error { + 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)) + } + + return ctx.NoContent(200) +} + +// (GET /questionnaires/{questionnaireID}/myRemindStatus) +func (h Handler) GetQuestionnaireMyRemindStatus(ctx echo.Context, questionnaireID openapi.QuestionnaireIDInPath) error { + res := openapi.QuestionnaireIsRemindEnabled{} + status, err := h.Questionnaire.GetQuestionnaireMyRemindStatus(ctx, questionnaireID) + if err != nil { + ctx.Logger().Errorf("failed to get questionnaire my remind status: %+v", err) + return err + } + res.IsRemindEnabled = status + + return ctx.JSON(200, res) +} + +// (PATCH /questionnaires/{questionnaireID}/myRemindStatus) +func (h Handler) EditQuestionnaireMyRemindStatus(ctx echo.Context, questionnaireID openapi.QuestionnaireIDInPath) error { + 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 err + } + return ctx.NoContent(200) +} + +// (GET /questionnaires/{questionnaireID}/responses) +func (h Handler) GetQuestionnaireResponses(ctx echo.Context, questionnaireID openapi.QuestionnaireIDInPath, params openapi.GetQuestionnaireResponsesParams) error { + 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 := h.Questionnaire.GetQuestionnaireResponses(ctx, questionnaireID, params, userID) + if err != nil { + ctx.Logger().Errorf("failed to get questionnaire responses: %+v", err) + return err + } + + return ctx.JSON(200, res) +} + +// (POST /questionnaires/{questionnaireID}/responses) +func (h Handler) PostQuestionnaireResponse(ctx echo.Context, questionnaireID openapi.QuestionnaireIDInPath) error { + res := openapi.Response{} + 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)) + } + + params := openapi.PostQuestionnaireResponseJSONRequestBody{} + 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)) + } + validate, err := getValidator(ctx) + if err != nil { + ctx.Logger().Errorf("failed to get validator: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get validator: %w", err)) + } + + err = validate.StructCtx(ctx.Request().Context(), params) + if err != nil { + ctx.Logger().Errorf("failed to validate request body: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("failed to validate request body: %w", err)) + } + + res, err = h.Questionnaire.PostQuestionnaireResponse(ctx, questionnaireID, params, userID) + if err != nil { + ctx.Logger().Errorf("failed to post questionnaire response: %+v", err) + return err + } + + return ctx.JSON(201, res) +} diff --git a/handler/response.go b/handler/response.go new file mode 100644 index 00000000..5bf97c57 --- /dev/null +++ b/handler/response.go @@ -0,0 +1,84 @@ +package handler + +import ( + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/traPtitech/anke-to/openapi" +) + +// (GET /responses/myResponses) +func (h Handler) GetMyResponses(ctx echo.Context, params openapi.GetMyResponsesParams) error { + res := openapi.ResponsesWithQuestionnaireInfo{} + 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 = 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)) + } + return ctx.JSON(200, res) +} + +// (DELETE /responses/{responseID}) +func (h Handler) DeleteResponse(ctx echo.Context, responseID openapi.ResponseIDInPath) error { + 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)) + } + + err = h.Response.DeleteResponse(ctx, responseID, userID) + if err != nil { + ctx.Logger().Errorf("failed to delete response: %+v", err) + return err + } + + return ctx.NoContent(200) +} + +// (GET /responses/{responseID}) +func (h Handler) GetResponse(ctx echo.Context, responseID openapi.ResponseIDInPath) error { + res := openapi.Response{} + + res, err := h.Response.GetResponse(ctx, responseID) + if err != nil { + ctx.Logger().Errorf("failed to get response: %+v", err) + return err + } + return ctx.JSON(200, res) +} + +// (PATCH /responses/{responseID}) +func (h Handler) EditResponse(ctx echo.Context, responseID openapi.ResponseIDInPath) error { + req := openapi.EditResponseJSONRequestBody{} + if err := ctx.Bind(&req); err != nil { + ctx.Logger().Errorf("failed to bind Responses: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("failed to bind Responses: %w", err)) + } + + validate, err := getValidator(ctx) + if err != nil { + ctx.Logger().Errorf("failed to get validator: %+v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to get validator: %w", err)) + } + + err = validate.Struct(req) + if err != nil { + ctx.Logger().Errorf("failed to validate request body: %+v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("failed to validate request body: %w", err)) + } + + err = h.Response.EditResponse(ctx, responseID, req) + if err != nil { + ctx.Logger().Errorf("failed to edit response: %+v", err) + return err + } + + return ctx.NoContent(200) +} diff --git a/main.go b/main.go index f5457edc..2aad8ef1 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,13 @@ import ( "os" "runtime" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + oapiMiddleware "github.com/oapi-codegen/echo-middleware" + "github.com/traPtitech/anke-to/controller" "github.com/traPtitech/anke-to/model" + "github.com/traPtitech/anke-to/openapi" + "github.com/traPtitech/anke-to/tuning" ) @@ -51,5 +57,53 @@ func main() { panic("no PORT") } - SetRouting(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/middleware.go b/middleware.go new file mode 100644 index 00000000..5a7ba3cf --- /dev/null +++ b/middleware.go @@ -0,0 +1,80 @@ +package main + +import ( + "strings" + + "github.com/labstack/echo/v4" +) + +type RouteConfig struct { + path string + method string + middlewares []echo.MiddlewareFunc + isGroup bool +} + +type MiddlewareSwitcher struct { + routeConfigs []RouteConfig +} + +func NewMiddlewareSwitcher() *MiddlewareSwitcher { + return &MiddlewareSwitcher{ + routeConfigs: []RouteConfig{}, + } +} + +func (m *MiddlewareSwitcher) AddGroupConfig(grouppath string, middlewares ...echo.MiddlewareFunc) { + m.routeConfigs = append(m.routeConfigs, RouteConfig{ + path: grouppath, + middlewares: middlewares, + isGroup: true, + }) +} + +func (m *MiddlewareSwitcher) AddRouteConfig(path string, method string, middlewares ...echo.MiddlewareFunc) { + m.routeConfigs = append(m.routeConfigs, RouteConfig{ + path: path, + method: method, + middlewares: middlewares, + isGroup: false, + }) +} + +func (m *MiddlewareSwitcher) IsWithinGroup(groupPath string, path string) bool { + if !strings.HasPrefix(path, groupPath) { + return false + } + return len(groupPath) == len(path) || path[len(groupPath)] == '/' +} + +func (m *MiddlewareSwitcher) FindMiddlewares(path string, method string) []echo.MiddlewareFunc { + var matchedMiddlewares []echo.MiddlewareFunc + + for _, config := range m.routeConfigs { + if config.isGroup && m.IsWithinGroup(config.path, path) { + matchedMiddlewares = append(matchedMiddlewares, config.middlewares...) + } + if !config.isGroup && config.path == path && config.method == method { + matchedMiddlewares = append(matchedMiddlewares, config.middlewares...) + } + } + + return matchedMiddlewares +} + +func (m *MiddlewareSwitcher) ApplyMiddlewares(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + path := c.Path() + method := c.Request().Method + + middlewares := m.FindMiddlewares(path, method) + + for _, mw := range middlewares { + if err := mw(next)(c); err != nil { + return err + } + } + + return next(c) + } +} diff --git a/model/administratorGroups.go b/model/administratorGroups.go new file mode 100644 index 00000000..dfddc73a --- /dev/null +++ b/model/administratorGroups.go @@ -0,0 +1,10 @@ +package model + +import "context" + +// IAdministratorGroup AdministratorGroupのRepository +type IAdministratorGroup interface { + InsertAdministratorGroups(ctx context.Context, questionnaireID int, administratorGroups []string) error + DeleteAdministratorGroups(ctx context.Context, questionnaireID int) error + GetAdministratorGroups(ctx context.Context, questionnaireIDs []int) ([]AdministratorGroups, error) +} diff --git a/model/administratorGroups_impl.go b/model/administratorGroups_impl.go new file mode 100644 index 00000000..ccf97e6b --- /dev/null +++ b/model/administratorGroups_impl.go @@ -0,0 +1,83 @@ +package model + +import ( + "context" + "fmt" + + "github.com/gofrs/uuid" +) + +// AdministratorGroup AdministratorGroupRepositoryの実装 +type AdministratorGroup struct{} + +// NewAdministratorGroup AdministratorGroupRepositoryのコンストラクタ +func NewAdministratorGroup() *AdministratorGroup { + return new(AdministratorGroup) +} + +type AdministratorGroups struct { + QuestionnaireID int `gorm:"type:int(11);not null;primaryKey"` + GroupID uuid.UUID `gorm:"type:varchar(36);size:36;not null;primaryKey"` +} + +// InsertAdministratorGroups アンケートの管理者グループを追加 +func (*AdministratorGroup) InsertAdministratorGroups(ctx context.Context, questionnaireID int, groupID []uuid.UUID) error { + db, err := getTx(ctx) + if err != nil { + return fmt.Errorf("failed to get transaction: %w", err) + } + + if len(groupID) == 0 { + return nil + } + + dbAdministratorGroups := make([]AdministratorGroups, 0, len(groupID)) + for _, administratorGroup := range groupID { + dbAdministratorGroups = append(dbAdministratorGroups, AdministratorGroups{ + QuestionnaireID: questionnaireID, + GroupID: administratorGroup, + }) + } + + err = db.Create(&dbAdministratorGroups).Error + if err != nil { + return fmt.Errorf("failed to insert administrator groups: %w", err) + } + + return nil +} + +// DeleteAdministratorGroups アンケートの管理者グループを削除 +func (*AdministratorGroup) DeleteAdministratorGroups(ctx context.Context, questionnaireID int) error { + db, err := getTx(ctx) + if err != nil { + return fmt.Errorf("failed to get transaction: %w", err) + } + + err = db. + Where("questionnaire_id = ?", questionnaireID). + Delete(AdministratorGroups{}).Error + if err != nil { + return fmt.Errorf("failed to delete administrator groups: %w", err) + } + + return nil +} + +// GetAdministratorGroups アンケートの管理者グループを取得 +func (*AdministratorGroup) GetAdministratorGroups(ctx context.Context, questionnaireIDs []int) ([]AdministratorGroups, error) { + db, err := getTx(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get transaction: %w", err) + } + + var administratorGroups []AdministratorGroups + err = db. + Where("questionnaire_id IN ?", questionnaireIDs). + Find(&administratorGroups).Error + if err != nil { + return nil, fmt.Errorf("failed to get administrator groups: %w", err) + } + + return administratorGroups, nil +} diff --git a/model/current.go b/model/current.go index 9b3fea3e..973a7fda 100644 --- a/model/current.go +++ b/model/current.go @@ -6,7 +6,9 @@ import ( // Migrations is all db migrations func Migrations() []*gormigrate.Migration { - return []*gormigrate.Migration{} + return []*gormigrate.Migration{ + v3(), + } } func AllTables() []interface{} { diff --git a/model/errors.go b/model/errors.go index 9c7df7cc..eb74acac 100755 --- a/model/errors.go +++ b/model/errors.go @@ -29,4 +29,6 @@ var ( ErrInvalidTx = errors.New("invalid tx") // ErrDeadlineExceeded deadline exceeded ErrDeadlineExceeded = errors.New("deadline exceeded") + // ErrDuplicatedAnswered + ErrDuplicatedAnswered = errors.New("duplicated answered is not allowed") ) diff --git a/model/questionnaires.go b/model/questionnaires.go index ec759d2e..67d20177 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) (int, error) - UpdateQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, questionnaireID int) error + InsertQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, isPublished bool, isAnonymous bool, IsDuplicateAnswerAllowed bool) (int, error) + UpdateQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, questionnaireID int, isPublished bool, isAnonymous bool, IsDuplicateAnswerAllowed bool) error DeleteQuestionnaire(ctx context.Context, questionnaireID int) error - GetQuestionnaires(ctx context.Context, userID string, sort string, search string, pageNum int, nontargeted bool) ([]QuestionnaireInfo, 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, 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 e7756bc7..eb93db5f 100755 --- a/model/questionnaires_impl.go +++ b/model/questionnaires_impl.go @@ -7,6 +7,7 @@ import ( "regexp" "time" + "github.com/google/uuid" "gopkg.in/guregu/null.v4" "gorm.io/gorm" ) @@ -21,18 +22,22 @@ func NewQuestionnaire() *Questionnaire { // Questionnaires questionnairesテーブルの構造体 type Questionnaires 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"` - Description string `json:"description" gorm:"type:text;not null"` - 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"` - 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"` - Questions []Questions `json:"-" gorm:"foreignKey:QuestionnaireID"` - Respondents []Respondents `json:"-" gorm:"foreignKey:QuestionnaireID"` + 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"` + Description string `json:"description" gorm:"type:text;not null"` + 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"` + 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"` + IsAnonymous bool `json:"is_anonymous" gorm:"type:boolean;not null;default:false"` + IsDuplicateAnswerAllowed bool `json:"is_duplicate_answer_allowed" gorm:"type:tinyint(4);size:4;not null;default:0"` } // BeforeCreate Update時に自動でmodified_atを現在時刻に @@ -79,7 +84,7 @@ type ResponseReadPrivilegeInfo struct { } // InsertQuestionnaire アンケートの追加 -func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string) (int, error) { +func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, isPublished bool, isAnonymous bool, isDuplicateAnswerAllowed bool) (int, error) { db, err := getTx(ctx) if err != nil { return 0, fmt.Errorf("failed to get tx: %w", err) @@ -88,16 +93,22 @@ func (*Questionnaire) InsertQuestionnaire(ctx context.Context, title string, des var questionnaire Questionnaires if !resTimeLimit.Valid { questionnaire = Questionnaires{ - Title: title, - Description: description, - ResSharedTo: resSharedTo, + Title: title, + Description: description, + ResSharedTo: resSharedTo, + IsPublished: isPublished, + IsAnonymous: isAnonymous, + IsDuplicateAnswerAllowed: isDuplicateAnswerAllowed, } } else { questionnaire = Questionnaires{ - Title: title, - Description: description, - ResTimeLimit: resTimeLimit, - ResSharedTo: resSharedTo, + Title: title, + Description: description, + ResTimeLimit: resTimeLimit, + ResSharedTo: resSharedTo, + IsPublished: isPublished, + IsAnonymous: isAnonymous, + IsDuplicateAnswerAllowed: isDuplicateAnswerAllowed, } } @@ -110,7 +121,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) error { +func (*Questionnaire) UpdateQuestionnaire(ctx context.Context, title string, description string, resTimeLimit null.Time, resSharedTo string, questionnaireID int, isPublished bool, isAnonymous bool, isDuplicateAnswerAllowed bool) error { db, err := getTx(ctx) if err != nil { return fmt.Errorf("failed to get tx: %w", err) @@ -119,17 +130,23 @@ func (*Questionnaire) UpdateQuestionnaire(ctx context.Context, title string, des var questionnaire interface{} if resTimeLimit.Valid { questionnaire = Questionnaires{ - Title: title, - Description: description, - ResTimeLimit: resTimeLimit, - ResSharedTo: resSharedTo, + Title: title, + Description: description, + ResTimeLimit: resTimeLimit, + ResSharedTo: resSharedTo, + IsPublished: isPublished, + IsAnonymous: isAnonymous, + IsDuplicateAnswerAllowed: isDuplicateAnswerAllowed, } } else { questionnaire = map[string]interface{}{ - "title": title, - "description": description, - "res_time_limit": gorm.Expr("NULL"), - "res_shared_to": resSharedTo, + "title": title, + "description": description, + "res_time_limit": gorm.Expr("NULL"), + "res_shared_to": resSharedTo, + "is_published": isPublished, + "is_anonymous": isAnonymous, + "is_duplicate_answer_allowed": isDuplicateAnswerAllowed, } } @@ -171,7 +188,7 @@ func (*Questionnaire) DeleteQuestionnaire(ctx context.Context, questionnaireID i GetQuestionnaires アンケートの一覧 2つ目の戻り値はページ数の最大値 */ -func (*Questionnaire) GetQuestionnaires(ctx context.Context, userID string, sort string, search string, pageNum int, nontargeted bool) ([]QuestionnaireInfo, int, error) { +func (*Questionnaire) GetQuestionnaires(ctx context.Context, userID string, sort string, search string, pageNum int, onlyTargetingMe bool, onlyAdministratedByMe bool) ([]QuestionnaireInfo, int, error) { ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() @@ -184,7 +201,7 @@ func (*Questionnaire) GetQuestionnaires(ctx context.Context, userID string, sort query := db. Table("questionnaires"). - Where("deleted_at IS NULL"). + Where("deleted_at IS NULL AND is_published IS TRUE"). Joins("LEFT OUTER JOIN targets ON questionnaires.id = targets.questionnaire_id") query, err = setQuestionnairesOrder(query, sort) @@ -192,9 +209,16 @@ func (*Questionnaire) GetQuestionnaires(ctx context.Context, userID string, sort return nil, 0, fmt.Errorf("failed to set the order of the questionnaire table: %w", err) } - if nontargeted { - query = query.Where("targets.questionnaire_id IS NULL OR (targets.user_traqid != ? AND targets.user_traqid != 'traP')", userID) + if onlyTargetingMe { + query = query.Where("targets.user_traqid = ? OR targets.user_traqid = 'traP'", userID) } + + if onlyAdministratedByMe { + query = query. + Joins("INNER JOIN administrators ON questionnaires.id = administrators.questionnaire_id"). + Where("administrators.user_traqid = ?", userID) + } + if len(search) != 0 { // MySQLでのregexpの構文は少なくともGoのregexpの構文でvalidである必要がある _, err := regexp.Compile(search) @@ -267,25 +291,28 @@ func (*Questionnaire) GetAdminQuestionnaires(ctx context.Context, userID string) } // GetQuestionnaireInfo アンケートの詳細な情報取得 -func (*Questionnaire) GetQuestionnaireInfo(ctx context.Context, questionnaireID int) (*Questionnaires, []string, []string, []string, error) { +func (*Questionnaire) GetQuestionnaireInfo(ctx context.Context, questionnaireID int) (*Questionnaires, []string, []uuid.UUID, []string, []uuid.UUID, []string, error) { db, err := getTx(ctx) if err != nil { - return nil, nil, nil, nil, fmt.Errorf("failed to get tx: %w", err) + return nil, nil, nil, nil, nil, nil, fmt.Errorf("failed to get tx: %w", err) } questionnaire := Questionnaires{} targets := []string{} + targetGroups := []uuid.UUID{} administrators := []string{} + administoratorGroups := []uuid.UUID{} respondents := []string{} err = db. Where("questionnaires.id = ?", questionnaireID). + Preload("Targets"). First(&questionnaire).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil, nil, nil, ErrRecordNotFound + return nil, nil, nil, nil, nil, nil, ErrRecordNotFound } if err != nil { - return nil, nil, nil, nil, fmt.Errorf("failed to get a questionnaire: %w", err) + return nil, nil, nil, nil, nil, nil, fmt.Errorf("failed to get a questionnaire: %w", err) } err = db. @@ -294,7 +321,7 @@ func (*Questionnaire) GetQuestionnaireInfo(ctx context.Context, questionnaireID Where("questionnaire_id = ?", questionnaire.ID). Pluck("user_traqid", &targets).Error if err != nil { - return nil, nil, nil, nil, fmt.Errorf("failed to get targets: %w", err) + return nil, nil, nil, nil, nil, nil, fmt.Errorf("failed to get targets: %w", err) } err = db. @@ -303,7 +330,7 @@ func (*Questionnaire) GetQuestionnaireInfo(ctx context.Context, questionnaireID Where("questionnaire_id = ?", questionnaire.ID). Pluck("user_traqid", &administrators).Error if err != nil { - return nil, nil, nil, nil, fmt.Errorf("failed to get administrators: %w", err) + return nil, nil, nil, nil, nil, nil, fmt.Errorf("failed to get administrators: %w", err) } err = db. @@ -312,10 +339,10 @@ func (*Questionnaire) GetQuestionnaireInfo(ctx context.Context, questionnaireID Where("questionnaire_id = ? AND deleted_at IS NULL AND submitted_at IS NOT NULL", questionnaire.ID). Pluck("user_traqid", &respondents).Error if err != nil { - return nil, nil, nil, nil, fmt.Errorf("failed to get respondents: %w", err) + return nil, nil, nil, nil, nil, nil, fmt.Errorf("failed to get respondents: %w", err) } - return &questionnaire, targets, administrators, respondents, nil + return &questionnaire, targets, targetGroups, administrators, administoratorGroups, respondents, nil } // GetTargettedQuestionnaires targetになっているアンケートの取得 @@ -362,6 +389,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) @@ -409,6 +454,7 @@ func (*Questionnaire) GetQuestionnaireLimitByResponseID(ctx context.Context, res return res.ResTimeLimit, nil } +// GetResponseReadPrivilegeInfoByResponseID 回答のIDから回答の閲覧権限情報を取得 func (*Questionnaire) GetResponseReadPrivilegeInfoByResponseID(ctx context.Context, userID string, responseID int) (*ResponseReadPrivilegeInfo, error) { db, err := getTx(ctx) if err != nil { @@ -434,6 +480,7 @@ func (*Questionnaire) GetResponseReadPrivilegeInfoByResponseID(ctx context.Conte return &responseReadPrivilegeInfo, nil } +// GetResponseReadPrivilegeInfoByQuestionnaireID アンケートのIDから回答の閲覧権限情報を取得 func (*Questionnaire) GetResponseReadPrivilegeInfoByQuestionnaireID(ctx context.Context, userID string, questionnaireID int) (*ResponseReadPrivilegeInfo, error) { db, err := getTx(ctx) if err != nil { @@ -458,6 +505,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 16b80506..bb173f15 100644 --- a/model/questionnaires_test.go +++ b/model/questionnaires_test.go @@ -67,6 +67,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(questionnairesNow, true), ResSharedTo: "public", + IsPublished: true, CreatedAt: questionnairesNow, ModifiedAt: questionnairesNow, }, @@ -80,6 +81,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(time.Time{}, false), ResSharedTo: "respondents", + IsPublished: true, CreatedAt: questionnairesNow, ModifiedAt: questionnairesNow, }, @@ -93,6 +95,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(time.Time{}, false), ResSharedTo: "administrators", + IsPublished: true, CreatedAt: questionnairesNow.Add(time.Second), ModifiedAt: questionnairesNow.Add(2 * time.Second), }, @@ -106,6 +109,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(time.Time{}, false), ResSharedTo: "public", + IsPublished: true, CreatedAt: questionnairesNow, ModifiedAt: questionnairesNow, }, @@ -126,6 +130,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(time.Time{}, false), ResSharedTo: "public", + IsPublished: true, CreatedAt: questionnairesNow, ModifiedAt: questionnairesNow, }, @@ -145,6 +150,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(time.Time{}, false), ResSharedTo: "public", + IsPublished: true, CreatedAt: questionnairesNow.Add(2 * time.Second), ModifiedAt: questionnairesNow.Add(3 * time.Second), }, @@ -158,6 +164,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(time.Time{}, false), ResSharedTo: "public", + IsPublished: true, CreatedAt: questionnairesNow, ModifiedAt: questionnairesNow, DeletedAt: gorm.DeletedAt{ @@ -177,6 +184,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(time.Time{}, false), ResSharedTo: "public", + IsPublished: true, CreatedAt: questionnairesNow.Add(time.Duration(len(datas)) * time.Second), ModifiedAt: questionnairesNow, }, @@ -191,6 +199,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(time.Time{}, false), ResSharedTo: "public", + IsPublished: true, CreatedAt: questionnairesNow.Add(2 * time.Second), ModifiedAt: questionnairesNow.Add(3 * time.Second), }, @@ -203,6 +212,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(time.Time{}, false), ResSharedTo: "public", + IsPublished: true, CreatedAt: questionnairesNow, ModifiedAt: questionnairesNow, }, @@ -222,6 +232,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(time.Time{}, false), ResSharedTo: "public", + IsPublished: true, CreatedAt: questionnairesNow, ModifiedAt: questionnairesNow, }, @@ -240,6 +251,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(questionnairesNow, true), ResSharedTo: "public", + IsPublished: true, CreatedAt: questionnairesNow, ModifiedAt: questionnairesNow, }, @@ -259,6 +271,7 @@ func setupQuestionnairesTest(t *testing.T) { Description: "第1回集会らん☆ぷろ参加者募集", ResTimeLimit: null.NewTime(time.Time{}, false), ResSharedTo: "public", + IsPublished: true, CreatedAt: questionnairesNow, ModifiedAt: questionnairesNow, }, @@ -351,10 +364,13 @@ func insertQuestionnaireTest(t *testing.T) { assertion := assert.New(t) type args struct { - title string - description string - resTimeLimit null.Time - resSharedTo string + title string + description string + resTimeLimit null.Time + resSharedTo string + isPublished bool + isAnonymous bool + isDuplicateAnswerAllowed bool } type expect struct { isErr bool @@ -375,6 +391,8 @@ func insertQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, }, { @@ -384,6 +402,8 @@ func insertQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, }, { @@ -393,6 +413,8 @@ func insertQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "respondents", + isPublished: true, + isAnonymous: false, }, }, { @@ -402,6 +424,8 @@ func insertQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "administrators", + isPublished: true, + isAnonymous: false, }, }, { @@ -411,6 +435,8 @@ func insertQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, }, { @@ -420,6 +446,8 @@ func insertQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, expect: expect{ isErr: true, @@ -432,6 +460,8 @@ func insertQuestionnaireTest(t *testing.T) { description: strings.Repeat("a", 2000), resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, }, { @@ -441,17 +471,41 @@ func insertQuestionnaireTest(t *testing.T) { description: strings.Repeat("a", 200000), resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, expect: expect{ isErr: true, }, }, + { + description: "not published", + args: args{ + title: "第1回集会らん☆ぷろ募集アンケート", + description: "第1回集会らん☆ぷろ参加者募集", + 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, + }, + }, } for _, testCase := range testCases { ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, testCase.args.title, testCase.args.description, testCase.args.resTimeLimit, testCase.args.resSharedTo) + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, testCase.args.title, testCase.args.description, testCase.args.resTimeLimit, testCase.args.resSharedTo, testCase.args.isPublished, testCase.args.isAnonymous, testCase.args.isDuplicateAnswerAllowed) if !testCase.expect.isErr { assertion.NoError(err, testCase.description, "no error") @@ -475,6 +529,8 @@ func insertQuestionnaireTest(t *testing.T) { assertion.Equal(testCase.args.description, questionnaire.Description, testCase.description, "description") assertion.WithinDuration(testCase.args.resTimeLimit.ValueOrZero(), questionnaire.ResTimeLimit.ValueOrZero(), 2*time.Second, testCase.description, "res_time_limit") assertion.Equal(testCase.args.resSharedTo, questionnaire.ResSharedTo, testCase.description, "res_shared_to") + assertion.Equal(testCase.args.isPublished, questionnaire.IsPublished, testCase.description, "is_published") + assertion.Equal(testCase.args.isDuplicateAnswerAllowed, questionnaire.IsDuplicateAnswerAllowed, testCase.description, "is_duplicate_answer_allowed") assertion.WithinDuration(time.Now(), questionnaire.CreatedAt, 2*time.Second, testCase.description, "created_at") assertion.WithinDuration(time.Now(), questionnaire.ModifiedAt, 2*time.Second, testCase.description, "modified_at") @@ -487,10 +543,13 @@ func updateQuestionnaireTest(t *testing.T) { assertion := assert.New(t) type args struct { - title string - description string - resTimeLimit null.Time - resSharedTo string + title string + description string + resTimeLimit null.Time + resSharedTo string + isPublished bool + isAnonymous bool + isDuplicateAnswerAllowed bool } type expect struct { isErr bool @@ -512,12 +571,16 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "respondents", + isPublished: true, + isAnonymous: false, }, }, { @@ -527,12 +590,16 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, after: args{ title: "第2回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, }, { @@ -542,12 +609,16 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第2回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, }, { @@ -557,12 +628,16 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "respondents", + isPublished: true, + isAnonymous: false, }, }, { @@ -572,12 +647,16 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, after: args{ title: "第2回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, }, { @@ -587,12 +666,16 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第2回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, }, { @@ -602,12 +685,16 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, }, { @@ -617,12 +704,16 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now().Add(time.Minute), true), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, }, { @@ -632,12 +723,54 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Now(), true), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, after: args{ title: "第1回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, + }, + }, + { + description: "update is_published(false->true)", + before: args{ + title: "第1回集会らん☆ぷろ募集アンケート", + description: "第1回集会らん☆ぷろ参加者募集", + resTimeLimit: null.NewTime(time.Time{}, false), + resSharedTo: "public", + isPublished: false, + isAnonymous: false, + }, + after: args{ + title: "第1回集会らん☆ぷろ募集アンケート", + description: "第1回集会らん☆ぷろ参加者募集", + 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, }, }, } @@ -647,10 +780,12 @@ func updateQuestionnaireTest(t *testing.T) { before := &testCase.before questionnaire := Questionnaires{ - Title: before.title, - Description: before.description, - ResTimeLimit: before.resTimeLimit, - ResSharedTo: before.resSharedTo, + Title: before.title, + Description: before.description, + ResTimeLimit: before.resTimeLimit, + ResSharedTo: before.resSharedTo, + IsPublished: before.isPublished, + IsDuplicateAnswerAllowed: before.isDuplicateAnswerAllowed, } err := db. Session(&gorm.Session{NewDB: true}). @@ -662,7 +797,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) + err = questionnaireImpl.UpdateQuestionnaire(ctx, after.title, after.description, after.resTimeLimit, after.resSharedTo, questionnaireID, after.isPublished, after.isAnonymous, after.isDuplicateAnswerAllowed) if !testCase.expect.isErr { assertion.NoError(err, testCase.description, "no error") @@ -686,6 +821,8 @@ func updateQuestionnaireTest(t *testing.T) { assertion.Equal(after.description, questionnaire.Description, testCase.description, "description") assertion.WithinDuration(after.resTimeLimit.ValueOrZero(), questionnaire.ResTimeLimit.ValueOrZero(), 2*time.Second, testCase.description, "res_time_limit") assertion.Equal(after.resSharedTo, questionnaire.ResSharedTo, testCase.description, "res_shared_to") + assertion.Equal(after.isPublished, questionnaire.IsPublished, testCase.description, "is_published") + assertion.Equal(after.isDuplicateAnswerAllowed, questionnaire.IsDuplicateAnswerAllowed, testCase.description, "is_duplicate_answer_allowed") assertion.WithinDuration(createdAt, questionnaire.CreatedAt, 2*time.Second, testCase.description, "created_at") assertion.WithinDuration(time.Now(), questionnaire.ModifiedAt, 2*time.Second, testCase.description, "modified_at") @@ -714,19 +851,23 @@ func updateQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, { title: "第1回集会らん☆ぷろ募集アンケート", description: "第1回集会らん☆ぷろ参加者募集", 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) + err := questionnaireImpl.UpdateQuestionnaire(ctx, arg.title, arg.description, arg.resTimeLimit, arg.resSharedTo, invalidQuestionnaireID, arg.isPublished, arg.isAnonymous, arg.isDuplicateAnswerAllowed) if !errors.Is(err, ErrNoRecordUpdated) { if err == nil { t.Errorf("Succeeded with invalid questionnaireID") @@ -743,10 +884,13 @@ func deleteQuestionnaireTest(t *testing.T) { assertion := assert.New(t) type args struct { - title string - description string - resTimeLimit null.Time - resSharedTo string + title string + description string + resTimeLimit null.Time + resSharedTo string + isPublished bool + isAnonymous bool + isDuplicateAnswerAllowed bool } type expect struct { isErr bool @@ -764,6 +908,8 @@ func deleteQuestionnaireTest(t *testing.T) { description: "第1回集会らん☆ぷろ参加者募集", resTimeLimit: null.NewTime(time.Time{}, false), resSharedTo: "public", + isPublished: true, + isAnonymous: false, }, }, } @@ -772,10 +918,13 @@ func deleteQuestionnaireTest(t *testing.T) { ctx := context.Background() questionnaire := Questionnaires{ - Title: testCase.args.title, - Description: testCase.args.description, - ResTimeLimit: testCase.args.resTimeLimit, - ResSharedTo: testCase.args.resSharedTo, + Title: testCase.args.title, + Description: testCase.args.description, + ResTimeLimit: testCase.args.resTimeLimit, + ResSharedTo: testCase.args.resSharedTo, + IsPublished: testCase.isPublished, + IsAnonymous: testCase.args.isAnonymous, + IsDuplicateAnswerAllowed: testCase.args.isDuplicateAnswerAllowed, } err := db. Session(&gorm.Session{NewDB: true}). @@ -883,11 +1032,12 @@ func getQuestionnairesTest(t *testing.T) { } type args struct { - userID string - sort string - search string - pageNum int - nontargeted bool + userID string + sort string + search string + pageNum int + onlyTargetingMe bool + onlyAdministratedByMe bool } type expect struct { isErr bool @@ -905,81 +1055,89 @@ func getQuestionnairesTest(t *testing.T) { { description: "userID:valid, sort:no, search:no, page:1", args: args{ - userID: questionnairesTestUserID, - sort: "", - search: "", - pageNum: 1, - nontargeted: false, + userID: questionnairesTestUserID, + sort: "", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: true, }, }, { description: "userID:valid, sort:created_at, search:no, page:1", args: args{ - userID: questionnairesTestUserID, - sort: "created_at", - search: "", - pageNum: 1, - nontargeted: false, + userID: questionnairesTestUserID, + sort: "created_at", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: true, }, }, { description: "userID:valid, sort:-created_at, search:no, page:1", args: args{ - userID: questionnairesTestUserID, - sort: "-created_at", - search: "", - pageNum: 1, - nontargeted: false, + userID: questionnairesTestUserID, + sort: "-created_at", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: true, }, }, { description: "userID:valid, sort:title, search:no, page:1", args: args{ - userID: questionnairesTestUserID, - sort: "title", - search: "", - pageNum: 1, - nontargeted: false, + userID: questionnairesTestUserID, + sort: "title", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: true, }, }, { description: "userID:valid, sort:-title, search:no, page:1", args: args{ - userID: questionnairesTestUserID, - sort: "-title", - search: "", - pageNum: 1, - nontargeted: false, + userID: questionnairesTestUserID, + sort: "-title", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: true, }, }, { description: "userID:valid, sort:modified_at, search:no, page:1", args: args{ - userID: questionnairesTestUserID, - sort: "modified_at", - search: "", - pageNum: 1, - nontargeted: false, + userID: questionnairesTestUserID, + sort: "modified_at", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: true, }, }, { description: "userID:valid, sort:-modified_at, search:no, page:1", args: args{ - userID: questionnairesTestUserID, - sort: "-modified_at", - search: "", - pageNum: 1, - nontargeted: false, + userID: questionnairesTestUserID, + sort: "-modified_at", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: true, }, }, { description: "userID:valid, sort:no, search:GetQuestionnaireTest$, page:1", args: args{ - userID: questionnairesTestUserID, - sort: "", - search: "GetQuestionnaireTest$", - pageNum: 1, - nontargeted: false, + userID: questionnairesTestUserID, + sort: "", + search: "GetQuestionnaireTest$", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: true, }, expect: expect{ isCheckLen: true, @@ -989,21 +1147,23 @@ func getQuestionnairesTest(t *testing.T) { { description: "userID:valid, sort:no, search:no, page:2", args: args{ - userID: questionnairesTestUserID, - sort: "", - search: "", - pageNum: 2, - nontargeted: false, + userID: questionnairesTestUserID, + sort: "", + search: "", + pageNum: 2, + onlyTargetingMe: true, + onlyAdministratedByMe: true, }, }, { description: "too large page", args: args{ - userID: questionnairesTestUserID, - sort: "", - search: "", - pageNum: 100000, - nontargeted: false, + userID: questionnairesTestUserID, + sort: "", + search: "", + pageNum: 100000, + onlyTargetingMe: true, + onlyAdministratedByMe: true, }, expect: expect{ isErr: true, @@ -1013,21 +1173,23 @@ func getQuestionnairesTest(t *testing.T) { { description: "userID:valid, sort:no, search:no, page:1, nontargetted", args: args{ - userID: questionnairesTestUserID, - sort: "", - search: "", - pageNum: 1, - nontargeted: true, + userID: questionnairesTestUserID, + sort: "", + search: "", + pageNum: 1, + onlyTargetingMe: true, + onlyAdministratedByMe: true, }, }, { description: "userID:valid, sort:no, search:notFoundQuestionnaire, page:1", args: args{ - userID: questionnairesTestUserID, - sort: "", - search: "notFoundQuestionnaire", - pageNum: 1, - nontargeted: true, + userID: questionnairesTestUserID, + sort: "", + search: "notFoundQuestionnaire", + pageNum: 1, + onlyTargetingMe: true, + onlyAdministratedByMe: true, }, expect: expect{ isCheckLen: false, @@ -1037,11 +1199,171 @@ func getQuestionnairesTest(t *testing.T) { { description: "userID:valid, sort:invalid, search:no, page:1", args: args{ - userID: questionnairesTestUserID, - sort: "hogehoge", - search: "", - pageNum: 1, - nontargeted: false, + userID: questionnairesTestUserID, + sort: "hogehoge", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: true, + }, + expect: expect{ + isErr: true, + err: ErrInvalidSortParam, + }, + }, + { + description: "userID:valid, sort:no, search:no, page:1", + args: args{ + userID: questionnairesTestUserID, + sort: "", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: false, + }, + }, + { + description: "userID:valid, sort:created_at, search:no, page:1", + args: args{ + userID: questionnairesTestUserID, + sort: "created_at", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: false, + }, + }, + { + description: "userID:valid, sort:-created_at, search:no, page:1", + args: args{ + userID: questionnairesTestUserID, + sort: "-created_at", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: false, + }, + }, + { + description: "userID:valid, sort:title, search:no, page:1", + args: args{ + userID: questionnairesTestUserID, + sort: "title", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: false, + }, + }, + { + description: "userID:valid, sort:-title, search:no, page:1", + args: args{ + userID: questionnairesTestUserID, + sort: "-title", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: false, + }, + }, + { + description: "userID:valid, sort:modified_at, search:no, page:1", + args: args{ + userID: questionnairesTestUserID, + sort: "modified_at", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: false, + }, + }, + { + description: "userID:valid, sort:-modified_at, search:no, page:1", + args: args{ + userID: questionnairesTestUserID, + sort: "-modified_at", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: false, + }, + }, + { + description: "userID:valid, sort:no, search:GetQuestionnaireTest$, page:1", + args: args{ + userID: questionnairesTestUserID, + sort: "", + search: "GetQuestionnaireTest$", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: false, + }, + expect: expect{ + isCheckLen: true, + length: 4, + }, + }, + { + description: "userID:valid, sort:no, search:no, page:2", + args: args{ + userID: questionnairesTestUserID, + sort: "", + search: "", + pageNum: 2, + onlyTargetingMe: true, + onlyAdministratedByMe: false, + }, + }, + { + description: "too large page", + args: args{ + userID: questionnairesTestUserID, + sort: "", + search: "", + pageNum: 100000, + onlyTargetingMe: true, + onlyAdministratedByMe: false, + }, + expect: expect{ + isErr: true, + err: ErrTooLargePageNum, + }, + }, + { + description: "userID:valid, sort:no, search:no, page:1, nontargetted", + args: args{ + userID: questionnairesTestUserID, + sort: "", + search: "", + pageNum: 1, + onlyTargetingMe: true, + onlyAdministratedByMe: false, + }, + }, + { + description: "userID:valid, sort:no, search:notFoundQuestionnaire, page:1", + args: args{ + userID: questionnairesTestUserID, + sort: "", + search: "notFoundQuestionnaire", + pageNum: 1, + onlyTargetingMe: true, + onlyAdministratedByMe: false, + }, + expect: expect{ + isCheckLen: false, + length: 0, + }, + }, + { + description: "userID:valid, sort:invalid, search:no, page:1", + args: args{ + userID: questionnairesTestUserID, + sort: "hogehoge", + search: "", + pageNum: 1, + onlyTargetingMe: false, + onlyAdministratedByMe: false, }, expect: expect{ isErr: true, @@ -1053,7 +1375,7 @@ func getQuestionnairesTest(t *testing.T) { for _, testCase := range testCases { ctx := context.Background() - questionnaires, pageMax, err := questionnaireImpl.GetQuestionnaires(ctx, testCase.args.userID, testCase.args.sort, testCase.args.search, testCase.args.pageNum, testCase.args.nontargeted) + questionnaires, pageMax, err := questionnaireImpl.GetQuestionnaires(ctx, testCase.args.userID, testCase.args.sort, testCase.args.search, testCase.args.pageNum, testCase.args.onlyTargetingMe, testCase.args.onlyAdministratedByMe) if !testCase.expect.isErr { assertion.NoError(err, testCase.description, "no error") @@ -1070,7 +1392,7 @@ func getQuestionnairesTest(t *testing.T) { err = db. Session(&gorm.Session{NewDB: true}). Model(&Questionnaires{}). - Where("deleted_at IS NULL"). + Where("deleted_at IS NULL AND is_published IS TRUE"). Count(&questionnaireNum).Error if err != nil { t.Errorf("failed to count questionnaire(%s): %v", testCase.description, err) @@ -1080,11 +1402,16 @@ func getQuestionnairesTest(t *testing.T) { for _, questionnaire := range questionnaires { actualQuestionnaireIDs = append(actualQuestionnaireIDs, questionnaire.ID) } - if testCase.args.nontargeted { + if testCase.args.onlyTargetingMe { for _, targettedQuestionnaireID := range userTargetMap[questionnairesTestUserID] { assertion.NotContains(actualQuestionnaireIDs, targettedQuestionnaireID, testCase.description, "not contain(targetted)") } } + if testCase.args.onlyAdministratedByMe { + for _, targettedQuestionnaireID := range userTargetMap[questionnairesTestUserID] { + assertion.NotContains(actualQuestionnaireIDs, targettedQuestionnaireID, testCase.description, "not contain(administrated)") + } + } for _, deletedQuestionnaireID := range deletedQuestionnaireIDs { assertion.NotContains(actualQuestionnaireIDs, deletedQuestionnaireID, testCase.description, "not contain(deleted)") } @@ -1093,7 +1420,7 @@ func getQuestionnairesTest(t *testing.T) { assertion.Regexp(testCase.args.search, questionnaire.Title, testCase.description, "regexp") } - if len(testCase.args.search) == 0 && !testCase.args.nontargeted { + if len(testCase.args.search) == 0 && !testCase.args.onlyTargetingMe && !testCase.args.onlyAdministratedByMe { fmt.Println(testCase.description) fmt.Println(questionnaireNum) fmt.Println(pageMax) @@ -1324,7 +1651,10 @@ func getQuestionnaireInfoTest(t *testing.T) { for _, testCase := range testCases { ctx := context.Background() - actualQuestionnaire, actualTargets, actualAdministrators, actualRespondents, err := questionnaireImpl.GetQuestionnaireInfo(ctx, testCase.questionnaireID) + actualQuestionnaire, actualTargets, actualTargetGroups, actualAdministrators, actualAdministratorGroups, actualRespondents, err := questionnaireImpl.GetQuestionnaireInfo(ctx, testCase.questionnaireID) + + _ = actualTargetGroups + _ = actualAdministratorGroups if !testCase.expect.isErr { assertion.NoError(err, testCase.description, "no error") @@ -1341,6 +1671,8 @@ func getQuestionnaireInfoTest(t *testing.T) { assertion.Equal(testCase.expect.questionnaire.Title, actualQuestionnaire.Title, testCase.description, "questionnaire(Title)") assertion.Equal(testCase.expect.questionnaire.Description, actualQuestionnaire.Description, testCase.description, "questionnaire(Description)") assertion.Equal(testCase.expect.questionnaire.ResSharedTo, actualQuestionnaire.ResSharedTo, testCase.description, "questionnaire(ResSharedTo)") + assertion.Equal(testCase.expect.questionnaire.IsPublished, actualQuestionnaire.IsPublished, testCase.description, "questionnaire(IsPublished)") + assertion.Equal(testCase.expect.questionnaire.IsDuplicateAnswerAllowed, actualQuestionnaire.IsDuplicateAnswerAllowed, testCase.description, "questionnaire(IsDuplicateAnswerAllowed)") assertion.WithinDuration(testCase.expect.questionnaire.ResTimeLimit.ValueOrZero(), actualQuestionnaire.ResTimeLimit.ValueOrZero(), 2*time.Second, testCase.description, "questionnaire(ResTimeLimit)") assertion.WithinDuration(testCase.expect.questionnaire.CreatedAt, actualQuestionnaire.CreatedAt, 2*time.Second, testCase.description, "questionnaire(CreatedAt)") assertion.WithinDuration(testCase.expect.questionnaire.ModifiedAt, actualQuestionnaire.ModifiedAt, 2*time.Second, testCase.description, "questionnaire(ModifiedAt)") diff --git a/model/questions_impl.go b/model/questions_impl.go index 0ad5335a..6b48077e 100644 --- a/model/questions_impl.go +++ b/model/questions_impl.go @@ -17,7 +17,7 @@ func NewQuestion() *Question { return new(Question) } -//Questions questionテーブルの構造体 +// Questions questionテーブルの構造体 type Questions struct { ID int `json:"id" gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` QuestionnaireID int `json:"questionnaireID" gorm:"type:int(11);not null"` @@ -41,18 +41,18 @@ func (questionnaire *Questions) BeforeCreate(tx *gorm.DB) error { return nil } -//TableName テーブル名が単数形なのでその対応 +// TableName テーブル名が単数形なのでその対応 func (*Questions) TableName() string { return "question" } -//QuestionIDType 質問のIDと種類の構造体 +// QuestionIDType 質問のIDと種類の構造体 type QuestionIDType struct { ID int Type string } -//InsertQuestion 質問の追加 +// InsertQuestion 質問の追加 func (*Question) InsertQuestion(ctx context.Context, questionnaireID int, pageNum int, questionNum int, questionType string, body string, isRequired bool) (int, error) { db, err := getTx(ctx) if err != nil { @@ -77,7 +77,7 @@ func (*Question) InsertQuestion(ctx context.Context, questionnaireID int, pageNu return question.ID, nil } -//UpdateQuestion 質問の修正 +// UpdateQuestion 質問の修正 func (*Question) UpdateQuestion(ctx context.Context, questionnaireID int, pageNum int, questionNum int, questionType string, body string, isRequired bool, questionID int) error { db, err := getTx(ctx) if err != nil { @@ -104,7 +104,7 @@ func (*Question) UpdateQuestion(ctx context.Context, questionnaireID int, pageNu return nil } -//DeleteQuestion 質問の削除 +// DeleteQuestion 質問の削除 func (*Question) DeleteQuestion(ctx context.Context, questionID int) error { db, err := getTx(ctx) if err != nil { @@ -125,7 +125,7 @@ func (*Question) DeleteQuestion(ctx context.Context, questionID int) error { return nil } -//GetQuestions 質問一覧の取得 +// GetQuestions 質問一覧の取得 func (*Question) GetQuestions(ctx context.Context, questionnaireID int) ([]Questions, error) { db, err := getTx(ctx) if err != nil { diff --git a/model/respondents.go b/model/respondents.go index 67504296..e5cff356 100644 --- a/model/respondents.go +++ b/model/respondents.go @@ -16,7 +16,8 @@ type IRespondent interface { GetRespondent(ctx context.Context, responseID int) (*Respondents, error) GetRespondentInfos(ctx context.Context, userID string, questionnaireIDs ...int) ([]RespondentInfo, error) GetRespondentDetail(ctx context.Context, responseID int) (RespondentDetail, error) - GetRespondentDetails(ctx context.Context, questionnaireID int, sort string) ([]RespondentDetail, error) + GetRespondentDetails(ctx context.Context, questionnaireID int, sort string, onlyMyResponse bool, userID string) ([]RespondentDetail, error) GetRespondentsUserIDs(ctx context.Context, questionnaireIDs []int) ([]Respondents, error) + GetMyResponseIDs(ctx context.Context, sort string, userID string) ([]int, error) CheckRespondent(ctx context.Context, userID string, questionnaireID int) (bool, error) } diff --git a/model/respondents_impl.go b/model/respondents_impl.go index 29019230..456e3488 100755 --- a/model/respondents_impl.go +++ b/model/respondents_impl.go @@ -72,7 +72,19 @@ func (*Respondent) InsertRespondent(ctx context.Context, userID string, question return 0, fmt.Errorf("failed to get tx: %w", err) } + var questionnaire Questionnaires var respondent Respondents + + err = db. + Where("id = ?", questionnaireID). + First(&questionnaire).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, ErrRecordNotFound + } + if err != nil { + return 0, fmt.Errorf("failed to get questionnaire: %w", err) + } + if submittedAt.Valid { respondent = Respondents{ QuestionnaireID: questionnaireID, @@ -86,12 +98,26 @@ func (*Respondent) InsertRespondent(ctx context.Context, userID string, question } } + if !questionnaire.IsDuplicateAnswerAllowed { + err = db. + Where("questionnaire_id = ? AND user_traqid = ?", questionnaireID, userID). + First(&Respondents{}).Error + if err == nil { + return 0, ErrDuplicatedAnswered + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return 0, fmt.Errorf("failed to check duplicate answer: %w", err) + } + + } + err = db.Create(&respondent).Error if err != nil { return 0, fmt.Errorf("failed to insert a respondent record: %w", err) } return respondent.ResponseID, nil + } // UpdateSubmittedAt 投稿日時更新 @@ -257,7 +283,7 @@ func (*Respondent) GetRespondentDetail(ctx context.Context, responseID int) (Res } // GetRespondentDetails アンケートの回答の詳細情報一覧の取得 -func (*Respondent) GetRespondentDetails(ctx context.Context, questionnaireID int, sort string) ([]RespondentDetail, error) { +func (*Respondent) GetRespondentDetails(ctx context.Context, questionnaireID int, sort string, onlyMyResponse bool, userID string) ([]RespondentDetail, error) { db, err := getTx(ctx) if err != nil { return nil, fmt.Errorf("failed to get tx: %w", err) @@ -291,22 +317,31 @@ 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] } questions := []Questions{} - err = db. + query = db. Preload("Responses", func(db *gorm.DB) *gorm.DB { return db. Select("ResponseID", "QuestionID", "Body"). @@ -314,7 +349,11 @@ func (*Respondent) GetRespondentDetails(ctx context.Context, questionnaireID int }). Where("questionnaire_id = ?", questionnaireID). Order("question_num"). - Select("ID", "Type"). + Select("ID", "Type") + if onlyMyResponse { + query = query.Where("user_traqid = ?", userID) + } + err = query. Find(&questions).Error if err != nil { return nil, fmt.Errorf("failed to get questions: %w", err) @@ -382,6 +421,31 @@ func (*Respondent) GetRespondentsUserIDs(ctx context.Context, questionnaireIDs [ return respondents, nil } +// GetMyResponses 自分のすべての回答を取得 +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{} + query := db.Model(&Respondents{}). + Where("user_traqid = ?", userID). + 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) + } + + return responsesID, nil +} + // CheckRespondent 回答者かどうかの確認 func (*Respondent) CheckRespondent(ctx context.Context, userID string, questionnaireID int) (bool, error) { db, err := getTx(ctx) diff --git a/model/respondents_test.go b/model/respondents_test.go index cafc7dd7..2805fc3f 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false, true) 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false, true) 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false, true) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) @@ -290,6 +290,7 @@ func TestGetRespondent(t *testing.T) { Description: "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", ResTimeLimit: null.NewTime(time.Now(), false), ResSharedTo: "private", + IsPublished: true, } err := db. Session(&gorm.Session{NewDB: true}). @@ -390,9 +391,9 @@ func TestGetRespondentInfos(t *testing.T) { args expect } - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第2回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第2回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) require.NoError(t, err) - questionnaireID2, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第2回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public") + questionnaireID2, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第2回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) require.NoError(t, err) questionnaire := Questionnaires{} @@ -522,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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false, true) require.NoError(t, err) questionnaire := Questionnaires{} @@ -619,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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false, true) require.NoError(t, err) questionnaire := Questionnaires{} @@ -636,6 +637,8 @@ func TestGetRespondentDetails(t *testing.T) { type args struct { questionnaireID int sort string + onlyMyResponse bool + userID string } type expect struct { isErr bool @@ -682,7 +685,6 @@ func TestGetRespondentDetails(t *testing.T) { questionID, err := questionImpl.InsertQuestion(ctx, question.QuestionnaireID, question.PageNum, question.QuestionNum, question.Type, question.Body, question.IsRequired) require.NoError(t, err) questionIDs = append(questionIDs, questionID) - } respondents := []Respondents{ @@ -749,6 +751,150 @@ func TestGetRespondentDetails(t *testing.T) { args: args{ questionnaireID: questionnaireID, sort: "traqid", + onlyMyResponse: false, + userID: userOne, + }, + expect: expect{ + length: 3, + sortIdx: []int{0, 1, 2}, + }, + }, + { + description: "-traqid", + args: args{ + questionnaireID: questionnaireID, + sort: "-traqid", + onlyMyResponse: false, + userID: userOne, + }, + expect: expect{ + length: 3, + sortIdx: []int{2, 1, 0}, + }, + }, + { + description: "submitted_at", + args: args{ + questionnaireID: questionnaireID, + sort: "submitted_at", + onlyMyResponse: false, + userID: userOne, + }, + expect: expect{ + length: 3, + sortIdx: []int{0, 2, 1}, + }, + }, + { + description: "-submitted_at", + args: args{ + questionnaireID: questionnaireID, + sort: "-submitted_at", + onlyMyResponse: false, + userID: userOne, + }, + expect: expect{ + length: 3, + sortIdx: []int{1, 2, 0}, + }, + }, + { + description: "questionnaire does not exist", + args: args{ + questionnaireID: -1, + sort: "1", + onlyMyResponse: false, + userID: userOne, + }, + expect: expect{ + length: 0, + sortIdx: []int{}, + }, + }, + { + description: "sortNum Number", + args: args{ + questionnaireID: questionnaireID, + sort: "3", + onlyMyResponse: false, + userID: userOne, + }, + expect: expect{ + length: 3, + sortIdx: []int{2, 1, 0}, + }, + }, + { + description: "sortNum Number", + args: args{ + questionnaireID: questionnaireID, + sort: "-3", + onlyMyResponse: false, + userID: userOne, + }, + expect: expect{ + length: 3, + sortIdx: []int{0, 1, 2}, + }, + }, + { + description: "sortNum Text", + args: args{ + questionnaireID: questionnaireID, + sort: "1", + onlyMyResponse: false, + userID: userOne, + }, + expect: expect{ + length: 3, + sortIdx: []int{0, 1, 2}, + }, + }, + { + description: "sortNum Text desc", + args: args{ + questionnaireID: questionnaireID, + sort: "-1", + onlyMyResponse: false, + userID: userOne, + }, + expect: expect{ + length: 3, + sortIdx: []int{2, 1, 0}, + }, + }, + { + description: "invalid sortnum", + args: args{ + questionnaireID: questionnaireID, + sort: "a", + onlyMyResponse: false, + userID: userOne, + }, + expect: expect{ + isErr: true, + }, + }, + { + description: "empty sortnum", + args: args{ + questionnaireID: questionnaireID, + sort: "", + onlyMyResponse: false, + userID: userOne, + }, + expect: expect{ + length: 3, + sortIdx: []int{0, 1, 2}, + }, + }, + { + description: "traqid", + args: args{ + questionnaireID: questionnaireID, + sort: "traqid", + onlyMyResponse: true, + userID: userOne, }, expect: expect{ length: 3, @@ -760,6 +906,8 @@ func TestGetRespondentDetails(t *testing.T) { args: args{ questionnaireID: questionnaireID, sort: "-traqid", + onlyMyResponse: true, + userID: userOne, }, expect: expect{ length: 3, @@ -771,6 +919,8 @@ func TestGetRespondentDetails(t *testing.T) { args: args{ questionnaireID: questionnaireID, sort: "submitted_at", + onlyMyResponse: true, + userID: userOne, }, expect: expect{ length: 3, @@ -782,6 +932,8 @@ func TestGetRespondentDetails(t *testing.T) { args: args{ questionnaireID: questionnaireID, sort: "-submitted_at", + onlyMyResponse: true, + userID: userOne, }, expect: expect{ length: 3, @@ -793,6 +945,8 @@ func TestGetRespondentDetails(t *testing.T) { args: args{ questionnaireID: -1, sort: "1", + onlyMyResponse: true, + userID: userOne, }, expect: expect{ length: 0, @@ -804,6 +958,8 @@ func TestGetRespondentDetails(t *testing.T) { args: args{ questionnaireID: questionnaireID, sort: "3", + onlyMyResponse: true, + userID: userOne, }, expect: expect{ length: 3, @@ -815,6 +971,8 @@ func TestGetRespondentDetails(t *testing.T) { args: args{ questionnaireID: questionnaireID, sort: "-3", + onlyMyResponse: true, + userID: userOne, }, expect: expect{ length: 3, @@ -826,6 +984,8 @@ func TestGetRespondentDetails(t *testing.T) { args: args{ questionnaireID: questionnaireID, sort: "1", + onlyMyResponse: true, + userID: userOne, }, expect: expect{ length: 3, @@ -837,6 +997,8 @@ func TestGetRespondentDetails(t *testing.T) { args: args{ questionnaireID: questionnaireID, sort: "-1", + onlyMyResponse: true, + userID: userOne, }, expect: expect{ length: 3, @@ -848,6 +1010,8 @@ func TestGetRespondentDetails(t *testing.T) { args: args{ questionnaireID: questionnaireID, sort: "a", + onlyMyResponse: true, + userID: userOne, }, expect: expect{ isErr: true, @@ -858,6 +1022,8 @@ func TestGetRespondentDetails(t *testing.T) { args: args{ questionnaireID: questionnaireID, sort: "", + onlyMyResponse: true, + userID: userOne, }, expect: expect{ length: 3, @@ -867,7 +1033,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, testCase.args.onlyMyResponse, testCase.args.userID) if !testCase.expect.isErr { assertion.NoError(err, testCase.description, "no error") } else if testCase.expect.err != nil { @@ -908,7 +1074,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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) require.NoError(t, err) questionnaireIDs = append(questionnaireIDs, questionnaireID) } @@ -990,13 +1156,112 @@ func TestGetRespondentsUserIDs(t *testing.T) { } } +func TestGetMyResponseIDs(t *testing.T) { + t.Parallel() + + assertion := assert.New(t) + ctx := context.Background() + + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false, true) + require.NoError(t, err) + + respondents := []Respondents{ + { + QuestionnaireID: questionnaireID, + UserTraqid: userOne, + SubmittedAt: null.NewTime(time.Now(), true), + }, + { + QuestionnaireID: questionnaireID, + UserTraqid: userTwo, + SubmittedAt: null.NewTime(time.Now(), true), + }, + { + QuestionnaireID: questionnaireID, + UserTraqid: userTwo, + SubmittedAt: null.NewTime(time.Now(), true), + }, + } + responseIDs := []int{} + for _, respondent := range respondents { + responseID, err := respondentImpl.InsertRespondent(ctx, respondent.UserTraqid, questionnaireID, respondent.SubmittedAt) + require.NoError(t, err) + responseIDs = append(responseIDs, responseID) + } + + type args struct { + sort string + userID string + } + type expect struct { + isErr bool + err error + responseIDs []int + } + type test struct { + description string + args + expect + } + + testCases := []test{ + { + description: "valid user with one resonse", + args: args{ + sort: "submitted_at", + userID: userOne, + }, + expect: expect{ + responseIDs: []int{responseIDs[1]}, + }, + }, + { + description: "valid user with multiple responses", + args: args{ + sort: "submitted_at", + userID: userTwo, + }, + expect: expect{ + responseIDs: []int{responseIDs[2], responseIDs[3]}, + }, + }, + { + description: "valid user with no response", + args: args{ + sort: "submitted_at", + userID: userThree, + }, + expect: expect{ + responseIDs: []int{}, + }, + }, + } + + for _, testCase := range testCases { + MyResponseIDs, err := respondentImpl.GetMyResponseIDs(ctx, testCase.args.sort, testCase.args.userID) + + if !testCase.expect.isErr { + assertion.NoError(err, testCase.description, "no error") + } else if testCase.expect.err != nil { + assertion.Equal(true, errors.Is(err, testCase.expect.err), testCase.description, "errorIs") + } else { + assertion.Error(err, testCase.description, "any error") + } + if err != nil { + continue + } + + assertion.Equal(testCase.expect.responseIDs, MyResponseIDs, testCase.description, "responseIDs") + } +} + func TestTestCheckRespondent(t *testing.T) { t.Parallel() assertion := assert.New(t) ctx := context.Background() - questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false, true) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) diff --git a/model/responses_test.go b/model/responses_test.go index 9790b350..37b0a676 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) 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 f4f79ccd..780db5d9 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) diff --git a/model/targetGroups.go b/model/targetGroups.go new file mode 100644 index 00000000..7217c148 --- /dev/null +++ b/model/targetGroups.go @@ -0,0 +1,12 @@ +package model + +import ( + "context" +) + +// ITargetGroup TargetGroupのRepository +type ITargetGroup interface { + InsertTargetGroups(ctx context.Context, questionnaireID int, groupID []string) error + GetTargetGroups(ctx context.Context, questionnaireIDs []int) ([]TargetGroups, error) + DeleteTargetGroups(ctx context.Context, questionnaireIDs int) error +} diff --git a/model/targetGroups_impl.go b/model/targetGroups_impl.go new file mode 100644 index 00000000..ea04f23d --- /dev/null +++ b/model/targetGroups_impl.go @@ -0,0 +1,84 @@ +package model + +import ( + "context" + "fmt" + + "github.com/gofrs/uuid" +) + +// TargetGroup TargetGroupsRepositoryの実装 +type TargetGroup struct{} + +// NewTargetGroups TargetGroupsのコンストラクター +func NewTargetGroups() *TargetGroups { + return new(TargetGroups) +} + +// TargetGroups targets_groupsテーブルの構造体 +type TargetGroups struct { + QuestionnaireID int `gorm:"type:int(11) AUTO_INCREMENT;not null;primaryKey"` + GroupID uuid.UUID `gorm:"type:char(36);size:36;not null;primaryKey"` +} + +// InsertTargetGroups アンケートの対象としてuser_groupを追加 +func (*TargetGroup) InsertTargetGroups(ctx context.Context, questionnaireID int, groupID []uuid.UUID) error { + db, err := getTx(ctx) + if err != nil { + return fmt.Errorf("failed to get transaction: %w", err) + } + + if len(groupID) == 0 { + return nil + } + + dbTargetGroups := make([]TargetGroups, 0, len(groupID)) + for _, targetGroup := range groupID { + dbTargetGroups = append(dbTargetGroups, TargetGroups{ + QuestionnaireID: questionnaireID, + GroupID: targetGroup, + }) + } + + err = db.Create(&dbTargetGroups).Error + if err != nil { + return fmt.Errorf("failed to insert target groups: %w", err) + } + + return nil +} + +// GetTargetGroups アンケートの対象としてuser_groupを取得 +func (*TargetGroup) GetTargetGroups(ctx context.Context, questionnaireIDs []int) ([]TargetGroups, error) { + db, err := getTx(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get transaction: %w", err) + } + + var targetGroups []TargetGroups + err = db. + Where("questionnaire_id IN ?", questionnaireIDs). + Find(&targetGroups).Error + if err != nil { + return nil, fmt.Errorf("failed to get target groups: %w", err) + } + + return targetGroups, nil +} + +// DeleteTargetGroups アンケートの対象としてuser_groupを削除 +func (*TargetGroup) DeleteTargetGroups(ctx context.Context, questionnaireID int) error { + db, err := getTx(ctx) + if err != nil { + return fmt.Errorf("failed to get transaction: %w", err) + } + + err = db. + Where("questionnaire_id = ?", questionnaireID). + Delete(&TargetGroups{}).Error + if err != nil { + return fmt.Errorf("failed to delete target groups: %w", err) + } + + return nil +} diff --git a/model/targets.go b/model/targets.go index e6557b12..e6d4cc17 100644 --- a/model/targets.go +++ b/model/targets.go @@ -9,4 +9,6 @@ type ITarget interface { InsertTargets(ctx context.Context, questionnaireID int, targets []string) error 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 c3d73dd8..65196014 100644 --- a/model/targets_impl.go +++ b/model/targets_impl.go @@ -13,10 +13,11 @@ func NewTarget() *Target { return new(Target) } -//Targets targetsテーブルの構造体 +// Targets targetsテーブルの構造体 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, }) } @@ -80,3 +82,42 @@ func (*Target) GetTargets(ctx context.Context, questionnaireIDs []int) ([]Target return targets, nil } + +func (*Target) IsTargetingMe(ctx context.Context, questionnairID int, userID string) (bool, error) { + db, err := getTx(ctx) + if err != nil { + return false, fmt.Errorf("failed to get transaction: %w", err) + } + + var count int64 + err = db. + Model(&Targets{}). + Where("questionnaire_id = ? AND user_traqid = ?", questionnairID, userID). + Count(&count).Error + if err != nil { + return false, fmt.Errorf("failed to get targets which are targeting me: %w", err) + } + + if count > 0 { + return true, nil + } + 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 a00d7487..6028d1fb 100644 --- a/model/targets_test.go +++ b/model/targets_test.go @@ -4,8 +4,11 @@ import ( "context" "errors" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/guregu/null.v4" "gorm.io/gorm" ) @@ -308,3 +311,184 @@ func TestGetTargets(t *testing.T) { }) } } + +func TestIsTargetingMe(t *testing.T) { + t.Parallel() + + assertion := assert.New(t) + ctx := context.Background() + + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "private", true, false, true) + require.NoError(t, err) + + err = targetImpl.InsertTargets(ctx, questionnaireID, []string{userOne}) + require.NoError(t, err) + + type args struct { + userID string + } + type expect struct { + isErr bool + err error + isTargeted bool + } + type test struct { + description string + args + expect + } + + testCases := []test{ + { + description: "is targeted", + args: args{ + userID: userOne, + }, + expect: expect{ + isTargeted: true, + }, + }, + { + description: "not targeted", + args: args{ + userID: userTwo, + }, + expect: expect{ + isTargeted: false, + }, + }, + } + + for _, testCase := range testCases { + isTargeted, err := targetImpl.IsTargetingMe(ctx, questionnaireID, testCase.args.userID) + + if !testCase.expect.isErr { + assertion.NoError(err, testCase.description, "no error") + } else if testCase.expect.err != nil { + assertion.Equal(true, errors.Is(err, testCase.expect.err), testCase.description, "errorIs") + } else { + assertion.Error(err, testCase.description, "any error") + } + if err != nil { + continue + } + + 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 new file mode 100644 index 00000000..b28160e9 --- /dev/null +++ b/model/v3.go @@ -0,0 +1,57 @@ +package model + +import ( + "time" + + "github.com/go-gormigrate/gormigrate/v2" + "gopkg.in/guregu/null.v4" + "gorm.io/gorm" +) + +func v3() *gormigrate.Migration { + return &gormigrate.Migration{ + ID: "3", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&v3Targets{}); err != nil { + return err + } + if err := tx.AutoMigrate(&v3Questionnaires{}); err != nil { + return err + } + return nil + }, + } +} + +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"` + Description string `json:"description" gorm:"type:text;not null"` + 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"` + 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"` + IsAnonymous bool `json:"is_anonymous" gorm:"type:boolean;not null;default:false"` + IsDuplicateAnswerAllowed bool `json:"is_duplicate_answer_allowed" gorm:"type:tinyint(4);size:4;not null;default:0"` +} + +func (*v3Questionnaires) TableName() string { + return "questionnaires" +} diff --git a/model/validations_test.go b/model/validations_test.go index a56434a3..dd84b715 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) 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") + questionnaireID, err := questionnaireImpl.InsertQuestionnaire(ctx, "第1回集会らん☆ぷろ募集アンケート", "第1回メンバー集会でのらん☆ぷろで発表したい人を募集します らん☆ぷろで発表したい人あつまれー!", null.NewTime(time.Now(), false), "public", true, false, true) require.NoError(t, err) err = administratorImpl.InsertAdministrators(ctx, questionnaireID, []string{userOne}) diff --git a/openapi/server.go b/openapi/server.go new file mode 100644 index 00000000..323878be --- /dev/null +++ b/openapi/server.go @@ -0,0 +1,379 @@ +// Package openapi provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. +package openapi + +import ( + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/oapi-codegen/runtime" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + + // (GET /questionnaires) + GetQuestionnaires(ctx echo.Context, params GetQuestionnairesParams) error + + // (POST /questionnaires) + PostQuestionnaire(ctx echo.Context) error + + // (DELETE /questionnaires/{questionnaireID}) + DeleteQuestionnaire(ctx echo.Context, questionnaireID QuestionnaireIDInPath) error + + // (GET /questionnaires/{questionnaireID}) + GetQuestionnaire(ctx echo.Context, questionnaireID QuestionnaireIDInPath) error + + // (PATCH /questionnaires/{questionnaireID}) + EditQuestionnaire(ctx echo.Context, questionnaireID QuestionnaireIDInPath) error + + // (GET /questionnaires/{questionnaireID}/myRemindStatus) + GetQuestionnaireMyRemindStatus(ctx echo.Context, questionnaireID QuestionnaireIDInPath) error + + // (PATCH /questionnaires/{questionnaireID}/myRemindStatus) + EditQuestionnaireMyRemindStatus(ctx echo.Context, questionnaireID QuestionnaireIDInPath) error + + // (GET /questionnaires/{questionnaireID}/responses) + GetQuestionnaireResponses(ctx echo.Context, questionnaireID QuestionnaireIDInPath, params GetQuestionnaireResponsesParams) error + + // (POST /questionnaires/{questionnaireID}/responses) + PostQuestionnaireResponse(ctx echo.Context, questionnaireID QuestionnaireIDInPath) error + + // (GET /responses/myResponses) + GetMyResponses(ctx echo.Context, params GetMyResponsesParams) error + + // (DELETE /responses/{responseID}) + DeleteResponse(ctx echo.Context, responseID ResponseIDInPath) error + + // (GET /responses/{responseID}) + GetResponse(ctx echo.Context, responseID ResponseIDInPath) error + + // (PATCH /responses/{responseID}) + EditResponse(ctx echo.Context, responseID ResponseIDInPath) error +} + +// ServerInterfaceWrapper converts echo contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +// GetQuestionnaires converts echo context to params. +func (w *ServerInterfaceWrapper) GetQuestionnaires(ctx echo.Context) error { + var err error + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetQuestionnairesParams + // ------------- Optional query parameter "sort" ------------- + + err = runtime.BindQueryParameter("form", true, false, "sort", ctx.QueryParams(), ¶ms.Sort) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter sort: %s", err)) + } + + // ------------- Optional query parameter "search" ------------- + + err = runtime.BindQueryParameter("form", true, false, "search", ctx.QueryParams(), ¶ms.Search) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter search: %s", err)) + } + + // ------------- Optional query parameter "page" ------------- + + err = runtime.BindQueryParameter("form", true, false, "page", ctx.QueryParams(), ¶ms.Page) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter page: %s", err)) + } + + // ------------- Optional query parameter "onlyTargetingMe" ------------- + + err = runtime.BindQueryParameter("form", true, false, "onlyTargetingMe", ctx.QueryParams(), ¶ms.OnlyTargetingMe) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter onlyTargetingMe: %s", err)) + } + + // ------------- Optional query parameter "onlyAdministratedByMe" ------------- + + err = runtime.BindQueryParameter("form", true, false, "onlyAdministratedByMe", ctx.QueryParams(), ¶ms.OnlyAdministratedByMe) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter onlyAdministratedByMe: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetQuestionnaires(ctx, params) + return err +} + +// PostQuestionnaire converts echo context to params. +func (w *ServerInterfaceWrapper) PostQuestionnaire(ctx echo.Context) error { + var err error + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.PostQuestionnaire(ctx) + return err +} + +// DeleteQuestionnaire converts echo context to params. +func (w *ServerInterfaceWrapper) DeleteQuestionnaire(ctx echo.Context) error { + var err error + // ------------- Path parameter "questionnaireID" ------------- + var questionnaireID QuestionnaireIDInPath + + err = runtime.BindStyledParameterWithOptions("simple", "questionnaireID", ctx.Param("questionnaireID"), &questionnaireID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter questionnaireID: %s", err)) + } + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.DeleteQuestionnaire(ctx, questionnaireID) + return err +} + +// GetQuestionnaire converts echo context to params. +func (w *ServerInterfaceWrapper) GetQuestionnaire(ctx echo.Context) error { + var err error + // ------------- Path parameter "questionnaireID" ------------- + var questionnaireID QuestionnaireIDInPath + + err = runtime.BindStyledParameterWithOptions("simple", "questionnaireID", ctx.Param("questionnaireID"), &questionnaireID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter questionnaireID: %s", err)) + } + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetQuestionnaire(ctx, questionnaireID) + return err +} + +// EditQuestionnaire converts echo context to params. +func (w *ServerInterfaceWrapper) EditQuestionnaire(ctx echo.Context) error { + var err error + // ------------- Path parameter "questionnaireID" ------------- + var questionnaireID QuestionnaireIDInPath + + err = runtime.BindStyledParameterWithOptions("simple", "questionnaireID", ctx.Param("questionnaireID"), &questionnaireID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter questionnaireID: %s", err)) + } + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.EditQuestionnaire(ctx, questionnaireID) + return err +} + +// GetQuestionnaireMyRemindStatus converts echo context to params. +func (w *ServerInterfaceWrapper) GetQuestionnaireMyRemindStatus(ctx echo.Context) error { + var err error + // ------------- Path parameter "questionnaireID" ------------- + var questionnaireID QuestionnaireIDInPath + + err = runtime.BindStyledParameterWithOptions("simple", "questionnaireID", ctx.Param("questionnaireID"), &questionnaireID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter questionnaireID: %s", err)) + } + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetQuestionnaireMyRemindStatus(ctx, questionnaireID) + return err +} + +// EditQuestionnaireMyRemindStatus converts echo context to params. +func (w *ServerInterfaceWrapper) EditQuestionnaireMyRemindStatus(ctx echo.Context) error { + var err error + // ------------- Path parameter "questionnaireID" ------------- + var questionnaireID QuestionnaireIDInPath + + err = runtime.BindStyledParameterWithOptions("simple", "questionnaireID", ctx.Param("questionnaireID"), &questionnaireID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter questionnaireID: %s", err)) + } + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.EditQuestionnaireMyRemindStatus(ctx, questionnaireID) + return err +} + +// GetQuestionnaireResponses converts echo context to params. +func (w *ServerInterfaceWrapper) GetQuestionnaireResponses(ctx echo.Context) error { + var err error + // ------------- Path parameter "questionnaireID" ------------- + var questionnaireID QuestionnaireIDInPath + + err = runtime.BindStyledParameterWithOptions("simple", "questionnaireID", ctx.Param("questionnaireID"), &questionnaireID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter questionnaireID: %s", err)) + } + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetQuestionnaireResponsesParams + // ------------- Optional query parameter "sort" ------------- + + err = runtime.BindQueryParameter("form", true, false, "sort", ctx.QueryParams(), ¶ms.Sort) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter sort: %s", err)) + } + + // ------------- Optional query parameter "onlyMyResponse" ------------- + + err = runtime.BindQueryParameter("form", true, false, "onlyMyResponse", ctx.QueryParams(), ¶ms.OnlyMyResponse) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter onlyMyResponse: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetQuestionnaireResponses(ctx, questionnaireID, params) + return err +} + +// PostQuestionnaireResponse converts echo context to params. +func (w *ServerInterfaceWrapper) PostQuestionnaireResponse(ctx echo.Context) error { + var err error + // ------------- Path parameter "questionnaireID" ------------- + var questionnaireID QuestionnaireIDInPath + + err = runtime.BindStyledParameterWithOptions("simple", "questionnaireID", ctx.Param("questionnaireID"), &questionnaireID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter questionnaireID: %s", err)) + } + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.PostQuestionnaireResponse(ctx, questionnaireID) + return err +} + +// GetMyResponses converts echo context to params. +func (w *ServerInterfaceWrapper) GetMyResponses(ctx echo.Context) error { + var err error + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetMyResponsesParams + // ------------- Optional query parameter "sort" ------------- + + err = runtime.BindQueryParameter("form", true, false, "sort", ctx.QueryParams(), ¶ms.Sort) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter sort: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetMyResponses(ctx, params) + return err +} + +// DeleteResponse converts echo context to params. +func (w *ServerInterfaceWrapper) DeleteResponse(ctx echo.Context) error { + var err error + // ------------- Path parameter "responseID" ------------- + var responseID ResponseIDInPath + + err = runtime.BindStyledParameterWithOptions("simple", "responseID", ctx.Param("responseID"), &responseID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter responseID: %s", err)) + } + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.DeleteResponse(ctx, responseID) + return err +} + +// GetResponse converts echo context to params. +func (w *ServerInterfaceWrapper) GetResponse(ctx echo.Context) error { + var err error + // ------------- Path parameter "responseID" ------------- + var responseID ResponseIDInPath + + err = runtime.BindStyledParameterWithOptions("simple", "responseID", ctx.Param("responseID"), &responseID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter responseID: %s", err)) + } + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetResponse(ctx, responseID) + return err +} + +// EditResponse converts echo context to params. +func (w *ServerInterfaceWrapper) EditResponse(ctx echo.Context) error { + var err error + // ------------- Path parameter "responseID" ------------- + var responseID ResponseIDInPath + + err = runtime.BindStyledParameterWithOptions("simple", "responseID", ctx.Param("responseID"), &responseID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter responseID: %s", err)) + } + + ctx.Set(ApplicationScopes, []string{"read", "write"}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.EditResponse(ctx, responseID) + return err +} + +// This is a simple interface which specifies echo.Route addition functions which +// are present on both echo.Echo and echo.Group, since we want to allow using +// either of them for path registration +type EchoRouter interface { + CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route + TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route +} + +// RegisterHandlers adds each server route to the EchoRouter. +func RegisterHandlers(router EchoRouter, si ServerInterface) { + RegisterHandlersWithBaseURL(router, si, "") +} + +// Registers handlers, and prepends BaseURL to the paths, so that the paths +// can be served under a prefix. +func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { + + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + router.GET(baseURL+"/questionnaires", wrapper.GetQuestionnaires) + router.POST(baseURL+"/questionnaires", wrapper.PostQuestionnaire) + router.DELETE(baseURL+"/questionnaires/:questionnaireID", wrapper.DeleteQuestionnaire) + router.GET(baseURL+"/questionnaires/:questionnaireID", wrapper.GetQuestionnaire) + router.PATCH(baseURL+"/questionnaires/:questionnaireID", wrapper.EditQuestionnaire) + router.GET(baseURL+"/questionnaires/:questionnaireID/myRemindStatus", wrapper.GetQuestionnaireMyRemindStatus) + router.PATCH(baseURL+"/questionnaires/:questionnaireID/myRemindStatus", wrapper.EditQuestionnaireMyRemindStatus) + router.GET(baseURL+"/questionnaires/:questionnaireID/responses", wrapper.GetQuestionnaireResponses) + router.POST(baseURL+"/questionnaires/:questionnaireID/responses", wrapper.PostQuestionnaireResponse) + router.GET(baseURL+"/responses/myResponses", wrapper.GetMyResponses) + router.DELETE(baseURL+"/responses/:responseID", wrapper.DeleteResponse) + router.GET(baseURL+"/responses/:responseID", wrapper.GetResponse) + router.PATCH(baseURL+"/responses/:responseID", wrapper.EditResponse) + +} diff --git a/openapi/spec.go b/openapi/spec.go new file mode 100644 index 00000000..a18514d4 --- /dev/null +++ b/openapi/spec.go @@ -0,0 +1,162 @@ +// Package openapi provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +package openapi + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "net/url" + "path" + "strings" + + "github.com/getkin/kin-openapi/openapi3" +) + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/+xcb3PTSJP/Ki7dvYA6BZvAU/Wc3wWy9VSqlueAwN0LknIp9iTRPrZkJBnwUanKyPwJ", + "xJBUgGRD2ISwWQhkSdiF47L8/S43kWO/uq9wNaN/I2lkSY4Nu1tXRVGxND3T0/2b7p6eHl3l8nKpLEtA", + "0lQue5UrC4pQAhpQyC9ZKlYHCiVRElVNETRQOFE9BYakMxWgVPH7AlDziljWRFnislzz5gtj5gaC9f3t", + "9f35G83p6whuIfgCwR8RfIrgNfL3NaTrCG6Tf5+NuUXj01LqkKZUwGE+FUqoz1pUum7MbyEdkufLCP6G", + "4FO7k3GhqILDaFpHtZuo9gDpz1FtC9VmENwhr9C0PiJxPCdiZi+SOfCcJJQAl2XPlOM5NT8JSgKeq1Yt", + "44ZjslwEgsRNTfGE6FT1LFDLsqRGy2XbWFndf3mfOXNfm733Pxkbiz2drct4nGmeE5QJoInSRBz9I/0z", + "qn1A+q+oViMchSiTJYi4tL0UDTXZKNmUhYlwgex9fIBqD8l0dvdXthGcTR1qPHphbD/c//Qc6/rxG2Me", + "c3WUbnY4hDM8FIsdUdLABFAIOxcrQMWDS4KogKHBIem0oE0GGUP6E1R7jfRf8KC1maFBVxxlTOCM6euP", + "4zkFXKyICihwWaywCHYUe2mEcmLCPZwBt4dOxx6WFS1cQ7tPEXzdenwjdWjv46PGzHxj6afGso5gvbH4", + "CsElBK+lRji1MlYSNQ0UcoI2wvEpX1NjbsNs1+dviMGsb2Dk1bYQ3G58fxMPNcJpolYEjAat5Ttmgz6n", + "RWPlTWPxFZOtklwQx0VnMF9LlytPu1QYvFRZ0Tzw+mcFjHNZ7p/SrodIm2/V9FlKuOew7LHEVSAo+clQ", + "WfuFsfFo/82TMGZIVyy0q5oiShPmeAfXbF4BQgy9epv9abVKaXPKpiFxwN8UuVImf4kaKKlBaZMGqfPn", + "rYUMrgilchFw2aO8X2/OA0FRhCr+/Xdw+YxlZnDHQrH4b+Nc9kJ7Vm2KE4IKuCk+XuNhoGGrrp6omrMc", + "9Y5OjFxyFgiZw0dZkctA0URApGTbT6/s2vVKSyMgrCnaBl6geh+15+L49KyflTG5UI3Nhd3NCUzE0Jmo", + "5gqKMK7hfhxlm0aZ4SRpnh1K3uRo1KGQx74DeQ33/pXgENCeu+w98+T6M/2ZvszRvszRc5lMlvz7l8y/", + "ZjMZjufGZaWE23MFQQN9mljCPjuwBmzN5cQCI456/cJ4MIf0hebnj8btxzi80WedWEGqFIt2AMNYZ44L", + "9AUD1kjtKELQ5ZDztEBMxHnkHgq5wPRFNeeOxA4JjM/XW49nEJxF8DmCNxCcJTOOQBcZ0dt/O4D5EEB2", + "PCAB3Gz6c+CKlhh2mOhbmQgkGeHfK6UxouFkZMOiNFEEJydlMZ98jZyqFDWx3DH5cF4oWgY3oufESx5r", + "jsGdF4lyOWiFIxyTD1l2D0E4sSZlqaijyVDq9U6iJFzJXRKKFcCKeHmuJErhr6disW2qqSOuLQ0zmS4K", + "Y6DIFHmMKbUhjpgwrT63LT1oPGV6Fk5nwvEvva+LTmKuOpqIY+gYSgbShLnD6wx7jjnsmDHHmMZgTrIW", + "WXveGLYlGxbi5TTLhQCpUsI68RGO8owtDMvfmh21c1uUlYjPjkXQZTYcixGTC7N9t5nwLdC4vNBkXWbJ", + "XmIxWSHNe8CCvaASsEFIusiKuz3qbGd1Du+ZYwcchGSQiicTEdrbnsEKGBQ0cA5H7x118O8iuCyMFcGJ", + "ajL6IXVAkqVqSa6oSQkHK+WimBc0MCCpl4EyUCzKl0EhaS+nK2NFUZ1MSmhmTNUBqUCS6KrXjJImJ829", + "wwBjWfRqn+XDLL17iQIsDaEAvwXvS5fh/Z9/PmqsrKLaOqq9RrV5VPvQWrmx9+Ehgs9I0voW0u/9z/c3", + "EPxvpN9B8Nn+8rvm+ibJ/awheG3v3TukLxiza62VG+ThJwSXU7HIoI7gBibQ66j24X8/wEhx0LOIIQ9N", + "EIsdLuGhwWRoSrSL94ErEdkpK4c2oHUldxOeuLHT0QX7nK1dN+dVoKhtkj3ezkaDS21oMNzmdysZEImY", + "IWlc/nIm/8CWuzsISmBp6UOmqWhpUo4hoFtRzQn0W18+pf7ZmL9jnz368imJk3fuQDF4DvFJrAkU7KY5", + "gbTNCW5jf2Z/mhi8enPjZuPBK2Nl1Z7bMwTvIH324JMMZSbGnF0PyppmmX7b9lAOwbpx/efW4iyCD7BR", + "d84/Dzw7l4cY0zkLSqJU+EbC0Qx7SgppkQNuE/ax8Jax88nMZaLaC1RbJUckr1HtFoL1xqNbxu3f6KmR", + "09un5FD0Lf4f1o2dT81f1klxwDPs7vTbpP2Grf0lusjASpdOQwTvIbhtTG8gWMcCsonrCL4K8tGahnuf", + "1y2J67N2njVapj4hxBAsvf5ZYtXs97kSONBZ+4Hx4uEkcmbfiiojxCsLEyBXEq4wzNP8THNzBkdH9hl4", + "48GrpNnt5D6akA1XSiVBqUamVxzuA8NGioNyEAGhUEd4PQ186XEiGWZ50gDn9uF6rlABOcxIThNZMDXX", + "ZuPRWmt5Hq9oshitw0x9oQXvIvxvDem3PDYcI3gO/0+WIDnxoKolPJ3ipkuB85BuSDCemKidXriULlmN", + "cmPVGCdvw5OCApxTdVqRzA4jNWqj/MsE7/+/X/8D7Nd94efX3moJxWLO3tOwIiO7wAtbATcKqFuGYHcG", + "wc+OgUhht5Yip6f3MSG2Oj8gvW6V1MGdFKkB87RIHfJ0+/J745G5u6ZCCbjj6/lwHG/Kc5OCmitV3eNy", + "fzA721jZJTbPGReHSFybrhTqjJ9lbuN05Ujbtd65saoVanTgYjyzDHLK+1TM2LQGUkgBcyo4zyN3zwNS", + "wapZwYbc7DkpoW+Gdi+8zUek3TUtYWAamv04kDeyckXedI+VDfJuDiL1YQ7CYtHjYKI3Idv7/zXfWH2E", + "9AU+1YKzxuJbBHeaT2cJlzhGTh0asSQywh3mU741SXZqgfZU/mKEO5xqvniFHb+uB/qVqrIERjhrqVlZ", + "akv+3jQIbzXGc3YFaz1jVGDQhTKde8X2+/BI29CzAJCWTRTuzynCxaECR5VMxkgP8Z6iyJ5GrzRXvmH5", + "qNA2stbKgQExSJ6yp9jVIDRVrEoQP0GsKhCaKGYFCE2SqPqDJkxY+eEZ0636oB+fEFQwZAEpaORJNBTj", + "RN9qGGLiPIOFnZUGxqJOhTscatjEcPRQIWCPOVSPima8vqAFdxu3V5v6E3J0YSZJbqJaDek7SP8Nwe3W", + "9TvGzBKxzWGz9dczUAak7Y47VA5+MHWp0iYupCk4BVjpSvVMEkbsRRTkpPsVKx3xlRBSI1JwKl2oVUnC", + "urV+mWx0qTrloOw4VdqB4M32lPv3NxHcVmVFw/Hb5nZrfZUKn3wetM/324wceatY3edieU/debBMgOeu", + "9OFx+i4JiiSUsDW4wA3bAwxoA8MnOZ5+MPgNeUJC5QH3T+uxu2kc8P0mDWip/IeoTQbOn4Y0UIqvMTcW", + "4CNOz6yjrfjRIiZg1Rq1MfNq4mJt1rGj0xtTPomHCJcyY+xwoNriRNPQI1kTuEj/6O4+/OilDvF5+mJG", + "T5Dr5DpM+Dk/D4xaK+YOCEZThDMp/yUKLl/uz7BifPOgOK4O3TjfryjfzjcQvEw4z9v17264KzZfyU+5", + "TVLeHjK4QMg9o3xFEbXqMO7JCjjKZjLNqsgYL8qXzecVbVJWxP8kb07KBRB4eF4pclluUtPKajadvnhE", + "U4Tyke/KaaEspi8dS8u4cX/aJjEv4MllO78rFLAci3i4FP4lShMpBahyRckDPI3LiqgBtwlBYNXbCOtD", + "/geIyQhp6jEl5Ll5Y8e2S3lZ0oQ8cZvWNSBNEU5zPFfxjDEhapOVsSN5uZTG7zVRA/nJtCD9A/RpMubL", + "i03rRWrg9JCz2PxPLwFFNVsfPZI5kumTBfUY7kkuA0koi1yWO4af44BR0CaJENPBg5wJwMyU3UVwxkoP", + "wLXGD+t7798ifaHxbppUwSz3Z/bev917/9Pe7iwxIv5EBqq9wGFrbQbpC+ZNUafEBk3rHGFSIZjAK5P7", + "G9DOeDnjPbejQ5yJ2yRN308Lc/x0c8/1uRgE9N3TGM1DrvHGpAy9Bo6dmEL7rP5MxgahlX2gVmf6O9Vc", + "ovFupAVPFgnQvchovPzR2N1FcMvWqnmG9Mmqj5rWg1gwdy52KpdCwRTPHTf5b4++2nXj8S8Ibhsfnxgf", + "5hCs7z94RQ6sbpt94Y7+wurIz4u+EM7+CtLvkZ94HmaPx4I9Dp/5FjOyvdZcrzeW9dbiPQTrx1Q8ubfX", + "MdNwzT4N1/d23yO41Xj5Y/PpXHN9c3/uE4J14+6asfKYzJ5kSyfUQMUPcVplWWWsS+fuYnBm5n3Ktqvs", + "tKx6l5l1zReomp2E6QqQAtf8pryeR1MqYCoA5KO9AbJVTNcOyuHC9IPbfm4WiawFCX9PEA9MwgfxNvib", + "4v2uIn3Vdz19yuSlCDQQhyvj1u3W8kZbeA6SzvwATeYG2Hfyw8xmbDzY3PvxEKrfoBUcGsRavbZOSm+e", + "JVEp3LaHr3ekTJ7t44PDWCjsyGP3WFNfxS6EubiDav145nisijT3aNTSdy8cXXsvJGj5yWTQ2bjVWHlD", + "Q8csiWREifpC64dV662/wy27H1LCBu8huMmAPwOX3xTEHgKz+74yFJNR7jKhAbPE2X0o/yWsCLYXGg9d", + "AEmm3l03mC5VzbLNYU3QKuFbqg4rMzsyxqe8LP3RTLO/EPZPYqO96m5uvjS2H/baUneOuoAdjza1PYRd", + "jw0vE3EHscC9s7dfHpY9tp8eqcaMVI3rm1aFmlmExbKS3qsYbkXrWadYA8v3+bvG4k3j5ZIxs2SVc9tb", + "rjiG1j0/6BLYo/NDrE9txUwrBT+f11Ob7grnz2HAHax1yWK3z/A4owUTO8FZOLdz9l/eN+Z2mrWPhAvP", + "5YwWvGvcfU99GbFubyvNbEbsrBH1KcPfs4H3FBx92QSUd9ww4PsVHJFsctr3Msd0oFVzvL8/4lYCrLvX", + "EOAa0qH3JkLsKN+B8AFzXA4CSBQf5Yec74Z6aqSd74wmPvI4Ve3cfzDdwEGteaLj6ZBT7mCxUXLb78i0", + "3dEBEyGMz78y1BIDLU41tR8oV92vZLZNgbqRiTfzGZL27NioBj77mTDZGeDTo4y4xsURt21l2MblWAw5", + "4U1KY/O5aS7su4dUxie2lXIx4Fgfcme1HrA+YfaMmdyIY88Y0mxr2FwldozSkCxvmwCZZZJ6D8Mv6GCT", + "RpaUFljxZDvwthZ/bT591jl4I5DY/XjUg5yQ5IGLnXjJgG6Dp/sxYbKAMJ7ZZO30v6TZpNKmYcj7Q9jM", + "L2cq6bomAk9PRdMFs9TIricaxVhUgXLJxrKXnbIiFyp58sNf8mNV69DFRYwq3aKcF4oe2mw6TR5OyqqW", + "/WvmrxmTctSZy1XmF8NJ374veXNTo1P/FwAA//9yq4ZkdmAAAA==", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/openapi/types.go b/openapi/types.go new file mode 100644 index 00000000..49d72551 --- /dev/null +++ b/openapi/types.go @@ -0,0 +1,1485 @@ +// Package openapi provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. +package openapi + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/oapi-codegen/runtime" +) + +const ( + ApplicationScopes = "application.Scopes" +) + +// Defines values for QuestionSettingsMultipleChoiceQuestionType. +const ( + QuestionSettingsMultipleChoiceQuestionTypeMultipleChoice QuestionSettingsMultipleChoiceQuestionType = "MultipleChoice" +) + +// Defines values for QuestionSettingsNumberQuestionType. +const ( + QuestionSettingsNumberQuestionTypeNumber QuestionSettingsNumberQuestionType = "Number" +) + +// Defines values for QuestionSettingsScaleQuestionType. +const ( + QuestionSettingsScaleQuestionTypeScale QuestionSettingsScaleQuestionType = "Scale" +) + +// Defines values for QuestionSettingsSingleChoiceQuestionType. +const ( + QuestionSettingsSingleChoiceQuestionTypeSingleChoice QuestionSettingsSingleChoiceQuestionType = "SingleChoice" +) + +// Defines values for QuestionSettingsTextQuestionType. +const ( + QuestionSettingsTextQuestionTypeText QuestionSettingsTextQuestionType = "Text" +) + +// Defines values for QuestionSettingsTextLongQuestionType. +const ( + QuestionSettingsTextLongQuestionTypeTextLong QuestionSettingsTextLongQuestionType = "TextLong" +) + +// Defines values for QuestionTypeMultipleChoiceQuestionType. +const ( + QuestionTypeMultipleChoiceQuestionTypeMultipleChoice QuestionTypeMultipleChoiceQuestionType = "MultipleChoice" +) + +// Defines values for QuestionTypeNumberQuestionType. +const ( + QuestionTypeNumberQuestionTypeNumber QuestionTypeNumberQuestionType = "Number" +) + +// Defines values for QuestionTypeScaleQuestionType. +const ( + QuestionTypeScaleQuestionTypeScale QuestionTypeScaleQuestionType = "Scale" +) + +// Defines values for QuestionTypeSingleChoiceQuestionType. +const ( + QuestionTypeSingleChoiceQuestionTypeSingleChoice QuestionTypeSingleChoiceQuestionType = "SingleChoice" +) + +// Defines values for QuestionTypeTextQuestionType. +const ( + QuestionTypeTextQuestionTypeText QuestionTypeTextQuestionType = "Text" +) + +// Defines values for QuestionTypeTextLongQuestionType. +const ( + QuestionTypeTextLongQuestionTypeTextLong QuestionTypeTextLongQuestionType = "TextLong" +) + +// Defines values for ResShareType. +const ( + Admins ResShareType = "admins" + Anyone ResShareType = "anyone" + Respondents ResShareType = "respondents" +) + +// Defines values for ResponseBodyMultipleChoiceQuestionType. +const ( + MultipleChoice ResponseBodyMultipleChoiceQuestionType = "MultipleChoice" +) + +// Defines values for ResponseBodyNumberQuestionType. +const ( + Number ResponseBodyNumberQuestionType = "Number" +) + +// Defines values for ResponseBodyScaleQuestionType. +const ( + Scale ResponseBodyScaleQuestionType = "Scale" +) + +// Defines values for ResponseBodySingleChoiceQuestionType. +const ( + SingleChoice ResponseBodySingleChoiceQuestionType = "SingleChoice" +) + +// Defines values for ResponseBodyTextQuestionType. +const ( + Text ResponseBodyTextQuestionType = "Text" +) + +// Defines values for ResponseBodyTextLongQuestionType. +const ( + TextLong ResponseBodyTextLongQuestionType = "TextLong" +) + +// Defines values for ResponseSortType. +const ( + ResponseSortTypeModifiedAtASC ResponseSortType = "modified_at" + ResponseSortTypeModifiedAtDESC ResponseSortType = "-modified_at" + ResponseSortTypeSubmittedAtASC ResponseSortType = "submitted_at" + ResponseSortTypeSubmittedAtDESC ResponseSortType = "-submitted_at" + ResponseSortTypeTitleASC ResponseSortType = "title" + ResponseSortTypeTitleDESC ResponseSortType = "-title" +) + +// Defines values for SortType. +const ( + SortTypeCreatedAtASC SortType = "created_at" + SortTypeCreatedAtDESC SortType = "-created_at" + SortTypeModifiedAtASC SortType = "modified_at" + SortTypeModifiedAtDESC SortType = "-modified_at" + SortTypeTitleASC SortType = "title" + SortTypeTitleDESC SortType = "-title" +) + +// Groups defines model for Groups. +type Groups = []string + +// NewQuestion defines model for NewQuestion. +type NewQuestion struct { + Body string `json:"body"` + + // IsRequired 回答必須かどうか + IsRequired bool `json:"is_required"` + union json.RawMessage +} + +// NewQuestionnaire defines model for NewQuestionnaire. +type NewQuestionnaire struct { + Admins UsersAndGroups `json:"admins"` + Description string `json:"description"` + + // IsAnonymous 匿名回答かどうか + IsAnonymous bool `json:"is_anonymous"` + + // IsDuplicateAnswerAllowed 一人が複数回回答できるかどうか + IsDuplicateAnswerAllowed bool `json:"is_duplicate_answer_allowed"` + + // IsPublished アンケートが公開されているかどうか + IsPublished bool `json:"is_published"` + Questions []NewQuestion `json:"questions"` + + // ResponseDueDateTime 回答期限。この日時を過ぎたら回答できなくなる。nullの場合は回答期限なし。 + ResponseDueDateTime *time.Time `json:"response_due_date_time,omitempty"` + + // ResponseViewableBy アンケートの結果を, 運営は見られる ("admins"), 回答済みの人は見られる ("respondents") 誰でも見られる ("anyone") + ResponseViewableBy ResShareType `json:"response_viewable_by"` + Targets UsersAndGroups `json:"targets"` + Title string `json:"title"` +} + +// NewResponse defines model for NewResponse. +type NewResponse struct { + Body []ResponseBody `json:"body"` + IsDraft bool `json:"is_draft"` +} + +// Question defines model for Question. +type Question struct { + Body string `json:"body"` + CreatedAt time.Time `json:"created_at"` + + // IsRequired 回答必須かどうか + 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 { + Body string `json:"body"` + + // IsRequired 回答必須かどうか + IsRequired bool `json:"is_required"` +} + +// QuestionSettingsByType defines model for QuestionSettingsByType. +type QuestionSettingsByType struct { + union json.RawMessage +} + +// QuestionSettingsMultipleChoice defines model for QuestionSettingsMultipleChoice. +type QuestionSettingsMultipleChoice struct { + Options []string `json:"options"` + QuestionType QuestionSettingsMultipleChoiceQuestionType `json:"question_type"` +} + +// QuestionSettingsMultipleChoiceQuestionType defines model for QuestionSettingsMultipleChoice.QuestionType. +type QuestionSettingsMultipleChoiceQuestionType string + +// QuestionSettingsNumber defines model for QuestionSettingsNumber. +type QuestionSettingsNumber struct { + MaxValue *int `json:"max_value,omitempty"` + MinValue *int `json:"min_value,omitempty"` + QuestionType QuestionSettingsNumberQuestionType `json:"question_type"` +} + +// QuestionSettingsNumberQuestionType defines model for QuestionSettingsNumber.QuestionType. +type QuestionSettingsNumberQuestionType string + +// QuestionSettingsScale defines model for QuestionSettingsScale. +type QuestionSettingsScale struct { + MaxLabel *string `json:"max_label,omitempty"` + MaxValue int `json:"max_value"` + MinLabel *string `json:"min_label,omitempty"` + MinValue int `json:"min_value"` + QuestionType QuestionSettingsScaleQuestionType `json:"question_type"` +} + +// QuestionSettingsScaleQuestionType defines model for QuestionSettingsScale.QuestionType. +type QuestionSettingsScaleQuestionType string + +// QuestionSettingsSingleChoice defines model for QuestionSettingsSingleChoice. +type QuestionSettingsSingleChoice struct { + Options []string `json:"options"` + QuestionType QuestionSettingsSingleChoiceQuestionType `json:"question_type"` +} + +// QuestionSettingsSingleChoiceQuestionType defines model for QuestionSettingsSingleChoice.QuestionType. +type QuestionSettingsSingleChoiceQuestionType string + +// QuestionSettingsText defines model for QuestionSettingsText. +type QuestionSettingsText struct { + MaxLength *int `json:"max_length,omitempty"` + QuestionType QuestionSettingsTextQuestionType `json:"question_type"` +} + +// QuestionSettingsTextQuestionType defines model for QuestionSettingsText.QuestionType. +type QuestionSettingsTextQuestionType string + +// QuestionSettingsTextLong defines model for QuestionSettingsTextLong. +type QuestionSettingsTextLong struct { + MaxLength *float32 `json:"max_length,omitempty"` + QuestionType QuestionSettingsTextLongQuestionType `json:"question_type"` +} + +// QuestionSettingsTextLongQuestionType defines model for QuestionSettingsTextLong.QuestionType. +type QuestionSettingsTextLongQuestionType string + +// QuestionTypeMultipleChoice defines model for QuestionTypeMultipleChoice. +type QuestionTypeMultipleChoice struct { + QuestionType QuestionTypeMultipleChoiceQuestionType `json:"question_type"` +} + +// QuestionTypeMultipleChoiceQuestionType defines model for QuestionTypeMultipleChoice.QuestionType. +type QuestionTypeMultipleChoiceQuestionType string + +// QuestionTypeNumber defines model for QuestionTypeNumber. +type QuestionTypeNumber struct { + QuestionType QuestionTypeNumberQuestionType `json:"question_type"` +} + +// QuestionTypeNumberQuestionType defines model for QuestionTypeNumber.QuestionType. +type QuestionTypeNumberQuestionType string + +// QuestionTypeScale defines model for QuestionTypeScale. +type QuestionTypeScale struct { + QuestionType QuestionTypeScaleQuestionType `json:"question_type"` +} + +// QuestionTypeScaleQuestionType defines model for QuestionTypeScale.QuestionType. +type QuestionTypeScaleQuestionType string + +// QuestionTypeSingleChoice defines model for QuestionTypeSingleChoice. +type QuestionTypeSingleChoice struct { + QuestionType QuestionTypeSingleChoiceQuestionType `json:"question_type"` +} + +// QuestionTypeSingleChoiceQuestionType defines model for QuestionTypeSingleChoice.QuestionType. +type QuestionTypeSingleChoiceQuestionType string + +// QuestionTypeText defines model for QuestionTypeText. +type QuestionTypeText struct { + QuestionType QuestionTypeTextQuestionType `json:"question_type"` +} + +// QuestionTypeTextQuestionType defines model for QuestionTypeText.QuestionType. +type QuestionTypeTextQuestionType string + +// QuestionTypeTextLong defines model for QuestionTypeTextLong. +type QuestionTypeTextLong struct { + QuestionType QuestionTypeTextLongQuestionType `json:"question_type"` +} + +// QuestionTypeTextLongQuestionType defines model for QuestionTypeTextLong.QuestionType. +type QuestionTypeTextLongQuestionType string + +// QuestionnaireBase defines model for QuestionnaireBase. +type QuestionnaireBase struct { + Admins UsersAndGroups `json:"admins"` + Description string `json:"description"` + + // IsAnonymous 匿名回答かどうか + IsAnonymous bool `json:"is_anonymous"` + + // IsDuplicateAnswerAllowed 一人が複数回回答できるかどうか + IsDuplicateAnswerAllowed bool `json:"is_duplicate_answer_allowed"` + + // IsPublished アンケートが公開されているかどうか + IsPublished bool `json:"is_published"` + + // ResponseDueDateTime 回答期限。この日時を過ぎたら回答できなくなる。nullの場合は回答期限なし。 + ResponseDueDateTime *time.Time `json:"response_due_date_time,omitempty"` + + // ResponseViewableBy アンケートの結果を, 運営は見られる ("admins"), 回答済みの人は見られる ("respondents") 誰でも見られる ("anyone") + ResponseViewableBy ResShareType `json:"response_viewable_by"` + Targets UsersAndGroups `json:"targets"` + Title string `json:"title"` +} + +// QuestionnaireCreatedAt defines model for QuestionnaireCreatedAt. +type QuestionnaireCreatedAt struct { + CreatedAt time.Time `json:"created_at"` +} + +// QuestionnaireDescription defines model for QuestionnaireDescription. +type QuestionnaireDescription struct { + Description string `json:"description"` +} + +// QuestionnaireDetail defines model for QuestionnaireDetail. +type QuestionnaireDetail struct { + Admins UsersAndGroups `json:"admins"` + CreatedAt time.Time `json:"created_at"` + Description string `json:"description"` + + // IsAnonymous 匿名回答かどうか + IsAnonymous bool `json:"is_anonymous"` + + // IsDuplicateAnswerAllowed 一人が複数回回答できるかどうか + IsDuplicateAnswerAllowed bool `json:"is_duplicate_answer_allowed"` + + // IsPublished アンケートが公開されているかどうか + IsPublished bool `json:"is_published"` + ModifiedAt time.Time `json:"modified_at"` + QuestionnaireId int `json:"questionnaire_id"` + Questions []Question `json:"questions"` + Respondents Users `json:"respondents"` + + // ResponseDueDateTime 回答期限。この日時を過ぎたら回答できなくなる。nullの場合は回答期限なし。 + ResponseDueDateTime *time.Time `json:"response_due_date_time,omitempty"` + + // ResponseViewableBy アンケートの結果を, 運営は見られる ("admins"), 回答済みの人は見られる ("respondents") 誰でも見られる ("anyone") + ResponseViewableBy ResShareType `json:"response_viewable_by"` + Targets UsersAndGroups `json:"targets"` + Title string `json:"title"` +} + +// QuestionnaireID defines model for QuestionnaireID. +type QuestionnaireID struct { + QuestionnaireId int `json:"questionnaire_id"` +} + +// QuestionnaireInfo defines model for QuestionnaireInfo. +type QuestionnaireInfo struct { + CreatedAt time.Time `json:"created_at"` + + // IsTargetingMe 自分がターゲットになっているかどうか + IsTargetingMe bool `json:"is_targeting_me"` + ModifiedAt time.Time `json:"modified_at"` + + // ResponseDueDateTime 回答期限。この日時を過ぎたら回答できなくなる。nullの場合は回答期限なし。 + ResponseDueDateTime *time.Time `json:"response_due_date_time,omitempty"` + Title string `json:"title"` +} + +// QuestionnaireIsAnonymous defines model for QuestionnaireIsAnonymous. +type QuestionnaireIsAnonymous struct { + // IsAnonymous 匿名回答かどうか + IsAnonymous bool `json:"is_anonymous"` +} + +// QuestionnaireIsDuplicateAnswerAllowed defines model for QuestionnaireIsDuplicateAnswerAllowed. +type QuestionnaireIsDuplicateAnswerAllowed struct { + // IsDuplicateAnswerAllowed 一人が複数回回答できるかどうか + IsDuplicateAnswerAllowed bool `json:"is_duplicate_answer_allowed"` +} + +// QuestionnaireIsPublished defines model for QuestionnaireIsPublished. +type QuestionnaireIsPublished struct { + // IsPublished アンケートが公開されているかどうか + IsPublished bool `json:"is_published"` +} + +// QuestionnaireIsRemindEnabled defines model for QuestionnaireIsRemindEnabled. +type QuestionnaireIsRemindEnabled struct { + // IsRemindEnabled 自分に対するリマインドが有効かどうか。ユーザーが対象者でありかつ回答していない場合、この値がtrueであればリマインドが送信される。 + IsRemindEnabled bool `json:"is_remind_enabled"` +} + +// QuestionnaireIsTargetingMe defines model for QuestionnaireIsTargetingMe. +type QuestionnaireIsTargetingMe struct { + // IsTargetingMe 自分がターゲットになっているかどうか + IsTargetingMe bool `json:"is_targeting_me"` +} + +// QuestionnaireList defines model for QuestionnaireList. +type QuestionnaireList struct { + // PageMax 合計のページ数 + PageMax int `json:"page_max"` + Questionnaires []QuestionnaireSummary `json:"questionnaires"` +} + +// QuestionnaireModifiedAt defines model for QuestionnaireModifiedAt. +type QuestionnaireModifiedAt struct { + ModifiedAt time.Time `json:"modified_at"` +} + +// QuestionnaireResponseDueDateTime defines model for QuestionnaireResponseDueDateTime. +type QuestionnaireResponseDueDateTime struct { + // ResponseDueDateTime 回答期限。この日時を過ぎたら回答できなくなる。nullの場合は回答期限なし。 + ResponseDueDateTime *time.Time `json:"response_due_date_time,omitempty"` +} + +// QuestionnaireResponseViewableBy defines model for QuestionnaireResponseViewableBy. +type QuestionnaireResponseViewableBy struct { + // ResponseViewableBy アンケートの結果を, 運営は見られる ("admins"), 回答済みの人は見られる ("respondents") 誰でも見られる ("anyone") + ResponseViewableBy ResShareType `json:"response_viewable_by"` +} + +// QuestionnaireSummary defines model for QuestionnaireSummary. +type QuestionnaireSummary struct { + // AllResponded すべての対象者が回答済みの場合 true を返す。それ以外は false を返す。 (対象者が存在しない場合は true を返す) + AllResponded bool `json:"all_responded"` + CreatedAt time.Time `json:"created_at"` + Description string `json:"description"` + + // HasMyDraft 下書きが存在する + HasMyDraft bool `json:"has_my_draft"` + + // HasMyResponse 回答が存在する + HasMyResponse bool `json:"has_my_response"` + + // IsAnonymous 匿名回答かどうか + IsAnonymous bool `json:"is_anonymous"` + + // IsDuplicateAnswerAllowed 一人が複数回回答できるかどうか + IsDuplicateAnswerAllowed bool `json:"is_duplicate_answer_allowed"` + + // IsPublished アンケートが公開されているかどうか + IsPublished bool `json:"is_published"` + + // IsTargetingMe 自分がターゲットになっているかどうか + IsTargetingMe bool `json:"is_targeting_me"` + ModifiedAt time.Time `json:"modified_at"` + QuestionnaireId int `json:"questionnaire_id"` + RespondedDateTimeByMe *time.Time `json:"responded_date_time_by_me,omitempty"` + + // ResponseDueDateTime 回答期限。この日時を過ぎたら回答できなくなる。nullの場合は回答期限なし。 + ResponseDueDateTime *time.Time `json:"response_due_date_time,omitempty"` + + // ResponseViewableBy アンケートの結果を, 運営は見られる ("admins"), 回答済みの人は見られる ("respondents") 誰でも見られる ("anyone") + ResponseViewableBy ResShareType `json:"response_viewable_by"` + Title string `json:"title"` +} + +// QuestionnaireTargetsAndAdmins defines model for QuestionnaireTargetsAndAdmins. +type QuestionnaireTargetsAndAdmins struct { + Admins UsersAndGroups `json:"admins"` + Targets UsersAndGroups `json:"targets"` +} + +// QuestionnaireTitle defines model for QuestionnaireTitle. +type QuestionnaireTitle struct { + Title string `json:"title"` +} + +// ResShareType アンケートの結果を, 運営は見られる ("admins"), 回答済みの人は見られる ("respondents") 誰でも見られる ("anyone") +type ResShareType string + +// Response defines model for Response. +type Response struct { + Body []ResponseBody `json:"body"` + IsAnonymous *bool `json:"is_anonymous,omitempty"` + IsDraft bool `json:"is_draft"` + ModifiedAt time.Time `json:"modified_at"` + QuestionnaireId int `json:"questionnaire_id"` + + // Respondent traQ ID + Respondent *TraqId `json:"respondent,omitempty"` + ResponseId int `json:"response_id"` + SubmittedAt time.Time `json:"submitted_at"` +} + +// ResponseBody defines model for ResponseBody. +type ResponseBody struct { + union json.RawMessage +} + +// ResponseBodyBaseInteger defines model for ResponseBodyBaseInteger. +type ResponseBodyBaseInteger struct { + Answer int `json:"answer"` +} + +// ResponseBodyBaseNumber defines model for ResponseBodyBaseNumber. +type ResponseBodyBaseNumber struct { + Answer float32 `json:"answer"` +} + +// ResponseBodyBaseString defines model for ResponseBodyBaseString. +type ResponseBodyBaseString struct { + Answer string `json:"answer"` +} + +// ResponseBodyMultipleChoice defines model for ResponseBodyMultipleChoice. +type ResponseBodyMultipleChoice struct { + Answer []int `json:"answer"` + QuestionType ResponseBodyMultipleChoiceQuestionType `json:"question_type"` +} + +// ResponseBodyMultipleChoiceQuestionType defines model for ResponseBodyMultipleChoice.QuestionType. +type ResponseBodyMultipleChoiceQuestionType string + +// ResponseBodyNumber defines model for ResponseBodyNumber. +type ResponseBodyNumber struct { + Answer float32 `json:"answer"` + QuestionType ResponseBodyNumberQuestionType `json:"question_type"` +} + +// ResponseBodyNumberQuestionType defines model for ResponseBodyNumber.QuestionType. +type ResponseBodyNumberQuestionType string + +// ResponseBodyScale defines model for ResponseBodyScale. +type ResponseBodyScale struct { + Answer int `json:"answer"` + QuestionType ResponseBodyScaleQuestionType `json:"question_type"` +} + +// ResponseBodyScaleQuestionType defines model for ResponseBodyScale.QuestionType. +type ResponseBodyScaleQuestionType string + +// ResponseBodySingleChoice defines model for ResponseBodySingleChoice. +type ResponseBodySingleChoice struct { + Answer int `json:"answer"` + QuestionType ResponseBodySingleChoiceQuestionType `json:"question_type"` +} + +// ResponseBodySingleChoiceQuestionType defines model for ResponseBodySingleChoice.QuestionType. +type ResponseBodySingleChoiceQuestionType string + +// ResponseBodyText defines model for ResponseBodyText. +type ResponseBodyText struct { + Answer string `json:"answer"` + QuestionType ResponseBodyTextQuestionType `json:"question_type"` +} + +// ResponseBodyTextQuestionType defines model for ResponseBodyText.QuestionType. +type ResponseBodyTextQuestionType string + +// ResponseBodyTextLong defines model for ResponseBodyTextLong. +type ResponseBodyTextLong struct { + Answer string `json:"answer"` + QuestionType ResponseBodyTextLongQuestionType `json:"question_type"` +} + +// ResponseBodyTextLongQuestionType defines model for ResponseBodyTextLong.QuestionType. +type ResponseBodyTextLongQuestionType string + +// ResponseSortType response用のsortの種類 +type ResponseSortType string + +// ResponseWithQuestionnaireInfoItem defines model for ResponseWithQuestionnaireInfoItem. +type ResponseWithQuestionnaireInfoItem struct { + Body []ResponseBody `json:"body"` + IsAnonymous *bool `json:"is_anonymous,omitempty"` + IsDraft bool `json:"is_draft"` + ModifiedAt time.Time `json:"modified_at"` + QuestionnaireId int `json:"questionnaire_id"` + QuestionnaireInfo *QuestionnaireInfo `json:"questionnaire_info,omitempty"` + + // Respondent traQ ID + Respondent *TraqId `json:"respondent,omitempty"` + ResponseId int `json:"response_id"` + SubmittedAt time.Time `json:"submitted_at"` +} + +// Responses defines model for Responses. +type Responses = []Response + +// ResponsesWithQuestionnaireInfo defines model for ResponsesWithQuestionnaireInfo. +type ResponsesWithQuestionnaireInfo = []ResponseWithQuestionnaireInfoItem + +// SortType question、questionnaire用のソートの種類 +type SortType string + +// TraqId traQ ID +type TraqId = string + +// Users defines model for Users. +type Users = []TraqId + +// UsersAndGroups defines model for UsersAndGroups. +type UsersAndGroups struct { + Groups Groups `json:"groups"` + Users Users `json:"users"` +} + +// OnlyAdministratedByMeInQuery defines model for onlyAdministratedByMeInQuery. +type OnlyAdministratedByMeInQuery = bool + +// OnlyMyResponseInQuery defines model for onlyMyResponseInQuery. +type OnlyMyResponseInQuery = bool + +// OnlyTargetingMeInQuery defines model for onlyTargetingMeInQuery. +type OnlyTargetingMeInQuery = bool + +// PageInQuery defines model for pageInQuery. +type PageInQuery = int + +// QuestionnaireIDInPath defines model for questionnaireIDInPath. +type QuestionnaireIDInPath = int + +// ResponseIDInPath defines model for responseIDInPath. +type ResponseIDInPath = int + +// ResponseSortInQuery response用のsortの種類 +type ResponseSortInQuery = ResponseSortType + +// SearchInQuery defines model for searchInQuery. +type SearchInQuery = string + +// SortInQuery question、questionnaire用のソートの種類 +type SortInQuery = SortType + +// GetQuestionnairesParams defines parameters for GetQuestionnaires. +type GetQuestionnairesParams struct { + // Sort 並び順 (作成日時が新しい "created_at", 作成日時が古い "-created_at", タイトルの昇順 "title", タイトルの降順 "-title", 更新日時が新しい "modified_at", 更新日時が古い "-modified_at" ) + Sort *SortInQuery `form:"sort,omitempty" json:"sort,omitempty"` + + // Search タイトルの検索 + Search *SearchInQuery `form:"search,omitempty" json:"search,omitempty"` + + // Page 何ページ目か (未定義の場合は1ページ目) + Page *PageInQuery `form:"page,omitempty" json:"page,omitempty"` + + // OnlyTargetingMe 自分がターゲットになっているもののみ取得 (true), ターゲットになっているものも含めてすべて取得 (false)。デフォルトはfalse。 + OnlyTargetingMe *OnlyTargetingMeInQuery `form:"onlyTargetingMe,omitempty" json:"onlyTargetingMe,omitempty"` + + // OnlyAdministratedByMe 自分が管理者になっていないもののみ取得 (true), 管理者になっているものも含めてすべて取得 (false)。デフォルトはfalse。 + OnlyAdministratedByMe *OnlyAdministratedByMeInQuery `form:"onlyAdministratedByMe,omitempty" json:"onlyAdministratedByMe,omitempty"` +} + +// GetQuestionnaireResponsesParams defines parameters for GetQuestionnaireResponses. +type GetQuestionnaireResponsesParams struct { + // Sort 並び順 (作成日時が新しい "submitted_at", 作成日時が古い "-submitted_at", タイトルの昇順 "title", タイトルの降順 "-title", 更新日時が新しい "modified_at", 更新日時が古い "-modified_at" ) + Sort *ResponseSortInQuery `form:"sort,omitempty" json:"sort,omitempty"` + + // OnlyMyResponse 自分の回答のみ取得 (true), 自分の回答以外も含めてすべて取得 (false)。デフォルトはfalse。 + OnlyMyResponse *OnlyMyResponseInQuery `form:"onlyMyResponse,omitempty" json:"onlyMyResponse,omitempty"` +} + +// GetMyResponsesParams defines parameters for GetMyResponses. +type GetMyResponsesParams struct { + // Sort 並び順 (作成日時が新しい "submitted_at", 作成日時が古い "-submitted_at", タイトルの昇順 "title", タイトルの降順 "-title", 更新日時が新しい "modified_at", 更新日時が古い "-modified_at" ) + Sort *ResponseSortInQuery `form:"sort,omitempty" json:"sort,omitempty"` +} + +// PostQuestionnaireJSONRequestBody defines body for PostQuestionnaire for application/json ContentType. +type PostQuestionnaireJSONRequestBody = NewQuestionnaire + +// EditQuestionnaireJSONRequestBody defines body for EditQuestionnaire for application/json ContentType. +type EditQuestionnaireJSONRequestBody = QuestionnaireDetail + +// EditQuestionnaireMyRemindStatusJSONRequestBody defines body for EditQuestionnaireMyRemindStatus for application/json ContentType. +type EditQuestionnaireMyRemindStatusJSONRequestBody = QuestionnaireIsRemindEnabled + +// PostQuestionnaireResponseJSONRequestBody defines body for PostQuestionnaireResponse for application/json ContentType. +type PostQuestionnaireResponseJSONRequestBody = NewResponse + +// EditResponseJSONRequestBody defines body for EditResponse for application/json ContentType. +type EditResponseJSONRequestBody = Response + +// AsQuestionSettingsText returns the union data inside the NewQuestion as a QuestionSettingsText +func (t NewQuestion) AsQuestionSettingsText() (QuestionSettingsText, error) { + var body QuestionSettingsText + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsText overwrites any union data inside the NewQuestion as the provided QuestionSettingsText +func (t *NewQuestion) FromQuestionSettingsText(v QuestionSettingsText) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsText performs a merge with any union data inside the NewQuestion, using the provided QuestionSettingsText +func (t *NewQuestion) MergeQuestionSettingsText(v QuestionSettingsText) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsTextLong returns the union data inside the NewQuestion as a QuestionSettingsTextLong +func (t NewQuestion) AsQuestionSettingsTextLong() (QuestionSettingsTextLong, error) { + var body QuestionSettingsTextLong + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsTextLong overwrites any union data inside the NewQuestion as the provided QuestionSettingsTextLong +func (t *NewQuestion) FromQuestionSettingsTextLong(v QuestionSettingsTextLong) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsTextLong performs a merge with any union data inside the NewQuestion, using the provided QuestionSettingsTextLong +func (t *NewQuestion) MergeQuestionSettingsTextLong(v QuestionSettingsTextLong) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsNumber returns the union data inside the NewQuestion as a QuestionSettingsNumber +func (t NewQuestion) AsQuestionSettingsNumber() (QuestionSettingsNumber, error) { + var body QuestionSettingsNumber + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsNumber overwrites any union data inside the NewQuestion as the provided QuestionSettingsNumber +func (t *NewQuestion) FromQuestionSettingsNumber(v QuestionSettingsNumber) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsNumber performs a merge with any union data inside the NewQuestion, using the provided QuestionSettingsNumber +func (t *NewQuestion) MergeQuestionSettingsNumber(v QuestionSettingsNumber) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsSingleChoice returns the union data inside the NewQuestion as a QuestionSettingsSingleChoice +func (t NewQuestion) AsQuestionSettingsSingleChoice() (QuestionSettingsSingleChoice, error) { + var body QuestionSettingsSingleChoice + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsSingleChoice overwrites any union data inside the NewQuestion as the provided QuestionSettingsSingleChoice +func (t *NewQuestion) FromQuestionSettingsSingleChoice(v QuestionSettingsSingleChoice) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsSingleChoice performs a merge with any union data inside the NewQuestion, using the provided QuestionSettingsSingleChoice +func (t *NewQuestion) MergeQuestionSettingsSingleChoice(v QuestionSettingsSingleChoice) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsMultipleChoice returns the union data inside the NewQuestion as a QuestionSettingsMultipleChoice +func (t NewQuestion) AsQuestionSettingsMultipleChoice() (QuestionSettingsMultipleChoice, error) { + var body QuestionSettingsMultipleChoice + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsMultipleChoice overwrites any union data inside the NewQuestion as the provided QuestionSettingsMultipleChoice +func (t *NewQuestion) FromQuestionSettingsMultipleChoice(v QuestionSettingsMultipleChoice) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsMultipleChoice performs a merge with any union data inside the NewQuestion, using the provided QuestionSettingsMultipleChoice +func (t *NewQuestion) MergeQuestionSettingsMultipleChoice(v QuestionSettingsMultipleChoice) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsScale returns the union data inside the NewQuestion as a QuestionSettingsScale +func (t NewQuestion) AsQuestionSettingsScale() (QuestionSettingsScale, error) { + var body QuestionSettingsScale + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsScale overwrites any union data inside the NewQuestion as the provided QuestionSettingsScale +func (t *NewQuestion) FromQuestionSettingsScale(v QuestionSettingsScale) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsScale performs a merge with any union data inside the NewQuestion, using the provided QuestionSettingsScale +func (t *NewQuestion) MergeQuestionSettingsScale(v QuestionSettingsScale) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t NewQuestion) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + if err != nil { + return nil, err + } + object := make(map[string]json.RawMessage) + if t.union != nil { + err = json.Unmarshal(b, &object) + if err != nil { + return nil, err + } + } + + object["body"], err = json.Marshal(t.Body) + if err != nil { + return nil, fmt.Errorf("error marshaling 'body': %w", err) + } + + object["is_required"], err = json.Marshal(t.IsRequired) + if err != nil { + return nil, fmt.Errorf("error marshaling 'is_required': %w", err) + } + + b, err = json.Marshal(object) + return b, err +} + +func (t *NewQuestion) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + if err != nil { + return err + } + object := make(map[string]json.RawMessage) + err = json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["body"]; found { + err = json.Unmarshal(raw, &t.Body) + if err != nil { + return fmt.Errorf("error reading 'body': %w", err) + } + } + + if raw, found := object["is_required"]; found { + err = json.Unmarshal(raw, &t.IsRequired) + if err != nil { + return fmt.Errorf("error reading 'is_required': %w", err) + } + } + + return err +} + +// AsQuestionSettingsText returns the union data inside the Question as a QuestionSettingsText +func (t Question) AsQuestionSettingsText() (QuestionSettingsText, error) { + var body QuestionSettingsText + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsText overwrites any union data inside the Question as the provided QuestionSettingsText +func (t *Question) FromQuestionSettingsText(v QuestionSettingsText) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsText performs a merge with any union data inside the Question, using the provided QuestionSettingsText +func (t *Question) MergeQuestionSettingsText(v QuestionSettingsText) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsTextLong returns the union data inside the Question as a QuestionSettingsTextLong +func (t Question) AsQuestionSettingsTextLong() (QuestionSettingsTextLong, error) { + var body QuestionSettingsTextLong + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsTextLong overwrites any union data inside the Question as the provided QuestionSettingsTextLong +func (t *Question) FromQuestionSettingsTextLong(v QuestionSettingsTextLong) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsTextLong performs a merge with any union data inside the Question, using the provided QuestionSettingsTextLong +func (t *Question) MergeQuestionSettingsTextLong(v QuestionSettingsTextLong) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsNumber returns the union data inside the Question as a QuestionSettingsNumber +func (t Question) AsQuestionSettingsNumber() (QuestionSettingsNumber, error) { + var body QuestionSettingsNumber + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsNumber overwrites any union data inside the Question as the provided QuestionSettingsNumber +func (t *Question) FromQuestionSettingsNumber(v QuestionSettingsNumber) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsNumber performs a merge with any union data inside the Question, using the provided QuestionSettingsNumber +func (t *Question) MergeQuestionSettingsNumber(v QuestionSettingsNumber) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsSingleChoice returns the union data inside the Question as a QuestionSettingsSingleChoice +func (t Question) AsQuestionSettingsSingleChoice() (QuestionSettingsSingleChoice, error) { + var body QuestionSettingsSingleChoice + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsSingleChoice overwrites any union data inside the Question as the provided QuestionSettingsSingleChoice +func (t *Question) FromQuestionSettingsSingleChoice(v QuestionSettingsSingleChoice) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsSingleChoice performs a merge with any union data inside the Question, using the provided QuestionSettingsSingleChoice +func (t *Question) MergeQuestionSettingsSingleChoice(v QuestionSettingsSingleChoice) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsMultipleChoice returns the union data inside the Question as a QuestionSettingsMultipleChoice +func (t Question) AsQuestionSettingsMultipleChoice() (QuestionSettingsMultipleChoice, error) { + var body QuestionSettingsMultipleChoice + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsMultipleChoice overwrites any union data inside the Question as the provided QuestionSettingsMultipleChoice +func (t *Question) FromQuestionSettingsMultipleChoice(v QuestionSettingsMultipleChoice) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsMultipleChoice performs a merge with any union data inside the Question, using the provided QuestionSettingsMultipleChoice +func (t *Question) MergeQuestionSettingsMultipleChoice(v QuestionSettingsMultipleChoice) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsScale returns the union data inside the Question as a QuestionSettingsScale +func (t Question) AsQuestionSettingsScale() (QuestionSettingsScale, error) { + var body QuestionSettingsScale + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsScale overwrites any union data inside the Question as the provided QuestionSettingsScale +func (t *Question) FromQuestionSettingsScale(v QuestionSettingsScale) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsScale performs a merge with any union data inside the Question, using the provided QuestionSettingsScale +func (t *Question) MergeQuestionSettingsScale(v QuestionSettingsScale) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t Question) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + if err != nil { + return nil, err + } + object := make(map[string]json.RawMessage) + if t.union != nil { + err = json.Unmarshal(b, &object) + if err != nil { + return nil, err + } + } + + object["body"], err = json.Marshal(t.Body) + if err != nil { + return nil, fmt.Errorf("error marshaling 'body': %w", err) + } + + object["created_at"], err = json.Marshal(t.CreatedAt) + if err != nil { + return nil, fmt.Errorf("error marshaling 'created_at': %w", err) + } + + object["is_required"], err = json.Marshal(t.IsRequired) + if err != nil { + return nil, fmt.Errorf("error marshaling 'is_required': %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) + if err != nil { + return nil, fmt.Errorf("error marshaling 'questionnaire_id': %w", err) + } + + b, err = json.Marshal(object) + return b, err +} + +func (t *Question) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + if err != nil { + return err + } + object := make(map[string]json.RawMessage) + err = json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["body"]; found { + err = json.Unmarshal(raw, &t.Body) + if err != nil { + return fmt.Errorf("error reading 'body': %w", err) + } + } + + if raw, found := object["created_at"]; found { + err = json.Unmarshal(raw, &t.CreatedAt) + if err != nil { + return fmt.Errorf("error reading 'created_at': %w", err) + } + } + + if raw, found := object["is_required"]; found { + err = json.Unmarshal(raw, &t.IsRequired) + if err != nil { + return fmt.Errorf("error reading 'is_required': %w", err) + } + } + + if raw, found := object["question_id"]; found { + err = json.Unmarshal(raw, &t.QuestionId) + if err != nil { + return fmt.Errorf("error reading 'question_id': %w", err) + } + } + + if raw, found := object["questionnaire_id"]; found { + err = json.Unmarshal(raw, &t.QuestionnaireId) + if err != nil { + return fmt.Errorf("error reading 'questionnaire_id': %w", err) + } + } + + return err +} + +// AsQuestionSettingsText returns the union data inside the QuestionSettingsByType as a QuestionSettingsText +func (t QuestionSettingsByType) AsQuestionSettingsText() (QuestionSettingsText, error) { + var body QuestionSettingsText + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsText overwrites any union data inside the QuestionSettingsByType as the provided QuestionSettingsText +func (t *QuestionSettingsByType) FromQuestionSettingsText(v QuestionSettingsText) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsText performs a merge with any union data inside the QuestionSettingsByType, using the provided QuestionSettingsText +func (t *QuestionSettingsByType) MergeQuestionSettingsText(v QuestionSettingsText) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsTextLong returns the union data inside the QuestionSettingsByType as a QuestionSettingsTextLong +func (t QuestionSettingsByType) AsQuestionSettingsTextLong() (QuestionSettingsTextLong, error) { + var body QuestionSettingsTextLong + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsTextLong overwrites any union data inside the QuestionSettingsByType as the provided QuestionSettingsTextLong +func (t *QuestionSettingsByType) FromQuestionSettingsTextLong(v QuestionSettingsTextLong) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsTextLong performs a merge with any union data inside the QuestionSettingsByType, using the provided QuestionSettingsTextLong +func (t *QuestionSettingsByType) MergeQuestionSettingsTextLong(v QuestionSettingsTextLong) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsNumber returns the union data inside the QuestionSettingsByType as a QuestionSettingsNumber +func (t QuestionSettingsByType) AsQuestionSettingsNumber() (QuestionSettingsNumber, error) { + var body QuestionSettingsNumber + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsNumber overwrites any union data inside the QuestionSettingsByType as the provided QuestionSettingsNumber +func (t *QuestionSettingsByType) FromQuestionSettingsNumber(v QuestionSettingsNumber) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsNumber performs a merge with any union data inside the QuestionSettingsByType, using the provided QuestionSettingsNumber +func (t *QuestionSettingsByType) MergeQuestionSettingsNumber(v QuestionSettingsNumber) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsSingleChoice returns the union data inside the QuestionSettingsByType as a QuestionSettingsSingleChoice +func (t QuestionSettingsByType) AsQuestionSettingsSingleChoice() (QuestionSettingsSingleChoice, error) { + var body QuestionSettingsSingleChoice + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsSingleChoice overwrites any union data inside the QuestionSettingsByType as the provided QuestionSettingsSingleChoice +func (t *QuestionSettingsByType) FromQuestionSettingsSingleChoice(v QuestionSettingsSingleChoice) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsSingleChoice performs a merge with any union data inside the QuestionSettingsByType, using the provided QuestionSettingsSingleChoice +func (t *QuestionSettingsByType) MergeQuestionSettingsSingleChoice(v QuestionSettingsSingleChoice) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsMultipleChoice returns the union data inside the QuestionSettingsByType as a QuestionSettingsMultipleChoice +func (t QuestionSettingsByType) AsQuestionSettingsMultipleChoice() (QuestionSettingsMultipleChoice, error) { + var body QuestionSettingsMultipleChoice + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsMultipleChoice overwrites any union data inside the QuestionSettingsByType as the provided QuestionSettingsMultipleChoice +func (t *QuestionSettingsByType) FromQuestionSettingsMultipleChoice(v QuestionSettingsMultipleChoice) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsMultipleChoice performs a merge with any union data inside the QuestionSettingsByType, using the provided QuestionSettingsMultipleChoice +func (t *QuestionSettingsByType) MergeQuestionSettingsMultipleChoice(v QuestionSettingsMultipleChoice) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsQuestionSettingsScale returns the union data inside the QuestionSettingsByType as a QuestionSettingsScale +func (t QuestionSettingsByType) AsQuestionSettingsScale() (QuestionSettingsScale, error) { + var body QuestionSettingsScale + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromQuestionSettingsScale overwrites any union data inside the QuestionSettingsByType as the provided QuestionSettingsScale +func (t *QuestionSettingsByType) FromQuestionSettingsScale(v QuestionSettingsScale) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeQuestionSettingsScale performs a merge with any union data inside the QuestionSettingsByType, using the provided QuestionSettingsScale +func (t *QuestionSettingsByType) MergeQuestionSettingsScale(v QuestionSettingsScale) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t QuestionSettingsByType) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *QuestionSettingsByType) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + +// AsResponseBodyText returns the union data inside the ResponseBody as a ResponseBodyText +func (t ResponseBody) AsResponseBodyText() (ResponseBodyText, error) { + var body ResponseBodyText + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromResponseBodyText overwrites any union data inside the ResponseBody as the provided ResponseBodyText +func (t *ResponseBody) FromResponseBodyText(v ResponseBodyText) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeResponseBodyText performs a merge with any union data inside the ResponseBody, using the provided ResponseBodyText +func (t *ResponseBody) MergeResponseBodyText(v ResponseBodyText) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsResponseBodyTextLong returns the union data inside the ResponseBody as a ResponseBodyTextLong +func (t ResponseBody) AsResponseBodyTextLong() (ResponseBodyTextLong, error) { + var body ResponseBodyTextLong + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromResponseBodyTextLong overwrites any union data inside the ResponseBody as the provided ResponseBodyTextLong +func (t *ResponseBody) FromResponseBodyTextLong(v ResponseBodyTextLong) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeResponseBodyTextLong performs a merge with any union data inside the ResponseBody, using the provided ResponseBodyTextLong +func (t *ResponseBody) MergeResponseBodyTextLong(v ResponseBodyTextLong) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsResponseBodyNumber returns the union data inside the ResponseBody as a ResponseBodyNumber +func (t ResponseBody) AsResponseBodyNumber() (ResponseBodyNumber, error) { + var body ResponseBodyNumber + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromResponseBodyNumber overwrites any union data inside the ResponseBody as the provided ResponseBodyNumber +func (t *ResponseBody) FromResponseBodyNumber(v ResponseBodyNumber) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeResponseBodyNumber performs a merge with any union data inside the ResponseBody, using the provided ResponseBodyNumber +func (t *ResponseBody) MergeResponseBodyNumber(v ResponseBodyNumber) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsResponseBodySingleChoice returns the union data inside the ResponseBody as a ResponseBodySingleChoice +func (t ResponseBody) AsResponseBodySingleChoice() (ResponseBodySingleChoice, error) { + var body ResponseBodySingleChoice + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromResponseBodySingleChoice overwrites any union data inside the ResponseBody as the provided ResponseBodySingleChoice +func (t *ResponseBody) FromResponseBodySingleChoice(v ResponseBodySingleChoice) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeResponseBodySingleChoice performs a merge with any union data inside the ResponseBody, using the provided ResponseBodySingleChoice +func (t *ResponseBody) MergeResponseBodySingleChoice(v ResponseBodySingleChoice) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsResponseBodyMultipleChoice returns the union data inside the ResponseBody as a ResponseBodyMultipleChoice +func (t ResponseBody) AsResponseBodyMultipleChoice() (ResponseBodyMultipleChoice, error) { + var body ResponseBodyMultipleChoice + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromResponseBodyMultipleChoice overwrites any union data inside the ResponseBody as the provided ResponseBodyMultipleChoice +func (t *ResponseBody) FromResponseBodyMultipleChoice(v ResponseBodyMultipleChoice) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeResponseBodyMultipleChoice performs a merge with any union data inside the ResponseBody, using the provided ResponseBodyMultipleChoice +func (t *ResponseBody) MergeResponseBodyMultipleChoice(v ResponseBodyMultipleChoice) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsResponseBodyScale returns the union data inside the ResponseBody as a ResponseBodyScale +func (t ResponseBody) AsResponseBodyScale() (ResponseBodyScale, error) { + var body ResponseBodyScale + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromResponseBodyScale overwrites any union data inside the ResponseBody as the provided ResponseBodyScale +func (t *ResponseBody) FromResponseBodyScale(v ResponseBodyScale) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeResponseBodyScale performs a merge with any union data inside the ResponseBody, using the provided ResponseBodyScale +func (t *ResponseBody) MergeResponseBodyScale(v ResponseBodyScale) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t ResponseBody) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *ResponseBody) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + 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_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/tools.go b/tools.go index a73f8352..005b37f2 100644 --- a/tools.go +++ b/tools.go @@ -6,4 +6,5 @@ package main import ( _ "github.com/golang/mock/mockgen" _ "github.com/google/wire/cmd/wire" + _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" ) diff --git a/traq/traq.go b/traq/traq.go new file mode 100644 index 00000000..6f08b8ac --- /dev/null +++ b/traq/traq.go @@ -0,0 +1,45 @@ +package traq + +import ( + "context" + + traq "github.com/traPtitech/go-traq" +) + +const TOKEN = "/* your token */" + +type TraqAPIClient struct { + client *traq.APIClient + auth context.Context +} + +func NewTraqAPIClient() *TraqAPIClient { + return &TraqAPIClient{ + client: traq.NewAPIClient(traq.NewConfiguration()), + auth: context.WithValue(context.Background(), traq.ContextAccessToken, TOKEN), + } +} + +func (t *TraqAPIClient) GetGroupMembers(ctx context.Context, groupID string) ([]traq.UserGroupMember, error) { + v, _, err := t.client.GroupApi.GetUserGroupMembers(ctx, groupID).Execute() + if err != nil { + return nil, err + } + return v, nil +} + +func (t *TraqAPIClient) GetUserTraqID(ctx context.Context, userUUID string) (string, error) { + v, _, err := t.client.UserApi.GetUser(ctx, userUUID).Execute() + if err != nil { + return "", err + } + return v.Name, nil +} + +func (t *TraqAPIClient) GetGroupName(ctx context.Context, groupID string) (string, error) { + v, _, err := t.client.GroupApi.GetUserGroup(ctx, groupID).Execute() + if err != nil { + return "", err + } + return v.Name, nil +} 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: