auth_service_email_bind_test.go 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167
  1. //go:build unit
  2. package service_test
  3. import (
  4. "context"
  5. "database/sql"
  6. "errors"
  7. "fmt"
  8. "sync"
  9. "testing"
  10. "time"
  11. dbent "github.com/Wei-Shaw/sub2api/ent"
  12. "github.com/Wei-Shaw/sub2api/ent/authidentity"
  13. "github.com/Wei-Shaw/sub2api/ent/enttest"
  14. dbuser "github.com/Wei-Shaw/sub2api/ent/user"
  15. "github.com/Wei-Shaw/sub2api/internal/config"
  16. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  17. "github.com/Wei-Shaw/sub2api/internal/repository"
  18. "github.com/Wei-Shaw/sub2api/internal/service"
  19. "github.com/stretchr/testify/require"
  20. "entgo.io/ent/dialect"
  21. entsql "entgo.io/ent/dialect/sql"
  22. _ "modernc.org/sqlite"
  23. )
  24. type emailBindDefaultSubAssignerStub struct {
  25. calls []*service.AssignSubscriptionInput
  26. }
  27. func (s *emailBindDefaultSubAssignerStub) AssignOrExtendSubscription(
  28. _ context.Context,
  29. input *service.AssignSubscriptionInput,
  30. ) (*service.UserSubscription, bool, error) {
  31. cloned := *input
  32. s.calls = append(s.calls, &cloned)
  33. return &service.UserSubscription{UserID: input.UserID, GroupID: input.GroupID}, false, nil
  34. }
  35. type flakyEmailBindDefaultSubAssignerStub struct {
  36. err error
  37. calls []*service.AssignSubscriptionInput
  38. }
  39. func (s *flakyEmailBindDefaultSubAssignerStub) AssignOrExtendSubscription(
  40. _ context.Context,
  41. input *service.AssignSubscriptionInput,
  42. ) (*service.UserSubscription, bool, error) {
  43. cloned := *input
  44. s.calls = append(s.calls, &cloned)
  45. return nil, false, s.err
  46. }
  47. func newAuthServiceForEmailBind(
  48. t *testing.T,
  49. settings map[string]string,
  50. emailCache service.EmailCache,
  51. defaultSubAssigner service.DefaultSubscriptionAssigner,
  52. ) (*service.AuthService, service.UserRepository, *dbent.Client) {
  53. return newAuthServiceForEmailBindWithRefreshCache(t, settings, emailCache, defaultSubAssigner, nil)
  54. }
  55. func newAuthServiceForEmailBindWithRefreshCache(
  56. t *testing.T,
  57. settings map[string]string,
  58. emailCache service.EmailCache,
  59. defaultSubAssigner service.DefaultSubscriptionAssigner,
  60. refreshTokenCache service.RefreshTokenCache,
  61. ) (*service.AuthService, service.UserRepository, *dbent.Client) {
  62. t.Helper()
  63. dbName := fmt.Sprintf("file:auth_service_email_bind_%d?mode=memory&cache=shared", time.Now().UnixNano())
  64. db, err := sql.Open("sqlite", dbName)
  65. require.NoError(t, err)
  66. t.Cleanup(func() { _ = db.Close() })
  67. _, err = db.Exec("PRAGMA foreign_keys = ON")
  68. require.NoError(t, err)
  69. _, err = db.Exec(`
  70. CREATE TABLE IF NOT EXISTS user_provider_default_grants (
  71. id INTEGER PRIMARY KEY AUTOINCREMENT,
  72. user_id INTEGER NOT NULL,
  73. provider_type TEXT NOT NULL,
  74. grant_reason TEXT NOT NULL DEFAULT 'first_bind',
  75. created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  76. UNIQUE(user_id, provider_type, grant_reason)
  77. )`)
  78. require.NoError(t, err)
  79. drv := entsql.OpenDB(dialect.SQLite, db)
  80. client := enttest.NewClient(t, enttest.WithOptions(dbent.Driver(drv)))
  81. t.Cleanup(func() { _ = client.Close() })
  82. repo := repository.NewUserRepository(client, db)
  83. cfg := &config.Config{
  84. JWT: config.JWTConfig{
  85. Secret: "test-bind-email-secret",
  86. ExpireHour: 1,
  87. },
  88. Default: config.DefaultConfig{
  89. UserBalance: 3.5,
  90. UserConcurrency: 2,
  91. },
  92. }
  93. settingRepo := &emailBindSettingRepoStub{values: settings}
  94. settingSvc := service.NewSettingService(settingRepo, cfg)
  95. var emailSvc *service.EmailService
  96. if emailCache != nil {
  97. emailSvc = service.NewEmailService(settingRepo, emailCache)
  98. }
  99. svc := service.NewAuthService(client, repo, nil, refreshTokenCache, cfg, settingSvc, emailSvc, nil, nil, nil, defaultSubAssigner, nil, nil)
  100. return svc, repo, client
  101. }
  102. func TestAuthServiceBindEmailIdentity_UpdatesEmailAndAppliesFirstBindDefaults(t *testing.T) {
  103. assigner := &emailBindDefaultSubAssignerStub{}
  104. cache := &emailBindCacheStub{
  105. data: &service.VerificationCodeData{
  106. Code: "123456",
  107. CreatedAt: time.Now().UTC(),
  108. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  109. },
  110. }
  111. svc, _, client := newAuthServiceForEmailBind(t, map[string]string{
  112. service.SettingKeyAuthSourceDefaultEmailBalance: "8.5",
  113. service.SettingKeyAuthSourceDefaultEmailConcurrency: "4",
  114. service.SettingKeyAuthSourceDefaultEmailSubscriptions: `[{"group_id":11,"validity_days":30}]`,
  115. service.SettingKeyAuthSourceDefaultEmailGrantOnFirstBind: "true",
  116. }, cache, assigner)
  117. ctx := context.Background()
  118. user, err := client.User.Create().
  119. SetEmail("legacy-user" + service.LinuxDoConnectSyntheticEmailDomain).
  120. SetUsername("legacy-user").
  121. SetPasswordHash("old-hash").
  122. SetBalance(2.5).
  123. SetConcurrency(1).
  124. SetRole(service.RoleUser).
  125. SetStatus(service.StatusActive).
  126. Save(ctx)
  127. require.NoError(t, err)
  128. updatedUser, err := svc.BindEmailIdentity(ctx, user.ID, " NewEmail@Example.com ", "123456", "new-password")
  129. require.NoError(t, err)
  130. require.NotNil(t, updatedUser)
  131. require.Equal(t, "newemail@example.com", updatedUser.Email)
  132. storedUser, err := client.User.Get(ctx, user.ID)
  133. require.NoError(t, err)
  134. require.Equal(t, "newemail@example.com", storedUser.Email)
  135. require.Equal(t, 11.0, storedUser.Balance)
  136. require.Equal(t, 5, storedUser.Concurrency)
  137. require.True(t, svc.CheckPassword("new-password", storedUser.PasswordHash))
  138. identityCount, err := client.AuthIdentity.Query().
  139. Where(
  140. authidentity.UserIDEQ(user.ID),
  141. authidentity.ProviderTypeEQ("email"),
  142. authidentity.ProviderKeyEQ("email"),
  143. authidentity.ProviderSubjectEQ("newemail@example.com"),
  144. ).
  145. Count(ctx)
  146. require.NoError(t, err)
  147. require.Equal(t, 1, identityCount)
  148. require.Len(t, assigner.calls, 1)
  149. require.Equal(t, user.ID, assigner.calls[0].UserID)
  150. require.Equal(t, int64(11), assigner.calls[0].GroupID)
  151. require.Equal(t, 30, assigner.calls[0].ValidityDays)
  152. require.Equal(t, 1, countProviderGrantRecords(t, client, user.ID, "email", "first_bind"))
  153. }
  154. func TestAuthServiceBindEmailIdentity_RejectsExistingEmailOnAnotherUser(t *testing.T) {
  155. cache := &emailBindCacheStub{
  156. data: &service.VerificationCodeData{
  157. Code: "123456",
  158. CreatedAt: time.Now().UTC(),
  159. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  160. },
  161. }
  162. svc, _, client := newAuthServiceForEmailBind(t, nil, cache, nil)
  163. ctx := context.Background()
  164. sourceUser, err := client.User.Create().
  165. SetEmail("source-user" + service.OIDCConnectSyntheticEmailDomain).
  166. SetUsername("source-user").
  167. SetPasswordHash("old-hash").
  168. SetBalance(1).
  169. SetConcurrency(1).
  170. SetRole(service.RoleUser).
  171. SetStatus(service.StatusActive).
  172. Save(ctx)
  173. require.NoError(t, err)
  174. _, err = client.User.Create().
  175. SetEmail("taken@example.com").
  176. SetUsername("taken-user").
  177. SetPasswordHash("hash").
  178. SetBalance(1).
  179. SetConcurrency(1).
  180. SetRole(service.RoleUser).
  181. SetStatus(service.StatusActive).
  182. Save(ctx)
  183. require.NoError(t, err)
  184. updatedUser, err := svc.BindEmailIdentity(ctx, sourceUser.ID, "taken@example.com", "123456", "new-password")
  185. require.ErrorIs(t, err, service.ErrEmailExists)
  186. require.Nil(t, updatedUser)
  187. storedUser, err := client.User.Get(ctx, sourceUser.ID)
  188. require.NoError(t, err)
  189. require.Equal(t, "source-user"+service.OIDCConnectSyntheticEmailDomain, storedUser.Email)
  190. require.Equal(t, 0, countProviderGrantRecords(t, client, sourceUser.ID, "email", "first_bind"))
  191. }
  192. func TestAuthServiceBindEmailIdentity_RejectsAliasOfExistingEmailOnAnotherUser(t *testing.T) {
  193. cache := &emailBindCacheStub{
  194. data: &service.VerificationCodeData{
  195. Code: "123456",
  196. CreatedAt: time.Now().UTC().Add(-10 * time.Minute),
  197. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  198. },
  199. }
  200. svc, _, client := newAuthServiceForEmailBind(t, nil, cache, nil)
  201. ctx := context.Background()
  202. sourceUser := createEmailBindTestUser(
  203. t,
  204. client,
  205. "source-user"+service.OIDCConnectSyntheticEmailDomain,
  206. "source-user",
  207. "old-hash",
  208. )
  209. createEmailBindTestUser(t, client, "zck.ioio123@gmail.com", "inbox-owner", "hash")
  210. err := svc.SendEmailIdentityBindCode(ctx, sourceUser.ID, "zckioio123+new@gmail.com")
  211. require.ErrorIs(t, err, service.ErrEmailExists)
  212. require.Empty(t, cache.setEmails)
  213. updatedUser, err := svc.BindEmailIdentity(
  214. ctx,
  215. sourceUser.ID,
  216. "zckioio123+new@gmail.com",
  217. "123456",
  218. "new-password",
  219. )
  220. require.ErrorIs(t, err, service.ErrEmailExists)
  221. require.Nil(t, updatedUser)
  222. storedUser, err := client.User.Get(ctx, sourceUser.ID)
  223. require.NoError(t, err)
  224. require.Equal(t, "source-user"+service.OIDCConnectSyntheticEmailDomain, storedUser.Email)
  225. require.Equal(t, "old-hash", storedUser.PasswordHash)
  226. }
  227. func TestAuthServiceBindEmailIdentity_AllowsOnlyOneConcurrentAliasVariant(t *testing.T) {
  228. cache := &emailBindCacheStub{
  229. data: &service.VerificationCodeData{
  230. Code: "123456",
  231. CreatedAt: time.Now().UTC(),
  232. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  233. },
  234. }
  235. svc, _, client := newAuthServiceForEmailBind(t, nil, cache, nil)
  236. ctx := context.Background()
  237. unique := fmt.Sprintf("%d", time.Now().UnixNano())
  238. first := createEmailBindTestUser(
  239. t,
  240. client,
  241. "first-"+unique+service.OIDCConnectSyntheticEmailDomain,
  242. "first-"+unique,
  243. "old-hash",
  244. )
  245. second := createEmailBindTestUser(
  246. t,
  247. client,
  248. "second-"+unique+service.OIDCConnectSyntheticEmailDomain,
  249. "second-"+unique,
  250. "old-hash",
  251. )
  252. start := make(chan struct{})
  253. results := make(chan error, 2)
  254. go func() {
  255. <-start
  256. _, err := svc.BindEmailIdentity(ctx, first.ID, "inbox-"+unique+"+one@gmail.com", "123456", "new-password")
  257. results <- err
  258. }()
  259. go func() {
  260. <-start
  261. _, err := svc.BindEmailIdentity(ctx, second.ID, "inbox-"+unique+"+two@gmail.com", "123456", "new-password")
  262. results <- err
  263. }()
  264. close(start)
  265. var successes, conflicts int
  266. for range 2 {
  267. err := <-results
  268. switch {
  269. case err == nil:
  270. successes++
  271. case errors.Is(err, service.ErrEmailExists):
  272. conflicts++
  273. default:
  274. t.Fatalf("unexpected bind error: %v", err)
  275. }
  276. }
  277. require.Equal(t, 1, successes)
  278. require.Equal(t, 1, conflicts)
  279. boundCount, err := client.User.Query().
  280. Where(dbuser.EmailIn(
  281. "inbox-"+unique+"+one@gmail.com",
  282. "inbox-"+unique+"+two@gmail.com",
  283. )).
  284. Count(ctx)
  285. require.NoError(t, err)
  286. require.Equal(t, 1, boundCount)
  287. }
  288. func TestAuthServiceBindEmailIdentity_RejectsNewAliasWhenAnotherUserSharesCurrentUserInbox(t *testing.T) {
  289. cache := &emailBindCacheStub{
  290. data: &service.VerificationCodeData{
  291. Code: "123456",
  292. CreatedAt: time.Now().UTC(),
  293. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  294. },
  295. }
  296. svc, _, client := newAuthServiceForEmailBind(t, nil, cache, nil)
  297. ctx := context.Background()
  298. hashedPassword, err := svc.HashPassword("current-password")
  299. require.NoError(t, err)
  300. currentUser := createEmailBindTestUser(t, client, "inbox+own@gmail.com", "current", hashedPassword)
  301. createEmailBindTestUser(t, client, "inbox+legacy@gmail.com", "legacy", "hash")
  302. updatedUser, err := svc.BindEmailIdentity(
  303. ctx,
  304. currentUser.ID,
  305. "inbox+new@gmail.com",
  306. "123456",
  307. "current-password",
  308. )
  309. require.ErrorIs(t, err, service.ErrEmailExists)
  310. require.Nil(t, updatedUser)
  311. storedUser, err := client.User.Get(ctx, currentUser.ID)
  312. require.NoError(t, err)
  313. require.Equal(t, "inbox+own@gmail.com", storedUser.Email)
  314. }
  315. func TestAuthServiceBindEmailIdentity_RollsBackWhenFirstBindDefaultsFail(t *testing.T) {
  316. assigner := &flakyEmailBindDefaultSubAssignerStub{err: errors.New("temporary assign failure")}
  317. cache := &emailBindCacheStub{
  318. data: &service.VerificationCodeData{
  319. Code: "123456",
  320. CreatedAt: time.Now().UTC(),
  321. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  322. },
  323. }
  324. svc, _, client := newAuthServiceForEmailBind(t, map[string]string{
  325. service.SettingKeyAuthSourceDefaultEmailBalance: "8.5",
  326. service.SettingKeyAuthSourceDefaultEmailConcurrency: "4",
  327. service.SettingKeyAuthSourceDefaultEmailSubscriptions: `[{"group_id":11,"validity_days":30}]`,
  328. service.SettingKeyAuthSourceDefaultEmailGrantOnFirstBind: "true",
  329. }, cache, assigner)
  330. ctx := context.Background()
  331. originalEmail := "legacy-rollback" + service.LinuxDoConnectSyntheticEmailDomain
  332. user, err := client.User.Create().
  333. SetEmail(originalEmail).
  334. SetUsername("legacy-rollback").
  335. SetPasswordHash("old-hash").
  336. SetBalance(2.5).
  337. SetConcurrency(1).
  338. SetRole(service.RoleUser).
  339. SetStatus(service.StatusActive).
  340. Save(ctx)
  341. require.NoError(t, err)
  342. updatedUser, err := svc.BindEmailIdentity(ctx, user.ID, "rollback@example.com", "123456", "new-password")
  343. require.ErrorContains(t, err, "apply email first bind defaults")
  344. require.ErrorContains(t, err, "temporary assign failure")
  345. require.Nil(t, updatedUser)
  346. storedUser, err := client.User.Get(ctx, user.ID)
  347. require.NoError(t, err)
  348. require.Equal(t, originalEmail, storedUser.Email)
  349. require.Equal(t, "old-hash", storedUser.PasswordHash)
  350. require.Equal(t, 2.5, storedUser.Balance)
  351. require.Equal(t, 1, storedUser.Concurrency)
  352. identityCount, err := client.AuthIdentity.Query().
  353. Where(
  354. authidentity.UserIDEQ(user.ID),
  355. authidentity.ProviderTypeEQ("email"),
  356. authidentity.ProviderKeyEQ("email"),
  357. authidentity.ProviderSubjectEQ("rollback@example.com"),
  358. ).
  359. Count(ctx)
  360. require.NoError(t, err)
  361. require.Equal(t, 0, identityCount)
  362. require.Len(t, assigner.calls, 1)
  363. require.Equal(t, 0, countProviderGrantRecords(t, client, user.ID, "email", "first_bind"))
  364. }
  365. func TestAuthServiceBindEmailIdentity_RejectsReservedEmail(t *testing.T) {
  366. cache := &emailBindCacheStub{
  367. data: &service.VerificationCodeData{
  368. Code: "123456",
  369. CreatedAt: time.Now().UTC(),
  370. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  371. },
  372. }
  373. svc, _, client := newAuthServiceForEmailBind(t, nil, cache, nil)
  374. ctx := context.Background()
  375. user, err := client.User.Create().
  376. SetEmail("source-user@example.com").
  377. SetUsername("source-user").
  378. SetPasswordHash("old-hash").
  379. SetBalance(1).
  380. SetConcurrency(1).
  381. SetRole(service.RoleUser).
  382. SetStatus(service.StatusActive).
  383. Save(ctx)
  384. require.NoError(t, err)
  385. updatedUser, err := svc.BindEmailIdentity(ctx, user.ID, "reserved"+service.LinuxDoConnectSyntheticEmailDomain, "123456", "new-password")
  386. require.ErrorIs(t, err, service.ErrEmailReserved)
  387. require.Nil(t, updatedUser)
  388. }
  389. func TestAuthServiceBindEmailIdentity_ReplacesBoundEmailAndSkipsFirstBindDefaults(t *testing.T) {
  390. assigner := &emailBindDefaultSubAssignerStub{}
  391. cache := &emailBindCacheStub{
  392. data: &service.VerificationCodeData{
  393. Code: "123456",
  394. CreatedAt: time.Now().UTC(),
  395. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  396. },
  397. }
  398. svc, _, client := newAuthServiceForEmailBind(t, map[string]string{
  399. service.SettingKeyAuthSourceDefaultEmailBalance: "8.5",
  400. service.SettingKeyAuthSourceDefaultEmailConcurrency: "4",
  401. service.SettingKeyAuthSourceDefaultEmailSubscriptions: `[{"group_id":11,"validity_days":30}]`,
  402. service.SettingKeyAuthSourceDefaultEmailGrantOnFirstBind: "true",
  403. }, cache, assigner)
  404. ctx := context.Background()
  405. hashedPassword, err := svc.HashPassword("current-password")
  406. require.NoError(t, err)
  407. user, err := client.User.Create().
  408. SetEmail("current@example.com").
  409. SetUsername("bound-user").
  410. SetPasswordHash(hashedPassword).
  411. SetBalance(7.5).
  412. SetConcurrency(3).
  413. SetRole(service.RoleUser).
  414. SetStatus(service.StatusActive).
  415. Save(ctx)
  416. require.NoError(t, err)
  417. require.NoError(t, client.AuthIdentity.Create().
  418. SetUserID(user.ID).
  419. SetProviderType("email").
  420. SetProviderKey("email").
  421. SetProviderSubject("current@example.com").
  422. SetVerifiedAt(time.Now().UTC()).
  423. SetMetadata(map[string]any{"source": "test"}).
  424. Exec(ctx))
  425. updatedUser, err := svc.BindEmailIdentity(ctx, user.ID, "new@example.com", "123456", "current-password")
  426. require.NoError(t, err)
  427. require.NotNil(t, updatedUser)
  428. require.Equal(t, "new@example.com", updatedUser.Email)
  429. storedUser, err := client.User.Get(ctx, user.ID)
  430. require.NoError(t, err)
  431. require.Equal(t, "new@example.com", storedUser.Email)
  432. require.Equal(t, 7.5, storedUser.Balance)
  433. require.Equal(t, 3, storedUser.Concurrency)
  434. require.True(t, svc.CheckPassword("current-password", storedUser.PasswordHash))
  435. newIdentityCount, err := client.AuthIdentity.Query().
  436. Where(
  437. authidentity.UserIDEQ(user.ID),
  438. authidentity.ProviderTypeEQ("email"),
  439. authidentity.ProviderKeyEQ("email"),
  440. authidentity.ProviderSubjectEQ("new@example.com"),
  441. ).
  442. Count(ctx)
  443. require.NoError(t, err)
  444. require.Equal(t, 1, newIdentityCount)
  445. oldIdentityCount, err := client.AuthIdentity.Query().
  446. Where(
  447. authidentity.UserIDEQ(user.ID),
  448. authidentity.ProviderTypeEQ("email"),
  449. authidentity.ProviderKeyEQ("email"),
  450. authidentity.ProviderSubjectEQ("current@example.com"),
  451. ).
  452. Count(ctx)
  453. require.NoError(t, err)
  454. require.Equal(t, 0, oldIdentityCount)
  455. require.Empty(t, assigner.calls)
  456. require.Equal(t, 0, countProviderGrantRecords(t, client, user.ID, "email", "first_bind"))
  457. }
  458. func TestAuthServiceBindEmailIdentity_RejectsWrongCurrentPasswordForBoundEmail(t *testing.T) {
  459. cache := &emailBindCacheStub{
  460. data: &service.VerificationCodeData{
  461. Code: "123456",
  462. CreatedAt: time.Now().UTC(),
  463. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  464. },
  465. }
  466. svc, _, client := newAuthServiceForEmailBind(t, nil, cache, nil)
  467. ctx := context.Background()
  468. hashedPassword, err := svc.HashPassword("current-password")
  469. require.NoError(t, err)
  470. user, err := client.User.Create().
  471. SetEmail("current@example.com").
  472. SetUsername("bound-user").
  473. SetPasswordHash(hashedPassword).
  474. SetBalance(1).
  475. SetConcurrency(1).
  476. SetRole(service.RoleUser).
  477. SetStatus(service.StatusActive).
  478. Save(ctx)
  479. require.NoError(t, err)
  480. require.NoError(t, client.AuthIdentity.Create().
  481. SetUserID(user.ID).
  482. SetProviderType("email").
  483. SetProviderKey("email").
  484. SetProviderSubject("current@example.com").
  485. SetVerifiedAt(time.Now().UTC()).
  486. SetMetadata(map[string]any{"source": "test"}).
  487. Exec(ctx))
  488. updatedUser, err := svc.BindEmailIdentity(ctx, user.ID, "new@example.com", "123456", "wrong-password")
  489. require.ErrorIs(t, err, service.ErrPasswordIncorrect)
  490. require.Nil(t, updatedUser)
  491. storedUser, err := client.User.Get(ctx, user.ID)
  492. require.NoError(t, err)
  493. require.Equal(t, "current@example.com", storedUser.Email)
  494. require.True(t, svc.CheckPassword("current-password", storedUser.PasswordHash))
  495. oldIdentityCount, err := client.AuthIdentity.Query().
  496. Where(
  497. authidentity.UserIDEQ(user.ID),
  498. authidentity.ProviderTypeEQ("email"),
  499. authidentity.ProviderKeyEQ("email"),
  500. authidentity.ProviderSubjectEQ("current@example.com"),
  501. ).
  502. Count(ctx)
  503. require.NoError(t, err)
  504. require.Equal(t, 1, oldIdentityCount)
  505. newIdentityCount, err := client.AuthIdentity.Query().
  506. Where(
  507. authidentity.UserIDEQ(user.ID),
  508. authidentity.ProviderTypeEQ("email"),
  509. authidentity.ProviderKeyEQ("email"),
  510. authidentity.ProviderSubjectEQ("new@example.com"),
  511. ).
  512. Count(ctx)
  513. require.NoError(t, err)
  514. require.Equal(t, 0, newIdentityCount)
  515. }
  516. func TestAuthServiceBindEmailIdentity_RevokesExistingAccessAndRefreshTokens(t *testing.T) {
  517. ctx := context.Background()
  518. cache := &emailBindCacheStub{
  519. data: &service.VerificationCodeData{
  520. Code: "123456",
  521. CreatedAt: time.Now().UTC(),
  522. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  523. },
  524. }
  525. refreshTokenCache := newEmailBindRefreshTokenCacheStub()
  526. userRepo := newEmailBindUserRepoStub(&service.User{
  527. ID: 41,
  528. Email: "legacy-user" + service.OIDCConnectSyntheticEmailDomain,
  529. Username: "legacy-user",
  530. PasswordHash: "old-hash",
  531. Role: service.RoleUser,
  532. Status: service.StatusActive,
  533. TokenVersion: 4,
  534. })
  535. cfg := &config.Config{
  536. JWT: config.JWTConfig{
  537. Secret: "test-bind-email-secret",
  538. ExpireHour: 1,
  539. AccessTokenExpireMinutes: 60,
  540. RefreshTokenExpireDays: 7,
  541. },
  542. }
  543. emailService := service.NewEmailService(nil, cache)
  544. svc := service.NewAuthService(nil, userRepo, nil, refreshTokenCache, cfg, nil, emailService, nil, nil, nil, nil, nil, nil)
  545. oldTokenPair, err := svc.GenerateTokenPair(ctx, &service.User{
  546. ID: 41,
  547. Email: "legacy-user" + service.OIDCConnectSyntheticEmailDomain,
  548. Role: service.RoleUser,
  549. Status: service.StatusActive,
  550. TokenVersion: 4,
  551. }, "")
  552. require.NoError(t, err)
  553. updatedUser, err := svc.BindEmailIdentity(ctx, 41, "new@example.com", "123456", "new-password")
  554. require.NoError(t, err)
  555. require.NotNil(t, updatedUser)
  556. storedUser, err := userRepo.GetByID(ctx, 41)
  557. require.NoError(t, err)
  558. require.Equal(t, "new@example.com", storedUser.Email)
  559. require.True(t, svc.CheckPassword("new-password", storedUser.PasswordHash))
  560. _, err = svc.RefreshToken(ctx, oldTokenPair.AccessToken)
  561. require.ErrorIs(t, err, service.ErrTokenRevoked)
  562. _, err = svc.RefreshTokenPair(ctx, oldTokenPair.RefreshToken)
  563. require.True(t, errors.Is(err, service.ErrTokenRevoked) || errors.Is(err, service.ErrRefreshTokenInvalid))
  564. }
  565. func TestAuthServiceEmailIdentityBinding_RejectsEmailOutsideRegistrationSuffixWhitelist(t *testing.T) {
  566. ctx := context.Background()
  567. cache := &emailBindCacheStub{
  568. data: &service.VerificationCodeData{
  569. Code: "123456",
  570. CreatedAt: time.Now().UTC(),
  571. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  572. },
  573. }
  574. svc, _, client := newAuthServiceForEmailBind(t, map[string]string{
  575. service.SettingKeyRegistrationEmailSuffixWhitelist: `["@qq.com"]`,
  576. }, cache, nil)
  577. user := createEmailBindTestUser(t, client, "legacy-user"+service.OIDCConnectSyntheticEmailDomain, "legacy-user", "old-hash")
  578. err := svc.SendEmailIdentityBindCode(ctx, user.ID, "intruder@gmail.com")
  579. require.ErrorIs(t, err, service.ErrEmailSuffixNotAllowed)
  580. require.Empty(t, cache.setEmails)
  581. updatedUser, err := svc.BindEmailIdentity(ctx, user.ID, "intruder@gmail.com", "123456", "new-password")
  582. require.ErrorIs(t, err, service.ErrEmailSuffixNotAllowed)
  583. require.Nil(t, updatedUser)
  584. storedUser, err := client.User.Get(ctx, user.ID)
  585. require.NoError(t, err)
  586. require.Equal(t, "legacy-user"+service.OIDCConnectSyntheticEmailDomain, storedUser.Email)
  587. }
  588. func TestAuthServiceBindEmailIdentity_AllowsEmailInsideRegistrationSuffixWhitelist(t *testing.T) {
  589. ctx := context.Background()
  590. cache := &emailBindCacheStub{
  591. data: &service.VerificationCodeData{
  592. Code: "123456",
  593. CreatedAt: time.Now().UTC(),
  594. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  595. },
  596. }
  597. svc, _, client := newAuthServiceForEmailBind(t, map[string]string{
  598. service.SettingKeyRegistrationEmailSuffixWhitelist: `["@qq.com"]`,
  599. }, cache, nil)
  600. user := createEmailBindTestUser(t, client, "legacy-qq"+service.LinuxDoConnectSyntheticEmailDomain, "legacy-qq", "old-hash")
  601. updatedUser, err := svc.BindEmailIdentity(ctx, user.ID, " Member@QQ.com ", "123456", "new-password")
  602. require.NoError(t, err)
  603. require.NotNil(t, updatedUser)
  604. require.Equal(t, "member@qq.com", updatedUser.Email)
  605. storedUser, err := client.User.Get(ctx, user.ID)
  606. require.NoError(t, err)
  607. require.Equal(t, "member@qq.com", storedUser.Email)
  608. }
  609. func TestAuthServiceBindEmailIdentity_RegistrationSuffixWhitelistWildcard(t *testing.T) {
  610. ctx := context.Background()
  611. t.Run("allows wildcard suffix", func(t *testing.T) {
  612. cache := &emailBindCacheStub{
  613. data: &service.VerificationCodeData{
  614. Code: "123456",
  615. CreatedAt: time.Now().UTC(),
  616. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  617. },
  618. }
  619. svc, _, client := newAuthServiceForEmailBind(t, map[string]string{
  620. service.SettingKeyRegistrationEmailSuffixWhitelist: `["*.edu.cn"]`,
  621. }, cache, nil)
  622. user := createEmailBindTestUser(t, client, "legacy-student"+service.OIDCConnectSyntheticEmailDomain, "legacy-student", "old-hash")
  623. updatedUser, err := svc.BindEmailIdentity(ctx, user.ID, "student@cs.edu.cn", "123456", "new-password")
  624. require.NoError(t, err)
  625. require.NotNil(t, updatedUser)
  626. require.Equal(t, "student@cs.edu.cn", updatedUser.Email)
  627. })
  628. t.Run("rejects outside wildcard suffix", func(t *testing.T) {
  629. cache := &emailBindCacheStub{
  630. data: &service.VerificationCodeData{
  631. Code: "123456",
  632. CreatedAt: time.Now().UTC(),
  633. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  634. },
  635. }
  636. svc, _, client := newAuthServiceForEmailBind(t, map[string]string{
  637. service.SettingKeyRegistrationEmailSuffixWhitelist: `["*.edu.cn"]`,
  638. }, cache, nil)
  639. user := createEmailBindTestUser(t, client, "legacy-wildcard"+service.OIDCConnectSyntheticEmailDomain, "legacy-wildcard", "old-hash")
  640. updatedUser, err := svc.BindEmailIdentity(ctx, user.ID, "foo@gmail.com", "123456", "new-password")
  641. require.ErrorIs(t, err, service.ErrEmailSuffixNotAllowed)
  642. require.Nil(t, updatedUser)
  643. storedUser, err := client.User.Get(ctx, user.ID)
  644. require.NoError(t, err)
  645. require.Equal(t, "legacy-wildcard"+service.OIDCConnectSyntheticEmailDomain, storedUser.Email)
  646. })
  647. }
  648. func TestAuthServiceBindEmailIdentity_AllowsAnyEmailWhenRegistrationSuffixWhitelistEmpty(t *testing.T) {
  649. ctx := context.Background()
  650. cache := &emailBindCacheStub{
  651. data: &service.VerificationCodeData{
  652. Code: "123456",
  653. CreatedAt: time.Now().UTC(),
  654. ExpiresAt: time.Now().UTC().Add(10 * time.Minute),
  655. },
  656. }
  657. svc, _, client := newAuthServiceForEmailBind(t, map[string]string{
  658. service.SettingKeyRegistrationEmailSuffixWhitelist: "[]",
  659. }, cache, nil)
  660. user := createEmailBindTestUser(t, client, "legacy-empty"+service.LinuxDoConnectSyntheticEmailDomain, "legacy-empty", "old-hash")
  661. updatedUser, err := svc.BindEmailIdentity(ctx, user.ID, "anyone@gmail.com", "123456", "new-password")
  662. require.NoError(t, err)
  663. require.NotNil(t, updatedUser)
  664. require.Equal(t, "anyone@gmail.com", updatedUser.Email)
  665. }
  666. func createEmailBindTestUser(t *testing.T, client *dbent.Client, email, username, passwordHash string) *dbent.User {
  667. t.Helper()
  668. user, err := client.User.Create().
  669. SetEmail(email).
  670. SetUsername(username).
  671. SetPasswordHash(passwordHash).
  672. SetBalance(1).
  673. SetConcurrency(1).
  674. SetRole(service.RoleUser).
  675. SetStatus(service.StatusActive).
  676. Save(context.Background())
  677. require.NoError(t, err)
  678. return user
  679. }
  680. type emailBindSettingRepoStub struct {
  681. values map[string]string
  682. }
  683. func (s *emailBindSettingRepoStub) Get(context.Context, string) (*service.Setting, error) {
  684. panic("unexpected Get call")
  685. }
  686. func (s *emailBindSettingRepoStub) GetValue(_ context.Context, key string) (string, error) {
  687. if v, ok := s.values[key]; ok {
  688. return v, nil
  689. }
  690. return "", service.ErrSettingNotFound
  691. }
  692. func (s *emailBindSettingRepoStub) Set(context.Context, string, string) error {
  693. panic("unexpected Set call")
  694. }
  695. func (s *emailBindSettingRepoStub) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
  696. out := make(map[string]string, len(keys))
  697. for _, key := range keys {
  698. if v, ok := s.values[key]; ok {
  699. out[key] = v
  700. }
  701. }
  702. return out, nil
  703. }
  704. func (s *emailBindSettingRepoStub) SetMultiple(context.Context, map[string]string) error {
  705. panic("unexpected SetMultiple call")
  706. }
  707. func (s *emailBindSettingRepoStub) GetAll(context.Context) (map[string]string, error) {
  708. panic("unexpected GetAll call")
  709. }
  710. func (s *emailBindSettingRepoStub) Delete(context.Context, string) error {
  711. panic("unexpected Delete call")
  712. }
  713. type emailBindCacheStub struct {
  714. data *service.VerificationCodeData
  715. err error
  716. setEmails []string
  717. }
  718. func (s *emailBindCacheStub) GetVerificationCode(context.Context, string) (*service.VerificationCodeData, error) {
  719. if s.err != nil {
  720. return nil, s.err
  721. }
  722. return s.data, nil
  723. }
  724. func (s *emailBindCacheStub) SetVerificationCode(_ context.Context, email string, _ *service.VerificationCodeData, _ time.Duration) error {
  725. s.setEmails = append(s.setEmails, email)
  726. return nil
  727. }
  728. func (s *emailBindCacheStub) DeleteVerificationCode(context.Context, string) error {
  729. return nil
  730. }
  731. func (s *emailBindCacheStub) GetNotifyVerifyCode(context.Context, string) (*service.VerificationCodeData, error) {
  732. return nil, nil
  733. }
  734. func (s *emailBindCacheStub) SetNotifyVerifyCode(context.Context, string, *service.VerificationCodeData, time.Duration) error {
  735. return nil
  736. }
  737. func (s *emailBindCacheStub) DeleteNotifyVerifyCode(context.Context, string) error {
  738. return nil
  739. }
  740. func (s *emailBindCacheStub) GetPasswordResetToken(context.Context, string) (*service.PasswordResetTokenData, error) {
  741. return nil, nil
  742. }
  743. func (s *emailBindCacheStub) SetPasswordResetToken(context.Context, string, *service.PasswordResetTokenData, time.Duration) error {
  744. return nil
  745. }
  746. func (s *emailBindCacheStub) DeletePasswordResetToken(context.Context, string) error {
  747. return nil
  748. }
  749. func (s *emailBindCacheStub) IsPasswordResetEmailInCooldown(context.Context, string) bool {
  750. return false
  751. }
  752. func (s *emailBindCacheStub) SetPasswordResetEmailCooldown(context.Context, string, time.Duration) error {
  753. return nil
  754. }
  755. func (s *emailBindCacheStub) GetNotifyCodeUserRate(context.Context, int64) (int64, error) {
  756. return 0, nil
  757. }
  758. func (s *emailBindCacheStub) IncrNotifyCodeUserRate(context.Context, int64, time.Duration) (int64, error) {
  759. return 0, nil
  760. }
  761. type emailBindRefreshTokenCacheStub struct {
  762. mu sync.Mutex
  763. tokens map[string]*service.RefreshTokenData
  764. userSets map[int64]map[string]struct{}
  765. families map[string]map[string]struct{}
  766. }
  767. func newEmailBindRefreshTokenCacheStub() *emailBindRefreshTokenCacheStub {
  768. return &emailBindRefreshTokenCacheStub{
  769. tokens: make(map[string]*service.RefreshTokenData),
  770. userSets: make(map[int64]map[string]struct{}),
  771. families: make(map[string]map[string]struct{}),
  772. }
  773. }
  774. func (s *emailBindRefreshTokenCacheStub) StoreRefreshToken(_ context.Context, tokenHash string, data *service.RefreshTokenData, _ time.Duration) error {
  775. s.mu.Lock()
  776. defer s.mu.Unlock()
  777. cloned := *data
  778. s.tokens[tokenHash] = &cloned
  779. return nil
  780. }
  781. func (s *emailBindRefreshTokenCacheStub) GetRefreshToken(_ context.Context, tokenHash string) (*service.RefreshTokenData, error) {
  782. s.mu.Lock()
  783. defer s.mu.Unlock()
  784. data, ok := s.tokens[tokenHash]
  785. if !ok {
  786. return nil, service.ErrRefreshTokenNotFound
  787. }
  788. cloned := *data
  789. return &cloned, nil
  790. }
  791. func (s *emailBindRefreshTokenCacheStub) DeleteRefreshToken(_ context.Context, tokenHash string) error {
  792. s.mu.Lock()
  793. defer s.mu.Unlock()
  794. delete(s.tokens, tokenHash)
  795. for _, tokenSet := range s.userSets {
  796. delete(tokenSet, tokenHash)
  797. }
  798. for _, tokenSet := range s.families {
  799. delete(tokenSet, tokenHash)
  800. }
  801. return nil
  802. }
  803. func (s *emailBindRefreshTokenCacheStub) DeleteUserRefreshTokens(_ context.Context, userID int64) error {
  804. s.mu.Lock()
  805. defer s.mu.Unlock()
  806. for tokenHash := range s.userSets[userID] {
  807. delete(s.tokens, tokenHash)
  808. for _, tokenSet := range s.families {
  809. delete(tokenSet, tokenHash)
  810. }
  811. }
  812. delete(s.userSets, userID)
  813. return nil
  814. }
  815. func (s *emailBindRefreshTokenCacheStub) DeleteTokenFamily(_ context.Context, familyID string) error {
  816. s.mu.Lock()
  817. defer s.mu.Unlock()
  818. for tokenHash := range s.families[familyID] {
  819. delete(s.tokens, tokenHash)
  820. for _, tokenSet := range s.userSets {
  821. delete(tokenSet, tokenHash)
  822. }
  823. }
  824. delete(s.families, familyID)
  825. return nil
  826. }
  827. func (s *emailBindRefreshTokenCacheStub) AddToUserTokenSet(_ context.Context, userID int64, tokenHash string, _ time.Duration) error {
  828. s.mu.Lock()
  829. defer s.mu.Unlock()
  830. if s.userSets[userID] == nil {
  831. s.userSets[userID] = make(map[string]struct{})
  832. }
  833. s.userSets[userID][tokenHash] = struct{}{}
  834. return nil
  835. }
  836. func (s *emailBindRefreshTokenCacheStub) AddToFamilyTokenSet(_ context.Context, familyID string, tokenHash string, _ time.Duration) error {
  837. s.mu.Lock()
  838. defer s.mu.Unlock()
  839. if s.families[familyID] == nil {
  840. s.families[familyID] = make(map[string]struct{})
  841. }
  842. s.families[familyID][tokenHash] = struct{}{}
  843. return nil
  844. }
  845. func (s *emailBindRefreshTokenCacheStub) GetUserTokenHashes(_ context.Context, userID int64) ([]string, error) {
  846. s.mu.Lock()
  847. defer s.mu.Unlock()
  848. tokenSet := s.userSets[userID]
  849. out := make([]string, 0, len(tokenSet))
  850. for tokenHash := range tokenSet {
  851. out = append(out, tokenHash)
  852. }
  853. return out, nil
  854. }
  855. func (s *emailBindRefreshTokenCacheStub) GetFamilyTokenHashes(_ context.Context, familyID string) ([]string, error) {
  856. s.mu.Lock()
  857. defer s.mu.Unlock()
  858. tokenSet := s.families[familyID]
  859. out := make([]string, 0, len(tokenSet))
  860. for tokenHash := range tokenSet {
  861. out = append(out, tokenHash)
  862. }
  863. return out, nil
  864. }
  865. func (s *emailBindRefreshTokenCacheStub) IsTokenInFamily(_ context.Context, familyID string, tokenHash string) (bool, error) {
  866. s.mu.Lock()
  867. defer s.mu.Unlock()
  868. _, ok := s.families[familyID][tokenHash]
  869. return ok, nil
  870. }
  871. type emailBindUserRepoStub struct {
  872. mu sync.Mutex
  873. usersByID map[int64]*service.User
  874. usersByEmail map[string]*service.User
  875. }
  876. func newEmailBindUserRepoStub(user *service.User) *emailBindUserRepoStub {
  877. cloned := cloneEmailBindUser(user)
  878. return &emailBindUserRepoStub{
  879. usersByID: map[int64]*service.User{
  880. cloned.ID: cloned,
  881. },
  882. usersByEmail: map[string]*service.User{
  883. cloned.Email: cloned,
  884. },
  885. }
  886. }
  887. func (s *emailBindUserRepoStub) Create(context.Context, *service.User) error { return nil }
  888. func (s *emailBindUserRepoStub) CreateWithEmailAliasGuard(ctx context.Context, user *service.User) error {
  889. return s.Create(ctx, user)
  890. }
  891. func (s *emailBindUserRepoStub) GetByID(_ context.Context, id int64) (*service.User, error) {
  892. s.mu.Lock()
  893. defer s.mu.Unlock()
  894. user, ok := s.usersByID[id]
  895. if !ok {
  896. return nil, service.ErrUserNotFound
  897. }
  898. return cloneEmailBindUser(user), nil
  899. }
  900. func (s *emailBindUserRepoStub) GetByEmail(_ context.Context, email string) (*service.User, error) {
  901. s.mu.Lock()
  902. defer s.mu.Unlock()
  903. user, ok := s.usersByEmail[email]
  904. if !ok {
  905. return nil, service.ErrUserNotFound
  906. }
  907. return cloneEmailBindUser(user), nil
  908. }
  909. func (s *emailBindUserRepoStub) GetFirstAdmin(context.Context) (*service.User, error) {
  910. panic("unexpected GetFirstAdmin call")
  911. }
  912. func (s *emailBindUserRepoStub) Update(_ context.Context, user *service.User, _ service.UserUpdateFields) error {
  913. s.mu.Lock()
  914. defer s.mu.Unlock()
  915. existing, ok := s.usersByID[user.ID]
  916. if !ok {
  917. return service.ErrUserNotFound
  918. }
  919. delete(s.usersByEmail, existing.Email)
  920. cloned := cloneEmailBindUser(user)
  921. s.usersByID[user.ID] = cloned
  922. s.usersByEmail[cloned.Email] = cloned
  923. return nil
  924. }
  925. func (s *emailBindUserRepoStub) Delete(context.Context, int64) error { return nil }
  926. func (s *emailBindUserRepoStub) GetUserAvatar(context.Context, int64) (*service.UserAvatar, error) {
  927. return nil, nil
  928. }
  929. func (s *emailBindUserRepoStub) UpsertUserAvatar(context.Context, int64, service.UpsertUserAvatarInput) (*service.UserAvatar, error) {
  930. panic("unexpected UpsertUserAvatar call")
  931. }
  932. func (s *emailBindUserRepoStub) DeleteUserAvatar(context.Context, int64) error {
  933. panic("unexpected DeleteUserAvatar call")
  934. }
  935. func (s *emailBindUserRepoStub) List(context.Context, pagination.PaginationParams) ([]service.User, *pagination.PaginationResult, error) {
  936. panic("unexpected List call")
  937. }
  938. func (s *emailBindUserRepoStub) ListWithFilters(context.Context, pagination.PaginationParams, service.UserListFilters) ([]service.User, *pagination.PaginationResult, error) {
  939. panic("unexpected ListWithFilters call")
  940. }
  941. func (s *emailBindUserRepoStub) GetLatestUsedAtByUserIDs(context.Context, []int64) (map[int64]*time.Time, error) {
  942. return map[int64]*time.Time{}, nil
  943. }
  944. func (s *emailBindUserRepoStub) GetLatestUsedAtByUserID(context.Context, int64) (*time.Time, error) {
  945. return nil, nil
  946. }
  947. func (s *emailBindUserRepoStub) UpdateUserLastActiveAt(context.Context, int64, time.Time) error {
  948. return nil
  949. }
  950. func (s *emailBindUserRepoStub) UpdateBalance(context.Context, int64, float64) error { return nil }
  951. func (s *emailBindUserRepoStub) DeductBalance(context.Context, int64, float64) error { return nil }
  952. func (s *emailBindUserRepoStub) UpdateConcurrency(context.Context, int64, int) error { return nil }
  953. func (s *emailBindUserRepoStub) ExistsByEmail(_ context.Context, email string) (bool, error) {
  954. s.mu.Lock()
  955. defer s.mu.Unlock()
  956. _, ok := s.usersByEmail[email]
  957. return ok, nil
  958. }
  959. func (s *emailBindUserRepoStub) AdjustBalance(ctx context.Context, id int64, delta float64) (service.BalanceChange, error) {
  960. panic("unexpected AdjustBalance call")
  961. }
  962. func (s *emailBindUserRepoStub) SetBalance(ctx context.Context, id int64, value float64) (service.BalanceChange, error) {
  963. panic("unexpected SetBalance call")
  964. }
  965. func (s *emailBindUserRepoStub) ExistsByEmailAlias(_ context.Context, email string) (bool, error) {
  966. s.mu.Lock()
  967. defer s.mu.Unlock()
  968. identity := service.NormalizeEmailForAliasDedup(email)
  969. for stored := range s.usersByEmail {
  970. if service.NormalizeEmailForAliasDedup(stored) == identity {
  971. return true, nil
  972. }
  973. }
  974. return false, nil
  975. }
  976. func (s *emailBindUserRepoStub) BatchSetConcurrency(context.Context, []int64, int) (int, error) {
  977. return 0, nil
  978. }
  979. func (s *emailBindUserRepoStub) BatchAddConcurrency(context.Context, []int64, int) (int, error) {
  980. return 0, nil
  981. }
  982. func (s *emailBindUserRepoStub) BatchUpdateLimits(context.Context, []int64, *int, *int) (int, error) {
  983. return 0, nil
  984. }
  985. func (s *emailBindUserRepoStub) RemoveGroupFromAllowedGroups(context.Context, int64) (int64, error) {
  986. return 0, nil
  987. }
  988. func (s *emailBindUserRepoStub) AddGroupToAllowedGroups(context.Context, int64, int64) error {
  989. return nil
  990. }
  991. func (s *emailBindUserRepoStub) RemoveGroupFromUserAllowedGroups(context.Context, int64, int64) error {
  992. return nil
  993. }
  994. func (s *emailBindUserRepoStub) ListUserAuthIdentities(context.Context, int64) ([]service.UserAuthIdentityRecord, error) {
  995. return nil, nil
  996. }
  997. func (s *emailBindUserRepoStub) UnbindUserAuthProvider(context.Context, int64, string) error {
  998. return nil
  999. }
  1000. func (s *emailBindUserRepoStub) UpdateTotpSecret(context.Context, int64, *string) error { return nil }
  1001. func (s *emailBindUserRepoStub) EnableTotp(context.Context, int64) error { return nil }
  1002. func (s *emailBindUserRepoStub) DisableTotp(context.Context, int64) error { return nil }
  1003. func (s *emailBindUserRepoStub) GetByIDIncludeDeleted(ctx context.Context, id int64) (*service.User, error) {
  1004. return s.GetByID(ctx, id)
  1005. }
  1006. func cloneEmailBindUser(user *service.User) *service.User {
  1007. if user == nil {
  1008. return nil
  1009. }
  1010. cloned := *user
  1011. return &cloned
  1012. }