1 package imgbundler
2
3 import (
4 "context"
5 "crypto/rand"
6 _ "embed"
7 "fmt"
8 "net/http"
9 "net/http/httptest"
10 "path/filepath"
11 "strings"
12 "sync"
13 "testing"
14
15 "cdr.dev/slog/sloggers/slogtest"
16 tassert "github.com/stretchr/testify/assert"
17
18 "oss.terrastruct.com/d2/lib/log"
19 "oss.terrastruct.com/d2/lib/simplelog"
20 )
21
22
23 var testPNGFile []byte
24
25 type roundTripFunc func(req *http.Request) *http.Response
26
27 func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
28 return f(req), nil
29 }
30
31 func TestRegex(t *testing.T) {
32 urls := []string{
33 "https://icons.terrastruct.com/essentials/004-picture.svg",
34 "http://icons.terrastruct.com/essentials/004-picture.svg",
35 }
36
37 notURLs := []string{
38 "hi.png",
39 "./cat.png",
40 "/cat.png",
41 }
42
43 for _, href := range append(urls, notURLs...) {
44 str := fmt.Sprintf(`<image href="%s" />`, href)
45 matches := imageRegex.FindAllStringSubmatch(str, -1)
46 if len(matches) != 1 {
47 t.Fatalf("uri regex didn't match %s", str)
48 }
49 }
50 }
51
52 func TestInlineRemote(t *testing.T) {
53 imgCache = sync.Map{}
54
55 ctx := log.WithTB(context.Background(), t, &slogtest.Options{IgnoreErrors: true})
56 svgURL := "https://icons.terrastruct.com/essentials/004-picture.svg"
57 pngURL := "https://cdn4.iconfinder.com/data/icons/smart-phones-technologies/512/android-phone.png"
58
59 sampleSVG := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?>
60 <svg
61 id="d2-svg"
62 style="background: white;"
63 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
64 width="328" height="587" viewBox="-100 -131 328 587"><style type="text/css">
65 <![CDATA[
66 .shape {
67 shape-rendering: geometricPrecision;
68 stroke-linejoin: round;
69 }
70 .connection {
71 stroke-linecap: round;
72 stroke-linejoin: round;
73 }
74
75 ]]>
76 </style><g id="a"><g class="shape" ><image href="%s" x="0" y="0" width="128" height="128" style="fill:#FFFFFF;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="64.000000" y="-15.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">a</text></g><g id="b"><g class="shape" ><image href="%s" x="0" y="228" width="128" height="128" style="fill:#FFFFFF;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="64.000000" y="213.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">b</text></g><g id="(a -> b)[0]"><marker id="mk-3990223579" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon class="connection" fill="#0D32B2" stroke-width="2" points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" /> </marker><path d="M 64.000000 130.000000 C 64.000000 168.000000 64.000000 188.000000 64.000000 224.000000" class="connection" style="fill:none;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" marker-end="url(#mk-3990223579)" /></g><style type="text/css"><![CDATA[
77 .text-bold {
78 font-family: "font-bold";
79 }
80 @font-face {
81 font-family: font-bold;
82 src: url("REMOVED");
83 }]]></style></svg>
84 `, svgURL, pngURL)
85
86 httpClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
87 respRecorder := httptest.NewRecorder()
88 switch req.URL.String() {
89 case svgURL:
90 respRecorder.WriteString(`<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\r\n<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->\r\n<svg version=\"1.1\" id=\"Capa_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t viewBox=\"0 0 58 58\" style=\"enable-background:new 0 0 58 58;\" xml:space=\"preserve\">\r\n<rect x=\"1\" y=\"7\" style=\"fill:#C3E1ED;stroke:#E7ECED;stroke-width:2;stroke-miterlimit:10;\" width=\"56\" height=\"44\"/>\r\n<circle style=\"fill:#ED8A19;\" cx=\"16\" cy=\"17.569\" r=\"6.569\"/>\r\n<polygon style=\"fill:#1A9172;\" points=\"56,36.111 55,35 43,24 32.5,35.5 37.983,40.983 42,45 56,45 \"/>\r\n<polygon style=\"fill:#1A9172;\" points=\"2,49 26,49 21.983,44.983 11.017,34.017 2,41.956 \"/>\r\n<rect x=\"2\" y=\"45\" style=\"fill:#6B5B4B;\" width=\"54\" height=\"5\"/>\r\n<polygon style=\"fill:#25AE88;\" points=\"37.983,40.983 27.017,30.017 10,45 42,45 \"/>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n</svg>`)
91 case pngURL:
92 respRecorder.Write(testPNGFile)
93 default:
94 t.Fatal(req.URL)
95 }
96 respRecorder.WriteHeader(200)
97 return respRecorder.Result()
98 })
99
100 l := simplelog.FromLibLog(ctx)
101 out, err := BundleRemote(ctx, l, []byte(sampleSVG), false)
102 if err != nil {
103 t.Fatal(err)
104 }
105 if strings.Contains(string(out), "https://") {
106 t.Fatal("links still exist")
107 }
108 if !strings.Contains(string(out), "image/svg+xml") {
109 t.Fatal("no svg image inserted")
110 }
111 if !strings.Contains(string(out), "image/png") {
112 t.Fatal("no png image inserted")
113 }
114
115 imgCache = sync.Map{}
116
117 httpClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
118 respRecorder := httptest.NewRecorder()
119 bytes := make([]byte, maxImageSize)
120 rand.Read(bytes)
121 respRecorder.Write(bytes)
122 respRecorder.WriteHeader(200)
123 return respRecorder.Result()
124 })
125 _, err = BundleRemote(ctx, l, []byte(sampleSVG), false)
126 if err != nil {
127 t.Fatal(err)
128 }
129
130 imgCache = sync.Map{}
131
132 httpClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
133 respRecorder := httptest.NewRecorder()
134 bytes := make([]byte, maxImageSize+1)
135 rand.Read(bytes)
136 respRecorder.Write(bytes)
137 respRecorder.WriteHeader(200)
138 return respRecorder.Result()
139 })
140 _, err = BundleRemote(ctx, l, []byte(sampleSVG), false)
141 if err == nil {
142 t.Fatal("expected error")
143 }
144
145 imgCache = sync.Map{}
146
147 httpClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
148 respRecorder := httptest.NewRecorder()
149 respRecorder.WriteHeader(500)
150 return respRecorder.Result()
151 })
152 _, err = BundleRemote(ctx, l, []byte(sampleSVG), false)
153 if err == nil {
154 t.Fatal("expected error")
155 }
156 }
157
158 func TestInlineLocal(t *testing.T) {
159 imgCache = sync.Map{}
160 ctx := log.WithTB(context.Background(), t, nil)
161 svgURL, err := filepath.Abs("./test_svg.svg")
162 if err != nil {
163 t.Fatal(err)
164 }
165 pngURL, err := filepath.Abs("./test_png.png")
166 if err != nil {
167 t.Fatal(err)
168 }
169
170 sampleSVG := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?>
171 <svg
172 id="d2-svg"
173 style="background: white;"
174 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
175 width="328" height="587" viewBox="-100 -131 328 587"><style type="text/css">
176 <![CDATA[
177 .shape {
178 shape-rendering: geometricPrecision;
179 stroke-linejoin: round;
180 }
181 .connection {
182 stroke-linecap: round;
183 stroke-linejoin: round;
184 }
185
186 ]]>
187 </style><g id="a"><g class="shape" ><image href="%s" x="0" y="0" width="128" height="128" style="fill:#FFFFFF;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="64.000000" y="-15.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">a</text></g><g id="b"><g class="shape" ><image href="%s" x="0" y="228" width="128" height="128" style="fill:#FFFFFF;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="64.000000" y="213.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">b</text></g><g id="(a -> b)[0]"><marker id="mk-3990223579" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon class="connection" fill="#0D32B2" stroke-width="2" points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" /> </marker><path d="M 64.000000 130.000000 C 64.000000 168.000000 64.000000 188.000000 64.000000 224.000000" class="connection" style="fill:none;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" marker-end="url(#mk-3990223579)" /></g><style type="text/css"><![CDATA[
188 .text-bold {
189 font-family: "font-bold";
190 }
191 @font-face {
192 font-family: font-bold;
193 src: url("REMOVED");
194 }]]></style></svg>
195 `, svgURL, pngURL)
196
197 l := simplelog.FromLibLog(ctx)
198 out, err := BundleLocal(ctx, l, []byte(sampleSVG), false)
199 if err != nil {
200 t.Fatal(err)
201 }
202 if strings.Contains(string(out), svgURL) {
203 t.Fatal("links still exist")
204 }
205 if !strings.Contains(string(out), "image/svg+xml") {
206 t.Fatal("no svg image inserted")
207 }
208 if !strings.Contains(string(out), "image/png") {
209 t.Fatal("no png image inserted")
210 }
211 }
212
213
214 func TestDuplicateURL(t *testing.T) {
215 imgCache = sync.Map{}
216 ctx := log.WithTB(context.Background(), t, nil)
217 url1 := "https://icons.terrastruct.com/essentials/004-picture.svg"
218 url2 := "https://icons.terrastruct.com/essentials/004-picture.svg"
219
220 sampleSVG := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?>
221 <svg
222 id="d2-svg"
223 style="background: white;"
224 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
225 width="328" height="587" viewBox="-100 -131 328 587"><style type="text/css">
226 <![CDATA[
227 .shape {
228 shape-rendering: geometricPrecision;
229 stroke-linejoin: round;
230 }
231 .connection {
232 stroke-linecap: round;
233 stroke-linejoin: round;
234 }
235
236 ]]>
237 </style><g id="a"><g class="shape" ><image href="%s" x="0" y="0" width="128" height="128" style="fill:#FFFFFF;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="64.000000" y="-15.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">a</text></g><g id="b"><g class="shape" ><image href="%s" x="0" y="228" width="128" height="128" style="fill:#FFFFFF;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="64.000000" y="213.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">b</text></g><g id="(a -> b)[0]"><marker id="mk-3990223579" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon class="connection" fill="#0D32B2" stroke-width="2" points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" /> </marker><path d="M 64.000000 130.000000 C 64.000000 168.000000 64.000000 188.000000 64.000000 224.000000" class="connection" style="fill:none;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" marker-end="url(#mk-3990223579)" /></g><style type="text/css"><![CDATA[
238 .text-bold {
239 font-family: "font-bold";
240 }
241 @font-face {
242 font-family: font-bold;
243 src: url("REMOVED");
244 }]]></style></svg>
245 `, url1, url2)
246
247 count := 0
248
249 httpClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
250 count++
251 respRecorder := httptest.NewRecorder()
252 respRecorder.WriteString(`<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\r\n<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->\r\n<svg version=\"1.1\" id=\"Capa_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t viewBox=\"0 0 58 58\" style=\"enable-background:new 0 0 58 58;\" xml:space=\"preserve\">\r\n<rect x=\"1\" y=\"7\" style=\"fill:#C3E1ED;stroke:#E7ECED;stroke-width:2;stroke-miterlimit:10;\" width=\"56\" height=\"44\"/>\r\n<circle style=\"fill:#ED8A19;\" cx=\"16\" cy=\"17.569\" r=\"6.569\"/>\r\n<polygon style=\"fill:#1A9172;\" points=\"56,36.111 55,35 43,24 32.5,35.5 37.983,40.983 42,45 56,45 \"/>\r\n<polygon style=\"fill:#1A9172;\" points=\"2,49 26,49 21.983,44.983 11.017,34.017 2,41.956 \"/>\r\n<rect x=\"2\" y=\"45\" style=\"fill:#6B5B4B;\" width=\"54\" height=\"5\"/>\r\n<polygon style=\"fill:#25AE88;\" points=\"37.983,40.983 27.017,30.017 10,45 42,45 \"/>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n</svg>`)
253 respRecorder.WriteHeader(200)
254 return respRecorder.Result()
255 })
256
257 l := simplelog.FromLibLog(ctx)
258 out, err := BundleRemote(ctx, l, []byte(sampleSVG), false)
259 if err != nil {
260 t.Fatal(err)
261 }
262 tassert.Equal(t, 1, count)
263 if strings.Contains(string(out), url1) {
264 t.Fatal("links still exist")
265 }
266 tassert.Equal(t, 2, strings.Count(string(out), "image/svg+xml"))
267 }
268
269 func TestImgCache(t *testing.T) {
270 imgCache = sync.Map{}
271 ctx := log.WithTB(context.Background(), t, nil)
272 url1 := "https://icons.terrastruct.com/essentials/004-picture.svg"
273 url2 := "https://icons.terrastruct.com/essentials/004-picture.svg"
274
275 sampleSVG := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?>
276 <svg
277 id="d2-svg"
278 style="background: white;"
279 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
280 width="328" height="587" viewBox="-100 -131 328 587"><style type="text/css">
281 <![CDATA[
282 .shape {
283 shape-rendering: geometricPrecision;
284 stroke-linejoin: round;
285 }
286 .connection {
287 stroke-linecap: round;
288 stroke-linejoin: round;
289 }
290
291 ]]>
292 </style><g id="a"><g class="shape" ><image href="%s" x="0" y="0" width="128" height="128" style="fill:#FFFFFF;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="64.000000" y="-15.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">a</text></g><g id="b"><g class="shape" ><image href="%s" x="0" y="228" width="128" height="128" style="fill:#FFFFFF;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="64.000000" y="213.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">b</text></g><g id="(a -> b)[0]"><marker id="mk-3990223579" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon class="connection" fill="#0D32B2" stroke-width="2" points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" /> </marker><path d="M 64.000000 130.000000 C 64.000000 168.000000 64.000000 188.000000 64.000000 224.000000" class="connection" style="fill:none;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" marker-end="url(#mk-3990223579)" /></g><style type="text/css"><![CDATA[
293 .text-bold {
294 font-family: "font-bold";
295 }
296 @font-face {
297 font-family: font-bold;
298 src: url("REMOVED");
299 }]]></style></svg>
300 `, url1, url2)
301
302 count := 0
303
304 httpClient.Transport = roundTripFunc(func(req *http.Request) *http.Response {
305 count++
306 respRecorder := httptest.NewRecorder()
307 respRecorder.WriteString(`<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\r\n<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->\r\n<svg version=\"1.1\" id=\"Capa_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t viewBox=\"0 0 58 58\" style=\"enable-background:new 0 0 58 58;\" xml:space=\"preserve\">\r\n<rect x=\"1\" y=\"7\" style=\"fill:#C3E1ED;stroke:#E7ECED;stroke-width:2;stroke-miterlimit:10;\" width=\"56\" height=\"44\"/>\r\n<circle style=\"fill:#ED8A19;\" cx=\"16\" cy=\"17.569\" r=\"6.569\"/>\r\n<polygon style=\"fill:#1A9172;\" points=\"56,36.111 55,35 43,24 32.5,35.5 37.983,40.983 42,45 56,45 \"/>\r\n<polygon style=\"fill:#1A9172;\" points=\"2,49 26,49 21.983,44.983 11.017,34.017 2,41.956 \"/>\r\n<rect x=\"2\" y=\"45\" style=\"fill:#6B5B4B;\" width=\"54\" height=\"5\"/>\r\n<polygon style=\"fill:#25AE88;\" points=\"37.983,40.983 27.017,30.017 10,45 42,45 \"/>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n</svg>`)
308 respRecorder.WriteHeader(200)
309 return respRecorder.Result()
310 })
311
312 l := simplelog.FromLibLog(ctx)
313
314 _, err := BundleRemote(ctx, l, []byte(sampleSVG), true)
315 if err != nil {
316 t.Fatal(err)
317 }
318 _, err = BundleRemote(ctx, l, []byte(sampleSVG), true)
319 if err != nil {
320 t.Fatal(err)
321 }
322 tassert.Equal(t, 1, count)
323
324
325 count = 0
326 _, err = BundleRemote(ctx, l, []byte(sampleSVG), false)
327 if err != nil {
328 t.Fatal(err)
329 }
330 _, err = BundleRemote(ctx, l, []byte(sampleSVG), false)
331 if err != nil {
332 t.Fatal(err)
333 }
334 tassert.Equal(t, 2, count)
335 }
336
View as plain text