From 4bfa4242ef40933f5125a4b3d3cf41865d3dc80b Mon Sep 17 00:00:00 2001 From: liangxu Date: Sat, 18 Apr 2026 14:03:07 +0800 Subject: [PATCH] feat(kol): add KOL profile field validators --- .../tenant/app/kol_profile_validate.go | 53 ++++++++++++ .../tenant/app/kol_profile_validate_test.go | 86 +++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 server/internal/tenant/app/kol_profile_validate.go create mode 100644 server/internal/tenant/app/kol_profile_validate_test.go diff --git a/server/internal/tenant/app/kol_profile_validate.go b/server/internal/tenant/app/kol_profile_validate.go new file mode 100644 index 0000000..8a5587d --- /dev/null +++ b/server/internal/tenant/app/kol_profile_validate.go @@ -0,0 +1,53 @@ +package app + +import ( + "strings" + "unicode/utf8" + + "github.com/geo-platform/tenant-api/internal/shared/response" +) + +const ( + kolDisplayNameMaxLen = 40 + kolBioMaxLen = 500 + kolAvatarURLPrefix = "/api/public/assets/" +) + +func validateKolDisplayName(raw string) (string, error) { + v := strings.TrimSpace(raw) + if v == "" { + return "", response.ErrBadRequest(40001, "kol_profile_invalid_display_name", "display_name is required") + } + if utf8.RuneCountInString(v) > kolDisplayNameMaxLen { + return "", response.ErrBadRequest(40001, "kol_profile_invalid_display_name", "display_name must be at most 40 characters") + } + return v, nil +} + +func validateKolBio(raw *string) (*string, error) { + if raw == nil { + return nil, nil + } + v := strings.TrimSpace(*raw) + if v == "" { + return nil, nil + } + if utf8.RuneCountInString(v) > kolBioMaxLen { + return nil, response.ErrBadRequest(40002, "kol_profile_invalid_bio", "bio must be at most 500 characters") + } + return &v, nil +} + +func validateKolAvatarURL(raw *string) (*string, error) { + if raw == nil { + return nil, nil + } + v := strings.TrimSpace(*raw) + if v == "" { + return nil, nil + } + if !strings.HasPrefix(v, kolAvatarURLPrefix) { + return nil, response.ErrBadRequest(40003, "kol_profile_invalid_avatar_url", "avatar_url must be an asset path served by this service") + } + return &v, nil +} diff --git a/server/internal/tenant/app/kol_profile_validate_test.go b/server/internal/tenant/app/kol_profile_validate_test.go new file mode 100644 index 0000000..713ee4f --- /dev/null +++ b/server/internal/tenant/app/kol_profile_validate_test.go @@ -0,0 +1,86 @@ +package app + +import ( + "strings" + "testing" +) + +func TestValidateKolDisplayName(t *testing.T) { + cases := []struct { + name string + input string + wantOK bool + wantVal string + }{ + {"trim spaces", " alice ", true, "alice"}, + {"keep unicode", "张小明", true, "张小明"}, + {"empty rejected", " ", false, ""}, + {"40 chars ok", strings.Repeat("a", 40), true, strings.Repeat("a", 40)}, + {"41 chars rejected", strings.Repeat("a", 41), false, ""}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := validateKolDisplayName(tc.input) + if tc.wantOK && err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !tc.wantOK && err == nil { + t.Fatalf("expected error, got nil (value=%q)", got) + } + if tc.wantOK && got != tc.wantVal { + t.Fatalf("got %q want %q", got, tc.wantVal) + } + }) + } +} + +func TestValidateKolBio(t *testing.T) { + cases := []struct { + name string + input *string + wantOK bool + }{ + {"nil ok", nil, true}, + {"empty trimmed to nil", strPtr(" "), true}, + {"500 chars ok", strPtr(strings.Repeat("b", 500)), true}, + {"501 chars rejected", strPtr(strings.Repeat("b", 501)), false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := validateKolBio(tc.input) + if tc.wantOK && err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !tc.wantOK && err == nil { + t.Fatalf("expected error, got nil") + } + }) + } +} + +func TestValidateKolAvatarURL(t *testing.T) { + cases := []struct { + name string + input *string + wantOK bool + }{ + {"nil ok", nil, true}, + {"empty trimmed to nil", strPtr(" "), true}, + {"asset path ok", strPtr("/api/public/assets/abc.def"), true}, + {"external url rejected", strPtr("https://evil.example.com/a.jpg"), false}, + {"javascript scheme rejected", strPtr("javascript:alert(1)"), false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := validateKolAvatarURL(tc.input) + if tc.wantOK && err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !tc.wantOK && err == nil { + t.Fatalf("expected error, got nil") + } + }) + } +} + +func strPtr(s string) *string { return &s }