Files
marcandClaude Opus 5 9f44215698 feat(oidc): Profilbild vom Identitätsanbieter übernehmen
Schickt der Anbieter ein `picture`, wird es bei der Anmeldung geholt,
mittig auf 256x256 zugeschnitten und oben rechts angezeigt. Fehlt es
oder scheitert das Holen, bleibt es beim Buchstabenkreis - die
Anmeldung selbst hängt nicht davon ab.

Die Bildadresse kommt aus einem Token, also von außen. Erlaubt ist
deshalb ausschließlich der Ursprung aus OIDC_ISSUER, Weiterleitungen
eingeschlossen; dazu 4 MB und 5 Sekunden als Deckel. Ohne das wäre es
eine Aufforderung an den Server, für einen Fremden beliebige Adressen
im internen Netz anzufragen.

Neu: Migration 000019 (avatar_key/-source/-updated_at), Paket
internal/avatar, Route GET /api/v1/users/{id}/avatar. Der GC durchsucht
"avatars/" mit und kennt die Schlüssel - sonst räumte er die Bilder
nach einem Tag weg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 13:43:22 +02:00

103 lines
2.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package avatar
import (
"bytes"
"image"
"image/color"
"image/jpeg"
"image/png"
"strings"
"testing"
)
// Ein Bild, dessen linke Hälfte rot und rechte Hälfte blau ist – so lässt sich
// nachrechnen, welcher Ausschnitt genommen wurde.
func striped(w, h int) image.Image {
img := image.NewRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
if x < w/2 {
img.Set(x, y, color.RGBA{255, 0, 0, 255})
} else {
img.Set(x, y, color.RGBA{0, 0, 255, 255})
}
}
}
return img
}
func encodePNG(t *testing.T, img image.Image) []byte {
t.Helper()
var b bytes.Buffer
if err := png.Encode(&b, img); err != nil {
t.Fatal(err)
}
return b.Bytes()
}
func TestProcessMachtQuadrat(t *testing.T) {
cases := []struct {
name string
w, h int
}{
{"breit", 800, 200},
{"hoch", 200, 800},
{"quadratisch", 512, 512},
{"winzig", 16, 16},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
out, err := Process(bytes.NewReader(encodePNG(t, striped(c.w, c.h))))
if err != nil {
t.Fatalf("Process: %v", err)
}
img, _, err := image.Decode(bytes.NewReader(out))
if err != nil {
t.Fatalf("Ergebnis nicht lesbar: %v", err)
}
b := img.Bounds()
if b.Dx() != Size || b.Dy() != Size {
t.Errorf("Größe %dx%d, erwartet %dx%d", b.Dx(), b.Dy(), Size, Size)
}
})
}
}
// Bei einem breiten Bild wird mittig geschnitten, nicht links angesetzt: Die
// linke Kante des Ergebnisses muss deshalb noch rot sein, die rechte blau.
func TestProcessSchneidetMittig(t *testing.T) {
out, err := Process(bytes.NewReader(encodePNG(t, striped(1000, 200))))
if err != nil {
t.Fatal(err)
}
img, _, _ := image.Decode(bytes.NewReader(out))
r, _, bl, _ := img.At(4, Size/2).RGBA()
if r < 0x8000 || bl > 0x4000 {
t.Errorf("linke Kante ist nicht rot – Ausschnitt sitzt falsch")
}
r, _, bl = func() (uint32, uint32, uint32) { a, b, c, _ := img.At(Size-5, Size/2).RGBA(); return a, b, c }()
if bl < 0x8000 || r > 0x4000 {
t.Errorf("rechte Kante ist nicht blau – Ausschnitt sitzt falsch")
}
}
func TestProcessNimmtJPEG(t *testing.T) {
var b bytes.Buffer
if err := jpeg.Encode(&b, striped(300, 300), nil); err != nil {
t.Fatal(err)
}
if _, err := Process(bytes.NewReader(b.Bytes())); err != nil {
t.Errorf("JPEG abgelehnt: %v", err)
}
}
// Was kein Bild ist, muss als Fehler zurückkommen – und nicht etwa als leere
// Datei im Speicher landen.
func TestProcessLehntUnsinnAb(t *testing.T) {
for _, s := range []string{"", "<svg/>", "nicht mal ansatzweise ein bild"} {
if _, err := Process(strings.NewReader(s)); err == nil {
t.Errorf("kein Fehler für %q", s)
}
}
}