From cdb0fa1bb3e22e402dd87b031548160a78841b26 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 29 Apr 2026 02:47:21 -0700 Subject: [PATCH] Enforce strict initial assessment set validation. Require INITIAL_ASSESSMENT titles to follow the Level Test A1/A2/B1/B2 format and ensure passing_score is always present on create and update. Made-with: Cursor --- internal/web_server/handlers/questions.go | 56 +++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/internal/web_server/handlers/questions.go b/internal/web_server/handlers/questions.go index 95e6abf..8059015 100644 --- a/internal/web_server/handlers/questions.go +++ b/internal/web_server/handlers/questions.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "regexp" "strconv" "strings" @@ -550,6 +551,15 @@ type listQuestionSetsRes struct { TotalCount int64 `json:"total_count"` } +var initialAssessmentTitlePattern = regexp.MustCompile(`^Level Test (A1|A2|B1|B2) \([^)]+\)$`) + +func validateInitialAssessmentTitle(title string) error { + if !initialAssessmentTitlePattern.MatchString(strings.TrimSpace(title)) { + return fmt.Errorf("title must match format: Level Test ()") + } + return nil +} + func isSequenceGatedPractice(set domain.QuestionSet) bool { if !strings.EqualFold(set.SetType, string(domain.QuestionSetTypePractice)) || set.OwnerType == nil { return false @@ -605,6 +615,20 @@ func (h *Handler) CreateQuestionSet(c *fiber.Ctx) error { Error: err.Error(), }) } + if strings.EqualFold(req.SetType, string(domain.QuestionSetTypeInitialAssessment)) { + if err := validateInitialAssessmentTitle(req.Title); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid initial assessment title", + Error: err.Error(), + }) + } + if req.PassingScore == nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid initial assessment set", + Error: "passing_score is required for INITIAL_ASSESSMENT question sets", + }) + } + } input := domain.CreateQuestionSetInput{ Title: req.Title, @@ -883,6 +907,14 @@ func (h *Handler) UpdateQuestionSet(c *fiber.Ctx) error { }) } + existingSet, err := h.questionsSvc.GetQuestionSetByID(c.Context(), id) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ + Message: "Question set not found", + Error: err.Error(), + }) + } + var req updateQuestionSetReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ @@ -896,6 +928,30 @@ func (h *Handler) UpdateQuestionSet(c *fiber.Ctx) error { title = *req.Title } + if strings.EqualFold(existingSet.SetType, string(domain.QuestionSetTypeInitialAssessment)) { + effectiveTitle := existingSet.Title + if req.Title != nil { + effectiveTitle = *req.Title + } + if err := validateInitialAssessmentTitle(effectiveTitle); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid initial assessment title", + Error: err.Error(), + }) + } + + effectivePassingScore := existingSet.PassingScore + if req.PassingScore != nil { + effectivePassingScore = req.PassingScore + } + if effectivePassingScore == nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid initial assessment set", + Error: "passing_score is required for INITIAL_ASSESSMENT question sets", + }) + } + } + input := domain.CreateQuestionSetInput{ Title: title, Description: req.Description,