Files
sm-user-plugin/avatar_test.go
Jakub Zych 02ba1f0666 refactor: rename module to git.golem15.com/golem15/sm-user-plugin
Prefix replacement of the old application-nested module path in go.mod
and every import. No identifier, string, table, migration ID, config key
or comment changes.
2026-10-02 10:56:06 +02:00

184 lines
5.5 KiB
Go

package user
import (
"bytes"
"context"
"encoding/json"
"image"
"image/jpeg"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"git.golem15.com/golem15/sm-user-plugin/controllers"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
"gocloud.dev/blob"
"gocloud.dev/blob/memblob"
)
func TestUploadAvatar(t *testing.T) {
app, gdb := sessionApp(t, "0s")
publishAvatarBucket(t, app)
token := loginToken(t, app, gdb, "avatar-upload@example.test")
ok := postAvatar(t, controllers.UploadAvatar(app), token, "avatar", "face.jpg", jpegBytes(t))
if ok.Code != http.StatusOK {
t.Fatalf("upload %d %s", ok.Code, ok.Body.String())
}
var body map[string]any
if err := json.Unmarshal(ok.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["message"] != "Avatar updated" {
t.Fatalf("message = %v", body["message"])
}
user, _ := body["user"].(map[string]any)
if user["has_avatar"] != true {
t.Fatalf("has_avatar = %v", user["has_avatar"])
}
url, _ := user["avatar_url"].(string)
if url == "" {
t.Fatalf("avatar_url = %#v", user["avatar_url"])
}
svg := postAvatar(t, controllers.UploadAvatar(app), token, "avatar", "pic.svg", []byte(`<svg xmlns="http://www.w3.org/2000/svg"></svg>`))
assertAvatarError(t, svg, "The avatar must be a file of type: jpeg, jpg, png, webp, gif.")
big := make([]byte, 4_096_001)
copy(big, []byte{0xff, 0xd8, 0xff, 0xe0})
over := postAvatar(t, controllers.UploadAvatar(app), token, "avatar", "big.jpg", big)
assertAvatarError(t, over, "The avatar must not be greater than 4000 kilobytes.")
missing := postAvatar(t, controllers.UploadAvatar(app), token, "", "", nil)
assertAvatarError(t, missing, "The avatar field is required.")
var n int64
if err := gdb.Model(&attach.File{}).Where("field = ?", "avatar").Count(&n).Error; err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("avatar rows = %d, want 1 after rejected replacements", n)
}
}
func TestRemoveAvatar(t *testing.T) {
app, gdb := sessionApp(t, "0s")
bucket := publishAvatarBucket(t, app)
token := loginToken(t, app, gdb, "avatar-remove@example.test")
up := postAvatar(t, controllers.UploadAvatar(app), token, "avatar", "face.jpg", jpegBytes(t))
if up.Code != http.StatusOK {
t.Fatalf("upload %d %s", up.Code, up.Body.String())
}
var row attach.File
if err := gdb.Where("field = ?", "avatar").Last(&row).Error; err != nil {
t.Fatal(err)
}
key := attach.BlobKey(row.DiskName)
exists, err := bucket.Exists(context.Background(), key)
if err != nil || !exists {
t.Fatalf("original blob exists=%v err=%v", exists, err)
}
gone := bearer(t, controllers.RemoveAvatar(app), http.MethodPost, "/_user/api/v1/avatar/remove", token)
if gone.Code != http.StatusOK {
t.Fatalf("remove %d %s", gone.Code, gone.Body.String())
}
var body map[string]any
if err := json.Unmarshal(gone.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["message"] != "Avatar removed" {
t.Fatalf("message = %v", body["message"])
}
user, _ := body["user"].(map[string]any)
if user["has_avatar"] != false {
t.Fatalf("has_avatar = %v", user["has_avatar"])
}
var n int64
if err := gdb.Model(&attach.File{}).Where("disk_name = ?", row.DiskName).Count(&n).Error; err != nil {
t.Fatal(err)
}
if n != 0 {
t.Fatalf("system_files rows left = %d", n)
}
exists, err = bucket.Exists(context.Background(), key)
if err != nil {
t.Fatal(err)
}
if exists {
t.Fatal("original blob still present")
}
again := bearer(t, controllers.RemoveAvatar(app), http.MethodPost, "/_user/api/v1/avatar/remove", token)
const want = `{"error":"Your account has no display picture to remove.","errors":{"avatar":["Your account has no display picture to remove."]}}`
if again.Code != http.StatusUnprocessableEntity || again.Body.String() != want {
t.Fatalf("missing avatar %d %s", again.Code, again.Body.String())
}
}
func publishAvatarBucket(t *testing.T, app *backpack.App) *blob.Bucket {
t.Helper()
bucket := memblob.OpenBucket(nil)
t.Cleanup(func() { _ = bucket.Close() })
if err := attach.Publish(app, bucket); err != nil {
t.Fatal(err)
}
return bucket
}
func jpegBytes(t *testing.T) []byte {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, 8, 8))
var buf bytes.Buffer
if err := jpeg.Encode(&buf, img, nil); err != nil {
t.Fatal(err)
}
return buf.Bytes()
}
func postAvatar(t *testing.T, h http.HandlerFunc, token, field, filename string, body []byte) *httptest.ResponseRecorder {
t.Helper()
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
if field != "" {
part, err := w.CreateFormFile(field, filename)
if err != nil {
t.Fatal(err)
}
if _, err := part.Write(body); err != nil {
t.Fatal(err)
}
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPost, "/_user/api/v1/avatar", &buf)
req.Host = "example.com"
req.Header.Set("Content-Type", w.FormDataContentType())
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
rec := httptest.NewRecorder()
h(rec, req)
return rec
}
func assertAvatarError(t *testing.T, rec *httptest.ResponseRecorder, message string) {
t.Helper()
if rec.Code != http.StatusUnprocessableEntity {
t.Fatalf("status %d body %s", rec.Code, rec.Body.String())
}
var body struct {
Error string `json:"error"`
Errors map[string][]string `json:"errors"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body.Error != message || len(body.Errors["avatar"]) != 1 || body.Errors["avatar"][0] != message {
t.Fatalf("body = %s", rec.Body.String())
}
}