1package favicon
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "image"
8 "image/color"
9 "image/png"
10 "testing"
11
12 appport "github.com/bnema/dumber/internal/application/port"
13)
14
15func TestConverterPNGNormalizesAndSizes(t *testing.T) {
16 conv := NewImageConverter()
17 out, err := conv.Convert(context.Background(), testPNG(t, 64, 48), "image/png", []int{32})
18 if err != nil {
19 t.Fatal(err)
20 }
21 if _, decodeErr := png.Decode(bytes.NewReader(out.PNG)); decodeErr != nil {
22 t.Fatalf("normalized png invalid: %v", decodeErr)
23 }
24 img, err := png.Decode(bytes.NewReader(out.SizedPNG[32]))
25 if err != nil {
26 t.Fatal(err)
27 }
28 if img.Bounds().Dx() != 32 || img.Bounds().Dy() != 32 {
29 t.Fatalf("size = %v", img.Bounds())
30 }
31}
32
33func TestConverterAcceptedFaviconFormatsProducePNGs(t *testing.T) {
34 conv := NewImageConverter()
35 for _, tc := range []struct {
36 name, ct string
37 data []byte
38 }{
39 {"png", "image/png", testPNG(t, 16, 16)},
40 {"ico with png payload", "image/x-icon", testICOWithPNG(t, testPNG(t, 16, 16))},
41 {"ico with bitmap payload", "image/x-icon", testICOWithDIB(t)},
42 } {
43 t.Run(tc.name, func(t *testing.T) {
44 out, err := conv.Convert(context.Background(), tc.data, tc.ct, []int{32})
45 if err != nil {
46 t.Fatal(err)
47 }
48 if _, decodeErr := png.Decode(bytes.NewReader(out.PNG)); decodeErr != nil {
49 t.Fatalf("normalized png invalid: %v", decodeErr)
50 }
51 img, err := png.Decode(bytes.NewReader(out.SizedPNG[32]))
52 if err != nil {
53 t.Fatal(err)
54 }
55 if img.Bounds().Dx() != 32 || img.Bounds().Dy() != 32 {
56 t.Fatalf("size = %v", img.Bounds())
57 }
58 })
59 }
60}
61
62func TestConverterRejectsFormatsThatWouldProducePlaceholders(t *testing.T) {
63 conv := NewImageConverter()
64 for _, tc := range []struct {
65 name, ct string
66 data []byte
67 }{
68 {"svg", "image/svg+xml", []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><rect width="16" height="16" fill="#ff0000"/></svg>`)},
69 {"webp", "image/webp", []byte("RIFF\x04\x00\x00\x00WEBPVP8 ")},
70 } {
71 t.Run(tc.name, func(t *testing.T) {
72 if _, err := conv.Convert(context.Background(), tc.data, tc.ct, []int{32}); !errors.Is(err, appport.ErrFaviconMiss) {
73 t.Fatalf("err = %v, want favicon miss", err)
74 }
75 })
76 }
77}
78
79func TestConverterMalformedInputsAreMisses(t *testing.T) {
80 conv := NewImageConverter()
81 for _, tc := range []struct {
82 name, ct string
83 data []byte
84 }{
85 {"ico", "image/x-icon", []byte{0, 0, 1, 0}},
86 {"svg", "image/svg+xml", []byte(`not svg`)},
87 {"webp", "image/webp", []byte("not webp")},
88 {"malformed", "image/png", []byte("not an image")},
89 } {
90 t.Run(tc.name, func(t *testing.T) {
91 if _, err := conv.Convert(context.Background(), tc.data, tc.ct, []int{32}); !errors.Is(err, appport.ErrFaviconMiss) {
92 t.Fatalf("err = %v", err)
93 }
94 })
95 }
96}
97
98func testICOWithDIB(t *testing.T) []byte {
99 t.Helper()
100 const width, height, bitCount = 2, 2, 32
101 var dib bytes.Buffer
102 writeLE32 := func(v uint32) { dib.Write([]byte{byte(v), byte(v >> 8), byte(v >> 16), byte(v >> 24)}) }
103 writeLE16 := func(v uint16) { dib.Write([]byte{byte(v), byte(v >> 8)}) }
104 writeLE32(40) // BITMAPINFOHEADER size
105 writeLE32(width) // width
106 writeLE32(height * 2) // xor + and mask height
107 writeLE16(1) // planes
108 writeLE16(bitCount) // bit count
109 writeLE32(0) // BI_RGB
110 writeLE32(width * height * 4) // image size
111 writeLE32(0)
112 writeLE32(0) // pels per meter
113 writeLE32(0)
114 writeLE32(0) // colors
115 dib.Write([]byte{0, 0, 255, 255, 0, 255, 0, 255}) // bottom row BGRA
116 dib.Write([]byte{255, 0, 0, 255, 0, 0, 0, 255}) // top row BGRA
117 return testICOWithPayload(t, dib.Bytes(), bitCount)
118}
119
120func testICOWithPNG(t *testing.T, pngData []byte) []byte {
121 t.Helper()
122 return testICOWithPayload(t, pngData, 32)
123}
124
125func testICOWithPayload(t *testing.T, payload []byte, bitCount byte) []byte {
126 t.Helper()
127 buf := bytes.NewBuffer(nil)
128 buf.Write([]byte{0, 0, 1, 0, 1, 0}) // reserved, type icon, count
129 buf.Write([]byte{16, 16, 0, 0}) // width, height, colors, reserved
130 buf.Write([]byte{1, 0, bitCount, 0}) // planes, bit count
131 size := uint32(len(payload))
132 offset := uint32(6 + 16)
133 buf.Write([]byte{byte(size), byte(size >> 8), byte(size >> 16), byte(size >> 24)})
134 buf.Write([]byte{byte(offset), byte(offset >> 8), byte(offset >> 16), byte(offset >> 24)})
135 buf.Write(payload)
136 return buf.Bytes()
137}
138
139func testPNG(t *testing.T, w, h int) []byte {
140 t.Helper()
141 img := image.NewRGBA(image.Rect(0, 0, w, h))
142 for y := range h {
143 for x := range w {
144 img.Set(x, y, color.RGBA{R: 200, A: 255})
145 }
146 }
147 var buf bytes.Buffer
148 if err := png.Encode(&buf, img); err != nil {
149 t.Fatal(err)
150 }
151 return buf.Bytes()
152}