Files
libredesk/internal/stringutil/stringutil_test.go
T
Abhinav Raut 72c3f29830 fix hyperlinks getting lost across AI features
AI grammar fix rewrote the draft as plain text, so links in the reply box were dropped. It now sends the editor HTML and asks the model to keep tags. Conversation transcripts, the AI agent history and copilot context also stripped link URLs when converting HTML to text, so the model never saw them. They now keep links as "text ( url )".
2026-07-31 16:08:19 +05:30

330 lines
8.5 KiB
Go

package stringutil
import (
"testing"
"time"
)
func TestRemoveItemByValue(t *testing.T) {
tests := []struct {
name string
input []string
remove string
expected []string
}{
{
name: "empty slice",
input: []string{},
remove: "a",
expected: []string{},
},
{
name: "no matches",
input: []string{"b", "c"},
remove: "a",
expected: []string{"b", "c"},
},
{
name: "single match",
input: []string{"a", "b", "c"},
remove: "b",
expected: []string{"a", "c"},
},
{
name: "multiple matches",
input: []string{"a", "b", "a", "c", "a"},
remove: "a",
expected: []string{"b", "c"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := RemoveItemByValue(tt.input, tt.remove)
if len(result) != len(tt.expected) {
t.Errorf("got len %d, want %d", len(result), len(tt.expected))
}
for i := range result {
if result[i] != tt.expected[i] {
t.Errorf("at index %d got %s, want %s", i, result[i], tt.expected[i])
}
}
})
}
}
func TestFormatDuration(t *testing.T) {
tests := []struct {
name string
duration time.Duration
includeSeconds bool
expected string
}{
{
name: "zero duration with seconds",
duration: 0,
includeSeconds: true,
expected: "0 minutes",
},
{
name: "hours only",
duration: 2 * time.Hour,
includeSeconds: false,
expected: "2 hours 0 minutes",
},
{
name: "hours and minutes",
duration: 2*time.Hour + 30*time.Minute,
includeSeconds: false,
expected: "2 hours 30 minutes",
},
{
name: "full duration with seconds",
duration: 2*time.Hour + 30*time.Minute + 15*time.Second,
includeSeconds: true,
expected: "2 hours 30 minutes 15 seconds",
},
{
name: "full duration without seconds",
duration: 2*time.Hour + 30*time.Minute + 15*time.Second,
includeSeconds: false,
expected: "2 hours 30 minutes",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FormatDuration(tt.duration, tt.includeSeconds)
if result != tt.expected {
t.Errorf("got %q, want %q", result, tt.expected)
}
})
}
}
func TestExtractConvUUID(t *testing.T) {
tests := []struct {
name string
email string
expected string
}{
{
name: "valid UUID v4",
email: "support+conv-13216cf7-6626-4b0d-a938-46ce65a20701@domain.com",
expected: "13216cf7-6626-4b0d-a938-46ce65a20701",
},
{
name: "uppercase UUID v4",
email: "support+conv-13216CF7-6626-4B0D-A938-46CE65A20701@domain.com",
expected: "13216CF7-6626-4B0D-A938-46CE65A20701",
},
{
name: "no plus addressing",
email: "support@domain.com",
expected: "",
},
{
name: "non-conv plus addressing",
email: "support+other@domain.com",
expected: "",
},
{
name: "short non-UUID (user email)",
email: "support+conv-21321@domain.com",
expected: "",
},
{
name: "invalid UUID format",
email: "support+conv-abc123-def456@domain.com",
expected: "",
},
{
name: "missing 4 in UUID (invalid v4)",
email: "support+conv-13216cf7-6626-ab0d-a938-46ce65a20701@domain.com",
expected: "",
},
{
name: "empty string",
email: "",
expected: "",
},
{
name: "missing @ symbol",
email: "support+conv-13216cf7-6626-4b0d-a938-46ce65a20701",
expected: "",
},
{
name: "UUID with extra chars",
email: "support+conv-13216cf7-6626-4b0d-a938-46ce65a20701-extra@domain.com",
expected: "",
},
{
name: "valid UUID different local part",
email: "inbox+conv-a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d@example.org",
expected: "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExtractConvUUID(tt.email)
if result != tt.expected {
t.Errorf("ExtractConvUUID(%q) = %q, want %q", tt.email, result, tt.expected)
}
})
}
}
func TestExtractReferenceNumber(t *testing.T) {
tests := []struct {
name string
subject string
expected string
}{
{
name: "simple reference number",
subject: "Test - #392",
expected: "392",
},
{
name: "with RE prefix",
subject: "RE: Test - #392",
expected: "392",
},
{
name: "multiple hashes picks last",
subject: "Order #123 - #392",
expected: "392",
},
{
name: "no reference number",
subject: "Just a regular subject",
expected: "",
},
{
name: "hash without number",
subject: "Test #abc",
expected: "",
},
{
name: "empty string",
subject: "",
expected: "",
},
{
name: "number without hash",
subject: "Test 392",
expected: "",
},
{
name: "multiple RE prefixes",
subject: "RE: RE: Test - #100",
expected: "100",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExtractReferenceNumber(tt.subject)
if result != tt.expected {
t.Errorf("ExtractReferenceNumber(%q) = %q, want %q", tt.subject, result, tt.expected)
}
})
}
}
func TestSanitizeUTF8(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"empty", "", ""},
{"plain ascii unchanged", "Hello, world!", "Hello, world!"},
{"valid copyright unchanged", "© 2026", "© 2026"},
{"orphan 0xa9 replaced", "\xa9 2026 Upstox", " 2026 Upstox"},
{"nul stripped", "a\x00b", "ab"},
{"chinese unchanged", "你好世界", "你好世界"},
{"devanagari unchanged", "नमस्ते", "नमस्ते"},
{"arabic unchanged", "مرحبا", "مرحبا"},
{"emoji unchanged", "ok 😀👍", "ok 😀👍"},
{"accented latin unchanged", "café résumé", "café résumé"},
{"run of invalid bytes collapses to one replacement", "x\xa9\xa9y", "xy"},
{"truncated 3-byte char replaced", "\xe4\xbd", ""},
{"lead byte at end replaced", "abc\xc3", "abc"},
{"overlong encoding replaced", "x\xc0\x80y", "xy"},
{"cp1252 smart quotes replaced", "\x93hi\x94", "hi"},
{"multiple embedded nuls stripped", "a\x00\x00b\x00c", "abc"},
{"nul and invalid byte combined", "a\x00\xa9b", "ab"},
{"valid multibyte preserved around invalid byte", "a你\xa9好b", "a你好b"},
{"bom preserved", "\ufeffhi", "\ufeffhi"},
{"existing replacement char preserved", "ab", "ab"},
{"crlf and tab preserved", "l1\r\nl2\t", "l1\r\nl2\t"},
{"paired continuation kept, orphan replaced", "é\xa9", "},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := SanitizeUTF8(tt.input); got != tt.expected {
t.Errorf("SanitizeUTF8(%q) = %q, want %q", tt.input, got, tt.expected)
}
})
}
}
func TestSplitName(t *testing.T) {
tests := []struct {
name string
input string
wantFirst string
wantLast string
}{
{name: "empty", input: "", wantFirst: "", wantLast: ""},
{name: "whitespace only", input: " ", wantFirst: "", wantLast: ""},
{name: "single name", input: "Cher", wantFirst: "Cher", wantLast: ""},
{name: "first and last", input: "John Doe", wantFirst: "John", wantLast: "Doe"},
{name: "middle name", input: "John Michael Doe", wantFirst: "John", wantLast: "Michael Doe"},
{name: "multi-word surname", input: "Ludwig van der Berg", wantFirst: "Ludwig", wantLast: "van der Berg"},
{name: "extra spaces", input: " John Doe ", wantFirst: "John", wantLast: "Doe"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
first, last := SplitName(tt.input)
if first != tt.wantFirst || last != tt.wantLast {
t.Errorf("SplitName(%q) = (%q, %q), want (%q, %q)", tt.input, first, last, tt.wantFirst, tt.wantLast)
}
})
}
}
func TestHTML2TextWithLinks(t *testing.T) {
tests := []struct {
name string
html string
want string
}{
{
name: "link with distinct text keeps url",
html: `<p>See <a href="https://example.com/guide">the guide</a> for steps.</p>`,
want: "See the guide ( https://example.com/guide ) for steps.",
},
{
name: "link text equal to url not duplicated",
html: `<p><a href="https://example.com">https://example.com</a></p>`,
want: "https://example.com",
},
{
name: "plain text unchanged",
html: `<p>No links here.</p>`,
want: "No links here.",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HTML2TextWithLinks(tt.html); got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}