mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-11-01 17:09:52 +00:00
e19a428dd5
Help center saves now reject what they used to silently drop. name, page_title and meta_description get length caps on the backend, and the zod schema mirrors them so the error shows before the request goes out. A theme that cannot be read returns an error instead of collapsing to an empty theme and wiping the saved appearance. Protocol-relative URLs like //evil.com are discarded, nav links with no label are dropped, and an unknown social platform falls back to website. An announcement link with no text is a form error now, because save drops the whole banner in that case. Added tests for theme normalization. Request validators return envelope errors instead of writing the response themselves, so the handler decides the status code. They also trim names and emails before checking them. Same change in the chat, users and help center handlers. Automation previous_* filters no longer see post-change values. The by-ID path passes nil, so a rule filtering on previous_status will not match instead of comparing a value against itself. The incoming message path drops its fallback for the same reason. Team assignment also stopped firing its automation event when an unrelated step failed, for example fetching the team or applying its SLA. Those errors log and carry on now. Small UI fixes: slug generation strips accents, a language already picked in one row is hidden in the others, and a failed submit scrolls the first error into view without jumping the page.
153 lines
4.7 KiB
Go
153 lines
4.7 KiB
Go
package helpcenter
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/abhinavxd/libredesk/internal/helpcenter/models"
|
|
)
|
|
|
|
func TestSanitizeAssetURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{"absolute https", "https://example.com/a.png", "https://example.com/a.png"},
|
|
{"absolute http", "http://example.com/a.png", "http://example.com/a.png"},
|
|
{"root relative", "/uploads/a.png", "/uploads/a.png"},
|
|
{"site root", "/", "/"},
|
|
{"scheme only", "https://", ""},
|
|
{"trims space", " /uploads/a.png ", "/uploads/a.png"},
|
|
{"protocol relative", "//evil.com/a.png", ""},
|
|
{"javascript", "javascript:alert(1)", ""},
|
|
{"data uri", "data:image/png;base64,AAA", ""},
|
|
{"bare host", "example.com/a.png", ""},
|
|
{"css escape", "/a.png\") ; body{display:none", ""},
|
|
{"empty", "", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := sanitizeAssetURL(tt.input); got != tt.want {
|
|
t.Errorf("sanitizeAssetURL(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSanitizeNavLinks(t *testing.T) {
|
|
got := sanitizeNavLinks([]models.NavLink{
|
|
{Label: "Home", URL: "/"},
|
|
{Label: "", URL: "https://example.com"},
|
|
{Label: " ", URL: "https://example.com"},
|
|
{Label: "Bad", URL: "javascript:alert(1)"},
|
|
{Label: "Offsite", URL: "//evil.com"},
|
|
{Label: " Docs ", URL: "https://example.com/docs"},
|
|
})
|
|
want := []models.NavLink{
|
|
{Label: "Home", URL: "/"},
|
|
{Label: "Docs", URL: "https://example.com/docs"},
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %d links, want %d: %+v", len(got), len(want), got)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("link %d = %+v, want %+v", i, got[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSanitizeSocialLinks(t *testing.T) {
|
|
got := sanitizeSocialLinks([]models.SocialLink{
|
|
{Platform: "github", URL: "https://github.com/x"},
|
|
{Platform: "myspace", URL: "https://myspace.com/x"},
|
|
{Platform: "twitter", URL: "javascript:alert(1)"},
|
|
})
|
|
want := []models.SocialLink{
|
|
{Platform: "github", URL: "https://github.com/x"},
|
|
{Platform: "website", URL: "https://myspace.com/x"},
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %d links, want %d: %+v", len(got), len(want), got)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("link %d = %+v, want %+v", i, got[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalizeThemeRejectsUnreadableTheme(t *testing.T) {
|
|
if _, err := normalizeTheme(json.RawMessage(`{"layout":{"columns":"3"}}`)); err == nil {
|
|
t.Error("a theme with a mistyped field must be rejected, not silently blanked")
|
|
}
|
|
if _, err := normalizeTheme(json.RawMessage(`not json`)); err == nil {
|
|
t.Error("invalid JSON must be rejected")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeThemeDropsUnsafeValues(t *testing.T) {
|
|
raw, err := normalizeTheme(json.RawMessage(`{
|
|
"color": "red",
|
|
"logo_url": "javascript:alert(1)",
|
|
"header": {"background_type": "wat", "background_color": "notacolor"},
|
|
"layout": {"collections": "wat", "columns": 5},
|
|
"cards": {"icon_position": "wat"},
|
|
"announcement": {"text": "", "link_url": "https://example.com", "link_label": "Go"}
|
|
}`))
|
|
if err != nil {
|
|
t.Fatalf("normalizeTheme: %v", err)
|
|
}
|
|
var got models.Theme
|
|
if err := json.Unmarshal(raw, &got); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if got.Color != defaultAccentColor {
|
|
t.Errorf("color = %q, want %q", got.Color, defaultAccentColor)
|
|
}
|
|
if got.LogoURL != "" {
|
|
t.Errorf("logo_url = %q, want empty", got.LogoURL)
|
|
}
|
|
if got.Header.BackgroundType != "" || got.Header.BackgroundColor != "" {
|
|
t.Errorf("header = %+v, want blanked", got.Header)
|
|
}
|
|
if got.Layout.Collections != "" || got.Layout.Columns != 0 {
|
|
t.Errorf("layout = %+v, want blanked", got.Layout)
|
|
}
|
|
if got.Cards.IconPosition != cardIconPositions[0] {
|
|
t.Errorf("icon_position = %q, want %q", got.Cards.IconPosition, cardIconPositions[0])
|
|
}
|
|
if got.Announcement != (models.AnnouncementTheme{}) {
|
|
t.Errorf("announcement = %+v, want cleared when the text is empty", got.Announcement)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeLocales(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
locales []string
|
|
defaultLocale string
|
|
want []string
|
|
}{
|
|
{"default first", []string{"es", "en"}, "en", []string{"en", "es"}},
|
|
{"dedupes", []string{"en", "en", "es"}, "en", []string{"en", "es"}},
|
|
{"trims", []string{" es "}, "en", []string{"en", "es"}},
|
|
{"drops blanks", []string{"", "es"}, "en", []string{"en", "es"}},
|
|
{"adds missing default", []string{"es"}, "fr", []string{"fr", "es"}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := normalizeLocales(tt.locales, tt.defaultLocale)
|
|
if len(got) != len(tt.want) {
|
|
t.Fatalf("got %v, want %v", got, tt.want)
|
|
}
|
|
for i := range tt.want {
|
|
if got[i] != tt.want[i] {
|
|
t.Fatalf("got %v, want %v", got, tt.want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|