fix(数据层): 修复数据完整性与仓储一致性问题
## 数据完整性修复 (fix-critical-data-integrity) - 添加 error_translate.go 统一错误转换层 - 修复 nil 输入和 NotFound 错误处理 - 增强仓储层错误一致性 ## 仓储一致性修复 (fix-high-repository-consistency) - Group schema 添加 default_validity_days 字段 - Account schema 添加 proxy edge 关联 - 新增 UsageLog ent schema 定义 - 修复 UpdateBalance/UpdateConcurrency 受影响行数校验 ## 数据卫生修复 (fix-medium-data-hygiene) - UserSubscription 添加软删除支持 (SoftDeleteMixin) - RedeemCode/Setting 添加硬删除策略文档 - account_groups/user_allowed_groups 的 created_at 声明 timestamptz - 停止写入 legacy users.allowed_groups 列 - 新增迁移: 011-014 (索引优化、软删除、孤立数据审计、列清理) ## 测试补充 - 添加 UserSubscription 软删除测试 - 添加迁移回归测试 - 添加 NotFound 错误测试 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,10 +7,12 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/ent/apikey"
|
||||
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||
"github.com/Wei-Shaw/sub2api/ent/usersubscription"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -111,3 +113,104 @@ func TestEntSoftDelete_ApiKey_HardDeleteViaSkipSoftDelete(t *testing.T) {
|
||||
Only(mixins.SkipSoftDelete(ctx))
|
||||
require.True(t, dbent.IsNotFound(err), "expected row to be hard deleted")
|
||||
}
|
||||
|
||||
// --- UserSubscription 软删除测试 ---
|
||||
|
||||
func createEntGroup(t *testing.T, ctx context.Context, client *dbent.Client, name string) *dbent.Group {
|
||||
t.Helper()
|
||||
|
||||
g, err := client.Group.Create().
|
||||
SetName(name).
|
||||
SetStatus(service.StatusActive).
|
||||
Save(ctx)
|
||||
require.NoError(t, err, "create ent group")
|
||||
return g
|
||||
}
|
||||
|
||||
func TestEntSoftDelete_UserSubscription_DefaultFilterAndSkip(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := testEntClient(t)
|
||||
|
||||
u := createEntUser(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-user")+"@example.com")
|
||||
g := createEntGroup(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-group"))
|
||||
|
||||
repo := NewUserSubscriptionRepository(client)
|
||||
sub := &service.UserSubscription{
|
||||
UserID: u.ID,
|
||||
GroupID: g.ID,
|
||||
Status: service.SubscriptionStatusActive,
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
require.NoError(t, repo.Create(ctx, sub), "create user subscription")
|
||||
|
||||
require.NoError(t, repo.Delete(ctx, sub.ID), "soft delete user subscription")
|
||||
|
||||
_, err := repo.GetByID(ctx, sub.ID)
|
||||
require.Error(t, err, "deleted rows should be hidden by default")
|
||||
|
||||
_, err = client.UserSubscription.Query().Where(usersubscription.IDEQ(sub.ID)).Only(ctx)
|
||||
require.Error(t, err, "default ent query should not see soft-deleted rows")
|
||||
require.True(t, dbent.IsNotFound(err), "expected ent not-found after default soft delete filter")
|
||||
|
||||
got, err := client.UserSubscription.Query().
|
||||
Where(usersubscription.IDEQ(sub.ID)).
|
||||
Only(mixins.SkipSoftDelete(ctx))
|
||||
require.NoError(t, err, "SkipSoftDelete should include soft-deleted rows")
|
||||
require.NotNil(t, got.DeletedAt, "deleted_at should be set after soft delete")
|
||||
}
|
||||
|
||||
func TestEntSoftDelete_UserSubscription_DeleteIdempotent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := testEntClient(t)
|
||||
|
||||
u := createEntUser(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-user2")+"@example.com")
|
||||
g := createEntGroup(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-group2"))
|
||||
|
||||
repo := NewUserSubscriptionRepository(client)
|
||||
sub := &service.UserSubscription{
|
||||
UserID: u.ID,
|
||||
GroupID: g.ID,
|
||||
Status: service.SubscriptionStatusActive,
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
require.NoError(t, repo.Create(ctx, sub), "create user subscription")
|
||||
|
||||
require.NoError(t, repo.Delete(ctx, sub.ID), "first delete")
|
||||
require.NoError(t, repo.Delete(ctx, sub.ID), "second delete should be idempotent")
|
||||
}
|
||||
|
||||
func TestEntSoftDelete_UserSubscription_ListExcludesDeleted(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := testEntClient(t)
|
||||
|
||||
u := createEntUser(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-user3")+"@example.com")
|
||||
g1 := createEntGroup(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-group3a"))
|
||||
g2 := createEntGroup(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-group3b"))
|
||||
|
||||
repo := NewUserSubscriptionRepository(client)
|
||||
|
||||
sub1 := &service.UserSubscription{
|
||||
UserID: u.ID,
|
||||
GroupID: g1.ID,
|
||||
Status: service.SubscriptionStatusActive,
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
require.NoError(t, repo.Create(ctx, sub1), "create subscription 1")
|
||||
|
||||
sub2 := &service.UserSubscription{
|
||||
UserID: u.ID,
|
||||
GroupID: g2.ID,
|
||||
Status: service.SubscriptionStatusActive,
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
require.NoError(t, repo.Create(ctx, sub2), "create subscription 2")
|
||||
|
||||
// 软删除 sub1
|
||||
require.NoError(t, repo.Delete(ctx, sub1.ID), "soft delete subscription 1")
|
||||
|
||||
// ListByUserID 应只返回未删除的订阅
|
||||
subs, err := repo.ListByUserID(ctx, u.ID)
|
||||
require.NoError(t, err, "ListByUserID")
|
||||
require.Len(t, subs, 1, "should only return non-deleted subscriptions")
|
||||
require.Equal(t, sub2.ID, subs[0].ID, "expected sub2 to be returned")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user