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>
103 lines
2.6 KiB
Go
103 lines
2.6 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|