package services import ( "context" "encoding/json" "fmt" "platform/pkg/env" "platform/pkg/remote" "platform/pkg/testutil" "platform/web/models" "strings" "testing" "time" "github.com/gofiber/fiber/v2/middleware/requestid" ) func Test_genPassPair(t *testing.T) { tests := []struct { name string }{ { name: "正常生成随机用户名和密码", }, { name: "多次调用生成不同的值", }, } // 第一个测试:检查生成的用户名和密码是否有效 t.Run(tests[0].name, func(t *testing.T) { username, password := genPassPair() if username == "" { t.Errorf("genPassPair() username is empty") } if password == "" { t.Errorf("genPassPair() password is empty") } }) // 第二个测试:确保多次调用生成不同的值 t.Run(tests[1].name, func(t *testing.T) { username1, password1 := genPassPair() username2, password2 := genPassPair() if username1 == username2 { t.Errorf("genPassPair() generated the same username twice: %v", username1) } if password1 == password2 { t.Errorf("genPassPair() generated the same password twice: %v", password1) } }) } func Test_chKey(t *testing.T) { type args struct { channel *models.Channel } tests := []struct { name string args args want string }{ { name: "ID为1的通道", args: args{ channel: &models.Channel{ ID: 1, }, }, want: "channel:1", }, { name: "ID为100的通道", args: args{ channel: &models.Channel{ ID: 100, }, }, want: "channel:100", }, { name: "ID为0的通道", args: args{ channel: &models.Channel{ ID: 0, }, }, want: "channel:0", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := chKey(tt.args.channel); got != tt.want { t.Errorf("chKey() = %v, want %v", got, tt.want) } }) } } func Test_cache(t *testing.T) { mr := testutil.SetupRedisTest(t) type args struct { ctx context.Context channels []*models.Channel } // 准备测试数据 now := time.Now() expiration := now.Add(24 * time.Hour) testChannels := []*models.Channel{ { ID: 1, UserID: 100, ProxyID: 10, ProxyPort: 8080, Protocol: "http", Expiration: expiration, }, { ID: 2, UserID: 101, ProxyID: 11, ProxyPort: 8081, Protocol: "socks5", Expiration: expiration, }, } tests := []struct { name string args args wantErr bool }{ { name: "正常缓存多个通道", args: args{ ctx: context.Background(), channels: testChannels, }, wantErr: false, }, { name: "空通道列表", args: args{ ctx: context.Background(), channels: []*models.Channel{}, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mr.FlushAll() // 清空 Redis 数据 if err := cache(tt.args.ctx, tt.args.channels); (err != nil) != tt.wantErr { t.Errorf("cache() error = %v, wantErr %v", err, tt.wantErr) return } // 验证缓存结果 if len(tt.args.channels) > 0 { for _, channel := range tt.args.channels { key := fmt.Sprintf("channel:%d", channel.ID) if !mr.Exists(key) { t.Errorf("缓存未包含通道键 %s", key) } else { // 验证缓存的数据是否正确 data, _ := mr.Get(key) var cachedChannel models.Channel err := json.Unmarshal([]byte(data), &cachedChannel) if err != nil { t.Errorf("无法解析缓存数据: %v", err) } if cachedChannel.ID != channel.ID { t.Errorf("缓存数据不匹配: 期望 ID %d, 得到 %d", channel.ID, cachedChannel.ID) } } } // 验证是否设置了过期时间 for _, channel := range tt.args.channels { key := fmt.Sprintf("channel:%d", channel.ID) ttl := mr.TTL(key) if ttl <= 0 { t.Errorf("键 %s 没有设置过期时间", key) } } // 验证是否添加了有序集合 if !mr.Exists("tasks:channel") { t.Errorf("ZAdd未创建有序集合 tasks:channel") } } }) } } func Test_deleteCache(t *testing.T) { mr := testutil.SetupRedisTest(t) type args struct { ctx context.Context channels []*models.Channel } // 准备测试数据 testChannels := []*models.Channel{ {ID: 1}, {ID: 2}, {ID: 3}, } ctx := context.Background() tests := []struct { name string args args wantErr bool }{ { name: "正常删除多个通道缓存", args: args{ ctx: ctx, channels: testChannels, }, wantErr: false, }, { name: "空通道列表", args: args{ ctx: ctx, channels: []*models.Channel{}, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mr.FlushAll() // 清空 Redis 数据 // 预先设置缓存数据 for _, channel := range testChannels { key := fmt.Sprintf("channel:%d", channel.ID) data, _ := json.Marshal(channel) mr.Set(key, string(data)) mr.SetTTL(key, 1*time.Hour) // 设置1小时的过期时间 } if err := deleteCache(tt.args.ctx, tt.args.channels); (err != nil) != tt.wantErr { t.Errorf("deleteCache() error = %v, wantErr %v", err, tt.wantErr) return } // 验证删除结果 for _, channel := range tt.args.channels { key := fmt.Sprintf("channel:%d", channel.ID) if mr.Exists(key) { t.Errorf("通道键 %s 未被删除", key) } } }) } } func Test_channelService_CreateChannel(t *testing.T) { mr := testutil.SetupRedisTest(t) db := testutil.SetupDBTest(t) mc := testutil.SetupCloudClientMock(t) env.DebugExternalChange = false type args struct { ctx context.Context auth *AuthContext resourceId int32 protocol ChannelProtocol authType ChannelAuthType count int nodeFilter []NodeFilterConfig } // 准备测试数据 ctx := context.WithValue(context.Background(), requestid.ConfigDefault.ContextKey, "test-request-id") var adminAuth = &AuthContext{Payload: Payload{Id: 100, Type: PayloadAdmin}} var userAuth = &AuthContext{Payload: Payload{Id: 101, Type: PayloadUser}} var user = &models.User{ ID: 101, Phone: "12312341234", } db.Create(user) var whitelists = []*models.Whitelist{ {ID: 1, UserID: 101, Host: "123.123.123.123"}, {ID: 2, UserID: 101, Host: "456.456.456.456"}, {ID: 3, UserID: 101, Host: "789.789.789.789"}, } db.Create(whitelists) var resource = &models.Resource{ ID: 1, UserID: 101, Active: true, } db.Create(resource) var resourcePss = &models.ResourcePss{ ID: 1, ResourceID: 1, Type: 1, Live: 180, Expire: time.Now().AddDate(1, 0, 0), DailyLimit: 10000, } db.Create(resourcePss) var proxy = &models.Proxy{ ID: 1, Version: 1, Name: "test-proxy", Host: "111.111.111.111", Type: 1, Secret: "test:secret", } db.Create(proxy) mc.AutoQueryMock = func() (remote.CloudConnectResp, error) { return remote.CloudConnectResp{ "test-proxy": []remote.AutoConfig{ {Province: "河南", City: "郑州", Isp: "电信", Count: 10}, }, }, nil } var clearDb = func() { db.Exec("delete from channel where true") db.Exec("update resource_pss set daily_used = 0, daily_last = null, used = 0 where true") } tests := []struct { name string args args setup func() want []string wantErr bool wantErrContains string checkCache func(channels []models.Channel) error }{ { name: "用户创建HTTP密码通道", args: args{ ctx: ctx, auth: userAuth, resourceId: 1, protocol: ProtocolHTTP, authType: ChannelAuthTypePass, count: 3, nodeFilter: []NodeFilterConfig{{Prov: "河南", City: "郑州", Isp: "电信"}}, }, want: []string{ "http://111.111.111.111:10000", "http://111.111.111.111:10001", "http://111.111.111.111:10002", }, }, { name: "用户创建HTTP白名单通道", args: args{ ctx: ctx, auth: userAuth, resourceId: 1, protocol: ProtocolHTTP, authType: ChannelAuthTypeIp, count: 2, }, want: []string{ "http://111.111.111.111:10000", "http://111.111.111.111:10001", }, }, { name: "管理员创建SOCKS5密码通道", args: args{ ctx: ctx, auth: adminAuth, resourceId: 1, protocol: ProtocolSocks5, authType: ChannelAuthTypePass, count: 2, }, want: []string{ "socks5://111.111.111.111:10000", "socks5://111.111.111.111:10001", }, }, { name: "套餐不存在", args: args{ ctx: ctx, auth: userAuth, resourceId: 999, protocol: ProtocolHTTP, authType: ChannelAuthTypeIp, count: 1, }, wantErr: true, wantErrContains: "套餐不存在", }, { name: "套餐没有权限", args: args{ ctx: ctx, auth: userAuth, resourceId: 2, protocol: ProtocolHTTP, authType: ChannelAuthTypeIp, count: 1, }, wantErr: true, wantErrContains: "无权限访问", }, { name: "套餐配额不足", args: args{ ctx: ctx, auth: &AuthContext{ Payload: Payload{ Type: PayloadUser, Id: 100, }, }, resourceId: 2, protocol: ProtocolHTTP, authType: ChannelAuthTypeIp, count: 10, }, setup: func() { // 创建一个配额几乎用完的资源包 resource2 := models.Resource{ ID: 2, UserID: 101, Active: true, } resourcePss2 := models.ResourcePss{ ID: 1, ResourceID: 1, Type: 2, Quota: 100, Used: 91, Live: 180, DailyLimit: 10000, } db.Create(&resource2).Create(&resourcePss2) }, wantErr: true, wantErrContains: "套餐配额不足", }, { name: "端口数量达到上限", args: args{ ctx: ctx, auth: userAuth, resourceId: 1, protocol: ProtocolHTTP, authType: ChannelAuthTypeIp, count: 1, }, setup: func() { // 创建大量占用端口的通道 for i := 10000; i < 20000; i++ { channel := models.Channel{ ProxyID: 1, ProxyPort: int32(i), UserID: 101, } db.Create(&channel) } }, wantErr: true, wantErrContains: "端口数量不足", }, // todo 跨天用量更新 // todo 多地区混杂条件提取 } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mr.FlushAll() clearDb() if tt.setup != nil { tt.setup() } s := &channelService{} got, err := s.CreateChannel(tt.args.ctx, tt.args.auth, tt.args.resourceId, tt.args.protocol, tt.args.authType, tt.args.count, tt.args.nodeFilter...) // 检查错误或结果 if tt.wantErr { if err == nil { t.Errorf("CreateChannel() 应当返回错误") return } if tt.wantErrContains != "" && !strings.Contains(err.Error(), tt.wantErrContains) { t.Errorf("CreateChannel() 错误 = %v, 应包含 %v", err, tt.wantErrContains) } return } if err != nil { t.Errorf("CreateChannel() 错误 = %v, wantErr %v", err, tt.wantErr) return } if len(got) != len(tt.want) { t.Errorf("CreateChannel() 返回长度 = %v, want %v", len(got), len(tt.want)) return } // 查询创建的通道 var channels []models.Channel db.Where( "user_id = ? and proxy_id = ?", userAuth.Payload.Id, proxy.ID, ).Find(&channels) if len(channels) != 2 { t.Errorf("期望创建2个通道,但是创建了%d个", len(channels)) } // 检查Redis缓存 for _, ch := range channels { key := fmt.Sprintf("channel:%d", ch.ID) if !mr.Exists(key) { t.Errorf("Redis缓存中应有键 %s", key) } } if tt.checkCache != nil { var err = tt.checkCache(channels) if err != nil { t.Errorf("检查缓存失败: %v", err) } } }) } } func Test_channelService_RemoveChannels(t *testing.T) { mr := testutil.SetupRedisTest(t) db := testutil.SetupDBTest(t) mg := testutil.SetupGatewayClientMock(t) env.DebugExternalChange = false type args struct { ctx context.Context auth *AuthContext id []int32 } // 准备测试数据 ctx := context.WithValue(context.Background(), requestid.ConfigDefault.ContextKey, "test-request-id") tests := []struct { name string args args setup func() wantErr bool wantErrContains string checkCache func(t *testing.T) }{ { name: "管理员删除多个通道", args: args{ ctx: ctx, auth: &AuthContext{ Payload: Payload{ Type: PayloadAdmin, Id: 1, }, }, id: []int32{1, 2, 3}, }, setup: func() { // 预设 Redis 缓存 mr.FlushAll() for _, id := range []int32{1, 2, 3} { key := fmt.Sprintf("channel:%d", id) channel := models.Channel{ID: id, UserID: 100} data, _ := json.Marshal(channel) mr.Set(key, string(data)) } // 清空数据库表 db.Exec("delete from channel") db.Exec("delete from proxy") // 创建代理 proxies := []models.Proxy{ {ID: 1, Name: "proxy1", Host: "proxy1.example.com", Secret: "key:secret", Type: 1}, {ID: 2, Name: "proxy2", Host: "proxy2.example.com", Secret: "key:secret", Type: 1}, } for _, p := range proxies { db.Create(&p) } // 创建通道 channels := []models.Channel{ {ID: 1, UserID: 100, ProxyID: 1, ProxyPort: 10001, Protocol: "http", Expiration: time.Now().Add(24 * time.Hour)}, {ID: 2, UserID: 100, ProxyID: 1, ProxyPort: 10002, Protocol: "http", Expiration: time.Now().Add(24 * time.Hour)}, {ID: 3, UserID: 101, ProxyID: 2, ProxyPort: 10001, Protocol: "socks5", Expiration: time.Now().Add(24 * time.Hour)}, } for _, c := range channels { db.Create(&c) } }, checkCache: func(t *testing.T) { // 检查通道是否被软删除 var count int64 db.Model(&models.Channel{}).Where("id IN ? AND deleted_at IS NULL", []int32{1, 2, 3}).Count(&count) if count > 0 { t.Errorf("应该软删除了所有通道,但仍有 %d 个未删除", count) } // 检查Redis缓存是否被删除 for _, id := range []int32{1, 2, 3} { key := fmt.Sprintf("channel:%d", id) if mr.Exists(key) { t.Errorf("通道缓存 %s 应被删除但仍存在", key) } } }, }, { name: "用户删除自己的通道", args: args{ ctx: ctx, auth: &AuthContext{ Payload: Payload{ Type: PayloadUser, Id: 100, }, }, id: []int32{1}, }, setup: func() { // 预设 Redis 缓存 mr.FlushAll() key := "channel:1" channel := models.Channel{ID: 1, UserID: 100} data, _ := json.Marshal(channel) mr.Set(key, string(data)) // 清空数据库表 db.Exec("delete from channel") db.Exec("delete from proxy") // 创建代理 proxy := models.Proxy{ ID: 1, Name: "proxy1", Host: "proxy1.example.com", Secret: "key:secret", Type: 1, } db.Create(&proxy) // 创建通道 ch := models.Channel{ ID: 1, UserID: 100, ProxyID: 1, ProxyPort: 10001, Protocol: "http", Expiration: time.Now().Add(24 * time.Hour), } db.Create(&ch) // 模拟查询已激活的端口 mg.PortActiveMock = func(param ...remote.PortActiveReq) (map[string]remote.PortData, error) { return map[string]remote.PortData{ "10001": { Edge: []string{"edge1", "edge2"}, }, }, nil } }, checkCache: func(t *testing.T) { // 检查通道是否被软删除 var count int64 db.Model(&models.Channel{}).Where("id = ? AND deleted_at IS NULL", 1).Count(&count) if count > 0 { t.Errorf("应该软删除了通道,但仍未删除") } // 检查Redis缓存是否被删除 key := "channel:1" if mr.Exists(key) { t.Errorf("通道缓存 %s 应被删除但仍存在", key) } }, }, { name: "用户删除不属于自己的通道", args: args{ ctx: ctx, auth: &AuthContext{ Payload: Payload{ Type: PayloadUser, Id: 100, }, }, id: []int32{5}, }, setup: func() { // 预设 Redis 缓存 mr.FlushAll() key := "channel:5" channel := models.Channel{ID: 5, UserID: 101} data, _ := json.Marshal(channel) mr.Set(key, string(data)) // 清空数据库表 db.Exec("delete from channel") // 创建一个属于用户101的通道 ch := models.Channel{ ID: 5, UserID: 101, ProxyID: 1, ProxyPort: 10005, Protocol: "http", Expiration: time.Now().Add(24 * time.Hour), } db.Create(&ch) }, wantErr: true, wantErrContains: "无权限访问", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.setup != nil { tt.setup() } s := &channelService{} err := s.RemoveChannels(tt.args.ctx, tt.args.auth, tt.args.id...) // 检查错误 if tt.wantErr { if err == nil { t.Errorf("RemoveChannels() 应当返回错误") return } if tt.wantErrContains != "" && !strings.Contains(err.Error(), tt.wantErrContains) { t.Errorf("RemoveChannels() 错误 = %v, 应包含 %v", err, tt.wantErrContains) } return } if err != nil { t.Errorf("RemoveChannels() 错误 = %v, wantErr %v", err, tt.wantErr) return } // 检查 Redis 缓存是否正确设置 if tt.checkCache != nil { tt.checkCache(t) } }) } }