Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA. Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb, Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming, Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office (Collabora/OnlyOffice). Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort) sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
3.6 KiB
Go
123 lines
3.6 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|