package sky import ( "bytes" "encoding/base64" "encoding/xml" "flag" "html/template" "image/png" "io" "net/http" "net/http/httptest" "os" pathpkg "path" "path/filepath" "strings" "testing" ) func layers(id string) map[string]string { out := map[string]string{} for _, f := range []string{"haze-light.svg", "haze-dark.svg", "clouds-light.svg", "clouds-dark.svg", "grain.svg", "filaments.svg"} { out[f] = draw(id, f) } return out } // A team's sky is the same every time it is drawn, anywhere, and not the // same as another team's. func TestSkiesAreSeededAndDistinct(t *testing.T) { a, again, b := layers(ID("acme")), layers(ID("acme")), layers(ID("orchard")) for f := range a { if a[f] != again[f] { t.Errorf("%s is not deterministic", f) } if a[f] == b[f] { t.Errorf("%s is the same for two teams", f) } } } func TestEveryLayerIsWellFormedSVG(t *testing.T) { for _, seed := range []string{SuiteSeed, "acme", "a team with spaces", ""} { for f, body := range layers(ID(seed)) { if strings.Contains(body, "NaN") || strings.Contains(body, "Inf") { t.Errorf("%s for %q contains a non-number", f, seed) } d := xml.NewDecoder(strings.NewReader(body)) for { _, err := d.Token() if err == io.EOF { break } if err != nil { t.Errorf("%s for %q is not well-formed: %v", f, seed, err) break } } } } } // Every app on the ring has a place in every sky. func TestEveryAppGathersInTheSky(t *testing.T) { if got := len(anchors(ID(SuiteSeed))); got != 9 { t.Errorf("the suite sky has %d anchors, want one per app (9)", got) } } func TestIDIsClosedAndOpaque(t *testing.T) { id := ID("acme") if !idShape.MatchString(id) || strings.Contains(id, "acme") { t.Errorf("ID(%q) = %q", "acme", id) } } func TestHandlerServesOnlyTheSixLayers(t *testing.T) { h := Handler() id := ID("acme") for path, want := range map[string]int{ Prefix + id + "/clouds-dark.svg": 200, Prefix + id + "/filaments.svg": 200, Prefix + id + "/grain.svg": 200, Prefix + id + "/clouds.svg": 404, Prefix + id + "/haze-light.svg/x": 404, Prefix + "ACME0000000000000/haze-light.svg": 404, Prefix + "acme/haze-light.svg": 404, Prefix + id: 404, "/static/estilo/sky/v0/" + id + "/haze-light.svg": 404, } { rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil)) if rec.Code != want { t.Errorf("GET %s = %d, want %d", path, rec.Code, want) } if want == 200 && (rec.Header().Get("Content-Type") != "image/svg+xml" || !strings.Contains(rec.Header().Get("Cache-Control"), "immutable")) { t.Errorf("GET %s headers = %v", path, rec.Header()) } } rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodDelete, Prefix+id+"/filaments.svg", nil)) if rec.Code != http.StatusMethodNotAllowed { t.Errorf("DELETE = %d, want 405", rec.Code) } } func TestTemplateRendersEveryLayerDecoratively(t *testing.T) { tmpl := template.Must(template.ParseFS(Templates(), "templates/*.html")) var b bytes.Buffer if err := tmpl.ExecuteTemplate(&b, "estilo-sky", For("acme")); err != nil { t.Fatal(err) } html := b.String() if !strings.Contains(html, `aria-hidden="true"`) || strings.Count(html, `alt=""`) != 4 || strings.Count(html, `estilo-sky-tone=`) != 2 || strings.Count(html, `src="data:image/png;base64,`) != 2 || strings.Contains(html, "ZgotmplZ") || strings.Contains(html, `loading="lazy"`) { t.Errorf("unexpected markup:\n%s", html) } for _, l := range For("acme").Layers { if l.Src != "" && !strings.Contains(html, `src="`+l.Src+`"`) { t.Errorf("layer %s missing", l.Src) } } } var update = flag.Bool("update", false, "rewrite sky/testdata goldens (and bump Prefix's version)") // TestSkyGoldens pins what one ID draws. A drawing change — to generate.go, // or to the ring's hues in estilo's theme, which the sky is made of — fails // here, and a failure here means Prefix's version must change: browsers // hold each file for a year under its versioned URL. func TestSkyGoldens(t *testing.T) { for file, body := range layers(ID("golden")) { path := filepath.Join("testdata", pathpkg.Base(Prefix)+"-"+file) if *update { if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } continue } want, err := os.ReadFile(path) if err != nil { t.Fatalf("%v (run with -update after bumping Prefix)", err) } if string(want) != body { t.Errorf("%s draws differently from its golden: bump Prefix's version, then -update", file) } } } // The placeholder is a small, deterministic PNG per sky and scheme, and it // is the sky it stands in for: not blank, and not another team's. func TestPlaceholderIsTheSkyInAFewPixels(t *testing.T) { id := ID("acme") for _, tone := range []Tone{Light, Dark} { uri := Placeholder(id, tone) if uri != Placeholder(id, tone) || uri == Placeholder(ID("orchard"), tone) { t.Errorf("%s placeholder is not deterministic per sky", tone) } raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(uri, "data:image/png;base64,")) if err != nil { t.Fatal(err) } img, err := png.Decode(bytes.NewReader(raw)) if err != nil { t.Fatal(err) } if b := img.Bounds(); b.Dx() != phW || b.Dy() != phH { t.Errorf("placeholder is %v", b) } var opaque int for y := range phH { for x := range phW { if _, _, _, a := img.At(x, y).RGBA(); a > 0x4000 { opaque++ } } } if opaque < phW*phH/4 { t.Errorf("%s placeholder is mostly empty (%d of %d pixels)", tone, opaque, phW*phH) } if len(uri) > 4096 { t.Errorf("%s placeholder is %d bytes inline; keep it small", tone, len(uri)) } } }