user_repo_email_alias_test.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. package repository
  2. import (
  3. "context"
  4. "testing"
  5. "github.com/Wei-Shaw/sub2api/internal/service"
  6. "github.com/stretchr/testify/require"
  7. )
  8. func seedUserForAliasTest(t *testing.T, repo *userRepository, email string) {
  9. t.Helper()
  10. require.NoError(t, repo.Create(context.Background(), &service.User{
  11. Email: email,
  12. Username: email,
  13. PasswordHash: "hash",
  14. Role: service.RoleUser,
  15. Status: service.StatusActive,
  16. }))
  17. }
  18. func TestUserRepositoryExistsByEmailAlias(t *testing.T) {
  19. cases := []struct {
  20. name string
  21. stored string
  22. probe string
  23. want bool
  24. }{
  25. {"same address", "someone@gmail.com", "someone@gmail.com", true},
  26. {"gmail plus alias", "someone@gmail.com", "someone+bulk294@gmail.com", true},
  27. {"gmail dot trick", "d.axis.2026@gmail.com", "daxis2026@gmail.com", true},
  28. {"gmail dot trick both sides", "d.axis.2026@gmail.com", "da.xis.2026@gmail.com", true},
  29. {"stored plus alias found by canonical form", "someone+tag@gmail.com", "someone@gmail.com", true},
  30. {"googlemail is a gmail alias", "someone@googlemail.com", "some.one@gmail.com", true},
  31. {"fqdn root dot on probe", "d.axis.2026@gmail.com", "da.xis.2026@gmail.com.", true},
  32. {"fqdn root dot on stored row", "d.axis.2026@gmail.com.", "daxis2026@gmail.com", true},
  33. {"legacy row with spacing and case", " D.Axis.2026@Gmail.com ", "daxis2026@gmail.com", true},
  34. {"non-gmail plus alias", "first.last@qq.com", "first.last+tag@qq.com", true},
  35. {"different gmail inbox", "someone@gmail.com", "someoneelse@gmail.com", false},
  36. {"non-gmail dots are significant", "first.last@qq.com", "firstlast@qq.com", false},
  37. {"different domain", "someone@gmail.com", "someone@qq.com", false},
  38. {"distinct plus-prefixed locals", "+alice@gmail.com", "+bob@gmail.com", false},
  39. {"underscore is not a wildcard", "user_x@qq.com", "userax@qq.com", false},
  40. {"percent is not a wildcard", "a%b@qq.com", "axxb@qq.com", false},
  41. }
  42. for _, tc := range cases {
  43. t.Run(tc.name, func(t *testing.T) {
  44. repo, _ := newUserEntRepo(t)
  45. seedUserForAliasTest(t, repo, tc.stored)
  46. got, err := repo.ExistsByEmailAlias(context.Background(), tc.probe)
  47. require.NoError(t, err)
  48. require.Equal(t, tc.want, got)
  49. })
  50. }
  51. }
  52. func TestUserRepositoryExistsByEmailAliasIgnoresMalformedInput(t *testing.T) {
  53. repo, _ := newUserEntRepo(t)
  54. seedUserForAliasTest(t, repo, "someone@gmail.com")
  55. got, err := repo.ExistsByEmailAlias(context.Background(), "not-an-email")
  56. require.NoError(t, err)
  57. require.False(t, got)
  58. }
  59. func TestUserRepositoryCreateWithEmailAliasGuard(t *testing.T) {
  60. repo, _ := newUserEntRepo(t)
  61. ctx := context.Background()
  62. seedUserForAliasTest(t, repo, "d.axis.2026@gmail.com")
  63. // 注册路径:别名变体在唯一性锁内被拒绝。
  64. err := repo.CreateWithEmailAliasGuard(ctx, &service.User{
  65. Email: "da.xis.2026+free@googlemail.com",
  66. Username: "alias-variant",
  67. PasswordHash: "hash",
  68. Role: service.RoleUser,
  69. Status: service.StatusActive,
  70. })
  71. require.ErrorIs(t, err, service.ErrEmailExists)
  72. // 不同收件箱仍可注册。
  73. require.NoError(t, repo.CreateWithEmailAliasGuard(ctx, &service.User{
  74. Email: "other.person@gmail.com",
  75. Username: "other-person",
  76. PasswordHash: "hash",
  77. Role: service.RoleUser,
  78. Status: service.StatusActive,
  79. }))
  80. // 管理员建号(Create)不受别名限制。
  81. require.NoError(t, repo.Create(ctx, &service.User{
  82. Email: "daxis2026+support@gmail.com",
  83. Username: "admin-created",
  84. PasswordHash: "hash",
  85. Role: service.RoleUser,
  86. Status: service.StatusActive,
  87. }))
  88. }
  89. func TestUserRepositoryCountUsersByEmailDomain(t *testing.T) {
  90. repo, _ := newUserEntRepo(t)
  91. ctx := context.Background()
  92. active := &service.User{
  93. Email: "first@custom.example",
  94. Username: "first",
  95. PasswordHash: "hash",
  96. Role: service.RoleUser,
  97. Status: service.StatusActive,
  98. }
  99. seedUserForAliasTest(t, repo, active.Email)
  100. seedUserForAliasTest(t, repo, "other@sub.custom.example")
  101. deleted := &service.User{
  102. Email: "deleted@custom.example",
  103. Username: "deleted",
  104. PasswordHash: "hash",
  105. Role: service.RoleUser,
  106. Status: service.StatusActive,
  107. }
  108. require.NoError(t, repo.Create(ctx, deleted))
  109. require.NoError(t, repo.Delete(ctx, deleted.ID))
  110. count, err := repo.CountUsersByEmailDomain(ctx, "custom.example")
  111. require.NoError(t, err)
  112. require.Equal(t, 2, count)
  113. }
  114. func TestUserRepositoryCreateWithEmailAliasGuardAndDomainLimit(t *testing.T) {
  115. repo, _ := newUserEntRepo(t)
  116. ctx := context.Background()
  117. seedUserForAliasTest(t, repo, "first@custom.example.")
  118. err := repo.CreateWithEmailAliasGuardAndDomainLimit(ctx, &service.User{
  119. Email: "second@custom.example",
  120. Username: "second",
  121. PasswordHash: "hash",
  122. Role: service.RoleUser,
  123. Status: service.StatusActive,
  124. }, "custom.example")
  125. require.ErrorIs(t, err, service.ErrEmailDomainRegistrationLimit)
  126. }
  127. func TestUserRepositoryCountUsersByEmailDomainEscapesLikeWildcards(t *testing.T) {
  128. repo, _ := newUserEntRepo(t)
  129. ctx := context.Background()
  130. seedUserForAliasTest(t, repo, "first@foo_bar.com")
  131. seedUserForAliasTest(t, repo, "other@fooxbar.com")
  132. count, err := repo.CountUsersByEmailDomain(ctx, "foo_bar.com")
  133. require.NoError(t, err)
  134. require.Equal(t, 1, count)
  135. }