Files
ttstd-infinite-canvas/backend/internal/app/admin_storage_delete_test.go

191 lines
8.2 KiB
Go

package app
import (
"os"
"path/filepath"
"strings"
"testing"
"infinite-canvas/backend/internal/database"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestDeleteAdminResourcesSupportsPartialSuccessAndBatchedReferences(t *testing.T) {
svc, db, dataDir, admin := newAdminStorageDeleteTestService(t)
resources := []model.Resource{
{ID: "resource-free", UserID: "user-1", Kind: "image", Status: model.ResourceStatusReady, Provider: "local", ObjectKey: "users/user-1/image/delete-directory"},
{ID: "resource-blocked", UserID: "user-1", Kind: "image", Status: model.ResourceStatusReady, Provider: "local", ObjectKey: "users/user-1/image/blocked.png"},
}
if err := db.Create(&resources).Error; err != nil {
t.Fatal(err)
}
canvas := model.CanvasProject{ID: "canvas-1", UserID: "user-1", Title: "广告分镜", PayloadJSON: `{"nodes":[{"data":{"storageKey":"resource:resource-blocked"}}]}`}
if err := db.Create(&canvas).Error; err != nil {
t.Fatal(err)
}
directoryPath := filepath.Join(dataDir, "resources", filepath.FromSlash(resources[0].ObjectKey))
if err := os.MkdirAll(directoryPath, 0o750); err != nil {
t.Fatal(err)
}
result, err := svc.DeleteAdminResources(admin, AdminResourceDeleteRequest{ResourceIDs: []string{"resource-free", "resource-blocked", "resource-free"}})
if err != nil {
t.Fatal(err)
}
if len(result.Deleted) != 1 || result.Deleted[0] != "resource-free" {
t.Fatalf("deleted = %#v", result.Deleted)
}
if len(result.Blocked) != 1 || result.Blocked[0].ID != "resource-blocked" || len(result.Blocked[0].References) != 1 || result.Blocked[0].References[0].Kind != "画布" {
t.Fatalf("blocked = %#v", result.Blocked)
}
assertModelCount(t, db, &model.Resource{}, "id = ?", 0, "resource-free")
assertModelCount(t, db, &model.Resource{}, "id = ?", 1, "resource-blocked")
assertModelCount(t, db, &model.ResourceDeletionJob{}, "resource_id = ?", 1, "resource-free")
assertModelCount(t, db, &model.AdminAuditEvent{}, "action = ? AND target_id = ?", 1, "resource.delete", "resource-free")
}
func TestDeleteAdminResourcesBlocksAnnouncementImage(t *testing.T) {
svc, db, _, admin := newAdminStorageDeleteTestService(t)
resource := model.Resource{ID: "announcement-image", UserID: admin.ID, Kind: "image", Status: model.ResourceStatusReady, Provider: "local", ObjectKey: "announcements/cover.png"}
announcement := model.Announcement{ID: "announcement-1", Title: "维护通知", Content: "content", ImageResourceID: resource.ID, CreatedBy: admin.ID, Status: model.AnnouncementStatusActive}
if err := db.Create(&resource).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&announcement).Error; err != nil {
t.Fatal(err)
}
result, err := svc.DeleteAdminResources(admin, AdminResourceDeleteRequest{ResourceIDs: []string{resource.ID}})
if err != nil {
t.Fatal(err)
}
if len(result.Deleted) != 0 || len(result.Blocked) != 1 || result.Blocked[0].References[0].Kind != "公告" {
t.Fatalf("result = %#v", result)
}
assertModelCount(t, db, &model.Resource{}, "id = ?", 1, resource.ID)
}
func TestDeleteAdminResourcesBlocksDarkAppearanceLogo(t *testing.T) {
svc, db, _, admin := newAdminStorageDeleteTestService(t)
resource := model.Resource{ID: "appearance-logo", UserID: admin.ID, Kind: "image", Status: model.ResourceStatusReady, Provider: "local", ObjectKey: "appearance/logo.png", MimeType: "image/png"}
if err := db.Create(&resource).Error; err != nil {
t.Fatal(err)
}
if _, err := svc.UpdateAppearance(admin, AppearanceSetting{BrandName: "HIMA Studio", BrandSlug: "hima-studio", AuthHeroTitle: defaultAppearanceHeroTitle, DarkLogoResourceID: resource.ID, LogoFrameEnabled: true, SkinID: defaultAppearanceSkinID}); err != nil {
t.Fatal(err)
}
result, err := svc.DeleteAdminResources(admin, AdminResourceDeleteRequest{ResourceIDs: []string{resource.ID}})
if err != nil {
t.Fatal(err)
}
if len(result.Deleted) != 0 || len(result.Blocked) != 1 || len(result.Blocked[0].References) != 1 || result.Blocked[0].References[0].Kind != "外观" {
t.Fatalf("result = %#v", result)
}
assertModelCount(t, db, &model.Resource{}, "id = ?", 1, resource.ID)
}
func TestDeleteAdminResourcesKeepsSharedPhysicalObject(t *testing.T) {
svc, db, _, admin := newAdminStorageDeleteTestService(t)
resources := []model.Resource{
{ID: "resource-delete", UserID: "user-1", Kind: "image", Status: model.ResourceStatusReady, Provider: "", ObjectKey: "shared/object.png"},
{ID: "resource-keep", UserID: "user-2", Kind: "image", Status: model.ResourceStatusReady, Provider: "local", ObjectKey: "shared/object.png"},
}
if err := db.Create(&resources).Error; err != nil {
t.Fatal(err)
}
result, err := svc.DeleteAdminResources(admin, AdminResourceDeleteRequest{ResourceIDs: []string{"resource-delete"}})
if err != nil {
t.Fatal(err)
}
if len(result.Deleted) != 1 {
t.Fatalf("result = %#v", result)
}
assertModelCount(t, db, &model.Resource{}, "id = ?", 0, "resource-delete")
assertModelCount(t, db, &model.Resource{}, "id = ?", 1, "resource-keep")
assertModelCount(t, db, &model.ResourceDeletionJob{}, "1 = 1", 0)
var audit model.AdminAuditEvent
if err := db.First(&audit, "target_id = ?", "resource-delete").Error; err != nil {
t.Fatal(err)
}
if !strings.Contains(audit.MetadataJSON, `"physicalDeleteQueued":false`) {
t.Fatalf("audit metadata = %s", audit.MetadataJSON)
}
}
func TestDeleteAdminResourcesRollsBackWhenAuditInsertFails(t *testing.T) {
svc, db, _, admin := newAdminStorageDeleteTestService(t)
resource := model.Resource{ID: "resource-rollback", UserID: admin.ID, Kind: "image", Status: model.ResourceStatusReady, Provider: "local"}
binding := model.ArkPrivateAssetBinding{ID: "binding-1", UserID: admin.ID, ResourceID: resource.ID, ProjectName: "project", Status: "active"}
draft := model.AnnouncementImageDraft{ResourceID: resource.ID, UserID: admin.ID}
if err := db.Create(&resource).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&binding).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&draft).Error; err != nil {
t.Fatal(err)
}
if err := db.Exec("CREATE TRIGGER fail_resource_audit BEFORE INSERT ON admin_audit_events WHEN NEW.action = 'resource.delete' BEGIN SELECT RAISE(ABORT, 'forced audit failure'); END;").Error; err != nil {
t.Fatal(err)
}
if _, err := svc.DeleteAdminResources(admin, AdminResourceDeleteRequest{ResourceIDs: []string{resource.ID}}); err == nil {
t.Fatal("expected audit failure")
}
assertModelCount(t, db, &model.Resource{}, "id = ?", 1, resource.ID)
assertModelCount(t, db, &model.ArkPrivateAssetBinding{}, "resource_id = ?", 1, resource.ID)
assertModelCount(t, db, &model.AnnouncementImageDraft{}, "resource_id = ?", 1, resource.ID)
assertModelCount(t, db, &model.ResourceDeletionJob{}, "1 = 1", 0)
}
func TestDeleteAdminResourcesRejectsNonAdminAndOversizedBatch(t *testing.T) {
svc := &Service{}
user := &model.User{ID: "user", Role: model.UserRoleUser, Status: model.UserStatusActive}
if _, err := svc.DeleteAdminResources(user, AdminResourceDeleteRequest{ResourceIDs: []string{"resource-1"}}); err == nil {
t.Fatal("expected non-admin rejection")
}
ids := make([]string, maxAdminResourceDeleteCount+1)
for index := range ids {
ids[index] = newID()
}
if _, err := normalizeAdminResourceDeleteIDs(ids); err == nil {
t.Fatal("expected oversized batch rejection")
}
}
func newAdminStorageDeleteTestService(t *testing.T) (*Service, *gorm.DB, string, *model.User) {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+newID()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := database.MigrateSchema(db); err != nil {
t.Fatal(err)
}
admin := &model.User{ID: "admin", Username: "admin", Role: model.UserRoleAdmin, Status: model.UserStatusActive}
if err := db.Create(admin).Error; err != nil {
t.Fatal(err)
}
dataDir := t.TempDir()
return New(repository.New(db), dataDir), db, dataDir, admin
}
func assertModelCount(t *testing.T, db *gorm.DB, value any, query string, expected int64, args ...any) {
t.Helper()
var count int64
dbQuery := db.Model(value).Where(query, args...)
if err := dbQuery.Count(&count).Error; err != nil {
t.Fatal(err)
}
if count != expected {
t.Fatalf("count = %d, want %d", count, expected)
}
}