package admin import ( "bytes" "image" "image/color" "image/png" "testing" ) func testImage(w, h int) []byte { img := image.NewRGBA(image.Rect(0, 0, w, h)) for y := range h { for x := range w { img.Set(x, y, color.RGBA{R: 20, G: 180, B: 120, A: 255}) } } var buf bytes.Buffer if err := png.Encode(&buf, img); err != nil { panic(err) } return buf.Bytes() } func decodeSize(t *testing.T, blob []byte) (int, int) { t.Helper() img, err := png.Decode(bytes.NewReader(blob)) if err != nil { t.Fatalf("decode result: %v", err) } return img.Bounds().Dx(), img.Bounds().Dy() } var testBG = color.RGBA{R: 31, G: 107, B: 92, A: 255} func TestRenderAppIconSizes(t *testing.T) { out, err := renderAppIcon(bytes.NewReader(testImage(1000, 1000)), testBG) if err != nil { t.Fatalf("render: %v", err) } for name, want := range map[string]int{ "64.png": 64, "192.png": 192, "512.png": 512, "maskable.png": 512, } { blob, ok := out[name] if !ok { t.Fatalf("%s missing", name) } if w, h := decodeSize(t, blob); w != want || h != want { t.Errorf("%s: got %dx%d, want %dx%d", name, w, h, want, want) } } } // Aus einer kleinen Vorlage darf kein 512er entstehen – hochskaliert sähe es // schlechter aus als gar keins, und das Manifest nennt dann nur, was es gibt. func TestRenderAppIconNeverUpscales(t *testing.T) { out, err := renderAppIcon(bytes.NewReader(testImage(256, 256)), testBG) if err != nil { t.Fatalf("render: %v", err) } if _, ok := out["512.png"]; ok { t.Error("512.png was generated from a 256px source") } if w, _ := decodeSize(t, out["maskable.png"]); w != 192 { t.Errorf("maskable: got %d, want 192", w) } } func TestRenderAppIconRejectsSmall(t *testing.T) { if _, err := renderAppIcon(bytes.NewReader(testImage(100, 100)), testBG); err == nil { t.Fatal("expected an error for a 100px source") } } // Nicht quadratische Vorlagen werden gerahmt, nicht beschnitten: Ein Zuschnitt // würde ein Logo anschneiden, und das fällt erst auf, wenn das Symbol klebt. func TestRenderAppIconFramesInsteadOfCropping(t *testing.T) { out, err := renderAppIcon(bytes.NewReader(testImage(400, 150)), testBG) if err != nil { t.Fatalf("render: %v", err) } img, err := png.Decode(bytes.NewReader(out["192.png"])) if err != nil { t.Fatalf("decode: %v", err) } if _, _, _, a := img.At(96, 2).RGBA(); a != 0 { t.Error("top edge should be transparent padding, not image content") } if _, g, _, a := img.At(96, 96).RGBA(); a == 0 || g>>8 < 150 { t.Error("centre should carry the motif") } } // Der maskable-Grund ist die Primärfarbe: Durchsichtig füllt Android mit Weiß, // und Weiß leuchtet im dunklen App-Menü. func TestRenderAppIconMaskableHasBackground(t *testing.T) { out, err := renderAppIcon(bytes.NewReader(testImage(512, 512)), testBG) if err != nil { t.Fatalf("render: %v", err) } img, err := png.Decode(bytes.NewReader(out["maskable.png"])) if err != nil { t.Fatalf("decode: %v", err) } r, g, b, a := img.At(2, 2).RGBA() if a>>8 != 255 || r>>8 != 31 || g>>8 != 107 || b>>8 != 92 { t.Errorf("corner: got %d,%d,%d,%d – want the background colour", r>>8, g>>8, b>>8, a>>8) } } func TestParseHexColor(t *testing.T) { fallback := color.RGBA{R: 1, G: 2, B: 3, A: 255} if got := parseHexColor("#1f6b5c", fallback); got != (color.RGBA{R: 31, G: 107, B: 92, A: 255}) { t.Errorf("valid colour: got %v", got) } // Eine kaputte Einstellung darf keinen Upload scheitern lassen. for _, bad := range []string{"", "1f6b5c", "#xyz", "#1f6b5cff"} { if got := parseHexColor(bad, fallback); got != fallback { t.Errorf("%q: got %v, want the fallback", bad, got) } } }