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 TestFindLinks(t *testing.T) { tests := []struct { name string content string want []output.Link }{ { // The offsets are byte offsets, so the multibyte prefix counts // for more than the three characters it looks like. "after multibyte text", "みてね https://example.com/article", []output.Link{{URL: "https://example.com/article", ByteStart: 10, ByteEnd: 37}}, }, { "trailing punctuation is outside the link", "これ→https://example.com/x。", []output.Link{{URL: "https://example.com/x", ByteStart: 9, ByteEnd: 30}}, }, { "several", "https://one.example https://two.example", []output.Link{ {URL: "https://one.example", ByteStart: 0, ByteEnd: 19}, {URL: "https://two.example", ByteStart: 20, ByteEnd: 39}, }, }, {"no link", "ただのつぶやき", nil}, {"scheme only", "http:// と書いただけ", nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := findLinks(tt.content) if len(got) != len(tt.want) { t.Fatalf("findLinks(%q) returned %d links, want %d", tt.content, len(got), len(tt.want)) } for i, link := range got { if link != tt.want[i] { t.Errorf("link %d = %+v, want %+v", i, link, tt.want[i]) } // A facet pointing at the wrong bytes marks up the wrong text. if slice := tt.content[link.ByteStart:link.ByteEnd]; slice != link.URL { t.Errorf("bytes [%d:%d] are %q, want %q", link.ByteStart, link.ByteEnd, slice, link.URL) } } }) } } 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") } }