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>
This commit is contained in:
1 parent
3019e84681
commit
9f44215698
18 files changed
+814
-26
No files matched your search
@@ -0,0 +1,102 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user