package validator import ( "fmt" "testing" govalidator "github.com/go-playground/validator/v10" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestValidatePasswordComplexity(t *testing.T) { tests := []struct { name string password string valid bool }{ {"valid password", "Password1", true}, {"valid complex password", "MyP@ssw0rd!", true}, {"missing uppercase", "password1", false}, {"missing lowercase", "PASSWORD1", false}, {"missing digit", "Password", false}, {"only digits", "12345678", false}, {"only lowercase", "abcdefgh", false}, {"only uppercase", "ABCDEFGH", false}, {"empty string", "", false}, {"single valid char each", "aA1", true}, {"unicode uppercase with digit and lower", "Über1abc", true}, } v := govalidator.New() v.RegisterValidation("password_complexity", validatePasswordComplexity) type testStruct struct { Password string `validate:"password_complexity"` } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { s := testStruct{Password: tc.password} err := v.Struct(s) if tc.valid && err != nil { t.Errorf("expected password %q to be valid, got error: %v", tc.password, err) } if !tc.valid && err == nil { t.Errorf("expected password %q to be invalid, got nil error", tc.password) } }) } } func TestValidatePasswordComplexityWithMinLength(t *testing.T) { v := govalidator.New() v.RegisterValidation("password_complexity", validatePasswordComplexity) type request struct { Password string `validate:"required,min=8,password_complexity"` } tests := []struct { name string password string valid bool }{ {"valid 8+ chars with complexity", "Abcdefg1", true}, {"too short but complex", "Ab1", false}, {"long but no uppercase", "abcdefgh1", false}, {"long but no lowercase", "ABCDEFGH1", false}, {"long but no digit", "Abcdefghi", false}, {"empty", "", false}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { r := request{Password: tc.password} err := v.Struct(r) if tc.valid && err != nil { t.Errorf("expected %q to be valid, got error: %v", tc.password, err) } if !tc.valid && err == nil { t.Errorf("expected %q to be invalid, got nil", tc.password) } }) } } func TestFormatMessagePasswordComplexity(t *testing.T) { cv := NewCustomValidator() type request struct { Password string `json:"password" validate:"required,min=8,password_complexity"` } r := request{Password: "lowercase1"} err := cv.Validate(r) if err == nil { t.Fatal("expected validation error for password without uppercase") } resp := FormatValidationErrors(err) if resp == nil { t.Fatal("expected non-nil error response") } field, ok := resp.Fields["password"] if !ok { t.Fatal("expected 'password' field in error response") } expectedMsg := "Password must be at least 8 characters with at least one uppercase letter, one lowercase letter, and one digit" if field.Message != expectedMsg { t.Errorf("expected message %q, got %q", expectedMsg, field.Message) } if field.Tag != "password_complexity" { t.Errorf("expected tag 'password_complexity', got %q", field.Tag) } } func TestPasswordComplexity_AdditionalCases(t *testing.T) { cv := NewCustomValidator() type request struct { Password string `json:"password" validate:"required,min=8,password_complexity"` } tests := []struct { name string pw string valid bool }{ {"no uppercase no digit", "password", false}, {"no lowercase", "PASSWORD1", false}, {"no digit", "Password", false}, {"too short", "Pass1", false}, {"valid standard", "Password1", true}, {"valid with special chars", "P@ssw0rd", true}, {"spaces with complexity", "Pass 1234", true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { r := request{Password: tc.pw} err := cv.Validate(r) if tc.valid { assert.NoError(t, err, "expected %q to be valid", tc.pw) } else { assert.Error(t, err, "expected %q to be invalid", tc.pw) } }) } } func TestFormatValidationErrors_AllTags(t *testing.T) { cv := NewCustomValidator() type allTags struct { Required string `json:"required" validate:"required"` Email string `json:"email" validate:"email"` MinLen string `json:"min_len" validate:"min=5"` MaxLen string `json:"max_len" validate:"max=3"` OneOf string `json:"one_of" validate:"oneof=a b c"` URL string `json:"url" validate:"url"` } input := allTags{ Required: "", // fails required Email: "bad", // fails email MinLen: "ab", // fails min=5 MaxLen: "abcde", // fails max=3 OneOf: "z", // fails oneof URL: "nope", // fails url } err := cv.Validate(input) require.Error(t, err) resp := FormatValidationErrors(err) require.NotNil(t, resp) assert.Equal(t, "Validation failed", resp.Error) expectedMessages := map[string]string{ "required": "This field is required", "email": "Must be a valid email address", "min_len": "Must be at least 5 characters", "max_len": "Must be at most 3 characters", "one_of": "Must be one of: a b c", "url": "Must be a valid URL", } for field, expectedMsg := range expectedMessages { fe, ok := resp.Fields[field] assert.True(t, ok, "expected field %q in error response", field) if ok { assert.Equal(t, expectedMsg, fe.Message, "message mismatch for field %q", field) } } } func TestFormatValidationErrors_NonValidationError(t *testing.T) { err := fmt.Errorf("some random error") resp := FormatValidationErrors(err) require.NotNil(t, resp) assert.Equal(t, "some random error", resp.Error) assert.Nil(t, resp.Fields) } func TestNewCustomValidator_UsesJSONTagNames(t *testing.T) { cv := NewCustomValidator() type request struct { FirstName string `json:"first_name" validate:"required"` } err := cv.Validate(request{}) require.Error(t, err) resp := FormatValidationErrors(err) require.NotNil(t, resp) _, ok := resp.Fields["first_name"] assert.True(t, ok, "expected JSON tag name 'first_name' in error fields") } func TestCustomValidator_Validate_Success(t *testing.T) { cv := NewCustomValidator() type request struct { Name string `json:"name" validate:"required"` } err := cv.Validate(request{Name: "test"}) assert.NoError(t, err) }