| package models |
|
|
| import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| "testing" |
|
|
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
| ) |
|
|
| func TestValidationIssue_JSON(t *testing.T) { |
| tests := []struct { |
| name string |
| issue ValidationIssue |
| expectedJSON string |
| }{ |
| { |
| name: "empty", |
| issue: ValidationIssue{}, |
| expectedJSON: `{"message":"", "severity":"critical"}`, |
| }, |
| { |
| name: "bare", |
| issue: ValidationIssue{ |
| message: "error message", |
| }, |
| expectedJSON: `{"message":"error message", "severity":"critical"}`, |
| }, |
| { |
| name: "generic", |
| issue: ValidationIssue{ |
| message: "error message", |
| field: NewFieldSelectorGroup(NewFieldSelector("field_name")), |
| code: "invalid_param", |
| component: "openmeter", |
| severity: ErrorSeverityCritical, |
| }, |
| expectedJSON: `{ |
| "code":"invalid_param", |
| "component":"openmeter", |
| "message":"error message", |
| "field":"$.field_name", |
| "severity":"critical" |
| }`, |
| }, |
| { |
| name: "with attributes", |
| issue: ValidationIssue{ |
| message: "error message", |
| field: NewFieldSelectorGroup(NewFieldSelector("field_name").WithExpression( |
| NewFieldAttrValue("key", "value"), |
| )), |
| code: "invalid_param", |
| component: "openmeter", |
| severity: ErrorSeverityWarning, |
| attributes: Attributes{ |
| "attr1": "value1", |
| }, |
| }, |
| expectedJSON: `{ |
| "code":"invalid_param", |
| "component":"openmeter", |
| "message":"error message", |
| "field":"$.field_name[?(@.key=='value')]", |
| "severity":"warning", |
| "attr1": "value1" |
| }`, |
| }, |
| } |
|
|
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| b, err := json.Marshal(test.issue) |
| require.NoErrorf(t, err, "marshaling ValidationIssue to JSON must not fail") |
|
|
| assert.JSONEqf(t, test.expectedJSON, string(b), "JSON representation must match") |
| }) |
| } |
| } |
|
|
| var errTestValidationIssue = ValidationIssue{ |
| code: "test_validation_issue", |
| message: "test validation issue", |
| field: NewFieldSelectorGroup(NewFieldSelector("field")), |
| severity: ErrorSeverityCritical, |
| } |
|
|
| func TestValidationIssue_WithAttrs(t *testing.T) { |
| tests := []struct { |
| name string |
| issue ValidationIssue |
| attrs Attributes |
| }{ |
| { |
| name: "empty", |
| issue: errTestValidationIssue, |
| attrs: nil, |
| }, |
| { |
| name: "with attributes", |
| issue: errTestValidationIssue, |
| attrs: Attributes{ |
| "key1": "value1", |
| "key2": "value2", |
| }, |
| }, |
| } |
|
|
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| actual := test.issue.WithAttrs(test.attrs) |
|
|
| assert.Emptyf(t, errTestValidationIssue.Attributes(), "original attributes must be empty") |
| assert.Equalf(t, test.attrs, actual.Attributes(), "attributes must match") |
| }) |
| } |
| } |
|
|
| func TestValidationIssue_WithField(t *testing.T) { |
| t.Run("Should UNSET field if nil is passed", func(t *testing.T) { |
| issue := errTestValidationIssue.Clone() |
|
|
| withField := issue.WithField() |
|
|
| assert.Equal(t, "", withField.Field().String(), "field must match") |
| assert.Equal(t, "", withField.Field().JSONPath(), "field path must match") |
| }) |
|
|
| t.Run("Should override field if non-nil path is passed", func(t *testing.T) { |
| issue := errTestValidationIssue.Clone() |
|
|
| desc := NewFieldSelectorGroup(NewFieldSelector("field_name")) |
|
|
| withField := issue.WithField(desc) |
|
|
| assert.Equal(t, desc.String(), withField.Field().String(), "field must match") |
| assert.Equal(t, "$.field_name", withField.Field().JSONPath(), "field path must match") |
| }) |
| } |
|
|
| func TestValidationIssue_Clone(t *testing.T) { |
| tests := []struct { |
| name string |
| issue ValidationIssue |
| expectedIssue ValidationIssue |
| }{ |
| { |
| name: "clone", |
| issue: errTestValidationIssue, |
| expectedIssue: ValidationIssue{ |
| attributes: errTestValidationIssue.attributes, |
| code: errTestValidationIssue.code, |
| component: errTestValidationIssue.component, |
| message: errTestValidationIssue.message, |
| field: errTestValidationIssue.field, |
| severity: errTestValidationIssue.severity, |
| wraps: errTestValidationIssue, |
| }, |
| }, |
| { |
| name: "empty", |
| issue: ValidationIssue{}, |
| expectedIssue: ValidationIssue{wraps: ValidationIssue{}}, |
| }, |
| } |
|
|
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| actual := test.issue.Clone() |
|
|
| assert.Equalf(t, test.expectedIssue, actual, "must match after clone") |
| }) |
| } |
| } |
|
|
| func TestValidationIssue_UnwrapAndErrorsIs(t *testing.T) { |
| base := errTestValidationIssue |
| withField := base.WithField(NewFieldSelectorGroup(NewFieldSelector("field_name"))) |
| withAttrs := withField.WithAttr("key", "value") |
|
|
| require.ErrorIs(t, withField, base) |
| require.ErrorIs(t, withAttrs, withField) |
| require.ErrorIs(t, withAttrs, base) |
|
|
| assert.Equal(t, base.Error(), withField.Error()) |
| assert.Equal(t, withField.Error(), withAttrs.Error()) |
| } |
|
|
| func TestValidationIssue_ErrorsIsWithJoin(t *testing.T) { |
| base := NewValidationError("joined_code", "joined message") |
| derived := base.WithPathString("field_name") |
| joined := errors.Join(errors.New("other error"), derived) |
|
|
| require.ErrorIs(t, joined, derived) |
| require.ErrorIs(t, joined, base) |
| } |
|
|
| func TestAsValidationIssues(t *testing.T) { |
| tests := []struct { |
| name string |
| err error |
| expectError bool |
| expectedIssues ValidationIssues |
| }{ |
| { |
| name: "nil", |
| err: nil, |
| expectError: false, |
| expectedIssues: nil, |
| }, |
| { |
| name: "error", |
| err: errors.New("error"), |
| expectError: true, |
| expectedIssues: nil, |
| }, |
| { |
| name: "error/wrapped", |
| err: fmt.Errorf("test: %w", errors.New("error")), |
| expectError: true, |
| expectedIssues: nil, |
| }, |
| { |
| name: "mixed", |
| err: fmt.Errorf("test: %w", |
| errors.Join( |
| errors.New("error 1"), |
| NewValidationError("errcode1", "error message 1"), |
| NewValidationWarning("errcode2", "error message 2"), |
| ), |
| ), |
| expectError: true, |
| expectedIssues: nil, |
| }, |
| { |
| name: "mixed", |
| err: fmt.Errorf("test: %w", |
| errors.Join( |
| errors.New("error 1"), |
| NewValidationError("errcode1", "error message 1"), |
| NewValidationWarning("errcode2", "error message 2"), |
| ), |
| ), |
| expectError: true, |
| expectedIssues: nil, |
| }, |
| { |
| name: "unknown wrapped", |
| err: fmt.Errorf("test: %w", |
| errors.Join( |
| ErrorWithComponent("component1", |
| errors.New("error 1"), |
| ), |
| NewValidationError("errcode1", "error message 1"), |
| NewValidationWarning("errcode2", "error message 2"), |
| ), |
| ), |
| expectError: false, |
| expectedIssues: ValidationIssues{ |
| ValidationIssue{ |
| component: "component1", |
| message: "error 1", |
| severity: ErrorSeverityCritical, |
| }, |
| ValidationIssue{ |
| code: "errcode1", |
| message: "error message 1", |
| severity: ErrorSeverityCritical, |
| }, |
| ValidationIssue{ |
| code: "errcode2", |
| message: "error message 2", |
| severity: ErrorSeverityWarning, |
| }, |
| }, |
| }, |
| { |
| name: "nested and wrapped with unknown", |
| err: fmt.Errorf("test: %w", |
| errors.Join( |
| ErrorWithComponent("component1", |
| errors.New("error 1"), |
| ), |
| ErrorWithComponent("component2", |
| ErrorWithFieldPrefix(NewFieldSelectorGroup(NewFieldSelector("prefix")), |
| errors.Join( |
| NewValidationIssue("errcode1", "error message 1", |
| WithFieldString("field1"), |
| WithCriticalSeverity(), |
| ), |
| NewValidationIssue("errcode2", "error message 2", |
| WithFieldString("field2"), |
| WithWarningSeverity(), |
| ), |
| ErrorWithFieldPrefix(NewFieldSelectorGroup(NewFieldSelector("prefix2")), |
| ErrorWithComponent("component3", |
| errors.Join( |
| NewValidationIssue("errcode3", "error message 3", |
| WithFieldString("field3"), |
| WithCriticalSeverity(), |
| ), |
| NewValidationIssue("errcode4", "error message 4", |
| WithField( |
| NewFieldSelector("prefix3"), |
| NewFieldSelector("field4"), |
| ), |
| WithWarningSeverity(), |
| WithAttributes(Attributes{ |
| "attr1": "value1", |
| }), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| NewValidationIssue("errcode5", "error message 5", |
| WithField(NewFieldSelector("field5")), |
| WithCriticalSeverity(), |
| WithComponent("component4"), |
| ), |
| ), |
| ), |
| expectError: false, |
| expectedIssues: ValidationIssues{ |
| ValidationIssue{ |
| component: "component1", |
| message: "error 1", |
| severity: ErrorSeverityCritical, |
| }, |
| ValidationIssue{ |
| code: "errcode1", |
| component: "component2", |
| message: "error message 1", |
| field: NewFieldSelectorGroup( |
| NewFieldSelector("prefix"), |
| NewFieldSelector("field1"), |
| ), |
| severity: ErrorSeverityCritical, |
| }, |
| ValidationIssue{ |
| code: "errcode2", |
| component: "component2", |
| message: "error message 2", |
| field: NewFieldSelectorGroup( |
| NewFieldSelector("prefix"), |
| NewFieldSelector("field2"), |
| ), |
| severity: ErrorSeverityWarning, |
| }, |
| ValidationIssue{ |
| code: "errcode3", |
| component: "component3", |
| message: "error message 3", |
| field: NewFieldSelectorGroup( |
| NewFieldSelector("prefix"), |
| NewFieldSelector("prefix2"), |
| NewFieldSelector("field3"), |
| ), |
| severity: ErrorSeverityCritical, |
| }, |
| ValidationIssue{ |
| code: "errcode4", |
| component: "component3", |
| message: "error message 4", |
| field: NewFieldSelectorGroup( |
| NewFieldSelector("prefix"), |
| NewFieldSelector("prefix2"), |
| NewFieldSelector("prefix3"), |
| NewFieldSelector("field4"), |
| ), |
| severity: ErrorSeverityWarning, |
| attributes: Attributes{ |
| "attr1": "value1", |
| }, |
| }, |
| ValidationIssue{ |
| code: "errcode5", |
| component: "component4", |
| message: "error message 5", |
| field: NewFieldSelectorGroup( |
| NewFieldSelector("field5"), |
| ), |
| severity: ErrorSeverityCritical, |
| }, |
| }, |
| }, |
| } |
|
|
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| actual, err := AsValidationIssues(test.err) |
|
|
| if test.expectError { |
| require.EqualErrorf(t, err, test.err.Error(), "error must match") |
| } else { |
| require.NoError(t, err, "error must be nil") |
| } |
|
|
| RequireValidationIssuesMatch(t, test.expectedIssues, actual) |
| }) |
| } |
| } |
|
|
| func TestValidationIssues_AsError(t *testing.T) { |
| issues := ValidationIssues{ |
| { |
| severity: ErrorSeverityCritical, |
| message: "error1", |
| component: "component1", |
| field: NewFieldSelectorGroup(NewFieldSelector("field1")), |
| }, |
| } |
|
|
| err := issues.AsError() |
|
|
| validationIssues, err := AsValidationIssues(err) |
| require.NoError(t, err) |
| RequireValidationIssuesMatch(t, issues, validationIssues) |
| } |
|
|
| func TestAsValidationIssues_PointerValidationIssue(t *testing.T) { |
| issue := NewValidationIssue("errcode1", "error message 1", |
| WithFieldString("field1"), |
| WithWarningSeverity(), |
| WithAttributes(Attributes{ |
| "attr1": "value1", |
| }), |
| ) |
|
|
| actual, err := AsValidationIssues(&issue) |
| require.NoError(t, err) |
|
|
| RequireValidationIssuesMatch(t, ValidationIssues{issue}, actual) |
| } |
|
|