// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package db import ( "context" "testing" "github.com/stretchr/testify/require" "gorm.io/gorm" ) // TestQuestionDeleteCascadesHardDelete 验证删除提问时,追问与上传图片也被**真删**, // 而不是软删残留。 // // 背景:questions.DeleteByID 对主表用 Unscoped 真删,但历史上一度只有主表真删、 // 追问/图片仍软删,导致提问正文删干净了、追问文本与图片元数据却带着 deleted_at // 永久留在库里(这正是「真删以免个人数据删不干净」想消灭的问题,却在子表漏改)。 // 本测试用 Unscoped 计数兜底:软删的行在普通查询里不可见,只有 Unscoped 才能看到, // 因此断言「删除后 Unscoped 计数 = 0」才能证明是真删。 func TestQuestionDeleteCascadesHardDelete(t *testing.T) { db, cleanup := newTestDB(t) ctx := context.Background() questions := NewQuestionsStore(db) followUps := NewFollowUpsStore(db) uploadImages := NewUploadImagesStore(db) t.Cleanup(func() { // 三张表都要清:follow_ups / upload_images(含链接表)在删除后应该已经空了, // 但其他子测试的 data 可能残留,用 cleanup 统一兜底。 _ = cleanup("questions", "follow_ups", "upload_images", "upload_image_questions") }) // 建一条提问 q, err := questions.Create(ctx, CreateQuestionOptions{ FromIP: "114.5.1.4", UserID: 1, Content: "需要被彻底删除的提问", AskerUserID: 1, }) require.NoError(t, err) // 给它加一条追问 created, err := followUps.CreateIfBelowLimit(ctx, CreateFollowUpOptions{ QuestionID: q.ID, Content: "追问内容,也应被真删", }) require.NoError(t, err) require.True(t, created) // 给它加一张上传图片 img, err := uploadImages.Create(ctx, CreateUploadImageOptions{ Type: UploadImageQuestionTypeAsk, QuestionID: q.ID, UploaderUserID: 1, Name: "secret.png", FileSize: 1024, Md5: "deadbeef", Key: "2026/10/29/secret.png", }) require.NoError(t, err) require.NotZero(t, img.ID) // 删除提问 require.NoError(t, questions.DeleteByID(ctx, q.ID)) // 主表:真删 require.Equal(t, int64(0), hardCount(t, db, &Question{}, "id = ?", q.ID)) // 追问:真删(软删的话这里会是 1) require.Equal(t, int64(0), hardCount(t, db, &FollowUp{}, "question_id = ?", q.ID)) // 上传图片主体:真删 require.Equal(t, int64(0), hardCount(t, db, &UploadImage{}, "id = ?", img.ID)) // 关联行:真删 require.Equal(t, int64(0), hardCount(t, db, &UploadImageQuestion{}, "question_id = ?", q.ID)) } // hardCount 用 Unscoped 统计满足条件的行数,能穿透软删除过滤, // 用于断言「物理上也不存在」。 func hardCount(t *testing.T, db *gorm.DB, model interface{}, where string, args ...interface{}) int64 { t.Helper() var n int64 err := db.WithContext(context.Background()).Unscoped(). Model(model). Where(where, args...). Count(&n).Error require.NoError(t, err) return n }