Files
tweetdistributor/main_test.go
sirrow e2f8036682 count links as a fixed length against the tweet limit
Twitter rewrites every URL to a t.co address of a fixed 23 characters,
so counting the raw characters rejected messages Twitter would have
accepted: 100 characters of text plus a 50 character YouTube link
counted as 150 and was refused, where Twitter sees 123.

The punctuation trimmed off the end of a link keeps counting as ordinary
text. Only links written with a scheme are recognised; Twitter also
shortens bare hosts like example.com/foo, but detecting those without
mistaking Node.js for a link is not worth it here, and counting them in
full errs toward refusing rather than posting something too long.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:00:22 +09:00

104 lines
3.3 KiB
Go

package main
import (
"bytes"
"image"
"image/color"
"image/jpeg"
"math/rand"
"strings"
"testing"
"tweetdistributor/output"
"unicode/utf8"
)
// noiseJPEG returns a JPEG image of random noise, which compresses poorly
// and reliably exceeds maxImageBytes at large dimensions.
func noiseJPEG(t *testing.T, width, height int) []byte {
t.Helper()
rng := rand.New(rand.NewSource(1))
img := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
img.Set(x, y, color.RGBA{uint8(rng.Intn(256)), uint8(rng.Intn(256)), uint8(rng.Intn(256)), 255})
}
}
var buf bytes.Buffer
if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 95}); err != nil {
t.Fatal(err)
}
return buf.Bytes()
}
func TestTweetLength(t *testing.T) {
tests := []struct {
name string
content string
want int
}{
{"no link", "こんにちは", 5},
{"long link", "https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLabcdefghijklmnop", urlLength},
{"short link", "https://a.jp", urlLength},
{"link with text", "みてね " + strings.Repeat("x", 10) + " https://youtu.be/dQw4w9WgXcQ", 4 + 10 + 1 + urlLength},
{"two links", "https://a.jp https://b.jp", urlLength*2 + 1},
{"trailing punctuation still counts", "https://a.jp。", urlLength + 1},
{"multibyte path", "https://ja.wikipedia.org/wiki/日本語", urlLength},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tweetLength(tt.content); got != tt.want {
t.Errorf("tweetLength(%q) = %d, want %d", tt.content, got, tt.want)
}
})
}
}
func TestTweetLengthAcceptsMessageOnlyLinksMakeTooLong(t *testing.T) {
// A message that the raw rune count would reject but Twitter accepts.
content := strings.Repeat("あ", 100) + " https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=120s"
if raw := utf8.RuneCountInString(content); raw <= maxTweetLength {
t.Fatalf("test message should be %d raw characters, got %d", maxTweetLength+1, raw)
}
if got := tweetLength(content); got > maxTweetLength {
t.Errorf("tweetLength = %d, want it to fit within %d", got, maxTweetLength)
}
}
func TestShrinkImageLarge(t *testing.T) {
data := noiseJPEG(t, 3000, 3000)
if len(data) <= maxImageBytes {
t.Fatalf("test image should exceed maxImageBytes, got %d", len(data))
}
shrunk, err := shrinkImage(output.Image{Data: data, ContentType: "image/jpeg", Filename: "DSC_7277.jpg"})
if err != nil {
t.Fatal(err)
}
if len(shrunk.Data) > maxImageBytes {
t.Errorf("shrunk image is %d bytes, want <= %d", len(shrunk.Data), maxImageBytes)
}
if shrunk.ContentType != "image/jpeg" {
t.Errorf("ContentType = %q, want image/jpeg", shrunk.ContentType)
}
if shrunk.Filename != "DSC_7277.jpg" {
t.Errorf("Filename = %q, want DSC_7277.jpg", shrunk.Filename)
}
if _, _, err := image.Decode(bytes.NewReader(shrunk.Data)); err != nil {
t.Errorf("shrunk image is not decodable: %s", err)
}
}
func TestShrinkImageSmallPassthrough(t *testing.T) {
data := noiseJPEG(t, 100, 100)
img := output.Image{Data: data, ContentType: "image/png", Filename: "small.png"}
shrunk, err := shrinkImage(img)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(shrunk.Data, img.Data) || shrunk.ContentType != img.ContentType || shrunk.Filename != img.Filename {
t.Error("small image should pass through unchanged")
}
}