Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions api/questionnaire.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"fmt"
"net/http"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -95,6 +96,12 @@ func (h QuestionnaireHandler) Create(ctx *gin.Context) {
_ = ctx.Error(err)
return
}
// Additional questionaire fields validation
err = r.Validate()
if err != nil {
_ = ctx.Error(err)
return
}
m := r.Model()
m.CreateUser = h.CurrentUser(ctx)
result := h.DB(ctx).Create(m)
Expand Down Expand Up @@ -169,6 +176,12 @@ func (h QuestionnaireHandler) Update(ctx *gin.Context) {
m.UpdateUser = updated.UpdateUser
m.Required = updated.Required
} else {
// Additional validation for non-builtin questionnaires fields
err = r.Validate()
if err != nil {
_ = ctx.Error(err)
return
}
m = updated
}

Expand Down Expand Up @@ -225,3 +238,95 @@ func (r *Questionnaire) Model() (m *model.Questionnaire) {

return
}

// Validate performs additional validation on the questionnaire beyond binding tags.
func (r *Questionnaire) Validate() error {
// Validate sections have unique order values
sectionOrders := make(map[uint]bool)
for i, section := range r.Sections {
// Check for duplicate section order
if sectionOrders[section.Order] {
return &BadRequestError{
fmt.Sprintf("duplicate section order %d found", section.Order),
}
}
sectionOrders[section.Order] = true

// Validate each section has at least one question
if len(section.Questions) == 0 {
return &BadRequestError{
fmt.Sprintf("section %d (%s) must have at least one question", i, section.Name),
}
}

// Validate questions within section
questionOrders := make(map[uint]bool)
for j, question := range section.Questions {
// Check for duplicate question order within section
if questionOrders[question.Order] {
return &BadRequestError{
fmt.Sprintf("duplicate question order %d found in section %d (%s)", question.Order, i, section.Name),
}
}
questionOrders[question.Order] = true

// Validate question text is not empty
if question.Text == "" {
return &BadRequestError{
fmt.Sprintf("question %d in section %d (%s) must have text", j, i, section.Name),
}
}

// Validate each question has at least one answer
if len(question.Answers) == 0 {
return &BadRequestError{
fmt.Sprintf("question %d (%s) in section %d (%s) must have at least one answer", j, question.Text, i, section.Name),
}
}

// Validate answers within question
answerOrders := make(map[uint]bool)
for k, answer := range question.Answers {
// Check for duplicate answer order within question
if answerOrders[answer.Order] {
return &BadRequestError{
fmt.Sprintf("duplicate answer order %d found in question %d (%s) in section %d (%s)", answer.Order, j, question.Text, i, section.Name),
}
}
answerOrders[answer.Order] = true

// Validate answer text is not empty
if answer.Text == "" {
return &BadRequestError{
fmt.Sprintf("answer %d in question %d (%s) in section %d (%s) must have text", k, j, question.Text, i, section.Name),
}
}

// Validate risk level (already validated by binding tag, but double-check)
validRisks := map[string]bool{"red": true, "yellow": true, "green": true, "unknown": true}
if !validRisks[answer.Risk] {
return &BadRequestError{
fmt.Sprintf("answer %d (%s) in question %d (%s) has invalid risk level '%s', must be one of: red, yellow, green, unknown", k, answer.Text, j, question.Text, answer.Risk),
}
}
}
}
}

// Validate threshold values
if r.Thresholds.Red == 0 && r.Thresholds.Yellow == 0 && r.Thresholds.Unknown == 0 {
return &BadRequestError{
"at least one threshold (red, yellow, or unknown) must be greater than 0",
}
}

// Validate risk messages are not empty
if r.RiskMessages.Red == "" || r.RiskMessages.Yellow == "" ||
r.RiskMessages.Green == "" || r.RiskMessages.Unknown == "" {
return &BadRequestError{
"all risk messages (red, yellow, green, unknown) must be provided",
}
}

return nil
}
13 changes: 11 additions & 2 deletions test/api/questionnaire/samples.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,17 @@ var (
Name: "Questionnaire1",
Description: "Questionnaire minimal sample 1",
Required: true,
Thresholds: api.Thresholds{},
RiskMessages: api.RiskMessages{},
Thresholds: api.Thresholds{
Red: 30,
Yellow: 20,
Unknown: 10,
},
RiskMessages: api.RiskMessages{
Red: "Application requires deep changes in code or infrastructure.",
Yellow: "Application requires some changes in code or configuration.",
Green: "Application is ready for modernization.",
Unknown: "More information needed.",
},
Sections: []api.Section{
{
Order: 1,
Expand Down
Loading
Loading