package validate import ( "regexp" "testing" ) func TestValidationError_Error(t *testing.T) { err := ValidationError{Field: "name", Message: "is required"} expected := "name: is required" if err.Error() != expected { t.Errorf("expected %q, got %q", expected, err.Error()) } } func TestValidationErrors_Error(t *testing.T) { tests := []struct { name string errors ValidationErrors expected string }{ { name: "no errors", errors: ValidationErrors{}, expected: "", }, { name: "single error", errors: ValidationErrors{ {Field: "name", Message: "is required"}, }, expected: "name: is required", }, { name: "multiple errors", errors: ValidationErrors{ {Field: "name", Message: "is required"}, {Field: "email", Message: "is invalid"}, }, expected: "validation failed: name: is required; email: is invalid", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.errors.Error(); got != tt.expected { t.Errorf("expected %q, got %q", tt.expected, got) } }) } } func TestValidationErrors_HasErrors(t *testing.T) { if (ValidationErrors{}).HasErrors() { t.Error("expected empty ValidationErrors to have no errors") } errs := ValidationErrors{{Field: "test", Message: "error"}} if !errs.HasErrors() { t.Error("expected non-empty ValidationErrors to have errors") } } func TestValidationErrors_Fields(t *testing.T) { errs := ValidationErrors{ {Field: "name", Message: "is required"}, {Field: "name", Message: "is too short"}, // Second error for same field {Field: "email", Message: "is invalid"}, } fields := errs.Fields() if len(fields) != 2 { t.Errorf("expected 2 fields, got %d", len(fields)) } if fields["name"] != "is required" { t.Errorf("expected name error to be 'is required', got %q", fields["name"]) } if fields["email"] != "is invalid" { t.Errorf("expected email error to be 'is invalid', got %q", fields["email"]) } } func TestValidator_Required(t *testing.T) { tests := []struct { name string value string wantErr bool }{ {"valid", "hello", false}, {"empty", "", true}, {"whitespace only", " ", true}, {"with whitespace", " hello ", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := New() v.Required(tt.value, "field") if (v.Error() != nil) != tt.wantErr { t.Errorf("Required(%q) error = %v, wantErr %v", tt.value, v.Error(), tt.wantErr) } }) } } func TestValidator_RequiredSlice(t *testing.T) { tests := []struct { name string value []string wantErr bool }{ {"valid", []string{"a", "b"}, false}, {"single item", []string{"a"}, false}, {"empty", []string{}, true}, {"nil", nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := New() v.RequiredSlice(tt.value, "field") if (v.Error() != nil) != tt.wantErr { t.Errorf("RequiredSlice(%v) error = %v, wantErr %v", tt.value, v.Error(), tt.wantErr) } }) } } func TestValidator_StringLength(t *testing.T) { tests := []struct { name string value string min int max int wantErr bool errMsg string }{ {"valid", "hello", 1, 10, false, ""}, {"exact min", "ab", 2, 10, false, ""}, {"exact max", "hello", 1, 5, false, ""}, {"too short", "a", 2, 10, true, "must be at least 2 characters"}, {"too long", "hello world", 1, 5, true, "must be at most 5 characters"}, {"no min check", "", 0, 10, false, ""}, {"no max check", "very long string", 1, 0, false, ""}, {"empty with min", "", 1, 10, true, "must be at least 1 characters"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := New() v.StringLength(tt.value, "field", tt.min, tt.max) err := v.Error() if (err != nil) != tt.wantErr { t.Errorf("StringLength(%q, %d, %d) error = %v, wantErr %v", tt.value, tt.min, tt.max, err, tt.wantErr) } if tt.wantErr && tt.errMsg != "" { verr := v.errors[0] if verr.Message != tt.errMsg { t.Errorf("expected message %q, got %q", tt.errMsg, verr.Message) } } }) } } func TestValidator_Pattern(t *testing.T) { alphaOnly := regexp.MustCompile(`^[a-zA-Z]+$`) tests := []struct { name string value string wantErr bool }{ {"valid", "hello", false}, {"empty (skipped)", "", false}, {"invalid", "hello123", true}, {"with spaces", "hello world", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := New() v.Pattern(tt.value, "field", alphaOnly, "must contain only letters") if (v.Error() != nil) != tt.wantErr { t.Errorf("Pattern(%q) error = %v, wantErr %v", tt.value, v.Error(), tt.wantErr) } }) } } func TestValidator_Custom(t *testing.T) { tests := []struct { name string valid bool wantErr bool }{ {"valid", true, false}, {"invalid", false, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := New() v.Custom(tt.valid, "field", "custom error") if (v.Error() != nil) != tt.wantErr { t.Errorf("Custom(%v) error = %v, wantErr %v", tt.valid, v.Error(), tt.wantErr) } }) } } func TestValidator_AddError(t *testing.T) { v := New() v.AddError("field", "custom message") if !v.HasErrors() { t.Error("expected validator to have errors after AddError") } err := v.Error() if err == nil { t.Fatal("expected non-nil error") } verrs := err.(ValidationErrors) if len(verrs) != 1 || verrs[0].Field != "field" || verrs[0].Message != "custom message" { t.Errorf("unexpected error: %v", verrs) } } func TestValidator_Composable(t *testing.T) { // Test chaining multiple validations v := New() v.Required("test", "name"). StringLength("test", "name", 1, 64). Pattern("test", "name", AlphanumericDashUnderscore, "must be alphanumeric") if v.HasErrors() { t.Errorf("expected no errors, got %v", v.Error()) } // Test with failures v2 := New() v2.Required("", "name"). Required("", "email"). StringLength("verylongnamethatshouldexceedlimit", "description", 0, 10) if !v2.HasErrors() { t.Error("expected validation errors") } errs := v2.Errors() if len(errs) != 3 { t.Errorf("expected 3 errors, got %d", len(errs)) } } func TestValidator_Errors(t *testing.T) { v := New() v.Required("", "field1") v.Required("", "field2") errs := v.Errors() if len(errs) != 2 { t.Errorf("expected 2 errors, got %d", len(errs)) } } // --- Standalone function tests --- func TestRequired(t *testing.T) { tests := []struct { name string value string wantErr bool }{ {"valid", "hello", false}, {"empty", "", true}, {"whitespace", " ", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := Required(tt.value, "field") if (err != nil) != tt.wantErr { t.Errorf("Required(%q) error = %v, wantErr %v", tt.value, err, tt.wantErr) } }) } } func TestRequiredSlice(t *testing.T) { if err := RequiredSlice([]string{"a"}, "field"); err != nil { t.Errorf("expected no error, got %v", err) } if err := RequiredSlice([]string{}, "field"); err == nil { t.Error("expected error for empty slice") } if err := RequiredSlice(nil, "field"); err == nil { t.Error("expected error for nil slice") } } func TestStringLength(t *testing.T) { if err := StringLength("hello", "field", 1, 10); err != nil { t.Errorf("expected no error, got %v", err) } if err := StringLength("a", "field", 2, 10); err == nil { t.Error("expected error for too short string") } if err := StringLength("hello world", "field", 1, 5); err == nil { t.Error("expected error for too long string") } } func TestPattern(t *testing.T) { alphaOnly := regexp.MustCompile(`^[a-zA-Z]+$`) if err := Pattern("hello", "field", alphaOnly, "must be alpha"); err != nil { t.Errorf("expected no error, got %v", err) } if err := Pattern("hello123", "field", alphaOnly, "must be alpha"); err == nil { t.Error("expected error for invalid pattern") } // Empty string should pass (use Required for that check) if err := Pattern("", "field", alphaOnly, "must be alpha"); err != nil { t.Errorf("expected no error for empty string, got %v", err) } } // --- Common patterns tests --- func TestCommonPatterns(t *testing.T) { tests := []struct { name string pattern *regexp.Regexp valid []string invalid []string }{ { name: "AlphanumericDashUnderscore", pattern: AlphanumericDashUnderscore, valid: []string{"hello", "hello-world", "hello_world", "HelloWorld123", "a", "A1"}, invalid: []string{"hello world", "hello.world", "@test", ""}, }, { name: "AlphanumericDashUnderscoreDot", pattern: AlphanumericDashUnderscoreDot, valid: []string{"hello", "hello.world", "config.yaml", "test-file_v1.2"}, invalid: []string{"hello world", "@test", "path/to/file"}, }, { name: "Email", pattern: Email, valid: []string{"test@example.com", "user.name@domain.org", "a@b.co"}, invalid: []string{"@example.com", "test@", "test@.com", "test"}, }, { name: "UUID", pattern: UUID, valid: []string{"550e8400-e29b-41d4-a716-446655440000", "ABCD1234-EF56-7890-ABCD-EF1234567890"}, invalid: []string{"550e8400", "not-a-uuid", "550e8400-e29b-41d4-a716-44665544000"}, }, { name: "Slug", pattern: Slug, valid: []string{"hello", "hello-world", "my-blog-post", "a1b2"}, invalid: []string{"Hello", "hello_world", "hello--world", "-hello", "hello-"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { for _, v := range tt.valid { if !tt.pattern.MatchString(v) { t.Errorf("%s: expected %q to match", tt.name, v) } } for _, v := range tt.invalid { if tt.pattern.MatchString(v) { t.Errorf("%s: expected %q to not match", tt.name, v) } } }) } } func TestName(t *testing.T) { tests := []struct { name string value string wantErr bool }{ {"valid", "my-config", false}, {"valid with underscore", "my_config", false}, {"valid alphanumeric", "config123", false}, {"empty", "", true}, {"too long", "this-name-is-way-too-long-and-exceeds-the-sixty-four-character-limit-allowed", true}, {"invalid chars", "my config", true}, {"dots not allowed", "my.config", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := Name(tt.value, "name") if (err != nil) != tt.wantErr { t.Errorf("Name(%q) error = %v, wantErr %v", tt.value, err, tt.wantErr) } }) } } func TestIsValidationError(t *testing.T) { tests := []struct { name string err error expected bool }{ {"nil", nil, false}, {"ValidationError", ValidationError{Field: "f", Message: "m"}, true}, {"ValidationErrors", ValidationErrors{{Field: "f", Message: "m"}}, true}, {"other error", errOther, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := IsValidationError(tt.err); got != tt.expected { t.Errorf("IsValidationError(%v) = %v, want %v", tt.err, got, tt.expected) } }) } } // Custom error type for testing type otherError struct{} func (e otherError) Error() string { return "other error" } var errOther error = otherError{} func TestAsValidationErrors(t *testing.T) { tests := []struct { name string err error expectNil bool expectCount int }{ {"nil", nil, true, 0}, {"ValidationError", ValidationError{Field: "f", Message: "m"}, false, 1}, {"ValidationErrors", ValidationErrors{{Field: "f1", Message: "m1"}, {Field: "f2", Message: "m2"}}, false, 2}, {"other error", errOther, true, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := AsValidationErrors(tt.err) if tt.expectNil { if got != nil { t.Errorf("expected nil, got %v", got) } } else { if got == nil { t.Error("expected non-nil ValidationErrors") } else if len(got) != tt.expectCount { t.Errorf("expected %d errors, got %d", tt.expectCount, len(got)) } } }) } } // --- Integration-style tests --- func TestValidator_RealWorldUsage(t *testing.T) { // Simulating the CreateKeyRequest validation from keys.go type CreateKeyRequest struct { Name string Scopes []string } validate := func(req CreateKeyRequest) error { v := New() v.Required(req.Name, "name") v.RequiredSlice(req.Scopes, "scopes") return v.Error() } // Valid request if err := validate(CreateKeyRequest{Name: "my-key", Scopes: []string{"read"}}); err != nil { t.Errorf("expected no error for valid request, got %v", err) } // Missing name if err := validate(CreateKeyRequest{Name: "", Scopes: []string{"read"}}); err == nil { t.Error("expected error for missing name") } // Missing scopes if err := validate(CreateKeyRequest{Name: "my-key", Scopes: []string{}}); err == nil { t.Error("expected error for missing scopes") } // Both missing err := validate(CreateKeyRequest{Name: "", Scopes: []string{}}) if err == nil { t.Error("expected error for missing name and scopes") } verrs := AsValidationErrors(err) if len(verrs) != 2 { t.Errorf("expected 2 validation errors, got %d", len(verrs)) } } func TestValidator_ConfigItemValidation(t *testing.T) { // Simulating the ConfigItemRequest validation from claude_config.go type ConfigItemRequest struct { Name string Content string } validate := func(req ConfigItemRequest) error { v := New() v.Required(req.Name, "name") v.Required(req.Content, "content") v.StringLength(req.Name, "name", 1, 64) v.Pattern(req.Name, "name", AlphanumericDashUnderscore, "must be alphanumeric with dashes or underscores") return v.Error() } // Valid request if err := validate(ConfigItemRequest{Name: "my-skill", Content: "# Skill content"}); err != nil { t.Errorf("expected no error for valid request, got %v", err) } // Invalid name pattern err := validate(ConfigItemRequest{Name: "my skill", Content: "content"}) if err == nil { t.Error("expected error for invalid name pattern") } // Name too long longName := "this-name-is-way-too-long-and-exceeds-the-sixty-four-character-limit" err = validate(ConfigItemRequest{Name: longName, Content: "content"}) if err == nil { t.Error("expected error for name too long") } }