/* Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments Copyright (C) ITsysCOM GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see */ package engine import ( "reflect" "slices" "strings" "sync" "testing" "time" "github.com/cgrates/birpc/context" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/utils" "github.com/cgrates/cron" ) func TestNewRankingS(t *testing.T) { dm := &DataManager{} connMgr := &ConnManager{} filterS := &FilterS{} cgrcfg := &config.CGRConfig{} rankingS := NewRankingS(dm, connMgr, filterS, cgrcfg) if rankingS == nil { t.Fatal("Expected RankingS to be non-nil, got nil") } if rankingS.dm != dm { t.Errorf("Expected dm to be '%v', got '%v'", dm, rankingS.dm) } if rankingS.connMgr != connMgr { t.Errorf("Expected connMgr to be '%v', got '%v'", connMgr, rankingS.connMgr) } if rankingS.filterS != filterS { t.Errorf("Expected filterS to be '%v', got '%v'", filterS, rankingS.filterS) } if rankingS.cgrcfg != cgrcfg { t.Errorf("Expected cgrcfg to be '%v', got '%v'", cgrcfg, rankingS.cgrcfg) } if rankingS.crn == nil { t.Error("Expected crn to be initialized, got nil") } if rankingS.crnTQsMux == nil { t.Error("Expected crnTQsMux to be initialized, got nil") } if len(rankingS.crnTQs) != 0 { t.Errorf("Expected crnTQs to be empty, got length %d", len(rankingS.crnTQs)) } if rankingS.storedRankings == nil { t.Error("Expected storedRankings to be initialized, got nil") } if rankingS.storingStopped == nil { t.Error("Expected storingStopped channel to be initialized, got nil") } if rankingS.rankingStop == nil { t.Error("Expected rankingStop channel to be initialized, got nil") } } func TestAsRankingSummary(t *testing.T) { lastUpdateTime, err := time.Parse(time.RFC3339, "2024-10-21T00:00:00Z") if err != nil { t.Fatalf("Failed to parse time: %v", err) } ranking := &Ranking{ Tenant: "tenant1", ID: "ranking1", LastUpdate: lastUpdateTime, SortedStatIDs: []string{"stat1", "stat2", "stat3"}, Metrics: make(map[string]map[string]float64), Sorting: "asc", SortingParameters: []string{"param1", "param2"}, } rkSm := ranking.asRankingSummary() if rkSm.Tenant != ranking.Tenant { t.Errorf("Expected Tenant to be '%s', got '%s'", ranking.Tenant, rkSm.Tenant) } if rkSm.ID != ranking.ID { t.Errorf("Expected ID to be '%s', got '%s'", ranking.ID, rkSm.ID) } if rkSm.LastUpdate != ranking.LastUpdate { t.Errorf("Expected LastUpdate to be '%v', got '%v'", ranking.LastUpdate, rkSm.LastUpdate) } } func TestRankingProcessEvent(t *testing.T) { Cache.Clear(nil) cfg := config.NewDefaultCGRConfig() cfg.RankingSCfg().StoreInterval = 1 data, dErr := NewInternalDB(nil, nil, true, nil, config.CgrConfig().DataDbCfg().Items) if dErr != nil { t.Error(dErr) } dm := NewDataManager(data, cfg.CacheCfg(), nil) rankingProfile := &RankingProfile{ Tenant: "cgrates.org", ID: "RankingProfile1", Schedule: "0 0 * * *", StatIDs: []string{"Stat1", "Stat2"}, MetricIDs: []string{"MetricA", "MetricB"}, Sorting: "*asc", SortingParameters: []string{"MetricA", "MetricB"}, Stored: true, ThresholdIDs: []string{"*none"}, } if err := dm.SetRankingProfile(rankingProfile); err != nil { t.Error(err) } if retrievedProfile, err := dm.GetRankingProfile(rankingProfile.Tenant, rankingProfile.ID, true, false, ""); err != nil { t.Errorf("Error retrieving ranking profile: %+v", err) } else if !reflect.DeepEqual(rankingProfile, retrievedProfile) { t.Errorf("Expecting: %+v, received: %+v", rankingProfile, retrievedProfile) } } func TestProcessThresholdsEmptySortedStatIDs(t *testing.T) { rankingService := &RankingS{ connMgr: &ConnManager{}, cgrcfg: &config.CGRConfig{}, } ranking := &Ranking{ Tenant: "cgrates.org", ID: "ID", rkPrfl: &RankingProfile{ ThresholdIDs: []string{"threshold1"}, }, SortedStatIDs: []string{}, } err := rankingService.processThresholds(ranking) if err != nil { t.Errorf("Expected no error, got %v", err) } } func TestProcessEEsHandlesEmptySortedStatIDs(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.RankingSCfg().StoreInterval = 1 data, dErr := NewInternalDB(nil, nil, true, nil, config.CgrConfig().DataDbCfg().Items) if dErr != nil { t.Error(dErr) } dm := NewDataManager(data, cfg.CacheCfg(), nil) rankingService := &RankingS{ cgrcfg: cfg, connMgr: &ConnManager{}, dm: dm, } rk := &Ranking{ Tenant: "cgrates.org", ID: "ID", LastUpdate: time.Now(), SortedStatIDs: []string{}, Metrics: make(map[string]map[string]float64), Sorting: "", SortingParameters: []string{}, rkPrfl: nil, metricIDs: utils.StringSet{}, } err := rankingService.processEEs(rk) if err != nil { t.Errorf("Expected no error, got %v", err) } } func TestProcessEEsHandlesEmptyEEsConns(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.RankingSCfg().StoreInterval = 1 data, dErr := NewInternalDB(nil, nil, true, nil, config.CgrConfig().DataDbCfg().Items) if dErr != nil { t.Error(dErr) } dm := NewDataManager(data, cfg.CacheCfg(), nil) rankingService := &RankingS{ cgrcfg: cfg, connMgr: &ConnManager{}, dm: dm, } rk := &Ranking{ Tenant: "cgrates.org", ID: "ID", LastUpdate: time.Now(), SortedStatIDs: []string{"stat_id_1", "stat_id_2"}, Metrics: make(map[string]map[string]float64), Sorting: "", SortingParameters: []string{}, rkPrfl: nil, metricIDs: utils.StringSet{}, } cfg.RankingSCfg().EEsConns = []string{} err := rankingService.processEEs(rk) if err != nil { t.Errorf("Expected no error, got %v", err) } } func TestV1ScheduleQueriesInvalidRankingID(t *testing.T) { ctx := context.Background() tS := &RankingS{ crn: cron.New(), crnTQs: make(map[string]map[string]cron.EntryID), crnTQsMux: &sync.RWMutex{}, } args := &utils.ArgScheduleRankingQueries{ TenantIDWithAPIOpts: utils.TenantIDWithAPIOpts{ TenantID: &utils.TenantID{ Tenant: "cgrates.org", ID: "ID", }, APIOpts: make(map[string]any), }, RankingIDs: []string{"invalidID"}, } var scheduled int err := tS.V1ScheduleQueries(ctx, args, &scheduled) if err == nil { t.Errorf("expected an error but got none") } if scheduled != 0 { t.Errorf("expected scheduled to be 0 but got %d", scheduled) } } func TestStoreRanking(t *testing.T) { cfg := config.NewDefaultCGRConfig() dataDB, err := NewInternalDB(nil, nil, true, nil, nil) if err != nil { t.Fatal(err) } dm := NewDataManager(dataDB, cfg.CacheCfg(), nil) rkg := NewRankingS(dm, nil, nil, cfg) ranking := &Ranking{ rkPrfl: &RankingProfile{ Tenant: "cgrates.org", ID: "ID1", Schedule: "@every 1s", StatIDs: []string{"stat1", "stat2"}, MetricIDs: []string{"metric1", "metric2"}, Sorting: "asc", SortingParameters: []string{"metric1:true"}, Stored: true, ThresholdIDs: []string{"threshold1"}, }, } cfg.RankingSCfg().StoreInterval = 0 if err := rkg.storeRanking(ranking); err != nil { t.Errorf("Expected no error when StoreInterval is 0, but got: %v", err) } if len(rkg.storedRankings) != 0 { t.Error("Expected storedRankings to be empty when StoreInterval is 0") } cfg.RankingSCfg().StoreInterval = -1 if err := rkg.storeRanking(ranking); err != nil { t.Errorf("Expected no error when StoreInterval is -1, but got: %v", err) } cfg.RankingSCfg().StoreInterval = time.Second if err := rkg.storeRanking(ranking); err != nil { t.Errorf("Expected no error when StoreInterval is positive, but got: %v", err) } } func TestRankingsStoreRankings(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.RankingSCfg().Enabled = true cfg.RankingSCfg().StoreInterval = -1 cfg.RankingSCfg().StatSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaStats)} dataDB, dErr := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items) if dErr != nil { t.Error(dErr) } dm := NewDataManager(dataDB, cfg.CacheCfg(), nil) conn := make(chan context.ClientConnector, 1) conn <- &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.StatSv1GetQueueFloatMetrics: func(ctx *context.Context, args, reply any) error { if args.(*utils.TenantIDWithAPIOpts).ID == "stat1" { *reply.(*map[string]float64) = map[string]float64{ utils.MetaTCD: float64(20 * time.Second), utils.MetaACC: 22.2, } } else if args.(*utils.TenantIDWithAPIOpts).ID == "stat2" { *reply.(*map[string]float64) = map[string]float64{ utils.MetaTCD: float64(23 * time.Second), utils.MetaACC: 22.2, } } return nil }, }, } connMgr = NewConnManager(config.NewDefaultCGRConfig(), map[string]chan context.ClientConnector{ utils.ConcatenatedKey(utils.MetaInternal, utils.MetaStats): conn, }) rkg := NewRankingS(dm, connMgr, nil, cfg) rankingProfile := &RankingProfile{ Tenant: "cgrates.org", ID: "ID1", Schedule: "@every 1s", StatIDs: []string{"stat1", "stat2"}, MetricIDs: []string{}, Sorting: "*desc", SortingParameters: []string{utils.MetaTCD, utils.MetaACC}, Stored: true, } dm.SetRankingProfile(rankingProfile) Cache.Clear(nil) if err := rkg.StartRankingS(); err != nil { t.Fatalf("Unexpected error when starting rankings: %v", err) } t.Cleanup(func() { rkg.StopRankingS() }) time.Sleep(1200 * time.Millisecond) profile, err := dm.GetRanking("cgrates.org", "ID1", false, false, "") if err != nil { t.Errorf("Error retrieving ranking profile: %v", err) } if profile == nil { t.Fatal("Expected ranking profile to be present, but it was not found") } if profile.ID != "ID1" { t.Errorf("Expected profile ID to be 'ID1', but got %v", profile.ID) } if profile.Tenant != "cgrates.org" { t.Errorf("Expected tenant to be 'cgrates.org', but got %v", profile.Tenant) } if profile.Sorting != "*desc" { t.Errorf("Expected sorting to be 'desc', but got %v", profile.Sorting) } if !slices.Equal(profile.SortingParameters, profile.SortingParameters) { t.Errorf("Expected SortingParameters to be ['metric1:true'], but got %v", profile.SortingParameters) } if !reflect.DeepEqual(profile.Metrics, map[string]map[string]float64{"stat1": {"*acc": 22.2, "*tcd": 20000000000}, "stat2": {"*acc": 22.2, "*tcd": 23000000000}}) { t.Errorf("Expected sorting to be 'asc', but got %v", profile.Metrics) } } func TestV1GetRankingSummary(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.RankingSCfg().Enabled = true dataDB, dErr := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items) if dErr != nil { t.Error(dErr) } dm := NewDataManager(dataDB, cfg.CacheCfg(), nil) connMgr := NewConnManager(config.NewDefaultCGRConfig(), nil) rkg := NewRankingS(dm, connMgr, nil, cfg) rankingProfile := &RankingProfile{ Tenant: "cgrates.org", ID: "TestRanking", Schedule: "@every 1s", StatIDs: []string{"stat1", "stat2"}, MetricIDs: []string{}, Sorting: "*desc", SortingParameters: []string{utils.MetaTCD, utils.MetaACC}, Stored: true, } dm.SetRankingProfile(rankingProfile) if err := rkg.StartRankingS(); err != nil { t.Fatalf("Unexpected error when starting rankings: %v", err) } time.Sleep(1200 * time.Millisecond) arg := &utils.TenantIDWithAPIOpts{ TenantID: &utils.TenantID{ Tenant: "cgrates.org", ID: "TestRanking", }, APIOpts: make(map[string]any), } reply := &RankingSummary{} err := rkg.V1GetRankingSummary(nil, arg, reply) if err != nil { t.Fatalf("V1GetRankingSummary failed with error: %v", err) } expectedTenant := "cgrates.org" if reply.Tenant != expectedTenant { t.Errorf("Expected Tenant to be %q, but got %q", expectedTenant, reply.Tenant) } expectedID := "TestRanking" if reply.ID != expectedID { t.Errorf("Expected ID to be %q, but got %q", expectedID, reply.ID) } } func TestV1GetSchedule(t *testing.T) { cfg := config.NewDefaultCGRConfig() dataDB, dErr := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items) if dErr != nil { t.Error(dErr) } dm := NewDataManager(dataDB, cfg.CacheCfg(), nil) connMgr := NewConnManager(config.NewDefaultCGRConfig(), nil) rkg := NewRankingS(dm, connMgr, nil, cfg) rankingProfile := &RankingProfile{ Tenant: "cgrates.org", ID: "TestRanking", Schedule: "@every 1s", StatIDs: []string{"stat1", "stat2"}, MetricIDs: []string{}, Sorting: "*desc", SortingParameters: []string{utils.MetaTCD, utils.MetaACC}, Stored: true, } dm.SetRankingProfile(rankingProfile) if err := rkg.StartRankingS(); err != nil { t.Fatalf("Unexpected error when starting rankings: %v", err) } time.Sleep(1200 * time.Millisecond) arg := &utils.ArgScheduledRankings{ TenantIDWithAPIOpts: utils.TenantIDWithAPIOpts{ TenantID: &utils.TenantID{ Tenant: "cgrates.org", ID: "TestRanking", }, APIOpts: make(map[string]any), }, RankingIDPrefixes: []string{"TestRanking"}, } var schedRankings []utils.ScheduledRanking err := rkg.V1GetSchedule(nil, arg, &schedRankings) if err != nil { t.Fatalf("V1GetSchedule failed with error: %v", err) } if len(schedRankings) == 0 { t.Errorf("Expected at least one scheduled ranking, but got none") } if schedRankings[0].RankingID != "TestRanking" { t.Errorf("Expected RankingID to be %q, but got %q", "TestRanking", schedRankings[0].RankingID) } if schedRankings[0].Next.IsZero() { t.Errorf("Expected Next to be non-zero, but got zero time") } if schedRankings[0].Previous.IsZero() { t.Errorf("Expected Previous to be non-zero, but got zero time") } } func TestV1GetRankingMissingID(t *testing.T) { cfg := config.NewDefaultCGRConfig() dataDB, dErr := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items) if dErr != nil { t.Error(dErr) } dm := NewDataManager(dataDB, cfg.CacheCfg(), nil) connMgr := NewConnManager(config.NewDefaultCGRConfig(), nil) rkg := NewRankingS(dm, connMgr, nil, cfg) arg := &utils.TenantIDWithAPIOpts{ TenantID: &utils.TenantID{ Tenant: "cgrates.org", ID: "", }, APIOpts: make(map[string]any), } retRanking := &Ranking{} err := rkg.V1GetRanking(nil, arg, retRanking) if err == nil { t.Fatalf("Expected error when ID is missing, but got none") } if !strings.Contains(err.Error(), "MANDATORY_IE_MISSING: [ID") { t.Errorf("Expected error related to missing fields, but got: %v", err) } } func TestV1GetRankingSortedStatIDs(t *testing.T) { rk := &Ranking{ SortedStatIDs: []string{"stat1", "stat2", "stat3"}, } retRanking := &Ranking{} retRanking.SortedStatIDs = make([]string, len(rk.SortedStatIDs)) copy(retRanking.SortedStatIDs, rk.SortedStatIDs) if len(retRanking.SortedStatIDs) != len(rk.SortedStatIDs) { t.Errorf("Expected SortedStatIDs length: %d, but got: %d", len(rk.SortedStatIDs), len(retRanking.SortedStatIDs)) } for i, v := range rk.SortedStatIDs { if retRanking.SortedStatIDs[i] != v { t.Errorf("Expected SortedStatIDs[%d]: %s, but got: %s", i, v, retRanking.SortedStatIDs[i]) } } rk.SortedStatIDs = []string{} retRanking = &Ranking{} retRanking.SortedStatIDs = make([]string, len(rk.SortedStatIDs)) copy(retRanking.SortedStatIDs, rk.SortedStatIDs) if len(retRanking.SortedStatIDs) != 0 { t.Errorf("Expected empty SortedStatIDs, but got length: %d", len(retRanking.SortedStatIDs)) } rk.SortedStatIDs = nil retRanking = &Ranking{} retRanking.SortedStatIDs = make([]string, len(rk.SortedStatIDs)) copy(retRanking.SortedStatIDs, rk.SortedStatIDs) if len(retRanking.SortedStatIDs) != 0 { t.Errorf("Expected empty SortedStatIDs for nil input, but got length: %d", len(retRanking.SortedStatIDs)) } } func TestV1ScheduleQueries(t *testing.T) { cfg := config.NewDefaultCGRConfig() dataDB, dErr := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items) if dErr != nil { t.Error(dErr) } dm := NewDataManager(dataDB, cfg.CacheCfg(), nil) connMgr := NewConnManager(config.NewDefaultCGRConfig(), nil) rkg := NewRankingS(dm, connMgr, nil, cfg) rankingProfile := &RankingProfile{ Tenant: "cgrates.org", ID: "TestRanking", Schedule: "@every 1s", StatIDs: []string{"stat1", "stat2"}, MetricIDs: []string{}, Sorting: "*desc", SortingParameters: []string{utils.MetaTCD, utils.MetaACC}, Stored: true, } dm.SetRankingProfile(rankingProfile) if err := rkg.StartRankingS(); err != nil { t.Fatalf("Unexpected error when starting rankings: %v", err) } arg := &utils.ArgScheduleRankingQueries{ TenantIDWithAPIOpts: utils.TenantIDWithAPIOpts{ TenantID: &utils.TenantID{ Tenant: "cgrates.org", ID: "TestRanking", }, APIOpts: make(map[string]any), }, RankingIDs: []string{"TestRanking"}, } var scheduled int err := rkg.V1ScheduleQueries(context.Background(), arg, &scheduled) if err != nil { t.Fatalf("V1ScheduleQueries failed with error: %v", err) } if scheduled == 0 { t.Errorf("Expected at least one scheduled ranking query, but got none") } if len(rkg.crnTQs["cgrates.org"]) == 0 { t.Errorf("Expected cron entries for tenant 'cgrates.org', but got none") } if _, exists := rkg.crnTQs["cgrates.org"]["TestRanking"]; !exists { t.Errorf("Expected cron entry for ranking ID 'TestRanking', but it was not found") } }