proxy_repo_upstream_billing_probe_test.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. package repository
  2. import (
  3. "context"
  4. "errors"
  5. "regexp"
  6. "testing"
  7. "time"
  8. "github.com/DATA-DOG/go-sqlmock"
  9. dbent "github.com/Wei-Shaw/sub2api/ent"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/stretchr/testify/require"
  12. "entgo.io/ent/dialect"
  13. entsql "entgo.io/ent/dialect/sql"
  14. )
  15. func TestProxyUpdateInvalidatesBoundProbeSnapshotsAndEnqueuesOutboxAtomically(t *testing.T) {
  16. db, mock, err := sqlmock.New()
  17. require.NoError(t, err)
  18. t.Cleanup(func() { _ = db.Close() })
  19. client := dbent.NewClient(dbent.Driver(entsql.OpenDB(dialect.Postgres, db)))
  20. t.Cleanup(func() { _ = client.Close() })
  21. mock.ExpectBegin()
  22. mock.ExpectQuery(`(?s)` + regexp.QuoteMeta("SELECT protocol, host, port") + `.*` + regexp.QuoteMeta("FOR NO KEY UPDATE")).
  23. WithArgs(int64(9)).
  24. WillReturnRows(sqlmock.NewRows([]string{"protocol", "host", "port", "username", "password", "status"}).
  25. AddRow("http", "old.example", 8080, "user", "pass", service.StatusActive))
  26. mock.ExpectExec(`(?s)UPDATE "proxies" SET`).WillReturnResult(sqlmock.NewResult(0, 1))
  27. expectProxyUpdateReload(mock, 9, "new.example", "user", "pass")
  28. mock.ExpectQuery(`(?s)UPDATE accounts.*- 'upstream_billing_probe'.*- 'ollama_cloud_usage_snapshot'.*type = 'apikey'.*extra \? 'upstream_billing_probe'.*platform IN \('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek', 'minimax'\).*extra \? 'ollama_cloud_usage_snapshot'.*RETURNING id`).
  29. WithArgs(int64(9)).
  30. WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(int64(17)).AddRow(int64(18)))
  31. mock.ExpectExec(regexp.QuoteMeta("INSERT INTO scheduler_outbox (event_type, account_id, group_id, payload)")).
  32. WithArgs(service.SchedulerOutboxEventAccountBulkChanged, nil, nil, accountIDsPayloadMatcher{want: []int64{17, 18}}).
  33. WillReturnResult(sqlmock.NewResult(1, 1))
  34. mock.ExpectCommit()
  35. repo := newProxyRepositoryWithSQL(client, db)
  36. proxy := &service.Proxy{
  37. ID: 9,
  38. Name: "proxy",
  39. Protocol: "http",
  40. Host: "new.example",
  41. Port: 8080,
  42. Username: "user",
  43. Password: "pass",
  44. Status: service.StatusActive,
  45. }
  46. err = repo.Update(context.Background(), proxy)
  47. require.NoError(t, err)
  48. require.NoError(t, mock.ExpectationsWereMet())
  49. }
  50. func TestProxyUpdateRollsBackWhenProbeInvalidationOutboxFails(t *testing.T) {
  51. db, mock, err := sqlmock.New()
  52. require.NoError(t, err)
  53. t.Cleanup(func() { _ = db.Close() })
  54. client := dbent.NewClient(dbent.Driver(entsql.OpenDB(dialect.Postgres, db)))
  55. t.Cleanup(func() { _ = client.Close() })
  56. mock.ExpectBegin()
  57. mock.ExpectQuery(`(?s)` + regexp.QuoteMeta("SELECT protocol, host, port") + `.*` + regexp.QuoteMeta("FOR NO KEY UPDATE")).
  58. WithArgs(int64(9)).
  59. WillReturnRows(sqlmock.NewRows([]string{"protocol", "host", "port", "username", "password", "status"}).
  60. AddRow("http", "old.example", 8080, "", "", service.StatusActive))
  61. mock.ExpectExec(`(?s)UPDATE "proxies" SET`).WillReturnResult(sqlmock.NewResult(0, 1))
  62. expectProxyUpdateReload(mock, 9, "new.example", "", "")
  63. mock.ExpectQuery(`(?s)UPDATE accounts.*- 'upstream_billing_probe'.*- 'ollama_cloud_usage_snapshot'.*type = 'apikey'.*extra \? 'upstream_billing_probe'.*platform IN \('openai', 'anthropic', 'kimi', 'zhipu', 'deepseek', 'minimax'\).*extra \? 'ollama_cloud_usage_snapshot'.*RETURNING id`).
  64. WithArgs(int64(9)).
  65. WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(int64(17)))
  66. mock.ExpectExec(regexp.QuoteMeta("INSERT INTO scheduler_outbox (event_type, account_id, group_id, payload)")).
  67. WillReturnError(errors.New("outbox failed"))
  68. mock.ExpectRollback()
  69. repo := newProxyRepositoryWithSQL(client, db)
  70. proxy := &service.Proxy{ID: 9, Name: "proxy", Protocol: "http", Host: "new.example", Port: 8080, Status: service.StatusActive}
  71. err = repo.Update(context.Background(), proxy)
  72. require.EqualError(t, err, "outbox failed")
  73. require.NoError(t, mock.ExpectationsWereMet())
  74. }
  75. func TestProxyUpdateSkipsProbeInvalidationForNonIdentityChange(t *testing.T) {
  76. db, mock, err := sqlmock.New()
  77. require.NoError(t, err)
  78. t.Cleanup(func() { _ = db.Close() })
  79. client := dbent.NewClient(dbent.Driver(entsql.OpenDB(dialect.Postgres, db)))
  80. t.Cleanup(func() { _ = client.Close() })
  81. mock.ExpectBegin()
  82. mock.ExpectQuery(`(?s)` + regexp.QuoteMeta("SELECT protocol, host, port") + `.*` + regexp.QuoteMeta("FOR NO KEY UPDATE")).
  83. WithArgs(int64(9)).
  84. WillReturnRows(sqlmock.NewRows([]string{"protocol", "host", "port", "username", "password", "status"}).
  85. AddRow("http", "same.example", 8080, "", "", service.StatusActive))
  86. mock.ExpectExec(`(?s)UPDATE "proxies" SET`).WillReturnResult(sqlmock.NewResult(0, 1))
  87. expectProxyUpdateReload(mock, 9, "same.example", "", "")
  88. mock.ExpectCommit()
  89. repo := newProxyRepositoryWithSQL(client, db)
  90. proxy := &service.Proxy{ID: 9, Name: "renamed", Protocol: "http", Host: "same.example", Port: 8080, Status: service.StatusActive}
  91. err = repo.Update(context.Background(), proxy)
  92. require.NoError(t, err)
  93. require.NoError(t, mock.ExpectationsWereMet())
  94. }
  95. func expectProxyUpdateReload(mock sqlmock.Sqlmock, id int64, host, username, password string) {
  96. now := time.Now()
  97. mock.ExpectQuery(`(?s)SELECT .* FROM "proxies" WHERE "id" = \$1`).
  98. WithArgs(id).
  99. WillReturnRows(sqlmock.NewRows([]string{
  100. "id", "created_at", "updated_at", "deleted_at", "name", "protocol", "host", "port",
  101. "username", "password", "status", "expires_at", "fallback_mode", "backup_proxy_id", "expiry_warn_days",
  102. }).AddRow(
  103. id, now, now, nil, "proxy", "http", host, 8080,
  104. username, password, service.StatusActive, nil, service.FallbackModeNone, nil, 0,
  105. ))
  106. }
  107. func TestEnqueueProxyAccountChangesChunksLargePayloads(t *testing.T) {
  108. db, mock, err := sqlmock.New()
  109. require.NoError(t, err)
  110. t.Cleanup(func() { _ = db.Close() })
  111. accountIDs := make([]int64, 1001)
  112. for i := range accountIDs {
  113. accountIDs[i] = int64(i + 1)
  114. }
  115. for start := 0; start < len(accountIDs); start += proxyProbeOutboxAccountChunkSize {
  116. end := start + proxyProbeOutboxAccountChunkSize
  117. if end > len(accountIDs) {
  118. end = len(accountIDs)
  119. }
  120. mock.ExpectExec(regexp.QuoteMeta("INSERT INTO scheduler_outbox (event_type, account_id, group_id, payload)")).
  121. WithArgs(service.SchedulerOutboxEventAccountBulkChanged, nil, nil, accountIDsPayloadMatcher{want: accountIDs[start:end]}).
  122. WillReturnResult(sqlmock.NewResult(1, 1))
  123. }
  124. err = enqueueProxyProbeAccountChanges(context.Background(), db, accountIDs)
  125. require.NoError(t, err)
  126. require.NoError(t, mock.ExpectationsWereMet())
  127. }