package valid import ( "errors" "strings" "testing" "code.nochebuena.dev/go/xerrors" playground "github.com/go-playground/validator/v10" ) type testUser struct { Name string `validate:"required"` Email string `validate:"required,email"` Age int `validate:"min=18,max=120"` } // --------------------------------------------------------------- // Constructor tests // --------------------------------------------------------------- func TestNew_Defaults(t *testing.T) { v := New() if v == nil { t.Fatal("New() returned nil") } } func TestNew_WithMessageProvider(t *testing.T) { called := false mp := &testMP{fn: func(field, tag, param string) string { called = true return "custom: " + field }} v := New(WithMessageProvider(mp)) _ = v.Struct(testUser{}) // triggers validation failure (missing Name/Email) if !called { t.Error("custom MessageProvider was not called") } } // --------------------------------------------------------------- // Struct validation tests // --------------------------------------------------------------- func TestValidator_Struct_Valid(t *testing.T) { v := New() u := testUser{Name: "Alice", Email: "alice@example.com", Age: 25} if err := v.Struct(u); err != nil { t.Errorf("expected nil, got %v", err) } } func TestValidator_Struct_MissingRequired(t *testing.T) { v := New() u := testUser{Email: "a@b.com", Age: 20} // Name is missing err := v.Struct(u) if err == nil { t.Fatal("expected error, got nil") } var xe *xerrors.Err if !errors.As(err, &xe) { t.Fatalf("expected *xerrors.Err, got %T", err) } if xe.Code() != xerrors.ErrInvalidInput { t.Errorf("code = %s, want %s", xe.Code(), xerrors.ErrInvalidInput) } if xe.Fields()["field"] != "Name" { t.Errorf("field = %v, want Name", xe.Fields()["field"]) } } func TestValidator_Struct_InvalidEmail(t *testing.T) { v := New() u := testUser{Name: "Bob", Email: "not-an-email", Age: 25} err := v.Struct(u) if err == nil { t.Fatal("expected error, got nil") } var xe *xerrors.Err if !errors.As(err, &xe) { t.Fatalf("expected *xerrors.Err, got %T", err) } if xe.Fields()["tag"] != "email" { t.Errorf("tag = %v, want email", xe.Fields()["tag"]) } } func TestValidator_Struct_BelowMin(t *testing.T) { v := New() u := testUser{Name: "Carol", Email: "carol@example.com", Age: 10} err := v.Struct(u) if err == nil { t.Fatal("expected error, got nil") } var xe *xerrors.Err if !errors.As(err, &xe) { t.Fatalf("expected *xerrors.Err, got %T", err) } if xe.Code() != xerrors.ErrInvalidInput { t.Errorf("code = %s, want %s", xe.Code(), xerrors.ErrInvalidInput) } } func TestValidator_Struct_AboveMax(t *testing.T) { v := New() u := testUser{Name: "Dave", Email: "dave@example.com", Age: 200} err := v.Struct(u) if err == nil { t.Fatal("expected error, got nil") } var xe *xerrors.Err if !errors.As(err, &xe) { t.Fatalf("expected *xerrors.Err, got %T", err) } if xe.Code() != xerrors.ErrInvalidInput { t.Errorf("code = %s, want %s", xe.Code(), xerrors.ErrInvalidInput) } } func TestValidator_Struct_NotAStruct(t *testing.T) { v := New() err := v.Struct("not a struct") if err == nil { t.Fatal("expected error, got nil") } var xe *xerrors.Err if !errors.As(err, &xe) { t.Fatalf("expected *xerrors.Err, got %T", err) } if xe.Code() != xerrors.ErrInternal { t.Errorf("code = %s, want %s", xe.Code(), xerrors.ErrInternal) } } func TestValidator_Struct_ErrorCode(t *testing.T) { v := New() u := testUser{} // all fields fail err := v.Struct(u) var xe *xerrors.Err if !errors.As(err, &xe) { t.Fatalf("errors.As failed: %T", err) } if xe.Code() != xerrors.ErrInvalidInput { t.Errorf("Code() = %s, want %s", xe.Code(), xerrors.ErrInvalidInput) } } func TestValidator_Struct_Fields(t *testing.T) { v := New() u := testUser{} // missing Name triggers required err := v.Struct(u) var xe *xerrors.Err errors.As(err, &xe) fields := xe.Fields() if _, ok := fields["field"]; !ok { t.Error("Fields() missing 'field' key") } if _, ok := fields["tag"]; !ok { t.Error("Fields() missing 'tag' key") } } func TestValidator_Struct_Unwrap(t *testing.T) { v := New() u := testUser{} // triggers validation error err := v.Struct(u) var xe *xerrors.Err errors.As(err, &xe) // The wrapped error must be a playground.ValidationErrors. cause := errors.Unwrap(xe) if cause == nil { t.Fatal("Unwrap returned nil") } var ve playground.ValidationErrors if !errors.As(cause, &ve) { t.Errorf("Unwrap should return validator.ValidationErrors, got %T", cause) } } func TestValidator_Struct_SpanishMessages(t *testing.T) { v := New(WithMessageProvider(SpanishMessages)) u := testUser{Email: "a@b.com", Age: 20} // missing Name err := v.Struct(u) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), "obligatorio") { t.Errorf("expected Spanish message, got: %s", err.Error()) } } func TestValidator_Struct_EnglishMessage_Required(t *testing.T) { v := New() u := testUser{Email: "a@b.com", Age: 20} // missing Name err := v.Struct(u) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), "required") { t.Errorf("expected English 'required' message, got: %s", err.Error()) } } // --------------------------------------------------------------- // MessageProvider tests // --------------------------------------------------------------- func TestDefaultMessages_AllTags(t *testing.T) { mp := DefaultMessages tags := []struct { tag string param string }{ {"required", ""}, {"email", ""}, {"min", "5"}, {"max", "100"}, {"unknown_rule", ""}, } for _, tt := range tags { msg := mp.Message("Field", tt.tag, tt.param) if msg == "" { t.Errorf("DefaultMessages.Message(%q) returned empty string", tt.tag) } } } func TestSpanishMessages_AllTags(t *testing.T) { mp := SpanishMessages tags := []struct { tag string param string }{ {"required", ""}, {"email", ""}, {"min", "5"}, {"max", "100"}, {"unknown_rule", ""}, } for _, tt := range tags { msg := mp.Message("Campo", tt.tag, tt.param) if msg == "" { t.Errorf("SpanishMessages.Message(%q) returned empty string", tt.tag) } } } func TestWithMessageProvider_CustomImpl(t *testing.T) { mp := &testMP{fn: func(field, tag, param string) string { return "CUSTOM:" + field + ":" + tag }} v := New(WithMessageProvider(mp)) u := testUser{Email: "a@b.com", Age: 20} // missing Name err := v.Struct(u) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), "CUSTOM:") { t.Errorf("expected custom message prefix, got: %s", err.Error()) } } // --------------------------------------------------------------- // Helpers // --------------------------------------------------------------- type testMP struct { fn func(field, tag, param string) string } func (m *testMP) Message(field, tag, param string) string { return m.fn(field, tag, param) }