feat(aether): initial commit – self-hosted Cloud, Phasen 1–7

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>
This commit is contained in:
HoneyandClaude Opus 5 committed 2026-08-24 09:25:36 +02:00
commit 31a027c93a
225 files changed
+38042

No files matched your search

+122
View File
@@ -0,0 +1,122 @@
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)
}
}
}