Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions kustomer.go
Original file line number Diff line number Diff line change
Expand Up @@ -710,21 +710,32 @@ func getResponseBody(resp *http.Response) ([]byte, error) {
return buf.Bytes(), nil
}

// chunkHTMLBody is used to split an HTML body into chunks of 1024 characters that
// are then passed to Kustomer as custom variables in the conversation.
// chunkHTMLBody splits an HTML body into chunks that are passed to Kustomer as
// custom variables in the conversation. Each chunk is at most htmlBodyChunkSize
// characters long, and splits are made at the last '>' boundary so that HTML
// tags are never broken across chunks.
func chunkHTMLBody(input string) map[string]string {
result := make(map[string]string)
var counter int

for start := 0; start < len(input); start += htmlBodyChunkSize {
for start := 0; start < len(input); {
end := start + htmlBodyChunkSize
if end > len(input) {
if end >= len(input) {
end = len(input)
} else {
// Find the last '>' at or before the chunk boundary so we
// never split in the middle of an HTML tag.
lastClose := strings.LastIndex(input[start:end], ">")
if lastClose != -1 {
end = start + lastClose + 1
}
}

key := fmt.Sprintf("htmlContent%dStr", counter)
result[key] = input[start:end]
counter++

start = end
}

return result
Expand Down
79 changes: 79 additions & 0 deletions kustomer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kustomersdk
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
Expand Down Expand Up @@ -176,6 +177,84 @@ var _ = Describe("Kustomer SDK", func() {
})
})

Context("chunkHTMLBody", func() {
It("returns a single chunk when content is under 1024 chars", func() {
msg := "<p>Hello world</p>"
chunks := chunkHTMLBody(msg)
Expect(chunks).To(HaveLen(1))
Expect(chunks["htmlContent0Str"]).To(Equal(msg))
})

It("splits at the last '>' before the 1024-char boundary", func() {
// Build two paragraphs that together exceed 1024 chars.
// The split should land on a '>' boundary, never mid-tag.
first := "<p>" + strings.Repeat("a", 1005) + "</p>" // 1012 chars
second := "<p>" + strings.Repeat("b", 500) + "</p>" // 507 chars
msg := first + second

chunks := chunkHTMLBody(msg)

// Reassemble and verify nothing was lost
reassembled := ""
for i := 0; i < len(chunks); i++ {
key := fmt.Sprintf("htmlContent%dStr", i)
chunk := chunks[key]
reassembled += chunk

// Every chunk must end with '>' or be the last chunk (plain text tail)
if i < len(chunks)-1 {
Expect(chunk[len(chunk)-1]).To(Equal(byte('>')))
}
}
Expect(reassembled).To(Equal(msg))
})

It("does not split in the middle of an HTML tag", func() {
// Place a tag so the naive 1024 split would land inside it
before := strings.Repeat("x", 1020)
tag := "<strong>"
after := "bold</strong>"
msg := before + tag + after

chunks := chunkHTMLBody(msg)

// The first chunk should NOT contain a partial tag
reassembled := ""
for i := 0; i < len(chunks); i++ {
key := fmt.Sprintf("htmlContent%dStr", i)
reassembled += chunks[key]
}
Expect(reassembled).To(Equal(msg))

// First chunk must end at or before the '<' of the tag
Expect(strings.HasSuffix(chunks["htmlContent0Str"], "<stro")).To(BeFalse())
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test asserts wrong suffix, missing actual mid-tag split

Medium Severity

The test "does not split in the middle of an HTML tag" doesn't actually verify its claim. With 1020 xs before <strong>, there's no > anywhere in input[0:1024], so lastClose is -1 and the function falls back to splitting at position 1024 — right in the middle of <strong>. The first chunk ends with <str, which IS a mid-tag split. But the assertion only checks that the suffix isn't <stro (off by one character), so the test passes despite the function doing exactly what it claims not to do.

Fix in Cursor Fix in Web


It("reassembles to the original content", func() {
msg := strings.Repeat("<p>"+strings.Repeat("a", 500)+"</p>", 10)
chunks := chunkHTMLBody(msg)

reassembled := ""
for i := 0; i < len(chunks); i++ {
key := fmt.Sprintf("htmlContent%dStr", i)
reassembled += chunks[key]
}
Expect(reassembled).To(Equal(msg))
})

It("handles content with no HTML tags by falling back to max chunk size", func() {
msg := strings.Repeat("a", 2500)
chunks := chunkHTMLBody(msg)

reassembled := ""
for i := 0; i < len(chunks); i++ {
key := fmt.Sprintf("htmlContent%dStr", i)
reassembled += chunks[key]
}
Expect(reassembled).To(Equal(msg))
})
})

Context("chunkSMSMessage", func() {
It("returns a single element when the message is less than 1600 chars", func() {
msg := "This is a short message"
Expand Down