Validate cookie policy link in banner description
Translations are no longer part of the version snapshot, so the
banner_description text must be validated at write time to ensure
the {{cookie_policy_link}} placeholder is present. Without it
the cookie policy URL silently disappears from the rendered banner.
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -328,7 +328,12 @@ func (r *UpsertCookieBannerTranslationRequest) Validate() error {
|
|||||||
if json.Unmarshal(raw, &s) != nil {
|
if json.Unmarshal(raw, &s) != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
v.Check(s, "translations."+key, validator.NoHTML(), validator.MaxLen(2000))
|
|
||||||
|
validators := []validator.ValidatorFunc{validator.NoHTML(), validator.MaxLen(2000)}
|
||||||
|
if key == "banner_description" {
|
||||||
|
validators = append(validators, validator.ContainsSubstring("{{cookie_policy_link}}"))
|
||||||
|
}
|
||||||
|
v.Check(s, "translations."+key, validators...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return v.Error()
|
return v.Error()
|
||||||
|
|||||||
@@ -126,6 +126,68 @@ func TestSnapshotsEqual(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpsertCookieBannerTranslationRequest_Validate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
bannerID := gid.New(gid.NewTenantID(), coredata.CookieBannerEntityType)
|
||||||
|
|
||||||
|
t.Run("banner_description with cookie_policy_link passes", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
translations, err := json.Marshal(map[string]string{
|
||||||
|
"banner_title": "Cookie Preferences",
|
||||||
|
"banner_description": "We use cookies. {{cookie_policy_link}}",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := UpsertCookieBannerTranslationRequest{
|
||||||
|
CookieBannerID: bannerID,
|
||||||
|
Language: "en",
|
||||||
|
Translations: translations,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, req.Validate())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("banner_description without cookie_policy_link fails", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
translations, err := json.Marshal(map[string]string{
|
||||||
|
"banner_title": "Cookie Preferences",
|
||||||
|
"banner_description": "We use cookies to improve your experience.",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := UpsertCookieBannerTranslationRequest{
|
||||||
|
CookieBannerID: bannerID,
|
||||||
|
Language: "en",
|
||||||
|
Translations: translations,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = req.Validate()
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "translations.banner_description")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("translations without banner_description passes", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
translations, err := json.Marshal(map[string]string{
|
||||||
|
"banner_title": "Cookie Preferences",
|
||||||
|
"button_save": "Save",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req := UpsertCookieBannerTranslationRequest{
|
||||||
|
CookieBannerID: bannerID,
|
||||||
|
Language: "en",
|
||||||
|
Translations: translations,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, req.Validate())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildSnapshot_RankInvariant(t *testing.T) {
|
func TestBuildSnapshot_RankInvariant(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,30 @@ func MaxLen(maxLength int) ValidatorFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContainsSubstring validates that a string contains the specified substring.
|
||||||
|
func ContainsSubstring(substr string) ValidatorFunc {
|
||||||
|
return func(value any) *ValidationError {
|
||||||
|
actualValue, isNil := dereferenceValue(value)
|
||||||
|
if isNil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
str, ok := actualValue.(string)
|
||||||
|
if !ok {
|
||||||
|
return newValidationError(ErrorCodeInvalidFormat, "value must be a string")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(str, substr) {
|
||||||
|
return newValidationError(
|
||||||
|
ErrorCodeInvalidFormat,
|
||||||
|
fmt.Sprintf("must contain %q", substr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// OneOfSlice validates that a value is one of the allowed values in the slice.
|
// OneOfSlice validates that a value is one of the allowed values in the slice.
|
||||||
// Accepts a slice of any type. Compares by value first, then by string representation.
|
// Accepts a slice of any type. Compares by value first, then by string representation.
|
||||||
func OneOfSlice[T any](allowed []T) ValidatorFunc {
|
func OneOfSlice[T any](allowed []T) ValidatorFunc {
|
||||||
|
|||||||
@@ -67,6 +67,31 @@ func TestMaxLen(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainsSubstring(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value any
|
||||||
|
substr string
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{"contains substring", "hello {{cookie_policy_link}} world", "{{cookie_policy_link}}", false},
|
||||||
|
{"missing substring", "hello world", "{{cookie_policy_link}}", true},
|
||||||
|
{"exact match", "{{cookie_policy_link}}", "{{cookie_policy_link}}", false},
|
||||||
|
{"empty string", "", "{{cookie_policy_link}}", true},
|
||||||
|
{"nil pointer", (*string)(nil), "{{cookie_policy_link}}", false},
|
||||||
|
{"non-string", 123, "foo", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := ContainsSubstring(tt.substr)(tt.value)
|
||||||
|
if (err != nil) != tt.wantError {
|
||||||
|
t.Errorf("ContainsSubstring() error = %v, wantError %v", err, tt.wantError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOneOf(t *testing.T) {
|
func TestOneOf(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user