registration_email_alias_test.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "errors"
  6. "testing"
  7. "github.com/stretchr/testify/require"
  8. )
  9. func TestNormalizeEmailForAliasDedup(t *testing.T) {
  10. cases := []struct {
  11. name string
  12. email string
  13. want string
  14. }{
  15. {"plain", "user@example.com", "user@example.com"},
  16. {"uppercase and spaces", " User@Example.COM ", "user@example.com"},
  17. {"plus alias stripped", "user+tag@example.com", "user@example.com"},
  18. {"gmail plus alias", "someone+bulk294@gmail.com", "someone@gmail.com"},
  19. {"gmail dots removed", "some.one@gmail.com", "someone@gmail.com"},
  20. {"gmail dots and plus", "s.o.m.e+x@gmail.com", "some@gmail.com"},
  21. {"googlemail folded to gmail", "user@googlemail.com", "user@gmail.com"},
  22. {"non-gmail keeps dots", "first.last@qq.com", "first.last@qq.com"},
  23. {"fqdn root dot dropped", "d.axis.2026@gmail.com.", "daxis2026@gmail.com"},
  24. {"fqdn root dot on other domain", "first.last@qq.com.", "first.last@qq.com"},
  25. {"leading plus keeps local part", "+alice@gmail.com", "+alice@gmail.com"},
  26. {"dot-only local part kept", "...@gmail.com", "...@gmail.com"},
  27. {"invalid keeps lowered raw", "not-an-email", "not-an-email"},
  28. }
  29. for _, tc := range cases {
  30. t.Run(tc.name, func(t *testing.T) {
  31. require.Equal(t, tc.want, NormalizeEmailForAliasDedup(tc.email))
  32. })
  33. }
  34. }
  35. func TestNormalizeEmailForAliasDedupKeepsDistinctInboxes(t *testing.T) {
  36. // 剥离 "+后缀" 不能把同域下不同用户折叠成同一身份。
  37. require.NotEqual(t,
  38. NormalizeEmailForAliasDedup("+alice@gmail.com"),
  39. NormalizeEmailForAliasDedup("+bob@gmail.com"),
  40. )
  41. require.NotEqual(t,
  42. NormalizeEmailForAliasDedup("alice@gmail.com"),
  43. NormalizeEmailForAliasDedup("bob@gmail.com"),
  44. )
  45. }
  46. func TestEmailAliasDedupProbes(t *testing.T) {
  47. require.ElementsMatch(t,
  48. []EmailAliasProbe{{Local: "someone", Domain: "gmailcom"}, {Local: "someone", Domain: "googlemailcom"}},
  49. EmailAliasDedupProbes("Some.One+tag@gmail.com"),
  50. )
  51. require.ElementsMatch(t,
  52. []EmailAliasProbe{{Local: "daxis2026", Domain: "gmailcom"}, {Local: "daxis2026", Domain: "googlemailcom"}},
  53. EmailAliasDedupProbes("d.axis.2026@googlemail.com."),
  54. )
  55. require.Equal(t,
  56. []EmailAliasProbe{{Local: "firstlast", Domain: "qqcom"}},
  57. EmailAliasDedupProbes("first.last+tag@qq.com"),
  58. )
  59. require.Nil(t, EmailAliasDedupProbes("not-an-email"))
  60. require.Nil(t, EmailAliasDedupProbes("...@gmail.com"))
  61. }
  62. // aliasDedupRepoStub implements only the methods alias dedup uses; other
  63. // UserRepository methods come from the embedded nil interface (a wrong call
  64. // would panic, failing the test).
  65. type aliasDedupRepoStub struct {
  66. UserRepository
  67. exists bool
  68. existsErr error
  69. stored []string
  70. aliasErr error
  71. aliasChecks []string
  72. }
  73. func (s *aliasDedupRepoStub) ExistsByEmail(context.Context, string) (bool, error) {
  74. return s.exists, s.existsErr
  75. }
  76. func (s *aliasDedupRepoStub) ExistsByEmailAlias(_ context.Context, email string) (bool, error) {
  77. s.aliasChecks = append(s.aliasChecks, email)
  78. if s.aliasErr != nil {
  79. return false, s.aliasErr
  80. }
  81. identity := NormalizeEmailForAliasDedup(email)
  82. for _, candidate := range s.stored {
  83. if NormalizeEmailForAliasDedup(candidate) == identity {
  84. return true, nil
  85. }
  86. }
  87. return false, nil
  88. }
  89. func TestExistsByEmailOrAlias(t *testing.T) {
  90. ctx := context.Background()
  91. t.Run("exact duplicate short-circuits", func(t *testing.T) {
  92. repo := &aliasDedupRepoStub{exists: true}
  93. svc := &AuthService{userRepo: repo}
  94. got, err := svc.existsByEmailOrAlias(ctx, "user@gmail.com")
  95. require.NoError(t, err)
  96. require.True(t, got)
  97. require.Empty(t, repo.aliasChecks, "no alias probe expected after exact hit")
  98. })
  99. t.Run("plus alias variant detected", func(t *testing.T) {
  100. repo := &aliasDedupRepoStub{stored: []string{"someone+bulk294@gmail.com"}}
  101. svc := &AuthService{userRepo: repo}
  102. got, err := svc.existsByEmailOrAlias(ctx, "Someone@gmail.com")
  103. require.NoError(t, err)
  104. require.True(t, got)
  105. })
  106. t.Run("gmail dot variant detected", func(t *testing.T) {
  107. repo := &aliasDedupRepoStub{stored: []string{"some.one@gmail.com"}}
  108. svc := &AuthService{userRepo: repo}
  109. got, err := svc.existsByEmailOrAlias(ctx, "someone@gmail.com")
  110. require.NoError(t, err)
  111. require.True(t, got)
  112. })
  113. t.Run("fqdn root dot variant detected", func(t *testing.T) {
  114. repo := &aliasDedupRepoStub{stored: []string{"d.axis.2026@gmail.com"}}
  115. svc := &AuthService{userRepo: repo}
  116. got, err := svc.existsByEmailOrAlias(ctx, "da.xis.2026@gmail.com.")
  117. require.NoError(t, err)
  118. require.True(t, got)
  119. })
  120. t.Run("different inbox allowed", func(t *testing.T) {
  121. repo := &aliasDedupRepoStub{stored: []string{"other@gmail.com"}}
  122. svc := &AuthService{userRepo: repo}
  123. got, err := svc.existsByEmailOrAlias(ctx, "user@gmail.com")
  124. require.NoError(t, err)
  125. require.False(t, got)
  126. })
  127. t.Run("distinct plus-prefixed locals allowed", func(t *testing.T) {
  128. repo := &aliasDedupRepoStub{stored: []string{"+alice@gmail.com"}}
  129. svc := &AuthService{userRepo: repo}
  130. got, err := svc.existsByEmailOrAlias(ctx, "+bob@gmail.com")
  131. require.NoError(t, err)
  132. require.False(t, got)
  133. })
  134. t.Run("alias probe error fails closed", func(t *testing.T) {
  135. repo := &aliasDedupRepoStub{aliasErr: errors.New("db down")}
  136. svc := &AuthService{userRepo: repo}
  137. _, err := svc.existsByEmailOrAlias(ctx, "user@gmail.com")
  138. require.Error(t, err)
  139. })
  140. t.Run("exact check error propagates", func(t *testing.T) {
  141. repo := &aliasDedupRepoStub{existsErr: errors.New("db down")}
  142. svc := &AuthService{userRepo: repo}
  143. _, err := svc.existsByEmailOrAlias(ctx, "user@gmail.com")
  144. require.Error(t, err)
  145. })
  146. }