Show images in asset directories with preview, rename, replacement, and trash recovery. Update local Markdown references when images move and restore original files if related metadata cannot be committed. Refresh image previews after replacement and compare actual contents when deduplicating subsequent uploads.
59 lines
2.4 KiB
Go
59 lines
2.4 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"testing"
|
|
)
|
|
|
|
func TestReplaceImageAPI(t *testing.T) {
|
|
f := newFixture(t)
|
|
f.write("assets/a.png", "original")
|
|
f.write("a.md", "")
|
|
var body bytes.Buffer
|
|
form := multipart.NewWriter(&body)
|
|
file, err := form.CreateFormFile("file", "replacement.png")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := io.WriteString(file, "replacement"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := form.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
headers := map[string]string{"Content-Type": form.FormDataContentType()}
|
|
if code := f.status(newClient(), "PUT", "/api/v1/assets/assets/a.png", body.String(), headers); code != 401 {
|
|
t.Fatalf("未登录应被拒绝:%d", code)
|
|
}
|
|
readToken := decode[struct{ Token string }](t, f.do(f.admin, "POST", "/api/v1/tokens", `{"name":"read","scope":"read"}`, nil)).Token
|
|
if code := f.status(newClient(), "PUT", "/api/v1/assets/assets/a.png", body.String(), map[string]string{"Content-Type": form.FormDataContentType(), "Authorization": "Bearer " + readToken}); code != 403 {
|
|
t.Fatalf("只读令牌不能替换图片:%d", code)
|
|
}
|
|
for route, want := range map[string]int{"/api/v1/assets/assets/a.png": 200, "/api/v1/assets/missing.png": 404, "/api/v1/assets/a.md": 400} {
|
|
if code := f.status(f.admin, "PUT", route, body.String(), headers); code != want {
|
|
t.Fatalf("%s:得到 %d,期望 %d", route, code, want)
|
|
}
|
|
}
|
|
res := f.do(f.admin, "GET", "/api/v1/raw/assets/a.png", "", nil)
|
|
data, err := io.ReadAll(res.Body)
|
|
if err != nil || string(data) != "replacement" || res.Header.Get("Content-Security-Policy") == "" || res.Header.Get("Cache-Control") != "no-store" {
|
|
t.Fatalf("替换内容或安全边界错误:%q %v", data, err)
|
|
}
|
|
note := f.do(f.admin, "GET", "/api/v1/files/a.md", "", nil)
|
|
data, err = io.ReadAll(note.Body)
|
|
if err != nil || string(data) != "" {
|
|
t.Fatalf("替换不应改变引用:%q %v", data, err)
|
|
}
|
|
// 复用现有移动与回收站接口,图片不会被当作 Markdown 正文读取。
|
|
if code := f.status(f.admin, "POST", "/api/v1/move", `{"from":"assets/a.png","to":"assets/b.png"}`, nil); code != 200 {
|
|
t.Fatal(code)
|
|
}
|
|
deleted := decode[struct{ ID string }](t, f.do(f.admin, "DELETE", "/api/v1/files/assets/b.png", "", nil))
|
|
if code := f.status(f.admin, "POST", "/api/v1/trash/"+deleted.ID+"/restore", "", nil); code != http.StatusOK {
|
|
t.Fatal(code)
|
|
}
|
|
}
|