soft_delete_ent_integration_test.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. //go:build integration
  2. package repository
  3. import (
  4. "context"
  5. "fmt"
  6. "strings"
  7. "testing"
  8. "time"
  9. dbent "github.com/Wei-Shaw/sub2api/ent"
  10. "github.com/Wei-Shaw/sub2api/ent/apikey"
  11. "github.com/Wei-Shaw/sub2api/ent/schema/mixins"
  12. "github.com/Wei-Shaw/sub2api/ent/usersubscription"
  13. "github.com/Wei-Shaw/sub2api/internal/service"
  14. "github.com/stretchr/testify/require"
  15. )
  16. func uniqueSoftDeleteValue(t *testing.T, prefix string) string {
  17. t.Helper()
  18. safeName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
  19. return fmt.Sprintf("%s-%s", prefix, safeName)
  20. }
  21. func createEntUser(t *testing.T, ctx context.Context, client *dbent.Client, email string) *dbent.User {
  22. t.Helper()
  23. u, err := client.User.Create().
  24. SetEmail(email).
  25. SetPasswordHash("test-password-hash").
  26. Save(ctx)
  27. require.NoError(t, err, "create ent user")
  28. return u
  29. }
  30. func TestEntSoftDelete_ApiKey_DefaultFilterAndSkip(t *testing.T) {
  31. ctx := context.Background()
  32. // 使用全局 ent client,确保软删除验证在实际持久化数据上进行。
  33. client := testEntClient(t)
  34. u := createEntUser(t, ctx, client, uniqueSoftDeleteValue(t, "sd-user")+"@example.com")
  35. repo := NewAPIKeyRepository(client, integrationDB)
  36. key := &service.APIKey{
  37. UserID: u.ID,
  38. Key: uniqueSoftDeleteValue(t, "sk-soft-delete"),
  39. Name: "soft-delete",
  40. Status: service.StatusActive,
  41. }
  42. require.NoError(t, repo.Create(ctx, key), "create api key")
  43. require.NoError(t, repo.Delete(ctx, key.ID), "soft delete api key")
  44. _, err := repo.GetByID(ctx, key.ID)
  45. require.ErrorIs(t, err, service.ErrAPIKeyNotFound, "deleted rows should be hidden by default")
  46. _, err = client.APIKey.Query().Where(apikey.IDEQ(key.ID)).Only(ctx)
  47. require.Error(t, err, "default ent query should not see soft-deleted rows")
  48. require.True(t, dbent.IsNotFound(err), "expected ent not-found after default soft delete filter")
  49. got, err := client.APIKey.Query().
  50. Where(apikey.IDEQ(key.ID)).
  51. Only(mixins.SkipSoftDelete(ctx))
  52. require.NoError(t, err, "SkipSoftDelete should include soft-deleted rows")
  53. require.NotNil(t, got.DeletedAt, "deleted_at should be set after soft delete")
  54. }
  55. func TestEntSoftDelete_ApiKey_DeleteIdempotent(t *testing.T) {
  56. ctx := context.Background()
  57. // 使用全局 ent client,避免事务回滚影响幂等性验证。
  58. client := testEntClient(t)
  59. u := createEntUser(t, ctx, client, uniqueSoftDeleteValue(t, "sd-user2")+"@example.com")
  60. repo := NewAPIKeyRepository(client, integrationDB)
  61. key := &service.APIKey{
  62. UserID: u.ID,
  63. Key: uniqueSoftDeleteValue(t, "sk-soft-delete2"),
  64. Name: "soft-delete2",
  65. Status: service.StatusActive,
  66. }
  67. require.NoError(t, repo.Create(ctx, key), "create api key")
  68. require.NoError(t, repo.Delete(ctx, key.ID), "first delete")
  69. require.NoError(t, repo.Delete(ctx, key.ID), "second delete should be idempotent")
  70. }
  71. func TestEntSoftDelete_ApiKey_HardDeleteViaSkipSoftDelete(t *testing.T) {
  72. ctx := context.Background()
  73. // 使用全局 ent client,确保 SkipSoftDelete 的硬删除语义可验证。
  74. client := testEntClient(t)
  75. u := createEntUser(t, ctx, client, uniqueSoftDeleteValue(t, "sd-user3")+"@example.com")
  76. repo := NewAPIKeyRepository(client, integrationDB)
  77. key := &service.APIKey{
  78. UserID: u.ID,
  79. Key: uniqueSoftDeleteValue(t, "sk-soft-delete3"),
  80. Name: "soft-delete3",
  81. Status: service.StatusActive,
  82. }
  83. require.NoError(t, repo.Create(ctx, key), "create api key")
  84. require.NoError(t, repo.Delete(ctx, key.ID), "soft delete api key")
  85. // Hard delete using SkipSoftDelete so the hook doesn't convert it to update-deleted_at.
  86. _, err := client.APIKey.Delete().Where(apikey.IDEQ(key.ID)).Exec(mixins.SkipSoftDelete(ctx))
  87. require.NoError(t, err, "hard delete")
  88. _, err = client.APIKey.Query().
  89. Where(apikey.IDEQ(key.ID)).
  90. Only(mixins.SkipSoftDelete(ctx))
  91. require.True(t, dbent.IsNotFound(err), "expected row to be hard deleted")
  92. }
  93. // --- UserSubscription 软删除测试 ---
  94. func createEntGroup(t *testing.T, ctx context.Context, client *dbent.Client, name string) *dbent.Group {
  95. t.Helper()
  96. g, err := client.Group.Create().
  97. SetName(name).
  98. SetStatus(service.StatusActive).
  99. Save(ctx)
  100. require.NoError(t, err, "create ent group")
  101. return g
  102. }
  103. func TestEntSoftDelete_UserSubscription_DefaultFilterAndSkip(t *testing.T) {
  104. ctx := context.Background()
  105. client := testEntClient(t)
  106. u := createEntUser(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-user")+"@example.com")
  107. g := createEntGroup(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-group"))
  108. repo := NewUserSubscriptionRepository(client)
  109. sub := &service.UserSubscription{
  110. UserID: u.ID,
  111. GroupID: g.ID,
  112. Status: service.SubscriptionStatusActive,
  113. ExpiresAt: time.Now().Add(24 * time.Hour),
  114. }
  115. require.NoError(t, repo.Create(ctx, sub), "create user subscription")
  116. require.NoError(t, repo.Delete(ctx, sub.ID), "soft delete user subscription")
  117. _, err := repo.GetByID(ctx, sub.ID)
  118. require.Error(t, err, "deleted rows should be hidden by default")
  119. _, err = client.UserSubscription.Query().Where(usersubscription.IDEQ(sub.ID)).Only(ctx)
  120. require.Error(t, err, "default ent query should not see soft-deleted rows")
  121. require.True(t, dbent.IsNotFound(err), "expected ent not-found after default soft delete filter")
  122. got, err := client.UserSubscription.Query().
  123. Where(usersubscription.IDEQ(sub.ID)).
  124. Only(mixins.SkipSoftDelete(ctx))
  125. require.NoError(t, err, "SkipSoftDelete should include soft-deleted rows")
  126. require.NotNil(t, got.DeletedAt, "deleted_at should be set after soft delete")
  127. }
  128. func TestEntSoftDelete_UserSubscription_DeleteIdempotent(t *testing.T) {
  129. ctx := context.Background()
  130. client := testEntClient(t)
  131. u := createEntUser(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-user2")+"@example.com")
  132. g := createEntGroup(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-group2"))
  133. repo := NewUserSubscriptionRepository(client)
  134. sub := &service.UserSubscription{
  135. UserID: u.ID,
  136. GroupID: g.ID,
  137. Status: service.SubscriptionStatusActive,
  138. ExpiresAt: time.Now().Add(24 * time.Hour),
  139. }
  140. require.NoError(t, repo.Create(ctx, sub), "create user subscription")
  141. require.NoError(t, repo.Delete(ctx, sub.ID), "first delete")
  142. require.NoError(t, repo.Delete(ctx, sub.ID), "second delete should be idempotent")
  143. }
  144. func TestEntSoftDelete_UserSubscription_ListExcludesDeleted(t *testing.T) {
  145. ctx := context.Background()
  146. client := testEntClient(t)
  147. u := createEntUser(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-user3")+"@example.com")
  148. g1 := createEntGroup(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-group3a"))
  149. g2 := createEntGroup(t, ctx, client, uniqueSoftDeleteValue(t, "sd-sub-group3b"))
  150. repo := NewUserSubscriptionRepository(client)
  151. sub1 := &service.UserSubscription{
  152. UserID: u.ID,
  153. GroupID: g1.ID,
  154. Status: service.SubscriptionStatusActive,
  155. ExpiresAt: time.Now().Add(24 * time.Hour),
  156. }
  157. require.NoError(t, repo.Create(ctx, sub1), "create subscription 1")
  158. sub2 := &service.UserSubscription{
  159. UserID: u.ID,
  160. GroupID: g2.ID,
  161. Status: service.SubscriptionStatusActive,
  162. ExpiresAt: time.Now().Add(24 * time.Hour),
  163. }
  164. require.NoError(t, repo.Create(ctx, sub2), "create subscription 2")
  165. // 软删除 sub1
  166. require.NoError(t, repo.Delete(ctx, sub1.ID), "soft delete subscription 1")
  167. // ListByUserID 应只返回未删除的订阅
  168. subs, err := repo.ListByUserID(ctx, u.ID)
  169. require.NoError(t, err, "ListByUserID")
  170. require.Len(t, subs, 1, "should only return non-deleted subscriptions")
  171. require.Equal(t, sub2.ID, subs[0].ID, "expected sub2 to be returned")
  172. }