package api_test import ( "bytes" "image" "image/color" "image/jpeg" "image/png" "io" "net/http" "testing" ) func testImage(w, h int) *image.RGBA { img := image.NewRGBA(image.Rect(0, 0, w, h)) for y := 0; y < h; y++ { for x := 0; x < w; x++ { img.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), B: 200, A: 255}) } } return img } func pngBytes(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() } // putRaw sends a non-JSON body with the administrator's key. func (s *ts) putRaw(t *testing.T, path string, body []byte) *http.Response { t.Helper() req, _ := http.NewRequest(http.MethodPut, s.URL+path, bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+s.key) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } return resp } func TestAvatar_UploadNormalizesAndServes(t *testing.T) { s := newTS(t) uid, call := member(t, s, "ava") path := "/api/users/" + id64(uid) + "/avatar" if resp := call(http.MethodGet, path, nil); resp.StatusCode != http.StatusNotFound { t.Fatalf("GET before upload: %d, want 404", resp.StatusCode) } // A non-square JPEG, bigger than the stored size: cropped and scaled to 256². var jb bytes.Buffer if err := jpeg.Encode(&jb, testImage(600, 400), nil); err != nil { t.Fatal(err) } resp := s.putRaw(t, path, jb.Bytes()) // an administrator may set anybody's if resp.StatusCode != http.StatusOK { t.Fatalf("PUT: %d", resp.StatusCode) } var out struct { Version int64 `json:"avatar_version"` } decode(t, resp, &out) resp = call(http.MethodGet, path, nil) if resp.StatusCode != http.StatusOK || resp.Header.Get("Content-Type") != "image/png" { t.Fatalf("GET: %d %q", resp.StatusCode, resp.Header.Get("Content-Type")) } body, _ := io.ReadAll(resp.Body) resp.Body.Close() img, err := png.Decode(bytes.NewReader(body)) if err != nil { t.Fatalf("served bytes are not a PNG: %v", err) } if b := img.Bounds(); b.Dx() != 256 || b.Dy() != 256 { t.Errorf("stored size %v, want 256x256", b) } var users []struct { ID int64 `json:"id"` AvatarVersion *int64 `json:"avatar_version"` } decode(t, call(http.MethodGet, "/api/users", nil), &users) found := false for _, u := range users { if u.ID == uid { found = true if u.AvatarVersion == nil || *u.AvatarVersion != out.Version { t.Errorf("user list avatar_version = %v, want %d", u.AvatarVersion, out.Version) } } } if !found { t.Fatal("user not listed") } if resp := call(http.MethodDelete, path, nil); resp.StatusCode != http.StatusNoContent { t.Fatalf("DELETE: %d", resp.StatusCode) } if resp := call(http.MethodGet, path, nil); resp.StatusCode != http.StatusNotFound { t.Fatalf("GET after delete: %d, want 404", resp.StatusCode) } } func TestAvatar_Rejections(t *testing.T) { s := newTS(t) _, other := member(t, s, "other") uid, _ := member(t, s, "ava2") path := "/api/users/" + id64(uid) + "/avatar" if resp := other(http.MethodPut, path, nil); resp.StatusCode != http.StatusForbidden { t.Errorf("another member PUT: %d, want 403", resp.StatusCode) } if resp := other(http.MethodDelete, path, nil); resp.StatusCode != http.StatusForbidden { t.Errorf("another member DELETE: %d, want 403", resp.StatusCode) } if resp := s.putRaw(t, path, []byte("not an image")); resp.StatusCode != http.StatusBadRequest { t.Errorf("garbage: %d, want 400", resp.StatusCode) } if resp := s.putRaw(t, path, make([]byte, 2<<20)); resp.StatusCode != http.StatusRequestEntityTooLarge { t.Errorf("oversize: %d, want 413", resp.StatusCode) } if resp := s.putRaw(t, "/api/users/999999/avatar", pngBytes(t, testImage(32, 32))); resp.StatusCode != http.StatusNotFound { t.Errorf("unknown user: %d, want 404", resp.StatusCode) } req, _ := http.NewRequest(http.MethodGet, s.URL+path, nil) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusUnauthorized { t.Errorf("unauthenticated GET: %d, want 401", resp.StatusCode) } } func TestAvatar_RemovedWithTheUser(t *testing.T) { s := newTS(t) uid, _ := member(t, s, "gone") path := "/api/users/" + id64(uid) + "/avatar" if resp := s.putRaw(t, path, pngBytes(t, testImage(64, 64))); resp.StatusCode != http.StatusOK { t.Fatalf("PUT: %d", resp.StatusCode) } if resp := s.req(t, http.MethodDelete, "/api/users/"+id64(uid), nil); resp.StatusCode >= 300 { t.Fatalf("delete user: %d", resp.StatusCode) } var n int if err := s.db.QueryRow("SELECT COUNT(*) FROM user_avatars WHERE user_id = $1", uid).Scan(&n); err != nil || n != 0 { t.Errorf("avatar rows after user delete = %d (%v), want 0", n, err) } }