proxy_repo_integration_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. //go:build integration
  2. package repository
  3. import (
  4. "context"
  5. "testing"
  6. "time"
  7. dbent "github.com/Wei-Shaw/sub2api/ent"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/stretchr/testify/suite"
  11. )
  12. type ProxyRepoSuite struct {
  13. suite.Suite
  14. ctx context.Context
  15. tx *dbent.Tx
  16. repo *proxyRepository
  17. }
  18. func (s *ProxyRepoSuite) SetupTest() {
  19. s.ctx = context.Background()
  20. tx := testEntTx(s.T())
  21. s.tx = tx
  22. s.repo = newProxyRepositoryWithSQL(tx.Client(), tx)
  23. }
  24. func TestProxyRepoSuite(t *testing.T) {
  25. suite.Run(t, new(ProxyRepoSuite))
  26. }
  27. // --- Create / GetByID / Update / Delete ---
  28. func (s *ProxyRepoSuite) TestCreate() {
  29. proxy := &service.Proxy{
  30. Name: "test-create",
  31. Protocol: "http",
  32. Host: "127.0.0.1",
  33. Port: 8080,
  34. Status: service.StatusActive,
  35. }
  36. err := s.repo.Create(s.ctx, proxy)
  37. s.Require().NoError(err, "Create")
  38. s.Require().NotZero(proxy.ID, "expected ID to be set")
  39. got, err := s.repo.GetByID(s.ctx, proxy.ID)
  40. s.Require().NoError(err, "GetByID")
  41. s.Require().Equal("test-create", got.Name)
  42. }
  43. func (s *ProxyRepoSuite) TestGetByID_NotFound() {
  44. _, err := s.repo.GetByID(s.ctx, 999999)
  45. s.Require().Error(err, "expected error for non-existent ID")
  46. }
  47. func (s *ProxyRepoSuite) TestUpdate() {
  48. proxy := &service.Proxy{
  49. Name: "original",
  50. Protocol: "http",
  51. Host: "127.0.0.1",
  52. Port: 8080,
  53. Status: service.StatusActive,
  54. }
  55. s.Require().NoError(s.repo.Create(s.ctx, proxy))
  56. proxy.Name = "updated"
  57. err := s.repo.Update(s.ctx, proxy)
  58. s.Require().NoError(err, "Update")
  59. got, err := s.repo.GetByID(s.ctx, proxy.ID)
  60. s.Require().NoError(err, "GetByID after update")
  61. s.Require().Equal("updated", got.Name)
  62. }
  63. func (s *ProxyRepoSuite) TestDelete() {
  64. proxy := &service.Proxy{
  65. Name: "to-delete",
  66. Protocol: "http",
  67. Host: "127.0.0.1",
  68. Port: 8080,
  69. Status: service.StatusActive,
  70. }
  71. s.Require().NoError(s.repo.Create(s.ctx, proxy))
  72. err := s.repo.Delete(s.ctx, proxy.ID)
  73. s.Require().NoError(err, "Delete")
  74. _, err = s.repo.GetByID(s.ctx, proxy.ID)
  75. s.Require().Error(err, "expected error after delete")
  76. }
  77. // --- List / ListWithFilters ---
  78. func (s *ProxyRepoSuite) TestList() {
  79. s.mustCreateProxy(&service.Proxy{Name: "p1", Protocol: "http", Host: "127.0.0.1", Port: 8080, Status: service.StatusActive})
  80. s.mustCreateProxy(&service.Proxy{Name: "p2", Protocol: "http", Host: "127.0.0.1", Port: 8081, Status: service.StatusActive})
  81. proxies, page, err := s.repo.List(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10})
  82. s.Require().NoError(err, "List")
  83. s.Require().Len(proxies, 2)
  84. s.Require().Equal(int64(2), page.Total)
  85. }
  86. func (s *ProxyRepoSuite) TestListWithFilters_Protocol() {
  87. s.mustCreateProxy(&service.Proxy{Name: "p1", Protocol: "http", Host: "127.0.0.1", Port: 8080, Status: service.StatusActive})
  88. s.mustCreateProxy(&service.Proxy{Name: "p2", Protocol: "socks5", Host: "127.0.0.1", Port: 8081, Status: service.StatusActive})
  89. proxies, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, "socks5", "", "")
  90. s.Require().NoError(err)
  91. s.Require().Len(proxies, 1)
  92. s.Require().Equal("socks5", proxies[0].Protocol)
  93. }
  94. func (s *ProxyRepoSuite) TestListWithFilters_Status() {
  95. s.mustCreateProxy(&service.Proxy{Name: "p1", Protocol: "http", Host: "127.0.0.1", Port: 8080, Status: service.StatusActive})
  96. s.mustCreateProxy(&service.Proxy{Name: "p2", Protocol: "http", Host: "127.0.0.1", Port: 8081, Status: service.StatusDisabled})
  97. proxies, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, "", service.StatusDisabled, "")
  98. s.Require().NoError(err)
  99. s.Require().Len(proxies, 1)
  100. s.Require().Equal(service.StatusDisabled, proxies[0].Status)
  101. }
  102. func (s *ProxyRepoSuite) TestListWithFilters_Search() {
  103. s.mustCreateProxy(&service.Proxy{Name: "production-proxy", Protocol: "http", Host: "127.0.0.1", Port: 8080, Status: service.StatusActive})
  104. s.mustCreateProxy(&service.Proxy{Name: "dev-proxy", Protocol: "http", Host: "127.0.0.1", Port: 8081, Status: service.StatusActive})
  105. proxies, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}, "", "", "prod")
  106. s.Require().NoError(err)
  107. s.Require().Len(proxies, 1)
  108. s.Require().Contains(proxies[0].Name, "production")
  109. }
  110. // --- ListActive ---
  111. func (s *ProxyRepoSuite) TestListActive() {
  112. s.mustCreateProxy(&service.Proxy{Name: "active1", Protocol: "http", Host: "127.0.0.1", Port: 8080, Status: service.StatusActive})
  113. s.mustCreateProxy(&service.Proxy{Name: "inactive1", Protocol: "http", Host: "127.0.0.1", Port: 8081, Status: service.StatusDisabled})
  114. proxies, err := s.repo.ListActive(s.ctx)
  115. s.Require().NoError(err, "ListActive")
  116. s.Require().Len(proxies, 1)
  117. s.Require().Equal("active1", proxies[0].Name)
  118. }
  119. // --- ExistsByHostPortAuth ---
  120. func (s *ProxyRepoSuite) TestExistsByHostPortAuth() {
  121. s.mustCreateProxy(&service.Proxy{
  122. Name: "p1",
  123. Protocol: "http",
  124. Host: "1.2.3.4",
  125. Port: 8080,
  126. Username: "user",
  127. Password: "pass",
  128. Status: service.StatusActive,
  129. })
  130. exists, err := s.repo.ExistsByHostPortAuth(s.ctx, "1.2.3.4", 8080, "user", "pass")
  131. s.Require().NoError(err, "ExistsByHostPortAuth")
  132. s.Require().True(exists)
  133. notExists, err := s.repo.ExistsByHostPortAuth(s.ctx, "1.2.3.4", 8080, "wrong", "creds")
  134. s.Require().NoError(err)
  135. s.Require().False(notExists)
  136. }
  137. func (s *ProxyRepoSuite) TestExistsByHostPortAuth_NoAuth() {
  138. s.mustCreateProxy(&service.Proxy{
  139. Name: "p-noauth",
  140. Protocol: "http",
  141. Host: "5.6.7.8",
  142. Port: 8081,
  143. Username: "",
  144. Password: "",
  145. Status: service.StatusActive,
  146. })
  147. exists, err := s.repo.ExistsByHostPortAuth(s.ctx, "5.6.7.8", 8081, "", "")
  148. s.Require().NoError(err)
  149. s.Require().True(exists)
  150. }
  151. // --- CountAccountsByProxyID ---
  152. func (s *ProxyRepoSuite) TestCountAccountsByProxyID() {
  153. proxy := s.mustCreateProxy(&service.Proxy{Name: "p-count", Protocol: "http", Host: "127.0.0.1", Port: 8080, Status: service.StatusActive})
  154. s.mustInsertAccount("a1", &proxy.ID)
  155. s.mustInsertAccount("a2", &proxy.ID)
  156. s.mustInsertAccount("a3", nil) // no proxy
  157. count, err := s.repo.CountAccountsByProxyID(s.ctx, proxy.ID)
  158. s.Require().NoError(err, "CountAccountsByProxyID")
  159. s.Require().Equal(int64(2), count)
  160. }
  161. func (s *ProxyRepoSuite) TestCountAccountsByProxyID_Zero() {
  162. proxy := s.mustCreateProxy(&service.Proxy{Name: "p-zero", Protocol: "http", Host: "127.0.0.1", Port: 8080, Status: service.StatusActive})
  163. count, err := s.repo.CountAccountsByProxyID(s.ctx, proxy.ID)
  164. s.Require().NoError(err)
  165. s.Require().Zero(count)
  166. }
  167. // --- GetAccountCountsForProxies ---
  168. func (s *ProxyRepoSuite) TestGetAccountCountsForProxies() {
  169. p1 := s.mustCreateProxy(&service.Proxy{Name: "p1", Protocol: "http", Host: "127.0.0.1", Port: 8080, Status: service.StatusActive})
  170. p2 := s.mustCreateProxy(&service.Proxy{Name: "p2", Protocol: "http", Host: "127.0.0.1", Port: 8081, Status: service.StatusActive})
  171. s.mustInsertAccount("a1", &p1.ID)
  172. s.mustInsertAccount("a2", &p1.ID)
  173. s.mustInsertAccount("a3", &p2.ID)
  174. counts, err := s.repo.GetAccountCountsForProxies(s.ctx)
  175. s.Require().NoError(err, "GetAccountCountsForProxies")
  176. s.Require().Equal(int64(2), counts[p1.ID])
  177. s.Require().Equal(int64(1), counts[p2.ID])
  178. }
  179. func (s *ProxyRepoSuite) TestGetAccountCountsForProxies_Empty() {
  180. counts, err := s.repo.GetAccountCountsForProxies(s.ctx)
  181. s.Require().NoError(err)
  182. s.Require().Empty(counts)
  183. }
  184. // --- ListActiveWithAccountCount ---
  185. func (s *ProxyRepoSuite) TestListActiveWithAccountCount() {
  186. base := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
  187. p1 := s.mustCreateProxyWithTimes("p1", service.StatusActive, base.Add(-1*time.Hour))
  188. p2 := s.mustCreateProxyWithTimes("p2", service.StatusActive, base)
  189. s.mustCreateProxyWithTimes("p3-inactive", service.StatusDisabled, base.Add(1*time.Hour))
  190. s.mustInsertAccount("a1", &p1.ID)
  191. s.mustInsertAccount("a2", &p1.ID)
  192. s.mustInsertAccount("a3", &p2.ID)
  193. withCounts, err := s.repo.ListActiveWithAccountCount(s.ctx)
  194. s.Require().NoError(err, "ListActiveWithAccountCount")
  195. s.Require().Len(withCounts, 2, "expected 2 active proxies")
  196. // Sorted by created_at DESC, so p2 first
  197. s.Require().Equal(p2.ID, withCounts[0].ID)
  198. s.Require().Equal(int64(1), withCounts[0].AccountCount)
  199. s.Require().Equal(p1.ID, withCounts[1].ID)
  200. s.Require().Equal(int64(2), withCounts[1].AccountCount)
  201. }
  202. // --- Combined original test ---
  203. func (s *ProxyRepoSuite) TestExistsByHostPortAuth_And_AccountCountAggregates() {
  204. p1 := s.mustCreateProxy(&service.Proxy{Name: "p1", Protocol: "http", Host: "1.2.3.4", Port: 8080, Username: "u", Password: "p", Status: service.StatusActive})
  205. p2 := s.mustCreateProxy(&service.Proxy{Name: "p2", Protocol: "http", Host: "5.6.7.8", Port: 8081, Username: "", Password: "", Status: service.StatusActive})
  206. exists, err := s.repo.ExistsByHostPortAuth(s.ctx, "1.2.3.4", 8080, "u", "p")
  207. s.Require().NoError(err, "ExistsByHostPortAuth")
  208. s.Require().True(exists, "expected proxy to exist")
  209. s.mustInsertAccount("a1", &p1.ID)
  210. s.mustInsertAccount("a2", &p1.ID)
  211. s.mustInsertAccount("a3", &p2.ID)
  212. count1, err := s.repo.CountAccountsByProxyID(s.ctx, p1.ID)
  213. s.Require().NoError(err, "CountAccountsByProxyID")
  214. s.Require().Equal(int64(2), count1, "expected 2 accounts for p1")
  215. counts, err := s.repo.GetAccountCountsForProxies(s.ctx)
  216. s.Require().NoError(err, "GetAccountCountsForProxies")
  217. s.Require().Equal(int64(2), counts[p1.ID])
  218. s.Require().Equal(int64(1), counts[p2.ID])
  219. withCounts, err := s.repo.ListActiveWithAccountCount(s.ctx)
  220. s.Require().NoError(err, "ListActiveWithAccountCount")
  221. s.Require().Len(withCounts, 2, "expected 2 proxies")
  222. for _, pc := range withCounts {
  223. switch pc.ID {
  224. case p1.ID:
  225. s.Require().Equal(int64(2), pc.AccountCount, "p1 count mismatch")
  226. case p2.ID:
  227. s.Require().Equal(int64(1), pc.AccountCount, "p2 count mismatch")
  228. default:
  229. s.Require().Fail("unexpected proxy id", pc.ID)
  230. }
  231. }
  232. }
  233. func (s *ProxyRepoSuite) mustCreateProxy(p *service.Proxy) *service.Proxy {
  234. s.T().Helper()
  235. s.Require().NoError(s.repo.Create(s.ctx, p), "create proxy")
  236. return p
  237. }
  238. func (s *ProxyRepoSuite) mustCreateProxyWithTimes(name, status string, createdAt time.Time) *service.Proxy {
  239. s.T().Helper()
  240. // Use the repository create for standard fields, then update timestamps via raw SQL to keep deterministic ordering.
  241. p := s.mustCreateProxy(&service.Proxy{
  242. Name: name,
  243. Protocol: "http",
  244. Host: "127.0.0.1",
  245. Port: 8080,
  246. Status: status,
  247. })
  248. _, err := s.tx.ExecContext(s.ctx, "UPDATE proxies SET created_at = $1, updated_at = $1 WHERE id = $2", createdAt, p.ID)
  249. s.Require().NoError(err, "update proxy timestamps")
  250. return p
  251. }
  252. func (s *ProxyRepoSuite) mustInsertAccount(name string, proxyID *int64) {
  253. s.T().Helper()
  254. var pid any
  255. if proxyID != nil {
  256. pid = *proxyID
  257. }
  258. _, err := s.tx.ExecContext(
  259. s.ctx,
  260. "INSERT INTO accounts (name, platform, type, proxy_id) VALUES ($1, $2, $3, $4)",
  261. name,
  262. service.PlatformAnthropic,
  263. service.AccountTypeOAuth,
  264. pid,
  265. )
  266. s.Require().NoError(err, "insert account")
  267. }