package server import ( "strings" "testing" "golang.org/x/net/html" "github.com/sartoopjj/thefeed/internal/protocol" ) func TestParsePublicMessages(t *testing.T) { body := []byte(`
hello
world
photo caption
`) msgs, err := parsePublicMessages(body) if err != nil { t.Fatalf("parsePublicMessages: %v", err) } if len(msgs) != 3 { t.Fatalf("len(msgs) = %d, want 3", len(msgs)) } // Photo with caption (newest first) if msgs[0].ID != 106 { t.Fatalf("msgs[0].ID = %d, want 106", msgs[0].ID) } want := protocol.MediaImage + "\n" + "photo caption" if msgs[0].Text != want { t.Fatalf("msgs[0].Text = %q, want %q", msgs[0].Text, want) } // Photo only if msgs[1].ID != 105 { t.Fatalf("msgs[1].ID = %d, want 105", msgs[1].ID) } if msgs[1].Text != protocol.MediaImage { t.Fatalf("msgs[1].Text = %q, want %q", msgs[1].Text, protocol.MediaImage) } // Text only if msgs[2].ID != 101 { t.Fatalf("msgs[2].ID = %d, want 101", msgs[2].ID) } if msgs[2].Text != "hello\nworld" { t.Fatalf("msgs[2].Text = %q, want %q", msgs[2].Text, "hello\nworld") } } func TestParsePublicMessagesNoLimit(t *testing.T) { body := []byte(`
one
two
three
`) msgs, err := parsePublicMessages(body) if err != nil { t.Fatalf("parsePublicMessages: %v", err) } if len(msgs) != 3 { t.Fatalf("len(msgs) = %d, want 3", len(msgs)) } if msgs[0].ID != 3 || msgs[1].ID != 2 || msgs[2].ID != 1 { t.Fatalf("got ids %d,%d,%d want 3,2,1", msgs[0].ID, msgs[1].ID, msgs[2].ID) } } func TestMergeMessages(t *testing.T) { old := []protocol.Message{ {ID: 100, Timestamp: 1000, Text: "old100"}, {ID: 99, Timestamp: 999, Text: "old99"}, } newMsgs := []protocol.Message{ {ID: 101, Timestamp: 1001, Text: "new101"}, {ID: 100, Timestamp: 1000, Text: "edited100"}, } merged := mergeMessages(old, newMsgs) if len(merged) != 3 { t.Fatalf("len(merged) = %d, want 3", len(merged)) } if merged[0].ID != 101 { t.Fatalf("merged[0].ID = %d, want 101", merged[0].ID) } if merged[1].Text != "edited100" { t.Fatalf("merged[1].Text = %q, want edited100", merged[1].Text) } if merged[2].ID != 99 { t.Fatalf("merged[2].ID = %d, want 99", merged[2].ID) } } func TestParsePublicMessagesAlbumStacksHeaders(t *testing.T) { // Album = one data-post with N nested photo wraps. We must emit N // stacked [IMAGE] headers so albumSpan suppresses the absorbed-sibling // "1 missed" gap. body := []byte(`
album caption
`) msgs, sources, err := parsePublicMessagesWithMedia(body) if err != nil { t.Fatalf("parsePublicMessagesWithMedia: %v", err) } if len(msgs) != 1 { t.Fatalf("len(msgs) = %d, want 1", len(msgs)) } wantText := "[IMAGE]\n[IMAGE]\n[IMAGE]\nalbum caption" if msgs[0].Text != wantText { t.Fatalf("msgs[0].Text = %q, want %q", msgs[0].Text, wantText) } if len(sources) != 1 { t.Fatalf("len(sources) = %d, want 1", len(sources)) } src := sources[0] if src.tag != protocol.MediaImage { t.Errorf("src.tag = %q, want %q", src.tag, protocol.MediaImage) } if src.url != "https://cdn.telegram.org/img1.jpg" { t.Errorf("src.url = %q, want first photo URL", src.url) } if len(src.extraURLs) != 2 || src.extraURLs[0] != "https://cdn.telegram.org/img2.jpg" || src.extraURLs[1] != "https://cdn.telegram.org/img3.jpg" { t.Errorf("src.extraURLs = %v, want [img2, img3]", src.extraURLs) } } func TestParsePublicMessagesSinglePhotoUnchanged(t *testing.T) { // Single photo: one [IMAGE] header, no extraURLs. body := []byte(`
just one
`) msgs, sources, err := parsePublicMessagesWithMedia(body) if err != nil { t.Fatalf("parsePublicMessagesWithMedia: %v", err) } if len(msgs) != 1 { t.Fatalf("len(msgs) = %d, want 1", len(msgs)) } wantText := "[IMAGE]\njust one" if msgs[0].Text != wantText { t.Fatalf("msgs[0].Text = %q, want %q", msgs[0].Text, wantText) } if sources[0].url != "https://cdn.telegram.org/single.jpg" || len(sources[0].extraURLs) != 0 { t.Errorf("source = %+v, want url=single, extraURLs empty", sources[0]) } } func TestParsePublicMessagesReplyPreviewUsesMainBody(t *testing.T) { body := []byte(`
old replied message preview
this is the real new post
`) msgs, err := parsePublicMessages(body) if err != nil { t.Fatalf("parsePublicMessages: %v", err) } if len(msgs) != 1 { t.Fatalf("len(msgs) = %d, want 1", len(msgs)) } if msgs[0].Text != "[REPLY]\nthis is the real new post" { t.Fatalf("msgs[0].Text = %q, want %q", msgs[0].Text, "[REPLY]\nthis is the real new post") } } func TestParsePublicMessagesReplyWithID(t *testing.T) { body := []byte(`
original post
my reply text
`) msgs, err := parsePublicMessages(body) if err != nil { t.Fatalf("parsePublicMessages: %v", err) } if len(msgs) != 1 { t.Fatalf("len(msgs) = %d, want 1", len(msgs)) } want := "[REPLY]:300\nmy reply text" if msgs[0].Text != want { t.Fatalf("msgs[0].Text = %q, want %q", msgs[0].Text, want) } } func TestParsePublicMessagesPoll(t *testing.T) { body := []byte(`
What is your favorite color?
Red
Blue
Green
`) msgs, err := parsePublicMessages(body) if err != nil { t.Fatalf("parsePublicMessages: %v", err) } if len(msgs) != 1 { t.Fatalf("len(msgs) = %d, want 1", len(msgs)) } want := "[POLL]\nšŸ“Š What is your favorite color?\nā—‹ Red\nā—‹ Blue\nā—‹ Green" if msgs[0].Text != want { t.Fatalf("msgs[0].Text = %q, want %q", msgs[0].Text, want) } } func TestExtractMessageTextPreservesLinks(t *testing.T) { htmlStr := `
Check out this link for details
` doc, err := html.Parse(strings.NewReader(htmlStr)) if err != nil { t.Fatalf("html.Parse: %v", err) } node := findFirstByClass(doc, "tgme_widget_message_text") text := extractMessageText(node) want := "Check out [this link](https://example.com) for details" if text != want { t.Fatalf("extractMessageText = %q, want %q", text, want) } } func TestExtractMessageTextBareURL(t *testing.T) { htmlStr := `
Visit https://example.com now
` doc, err := html.Parse(strings.NewReader(htmlStr)) if err != nil { t.Fatalf("html.Parse: %v", err) } node := findFirstByClass(doc, "tgme_widget_message_text") text := extractMessageText(node) want := "Visit https://example.com now" if text != want { t.Fatalf("extractMessageText = %q, want %q", text, want) } } func TestExtractMessageTextRejectsJavascriptURL(t *testing.T) { htmlStr := `
click me
` doc, err := html.Parse(strings.NewReader(htmlStr)) if err != nil { t.Fatalf("html.Parse: %v", err) } node := findFirstByClass(doc, "tgme_widget_message_text") text := extractMessageText(node) // javascript: URLs should be stripped — only text remains want := "click me" if text != want { t.Fatalf("extractMessageText = %q, want %q", text, want) } } func TestExtractMessageTextRejectsDataURL(t *testing.T) { htmlStr := `
link
` doc, err := html.Parse(strings.NewReader(htmlStr)) if err != nil { t.Fatalf("html.Parse: %v", err) } node := findFirstByClass(doc, "tgme_widget_message_text") text := extractMessageText(node) // data: URLs should be stripped — only text remains want := "link" if text != want { t.Fatalf("extractMessageText = %q, want %q", text, want) } } func TestParsePublicMessagesUnsupportedMedia(t *testing.T) { // Real Telegram HTML for polls/quizzes in public view: no poll widget, // just a "message_media_not_supported" div, and no message body text. body := []byte(`
Please open Telegram to view this post
VIEW IN TELEGRAM
`) msgs, err := parsePublicMessages(body) if err != nil { t.Fatalf("parsePublicMessages: %v", err) } if len(msgs) != 1 { t.Fatalf("len(msgs) = %d, want 1", len(msgs)) } if msgs[0].ID != 181 { t.Fatalf("msgs[0].ID = %d, want 181", msgs[0].ID) } if msgs[0].Text != protocol.MediaPoll { t.Fatalf("msgs[0].Text = %q, want %q", msgs[0].Text, protocol.MediaPoll) } } // Regression test for https://t.me/networkti/239 — a normal text post that // also contains Telegram Premium custom emojis was being mis-tagged as a // [POLL] because Telegram's public web view emits a // "message_media_not_supported" element for premium emojis it can't render. // We must NOT prefix such messages with [POLL] when there is real body text. func TestParsePublicMessagesPremiumEmojiNotPoll(t *testing.T) { body := []byte(`
salam this is a normal post with premium emoji
Please open Telegram to view this post
VIEW IN TELEGRAM
`) msgs, err := parsePublicMessages(body) if err != nil { t.Fatalf("parsePublicMessages: %v", err) } if len(msgs) != 1 { t.Fatalf("len(msgs) = %d, want 1", len(msgs)) } if msgs[0].ID != 239 { t.Fatalf("msgs[0].ID = %d, want 239", msgs[0].ID) } if strings.Contains(msgs[0].Text, protocol.MediaPoll) { t.Fatalf("premium-emoji message tagged as poll: %q", msgs[0].Text) } want := "salam this is a normal post with premium emoji" if msgs[0].Text != want { t.Fatalf("msgs[0].Text = %q, want %q", msgs[0].Text, want) } }