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"]) } }