diff --git a/engine/libtrends_test.go b/engine/libtrends_test.go
new file mode 100644
index 000000000..b4fd03937
--- /dev/null
+++ b/engine/libtrends_test.go
@@ -0,0 +1,251 @@
+/*
+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 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see
+*/
+
+package engine
+
+import (
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/cgrates/cgrates/utils"
+)
+
+func TestTrendProfileClone(t *testing.T) {
+
+ original := &TrendProfile{
+ Tenant: "cgrates.org",
+ ID: "ID",
+ Schedule: "Schedule",
+ StatID: "StatID",
+ Metrics: []string{"metric1", "metric2"},
+ TTL: 10 * time.Minute,
+ QueueLength: 100,
+ MinItems: 10,
+ CorrelationType: "average",
+ Tolerance: 0.05,
+ Stored: true,
+ ThresholdIDs: []string{"thresh1", "thresh2"},
+ }
+
+ cloned := original.Clone()
+
+ if cloned.Tenant != original.Tenant {
+ t.Errorf("Expected Tenant %s, but got %s", original.Tenant, cloned.Tenant)
+ }
+ if cloned.ID != original.ID {
+ t.Errorf("Expected ID %s, but got %s", original.ID, cloned.ID)
+ }
+ if cloned.Schedule != original.Schedule {
+ t.Errorf("Expected Schedule %s, but got %s", original.Schedule, cloned.Schedule)
+ }
+ if cloned.StatID != original.StatID {
+ t.Errorf("Expected StatID %s, but got %s", original.StatID, cloned.StatID)
+ }
+ if cloned.QueueLength != original.QueueLength {
+ t.Errorf("Expected QueueLength %d, but got %d", original.QueueLength, cloned.QueueLength)
+ }
+ if cloned.TTL != original.TTL {
+ t.Errorf("Expected TTL %v, but got %v", original.TTL, cloned.TTL)
+ }
+ if cloned.MinItems != original.MinItems {
+ t.Errorf("Expected MinItems %d, but got %d", original.MinItems, cloned.MinItems)
+ }
+ if cloned.CorrelationType != original.CorrelationType {
+ t.Errorf("Expected CorrelationType %s, but got %s", original.CorrelationType, cloned.CorrelationType)
+ }
+ if cloned.Tolerance != original.Tolerance {
+ t.Errorf("Expected Tolerance %f, but got %f", original.Tolerance, cloned.Tolerance)
+ }
+ if cloned.Stored != original.Stored {
+ t.Errorf("Expected Stored %v, but got %v", original.Stored, cloned.Stored)
+ }
+
+ if !reflect.DeepEqual(cloned.Metrics, original.Metrics) {
+ t.Errorf("Expected Metrics %v, but got %v", original.Metrics, cloned.Metrics)
+ }
+ if !reflect.DeepEqual(cloned.ThresholdIDs, original.ThresholdIDs) {
+ t.Errorf("Expected ThresholdIDs %v, but got %v", original.ThresholdIDs, cloned.ThresholdIDs)
+ }
+
+ if len(cloned.Metrics) > 0 && &cloned.Metrics[0] == &original.Metrics[0] {
+ t.Errorf("Metrics slice was not deep copied")
+ }
+ if len(cloned.ThresholdIDs) > 0 && &cloned.ThresholdIDs[0] == &original.ThresholdIDs[0] {
+ t.Errorf("ThresholdIDs slice was not deep copied")
+ }
+}
+
+func TestTrendProfileTenantIDAndTrendProfileWithAPIOpts(t *testing.T) {
+
+ tp := &TrendProfile{
+ Tenant: "cgrates.org",
+ ID: "trend1",
+ Schedule: "*/5 * * * *",
+ StatID: "StatID",
+ Metrics: []string{"metric1", "metric2"},
+ TTL: 10 * time.Minute,
+ QueueLength: 100,
+ MinItems: 10,
+ CorrelationType: "average",
+ Tolerance: 0.05,
+ Stored: true,
+ ThresholdIDs: []string{"thresh1", "thresh2"},
+ }
+
+ tenantID := tp.TenantID()
+
+ expectedTenantID := "cgrates.org" + utils.ConcatenatedKeySep + "trend1"
+ if tenantID != expectedTenantID {
+ t.Errorf("Expected TenantID %s, but got %s", expectedTenantID, tenantID)
+ }
+
+ apiOpts := map[string]any{
+ "option1": "value1",
+ "option2": 42,
+ }
+
+ tpWithAPIOpts := &TrendProfileWithAPIOpts{
+ TrendProfile: tp,
+ APIOpts: apiOpts,
+ }
+
+ if tpWithAPIOpts.Tenant != "cgrates.org" {
+ t.Errorf("Expected Tenant %s, but got %s", "cgrates.org", tpWithAPIOpts.Tenant)
+ }
+ if tpWithAPIOpts.ID != "trend1" {
+ t.Errorf("Expected ID %s, but got %s", "trend1", tpWithAPIOpts.ID)
+ }
+
+ expectedAPIOpts := map[string]any{
+ "option1": "value1",
+ "option2": 42,
+ }
+ if !reflect.DeepEqual(tpWithAPIOpts.APIOpts, expectedAPIOpts) {
+ t.Errorf("Expected APIOpts %v, but got %v", expectedAPIOpts, tpWithAPIOpts.APIOpts)
+ }
+
+}
+
+func TestIndexesAppendMetric(t *testing.T) {
+
+ trend := &Trend{
+ mLast: make(map[string]time.Time),
+ mCounts: make(map[string]int),
+ mTotals: make(map[string]float64),
+ }
+
+ metric1 := &MetricWithTrend{ID: "metric1", Value: 5.0}
+ metric2 := &MetricWithTrend{ID: "metric2", Value: 3.0}
+
+ rTime1 := time.Now()
+ rTime2 := rTime1.Add(10 * time.Minute)
+
+ trend.indexesAppendMetric(metric1, rTime1)
+ trend.indexesAppendMetric(metric2, rTime2)
+ trend.indexesAppendMetric(metric1, rTime2)
+
+ expectedMLast := map[string]time.Time{
+ "metric1": rTime2,
+ "metric2": rTime2,
+ }
+ if !reflect.DeepEqual(trend.mLast, expectedMLast) {
+ t.Errorf("Expected mLast %v, but got %v", expectedMLast, trend.mLast)
+ }
+
+ expectedMCounts := map[string]int{
+ "metric1": 2,
+ "metric2": 1,
+ }
+ if !reflect.DeepEqual(trend.mCounts, expectedMCounts) {
+ t.Errorf("Expected mCounts %v, but got %v", expectedMCounts, trend.mCounts)
+ }
+
+ expectedMTotals := map[string]float64{
+ "metric1": 10.0,
+ "metric2": 3.0,
+ }
+ if !reflect.DeepEqual(trend.mTotals, expectedMTotals) {
+ t.Errorf("Expected mTotals %v, but got %v", expectedMTotals, trend.mTotals)
+ }
+}
+
+func TestTrendTenantID(t *testing.T) {
+ trend := &Trend{
+ Tenant: "cgrates.org",
+ ID: "ID",
+ RunTimes: []time.Time{
+ time.Now(),
+ time.Now().Add(-1 * time.Hour),
+ },
+ Metrics: map[time.Time]map[string]*MetricWithTrend{
+ time.Now(): {
+ "metric1": {ID: "metric1", Value: 1.5},
+ "metric2": {ID: "metric2", Value: 2.0},
+ },
+ time.Now().Add(-1 * time.Hour): {
+ "metric1": {ID: "metric1", Value: 1.0},
+ },
+ },
+ CompressedMetrics: []byte{0x00, 0x01},
+ mLast: map[string]time.Time{
+ "metric1": time.Now(),
+ "metric2": time.Now().Add(-1 * time.Hour),
+ },
+ mCounts: map[string]int{
+ "metric1": 2,
+ "metric2": 1,
+ },
+ mTotals: map[string]float64{
+ "metric1": 2.5,
+ "metric2": 2.0,
+ },
+ tP: &TrendProfile{
+ Tenant: "cgrates.org",
+ ID: "trendProfileID",
+ Schedule: "0 * * * *",
+ StatID: "statID1",
+ QueueLength: 10,
+ TTL: 5 * time.Minute,
+ MinItems: 1,
+ CorrelationType: "average",
+ Tolerance: 0.1,
+ Stored: true,
+ ThresholdIDs: []string{"threshold1", "threshold2"},
+ },
+ }
+
+ tenantID := trend.TenantID()
+
+ expectedTenantID := "cgrates.org:ID"
+ if tenantID != expectedTenantID {
+ t.Errorf("Expected TenantID %v, but got %v", expectedTenantID, tenantID)
+ }
+
+ if len(trend.RunTimes) != 2 {
+ t.Errorf("Expected 2 run times, but got %d", len(trend.RunTimes))
+ }
+
+ if len(trend.Metrics) != 2 {
+ t.Errorf("Expected 2 metrics time entries, but got %d", len(trend.Metrics))
+ }
+
+ if trend.tP.QueueLength != 10 {
+ t.Errorf("Expected QueueLength 10, but got %d", trend.tP.QueueLength)
+ }
+}
diff --git a/engine/rankings_test.go b/engine/rankings_test.go
new file mode 100644
index 000000000..e4bd913bd
--- /dev/null
+++ b/engine/rankings_test.go
@@ -0,0 +1,126 @@
+/*
+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 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see
+*/
+
+package engine
+
+import (
+ "testing"
+ "time"
+
+ "github.com/cgrates/cgrates/config"
+)
+
+func TestTenantID(t *testing.T) {
+ rp := &RankingProfile{
+ Tenant: "cgrates.org",
+ ID: "01",
+ QueryInterval: 5 * time.Minute,
+ StatIDs: []string{"stat1", "stat2"},
+ MetricIDs: []string{"metric1"},
+ Sorting: "asc",
+ SortingParameters: []string{"param1"},
+ ThresholdIDs: []string{"threshold1"},
+ }
+
+ tenantID := rp.TenantID()
+
+ expectedTenantID := "cgrates.org:01"
+
+ if tenantID != expectedTenantID {
+ t.Errorf("TenantID() = %v; want %v", tenantID, expectedTenantID)
+ }
+}
+
+func TestRankingProfileWithAPIOpts(t *testing.T) {
+ rp := &RankingProfile{
+ Tenant: "cgrates.org",
+ ID: "ID",
+ QueryInterval: 5 * time.Minute,
+ StatIDs: []string{"stat1", "stat2"},
+ MetricIDs: []string{"metric1"},
+ Sorting: "asc",
+ SortingParameters: []string{"param1"},
+ ThresholdIDs: []string{"threshold1"},
+ }
+
+ rpo := RankingProfileWithAPIOpts{
+ RankingProfile: rp,
+ APIOpts: map[string]any{"option1": "value1"},
+ }
+
+ if rpo.APIOpts["option1"] != "value1" {
+ t.Errorf("APIOpts[option1] = %v; want %v", rpo.APIOpts["option1"], "value1")
+ }
+
+ if rpo.Tenant != rp.Tenant {
+ t.Errorf("RankingProfile Tenant = %v; want %v", rpo.Tenant, rp.Tenant)
+ }
+
+ if rpo.ID != rp.ID {
+ t.Errorf("RankingProfile ID = %v; want %v", rpo.ID, rp.ID)
+ }
+}
+
+func TestRankingProfileLockKey(t *testing.T) {
+ tests := []struct {
+ tenant string
+ id string
+ expected string
+ }{
+ {"cgrates.org", "01", "*ranking_profiles:cgrates.org:01"},
+ {"cgrates.org", "02", "*ranking_profiles:cgrates.org:02"},
+ {"cgrates.org", "03", "*ranking_profiles:cgrates.org:03"},
+ }
+
+ for _, test := range tests {
+ result := rankingProfileLockKey(test.tenant, test.id)
+
+ if result != test.expected {
+ t.Errorf("rankingProfileLockKey(%q, %q) = %v; want %v", test.tenant, test.id, result, test.expected)
+ }
+ }
+}
+
+func TestNewRankingService(t *testing.T) {
+ dm := &DataManager{}
+ cgrcfg := &config.CGRConfig{}
+ filterS := &FilterS{}
+ connMgr := &ConnManager{}
+
+ rankingService := NewRankingService(dm, cgrcfg, filterS, connMgr)
+
+ if rankingService == nil {
+ t.Fatal("NewRankingService() returned nil")
+ }
+
+ if rankingService.dm != dm {
+ t.Errorf("Expected dm to be %v, got %v", dm, rankingService.dm)
+ }
+
+ if rankingService.cfg != cgrcfg {
+ t.Errorf("Expected cfg to be %v, got %v", cgrcfg, rankingService.cfg)
+ }
+
+ if rankingService.fltrS != filterS {
+ t.Errorf("Expected fltrS to be %v, got %v", filterS, rankingService.fltrS)
+ }
+
+ if rankingService.connMgr != connMgr {
+ t.Errorf("Expected connMgr to be %v, got %v", connMgr, rankingService.connMgr)
+ }
+}