auth_service_register_test.go 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "errors"
  6. "sync"
  7. "testing"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/config"
  10. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  11. "github.com/stretchr/testify/require"
  12. )
  13. type settingRepoStub struct {
  14. mu sync.Mutex
  15. values map[string]string
  16. err error
  17. getValueCalls int
  18. getMultipleCalls int
  19. }
  20. func (s *settingRepoStub) Get(ctx context.Context, key string) (*Setting, error) {
  21. panic("unexpected Get call")
  22. }
  23. func (s *settingRepoStub) GetValue(ctx context.Context, key string) (string, error) {
  24. s.mu.Lock()
  25. defer s.mu.Unlock()
  26. s.getValueCalls++
  27. if s.err != nil {
  28. return "", s.err
  29. }
  30. if v, ok := s.values[key]; ok {
  31. return v, nil
  32. }
  33. return "", ErrSettingNotFound
  34. }
  35. func (s *settingRepoStub) Set(ctx context.Context, key, value string) error {
  36. panic("unexpected Set call")
  37. }
  38. func (s *settingRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
  39. s.mu.Lock()
  40. defer s.mu.Unlock()
  41. s.getMultipleCalls++
  42. if s.err != nil {
  43. return nil, s.err
  44. }
  45. result := make(map[string]string, len(keys))
  46. for _, key := range keys {
  47. if v, ok := s.values[key]; ok {
  48. result[key] = v
  49. }
  50. }
  51. return result, nil
  52. }
  53. func (s *settingRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error {
  54. panic("unexpected SetMultiple call")
  55. }
  56. func (s *settingRepoStub) GetAll(ctx context.Context) (map[string]string, error) {
  57. panic("unexpected GetAll call")
  58. }
  59. func (s *settingRepoStub) Delete(ctx context.Context, key string) error {
  60. panic("unexpected Delete call")
  61. }
  62. type emailCacheStub struct {
  63. data *VerificationCodeData
  64. err error
  65. }
  66. type defaultSubscriptionAssignerStub struct {
  67. calls []AssignSubscriptionInput
  68. err error
  69. }
  70. type refreshTokenCacheStub struct{}
  71. type userPlatformQuotaRepoStub struct {
  72. bulkInsertCalls [][]UserPlatformQuotaRecord
  73. bulkInsertErr error
  74. }
  75. func (s *userPlatformQuotaRepoStub) BulkInsertInitial(_ context.Context, records []UserPlatformQuotaRecord) error {
  76. cloned := make([]UserPlatformQuotaRecord, len(records))
  77. copy(cloned, records)
  78. s.bulkInsertCalls = append(s.bulkInsertCalls, cloned)
  79. return s.bulkInsertErr
  80. }
  81. func (s *userPlatformQuotaRepoStub) GetByUserPlatform(context.Context, int64, string) (*UserPlatformQuotaRecord, error) {
  82. panic("unexpected GetByUserPlatform call")
  83. }
  84. func (s *userPlatformQuotaRepoStub) ListByUser(context.Context, int64) ([]UserPlatformQuotaRecord, error) {
  85. panic("unexpected ListByUser call")
  86. }
  87. func (s *userPlatformQuotaRepoStub) IncrementUsageWithReset(context.Context, int64, string, float64, time.Time) error {
  88. panic("unexpected IncrementUsageWithReset call")
  89. }
  90. func (s *userPlatformQuotaRepoStub) UpsertForUser(context.Context, int64, []UserPlatformQuotaRecord) error {
  91. panic("unexpected UpsertForUser call")
  92. }
  93. func (s *userPlatformQuotaRepoStub) ResetExpiredWindow(context.Context, int64, string, string, time.Time) error {
  94. panic("unexpected ResetExpiredWindow call")
  95. }
  96. func (s *userPlatformQuotaRepoStub) BatchSnapshotUsage(_ context.Context, _ []UserPlatformQuotaSnapshot, _ time.Time) error {
  97. return nil
  98. }
  99. func (s *defaultSubscriptionAssignerStub) AssignOrExtendSubscription(_ context.Context, input *AssignSubscriptionInput) (*UserSubscription, bool, error) {
  100. if input != nil {
  101. s.calls = append(s.calls, *input)
  102. }
  103. if s.err != nil {
  104. return nil, false, s.err
  105. }
  106. return &UserSubscription{UserID: input.UserID, GroupID: input.GroupID}, false, nil
  107. }
  108. func (s *refreshTokenCacheStub) StoreRefreshToken(context.Context, string, *RefreshTokenData, time.Duration) error {
  109. return nil
  110. }
  111. func (s *refreshTokenCacheStub) GetRefreshToken(context.Context, string) (*RefreshTokenData, error) {
  112. return nil, ErrRefreshTokenNotFound
  113. }
  114. func (s *refreshTokenCacheStub) DeleteRefreshToken(context.Context, string) error {
  115. return nil
  116. }
  117. func (s *refreshTokenCacheStub) DeleteUserRefreshTokens(context.Context, int64) error {
  118. return nil
  119. }
  120. func (s *refreshTokenCacheStub) DeleteTokenFamily(context.Context, string) error {
  121. return nil
  122. }
  123. func (s *refreshTokenCacheStub) AddToUserTokenSet(context.Context, int64, string, time.Duration) error {
  124. return nil
  125. }
  126. func (s *refreshTokenCacheStub) AddToFamilyTokenSet(context.Context, string, string, time.Duration) error {
  127. return nil
  128. }
  129. func (s *refreshTokenCacheStub) GetUserTokenHashes(context.Context, int64) ([]string, error) {
  130. return nil, nil
  131. }
  132. func (s *refreshTokenCacheStub) GetFamilyTokenHashes(context.Context, string) ([]string, error) {
  133. return nil, nil
  134. }
  135. func (s *refreshTokenCacheStub) IsTokenInFamily(context.Context, string, string) (bool, error) {
  136. return false, nil
  137. }
  138. func (s *emailCacheStub) GetVerificationCode(ctx context.Context, email string) (*VerificationCodeData, error) {
  139. if s.err != nil {
  140. return nil, s.err
  141. }
  142. return s.data, nil
  143. }
  144. func (s *emailCacheStub) SetVerificationCode(ctx context.Context, email string, data *VerificationCodeData, ttl time.Duration) error {
  145. return nil
  146. }
  147. func (s *emailCacheStub) DeleteVerificationCode(ctx context.Context, email string) error {
  148. return nil
  149. }
  150. func (s *emailCacheStub) GetNotifyVerifyCode(ctx context.Context, email string) (*VerificationCodeData, error) {
  151. return nil, nil
  152. }
  153. func (s *emailCacheStub) SetNotifyVerifyCode(ctx context.Context, email string, data *VerificationCodeData, ttl time.Duration) error {
  154. return nil
  155. }
  156. func (s *emailCacheStub) DeleteNotifyVerifyCode(ctx context.Context, email string) error {
  157. return nil
  158. }
  159. func (s *emailCacheStub) GetPasswordResetToken(ctx context.Context, email string) (*PasswordResetTokenData, error) {
  160. return nil, nil
  161. }
  162. func (s *emailCacheStub) SetPasswordResetToken(ctx context.Context, email string, data *PasswordResetTokenData, ttl time.Duration) error {
  163. return nil
  164. }
  165. func (s *emailCacheStub) DeletePasswordResetToken(ctx context.Context, email string) error {
  166. return nil
  167. }
  168. func (s *emailCacheStub) IsPasswordResetEmailInCooldown(ctx context.Context, email string) bool {
  169. return false
  170. }
  171. func (s *emailCacheStub) SetPasswordResetEmailCooldown(ctx context.Context, email string, ttl time.Duration) error {
  172. return nil
  173. }
  174. func (s *emailCacheStub) GetNotifyCodeUserRate(ctx context.Context, userID int64) (int64, error) {
  175. return 0, nil
  176. }
  177. func (s *emailCacheStub) IncrNotifyCodeUserRate(ctx context.Context, userID int64, window time.Duration) (int64, error) {
  178. return 0, nil
  179. }
  180. func newAuthService(repo *userRepoStub, settings map[string]string, emailCache EmailCache, quotaRepo UserPlatformQuotaRepository) *AuthService {
  181. cfg := &config.Config{
  182. JWT: config.JWTConfig{
  183. Secret: "test-secret",
  184. ExpireHour: 1,
  185. },
  186. Default: config.DefaultConfig{
  187. UserBalance: 3.5,
  188. UserConcurrency: 2,
  189. },
  190. }
  191. var settingService *SettingService
  192. if settings != nil {
  193. settingService = NewSettingService(&settingRepoStub{values: settings}, cfg)
  194. }
  195. var emailService *EmailService
  196. if emailCache != nil {
  197. emailService = NewEmailService(&settingRepoStub{values: settings}, emailCache)
  198. }
  199. return NewAuthService(
  200. nil, // entClient
  201. repo,
  202. nil, // redeemRepo
  203. nil, // refreshTokenCache
  204. cfg,
  205. settingService,
  206. emailService,
  207. nil,
  208. nil,
  209. nil, // promoService
  210. nil, // defaultSubAssigner
  211. nil, // affiliateService
  212. quotaRepo,
  213. )
  214. }
  215. func TestAuthService_Register_Disabled(t *testing.T) {
  216. repo := &userRepoStub{}
  217. service := newAuthService(repo, map[string]string{
  218. SettingKeyRegistrationEnabled: "false",
  219. }, nil, nil)
  220. _, _, err := service.Register(context.Background(), "user@test.com", "password")
  221. require.ErrorIs(t, err, ErrRegDisabled)
  222. }
  223. func TestAuthService_Register_DisabledByDefault(t *testing.T) {
  224. // 当 settings 为 nil(设置项不存在)时,注册应该默认关闭
  225. repo := &userRepoStub{}
  226. service := newAuthService(repo, nil, nil, nil)
  227. _, _, err := service.Register(context.Background(), "user@test.com", "password")
  228. require.ErrorIs(t, err, ErrRegDisabled)
  229. }
  230. func TestAuthService_Register_SnapshotsPlatformQuotaDefaults(t *testing.T) {
  231. repo := &userRepoStub{nextID: 77}
  232. quotaRepo := &userPlatformQuotaRepoStub{}
  233. service := newAuthService(repo, map[string]string{
  234. SettingKeyRegistrationEnabled: "true",
  235. SettingKeyDefaultPlatformQuotas: `{"openai": {"weekly": 12.34}}`,
  236. }, nil, quotaRepo)
  237. _, user, err := service.Register(context.Background(), "newuser@test.com", "password")
  238. require.NoError(t, err)
  239. require.NotNil(t, user)
  240. require.Len(t, quotaRepo.bulkInsertCalls, 1)
  241. records := quotaRepo.bulkInsertCalls[0]
  242. var openaiRecord *UserPlatformQuotaRecord
  243. for i := range records {
  244. if records[i].Platform == "openai" {
  245. openaiRecord = &records[i]
  246. break
  247. }
  248. }
  249. require.NotNil(t, openaiRecord, "expected openai platform record")
  250. require.Equal(t, int64(77), openaiRecord.UserID)
  251. require.NotNil(t, openaiRecord.WeeklyLimitUSD)
  252. require.InDelta(t, 12.34, *openaiRecord.WeeklyLimitUSD, 0.0001)
  253. }
  254. func TestAuthService_Register_DoesNotSnapshotOnDisabled(t *testing.T) {
  255. repo := &userRepoStub{}
  256. quotaRepo := &userPlatformQuotaRepoStub{}
  257. service := newAuthService(repo, map[string]string{
  258. SettingKeyRegistrationEnabled: "false",
  259. }, nil, quotaRepo)
  260. _, _, err := service.Register(context.Background(), "user@test.com", "password")
  261. require.ErrorIs(t, err, ErrRegDisabled)
  262. require.Empty(t, quotaRepo.bulkInsertCalls, "registration rejected before user creation must not snapshot")
  263. }
  264. func TestAuthService_Register_EmailVerifyEnabledButServiceNotConfigured(t *testing.T) {
  265. repo := &userRepoStub{}
  266. // 邮件验证开启但 emailCache 为 nil(emailService 未配置)
  267. service := newAuthService(repo, map[string]string{
  268. SettingKeyRegistrationEnabled: "true",
  269. SettingKeyEmailVerifyEnabled: "true",
  270. }, nil, nil)
  271. // 应返回服务不可用错误,而不是允许绕过验证
  272. _, _, err := service.RegisterWithVerification(context.Background(), "user@test.com", "password", "any-code", "", "", "")
  273. require.ErrorIs(t, err, ErrServiceUnavailable)
  274. }
  275. func TestAuthService_Register_EmailVerifyRequired(t *testing.T) {
  276. repo := &userRepoStub{}
  277. cache := &emailCacheStub{} // 配置 emailService
  278. service := newAuthService(repo, map[string]string{
  279. SettingKeyRegistrationEnabled: "true",
  280. SettingKeyEmailVerifyEnabled: "true",
  281. }, cache, nil)
  282. _, _, err := service.RegisterWithVerification(context.Background(), "user@test.com", "password", "", "", "", "")
  283. require.ErrorIs(t, err, ErrEmailVerifyRequired)
  284. }
  285. func TestAuthService_Register_EmailVerifyInvalid(t *testing.T) {
  286. repo := &userRepoStub{}
  287. cache := &emailCacheStub{
  288. data: &VerificationCodeData{Code: "expected", Attempts: 0},
  289. }
  290. service := newAuthService(repo, map[string]string{
  291. SettingKeyRegistrationEnabled: "true",
  292. SettingKeyEmailVerifyEnabled: "true",
  293. }, cache, nil)
  294. _, _, err := service.RegisterWithVerification(context.Background(), "user@test.com", "password", "wrong", "", "", "")
  295. require.ErrorIs(t, err, ErrInvalidVerifyCode)
  296. require.ErrorContains(t, err, "verify code")
  297. }
  298. func TestAuthService_Register_EmailExists(t *testing.T) {
  299. repo := &userRepoStub{exists: true}
  300. service := newAuthService(repo, map[string]string{
  301. SettingKeyRegistrationEnabled: "true",
  302. }, nil, nil)
  303. _, _, err := service.Register(context.Background(), "user@test.com", "password")
  304. require.ErrorIs(t, err, ErrEmailExists)
  305. }
  306. func TestAuthService_Register_AliasDuplicateRejected(t *testing.T) {
  307. repo := &userRepoStub{aliasExists: true}
  308. service := newAuthService(repo, map[string]string{
  309. SettingKeyRegistrationEnabled: "true",
  310. }, nil, nil)
  311. _, _, err := service.Register(context.Background(), "some.one+bulk294@gmail.com", "password")
  312. require.ErrorIs(t, err, ErrEmailExists)
  313. require.Empty(t, repo.created)
  314. }
  315. func TestAuthService_Register_UsesAliasGuardedCreate(t *testing.T) {
  316. // 注册必须走带别名兜底的创建路径:服务层前置查重与写入之间存在竞态窗口。
  317. repo := &userRepoStub{nextID: 91}
  318. service := newAuthService(repo, map[string]string{
  319. SettingKeyRegistrationEnabled: "true",
  320. }, nil, nil)
  321. _, user, err := service.Register(context.Background(), "newuser@gmail.com", "password")
  322. require.NoError(t, err)
  323. require.NotNil(t, user)
  324. require.Equal(t, 1, repo.guardedCreates)
  325. }
  326. func TestAuthService_Register_CheckEmailError(t *testing.T) {
  327. repo := &userRepoStub{existsErr: errors.New("db down")}
  328. service := newAuthService(repo, map[string]string{
  329. SettingKeyRegistrationEnabled: "true",
  330. }, nil, nil)
  331. _, _, err := service.Register(context.Background(), "user@test.com", "password")
  332. require.ErrorIs(t, err, ErrServiceUnavailable)
  333. }
  334. func TestAuthService_Register_ReservedEmail(t *testing.T) {
  335. repo := &userRepoStub{}
  336. service := newAuthService(repo, map[string]string{
  337. SettingKeyRegistrationEnabled: "true",
  338. }, nil, nil)
  339. _, _, err := service.Register(context.Background(), "linuxdo-123@linuxdo-connect.invalid", "password")
  340. require.ErrorIs(t, err, ErrEmailReserved)
  341. }
  342. func TestAuthService_Register_EmailSuffixNotAllowed(t *testing.T) {
  343. repo := &userRepoStub{domainCounts: map[string]int{"other.com": 1}}
  344. service := newAuthService(repo, map[string]string{
  345. SettingKeyRegistrationEnabled: "true",
  346. SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com","@company.com"]`,
  347. SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
  348. }, nil, nil)
  349. _, _, err := service.Register(context.Background(), "user@other.com", "password")
  350. require.ErrorIs(t, err, ErrEmailDomainRegistrationLimit)
  351. appErr := infraerrors.FromError(err)
  352. require.Equal(t, "EMAIL_DOMAIN_REGISTRATION_LIMIT", appErr.Reason)
  353. require.Contains(t, appErr.Message, "mainstream email")
  354. }
  355. func TestAuthService_Register_NonWhitelistDomainAllowsFirstAccount(t *testing.T) {
  356. repo := &userRepoStub{nextID: 9, domainCounts: map[string]int{"custom.example": 0}}
  357. svc := newAuthService(repo, map[string]string{
  358. SettingKeyRegistrationEnabled: "true",
  359. SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
  360. SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
  361. }, nil, nil)
  362. _, user, err := svc.Register(context.Background(), "first@custom.example", "password")
  363. require.NoError(t, err)
  364. require.Equal(t, int64(9), user.ID)
  365. }
  366. func TestAuthService_Register_NonWhitelistDomainRejectsSecondAccount(t *testing.T) {
  367. repo := &userRepoStub{domainCounts: map[string]int{"custom.example": 1}}
  368. svc := newAuthService(repo, map[string]string{
  369. SettingKeyRegistrationEnabled: "true",
  370. SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
  371. SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
  372. }, nil, nil)
  373. _, _, err := svc.Register(context.Background(), "second@sub.custom.example", "password")
  374. require.ErrorIs(t, err, ErrEmailDomainRegistrationLimit)
  375. }
  376. // 域名限量注册开关默认关闭:白名单外域名保持 PR5423 之前的严格拒绝语义,
  377. // 即使该域名下还没有任何账户也不放行。
  378. func TestAuthService_Register_NonWhitelistDomainRejectedWhenQuotaDisabledByDefault(t *testing.T) {
  379. repo := &userRepoStub{nextID: 9, domainCounts: map[string]int{"custom.example": 0}}
  380. svc := newAuthService(repo, map[string]string{
  381. SettingKeyRegistrationEnabled: "true",
  382. SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
  383. }, nil, nil)
  384. _, _, err := svc.Register(context.Background(), "first@custom.example", "password")
  385. require.ErrorIs(t, err, ErrEmailSuffixNotAllowed)
  386. appErr := infraerrors.FromError(err)
  387. require.Equal(t, "EMAIL_SUFFIX_NOT_ALLOWED", appErr.Reason)
  388. require.Empty(t, repo.created)
  389. require.Zero(t, repo.domainLimitedCreates)
  390. }
  391. func TestAuthService_Register_NonWhitelistDomainRejectedWhenQuotaExplicitlyDisabled(t *testing.T) {
  392. repo := &userRepoStub{nextID: 9, domainCounts: map[string]int{"custom.example": 0}}
  393. svc := newAuthService(repo, map[string]string{
  394. SettingKeyRegistrationEnabled: "true",
  395. SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
  396. SettingKeyRegistrationEmailDomainQuotaEnabled: "false",
  397. }, nil, nil)
  398. _, _, err := svc.Register(context.Background(), "first@custom.example", "password")
  399. require.ErrorIs(t, err, ErrEmailSuffixNotAllowed)
  400. require.Empty(t, repo.created)
  401. }
  402. // 开关关闭不影响白名单命中域名的正常注册。
  403. func TestAuthService_Register_WhitelistDomainAllowedWhenQuotaDisabled(t *testing.T) {
  404. repo := &userRepoStub{nextID: 12}
  405. svc := newAuthService(repo, map[string]string{
  406. SettingKeyRegistrationEnabled: "true",
  407. SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
  408. }, nil, nil)
  409. _, user, err := svc.Register(context.Background(), "user@example.com", "password")
  410. require.NoError(t, err)
  411. require.Equal(t, int64(12), user.ID)
  412. require.Zero(t, repo.domainLimitedCreates)
  413. }
  414. func TestAuthService_SendVerifyCode_NonWhitelistDomainRejectedWhenQuotaDisabled(t *testing.T) {
  415. repo := &userRepoStub{domainCounts: map[string]int{"custom.example": 0}}
  416. svc := newAuthService(repo, map[string]string{
  417. SettingKeyRegistrationEnabled: "true",
  418. SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
  419. }, nil, nil)
  420. err := svc.SendVerifyCode(context.Background(), "user@custom.example")
  421. require.ErrorIs(t, err, ErrEmailSuffixNotAllowed)
  422. }
  423. func TestAuthService_SendVerifyCodeAsync_NonWhitelistDomainRejectedWhenQuotaDisabled(t *testing.T) {
  424. repo := &userRepoStub{domainCounts: map[string]int{"custom.example": 0}}
  425. svc := newAuthService(repo, map[string]string{
  426. SettingKeyRegistrationEnabled: "true",
  427. SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
  428. }, nil, nil)
  429. _, err := svc.SendVerifyCodeAsync(context.Background(), "user@custom.example")
  430. require.ErrorIs(t, err, ErrEmailSuffixNotAllowed)
  431. }
  432. func TestAuthService_Register_EmptyWhitelistAllowsAllDomains(t *testing.T) {
  433. repo := &userRepoStub{nextID: 10}
  434. svc := newAuthService(repo, map[string]string{
  435. SettingKeyRegistrationEnabled: "true",
  436. SettingKeyRegistrationEmailSuffixWhitelist: `[]`,
  437. }, nil, nil)
  438. _, _, err := svc.Register(context.Background(), "any@custom.example", "password")
  439. require.NoError(t, err)
  440. }
  441. func TestAuthService_Register_EmailSuffixAllowed(t *testing.T) {
  442. repo := &userRepoStub{nextID: 8}
  443. service := newAuthService(repo, map[string]string{
  444. SettingKeyRegistrationEnabled: "true",
  445. SettingKeyRegistrationEmailSuffixWhitelist: `["example.com"]`,
  446. }, nil, nil)
  447. _, user, err := service.Register(context.Background(), "user@example.com", "password")
  448. require.NoError(t, err)
  449. require.NotNil(t, user)
  450. require.Equal(t, int64(8), user.ID)
  451. }
  452. func TestAuthService_SendVerifyCode_EmailSuffixNotAllowed(t *testing.T) {
  453. repo := &userRepoStub{domainCounts: map[string]int{"other.com": 1}}
  454. service := newAuthService(repo, map[string]string{
  455. SettingKeyRegistrationEnabled: "true",
  456. SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com","@company.com"]`,
  457. SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
  458. }, nil, nil)
  459. err := service.SendVerifyCode(context.Background(), "user@other.com")
  460. require.ErrorIs(t, err, ErrEmailDomainRegistrationLimit)
  461. appErr := infraerrors.FromError(err)
  462. require.Equal(t, "EMAIL_DOMAIN_REGISTRATION_LIMIT", appErr.Reason)
  463. }
  464. func TestAuthService_SendVerifyCode_NonWhitelistDomainLimit(t *testing.T) {
  465. repo := &userRepoStub{domainCounts: map[string]int{"custom.example": 1}}
  466. svc := newAuthService(repo, map[string]string{
  467. SettingKeyRegistrationEnabled: "true",
  468. SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
  469. SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
  470. }, nil, nil)
  471. err := svc.SendVerifyCode(context.Background(), "user@custom.example")
  472. require.ErrorIs(t, err, ErrEmailDomainRegistrationLimit)
  473. }
  474. func TestAuthService_SendVerifyCodeAsync_NonWhitelistDomainLimit(t *testing.T) {
  475. repo := &userRepoStub{domainCounts: map[string]int{"custom.example": 1}}
  476. svc := newAuthService(repo, map[string]string{
  477. SettingKeyRegistrationEnabled: "true",
  478. SettingKeyRegistrationEmailSuffixWhitelist: `["@example.com"]`,
  479. SettingKeyRegistrationEmailDomainQuotaEnabled: "true",
  480. }, nil, nil)
  481. _, err := svc.SendVerifyCodeAsync(context.Background(), "user@custom.example")
  482. require.ErrorIs(t, err, ErrEmailDomainRegistrationLimit)
  483. }
  484. func TestAuthService_Register_CreateError(t *testing.T) {
  485. repo := &userRepoStub{createErr: errors.New("create failed")}
  486. service := newAuthService(repo, map[string]string{
  487. SettingKeyRegistrationEnabled: "true",
  488. }, nil, nil)
  489. _, _, err := service.Register(context.Background(), "user@test.com", "password")
  490. require.ErrorIs(t, err, ErrServiceUnavailable)
  491. }
  492. func TestAuthService_Register_CreateEmailExistsRace(t *testing.T) {
  493. // 模拟竞态条件:ExistsByEmail 返回 false,但 Create 时因唯一约束失败
  494. repo := &userRepoStub{createErr: ErrEmailExists}
  495. service := newAuthService(repo, map[string]string{
  496. SettingKeyRegistrationEnabled: "true",
  497. }, nil, nil)
  498. _, _, err := service.Register(context.Background(), "user@test.com", "password")
  499. require.ErrorIs(t, err, ErrEmailExists)
  500. }
  501. func TestAuthService_Register_Success(t *testing.T) {
  502. repo := &userRepoStub{nextID: 5}
  503. service := newAuthService(repo, map[string]string{
  504. SettingKeyRegistrationEnabled: "true",
  505. SettingKeyAuthSourceDefaultEmailGrantOnSignup: "false",
  506. }, nil, nil)
  507. token, user, err := service.Register(context.Background(), "user@test.com", "password")
  508. require.NoError(t, err)
  509. require.NotEmpty(t, token)
  510. require.NotNil(t, user)
  511. require.Equal(t, int64(5), user.ID)
  512. require.Equal(t, "user@test.com", user.Email)
  513. require.Equal(t, RoleUser, user.Role)
  514. require.Equal(t, StatusActive, user.Status)
  515. require.Equal(t, 3.5, user.Balance)
  516. require.Equal(t, 2, user.Concurrency)
  517. require.Len(t, repo.created, 1)
  518. require.True(t, user.CheckPassword("password"))
  519. }
  520. func TestAuthService_ValidateToken_ExpiredReturnsClaimsWithError(t *testing.T) {
  521. repo := &userRepoStub{}
  522. service := newAuthService(repo, nil, nil, nil)
  523. // 创建用户并生成 token
  524. user := &User{
  525. ID: 1,
  526. Email: "test@test.com",
  527. Role: RoleUser,
  528. Status: StatusActive,
  529. TokenVersion: 1,
  530. }
  531. token, err := service.GenerateToken(context.Background(), user)
  532. require.NoError(t, err)
  533. // 验证有效 token
  534. claims, err := service.ValidateToken(token)
  535. require.NoError(t, err)
  536. require.NotNil(t, claims)
  537. require.Equal(t, int64(1), claims.UserID)
  538. // 模拟过期 token(通过创建一个过期很久的 token)
  539. service.cfg.JWT.ExpireHour = -1 // 设置为负数使 token 立即过期
  540. expiredToken, err := service.GenerateToken(context.Background(), user)
  541. require.NoError(t, err)
  542. service.cfg.JWT.ExpireHour = 1 // 恢复
  543. // 验证过期 token 应返回 claims 和 ErrTokenExpired
  544. claims, err = service.ValidateToken(expiredToken)
  545. require.ErrorIs(t, err, ErrTokenExpired)
  546. require.NotNil(t, claims, "claims should not be nil when token is expired")
  547. require.Equal(t, int64(1), claims.UserID)
  548. require.Equal(t, "test@test.com", claims.Email)
  549. }
  550. func TestAuthService_RefreshToken_ExpiredTokenNoPanic(t *testing.T) {
  551. user := &User{
  552. ID: 1,
  553. Email: "test@test.com",
  554. Role: RoleUser,
  555. Status: StatusActive,
  556. TokenVersion: 1,
  557. }
  558. repo := &userRepoStub{user: user}
  559. service := newAuthService(repo, nil, nil, nil)
  560. // 创建过期 token
  561. service.cfg.JWT.ExpireHour = -1
  562. expiredToken, err := service.GenerateToken(context.Background(), user)
  563. require.NoError(t, err)
  564. service.cfg.JWT.ExpireHour = 1
  565. // RefreshToken 使用过期 token 不应 panic
  566. require.NotPanics(t, func() {
  567. newToken, err := service.RefreshToken(context.Background(), expiredToken)
  568. require.NoError(t, err)
  569. require.NotEmpty(t, newToken)
  570. })
  571. }
  572. func TestAuthService_GetAccessTokenExpiresIn_FallbackToExpireHour(t *testing.T) {
  573. service := newAuthService(&userRepoStub{}, nil, nil, nil)
  574. service.cfg.JWT.ExpireHour = 24
  575. service.cfg.JWT.AccessTokenExpireMinutes = 0
  576. require.Equal(t, 24*3600, service.GetAccessTokenExpiresIn())
  577. }
  578. func TestAuthService_GetAccessTokenExpiresIn_MinutesHasPriority(t *testing.T) {
  579. service := newAuthService(&userRepoStub{}, nil, nil, nil)
  580. service.cfg.JWT.ExpireHour = 24
  581. service.cfg.JWT.AccessTokenExpireMinutes = 90
  582. require.Equal(t, 90*60, service.GetAccessTokenExpiresIn())
  583. }
  584. func TestAuthService_GenerateToken_UsesExpireHourWhenMinutesZero(t *testing.T) {
  585. service := newAuthService(&userRepoStub{}, nil, nil, nil)
  586. service.cfg.JWT.ExpireHour = 24
  587. service.cfg.JWT.AccessTokenExpireMinutes = 0
  588. user := &User{
  589. ID: 1,
  590. Email: "test@test.com",
  591. Role: RoleUser,
  592. Status: StatusActive,
  593. TokenVersion: 1,
  594. }
  595. token, err := service.GenerateToken(context.Background(), user)
  596. require.NoError(t, err)
  597. claims, err := service.ValidateToken(token)
  598. require.NoError(t, err)
  599. require.NotNil(t, claims)
  600. require.NotNil(t, claims.IssuedAt)
  601. require.NotNil(t, claims.ExpiresAt)
  602. require.WithinDuration(t, claims.IssuedAt.Time.Add(24*time.Hour), claims.ExpiresAt.Time, 2*time.Second)
  603. }
  604. func TestAuthService_GenerateToken_UsesMinutesWhenConfigured(t *testing.T) {
  605. service := newAuthService(&userRepoStub{}, nil, nil, nil)
  606. service.cfg.JWT.ExpireHour = 24
  607. service.cfg.JWT.AccessTokenExpireMinutes = 90
  608. user := &User{
  609. ID: 2,
  610. Email: "test2@test.com",
  611. Role: RoleUser,
  612. Status: StatusActive,
  613. TokenVersion: 1,
  614. }
  615. token, err := service.GenerateToken(context.Background(), user)
  616. require.NoError(t, err)
  617. claims, err := service.ValidateToken(token)
  618. require.NoError(t, err)
  619. require.NotNil(t, claims)
  620. require.NotNil(t, claims.IssuedAt)
  621. require.NotNil(t, claims.ExpiresAt)
  622. require.WithinDuration(t, claims.IssuedAt.Time.Add(90*time.Minute), claims.ExpiresAt.Time, 2*time.Second)
  623. }
  624. func TestAuthService_Register_AssignsDefaultSubscriptions(t *testing.T) {
  625. repo := &userRepoStub{nextID: 42}
  626. assigner := &defaultSubscriptionAssignerStub{}
  627. service := newAuthService(repo, map[string]string{
  628. SettingKeyRegistrationEnabled: "true",
  629. SettingKeyDefaultSubscriptions: `[{"group_id":11,"validity_days":30},{"group_id":12,"validity_days":7}]`,
  630. SettingKeyAuthSourceDefaultEmailGrantOnSignup: "false",
  631. }, nil, nil)
  632. service.defaultSubAssigner = assigner
  633. _, user, err := service.Register(context.Background(), "default-sub@test.com", "password")
  634. require.NoError(t, err)
  635. require.NotNil(t, user)
  636. require.Len(t, assigner.calls, 2)
  637. require.Equal(t, int64(42), assigner.calls[0].UserID)
  638. require.Equal(t, int64(11), assigner.calls[0].GroupID)
  639. require.Equal(t, 30, assigner.calls[0].ValidityDays)
  640. require.Equal(t, int64(12), assigner.calls[1].GroupID)
  641. require.Equal(t, 7, assigner.calls[1].ValidityDays)
  642. }
  643. func TestAuthService_Register_UsesEmailAuthSourceDefaultsWhenGrantEnabled(t *testing.T) {
  644. repo := &userRepoStub{nextID: 52}
  645. assigner := &defaultSubscriptionAssignerStub{}
  646. service := newAuthService(repo, map[string]string{
  647. SettingKeyRegistrationEnabled: "true",
  648. SettingKeyDefaultSubscriptions: `[{"group_id":91,"validity_days":3}]`,
  649. SettingKeyAuthSourceDefaultEmailBalance: "12.5",
  650. SettingKeyAuthSourceDefaultEmailConcurrency: "7",
  651. SettingKeyAuthSourceDefaultEmailSubscriptions: `[{"group_id":11,"validity_days":30}]`,
  652. SettingKeyAuthSourceDefaultEmailGrantOnSignup: "true",
  653. }, nil, nil)
  654. service.defaultSubAssigner = assigner
  655. _, user, err := service.Register(context.Background(), "email-defaults@test.com", "password")
  656. require.NoError(t, err)
  657. require.NotNil(t, user)
  658. require.Equal(t, 12.5, user.Balance)
  659. require.Equal(t, 7, user.Concurrency)
  660. require.Len(t, assigner.calls, 1)
  661. require.Equal(t, int64(11), assigner.calls[0].GroupID)
  662. require.Equal(t, 30, assigner.calls[0].ValidityDays)
  663. }
  664. func TestAuthService_Register_GrantOnSignupFalseFallsBackToGlobalDefaults(t *testing.T) {
  665. repo := &userRepoStub{nextID: 53}
  666. assigner := &defaultSubscriptionAssignerStub{}
  667. service := newAuthService(repo, map[string]string{
  668. SettingKeyRegistrationEnabled: "true",
  669. SettingKeyDefaultSubscriptions: `[{"group_id":31,"validity_days":5}]`,
  670. SettingKeyAuthSourceDefaultEmailBalance: "99",
  671. SettingKeyAuthSourceDefaultEmailConcurrency: "88",
  672. SettingKeyAuthSourceDefaultEmailSubscriptions: `[{"group_id":32,"validity_days":9}]`,
  673. SettingKeyAuthSourceDefaultEmailGrantOnSignup: "false",
  674. }, nil, nil)
  675. service.defaultSubAssigner = assigner
  676. _, user, err := service.Register(context.Background(), "email-global@test.com", "password")
  677. require.NoError(t, err)
  678. require.NotNil(t, user)
  679. require.Equal(t, 3.5, user.Balance)
  680. require.Equal(t, 2, user.Concurrency)
  681. require.Len(t, assigner.calls, 1)
  682. require.Equal(t, int64(31), assigner.calls[0].GroupID)
  683. require.Equal(t, 5, assigner.calls[0].ValidityDays)
  684. }
  685. func TestAuthService_Register_GrantOnSignupMergesSourceOverridesWithGlobalDefaults(t *testing.T) {
  686. repo := &userRepoStub{nextID: 54}
  687. assigner := &defaultSubscriptionAssignerStub{}
  688. service := newAuthService(repo, map[string]string{
  689. SettingKeyRegistrationEnabled: "true",
  690. SettingKeyDefaultSubscriptions: `[{"group_id":31,"validity_days":5}]`,
  691. SettingKeyAuthSourceDefaultEmailBalance: "9.5",
  692. SettingKeyAuthSourceDefaultEmailConcurrency: "5",
  693. SettingKeyAuthSourceDefaultEmailSubscriptions: `[]`,
  694. SettingKeyAuthSourceDefaultEmailGrantOnSignup: "true",
  695. }, nil, nil)
  696. service.defaultSubAssigner = assigner
  697. _, user, err := service.Register(context.Background(), "email-merged@test.com", "password")
  698. require.NoError(t, err)
  699. require.NotNil(t, user)
  700. require.Equal(t, 9.5, user.Balance)
  701. require.Equal(t, 5, user.Concurrency)
  702. require.Len(t, assigner.calls, 1)
  703. require.Equal(t, int64(31), assigner.calls[0].GroupID)
  704. require.Equal(t, 5, assigner.calls[0].ValidityDays)
  705. }
  706. func TestAuthService_LoginOrRegisterOAuthWithTokenPair_UsesLinuxDoAuthSourceDefaultsOnSignup(t *testing.T) {
  707. repo := &userRepoStub{nextID: 61}
  708. assigner := &defaultSubscriptionAssignerStub{}
  709. service := newAuthService(repo, map[string]string{
  710. SettingKeyRegistrationEnabled: "true",
  711. SettingKeyDefaultSubscriptions: `[{"group_id":81,"validity_days":1}]`,
  712. SettingKeyAuthSourceDefaultLinuxDoBalance: "21.75",
  713. SettingKeyAuthSourceDefaultLinuxDoConcurrency: "9",
  714. SettingKeyAuthSourceDefaultLinuxDoSubscriptions: `[{"group_id":22,"validity_days":14}]`,
  715. SettingKeyAuthSourceDefaultLinuxDoGrantOnSignup: "true",
  716. }, nil, nil)
  717. service.defaultSubAssigner = assigner
  718. service.refreshTokenCache = &refreshTokenCacheStub{}
  719. tokenPair, user, err := service.LoginOrRegisterOAuthWithTokenPair(context.Background(), "linuxdo-123@linuxdo-connect.invalid", "linuxdo_user", "", "", "linuxdo")
  720. require.NoError(t, err)
  721. require.NotNil(t, tokenPair)
  722. require.NotNil(t, user)
  723. require.Equal(t, int64(61), user.ID)
  724. require.Equal(t, 21.75, user.Balance)
  725. require.Equal(t, 9, user.Concurrency)
  726. require.Len(t, repo.created, 1)
  727. require.Len(t, assigner.calls, 1)
  728. require.Equal(t, int64(22), assigner.calls[0].GroupID)
  729. require.Equal(t, 14, assigner.calls[0].ValidityDays)
  730. }
  731. func TestAuthService_LoginOrRegisterOAuthWithTokenPair_ExistingUserDoesNotGrantAgain(t *testing.T) {
  732. existing := &User{
  733. ID: 88,
  734. Email: "linuxdo-123@linuxdo-connect.invalid",
  735. Username: "existing-linuxdo",
  736. Role: RoleUser,
  737. Status: StatusActive,
  738. Balance: 4,
  739. Concurrency: 1,
  740. TokenVersion: 2,
  741. }
  742. repo := &userRepoStub{user: existing}
  743. assigner := &defaultSubscriptionAssignerStub{}
  744. service := newAuthService(repo, map[string]string{
  745. SettingKeyRegistrationEnabled: "true",
  746. SettingKeyAuthSourceDefaultLinuxDoBalance: "21.75",
  747. SettingKeyAuthSourceDefaultLinuxDoConcurrency: "9",
  748. SettingKeyAuthSourceDefaultLinuxDoSubscriptions: `[{"group_id":22,"validity_days":14}]`,
  749. SettingKeyAuthSourceDefaultLinuxDoGrantOnSignup: "true",
  750. }, nil, nil)
  751. service.defaultSubAssigner = assigner
  752. service.refreshTokenCache = &refreshTokenCacheStub{}
  753. tokenPair, user, err := service.LoginOrRegisterOAuthWithTokenPair(context.Background(), existing.Email, "linuxdo_user", "", "", "linuxdo")
  754. require.NoError(t, err)
  755. require.NotNil(t, tokenPair)
  756. require.Equal(t, existing.ID, user.ID)
  757. require.Equal(t, 4.0, user.Balance)
  758. require.Equal(t, 1, user.Concurrency)
  759. require.Empty(t, repo.created)
  760. require.Empty(t, assigner.calls)
  761. }
  762. // newAuthServiceWithDingTalkCfg 构建一个含完整 DingTalk config 的 AuthService,
  763. // 用于测试 canBypassRegistrationDisabledForOAuth。
  764. func newAuthServiceWithDingTalkCfg(settings map[string]string, dtCfg config.DingTalkConnectConfig) *AuthService {
  765. cfg := &config.Config{
  766. JWT: config.JWTConfig{Secret: "test-secret", ExpireHour: 1},
  767. Default: config.DefaultConfig{UserBalance: 3.5, UserConcurrency: 2},
  768. DingTalk: dtCfg,
  769. }
  770. settingService := NewSettingService(&settingRepoStub{values: settings}, cfg)
  771. return NewAuthService(nil, nil, nil, nil, cfg, settingService, nil, nil, nil, nil, nil, nil, nil)
  772. }
  773. // minDingTalkURLs 返回一个包含必填字段的基础 DingTalkConnectConfig(不设 Enabled/BypassRegistration/Policy)。
  774. func minDingTalkURLs() config.DingTalkConnectConfig {
  775. return config.DingTalkConnectConfig{
  776. ClientID: "test-client",
  777. ClientSecret: "test-secret",
  778. AuthorizeURL: "https://example.com/oauth2/auth",
  779. TokenURL: "https://example.com/oauth2/token",
  780. UserInfoURL: "https://example.com/oauth2/userinfo",
  781. RedirectURL: "https://example.com/callback",
  782. FrontendRedirectURL: "https://example.com/auth/callback",
  783. DingTalkAppKind: "internal_app",
  784. AppType: "internal",
  785. }
  786. }
  787. func TestCanBypassRegistrationDisabledForOAuth(t *testing.T) {
  788. cases := []struct {
  789. name string
  790. signupSource string
  791. settings map[string]string
  792. dtCfg config.DingTalkConnectConfig
  793. want bool
  794. }{
  795. {
  796. name: "non-dingtalk source → false",
  797. signupSource: "linuxdo",
  798. settings: map[string]string{},
  799. dtCfg: minDingTalkURLs(),
  800. want: false,
  801. },
  802. {
  803. name: "dingtalk but cfg.Enabled=false → false",
  804. signupSource: "dingtalk",
  805. settings: map[string]string{
  806. SettingKeyDingTalkConnectEnabled: "false",
  807. SettingKeyDingTalkConnectBypassRegistration: "true",
  808. SettingKeyDingTalkConnectCorpRestrictionPolicy: "internal_only",
  809. },
  810. dtCfg: minDingTalkURLs(),
  811. want: false,
  812. },
  813. {
  814. name: "dingtalk enabled but BypassRegistration=false → false",
  815. signupSource: "dingtalk",
  816. settings: map[string]string{
  817. SettingKeyDingTalkConnectEnabled: "true",
  818. SettingKeyDingTalkConnectBypassRegistration: "false",
  819. SettingKeyDingTalkConnectCorpRestrictionPolicy: "internal_only",
  820. },
  821. dtCfg: minDingTalkURLs(),
  822. want: false,
  823. },
  824. {
  825. name: "dingtalk enabled + bypass=true but policy=none → false",
  826. signupSource: "dingtalk",
  827. settings: map[string]string{
  828. SettingKeyDingTalkConnectEnabled: "true",
  829. SettingKeyDingTalkConnectBypassRegistration: "true",
  830. SettingKeyDingTalkConnectCorpRestrictionPolicy: "none",
  831. },
  832. dtCfg: minDingTalkURLs(),
  833. want: false,
  834. },
  835. {
  836. name: "dingtalk enabled + bypass=true + policy=internal_only → true",
  837. signupSource: "dingtalk",
  838. settings: map[string]string{
  839. SettingKeyDingTalkConnectEnabled: "true",
  840. SettingKeyDingTalkConnectBypassRegistration: "true",
  841. SettingKeyDingTalkConnectCorpRestrictionPolicy: "internal_only",
  842. },
  843. dtCfg: minDingTalkURLs(),
  844. want: true,
  845. },
  846. }
  847. for _, tc := range cases {
  848. t.Run(tc.name, func(t *testing.T) {
  849. svc := newAuthServiceWithDingTalkCfg(tc.settings, tc.dtCfg)
  850. got := svc.canBypassRegistrationDisabledForOAuth(context.Background(), tc.signupSource)
  851. require.Equal(t, tc.want, got)
  852. })
  853. }
  854. }