...

Source file src/oss.terrastruct.com/d2/lib/imgbundler/imgbundler_test.go

Documentation: oss.terrastruct.com/d2/lib/imgbundler

     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  //go:embed test_png.png
    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  	// we don't want log.Error to cause this test to fail
    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 -&gt; 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  	// Test almost too large response
   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  	// Test too large response
   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  	// Test error response
   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 -&gt; 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  // TestDuplicateURL ensures that we don't fetch the same image twice
   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 -&gt; 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 -&gt; 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  	// Using a cache, imgs are not refetched on multiple runs
   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  	// With cache disabled, it refetches
   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