From 2fce6136f1ac24d7b1f1f5626bc8aaf417055b40 Mon Sep 17 00:00:00 2001 From: Jonathan Davies Date: Thu, 9 Oct 2025 15:40:48 +0100 Subject: [PATCH 1/2] feat: change error response --- internal/api/error.go | 33 ++++++++++++++++--- internal/api/handler/evidence.go | 2 +- internal/api/handler/filter.go | 7 ++-- internal/api/handler/heartbeat.go | 8 ++--- internal/api/handler/users.go | 5 +++ .../api/handler/users_integration_test.go | 21 ++++++++++++ 6 files changed, 64 insertions(+), 12 deletions(-) diff --git a/internal/api/error.go b/internal/api/error.go index e2e38f84..b8868b29 100644 --- a/internal/api/error.go +++ b/internal/api/error.go @@ -3,6 +3,8 @@ package api import ( "errors" "fmt" + "reflect" + "strings" "github.com/go-playground/validator/v10" @@ -26,13 +28,36 @@ func NewError(err error) Error { return e } -func Validator(err error) Error { +func Validator(err error, invalidObj any) Error { e := Error{} - e.Errors = make(map[string]any) + e.Errors = make(map[string]interface{}) var errs validator.ValidationErrors - errors.As(err, &errs) + + if !errors.As(err, &errs) { + return e + } + + invalidObjType := reflect.TypeOf(invalidObj) + + if invalidObjType.Kind() == reflect.Ptr { + invalidObjType = invalidObjType.Elem() + } + for _, v := range errs { - e.Errors[v.Field()] = fmt.Sprintf("%v", v.Tag()) + fieldName := v.StructField() + jsonTag := fieldName + if f, ok := invalidObjType.FieldByName(fieldName); ok { + tag := f.Tag.Get("json") + if tag != "" && tag != "-" { + jsonTag = strings.Split(tag, ",")[0] + } + } + msg := fmt.Sprintf("validation failed on '%s'", v.Tag()) + if errList, ok := e.Errors[jsonTag].([]string); ok { + e.Errors[jsonTag] = append(errList, msg) + } else { + e.Errors[jsonTag] = []string{msg} + } } return e } diff --git a/internal/api/handler/evidence.go b/internal/api/handler/evidence.go index a4a5eac3..cb870a03 100644 --- a/internal/api/handler/evidence.go +++ b/internal/api/handler/evidence.go @@ -179,7 +179,7 @@ func (h *EvidenceHandler) Create(ctx echo.Context) error { err := ctx.Validate(input) if err != nil { - return ctx.JSON(http.StatusBadRequest, api.Validator(err)) + return ctx.JSON(http.StatusBadRequest, api.Validator(err, input)) } components := []relational.SystemComponent{} diff --git a/internal/api/handler/filter.go b/internal/api/handler/filter.go index 2fffa0cb..b0221159 100644 --- a/internal/api/handler/filter.go +++ b/internal/api/handler/filter.go @@ -2,6 +2,8 @@ package handler import ( "errors" + "net/http" + "github.com/compliance-framework/api/internal/api" "github.com/compliance-framework/api/internal/service/relational" oscalTypes_1_1_3 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" @@ -10,7 +12,6 @@ import ( "go.uber.org/zap" "gorm.io/datatypes" "gorm.io/gorm" - "net/http" ) // FilterHandler handles CRUD operations for filters. @@ -136,7 +137,7 @@ func (h *FilterHandler) Create(ctx echo.Context) error { return ctx.JSON(http.StatusBadRequest, api.NewError(err)) } if err := ctx.Validate(req); err != nil { - return ctx.JSON(http.StatusBadRequest, api.Validator(err)) + return ctx.JSON(http.StatusBadRequest, api.Validator(err, req)) } filter := relational.Filter{ @@ -190,7 +191,7 @@ func (h *FilterHandler) Update(ctx echo.Context) error { return ctx.JSON(http.StatusBadRequest, api.NewError(err)) } if err := ctx.Validate(req); err != nil { - return ctx.JSON(http.StatusBadRequest, api.Validator(err)) + return ctx.JSON(http.StatusBadRequest, api.Validator(err, req)) } var filter relational.Filter diff --git a/internal/api/handler/heartbeat.go b/internal/api/handler/heartbeat.go index 579909d3..582e806a 100644 --- a/internal/api/handler/heartbeat.go +++ b/internal/api/handler/heartbeat.go @@ -1,14 +1,15 @@ package handler import ( + "net/http" + "time" + "github.com/compliance-framework/api/internal/api" "github.com/compliance-framework/api/internal/service" "github.com/google/uuid" "github.com/labstack/echo/v4" "go.uber.org/zap" "gorm.io/gorm" - "net/http" - "time" ) type HeartbeatHandler struct { @@ -54,7 +55,7 @@ func (h *HeartbeatHandler) Create(ctx echo.Context) error { err := ctx.Validate(heartbeat) if err != nil { - return ctx.JSON(http.StatusBadRequest, api.Validator(err)) + return ctx.JSON(http.StatusBadRequest, api.Validator(err, heartbeat)) } if err := h.db.Create(&service.Heartbeat{ @@ -78,7 +79,6 @@ func (h *HeartbeatHandler) Create(ctx echo.Context) error { // @Failure 500 {object} api.Error // @Router /agent/heartbeat/over-time [get] func (h *HeartbeatHandler) OverTime(ctx echo.Context) error { - type HeartbeatInterval struct { Interval time.Time `json:"interval"` Total int64 `json:"total"` diff --git a/internal/api/handler/users.go b/internal/api/handler/users.go index a4771733..9454d86c 100644 --- a/internal/api/handler/users.go +++ b/internal/api/handler/users.go @@ -156,6 +156,11 @@ func (h *UserHandler) CreateUser(ctx echo.Context) error { return ctx.JSON(400, api.NewError(err)) } + err := ctx.Validate(req) + if err != nil { + return ctx.JSON(400, api.Validator(err, req)) + } + user := &relational.User{ Email: req.Email, FirstName: req.FirstName, diff --git a/internal/api/handler/users_integration_test.go b/internal/api/handler/users_integration_test.go index 7a53f441..587d41ca 100644 --- a/internal/api/handler/users_integration_test.go +++ b/internal/api/handler/users_integration_test.go @@ -161,6 +161,27 @@ func (suite *UserApiIntegrationSuite) TestCreateUser() { suite.Equal(409, rec.Code, "Expected Conflict response for CreateUser with existing email") suite.Contains(rec.Body.String(), "email already exists", "Expected error message for existing email in CreateUser response") }) + + suite.Run("CreateUserWithInvalidEmail", func() { + existingUser := createUserRequest{ + Email: "", + Password: "password123", + FirstName: "Existing", + LastName: "User", + } + + existingUserJSON, err := json.Marshal(existingUser) + suite.Require().NoError(err, "Failed to marshal existing user request") + + rec := httptest.NewRecorder() + req := httptest.NewRequest("POST", "/api/users", bytes.NewReader(existingUserJSON)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+*token) + + suite.server.E().ServeHTTP(rec, req) + suite.Equal(400, rec.Code, "Expected error response for CreateUser with invalid email") + suite.Contains(rec.Body.String(), "{\"errors\":{\"email\":[\"validation failed on 'email'\"]}}\n", "Expected error message for invalid email in CreateUser response") + }) } func (suite *UserApiIntegrationSuite) ModifyUser() { From 3118d00f9c05526794ed4841c3df594cfd559f3f Mon Sep 17 00:00:00 2001 From: Jonathan Davies Date: Wed, 15 Oct 2025 17:36:27 +0100 Subject: [PATCH 2/2] fix: tests --- .../api/handler/users_integration_test.go | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/internal/api/handler/users_integration_test.go b/internal/api/handler/users_integration_test.go index 587d41ca..b62c2fc3 100644 --- a/internal/api/handler/users_integration_test.go +++ b/internal/api/handler/users_integration_test.go @@ -164,7 +164,7 @@ func (suite *UserApiIntegrationSuite) TestCreateUser() { suite.Run("CreateUserWithInvalidEmail", func() { existingUser := createUserRequest{ - Email: "", + Email: "invalid", Password: "password123", FirstName: "Existing", LastName: "User", @@ -182,6 +182,26 @@ func (suite *UserApiIntegrationSuite) TestCreateUser() { suite.Equal(400, rec.Code, "Expected error response for CreateUser with invalid email") suite.Contains(rec.Body.String(), "{\"errors\":{\"email\":[\"validation failed on 'email'\"]}}\n", "Expected error message for invalid email in CreateUser response") }) + suite.Run("CreateUserWithMissingEmail", func() { + existingUser := createUserRequest{ + Email: "", + Password: "password123", + FirstName: "Existing", + LastName: "User", + } + + existingUserJSON, err := json.Marshal(existingUser) + suite.Require().NoError(err, "Failed to marshal existing user request") + + rec := httptest.NewRecorder() + req := httptest.NewRequest("POST", "/api/users", bytes.NewReader(existingUserJSON)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+*token) + + suite.server.E().ServeHTTP(rec, req) + suite.Equal(400, rec.Code, "Expected error response for CreateUser with invalid email") + suite.Contains(rec.Body.String(), "{\"errors\":{\"email\":[\"validation failed on 'required'\"]}}\n", "Expected error message for invalid email in CreateUser response") + }) } func (suite *UserApiIntegrationSuite) ModifyUser() {