Files
sub2api/backend/internal/repository/proxy_expiry_integration_test.go
T
DaydreamCoding af19d44327 feat(proxies): 代理有效期与失败回退
- schema/迁移: 代理有效期、提醒天数、失败回退配置 + 账号 fallback 来源字段
- service/repo/DTO/handler: CRUD 透传新字段 + 校验
- fallback 目标解析纯函数(链式解析 + 环检测 + 兜底)
- SweepExpiredProxies 到期改投账号 + outbox 失效
- ProxyExpiryService 后台到期扫描任务 + wire 注册
- 账号侧手动回切原代理 + fallback 来源徽章/按钮
- 前端: 创建/编辑表单、列表到期徽章、类型/API/i18n
- ops 告警: proxy_expired_count / proxy_expiring_soon_count 指标
- 导入导出携带有效期/回退字段(备用按 name 映射)
- 补全测试 stub + 集成测试 + review 问题修复
2026-06-08 00:01:30 +08:00

105 lines
3.4 KiB
Go

//go:build integration
package repository
import (
"context"
"testing"
"time"
dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/stretchr/testify/suite"
)
type ProxyExpirySuite struct {
suite.Suite
ctx context.Context
tx *dbent.Tx
repo *proxyRepository
}
func (s *ProxyExpirySuite) SetupTest() {
s.ctx = context.Background()
s.tx = testEntTx(s.T())
s.repo = newProxyRepositoryWithSQL(s.tx.Client(), s.tx)
}
func TestProxyExpirySuite(t *testing.T) { suite.Run(t, new(ProxyExpirySuite)) }
func (s *ProxyExpirySuite) mkProxy(name, mode string, expiresAt *time.Time, backupID *int64) int64 {
p := &service.Proxy{Name: name, Protocol: "http", Host: "127.0.0.1", Port: 8080,
Status: service.StatusActive, FallbackMode: mode, ExpiryWarnDays: 7,
ExpiresAt: expiresAt, BackupProxyID: backupID}
s.Require().NoError(s.repo.Create(s.ctx, p))
return p.ID
}
func (s *ProxyExpirySuite) mkAccountWithProxy(proxyID int64) int64 {
var id int64
err := scanSingleRow(s.ctx, s.tx, `
INSERT INTO accounts (name, platform, type, credentials, extra, status, proxy_id, created_at, updated_at)
VALUES ($1,'claude','api','{}','{}','active',$2,NOW(),NOW()) RETURNING id`,
[]any{"acc-" + time.Now().Format("150405.000000"), proxyID}, &id)
s.Require().NoError(err)
return id
}
func (s *ProxyExpirySuite) accountProxyID(id int64) *int64 {
var pid *int64
err := scanSingleRow(s.ctx, s.tx, `SELECT proxy_id FROM accounts WHERE id=$1`, []any{id}, &pid)
s.Require().NoError(err)
return pid
}
func (s *ProxyExpirySuite) TestSweep_DirectMode() {
past := time.Now().Add(-time.Hour)
pid := s.mkProxy("p-direct", service.FallbackModeDirect, &past, nil)
aid := s.mkAccountWithProxy(pid)
changed, err := s.repo.SweepExpiredProxies(s.ctx, time.Now())
s.Require().NoError(err)
s.Require().GreaterOrEqual(changed, int64(1))
got, _ := s.repo.GetByID(s.ctx, pid)
s.Require().Equal(service.StatusExpired, got.Status)
s.Require().Nil(s.accountProxyID(aid))
var origin *int64
err = scanSingleRow(s.ctx, s.tx, `SELECT proxy_fallback_origin_id FROM accounts WHERE id=$1`, []any{aid}, &origin)
s.Require().NoError(err)
s.Require().NotNil(origin)
s.Require().Equal(pid, *origin)
}
func (s *ProxyExpirySuite) TestSweep_ProxyMode_Healthy() {
future := time.Now().Add(24 * time.Hour)
past := time.Now().Add(-time.Hour)
backup := s.mkProxy("p-backup", service.FallbackModeNone, &future, nil)
pid := s.mkProxy("p-main", service.FallbackModeProxy, &past, &backup)
aid := s.mkAccountWithProxy(pid)
_, err := s.repo.SweepExpiredProxies(s.ctx, time.Now())
s.Require().NoError(err)
s.Require().Equal(backup, *s.accountProxyID(aid))
var origin *int64
err = scanSingleRow(s.ctx, s.tx, `SELECT proxy_fallback_origin_id FROM accounts WHERE id=$1`, []any{aid}, &origin)
s.Require().NoError(err)
s.Require().NotNil(origin)
s.Require().Equal(pid, *origin)
}
func (s *ProxyExpirySuite) TestSweep_NoneMode_KeepsAccount() {
past := time.Now().Add(-time.Hour)
pid := s.mkProxy("p-none", service.FallbackModeNone, &past, nil)
aid := s.mkAccountWithProxy(pid)
_, err := s.repo.SweepExpiredProxies(s.ctx, time.Now())
s.Require().NoError(err)
got, _ := s.repo.GetByID(s.ctx, pid)
s.Require().Equal(service.StatusExpired, got.Status)
s.Require().Equal(pid, *s.accountProxyID(aid))
var origin *int64
err = scanSingleRow(s.ctx, s.tx, `SELECT proxy_fallback_origin_id FROM accounts WHERE id=$1`, []any{aid}, &origin)
s.Require().NoError(err)
s.Require().Nil(origin)
}