fix(*): 素材库与画布 - 修复批量删除刷新、选择布局及工具引用
This commit is contained in:
1 parent
c5b81f696c
commit
dc1ad68015
27 files changed
+852
-109
No files matched your search
@@ -0,0 +1,126 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/auth"
|
||||
"infinite-canvas/backend/internal/database"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
"infinite-canvas/backend/internal/repository"
|
||||
"infinite-canvas/backend/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestAssetBatchDeleteHTTP(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
if err := database.MigrateSchema(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, record := range []any{
|
||||
&model.User{ID: "batch-user", Username: "batch-user", Email: "batch@example.invalid", Role: model.UserRoleUser, Status: model.UserStatusActive},
|
||||
&model.AuthSession{ID: "batch-session", UserID: "batch-user", TokenHash: auth.HashToken("test-token"), ExpiresAt: time.Now().Add(time.Hour)},
|
||||
&model.Asset{ID: "foreign", UserID: "other-user", PayloadJSON: `{}`},
|
||||
} {
|
||||
if err := db.Create(record).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
ids := make([]string, 100)
|
||||
for i := range ids {
|
||||
ids[i] = fmt.Sprintf("batch-%03d", i)
|
||||
if err := db.Create(&model.Asset{ID: ids[i], UserID: "batch-user", Status: model.AssetVersionStatusConfirmed, PayloadJSON: `{}`}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
router := gin.New()
|
||||
RegisterUserDataRoutes(router.Group("/api"), service.New(repository.New(db), t.TempDir()))
|
||||
call := func(method, path, body string, authenticated bool) *httptest.ResponseRecorder {
|
||||
r := httptest.NewRequest(method, path, bytes.NewBufferString(body))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
if authenticated {
|
||||
r.AddCookie(&http.Cookie{Name: service.SessionCookieName, Value: "batch-session.test-token"})
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, r)
|
||||
return w
|
||||
}
|
||||
assertAssets := func(want int64) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
if err := db.Model(&model.Asset{}).Count(&count).Error; err != nil || count != want {
|
||||
t.Fatalf("asset count=%d want=%d err=%v", count, want, err)
|
||||
}
|
||||
}
|
||||
path := "/api/assets/batch-delete"
|
||||
if w := call(http.MethodPost, path, `["batch-000"]`, false); w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("unauthenticated: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
for _, body := range []string{`[]`, `null`, `{ "ids": ["batch-000"] }`, `["batch-000", ""]`, `[1]`} {
|
||||
if w := call(http.MethodPost, path, body, true); w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("invalid body %s: %d %s", body, w.Code, w.Body.String())
|
||||
}
|
||||
assertAssets(101)
|
||||
}
|
||||
for _, badID := range []string{"foreign", "missing"} {
|
||||
w := call(http.MethodPost, path, `["batch-000","`+badID+`"]`, true)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("ownership/missing %s: %d %s", badID, w.Code, w.Body.String())
|
||||
}
|
||||
assertAssets(101)
|
||||
}
|
||||
body, _ := json.Marshal(ids)
|
||||
if err := db.Exec("CREATE TRIGGER fail_batch_delete BEFORE DELETE ON assets WHEN OLD.id = 'batch-099' BEGIN SELECT RAISE(ABORT, 'forced failure'); END;").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w := call(http.MethodPost, path, string(body), true); w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("database failure: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
assertAssets(101)
|
||||
if err := db.Exec("DROP TRIGGER fail_batch_delete").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := call(http.MethodPost, path, string(body), true)
|
||||
var response struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
IDs []string `json:"ids"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil || w.Code != http.StatusOK || response.Code != 0 || len(response.Data.IDs) != 100 {
|
||||
t.Fatalf("batch response: %d %s err=%v", w.Code, w.Body.String(), err)
|
||||
}
|
||||
assertAssets(1)
|
||||
|
||||
// The existing DELETE endpoint uses the same purge policy for non-archived assets.
|
||||
resource := model.Resource{ID: "single-resource", UserID: "batch-user", Provider: "unsupported-test-provider", ObjectKey: "single.png"}
|
||||
payload := `{"url":"/api/resources/single-resource/file"}`
|
||||
for _, record := range []any{
|
||||
&resource,
|
||||
&model.Asset{ID: "single", UserID: "batch-user", Status: model.AssetVersionStatusConfirmed, PayloadJSON: payload},
|
||||
&model.Task{ID: "running-task", UserID: "batch-user", Status: model.TaskStatusRunning, InputJSON: payload},
|
||||
} {
|
||||
if err := db.Create(record).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if w := call(http.MethodDelete, "/api/assets/single", "", true); w.Code != http.StatusOK {
|
||||
t.Fatalf("single referenced delete: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
assertAssets(1)
|
||||
}
|
||||
Reference in new issue
Block a user