34cd1c524e
Co-authored-by: multica-agent <github@multica.ai>
347 lines
11 KiB
Go
347 lines
11 KiB
Go
package group
|
|
|
|
import (
|
|
"context"
|
|
"encoding/csv"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
modelgroup "github.com/perfect-panel/server/internal/model/group"
|
|
"github.com/perfect-panel/server/internal/svc"
|
|
"github.com/perfect-panel/server/internal/types"
|
|
"github.com/perfect-panel/server/pkg/logger"
|
|
"gorm.io/driver/mysql"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func TestGetGroupHistoryDetailReadsConfigFromSystem(t *testing.T) {
|
|
db, mock, cleanup := newGroupTestDB(t)
|
|
defer cleanup()
|
|
|
|
now := time.Unix(1710000000, 0)
|
|
mock.ExpectQuery("FROM `group_history`").
|
|
WithArgs(int64(9), 1).
|
|
WillReturnRows(sqlmock.NewRows([]string{
|
|
"id", "group_mode", "trigger_type", "state", "total_users", "success_count", "failed_count", "start_time", "end_time", "error_message", "created_at",
|
|
}).AddRow(int64(9), "traffic", "manual", "completed", 1, 1, 0, now, now, "", now))
|
|
mock.ExpectQuery("FROM `group_history_detail`").
|
|
WithArgs(int64(9)).
|
|
WillReturnRows(sqlmock.NewRows([]string{
|
|
"id", "history_id", "node_group_id", "user_count", "node_count", "user_data", "created_at",
|
|
}).AddRow(int64(1), int64(9), int64(3), 1, 2, `[{"id":7,"email":"u@example.com"}]`, now))
|
|
mock.ExpectQuery("FROM `system`").
|
|
WithArgs("group", "traffic_config").
|
|
WillReturnRows(sqlmock.NewRows([]string{"value"}).AddRow(`{"strategy":"closed"}`))
|
|
|
|
logic := newTestGroupHistoryDetailLogic(db)
|
|
resp, err := logic.GetGroupHistoryDetail(&types.GetGroupHistoryDetailRequest{Id: 9})
|
|
if err != nil {
|
|
t.Fatalf("GetGroupHistoryDetail error: %v", err)
|
|
}
|
|
if got := resp.ConfigSnapshot["config"].(map[string]interface{})["strategy"]; got != "closed" {
|
|
t.Fatalf("config snapshot strategy = %v, want closed", got)
|
|
}
|
|
assertGroupExpectations(t, mock)
|
|
}
|
|
|
|
func TestExportGroupResultParsesHistoryUserDataJSON(t *testing.T) {
|
|
db, mock, cleanup := newGroupTestDB(t)
|
|
defer cleanup()
|
|
|
|
historyID := int64(11)
|
|
now := time.Unix(1710000000, 0)
|
|
mock.ExpectQuery("FROM `group_history_detail`").
|
|
WithArgs(historyID).
|
|
WillReturnRows(sqlmock.NewRows([]string{
|
|
"id", "history_id", "node_group_id", "user_count", "node_count", "user_data", "created_at",
|
|
}).AddRow(int64(1), historyID, int64(8), 1, 2, `[{"id":42,"email":"u@example.com"}]`, now))
|
|
mock.ExpectQuery("FROM `node_group`").
|
|
WithArgs(int64(8), 1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(int64(8), "VIP"))
|
|
|
|
logic := newTestExportGroupResultLogic(db)
|
|
data, filename, err := logic.ExportGroupResult(&types.ExportGroupResultRequest{HistoryId: &historyID})
|
|
if err != nil {
|
|
t.Fatalf("ExportGroupResult error: %v", err)
|
|
}
|
|
if filename != "group_result_11.csv" {
|
|
t.Fatalf("filename = %q, want group_result_11.csv", filename)
|
|
}
|
|
|
|
records, err := csv.NewReader(strings.NewReader(strings.TrimPrefix(string(data), "\ufeff"))).ReadAll()
|
|
if err != nil {
|
|
t.Fatalf("read csv: %v", err)
|
|
}
|
|
if len(records) != 2 || strings.Join(records[1], ",") != "42,8,VIP" {
|
|
t.Fatalf("csv records = %#v, want user/group row", records)
|
|
}
|
|
assertGroupExpectations(t, mock)
|
|
}
|
|
|
|
func TestExportGroupResultRejectsInvalidHistoryUserDataJSON(t *testing.T) {
|
|
db, mock, cleanup := newGroupTestDB(t)
|
|
defer cleanup()
|
|
|
|
historyID := int64(12)
|
|
now := time.Unix(1710000000, 0)
|
|
mock.ExpectQuery("FROM `group_history_detail`").
|
|
WithArgs(historyID).
|
|
WillReturnRows(sqlmock.NewRows([]string{
|
|
"id", "history_id", "node_group_id", "user_count", "node_count", "user_data", "created_at",
|
|
}).AddRow(int64(1), historyID, int64(8), 1, 2, `{bad-json`, now))
|
|
|
|
logic := newTestExportGroupResultLogic(db)
|
|
_, _, err := logic.ExportGroupResult(&types.ExportGroupResultRequest{HistoryId: &historyID})
|
|
if err == nil || !strings.Contains(err.Error(), "parse group history user_data failed") {
|
|
t.Fatalf("ExportGroupResult error = %v, want parse error", err)
|
|
}
|
|
assertGroupExpectations(t, mock)
|
|
}
|
|
|
|
func TestDeleteNodeGroupRejectsSubscribeReferences(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
defaultSubscribeCount int64
|
|
subscribeGroupCount int64
|
|
userSubscribeCount int64
|
|
wantErr string
|
|
}{
|
|
{name: "subscribe default group", defaultSubscribeCount: 1, wantErr: "default node group"},
|
|
{name: "subscribe group list", subscribeGroupCount: 1, wantErr: "node group list"},
|
|
{name: "user subscribe group", userSubscribeCount: 1, wantErr: "user subscribes"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
db, mock, cleanup := newGroupTestDB(t)
|
|
defer cleanup()
|
|
|
|
expectDeleteNodeGroupBaseChecks(mock, 5)
|
|
mock.ExpectQuery("FROM `subscribe`").
|
|
WithArgs(int64(5)).
|
|
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(tt.defaultSubscribeCount))
|
|
if tt.defaultSubscribeCount == 0 {
|
|
mock.ExpectQuery("FROM `subscribe`").
|
|
WithArgs("[5]").
|
|
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(tt.subscribeGroupCount))
|
|
}
|
|
if tt.defaultSubscribeCount == 0 && tt.subscribeGroupCount == 0 {
|
|
mock.ExpectQuery("FROM `user_subscribe`").
|
|
WithArgs(int64(5)).
|
|
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(tt.userSubscribeCount))
|
|
}
|
|
|
|
logic := newTestDeleteNodeGroupLogic(db)
|
|
err := logic.DeleteNodeGroup(&types.DeleteNodeGroupRequest{Id: 5})
|
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("DeleteNodeGroup error = %v, want contains %q", err, tt.wantErr)
|
|
}
|
|
assertGroupExpectations(t, mock)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResetGroupsRollsBackOnFailure(t *testing.T) {
|
|
db, mock, cleanup := newGroupTestDB(t)
|
|
defer cleanup()
|
|
|
|
mock.ExpectBegin()
|
|
mock.ExpectExec("DELETE FROM `node_group`").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectExec("UPDATE `subscribe`").
|
|
WithArgs(int64(0), "[]").
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectExec("UPDATE `nodes`").
|
|
WithArgs("[]").
|
|
WillReturnError(fmt.Errorf("node update failed"))
|
|
mock.ExpectRollback()
|
|
|
|
logic := newTestResetGroupsLogic(db)
|
|
err := logic.ResetGroups()
|
|
if err == nil || !strings.Contains(err.Error(), "node update failed") {
|
|
t.Fatalf("ResetGroups error = %v, want node update failure", err)
|
|
}
|
|
assertGroupExpectations(t, mock)
|
|
}
|
|
|
|
func TestPreviewUserNodesIncludesPublicNodesInGroupMode(t *testing.T) {
|
|
db, mock, cleanup := newGroupTestDB(t)
|
|
defer cleanup()
|
|
|
|
now := time.Unix(1710000000, 0)
|
|
mock.ExpectQuery("FROM `user_subscribe`").
|
|
WithArgs(int64(100), int8(0), int8(1)).
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subscribe_id", "node_group_id"}).
|
|
AddRow(int64(1), int64(100), int64(10), int64(5)))
|
|
mock.ExpectQuery("FROM `subscribe`").
|
|
WithArgs(int64(10)).
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "node_group_id", "node_group_ids", "nodes", "node_tags"}).
|
|
AddRow(int64(10), int64(0), "[]", "", ""))
|
|
mock.ExpectQuery("FROM `system`").
|
|
WithArgs("group", "enabled").
|
|
WillReturnRows(sqlmock.NewRows([]string{"value"}).AddRow("true"))
|
|
mock.ExpectQuery("FROM `nodes`").
|
|
WithArgs(true).
|
|
WillReturnRows(sqlmock.NewRows([]string{
|
|
"id", "name", "tags", "port", "address", "server_id", "protocol", "enabled", "sort", "node_group_ids", "created_at", "updated_at",
|
|
}).
|
|
AddRow(int64(1), "group-node", "", uint16(443), "g.example.com", int64(1), "vless", true, 1, "[5]", now, now).
|
|
AddRow(int64(2), "public-node", "", uint16(443), "p.example.com", int64(1), "vless", true, 2, "[]", now, now))
|
|
mock.ExpectQuery("FROM `node_group`").
|
|
WithArgs(int64(5)).
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(int64(5), "Group A"))
|
|
|
|
logic := newTestPreviewUserNodesLogic(db)
|
|
resp, err := logic.PreviewUserNodes(&types.PreviewUserNodesRequest{UserId: 100})
|
|
if err != nil {
|
|
t.Fatalf("PreviewUserNodes error: %v", err)
|
|
}
|
|
if !hasNodeGroup(resp.NodeGroups, 0, "public-node") {
|
|
t.Fatalf("PreviewUserNodes node groups = %#v, want public node group", resp.NodeGroups)
|
|
}
|
|
assertGroupExpectations(t, mock)
|
|
}
|
|
|
|
func TestTrafficRangeMatchingUsesClosedBounds(t *testing.T) {
|
|
min0, max10 := int64(0), int64(10)
|
|
min10, max20 := int64(10), int64(20)
|
|
nodeGroups := []modelgroup.NodeGroup{
|
|
{Id: 1, MinTrafficGB: &min0, MaxTrafficGB: &max10},
|
|
{Id: 2, MinTrafficGB: &min10, MaxTrafficGB: &max20},
|
|
}
|
|
|
|
tests := map[float64]int64{
|
|
0: 1,
|
|
10: 1,
|
|
15: 2,
|
|
21: 0,
|
|
}
|
|
for used, want := range tests {
|
|
if got := matchTrafficNodeGroup(used, nodeGroups); got != want {
|
|
t.Fatalf("matchTrafficNodeGroup(%v) = %d, want %d", used, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalizePaginationDefaultsAndMax(t *testing.T) {
|
|
tests := []struct {
|
|
page, size int
|
|
wantPage int
|
|
wantSize int
|
|
}{
|
|
{page: 0, size: 0, wantPage: 1, wantSize: defaultPageSize},
|
|
{page: -1, size: -5, wantPage: 1, wantSize: defaultPageSize},
|
|
{page: 2, size: maxPageSize + 1, wantPage: 2, wantSize: maxPageSize},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
gotPage, gotSize := normalizePagination(tt.page, tt.size)
|
|
if gotPage != tt.wantPage || gotSize != tt.wantSize {
|
|
t.Fatalf("normalizePagination(%d, %d) = (%d, %d), want (%d, %d)", tt.page, tt.size, gotPage, gotSize, tt.wantPage, tt.wantSize)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newGroupTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) {
|
|
t.Helper()
|
|
|
|
sqlDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherFunc(func(expectedSQL, actualSQL string) error {
|
|
if strings.Contains(actualSQL, expectedSQL) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("actual sql %q does not contain %q", actualSQL, expectedSQL)
|
|
})))
|
|
if err != nil {
|
|
t.Fatalf("create sqlmock: %v", err)
|
|
}
|
|
|
|
db, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), &gorm.Config{})
|
|
if err != nil {
|
|
_ = sqlDB.Close()
|
|
t.Fatalf("open gorm db: %v", err)
|
|
}
|
|
|
|
return db, mock, func() {
|
|
_ = sqlDB.Close()
|
|
}
|
|
}
|
|
|
|
func newTestGroupHistoryDetailLogic(db *gorm.DB) *GetGroupHistoryDetailLogic {
|
|
ctx := context.Background()
|
|
return &GetGroupHistoryDetailLogic{
|
|
Logger: logger.WithContext(ctx),
|
|
ctx: ctx,
|
|
svcCtx: &svc.ServiceContext{DB: db},
|
|
}
|
|
}
|
|
|
|
func newTestExportGroupResultLogic(db *gorm.DB) *ExportGroupResultLogic {
|
|
ctx := context.Background()
|
|
return &ExportGroupResultLogic{
|
|
Logger: logger.WithContext(ctx),
|
|
ctx: ctx,
|
|
svcCtx: &svc.ServiceContext{DB: db},
|
|
}
|
|
}
|
|
|
|
func newTestDeleteNodeGroupLogic(db *gorm.DB) *DeleteNodeGroupLogic {
|
|
ctx := context.Background()
|
|
return &DeleteNodeGroupLogic{
|
|
Logger: logger.WithContext(ctx),
|
|
ctx: ctx,
|
|
svcCtx: &svc.ServiceContext{DB: db},
|
|
}
|
|
}
|
|
|
|
func newTestResetGroupsLogic(db *gorm.DB) *ResetGroupsLogic {
|
|
ctx := context.Background()
|
|
return &ResetGroupsLogic{
|
|
Logger: logger.WithContext(ctx),
|
|
ctx: ctx,
|
|
svcCtx: &svc.ServiceContext{DB: db},
|
|
}
|
|
}
|
|
|
|
func newTestPreviewUserNodesLogic(db *gorm.DB) *PreviewUserNodesLogic {
|
|
ctx := context.Background()
|
|
return &PreviewUserNodesLogic{
|
|
Logger: logger.WithContext(ctx),
|
|
ctx: ctx,
|
|
svcCtx: &svc.ServiceContext{DB: db},
|
|
}
|
|
}
|
|
|
|
func expectDeleteNodeGroupBaseChecks(mock sqlmock.Sqlmock, nodeGroupId int64) {
|
|
now := time.Unix(1710000000, 0)
|
|
mock.ExpectQuery("FROM `node_group`").
|
|
WithArgs(nodeGroupId, 1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "created_at", "updated_at"}).
|
|
AddRow(nodeGroupId, "Group", now, now))
|
|
mock.ExpectQuery("FROM `nodes`").
|
|
WithArgs(fmt.Sprintf("[%d]", nodeGroupId)).
|
|
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
|
|
}
|
|
|
|
func hasNodeGroup(items []types.NodeGroupItem, id int64, nodeName string) bool {
|
|
for _, item := range items {
|
|
if item.Id != id {
|
|
continue
|
|
}
|
|
for _, n := range item.Nodes {
|
|
if n.Name == nodeName {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func assertGroupExpectations(t *testing.T, mock sqlmock.Sqlmock) {
|
|
t.Helper()
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("unmet sql expectations: %v", err)
|
|
}
|
|
}
|