Files
sm-user-plugin/avatar_url_test.go
Jakub Zych 9335e80342 test: RequestUser, EndRequestSession, AvatarURL and user.Deactivating
- TestRequestUser: bearer, token and auth_token cookies in guard order;
  expired, foreign, blacklisted, revoked by tokens_valid_after and
  deactivated tokens are anonymous; only infrastructure failures error
- TestEndRequestSession (no database): jti blacklisted until the token
  expires, forget cookie with the configured Domain and Path, best effort
- TestAvatarURL: thumbnail, broken-image and storage-failure fallbacks,
  gravatar with the size, the user payload's avatar_url unchanged
- TestDeactivatingEvent: one event per newly deactivated user inside the
  bulk action's transaction, a listener error rolls the action back
2026-10-19 21:40:41 +02:00

137 lines
5.5 KiB
Go

package user
import (
"context"
"crypto/md5"
"fmt"
"strconv"
"strings"
"testing"
"git.golem15.com/golem15/sm-user-plugin/classes"
"git.golem15.com/golem15/sm-user-plugin/controllers"
"git.golem15.com/golem15/sm-user-plugin/models"
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
"gocloud.dev/blob/memblob"
)
// jpegAvatar stores a real JPEG avatar for user (a system_files row and its
// blob) and returns the row.
func (e *adminEnv) jpegAvatar(t *testing.T, user *models.User) attach.File {
t.Helper()
disk := "face" + adminStamp() + ".jpg"
data := jpegBytes(t)
if err := e.bucket.WriteAll(context.Background(), attach.BlobKey(disk), data, nil); err != nil {
t.Fatal(err)
}
row := attach.File{
DiskName: disk, FileName: "face.jpg", FileSize: int64(len(data)), ContentType: "image/jpeg",
Field: models.AvatarField, AttachmentID: strconv.FormatUint(uint64(user.ID), 10), AttachmentType: user.MorphName(),
}
if err := e.gdb.Create(&row).Error; err != nil {
t.Fatal(err)
}
return row
}
func gravatarFor(email string, size int) string {
return fmt.Sprintf("//www.gravatar.com/avatar/%x?s=%d&d=mm", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email)))), size)
}
// TestAvatarURL pins classes.AvatarURL (D-05): the avatar thumbnail at the
// asked size, the file path when the thumbnail fails, the gravatar
// fallback with the size, errors only for a failing lookup, and the user
// API payload (applyAvatar) unchanged: its avatar_url is AvatarURL at 128.
func TestAvatarURL(t *testing.T) {
env := newAdminEnv(t)
ctx := t.Context()
if _, err := classes.AvatarURL(ctx, env.app, nil, 128); err == nil {
t.Fatal("a nil user must be refused")
}
// Gravatar: no app, an unsaved user, no attachment; the email is
// trimmed and lower-cased, the size is the one asked for.
plain := env.seedUser(t, "plain", func(u *models.User) { u.Email = " Mixed.Case-" + adminStamp() + "@Example.TEST " })
for _, size := range []int{32, 128, 256} {
for name, call := range map[string]func() (string, error){
"no app": func() (string, error) { return classes.AvatarURL(ctx, nil, plain, size) },
"unsaved user": func() (string, error) { return classes.AvatarURL(ctx, env.app, &models.User{Email: plain.Email}, size) },
"no attachment": func() (string, error) { return classes.AvatarURL(ctx, env.app, plain, size) },
} {
got, err := call()
if err != nil || got != gravatarFor(plain.Email, size) {
t.Fatalf("%s at %d = %q %v, want %q", name, size, got, err, gravatarFor(plain.Email, size))
}
}
}
if gravatarFor(plain.Email, 1) != gravatarFor(strings.ToLower(strings.TrimSpace(plain.Email)), 1) {
t.Fatal("the gravatar hash is not of the normalised email")
}
// A file avatar: the thumbnail at size by size, mode auto.
withFile := env.seedUser(t, "withfile", nil)
row := env.jpegAvatar(t, withFile)
thumb, err := classes.AvatarURL(ctx, env.app, withFile, 64)
want := attach.ThumbFilename(row.ID, 64, 64, 0, 0, "auto", "jpg")
if err != nil || !strings.HasSuffix(thumb, want) || strings.HasPrefix(thumb, "//") {
t.Fatalf("thumbnail = %q %v, want a path ending in %s", thumb, err, want)
}
// Without a published bucket the gravatar is used even with a file.
noBucket := sessionUserApp(t, "SUMMER_GOLEM15__USER__JWT__SECRET="+sessionSecret)
if err := noBucket.Publish(env.gdb); err != nil {
t.Fatal(err)
}
if got, err := classes.AvatarURL(ctx, noBucket, withFile, 64); err != nil || got != gravatarFor(withFile.Email, 64) {
t.Fatalf("no bucket = %q %v", got, err)
}
// An original that does not decode gets WinterCMS's broken-image
// thumbnail, still a thumbnail path.
notImage := env.seedUser(t, "notimage", nil)
env.seedAvatar(t, notImage) // its blob is not an image
if got, err := classes.AvatarURL(ctx, env.app, notImage, 128); err != nil || !strings.Contains(got, "thumb_") {
t.Fatalf("non-image avatar = %q %v", got, err)
}
// A thumbnail that cannot be made (here: the storage fails) falls back
// to the file path.
closed := memblob.OpenBucket(nil)
_ = closed.Close()
failing := sessionUserApp(t, "SUMMER_GOLEM15__USER__JWT__SECRET="+sessionSecret)
if err := failing.Publish(env.gdb); err != nil {
t.Fatal(err)
}
if err := attach.Publish(failing, closed); err != nil {
t.Fatal(err)
}
if got, err := classes.AvatarURL(ctx, failing, withFile, 128); err != nil || got != "/"+attach.BlobKey(row.DiskName) {
t.Fatalf("failing thumbnail = %q %v, want /%s", got, err, attach.BlobKey(row.DiskName))
}
// A failing lookup is the only error.
canceled, cancel := context.WithCancel(ctx)
cancel()
if got, err := classes.AvatarURL(canceled, env.app, withFile, 128); err == nil {
t.Fatalf("failing lookup = %q, want an error", got)
}
// The user API payload is unchanged: avatar_url is AvatarURL at 128,
// next to has_avatar and the disk name.
payload, err := controllers.APIArray(ctx, env.app, withFile)
if err != nil {
t.Fatal(err)
}
at128, _ := classes.AvatarURL(ctx, env.app, withFile, 128)
if payload["has_avatar"] != true || payload["avatar"] != row.DiskName || payload["avatar_url"] != at128 {
t.Fatalf("payload avatar fields = %v %v %v, want true %s %s", payload["has_avatar"], payload["avatar"], payload["avatar_url"], row.DiskName, at128)
}
without, err := controllers.APIArray(ctx, env.app, plain)
if err != nil {
t.Fatal(err)
}
// Without one the payload keeps its gravatar default (size 25).
if without["has_avatar"] != false || without["avatar"] != gravatarFor(plain.Email, 25) {
t.Fatalf("payload without an avatar = %v %v", without["has_avatar"], without["avatar"])
}
}