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 {"webp", "image/webp", []byte("RIFF\x04\x00\x00\x00WEBPVP8 ")},
69 {"empty svg", "image/svg+xml", []byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"></svg>`)},
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 TestConverterRasterizesSVG(t *testing.T) {
80 conv := NewImageConverter()
81 svg := []byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
82 <rect x="0" y="0" width="32" height="16" fill="#ff0000"/>
83 <rect x="0" y="16" width="32" height="16" fill="#0000ff"/>
84</svg>`)
85 out, err := conv.Convert(context.Background(), svg, "image/svg+xml", []int{32})
86 if err != nil {
87 t.Fatal(err)
88 }
89 img, err := png.Decode(bytes.NewReader(out.SizedPNG[32]))
90 if err != nil {
91 t.Fatal(err)
92 }
93 if img.Bounds().Dx() != 32 || img.Bounds().Dy() != 32 {
94 t.Fatalf("size = %v", img.Bounds())
95 }
96 r, _, b, a := img.At(16, 4).RGBA()
97 if a == 0 || r>>8 < 200 || b>>8 > 50 {
98 t.Fatalf("top pixel = r%d b%d a%d, want red", r>>8, b>>8, a>>8)
99 }
100 r, _, b, a = img.At(16, 28).RGBA()
101 if a == 0 || b>>8 < 200 || r>>8 > 50 {
102 t.Fatalf("bottom pixel = r%d b%d a%d, want blue", r>>8, b>>8, a>>8)
103 }
104}
105
106func TestConverterRejectsRecursiveSVGUse(t *testing.T) {
107 conv := NewImageConverter()
108 for _, tc := range []struct{ name, svg string }{
109 {"self cycle", `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><g id="a"><use href="#a"/></g></defs><use href="#a"/></svg>`},
110 {"mutual cycle", `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><g id="a"><use xlink:href="#b"/></g><g id="b"><use xlink:href="#a"/></g></defs><use xlink:href="#a"/></svg>`},
111 {"use targets use", `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><use id="a" href="#a"/></defs><use href="#a"/></svg>`},
112 {"flat def run after sibling id", `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><g id="a"></g><use href="#a"/></defs><use href="#a"/></svg>`},
113 {"anonymous def", `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><use href="#"/></defs><use href="#"/></svg>`},
114 } {
115 t.Run(tc.name, func(t *testing.T) {
116 if _, err := conv.Convert(context.Background(), []byte(tc.svg), "image/svg+xml", []int{32}); !errors.Is(err, appport.ErrFaviconMiss) {
117 t.Fatalf("err = %v, want favicon miss", err)
118 }
119 })
120 }
121}
122
123func TestConverterRendersFlatSVGUse(t *testing.T) {
124 svg := []byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><rect id="r" width="16" height="16" fill="#00ff00"/></defs><use href="#r"/></svg>`)
125 if _, err := NewImageConverter().Convert(context.Background(), svg, "image/svg+xml", []int{32}); err != nil {
126 t.Fatal(err)
127 }
128}
129
130func TestConverterMalformedInputsAreMisses(t *testing.T) {
131 conv := NewImageConverter()
132 for _, tc := range []struct {
133 name, ct string
134 data []byte
135 }{
136 {"ico", "image/x-icon", []byte{0, 0, 1, 0}},
137 {"svg", "image/svg+xml", []byte(`not svg`)},
138 {"webp", "image/webp", []byte("not webp")},
139 {"malformed", "image/png", []byte("not an image")},
140 } {
141 t.Run(tc.name, func(t *testing.T) {
142 if _, err := conv.Convert(context.Background(), tc.data, tc.ct, []int{32}); !errors.Is(err, appport.ErrFaviconMiss) {
143 t.Fatalf("err = %v", err)
144 }
145 })
146 }
147}
148
149func testICOWithDIB(t *testing.T) []byte {
150 t.Helper()
151 const width, height, bitCount = 2, 2, 32
152 var dib bytes.Buffer
153 writeLE32 := func(v uint32) { dib.Write([]byte{byte(v), byte(v >> 8), byte(v >> 16), byte(v >> 24)}) }
154 writeLE16 := func(v uint16) { dib.Write([]byte{byte(v), byte(v >> 8)}) }
155 writeLE32(40) // BITMAPINFOHEADER size
156 writeLE32(width) // width
157 writeLE32(height * 2) // xor + and mask height
158 writeLE16(1) // planes
159 writeLE16(bitCount) // bit count
160 writeLE32(0) // BI_RGB
161 writeLE32(width * height * 4) // image size
162 writeLE32(0)
163 writeLE32(0) // pels per meter
164 writeLE32(0)
165 writeLE32(0) // colors
166 dib.Write([]byte{0, 0, 255, 255, 0, 255, 0, 255}) // bottom row BGRA
167 dib.Write([]byte{255, 0, 0, 255, 0, 0, 0, 255}) // top row BGRA
168 return testICOWithPayload(t, dib.Bytes(), bitCount)
169}
170
171func testICOWithPNG(t *testing.T, pngData []byte) []byte {
172 t.Helper()
173 return testICOWithPayload(t, pngData, 32)
174}
175
176func testICOWithPayload(t *testing.T, payload []byte, bitCount byte) []byte {
177 t.Helper()
178 buf := bytes.NewBuffer(nil)
179 buf.Write([]byte{0, 0, 1, 0, 1, 0}) // reserved, type icon, count
180 buf.Write([]byte{16, 16, 0, 0}) // width, height, colors, reserved
181 buf.Write([]byte{1, 0, bitCount, 0}) // planes, bit count
182 size := uint32(len(payload))
183 offset := uint32(6 + 16)
184 buf.Write([]byte{byte(size), byte(size >> 8), byte(size >> 16), byte(size >> 24)})
185 buf.Write([]byte{byte(offset), byte(offset >> 8), byte(offset >> 16), byte(offset >> 24)})
186 buf.Write(payload)
187 return buf.Bytes()
188}
189
190func testPNG(t *testing.T, w, h int) []byte {
191 t.Helper()
192 img := image.NewRGBA(image.Rect(0, 0, w, h))
193 for y := range h {
194 for x := range w {
195 img.Set(x, y, color.RGBA{R: 200, A: 255})
196 }
197 }
198 var buf bytes.Buffer
199 if err := png.Encode(&buf, img); err != nil {
200 t.Fatal(err)
201 }
202 return buf.Bytes()
203}