auth_service_identity_sync_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. //go:build unit
  2. package service_test
  3. import (
  4. "context"
  5. "database/sql"
  6. "errors"
  7. "testing"
  8. "time"
  9. dbent "github.com/Wei-Shaw/sub2api/ent"
  10. "github.com/Wei-Shaw/sub2api/ent/authidentity"
  11. "github.com/Wei-Shaw/sub2api/ent/enttest"
  12. "github.com/Wei-Shaw/sub2api/internal/config"
  13. "github.com/Wei-Shaw/sub2api/internal/repository"
  14. "github.com/Wei-Shaw/sub2api/internal/service"
  15. "github.com/stretchr/testify/require"
  16. "entgo.io/ent/dialect"
  17. entsql "entgo.io/ent/dialect/sql"
  18. _ "modernc.org/sqlite"
  19. )
  20. type authIdentityDefaultSubAssignerStub struct {
  21. calls []*service.AssignSubscriptionInput
  22. }
  23. func (s *authIdentityDefaultSubAssignerStub) AssignOrExtendSubscription(
  24. _ context.Context,
  25. input *service.AssignSubscriptionInput,
  26. ) (*service.UserSubscription, bool, error) {
  27. cloned := *input
  28. s.calls = append(s.calls, &cloned)
  29. return &service.UserSubscription{UserID: input.UserID, GroupID: input.GroupID}, true, nil
  30. }
  31. type flakyAuthIdentityDefaultSubAssignerStub struct {
  32. failuresRemaining int
  33. calls []*service.AssignSubscriptionInput
  34. }
  35. func (s *flakyAuthIdentityDefaultSubAssignerStub) AssignOrExtendSubscription(
  36. _ context.Context,
  37. input *service.AssignSubscriptionInput,
  38. ) (*service.UserSubscription, bool, error) {
  39. cloned := *input
  40. s.calls = append(s.calls, &cloned)
  41. if s.failuresRemaining > 0 {
  42. s.failuresRemaining--
  43. return nil, false, errors.New("temporary assign failure")
  44. }
  45. return &service.UserSubscription{UserID: input.UserID, GroupID: input.GroupID}, true, nil
  46. }
  47. type authIdentitySettingRepoStub struct {
  48. values map[string]string
  49. }
  50. func (s *authIdentitySettingRepoStub) Get(context.Context, string) (*service.Setting, error) {
  51. panic("unexpected Get call")
  52. }
  53. func (s *authIdentitySettingRepoStub) GetValue(_ context.Context, key string) (string, error) {
  54. if v, ok := s.values[key]; ok {
  55. return v, nil
  56. }
  57. return "", service.ErrSettingNotFound
  58. }
  59. func (s *authIdentitySettingRepoStub) Set(context.Context, string, string) error {
  60. panic("unexpected Set call")
  61. }
  62. func (s *authIdentitySettingRepoStub) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
  63. out := make(map[string]string, len(keys))
  64. for _, key := range keys {
  65. if v, ok := s.values[key]; ok {
  66. out[key] = v
  67. }
  68. }
  69. return out, nil
  70. }
  71. func (s *authIdentitySettingRepoStub) SetMultiple(context.Context, map[string]string) error {
  72. panic("unexpected SetMultiple call")
  73. }
  74. func (s *authIdentitySettingRepoStub) GetAll(context.Context) (map[string]string, error) {
  75. panic("unexpected GetAll call")
  76. }
  77. func (s *authIdentitySettingRepoStub) Delete(context.Context, string) error {
  78. panic("unexpected Delete call")
  79. }
  80. func newAuthServiceWithEnt(
  81. t *testing.T,
  82. settings map[string]string,
  83. defaultSubAssigner service.DefaultSubscriptionAssigner,
  84. ) (*service.AuthService, service.UserRepository, *dbent.Client) {
  85. t.Helper()
  86. db, err := sql.Open("sqlite", "file:auth_service_identity_sync?mode=memory&cache=shared")
  87. require.NoError(t, err)
  88. t.Cleanup(func() { _ = db.Close() })
  89. _, err = db.Exec("PRAGMA foreign_keys = ON")
  90. require.NoError(t, err)
  91. _, err = db.Exec(`
  92. CREATE TABLE IF NOT EXISTS user_provider_default_grants (
  93. id INTEGER PRIMARY KEY AUTOINCREMENT,
  94. user_id INTEGER NOT NULL,
  95. provider_type TEXT NOT NULL,
  96. grant_reason TEXT NOT NULL DEFAULT 'first_bind',
  97. created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  98. UNIQUE(user_id, provider_type, grant_reason)
  99. )`)
  100. require.NoError(t, err)
  101. drv := entsql.OpenDB(dialect.SQLite, db)
  102. client := enttest.NewClient(t, enttest.WithOptions(dbent.Driver(drv)))
  103. t.Cleanup(func() { _ = client.Close() })
  104. repo := repository.NewUserRepository(client, db)
  105. cfg := &config.Config{
  106. JWT: config.JWTConfig{
  107. Secret: "test-auth-identity-secret",
  108. ExpireHour: 1,
  109. },
  110. Default: config.DefaultConfig{
  111. UserBalance: 3.5,
  112. UserConcurrency: 2,
  113. },
  114. }
  115. settingSvc := service.NewSettingService(&authIdentitySettingRepoStub{
  116. values: settings,
  117. }, cfg)
  118. svc := service.NewAuthService(client, repo, nil, nil, cfg, settingSvc, nil, nil, nil, nil, defaultSubAssigner, nil, nil)
  119. return svc, repo, client
  120. }
  121. func TestAuthServiceRegisterDualWritesEmailIdentity(t *testing.T) {
  122. svc, _, client := newAuthServiceWithEnt(t, map[string]string{
  123. service.SettingKeyRegistrationEnabled: "true",
  124. }, nil)
  125. ctx := context.Background()
  126. token, user, err := svc.Register(ctx, "user@example.com", "password")
  127. require.NoError(t, err)
  128. require.NotEmpty(t, token)
  129. require.NotNil(t, user)
  130. storedUser, err := client.User.Get(ctx, user.ID)
  131. require.NoError(t, err)
  132. require.Equal(t, "email", storedUser.SignupSource)
  133. require.NotNil(t, storedUser.LastLoginAt)
  134. require.NotNil(t, storedUser.LastActiveAt)
  135. identity, err := client.AuthIdentity.Query().
  136. Where(
  137. authidentity.ProviderTypeEQ("email"),
  138. authidentity.ProviderKeyEQ("email"),
  139. authidentity.ProviderSubjectEQ("user@example.com"),
  140. ).
  141. Only(ctx)
  142. require.NoError(t, err)
  143. require.Equal(t, user.ID, identity.UserID)
  144. require.NotNil(t, identity.VerifiedAt)
  145. }
  146. func TestAuthServiceLoginDefersLastLoginTouchUntilRecordSuccessfulLogin(t *testing.T) {
  147. svc, _, client := newAuthServiceWithEnt(t, map[string]string{
  148. service.SettingKeyRegistrationEnabled: "true",
  149. }, nil)
  150. ctx := context.Background()
  151. passwordHash, err := svc.HashPassword("password")
  152. require.NoError(t, err)
  153. user, err := client.User.Create().
  154. SetEmail("login@example.com").
  155. SetPasswordHash(passwordHash).
  156. SetRole(service.RoleUser).
  157. SetStatus(service.StatusActive).
  158. SetBalance(1).
  159. SetConcurrency(1).
  160. Save(ctx)
  161. require.NoError(t, err)
  162. old := time.Now().Add(-2 * time.Hour).UTC().Round(time.Second)
  163. _, err = client.User.UpdateOneID(user.ID).
  164. SetLastLoginAt(old).
  165. SetLastActiveAt(old).
  166. Save(ctx)
  167. require.NoError(t, err)
  168. token, gotUser, err := svc.Login(ctx, user.Email, "password")
  169. require.NoError(t, err)
  170. require.NotEmpty(t, token)
  171. require.NotNil(t, gotUser)
  172. storedUser, err := client.User.Get(ctx, user.ID)
  173. require.NoError(t, err)
  174. require.NotNil(t, storedUser.LastLoginAt)
  175. require.NotNil(t, storedUser.LastActiveAt)
  176. require.True(t, storedUser.LastLoginAt.Equal(old))
  177. require.True(t, storedUser.LastActiveAt.Equal(old))
  178. identityCount, err := client.AuthIdentity.Query().
  179. Where(
  180. authidentity.ProviderTypeEQ("email"),
  181. authidentity.ProviderKeyEQ("email"),
  182. authidentity.ProviderSubjectEQ("login@example.com"),
  183. ).
  184. Count(ctx)
  185. require.NoError(t, err)
  186. require.Zero(t, identityCount)
  187. svc.RecordSuccessfulLogin(ctx, user.ID)
  188. identity, err := client.AuthIdentity.Query().
  189. Where(
  190. authidentity.ProviderTypeEQ("email"),
  191. authidentity.ProviderKeyEQ("email"),
  192. authidentity.ProviderSubjectEQ("login@example.com"),
  193. ).
  194. Only(ctx)
  195. require.NoError(t, err)
  196. require.Equal(t, user.ID, identity.UserID)
  197. }
  198. func TestAuthServiceRecordSuccessfulLoginBackfillsEmailIdentity(t *testing.T) {
  199. svc, repo, client := newAuthServiceWithEnt(t, map[string]string{
  200. service.SettingKeyRegistrationEnabled: "true",
  201. }, nil)
  202. ctx := context.Background()
  203. user := &service.User{
  204. Email: "record@example.com",
  205. Role: service.RoleUser,
  206. Status: service.StatusActive,
  207. Balance: 1,
  208. Concurrency: 1,
  209. }
  210. require.NoError(t, user.SetPassword("password"))
  211. require.NoError(t, repo.Create(ctx, user))
  212. svc.RecordSuccessfulLogin(ctx, user.ID)
  213. identity, err := client.AuthIdentity.Query().
  214. Where(
  215. authidentity.ProviderTypeEQ("email"),
  216. authidentity.ProviderKeyEQ("email"),
  217. authidentity.ProviderSubjectEQ("record@example.com"),
  218. ).
  219. Only(ctx)
  220. require.NoError(t, err)
  221. require.Equal(t, user.ID, identity.UserID)
  222. }
  223. func TestAuthServiceLogin_DoesNotApplyEmailFirstBindDefaultsWhenBackfillingLegacyEmailIdentity(t *testing.T) {
  224. assigner := &authIdentityDefaultSubAssignerStub{}
  225. svc, _, client := newAuthServiceWithEnt(t, map[string]string{
  226. service.SettingKeyRegistrationEnabled: "true",
  227. service.SettingKeyAuthSourceDefaultEmailBalance: "8.5",
  228. service.SettingKeyAuthSourceDefaultEmailConcurrency: "4",
  229. service.SettingKeyAuthSourceDefaultEmailSubscriptions: `[{"group_id":11,"validity_days":30}]`,
  230. service.SettingKeyAuthSourceDefaultEmailGrantOnFirstBind: "true",
  231. }, assigner)
  232. ctx := context.Background()
  233. passwordHash, err := svc.HashPassword("password")
  234. require.NoError(t, err)
  235. user, err := client.User.Create().
  236. SetEmail("legacy@example.com").
  237. SetUsername("legacy-user").
  238. SetPasswordHash(passwordHash).
  239. SetBalance(1.5).
  240. SetConcurrency(2).
  241. SetRole(service.RoleUser).
  242. SetStatus(service.StatusActive).
  243. Save(ctx)
  244. require.NoError(t, err)
  245. token, gotUser, err := svc.Login(ctx, user.Email, "password")
  246. require.NoError(t, err)
  247. require.NotEmpty(t, token)
  248. require.NotNil(t, gotUser)
  249. svc.RecordSuccessfulLogin(ctx, user.ID)
  250. storedUser, err := client.User.Get(ctx, user.ID)
  251. require.NoError(t, err)
  252. require.Equal(t, 1.5, storedUser.Balance)
  253. require.Equal(t, 2, storedUser.Concurrency)
  254. require.Empty(t, assigner.calls)
  255. identityCount, err := client.AuthIdentity.Query().
  256. Where(
  257. authidentity.ProviderTypeEQ("email"),
  258. authidentity.ProviderKeyEQ("email"),
  259. authidentity.ProviderSubjectEQ("legacy@example.com"),
  260. ).
  261. Count(ctx)
  262. require.NoError(t, err)
  263. require.Equal(t, 1, identityCount)
  264. require.Equal(t, 0, countProviderGrantRecords(t, client, user.ID, "email", "first_bind"))
  265. token, gotUser, err = svc.Login(ctx, user.Email, "password")
  266. require.NoError(t, err)
  267. require.NotEmpty(t, token)
  268. require.NotNil(t, gotUser)
  269. storedUser, err = client.User.Get(ctx, user.ID)
  270. require.NoError(t, err)
  271. require.Equal(t, 1.5, storedUser.Balance)
  272. require.Equal(t, 2, storedUser.Concurrency)
  273. require.Empty(t, assigner.calls)
  274. require.Equal(t, 0, countProviderGrantRecords(t, client, user.ID, "email", "first_bind"))
  275. }
  276. func TestAuthServiceLogin_DoesNotApplyMergedEmailFirstBindDefaultsWhenBackfillingLegacyEmailIdentity(t *testing.T) {
  277. assigner := &authIdentityDefaultSubAssignerStub{}
  278. svc, _, client := newAuthServiceWithEnt(t, map[string]string{
  279. service.SettingKeyRegistrationEnabled: "true",
  280. service.SettingKeyDefaultSubscriptions: `[{"group_id":21,"validity_days":14}]`,
  281. service.SettingKeyAuthSourceDefaultEmailBalance: "8.5",
  282. service.SettingKeyAuthSourceDefaultEmailConcurrency: "5",
  283. service.SettingKeyAuthSourceDefaultEmailSubscriptions: `[]`,
  284. service.SettingKeyAuthSourceDefaultEmailGrantOnFirstBind: "true",
  285. }, assigner)
  286. ctx := context.Background()
  287. passwordHash, err := svc.HashPassword("password")
  288. require.NoError(t, err)
  289. user, err := client.User.Create().
  290. SetEmail("merged-first-bind@example.com").
  291. SetUsername("merged-user").
  292. SetPasswordHash(passwordHash).
  293. SetBalance(1.5).
  294. SetConcurrency(2).
  295. SetRole(service.RoleUser).
  296. SetStatus(service.StatusActive).
  297. Save(ctx)
  298. require.NoError(t, err)
  299. token, gotUser, err := svc.Login(ctx, user.Email, "password")
  300. require.NoError(t, err)
  301. require.NotEmpty(t, token)
  302. require.NotNil(t, gotUser)
  303. svc.RecordSuccessfulLogin(ctx, user.ID)
  304. storedUser, err := client.User.Get(ctx, user.ID)
  305. require.NoError(t, err)
  306. require.Equal(t, 1.5, storedUser.Balance)
  307. require.Equal(t, 2, storedUser.Concurrency)
  308. require.Empty(t, assigner.calls)
  309. require.Equal(t, 0, countProviderGrantRecords(t, client, user.ID, "email", "first_bind"))
  310. }
  311. func TestAuthServiceLogin_DoesNotApplyEmailFirstBindDefaultsWhenIdentityAlreadyExists(t *testing.T) {
  312. assigner := &authIdentityDefaultSubAssignerStub{}
  313. svc, _, client := newAuthServiceWithEnt(t, map[string]string{
  314. service.SettingKeyRegistrationEnabled: "true",
  315. service.SettingKeyAuthSourceDefaultEmailBalance: "8.5",
  316. service.SettingKeyAuthSourceDefaultEmailConcurrency: "4",
  317. service.SettingKeyAuthSourceDefaultEmailSubscriptions: `[{"group_id":11,"validity_days":30}]`,
  318. service.SettingKeyAuthSourceDefaultEmailGrantOnFirstBind: "true",
  319. }, assigner)
  320. ctx := context.Background()
  321. passwordHash, err := svc.HashPassword("password")
  322. require.NoError(t, err)
  323. user, err := client.User.Create().
  324. SetEmail("bound@example.com").
  325. SetUsername("bound-user").
  326. SetPasswordHash(passwordHash).
  327. SetBalance(2).
  328. SetConcurrency(3).
  329. SetRole(service.RoleUser).
  330. SetStatus(service.StatusActive).
  331. Save(ctx)
  332. require.NoError(t, err)
  333. _, err = client.AuthIdentity.Create().
  334. SetUserID(user.ID).
  335. SetProviderType("email").
  336. SetProviderKey("email").
  337. SetProviderSubject("bound@example.com").
  338. SetVerifiedAt(time.Now().UTC()).
  339. SetMetadata(map[string]any{"source": "preexisting"}).
  340. Save(ctx)
  341. require.NoError(t, err)
  342. token, gotUser, err := svc.Login(ctx, user.Email, "password")
  343. require.NoError(t, err)
  344. require.NotEmpty(t, token)
  345. require.NotNil(t, gotUser)
  346. svc.RecordSuccessfulLogin(ctx, user.ID)
  347. storedUser, err := client.User.Get(ctx, user.ID)
  348. require.NoError(t, err)
  349. require.Equal(t, 2.0, storedUser.Balance)
  350. require.Equal(t, 3, storedUser.Concurrency)
  351. require.Empty(t, assigner.calls)
  352. require.Equal(t, 0, countProviderGrantRecords(t, client, user.ID, "email", "first_bind"))
  353. }
  354. func TestAuthServiceLogin_DoesNotRetryEmailFirstBindDefaultsForBackfilledEmailIdentity(t *testing.T) {
  355. assigner := &flakyAuthIdentityDefaultSubAssignerStub{failuresRemaining: 1}
  356. svc, _, client := newAuthServiceWithEnt(t, map[string]string{
  357. service.SettingKeyRegistrationEnabled: "true",
  358. service.SettingKeyAuthSourceDefaultEmailBalance: "8.5",
  359. service.SettingKeyAuthSourceDefaultEmailConcurrency: "4",
  360. service.SettingKeyAuthSourceDefaultEmailSubscriptions: `[{"group_id":11,"validity_days":30}]`,
  361. service.SettingKeyAuthSourceDefaultEmailGrantOnFirstBind: "true",
  362. }, assigner)
  363. ctx := context.Background()
  364. passwordHash, err := svc.HashPassword("password")
  365. require.NoError(t, err)
  366. user, err := client.User.Create().
  367. SetEmail("retry-first-bind@example.com").
  368. SetUsername("retry-user").
  369. SetPasswordHash(passwordHash).
  370. SetBalance(1.5).
  371. SetConcurrency(2).
  372. SetRole(service.RoleUser).
  373. SetStatus(service.StatusActive).
  374. Save(ctx)
  375. require.NoError(t, err)
  376. token, gotUser, err := svc.Login(ctx, user.Email, "password")
  377. require.NoError(t, err)
  378. require.NotEmpty(t, token)
  379. require.NotNil(t, gotUser)
  380. svc.RecordSuccessfulLogin(ctx, user.ID)
  381. storedUser, err := client.User.Get(ctx, user.ID)
  382. require.NoError(t, err)
  383. require.Equal(t, 1.5, storedUser.Balance)
  384. require.Equal(t, 2, storedUser.Concurrency)
  385. require.Empty(t, assigner.calls)
  386. require.Equal(t, 0, countProviderGrantRecords(t, client, user.ID, "email", "first_bind"))
  387. token, gotUser, err = svc.Login(ctx, user.Email, "password")
  388. require.NoError(t, err)
  389. require.NotEmpty(t, token)
  390. require.NotNil(t, gotUser)
  391. svc.RecordSuccessfulLogin(ctx, user.ID)
  392. storedUser, err = client.User.Get(ctx, user.ID)
  393. require.NoError(t, err)
  394. require.Equal(t, 1.5, storedUser.Balance)
  395. require.Equal(t, 2, storedUser.Concurrency)
  396. require.Empty(t, assigner.calls)
  397. require.Equal(t, 0, countProviderGrantRecords(t, client, user.ID, "email", "first_bind"))
  398. }
  399. func countProviderGrantRecords(
  400. t *testing.T,
  401. client *dbent.Client,
  402. userID int64,
  403. providerType string,
  404. grantReason string,
  405. ) int {
  406. t.Helper()
  407. var count int
  408. rows, err := client.QueryContext(
  409. context.Background(),
  410. `SELECT COUNT(*) FROM user_provider_default_grants WHERE user_id = ? AND provider_type = ? AND grant_reason = ?`,
  411. userID,
  412. providerType,
  413. grantReason,
  414. )
  415. require.NoError(t, err)
  416. defer rows.Close()
  417. require.True(t, rows.Next())
  418. require.NoError(t, rows.Scan(&count))
  419. require.NoError(t, rows.Err())
  420. return count
  421. }