user_profile_identity_repo_unit_test.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. package repository
  2. import (
  3. "context"
  4. "sync"
  5. "testing"
  6. "time"
  7. dbent "github.com/Wei-Shaw/sub2api/ent"
  8. "github.com/Wei-Shaw/sub2api/ent/authidentity"
  9. "github.com/Wei-Shaw/sub2api/ent/authidentitychannel"
  10. "github.com/Wei-Shaw/sub2api/ent/identityadoptiondecision"
  11. "github.com/Wei-Shaw/sub2api/internal/service"
  12. "github.com/stretchr/testify/require"
  13. )
  14. func TestUserRepositoryBindAuthIdentityToUserCanonicalizesLegacyWeChatAlias(t *testing.T) {
  15. repo, client := newUserEntRepo(t)
  16. ctx := context.Background()
  17. user := &service.User{
  18. Email: "wechat-legacy@example.com",
  19. Username: "wechat-legacy",
  20. PasswordHash: "hash",
  21. Role: service.RoleUser,
  22. Status: service.StatusActive,
  23. }
  24. require.NoError(t, repo.Create(ctx, user))
  25. legacyIdentity, err := client.AuthIdentity.Create().
  26. SetUserID(user.ID).
  27. SetProviderType("wechat").
  28. SetProviderKey("wechat").
  29. SetProviderSubject("union-legacy-123").
  30. SetMetadata(map[string]any{"source": "legacy-alias"}).
  31. Save(ctx)
  32. require.NoError(t, err)
  33. legacyChannel, err := client.AuthIdentityChannel.Create().
  34. SetIdentityID(legacyIdentity.ID).
  35. SetProviderType("wechat").
  36. SetProviderKey("wechat").
  37. SetChannel("oa").
  38. SetChannelAppID("wx-app-legacy").
  39. SetChannelSubject("openid-legacy-123").
  40. SetMetadata(map[string]any{"scene": "legacy-alias"}).
  41. Save(ctx)
  42. require.NoError(t, err)
  43. bound, err := repo.BindAuthIdentityToUser(ctx, BindAuthIdentityInput{
  44. UserID: user.ID,
  45. Canonical: AuthIdentityKey{
  46. ProviderType: "wechat",
  47. ProviderKey: "wechat-main",
  48. ProviderSubject: "union-legacy-123",
  49. },
  50. Channel: &AuthIdentityChannelKey{
  51. ProviderType: "wechat",
  52. ProviderKey: "wechat-main",
  53. Channel: "oa",
  54. ChannelAppID: "wx-app-legacy",
  55. ChannelSubject: "openid-legacy-123",
  56. },
  57. Metadata: map[string]any{"source": "canonical-bind"},
  58. ChannelMetadata: map[string]any{"scene": "canonical-bind"},
  59. })
  60. require.NoError(t, err)
  61. require.NotNil(t, bound)
  62. require.NotNil(t, bound.Identity)
  63. require.NotNil(t, bound.Channel)
  64. require.Equal(t, legacyIdentity.ID, bound.Identity.ID)
  65. require.Equal(t, legacyChannel.ID, bound.Channel.ID)
  66. require.Equal(t, "wechat-main", bound.Identity.ProviderKey)
  67. require.Equal(t, "wechat-main", bound.Channel.ProviderKey)
  68. reloadedIdentity, err := client.AuthIdentity.Get(ctx, legacyIdentity.ID)
  69. require.NoError(t, err)
  70. require.Equal(t, "wechat-main", reloadedIdentity.ProviderKey)
  71. require.Equal(t, "canonical-bind", reloadedIdentity.Metadata["source"])
  72. reloadedChannel, err := client.AuthIdentityChannel.Get(ctx, legacyChannel.ID)
  73. require.NoError(t, err)
  74. require.Equal(t, "wechat-main", reloadedChannel.ProviderKey)
  75. require.Equal(t, "canonical-bind", reloadedChannel.Metadata["scene"])
  76. identityCount, err := client.AuthIdentity.Query().
  77. Where(
  78. authidentity.UserIDEQ(user.ID),
  79. authidentity.ProviderTypeEQ("wechat"),
  80. authidentity.ProviderSubjectEQ("union-legacy-123"),
  81. ).
  82. Count(ctx)
  83. require.NoError(t, err)
  84. require.Equal(t, 1, identityCount)
  85. channelCount, err := client.AuthIdentityChannel.Query().
  86. Where(
  87. authidentitychannel.ProviderTypeEQ("wechat"),
  88. authidentitychannel.ChannelEQ("oa"),
  89. authidentitychannel.ChannelAppIDEQ("wx-app-legacy"),
  90. authidentitychannel.ChannelSubjectEQ("openid-legacy-123"),
  91. ).
  92. Count(ctx)
  93. require.NoError(t, err)
  94. require.Equal(t, 1, channelCount)
  95. }
  96. func TestUserRepositoryUpsertIdentityAdoptionDecisionIsIdempotentUnderConcurrency(t *testing.T) {
  97. repo, client := newUserEntRepo(t)
  98. ctx := context.Background()
  99. user := &service.User{
  100. Email: "repo-adoption@example.com",
  101. Username: "repo-adoption",
  102. PasswordHash: "hash",
  103. Role: service.RoleUser,
  104. Status: service.StatusActive,
  105. }
  106. require.NoError(t, repo.Create(ctx, user))
  107. identity, err := client.AuthIdentity.Create().
  108. SetUserID(user.ID).
  109. SetProviderType("wechat").
  110. SetProviderKey("wechat-main").
  111. SetProviderSubject("union-repo-adoption").
  112. SetMetadata(map[string]any{}).
  113. Save(ctx)
  114. require.NoError(t, err)
  115. session, err := client.PendingAuthSession.Create().
  116. SetSessionToken("pending-repo-adoption").
  117. SetIntent("bind_current_user").
  118. SetProviderType("wechat").
  119. SetProviderKey("wechat-main").
  120. SetProviderSubject("union-repo-adoption").
  121. SetExpiresAt(time.Now().UTC().Add(15 * time.Minute)).
  122. SetUpstreamIdentityClaims(map[string]any{"provider_subject": "union-repo-adoption"}).
  123. SetLocalFlowState(map[string]any{"step": "pending"}).
  124. Save(ctx)
  125. require.NoError(t, err)
  126. firstCreateStarted := make(chan struct{})
  127. releaseFirstCreate := make(chan struct{})
  128. var firstCreate sync.Once
  129. client.IdentityAdoptionDecision.Use(func(next dbent.Mutator) dbent.Mutator {
  130. return dbent.MutateFunc(func(ctx context.Context, m dbent.Mutation) (dbent.Value, error) {
  131. blocked := false
  132. if m.Op().Is(dbent.OpCreate) {
  133. firstCreate.Do(func() {
  134. blocked = true
  135. close(firstCreateStarted)
  136. })
  137. }
  138. if blocked {
  139. <-releaseFirstCreate
  140. }
  141. return next.Mutate(ctx, m)
  142. })
  143. })
  144. type adoptionResult struct {
  145. decision *dbent.IdentityAdoptionDecision
  146. err error
  147. }
  148. input := IdentityAdoptionDecisionInput{
  149. PendingAuthSessionID: session.ID,
  150. IdentityID: &identity.ID,
  151. AdoptDisplayName: true,
  152. AdoptAvatar: true,
  153. }
  154. results := make(chan adoptionResult, 2)
  155. go func() {
  156. decision, err := repo.UpsertIdentityAdoptionDecision(ctx, input)
  157. results <- adoptionResult{decision: decision, err: err}
  158. }()
  159. <-firstCreateStarted
  160. go func() {
  161. decision, err := repo.UpsertIdentityAdoptionDecision(ctx, input)
  162. results <- adoptionResult{decision: decision, err: err}
  163. }()
  164. time.Sleep(100 * time.Millisecond)
  165. close(releaseFirstCreate)
  166. first := <-results
  167. second := <-results
  168. require.NoError(t, first.err)
  169. require.NoError(t, second.err)
  170. require.NotNil(t, first.decision)
  171. require.NotNil(t, second.decision)
  172. require.Equal(t, first.decision.ID, second.decision.ID)
  173. count, err := client.IdentityAdoptionDecision.Query().
  174. Where(identityadoptiondecision.PendingAuthSessionIDEQ(session.ID)).
  175. Count(ctx)
  176. require.NoError(t, err)
  177. require.Equal(t, 1, count)
  178. loaded, err := client.IdentityAdoptionDecision.Query().
  179. Where(identityadoptiondecision.PendingAuthSessionIDEQ(session.ID)).
  180. Only(ctx)
  181. require.NoError(t, err)
  182. require.NotNil(t, loaded.IdentityID)
  183. require.Equal(t, identity.ID, *loaded.IdentityID)
  184. require.True(t, loaded.AdoptDisplayName)
  185. require.True(t, loaded.AdoptAvatar)
  186. }