added tests on thresholds processing

This commit is contained in:
gezimbll
2025-06-11 15:45:36 +02:00
committed by Dan Christian Bogos
parent f3ca5b0016
commit fbb625962c
6 changed files with 675 additions and 0 deletions

View File

@@ -877,3 +877,36 @@ func TestRSRParsersClone(t *testing.T) {
t.Errorf("Expected clone to not modify the cloned")
}
}
func TestRSRParsersValues(t *testing.T) {
ts := []struct {
name string
prsRules string
value string
parsedValue string
}{
{name: "TestSearchAndReplaceNumber", prsRules: "~*req.Config.FilterIDs[0]:s/^\\*gt:.*(\\d+)$/${1}/", value: "*gt:~*req.*sum#1:9", parsedValue: "9"},
{name: "TestSearchAndReplaceNumberNotMatch", prsRules: "~*req.Destination:s/^\\+41(\\d+)$/${1}/", value: "+415504", parsedValue: "5504"},
{name: "TestSearchAndReplaceEmptyReplace", prsRules: "~*req.Destination:s/^\\+41(\\d+)$//", value: "+415504", parsedValue: ""},
{name: "TestSearchAndReplaceEmptySearch", prsRules: "~*req.Account:s/^100/${1}/", value: "1001", parsedValue: ""},
{name: "TestReplaceInMiddle", prsRules: "~*req:User-Agent:s/^(kamailio)_(\\w+)$/${1}-${2}/", value: "kamailio_agent", parsedValue: "kamailio-agent"},
}
for _, tt := range ts {
t.Run(tt.name, func(t *testing.T) {
parser, err := NewRSRParser(tt.prsRules)
if err != nil {
t.Fatal(err)
}
if err := parser.Compile(); err != nil {
t.Error(err)
}
val, err := parser.parseValue(tt.value)
if err != nil {
t.Fatal(err)
}
if val != tt.parsedValue {
t.Errorf("expected %s, received %s", tt.parsedValue, val)
}
})
}
}

View File

@@ -768,3 +768,122 @@ func TestCheckDefaultTiming(t *testing.T) {
})
}
}
func TestActionTimingGetNextStartTime2(t *testing.T) {
tests := []struct {
name string
actionTiming *ActionTiming
startTime time.Time
expectedTime time.Time
expectedError bool
}{
{
name: "MonthlyEstimatedTiming",
actionTiming: &ActionTiming{
Timing: &RateInterval{
Timing: &RITiming{
ID: utils.MetaMonthlyEstimated,
StartTime: "00:00:00",
Years: []int{2024},
Months: []time.Month{1, 2, 3},
MonthDays: []int{31},
},
},
},
startTime: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC),
expectedTime: time.Date(2024, 1, 31, 0, 0, 0, 0, time.UTC),
},
{
name: "WeekDaysCron",
actionTiming: &ActionTiming{
Timing: &RateInterval{
Timing: &RITiming{
StartTime: "09:00:00",
Years: []int{2024},
Months: []time.Month{1, 2, 3},
MonthDays: []int{1, 15},
WeekDays: []time.Weekday{time.Monday, time.Wednesday, time.Friday},
EndTime: "17:00:00",
},
},
},
startTime: time.Date(2024, 1, 1, 10, 0, 0, 0, time.UTC),
expectedTime: time.Date(2024, 1, 3, 9, 0, 0, 0, time.UTC),
},
{
name: "YearTransition",
actionTiming: &ActionTiming{
Timing: &RateInterval{
Timing: &RITiming{
StartTime: "00:00:00",
Years: []int{2024, 2025},
Months: []time.Month{12, 1},
MonthDays: []int{31, 1},
},
},
},
startTime: time.Date(2024, 12, 31, 23, 0, 0, 0, time.UTC),
expectedTime: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
name: "LeapYear",
actionTiming: &ActionTiming{
Timing: &RateInterval{
Timing: &RITiming{
StartTime: "00:00:00",
Years: []int{2024},
Months: []time.Month{2},
MonthDays: []int{29},
},
},
},
startTime: time.Date(2024, 2, 28, 0, 0, 0, 0, time.UTC),
expectedTime: time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
},
{
name: "NilTiming",
actionTiming: &ActionTiming{
Timing: nil,
},
startTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
expectedTime: time.Time{},
expectedError: true,
},
{
name: "EmptyTiming",
actionTiming: &ActionTiming{
Timing: &RateInterval{
Timing: nil,
},
},
startTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
expectedTime: time.Time{},
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.actionTiming != nil {
tt.actionTiming.ResetStartTimeCache()
}
result := tt.actionTiming.GetNextStartTime(tt.startTime)
if tt.expectedError {
if !result.IsZero() {
t.Errorf("Expected zero time for error case, got: %v", result)
}
return
}
if !result.Equal(tt.expectedTime) {
t.Errorf("GetNextStartTime(%v) = %v; want %v",
tt.startTime, result, tt.expectedTime)
}
cachedResult := tt.actionTiming.GetNextStartTime(tt.startTime)
if !cachedResult.Equal(result) {
t.Errorf("Cached result differs: got %v, want %v",
cachedResult, result)
}
})
}
}

View File

@@ -3451,3 +3451,101 @@ func TestFilterToSQLQueryValidations(t *testing.T) {
})
}
}
func TestWeightFromDynamics2(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
connMgr = NewConnManager(cfg, nil)
db, _ := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items)
dm := NewDataManager(db, cfg.CacheCfg(), nil)
filterS := NewFilterS(cfg, connMgr, dm)
testCases := []struct {
name string
dynamicWeights []*utils.DynamicWeight
tenant string
event utils.DataProvider
expectedWeight float64
expectedErr bool
}{
{
name: "EmptyDynamicWeight",
dynamicWeights: []*utils.DynamicWeight{},
tenant: "cgrates.org",
event: utils.MapStorage{},
expectedWeight: 0.0,
expectedErr: false,
},
{
name: "MatchingWeight",
dynamicWeights: []*utils.DynamicWeight{
{
FilterIDs: []string{"*string:~*req.Account:1001"},
Weight: 10.0,
},
},
tenant: "cgrates.org",
event: utils.MapStorage{
"*req": utils.MapStorage{
"Account": "1001",
},
},
expectedWeight: 10.0,
},
{
name: "MultipleMatchingWeights",
dynamicWeights: []*utils.DynamicWeight{
{
FilterIDs: []string{"*prefix:~*req.Destination:GER"},
Weight: 10.0,
},
{
FilterIDs: []string{"*string:~*opts.subsys:*sessions"},
Weight: 5.0,
},
},
tenant: "cgrates.org",
event: utils.MapStorage{
"*req": utils.MapStorage{
"Destination": "GER_0055",
},
"*opts": utils.MapStorage{
"*subsys": "*sessions",
},
},
expectedWeight: 10.0,
},
{
name: "NoMatchingWeightFilterErr",
dynamicWeights: []*utils.DynamicWeight{
{
FilterIDs: []string{"FLTR1"},
Weight: 10.0,
},
},
tenant: "cgrates.org",
event: utils.MapStorage{
"*req": utils.MapStorage{
"Account": "1001",
},
},
expectedErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
weight, err := WeightFromDynamics(tc.dynamicWeights, filterS, tc.tenant, tc.event)
if tc.expectedErr {
if err == nil {
t.Error("Expected error but got none")
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if weight != tc.expectedWeight {
t.Errorf("Expected weight %v, got %v", tc.expectedWeight, weight)
}
}
})
}
}

View File

@@ -23,6 +23,7 @@ import (
"log"
"os"
"reflect"
"slices"
"sort"
"strings"
"testing"
@@ -2163,3 +2164,77 @@ func TestThresholdsStoreThresholdCacheSetErr(t *testing.T) {
utils.Logger.SetLogLevel(0)
}
func TestThresholdProcessEvent(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
cfg.ThresholdSCfg().IndexedSelects = false
db, _ := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items)
dm := NewDataManager(db, cfg.CacheCfg(), nil)
fS := NewFilterS(cfg, nil, dm)
ths := NewThresholdService(dm, cfg, fS, nil)
thps := []*ThresholdProfile{
{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 3,
MaxHits: 2,
},
{
Tenant: "cgrates.org",
ID: "TH2",
FilterIDs: []string{"*string:~*req.Account:1002"},
MinHits: 2,
MaxHits: 3,
}, {
Tenant: "cgrates.org",
ID: "TH3",
FilterIDs: []string{"*string:~*req.Account:1003"},
MinHits: 1,
MaxHits: -1,
},
}
for _, thP := range thps {
if err := ths.dm.SetThresholdProfile(thP, false); err != nil {
t.Error(err)
}
}
tts := []struct {
name string
runs int
cgrEvnt map[string]any
matchedthIDs []string
}{
{
name: "MinHitsLargerThanMaxHits",
runs: 3,
cgrEvnt: map[string]any{
utils.AccountField: "1001",
},
},
{
name: "MinHitsLargerThanMaxHits",
runs: 4,
cgrEvnt: map[string]any{
utils.AccountField: "1002",
},
},
{},
}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
var thIDs []string
for range tt.runs {
var err error
thIDs, err = ths.processEvent("cgrates.org", &utils.CGREvent{Event: tt.cgrEvnt})
if err != nil {
t.Error(err)
}
}
if !slices.Equal(thIDs, tt.matchedthIDs) {
t.Errorf("expected: %v, received: %v", tt.matchedthIDs, thIDs)
}
})
}
}

View File

@@ -978,3 +978,175 @@ func TestEngineUnitCounterString(t *testing.T) {
t.Errorf("Expected JSON: %s, got: %s", want, got)
}
}
func TestResetCounters(t *testing.T) {
tests := []struct {
name string
initialCounters UnitCounters
action *Action
expectedCounters UnitCounters
}{
{
name: "ResetAlCountersNilAction",
initialCounters: UnitCounters{
utils.MetaMonetary: []*UnitCounter{
{
CounterType: utils.MetaCounterEvent,
Counters: CounterFilters{
{Value: 100.0, Filter: &BalanceFilter{ID: utils.StringPointer("BAL_MON1")}},
{Value: 200.0, Filter: &BalanceFilter{ID: utils.StringPointer("BAL_MON2")}},
},
},
{
CounterType: utils.MetaBalance,
Counters: CounterFilters{
{Value: 50.0, Filter: &BalanceFilter{ID: utils.StringPointer("BAL_MON1")}},
},
},
},
utils.MetaVoice: []*UnitCounter{
{
CounterType: utils.MetaCounterEvent,
Counters: CounterFilters{
{Value: 150.0, Filter: &BalanceFilter{ID: utils.StringPointer("VOICE1")}},
},
},
},
},
action: nil,
expectedCounters: UnitCounters{
utils.MetaMonetary: []*UnitCounter{
{
CounterType: utils.MetaCounterEvent,
Counters: CounterFilters{
{Value: 0.0, Filter: &BalanceFilter{ID: utils.StringPointer("BAL_MON1")}},
{Value: 0.0, Filter: &BalanceFilter{ID: utils.StringPointer("BAL_MON2")}},
},
},
{
CounterType: utils.MetaBalance,
Counters: CounterFilters{
{Value: 0.0, Filter: &BalanceFilter{ID: utils.StringPointer("BAL_MON1")}},
},
},
},
utils.MetaVoice: []*UnitCounter{
{
CounterType: utils.MetaCounterEvent,
Counters: CounterFilters{
{Value: 0.0, Filter: &BalanceFilter{ID: utils.StringPointer("VOICE1")}},
},
},
},
},
},
{
name: "ResetCountersMonetaryBalanceType",
initialCounters: UnitCounters{
"*monetary": []*UnitCounter{
{
CounterType: utils.MetaBalance,
Counters: CounterFilters{
{Value: 100.0, Filter: &BalanceFilter{ID: utils.StringPointer("MON1")}},
{Value: 200.0, Filter: &BalanceFilter{ID: utils.StringPointer("MON2")}},
},
},
},
"*data": []*UnitCounter{
{
CounterType: utils.MetaCounterEvent,
Counters: CounterFilters{
{Value: 50.0, Filter: &BalanceFilter{ID: utils.StringPointer("MB_BAL")}},
},
},
},
},
action: &Action{
Balance: &BalanceFilter{Type: utils.StringPointer("*monetary")},
},
expectedCounters: UnitCounters{
"*monetary": []*UnitCounter{
{
CounterType: utils.MetaBalance,
Counters: CounterFilters{
{Value: 100.0, Filter: &BalanceFilter{ID: utils.StringPointer("MON1")}},
{Value: 200.0, Filter: &BalanceFilter{ID: utils.StringPointer("MON2")}},
},
},
},
"*data": []*UnitCounter{
{
CounterType: utils.MetaCounterEvent,
Counters: CounterFilters{
{Value: 50.0, Filter: &BalanceFilter{ID: utils.StringPointer("MB_BAL")}},
},
},
},
},
},
{
name: "ResetSpecificBalanceType",
initialCounters: UnitCounters{
"*monetary": []*UnitCounter{
{
CounterType: utils.MetaBalance,
Counters: CounterFilters{
{Value: 100.0, Filter: &BalanceFilter{ID: utils.StringPointer("MON1"), Type: utils.StringPointer("*monetary")}},
{Value: 200.0, Filter: &BalanceFilter{ID: utils.StringPointer("MON2"), Type: utils.StringPointer("*monetary")}},
},
},
},
},
action: &Action{
Balance: &BalanceFilter{ID: utils.StringPointer("MON1"), Type: utils.StringPointer("*monetary")},
},
expectedCounters: UnitCounters{
"*monetary": []*UnitCounter{
{
CounterType: utils.MetaBalance,
Counters: CounterFilters{
{Value: 0.0, Filter: &BalanceFilter{ID: utils.StringPointer("MON1"), Type: utils.StringPointer("*monetary")}},
{Value: 200.0, Filter: &BalanceFilter{ID: utils.StringPointer("MON2"), Type: utils.StringPointer("*monetary")}},
},
},
},
},
},
{
name: "ActionBalanceTypeNotExist",
initialCounters: UnitCounters{
"*data": []*UnitCounter{
{
CounterType: utils.MetaCounterEvent,
Counters: CounterFilters{
{Value: 150.0, Filter: &BalanceFilter{ID: utils.StringPointer("DATA1"), Type: utils.StringPointer("*data")}},
},
},
},
},
action: &Action{
Balance: &BalanceFilter{Type: utils.StringPointer("*monetary")},
},
expectedCounters: UnitCounters{
"*data": []*UnitCounter{
{
CounterType: utils.MetaCounterEvent,
Counters: CounterFilters{
{Value: 150.0, Filter: &BalanceFilter{ID: utils.StringPointer("DATA1"), Type: utils.StringPointer("*data")}},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cloneInitialCounters := tt.initialCounters.Clone()
cloneInitialCounters.resetCounters(tt.action)
if !reflect.DeepEqual(cloneInitialCounters, tt.expectedCounters) {
t.Errorf("mismatch after resetCounters.\nExpected:\n%s\nGot:\n%s",
utils.ToJSON(tt.expectedCounters), utils.ToJSON(cloneInitialCounters))
}
})
}
}

View File

@@ -0,0 +1,178 @@
//go:build integration
// +build integration
/*
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 <http://www.gnu.org/licenses/>
*/
package general_tests
import (
"bytes"
"testing"
"time"
"github.com/cgrates/birpc/context"
"github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/utils"
)
func TestThresholdEventEEs(t *testing.T) {
var dbConfig engine.DBCfg
switch *utils.DBType {
case utils.MetaMySQL:
case utils.MetaMongo:
dbConfig = engine.MongoDBCfg
case utils.MetaPostgres, utils.MetaInternal:
t.SkipNow()
default:
t.Fatal("unsupported dbtype value")
}
content := `{
"general": {
"log_level": 7,
},
"apiers": {
"enabled": true
},
"cdrs":{
"enabled": true,
"stats_conns": ["*localhost"],
},
"stats": {
"enabled": true,
"indexed_selects":false,
"thresholds_conns": ["*localhost"],
},
"thresholds": {
"enabled": true,
"indexed_selects":false,
"ees_conns": ["*localhost"]
},
"ees": {
"enabled": true,
"exporters": [
{
"id": "exporter1",
"type": "*virt",
"attempts": 1,
"synchronous": true,
"fields":[
{"tag": "Filter1", "path": "*uch.Filter1", "type": "*variable", "value": "~*req.Config.FilterIDs[0]"},
{"tag": "Filter2", "path": "*uch.Filter2", "type": "*variable", "value": "~*req.Config.FilterIDs[1]"},
],
},
]
}
}`
csvFiles := map[string]string{
utils.StatsCsv: `#Tenant[0],Id[1],FilterIDs[2],ActivationInterval[3],QueueLength[4],TTL[5],MinItems[6],Metrics[7],MetricFilterIDs[8],Stored[9],Blocker[10],Weight[11],ThresholdIDs[12]
cgrates.org,SQ_1,*string:~*req.Account:1001,,,-1,,*sum#1,,false,,,TH1`,
utils.ThresholdsCsv: `#Tenant[0],Id[1],FilterIDs[2],ActivationInterval[3],MaxHits[4],MinHits[5],MinSleep[6],Blocker[7],Weight[8],ActionIDs[9],Async[10],EeIDs[11]
cgrates.org,TH1,*string:~*req.StatID:SQ_1;*eq:~*req.*sum#1:2,,-1,1,0,false,,ACT_LOG,false,exporter1`,
utils.ActionsCsv: `#ActionsId[0],Action[1],ExtraParameters[2],Filter[3],BalanceId[4],BalanceType[5],Categories[6],DestinationIds[7],RatingSubject[8],SharedGroup[9],ExpiryTime[10],TimingIds[11],Units[12],BalanceWeight[13],BalanceBlocker[14],BalanceDisabled[15],Weight[16]
ACT_LOG,*log,,,,,,,,,,,,,,,0`,
}
ng := engine.TestEngine{
ConfigJSON: content,
DBCfg: dbConfig,
LogBuffer: bytes.NewBuffer(nil),
TpFiles: csvFiles,
}
client, _ := ng.Run(t)
t.Run("CDREventStatsToThreshold", func(t *testing.T) {
// event from StatS to ThresholdS returns NOT_FOUND but should be ignored
var reply string
if err := client.Call(context.Background(),
utils.CDRsV1ProcessEvent,
&engine.ArgV1ProcessEvent{
CGREvent: utils.CGREvent{
Tenant: "cgrates.org",
ID: "TestEv1",
Event: map[string]any{
utils.ToR: utils.MetaVoice,
utils.OriginID: "Origin2",
utils.RequestType: utils.MetaPrepaid,
utils.AccountField: "1001",
utils.Subject: "1001",
utils.Destination: "1002",
utils.Usage: time.Minute,
},
},
}, &reply); err != nil {
t.Error("Unexpected error: ", err.Error())
} else if reply != utils.OK {
t.Error("Unexpected reply received: ", reply)
}
})
t.Run("ThresholdsToEEsEvent", func(t *testing.T) {
// it matches the threshold and passes the event to EEs without any errors
var reply string
if err := client.Call(context.Background(),
utils.CDRsV1ProcessEvent,
&engine.ArgV1ProcessEvent{
CGREvent: utils.CGREvent{
Tenant: "cgrates.org",
ID: "TestEv1",
Event: map[string]any{
utils.ToR: utils.MetaVoice,
utils.OriginID: "Origin1",
utils.RequestType: utils.MetaPrepaid,
utils.AccountField: "1001",
utils.Subject: "1001",
utils.Destination: "1002",
utils.Usage: time.Minute,
},
},
}, &reply); err != nil {
t.Error("Unexpected error: ", err.Error())
} else if reply != utils.OK {
t.Error("Unexpected reply received: ", reply)
}
})
t.Run("CheckExporterIDs", func(t *testing.T) {
// filters in event always should be in ascending order
var filter1 any
if err := client.Call(context.Background(), utils.CacheSv1GetItem, &utils.ArgsGetCacheItemWithAPIOpts{
Tenant: "cgrates.org",
ArgsGetCacheItem: utils.ArgsGetCacheItem{
CacheID: utils.CacheUCH,
ItemID: "Filter1",
},
}, &filter1); err != nil {
t.Error(err)
} else if filter1 != "*eq:~*req.*sum#1:2" {
t.Errorf("expected %v, received %v", "*eq:~*req.*sum#1:2", filter1)
}
var filter2 any
if err := client.Call(context.Background(), utils.CacheSv1GetItem, &utils.ArgsGetCacheItemWithAPIOpts{
Tenant: "cgrates.org",
ArgsGetCacheItem: utils.ArgsGetCacheItem{
CacheID: utils.CacheUCH,
ItemID: "Filter2",
},
}, &filter2); err != nil {
t.Error(err)
} else if filter2 != "*string:~*req.StatID:SQ_1" {
t.Errorf("expected %v, received %v", "*string:~*req.StatID:SQ_1", filter2)
}
})
}