user_subscription_repo_integration_test.go 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  1. //go:build integration
  2. package repository
  3. import (
  4. "context"
  5. "fmt"
  6. "testing"
  7. "time"
  8. dbent "github.com/Wei-Shaw/sub2api/ent"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/stretchr/testify/suite"
  12. )
  13. type UserSubscriptionRepoSuite struct {
  14. suite.Suite
  15. ctx context.Context
  16. client *dbent.Client
  17. repo *userSubscriptionRepository
  18. }
  19. func (s *UserSubscriptionRepoSuite) SetupTest() {
  20. s.ctx = context.Background()
  21. tx := testEntTx(s.T())
  22. s.client = tx.Client()
  23. s.repo = NewUserSubscriptionRepository(s.client).(*userSubscriptionRepository)
  24. }
  25. func TestUserSubscriptionRepoSuite(t *testing.T) {
  26. suite.Run(t, new(UserSubscriptionRepoSuite))
  27. }
  28. func (s *UserSubscriptionRepoSuite) mustCreateUser(email string, role string) *service.User {
  29. s.T().Helper()
  30. if role == "" {
  31. role = service.RoleUser
  32. }
  33. u, err := s.client.User.Create().
  34. SetEmail(email).
  35. SetPasswordHash("test-password-hash").
  36. SetStatus(service.StatusActive).
  37. SetRole(role).
  38. Save(s.ctx)
  39. s.Require().NoError(err, "create user")
  40. return userEntityToService(u)
  41. }
  42. func (s *UserSubscriptionRepoSuite) mustCreateGroup(name string) *service.Group {
  43. s.T().Helper()
  44. g, err := s.client.Group.Create().
  45. SetName(name).
  46. SetStatus(service.StatusActive).
  47. Save(s.ctx)
  48. s.Require().NoError(err, "create group")
  49. return groupEntityToService(g)
  50. }
  51. func (s *UserSubscriptionRepoSuite) mustCreateSubscription(userID, groupID int64, mutate func(*dbent.UserSubscriptionCreate)) *dbent.UserSubscription {
  52. s.T().Helper()
  53. now := time.Now()
  54. create := s.client.UserSubscription.Create().
  55. SetUserID(userID).
  56. SetGroupID(groupID).
  57. SetStartsAt(now.Add(-1 * time.Hour)).
  58. SetExpiresAt(now.Add(24 * time.Hour)).
  59. SetStatus(service.SubscriptionStatusActive).
  60. SetAssignedAt(now).
  61. SetNotes("")
  62. if mutate != nil {
  63. mutate(create)
  64. }
  65. sub, err := create.Save(s.ctx)
  66. s.Require().NoError(err, "create user subscription")
  67. return sub
  68. }
  69. // --- Create / GetByID / Update / Delete ---
  70. func (s *UserSubscriptionRepoSuite) TestCreate() {
  71. user := s.mustCreateUser("sub-create@test.com", service.RoleUser)
  72. group := s.mustCreateGroup("g-create")
  73. sub := &service.UserSubscription{
  74. UserID: user.ID,
  75. GroupID: group.ID,
  76. Status: service.SubscriptionStatusActive,
  77. ExpiresAt: time.Now().Add(24 * time.Hour),
  78. }
  79. err := s.repo.Create(s.ctx, sub)
  80. s.Require().NoError(err, "Create")
  81. s.Require().NotZero(sub.ID, "expected ID to be set")
  82. got, err := s.repo.GetByID(s.ctx, sub.ID)
  83. s.Require().NoError(err, "GetByID")
  84. s.Require().Equal(sub.UserID, got.UserID)
  85. s.Require().Equal(sub.GroupID, got.GroupID)
  86. }
  87. func (s *UserSubscriptionRepoSuite) TestGetByID_WithPreloads() {
  88. user := s.mustCreateUser("preload@test.com", service.RoleUser)
  89. group := s.mustCreateGroup("g-preload")
  90. admin := s.mustCreateUser("admin@test.com", service.RoleAdmin)
  91. sub := s.mustCreateSubscription(user.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
  92. c.SetAssignedBy(admin.ID)
  93. })
  94. got, err := s.repo.GetByID(s.ctx, sub.ID)
  95. s.Require().NoError(err, "GetByID")
  96. s.Require().NotNil(got.User, "expected User preload")
  97. s.Require().NotNil(got.Group, "expected Group preload")
  98. s.Require().NotNil(got.AssignedByUser, "expected AssignedByUser preload")
  99. s.Require().Equal(user.ID, got.User.ID)
  100. s.Require().Equal(group.ID, got.Group.ID)
  101. s.Require().Equal(admin.ID, got.AssignedByUser.ID)
  102. }
  103. func (s *UserSubscriptionRepoSuite) TestGetByID_NotFound() {
  104. _, err := s.repo.GetByID(s.ctx, 999999)
  105. s.Require().Error(err, "expected error for non-existent ID")
  106. }
  107. func (s *UserSubscriptionRepoSuite) TestUpdate() {
  108. user := s.mustCreateUser("update@test.com", service.RoleUser)
  109. group := s.mustCreateGroup("g-update")
  110. created := s.mustCreateSubscription(user.ID, group.ID, nil)
  111. sub, err := s.repo.GetByID(s.ctx, created.ID)
  112. s.Require().NoError(err, "GetByID")
  113. sub.Notes = "updated notes"
  114. s.Require().NoError(s.repo.Update(s.ctx, sub), "Update")
  115. got, err := s.repo.GetByID(s.ctx, sub.ID)
  116. s.Require().NoError(err, "GetByID after update")
  117. s.Require().Equal("updated notes", got.Notes)
  118. }
  119. func (s *UserSubscriptionRepoSuite) TestDelete() {
  120. user := s.mustCreateUser("delete@test.com", service.RoleUser)
  121. group := s.mustCreateGroup("g-delete")
  122. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  123. err := s.repo.Delete(s.ctx, sub.ID)
  124. s.Require().NoError(err, "Delete")
  125. _, err = s.repo.GetByID(s.ctx, sub.ID)
  126. s.Require().Error(err, "expected error after delete")
  127. }
  128. func (s *UserSubscriptionRepoSuite) TestGetByIDIncludeDeleted_PreservesPersistedStatus() {
  129. user := s.mustCreateUser("include-deleted@test.com", service.RoleUser)
  130. group := s.mustCreateGroup("g-include-deleted")
  131. sub := s.mustCreateSubscription(user.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
  132. c.SetStatus(service.SubscriptionStatusActive)
  133. })
  134. s.Require().NoError(s.repo.Delete(s.ctx, sub.ID), "Delete")
  135. got, err := s.repo.GetByIDIncludeDeleted(s.ctx, sub.ID)
  136. s.Require().NoError(err, "GetByIDIncludeDeleted")
  137. s.Require().Equal(service.SubscriptionStatusActive, got.Status)
  138. s.Require().NotNil(got.DeletedAt)
  139. s.Require().NotNil(got.User)
  140. s.Require().NotNil(got.Group)
  141. }
  142. func (s *UserSubscriptionRepoSuite) TestRestore() {
  143. user := s.mustCreateUser("restore@test.com", service.RoleUser)
  144. group := s.mustCreateGroup("g-restore")
  145. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  146. s.Require().NoError(s.repo.Delete(s.ctx, sub.ID), "Delete")
  147. restored, err := s.repo.Restore(s.ctx, sub.ID, service.SubscriptionStatusExpired)
  148. s.Require().NoError(err, "Restore")
  149. s.Require().Equal(service.SubscriptionStatusExpired, restored.Status)
  150. s.Require().Nil(restored.DeletedAt)
  151. got, err := s.repo.GetByID(s.ctx, sub.ID)
  152. s.Require().NoError(err, "GetByID after restore")
  153. s.Require().Nil(got.DeletedAt)
  154. s.Require().Equal(service.SubscriptionStatusExpired, got.Status)
  155. }
  156. func (s *UserSubscriptionRepoSuite) TestDelete_Idempotent() {
  157. s.Require().NoError(s.repo.Delete(s.ctx, 42424242), "Delete should be idempotent")
  158. }
  159. // --- GetByUserIDAndGroupID / GetActiveByUserIDAndGroupID ---
  160. func (s *UserSubscriptionRepoSuite) TestGetByUserIDAndGroupID() {
  161. user := s.mustCreateUser("byuser@test.com", service.RoleUser)
  162. group := s.mustCreateGroup("g-byuser")
  163. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  164. got, err := s.repo.GetByUserIDAndGroupID(s.ctx, user.ID, group.ID)
  165. s.Require().NoError(err, "GetByUserIDAndGroupID")
  166. s.Require().Equal(sub.ID, got.ID)
  167. s.Require().NotNil(got.Group, "expected Group preload")
  168. }
  169. func (s *UserSubscriptionRepoSuite) TestGetByUserIDAndGroupID_NotFound() {
  170. _, err := s.repo.GetByUserIDAndGroupID(s.ctx, 999999, 999999)
  171. s.Require().Error(err, "expected error for non-existent pair")
  172. }
  173. func (s *UserSubscriptionRepoSuite) TestGetActiveByUserIDAndGroupID() {
  174. user := s.mustCreateUser("active@test.com", service.RoleUser)
  175. group := s.mustCreateGroup("g-active")
  176. active := s.mustCreateSubscription(user.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
  177. c.SetExpiresAt(time.Now().Add(2 * time.Hour))
  178. })
  179. got, err := s.repo.GetActiveByUserIDAndGroupID(s.ctx, user.ID, group.ID)
  180. s.Require().NoError(err, "GetActiveByUserIDAndGroupID")
  181. s.Require().Equal(active.ID, got.ID)
  182. }
  183. func (s *UserSubscriptionRepoSuite) TestGetActiveByUserIDAndGroupID_ExpiredIgnored() {
  184. user := s.mustCreateUser("expired@test.com", service.RoleUser)
  185. group := s.mustCreateGroup("g-expired")
  186. s.mustCreateSubscription(user.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
  187. c.SetExpiresAt(time.Now().Add(-2 * time.Hour))
  188. })
  189. _, err := s.repo.GetActiveByUserIDAndGroupID(s.ctx, user.ID, group.ID)
  190. s.Require().Error(err, "expected error for expired subscription")
  191. }
  192. // --- ListByUserID / ListActiveByUserID ---
  193. func (s *UserSubscriptionRepoSuite) TestListByUserID() {
  194. user := s.mustCreateUser("listby@test.com", service.RoleUser)
  195. g1 := s.mustCreateGroup("g-list1")
  196. g2 := s.mustCreateGroup("g-list2")
  197. s.mustCreateSubscription(user.ID, g1.ID, nil)
  198. s.mustCreateSubscription(user.ID, g2.ID, func(c *dbent.UserSubscriptionCreate) {
  199. c.SetStatus(service.SubscriptionStatusExpired)
  200. c.SetExpiresAt(time.Now().Add(-24 * time.Hour))
  201. })
  202. subs, err := s.repo.ListByUserID(s.ctx, user.ID)
  203. s.Require().NoError(err, "ListByUserID")
  204. s.Require().Len(subs, 2)
  205. for _, sub := range subs {
  206. s.Require().NotNil(sub.Group, "expected Group preload")
  207. }
  208. }
  209. func (s *UserSubscriptionRepoSuite) TestListActiveByUserID() {
  210. user := s.mustCreateUser("listactive@test.com", service.RoleUser)
  211. g1 := s.mustCreateGroup("g-act1")
  212. g2 := s.mustCreateGroup("g-act2")
  213. s.mustCreateSubscription(user.ID, g1.ID, func(c *dbent.UserSubscriptionCreate) {
  214. c.SetExpiresAt(time.Now().Add(24 * time.Hour))
  215. })
  216. s.mustCreateSubscription(user.ID, g2.ID, func(c *dbent.UserSubscriptionCreate) {
  217. c.SetStatus(service.SubscriptionStatusExpired)
  218. c.SetExpiresAt(time.Now().Add(-24 * time.Hour))
  219. })
  220. subs, err := s.repo.ListActiveByUserID(s.ctx, user.ID)
  221. s.Require().NoError(err, "ListActiveByUserID")
  222. s.Require().Len(subs, 1)
  223. s.Require().Equal(service.SubscriptionStatusActive, subs[0].Status)
  224. }
  225. // --- ListByGroupID ---
  226. func (s *UserSubscriptionRepoSuite) TestListByGroupID() {
  227. user1 := s.mustCreateUser("u1@test.com", service.RoleUser)
  228. user2 := s.mustCreateUser("u2@test.com", service.RoleUser)
  229. group := s.mustCreateGroup("g-listgrp")
  230. s.mustCreateSubscription(user1.ID, group.ID, nil)
  231. s.mustCreateSubscription(user2.ID, group.ID, nil)
  232. subs, page, err := s.repo.ListByGroupID(s.ctx, group.ID, pagination.PaginationParams{Page: 1, PageSize: 10})
  233. s.Require().NoError(err, "ListByGroupID")
  234. s.Require().Len(subs, 2)
  235. s.Require().Equal(int64(2), page.Total)
  236. for _, sub := range subs {
  237. s.Require().NotNil(sub.User, "expected User preload")
  238. s.Require().NotNil(sub.Group, "expected Group preload")
  239. }
  240. }
  241. // --- List with filters ---
  242. func (s *UserSubscriptionRepoSuite) TestList_NoFilters() {
  243. user := s.mustCreateUser("list@test.com", service.RoleUser)
  244. group := s.mustCreateGroup("g-list")
  245. s.mustCreateSubscription(user.ID, group.ID, nil)
  246. subs, page, err := s.repo.List(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, nil, nil, "", "", "", "")
  247. s.Require().NoError(err, "List")
  248. s.Require().Len(subs, 1)
  249. s.Require().Equal(int64(1), page.Total)
  250. }
  251. func (s *UserSubscriptionRepoSuite) TestList_FilterByUserID() {
  252. user1 := s.mustCreateUser("filter1@test.com", service.RoleUser)
  253. user2 := s.mustCreateUser("filter2@test.com", service.RoleUser)
  254. group := s.mustCreateGroup("g-filter")
  255. s.mustCreateSubscription(user1.ID, group.ID, nil)
  256. s.mustCreateSubscription(user2.ID, group.ID, nil)
  257. subs, _, err := s.repo.List(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, &user1.ID, nil, "", "", "", "")
  258. s.Require().NoError(err)
  259. s.Require().Len(subs, 1)
  260. s.Require().Equal(user1.ID, subs[0].UserID)
  261. }
  262. func (s *UserSubscriptionRepoSuite) TestList_FilterByGroupID() {
  263. user := s.mustCreateUser("grpfilter@test.com", service.RoleUser)
  264. g1 := s.mustCreateGroup("g-f1")
  265. g2 := s.mustCreateGroup("g-f2")
  266. s.mustCreateSubscription(user.ID, g1.ID, nil)
  267. s.mustCreateSubscription(user.ID, g2.ID, nil)
  268. subs, _, err := s.repo.List(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, nil, &g1.ID, "", "", "", "")
  269. s.Require().NoError(err)
  270. s.Require().Len(subs, 1)
  271. s.Require().Equal(g1.ID, subs[0].GroupID)
  272. }
  273. func (s *UserSubscriptionRepoSuite) TestList_FilterByStatus() {
  274. user1 := s.mustCreateUser("statfilter1@test.com", service.RoleUser)
  275. user2 := s.mustCreateUser("statfilter2@test.com", service.RoleUser)
  276. group1 := s.mustCreateGroup("g-stat-1")
  277. group2 := s.mustCreateGroup("g-stat-2")
  278. s.mustCreateSubscription(user1.ID, group1.ID, func(c *dbent.UserSubscriptionCreate) {
  279. c.SetStatus(service.SubscriptionStatusActive)
  280. c.SetExpiresAt(time.Now().Add(24 * time.Hour))
  281. })
  282. s.mustCreateSubscription(user2.ID, group2.ID, func(c *dbent.UserSubscriptionCreate) {
  283. c.SetStatus(service.SubscriptionStatusExpired)
  284. c.SetExpiresAt(time.Now().Add(-24 * time.Hour))
  285. })
  286. subs, _, err := s.repo.List(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, nil, nil, service.SubscriptionStatusExpired, "", "", "")
  287. s.Require().NoError(err)
  288. s.Require().Len(subs, 1)
  289. s.Require().Equal(service.SubscriptionStatusExpired, subs[0].Status)
  290. }
  291. func (s *UserSubscriptionRepoSuite) TestList_IncludesRevokedWhenStatusEmpty() {
  292. user1 := s.mustCreateUser("allstatus1@test.com", service.RoleUser)
  293. user2 := s.mustCreateUser("allstatus2@test.com", service.RoleUser)
  294. user3 := s.mustCreateUser("allstatus3@test.com", service.RoleUser)
  295. group1 := s.mustCreateGroup("g-allstatus-1")
  296. group2 := s.mustCreateGroup("g-allstatus-2")
  297. group3 := s.mustCreateGroup("g-allstatus-3")
  298. s.mustCreateSubscription(user1.ID, group1.ID, nil)
  299. s.mustCreateSubscription(user2.ID, group2.ID, func(c *dbent.UserSubscriptionCreate) {
  300. c.SetStatus(service.SubscriptionStatusExpired)
  301. c.SetExpiresAt(time.Now().Add(-24 * time.Hour))
  302. })
  303. revoked := s.mustCreateSubscription(user3.ID, group3.ID, nil)
  304. s.Require().NoError(s.repo.Delete(s.ctx, revoked.ID))
  305. subs, pag, err := s.repo.List(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, nil, nil, "", "", "", "")
  306. s.Require().NoError(err)
  307. s.Require().Len(subs, 3)
  308. s.Require().Equal(int64(3), pag.Total)
  309. var gotRevoked *service.UserSubscription
  310. for i := range subs {
  311. if subs[i].ID == revoked.ID {
  312. gotRevoked = &subs[i]
  313. break
  314. }
  315. }
  316. s.Require().NotNil(gotRevoked, "all status should include soft-deleted subscription")
  317. s.Require().Equal(service.SubscriptionStatusRevoked, gotRevoked.Status)
  318. s.Require().NotNil(gotRevoked.DeletedAt)
  319. s.Require().NotNil(gotRevoked.User)
  320. s.Require().NotNil(gotRevoked.Group)
  321. }
  322. func (s *UserSubscriptionRepoSuite) TestList_FilterByRevokedStatus() {
  323. user1 := s.mustCreateUser("revokedfilter1@test.com", service.RoleUser)
  324. user2 := s.mustCreateUser("revokedfilter2@test.com", service.RoleUser)
  325. group1 := s.mustCreateGroup("g-revoked-1")
  326. group2 := s.mustCreateGroup("g-revoked-2")
  327. active := s.mustCreateSubscription(user1.ID, group1.ID, nil)
  328. revoked := s.mustCreateSubscription(user2.ID, group2.ID, nil)
  329. s.Require().NoError(s.repo.Delete(s.ctx, revoked.ID))
  330. subs, pag, err := s.repo.List(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, nil, nil, service.SubscriptionStatusRevoked, "", "", "")
  331. s.Require().NoError(err)
  332. s.Require().Len(subs, 1)
  333. s.Require().Equal(int64(1), pag.Total)
  334. s.Require().Equal(revoked.ID, subs[0].ID)
  335. s.Require().NotEqual(active.ID, subs[0].ID)
  336. s.Require().Equal(service.SubscriptionStatusRevoked, subs[0].Status)
  337. s.Require().NotNil(subs[0].DeletedAt)
  338. }
  339. // --- Usage tracking ---
  340. func (s *UserSubscriptionRepoSuite) TestIncrementUsage() {
  341. user := s.mustCreateUser("usage@test.com", service.RoleUser)
  342. group := s.mustCreateGroup("g-usage")
  343. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  344. err := s.repo.IncrementUsage(s.ctx, sub.ID, 1.25)
  345. s.Require().NoError(err, "IncrementUsage")
  346. got, err := s.repo.GetByID(s.ctx, sub.ID)
  347. s.Require().NoError(err)
  348. s.Require().InDelta(1.25, got.DailyUsageUSD, 1e-6)
  349. s.Require().InDelta(1.25, got.WeeklyUsageUSD, 1e-6)
  350. s.Require().InDelta(1.25, got.MonthlyUsageUSD, 1e-6)
  351. }
  352. func (s *UserSubscriptionRepoSuite) TestIncrementUsage_Accumulates() {
  353. user := s.mustCreateUser("accum@test.com", service.RoleUser)
  354. group := s.mustCreateGroup("g-accum")
  355. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  356. s.Require().NoError(s.repo.IncrementUsage(s.ctx, sub.ID, 1.0))
  357. s.Require().NoError(s.repo.IncrementUsage(s.ctx, sub.ID, 2.5))
  358. got, err := s.repo.GetByID(s.ctx, sub.ID)
  359. s.Require().NoError(err)
  360. s.Require().InDelta(3.5, got.DailyUsageUSD, 1e-6)
  361. }
  362. func (s *UserSubscriptionRepoSuite) TestActivateWindows() {
  363. user := s.mustCreateUser("activate@test.com", service.RoleUser)
  364. group := s.mustCreateGroup("g-activate")
  365. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  366. dailyStart := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
  367. activateAt := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
  368. err := s.repo.ActivateWindows(s.ctx, sub.ID, dailyStart, activateAt)
  369. s.Require().NoError(err, "ActivateWindows")
  370. got, err := s.repo.GetByID(s.ctx, sub.ID)
  371. s.Require().NoError(err)
  372. s.Require().NotNil(got.DailyWindowStart)
  373. s.Require().NotNil(got.WeeklyWindowStart)
  374. s.Require().NotNil(got.MonthlyWindowStart)
  375. s.Require().WithinDuration(dailyStart, *got.DailyWindowStart, time.Microsecond)
  376. s.Require().WithinDuration(activateAt, *got.WeeklyWindowStart, time.Microsecond)
  377. s.Require().WithinDuration(activateAt, *got.MonthlyWindowStart, time.Microsecond)
  378. }
  379. func (s *UserSubscriptionRepoSuite) TestActivateWindows_StaleActivationPreservesExistingWindows() {
  380. user := s.mustCreateUser("activate-cas@test.com", service.RoleUser)
  381. group := s.mustCreateGroup("g-activate-cas")
  382. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  383. activatedAt := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
  384. manualResetAt := activatedAt.Add(2 * time.Hour)
  385. manualDailyStart := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
  386. s.Require().NoError(s.repo.ActivateWindows(s.ctx, sub.ID, activatedAt, activatedAt))
  387. s.Require().NoError(s.repo.ResetUsageWindows(s.ctx, sub.ID, true, true, true, manualDailyStart, manualResetAt))
  388. // Simulate a concurrent request carrying the original unactivated snapshot.
  389. s.Require().NoError(s.repo.ActivateWindows(s.ctx, sub.ID, activatedAt.Add(time.Hour), activatedAt.Add(time.Hour)))
  390. got, err := s.repo.GetByID(s.ctx, sub.ID)
  391. s.Require().NoError(err)
  392. s.Require().WithinDuration(manualDailyStart, *got.DailyWindowStart, time.Microsecond)
  393. s.Require().WithinDuration(manualResetAt, *got.WeeklyWindowStart, time.Microsecond)
  394. s.Require().WithinDuration(manualResetAt, *got.MonthlyWindowStart, time.Microsecond)
  395. }
  396. func (s *UserSubscriptionRepoSuite) TestResetDailyUsage() {
  397. user := s.mustCreateUser("resetd@test.com", service.RoleUser)
  398. group := s.mustCreateGroup("g-resetd")
  399. sub := s.mustCreateSubscription(user.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
  400. c.SetDailyUsageUsd(10.0)
  401. c.SetWeeklyUsageUsd(20.0)
  402. })
  403. resetAt := time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC)
  404. err := s.repo.ResetDailyUsage(s.ctx, sub.ID, sub.DailyWindowStart, resetAt)
  405. s.Require().NoError(err, "ResetDailyUsage")
  406. got, err := s.repo.GetByID(s.ctx, sub.ID)
  407. s.Require().NoError(err)
  408. s.Require().InDelta(0.0, got.DailyUsageUSD, 1e-6)
  409. s.Require().InDelta(20.0, got.WeeklyUsageUSD, 1e-6)
  410. s.Require().NotNil(got.DailyWindowStart)
  411. s.Require().WithinDuration(resetAt, *got.DailyWindowStart, time.Microsecond)
  412. }
  413. func (s *UserSubscriptionRepoSuite) TestResetDailyUsage_StaleResetDoesNotClearNewWindowUsage() {
  414. user := s.mustCreateUser("resetd-cas@test.com", service.RoleUser)
  415. group := s.mustCreateGroup("g-resetd-cas")
  416. oldWindowStart := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
  417. sub := s.mustCreateSubscription(user.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
  418. c.SetDailyWindowStart(oldWindowStart)
  419. c.SetDailyUsageUsd(10)
  420. })
  421. newWindowStart := oldWindowStart.Add(24 * time.Hour)
  422. s.Require().NoError(s.repo.ResetDailyUsage(s.ctx, sub.ID, &oldWindowStart, newWindowStart))
  423. s.Require().NoError(s.repo.IncrementUsage(s.ctx, sub.ID, 3))
  424. // Simulate a second request carrying the stale old-window snapshot.
  425. s.Require().NoError(s.repo.ResetDailyUsage(s.ctx, sub.ID, &oldWindowStart, newWindowStart))
  426. got, err := s.repo.GetByID(s.ctx, sub.ID)
  427. s.Require().NoError(err)
  428. s.Require().InDelta(3, got.DailyUsageUSD, 1e-6)
  429. s.Require().WithinDuration(newWindowStart, *got.DailyWindowStart, time.Microsecond)
  430. }
  431. func (s *UserSubscriptionRepoSuite) TestResetUsageWindows_ClearsUsageAfterAutomaticWindowAdvance() {
  432. user := s.mustCreateUser("admin-reset-current@test.com", service.RoleUser)
  433. group := s.mustCreateGroup("g-admin-reset-current")
  434. oldWindowStart := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
  435. sub := s.mustCreateSubscription(user.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
  436. c.SetDailyWindowStart(oldWindowStart)
  437. c.SetDailyUsageUsd(10)
  438. })
  439. newWindowStart := oldWindowStart.Add(24 * time.Hour)
  440. s.Require().NoError(s.repo.ResetDailyUsage(s.ctx, sub.ID, &oldWindowStart, newWindowStart))
  441. s.Require().NoError(s.repo.IncrementUsage(s.ctx, sub.ID, 3))
  442. s.Require().NoError(s.repo.ResetUsageWindows(s.ctx, sub.ID, true, false, false, newWindowStart, newWindowStart))
  443. got, err := s.repo.GetByID(s.ctx, sub.ID)
  444. s.Require().NoError(err)
  445. s.Require().InDelta(0, got.DailyUsageUSD, 1e-6)
  446. s.Require().WithinDuration(newWindowStart, *got.DailyWindowStart, time.Microsecond)
  447. }
  448. func (s *UserSubscriptionRepoSuite) TestResetWeeklyUsage() {
  449. user := s.mustCreateUser("resetw@test.com", service.RoleUser)
  450. group := s.mustCreateGroup("g-resetw")
  451. sub := s.mustCreateSubscription(user.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
  452. c.SetWeeklyUsageUsd(15.0)
  453. c.SetMonthlyUsageUsd(30.0)
  454. })
  455. resetAt := time.Date(2025, 1, 6, 0, 0, 0, 0, time.UTC)
  456. err := s.repo.ResetWeeklyUsage(s.ctx, sub.ID, sub.WeeklyWindowStart, resetAt)
  457. s.Require().NoError(err, "ResetWeeklyUsage")
  458. got, err := s.repo.GetByID(s.ctx, sub.ID)
  459. s.Require().NoError(err)
  460. s.Require().InDelta(0.0, got.WeeklyUsageUSD, 1e-6)
  461. s.Require().InDelta(30.0, got.MonthlyUsageUSD, 1e-6)
  462. s.Require().NotNil(got.WeeklyWindowStart)
  463. s.Require().WithinDuration(resetAt, *got.WeeklyWindowStart, time.Microsecond)
  464. }
  465. func (s *UserSubscriptionRepoSuite) TestResetMonthlyUsage() {
  466. user := s.mustCreateUser("resetm@test.com", service.RoleUser)
  467. group := s.mustCreateGroup("g-resetm")
  468. sub := s.mustCreateSubscription(user.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
  469. c.SetMonthlyUsageUsd(25.0)
  470. })
  471. resetAt := time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC)
  472. err := s.repo.ResetMonthlyUsage(s.ctx, sub.ID, sub.MonthlyWindowStart, resetAt)
  473. s.Require().NoError(err, "ResetMonthlyUsage")
  474. got, err := s.repo.GetByID(s.ctx, sub.ID)
  475. s.Require().NoError(err)
  476. s.Require().InDelta(0.0, got.MonthlyUsageUSD, 1e-6)
  477. s.Require().NotNil(got.MonthlyWindowStart)
  478. s.Require().WithinDuration(resetAt, *got.MonthlyWindowStart, time.Microsecond)
  479. }
  480. // --- UpdateStatus / ExtendExpiry / UpdateNotes ---
  481. func (s *UserSubscriptionRepoSuite) TestUpdateStatus() {
  482. user := s.mustCreateUser("status@test.com", service.RoleUser)
  483. group := s.mustCreateGroup("g-status")
  484. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  485. err := s.repo.UpdateStatus(s.ctx, sub.ID, service.SubscriptionStatusExpired)
  486. s.Require().NoError(err, "UpdateStatus")
  487. got, err := s.repo.GetByID(s.ctx, sub.ID)
  488. s.Require().NoError(err)
  489. s.Require().Equal(service.SubscriptionStatusExpired, got.Status)
  490. }
  491. func (s *UserSubscriptionRepoSuite) TestExtendExpiry() {
  492. user := s.mustCreateUser("extend@test.com", service.RoleUser)
  493. group := s.mustCreateGroup("g-extend")
  494. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  495. newExpiry := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
  496. err := s.repo.ExtendExpiry(s.ctx, sub.ID, newExpiry)
  497. s.Require().NoError(err, "ExtendExpiry")
  498. got, err := s.repo.GetByID(s.ctx, sub.ID)
  499. s.Require().NoError(err)
  500. s.Require().WithinDuration(newExpiry, got.ExpiresAt, time.Microsecond)
  501. }
  502. func (s *UserSubscriptionRepoSuite) TestUpdateNotes() {
  503. user := s.mustCreateUser("notes@test.com", service.RoleUser)
  504. group := s.mustCreateGroup("g-notes")
  505. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  506. err := s.repo.UpdateNotes(s.ctx, sub.ID, "VIP user")
  507. s.Require().NoError(err, "UpdateNotes")
  508. got, err := s.repo.GetByID(s.ctx, sub.ID)
  509. s.Require().NoError(err)
  510. s.Require().Equal("VIP user", got.Notes)
  511. }
  512. // --- ListExpired / BatchUpdateExpiredStatus ---
  513. func (s *UserSubscriptionRepoSuite) TestListExpired() {
  514. user := s.mustCreateUser("listexp@test.com", service.RoleUser)
  515. groupActive := s.mustCreateGroup("g-listexp-active")
  516. groupExpired := s.mustCreateGroup("g-listexp-expired")
  517. s.mustCreateSubscription(user.ID, groupActive.ID, func(c *dbent.UserSubscriptionCreate) {
  518. c.SetExpiresAt(time.Now().Add(24 * time.Hour))
  519. })
  520. s.mustCreateSubscription(user.ID, groupExpired.ID, func(c *dbent.UserSubscriptionCreate) {
  521. c.SetExpiresAt(time.Now().Add(-24 * time.Hour))
  522. })
  523. expired, err := s.repo.ListExpired(s.ctx)
  524. s.Require().NoError(err, "ListExpired")
  525. s.Require().Len(expired, 1)
  526. }
  527. func (s *UserSubscriptionRepoSuite) TestBatchUpdateExpiredStatus() {
  528. user := s.mustCreateUser("batch@test.com", service.RoleUser)
  529. groupFuture := s.mustCreateGroup("g-batch-future")
  530. groupPast := s.mustCreateGroup("g-batch-past")
  531. active := s.mustCreateSubscription(user.ID, groupFuture.ID, func(c *dbent.UserSubscriptionCreate) {
  532. c.SetExpiresAt(time.Now().Add(24 * time.Hour))
  533. })
  534. expiredActive := s.mustCreateSubscription(user.ID, groupPast.ID, func(c *dbent.UserSubscriptionCreate) {
  535. c.SetExpiresAt(time.Now().Add(-24 * time.Hour))
  536. })
  537. affected, err := s.repo.BatchUpdateExpiredStatus(s.ctx)
  538. s.Require().NoError(err, "BatchUpdateExpiredStatus")
  539. s.Require().Equal(int64(1), affected)
  540. gotActive, _ := s.repo.GetByID(s.ctx, active.ID)
  541. s.Require().Equal(service.SubscriptionStatusActive, gotActive.Status)
  542. gotExpired, _ := s.repo.GetByID(s.ctx, expiredActive.ID)
  543. s.Require().Equal(service.SubscriptionStatusExpired, gotExpired.Status)
  544. }
  545. // --- ExistsByUserIDAndGroupID ---
  546. func (s *UserSubscriptionRepoSuite) TestExistsByUserIDAndGroupID() {
  547. user := s.mustCreateUser("exists@test.com", service.RoleUser)
  548. group := s.mustCreateGroup("g-exists")
  549. s.mustCreateSubscription(user.ID, group.ID, nil)
  550. exists, err := s.repo.ExistsByUserIDAndGroupID(s.ctx, user.ID, group.ID)
  551. s.Require().NoError(err, "ExistsByUserIDAndGroupID")
  552. s.Require().True(exists)
  553. notExists, err := s.repo.ExistsByUserIDAndGroupID(s.ctx, user.ID, 999999)
  554. s.Require().NoError(err)
  555. s.Require().False(notExists)
  556. }
  557. func (s *UserSubscriptionRepoSuite) TestExistsActiveByUserIDAndGroupID_IgnoresSoftDeletedRows() {
  558. user := s.mustCreateUser("exists-active@test.com", service.RoleUser)
  559. group := s.mustCreateGroup("g-exists-active")
  560. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  561. exists, err := s.repo.ExistsActiveByUserIDAndGroupID(s.ctx, user.ID, group.ID)
  562. s.Require().NoError(err, "ExistsActiveByUserIDAndGroupID")
  563. s.Require().True(exists)
  564. s.Require().NoError(s.repo.Delete(s.ctx, sub.ID), "Delete")
  565. exists, err = s.repo.ExistsActiveByUserIDAndGroupID(s.ctx, user.ID, group.ID)
  566. s.Require().NoError(err, "ExistsActiveByUserIDAndGroupID after delete")
  567. s.Require().False(exists)
  568. }
  569. // --- CountByGroupID / CountActiveByGroupID ---
  570. func (s *UserSubscriptionRepoSuite) TestCountByGroupID() {
  571. user1 := s.mustCreateUser("cnt1@test.com", service.RoleUser)
  572. user2 := s.mustCreateUser("cnt2@test.com", service.RoleUser)
  573. group := s.mustCreateGroup("g-count")
  574. s.mustCreateSubscription(user1.ID, group.ID, nil)
  575. s.mustCreateSubscription(user2.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
  576. c.SetStatus(service.SubscriptionStatusExpired)
  577. c.SetExpiresAt(time.Now().Add(-24 * time.Hour))
  578. })
  579. count, err := s.repo.CountByGroupID(s.ctx, group.ID)
  580. s.Require().NoError(err, "CountByGroupID")
  581. s.Require().Equal(int64(2), count)
  582. }
  583. func (s *UserSubscriptionRepoSuite) TestCountActiveByGroupID() {
  584. user1 := s.mustCreateUser("cntact1@test.com", service.RoleUser)
  585. user2 := s.mustCreateUser("cntact2@test.com", service.RoleUser)
  586. group := s.mustCreateGroup("g-cntact")
  587. s.mustCreateSubscription(user1.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
  588. c.SetExpiresAt(time.Now().Add(24 * time.Hour))
  589. })
  590. s.mustCreateSubscription(user2.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
  591. c.SetExpiresAt(time.Now().Add(-24 * time.Hour)) // expired by time
  592. })
  593. count, err := s.repo.CountActiveByGroupID(s.ctx, group.ID)
  594. s.Require().NoError(err, "CountActiveByGroupID")
  595. s.Require().Equal(int64(1), count, "only future expiry counts as active")
  596. }
  597. // --- DeleteByGroupID ---
  598. func (s *UserSubscriptionRepoSuite) TestDeleteByGroupID() {
  599. user1 := s.mustCreateUser("delgrp1@test.com", service.RoleUser)
  600. user2 := s.mustCreateUser("delgrp2@test.com", service.RoleUser)
  601. group := s.mustCreateGroup("g-delgrp")
  602. s.mustCreateSubscription(user1.ID, group.ID, nil)
  603. s.mustCreateSubscription(user2.ID, group.ID, nil)
  604. affected, err := s.repo.DeleteByGroupID(s.ctx, group.ID)
  605. s.Require().NoError(err, "DeleteByGroupID")
  606. s.Require().Equal(int64(2), affected)
  607. count, _ := s.repo.CountByGroupID(s.ctx, group.ID)
  608. s.Require().Zero(count)
  609. }
  610. // --- Combined scenario ---
  611. func (s *UserSubscriptionRepoSuite) TestActiveExpiredBoundaries_UsageAndReset_BatchUpdateExpiredStatus() {
  612. user := s.mustCreateUser("subr@example.com", service.RoleUser)
  613. groupActive := s.mustCreateGroup("g-subr-active")
  614. groupExpired := s.mustCreateGroup("g-subr-expired")
  615. active := s.mustCreateSubscription(user.ID, groupActive.ID, func(c *dbent.UserSubscriptionCreate) {
  616. c.SetExpiresAt(time.Now().Add(2 * time.Hour))
  617. })
  618. expiredActive := s.mustCreateSubscription(user.ID, groupExpired.ID, func(c *dbent.UserSubscriptionCreate) {
  619. c.SetExpiresAt(time.Now().Add(-2 * time.Hour))
  620. })
  621. got, err := s.repo.GetActiveByUserIDAndGroupID(s.ctx, user.ID, groupActive.ID)
  622. s.Require().NoError(err, "GetActiveByUserIDAndGroupID")
  623. s.Require().Equal(active.ID, got.ID, "expected active subscription")
  624. activateAt := time.Now().Add(-25 * time.Hour)
  625. s.Require().NoError(s.repo.ActivateWindows(s.ctx, active.ID, activateAt, activateAt), "ActivateWindows")
  626. s.Require().NoError(s.repo.IncrementUsage(s.ctx, active.ID, 1.25), "IncrementUsage")
  627. after, err := s.repo.GetByID(s.ctx, active.ID)
  628. s.Require().NoError(err, "GetByID")
  629. s.Require().InDelta(1.25, after.DailyUsageUSD, 1e-6)
  630. s.Require().InDelta(1.25, after.WeeklyUsageUSD, 1e-6)
  631. s.Require().InDelta(1.25, after.MonthlyUsageUSD, 1e-6)
  632. s.Require().NotNil(after.DailyWindowStart, "expected DailyWindowStart activated")
  633. s.Require().NotNil(after.WeeklyWindowStart, "expected WeeklyWindowStart activated")
  634. s.Require().NotNil(after.MonthlyWindowStart, "expected MonthlyWindowStart activated")
  635. resetAt := time.Now().Truncate(time.Microsecond) // truncate to microsecond for DB precision
  636. s.Require().NoError(s.repo.ResetDailyUsage(s.ctx, active.ID, after.DailyWindowStart, resetAt), "ResetDailyUsage")
  637. afterReset, err := s.repo.GetByID(s.ctx, active.ID)
  638. s.Require().NoError(err, "GetByID after reset")
  639. s.Require().InDelta(0.0, afterReset.DailyUsageUSD, 1e-6)
  640. s.Require().NotNil(afterReset.DailyWindowStart)
  641. s.Require().WithinDuration(resetAt, *afterReset.DailyWindowStart, time.Microsecond)
  642. affected, err := s.repo.BatchUpdateExpiredStatus(s.ctx)
  643. s.Require().NoError(err, "BatchUpdateExpiredStatus")
  644. s.Require().Equal(int64(1), affected, "expected 1 affected row")
  645. updated, err := s.repo.GetByID(s.ctx, expiredActive.ID)
  646. s.Require().NoError(err, "GetByID expired")
  647. s.Require().Equal(service.SubscriptionStatusExpired, updated.Status, "expected status expired")
  648. }
  649. // --- 软删除过滤测试 ---
  650. func (s *UserSubscriptionRepoSuite) TestIncrementUsage_SoftDeletedGroup() {
  651. user := s.mustCreateUser("softdeleted@test.com", service.RoleUser)
  652. group := s.mustCreateGroup("g-softdeleted")
  653. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  654. // 软删除分组
  655. _, err := s.client.Group.UpdateOneID(group.ID).SetDeletedAt(time.Now()).Save(s.ctx)
  656. s.Require().NoError(err, "soft delete group")
  657. // IncrementUsage 应该失败,因为分组已软删除
  658. err = s.repo.IncrementUsage(s.ctx, sub.ID, 1.0)
  659. s.Require().Error(err, "should fail for soft-deleted group")
  660. s.Require().ErrorIs(err, service.ErrSubscriptionNotFound)
  661. }
  662. func (s *UserSubscriptionRepoSuite) TestIncrementUsage_NotFound() {
  663. err := s.repo.IncrementUsage(s.ctx, 999999, 1.0)
  664. s.Require().Error(err, "should fail for non-existent subscription")
  665. s.Require().ErrorIs(err, service.ErrSubscriptionNotFound)
  666. }
  667. // --- nil 入参测试 ---
  668. func (s *UserSubscriptionRepoSuite) TestCreate_NilInput() {
  669. err := s.repo.Create(s.ctx, nil)
  670. s.Require().Error(err, "Create should fail with nil input")
  671. s.Require().ErrorIs(err, service.ErrSubscriptionNilInput)
  672. }
  673. func (s *UserSubscriptionRepoSuite) TestUpdate_NilInput() {
  674. err := s.repo.Update(s.ctx, nil)
  675. s.Require().Error(err, "Update should fail with nil input")
  676. s.Require().ErrorIs(err, service.ErrSubscriptionNilInput)
  677. }
  678. // --- 并发用量更新测试 ---
  679. func (s *UserSubscriptionRepoSuite) TestIncrementUsage_Concurrent() {
  680. user := s.mustCreateUser("concurrent@test.com", service.RoleUser)
  681. group := s.mustCreateGroup("g-concurrent")
  682. sub := s.mustCreateSubscription(user.ID, group.ID, nil)
  683. const numGoroutines = 10
  684. const incrementPerGoroutine = 1.5
  685. // 启动多个 goroutine 并发调用 IncrementUsage
  686. errCh := make(chan error, numGoroutines)
  687. for i := 0; i < numGoroutines; i++ {
  688. go func() {
  689. errCh <- s.repo.IncrementUsage(s.ctx, sub.ID, incrementPerGoroutine)
  690. }()
  691. }
  692. // 等待所有 goroutine 完成
  693. for i := 0; i < numGoroutines; i++ {
  694. err := <-errCh
  695. s.Require().NoError(err, "IncrementUsage should succeed")
  696. }
  697. // 验证累加结果正确
  698. got, err := s.repo.GetByID(s.ctx, sub.ID)
  699. s.Require().NoError(err)
  700. expectedUsage := float64(numGoroutines) * incrementPerGoroutine
  701. s.Require().InDelta(expectedUsage, got.DailyUsageUSD, 1e-6, "daily usage should be correctly accumulated")
  702. s.Require().InDelta(expectedUsage, got.WeeklyUsageUSD, 1e-6, "weekly usage should be correctly accumulated")
  703. s.Require().InDelta(expectedUsage, got.MonthlyUsageUSD, 1e-6, "monthly usage should be correctly accumulated")
  704. }
  705. func (s *UserSubscriptionRepoSuite) TestTxContext_RollbackIsolation() {
  706. baseClient := testEntClient(s.T())
  707. tx, err := baseClient.Tx(context.Background())
  708. s.Require().NoError(err, "begin tx")
  709. defer func() {
  710. if tx != nil {
  711. _ = tx.Rollback()
  712. }
  713. }()
  714. txCtx := dbent.NewTxContext(context.Background(), tx)
  715. suffix := fmt.Sprintf("%d", time.Now().UnixNano())
  716. userEnt, err := tx.Client().User.Create().
  717. SetEmail("tx-user-" + suffix + "@example.com").
  718. SetPasswordHash("test").
  719. Save(txCtx)
  720. s.Require().NoError(err, "create user in tx")
  721. groupEnt, err := tx.Client().Group.Create().
  722. SetName("tx-group-" + suffix).
  723. Save(txCtx)
  724. s.Require().NoError(err, "create group in tx")
  725. repo := NewUserSubscriptionRepository(baseClient)
  726. sub := &service.UserSubscription{
  727. UserID: userEnt.ID,
  728. GroupID: groupEnt.ID,
  729. ExpiresAt: time.Now().AddDate(0, 0, 30),
  730. Status: service.SubscriptionStatusActive,
  731. AssignedAt: time.Now(),
  732. Notes: "tx",
  733. }
  734. s.Require().NoError(repo.Create(txCtx, sub), "create subscription in tx")
  735. s.Require().NoError(repo.UpdateNotes(txCtx, sub.ID, "tx-note"), "update subscription in tx")
  736. s.Require().NoError(tx.Rollback(), "rollback tx")
  737. tx = nil
  738. _, err = repo.GetByID(context.Background(), sub.ID)
  739. s.Require().ErrorIs(err, service.ErrSubscriptionNotFound)
  740. }