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(``)) 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()) } }