admin_service_auth_identity_binding_test.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "database/sql"
  6. "testing"
  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/enttest"
  11. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  12. "github.com/stretchr/testify/require"
  13. "entgo.io/ent/dialect"
  14. entsql "entgo.io/ent/dialect/sql"
  15. _ "modernc.org/sqlite"
  16. )
  17. func newAdminServiceAuthIdentityBindingTestClient(t *testing.T) *dbent.Client {
  18. t.Helper()
  19. db, err := sql.Open("sqlite", "file:admin_service_auth_identity_binding?mode=memory&cache=shared&_fk=1")
  20. require.NoError(t, err)
  21. t.Cleanup(func() { _ = db.Close() })
  22. _, err = db.Exec("PRAGMA foreign_keys = ON")
  23. require.NoError(t, err)
  24. drv := entsql.OpenDB(dialect.SQLite, db)
  25. client := enttest.NewClient(t, enttest.WithOptions(dbent.Driver(drv)))
  26. t.Cleanup(func() { _ = client.Close() })
  27. return client
  28. }
  29. func TestAdminServiceBindUserAuthIdentityCreatesCanonicalAndChannelBinding(t *testing.T) {
  30. client := newAdminServiceAuthIdentityBindingTestClient(t)
  31. ctx := context.Background()
  32. user, err := client.User.Create().
  33. SetEmail("bind-target@example.com").
  34. SetPasswordHash("hash").
  35. SetRole(RoleUser).
  36. SetStatus(StatusActive).
  37. Save(ctx)
  38. require.NoError(t, err)
  39. svc := &adminServiceImpl{
  40. userRepo: &userRepoStub{user: &User{ID: user.ID, Email: user.Email, Status: StatusActive}},
  41. entClient: client,
  42. }
  43. result, err := svc.BindUserAuthIdentity(ctx, user.ID, AdminBindAuthIdentityInput{
  44. ProviderType: "wechat",
  45. ProviderKey: "wechat-main",
  46. ProviderSubject: "union-123",
  47. Metadata: map[string]any{"source": "admin-repair"},
  48. Channel: &AdminBindAuthIdentityChannelInput{
  49. Channel: "open",
  50. ChannelAppID: "wx-open",
  51. ChannelSubject: "openid-123",
  52. Metadata: map[string]any{"scene": "migration"},
  53. },
  54. })
  55. require.NoError(t, err)
  56. require.NotNil(t, result)
  57. require.Equal(t, user.ID, result.UserID)
  58. require.Equal(t, "wechat", result.ProviderType)
  59. require.Equal(t, "wechat-main", result.ProviderKey)
  60. require.NotNil(t, result.VerifiedAt)
  61. require.NotNil(t, result.Channel)
  62. require.Equal(t, "open", result.Channel.Channel)
  63. identity, err := client.AuthIdentity.Query().
  64. Where(
  65. authidentity.ProviderTypeEQ("wechat"),
  66. authidentity.ProviderKeyEQ("wechat-main"),
  67. authidentity.ProviderSubjectEQ("union-123"),
  68. ).
  69. Only(ctx)
  70. require.NoError(t, err)
  71. require.Equal(t, user.ID, identity.UserID)
  72. require.Equal(t, "admin-repair", identity.Metadata["source"])
  73. require.NotNil(t, identity.VerifiedAt)
  74. channel, err := client.AuthIdentityChannel.Query().
  75. Where(
  76. authidentitychannel.ProviderTypeEQ("wechat"),
  77. authidentitychannel.ProviderKeyEQ("wechat-main"),
  78. authidentitychannel.ChannelEQ("open"),
  79. authidentitychannel.ChannelAppIDEQ("wx-open"),
  80. authidentitychannel.ChannelSubjectEQ("openid-123"),
  81. ).
  82. Only(ctx)
  83. require.NoError(t, err)
  84. require.Equal(t, identity.ID, channel.IdentityID)
  85. require.Equal(t, "migration", channel.Metadata["scene"])
  86. }
  87. func TestAdminServiceBindUserAuthIdentityRejectsOtherOwner(t *testing.T) {
  88. client := newAdminServiceAuthIdentityBindingTestClient(t)
  89. ctx := context.Background()
  90. owner, err := client.User.Create().
  91. SetEmail("owner@example.com").
  92. SetPasswordHash("hash").
  93. SetRole(RoleUser).
  94. SetStatus(StatusActive).
  95. Save(ctx)
  96. require.NoError(t, err)
  97. target, err := client.User.Create().
  98. SetEmail("target@example.com").
  99. SetPasswordHash("hash").
  100. SetRole(RoleUser).
  101. SetStatus(StatusActive).
  102. Save(ctx)
  103. require.NoError(t, err)
  104. _, err = client.AuthIdentity.Create().
  105. SetUserID(owner.ID).
  106. SetProviderType("oidc").
  107. SetProviderKey("https://issuer.example").
  108. SetProviderSubject("subject-1").
  109. Save(ctx)
  110. require.NoError(t, err)
  111. svc := &adminServiceImpl{
  112. userRepo: &userRepoStub{user: &User{ID: target.ID, Email: target.Email, Status: StatusActive}},
  113. entClient: client,
  114. }
  115. _, err = svc.BindUserAuthIdentity(ctx, target.ID, AdminBindAuthIdentityInput{
  116. ProviderType: "oidc",
  117. ProviderKey: "https://issuer.example",
  118. ProviderSubject: "subject-1",
  119. })
  120. require.Error(t, err)
  121. require.Equal(t, "AUTH_IDENTITY_OWNERSHIP_CONFLICT", infraerrors.Reason(err))
  122. }
  123. func TestAdminServiceBindUserAuthIdentityIsIdempotentForSameUser(t *testing.T) {
  124. client := newAdminServiceAuthIdentityBindingTestClient(t)
  125. ctx := context.Background()
  126. user, err := client.User.Create().
  127. SetEmail("same-user@example.com").
  128. SetPasswordHash("hash").
  129. SetRole(RoleUser).
  130. SetStatus(StatusActive).
  131. Save(ctx)
  132. require.NoError(t, err)
  133. svc := &adminServiceImpl{
  134. userRepo: &userRepoStub{user: &User{ID: user.ID, Email: user.Email, Status: StatusActive}},
  135. entClient: client,
  136. }
  137. first, err := svc.BindUserAuthIdentity(ctx, user.ID, AdminBindAuthIdentityInput{
  138. ProviderType: "oidc",
  139. ProviderKey: "https://issuer.example",
  140. ProviderSubject: "subject-2",
  141. Metadata: map[string]any{"source": "first"},
  142. })
  143. require.NoError(t, err)
  144. second, err := svc.BindUserAuthIdentity(ctx, user.ID, AdminBindAuthIdentityInput{
  145. ProviderType: "oidc",
  146. ProviderKey: "https://issuer.example",
  147. ProviderSubject: "subject-2",
  148. Metadata: map[string]any{"source": "second"},
  149. })
  150. require.NoError(t, err)
  151. require.Equal(t, first.UserID, second.UserID)
  152. require.Equal(t, "second", second.Metadata["source"])
  153. identities, err := client.AuthIdentity.Query().
  154. Where(
  155. authidentity.ProviderTypeEQ("oidc"),
  156. authidentity.ProviderKeyEQ("https://issuer.example"),
  157. authidentity.ProviderSubjectEQ("subject-2"),
  158. ).
  159. All(ctx)
  160. require.NoError(t, err)
  161. require.Len(t, identities, 1)
  162. require.Equal(t, "second", identities[0].Metadata["source"])
  163. }
  164. func TestAdminServiceBindUserAuthIdentityReusesLegacyWeChatAliasRecords(t *testing.T) {
  165. client := newAdminServiceAuthIdentityBindingTestClient(t)
  166. ctx := context.Background()
  167. user, err := client.User.Create().
  168. SetEmail("wechat-alias@example.com").
  169. SetPasswordHash("hash").
  170. SetRole(RoleUser).
  171. SetStatus(StatusActive).
  172. Save(ctx)
  173. require.NoError(t, err)
  174. legacyIdentity, err := client.AuthIdentity.Create().
  175. SetUserID(user.ID).
  176. SetProviderType("wechat").
  177. SetProviderKey("wechat").
  178. SetProviderSubject("union-legacy-123").
  179. SetMetadata(map[string]any{"source": "legacy"}).
  180. Save(ctx)
  181. require.NoError(t, err)
  182. legacyChannel, err := client.AuthIdentityChannel.Create().
  183. SetIdentityID(legacyIdentity.ID).
  184. SetProviderType("wechat").
  185. SetProviderKey("wechat").
  186. SetChannel("open").
  187. SetChannelAppID("wx-open").
  188. SetChannelSubject("openid-legacy-123").
  189. SetMetadata(map[string]any{"scene": "legacy"}).
  190. Save(ctx)
  191. require.NoError(t, err)
  192. svc := &adminServiceImpl{
  193. userRepo: &userRepoStub{user: &User{ID: user.ID, Email: user.Email, Status: StatusActive}},
  194. entClient: client,
  195. }
  196. result, err := svc.BindUserAuthIdentity(ctx, user.ID, AdminBindAuthIdentityInput{
  197. ProviderType: "wechat",
  198. ProviderKey: "wechat-main",
  199. ProviderSubject: "union-legacy-123",
  200. Metadata: map[string]any{"source": "admin-repair"},
  201. Channel: &AdminBindAuthIdentityChannelInput{
  202. Channel: "open",
  203. ChannelAppID: "wx-open",
  204. ChannelSubject: "openid-legacy-123",
  205. Metadata: map[string]any{"scene": "admin-repair"},
  206. },
  207. })
  208. require.NoError(t, err)
  209. require.NotNil(t, result)
  210. require.Equal(t, "wechat-main", result.ProviderKey)
  211. require.NotNil(t, result.Channel)
  212. require.Equal(t, "open", result.Channel.Channel)
  213. identity, err := client.AuthIdentity.Get(ctx, legacyIdentity.ID)
  214. require.NoError(t, err)
  215. require.Equal(t, "wechat-main", identity.ProviderKey)
  216. require.Equal(t, "admin-repair", identity.Metadata["source"])
  217. channel, err := client.AuthIdentityChannel.Get(ctx, legacyChannel.ID)
  218. require.NoError(t, err)
  219. require.Equal(t, "wechat-main", channel.ProviderKey)
  220. require.Equal(t, legacyIdentity.ID, channel.IdentityID)
  221. require.Equal(t, "admin-repair", channel.Metadata["scene"])
  222. identityCount, err := client.AuthIdentity.Query().
  223. Where(
  224. authidentity.ProviderTypeEQ("wechat"),
  225. authidentity.ProviderSubjectEQ("union-legacy-123"),
  226. ).
  227. Count(ctx)
  228. require.NoError(t, err)
  229. require.Equal(t, 1, identityCount)
  230. channelCount, err := client.AuthIdentityChannel.Query().
  231. Where(
  232. authidentitychannel.ProviderTypeEQ("wechat"),
  233. authidentitychannel.ChannelEQ("open"),
  234. authidentitychannel.ChannelAppIDEQ("wx-open"),
  235. authidentitychannel.ChannelSubjectEQ("openid-legacy-123"),
  236. ).
  237. Count(ctx)
  238. require.NoError(t, err)
  239. require.Equal(t, 1, channelCount)
  240. }
  241. func TestAdminServiceBindUserAuthIdentityRejectsInvalidProviderType(t *testing.T) {
  242. client := newAdminServiceAuthIdentityBindingTestClient(t)
  243. ctx := context.Background()
  244. user, err := client.User.Create().
  245. SetEmail("invalid-provider@example.com").
  246. SetPasswordHash("hash").
  247. SetRole(RoleUser).
  248. SetStatus(StatusActive).
  249. Save(ctx)
  250. require.NoError(t, err)
  251. svc := &adminServiceImpl{
  252. userRepo: &userRepoStub{user: &User{ID: user.ID, Email: user.Email, Status: StatusActive}},
  253. entClient: client,
  254. }
  255. _, err = svc.BindUserAuthIdentity(ctx, user.ID, AdminBindAuthIdentityInput{
  256. ProviderType: "github",
  257. ProviderKey: "github-main",
  258. ProviderSubject: "subject-3",
  259. })
  260. require.Error(t, err)
  261. require.Equal(t, "INVALID_INPUT", infraerrors.Reason(err))
  262. }