/* 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 actions import ( "cmp" "fmt" "reflect" "slices" "strings" "testing" "time" "github.com/cgrates/birpc" "github.com/cgrates/birpc/context" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/engine" "github.com/cgrates/cgrates/utils" "github.com/cgrates/rpcclient" ) type ccMock struct { calls map[string]func(ctx *context.Context, args any, reply any) error } func (ccM *ccMock) Call(ctx *context.Context, serviceMethod string, args any, reply any) (err error) { if call, has := ccM.calls[serviceMethod]; !has { return rpcclient.ErrUnsupporteServiceMethod } else { return call(ctx, args, reply) } } func TestParseParamStringToMap(t *testing.T) { testcases := []struct { name string paramStr string wantMap map[string]any expectErr bool errMsg string }{ { name: "TenantAndAccount", paramStr: "*tenant:cgrates.org&*account:1001", wantMap: map[string]any{ "*tenant": "cgrates.org", "*account": "1001", }, expectErr: false, }, { name: "TenantAccountDestination", paramStr: "*tenant:cgrates.org&*account:1001&*destination:1002", wantMap: map[string]any{ "*tenant": "cgrates.org", "*account": "1001", "*destination": "1002", }, expectErr: false, }, { name: "InvalidPairMissingColon", paramStr: "*tenantcgrates.org&*profileID:ID1001", wantMap: nil, expectErr: true, errMsg: "invalid key-value pair: *tenantcgrates.org", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { got := make(map[string]any) err := parseParamStringToMap(tc.paramStr, got) if tc.expectErr { if err == nil { t.Errorf("expected error but got nil") } else if err.Error() != tc.errMsg { t.Errorf("expected error <%v>, got <%v>", tc.errMsg, err) } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(got, tc.wantMap) { t.Errorf("expected %+v, got %+v", tc.wantMap, got) } }) } } func TestActDynamicThresholdID(t *testing.T) { aL1 := &actDynamicThreshold{ aCfg: &utils.APAction{ ID: "TH1", }, } if aL1.id() != "TH1" { t.Errorf("expected ID , got <%s>", aL1.id()) } aL2 := &actDynamicThreshold{ aCfg: &utils.APAction{ ID: "", }, } if aL2.id() != "" { t.Errorf("expected empty ID, got <%s>", aL2.id()) } } func TestActDynamicThresholdCfg(t *testing.T) { aCfg1 := &utils.APAction{ID: "TH1"} aL1 := &actDynamicThreshold{aCfg: aCfg1} if got := aL1.cfg(); got != aCfg1 { t.Errorf("Scenario 1: expected cfg <%v>, got <%v>", aCfg1, got) } aCfg2 := &utils.APAction{} aL2 := &actDynamicThreshold{aCfg: aCfg2} if got := aL2.cfg(); got != aCfg2 { t.Errorf("Scenario 2: expected cfg <%v>, got <%v>", aCfg2, got) } aL3 := &actDynamicThreshold{aCfg: nil} if got := aL3.cfg(); got != nil { t.Errorf("Scenario 3: expected cfg , got <%v>", got) } } func TestActDynamicThresholdExecuteSort(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone cfg.ActionSCfg().AdminSConns = []string{"admins"} connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) data := utils.MapStorage{ utils.AccountField: "1001", } diktats := []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{{Weight: 10}}, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;THD1;;1;1;1s;true;AP1;true;ee1;key1:val1", }, }, { ID: "d2", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{{Weight: 20}}, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;THD2;;1;1;1s;true;AP2;true;ee2;key2:val2", }, }, } act := &actDynamicThreshold{ config: cfg, connMgr: connMgr, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{utils.AccountField: "1001"}, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC", Diktats: diktats, }, } weights := map[string]float64{ "d1": 10, "d2": 20, } sorted := make([]*utils.APDiktat, len(diktats)) copy(sorted, diktats) slices.SortFunc(sorted, func(a, b *utils.APDiktat) int { return cmp.Compare(weights[b.ID], weights[a.ID]) }) if sorted[0].ID != "d2" || sorted[1].ID != "d1" { t.Errorf("expected d2 to be first and d1 second after sort, got %v", []string{sorted[0].ID, sorted[1].ID}) } ctx := context.Background() trgID := "trigger1" err := act.execute(ctx, data, trgID) if err != nil { t.Fatalf("execute failed: %v", err) } } func TestActDynamicThresholdExecute(t *testing.T) { engine.Cache.Clear(nil) defer engine.Cache.Clear(nil) var tpwo *engine.ThresholdProfileWithAPIOpts ccM := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.AdminSv1SetThresholdProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if tpwo, canCast = args.(*engine.ThresholdProfileWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } testcases := []struct { name string diktats []*utils.APDiktat expTpwo *engine.ThresholdProfileWithAPIOpts wantErr bool }{ { name: "SuccessfulRequest", expTpwo: &engine.ThresholdProfileWithAPIOpts{ ThresholdProfile: &engine.ThresholdProfile{ Tenant: "cgrates.org", ID: "THD_ACNT_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, MaxHits: 1, MinHits: 1, MinSleep: 1 * time.Second, Blocker: true, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, ActionProfileIDs: []string{"ACT_LOG_WARNING"}, Async: true, EeIDs: []string{"eeID1", "eeID2"}, }, APIOpts: map[string]any{ "key": "value", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;THD_ACNT_1001;*string:~*req.Account:1001;&20;1;1;1s;true;ACT_LOG_WARNING;true;eeID1&eeID2;key:value", }, }, }, wantErr: false, }, { name: "SuccessfulRequestWithDyanmicPaths", expTpwo: &engine.ThresholdProfileWithAPIOpts{ ThresholdProfile: &engine.ThresholdProfile{ Tenant: "cgrates.org", ID: "THD_ACNT_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, MaxHits: 1, MinHits: 1, MinSleep: 1 * time.Second, Blocker: true, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, ActionProfileIDs: []string{"ACT_LOG_WARNING"}, Async: true, EeIDs: []string{"eeID1", "eeID2"}, }, APIOpts: map[string]any{ "key": "value", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;THD_ACNT_<~*req.Account>;*string:~*req.Account:<~*req.Account>;&20;1;1;1s;true;ACT_LOG_WARNING;true;eeID1&eeID2;key:value", }, }, }, wantErr: false, }, { name: "SuccessfulRequestEmptyFields", expTpwo: &engine.ThresholdProfileWithAPIOpts{ ThresholdProfile: &engine.ThresholdProfile{ Tenant: "cgrates.org", ID: "THD_ACNT_1001", FilterIDs: nil, MaxHits: 0, MinHits: 0, MinSleep: 0, Blocker: false, Weights: nil, ActionProfileIDs: nil, Async: false, EeIDs: nil, }, APIOpts: map[string]any{}, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;THD_ACNT_1001;;;;;;;;;;", }, }, }, wantErr: false, }, { name: "EmptyDiktats", diktats: []*utils.APDiktat{}, wantErr: true, }, { name: "InvalidNumberOfParams", diktats: []*utils.APDiktat{ { ID: "d4", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;THD_ACNT_1004;1;2", }, }, }, wantErr: true, }, { name: "InvalidMaxHitsParam", diktats: []*utils.APDiktat{ { ID: "d5", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;THD_ACNT_1005;*string:~*req.Account:1005;;invalid;1;1s;true;ACT_LOG_WARNING;true;eeID5;key:value", }, }, }, wantErr: true, }, { name: "InvalidMinSleepParam", diktats: []*utils.APDiktat{ { ID: "d6", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;THD_ACNT_1006;*string:~*req.Account:1006;&20;1;1;invalid;true;ACT_LOG_WARNING;true;eeID6;key:value", }, }, }, wantErr: true, }, { name: "WeightParamUnsupportedFormat", diktats: []*utils.APDiktat{ { ID: "d10", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;THD_ACNT_1010;;w1&w2&w3;1;1;1s;true;ACT_LOG_WARNING;true;eeID10;key:value", }, }, }, wantErr: true, }, { name: "WeightParamInvalidFloat", diktats: []*utils.APDiktat{ { ID: "d11", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;THD_ACNT_1011;;w1&invalid;1;1;1s;true;ACT_LOG_WARNING;true;eeID11;key:value", }, }, }, wantErr: true, }, { name: "APIOptsInvalidFormat", diktats: []*utils.APDiktat{ { ID: "d12", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;THD_ACNT_1012;;;;1;1;1s;true;ACT_LOG_WARNING;true;eeID12;keyvalue", }, }, }, wantErr: true, }, { name: "BlockerFail", diktats: []*utils.APDiktat{ { ID: "d14", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;THD_ACNT_1014;*string:~*req.Account:1014;;1;1;1s;notbool;ACT_LOG_WARNING;true;eeID14;key:value", }, }, }, wantErr: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone cfg.ActionSCfg().AdminSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS)} connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) rpcInternal := make(chan birpc.ClientConnector, 1) rpcInternal <- ccM connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS), utils.AdminSv1, rpcInternal) act := &actDynamicThreshold{ config: cfg, connMgr: connMgr, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{ utils.AccountField: "1001", }, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC", Diktats: tc.diktats, }, } t.Cleanup(func() { tpwo = nil }) ctx := context.Background() data := utils.MapStorage{ "*req": map[string]any{ utils.AccountField: "1001", }, } trgID := "trigger1" err := act.execute(ctx, data, trgID) if tc.wantErr { if err == nil { t.Errorf("expected an error, but got nil") } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(tpwo, tc.expTpwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expTpwo), utils.ToJSON(tpwo)) } }) } } func TestActDynamicStatsID(t *testing.T) { actionCfg := &utils.APAction{ ID: "StatsID", } aL := &actDynamicStats{ aCfg: actionCfg, } got := aL.id() if got != "StatsID" { t.Errorf("expected %q, got %q", "StatsID", got) } } func TestActDynamicStatsCfg(t *testing.T) { testcases := []struct { name string actCfg *utils.APAction expIsNil bool }{ { name: "ValidAPAction", actCfg: &utils.APAction{ ID: "StatsProfile!", }, expIsNil: false, }, { name: "NilAPAction", actCfg: nil, expIsNil: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { aL := &actDynamicStats{ config: config.NewDefaultCGRConfig(), connMgr: &engine.ConnManager{}, aCfg: tc.actCfg, tnt: "cgrates.org", cgrEv: &utils.CGREvent{}, } got := aL.cfg() if tc.expIsNil && got != nil { t.Errorf("expected nil, got %+v", got) } if !tc.expIsNil && got != tc.actCfg { t.Errorf("expected pointer %p, got %p", tc.actCfg, got) } }) } } func TestActDynamicStatsExecute(t *testing.T) { engine.Cache.Clear(nil) defer engine.Cache.Clear(nil) var sqpwo *engine.StatQueueProfileWithAPIOpts ccM := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.AdminSv1SetStatQueueProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if sqpwo, canCast = args.(*engine.StatQueueProfileWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } testcases := []struct { name string diktats []*utils.APDiktat expSqpwo *engine.StatQueueProfileWithAPIOpts wantErr bool }{ { name: "SuccessfulRequest", expSqpwo: &engine.StatQueueProfileWithAPIOpts{ StatQueueProfile: &engine.StatQueueProfile{ Tenant: "cgrates.org", ID: "STATS_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, QueueLength: 100, TTL: 1 * time.Hour, MinItems: 1, Stored: true, ThresholdIDs: []string{"THD_1", "THD_2"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*string:~*req.Account:1001"}, Weight: 20.0, }, }, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*string:~*req.Account:1001"}, Blocker: true, }, }, Metrics: []*engine.MetricWithFilters{ { MetricID: "ASR", FilterIDs: []string{"*string:~*req.Account:1001"}, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*string:~*req.Account:1001"}, Blocker: false, }, }, }, { MetricID: "PDD", FilterIDs: []string{"*string:~*req.Account:1001"}, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*string:~*req.Account:1001"}, Blocker: false, }, }, }, }, }, APIOpts: map[string]any{ "key": "value", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1001;*string:~*req.Account:1001;*string:~*req.Account:1001&20;*string:~*req.Account:1001&true;100;1h;1;true;THD_1&THD_2;ASR&PDD;*string:~*req.Account:1001;*string:~*req.Account:1001&false;key:value", }, }, }, wantErr: false, }, { name: "SuccessfulRequestWithDynamicPaths", expSqpwo: &engine.StatQueueProfileWithAPIOpts{ StatQueueProfile: &engine.StatQueueProfile{ Tenant: "cgrates.org", ID: "STATS_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, QueueLength: 100, TTL: 1 * time.Hour, MinItems: 1, Stored: true, ThresholdIDs: []string{"THD_1", "THD_2"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*string:~*req.Account:1001"}, Weight: 20.0, }, }, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*string:~*req.Account:1001"}, Blocker: true, }, }, Metrics: []*engine.MetricWithFilters{ { MetricID: "ASR", FilterIDs: []string{"*string:~*req.Account:1001"}, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*string:~*req.Account:1001"}, Blocker: false, }, }, }, { MetricID: "PDD", FilterIDs: []string{"*string:~*req.Account:1001"}, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*string:~*req.Account:1001"}, Blocker: false, }, }, }, }, }, APIOpts: map[string]any{ "key": "value", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_<~*req.Account>;*string:~*req.Account:<~*req.Account>;*string:~*req.Account:<~*req.Account>&20;*string:~*req.Account:<~*req.Account>&true;100;1h;1;true;THD_1&THD_2;ASR&PDD;*string:~*req.Account:<~*req.Account>;*string:~*req.Account:<~*req.Account>&false;key:value", }, }, }, wantErr: false, }, { name: "SuccessfulRequestEmptyFields", expSqpwo: &engine.StatQueueProfileWithAPIOpts{ StatQueueProfile: &engine.StatQueueProfile{ Tenant: "cgrates.org", ID: "STATS_1001", FilterIDs: nil, QueueLength: 0, TTL: 0, MinItems: 0, Stored: false, ThresholdIDs: nil, Weights: nil, Blockers: nil, Metrics: nil, }, APIOpts: map[string]any{}, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1001;;;;;;;;;;;;", }, }, }, wantErr: false, }, { name: "EmptyDiktats", diktats: []*utils.APDiktat{}, wantErr: true, }, { name: "InvalidNumberOfParams", diktats: []*utils.APDiktat{ { ID: "d4", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1004;param1;param2", }, }, }, wantErr: true, }, { name: "InvalidQueueLengthParam", diktats: []*utils.APDiktat{ { ID: "d5", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1005;*string:~*req.Account:1005;;;invalid;1h;1;true;;;;;;;", }, }, }, wantErr: true, }, { name: "InvalidTTLParam", diktats: []*utils.APDiktat{ { ID: "d6", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1006;*string:~*req.Account:1006;;;100;invalid;1;true;;;;;;;", }, }, }, wantErr: true, }, { name: "InvalidMinItemsParam", diktats: []*utils.APDiktat{ { ID: "d7", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1007;*string:~*req.Account:1007;;;100;1h;invalid;true;;;;;;;", }, }, }, wantErr: true, }, { name: "InvalidStoredParam", diktats: []*utils.APDiktat{ { ID: "d8", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1008;*string:~*req.Account:1008;;;100;1h;1;invalid;;;;;;;", }, }, }, wantErr: true, }, { name: "WeightParamUnsupportedFormat", diktats: []*utils.APDiktat{ { ID: "d9", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1009;;w1&w2&w3;;;;;;;;;;;;;", }, }, }, wantErr: true, }, { name: "WeightParamInvalidFloat", diktats: []*utils.APDiktat{ { ID: "d10", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1010;;w1&invalid;;;;;;;;;;;;;", }, }, }, wantErr: true, }, { name: "BlockerParamUnsupportedFormat", diktats: []*utils.APDiktat{ { ID: "d11", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1011;;;b1&b2&b3;;;;;;;;;;;;", }, }, }, wantErr: true, }, { name: "BlockerParamInvalidBool", diktats: []*utils.APDiktat{ { ID: "d12", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1012;;;b1&invalid;;;;;;;;;;;;", }, }, }, wantErr: true, }, { name: "MetricBlockerParamUnsupportedFormat", diktats: []*utils.APDiktat{ { ID: "d13", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1013;;;;;;;;;;ASR;;mb1&mb2&mb3;", }, }, }, wantErr: true, }, { name: "MetricBlockerParamInvalidBool", diktats: []*utils.APDiktat{ { ID: "d14", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1014;;;;;;;;;;ASR;;mb1&invalid;", }, }, }, wantErr: true, }, { name: "APIOptsInvalidFormat", diktats: []*utils.APDiktat{ { ID: "d15", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1015;;;;;;;;;;;;;keyvalue", }, }, }, wantErr: true, }, { name: "NoAdminSConns", diktats: []*utils.APDiktat{ { ID: "d16", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;STATS_1016;;;;;;;;;;;;;", }, }, }, wantErr: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone if tc.name != "NoAdminSConns" { cfg.ActionSCfg().AdminSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS)} } connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) rpcInternal := make(chan birpc.ClientConnector, 1) rpcInternal <- ccM connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS), utils.AdminSv1, rpcInternal) act := &actDynamicStats{ config: cfg, connMgr: connMgr, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{ utils.AccountField: "1001", }, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC_STATS", Diktats: tc.diktats, }, } t.Cleanup(func() { sqpwo = nil }) ctx := context.Background() data := utils.MapStorage{ "*req": map[string]any{ utils.AccountField: "1001", }, } trgID := "trigger1" err := act.execute(ctx, data, trgID) if tc.wantErr { if err == nil { t.Errorf("expected an error, but got nil") } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(sqpwo, tc.expSqpwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expSqpwo), utils.ToJSON(sqpwo)) } }) } } func TestActDynamicAttributeId(t *testing.T) { testcases := []struct { name string aCfg *utils.APAction expect string }{ { name: "WithValidID", aCfg: &utils.APAction{ ID: "AttrA1", FilterIDs: []string{"f1", "f2"}, TTL: time.Second, Type: "attribute", Opts: map[string]any{ "key1": "val1", }, }, expect: "AttrA1", }, { name: "WithEmptyID", aCfg: &utils.APAction{ ID: "", FilterIDs: nil, TTL: 0, Type: "attribute", Opts: nil, }, expect: "", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { al := &actDynamicAttribute{ aCfg: tc.aCfg, } got := al.id() if got != tc.expect { t.Errorf("expected %q, got %q", tc.expect, got) } }) } } func TestActDynamicAttributeCfg(t *testing.T) { aCfg := &utils.APAction{ID: "AttrA1"} al := &actDynamicAttribute{aCfg: aCfg} if got := al.cfg(); got != aCfg { t.Errorf("expected cfg() to return %+v, got %+v", aCfg, got) } } func TestActDynamicAttributeExecute(t *testing.T) { engine.Cache.Clear(nil) defer engine.Cache.Clear(nil) var apwo *utils.APIAttributeProfileWithAPIOpts ccM := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.AdminSv1SetAttributeProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if apwo, canCast = args.(*utils.APIAttributeProfileWithAPIOpts); !canCast { return fmt.Errorf("couldn't cast") } *reply.(*string) = utils.OK return nil }, }, } testcases := []struct { name string diktats []*utils.APDiktat expApwo *utils.APIAttributeProfileWithAPIOpts wantErr bool }{ { name: "SuccessfulRequest", expApwo: &utils.APIAttributeProfileWithAPIOpts{ APIAttributeProfile: &utils.APIAttributeProfile{ Tenant: "cgrates.org", ID: "ATTR_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*string:~*req.Account:1001"}, Weight: 20.0, }, }, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*string:~*req.Account:1001"}, Blocker: true, }, }, Attributes: []*utils.ExternalAttribute{ { FilterIDs: []string{"*string:~*req.Account:1001"}, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*string:~*req.Account:1001"}, Blocker: false, }, }, Path: "*req.Category", Type: "*constant", Value: "call", }, }, }, APIOpts: map[string]any{ "key": "value", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ATTR_1001;*string:~*req.Account:1001;*string:~*req.Account:1001&20;*string:~*req.Account:1001&true;*string:~*req.Account:1001;*string:~*req.Account:1001&false;*req.Category;*constant;call;key:value", }, }, }, wantErr: false, }, { name: "SuccessfulRequestWithDynamicPaths", expApwo: &utils.APIAttributeProfileWithAPIOpts{ APIAttributeProfile: &utils.APIAttributeProfile{ Tenant: "cgrates.org", ID: "ATTR_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*string:~*req.Account:1001"}, Weight: 20.0, }, }, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*string:~*req.Account:1001"}, Blocker: true, }, }, Attributes: []*utils.ExternalAttribute{ { FilterIDs: []string{"*string:~*req.Account:1001"}, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*string:~*req.Account:1001"}, Blocker: false, }, }, Path: "*req.Category", Type: "*constant", Value: "call", }, }, }, APIOpts: map[string]any{ "key": "value", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ATTR_<~*req.Account>;*string:~*req.Account:<~*req.Account>;*string:~*req.Account:<~*req.Account>&20;*string:~*req.Account:<~*req.Account>&true;*string:~*req.Account:<~*req.Account>;*string:~*req.Account:<~*req.Account>&false;*req.Category;*constant;call;key:value", }, }, }, wantErr: false, }, { name: "SuccessfulRequestEmptyFields", expApwo: &utils.APIAttributeProfileWithAPIOpts{ APIAttributeProfile: &utils.APIAttributeProfile{ Tenant: "cgrates.org", ID: "ATTR_1001", FilterIDs: nil, Weights: nil, Blockers: nil, Attributes: nil, }, APIOpts: map[string]any{}, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ATTR_1001;;;;;;;;;", }, }, }, wantErr: false, }, { name: "WeightParamUnsupportedFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ATTR_1001;;w1&w2&w3;;;;;;;;", }, }, }, wantErr: true, }, { name: "WeightParamInvalidFloat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ATTR_1001;;w1&invalid;;;;;;;;", }, }, }, wantErr: true, }, { name: "BlockerParamUnsupportedFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ATTR_1001;;;b1&b2&b3;;;;;;;", }, }, }, wantErr: true, }, { name: "BlockerParamInvalidBool", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ATTR_1001;;;b1&invalid;;;;;;;", }, }, }, wantErr: true, }, { name: "APIOptsInvalidFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ATTR_1001;;;;;;;;;keyvalue", }, }, }, wantErr: true, }, { name: "NoAdminSConns", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ATTR_1001;;;;;;;;;", }, }, }, wantErr: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone if tc.name != "NoAdminSConns" { cfg.ActionSCfg().AdminSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS)} } connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) rpcInternal := make(chan birpc.ClientConnector, 1) rpcInternal <- ccM connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS), utils.AdminSv1, rpcInternal) act := &actDynamicAttribute{ config: cfg, connMgr: connMgr, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{ utils.AccountField: "1001", }, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC_ATTR", Diktats: tc.diktats, }, } t.Cleanup(func() { apwo = nil }) ctx := context.Background() data := utils.MapStorage{ "*req": map[string]any{ utils.AccountField: "1001", }, } trgID := "trigger1" err := act.execute(ctx, data, trgID) if tc.wantErr { if err == nil { t.Errorf("expected an error, but got nil") } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(apwo, tc.expApwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expApwo), utils.ToJSON(apwo)) } }) } } func TestActDynamicResourceId(t *testing.T) { testcases := []struct { name string aCfg *utils.APAction expect string }{ { name: "WithValidID", aCfg: &utils.APAction{ ID: "ResA1", FilterIDs: []string{"f1", "f2"}, TTL: time.Second, Type: "resource", Opts: map[string]any{ "Opts": "Opts1", }, }, expect: "ResA1", }, { name: "WithEmptyID", aCfg: &utils.APAction{ ID: "", FilterIDs: nil, TTL: 0, Type: "resource", Opts: nil, }, expect: "", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { al := &actDynamicResource{ aCfg: tc.aCfg, } got := al.id() if got != tc.expect { t.Errorf("expected %q, got %q", tc.expect, got) } }) } } func TestActDynamicResourceCfg(t *testing.T) { testcases := []struct { name string aCfg *utils.APAction expect *utils.APAction }{ { name: "WithValidCfg", aCfg: &utils.APAction{ ID: "ResCfg1", FilterIDs: []string{"f1"}, TTL: time.Second, Type: "resource", Opts: map[string]any{ "Opts1": "val1", }, }, expect: &utils.APAction{ ID: "ResCfg1", FilterIDs: []string{"f1"}, TTL: time.Second, Type: "resource", Opts: map[string]any{ "Opts1": "val1", }, }, }, { name: "WithNilCfg", aCfg: nil, expect: nil, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { al := &actDynamicResource{ aCfg: tc.aCfg, } got := al.cfg() if !reflect.DeepEqual(got, tc.expect) { t.Errorf("expected %+v, got %+v", tc.expect, got) } }) } } func TestActDynamicResourceExecute(t *testing.T) { engine.Cache.Clear(nil) defer engine.Cache.Clear(nil) var rpwo *utils.ResourceProfileWithAPIOpts ccM := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.AdminSv1SetResourceProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if rpwo, canCast = args.(*utils.ResourceProfileWithAPIOpts); !canCast { return fmt.Errorf("couldn't cast") } *reply.(*string) = utils.OK return nil }, }, } testcases := []struct { name string diktats []*utils.APDiktat expRpwo *utils.ResourceProfileWithAPIOpts wantErr bool }{ { name: "SuccessfulRequest", expRpwo: &utils.ResourceProfileWithAPIOpts{ ResourceProfile: &utils.ResourceProfile{ Tenant: "cgrates.org", ID: "RES_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, UsageTTL: time.Duration(60 * time.Second), Limit: 100.0, AllocationMessage: "Resource allocated", Blocker: true, Stored: true, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*string:~*req.Account:1001"}, Weight: 20.0, }, }, ThresholdIDs: []string{"THD_1001"}, }, APIOpts: map[string]any{ "key": "value", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RES_1001;*string:~*req.Account:1001;*string:~*req.Account:1001&20;60s;100;Resource allocated;true;true;THD_1001;key:value", }, }, }, wantErr: false, }, { name: "SuccessfulRequestWithDynamicPaths", expRpwo: &utils.ResourceProfileWithAPIOpts{ ResourceProfile: &utils.ResourceProfile{ Tenant: "cgrates.org", ID: "RES_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, UsageTTL: time.Duration(60 * time.Second), Limit: 100.0, AllocationMessage: "Resource allocated", Blocker: true, Stored: true, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*string:~*req.Account:1001"}, Weight: 20.0, }, }, ThresholdIDs: []string{"THD_1001"}, }, APIOpts: map[string]any{ "key": "value", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RES_<~*req.Account>;*string:~*req.Account:<~*req.Account>;*string:~*req.Account:<~*req.Account>&20;60s;100;Resource allocated;true;true;THD_<~*req.Account>;key:value", }, }, }, wantErr: false, }, { name: "SuccessfulRequestEmptyFields", expRpwo: &utils.ResourceProfileWithAPIOpts{ ResourceProfile: &utils.ResourceProfile{ Tenant: "cgrates.org", ID: "RES_1001", FilterIDs: nil, UsageTTL: 0, Limit: 0.0, AllocationMessage: "", Blocker: false, Stored: false, Weights: nil, ThresholdIDs: nil, }, APIOpts: map[string]any{}, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RES_1001;;;;;;;;;", }, }, }, wantErr: false, }, { name: "WeightParamUnsupportedFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RES_1001;;w1&w2&w3;;;;;;;", }, }, }, wantErr: true, }, { name: "WeightParamInvalidFloat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RES_1001;;w1&invalid;;;;;;;", }, }, }, wantErr: true, }, { name: "UsageTTLInvalidDuration", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RES_1001;;;invalid;;;;;;", }, }, }, wantErr: true, }, { name: "LimitInvalidFloat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RES_1001;;;;invalid;;;;;", }, }, }, wantErr: true, }, { name: "BlockerInvalidBool", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RES_1001;;;;;;invalid;;;", }, }, }, wantErr: true, }, { name: "StoredInvalidBool", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RES_1001;;;;;;;invalid;;", }, }, }, wantErr: true, }, { name: "APIOptsInvalidFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RES_1001;;;;;;;;;keyvalue", }, }, }, wantErr: true, }, { name: "InvalidNumberOfParameters", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RES_1001;not_enough_params", }, }, }, wantErr: true, }, { name: "NoDiktatsSpecified", diktats: []*utils.APDiktat{}, wantErr: true, }, { name: "NoAdminSConns", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RES_1001;;;;;;;;;", }, }, }, wantErr: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone if tc.name != "NoAdminSConns" { cfg.ActionSCfg().AdminSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS)} } connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) rpcInternal := make(chan birpc.ClientConnector, 1) rpcInternal <- ccM connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS), utils.AdminSv1, rpcInternal) act := &actDynamicResource{ config: cfg, connMgr: connMgr, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{ utils.AccountField: "1001", }, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC_RESOURCE", Diktats: tc.diktats, }, } t.Cleanup(func() { rpwo = nil }) ctx := context.Background() data := utils.MapStorage{ "*req": map[string]any{ utils.AccountField: "1001", }, } trgID := "trigger1" err := act.execute(ctx, data, trgID) if tc.wantErr { if err == nil { t.Errorf("expected an error, but got nil") } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(rpwo, tc.expRpwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpwo), utils.ToJSON(rpwo)) } }) } } func TestActDynamicTrendId(t *testing.T) { tests := []struct { name string actionID string expected string }{ { name: "With ID", actionID: "DynamicTrendID", expected: "DynamicTrendID", }, { name: "Empty ID", actionID: "", expected: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actDT := &actDynamicTrend{ aCfg: &utils.APAction{ ID: tt.actionID, }, } result := actDT.id() if result != tt.expected { t.Errorf("Expected %s, got %s", tt.expected, result) } }) } } func TestActDynamicTrendCfg(t *testing.T) { apAction := &utils.APAction{ID: "TrendProfile1"} a := &actDynamicTrend{ aCfg: apAction, } if got := a.cfg(); !reflect.DeepEqual(got, apAction) { t.Errorf("cfg() = %v, want %v", got, apAction) } } func TestActDynamicTrendExecute(t *testing.T) { engine.Cache.Clear(nil) defer engine.Cache.Clear(nil) var tpwo *utils.TrendProfileWithAPIOpts ccM := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.AdminSv1SetTrendProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if tpwo, canCast = args.(*utils.TrendProfileWithAPIOpts); !canCast { return fmt.Errorf("couldn't cast") } *reply.(*string) = utils.OK return nil }, }, } testcases := []struct { name string diktats []*utils.APDiktat expTpwo *utils.TrendProfileWithAPIOpts wantErr bool }{ { name: "SuccessfulRequest", expTpwo: &utils.TrendProfileWithAPIOpts{ TrendProfile: &utils.TrendProfile{ Tenant: "cgrates.org", ID: "TREND_1001", Schedule: "* * * * *", StatID: "STAT_1001", Metrics: []string{"*acd", "*tcd"}, TTL: 60 * time.Second, QueueLength: 100, MinItems: 10, CorrelationType: "*last", Tolerance: 0.05, Stored: true, ThresholdIDs: []string{"THD_1001"}, }, APIOpts: map[string]any{ "key": "value", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;TREND_1001;* * * * *;STAT_1001;*acd&*tcd;60s;100;10;*last;0.05;true;THD_1001;key:value", }, }, }, wantErr: false, }, { name: "SuccessfulRequestWithDynamicPaths", expTpwo: &utils.TrendProfileWithAPIOpts{ TrendProfile: &utils.TrendProfile{ Tenant: "cgrates.org", ID: "TREND_1001", Schedule: "* * * * *", StatID: "STAT_1001", Metrics: []string{"*acd", "*tcd"}, TTL: 60 * time.Second, QueueLength: 100, MinItems: 10, CorrelationType: "*last", Tolerance: 0.05, Stored: true, ThresholdIDs: []string{"THD_1001"}, }, APIOpts: map[string]any{ "key": "value", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;TREND_<~*req.Account>;* * * * *;STAT_<~*req.Account>;*acd&*tcd;60s;100;10;*last;0.05;true;THD_<~*req.Account>;key:value", }, }, }, wantErr: false, }, { name: "SuccessfulRequestEmptyFields", expTpwo: &utils.TrendProfileWithAPIOpts{ TrendProfile: &utils.TrendProfile{ Tenant: "cgrates.org", ID: "TREND_1001", Schedule: "", StatID: "", Metrics: nil, TTL: 0, QueueLength: 0, MinItems: 0, CorrelationType: "", Tolerance: 0.0, Stored: false, ThresholdIDs: nil, }, APIOpts: map[string]any{}, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;TREND_1001;;;;;;;;;;;", }, }, }, wantErr: false, }, { name: "TTLInvalidDuration", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;TREND_1001;;;invalid;;;;;;", }, }, }, wantErr: true, }, { name: "QueueLengthInvalidInt", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;TREND_1001;;;;;invalid;;;;;", }, }, }, wantErr: true, }, { name: "MinItemsInvalidInt", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;TREND_1001;;;;;;invalid;;;;", }, }, }, wantErr: true, }, { name: "ToleranceInvalidFloat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;TREND_1001;;;;;;;;invalid;;", }, }, }, wantErr: true, }, { name: "StoredInvalidBool", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;TREND_1001;;;;;;;;;invalid;", }, }, }, wantErr: true, }, { name: "InvalidNumberOfParameters", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;TREND_1001;not_enough_params", }, }, }, wantErr: true, }, { name: "NoDiktatsSpecified", diktats: []*utils.APDiktat{}, wantErr: true, }, { name: "NoAdminSConns", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;TREND_1001;;;;;;;;;;;", }, }, }, wantErr: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone if tc.name != "NoAdminSConns" { cfg.ActionSCfg().AdminSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS)} } connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) rpcInternal := make(chan birpc.ClientConnector, 1) rpcInternal <- ccM connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS), utils.AdminSv1, rpcInternal) act := &actDynamicTrend{ config: cfg, connMgr: connMgr, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{ utils.AccountField: "1001", }, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC_TREND", Diktats: tc.diktats, }, } t.Cleanup(func() { tpwo = nil }) ctx := context.Background() data := utils.MapStorage{ "*req": map[string]any{ utils.AccountField: "1001", }, } trgID := "trigger1" err := act.execute(ctx, data, trgID) if tc.wantErr { if err == nil { t.Errorf("expected an error, but got nil") } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(tpwo, tc.expTpwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expTpwo), utils.ToJSON(tpwo)) } }) } } func TestActDynamicRankingId(t *testing.T) { tests := []struct { name string dr actDynamicRanking want string }{ { name: "WithValidID", dr: actDynamicRanking{aCfg: &utils.APAction{ID: "RankingProfile1"}}, want: "RankingProfile1", }, { name: "WithEmptyID", dr: actDynamicRanking{aCfg: &utils.APAction{ID: ""}}, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.dr.id() if got != tt.want { t.Errorf("id() = %v, want %v", got, tt.want) } }) } } func TestActDynamicRankingCfg(t *testing.T) { tests := []struct { name string al actDynamicRanking want *utils.APAction }{ { name: "WithValidCfg", al: actDynamicRanking{aCfg: &utils.APAction{ID: "RankingProfile1"}}, want: &utils.APAction{ID: "RankingProfile1"}, }, { name: "WithNilCfg", al: actDynamicRanking{aCfg: nil}, want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.al.cfg() if !reflect.DeepEqual(got, tt.want) { t.Errorf("cfg() = %v, want %v", got, tt.want) } }) } } func TestActDynamicRankingExecute(t *testing.T) { engine.Cache.Clear(nil) defer engine.Cache.Clear(nil) var rpwo *utils.RankingProfileWithAPIOpts ccM := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.AdminSv1SetRankingProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if rpwo, canCast = args.(*utils.RankingProfileWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } *(reply.(*string)) = utils.OK return nil }, }, } testcases := []struct { name string diktats []*utils.APDiktat expRpwo *utils.RankingProfileWithAPIOpts wantErr bool }{ { name: "SuccessfulRequest", expRpwo: &utils.RankingProfileWithAPIOpts{ RankingProfile: &utils.RankingProfile{ Tenant: "cgrates.org", ID: "RANK_ACNT_1001", Schedule: "* * * * *", Sorting: "*desc", SortingParameters: []string{"*tcc", "*tcd"}, Stored: true, StatIDs: []string{"STAT_1", "STAT_2"}, MetricIDs: []string{"*tcc", "*tcd"}, ThresholdIDs: []string{"THD_1", "THD_2"}, }, APIOpts: map[string]any{ "key1": "value1", "key2": "value2", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RANK_ACNT_1001;* * * * *;STAT_1&STAT_2;*tcc&*tcd;*desc;*tcc&*tcd;true;THD_1&THD_2;key1:value1&key2:value2", }, }, }, wantErr: false, }, { name: "SuccessfulRequestWithDynamicPaths", expRpwo: &utils.RankingProfileWithAPIOpts{ RankingProfile: &utils.RankingProfile{ Tenant: "cgrates.org", ID: "RANK_ACNT_1001", Schedule: "0 0 * * *", Sorting: "*asc", SortingParameters: []string{"*acc"}, Stored: false, StatIDs: []string{"STAT_1001"}, MetricIDs: []string{"*acc"}, ThresholdIDs: []string{"THD_1001"}, }, APIOpts: map[string]any{ "account": "1001", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 15.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RANK_ACNT_<~*req.Account>;0 0 * * *;STAT_<~*req.Account>;*acc;*asc;*acc;false;THD_<~*req.Account>;account:<~*req.Account>", }, }, }, wantErr: false, }, { name: "SuccessfulRequestEmptyFields", expRpwo: &utils.RankingProfileWithAPIOpts{ RankingProfile: &utils.RankingProfile{ Tenant: "cgrates.org", ID: "RANK_EMPTY", Schedule: "", Sorting: "", SortingParameters: nil, Stored: false, StatIDs: nil, MetricIDs: nil, ThresholdIDs: nil, }, APIOpts: map[string]any{}, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 10.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RANK_EMPTY;;;;;;;;", }, }, }, wantErr: false, }, { name: "EmptyDiktats", diktats: []*utils.APDiktat{}, wantErr: true, }, { name: "InvalidNumberOfParams", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RANK_INVALID;only;four;params", }, }, }, wantErr: true, }, { name: "InvalidStoredParam", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RANK_INVALID;* * * * *;STAT_1;*tcc;*desc;*tcc;invalid_bool;THD_1;key:value", }, }, }, wantErr: true, }, { name: "APIOptsInvalidFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RANK_INVALID;* * * * *;STAT_1;*tcc;*desc;*tcc;true;THD_1;invalidformat", }, }, }, wantErr: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone cfg.ActionSCfg().AdminSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS)} connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) rpcInternal := make(chan birpc.ClientConnector, 1) rpcInternal <- ccM connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS), utils.AdminSv1, rpcInternal) act := &actDynamicRanking{ config: cfg, connMgr: connMgr, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{ utils.AccountField: "1001", }, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC_RANKING", Diktats: tc.diktats, }, } t.Cleanup(func() { rpwo = nil }) ctx := context.Background() data := utils.MapStorage{ "*req": map[string]any{ utils.AccountField: "1001", }, } trgID := "trigger1" err := act.execute(ctx, data, trgID) if tc.wantErr { if err == nil { t.Errorf("expected an error, but got nil") } } else if err != nil { t.Error(err) } else if tc.expRpwo != nil && !reflect.DeepEqual(rpwo, tc.expRpwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpwo), utils.ToJSON(rpwo)) } }) } } func TestActDynamicFilterId(t *testing.T) { tests := []struct { name string df actDynamicFilter want string }{ { name: "WithValidID", df: actDynamicFilter{aCfg: &utils.APAction{ID: "FilterProfile1"}}, want: "FilterProfile1", }, { name: "WithEmptyID", df: actDynamicFilter{aCfg: &utils.APAction{ID: ""}}, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.df.id() if got != tt.want { t.Errorf("id() = %v, want %v", got, tt.want) } }) } } func TestActDynamicFilterCfg(t *testing.T) { tests := []struct { name string df actDynamicFilter want *utils.APAction }{ { name: "WithValidCfg", df: actDynamicFilter{aCfg: &utils.APAction{ID: "FilterProfile1"}}, want: &utils.APAction{ID: "FilterProfile1"}, }, { name: "WithNilCfg", df: actDynamicFilter{aCfg: nil}, want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.df.cfg() if !reflect.DeepEqual(got, tt.want) { t.Errorf("cfg() = %v, want %v", got, tt.want) } }) } } func TestActDynamicFilterExecute(t *testing.T) { engine.Cache.Clear(nil) defer engine.Cache.Clear(nil) var fwo *engine.FilterWithAPIOpts ccM := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.AdminSv1SetFilter: func(ctx *context.Context, args, reply any) error { var canCast bool if fwo, canCast = args.(*engine.FilterWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } *(reply.(*string)) = utils.OK return nil }, }, } testcases := []struct { name string diktats []*utils.APDiktat expFwo *engine.FilterWithAPIOpts wantErr bool }{ { name: "SuccessfulRequest", expFwo: &engine.FilterWithAPIOpts{ Filter: &engine.Filter{ Tenant: "cgrates.org", ID: "FLTR_ACNT_1001", Rules: []*engine.FilterRule{{ Type: "*string", Element: "~*req.Account", Values: []string{"1001", "1002"}, }}, }, APIOpts: map[string]any{ utils.MetaSubsys: "*sessions", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;FLTR_ACNT_1001;*string;~*req.Account;1001&1002;*subsys:*sessions", }, }, }, wantErr: false, }, { name: "SuccessfulRequestWithDynamicPaths", expFwo: &engine.FilterWithAPIOpts{ Filter: &engine.Filter{ Tenant: "cgrates.org", ID: "FLTR_ACNT_1001", Rules: []*engine.FilterRule{{ Type: "*string", Element: "~*req.Account", Values: []string{"1001"}, }}, }, APIOpts: map[string]any{ "account": "1001", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 15.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;FLTR_ACNT_<~*req.Account>;*string;~*req.Account;<~*req.Account>;account:<~*req.Account>", }, }, }, wantErr: false, }, { name: "SuccessfulRequestEmptyFields", expFwo: &engine.FilterWithAPIOpts{ Filter: &engine.Filter{ Tenant: "cgrates.org", ID: "FLTR_EMPTY", Rules: []*engine.FilterRule{{ Type: "*string", Element: "~*req.Field", Values: nil, }}, }, APIOpts: map[string]any{}, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 10.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;FLTR_EMPTY;*string;~*req.Field;;", }, }, }, wantErr: false, }, { name: "EmptyDiktats", diktats: []*utils.APDiktat{}, wantErr: true, }, { name: "InvalidNumberOfParams", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;FLTR_INVALID;only;four;params", }, }, }, wantErr: true, }, { name: "APIOptsInvalidFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;FLTR_INVALID;*string;~*req.Field;value;invalidformat", }, }, }, wantErr: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone cfg.ActionSCfg().AdminSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS)} connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) rpcInternal := make(chan birpc.ClientConnector, 1) rpcInternal <- ccM connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS), utils.AdminSv1, rpcInternal) act := &actDynamicFilter{ config: cfg, connMgr: connMgr, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{ utils.AccountField: "1001", }, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC_FILTER", Diktats: tc.diktats, }, } t.Cleanup(func() { fwo = nil }) ctx := context.Background() data := utils.MapStorage{ "*req": map[string]any{ utils.AccountField: "1001", }, } trgID := "trigger1" err := act.execute(ctx, data, trgID) if tc.wantErr { if err == nil { t.Errorf("expected an error, but got nil") } } else if err != nil { t.Error(err) } else if tc.expFwo != nil && !reflect.DeepEqual(fwo, tc.expFwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expFwo), utils.ToJSON(fwo)) } }) } } func TestActDynamicRouteId(t *testing.T) { tests := []struct { name string aCfg *utils.APAction want string }{ { name: "WithValidID", aCfg: &utils.APAction{ID: "RouteID1"}, want: "RouteID1", }, { name: "WithEmptyID", aCfg: &utils.APAction{ID: ""}, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { aL := &actDynamicRoute{aCfg: tt.aCfg} if got := aL.id(); got != tt.want { t.Errorf("id() = %v, want %v", got, tt.want) } }) } } func TestActDynamicRouteCfg(t *testing.T) { tests := []struct { name string dr actDynamicRoute want *utils.APAction }{ { name: "WithValidCfg", dr: actDynamicRoute{aCfg: &utils.APAction{ID: "RouteProfile1"}}, want: &utils.APAction{ID: "RouteProfile1"}, }, { name: "WithNilCfg", dr: actDynamicRoute{aCfg: nil}, want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.dr.cfg() if !reflect.DeepEqual(got, tt.want) { t.Errorf("cfg() = %v, want %v", got, tt.want) } }) } } func TestActDynamicRouteExecute(t *testing.T) { engine.Cache.Clear(nil) defer engine.Cache.Clear(nil) var rpo *utils.RouteProfileWithAPIOpts ccM := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.AdminSv1SetRouteProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if rpo, canCast = args.(*utils.RouteProfileWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } *(reply.(*string)) = utils.OK return nil }, }, } testcases := []struct { name string diktats []*utils.APDiktat expRpo *utils.RouteProfileWithAPIOpts wantErr bool }{ { name: "SuccessfulRequest", expRpo: &utils.RouteProfileWithAPIOpts{ RouteProfile: &utils.RouteProfile{ Tenant: "cgrates.org", ID: "ROUTE_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*weight"}, Weight: 10.0, }, }, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*blocker"}, Blocker: false, }, }, Sorting: "*weight", SortingParameters: []string{"*weight", "*cost"}, Routes: []*utils.Route{ { ID: "RT_1001", FilterIDs: []string{"*string:~*req.Route:1001"}, AccountIDs: []string{"1001", "1002"}, RateProfileIDs: []string{"RP_1001"}, ResourceIDs: []string{"RES_1001"}, StatIDs: []string{"STAT_1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*route_weight"}, Weight: 20.0, }, }, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*route_blocker"}, Blocker: false, }, }, RouteParameters: "param1=value1", }, }, }, APIOpts: map[string]any{ utils.MetaSubsys: "*sessions", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ROUTE_1001;*string:~*req.Account:1001;*weight&10.0;*blocker&false;*weight;*weight&*cost;RT_1001;*string:~*req.Route:1001;1001&1002;RP_1001;RES_1001;STAT_1001;*route_weight&20.0;*route_blocker&false;param1=value1;*subsys:*sessions", }, }, }, wantErr: false, }, { name: "SuccessfulRequestWithDynamicPaths", expRpo: &utils.RouteProfileWithAPIOpts{ RouteProfile: &utils.RouteProfile{ Tenant: "cgrates.org", ID: "ROUTE_1001", Sorting: "*weight", Routes: []*utils.Route{ { ID: "RT_1001", AccountIDs: []string{"1001"}, RouteParameters: "1001", }, }, }, APIOpts: map[string]any{ "account": "1001", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 15.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ROUTE_<~*req.Account>;;;;*weight;;RT_<~*req.Account>;;1001;;;;;;<~*req.Account>;account:<~*req.Account>", }, }, }, wantErr: false, }, { name: "SuccessfulRequestEmptyFields", expRpo: &utils.RouteProfileWithAPIOpts{ RouteProfile: &utils.RouteProfile{ Tenant: "cgrates.org", ID: "ROUTE_EMPTY", Sorting: "", }, APIOpts: map[string]any{}, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 10.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ROUTE_EMPTY;;;;;;;;;;;;;;;", }, }, }, wantErr: false, }, { name: "EmptyDiktats", diktats: []*utils.APDiktat{}, wantErr: true, }, { name: "InvalidNumberOfParams", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ROUTE_INVALID;only;four;params", }, }, }, wantErr: true, }, { name: "APIOptsInvalidFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ROUTE_INVALID;;;;;;;;;;;;;;;;invalidformat", }, }, }, wantErr: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone cfg.ActionSCfg().AdminSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS)} connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) rpcInternal := make(chan birpc.ClientConnector, 1) rpcInternal <- ccM connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS), utils.AdminSv1, rpcInternal) act := &actDynamicRoute{ config: cfg, connMgr: connMgr, dm: dm, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{ utils.AccountField: "1001", }, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC_ROUTE", Diktats: tc.diktats, }, } t.Cleanup(func() { rpo = nil }) ctx := context.Background() data := utils.MapStorage{ "*req": map[string]any{ utils.AccountField: "1001", }, } trgID := "trigger1" err := act.execute(ctx, data, trgID) if tc.wantErr { if err == nil { t.Errorf("expected an error, but got nil") } } else if err != nil { t.Error(err) } else if tc.expRpo != nil && !reflect.DeepEqual(rpo, tc.expRpo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpo), utils.ToJSON(rpo)) } }) } } func TestActDynamicRateId(t *testing.T) { tests := []struct { name string aL actDynamicRate want string }{ { name: "WithValidID", aL: actDynamicRate{aCfg: &utils.APAction{ID: "RateProfile1"}}, want: "RateProfile1", }, { name: "WithEmptyID", aL: actDynamicRate{aCfg: &utils.APAction{ID: ""}}, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.aL.id(); got != tt.want { t.Errorf("actDynamicRate.id() = %v, want %v", got, tt.want) } }) } } func TestActDynamicRateCfg(t *testing.T) { tests := []struct { name string aL actDynamicRate want *utils.APAction }{ { name: "WithValidCfg", aL: actDynamicRate{aCfg: &utils.APAction{ID: "RateProfile1"}}, want: &utils.APAction{ID: "RateProfile1"}, }, { name: "WithEmptyCfg", aL: actDynamicRate{aCfg: &utils.APAction{}}, want: &utils.APAction{}, }, { name: "WithNilCfg", aL: actDynamicRate{aCfg: nil}, want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.aL.cfg(); !reflect.DeepEqual(got, tt.want) { t.Errorf("actDynamicRate.cfg() = %v, want %v", got, tt.want) } }) } } func TestActDynamicIPExecute(t *testing.T) { engine.Cache.Clear(nil) defer engine.Cache.Clear(nil) var ipo *utils.IPProfileWithAPIOpts ccM := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.AdminSv1SetIPProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if ipo, canCast = args.(*utils.IPProfileWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } *(reply.(*string)) = utils.OK return nil }, }, } testcases := []struct { name string diktats []*utils.APDiktat expIpo *utils.IPProfileWithAPIOpts wantErr bool }{ { name: "SuccessfulRequest", expIpo: &utils.IPProfileWithAPIOpts{ IPProfile: &utils.IPProfile{ Tenant: "cgrates.org", ID: "IP_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*weight"}, Weight: 10.0, }, }, TTL: time.Duration(60) * time.Second, Stored: true, Pools: []*utils.IPPool{ { ID: "POOL_1001", FilterIDs: []string{"*string:~*req.Pool:1001"}, Type: "ipv4", Range: "192.168.1.0/24", Strategy: "*ascending", Message: "IP allocated", Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*pool_weight"}, Weight: 20.0, }, }, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*pool_blocker"}, Blocker: false, }, }, }, }, }, APIOpts: map[string]any{ utils.MetaSubsys: "*sessions", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;IP_1001;*string:~*req.Account:1001;*weight&10.0;60s;true;POOL_1001;*string:~*req.Pool:1001;ipv4;192.168.1.0/24;*ascending;IP allocated;*pool_weight&20.0;*pool_blocker&false;*subsys:*sessions", }, }, }, wantErr: false, }, { name: "SuccessfulRequestWithDynamicPaths", expIpo: &utils.IPProfileWithAPIOpts{ IPProfile: &utils.IPProfile{ Tenant: "cgrates.org", ID: "IP_1001", Pools: []*utils.IPPool{ { ID: "POOL_1001", Type: "ipv4", Range: "192.168.1.0/24", Strategy: "*ascending", Message: "1001", }, }, }, APIOpts: map[string]any{ "account": "1001", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 15.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;IP_<~*req.Account>;;;;false;POOL_<~*req.Account>;;ipv4;192.168.1.0/24;*ascending;<~*req.Account>;;;account:<~*req.Account>", }, }, }, wantErr: false, }, { name: "SuccessfulRequestEmptyFields", expIpo: &utils.IPProfileWithAPIOpts{ IPProfile: &utils.IPProfile{ Tenant: "cgrates.org", ID: "IP_EMPTY", Pools: []*utils.IPPool{ { ID: "", Type: "", Range: "", Strategy: "", Message: "", }, }, }, APIOpts: map[string]any{}, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 10.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;IP_EMPTY;;;;;;;;;;;;;", }, }, }, wantErr: false, }, { name: "EmptyDiktats", diktats: []*utils.APDiktat{}, wantErr: true, }, { name: "InvalidNumberOfParams", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;IP_INVALID;only;four;params", }, }, }, wantErr: true, }, { name: "APIOptsInvalidFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;IP_INVALID;;;;;;;;;;;;;;invalidformat", }, }, }, wantErr: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone cfg.ActionSCfg().AdminSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS)} connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) rpcInternal := make(chan birpc.ClientConnector, 1) rpcInternal <- ccM connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS), utils.AdminSv1, rpcInternal) act := &actDynamicIP{ config: cfg, connMgr: connMgr, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{ utils.AccountField: "1001", }, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC_IP", Diktats: tc.diktats, }, } t.Cleanup(func() { ipo = nil }) ctx := context.Background() data := utils.MapStorage{ "*req": map[string]any{ utils.AccountField: "1001", }, } trgID := "trigger1" err := act.execute(ctx, data, trgID) if tc.wantErr { if err == nil { t.Errorf("expected an error, but got nil") } } else if err != nil { t.Error(err) } else if tc.expIpo != nil && !reflect.DeepEqual(ipo, tc.expIpo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expIpo), utils.ToJSON(ipo)) } }) } } func TestActDynamicIPId(t *testing.T) { tests := []struct { name string aCfg *utils.APAction want string }{ { name: "WithValidID", aCfg: &utils.APAction{ID: "ActDynamicIP"}, want: "ActDynamicIP", }, { name: "WithEmptyID", aCfg: &utils.APAction{ID: ""}, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { act := &actDynamicIP{ aCfg: tt.aCfg, } if got := act.id(); got != tt.want { t.Errorf("id() = %v, want %v", got, tt.want) } }) } } func TestActDynamicIPCfg(t *testing.T) { tests := []struct { name string aCfg *utils.APAction want *utils.APAction }{ { name: "WithValidCfg", aCfg: &utils.APAction{ID: "ActDynamicIP"}, want: &utils.APAction{ID: "ActDynamicIP"}, }, { name: "WithNilCfg", aCfg: nil, want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { act := &actDynamicIP{ aCfg: tt.aCfg, } if got := act.cfg(); !reflect.DeepEqual(got, tt.want) { t.Errorf("cfg() = %v, want %v", got, tt.want) } }) } } func TestActDynamicRateExecute(t *testing.T) { engine.Cache.Clear(nil) defer engine.Cache.Clear(nil) var rateProfile *utils.APIRateProfile ccM := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.AdminSv1SetRateProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if rateProfile, canCast = args.(*utils.APIRateProfile); !canCast { return fmt.Errorf("couldn't cast") } *(reply.(*string)) = utils.OK return nil }, }, } testcases := []struct { name string diktats []*utils.APDiktat expRateProfile *utils.APIRateProfile wantErr bool }{ { name: "SuccessfulRequest", expRateProfile: &utils.APIRateProfile{ RateProfile: &utils.RateProfile{ Tenant: "cgrates.org", ID: "RATE_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*weight"}, Weight: 10.0, }, }, MinCost: utils.NewDecimal(5, 2), MaxCost: utils.NewDecimal(100, 2), MaxCostStrategy: "*free", Rates: map[string]*utils.Rate{ "RT_1001": { ID: "RT_1001", FilterIDs: []string{"*string:~*req.Destination:+49"}, ActivationTimes: "* * * * *", Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"fltr1"}, Weight: 20.0, }, }, Blocker: false, IntervalRates: []*utils.IntervalRate{ { IntervalStart: utils.NewDecimal(0, 0), FixedFee: utils.NewDecimal(10, 2), RecurrentFee: utils.NewDecimal(1, 2), Unit: utils.NewDecimal(int64(time.Second), 0), Increment: utils.NewDecimal(int64(time.Second), 0), }, }, }, }, }, APIOpts: map[string]any{ utils.MetaSubsys: "*sessions", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_1001;*string:~*req.Account:1001;*weight&10.0;0.05;1.00;*free;RT_1001;*string:~*req.Destination:+49;* * * * *;fltr1&20.0;false;0;0.10;0.01;1s;1s;*subsys:*sessions", }, }, }, wantErr: false, }, { name: "SuccessfulRequestWithDynamicPaths", expRateProfile: &utils.APIRateProfile{ RateProfile: &utils.RateProfile{ Tenant: "cgrates.org", ID: "RATE_1001", MaxCostStrategy: "*free", Rates: map[string]*utils.Rate{ "RT_1001": { ID: "RT_1001", ActivationTimes: "* * * * *", IntervalRates: []*utils.IntervalRate{ { RecurrentFee: utils.NewDecimal(2, 2), Unit: utils.NewDecimal(int64(time.Minute), 0), Increment: utils.NewDecimal(int64(time.Minute), 0), }, }, }, }, }, APIOpts: map[string]any{ "account": "1001", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 15.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_<~*req.Account>;;;;;*free;RT_<~*req.Account>;;* * * * *;;;;;<~*req.RecurrentFee>;1m;1m;account:<~*req.Account>", }, }, }, wantErr: false, }, { name: "SuccessfulRequestEmptyFields", expRateProfile: &utils.APIRateProfile{ RateProfile: &utils.RateProfile{ Tenant: "cgrates.org", ID: "RATE_EMPTY", MaxCostStrategy: "", Rates: map[string]*utils.Rate{ "": { ID: "", ActivationTimes: "", IntervalRates: []*utils.IntervalRate{{}}, }, }, }, APIOpts: map[string]any{}, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 10.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_EMPTY;;;;;;;;;;;;;;;;", }, }, }, wantErr: false, }, { name: "EmptyDiktats", diktats: []*utils.APDiktat{}, wantErr: true, }, { name: "InvalidNumberOfParams17", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;only;seventeen;params;here;not;eighteen;params;total;missing;one;more;param;here;now;still", }, }, }, wantErr: true, }, { name: "InvalidNumberOfParams19", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;too;many;params;here;now;we;have;nineteen;params;total;which;is;one;too;many;for;this", }, }, }, wantErr: true, }, { name: "InvalidWeightFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;;*weight&invalid_weight;;;;;;;;;;;;;;", }, }, }, wantErr: true, }, { name: "InvalidMinCostFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;;;invalid_decimal;;;;;;;;;;;;;;", }, }, }, wantErr: true, }, { name: "InvalidMaxCostFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;;;;invalid_decimal;;;;;;;;;;;;;", }, }, }, wantErr: true, }, { name: "InvalidRateWeightFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;;;;;;RT_1;;;*weight&invalid_weight;;;;;", }, }, }, wantErr: true, }, { name: "InvalidBlockerFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;;;;;;RT_1;;;;invalid_bool;;;;", }, }, }, wantErr: true, }, { name: "InvalidIntervalStartFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;;;;;;RT_1;;;;false;invalid_decimal;;;", }, }, }, wantErr: true, }, { name: "InvalidFixedFeeFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;;;;;;RT_1;;;;false;;invalid_decimal;;", }, }, }, wantErr: true, }, { name: "InvalidRecurrentFeeFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;;;;;;RT_1;;;;false;;;invalid_decimal;", }, }, }, wantErr: true, }, { name: "InvalidUnitFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;;;;;;RT_1;;;;false;;;;invalid_decimal", }, }, }, wantErr: true, }, { name: "InvalidIncrementFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;;;;;;RT_1;;;;;;;;;invalid_decimal;", }, }, }, wantErr: true, }, { name: "APIOptsInvalidFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;RATE_INVALID;;;;;;;;;;;;;;;;;invalidformat", }, }, }, wantErr: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone cfg.ActionSCfg().AdminSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS)} connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) rpcInternal := make(chan birpc.ClientConnector, 1) rpcInternal <- ccM connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS), utils.AdminSv1, rpcInternal) act := &actDynamicRate{ config: cfg, connMgr: connMgr, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{ utils.AccountField: "1001", utils.Destination: "+49123456789", utils.RecurrentFee: "0.02", }, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC_RATE", Diktats: tc.diktats, }, } t.Cleanup(func() { rateProfile = nil }) ctx := context.Background() data := utils.MapStorage{ "*req": map[string]any{ utils.AccountField: "1001", utils.Destination: "+49123456789", utils.RecurrentFee: "0.02", }, } trgID := "trigger1" err := act.execute(ctx, data, trgID) if tc.wantErr { if err == nil { t.Errorf("expected an error, but got nil") } } else if err != nil { t.Error(err) } else if tc.expRateProfile != nil && !reflect.DeepEqual(rateProfile, tc.expRateProfile) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRateProfile), utils.ToJSON(rateProfile)) } }) } } func TestActDynamicActionExecute(t *testing.T) { engine.Cache.Clear(nil) defer engine.Cache.Clear(nil) var actionProfile *utils.ActionProfileWithAPIOpts ccM := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.AdminSv1SetActionProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if actionProfile, canCast = args.(*utils.ActionProfileWithAPIOpts); !canCast { return fmt.Errorf("couldn't cast") } *(reply.(*string)) = utils.OK return nil }, }, } testcases := []struct { name string diktats []*utils.APDiktat expActionProfile *utils.ActionProfileWithAPIOpts wantErr bool expectErrContains string }{ { name: "SuccessfulRequest", expActionProfile: &utils.ActionProfileWithAPIOpts{ ActionProfile: &utils.ActionProfile{ Tenant: "cgrates.org", ID: "ACT_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*weight"}, Weight: 10.0, }, }, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*blocker"}, Blocker: false, }, }, Schedule: "* * * * *", Targets: map[string]utils.StringSet{ "*voice": utils.NewStringSet([]string{"account1", "account2"}), }, Actions: []*utils.APAction{ { ID: "SUB_ACT_1001", FilterIDs: []string{"*string:~*req.Destination:+49"}, TTL: 30 * time.Second, Type: "*log", Opts: map[string]any{ "level": "info", "msg": "test message", }, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"act_weight"}, Weight: 20.0, }, }, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"act_blocker"}, Blocker: true, }, }, Diktats: []*utils.APDiktat{ { ID: "DIKTAT_1001", FilterIDs: []string{"*string:~*req.Type:test"}, Opts: map[string]any{ "path": "*req.Balance", "value": "100", }, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"diktat_weight"}, Weight: 30.0, }, }, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"diktat_blocker"}, Blocker: false, }, }, }, }, }, }, }, APIOpts: map[string]any{ utils.MetaSubsys: "*sessions", "timeout": "60s", }, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_1001;*string:~*req.Account:1001;*weight&10.0;*blocker&false;* * * * *;*voice;account1&account2;SUB_ACT_1001;*string:~*req.Destination:+49;30s;*log;level:info&msg:test message;act_weight&20.0;act_blocker&true;DIKTAT_1001;*string:~*req.Type:test;path:*req.Balance&value:100;diktat_weight&30.0;diktat_blocker&false;*subsys:*sessions&timeout:60s", }, }, }, wantErr: false, }, { name: "SuccessfulRequestEmptyFields", expActionProfile: &utils.ActionProfileWithAPIOpts{ ActionProfile: &utils.ActionProfile{ Tenant: "cgrates.org", ID: "ACT_EMPTY", Targets: map[string]utils.StringSet{}, Actions: []*utils.APAction{ { ID: "", Type: "", Opts: map[string]any{}, Diktats: []*utils.APDiktat{ { ID: "", Opts: map[string]any{}, }, }, }, }, }, APIOpts: map[string]any{}, }, diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 10.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_EMPTY;;;;;;;;;;;;;;;;;;;", }, }, }, wantErr: false, }, { name: "NoAdminSConns", diktats: []*utils.APDiktat{ { ID: "d1", Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_1001;;;;;;;;;;;;;;;;;;;;", }, }, }, wantErr: true, expectErrContains: "no connection with AdminS", }, { name: "EmptyDiktats", diktats: []*utils.APDiktat{}, wantErr: true, expectErrContains: "No diktats were specified for action", }, { name: "InvalidNumberOfParams20", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_INVALID;only;twenty;params;here;not;twenty;one;params;total;missing;one;more;param;here;now;still;incomplete;test", }, }, }, wantErr: true, expectErrContains: "invalid number of parameters <20> expected 21", }, { name: "InvalidNumberOfParams22", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_INVALID;too;many;params;here;now;we;have;twenty;two;params;total;which;is;one;too;many;for;this;function;call", }, }, }, wantErr: true, expectErrContains: "invalid number of parameters <22> expected 21", }, { name: "InvalidWeightFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_INVALID;;*weight&invalid_weight;;;;;;;;;;;;;;;;", }, }, }, wantErr: true, }, { name: "InvalidBlockerFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_INVALID;;;*blocker&invalid_bool;;;;;;;;;;;;;;;", }, }, }, wantErr: true, }, { name: "InvalidTTLFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_INVALID;;;;;;;SUB_ACT;;invalid_ttl;;;;;;;;;;", }, }, }, wantErr: true, }, { name: "InvalidActionWeightFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_INVALID;;;;;;;SUB_ACT;;;*log;;*weight&invalid_weight;;;;;;;", }, }, }, wantErr: true, expectErrContains: "strconv.ParseFloat", }, { name: "InvalidActionBlockerFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_INVALID;;;;;;;SUB_ACT;;;*log;;;*blocker&invalid_bool;;;;;;", }, }, }, wantErr: true, expectErrContains: "strconv.ParseBool", }, { name: "InvalidDiktatWeightFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_INVALID;;;;;;;SUB_ACT;;;*log;;;;DIKTAT_1;;opt:val;*weight&invalid_weight;;", }, }, }, wantErr: true, expectErrContains: "strconv.ParseFloat", }, { name: "InvalidDiktatBlockerFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_INVALID;;;;;;;SUB_ACT;;;*log;;;;DIKTAT_1;;opt:val;;*blocker&invalid_bool;", }, }, }, wantErr: true, expectErrContains: "strconv.ParseBool", }, { name: "InvalidOptsFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_INVALID;;;;;;;SUB_ACT;;;*log;invalid_opts_format;;;;;;;;;;", }, }, }, wantErr: true, }, { name: "InvalidDiktatOptsFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_INVALID;;;;;;;SUB_ACT;;;*log;;;;DIKTAT_1;;invalid_opts_format;;;;;", }, }, }, wantErr: true, }, { name: "InvalidAPIOptsFormat", diktats: []*utils.APDiktat{ { ID: "d1", Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;ACT_INVALID;;;;;;;;;;;;;;;;;;;invalid_format", }, }, }, wantErr: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone if !strings.Contains(tc.expectErrContains, "no connection") { cfg.ActionSCfg().AdminSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS)} } connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) if len(cfg.ActionSCfg().AdminSConns) > 0 { rpcInternal := make(chan birpc.ClientConnector, 1) rpcInternal <- ccM connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS), utils.AdminSv1, rpcInternal) } act := &actDynamicAction{ config: cfg, connMgr: connMgr, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{ utils.AccountField: "1001", utils.Destination: "+49123456789", }, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC_ACTION", Diktats: tc.diktats, }, } t.Cleanup(func() { actionProfile = nil }) ctx := context.Background() data := utils.MapStorage{ "*req": map[string]any{ utils.AccountField: "1001", utils.Destination: "+49123456789", }, } trgID := "trigger1" err := act.execute(ctx, data, trgID) if tc.wantErr { if err == nil { t.Errorf("expected an error, but got nil") } else if tc.expectErrContains != "" && !strings.Contains(err.Error(), tc.expectErrContains) { t.Errorf("expected error to contain '%s', but got: %v", tc.expectErrContains, err) } } else if err != nil { t.Error(err) } else if tc.expActionProfile != nil && !reflect.DeepEqual(actionProfile, tc.expActionProfile) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expActionProfile), utils.ToJSON(actionProfile)) } }) } } func TestActDynamicActionId(t *testing.T) { tests := []struct { name string aL actDynamicAction want string }{ { name: "WithValidID", aL: actDynamicAction{ aCfg: &utils.APAction{ID: "ActionProfile1"}, }, want: "ActionProfile1", }, { name: "WithEmptyID", aL: actDynamicAction{ aCfg: &utils.APAction{ID: ""}, }, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.aL.id(); got != tt.want { t.Errorf("actDynamicAction.id() = %v, want %v", got, tt.want) } }) } } func TestActDynamicActionCfg(t *testing.T) { tests := []struct { name string aL actDynamicAction want *utils.APAction }{ { name: "WithValidCfg", aL: actDynamicAction{ aCfg: &utils.APAction{ID: "ActionProfile1"}, }, want: &utils.APAction{ID: "ActionProfile1"}, }, { name: "WithEmptyCfg", aL: actDynamicAction{ aCfg: &utils.APAction{}, }, want: &utils.APAction{}, }, { name: "WithNilCfg", aL: actDynamicAction{aCfg: nil}, want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.aL.cfg() if !reflect.DeepEqual(got, tt.want) { t.Errorf("actDynamicAction.cfg() = %#v, want %#v", got, tt.want) } }) } } func TestActDynamicRouteExecuteModifyExistingRoute(t *testing.T) { engine.Cache.Clear(nil) defer engine.Cache.Clear(nil) var rpo *utils.RouteProfileWithAPIOpts ccM := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.AdminSv1SetRouteProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if rpo, canCast = args.(*utils.RouteProfileWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } *(reply.(*string)) = utils.OK return nil }, }, } cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().DefaultCaching = utils.MetaNone cfg.ActionSCfg().AdminSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS)} connMgr := engine.NewConnManager(cfg) dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DbCfg().Items) dbCM := engine.NewDBConnManager(map[string]engine.DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg()) dm := engine.NewDataManager(dbCM, cfg, connMgr) fltrS := engine.NewFilterS(cfg, connMgr, dm) rpcInternal := make(chan birpc.ClientConnector, 1) rpcInternal <- ccM connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaAdminS), utils.AdminSv1, rpcInternal) existingRP := &utils.RouteProfile{ Tenant: "cgrates.org", ID: "existingroute", Sorting: "*ascending", Routes: []*utils.Route{ { ID: "rtexisting", FilterIDs: []string{"*string:~*req.OldFilter:original"}, AccountIDs: []string{"1001"}, RateProfileIDs: []string{"rporiginal"}, ResourceIDs: []string{"resoriginal"}, StatIDs: []string{"statoriginal"}, RouteParameters: "originalparam=value", }, }, } ctx := context.Background() dm.SetRouteProfile(ctx, existingRP, true) act := &actDynamicRoute{ config: cfg, connMgr: connMgr, dm: dm, fltrS: fltrS, tnt: "cgrates.org", cgrEv: &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Event: map[string]any{ utils.AccountField: "1001", }, }, aCfg: &utils.APAction{ ID: "ACT_DYNAMIC_ROUTE", Diktats: []*utils.APDiktat{ { ID: "d1", FilterIDs: []string{"*string:~*req.Account:1001"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{Weight: 20.0}, }, Opts: map[string]any{ utils.MetaTemplate: "cgrates.org;routefromswitch@existingroute;;;;*ascending;;rtexisting;*string:~*req.NewFilter:modified;2001&2002;rpmodified;resmodified;statmodified;*modifiedweight&30.0;*modifiedblocker&true;modifiedparam=value;", }, }, }, }, } data := utils.MapStorage{ "*req": map[string]any{ utils.AccountField: "1001", }, } trgID := "trigger1" err := act.execute(ctx, data, trgID) if err != nil { t.Fatalf("Unexpected error: %v", err) } expRpo := &utils.RouteProfileWithAPIOpts{ RouteProfile: &utils.RouteProfile{ Tenant: "cgrates.org", ID: "existingroute", Sorting: "*ascending", Routes: []*utils.Route{ { ID: "rtexisting", FilterIDs: []string{"*string:~*req.NewFilter:modified"}, AccountIDs: []string{"2001", "2002"}, RateProfileIDs: []string{"rpmodified"}, ResourceIDs: []string{"resmodified"}, StatIDs: []string{"statmodified"}, Weights: utils.DynamicWeights{ &utils.DynamicWeight{ FilterIDs: []string{"*modifiedweight"}, Weight: 30.0, }, }, Blockers: utils.DynamicBlockers{ &utils.DynamicBlocker{ FilterIDs: []string{"*modifiedblocker"}, Blocker: true, }, }, RouteParameters: "modifiedparam=value", }, }, }, APIOpts: map[string]any{}, } if !reflect.DeepEqual(rpo, expRpo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(expRpo), utils.ToJSON(rpo)) } }