179 lines
6.9 KiB
Go
179 lines
6.9 KiB
Go
package handler
|
||
|
||
// 事务级软删除探针:验证「用户删除日志 → 仅软删除」的四处语义。
|
||
// 默认跳过;仅在显式设置 PROBE_DB=1 时连接真实数据库执行(需要 backed/config.toml)。
|
||
// 全过程在单个事务内进行并最终回滚,不写入任何持久数据(AutoMigrate 除外,与 server 启动行为一致)。
|
||
//
|
||
// go test ./internal/handler -run TestSoftDeleteProbe -v # 需 PROBE_DB=1
|
||
//
|
||
// 覆盖点:
|
||
// 1. 前台列表(GORM 默认作用域)不展示已删除记录
|
||
// 2. 任务完成状态判定(Unscoped 查当天成功日志)不受删除影响
|
||
// 3. 后台统计(已执行累计 / 今日状态 / 今日卡片)计入已删除记录
|
||
// 4. 后台明细 UNION SQL(adminLogUnionSQL,与 handler 同一常量)含已删除记录并正确标注 deleted/source
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"testing"
|
||
"time"
|
||
|
||
"cpdaily-new/internal/config"
|
||
"cpdaily-new/internal/database"
|
||
"cpdaily-new/internal/model"
|
||
)
|
||
|
||
func TestSoftDeleteProbe(t *testing.T) {
|
||
if os.Getenv("PROBE_DB") != "1" {
|
||
t.Skip("跳过:需要 PROBE_DB=1 且可连接数据库")
|
||
}
|
||
if err := config.Load("../../config.toml"); err != nil {
|
||
t.Fatalf("加载配置失败: %v", err)
|
||
}
|
||
database.Init()
|
||
|
||
tx := database.DB.Begin()
|
||
if tx.Error != nil {
|
||
t.Fatalf("开启事务失败: %v", tx.Error)
|
||
}
|
||
defer tx.Rollback() // 无论成败一律回滚,探针不留痕
|
||
|
||
// ---------- 构造数据(全部在事务内,ID 由自增分配) ----------
|
||
user := model.SysUser{
|
||
Username: fmt.Sprintf("sdprobe_%d", time.Now().UnixNano()),
|
||
Password: "probe",
|
||
SchoolID: "probe",
|
||
StudentID: "probe",
|
||
InviteCode: fmt.Sprintf("p%09d", time.Now().UnixNano()%1000000000), // 避免唯一索引冲突
|
||
}
|
||
if err := tx.Create(&user).Error; err != nil {
|
||
t.Fatalf("创建探针用户失败: %v", err)
|
||
}
|
||
uid := user.ID
|
||
|
||
taskLog := model.CpTaskLog{
|
||
CpTaskID: 1, UserID: uid, TaskType: "sign",
|
||
Title: "探针签到", RealTitle: "探针签到", IsSuccess: true,
|
||
}
|
||
leaveLog := model.CpLeaveLog{
|
||
UserID: uid, LeaveID: "probe_leave", Title: "探针请假",
|
||
SchoolName: "探针学校", Action: model.LeaveActionApply, IsSuccess: true,
|
||
}
|
||
if err := tx.Create(&taskLog).Error; err != nil {
|
||
t.Fatalf("创建任务日志失败: %v", err)
|
||
}
|
||
if err := tx.Create(&leaveLog).Error; err != nil {
|
||
t.Fatalf("创建请假日志失败: %v", err)
|
||
}
|
||
|
||
// 用户端「删除」= GORM 软删除(handler 中即 database.DB.Delete(&log))
|
||
if err := tx.Delete(&taskLog).Error; err != nil {
|
||
t.Fatalf("软删除任务日志失败: %v", err)
|
||
}
|
||
if err := tx.Delete(&leaveLog).Error; err != nil {
|
||
t.Fatalf("软删除请假日志失败: %v", err)
|
||
}
|
||
|
||
// 回读确认 deleted_at 已置位(而非物理删除)
|
||
var rawCount int64
|
||
if err := tx.Unscoped().Model(&model.CpTaskLog{}).Where("id = ? AND deleted_at IS NOT NULL", taskLog.ID).Count(&rawCount).Error; err != nil || rawCount != 1 {
|
||
t.Fatalf("任务日志未软删除: count=%d err=%v", rawCount, err)
|
||
}
|
||
if err := tx.Unscoped().Model(&model.CpLeaveLog{}).Where("id = ? AND deleted_at IS NOT NULL", leaveLog.ID).Count(&rawCount).Error; err != nil || rawCount != 1 {
|
||
t.Fatalf("请假日志未软删除: count=%d err=%v", rawCount, err)
|
||
}
|
||
|
||
// ---------- 1. 前台列表不可见(GORM 默认作用域过滤 deleted_at) ----------
|
||
var frontTask []model.CpTaskLog
|
||
tx.Where("user_id = ?", uid).Order("created_at DESC").Limit(10).Find(&frontTask)
|
||
if len(frontTask) != 0 {
|
||
t.Errorf("[1] 前台任务日志应不可见已删除记录, got %d 条", len(frontTask))
|
||
}
|
||
var frontLeave []model.CpLeaveLog
|
||
tx.Where("user_id = ?", uid).Order("created_at DESC").Limit(10).Find(&frontLeave)
|
||
if len(frontLeave) != 0 {
|
||
t.Errorf("[1] 前台请假日志应不可见已删除记录, got %d 条", len(frontLeave))
|
||
}
|
||
|
||
// ---------- 2. 任务完成状态不受删除影响(task.go ListTasks 的跨天重置查询) ----------
|
||
now := time.Now()
|
||
startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||
var todaySuccessIDs []uint
|
||
tx.Unscoped().Model(&model.CpTaskLog{}).
|
||
Where("user_id = ? AND is_success = 1 AND created_at >= ?", uid, startOfToday).
|
||
Distinct("cp_task_id").Pluck("cp_task_id", &todaySuccessIDs)
|
||
if len(todaySuccessIDs) != 1 || todaySuccessIDs[0] != 1 {
|
||
t.Errorf("[2] 删除当天成功日志后任务完成状态被误重置: IDs=%v", todaySuccessIDs)
|
||
}
|
||
|
||
// ---------- 3. 后台统计计入已删除 ----------
|
||
var taskSuccess, leaveSuccess int64
|
||
tx.Unscoped().Model(&model.CpTaskLog{}).Where("user_id = ? AND is_success = 1", uid).Count(&taskSuccess)
|
||
tx.Unscoped().Model(&model.CpLeaveLog{}).Where("user_id = ? AND is_success = 1", uid).Count(&leaveSuccess)
|
||
if taskSuccess != 1 || leaveSuccess != 1 {
|
||
t.Errorf("[3] 后台已执行累计应计入已删除: task=%d leave=%d", taskSuccess, leaveSuccess)
|
||
}
|
||
var todayTask, todayLeave int64
|
||
tx.Unscoped().Model(&model.CpTaskLog{}).Where("user_id = ? AND created_at >= ?", uid, startOfToday).Count(&todayTask)
|
||
tx.Unscoped().Model(&model.CpLeaveLog{}).Where("user_id = ? AND created_at >= ?", uid, startOfToday).Count(&todayLeave)
|
||
if todayTask != 1 || todayLeave != 1 {
|
||
t.Errorf("[3] 今日统计卡片应计入已删除: task=%d leave=%d", todayTask, todayLeave)
|
||
}
|
||
|
||
// ---------- 4. 后台明细 UNION(同一 adminLogUnionSQL 常量) ----------
|
||
dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||
dayEnd := dayStart.AddDate(0, 0, 1)
|
||
type combinedRow struct {
|
||
ID uint
|
||
UserID uint
|
||
TaskType string
|
||
Deleted bool
|
||
Source int
|
||
IsSuccess bool
|
||
}
|
||
var rows []combinedRow
|
||
if err := tx.Raw(adminLogUnionSQL, dayStart, dayEnd, dayStart, dayEnd).Scan(&rows).Error; err != nil {
|
||
t.Fatalf("[4] UNION 查询失败: %v", err)
|
||
}
|
||
var mine []combinedRow
|
||
for _, r := range rows {
|
||
if r.UserID == uid {
|
||
mine = append(mine, r)
|
||
}
|
||
}
|
||
if len(mine) != 2 {
|
||
t.Fatalf("[4] UNION 应返回 2 条(任务+请假,均含已删除), got %d", len(mine))
|
||
}
|
||
sourceSeen := map[int]bool{}
|
||
taskTypeSeen := map[string]bool{}
|
||
for _, r := range mine {
|
||
if !r.Deleted {
|
||
t.Errorf("[4] 已删除记录在明细中未标注 deleted: %+v", r)
|
||
}
|
||
sourceSeen[r.Source] = true
|
||
taskTypeSeen[r.TaskType] = true
|
||
}
|
||
if !sourceSeen[1] || !sourceSeen[2] {
|
||
t.Errorf("[4] source 标注缺失: %v", sourceSeen)
|
||
}
|
||
if !taskTypeSeen["sign"] || !taskTypeSeen["leave_apply"] {
|
||
t.Errorf("[4] task_type 映射缺失: %v", taskTypeSeen)
|
||
}
|
||
|
||
// 用户端状态筛选(success=1)走 UNION 包裹分支,确认占位符与包裹 SQL 兼容
|
||
var successRows []combinedRow
|
||
tx.Raw("SELECT * FROM ("+adminLogUnionSQL+") t WHERE t.is_success = 1", dayStart, dayEnd, dayStart, dayEnd).Scan(&successRows)
|
||
found := 0
|
||
for _, r := range successRows {
|
||
if r.UserID == uid {
|
||
found++
|
||
}
|
||
}
|
||
if found != 2 {
|
||
t.Errorf("[4] success 过滤分支应保留 2 条已删除成功记录, got %d", found)
|
||
}
|
||
|
||
t.Log("软删除探针全部通过:前台隐藏 / 状态不受影响 / 后台计入 / 明细标注")
|
||
// defer 回滚,探针不落库
|
||
}
|