Files
cgrates/engine/thresholds_test.go
2025-10-19 13:15:57 +02:00

3644 lines
99 KiB
Go

/*
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 <https://www.gnu.org/licenses/>
*/
package engine
import (
"bytes"
"fmt"
"reflect"
"sort"
"strings"
"testing"
"time"
"github.com/cgrates/birpc/context"
"github.com/cgrates/cgrates/config"
"github.com/cgrates/cgrates/utils"
"github.com/cgrates/guardian"
)
func TestThresholdsCache(t *testing.T) {
var dmTH *DataManager
tPrfls := []*ThresholdProfile{
{
Tenant: "cgrates.org",
ID: "TH_1",
FilterIDs: []string{"FLTR_TH_1", "*ai:~*req.AnswerTime:2014-07-14T14:35:00Z"},
MaxHits: 12,
Blocker: false,
Weights: utils.DynamicWeights{
{
Weight: 20.0,
},
},
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
{
Tenant: "cgrates.org",
ID: "TH_2",
FilterIDs: []string{"FLTR_TH_2", "*ai:~*req.AnswerTime:2014-07-14T14:35:00Z"},
MaxHits: 12,
MinSleep: 5 * time.Minute,
Blocker: false,
Weights: utils.DynamicWeights{
{
Weight: 20.0,
},
},
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
{
Tenant: "cgrates.org",
ID: "TH_3",
FilterIDs: []string{"FLTR_TH_3", "*ai:~*req.AnswerTime:2014-07-14T14:35:00Z"},
MaxHits: 12,
MinSleep: 5 * time.Minute,
Blocker: false,
Weights: utils.DynamicWeights{
{
Weight: 20.0,
},
},
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
}
ths := []*Threshold{
{
Tenant: "cgrates.org",
ID: "TH_1",
Hits: 0,
},
{
Tenant: "cgrates.org",
ID: "TH_2",
Hits: 0,
},
{
Tenant: "cgrates.org",
ID: "TH_3",
Hits: 0,
},
}
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dmTH = NewDataManager(dbCM, cfg, nil)
cfg.ThresholdSCfg().StoreInterval = 0
cfg.ThresholdSCfg().StringIndexedFields = nil
cfg.ThresholdSCfg().PrefixIndexedFields = nil
fltrTh1 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_1",
Rules: []*FilterRule{
{
Type: utils.MetaString,
Element: "~*req.Threshold",
Values: []string{"TH_1"},
},
{
Type: utils.MetaGreaterOrEqual,
Element: utils.DynamicDataPrefix + utils.MetaReq + utils.NestingSep + utils.Weight,
Values: []string{"9.0"},
},
},
}
dmTH.SetFilter(context.Background(), fltrTh1, true)
fltrTh2 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_2",
Rules: []*FilterRule{
{
Type: utils.MetaString,
Element: "~*req.Threshold",
Values: []string{"TH_2"},
},
{
Type: utils.MetaGreaterOrEqual,
Element: utils.DynamicDataPrefix + utils.MetaReq + utils.NestingSep + utils.Weight,
Values: []string{"15.0"},
},
},
}
dmTH.SetFilter(context.Background(), fltrTh2, true)
fltrTh3 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_3",
Rules: []*FilterRule{
{
Type: utils.MetaPrefix,
Element: "~*req.Threshold",
Values: []string{"ThresholdPrefix"},
},
},
}
dmTH.SetFilter(context.Background(), fltrTh3, true)
for _, th := range tPrfls {
if err := dmTH.SetThresholdProfile(context.TODO(), th, true); err != nil {
t.Errorf("Error: %+v", err)
}
}
//Test each threshold profile from cache
for _, th := range tPrfls {
if temptTh, err := dmTH.GetThresholdProfile(context.TODO(), th.Tenant,
th.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(th, temptTh) {
t.Errorf("Expecting: %+v, received: %+v", th, temptTh)
}
}
for _, th := range ths {
if err := dmTH.SetThreshold(context.TODO(), th); err != nil {
t.Errorf("Error: %+v", err)
}
}
//Test each threshold profile from cache
for _, th := range ths {
if temptTh, err := dmTH.GetThreshold(context.TODO(), th.Tenant,
th.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(th, temptTh) {
t.Errorf("Expecting: %+v, received: %+v", th, temptTh)
}
}
}
func TestThresholdsmatchingThresholdsForEvent(t *testing.T) {
var dmTH *DataManager
var thServ *ThresholdS
var tPrfls = []*ThresholdProfile{
{
Tenant: "cgrates.org",
ID: "TH_1",
FilterIDs: []string{"FLTR_TH_1", "*ai:*now:2014-07-14T14:35:00Z"},
MaxHits: 12,
Blocker: false,
Weights: utils.DynamicWeights{
{
Weight: 20.0,
},
},
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
{
Tenant: "cgrates.org",
ID: "TH_2",
FilterIDs: []string{"FLTR_TH_2", "*ai:*now:2014-07-14T14:35:00Z"},
MaxHits: 12,
MinSleep: 5 * time.Minute,
Blocker: false,
Weights: utils.DynamicWeights{
{
Weight: 20.0,
},
},
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
{
Tenant: "cgrates.org",
ID: "TH_3",
FilterIDs: []string{"FLTR_TH_3", "*ai:*now:2014-07-14T14:35:00Z"},
MaxHits: 12,
MinSleep: 5 * time.Minute,
Blocker: false,
Weights: utils.DynamicWeights{
{
Weight: 20.0,
},
},
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
}
ths := []*Threshold{
{
Tenant: "cgrates.org",
ID: "TH_1",
Hits: 0,
},
{
Tenant: "cgrates.org",
ID: "TH_2",
Hits: 0,
},
{
Tenant: "cgrates.org",
ID: "TH_3",
Hits: 0,
},
}
argsGetThresholds := []*utils.CGREvent{
{
Tenant: "cgrates.org",
ID: "Ev1",
Event: map[string]any{
"Threshold": "TH_1",
"Weight": "10.0",
},
},
{
Tenant: "cgrates.org",
ID: "Ev1",
Event: map[string]any{
"Threshold": "TH_2",
"Weight": "20.0",
},
},
{
Tenant: "cgrates.org",
ID: "Ev1",
Event: map[string]any{
"Threshold": "ThresholdPrefix123",
},
},
}
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dmTH = NewDataManager(dbCM, cfg, nil)
cfg.ThresholdSCfg().StoreInterval = 0
cfg.ThresholdSCfg().StringIndexedFields = nil
cfg.ThresholdSCfg().PrefixIndexedFields = nil
thServ = NewThresholdService(dmTH, cfg, &FilterS{dm: dmTH, cfg: cfg}, nil)
fltrTh1 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_1",
Rules: []*FilterRule{
{
Type: utils.MetaString,
Element: "~*req.Threshold",
Values: []string{"TH_1"},
},
{
Type: utils.MetaGreaterOrEqual,
Element: utils.DynamicDataPrefix + utils.MetaReq + utils.NestingSep + utils.Weight,
Values: []string{"9.0"},
},
},
}
dmTH.SetFilter(context.Background(), fltrTh1, true)
fltrTh2 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_2",
Rules: []*FilterRule{
{
Type: utils.MetaString,
Element: "~*req.Threshold",
Values: []string{"TH_2"},
},
{
Type: utils.MetaGreaterOrEqual,
Element: utils.DynamicDataPrefix + utils.MetaReq + utils.NestingSep + utils.Weight,
Values: []string{"15.0"},
},
},
}
fltrTh3 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_3",
Rules: []*FilterRule{
{
Type: utils.MetaPrefix,
Element: "~*req.Threshold",
Values: []string{"ThresholdPrefix"},
},
},
}
dmTH.SetFilter(context.TODO(), fltrTh2, true)
dmTH.SetFilter(context.TODO(), fltrTh3, true)
for _, th := range tPrfls {
if err := dmTH.SetThresholdProfile(context.TODO(), th, true); err != nil {
t.Errorf("Error: %+v", err)
}
}
//Test each threshold profile from cache
for _, th := range tPrfls {
if temptTh, err := dmTH.GetThresholdProfile(context.TODO(), th.Tenant,
th.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(th, temptTh) {
t.Errorf("Expecting: %+v, received: %+v", th, temptTh)
}
}
for _, th := range ths {
if err := dmTH.SetThreshold(context.TODO(), th); err != nil {
t.Errorf("Error: %+v", err)
}
}
//Test each threshold profile from cache
for _, th := range ths {
if temptTh, err := dmTH.GetThreshold(context.TODO(), th.Tenant,
th.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(th, temptTh) {
t.Errorf("Expecting: %+v, received: %+v", th, temptTh)
}
}
thMatched, err := thServ.matchingThresholdsForEvent(context.TODO(), argsGetThresholds[0].Tenant, argsGetThresholds[0])
if err != nil {
t.Errorf("Error: %+v", err)
}
unlockThresholds(thMatched)
if !reflect.DeepEqual(ths[0].Tenant, thMatched[0].Tenant) {
t.Errorf("Expecting: %+v, received: %+v", ths[0].Tenant, thMatched[0].Tenant)
} else if !reflect.DeepEqual(ths[0].ID, thMatched[0].ID) {
t.Errorf("Expecting: %+v, received: %+v", ths[0].ID, thMatched[0].ID)
} else if !reflect.DeepEqual(ths[0].Hits, thMatched[0].Hits) {
t.Errorf("Expecting: %+v, received: %+v", ths[0].Hits, thMatched[0].Hits)
}
thMatched, err = thServ.matchingThresholdsForEvent(context.TODO(), argsGetThresholds[1].Tenant, argsGetThresholds[1])
if err != nil {
t.Errorf("Error: %+v", err)
}
unlockThresholds(thMatched)
if !reflect.DeepEqual(ths[1].Tenant, thMatched[0].Tenant) {
t.Errorf("Expecting: %+v, received: %+v", ths[1].Tenant, thMatched[0].Tenant)
} else if !reflect.DeepEqual(ths[1].ID, thMatched[0].ID) {
t.Errorf("Expecting: %+v, received: %+v", ths[1].ID, thMatched[0].ID)
} else if !reflect.DeepEqual(ths[1].Hits, thMatched[0].Hits) {
t.Errorf("Expecting: %+v, received: %+v", ths[1].Hits, thMatched[0].Hits)
}
thMatched, err = thServ.matchingThresholdsForEvent(context.TODO(), argsGetThresholds[2].Tenant, argsGetThresholds[2])
if err != nil {
t.Errorf("Error: %+v", err)
}
unlockThresholds(thMatched)
if !reflect.DeepEqual(ths[2].Tenant, thMatched[0].Tenant) {
t.Errorf("Expecting: %+v, received: %+v", ths[2].Tenant, thMatched[0].Tenant)
} else if !reflect.DeepEqual(ths[2].ID, thMatched[0].ID) {
t.Errorf("Expecting: %+v, received: %+v", ths[2].ID, thMatched[0].ID)
} else if !reflect.DeepEqual(ths[2].Hits, thMatched[0].Hits) {
t.Errorf("Expecting: %+v, received: %+v", ths[2].Hits, thMatched[0].Hits)
}
}
/*
func TestThresholdsProcessEvent(t *testing.T) {
var dmTH *DataManager
var thServ *ThresholdService
var tPrfls = []*ThresholdProfile{
{
Tenant: "cgrates.org",
ID: "TH_1",
FilterIDs: []string{"FLTR_TH_1"},
MaxHits: 12,
Blocker: false,
Weight: 20.0,
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
{
Tenant: "cgrates.org",
ID: "TH_2",
FilterIDs: []string{"FLTR_TH_2"},
MaxHits: 12,
MinSleep: 5 * time.Minute,
Blocker: false,
Weight: 20.0,
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
{
Tenant: "cgrates.org",
ID: "TH_3",
FilterIDs: []string{"FLTR_TH_3"},
MaxHits: 12,
MinSleep: 5 * time.Minute,
Blocker: false,
Weight: 20.0,
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
}
ths := Thresholds{
&Threshold{
Tenant: "cgrates.org",
ID: "TH_1",
Hits: 0,
},
&Threshold{
Tenant: "cgrates.org",
ID: "TH_2",
Hits: 0,
},
&Threshold{
Tenant: "cgrates.org",
ID: "TH_3",
Hits: 0,
},
}
argsGetThresholds := []*utils.CGREvent{
{
Tenant: "cgrates.org",
ID: "Ev1",
Event: map[string]any{
"Threshold": "TH_1",
"Weight": "10.0",
},
},
{
Tenant: "cgrates.org",
ID: "Ev1",
Event: map[string]any{
"Threshold": "TH_2",
"Weight": "20.0",
},
},
{
Tenant: "cgrates.org",
ID: "Ev1",
Event: map[string]any{
"Threshold": "ThresholdPrefix123",
},
},
}
cfg := config.NewDefaultCGRConfig()
data , _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dmTH = NewDataManager(dbCM, config.CgrConfig().CacheCfg(), nil)
cfg.ThresholdSCfg().StoreInterval = 0
cfg.ThresholdSCfg().StringIndexedFields = nil
cfg.ThresholdSCfg().PrefixIndexedFields = nil
thServ = NewThresholdService(dmTH, cfg, &FilterS{dm: dmTH, cfg: cfg})
if err != nil {
t.Errorf("Error: %+v", err)
}
fltrTh1 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_1",
Rules: []*FilterRule{
{
Type: utils.MetaString,
Element: "~*req.Threshold",
Values: []string{"TH_1"},
},
{
Type: utils.MetaGreaterOrEqual,
Element: utils.DynamicDataPrefix + utils.MetaReq + utils.NestingSep + utils.Weight,
Values: []string{"9.0"},
},
},
}
dmTH.SetFilter(context.TODO(), fltrTh1, true)
fltrTh2 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_2",
Rules: []*FilterRule{
{
Type: utils.MetaString,
Element: "~*req.Threshold",
Values: []string{"TH_2"},
},
{
Type: utils.MetaGreaterOrEqual,
Element: utils.DynamicDataPrefix + utils.MetaReq + utils.NestingSep + utils.Weight,
Values: []string{"15.0"},
},
},
}
dmTH.SetFilter(context.TODO(), fltrTh2, true)
fltrTh3 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_3",
Rules: []*FilterRule{
{
Type: utils.MetaPrefix,
Element: "~*req.Threshold",
Values: []string{"ThresholdPrefix"},
},
},
}
dmTH.SetFilter(context.TODO(), fltrTh3, true)
for _, th := range tPrfls {
if err = dmTH.SetThresholdProfile(context.Background(),th, true); err != nil {
t.Errorf("Error: %+v", err)
}
}
//Test each threshold profile from cache
for _, th := range tPrfls {
if temptTh, err := dmTH.GetThresholdProfile(context.Background(),th.Tenant,
th.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(th, temptTh) {
t.Errorf("Expecting: %+v, received: %+v", th, temptTh)
}
}
for _, th := range ths {
if err = dmTH.SetThreshold(context.Background(),th); err != nil {
t.Errorf("Error: %+v", err)
}
}
//Test each threshold profile from cache
for _, th := range ths {
if temptTh, err := dmTH.GetThreshold(context.Background(),th.Tenant,
th.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(th, temptTh) {
t.Errorf("Expecting: %+v, received: %+v", th, temptTh)
}
}
thIDs := []string{"TH_1"}
if thMatched, err := thServ.processEvent(context.Background(),argsGetThresholds[0].Tenant, argsGetThresholds[0]); err != utils.ErrPartiallyExecuted {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(thIDs, thMatched) {
t.Errorf("Expecting: %+v, received: %+v", thIDs, thMatched)
}
thIDs = []string{"TH_2"}
if thMatched, err := thServ.processEvent(context.Background(),argsGetThresholds[1].Tenant, argsGetThresholds[1]); err != utils.ErrPartiallyExecuted {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(thIDs, thMatched) {
t.Errorf("Expecting: %+v, received: %+v", thIDs, thMatched)
}
thIDs = []string{"TH_3"}
if thMatched, err := thServ.processEvent(context.Background(),argsGetThresholds[2].Tenant, argsGetThresholds[2]); err != utils.ErrPartiallyExecuted {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(thIDs, thMatched) {
t.Errorf("Expecting: %+v, received: %+v", thIDs, thMatched)
}
}
func TestThresholdsVerifyIfExecuted(t *testing.T) {
var dmTH *DataManager
var thServ *ThresholdService
var tPrfls = []*ThresholdProfile{
{
Tenant: "cgrates.org",
ID: "TH_1",
FilterIDs: []string{"FLTR_TH_1"},
MaxHits: 12,
Blocker: false,
Weight: 20.0,
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
{
Tenant: "cgrates.org",
ID: "TH_2",
FilterIDs: []string{"FLTR_TH_2"},
MaxHits: 12,
MinSleep: 5 * time.Minute,
Blocker: false,
Weight: 20.0,
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
{
Tenant: "cgrates.org",
ID: "TH_3",
FilterIDs: []string{"FLTR_TH_3"},
MaxHits: 12,
MinSleep: 5 * time.Minute,
Blocker: false,
Weight: 20.0,
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
}
ths := Thresholds{
&Threshold{
Tenant: "cgrates.org",
ID: "TH_1",
Hits: 0,
},
&Threshold{
Tenant: "cgrates.org",
ID: "TH_2",
Hits: 0,
},
&Threshold{
Tenant: "cgrates.org",
ID: "TH_3",
Hits: 0,
},
}
argsGetThresholds := []*utils.CGREvent{
{
Tenant: "cgrates.org",
ID: "Ev1",
Event: map[string]any{
"Threshold": "TH_1",
"Weight": "10.0",
},
},
{
Tenant: "cgrates.org",
ID: "Ev1",
Event: map[string]any{
"Threshold": "TH_2",
"Weight": "20.0",
},
},
{
Tenant: "cgrates.org",
ID: "Ev1",
Event: map[string]any{
"Threshold": "ThresholdPrefix123",
},
},
}
cfg := config.NewDefaultCGRConfig()
data , _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dmTH = NewDataManager(dbCM, config.CgrConfig().CacheCfg(), nil)
cfg.ThresholdSCfg().StoreInterval = 0
cfg.ThresholdSCfg().StringIndexedFields = nil
cfg.ThresholdSCfg().PrefixIndexedFields = nil
thServ = NewThresholdService(dmTH, cfg, &FilterS{dm: dmTH, cfg: cfg})
if err != nil {
t.Errorf("Error: %+v", err)
}
fltrTh1 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_1",
Rules: []*FilterRule{
{
Type: utils.MetaString,
Element: "~*req.Threshold",
Values: []string{"TH_1"},
},
{
Type: utils.MetaGreaterOrEqual,
Element: utils.DynamicDataPrefix + utils.MetaReq + utils.NestingSep + utils.Weight,
Values: []string{"9.0"},
},
},
}
dmTH.SetFilter(context.TODO(), fltrTh1, true)
fltrTh2 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_2",
Rules: []*FilterRule{
{
Type: utils.MetaString,
Element: "~*req.Threshold",
Values: []string{"TH_2"},
},
{
Type: utils.MetaGreaterOrEqual,
Element: utils.DynamicDataPrefix + utils.MetaReq + utils.NestingSep + utils.Weight,
Values: []string{"15.0"},
},
},
}
dmTH.SetFilter(context.TODO(), fltrTh2, true)
fltrTh3 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_3",
Rules: []*FilterRule{
{
Type: utils.MetaPrefix,
Element: "~*req.Threshold",
Values: []string{"ThresholdPrefix"},
},
},
}
dmTH.SetFilter(context.TODO(), fltrTh3, true)
for _, th := range tPrfls {
if err = dmTH.SetThresholdProfile(context.Background(),th, true); err != nil {
t.Errorf("Error: %+v", err)
}
}
//Test each threshold profile from cache
for _, th := range tPrfls {
if temptTh, err := dmTH.GetThresholdProfile(context.Background(),th.Tenant,
th.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(th, temptTh) {
t.Errorf("Expecting: %+v, received: %+v", th, temptTh)
}
}
for _, th := range ths {
if err = dmTH.SetThreshold(context.Background(),th); err != nil {
t.Errorf("Error: %+v", err)
}
}
thIDs := []string{"TH_1"}
if thMatched, err := thServ.processEvent(context.Background(),argsGetThresholds[0].Tenant, argsGetThresholds[0]); err != utils.ErrPartiallyExecuted {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(thIDs, thMatched) {
t.Errorf("Expecting: %+v, received: %+v", thIDs, thMatched)
}
thIDs = []string{"TH_2"}
if thMatched, err := thServ.processEvent(context.Background(),argsGetThresholds[1].Tenant, argsGetThresholds[1]); err != utils.ErrPartiallyExecuted {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(thIDs, thMatched) {
t.Errorf("Expecting: %+v, received: %+v", thIDs, thMatched)
}
thIDs = []string{"TH_3"}
if thMatched, err := thServ.processEvent(context.Background(),argsGetThresholds[2].Tenant, argsGetThresholds[2]); err != utils.ErrPartiallyExecuted {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(thIDs, thMatched) {
t.Errorf("Expecting: %+v, received: %+v", thIDs, thMatched)
}
thMatched, err := thServ.matchingThresholdsForEvent(argsGetThresholds[0].Tenant, argsGetThresholds[0])
if err != nil {
t.Errorf("Error: %+v", err)
}
thMatched.unlock()
if !reflect.DeepEqual(ths[0].Tenant, thMatched[0].Tenant) {
t.Errorf("Expecting: %+v, received: %+v", ths[0].Tenant, thMatched[0].Tenant)
} else if !reflect.DeepEqual(ths[0].ID, thMatched[0].ID) {
t.Errorf("Expecting: %+v, received: %+v", ths[0].ID, thMatched[0].ID)
} else if thMatched[0].Hits != 1 {
t.Errorf("Expecting: 1, received: %+v", thMatched[0].Hits)
}
thMatched, err = thServ.matchingThresholdsForEvent(argsGetThresholds[1].Tenant, argsGetThresholds[1])
if err != nil {
t.Errorf("Error: %+v", err)
}
thMatched.unlock()
if !reflect.DeepEqual(ths[1].Tenant, thMatched[0].Tenant) {
t.Errorf("Expecting: %+v, received: %+v", ths[1].Tenant, thMatched[0].Tenant)
} else if !reflect.DeepEqual(ths[1].ID, thMatched[0].ID) {
t.Errorf("Expecting: %+v, received: %+v", ths[1].ID, thMatched[0].ID)
} else if thMatched[0].Hits != 1 {
t.Errorf("Expecting: 1, received: %+v", thMatched[0].Hits)
}
thMatched, err = thServ.matchingThresholdsForEvent(argsGetThresholds[2].Tenant, argsGetThresholds[2])
if err != nil {
t.Errorf("Error: %+v", err)
}
thMatched.unlock()
if !reflect.DeepEqual(ths[2].Tenant, thMatched[0].Tenant) {
t.Errorf("Expecting: %+v, received: %+v", ths[2].Tenant, thMatched[0].Tenant)
} else if !reflect.DeepEqual(ths[2].ID, thMatched[0].ID) {
t.Errorf("Expecting: %+v, received: %+v", ths[2].ID, thMatched[0].ID)
} else if thMatched[0].Hits != 1 {
t.Errorf("Expecting: 1, received: %+v", thMatched[0].Hits)
}
}
func TestThresholdsProcessEvent2(t *testing.T) {
var dmTH *DataManager
var thServ *ThresholdService
tPrfls := []*ThresholdProfile{
{
Tenant: "cgrates.org",
ID: "TH_1",
FilterIDs: []string{"FLTR_TH_1"},
MaxHits: 12,
Blocker: false,
Weight: 20.0,
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
{
Tenant: "cgrates.org",
ID: "TH_2",
FilterIDs: []string{"FLTR_TH_2"},
MaxHits: 12,
MinSleep: 5 * time.Minute,
Blocker: false,
Weight: 20.0,
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
{
Tenant: "cgrates.org",
ID: "TH_3",
FilterIDs: []string{"FLTR_TH_3"},
MaxHits: 12,
MinSleep: 5 * time.Minute,
Blocker: false,
Weight: 20.0,
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
},
}
ths := Thresholds{
&Threshold{
Tenant: "cgrates.org",
ID: "TH_1",
Hits: 0,
},
&Threshold{
Tenant: "cgrates.org",
ID: "TH_2",
Hits: 0,
},
&Threshold{
Tenant: "cgrates.org",
ID: "TH_3",
Hits: 0,
},
}
argsGetThresholds := []*utils.CGREvent{
{
Tenant: "cgrates.org",
ID: "Ev1",
Event: map[string]any{
"Threshold": "TH_1",
"Weight": "10.0",
},
},
{
Tenant: "cgrates.org",
ID: "Ev1",
Event: map[string]any{
"Threshold": "TH_2",
"Weight": "20.0",
},
},
{
Tenant: "cgrates.org",
ID: "Ev1",
Event: map[string]any{
"Threshold": "ThresholdPrefix123",
},
},
}
cfg := config.NewDefaultCGRConfig()
data , _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dmTH = NewDataManager(dbCM, config.CgrConfig().CacheCfg(), nil)
cfg.ThresholdSCfg().StoreInterval = 0
cfg.ThresholdSCfg().StringIndexedFields = nil
cfg.ThresholdSCfg().PrefixIndexedFields = nil
thServ = NewThresholdService(dmTH, cfg, &FilterS{dm: dmTH, cfg: cfg})
if err != nil {
t.Errorf("Error: %+v", err)
}
fltrTh1 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_1",
Rules: []*FilterRule{
{
Type: utils.MetaString,
Element: "~*req.Threshold",
Values: []string{"TH_1"},
},
{
Type: utils.MetaGreaterOrEqual,
Element: utils.DynamicDataPrefix + utils.MetaReq + utils.NestingSep + utils.Weight,
Values: []string{"9.0"},
},
},
}
dmTH.SetFilter(context.TODO(), fltrTh1, true)
fltrTh2 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_2",
Rules: []*FilterRule{
{
Type: utils.MetaString,
Element: "~*req.Threshold",
Values: []string{"TH_2"},
},
{
Type: utils.MetaGreaterOrEqual,
Element: utils.DynamicDataPrefix + utils.MetaReq + utils.NestingSep + utils.Weight,
Values: []string{"15.0"},
},
},
}
dmTH.SetFilter(context.TODO(), fltrTh2, true)
fltrTh3 := &Filter{
Tenant: config.CgrConfig().GeneralCfg().DefaultTenant,
ID: "FLTR_TH_3",
Rules: []*FilterRule{
{
Type: utils.MetaPrefix,
Element: "~*req.Threshold",
Values: []string{"ThresholdPrefix"},
},
},
}
dmTH.SetFilter(context.TODO(), fltrTh3, true)
for _, th := range tPrfls {
if err = dmTH.SetThresholdProfile(context.Background(),th, true); err != nil {
t.Errorf("Error: %+v", err)
}
}
//Test each threshold profile from cache
for _, th := range tPrfls {
if temptTh, err := dmTH.GetThresholdProfile(context.Background(),th.Tenant,
th.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(th, temptTh) {
t.Errorf("Expecting: %+v, received: %+v", th, temptTh)
}
}
for _, th := range ths {
if err = dmTH.SetThreshold(context.Background(),th); err != nil {
t.Errorf("Error: %+v", err)
}
}
//Test each threshold profile from cache
for _, th := range ths {
if temptTh, err := dmTH.GetThreshold(context.Background(),th.Tenant,
th.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(th, temptTh) {
t.Errorf("Expecting: %+v, received: %+v", th, temptTh)
}
}
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH_4",
FilterIDs: []string{"FLTR_TH_1"},
MaxHits: 12,
Blocker: false,
Weight: 20.0,
ActionProfileIDs: []string{"ACT_1", "ACT_2"},
Async: false,
}
th := &Threshold{
Tenant: "cgrates.org",
ID: "TH_4",
Hits: 0,
}
ev := &ThresholdsArgsProcessEvent{
ThresholdIDs: []string{"TH_1", "TH_2", "TH_3", "TH_4"},
CGREvent: argsGetThresholds[0].CGREvent,
}
if err = dmTH.SetThresholdProfile(context.Background(),thPrf, true); err != nil {
t.Errorf("Error: %+v", err)
}
if temptTh, err := dmTH.GetThresholdProfile(context.Background(),thPrf.Tenant,
thPrf.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(thPrf, temptTh) {
t.Errorf("Expecting: %+v, received: %+v", th, temptTh)
}
if err = dmTH.SetThreshold(context.Background(),th); err != nil {
t.Errorf("Error: %+v", err)
}
if temptTh, err := dmTH.GetThreshold(context.Background(),th.Tenant,
th.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(th, temptTh) {
t.Errorf("Expecting: %+v, received: %+v", th, temptTh)
}
thIDs := []string{"TH_1", "TH_4"}
thIDsRev := []string{"TH_4", "TH_1"}
if thMatched, err := thServ.processEvent(ev.Tenant, ev); err != utils.ErrPartiallyExecuted {
t.Errorf("Error: %+v", err)
} else if !reflect.DeepEqual(thIDs, thMatched) && !reflect.DeepEqual(thIDsRev, thMatched) {
t.Errorf("Expecting: %+v, received: %+v", thIDs, thMatched)
}
thMatched, err := thServ.matchingThresholdsForEvent(ev.Tenant, ev)
if err != nil {
t.Fatalf("Error: %+v", err)
}
thMatched.unlock()
for _, thM := range thMatched {
if !reflect.DeepEqual(thPrf.Tenant, thM.Tenant) {
t.Errorf("Expecting: %+v, received: %+v", thPrf.Tenant, thM.Tenant)
} else if reflect.DeepEqual(thIDs[0], thM.ID) && thM.Hits != 1 {
t.Errorf("Expecting: 1 for %+v, received: %+v", thM.ID, thM.Hits)
} else if reflect.DeepEqual(thIDs[1], thM.ID) && thM.Hits != 1 {
t.Errorf("Expecting: 1 for %+v, received: %+v", thM.ID, thM.Hits)
}
}
}
*/
func TestThresholdsUpdateThreshold(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
idb, err := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
if err != nil {
t.Error(err)
}
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: idb}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
thp := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "THUP1",
}
th := &Threshold{
Tenant: thp.Tenant,
ID: thp.ID,
Hits: 5,
Snooze: time.Now(),
}
expTh := &Threshold{
Tenant: thp.Tenant,
ID: thp.ID,
}
if err := dm.SetThresholdProfile(context.Background(), thp, true); err != nil {
t.Fatal(err)
}
if th, err := dm.GetThreshold(context.Background(), thp.Tenant, thp.ID, false, false, utils.NonTransactional); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(expTh, th) {
t.Errorf("Expected: %s, received: %s", utils.ToJSON(expTh), utils.ToJSON(th))
}
if err := dm.RemoveThreshold(context.Background(), th.Tenant, th.ID); err != nil {
t.Fatal(err)
}
if err := dm.SetThresholdProfile(context.Background(), thp, true); err != nil {
t.Fatal(err)
}
if th, err := dm.GetThreshold(context.Background(), thp.Tenant, thp.ID, false, false, utils.NonTransactional); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(expTh, th) {
t.Errorf("Expected: %s, received: %s", utils.ToJSON(expTh), utils.ToJSON(th))
}
if err := dm.SetThreshold(context.Background(), th); err != nil {
t.Fatal(err)
}
thp = &ThresholdProfile{
Tenant: "cgrates.org",
ID: "THUP1",
MaxHits: 1,
}
if err := dm.SetThresholdProfile(context.Background(), thp, true); err != nil {
t.Fatal(err)
}
if th, err := dm.GetThreshold(context.Background(), thp.Tenant, thp.ID, false, false, utils.NonTransactional); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(expTh, th) {
t.Errorf("Expected: %s, received: %s", utils.ToJSON(expTh), utils.ToJSON(th))
}
if err := dm.SetThreshold(context.Background(), th); err != nil {
t.Fatal(err)
}
thp = &ThresholdProfile{
Tenant: "cgrates.org",
ID: "THUP1",
MaxHits: 1,
MinHits: 1,
}
if err := dm.SetThresholdProfile(context.Background(), thp, true); err != nil {
t.Fatal(err)
}
if th, err := dm.GetThreshold(context.Background(), thp.Tenant, thp.ID, false, false, utils.NonTransactional); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(expTh, th) {
t.Errorf("Expected: %s, received: %s", utils.ToJSON(expTh), utils.ToJSON(th))
}
if err := dm.SetThreshold(context.Background(), th); err != nil {
t.Fatal(err)
}
thp = &ThresholdProfile{
Tenant: "cgrates.org",
ID: "THUP1",
MaxHits: 1,
MinHits: 1,
MinSleep: 1,
}
if err := dm.SetThresholdProfile(context.Background(), thp, true); err != nil {
t.Fatal(err)
}
if th, err := dm.GetThreshold(context.Background(), thp.Tenant, thp.ID, false, false, utils.NonTransactional); err != nil {
t.Fatal(err)
} else if !reflect.DeepEqual(expTh, th) {
t.Errorf("Expected: %s, received: %s", utils.ToJSON(expTh), utils.ToJSON(th))
}
if err := dm.RemoveThresholdProfile(context.Background(), thp.Tenant, thp.ID, true); err != nil {
t.Fatal(err)
}
if _, err := dm.GetThreshold(context.Background(), thp.Tenant, thp.ID, false, false, utils.NonTransactional); err != utils.ErrNotFound {
t.Fatal(err)
}
}
// func TestThresholdsProcessEvent3(t *testing.T) {
// thPrf := &ThresholdProfile{
// Tenant: "cgrates.org",
// ID: "TH1",
// FilterIDs: []string{"*string:~*req.Account:1001"},
// MinHits: 3,
// MaxHits: 5,
// Weight: 10,
// ActionIDs: []string{"actPrf"},
// }
// th := &Threshold{
// Tenant: "cgrates.org",
// ID: "TH1",
// Hits: 2,
// tPrfl: thPrf,
// }
// args := &ThresholdsArgsProcessEvent{
// ThresholdIDs: []string{"TH1"},
// CGREvent: &utils.CGREvent{
// Tenant: "cgrates.org",
// ID: "ThresholdProcessEvent",
// Event: map[string]any{
// utils.AccountField: "1001",
// },
// APIOpts: map[string]any{
// utils.MetaEventType: utils.AccountUpdate,
// },
// },
// }
// if err := processEventWithThreshold(args, dm); err != nil {
// t.Error(err)
// }
// }
func TestThresholdsStoreThresholdsOK(t *testing.T) {
tmp := Cache
defer func() {
Cache = tmp
}()
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
tS := NewThresholdService(dm, cfg, nil, nil)
exp := &Threshold{
dirty: utils.BoolPointer(false),
Tenant: "cgrates.org",
ID: "TH1",
Hits: 2,
}
Cache.SetWithoutReplicate(utils.CacheThresholds, "cgrates.org:TH1", exp, nil, true,
utils.NonTransactional)
tS.storedTdIDs.Add("cgrates.org:TH1")
tS.storeThresholds(context.Background())
if rcv, err := tS.dm.GetThreshold(context.Background(), "cgrates.org", "TH1", true, false,
utils.NonTransactional); err != nil {
t.Error(err)
} else if !reflect.DeepEqual(rcv, exp) {
t.Errorf("expected: <%+v>, \nreceived: <%+v>",
utils.ToJSON(exp), utils.ToJSON(rcv))
}
Cache.Remove(context.Background(), utils.CacheThresholds, "cgrates.org:TH1", true, utils.NonTransactional)
}
func TestThresholdsStoreThresholdsStoreThErr(t *testing.T) {
tmp := Cache
tmpLogger := utils.Logger
defer func() {
Cache = tmp
utils.Logger = tmpLogger
}()
var buf bytes.Buffer
utils.Logger = utils.NewStdLoggerWithWriter(&buf, "", 4)
cfg := config.NewDefaultCGRConfig()
tS := NewThresholdService(nil, cfg, nil, nil)
value := &Threshold{
dirty: utils.BoolPointer(true),
Tenant: "cgrates.org",
ID: "TH1",
Hits: 2,
}
Cache.SetWithoutReplicate(utils.CacheThresholds, "TH1", value, nil, true,
utils.NonTransactional)
tS.storedTdIDs.Add("TH1")
exp := utils.StringSet{
"TH1": struct{}{},
}
expLog := `[WARNING] <ThresholdS> failed saving Threshold with tenant: cgrates.org and ID: TH1, error: NO_DATABASE_CONNECTION`
tS.storeThresholds(context.Background())
if !reflect.DeepEqual(tS.storedTdIDs, exp) {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", exp, tS.storedTdIDs)
}
if rcvLog := buf.String(); !strings.Contains(rcvLog, expLog) {
t.Errorf("expected log <%+v>\n to be in included in: <%+v>", expLog, rcvLog)
}
Cache.Remove(context.Background(), utils.CacheThresholds, "TH1", true, utils.NonTransactional)
}
func TestThresholdsStoreThresholdsCacheGetErr(t *testing.T) {
tmp := Cache
tmpLogger := utils.Logger
defer func() {
utils.Logger = tmpLogger
Cache = tmp
}()
var buf bytes.Buffer
utils.Logger = utils.NewStdLoggerWithWriter(&buf, "", 4)
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
tS := NewThresholdService(dm, cfg, nil, nil)
value := &Threshold{
dirty: utils.BoolPointer(true),
Tenant: "cgrates.org",
ID: "TH1",
Hits: 2,
}
Cache.SetWithoutReplicate(utils.CacheThresholds, "TH2", value, nil, true,
utils.NonTransactional)
tS.storedTdIDs.Add("TH1")
expLog := `[WARNING] <ThresholdS> failed retrieving from cache treshold with ID: TH1`
tS.storeThresholds(context.Background())
if rcvLog := buf.String(); !strings.Contains(rcvLog, expLog) {
t.Errorf("expected <%+v> \nto be included in: <%+v>", expLog, rcvLog)
}
Cache.Remove(context.Background(), utils.CacheThresholds, "TH2", true, utils.NonTransactional)
}
func TestThresholdsStoreThresholdNilDirtyField(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
tS := NewThresholdService(dm, cfg, nil, nil)
th := &Threshold{
Tenant: "cgrates.org",
ID: "TH1",
Hits: 2,
}
if err := tS.StoreThreshold(context.Background(), th); err != nil {
t.Error(err)
}
}
func TestThresholdsRPCClone(t *testing.T) {
args := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "EventTest",
Event: make(map[string]any),
APIOpts: map[string]any{
utils.OptsThresholdsProfileIDs: []string{"THD_ID"},
},
}
args.SetCloneable(true)
exp := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "EventTest",
Event: make(map[string]any),
APIOpts: map[string]any{
utils.OptsThresholdsProfileIDs: []string{"THD_ID"},
},
}
if out, err := args.RPCClone(); err != nil {
t.Error(err)
} else if !reflect.DeepEqual(out, exp) {
t.Errorf("expected: <%T>, \nreceived: <%T>",
args, exp)
}
}
func TestThresholdsProcessEventStoreThOK(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
cfg.ThresholdSCfg().StoreInterval = -1
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH2",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf, true); err != nil {
t.Error(err)
}
args := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "ThdProcessEvent",
Event: map[string]any{
utils.AccountField: "1001",
},
APIOpts: map[string]any{
utils.OptsThresholdsProfileIDs: []string{"TH2"},
},
}
exp := &Threshold{
Tenant: "cgrates.org",
ID: "TH2",
Hits: 1,
}
expIDs := []string{"TH2"}
if rcvIDs, err := tS.processEvent(context.Background(), args.Tenant, args); err != nil {
t.Error(err)
} else if !reflect.DeepEqual(rcvIDs, expIDs) {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", expIDs, rcvIDs)
} else if rcv, err := tS.dm.GetThreshold(context.Background(), "cgrates.org", "TH2", true, false,
utils.NonTransactional); err != nil {
t.Error(err)
} else {
rcv.tPrfl = nil
rcv.dirty = nil
rcv.Snooze = time.Time{}
if !reflect.DeepEqual(rcv, exp) {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", exp, rcv)
}
}
}
func TestThresholdsProcessEventMaxHitsDMErr(t *testing.T) {
tmpLogger := utils.Logger
var buf bytes.Buffer
utils.Logger = utils.NewStdLoggerWithWriter(&buf, "", 4)
tmp := config.CgrConfig()
tmpC := Cache
tmpCMgr := connMgr
cfg := config.NewDefaultCGRConfig()
cfg.RPCConns()["test"] = &config.RPCConn{Conns: []*config.RemoteHost{{}}}
cfg.CacheCfg().ReplicationConns = []string{"test"}
cfg.CacheCfg().Partitions[utils.CacheThresholds].Replicate = true
config.SetCgrConfig(cfg)
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
connMgr = NewConnManager(cfg)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, connMgr)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(nil, cfg, filterS, connMgr)
Cache = NewCacheS(cfg, dm, nil, nil)
defer func() {
connMgr = tmpCMgr
Cache = tmpC
config.SetCgrConfig(tmp)
utils.Logger = tmpLogger
}()
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH3",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10.0,
},
},
ActionProfileIDs: []string{utils.MetaNone},
Blocker: true,
}
th := &Threshold{
Tenant: "cgrates.org",
ID: "TH3",
Hits: 4,
tPrfl: thPrf,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf, true); err != nil {
t.Error(err)
}
if err := dm.SetThreshold(context.Background(), th); err != nil {
t.Error(err)
}
Cache.SetWithoutReplicate(utils.CacheThresholdProfiles, thPrf.TenantID(), thPrf, nil, true, utils.NonTransactional)
Cache.SetWithoutReplicate(utils.CacheThresholds, th.TenantID(), th, nil, true, utils.NonTransactional)
args := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "ThdProcessEvent",
Event: map[string]any{
utils.AccountField: "1001",
},
APIOpts: map[string]any{
utils.OptsThresholdsProfileIDs: []string{"TH3"},
},
}
if _, err := tS.processEvent(context.Background(), args.Tenant, args); err == nil ||
err != utils.ErrPartiallyExecuted {
t.Errorf("expected: <%+v>, \nreceived: <%+v>",
utils.ErrPartiallyExecuted, err)
}
}
func TestThresholdsProcessEventNotFound(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH5",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
th := &Threshold{
Tenant: "cgrates.org",
ID: "TH5",
Hits: 2,
tPrfl: thPrf,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf, true); err != nil {
t.Error(err)
}
if err := dm.SetThreshold(context.Background(), th); err != nil {
t.Error(err)
}
args := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "ThdProcessEvent",
Event: map[string]any{
utils.AccountField: "1001",
},
APIOpts: map[string]any{
utils.OptsThresholdsProfileIDs: []string{"TH6"},
},
}
if _, err := tS.processEvent(context.Background(), args.Tenant, args); err == nil ||
err != utils.ErrNotFound {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", utils.ErrNotFound, err)
}
}
func TestThresholdsV1ProcessEventOK(t *testing.T) {
tmp := Cache
defer func() {
guardian.Guardian = guardian.New()
Cache = tmp
}()
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf1 := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
ActionProfileIDs: []string{utils.MetaNone},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10.0,
},
},
Blocker: true,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf1, true); err != nil {
t.Error(err)
}
thPrf2 := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH2",
FilterIDs: []string{"*string:~*req.Account:1001"},
ActionProfileIDs: []string{utils.MetaNone},
MinHits: 0,
MaxHits: 7,
Weights: utils.DynamicWeights{
{
Weight: 20,
},
},
Blocker: false,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf2, true); err != nil {
t.Error(err)
}
args := &utils.CGREvent{
ID: "V1ProcessEventTest",
Event: map[string]any{
utils.AccountField: "1001",
},
}
var reply []string
if err := tS.V1ProcessEvent(context.Background(), args, &reply); err == nil || err.Error() != utils.ErrPartiallyExecuted.Error() {
t.Error(err)
}
}
func TestThresholdsV1ProcessEventPartExecErr(t *testing.T) {
tmp := Cache
tmpLogger := utils.Logger
defer func() {
utils.Logger = tmpLogger
Cache = tmp
guardian.Guardian = guardian.New()
}()
var buf bytes.Buffer
utils.Logger = utils.NewStdLoggerWithWriter(&buf, "", 4)
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf1 := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH3",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf1, true); err != nil {
t.Error(err)
}
thPrf2 := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH4",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 0,
MaxHits: 7,
Weights: utils.DynamicWeights{
{
Weight: 20,
},
},
Blocker: false,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf2, true); err != nil {
t.Error(err)
}
args := &utils.CGREvent{
ID: "V1ProcessEventTest",
Event: map[string]any{
utils.AccountField: "1001",
},
}
var reply []string
if err := tS.V1ProcessEvent(context.Background(), args, &reply); err == nil ||
err.Error() != utils.ErrPartiallyExecuted.Error() {
t.Errorf("expected: <%+v>,\nreceived: <%+v>", utils.ErrMandatoryIeMissing, err)
}
}
func TestThresholdsV1ProcessEventMissingArgs(t *testing.T) {
tmp := Cache
tmpLogger := utils.Logger
defer func() {
utils.Logger = tmpLogger
guardian.Guardian = guardian.New()
Cache = tmp
}()
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf1 := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf1, true); err != nil {
t.Error(err)
}
thPrf2 := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH2",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 0,
MaxHits: 7,
Weights: utils.DynamicWeights{
{
Weight: 20,
},
},
Blocker: false,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf2, true); err != nil {
t.Error(err)
}
args := &utils.CGREvent{
ID: "V1ProcessEventTest",
Event: map[string]any{
utils.AccountField: "1001",
},
}
args = &utils.CGREvent{
Event: map[string]any{
utils.AccountField: "1001",
},
}
var reply []string
experr := `MANDATORY_IE_MISSING: [ID]`
if err := tS.V1ProcessEvent(context.Background(), args, &reply); err == nil ||
err.Error() != experr {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", experr, err)
}
args = nil
experr = `MANDATORY_IE_MISSING: [CGREvent]`
if err := tS.V1ProcessEvent(context.Background(), args, &reply); err == nil ||
err.Error() != experr {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", experr, err)
}
args = &utils.CGREvent{
ID: "V1ProcessEventTest",
Event: nil,
}
experr = `MANDATORY_IE_MISSING: [Event]`
if err := tS.V1ProcessEvent(context.Background(), args, &reply); err == nil ||
err.Error() != experr {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", experr, err)
}
}
func TestThresholdsV1GetThresholdOK(t *testing.T) {
tmp := Cache
tmpLogger := utils.Logger
defer func() {
utils.Logger = tmpLogger
guardian.Guardian = guardian.New()
Cache = tmp
}()
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf, true); err != nil {
t.Error(err)
}
expTh := Threshold{
Tenant: "cgrates.org",
ID: "TH1",
Hits: 0,
}
var rplyTh Threshold
if err := tS.V1GetThreshold(context.Background(), &utils.TenantIDWithAPIOpts{
TenantID: &utils.TenantID{
ID: "TH1",
},
}, &rplyTh); err != nil {
t.Error(err)
} else {
var snooze time.Time
rplyTh.dirty = nil
rplyTh.Snooze = snooze
if !reflect.DeepEqual(rplyTh, expTh) {
t.Errorf("expected: <%+v>, \nreceived: <%+v>",
utils.ToJSON(expTh), utils.ToJSON(rplyTh))
}
}
}
func TestThresholdsV1GetThresholdNotFoundErr(t *testing.T) {
tmp := Cache
tmpLogger := utils.Logger
defer func() {
utils.Logger = tmpLogger
guardian.Guardian = guardian.New()
Cache = tmp
}()
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf, true); err != nil {
t.Error(err)
}
var rplyTh Threshold
if err := tS.V1GetThreshold(context.Background(), &utils.TenantIDWithAPIOpts{
TenantID: &utils.TenantID{
ID: "TH2",
},
}, &rplyTh); err == nil || err != utils.ErrNotFound {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", utils.ErrNotFound, err)
}
}
func TestThresholdMatchingThresholdForEventLocks2(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
tmp := Cache
defer func() {
Cache = tmp
guardian.Guardian = guardian.New()
}()
Cache = NewCacheS(cfg, nil, nil, nil)
db, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: db}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
cfg.ThresholdSCfg().StoreInterval = 1
cfg.ThresholdSCfg().StringIndexedFields = nil
cfg.ThresholdSCfg().PrefixIndexedFields = nil
rS := NewThresholdService(dm, cfg,
&FilterS{dm: dm, cfg: cfg}, nil)
prfs := make([]*ThresholdProfile, 0)
ids := utils.StringSet{}
for i := 0; i < 10; i++ {
rPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: fmt.Sprintf("TH%d", i),
Weights: utils.DynamicWeights{
{
Weight: 20.00,
},
},
MaxHits: 5,
}
dm.SetThresholdProfile(context.Background(), rPrf, true)
prfs = append(prfs, rPrf)
ids.Add(rPrf.ID)
}
rPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH20",
FilterIDs: []string{"FLTR_RES_201"},
Weights: utils.DynamicWeights{
{
Weight: 20.00,
},
},
MaxHits: 5,
}
err := db.SetThresholdProfileDrv(context.Background(), rPrf)
if err != nil {
t.Fatal(err)
}
prfs = append(prfs, rPrf)
ids.Add(rPrf.ID)
ev := &utils.CGREvent{
APIOpts: map[string]any{
utils.OptsThresholdsProfileIDs: ids.AsSlice(),
},
}
_, err = rS.matchingThresholdsForEvent(context.Background(), "cgrates.org", ev)
expErr := utils.ErrPrefixNotFound(rPrf.FilterIDs[0])
if err == nil || err.Error() != expErr.Error() {
t.Errorf("Expected error: %s ,received: %+v", expErr, err)
}
for _, rPrf := range prfs {
if rPrf.isLocked() {
t.Fatalf("Expected profile to not be locked %q", rPrf.ID)
}
if rPrf.ID == "TH20" {
continue
}
if r, err := dm.GetThreshold(context.Background(), rPrf.Tenant, rPrf.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("error %s for <%s>", err, rPrf.ID)
} else if r.isLocked() {
t.Fatalf("Expected Threshold to not be locked %q", rPrf.ID)
}
}
}
func TestThresholdMatchingThresholdForEventLocks3(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
prfs := make([]*ThresholdProfile, 0)
tmp := Cache
defer func() {
Cache = tmp
guardian.Guardian = guardian.New()
}()
Cache = NewCacheS(cfg, nil, nil, nil)
db := &DataDBMock{
GetThresholdProfileDrvF: func(ctx *context.Context, tnt, id string) (*ThresholdProfile, error) {
if id == "TH1" {
return nil, utils.ErrNotImplemented
}
rPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: id,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 20.00,
},
},
}
Cache.Set(ctx, utils.CacheThresholds, rPrf.TenantID(), &Threshold{
Tenant: rPrf.Tenant,
ID: rPrf.ID,
}, nil, true, utils.NonTransactional)
prfs = append(prfs, rPrf)
return rPrf, nil
},
}
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: db}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
cfg.ThresholdSCfg().StoreInterval = 1
cfg.ThresholdSCfg().StringIndexedFields = nil
cfg.ThresholdSCfg().PrefixIndexedFields = nil
rS := NewThresholdService(dm, cfg,
&FilterS{dm: dm, cfg: cfg}, nil)
ids := utils.StringSet{}
for i := 0; i < 10; i++ {
ids.Add(fmt.Sprintf("TH%d", i))
}
ev := &utils.CGREvent{
APIOpts: map[string]any{
utils.OptsThresholdsProfileIDs: ids.AsSlice(),
},
}
_, err := rS.matchingThresholdsForEvent(context.Background(), "cgrates.org", ev)
if err != utils.ErrNotImplemented {
t.Fatalf("Error: %+v", err)
}
for _, rPrf := range prfs {
if rPrf.isLocked() {
t.Fatalf("Expected profile to not be locked %q", rPrf.ID)
}
}
}
func TestThresholdMatchingThresholdForEventLocks5(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
tmp := Cache
tmpC := config.CgrConfig()
defer func() {
Cache = tmp
guardian.Guardian = guardian.New()
config.SetCgrConfig(tmpC)
}()
Cache = NewCacheS(cfg, nil, nil, nil)
db, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: db}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, NewConnManager(cfg))
cfg.ThresholdSCfg().StoreInterval = 1
cfg.ThresholdSCfg().StringIndexedFields = nil
cfg.ThresholdSCfg().PrefixIndexedFields = nil
cfg.RPCConns()["test"] = &config.RPCConn{Conns: []*config.RemoteHost{{}}}
cfg.DbCfg().DBConns[utils.MetaDefault].RmtConns = []string{"test"}
cfg.DbCfg().Items[utils.CacheThresholds].Remote = true
config.SetCgrConfig(cfg)
rS := NewThresholdService(dm, cfg,
&FilterS{dm: dm, cfg: cfg}, nil)
prfs := make([]*ThresholdProfile, 0)
ids := utils.StringSet{}
for i := 0; i < 10; i++ {
rPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: fmt.Sprintf("TH%d", i),
Weights: utils.DynamicWeights{
{
Weight: 20.00,
},
},
MaxHits: 5,
}
dm.SetThresholdProfile(context.Background(), rPrf, true)
prfs = append(prfs, rPrf)
ids.Add(rPrf.ID)
}
ev := &utils.CGREvent{
APIOpts: map[string]any{
utils.OptsThresholdsProfileIDs: ids.AsSlice(),
},
}
dm.RemoveThreshold(context.Background(), "cgrates.org", "TH1")
_, err := rS.matchingThresholdsForEvent(context.Background(), "cgrates.org", ev)
if err != utils.ErrDisconnected {
t.Errorf("Error: %+v", err)
}
for _, rPrf := range prfs {
if rPrf.isLocked() {
t.Fatalf("Expected profile to not be locked %q", rPrf.ID)
}
if rPrf.ID == "TH1" {
continue
}
if r, err := dm.GetThreshold(context.Background(), rPrf.Tenant, rPrf.ID, true, false, utils.NonTransactional); err != nil {
t.Errorf("error %s for <%s>", err, rPrf.ID)
} else if r.isLocked() {
t.Fatalf("Expected Threshold to not be locked %q", rPrf.ID)
}
}
}
func TestThresholdsRunBackupStoreIntervalLessThanZero(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
cfg.ThresholdSCfg().StoreInterval = -1
tS := &ThresholdS{
cfg: cfg,
loopStopped: make(chan struct{}, 1),
}
tS.runBackup(context.Background())
select {
case <-tS.loopStopped:
case <-time.After(time.Second):
t.Error("timed out waiting for loop to stop")
}
}
func TestThresholdsRunBackupStop(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
cfg.ThresholdSCfg().StoreInterval = 5 * time.Millisecond
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
tnt := "cgrates.org"
thID := "Th1"
tS := &ThresholdS{
dm: dm,
storedTdIDs: utils.StringSet{
thID: struct{}{},
},
cfg: cfg,
loopStopped: make(chan struct{}, 1),
stopBackup: make(chan struct{}),
}
value := &Threshold{
dirty: utils.BoolPointer(true),
Tenant: tnt,
ID: thID,
}
Cache.SetWithoutReplicate(utils.CacheThresholds, thID, value, nil, true, "")
// Backup loop checks for the state of the stopBackup
// channel after storing the threshold. Channel can be
// safely closed beforehand.
close(tS.stopBackup)
tS.runBackup(context.Background())
want := &Threshold{
dirty: utils.BoolPointer(false),
Tenant: tnt,
ID: thID,
}
if got, err := tS.dm.GetThreshold(context.Background(), tnt, thID, true, false, utils.NonTransactional); err != nil {
t.Errorf("dm.GetThreshold(%q,%q): got unexpected err=%v", tnt, thID, err)
} else if !reflect.DeepEqual(got, want) {
t.Errorf("dm.GetThreshold(%q,%q) = %v, want %v", tnt, thID, got, want)
}
select {
case <-tS.loopStopped:
case <-time.After(time.Second):
t.Error("timed out waiting for loop to stop")
}
}
func TestThresholdsReload(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
cfg.ThresholdSCfg().StoreInterval = 5 * time.Millisecond
tS := &ThresholdS{
stopBackup: make(chan struct{}),
loopStopped: make(chan struct{}, 1),
cfg: cfg,
}
tS.loopStopped <- struct{}{}
tS.Reload(context.Background())
close(tS.stopBackup)
select {
case <-tS.loopStopped:
case <-time.After(time.Second):
t.Error("timed out waiting for loop to stop")
}
}
func TestThresholdsStartLoop(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
cfg.ThresholdSCfg().StoreInterval = -1
tS := &ThresholdS{
loopStopped: make(chan struct{}, 1),
cfg: cfg,
}
tS.StartLoop(context.Background())
select {
case <-tS.loopStopped:
case <-time.After(time.Second):
t.Error("timed out waiting for loop to stop")
}
}
func TestThresholdsV1GetThresholdsForEventOK(t *testing.T) {
tmp := Cache
tmpC := config.CgrConfig()
defer func() {
guardian.Guardian = guardian.New()
Cache = tmp
config.SetCgrConfig(tmpC)
}()
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf, true); err != nil {
t.Error(err)
}
args := &utils.CGREvent{
ID: "TestGetThresholdsForEvent",
Event: map[string]any{
utils.AccountField: "1001",
},
}
exp := []*Threshold{
{
Tenant: "cgrates.org",
Hits: 0,
ID: "TH1",
tPrfl: thPrf,
dirty: utils.BoolPointer(false),
},
}
var reply []*Threshold
if err := tS.V1GetThresholdsForEvent(context.Background(), args, &reply); err != nil {
t.Error(err)
} else if !reflect.DeepEqual(reply, exp) {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", exp[0], reply[0])
}
}
func TestThresholdsV1GetThresholdsForEventMissingArgs(t *testing.T) {
tmp := Cache
tmpC := config.CgrConfig()
defer func() {
guardian.Guardian = guardian.New()
Cache = tmp
config.SetCgrConfig(tmpC)
}()
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf, true); err != nil {
t.Error(err)
}
var args *utils.CGREvent
experr := `MANDATORY_IE_MISSING: [CGREvent]`
var reply []*Threshold
if err := tS.V1GetThresholdsForEvent(context.Background(), args, &reply); err == nil ||
err.Error() != experr {
t.Error(err)
}
args = &utils.CGREvent{
Tenant: "cgrates.org",
Event: map[string]any{
utils.AccountField: "1001",
},
}
experr = `MANDATORY_IE_MISSING: [ID]`
if err := tS.V1GetThresholdsForEvent(context.Background(), args, &reply); err == nil ||
err.Error() != experr {
t.Error(err)
}
args = &utils.CGREvent{
Tenant: "cgrates.org",
ID: "TestGetThresholdsForEvent",
Event: nil,
}
experr = `MANDATORY_IE_MISSING: [Event]`
if err := tS.V1GetThresholdsForEvent(context.Background(), args, &reply); err == nil ||
err.Error() != experr {
t.Error(err)
}
}
func TestThresholdsV1GetThresholdIDsOK(t *testing.T) {
tmp := Cache
tmpC := config.CgrConfig()
defer func() {
guardian.Guardian = guardian.New()
Cache = tmp
config.SetCgrConfig(tmpC)
}()
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf1 := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf1, true); err != nil {
t.Error(err)
}
thPrf2 := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH2",
MinHits: 0,
MaxHits: 7,
Weights: utils.DynamicWeights{
{
Weight: 20,
},
},
Blocker: true,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf2, true); err != nil {
t.Error(err)
}
expIDs := []string{"TH1", "TH2"}
var reply []string
if err := tS.V1GetThresholdIDs(context.Background(), &utils.TenantWithAPIOpts{}, &reply); err != nil {
t.Error(err)
} else {
sort.Strings(reply)
if !reflect.DeepEqual(reply, expIDs) {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", expIDs, reply)
}
}
}
func TestThresholdsV1GetThresholdIDsGetKeysForPrefixErr(t *testing.T) {
tmp := Cache
tmpC := config.CgrConfig()
defer func() {
guardian.Guardian = guardian.New()
Cache = tmp
config.SetCgrConfig(tmpC)
}()
cfg := config.NewDefaultCGRConfig()
data := &DataDBMock{}
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
var reply []string
if err := tS.V1GetThresholdIDs(context.Background(), &utils.TenantWithAPIOpts{}, &reply); err == nil ||
err != utils.ErrNotImplemented {
t.Error(err)
}
}
func TestThresholdsV1ResetThresholdOK(t *testing.T) {
tmp := Cache
tmpC := config.CgrConfig()
defer func() {
guardian.Guardian = guardian.New()
Cache = tmp
config.SetCgrConfig(tmpC)
}()
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
th := &Threshold{
tPrfl: thPrf,
Tenant: "cgrates.org",
ID: "TH1",
Hits: 2,
dirty: utils.BoolPointer(false),
}
if err := dm.SetThreshold(context.Background(), th); err != nil {
t.Error(err)
}
expStored := utils.StringSet{
"cgrates.org:TH1": {},
}
var reply string
if err := tS.V1ResetThreshold(context.Background(), &utils.TenantIDWithAPIOpts{
TenantID: &utils.TenantID{
ID: "TH1",
},
}, &reply); err != nil {
t.Error(err)
} else if reply != utils.OK {
t.Errorf("Unexpected reply returned: <%q>", reply)
}
if x, ok := Cache.Get(utils.CacheThresholds, "cgrates.org:TH1"); !ok {
t.Errorf("not ok")
} else if x.(*Threshold).Hits != 0 {
t.Errorf("expected nr. of hits to be 0, received: <%+v>", x.(*Threshold).Hits)
} else if !reflect.DeepEqual(tS.storedTdIDs, expStored) {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", expStored, tS.storedTdIDs)
}
}
func TestThresholdsV1ResetThresholdErrNotFound(t *testing.T) {
tmp := Cache
tmpC := config.CgrConfig()
defer func() {
guardian.Guardian = guardian.New()
Cache = tmp
config.SetCgrConfig(tmpC)
}()
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH2",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
th := &Threshold{
tPrfl: thPrf,
Tenant: "cgrates.org",
ID: "TH2",
Hits: 2,
dirty: utils.BoolPointer(false),
}
if err := dm.SetThreshold(context.Background(), th); err != nil {
t.Error(err)
}
var reply string
if err := tS.V1ResetThreshold(context.Background(), &utils.TenantIDWithAPIOpts{
TenantID: &utils.TenantID{
ID: "TH1",
},
}, &reply); err == nil || err != utils.ErrNotFound {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", utils.ErrNotFound, err)
}
}
func TestThresholdsV1ResetThresholdNegativeStoreIntervalOK(t *testing.T) {
tmp := Cache
tmpC := config.CgrConfig()
defer func() {
Cache = tmp
config.SetCgrConfig(tmpC)
}()
cfg := config.NewDefaultCGRConfig()
cfg.ThresholdSCfg().StoreInterval = -1
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
th := &Threshold{
tPrfl: thPrf,
Tenant: "cgrates.org",
ID: "TH1",
Hits: 2,
dirty: utils.BoolPointer(false),
}
if err := dm.SetThreshold(context.Background(), th); err != nil {
t.Error(err)
}
Cache.Clear(nil)
var reply string
if err := tS.V1ResetThreshold(context.Background(), &utils.TenantIDWithAPIOpts{
TenantID: &utils.TenantID{
ID: "TH1",
},
}, &reply); err != nil {
t.Error(err)
} else if reply != utils.OK {
t.Errorf("Unexpected reply returned: <%q>", reply)
}
if gTH, err := dm.GetThreshold(context.Background(), th.Tenant, th.ID, false, false,
utils.NonTransactional); err != nil {
t.Error(err)
} else if gTH.Hits != 0 {
t.Errorf("expected nr. of hits to be 0, received: <%+v>", th.Hits)
}
}
func TestThresholdsV1ResetThresholdNegativeStoreIntervalErr(t *testing.T) {
tmp := Cache
tmpC := config.CgrConfig()
defer func() {
Cache = tmp
config.SetCgrConfig(tmpC)
}()
cfg := config.NewDefaultCGRConfig()
cfg.ThresholdSCfg().StoreInterval = -1
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(nil, cfg, filterS, nil)
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
th := &Threshold{
tPrfl: thPrf,
Tenant: "cgrates.org",
ID: "TH1",
Hits: 2,
dirty: utils.BoolPointer(false),
}
if err := dm.SetThreshold(context.Background(), th); err != nil {
t.Error(err)
}
var reply string
if err := tS.V1ResetThreshold(context.Background(), &utils.TenantIDWithAPIOpts{
TenantID: &utils.TenantID{
ID: "TH1",
},
}, &reply); err == nil || err != utils.ErrNoDatabaseConn {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", utils.ErrNoDatabaseConn, err)
}
}
func TestThresholdsLockUnlockThresholdProfiles(t *testing.T) {
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
MaxHits: 5,
MinHits: 2,
}
//lock profile with empty lkID parameter
thPrf.lock(utils.EmptyString)
if !thPrf.isLocked() {
t.Fatal("expected profile to be locked")
} else if thPrf.lkID == utils.EmptyString {
t.Fatal("expected struct field \"lkID\" to be non-empty")
}
//unlock previously locked profile
thPrf.unlock()
if thPrf.isLocked() {
t.Fatal("expected profile to be unlocked")
} else if thPrf.lkID != utils.EmptyString {
t.Fatal("expected struct field \"lkID\" to be empty")
}
//unlock an already unlocked profile - nothing happens
thPrf.unlock()
if thPrf.isLocked() {
t.Fatal("expected profile to be unlocked")
} else if thPrf.lkID != utils.EmptyString {
t.Fatal("expected struct field \"lkID\" to be empty")
}
}
func TestThresholdsLockUnlockThresholds(t *testing.T) {
th := &Threshold{
Tenant: "cgrates.org",
ID: "TH1",
}
//lock resource with empty lkID parameter
th.lock(utils.EmptyString)
if !th.isLocked() {
t.Fatal("expected resource to be locked")
} else if th.lkID == utils.EmptyString {
t.Fatal("expected struct field \"lkID\" to be non-empty")
}
//unlock previously locked resource
th.unlock()
if th.isLocked() {
t.Fatal("expected resource to be unlocked")
} else if th.lkID != utils.EmptyString {
t.Fatal("expected struct field \"lkID\" to be empty")
}
//unlock an already unlocked resource - nothing happens
th.unlock()
if th.isLocked() {
t.Fatal("expected resource to be unlocked")
} else if th.lkID != utils.EmptyString {
t.Fatal("expected struct field \"lkID\" to be empty")
}
}
func TestThresholdsMatchingThresholdsForEventNotFoundErr(t *testing.T) {
tmp := Cache
tmpC := config.CgrConfig()
defer func() {
Cache = tmp
config.SetCgrConfig(tmpC)
}()
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf1 := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf1, true); err != nil {
t.Error(err)
}
args := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "TestEvent",
Event: map[string]any{},
}
if _, err := tS.matchingThresholdsForEvent(context.Background(), "cgrates.org", args); err == nil ||
err != utils.ErrNotFound {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", utils.ErrNotFound, err)
}
}
func TestThresholdsStoreThresholdCacheSetErr(t *testing.T) {
tmpLogger := utils.Logger
var buf bytes.Buffer
utils.Logger = utils.NewStdLoggerWithWriter(&buf, "", 4)
tmp := Cache
tmpC := config.CgrConfig()
tmpCM := connMgr
defer func() {
Cache = tmp
config.SetCgrConfig(tmpC)
connMgr = tmpCM
utils.Logger = tmpLogger
}()
cfg := config.NewDefaultCGRConfig()
cfg.CacheCfg().ReplicationConns = []string{"test"}
cfg.CacheCfg().Partitions[utils.CacheThresholds].Replicate = true
cfg.RPCConns()["test"] = &config.RPCConn{Conns: []*config.RemoteHost{{}}}
config.SetCgrConfig(cfg)
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
connMgr = NewConnManager(cfg)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, connMgr)
th := &Threshold{
Tenant: "cgrates.org",
ID: "TH1",
dirty: utils.BoolPointer(true),
}
Cache.SetWithoutReplicate(utils.CacheThresholds, th.TenantID(), th, nil, true, utils.NonTransactional)
expLog := `[WARNING] <ThresholdService> failed caching Threshold with ID: cgrates.org:TH1, error: DISCONNECTED`
if err := tS.StoreThreshold(context.Background(), th); err == nil ||
err.Error() != utils.ErrDisconnected.Error() {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", utils.ErrDisconnected, err)
} else if rcv := buf.String(); !strings.Contains(rcv, expLog) {
t.Errorf("expected log <%+v> to be included in <%+v>", expLog, rcv)
}
}
func TestThreholdsMatchingThresholdsForEventDoesNotPass(t *testing.T) {
tmp := Cache
tmpC := config.CgrConfig()
defer func() {
Cache = tmp
config.SetCgrConfig(tmpC)
}()
cfg := config.NewDefaultCGRConfig()
config.SetCgrConfig(cfg)
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
thPrf1 := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH1",
FilterIDs: []string{"*string:~*req.Account:1001"},
MinHits: 2,
MaxHits: 5,
Weights: utils.DynamicWeights{
{
Weight: 10,
},
},
Blocker: true,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf1, true); err != nil {
t.Error(err)
}
args := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "TestEvent",
Event: map[string]any{
utils.AccountField: "1002",
},
APIOpts: map[string]any{
utils.OptsThresholdsProfileIDs: []string{"TH1"},
},
}
if _, err := tS.matchingThresholdsForEvent(context.Background(), "cgrates.org",
args); err == nil || err.Error() != utils.ErrNotFound.Error() {
t.Errorf("expected: <%+v>, received: <%+v>", utils.ErrNotFound, err)
}
}
func TestThresholdsProcessEventIgnoreFilters(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
cfg.ThresholdSCfg().Opts.ProfileIgnoreFilters = []*config.DynamicBoolOpt{
config.NewDynamicBoolOpt(nil, "", true, nil),
}
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH",
FilterIDs: []string{"*string:~*req.Threshold:testThresholdValue"},
}
th := &Threshold{
Tenant: "cgrates.org",
ID: "TH",
tPrfl: thPrf,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf, true); err != nil {
t.Error(err)
}
if err := dm.SetThreshold(context.Background(), th); err != nil {
t.Error(err)
}
// testing if the profile matches wtih profile ignore filters on false
args := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "ThdProcessEvent",
Event: map[string]any{
"Threshold": "testThresholdValue",
},
APIOpts: map[string]any{
utils.OptsThresholdsProfileIDs: []string{"TH"},
utils.MetaProfileIgnoreFilters: false,
},
}
exp := []string{"TH"}
if rcv, err := tS.processEvent(context.Background(), args.Tenant, args); err != nil {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", nil, err)
} else if !reflect.DeepEqual(rcv, exp) {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", exp, rcv)
}
// testing if the profile matches with wtih profile ignore filters on true
args2 := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "ThdProcessEvent",
Event: map[string]any{
"Threshold": "testThresholdValue2",
},
APIOpts: map[string]any{
utils.OptsThresholdsProfileIDs: []string{"TH"},
utils.MetaProfileIgnoreFilters: true,
},
}
exp2 := []string{"TH"}
if rcv2, err := tS.processEvent(context.Background(), args2.Tenant, args2); err != nil {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", nil, err)
} else if !reflect.DeepEqual(rcv2, exp) {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", exp2, rcv2)
}
}
func TestThresholdsProcessEventIgnoreFiltersErr(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, nil)
cfg.ThresholdSCfg().Opts.ProfileIgnoreFilters = []*config.DynamicBoolOpt{
config.NewDynamicBoolOpt(nil, "", true, nil),
}
thPrf := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "TH",
FilterIDs: []string{"*string:~*req.Threshold:testThresholdValue"},
}
th := &Threshold{
Tenant: "cgrates.org",
ID: "TH",
tPrfl: thPrf,
}
if err := dm.SetThresholdProfile(context.Background(), thPrf, true); err != nil {
t.Error(err)
}
if err := dm.SetThreshold(context.Background(), th); err != nil {
t.Error(err)
}
args := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "ThdProcessEvent",
Event: map[string]any{
"Threshold": "testThresholdValue",
},
APIOpts: map[string]any{
utils.OptsThresholdsProfileIDs: []string{"TH"},
utils.MetaProfileIgnoreFilters: time.Second,
},
}
if _, err := tS.processEvent(context.Background(), args.Tenant, args); err == nil || err.Error() != "cannot convert field: 1s to bool" {
t.Errorf("expected: <%+v>, \nreceived: <%+v>", nil, err)
}
}
func TestThresholdProfileSet(t *testing.T) {
th := ThresholdProfile{}
exp := ThresholdProfile{
Tenant: "cgrates.org",
ID: "ID",
FilterIDs: []string{"fltr1", "*string:~*req.Account:1001"},
Weights: utils.DynamicWeights{
{
Weight: 10.0,
},
},
MaxHits: 10,
MinHits: 10,
MinSleep: 10,
Blocker: true,
Async: true,
ActionProfileIDs: []string{"acc1"},
}
if err := th.Set([]string{}, "", false); err != utils.ErrWrongPath {
t.Error(err)
}
if err := th.Set([]string{"NotAField"}, "", false); err != utils.ErrWrongPath {
t.Error(err)
}
if err := th.Set([]string{"NotAField", "1"}, "", false); err != utils.ErrWrongPath {
t.Error(err)
}
if err := th.Set([]string{utils.Tenant}, "cgrates.org", false); err != nil {
t.Error(err)
}
if err := th.Set([]string{utils.ID}, "ID", false); err != nil {
t.Error(err)
}
if err := th.Set([]string{utils.FilterIDs}, "fltr1;*string:~*req.Account:1001", false); err != nil {
t.Error(err)
}
if err := th.Set([]string{utils.Weights}, ";10", false); err != nil {
t.Error(err)
}
if err := th.Set([]string{utils.MaxHits}, 10, false); err != nil {
t.Error(err)
}
if err := th.Set([]string{utils.MinHits}, 10, false); err != nil {
t.Error(err)
}
if err := th.Set([]string{utils.MinSleep}, 10, false); err != nil {
t.Error(err)
}
if err := th.Set([]string{utils.Blocker}, true, false); err != nil {
t.Error(err)
}
if err := th.Set([]string{utils.Async}, true, false); err != nil {
t.Error(err)
}
if err := th.Set([]string{utils.ActionProfileIDs}, "acc1", false); err != nil {
t.Error(err)
}
if !reflect.DeepEqual(exp, th) {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(th))
}
}
func TestThresholdProfileAsInterface(t *testing.T) {
tp := ThresholdProfile{
Tenant: "cgrates.org",
ID: "ID",
FilterIDs: []string{"fltr1", "*string:~*req.Account:1001"},
Weights: utils.DynamicWeights{
{
Weight: 10.0,
},
},
MaxHits: 10,
MinHits: 10,
MinSleep: 10,
Blocker: true,
Async: true,
ActionProfileIDs: []string{"acc1"},
}
if _, err := tp.FieldAsInterface(nil); err != utils.ErrNotFound {
t.Fatal(err)
}
if _, err := tp.FieldAsInterface([]string{"field"}); err != utils.ErrNotFound {
t.Fatal(err)
}
if _, err := tp.FieldAsInterface([]string{"field", ""}); err != utils.ErrNotFound {
t.Fatal(err)
}
if val, err := tp.FieldAsInterface([]string{utils.Tenant}); err != nil {
t.Fatal(err)
} else if exp := "cgrates.org"; exp != val {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if val, err := tp.FieldAsInterface([]string{utils.ID}); err != nil {
t.Fatal(err)
} else if exp := utils.ID; exp != val {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if val, err := tp.FieldAsInterface([]string{utils.FilterIDs}); err != nil {
t.Fatal(err)
} else if exp := tp.FilterIDs; !reflect.DeepEqual(exp, val) {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if val, err := tp.FieldAsInterface([]string{utils.FilterIDs + "[0]"}); err != nil {
t.Fatal(err)
} else if exp := tp.FilterIDs[0]; exp != val {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if val, err := tp.FieldAsInterface([]string{utils.Weights}); err != nil {
t.Fatal(err)
} else if exp := tp.Weights; !reflect.DeepEqual(exp, val) {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if val, err := tp.FieldAsInterface([]string{utils.ActionProfileIDs}); err != nil {
t.Fatal(err)
} else if exp := tp.ActionProfileIDs; !reflect.DeepEqual(exp, val) {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if val, err := tp.FieldAsInterface([]string{utils.ActionProfileIDs + "[0]"}); err != nil {
t.Fatal(err)
} else if exp := tp.ActionProfileIDs[0]; exp != val {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if val, err := tp.FieldAsInterface([]string{utils.MaxHits}); err != nil {
t.Fatal(err)
} else if exp := tp.MaxHits; !reflect.DeepEqual(exp, val) {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if val, err := tp.FieldAsInterface([]string{utils.MinHits}); err != nil {
t.Fatal(err)
} else if exp := tp.MinHits; !reflect.DeepEqual(exp, val) {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if val, err := tp.FieldAsInterface([]string{utils.MinSleep}); err != nil {
t.Fatal(err)
} else if exp := tp.MinSleep; !reflect.DeepEqual(exp, val) {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if val, err := tp.FieldAsInterface([]string{utils.Blocker}); err != nil {
t.Fatal(err)
} else if exp := tp.Blocker; !reflect.DeepEqual(exp, val) {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if val, err := tp.FieldAsInterface([]string{utils.Async}); err != nil {
t.Fatal(err)
} else if exp := tp.Async; !reflect.DeepEqual(exp, val) {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if _, err := tp.FieldAsString([]string{""}); err != utils.ErrNotFound {
t.Fatal(err)
}
if val, err := tp.FieldAsString([]string{utils.ID}); err != nil {
t.Fatal(err)
} else if exp := "ID"; exp != val {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
if val, exp := tp.String(), utils.ToJSON(tp); exp != val {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(val))
}
}
func TestThresholdProfileMerge(t *testing.T) {
dp := &ThresholdProfile{}
exp := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "ID",
FilterIDs: []string{"fltr1", "*string:~*req.Account:1001"},
Weights: utils.DynamicWeights{
{
Weight: 10.0,
},
},
MaxHits: 10,
MinHits: 10,
MinSleep: 10,
Blocker: true,
Async: true,
ActionProfileIDs: []string{"acc1"},
}
if dp.Merge(&ThresholdProfile{
Tenant: "cgrates.org",
ID: "ID",
FilterIDs: []string{"fltr1", "*string:~*req.Account:1001"},
Weights: utils.DynamicWeights{
{
Weight: 10.0,
},
},
MaxHits: 10,
MinHits: 10,
MinSleep: 10,
Blocker: true,
Async: true,
ActionProfileIDs: []string{"acc1"},
}); !reflect.DeepEqual(exp, dp) {
t.Errorf("Expected %v \n but received \n %v", utils.ToJSON(exp), utils.ToJSON(dp))
}
}
func TestThresholdSmatchingThresholdsForEventGetOptsErr(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
cfg.ThresholdSCfg().Opts.ProfileIDs = []*config.DynamicStringSliceOpt{
{
FilterIDs: []string{"*string"},
Tenant: "cgrates.org",
Values: []string{"ProfIdVal"},
},
}
dataDB, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
cM := NewConnManager(cfg)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, cM)
filterS := NewFilterS(cfg, cM, dm)
tS := &ThresholdS{
dm: dm,
storedTdIDs: utils.StringSet{
"Th1": struct{}{},
},
cfg: cfg,
fltrS: filterS,
loopStopped: make(chan struct{}, 1),
stopBackup: make(chan struct{}),
}
args := &utils.CGREvent{
ID: "TestV1DebitID",
Tenant: "cgrates.org",
Event: map[string]any{
utils.AccountField: "1004",
},
APIOpts: map[string]any{
utils.MetaUsage: "3m",
},
}
expErr := "inline parse error for string: <*string>"
if _, err := tS.matchingThresholdsForEvent(context.Background(), args.Tenant, args); err.Error() != expErr || err == nil {
t.Errorf("Expected error <%v>, Received <%v>", expErr, err)
}
}
func TestThresholdSmatchingThresholdsForEventWeightErr(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
cfg.ThresholdSCfg().Opts.ProfileIDs = []*config.DynamicStringSliceOpt{
{
Tenant: "cgrates.org",
Values: []string{"ACC1"},
},
}
cfg.ThresholdSCfg().Opts.ProfileIgnoreFilters = []*config.DynamicBoolOpt{
config.NewDynamicBoolOpt(nil, "cgrates.org", true, nil),
}
dataDB, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
cM := NewConnManager(cfg)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: dataDB}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, cM)
dm.dbConns.dataDBs[utils.MetaDefault] = &DataDBMock{
GetThresholdProfileDrvF: func(ctx *context.Context, tenant, id string) (tp *ThresholdProfile, err error) {
return &ThresholdProfile{
Tenant: "cgrates.org",
ID: "THD_2",
FilterIDs: []string{"*string:~*req.Account:1001"},
ActionProfileIDs: []string{"actPrfID"},
MaxHits: 7,
MinHits: 0,
Weights: []*utils.DynamicWeight{
{
FilterIDs: []string{"*stirng:~*req.Account:1001"},
Weight: 64,
},
},
Async: true,
}, nil
},
GetThresholdDrvF: func(ctx *context.Context, tenant, id string) (*Threshold, error) { return &Threshold{}, nil },
}
filterS := NewFilterS(cfg, cM, dm)
tS := &ThresholdS{
dm: dm,
storedTdIDs: utils.StringSet{
"Th1": struct{}{},
},
cfg: cfg,
fltrS: filterS,
loopStopped: make(chan struct{}, 1),
stopBackup: make(chan struct{}),
}
args := &utils.CGREvent{
ID: "EvID",
Tenant: "cgrates.org",
Event: map[string]any{
utils.AccountField: "1004",
},
APIOpts: map[string]any{
utils.MetaUsage: "3m",
},
}
expErr := "NOT_IMPLEMENTED:*stirng"
if _, err := tS.matchingThresholdsForEvent(context.Background(), args.Tenant, args); err.Error() != expErr || err == nil {
t.Errorf("Expected error <%v>, Received <%v>", expErr, err)
}
}
func TestThresholdsV1ResetThresholdStoreErr(t *testing.T) {
tmp := Cache
tmpC := config.CgrConfig()
tmpCM := connMgr
defer func() {
Cache = tmp
config.SetCgrConfig(tmpC)
connMgr = tmpCM
}()
cfg := config.NewDefaultCGRConfig()
cfg.ThresholdSCfg().StoreInterval = -1
cfg.CacheCfg().ReplicationConns = []string{"test"}
cfg.CacheCfg().Partitions[utils.CacheThresholds].Replicate = true
cfg.RPCConns()["test"] = &config.RPCConn{Conns: []*config.RemoteHost{{}}}
config.SetCgrConfig(cfg)
data, _ := NewInternalDB(nil, nil, nil, cfg.DbCfg().Items)
dbCM := NewDBConnManager(map[string]DataDB{utils.MetaDefault: data}, cfg.DbCfg())
dm := NewDataManager(dbCM, cfg, nil)
connMgr = NewConnManager(cfg)
Cache = NewCacheS(cfg, dm, nil, nil)
filterS := NewFilterS(cfg, nil, dm)
tS := NewThresholdService(dm, cfg, filterS, connMgr)
th := &Threshold{
Hits: 2,
Tenant: "cgrates.org",
ID: "TH1",
dirty: utils.BoolPointer(true),
}
if err := dm.SetThreshold(context.Background(), th); err != nil {
t.Error(err)
}
Cache.SetWithoutReplicate(utils.CacheThresholds, th.TenantID(), th, nil, true, utils.NonTransactional)
var reply string
if err := tS.V1ResetThreshold(context.Background(), &utils.TenantIDWithAPIOpts{
TenantID: &utils.TenantID{
ID: "TH1",
},
}, &reply); err == nil || err.Error() != utils.ErrDisconnected.Error() {
t.Errorf("Expected error <%+v>, Received error <%+v>", utils.ErrDisconnected, err)
}
}
func TestThresholdsProfileSet(t *testing.T) {
tests := []struct {
name string
path []string
val any
expectErr bool
}{
{
name: "Set Tenant",
path: []string{utils.Tenant},
val: "cgrates.org",
expectErr: false,
},
{
name: "Set ID",
path: []string{utils.ID},
val: "TH001",
expectErr: false,
},
{
name: "Set Blocker true",
path: []string{utils.Blocker},
val: true,
expectErr: false,
},
{
name: "Set Async true",
path: []string{utils.Async},
val: "true",
expectErr: false,
},
{
name: "Set MaxHits",
path: []string{utils.MaxHits},
val: 99,
expectErr: false,
},
{
name: "Set MinHits",
path: []string{utils.MinHits},
val: "10",
expectErr: false,
},
{
name: "Set MinSleep",
path: []string{utils.MinSleep},
val: "2s",
expectErr: false,
},
{
name: "Append FilterIDs",
path: []string{utils.FilterIDs},
val: []string{"flt1", "flt2"},
expectErr: false,
},
{
name: "Append ActionProfileIDs",
path: []string{utils.ActionProfileIDs},
val: []string{"act1"},
expectErr: false,
},
{
name: "Append EeIDs",
path: []string{utils.EeIDs},
val: []string{"ee1"},
expectErr: false,
},
{
name: "Invalid path",
path: []string{"Invalid"},
val: "test",
expectErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tp := &ThresholdProfile{}
err := tp.Set(tc.path, tc.val, false)
if (err != nil) != tc.expectErr {
t.Errorf("expected error=%v, got %v", tc.expectErr, err)
}
})
}
}
func TestThresholdProfileClone(t *testing.T) {
orig := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "THRESHOLD_TEST",
FilterIDs: []string{"*string:~*req.Account:1001", "*string:~*req.Subject:1001"},
MaxHits: 100,
MinHits: 5,
MinSleep: 10 * time.Second,
Blocker: true,
Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 20.0}},
ActionProfileIDs: []string{"ACT_LOG", "ACT_MAIL", "ACT_CDR"},
Async: true,
EeIDs: []string{"EE1", "EE2"},
lkID: "lkID",
}
cloned := orig.Clone()
if cloned == nil {
t.Errorf("Expected cloned ThresholdProfile, got nil")
return
}
if cloned.Tenant != orig.Tenant {
t.Errorf("Tenant mismatch: got %s, want %s", cloned.Tenant, orig.Tenant)
}
if cloned.ID != orig.ID {
t.Errorf("ID mismatch: got %s, want %s", cloned.ID, orig.ID)
}
if cloned.MaxHits != orig.MaxHits {
t.Errorf("MaxHits mismatch: got %d, want %d", cloned.MaxHits, orig.MaxHits)
}
if cloned.MinHits != orig.MinHits {
t.Errorf("MinHits mismatch: got %d, want %d", cloned.MinHits, orig.MinHits)
}
if cloned.MinSleep != orig.MinSleep {
t.Errorf("MinSleep mismatch: got %v, want %v", cloned.MinSleep, orig.MinSleep)
}
if cloned.Blocker != orig.Blocker {
t.Errorf("Blocker mismatch: got %v, want %v", cloned.Blocker, orig.Blocker)
}
if cloned.Async != orig.Async {
t.Errorf("Async mismatch: got %v, want %v", cloned.Async, orig.Async)
}
if !reflect.DeepEqual(cloned.FilterIDs, orig.FilterIDs) {
t.Errorf("FilterIDs mismatch: got %v, want %v", cloned.FilterIDs, orig.FilterIDs)
}
if !reflect.DeepEqual(cloned.ActionProfileIDs, orig.ActionProfileIDs) {
t.Errorf("ActionProfileIDs mismatch: got %v, want %v", cloned.ActionProfileIDs, orig.ActionProfileIDs)
}
if !reflect.DeepEqual(cloned.Weights, orig.Weights) {
t.Errorf("Weights mismatch: got %v, want %v", cloned.Weights, orig.Weights)
}
if len(cloned.Weights) > 0 && cloned.Weights[0] == orig.Weights[0] {
t.Errorf("Weights not deep copied")
}
if cloned.lkID != "" {
t.Errorf("lkID should not be cloned, got: %s", cloned.lkID)
}
if !reflect.DeepEqual(cloned.EeIDs, orig.EeIDs) {
t.Errorf("EeIDs mismatch: got %v, want %v", cloned.EeIDs, orig.EeIDs)
}
var nilTP *ThresholdProfile
nilClone := nilTP.Clone()
if nilClone != nil {
t.Errorf("Expected nil from Clone on nil receiver, got: %+v", nilClone)
}
}
func TestThresholdClone(t *testing.T) {
origDirty := true
orig := &Threshold{
Tenant: "cgrates.org",
ID: "TH001",
Hits: 42,
Snooze: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC),
dirty: &origDirty,
tPrfl: &ThresholdProfile{
Tenant: "cgrates.org",
ID: "THRESHOLD_TEST",
FilterIDs: []string{"*string:~*req.Account:1001", "*string:~*req.Subject:1001"},
MaxHits: 100,
MinHits: 5,
MinSleep: 10 * time.Second,
Blocker: true,
Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 20.0}},
ActionProfileIDs: []string{"ACT_LOG", "ACT_MAIL", "ACT_CDR"},
Async: true,
EeIDs: []string{"EE1", "EE2"},
lkID: "lkID",
},
}
cloned := orig.Clone()
if cloned == nil {
t.Errorf("Expected non-nil clone")
return
}
if cloned.Tenant != orig.Tenant {
t.Errorf("Tenant mismatch: got %s, want %s", cloned.Tenant, orig.Tenant)
}
if cloned.ID != orig.ID {
t.Errorf("ID mismatch: got %s, want %s", cloned.ID, orig.ID)
}
if cloned.Hits != orig.Hits {
t.Errorf("Hits mismatch: got %d, want %d", cloned.Hits, orig.Hits)
}
if cloned.Snooze != orig.Snooze {
t.Errorf("Snooze mismatch: got %v, want %v", cloned.Snooze, orig.Snooze)
}
if cloned.tPrfl == nil {
t.Errorf("Expected tPrfl to be cloned, got nil")
} else if cloned.tPrfl == orig.tPrfl {
t.Errorf("tPrfl not deep cloned")
} else if cloned.tPrfl.ID != orig.tPrfl.ID {
t.Errorf("tPrfl.ID mismatch: got %s, want %s", cloned.tPrfl.ID, orig.tPrfl.ID)
}
if cloned.dirty == nil {
t.Errorf("Expected dirty to be cloned, got nil")
} else if cloned.dirty == orig.dirty {
t.Errorf("dirty not deep copied")
} else if *cloned.dirty != *orig.dirty {
t.Errorf("dirty value mismatch: got %v, want %v", *cloned.dirty, *orig.dirty)
}
var nilThreshold *Threshold
nilClone := nilThreshold.Clone()
if nilClone != nil {
t.Errorf("Expected nil from Clone on nil receiver, got: %+v", nilClone)
}
}
func TestThresholdProfileFieldAsInterface(t *testing.T) {
tp := &ThresholdProfile{
Tenant: "cgrates.org",
ID: "THRESHOLD_TEST",
FilterIDs: []string{"flt1", "flt2"},
MaxHits: 100,
MinHits: 10,
MinSleep: 5 * time.Second,
Blocker: true,
Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 1.23}},
ActionProfileIDs: []string{"act1", "act2"},
Async: true,
EeIDs: []string{"ee1", "ee2"},
}
tests := []struct {
name string
path []string
want any
wantErr bool
}{
{
name: "Tenant",
path: []string{utils.Tenant},
want: "cgrates.org",
wantErr: false,
},
{
name: "ID",
path: []string{utils.ID},
want: "THRESHOLD_TEST",
wantErr: false,
},
{
name: "FilterIDs",
path: []string{utils.FilterIDs},
want: []string{"flt1", "flt2"},
wantErr: false,
},
{
name: "FilterIDs[1]",
path: []string{"FilterIDs[1]"},
want: "flt2",
wantErr: false,
},
{
name: "ActionProfileIDs",
path: []string{utils.ActionProfileIDs},
want: []string{"act1", "act2"},
wantErr: false,
},
{
name: "ActionProfileIDs[0]",
path: []string{"ActionProfileIDs[0]"},
want: "act1",
wantErr: false,
},
{
name: "EeIDs",
path: []string{utils.EeIDs},
want: []string{"ee1", "ee2"},
wantErr: false,
},
{
name: "EeIDs[1]",
path: []string{"EeIDs[1]"},
want: "ee2",
wantErr: false,
},
{
name: "MaxHits",
path: []string{utils.MaxHits},
want: 100,
wantErr: false,
},
{
name: "MinHits",
path: []string{utils.MinHits},
want: 10,
wantErr: false,
},
{
name: "MinSleep",
path: []string{utils.MinSleep},
want: 5 * time.Second,
wantErr: false,
},
{
name: "Blocker",
path: []string{utils.Blocker},
want: true,
wantErr: false,
},
{
name: "Async",
path: []string{utils.Async},
want: true,
wantErr: false,
},
{
name: "Weights",
path: []string{utils.Weights},
want: tp.Weights,
wantErr: false,
},
{
name: "Unknown field",
path: []string{"UnknownField"},
want: nil,
wantErr: true,
},
{
name: "Out of range index",
path: []string{"EeIDs[10]"},
want: nil,
wantErr: true,
},
{
name: "Invalid path length",
path: []string{"FilterIDs", "Extra"},
want: nil,
wantErr: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := tp.FieldAsInterface(tc.path)
if (err != nil) != tc.wantErr {
t.Errorf("Expected error: %v, got: %v", tc.wantErr, err)
return
}
if !tc.wantErr && !reflect.DeepEqual(got, tc.want) {
t.Errorf("Got value %v (%T), expected %v (%T)", got, got, tc.want, tc.want)
}
})
}
}