/* Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments Copyright (C) ITsysCOM GmbH This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see */ package engine import ( "bytes" "encoding/json" "errors" "fmt" "io" "log" "net/http" "net/http/httptest" "os" "reflect" "strings" "testing" "time" "github.com/cgrates/birpc" "github.com/cgrates/birpc/context" "github.com/cgrates/cgrates/config" "github.com/cgrates/ltcache" "github.com/cgrates/cgrates/utils" "github.com/cgrates/rpcclient" ) var ( err error //referenceDate = time.Date(2013, 7, 10, 10, 30, 0, 0, time.Local) //referenceDate = time.Date(2013, 12, 31, 23, 59, 59, 0, time.Local) //referenceDate = time.Date(2011, 1, 1, 0, 0, 0, 1, time.Local) referenceDate = time.Now() now = referenceDate ) func TestActionTimingAlways(t *testing.T) { at := &ActionTiming{Timing: &RateInterval{Timing: &RITiming{StartTime: "00:00:00"}}} st := at.GetNextStartTime(referenceDate) y, m, d := referenceDate.Date() expected := time.Date(y, m, d, 0, 0, 0, 0, time.Local).AddDate(0, 0, 1) if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanNothing(t *testing.T) { at := &ActionTiming{} st := at.GetNextStartTime(referenceDate) expected := time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC) if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionTimingMidnight(t *testing.T) { at := &ActionTiming{Timing: &RateInterval{Timing: &RITiming{StartTime: "00:00:00"}}} y, m, d := referenceDate.Date() now := time.Date(y, m, d, 0, 0, 1, 0, time.Local) st := at.GetNextStartTime(now) expected := time.Date(y, m, d, 0, 0, 0, 0, time.Local).AddDate(0, 0, 1) if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanOnlyHour(t *testing.T) { at := &ActionTiming{Timing: &RateInterval{Timing: &RITiming{StartTime: "10:01:00"}}} st := at.GetNextStartTime(referenceDate) y, m, d := now.Date() expected := time.Date(y, m, d, 10, 1, 0, 0, time.Local) if referenceDate.After(expected) { expected = expected.AddDate(0, 0, 1) } if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanHourYear(t *testing.T) { at := &ActionTiming{Timing: &RateInterval{Timing: &RITiming{Years: utils.Years{2035}, StartTime: "10:01:00"}}} st := at.GetNextStartTime(referenceDate) expected := time.Date(2035, 1, 1, 10, 1, 0, 0, time.Local) if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanOnlyWeekdays(t *testing.T) { at := &ActionTiming{Timing: &RateInterval{Timing: &RITiming{WeekDays: []time.Weekday{time.Monday}}}} st := at.GetNextStartTime(referenceDate) y, m, d := now.Date() h, min, s := now.Clock() e := time.Date(y, m, d, h, min, s, 0, time.Local) day := e.Day() e = time.Date(e.Year(), e.Month(), day, 0, 0, 0, 0, e.Location()) for i := 0; i < 8; i++ { n := e.AddDate(0, 0, i) if n.Weekday() == time.Monday && (n.Equal(now) || n.After(now)) { e = n break } } if !st.Equal(e) { t.Errorf("Expected %v was %v", e, st) } } func TestActionPlanHourWeekdays(t *testing.T) { at := &ActionTiming{Timing: &RateInterval{Timing: &RITiming{ WeekDays: []time.Weekday{time.Monday}, StartTime: "10:01:00"}}} st := at.GetNextStartTime(referenceDate) y, m, d := now.Date() e := time.Date(y, m, d, 10, 1, 0, 0, time.Local) day := e.Day() for i := 0; i < 8; i++ { e = time.Date(e.Year(), e.Month(), day, e.Hour(), e.Minute(), e.Second(), e.Nanosecond(), e.Location()) n := e.AddDate(0, 0, i) if n.Weekday() == time.Monday && (n.Equal(now) || n.After(now)) { e = n break } } if !st.Equal(e) { t.Errorf("Expected %v was %v", e, st) } } func TestActionPlanOnlyMonthdays(t *testing.T) { y, m, d := now.Date() tomorrow := time.Date(y, m, d, 0, 0, 0, 0, time.Local).AddDate(0, 0, 1) at := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{MonthDays: utils.MonthDays{1, 25, 2, tomorrow.Day()}}}} st := at.GetNextStartTime(referenceDate) expected := tomorrow if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanHourMonthdays(t *testing.T) { y, m, d := now.Date() testTime := time.Date(y, m, d, 10, 1, 0, 0, time.Local) tomorrow := time.Date(y, m, d, 0, 0, 0, 0, time.Local).AddDate(0, 0, 1) if now.After(testTime) { y, m, d = tomorrow.Date() } at := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{MonthDays: utils.MonthDays{now.Day(), tomorrow.Day()}, StartTime: "10:01:00"}}} st := at.GetNextStartTime(referenceDate) expected := time.Date(y, m, d, 10, 1, 0, 0, time.Local) if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanOnlyMonths(t *testing.T) { y, m, _ := now.Date() nextMonth := time.Date(y, m, 1, 0, 0, 0, 0, time.Local).AddDate(0, 1, 0) at := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{Months: utils.Months{time.February, time.May, nextMonth.Month()}}}} st := at.GetNextStartTime(referenceDate) expected := time.Date(nextMonth.Year(), nextMonth.Month(), 1, 0, 0, 0, 0, time.Local) if !st.Equal(expected) { t.Log("NextMonth: ", nextMonth) t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanHourMonths(t *testing.T) { y, m, d := now.Date() testTime := time.Date(y, m, d, 10, 1, 0, 0, time.Local) nextMonth := time.Date(y, m, 1, 0, 0, 0, 0, time.Local).AddDate(0, 1, 0) if now.After(testTime) { testTime = testTime.AddDate(0, 0, 1) y, m, _ = testTime.Date() } if now.After(testTime) { m = nextMonth.Month() y = nextMonth.Year() } at := &ActionTiming{Timing: &RateInterval{Timing: &RITiming{ Months: utils.Months{now.Month(), nextMonth.Month()}, StartTime: "10:01:00"}}} st := at.GetNextStartTime(referenceDate) expected := time.Date(y, m, 1, 10, 1, 0, 0, time.Local) if referenceDate.After(expected) { expected = expected.AddDate(0, 1, 0) } if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanHourMonthdaysMonths(t *testing.T) { y, m, d := now.Date() testTime := time.Date(y, m, d, 10, 1, 0, 0, time.Local) nextMonth := time.Date(y, m, 1, 0, 0, 0, 0, time.Local).AddDate(0, 1, 0) tomorrow := time.Date(y, m, d, 0, 0, 0, 0, time.Local).AddDate(0, 0, 1) if now.After(testTime) { y, m, d = tomorrow.Date() } nextDay := time.Date(y, m, d, 10, 1, 0, 0, time.Local) month := nextDay.Month() if nextDay.Before(now) { if now.After(testTime) { month = nextMonth.Month() } } at := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{ Months: utils.Months{now.Month(), nextMonth.Month()}, MonthDays: utils.MonthDays{now.Day(), tomorrow.Day()}, StartTime: "10:01:00", }, }} st := at.GetNextStartTime(referenceDate) expected := time.Date(y, month, d, 10, 1, 0, 0, time.Local) if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanFirstOfTheMonth(t *testing.T) { y, m, _ := now.Date() nextMonth := time.Date(y, m, 1, 0, 0, 0, 0, time.Local).AddDate(0, 1, 0) at := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{ MonthDays: utils.MonthDays{1}, }, }} st := at.GetNextStartTime(referenceDate) expected := nextMonth if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanOnlyYears(t *testing.T) { y, _, _ := referenceDate.Date() nextYear := time.Date(y, 1, 1, 0, 0, 0, 0, time.Local).AddDate(1, 0, 0) at := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{Years: utils.Years{now.Year(), nextYear.Year()}}}} st := at.GetNextStartTime(referenceDate) expected := nextYear if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanPast(t *testing.T) { at := &ActionTiming{Timing: &RateInterval{Timing: &RITiming{Years: utils.Years{2028}}}} st := at.GetNextStartTime(referenceDate) expected := time.Date(2028, 1, 1, 0, 0, 0, 0, time.Local) if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanHourYears(t *testing.T) { at := &ActionTiming{Timing: &RateInterval{Timing: &RITiming{ Years: utils.Years{referenceDate.Year(), referenceDate.Year() + 1}, StartTime: "10:01:00"}}} st := at.GetNextStartTime(referenceDate) expected := time.Date(referenceDate.Year(), 1, 1, 10, 1, 0, 0, time.Local) if referenceDate.After(expected) { expected = expected.AddDate(1, 0, 0) } if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanHourMonthdaysYear(t *testing.T) { y, m, d := now.Date() testTime := time.Date(y, m, d, 10, 1, 0, 0, time.Local) tomorrow := time.Date(y, m, d, 10, 1, 0, 0, time.Local).AddDate(0, 0, 1) nextYear := time.Date(y, 1, d, 10, 1, 0, 0, time.Local).AddDate(1, 0, 0) expected := testTime if referenceDate.After(testTime) { if referenceDate.After(tomorrow) { expected = nextYear } else { expected = tomorrow } } at := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{ Years: utils.Years{now.Year(), nextYear.Year()}, MonthDays: utils.MonthDays{now.Day(), tomorrow.Day()}, StartTime: "10:01:00", }, }} t.Log(at.Timing.Timing.CronString()) t.Log(time.Now(), referenceDate, referenceDate.After(testTime), referenceDate.After(testTime)) st := at.GetNextStartTime(referenceDate) if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanHourMonthdaysMonthYear(t *testing.T) { y, m, d := now.Date() testTime := time.Date(y, m, d, 10, 1, 0, 0, time.Local) nextYear := time.Date(y, m, 1, 0, 0, 0, 0, time.Local).AddDate(1, 0, 0) nextMonth := time.Date(y, m, 1, 0, 0, 0, 0, time.Local).AddDate(0, 1, 0) tomorrow := time.Date(y, m, d, 0, 0, 0, 0, time.Local).AddDate(0, 0, 1) day := now.Day() if now.After(testTime) { day = tomorrow.Day() } nextDay := time.Date(y, m, day, 10, 1, 0, 0, time.Local) month := now.Month() if nextDay.Before(now) { if now.After(testTime) { month = nextMonth.Month() } } nextDay = time.Date(y, month, day, 10, 1, 0, 0, time.Local) year := now.Year() if nextDay.Before(now) { if now.After(testTime) { year = nextYear.Year() } } at := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{ Years: utils.Years{now.Year(), nextYear.Year()}, Months: utils.Months{now.Month(), nextMonth.Month()}, MonthDays: utils.MonthDays{now.Day(), tomorrow.Day()}, StartTime: "10:01:00", }, }} st := at.GetNextStartTime(referenceDate) expected := time.Date(year, month, day, 10, 1, 0, 0, time.Local) if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanFirstOfTheYear(t *testing.T) { y, _, _ := now.Date() nextYear := time.Date(y, 1, 1, 0, 0, 0, 0, time.Local).AddDate(1, 0, 0) at := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{ Years: utils.Years{nextYear.Year()}, Months: utils.Months{time.January}, MonthDays: utils.MonthDays{1}, StartTime: "00:00:00", }, }} st := at.GetNextStartTime(referenceDate) expected := nextYear if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanFirstMonthOfTheYear(t *testing.T) { y, _, _ := now.Date() expected := time.Date(y, 1, 1, 0, 0, 0, 0, time.Local) if referenceDate.After(expected) { expected = expected.AddDate(1, 0, 0) } at := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{ Months: utils.Months{time.January}, }, }} st := at.GetNextStartTime(referenceDate) if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanFirstMonthOfTheYearSecondDay(t *testing.T) { y, _, _ := now.Date() expected := time.Date(y, 1, 2, 0, 0, 0, 0, time.Local) if referenceDate.After(expected) { expected = expected.AddDate(1, 0, 0) } at := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{ Months: utils.Months{time.January}, MonthDays: utils.MonthDays{2}, }, }} st := at.GetNextStartTime(referenceDate) if !st.Equal(expected) { t.Errorf("Expected %v was %v", expected, st) } } func TestActionPlanCheckForASAP(t *testing.T) { at := &ActionTiming{Timing: &RateInterval{Timing: &RITiming{StartTime: utils.MetaASAP}}} if !at.IsASAP() { t.Errorf("%v should be asap!", at) } } func TestActionPlanLogFunction(t *testing.T) { a := &Action{ ActionType: "*log", Balance: &BalanceFilter{ Type: utils.StringPointer("test"), Value: &utils.ValueFormula{Static: 1.1}, }, } at := &ActionTiming{ actions: []*Action{a}, } err := at.Execute(nil, "") if err != nil { t.Errorf("Could not execute LOG action: %v", err) } } func TestActionPlanFunctionNotAvailable(t *testing.T) { a := &Action{ ActionType: "VALID_FUNCTION_TYPE", Balance: &BalanceFilter{ Type: utils.StringPointer("test"), Value: &utils.ValueFormula{Static: 1.1}, }, } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:dy": true}, Timing: &RateInterval{}, actions: []*Action{a}, } err := at.Execute(nil, "") if err != utils.ErrPartiallyExecuted { // because we want to return err if we can't execute all actions t.Errorf("Faild to detect wrong function type: %v", err) } } func TestActionTimingPriorityListSortByWeight(t *testing.T) { at1 := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{ Years: utils.Years{2040}, Months: utils.Months{time.January, time.February, time.March, time.April, time.May, time.June, time.July, time.August, time.September, time.October, time.November, time.December}, MonthDays: utils.MonthDays{1}, StartTime: "00:00:00", }, Weight: 20, }} at2 := &ActionTiming{Timing: &RateInterval{ Timing: &RITiming{ Years: utils.Years{2040}, Months: utils.Months{time.January, time.February, time.March, time.April, time.May, time.June, time.July, time.August, time.September, time.October, time.November, time.December}, MonthDays: utils.MonthDays{2}, StartTime: "00:00:00", }, Weight: 10, }} var atpl ActionTimingPriorityList atpl = append(atpl, at2, at1) atpl.Sort() if atpl[0] != at1 || atpl[1] != at2 { t.Errorf("Timing list not sorted correctly: \n %+v, \n %+v \n %+v", utils.ToJSON(at1), utils.ToJSON(at2), utils.ToJSON(atpl)) } } func TestActionTimingPriorityListWeight(t *testing.T) { at1 := &ActionTiming{ Timing: &RateInterval{ Timing: &RITiming{ Months: utils.Months{time.January, time.February, time.March, time.April, time.May, time.June, time.July, time.August, time.September, time.October, time.November, time.December}, MonthDays: utils.MonthDays{1}, StartTime: "00:00:00", }, }, Weight: 20, } at2 := &ActionTiming{ Timing: &RateInterval{ Timing: &RITiming{ Months: utils.Months{time.January, time.February, time.March, time.April, time.May, time.June, time.July, time.August, time.September, time.October, time.November, time.December}, MonthDays: utils.MonthDays{1}, StartTime: "00:00:00", }, }, Weight: 10, } var atpl ActionTimingPriorityList atpl = append(atpl, at2, at1) atpl.Sort() if atpl[0] != at1 || atpl[1] != at2 { t.Error("Timing list not sorted correctly: ", atpl) } } func TestActionPlansRemoveMember(t *testing.T) { account1 := &Account{ID: "one"} account2 := &Account{ID: "two"} dm.SetAccount(account1) dm.SetAccount(account2) ap1 := &ActionPlan{ Id: "TestActionPlansRemoveMember1", AccountIDs: utils.StringMap{"one": true}, ActionTimings: []*ActionTiming{ { Uuid: "uuid1", Timing: &RateInterval{ Timing: &RITiming{ Years: utils.Years{2012}, Months: utils.Months{}, MonthDays: utils.MonthDays{}, WeekDays: utils.WeekDays{}, StartTime: utils.MetaASAP, }, }, Weight: 10, ActionsID: "MINI", }, }, } ap2 := &ActionPlan{ Id: "test2", AccountIDs: utils.StringMap{"two": true}, ActionTimings: []*ActionTiming{ { Uuid: "uuid2", Timing: &RateInterval{ Timing: &RITiming{ Years: utils.Years{2012}, Months: utils.Months{}, MonthDays: utils.MonthDays{}, WeekDays: utils.WeekDays{}, StartTime: utils.MetaASAP, }, }, Weight: 10, ActionsID: "MINI", }, }, } if err := dm.SetActionPlan(ap1.Id, ap1, true, utils.NonTransactional); err != nil { t.Error(err) } if err = dm.SetActionPlan(ap2.Id, ap2, true, utils.NonTransactional); err != nil { t.Error(err) } if err = dm.CacheDataFromDB(utils.ActionPlanPrefix, []string{ap1.Id, ap2.Id}, true); err != nil { t.Error(err) } if err = dm.SetAccountActionPlans(account1.ID, []string{ap1.Id}, false); err != nil { t.Error(err) } if err = dm.CacheDataFromDB(utils.AccountActionPlansPrefix, []string{account1.ID}, true); err != nil { t.Error(err) } dm.GetAccountActionPlans(account1.ID, false, true, utils.NonTransactional) // FixMe: remove here after finishing testing of map if err = dm.SetAccountActionPlans(account2.ID, []string{ap2.Id}, false); err != nil { t.Error(err) } if err = dm.CacheDataFromDB(utils.AccountActionPlansPrefix, []string{account2.ID}, false); err != nil { t.Error(err) } actions := []*Action{ { Id: "REMOVE", ActionType: utils.MetaRemoveAccount, }, } dm.SetActions(actions[0].Id, actions) at := &ActionTiming{ accountIDs: utils.StringMap{account1.ID: true}, Timing: &RateInterval{}, actions: actions, } if err = at.Execute(nil, ""); err != nil { t.Errorf("Execute Action: %v", err) } apr, err1 := dm.GetActionPlan(ap1.Id, true, true, utils.NonTransactional) if err1 != nil { t.Errorf("Get action plan test: %v", err1) } if _, exist := apr.AccountIDs[account1.ID]; exist { t.Errorf("Account one is not deleted ") } } func TestActionTriggerMatchNil(t *testing.T) { at := &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, ThresholdType: utils.TriggerMaxBalance, ThresholdValue: 2, } var a *Action if !at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } } func TestActionTriggerMatchAllBlank(t *testing.T) { at := &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, ThresholdType: utils.TriggerMaxBalance, ThresholdValue: 2, } a := &Action{} if !at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } } func TestActionTriggerMatchMinuteBucketBlank(t *testing.T) { at := &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, ThresholdType: utils.TriggerMaxBalance, ThresholdValue: 2, } a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ExtraParameters: `{"BalanceDirections":"*out"}`} if !at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } } func TestActionTriggerMatchMinuteBucketFull(t *testing.T) { at := &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, ThresholdType: utils.TriggerMaxBalance, ThresholdValue: 2, } a := &Action{ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v}`, utils.TriggerMaxBalance, 2)} if !at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } } func TestActionTriggerMatchAllFull(t *testing.T) { at := &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, ThresholdType: utils.TriggerMaxBalance, ThresholdValue: 2, } a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%v", "ThresholdValue": %v}`, utils.TriggerMaxBalance, 2)} if !at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } } func TestActionTriggerMatchSomeFalse(t *testing.T) { at := &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, ThresholdType: utils.TriggerMaxBalance, ThresholdValue: 2, } a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ExtraParameters: fmt.Sprintf(`{"ThresholdType":"%s"}`, utils.TriggerMaxBalanceCounter)} if at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } } func TestActionTriggerMatcBalanceFalse(t *testing.T) { at := &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, ThresholdType: utils.TriggerMaxBalance, ThresholdValue: 2, } a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ExtraParameters: fmt.Sprintf(`{"GroupID":"%s", "ThresholdType":"%s"}`, "TEST", utils.TriggerMaxBalance)} if at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } } func TestActionTriggerMatcAllFalse(t *testing.T) { at := &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, ThresholdType: utils.TriggerMaxBalance, ThresholdValue: 2, } a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ExtraParameters: fmt.Sprintf(`{"UniqueID":"ZIP", "GroupID":"%s", "ThresholdType":"%s"}`, "TEST", utils.TriggerMaxBalance)} if at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } } func TestActionTriggerMatchAll(t *testing.T) { at := &ActionTrigger{ ID: "TEST", UniqueID: "ZIP", ThresholdType: "TT", Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), RatingSubject: utils.StringPointer("test1"), Value: &utils.ValueFormula{Static: 2}, Weight: utils.Float64Pointer(1.0), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), SharedGroups: utils.StringMapPointer(utils.NewStringMap("test2")), }, } a := &Action{Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), RatingSubject: utils.StringPointer("test1"), Value: &utils.ValueFormula{Static: 2}, Weight: utils.Float64Pointer(1.0), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), SharedGroups: utils.StringMapPointer(utils.NewStringMap("test2")), }, ExtraParameters: `{"UniqueID":"ZIP", "GroupID":"TEST", "ThresholdType":"TT"}`} if !at.Match(a) { t.Errorf("Action trigger [%v] does not match action [%v]", at, a) } } func TestActionTriggers(t *testing.T) { at1 := &ActionTrigger{Weight: 30} at2 := &ActionTrigger{Weight: 20} at3 := &ActionTrigger{Weight: 10} var atpl ActionTriggers atpl = append(atpl, at2, at1, at3) atpl.Sort() if atpl[0] != at1 || atpl[2] != at3 || atpl[1] != at2 { t.Error("List not sorted: ", atpl) } } func TestActionResetTriggres(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 10}, }, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}, }, }, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ { Counters: CounterFilters{ &CounterFilter{Value: 1}, }, }, }, }, ActionTriggers: ActionTriggers{ &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true, }, &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true, }, }, } resetTriggersAction(ub, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.ActionTriggers[0].Executed == true || ub.ActionTriggers[1].Executed == true { t.Error("Reset triggers action failed!") } } func TestActionResetTriggresExecutesThem(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 10}, }, }, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ {Counters: CounterFilters{&CounterFilter{Value: 1}}}, }, }, ActionTriggers: ActionTriggers{ &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true, }, }, } resetTriggersAction(ub, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.ActionTriggers[0].Executed == true || ub.BalanceMap[utils.MetaMonetary][0].GetValue() == 12 { t.Error("Reset triggers action failed!") } } func TestActionResetTriggresActionFilter(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 10}, }, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}, }, }, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ {Counters: CounterFilters{&CounterFilter{Value: 1}}}, }, }, ActionTriggers: ActionTriggers{ &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } resetTriggersAction(ub, &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaSMS)}}, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.ActionTriggers[0].Executed == false || ub.ActionTriggers[1].Executed == false { t.Error("Reset triggers action failed!") } } func TestActionResetTriggresActionFilter2(t *testing.T) { ub := &Account{ ID: "TestActionResetTriggresActionFilter2", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 10}, }, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}, }, }, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ {Counters: CounterFilters{&CounterFilter{Value: 1}}}, }, }, ActionTriggers: ActionTriggers{ &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } resetTriggersAction(ub, &Action{}, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.ActionTriggers[0].Executed != false && ub.ActionTriggers[1].Executed != false { t.Error("Reset triggers action failed!") } } func TestActionSetPostpaid(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{Value: 100}}, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, UnitCounters: UnitCounters{utils.MetaMonetary: []*UnitCounter{ {Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } allowNegativeAction(ub, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if !ub.AllowNegative { t.Error("Set postpaid action failed!") } } func TestActionSetPrepaid(t *testing.T) { ub := &Account{ ID: "TEST_UB", AllowNegative: true, BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{Value: 100}}, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, UnitCounters: UnitCounters{utils.MetaMonetary: []*UnitCounter{ {Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } denyNegativeAction(ub, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.AllowNegative { t.Error("Set prepaid action failed!") } } func TestActionResetPrepaid(t *testing.T) { ub := &Account{ ID: "TEST_UB", AllowNegative: true, BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{Value: 100}}, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{{Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaSMS)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaSMS)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } resetAccountAction(ub, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if !ub.AllowNegative || ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 0 || len(ub.UnitCounters) != 0 || ub.BalanceMap[utils.MetaVoice][0].GetValue() != 0 || ub.ActionTriggers[0].Executed == true || ub.ActionTriggers[1].Executed == true { t.Log(ub.BalanceMap) t.Error("Reset account action failed!") } } func TestActionResetPostpaid(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{Value: 100}}, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{{Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaSMS)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaSMS)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } resetAccountAction(ub, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 0 || len(ub.UnitCounters) != 0 || ub.BalanceMap[utils.MetaVoice][0].GetValue() != 0 || ub.ActionTriggers[0].Executed == true || ub.ActionTriggers[1].Executed == true { t.Error("Reset account action failed!") } } func TestActionTopupResetCredit(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 100}}, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ {Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary), Value: &utils.ValueFormula{Static: 10}}} topupResetAction(ub, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.AllowNegative || ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 10 || len(ub.UnitCounters) != 0 || // InitCounters finds no counters len(ub.BalanceMap[utils.MetaVoice]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Errorf("Topup reset action failed: %+s", utils.ToIJSON(ub)) } } func TestActionTopupValueFactors(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{}, } a := &Action{ Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), Value: &utils.ValueFormula{Static: 10}, }, ExtraParameters: `{"*monetary":2}`, } topupResetAction(ub, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if len(ub.BalanceMap) != 1 || ub.BalanceMap[utils.MetaMonetary][0].Factors[utils.MetaMonetary] != 2.0 { t.Errorf("Topup reset action failed to set Factors: %+v", ub.BalanceMap[utils.MetaMonetary][0].Factors) } } func TestActionTopupResetCreditId(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 100}, &Balance{ID: "TEST_B", Value: 15}, }, }, } a := &Action{Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), ID: utils.StringPointer("TEST_B"), Value: &utils.ValueFormula{Static: 10}}} topupResetAction(ub, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.AllowNegative || ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 110 || len(ub.BalanceMap[utils.MetaMonetary]) != 2 { t.Errorf("Topup reset action failed: %+v", ub.BalanceMap[utils.MetaMonetary][0]) } } func TestActionTopupResetCreditNoId(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 100}, &Balance{ID: "TEST_B", Value: 15}, }, }, } a := &Action{Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), Value: &utils.ValueFormula{Static: 10}}} topupResetAction(ub, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.AllowNegative || ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 20 || len(ub.BalanceMap[utils.MetaMonetary]) != 2 { t.Errorf("Topup reset action failed: %+v", ub.BalanceMap[utils.MetaMonetary][1]) } } func TestActionTopupResetMinutes(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{Value: 100}}, utils.MetaVoice: {&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, UnitCounters: UnitCounters{utils.MetaMonetary: []*UnitCounter{ {Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{ Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{ Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } a := &Action{ Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaVoice), Value: &utils.ValueFormula{Static: 5}, Weight: utils.Float64Pointer(20), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT"))}} topupResetAction(ub, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.AllowNegative || ub.BalanceMap[utils.MetaVoice].GetTotalValue() != 5 || ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 100 || len(ub.UnitCounters) != 0 || len(ub.BalanceMap[utils.MetaVoice]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Errorf("Topup reset minutes action failed: %+v", ub.BalanceMap[utils.MetaVoice][0]) } } func TestActionTopupCredit(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{Value: 100}}, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ {Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } a := &Action{Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), Value: &utils.ValueFormula{Static: 10}}} topupAction(ub, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.AllowNegative || ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 110 || len(ub.UnitCounters) != 0 || len(ub.BalanceMap[utils.MetaVoice]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Error("Topup action failed!", ub.BalanceMap[utils.MetaMonetary].GetTotalValue()) } } func TestActionTopupMinutes(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 100}}, utils.MetaVoice: {&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ {Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaVoice), Value: &utils.ValueFormula{Static: 5}, Weight: utils.Float64Pointer(20), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT"))}} topupAction(ub, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.AllowNegative || ub.BalanceMap[utils.MetaVoice].GetTotalValue() != 15 || ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 100 || len(ub.UnitCounters) != 0 || len(ub.BalanceMap[utils.MetaVoice]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Error("Topup minutes action failed!", ub.BalanceMap[utils.MetaVoice]) } } func TestActionDebitCredit(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{Value: 100}}, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ {Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } a := &Action{Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), Value: &utils.ValueFormula{Static: 10}}} debitAction(ub, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.AllowNegative || ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 90 || len(ub.UnitCounters) != 0 || len(ub.BalanceMap[utils.MetaVoice]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Error("Debit action failed!", utils.ToIJSON(ub)) } } func TestActionDebitMinutes(t *testing.T) { ub := &Account{ ID: "TEST_UB", BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{Value: 100}}, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{{Counters: CounterFilters{&CounterFilter{Value: 1}}}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}, &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } a := &Action{Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaVoice), Value: &utils.ValueFormula{Static: 5}, Weight: utils.Float64Pointer(20), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT"))}} debitAction(ub, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if ub.AllowNegative || ub.BalanceMap[utils.MetaVoice][0].GetValue() != 5 || ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 100 || len(ub.UnitCounters) != 0 || len(ub.BalanceMap[utils.MetaVoice]) != 2 || ub.ActionTriggers[0].Executed != true || ub.ActionTriggers[1].Executed != true { t.Error("Debit minutes action failed!", ub.BalanceMap[utils.MetaVoice][0]) } } func TestActionResetAllCounters(t *testing.T) { ub := &Account{ ID: "TEST_UB", AllowNegative: true, BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{Value: 100}}, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{ThresholdType: utils.TriggerMaxEventCounter, ThresholdValue: 2, Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT")), Weight: utils.Float64Pointer(20)}, ActionsID: "TEST_ACTIONS", Executed: true}}, } ub.InitCounters() resetCountersAction(ub, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if !ub.AllowNegative || ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 100 || len(ub.UnitCounters) != 1 || len(ub.UnitCounters[utils.MetaMonetary][0].Counters) != 1 || len(ub.BalanceMap[utils.MetaMonetary]) != 1 || ub.ActionTriggers[0].Executed != true { t.Errorf("Reset counters action failed: %+v %+v %+v", ub.UnitCounters, ub.UnitCounters[utils.MetaMonetary][0], ub.UnitCounters[utils.MetaMonetary][0].Counters[0]) } if len(ub.UnitCounters) < 1 { t.FailNow() } c := ub.UnitCounters[utils.MetaMonetary][0].Counters[0] if c.Filter.GetWeight() != 20 || c.Value != 0 || c.Filter.GetDestinationIDs()["NAT"] == false { t.Errorf("Balance cloned incorrectly: %+v", c) } } func TestActionResetCounterOnlyDefault(t *testing.T) { ub := &Account{ ID: "TEST_UB", AllowNegative: true, BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{Value: 100}}, utils.MetaVoice: {&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdType: utils.TriggerMaxEventCounter, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}} ub.InitCounters() resetCountersAction(ub, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if !ub.AllowNegative || ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 100 || len(ub.UnitCounters) != 1 || len(ub.UnitCounters[utils.MetaMonetary][0].Counters) != 1 || len(ub.BalanceMap[utils.MetaVoice]) != 2 || ub.ActionTriggers[0].Executed != true { for _, b := range ub.UnitCounters[utils.MetaMonetary][0].Counters { t.Logf("B: %+v", b) } t.Errorf("Reset counters action failed: %+v", ub.UnitCounters) } if len(ub.UnitCounters) < 1 || len(ub.UnitCounters[utils.MetaMonetary][0].Counters) < 1 { t.FailNow() } c := ub.UnitCounters[utils.MetaMonetary][0].Counters[0] if c.Filter.GetWeight() != 0 || c.Value != 0 || len(c.Filter.GetDestinationIDs()) != 0 { t.Errorf("Balance cloned incorrectly: %+v!", c) } } func TestActionResetCounterCredit(t *testing.T) { ub := &Account{ ID: "TEST_UB", AllowNegative: true, BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{Value: 100}}, utils.MetaVoice: {&Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}}}, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ {Counters: CounterFilters{ &CounterFilter{Value: 1, Filter: new(BalanceFilter)}}}}, utils.MetaSMS: []*UnitCounter{ {Counters: CounterFilters{ &CounterFilter{Value: 1, Filter: new(BalanceFilter)}}}}}, ActionTriggers: ActionTriggers{ &ActionTrigger{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}, ThresholdValue: 2, ActionsID: "TEST_ACTIONS", Executed: true}}, } a := &Action{Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary)}} resetCountersAction(ub, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if !ub.AllowNegative || ub.BalanceMap[utils.MetaMonetary].GetTotalValue() != 100 || len(ub.UnitCounters) != 2 || len(ub.BalanceMap[utils.MetaVoice]) != 2 || ub.ActionTriggers[0].Executed != true { t.Error("Reset counters action failed!", ub.UnitCounters) } } func TestActionMakeNegative(t *testing.T) { a := &Action{Balance: &BalanceFilter{Value: &utils.ValueFormula{Static: 10}}} genericMakeNegative(a) if a.Balance.GetValue() > 0 { t.Error("Failed to make negative: ", a) } genericMakeNegative(a) if a.Balance.GetValue() > 0 { t.Error("Failed to preserve negative: ", a) } } func TestActionRemove(t *testing.T) { if _, err := dm.GetAccount("cgrates.org:remo"); err != nil { t.Errorf("account to be removed not found: %v", err) } a := &Action{ ActionType: utils.MetaRemoveAccount, } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:remo": true}, actions: Actions{a}, } at.Execute(nil, "") afterUb, err := dm.GetAccount("cgrates.org:remo") if err == nil || afterUb != nil { t.Error("error removing account: ", err, afterUb) } } func TestActionTopup(t *testing.T) { initialUb, _ := dm.GetAccount("vdf:minu") a := &Action{ ActionType: utils.MetaTopUp, Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary), Value: &utils.ValueFormula{Static: 25}, DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET")), Weight: utils.Float64Pointer(20)}, } at := &ActionTiming{ accountIDs: utils.StringMap{"vdf:minu": true}, actions: Actions{a}, } at.Execute(nil, "") afterUb, _ := dm.GetAccount("vdf:minu") initialValue := initialUb.BalanceMap[utils.MetaMonetary].GetTotalValue() afterValue := afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue() if afterValue != initialValue+25 { t.Error("Bad topup before and after: ", initialValue, afterValue) } } func TestActionTopupLoaded(t *testing.T) { initialUb, _ := dm.GetAccount("vdf:minitsboy") a := &Action{ ActionType: utils.MetaTopUp, Balance: &BalanceFilter{Type: utils.StringPointer(utils.MetaMonetary), Value: &utils.ValueFormula{Static: 25}, DestinationIDs: utils.StringMapPointer(utils.NewStringMap("RET")), Weight: utils.Float64Pointer(20)}, } at := &ActionTiming{ accountIDs: utils.StringMap{"vdf:minitsboy": true}, actions: Actions{a}, } at.Execute(nil, "") afterUb, _ := dm.GetAccount("vdf:minitsboy") initialValue := initialUb.BalanceMap[utils.MetaMonetary].GetTotalValue() afterValue := afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue() if afterValue != initialValue+25 { t.Logf("Initial: %+v", initialUb) t.Logf("After: %+v", afterUb) t.Error("Bad topup before and after: ", initialValue, afterValue) } } func TestActionTransactionFuncType(t *testing.T) { err := dm.SetAccount(&Account{ ID: "cgrates.org:trans", BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{ Value: 10, }}, }, }) if err != nil { t.Error("Error setting account: ", err) } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:trans": true}, Timing: &RateInterval{}, actions: []*Action{ { ActionType: utils.MetaTopUp, Balance: &BalanceFilter{Value: &utils.ValueFormula{Static: 1.1}, Type: utils.StringPointer(utils.MetaMonetary)}, }, { ActionType: "VALID_FUNCTION_TYPE", Balance: &BalanceFilter{Value: &utils.ValueFormula{Static: 1.1}, Type: utils.StringPointer("test")}, }, }, } at.Execute(nil, "") acc, err := dm.GetAccount("cgrates.org:trans") if err != nil || acc == nil { t.Error("Error getting account: ", acc, err) } if acc.BalanceMap[utils.MetaMonetary][0].Value != 10 { t.Errorf("Transaction didn't work: %v", acc.BalanceMap[utils.MetaMonetary][0].Value) } } func TestActionTransactionBalanceType(t *testing.T) { err := dm.SetAccount(&Account{ ID: "cgrates.org:trans", BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{ Value: 10, }}, }, }) if err != nil { t.Error("Error setting account: ", err) } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:trans": true}, Timing: &RateInterval{}, actions: []*Action{ { ActionType: utils.MetaTopUp, Balance: &BalanceFilter{Value: &utils.ValueFormula{Static: 1.1}, Type: utils.StringPointer(utils.MetaMonetary)}, }, { ActionType: utils.MetaTopUp, Balance: &BalanceFilter{Type: utils.StringPointer("test")}, }, }, } err = at.Execute(nil, "") if err != nil { t.Error(err) } acc, err := dm.GetAccount("cgrates.org:trans") if err != nil || acc == nil { t.Error("Error getting account: ", acc, err) } if acc.BalanceMap[utils.MetaMonetary][0].Value != 11.1 { t.Errorf("Transaction didn't work: %v", acc.BalanceMap[utils.MetaMonetary][0].Value) } } func TestActionTransactionBalanceNotType(t *testing.T) { err := dm.SetAccount(&Account{ ID: "cgrates.org:trans", BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{ Value: 10, }}, }, }) if err != nil { t.Error("Error setting account: ", err) } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:trans": true}, Timing: &RateInterval{}, actions: []*Action{ { ActionType: utils.MetaTopUp, Balance: &BalanceFilter{Value: &utils.ValueFormula{Static: 1.1}, Type: utils.StringPointer(utils.MetaVoice)}, }, { ActionType: utils.MetaTopUp, Balance: &BalanceFilter{Type: utils.StringPointer("test")}, }, }, } err = at.Execute(nil, "") if err != nil { t.Error(err) } acc, err := dm.GetAccount("cgrates.org:trans") if err != nil || acc == nil { t.Error("Error getting account: ", acc, err) } if acc.BalanceMap[utils.MetaMonetary][0].Value != 10.0 { t.Errorf("Transaction didn't work: %v", acc.BalanceMap[utils.MetaMonetary][0].Value) } } func TestActionWithExpireWithoutExpire(t *testing.T) { err := dm.SetAccount(&Account{ ID: "cgrates.org:exp", BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{ Value: 10, }}, }, }) if err != nil { t.Error("Error setting account: ", err) } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:exp": true}, Timing: &RateInterval{}, actions: []*Action{ { ActionType: utils.MetaTopUp, Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaVoice), Value: &utils.ValueFormula{Static: 15}, }, }, { ActionType: utils.MetaTopUp, Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaVoice), Value: &utils.ValueFormula{Static: 30}, ExpirationDate: utils.TimePointer(time.Now().Add(24 * time.Hour)), }, }, }, } err = at.Execute(nil, "") if err != nil { t.Error(err) } acc, err := dm.GetAccount("cgrates.org:exp") if err != nil || acc == nil { t.Errorf("Error getting account: %+v: %v", acc, err) } if len(acc.BalanceMap) != 2 || len(acc.BalanceMap[utils.MetaVoice]) != 2 { t.Errorf("Error debiting expir and unexpire: %+v", acc.BalanceMap[utils.MetaVoice][0]) } } func TestActionRemoveBalance(t *testing.T) { err := dm.SetAccount(&Account{ ID: "cgrates.org:rembal", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{ Value: 10, }, &Balance{ Value: 10, DestinationIDs: utils.NewStringMap("NAT", "RET"), ExpirationDate: time.Now().Add(24 * time.Hour), }, &Balance{ Value: 10, DestinationIDs: utils.NewStringMap("NAT", "RET"), }, }, }, }) if err != nil { t.Error("Error setting account: ", err) } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:rembal": true}, Timing: &RateInterval{}, actions: []*Action{ { ActionType: utils.MetaRemoveBalance, Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("NAT", "RET")), }, }, }, } err = at.Execute(nil, "") if err != nil { t.Error(err) } acc, err := dm.GetAccount("cgrates.org:rembal") if err != nil || acc == nil { t.Errorf("Error getting account: %+v: %v", acc, err) } if len(acc.BalanceMap) != 1 || len(acc.BalanceMap[utils.MetaMonetary]) != 1 { t.Errorf("Error removing balance: %+v", acc.BalanceMap[utils.MetaMonetary]) } } func TestActionRemoveExpiredBalance(t *testing.T) { err := dm.SetAccount(&Account{ ID: "cgrates.org:rembal2", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{ Value: 10, }, &Balance{ Value: 10, DestinationIDs: utils.NewStringMap("NAT", "RET"), ExpirationDate: time.Now().Add(24 * time.Hour), }, &Balance{ Value: 10, DestinationIDs: utils.NewStringMap("NAT", "RET"), ExpirationDate: time.Date(2010, time.November, 11, 22, 39, 0, 0, time.UTC), }, &Balance{ Value: 10, DestinationIDs: utils.NewStringMap("NAT", "RET"), ExpirationDate: time.Date(2012, time.November, 11, 22, 39, 0, 0, time.UTC), }, }, }, Disabled: true, }) if err != nil { t.Error("Error setting account: ", err) } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:rembal2": true}, Timing: &RateInterval{}, actions: []*Action{ { ActionType: utils.MetaRemoveExpired, Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, }, }, } err = at.Execute(nil, "") if err != nil { t.Error(err) } acc, err := dm.GetAccount("cgrates.org:rembal2") if err != nil || acc == nil { t.Errorf("Error getting account: %+v: %v", acc, err) } if len(acc.BalanceMap) != 1 || len(acc.BalanceMap[utils.MetaMonetary]) != 2 { t.Errorf("Error removing balance: %+v", utils.ToJSON(acc.BalanceMap[utils.MetaMonetary])) } } func TestActionTransferMonetaryDefault(t *testing.T) { err := dm.SetAccount( &Account{ ID: "cgrates.org:trans", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{ Uuid: utils.GenUUID(), ID: utils.MetaDefault, Value: 10, }, &Balance{ Uuid: utils.GenUUID(), Value: 3, }, &Balance{ Uuid: utils.GenUUID(), Value: 6, }, &Balance{ Uuid: utils.GenUUID(), Value: -2, }, }, }, }) if err != nil { t.Errorf("error setting account: %v", err) } a := &Action{ ActionType: utils.MetaTransferMonetaryDefault, } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:trans": true}, actions: Actions{a}, } at.Execute(nil, "") afterUb, err := dm.GetAccount("cgrates.org:trans") if err != nil { t.Error("account not found: ", err, afterUb) } if afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue() != 17 || afterUb.BalanceMap[utils.MetaMonetary][0].Value != 19 || afterUb.BalanceMap[utils.MetaMonetary][1].Value != 0 || afterUb.BalanceMap[utils.MetaMonetary][2].Value != 0 || afterUb.BalanceMap[utils.MetaMonetary][3].Value != -2 { for _, b := range afterUb.BalanceMap[utils.MetaMonetary] { t.Logf("B: %+v", b) } t.Error("transfer balance value: ", afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue()) } } func TestActionTransferMonetaryDefaultFilter(t *testing.T) { err := dm.SetAccount( &Account{ ID: "cgrates.org:trans", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{ Uuid: utils.GenUUID(), ID: utils.MetaDefault, Value: 10, Weight: 20, }, &Balance{ Uuid: utils.GenUUID(), Value: 3, Weight: 20, }, &Balance{ Uuid: utils.GenUUID(), Value: 1, Weight: 10, }, &Balance{ Uuid: utils.GenUUID(), Value: 6, Weight: 20, }, }, }, }) if err != nil { t.Errorf("error setting account: %v", err) } a := &Action{ ActionType: utils.MetaTransferMonetaryDefault, Balance: &BalanceFilter{Weight: utils.Float64Pointer(20)}, } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:trans": true}, actions: Actions{a}, } at.Execute(nil, "") afterUb, err := dm.GetAccount("cgrates.org:trans") if err != nil { t.Error("account not found: ", err, afterUb) } if afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue() != 20 || afterUb.BalanceMap[utils.MetaMonetary][0].Value != 19 || afterUb.BalanceMap[utils.MetaMonetary][1].Value != 0 || afterUb.BalanceMap[utils.MetaMonetary][2].Value != 1 || afterUb.BalanceMap[utils.MetaMonetary][3].Value != 0 { for _, b := range afterUb.BalanceMap[utils.MetaMonetary] { t.Logf("B: %+v", b) } t.Error("transfer balance value: ", afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue()) } } func TestActionConditionalTopup(t *testing.T) { err := dm.SetAccount( &Account{ ID: "cgrates.org:cond", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{ Uuid: utils.GenUUID(), ID: utils.MetaDefault, Value: 10, Weight: 20, }, &Balance{ Uuid: utils.GenUUID(), Value: 3, Weight: 20, }, &Balance{ Uuid: utils.GenUUID(), Value: 1, Weight: 10, }, &Balance{ Uuid: utils.GenUUID(), Value: 6, Weight: 20, }, }, }, }) if err != nil { t.Errorf("error setting account: %v", err) } a := &Action{ ActionType: utils.MetaTopUp, Filters: []string{`*lt:~*req.BalanceMap.*monetary.GetTotalValue:30`}, Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), Value: &utils.ValueFormula{Static: 11}, Weight: utils.Float64Pointer(30), }, } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:cond": true}, actions: Actions{a}, } if err = at.Execute(NewFilterS(config.CgrConfig(), nil, nil), ""); err != nil { t.Fatal(err) } afterUb, err := dm.GetAccount("cgrates.org:cond") if err != nil { t.Error("account not found: ", err, afterUb) } if len(afterUb.BalanceMap[utils.MetaMonetary]) != 5 || afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue() != 31 || afterUb.BalanceMap[utils.MetaMonetary][4].Value != 11 { for _, b := range afterUb.BalanceMap[utils.MetaMonetary] { t.Logf("B: %+v", b) } t.Error("transfer balance value: ", afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue()) } } func TestActionConditionalTopupNoMatch(t *testing.T) { err := dm.SetAccount( &Account{ ID: "cgrates.org:cond", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{ Uuid: utils.GenUUID(), ID: utils.MetaDefault, Value: 10, Weight: 20, }, &Balance{ Uuid: utils.GenUUID(), Value: 3, Weight: 20, }, &Balance{ Uuid: utils.GenUUID(), Value: 1, Weight: 10, }, &Balance{ Uuid: utils.GenUUID(), Value: 6, Weight: 20, }, }, }, }) if err != nil { t.Errorf("error setting account: %v", err) } a := &Action{ ActionType: utils.MetaTopUp, Filters: []string{`*lt:~*req.BalanceMap.*monetary.GetTotalValue:3`}, Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), Value: &utils.ValueFormula{Static: 11}, Weight: utils.Float64Pointer(30), }, } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:cond": true}, actions: Actions{a}, } at.Execute(NewFilterS(config.CgrConfig(), nil, nil), "") afterUb, err := dm.GetAccount("cgrates.org:cond") if err != nil { t.Error("account not found: ", err, afterUb) } if len(afterUb.BalanceMap[utils.MetaMonetary]) != 4 || afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue() != 20 { for _, b := range afterUb.BalanceMap[utils.MetaMonetary] { t.Logf("B: %+v", b) } t.Error("transfer balance value: ", afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue()) } } func TestActionConditionalTopupExistingBalance(t *testing.T) { err := dm.SetAccount( &Account{ ID: "cgrates.org:cond", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{ Uuid: utils.GenUUID(), Value: 1, Weight: 10, }, &Balance{ Uuid: utils.GenUUID(), Value: 6, Weight: 20, }, }, utils.MetaVoice: { &Balance{ Uuid: utils.GenUUID(), Value: 10, Weight: 10, }, &Balance{ Uuid: utils.GenUUID(), Value: 100, Weight: 20, }, }, }, }) if err != nil { t.Errorf("error setting account: %v", err) } a := &Action{ ActionType: utils.MetaTopUp, Filters: []string{`*gte:~*req.BalanceMap.*voice.GetTotalValue:100`}, Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), Value: &utils.ValueFormula{Static: 11}, Weight: utils.Float64Pointer(10), }, } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:cond": true}, actions: Actions{a}, } at.Execute(NewFilterS(config.CgrConfig(), nil, nil), "") afterUb, err := dm.GetAccount("cgrates.org:cond") if err != nil { t.Error("account not found: ", err, afterUb) } if len(afterUb.BalanceMap[utils.MetaMonetary]) != 2 || afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue() != 18 { for _, b := range afterUb.BalanceMap[utils.MetaMonetary] { t.Logf("B: %+v", b) } t.Error("transfer balance value: ", afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue()) } } func TestActionConditionalDisabledIfNegative(t *testing.T) { err := dm.SetAccount( &Account{ ID: "cgrates.org:af", BalanceMap: map[string]Balances{ utils.MetaData: { &Balance{ Uuid: "fc927edb-1bd6-425e-a2a3-9fd8bafaa524", ID: "for_v3hsillmilld500m_data_500_m", Value: 5.242, Weight: 10, RatingSubject: "for_v3hsillmilld500m_data_forfait", Categories: utils.StringMap{ "data_france": true, }, }, }, utils.MetaMonetary: { &Balance{ Uuid: "9fa1847a-f36a-41a7-8ec0-dfaab370141e", ID: utils.MetaDefault, Value: -1.95001, }, }, utils.MetaSMS: { &Balance{ Uuid: "d348d15d-2988-4ee4-b847-6a552f94e2ec", ID: "for_v3hsillmilld500m_mms_ill", Value: 20000, Weight: 10, DestinationIDs: utils.StringMap{ "FRANCE_NATIONAL": true, }, Categories: utils.StringMap{ "mms_france": true, "tmms_france": true, "vmms_france": true, }, }, &Balance{ Uuid: "f4643517-31f6-4199-980f-04cf535471ed", ID: "for_v3hsillmilld500m_sms_ill", Value: 20000, Weight: 10, DestinationIDs: utils.StringMap{ "FRANCE_NATIONAL": true, }, Categories: utils.StringMap{ "ms_france": true, }, }, }, utils.MetaVoice: { &Balance{ Uuid: "079ab190-77f4-44f3-9c6f-3a0dd1a59dfd", ID: "for_v3hsillmilld500m_voice_3_h", Value: 10800, Weight: 10, DestinationIDs: utils.StringMap{ "FRANCE_NATIONAL": true, }, Categories: utils.StringMap{ "call_france": true, }, }, }, }, }) if err != nil { t.Errorf("error setting account: %v", err) } a1 := &Action{ ActionType: utils.MetaSetBalance, Filters: []string{"*string:~*req.BalanceMap.*monetary[0].ID:*default", "*lt:~*req.BalanceMap.*monetary[0].Value:0"}, Balance: &BalanceFilter{ Type: utils.StringPointer("*sms"), ID: utils.StringPointer("for_v3hsillmilld500m_sms_ill"), Disabled: utils.BoolPointer(true), }, Weight: 9, } a2 := &Action{ ActionType: utils.MetaSetBalance, Filters: []string{"*string:~*req.BalanceMap.*monetary[0].ID:*default", "*lt:~*req.BalanceMap.*monetary[0].Value:0"}, Balance: &BalanceFilter{ Type: utils.StringPointer("*sms"), ID: utils.StringPointer("for_v3hsillmilld500m_mms_ill"), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("FRANCE_NATIONAL")), Weight: utils.Float64Pointer(10), Disabled: utils.BoolPointer(true), }, Weight: 8, } a3 := &Action{ ActionType: utils.MetaSetBalance, Filters: []string{"*string:~*req.BalanceMap.*monetary[0].ID:*default", "*lt:~*req.BalanceMap.*monetary[0].Value:0"}, Balance: &BalanceFilter{ Type: utils.StringPointer("*sms"), ID: utils.StringPointer("for_v3hsillmilld500m_sms_ill"), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("FRANCE_NATIONAL")), Weight: utils.Float64Pointer(10), Disabled: utils.BoolPointer(true), }, Weight: 8, } a4 := &Action{ ActionType: utils.MetaSetBalance, Filters: []string{"*string:~*req.BalanceMap.*monetary[0].ID:*default", "*lt:~*req.BalanceMap.*monetary[0].Value:0"}, Balance: &BalanceFilter{ Type: utils.StringPointer("*data"), Uuid: utils.StringPointer("fc927edb-1bd6-425e-a2a3-9fd8bafaa524"), RatingSubject: utils.StringPointer("for_v3hsillmilld500m_data_forfait"), Weight: utils.Float64Pointer(10), Disabled: utils.BoolPointer(true), }, Weight: 7, } a5 := &Action{ ActionType: utils.MetaSetBalance, Filters: []string{"*string:~*req.BalanceMap.*monetary[0].ID:*default", "*lt:~*req.BalanceMap.*monetary[0].Value:0"}, Balance: &BalanceFilter{ Type: utils.StringPointer("*voice"), ID: utils.StringPointer("for_v3hsillmilld500m_voice_3_h"), DestinationIDs: utils.StringMapPointer(utils.NewStringMap("FRANCE_NATIONAL")), Weight: utils.Float64Pointer(10), Disabled: utils.BoolPointer(true), }, Weight: 6, } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:af": true}, actions: Actions{a1, a2, a3, a4, a5}, } at.Execute(NewFilterS(config.CgrConfig(), nil, nil), "") afterUb, err := dm.GetAccount("cgrates.org:af") if err != nil { t.Error("account not found: ", err, afterUb) } for btype, chain := range afterUb.BalanceMap { if btype != utils.MetaMonetary { for _, b := range chain { if b.Disabled != true { t.Errorf("Failed to disabled balance (%s): %+v", btype, b) } } } } } func TestActionSetBalance(t *testing.T) { err := dm.SetAccount( &Account{ ID: "cgrates.org:setb", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{ ID: "m1", Uuid: utils.GenUUID(), Value: 1, Weight: 10, }, &Balance{ ID: "m2", Uuid: utils.GenUUID(), Value: 6, Weight: 20, }, }, utils.MetaVoice: { &Balance{ ID: "v1", Uuid: utils.GenUUID(), Value: 10, Weight: 10, }, &Balance{ ID: "v2", Uuid: utils.GenUUID(), Value: 100, Weight: 20, }, }, }, }) if err != nil { t.Errorf("error setting account: %v", err) } a := &Action{ ActionType: utils.MetaSetBalance, Balance: &BalanceFilter{ ID: utils.StringPointer("m2"), Type: utils.StringPointer(utils.MetaMonetary), Value: &utils.ValueFormula{Static: 11}, Weight: utils.Float64Pointer(10), }, } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:setb": true}, actions: Actions{a}, } at.Execute(nil, "") afterUb, err := dm.GetAccount("cgrates.org:setb") if err != nil { t.Error("account not found: ", err, afterUb) } if len(afterUb.BalanceMap[utils.MetaMonetary]) != 2 || afterUb.BalanceMap[utils.MetaMonetary][1].Value != 11 || afterUb.BalanceMap[utils.MetaMonetary][1].Weight != 10 { for _, b := range afterUb.BalanceMap[utils.MetaMonetary] { t.Logf("B: %+v", b) } t.Errorf("Balance: %+v", afterUb.BalanceMap[utils.MetaMonetary][1]) } } func TestActionCSVFilter(t *testing.T) { act, err := dm.GetActions("FILTER", false, utils.NonTransactional) if err != nil { t.Error("error getting actions: ", err) } if len(act) != 1 || !reflect.DeepEqual(act[0].Filters, []string{"*string:~*req.BalanceMap.*monetary[0].ID:*default", "*lt:~*req.BalanceMap.*monetary[0].Value:0"}) { t.Error("Error loading actions: ", act[0].Filters) } } func TestActionExpirationTime(t *testing.T) { a, err := dm.GetActions("EXP", false, utils.NonTransactional) if err != nil || a == nil { t.Error("Error getting actions: ", err) } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:expo": true}, actions: a, } for rep := 0; rep < 5; rep++ { at.Execute(nil, "") afterUb, err := dm.GetAccount("cgrates.org:expo") if err != nil || len(afterUb.BalanceMap[utils.MetaVoice]) != rep+1 { t.Error("error topuping expiration balance: ", utils.ToIJSON(afterUb)) } } } func TestActionExpNoExp(t *testing.T) { exp, err := dm.GetActions("EXP", false, utils.NonTransactional) if err != nil || exp == nil { t.Error("Error getting actions: ", err) } noexp, err := dm.GetActions("NOEXP", false, utils.NonTransactional) if err != nil || noexp == nil { t.Error("Error getting actions: ", err) } exp = append(exp, noexp...) at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:expnoexp": true}, actions: exp, } at.Execute(nil, "") afterUb, err := dm.GetAccount("cgrates.org:expnoexp") if err != nil || len(afterUb.BalanceMap[utils.MetaVoice]) != 2 { t.Error("error topuping expiration balance: ", utils.ToIJSON(afterUb)) } } func TestActionTopUpZeroNegative(t *testing.T) { account := &Account{ ID: "cgrates.org:zeroNegative", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{ ID: "Bal1", Value: -10, }, &Balance{ ID: "Bal2", Value: 5, }, }, }, } err := dm.SetAccount(account) if err != nil { t.Error("Error setting account: ", err) } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:zeroNegative": true}, Timing: &RateInterval{}, actions: []*Action{ { Id: "ZeroMonetary", ActionType: utils.TopUpZeroNegative, Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), }, }, }, } err = at.Execute(nil, "") if err != nil { t.Error(err) } acc, err := dm.GetAccount("cgrates.org:zeroNegative") if err != nil || acc == nil { t.Error("Error getting account: ", acc, err) } //Verify value for first balance(Bal1) should be 0 after execute action TopUpZeroNegative if acc.BalanceMap[utils.MetaMonetary][0].Value != 0 { t.Errorf("Expecting 0, received: %+v", acc.BalanceMap[utils.MetaMonetary][0].Value) } //Verify value for secound balance(Bal2) should be the same if acc.BalanceMap[utils.MetaMonetary][1].Value != 5 { t.Errorf("Expecting 5, received: %+v", acc.BalanceMap[utils.MetaMonetary][1].Value) } } func TestActionSetExpiry(t *testing.T) { timeNowPlus24h := time.Now().Add(24 * time.Hour) account := &Account{ ID: "cgrates.org:zeroNegative", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{ ID: "Bal1", Value: -10, }, &Balance{ ID: "Bal2", Value: 5, }, }, }, } err := dm.SetAccount(account) if err != nil { t.Error("Error setting account: ", err) } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:zeroNegative": true}, Timing: &RateInterval{}, actions: []*Action{ { Id: "SetExpiry", ActionType: utils.SetExpiry, Balance: &BalanceFilter{ ID: utils.StringPointer("Bal1"), Type: utils.StringPointer(utils.MetaMonetary), ExpirationDate: utils.TimePointer(timeNowPlus24h), }, }, }, } err = at.Execute(nil, "") if err != nil { t.Error(err) } acc, err := dm.GetAccount("cgrates.org:zeroNegative") if err != nil || acc == nil { t.Error("Error getting account: ", acc, err) } //Verify ExpirationDate for first balance(Bal1) if !acc.BalanceMap[utils.MetaMonetary][0].ExpirationDate.Equal(timeNowPlus24h) { t.Errorf("Expecting: %+v, received: %+v", timeNowPlus24h, acc.BalanceMap[utils.MetaMonetary][0].ExpirationDate) } } type TestRPCParameters struct { status string } type Attr struct { Name string Surname string Age float64 } func (trpcp *TestRPCParameters) Hopa(ctx *context.Context, in *Attr, out *float64) error { trpcp.status = utils.OK return nil } func TestCgrRpcAction(t *testing.T) { trpcp := &TestRPCParameters{} utils.RegisterRpcParams("", trpcp) a := &Action{ ExtraParameters: `{"Address": "*internal", "Transport": "*gob", "Method": "TestRPCParameters.Hopa", "Attempts":1, "Async" :false, "Params": {"Name":"n", "Surname":"s", "Age":10.2}}`, } if err := cgrRPCAction(nil, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Error("error executing cgr action: ", err) } if trpcp.status != utils.OK { t.Error("RPC not called!") } cfg := config.NewDefaultCGRConfig() cfg.GeneralCfg().ReplyTimeout = 1000 * time.Millisecond cfg.GeneralCfg().ConnectTimeout = 1000 * time.Millisecond config.SetCgrConfig(cfg) defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) }() a = &Action{ ExtraParameters: `{"Address": "*json*localhost", "Transport": "*gob", "Method": "TestRPCParameters.Hopa", "Attempts":1, "Async" :false, "Params": {"Name":"n", "Surname":"s", "Age":10.2}}`, } if err := cgrRPCAction(nil, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil { t.Error("error executing cgr action: ", err) } } func TestValueFormulaDebit(t *testing.T) { if _, err := dm.GetAccount("cgrates.org:vf"); err != nil { t.Errorf("account to be removed not found: %v", err) } at := &ActionTiming{ accountIDs: utils.StringMap{"cgrates.org:vf": true}, ActionsID: "VF", } at.Execute(nil, "") afterUb, err := dm.GetAccount("cgrates.org:vf") // not an exact value, depends of month v := afterUb.BalanceMap[utils.MetaMonetary].GetTotalValue() if err != nil || v > -0.30 || v < -0.36 { t.Error("error debiting account: ", err, utils.ToIJSON(afterUb), v) } } func TestClonedAction(t *testing.T) { var a *Action if clone := a.Clone(); clone != nil { t.Errorf("Expected nil but got %v", clone) } a = &Action{ Id: "test1", ActionType: utils.MetaTopUp, Balance: &BalanceFilter{ ID: utils.StringPointer(utils.MetaDefault), Value: &utils.ValueFormula{Static: 1}, Type: utils.StringPointer(utils.MetaMonetary), }, Weight: float64(10), } if clone := a.Clone(); !reflect.DeepEqual(a, clone) { t.Error("error cloning action: ", utils.ToIJSON(clone)) } } func TestClonedActions(t *testing.T) { actions := Actions{ &Action{ Id: "RECUR_FOR_V3HSILLMILLD1G", ActionType: utils.MetaTopUp, Balance: &BalanceFilter{ ID: utils.StringPointer(utils.MetaDefault), Value: &utils.ValueFormula{Static: 1}, Type: utils.StringPointer(utils.MetaMonetary), }, Weight: float64(30), }, &Action{ Id: "RECUR_FOR_V3HSILLMILLD5G", ActionType: utils.MetaDebit, Balance: &BalanceFilter{ ID: utils.StringPointer(utils.MetaDefault), Value: &utils.ValueFormula{Static: 2}, Type: utils.StringPointer(utils.MetaMonetary), }, Weight: float64(20), }, } clone := actions.Clone() if !reflect.DeepEqual(actions, clone) { t.Errorf("Expecting %+v, received: %+v", utils.ToIJSON(actions), utils.ToIJSON(clone)) } } func TestCacheGetClonedActions(t *testing.T) { actions := Actions{ &Action{ Id: "RECUR_FOR_V3HSILLMILLD1G", ActionType: utils.MetaTopUp, Balance: &BalanceFilter{ ID: utils.StringPointer(utils.MetaDefault), Value: &utils.ValueFormula{Static: 1}, Type: utils.StringPointer(utils.MetaMonetary), }, Weight: float64(30), }, &Action{ Id: "REACT_FOR_V3HSILLMILL", ActionType: utils.MetaSetBalance, Balance: &BalanceFilter{ ID: utils.StringPointer("for_v3hsillmill_sms_ill"), Type: utils.StringPointer(utils.MetaSMS), Value: &utils.ValueFormula{Static: 20000}, DestinationIDs: &utils.StringMap{ "FRANCE_NATIONAL": true, "FRANCE_NATIONAL_FREE": false, "ZONE1": false}, Categories: &utils.StringMap{ "sms_eurotarif": true, "sms_france": true}, Disabled: utils.BoolPointer(false), Blocker: utils.BoolPointer(false), }, Weight: float64(10), }, } if err := Cache.Set(utils.CacheActions, "MYTEST", actions, nil, true, ""); err != nil { t.Errorf("Expecting: nil, received: %s", err) } clned, ok := Cache.Get(utils.CacheActions, "MYTEST") if !ok { t.Error(ltcache.ErrNotFound) } aCloned := clned.(Actions).Clone() if !reflect.DeepEqual(actions, aCloned) { t.Errorf("Expecting: %+v, received: %+v", actions[1].Balance, aCloned[1].Balance) } } // TestCdrLogAction type RPCMock struct { args *ArgV1ProcessEvents } func (r *RPCMock) Call(ctx *context.Context, method string, args any, rply any) error { if method != utils.CDRsV1ProcessEvents { return rpcclient.ErrUnsupporteServiceMethod } if r.args != nil { return fmt.Errorf("There should be only one call to this function") } r.args = args.(*ArgV1ProcessEvents) rp := rply.(*string) *rp = utils.OK return nil } func TestCdrLogAction(t *testing.T) { mock := RPCMock{} dfltCfg := config.NewDefaultCGRConfig() if err != nil { t.Fatal(err) } dfltCfg.SchedulerCfg().CDRsConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaCDRs)} config.SetCgrConfig(dfltCfg) internalChan := make(chan birpc.ClientConnector, 1) internalChan <- &mock NewConnManager(dfltCfg, map[string]chan birpc.ClientConnector{ utils.ConcatenatedKey(utils.MetaInternal, utils.MetaCDRs): internalChan, }) extraData := map[string]any{ "test": "val", } acc := &Account{ ID: "cgrates.org:1001", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 20}, }, }, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ { Counters: CounterFilters{ &CounterFilter{Value: 1}, }, }, }, }, } a := &Action{ Id: "CDRLog1", ActionType: utils.CDRLog, ExtraParameters: "{\"BalanceID\":\"~*acnt.BalanceID\",\"ActionID\":\"~*act.ActionID\",\"BalanceValue\":\"~*acnt.BalanceValue\"}", Weight: 50, } acs := Actions{ a, &Action{ Id: "CdrDebit", ActionType: "*debit", Balance: &BalanceFilter{ ID: utils.StringPointer(utils.MetaDefault), Value: &utils.ValueFormula{Static: 9.95}, Type: utils.StringPointer(utils.MetaMonetary), Weight: utils.Float64Pointer(0), }, Weight: float64(90), balanceValue: 10, }, } if err := cdrLogAction(acc, a, acs, nil, extraData, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Fatal(err) } if mock.args == nil { t.Fatalf("Expected a call to %s", utils.CDRsV1ProcessEvent) } expCgrEv := &utils.CGREvent{ Tenant: "cgrates.org", ID: mock.args.CGREvents[0].ID, Event: map[string]any{ "Account": "1001", "ActionID": "CdrDebit", "AnswerTime": mock.args.CGREvents[0].Event["AnswerTime"], "BalanceID": "*default", "BalanceValue": "10", "CGRID": mock.args.CGREvents[0].Event["CGRID"], "Category": "", "Cost": 9.95, "CostSource": "", "Destination": "", "ExtraInfo": "", "OrderID": mock.args.CGREvents[0].Event["OrderID"], "OriginHost": "127.0.0.1", "OriginID": mock.args.CGREvents[0].Event["OriginID"], "Partial": false, "PreRated": true, "RequestType": "*none", "RunID": "*debit", "SetupTime": mock.args.CGREvents[0].Event["SetupTime"], "Source": utils.CDRLog, "Subject": "1001", "Tenant": "cgrates.org", "ToR": "*monetary", "Usage": mock.args.CGREvents[0].Event["Usage"], "test": "val", }, APIOpts: map[string]any{}, } if !reflect.DeepEqual(expCgrEv, mock.args.CGREvents[0]) { t.Errorf("Expected: %+v \n,received: %+v", utils.ToJSON(expCgrEv), utils.ToJSON(mock.args.CGREvents[0])) } } func TestRemoteSetAccountAction(t *testing.T) { expError := `Post "127.1.0.11//": unsupported protocol scheme ""` if err = remoteSetAccount(nil, &Action{ExtraParameters: "127.1.0.11//"}, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expError { t.Fatalf("Expected error: %s, received: %v", expError, err) } expError = `json: unsupported type: func()` if err = remoteSetAccount(&Account{ ActionTriggers: ActionTriggers{{ Balance: &BalanceFilter{ Value: &utils.ValueFormula{ Params: map[string]any{utils.MetaVoice: func() {}}, }, }, }}, }, &Action{ExtraParameters: "127.1.0.11//"}, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expError { t.Fatalf("Expected error: %s, received: %v", expError, err) } ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { rw.Write([]byte("5")) })) acc := &Account{ID: "1001"} expError = `json: cannot unmarshal number into Go value of type engine.Account` if err = remoteSetAccount(acc, &Action{ExtraParameters: ts.URL}, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expError { t.Fatalf("Expected error: %s, received: %v", expError, err) } exp := &Account{ID: "1001"} if !reflect.DeepEqual(exp, acc) { t.Errorf("Expected: %s,received: %s", utils.ToJSON(exp), utils.ToJSON(acc)) } ts.Close() acc = &Account{ID: "1001"} exp = &Account{ ID: "1001", BalanceMap: map[string]Balances{ utils.MetaVoice: {{ ID: "money", Value: 15, }}, }, } ts = httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { accStr := utils.ToJSON(acc) + "\n" val, err := io.ReadAll(r.Body) r.Body.Close() if err != nil { t.Error(err) return } if string(val) != accStr { t.Errorf("Expected %q,received: %q", accStr, string(val)) return } rw.Write([]byte(utils.ToJSON(exp))) })) if err = remoteSetAccount(acc, &Action{ExtraParameters: ts.URL}, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Fatal(err) } else if !reflect.DeepEqual(exp, acc) { t.Errorf("Expected: %s,received: %s", utils.ToJSON(exp), utils.ToJSON(acc)) } ts.Close() } /**************** Benchmarks ********************************/ func BenchmarkUUID(b *testing.B) { m := make(map[string]int, 1000) for i := 0; i < b.N; i++ { uuid := utils.GenUUID() if len(uuid) == 0 { b.Fatalf("GenUUID error %s", uuid) } b.StopTimer() c := m[uuid] if c > 0 { b.Fatalf("duplicate uuid[%s] count %d", uuid, c) } m[uuid] = c + 1 b.StartTimer() } } func TestResetAccountCDR(t *testing.T) { cfg := config.NewDefaultCGRConfig() defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetCdrStorage(cdrStorage) }() idb, dErr := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items) if dErr != nil { t.Error(dErr) } dm := NewDataManager(idb, cfg.CacheCfg(), nil) fltrs := NewFilterS(cfg, nil, dm) SetCdrStorage(idb) var extraData any acc := &Account{ ID: "cgrates.org:1001", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 20}, }, }, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ { Counters: CounterFilters{ &CounterFilter{Value: 1}, }, }, }, }, } a := &Action{ Id: "CDRLog1", ActionType: utils.CDRLog, ExtraParameters: "{\"BalanceID\":\"~*acnt.BalanceID\",\"ActionID\":\"~*act.ActionID\",\"BalanceValue\":\"~*acnt.BalanceValue\"}", Weight: 50, } acs := Actions{ a, &Action{ Id: "CdrDebit", ActionType: "*debit", Balance: &BalanceFilter{ ID: utils.StringPointer(utils.MetaDefault), Value: &utils.ValueFormula{Static: 9.95}, Type: utils.StringPointer(utils.MetaMonetary), Weight: utils.Float64Pointer(0), }, Weight: float64(90), balanceValue: 10, }, } if err := resetAccountCDR(nil, a, acs, fltrs, extraData, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != "nil account" { t.Errorf("expected ,received <%+v>", err) } else if err = resetAccountCDR(acc, a, acs, fltrs, extraData, SharedActionsData{}, ActionConnCfg{}); err == nil || err != utils.ErrNotFound { t.Error(err) } SetCdrStorage(nil) if err := resetAccountCDR(acc, a, acs, fltrs, extraData, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != fmt.Sprintf("nil cdrStorage for %s action", utils.ToJSON(a)) { t.Error(err) } } func TestSetRecurrentAction(t *testing.T) { ub := &Account{ ID: "ACCID", ActionTriggers: ActionTriggers{ &ActionTrigger{ ID: "acTrigger", UniqueID: "uuid_acc", Recurrent: false, }, &ActionTrigger{ ID: "acTrigger1", UniqueID: "uuid_acc1", Recurrent: false, }, }, } ac := &Action{ Id: "acTrigger", } if err = setRecurrentAction(ub, ac, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Error(err) } } func TestActionSetDDestinations(t *testing.T) { tmpDm := dm defer func() { dm = tmpDm }() cfg := config.NewDefaultCGRConfig() cfg.RalsCfg().StatSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.StatSConnsCfg)} cfg.GeneralCfg().DefaultTenant = "cgrates.org" cfg.DataDbCfg().Items = map[string]*config.ItemOpt{ utils.CacheDestinations: { Limit: 3, StaticTTL: true, }, utils.MetaReverseDestinations: { Limit: 3, Replicate: false, }, } ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.StatSv1GetStatQueue: func(ctx *context.Context, args, reply any) error { rpl := &StatQueue{ Tenant: "cgrates", ID: "id", SQMetrics: map[string]StatMetric{ utils.MetaDDC: &StatDDC{ FilterIDs: []string{"filters"}, Count: 7, }, }, SQItems: []SQItem{ { EventID: "event1", }, { EventID: "event2", }, }, } *reply.(*StatQueue) = *rpl return nil }, }, } clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock connMgr := NewConnManager(cfg, map[string]chan birpc.ClientConnector{ utils.ConcatenatedKey(utils.MetaInternal, utils.StatSConnsCfg): clientconn, }) SetConnManager(connMgr) config.SetCgrConfig(cfg) ub := &Account{ ID: "ACCID", ActionTriggers: ActionTriggers{ &ActionTrigger{ ID: "acTrigger", UniqueID: "uuid_acc", Recurrent: false, }, &ActionTrigger{ ID: "acTrigger1", UniqueID: "uuid_acc1", Recurrent: false, }, }, BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 10, DestinationIDs: utils.StringMap{ "*ddc:fr": true, "*ddc:ger": false, }}, }, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}, }, }, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ { Counters: CounterFilters{ &CounterFilter{Value: 1}, }, }, }, }, } a := &Action{ Id: "CDRLog1", ActionType: utils.CDRLog, ExtraParameters: "{\"BalanceID\";\"~*acnt.BalanceID\";\"ActionID\";\"~*act.ActionID\";\"BalanceValue\";\"~*acnt.BalanceValue\"}", Weight: 50, } acs := Actions{ a, &Action{ Id: "CdrDebit", ActionType: "*debit", Balance: &BalanceFilter{ ID: utils.StringPointer(utils.MetaDefault), Value: &utils.ValueFormula{Static: 9.95}, Type: utils.StringPointer(utils.MetaMonetary), Weight: utils.Float64Pointer(0), }, Weight: float64(90), balanceValue: 10, }, } if err := dm.dataDB.SetDestinationDrv(&Destination{ Id: "*ddc:fr", }, utils.NonTransactional); err != nil { t.Error(err) } if err := dm.dataDB.SetDestinationDrv(&Destination{ Id: "*ddc:ger", }, utils.NonTransactional); err != nil { t.Error(err) } SetDataStorage(dm) if err := setddestinations(ub, a, acs, nil, nil, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Error(err) } } func TestActionPublishAccount(t *testing.T) { utils.Logger.SetLogLevel(4) utils.Logger.SetSyslog(nil) buf := new(bytes.Buffer) log.SetOutput(buf) cfg := config.NewDefaultCGRConfig() tmpCfg := cfg defer func() { utils.Logger.SetLogLevel(0) log.SetOutput(os.Stderr) config.SetCgrConfig(tmpCfg) SetConnManager(nil) }() cfg.RalsCfg().ThresholdSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.ThreshSConnsCfg)} cfg.RalsCfg().StatSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.StatSConnsCfg)} clientConn := make(chan birpc.ClientConnector, 1) clientConn <- &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.ThresholdSv1ProcessEvent: func(ctx *context.Context, args, reply any) error { *reply.(*[]string) = []string{"*thr"} return errors.New("Can't publish!") }, utils.StatSv1ProcessEvent: func(ctx *context.Context, args, reply any) error { *reply.(*[]string) = []string{"*stat"} return errors.New("Can't publish!") }, }, } connMgr := NewConnManager(cfg, map[string]chan birpc.ClientConnector{ utils.ConcatenatedKey(utils.MetaInternal, utils.ThreshSConnsCfg): clientConn, utils.ConcatenatedKey(utils.MetaInternal, utils.StatSConnsCfg): clientConn, }) SetConnManager(connMgr) config.SetCgrConfig(cfg) ub := &Account{ ID: "ACCID", ActionTriggers: ActionTriggers{ &ActionTrigger{ ID: "acTrigger", UniqueID: "uuid_acc", Recurrent: false, }, &ActionTrigger{ ID: "acTrigger1", UniqueID: "uuid_acc1", Recurrent: false, }, }, BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 10, DestinationIDs: utils.StringMap{ "*ddc_dest": true, "*dest": false, }}, }, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}, }, }, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ { Counters: CounterFilters{ &CounterFilter{Value: 1}, }, }, }, }, } a := &Action{ Id: "CDRLog1", ActionType: utils.CDRLog, ExtraParameters: "{\"BalanceID\":\"~*acnt.BalanceID\",\"ActionID\":\"~*act.ActionID\",\"BalanceValue\":\"~*acnt.BalanceValue\"}", Weight: 50, } acs := Actions{ a, &Action{ Id: "CdrDebit", ActionType: "*debit", Balance: &BalanceFilter{ ID: utils.StringPointer(utils.MetaDefault), Value: &utils.ValueFormula{Static: 9.95}, Type: utils.StringPointer(utils.MetaMonetary), Weight: utils.Float64Pointer(0), }, Weight: float64(90), balanceValue: 10, }, } expLog := ` with ThresholdS` expLog2 := `with StatS.` if err := publishAccount(ub, a, acs, nil, nil, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Errorf("received %v", err) } else if rcvLog := buf.String(); !strings.Contains(rcvLog, expLog) { t.Errorf("Logger %v doesn't contain %v", rcvLog, expLog) } else if rcvLog := buf.String(); !strings.Contains(rcvLog, expLog2) { t.Errorf("Logger %v doesn't contain %v", rcvLog, expLog2) } } func TestExportAction(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.ApierCfg().EEsConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.EEsConnsCfg)} config.SetCgrConfig(cfg) ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args, reply any) error{ utils.EeSv1ProcessEvent: func(ctx *context.Context, args, reply any) error { rpl := &map[string]map[string]any{} *reply.(*map[string]map[string]any) = *rpl return nil }, }, } clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock connMgr := NewConnManager(cfg, map[string]chan birpc.ClientConnector{ utils.ConcatenatedKey(utils.MetaInternal, utils.EEsConnsCfg): clientconn, }) SetConnManager(connMgr) ub := &Account{ ID: "ACCID", ActionTriggers: ActionTriggers{ &ActionTrigger{ ID: "acTrigger", UniqueID: "uuid_acc", Recurrent: false, }, &ActionTrigger{ ID: "acTrigger1", UniqueID: "uuid_acc1", Recurrent: false, }, }, BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 10, DestinationIDs: utils.StringMap{ "*ddc_dest": true, "*dest": false, }}, }, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}, }, }, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ { Counters: CounterFilters{ &CounterFilter{Value: 1}, }, }, }, }, } a := &Action{ Id: "CDRLog1", ActionType: utils.CDRLog, ExtraParameters: "{\"BalanceID\":\"~*acnt.BalanceID\",\"ActionID\":\"~*act.ActionID\",\"BalanceValue\":\"~*acnt.BalanceValue\"}", Weight: 50, } acs := Actions{ a, &Action{ Id: "CdrDebit", ActionType: "*debit", Balance: &BalanceFilter{ ID: utils.StringPointer(utils.MetaDefault), Value: &utils.ValueFormula{Static: 9.95}, Type: utils.StringPointer(utils.MetaMonetary), Weight: utils.Float64Pointer(0), }, Weight: float64(90), balanceValue: 10, }, } extraData := &utils.CGREvent{ Tenant: "tenant", ID: "id1", Time: utils.TimePointer(time.Date(2022, 12, 1, 1, 0, 0, 0, time.UTC)), Event: map[string]any{}, APIOpts: map[string]any{}, } if err := export(ub, a, acs, nil, nil, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Errorf("received %v", err) } else if err = export(nil, a, acs, nil, extraData, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Errorf("received %v", err) } else if err = export(nil, a, acs, nil, "test", SharedActionsData{}, ActionConnCfg{}); err != nil { t.Error(err) } else if err = export(nil, a, acs, nil, nil, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Error(err) } } func TestResetStatQueue(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.SchedulerCfg().StatSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.StatSConnsCfg)} ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.StatSv1ResetStatQueue: func(ctx *context.Context, args, reply any) error { rpl := "reset" *reply.(*string) = rpl return nil }, }, } clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock connMgr := NewConnManager(cfg, map[string]chan birpc.ClientConnector{ utils.ConcatenatedKey(utils.MetaInternal, utils.StatSConnsCfg): clientconn, }) SetConnManager(connMgr) config.SetCgrConfig(cfg) ub := &Account{} a := &Action{ ExtraParameters: "cgrates.org:id", } acs := Actions{} if err := resetStatQueue(ub, a, acs, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil { t.Errorf("received <%+v>", err) } } func TestResetTreshold(t *testing.T) { cfg := config.NewDefaultCGRConfig() cfg.SchedulerCfg().ThreshSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.ThreshSConnsCfg)} ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.ThresholdSv1ResetThreshold: func(ctx *context.Context, args, reply any) error { rpl := "threshold_reset" *reply.(*string) = rpl return nil }, }, } clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock connMgr := NewConnManager(cfg, map[string]chan birpc.ClientConnector{ utils.ConcatenatedKey(utils.MetaInternal, utils.ThreshSConnsCfg): clientconn, }) SetConnManager(connMgr) config.SetCgrConfig(cfg) ub := &Account{} a := &Action{ ExtraParameters: "cgrates.org:id", } acs := Actions{} if err := resetThreshold(ub, a, acs, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil { t.Errorf("received <%+v>", err) } } func TestEnableDisableAccountAction(t *testing.T) { var acc *Account expErr := "nil account" if err := enableAccountAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = disableAccountAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = genericDebit(acc, nil, true, nil); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = resetCountersAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = debitAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = debitResetAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = topupAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = topupResetAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = resetAccountAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = denyNegativeAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = allowNegativeAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = unsetRecurrentAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = setRecurrentAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } else if err = resetTriggersAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != expErr { t.Errorf("expected %+v ,received %v", expErr, err) } } func TestResetAccountCDRSuccesful(t *testing.T) { cfg := config.NewDefaultCGRConfig() idb, dErr := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items) if dErr != nil { t.Error(dErr) } dm := NewDataManager(idb, cfg.CacheCfg(), nil) fltrs := NewFilterS(cfg, nil, dm) cdr := &CDR{ CGRID: "Cdr1", OrderID: 123, ToR: utils.MetaVoice, OriginID: "OriginCDR1", OriginHost: "192.168.1.1", Source: "test", RequestType: utils.MetaRated, Category: "call", Account: "1001", Subject: "1001", Destination: "+4986517174963", RunID: utils.MetaDefault, Usage: time.Duration(0), ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, Cost: 1.01, CostDetails: &EventCost{ CGRID: "ecId", RunID: "ecRunId", StartTime: time.Date(2022, 12, 1, 12, 0, 0, 0, time.UTC), Usage: utils.DurationPointer(1 * time.Hour), Cost: utils.Float64Pointer(12.1), Charges: []*ChargingInterval{}, AccountSummary: &AccountSummary{ Tenant: "cgrates.org", ID: "acc_Id", BalanceSummaries: BalanceSummaries{ { UUID: "uuid", ID: "summary_id", Type: "type", Initial: 1, Value: 12, Disabled: true, }, {}, }, AllowNegative: true, Disabled: false, }, Accounting: Accounting{}, RatingFilters: RatingFilters{}, Rates: ChargedRates{}, }, } if err := idb.SetCDR(cdr, true); err != nil { t.Error(err) } SetCdrStorage(idb) var extraData any acc := &Account{ ID: "cgrates.org:1001", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 20}, }, }, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ { Counters: CounterFilters{ &CounterFilter{Value: 1}, }, }, }, }, } a := &Action{ Id: "CDRLog1", ActionType: utils.CDRLog, ExtraParameters: "{\"BalanceID\":\"~*acnt.BalanceID\",\"ActionID\":\"~*act.ActionID\",\"BalanceValue\":\"~*acnt.BalanceValue\"}", Weight: 50, } acs := Actions{ a, &Action{ Id: "CdrDebit", ActionType: "*debit", Balance: &BalanceFilter{ ID: utils.StringPointer(utils.MetaDefault), Value: &utils.ValueFormula{Static: 9.95}, Type: utils.StringPointer(utils.MetaMonetary), Weight: utils.Float64Pointer(0), }, Weight: float64(90), balanceValue: 10, }, } if err = resetAccountCDR(acc, a, acs, fltrs, extraData, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Error(err) } } func TestRemoveSessionCost(t *testing.T) { tmp := Cache tmpCdr := cdrStorage utils.Logger.SetLogLevel(4) utils.Logger.SetSyslog(nil) buf := new(bytes.Buffer) log.SetOutput(buf) defer func() { utils.Logger.SetLogLevel(0) log.SetOutput(os.Stderr) Cache = tmp cdrStorage = tmpCdr }() Cache.Clear(nil) cfg := config.NewDefaultCGRConfig() action := &Action{ ExtraParameters: "*acnt.BalanceID;*act.ActionID", } Cache.Set(utils.CacheFilters, utils.ConcatenatedKey(cfg.GeneralCfg().DefaultTenant, "*acnt.BalanceID"), &Filter{ Tenant: "tnt", Rules: []*FilterRule{ { Values: []string{"val1,val2"}, Type: utils.MetaString, Element: utils.MetaScPrefix + utils.CGRID}, { Values: []string{"val1,val2"}, Type: utils.MetaString, Element: "test"}, }, }, []string{"grpId"}, true, utils.NonTransactional) expLog := `for filter` expLog2 := `in action:` if err := removeSessionCosts(nil, action, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err != utils.ErrNotFound { t.Error(err) } else if rcvLog := buf.String(); !strings.Contains(rcvLog, expLog) { t.Errorf("expected %v,received %v", expLog, rcvLog) } else if rcvLog := buf.String(); !strings.Contains(rcvLog, expLog2) { t.Errorf("expected %v,received %v", expLog, rcvLog) } } func TestLogAction(t *testing.T) { acc := &Account{ ID: "cgrates.org:1001", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 20}, }, }, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ { Counters: CounterFilters{ &CounterFilter{Value: 1}, }, }, }, }, } extraData := map[string]any{ "field1": "value", "field2": "second", } if err := logAction(acc, nil, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Error(err) } else if err = logAction(nil, nil, nil, nil, extraData, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Error(err) } } func TestCdrLogProviderFieldAsInterface(t *testing.T) { acc := &Account{ ID: "ACCID", ActionTriggers: ActionTriggers{ &ActionTrigger{ ID: "acTrigger", UniqueID: "uuid_acc", Recurrent: false, }, &ActionTrigger{ ID: "acTrigger1", UniqueID: "uuid_acc1", Recurrent: false, }, }, BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 10, DestinationIDs: utils.StringMap{ "*ddc_dest": true, "*dest": false, }}, }, utils.MetaVoice: { &Balance{Value: 10, Weight: 20, DestinationIDs: utils.NewStringMap("NAT")}, &Balance{Weight: 10, DestinationIDs: utils.NewStringMap("RET")}, }, }, UnitCounters: UnitCounters{ utils.MetaMonetary: []*UnitCounter{ { Counters: CounterFilters{ &CounterFilter{Value: 1}, }, }, }, }, } a := &Action{ Id: "CDRLog1", ActionType: utils.CDRLog, ExtraParameters: "{\"BalanceID\":\"~*acnt.BalanceID\",\"ActionID\":\"~*act.ActionID\",\"BalanceValue\":\"~*acnt.BalanceValue\"}", Weight: 50, Balance: &BalanceFilter{ Uuid: utils.StringPointer("uuid113"), ID: utils.StringPointer("b_id_22"), Type: utils.StringPointer("*prepaid"), RatingSubject: utils.StringPointer("rate"), DestinationIDs: &utils.StringMap{ "dest1": true, }, Value: &utils.ValueFormula{ Static: 3.0, }, Categories: &utils.StringMap{ "category": true, }, SharedGroups: &utils.StringMap{ "group1": true, }, ExpirationDate: utils.TimePointer(time.Date(2022, 1, 1, 2, 0, 0, 0, time.UTC)), Weight: utils.Float64Pointer(323.0), }, } cd := &cdrLogProvider{action: a, acnt: acc, cache: utils.MapStorage{ "field": "val", }} if val, err := cd.FieldAsInterface([]string{utils.MetaAcnt, utils.AccountID}); err != nil { t.Error(err) } else if val != acc.ID { t.Errorf("expected %v,received %v", acc.ID, val) } if _, has := cd.cache[utils.MetaAcnt]; !has { t.Error("field does not exist") } if val, err := cd.FieldAsInterface([]string{utils.MetaAcnt, utils.BalanceUUID}); err != nil { t.Error(err) } else if val != *a.Balance.Uuid { t.Errorf("expected %v,received %v", *a.Balance.Uuid, val) } if _, has := cd.cache[utils.MetaAcnt]; !has { t.Error("field does not exist") } if val, err := cd.FieldAsInterface([]string{utils.MetaAcnt, utils.DestinationIDs}); err != nil { t.Error(err) } else if val != a.Balance.DestinationIDs.String() { t.Errorf("expected %v,received %v", *a.Balance.Uuid, val) } if _, has := cd.cache[utils.MetaAcnt]; !has { t.Error("field does not exist") } if val, err := cd.FieldAsInterface([]string{utils.MetaAcnt, utils.ExtraParameters}); err != nil { t.Error() } else if val != a.ExtraParameters { t.Errorf("expected %v,received %v", *a.Balance.Uuid, val) } if _, has := cd.cache[utils.MetaAcnt]; !has { t.Error("field does not exist") } if val, err := cd.FieldAsInterface([]string{utils.MetaAcnt, utils.RatingSubject}); err != nil { t.Error() } else if val != *a.Balance.RatingSubject { t.Errorf("expected %v,received %v", *a.Balance.Uuid, val) } if _, has := cd.cache[utils.MetaAcnt]; !has { t.Error("field does not exist") } if val, err := cd.FieldAsInterface([]string{utils.MetaAcnt, utils.Category}); err != nil { t.Error() } else if val != a.Balance.Categories.String() { t.Errorf("expected %v,received %v", *a.Balance.Uuid, val) } if _, has := cd.cache[utils.MetaAcnt]; !has { t.Error("field does not exist") } if val, err := cd.FieldAsInterface([]string{utils.MetaAcnt, utils.SharedGroups}); err != nil { t.Error() } else if val != a.Balance.SharedGroups.String() { t.Errorf("expected %v,received %v", *a.Balance.Uuid, val) } if _, has := cd.cache[utils.MetaAcnt]; !has { t.Error("field does not exist") } if val, err := cd.FieldAsInterface([]string{utils.MetaAct, utils.ActionType}); err != nil { t.Error() } else if val != a.ActionType { t.Errorf("expected %v,received %v", *a.Balance.Uuid, val) } if _, has := cd.cache[utils.MetaAct]; !has { t.Error("field does not exist") } if val, err := cd.FieldAsInterface([]string{"val"}); err != nil { t.Error() } else if val != "val" { t.Errorf("expected %v,received %v", *a.Balance.Uuid, val) } if _, has := cd.cache["val"]; !has { t.Error("field does not exist") } } func TestRemoveAccountAcc(t *testing.T) { a := &Action{ Id: "CDRLog1", ActionType: utils.CDRLog, ExtraParameters: "{\"BalanceID\":\"~*acnt.BalanceID\",\"ActionID\":\"~*act.ActionID\",\"BalanceValue\":\"~*acnt.BalanceValue\"}", Weight: 50, } acs := Actions{ a, &Action{ Id: "CdrDebit", ActionType: "*debit", Balance: &BalanceFilter{ ID: utils.StringPointer(utils.MetaDefault), Value: &utils.ValueFormula{Static: 9.95}, Type: utils.StringPointer(utils.MetaMonetary), Weight: utils.Float64Pointer(0), }, Weight: float64(90), balanceValue: 10, }, } extraData := &utils.CGREvent{ Tenant: "tenant", ID: "id1", Time: utils.TimePointer(time.Date(2022, 12, 1, 1, 0, 0, 0, time.UTC)), Event: map[string]any{}, APIOpts: map[string]any{}, } if err := removeAccountAction(nil, a, acs, nil, extraData, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Error(err) } } func TestRemoveAccountActionErr(t *testing.T) { tmp := Cache tmpDm := dm setLogger := func(buf *bytes.Buffer) { utils.Logger.SetLogLevel(4) utils.Logger.SetSyslog(nil) log.SetOutput(buf) } removeLogger := func() { utils.Logger.SetLogLevel(0) log.SetOutput(os.Stderr) } buf := new(bytes.Buffer) setLogger(buf) cfg := config.NewDefaultCGRConfig() defer func() { removeLogger() Cache = tmp SetDataStorage(tmpDm) config.SetCgrConfig(config.NewDefaultCGRConfig()) }() cfg.DataDbCfg().Items = map[string]*config.ItemOpt{ utils.CacheAccounts: { Limit: 3, TTL: 2 * time.Minute, Remote: true, }, utils.CacheAccountActionPlans: { Limit: 3, StaticTTL: true, Remote: true, }, utils.CacheActionPlans: { Remote: true, Limit: 3, StaticTTL: true, }, } cfg.DataDbCfg().RmtConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaReplicator)} clientConn := make(chan birpc.ClientConnector, 1) clientConn <- &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.ReplicatorSv1GetAccountActionPlans: func(ctx *context.Context, args, reply any) error { return errors.New("ActionPlans not found") }, utils.ReplicatorSv1GetActionPlan: func(ctx *context.Context, args, reply any) error { return errors.New("ActionPlan not found") }, }, } connMgr := NewConnManager(cfg, map[string]chan birpc.ClientConnector{ utils.ConcatenatedKey(utils.MetaInternal, utils.MetaReplicator): clientConn, }) a := &Action{ Id: "CDRLog1", ActionType: utils.CDRLog, ExtraParameters: "{\"BalanceID\":\"~*acnt.BalanceID\",\"ActionID\":\"~*act.ActionID\",\"BalanceValue\":\"~*acnt.BalanceValue\"}", Weight: 50, } acs := Actions{ a, &Action{ Id: "CdrDebit", ActionType: "*debit", Balance: &BalanceFilter{ ID: utils.StringPointer(utils.MetaDefault), Value: &utils.ValueFormula{Static: 9.95}, Type: utils.StringPointer(utils.MetaMonetary), Weight: utils.Float64Pointer(0), }, Weight: float64(90), balanceValue: 10, }, } extraData := &utils.CGREvent{ Tenant: "tenant", ID: "id1", Time: utils.TimePointer(time.Date(2022, 12, 1, 1, 0, 0, 0, time.UTC)), Event: map[string]any{}, APIOpts: map[string]any{}, } ub := &Account{ BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{ Value: 10, }}, }, } db, dErr := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items) if dErr != nil { t.Error(dErr) } dm := NewDataManager(db, cfg.CacheCfg(), connMgr) SetDataStorage(nil) if err := removeAccountAction(ub, a, acs, nil, extraData, SharedActionsData{}, ActionConnCfg{}); err == nil || err != utils.ErrInvalidKey { t.Error(err) } ub.ID = "cgrates.org:exp" expLog := `[ERROR] Could not remove account Id: cgrates.org:exp: NO_DATABASE_CONNECTION` if err := removeAccountAction(ub, a, acs, nil, extraData, SharedActionsData{}, ActionConnCfg{}); err == nil { t.Error(err) } else if rcvLog := buf.String(); !strings.Contains(rcvLog, expLog) { t.Errorf("expected log <%+v> to be included in: <%+v>", expLog, rcvLog) } SetDataStorage(dm) config.SetCgrConfig(cfg) ub.ID = "acc_id" if err := dm.SetAccount(ub); err != nil { t.Error(err) } removeLogger() buf2 := new(bytes.Buffer) setLogger(buf2) expLog = `Could not get action plans` if err := removeAccountAction(ub, a, acs, nil, extraData, SharedActionsData{}, ActionConnCfg{}); err == nil { t.Error(err) } else if rcvLog := buf2.String(); !strings.Contains(rcvLog, expLog) { t.Errorf("Logger %v doesn't contain %v", rcvLog, expLog) } removeLogger() buf3 := new(bytes.Buffer) setLogger(buf3) expLog = `Could not retrieve action plan:` dm.SetAccountActionPlans(ub.ID, []string{"acc1"}, true) if err := removeAccountAction(ub, a, acs, nil, extraData, SharedActionsData{}, ActionConnCfg{}); err == nil { t.Error(err) } else if rcvLog := buf3.String(); !strings.Contains(rcvLog, expLog) { t.Errorf("Logger %v doesn't contain %v", rcvLog, expLog) } // if err := dm.SetActionPlan("acc1", &ActionPlan{ // ActionTimings: []*ActionTiming{ // {ActionsID: "ENABLE_ACNT"}, // }, // }, true, utils.NonTransactional); err != nil { // t.Error(err) // } } func TestRemoveExpiredErrs(t *testing.T) { var acc *Account action := &Action{ Id: "MINI", ActionType: utils.MetaTopUpReset, ExpirationString: utils.MetaUnlimited, ExtraParameters: "", Weight: 10, Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), Uuid: utils.StringPointer("uuid"), Value: &utils.ValueFormula{Static: 10}, Weight: utils.Float64Pointer(10), DestinationIDs: nil, TimingIDs: nil, SharedGroups: nil, Categories: nil, Disabled: utils.BoolPointer(false), Blocker: utils.BoolPointer(false), }, } if err := removeExpired(acc, action, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err.Error() != fmt.Sprintf("nil account for %s action", utils.ToJSON(action)) { t.Error(err) } acc = &Account{ ID: "cgrates.org:rembal2", BalanceMap: map[string]Balances{}, Disabled: true, } if err = removeExpired(acc, action, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err != utils.ErrNotFound { t.Error(err) } acc.BalanceMap = map[string]Balances{ utils.MetaMonetary: { &Balance{ Value: 10, }, &Balance{ Value: 10, DestinationIDs: utils.NewStringMap("NAT", "RET"), ExpirationDate: time.Date(2050, time.November, 11, 22, 39, 0, 0, time.UTC), }, &Balance{ Value: 10, DestinationIDs: utils.NewStringMap("NAT", "RET"), ExpirationDate: time.Date(2050, time.November, 15, 22, 39, 0, 0, time.UTC), }, &Balance{ Value: 10, DestinationIDs: utils.NewStringMap("NAT", "RET"), ExpirationDate: time.Date(2050, time.November, 11, 22, 39, 0, 0, time.UTC), }, }, } if err := removeExpired(acc, action, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err != utils.ErrNotFound { t.Error(err) } } func TestTransferMonetaryDefaultAction(t *testing.T) { utils.Logger.SetLogLevel(3) utils.Logger.SetSyslog(nil) buf := new(bytes.Buffer) log.SetOutput(buf) defer func() { utils.Logger.SetLogLevel(0) log.SetOutput(os.Stderr) }() a := &Action{ Id: "CDRLog1", ActionType: utils.CDRLog, ExtraParameters: "{\"BalanceID\":\"~*acnt.BalanceID\",\"ActionID\":\"~*act.ActionID\",\"BalanceValue\":\"~*acnt.BalanceValue\"}", Weight: 50, } acs := Actions{ &Action{ Id: "CdrDebit", ActionType: "*debit", Weight: float64(90), balanceValue: 10, }, } expLog := `*transfer_monetary_default called without account` if err := transferMonetaryDefaultAction(nil, a, acs, nil, "data", SharedActionsData{}, ActionConnCfg{}); err == nil || err != utils.ErrAccountNotFound { t.Errorf("expected <%v>,received <%v>", utils.ErrAccountNotFound, err) } else if rcvLog := buf.String(); !strings.Contains(rcvLog, expLog) { t.Errorf("expected log <%+v> to be included in: <%+v>", expLog, rcvLog) } ub := &Account{} if err := transferMonetaryDefaultAction(ub, a, acs, nil, "data", SharedActionsData{}, ActionConnCfg{}); err == nil || err != utils.ErrNotFound { t.Errorf("expected <%v>,received <%v>", utils.ErrNotFound, err) } } func TestRemoveBalanceActionErr(t *testing.T) { acc := &Account{ ID: "vdf:minu", BalanceMap: map[string]Balances{ utils.MetaMonetary: {&Balance{Value: 50}}, utils.MetaVoice: { &Balance{Value: 200 * float64(time.Second), ExpirationDate: time.Date(2022, 11, 22, 2, 0, 0, 0, time.UTC), DestinationIDs: utils.NewStringMap("NAT"), Weight: 10}, &Balance{Value: 100 * float64(time.Second), DestinationIDs: utils.NewStringMap("RET"), Weight: 20}, }, }, } acs := &Action{ Balance: &BalanceFilter{ ExpirationDate: utils.TimePointer(time.Date(2022, 11, 12, 2, 0, 0, 0, time.UTC)), Type: utils.StringPointer(utils.MetaMonetary), Value: &utils.ValueFormula{Static: 10}, }, } if err := removeBalanceAction(nil, acs, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil { t.Error(err) } if err := removeBalanceAction(acc, acs, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err != utils.ErrNotFound { t.Error(err) } } func TestDebitResetAction(t *testing.T) { ub := &Account{ ID: "OUT:CUSTOMER_1:rif", BalanceMap: map[string]Balances{ utils.MetaVoice: {&Balance{Value: 21}}, utils.MetaMonetary: {&Balance{Value: 21}}, }, } a := &Action{ Id: "MINI", ActionType: utils.MetaTopUpReset, ExpirationString: utils.MetaUnlimited, ExtraParameters: "", Weight: 10, Balance: &BalanceFilter{ Type: utils.StringPointer(utils.MetaMonetary), Uuid: utils.StringPointer("uuid"), Value: &utils.ValueFormula{Static: 10}, Weight: utils.Float64Pointer(10), DestinationIDs: nil, TimingIDs: nil, SharedGroups: nil, Categories: nil, Disabled: utils.BoolPointer(false), Blocker: utils.BoolPointer(false), }, } if err := debitResetAction(ub, a, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Error(err) } } func TestSetDestinationsErr(t *testing.T) { cfg := config.NewDefaultCGRConfig() tempConn := connMgr tmpDm := dm defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm }() cfg.RalsCfg().StatSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaStats)} cfg.DataDbCfg().RplConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaReplicator)} cfg.DataDbCfg().Items = map[string]*config.ItemOpt{ utils.MetaReverseDestinations: { Replicate: true, }, utils.MetaDestinations: { Replicate: false, }, } db, dErr := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items) if dErr != nil { t.Error(dErr) } clientConn := make(chan birpc.ClientConnector, 1) clientConn <- &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.StatSv1GetStatQueue: func(ctx *context.Context, args, reply any) error { rpl := StatQueue{ Tenant: "cgrates.org", ID: "StatsID", SQItems: []SQItem{{ EventID: "ev1", }}, SQMetrics: map[string]StatMetric{ utils.MetaDDC: &StatDDC{ FilterIDs: []string{"Test_Filter_ID"}, FieldValues: map[string]utils.StringSet{ "1001": { "EVENT_1": {}, }, "1002": { "EVENT_3": {}, }, }, Events: map[string]map[string]int64{ "Event1": { "FieldValue1": 1, }, "Event2": {}, }, MinItems: 3, Count: 3, }, }, } *reply.(*StatQueue) = rpl return nil }, utils.ReplicatorSv1SetReverseDestination: func(ctx *context.Context, args, reply any) error { return utils.ErrNotImplemented }, }, } connMgr := NewConnManager(cfg, map[string]chan birpc.ClientConnector{ utils.ConcatenatedKey(utils.MetaInternal, utils.MetaStats): clientConn, utils.ConcatenatedKey(utils.MetaInternal, utils.MetaReplicator): clientConn, }) dm := NewDataManager(db, cfg.CacheCfg(), connMgr) ub := &Account{ ID: "cgrates.org:1001", BalanceMap: map[string]Balances{ utils.MetaVoice: { { ID: "1001", Value: 0, DestinationIDs: utils.StringMap{ utils.MetaDDC + "TEST": true, }, }, }, utils.MetaMonetary: { { ID: utils.MetaDefault, Value: -1.8, }, }, }, } a := &Action{ Id: "TOPUP_RST_GNR_1000", ActionType: utils.MetaTopUpReset, ExtraParameters: `{"*voice": 60.0,"*data":1024.0,"*sms":1.0}`, Weight: 10, ExpirationString: utils.MetaUnlimited, } acs := Actions{ a, } dest := &Destination{Id: utils.MetaDDC + "TEST", Prefixes: []string{"+491", "+492", "+493"}} if err := dm.SetDestination(dest, utils.NonTransactional); err != nil { t.Error(err) } config.SetCgrConfig(cfg) SetConnManager(connMgr) SetDataStorage(dm) if err := setddestinations(ub, a, acs, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err != utils.ErrNotFound { t.Error(err) } ub = &Account{ ID: "cgrates.org:1001", BalanceMap: map[string]Balances{ utils.MetaMonetary: { &Balance{Value: 20}, }, }, } if err := setddestinations(ub, a, acs, nil, nil, SharedActionsData{}, ActionConnCfg{}); err == nil || err != utils.ErrNotFound { t.Error(err) } } func TestRemoveAccountActionLogg(t *testing.T) { cfg := config.NewDefaultCGRConfig() tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) cfg.DataDbCfg().Items = map[string]*config.ItemOpt{ utils.CacheAccountActionPlans: { Remote: true, }, utils.MetaAccounts: { Replicate: false, }, utils.MetaActionPlans: { Remote: true, Replicate: true, }, } cfg.DataDbCfg().RmtConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaReplicator)} cfg.DataDbCfg().RplConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaReplicator)} db, dErr := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items) if dErr != nil { t.Error(dErr) } clientConn := make(chan birpc.ClientConnector, 1) clientConn <- &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.ReplicatorSv1GetAccountActionPlans: func(ctx *context.Context, args, reply any) error { rpl := []string{"PACKAGE_10_SHARED_A_5"} *reply.(*[]string) = rpl return nil }, utils.ReplicatorSv1GetActionPlan: func(ctx *context.Context, args, reply any) error { rpl := ActionPlan{ Id: "PACKAGE_10_SHARED_A_5", AccountIDs: utils.StringMap{ "cgrates.org:1001": true, }, } *reply.(**ActionPlan) = &rpl return nil }, utils.ReplicatorSv1SetActionPlan: func(ctx *context.Context, args, reply any) error { return utils.ErrNotFound }, }} connMgr := NewConnManager(cfg, map[string]chan birpc.ClientConnector{ utils.ConcatenatedKey(utils.MetaInternal, utils.MetaReplicator): clientConn, }) dm := NewDataManager(db, cfg.CacheCfg(), connMgr) ub := &Account{ ID: "cgrates.org:1001", BalanceMap: map[string]Balances{ utils.MetaVoice: { { ID: "1001", Value: 0, DestinationIDs: utils.StringMap{ utils.MetaDDC + "TEST": true, }, }, }, utils.MetaMonetary: { { ID: utils.MetaDefault, Value: -1.8, }, }, }, } a := &Action{ Id: "TOPUP_RST_GNR_1000", ActionType: utils.MetaTopUpReset, ExtraParameters: `{"*voice": 60.0,"*data":1024.0,"*sms":1.0}`, Weight: 10, ExpirationString: utils.MetaUnlimited, } acs := Actions{ a, } SetDataStorage(dm) config.SetCgrConfig(cfg) if err := removeAccountAction(ub, a, acs, nil, nil, SharedActionsData{}, ActionConnCfg{}); err != nil { t.Error(err) } } func TestActionsTransferBalance(t *testing.T) { cfg := config.NewDefaultCGRConfig() var srcAcc *Account var destAcc *Account db := &DataDBMock{ SetAccountDrvF: func(acc *Account) error { switch { case strings.HasSuffix(acc.ID, "_FAIL"): return utils.ErrServerError case srcAcc.ID == acc.ID: srcAcc = acc case destAcc.ID == acc.ID: destAcc = acc default: return utils.ErrAccountNotFound } return nil }, GetAccountDrvF: func(id string) (*Account, error) { if destAcc == nil || destAcc.ID != id { return nil, utils.ErrNotFound } return destAcc, nil }, } mockedDM := NewDataManager(db, cfg.CacheCfg(), nil) originalDM := dm defer func() { dm = originalDM }() dm = mockedDM testcases := []struct { name string srcAcc *Account destAcc *Account act *Action expectedSrcBalance float64 expectedDestBalance float64 expectedErr string }{ { name: "SuccessfulTransfer", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_SRC", Value: 10, }, }, }, }, destAcc: &Account{ ID: "cgrates.org:ACC_DEST", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_DEST", Value: 5, }, }, }, }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ActionType: utils.MetaTransferBalance, ExtraParameters: `{ "DestinationAccountID": "cgrates.org:ACC_DEST", "DestinationBalanceID": "BALANCE_DEST" }`, Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), Value: &utils.ValueFormula{ Static: 3, }, }, }, expectedSrcBalance: 7, expectedDestBalance: 8, }, { name: "SuccessfulTransferNegativeSrcBalance", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "*default", Value: 2, }, }, }, }, destAcc: &Account{ ID: "cgrates.org:ACC_DEST", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_DEST", Value: 5, }, }, }, }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ActionType: utils.MetaTransferBalance, ExtraParameters: `{ "DestinationAccountID": "cgrates.org:ACC_DEST", "DestinationBalanceID": "BALANCE_DEST" }`, Balance: &BalanceFilter{ ID: utils.StringPointer("*default"), Value: &utils.ValueFormula{ Static: 3, }, }, }, expectedSrcBalance: -1, expectedDestBalance: 8, }, { name: "SuccessfulTransferNegativeDestBalance", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_SRC", Value: 5, }, }, }, }, destAcc: &Account{ ID: "cgrates.org:ACC_DEST", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "*default", Value: 2, }, }, }, }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ActionType: utils.MetaTransferBalance, ExtraParameters: `{ "DestinationAccountID": "cgrates.org:ACC_DEST", "DestinationBalanceID": "*default" }`, Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), Value: &utils.ValueFormula{ Static: -3, }, }, }, expectedSrcBalance: 8, expectedDestBalance: -1, }, { name: "SuccessfulTransferRefNegativeSrcBalance", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "*default", Value: 2, }, }, }, }, destAcc: &Account{ ID: "cgrates.org:ACC_DEST", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_DEST", Value: 5, }, }, }, }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ActionType: utils.MetaTransferBalance, ExtraParameters: `{ "DestinationAccountID": "cgrates.org:ACC_DEST", "DestinationBalanceID": "BALANCE_DEST", "DestinationReferenceValue": 8 }`, Balance: &BalanceFilter{ ID: utils.StringPointer("*default"), }, }, expectedSrcBalance: -1, expectedDestBalance: 8, }, { name: "SuccessfulTransferRefNegativeDestBalance", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_SRC", Value: 10, }, }, }, }, destAcc: &Account{ ID: "cgrates.org:ACC_DEST", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "*default", Value: 5, }, }, }, }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ActionType: utils.MetaTransferBalance, ExtraParameters: `{ "DestinationAccountID": "cgrates.org:ACC_DEST", "DestinationBalanceID": "*default", "DestinationReferenceValue": -0.01 }`, Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), Value: &utils.ValueFormula{ Static: 3, }, }, }, expectedSrcBalance: 15.01, expectedDestBalance: -0.01, }, { name: "NilAccount", expectedErr: "source account is nil", }, { name: "UnspecifiedBalanceID", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", Balance: &BalanceFilter{}, }, expectedErr: "source balance ID is missing", }, { name: "MissingDestinationParameters", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), }, }, expectedErr: "ExtraParameters used to identify the destination balance are missing", }, { name: "MissingSourceAccountBalances", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ExtraParameters: "invalid_params", Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), }, }, expectedErr: "account cgrates.org:ACC_SRC has no balances to transfer from", }, { name: "SourceAccountBalanceNotFound", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "*default", Value: 10, }, }, }, }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ExtraParameters: "invalid_params", Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), }, }, expectedErr: "source balance not found or expired", }, { name: "InvalidExtraParameters", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_SRC", Value: 10, }, }, }, }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ExtraParameters: "invalid_params", Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), Value: &utils.ValueFormula{ Static: 3, }, }, }, expectedErr: "invalid character 'i' looking for beginning of value", }, { name: "DestinationAccountNotFound", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_SRC", Value: 10, }, }, }, }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ExtraParameters: `{ "DestinationAccountID": "cgrates.org:ACC_DEST", "DestinationBalanceID": "BALANCE_DEST" }`, Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), Value: &utils.ValueFormula{ Static: 3, }, }, }, expectedErr: "retrieving destination account failed: NOT_FOUND", }, { name: "DestinationBalanceNotFound", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_SRC", Value: 10, }, }, }, }, destAcc: &Account{ ID: "cgrates.org:ACC_DEST", }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ExtraParameters: `{ "DestinationAccountID": "cgrates.org:ACC_DEST", "DestinationBalanceID": "BALANCE_DEST" }`, Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), Value: &utils.ValueFormula{ Static: 3, }, }, }, expectedSrcBalance: 7, expectedDestBalance: 3, }, { name: "TransferUnitsNotSpecifiedOr0", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_SRC", Value: 1, }, }, }, }, destAcc: &Account{ ID: "cgrates.org:ACC_DEST", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_DEST", Value: 5, }, }, }, }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ExtraParameters: `{ "DestinationAccountID": "cgrates.org:ACC_DEST", "DestinationBalanceID": "BALANCE_DEST" }`, Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), Value: &utils.ValueFormula{ Static: 0, }, }, }, expectedErr: "transfer amount is missing or 0", }, { name: "NotEnoughFundsInSourceBalance", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_SRC", Value: 1, }, }, }, }, destAcc: &Account{ ID: "cgrates.org:ACC_DEST", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_DEST", Value: 5, }, }, }, }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ExtraParameters: `{ "DestinationAccountID": "cgrates.org:ACC_DEST", "DestinationBalanceID": "BALANCE_DEST" }`, Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), Value: &utils.ValueFormula{ Static: 3, }, }, }, expectedErr: `insufficient credits in source balance "BALANCE_SRC" (account "cgrates.org:ACC_SRC") for transfer of 3.00 units`, }, { name: "NotEnoughFundsInDestBalance", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_SRC", Value: 1, }, }, }, }, destAcc: &Account{ ID: "cgrates.org:ACC_DEST", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_DEST", Value: 5, }, }, }, }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ExtraParameters: `{ "DestinationAccountID": "cgrates.org:ACC_DEST", "DestinationBalanceID": "BALANCE_DEST", "DestinationReferenceValue": -0.01 }`, Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), Value: &utils.ValueFormula{ // should be ignored (overwritten by the ReferenceValue) Static: 3, }, }, }, expectedErr: `insufficient credits in destination balance "BALANCE_DEST" (account "cgrates.org:ACC_DEST") for transfer of -5.01 units`, }, { name: "DestinationBalanceFailedUpdate", srcAcc: &Account{ ID: "cgrates.org:ACC_SRC", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_SRC", Value: 10, }, }, }, }, destAcc: &Account{ ID: "cgrates.org:ACC_DEST_FAIL", BalanceMap: map[string]Balances{ utils.MetaMonetary: { { ID: "BALANCE_DEST", Value: 5, }, }, }, }, act: &Action{ Id: "ACT_TRANSFER_BALANCE", ExtraParameters: `{ "DestinationAccountID": "cgrates.org:ACC_DEST_FAIL", "DestinationBalanceID": "BALANCE_DEST" }`, Balance: &BalanceFilter{ ID: utils.StringPointer("BALANCE_SRC"), Value: &utils.ValueFormula{ Static: 3, }, }, }, expectedErr: "updating destination account failed: SERVER_ERROR", }, } verifyBalance := func(t *testing.T, acc *Account, expected float64, accType, balanceType string, balanceIdx int) { t.Helper() balance := acc.BalanceMap[balanceType] balanceValue := balance[balanceIdx].Value if balanceValue != expected { t.Errorf("received wrong %s balance value: expected %v, received %v", accType, expected, balanceValue) } } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { srcAcc = tc.srcAcc destAcc = tc.destAcc err := transferBalanceAction(srcAcc, tc.act, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error %v, received %v", tc.expectedErr, err) } return } if err != nil { t.Fatal(err) } verifyBalance(t, srcAcc, tc.expectedSrcBalance, "source", utils.MetaMonetary, 0) verifyBalance(t, destAcc, tc.expectedDestBalance, "destination", utils.MetaMonetary, 0) }) } } func TestActionsAlterAndDisconnectSessions(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var alterSessionsRequest string var disconnectSessionsRequest string ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.SessionSv1AlterSessions: func(ctx *context.Context, args, reply any) error { alterSessionsRequest = utils.ToJSON(args) return nil }, utils.SessionSv1ForceDisconnect: func(ctx *context.Context, args, reply any) error { disconnectSessionsRequest = utils.ToJSON(args) return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaSessionS) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expectedRequest string expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expectedRequest: `{"Limit":1,"Filters":["*string:~*req.Account:1001","*prefix:~*req.Destination:+40"],"Tenant":"tenant.com","APIOpts":{"*radCoATemplate":"mytemplate","secondopt":"secondval"},"Event":{"Account":"1002","Destination":"+40123456"}}`, extraParams: "tenant.com;*string:~*req.Account:1001&*prefix:~*req.Destination:+40;1;*radCoATemplate:mytemplate&secondopt:secondval;Account:1002&Destination:+40123456", }, { name: "FailedRequest", expectedRequest: `{"Limit":1,"Filters":["*string:~*req.Account:1001","*prefix:~*req.Destination:+40"],"Tenant":"tenant.com","APIOpts":{"*radCoATemplate":"mytemplate","secondopt":"secondval"},"Event":{"Account":"1002","Destination":"+40123456"}}`, extraParams: "tenant.com;*string:~*req.Account:1001&*prefix:~*req.Destination:+40;1;*radCoATemplate:mytemplate&secondopt:secondval;Account:1002&Destination:+40123456", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "tenant;;1;", expectedErr: "invalid number of parameters <4> expected 5", }, { name: "InvalidEventMap", extraParams: "tenant;;1;opt:value;key", expectedErr: "invalid key-value pair: key", }, { name: "InvalidOptsMap", extraParams: "tenant;;1;opt;key:value", expectedErr: "invalid key-value pair: opt", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} t.Cleanup(func() { alterSessionsRequest = "" disconnectSessionsRequest = "" }) err := alterSessionsAction(nil, action, nil, nil, nil, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error %v, received %v", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if alterSessionsRequest != tc.expectedRequest { t.Errorf("expected: %v\nreceived: %v", tc.expectedRequest, alterSessionsRequest) } err = forceDisconnectSessionsAction(nil, action, nil, nil, nil, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error %v, received %v", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if disconnectSessionsRequest != tc.expectedRequest { t.Errorf("expected: %v\nreceived: %v", tc.expectedRequest, disconnectSessionsRequest) } }) } } func TestActionsStringMethod(t *testing.T) { cdrProvider := &cdrLogProvider{} expectedJSON := "{}" jsonStr := cdrProvider.String() if jsonStr != expectedJSON { t.Errorf("String() method result does not match expected JSON. Got: %s, Expected: %s", jsonStr, expectedJSON) } } func TestActionsClone(t *testing.T) { actions := Actions{} cloned := actions.Clone() if len(cloned) != len(actions) { t.Errorf("Expected cloned Actions length %d, got %d", len(actions), len(cloned)) } for i := range actions { if !reflect.DeepEqual(actions[i], cloned[i]) { t.Errorf("Expected cloned Action[%d] to match original, got %+v", i, cloned[i]) } } cloned = (Actions)(nil).Clone() if cloned != nil { t.Errorf("Expected cloned result to be nil when apl is nil, got %+v", cloned) } } func TestActionsNewActionConnCfgThresholdSNonSessionAction(t *testing.T) { Config := &config.CGRConfig{} act := newActionConnCfg(utils.ThresholdS, "someOtherAction", Config) if len(act.ConnIDs) != 0 { t.Errorf("Expected ConnIDs length 0, got %d", len(act.ConnIDs)) } } func TestDynamicThreshold(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var tpwo *ThresholdProfileWithAPIOpts ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetThresholdProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if tpwo, canCast = args.(*ThresholdProfileWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expTpwo *ThresholdProfileWithAPIOpts expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expTpwo: &ThresholdProfileWithAPIOpts{ ThresholdProfile: &ThresholdProfile{ Tenant: "cgrates.org", ID: "THD_ACNT_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Date(2014, 7, 29, 15, 0, 0, 0, time.UTC), }, MaxHits: 1, MinHits: 1, MinSleep: 1 * time.Second, Blocker: true, Weight: 10, ActionIDs: []string{"ACT_LOG_WARNING"}, Async: true, EeIDs: []string{"eeID1", "eeID2"}, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "cgrates.org;THD_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1;1;1s;true;10;ACT_LOG_WARNING;true;eeID1&eeID2;key:value", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expTpwo: &ThresholdProfileWithAPIOpts{ ThresholdProfile: &ThresholdProfile{ Tenant: "cgrates.org", ID: "THD_ACNT_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Now(), ExpiryTime: time.Date(3000, 7, 29, 15, 0, 0, 0, time.UTC), }, MaxHits: 1, MinHits: 1, MinSleep: 1 * time.Second, Blocker: true, Weight: 10, ActionIDs: []string{"ACT_LOG_WARNING"}, Async: true, EeIDs: []string{"eeID1", "eeID2"}, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "*tenant;THD_ACNT_<~*req.Account>;*string:~*req.Account:<~*req.Account>;*now&3000-07-29T15:00:00Z;1;1;1s;true;10;ACT_LOG_WARNING;true;eeID1&eeID2;key:value", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expTpwo: &ThresholdProfileWithAPIOpts{ ThresholdProfile: &ThresholdProfile{ Tenant: "cgrates.org", ID: "THD_ACNT_1001", FilterIDs: nil, ActivationInterval: &utils.ActivationInterval{}, MaxHits: 0, MinHits: 0, MinSleep: 0, Blocker: false, Weight: 0, ActionIDs: nil, Async: false, EeIDs: nil, }, APIOpts: map[string]any{}, }, extraParams: "cgrates.org;THD_ACNT_1001;;;;;;;;;;;", }, { name: "MissingConns", extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "tenant;;1;", expectedErr: "invalid number of parameters <4> expected 13", }, { name: "ActivationIntervalLengthFail", extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z&&;1;1;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;", expectedErr: utils.ErrUnsupportedFormat.Error(), }, { name: "ActivationIntervalBadStringFail", extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;bad String;1;1;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;", expectedErr: `Unsupported time format`, }, { name: "ActivationIntervalBadStringFail2", extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z&bad String;1;1;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;", expectedErr: `Unsupported time format`, }, { name: "MaxHitsFail", extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;BadString;1;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;", expectedErr: `strconv.Atoi: parsing "BadString": invalid syntax`, }, { name: "MinHitsFail", extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;BadString;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;", expectedErr: `strconv.Atoi: parsing "BadString": invalid syntax`, }, { name: "MinSleepFail", extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;BadString;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;", expectedErr: `time: invalid duration "BadString"`, }, { name: "BlockerFail", extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;BadString;10;ACT_LOG_WARNING;true;eeID1&eeID2;", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "WeightFail", extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;false;BadString;ACT_LOG_WARNING;true;eeID1&eeID2;", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, { name: "AsyncFail", extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;false;10;ACT_LOG_WARNING;BadString;eeID1&eeID2;", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "InvalidOptsMap", extraParams: "cgrates.org;THD_ACNT_1001;FLTR_ACNT_1001;2014-07-29T15:00:00Z;1;1;1s;false;10;ACT_LOG_WARNING;true;eeID1&eeID2;opt", expectedErr: "invalid key-value pair: opt", }, } for i, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { tpwo = nil }) err := dynamicThreshold(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(tpwo, tc.expTpwo) { if i != 1 { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expTpwo), utils.ToJSON(tpwo)) } else { // Get the absolute difference between the times diff := tpwo.ActivationInterval.ActivationTime.Sub(tc.expTpwo.ActivationInterval.ActivationTime) if diff < 0 { diff = -diff // Make sure it's positive } // Check if difference is less than or equal to 1 second if diff <= time.Second { tc.expTpwo.ActivationInterval.ActivationTime = tpwo.ActivationInterval.ActivationTime if !reflect.DeepEqual(tpwo, tc.expTpwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expTpwo), utils.ToJSON(tpwo)) } } else { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expTpwo), utils.ToJSON(tpwo)) } } } }) } } func TestDynamicStats(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var sqpwo *StatQueueProfileWithAPIOpts ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetStatQueueProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if sqpwo, canCast = args.(*StatQueueProfileWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expSqpwo *StatQueueProfileWithAPIOpts expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expSqpwo: &StatQueueProfileWithAPIOpts{ StatQueueProfile: &StatQueueProfile{ Tenant: "cgrates.org", ID: "Stat_1", FilterIDs: []string{"FLTR_STAT_1"}, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Date(2014, 7, 29, 15, 0, 0, 0, time.UTC), }, QueueLength: 100, TTL: 10 * time.Second, MinItems: 0, Metrics: []*MetricWithFilters{ { FilterIDs: []string{"Metric_FLTR"}, MetricID: "*acd", }, { FilterIDs: []string{"Metric_FLTR"}, MetricID: "*tcd", }, { FilterIDs: []string{"Metric_FLTR"}, MetricID: "*asr", }, }, Stored: false, Blocker: true, Weight: 30, ThresholdIDs: []string{"*none"}, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "cgrates.org;Stat_1;FLTR_STAT_1;2014-07-29T15:00:00Z;100;10s;0;*acd&*tcd&*asr;Metric_FLTR;false;true;30;*none;key:value", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expSqpwo: &StatQueueProfileWithAPIOpts{ StatQueueProfile: &StatQueueProfile{ Tenant: "cgrates.org", ID: "Stat_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Now(), ExpiryTime: time.Date(3000, 7, 29, 15, 0, 0, 0, time.UTC), }, QueueLength: 100, TTL: 10 * time.Second, MinItems: 0, Metrics: []*MetricWithFilters{ { FilterIDs: []string{"Metric_FLTR"}, MetricID: "*acd", }, { FilterIDs: []string{"Metric_FLTR"}, MetricID: "*tcd", }, { FilterIDs: []string{"Metric_FLTR"}, MetricID: "*asr", }, }, Stored: false, Blocker: true, Weight: 30, ThresholdIDs: []string{"*none"}, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "*tenant;Stat_<~*req.Account>;*string:~*req.Account:<~*req.Account>;*now&3000-07-29T15:00:00Z;100;10s;0;*acd&*tcd&*asr;Metric_FLTR;false;true;30;*none;key:value", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expSqpwo: &StatQueueProfileWithAPIOpts{ StatQueueProfile: &StatQueueProfile{ Tenant: "cgrates.org", ID: "Stat_1", FilterIDs: nil, ActivationInterval: &utils.ActivationInterval{}, QueueLength: 0, TTL: 0, MinItems: 0, Metrics: nil, Stored: false, Blocker: false, Weight: 0, ThresholdIDs: nil, }, APIOpts: map[string]any{}, }, extraParams: "cgrates.org;Stat_1;;;;;;;;;;;;", }, { name: "MissingConns", extraParams: "cgrates.org;Stat_1;FLTR_STAT_1;2014-07-29T15:00:00Z;100;10s;0;*acd&*tcd&*asr;Metric_FLTR;false;true;30;*none;key:value", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "tenant;;1;", expectedErr: "invalid number of parameters <4> expected 14", }, { name: "ActivationIntervalLengthFail", extraParams: "cgrates.org;Stat_1;FLTR_STAT_1;2014-07-29T15:00:00Z&&;100;10s;0;*acd&*tcd&*asr;Metric_FLTR;false;true;30;*none;key:value", expectedErr: utils.ErrUnsupportedFormat.Error(), }, { name: "ActivationIntervalBadStringFail", extraParams: "cgrates.org;Stat_1;FLTR_STAT_1;bad String;100;10s;0;*acd&*tcd&*asr;Metric_FLTR;false;true;30;*none;key:value", expectedErr: `Unsupported time format`, }, { name: "ActivationIntervalBadStringFail2", extraParams: "cgrates.org;Stat_1;FLTR_STAT_1;2014-07-29T15:00:00Z&bad String;100;10s;0;*acd&*tcd&*asr;Metric_FLTR;false;true;30;*none;key:value", expectedErr: `Unsupported time format`, }, { name: "QueueLengthFail", extraParams: "cgrates.org;Stat_1;FLTR_STAT_1;2014-07-29T15:00:00Z;BadString;10s;0;*acd&*tcd&*asr;Metric_FLTR;false;true;30;*none;key:value", expectedErr: `strconv.Atoi: parsing "BadString": invalid syntax`, }, { name: "TTLFail", extraParams: "cgrates.org;Stat_1;FLTR_STAT_1;2014-07-29T15:00:00Z;100;BadString;0;*acd&*tcd&*asr;Metric_FLTR;false;true;30;*none;key:value", expectedErr: `time: invalid duration "BadString"`, }, { name: "MinItemsFail", extraParams: "cgrates.org;Stat_1;FLTR_STAT_1;2014-07-29T15:00:00Z;100;10s;BadString;*acd&*tcd&*asr;Metric_FLTR;false;true;30;*none;key:value", expectedErr: `strconv.Atoi: parsing "BadString": invalid syntax`, }, { name: "StoredFail", extraParams: "cgrates.org;Stat_1;FLTR_STAT_1;2014-07-29T15:00:00Z;100;10s;0;*acd&*tcd&*asr;Metric_FLTR;BadString;true;30;*none;key:value", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "BlockerFail", extraParams: "cgrates.org;Stat_1;FLTR_STAT_1;2014-07-29T15:00:00Z;100;10s;0;*acd&*tcd&*asr;Metric_FLTR;false;BadString;30;*none;key:value", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "WeightFail", extraParams: "cgrates.org;Stat_1;FLTR_STAT_1;2014-07-29T15:00:00Z;100;10s;0;*acd&*tcd&*asr;Metric_FLTR;false;true;BadString;*none;key:value", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, { name: "InvalidOptsMap", extraParams: "cgrates.org;Stat_1;FLTR_STAT_1;2014-07-29T15:00:00Z;100;10s;0;*acd&*tcd&*asr;Metric_FLTR;false;true;30;*none;opt", expectedErr: "invalid key-value pair: opt", }, } for i, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { sqpwo = nil }) err := dynamicStats(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(sqpwo, tc.expSqpwo) { if i != 1 { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expSqpwo), utils.ToJSON(sqpwo)) } else { // Get the absolute difference between the times diff := sqpwo.ActivationInterval.ActivationTime.Sub(tc.expSqpwo.ActivationInterval.ActivationTime) if diff < 0 { diff = -diff // Make sure it's positive } // Check if difference is less than or equal to 1 second if diff <= time.Second { tc.expSqpwo.ActivationInterval.ActivationTime = sqpwo.ActivationInterval.ActivationTime if !reflect.DeepEqual(sqpwo, tc.expSqpwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expSqpwo), utils.ToJSON(sqpwo)) } } else { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expSqpwo), utils.ToJSON(sqpwo)) } } } }) } } func TestDynamicAttribute(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var apwo *AttributeProfileWithAPIOpts ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetAttributeProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if apwo, canCast = args.(*AttributeProfileWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expApwo *AttributeProfileWithAPIOpts expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expApwo: &AttributeProfileWithAPIOpts{ AttributeProfile: &AttributeProfile{ Tenant: "cgrates.org", ID: "Attr_1", Contexts: []string{"*sessions", "*chargers"}, FilterIDs: []string{"FLTR_ATTR_1", "FLTR_ATTR_2"}, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Date(2014, 7, 29, 15, 0, 0, 0, time.UTC), }, Attributes: []*Attribute{ { FilterIDs: []string{"AttrFltr_1", "AttrFltr2"}, Path: "*req.Subject", Type: "*constant", Value: config.NewRSRParsersMustCompile("SUPPLIER1&SUPPLIER2", "&"), }, }, Blocker: true, Weight: 10, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "cgrates.org;Attr_1;*sessions&*chargers;FLTR_ATTR_1&FLTR_ATTR_2;2014-07-29T15:00:00Z;AttrFltr_1&AttrFltr2;*req.Subject;*constant;SUPPLIER1&SUPPLIER2;true;10;key:value", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expApwo: &AttributeProfileWithAPIOpts{ AttributeProfile: &AttributeProfile{ Tenant: "cgrates.org", ID: "Attr_1001", Contexts: []string{"*any"}, FilterIDs: []string{"*string:~*req.Account:1001"}, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Now(), ExpiryTime: time.Date(3000, 7, 29, 15, 0, 0, 0, time.UTC), }, Attributes: []*Attribute{ { FilterIDs: []string{"AttrFltr_1", "AttrFltr2"}, Path: "*req.Subject", Type: "*constant", Value: config.NewRSRParsersMustCompile("SUPPLIER1", "&"), }, }, Blocker: true, Weight: 10, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "*tenant;Attr_<~*req.Account>;*any;*string:~*req.Account:<~*req.Account>;*now&3000-07-29T15:00:00Z;AttrFltr_1&AttrFltr2;*req.Subject;*constant;SUPPLIER1;true;10;key:value", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expApwo: &AttributeProfileWithAPIOpts{ AttributeProfile: &AttributeProfile{ Tenant: "cgrates.org", ID: "Attr_1", Contexts: nil, FilterIDs: nil, ActivationInterval: &utils.ActivationInterval{}, Attributes: nil, Blocker: false, Weight: 0, }, APIOpts: map[string]any{}, }, extraParams: "cgrates.org;Attr_1;;;;;;;;;;", }, { name: "MissingConns", extraParams: "cgrates.org;Attr_1;*sessions&*chargers;FLTR_ATTR_1&FLTR_ATTR_2;2014-07-29T15:00:00Z;AttrFltr_1&AttrFltr2;*req.Subject;*constant;SUPPLIER1;true;10;key:value", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "tenant;;1;", expectedErr: "invalid number of parameters <4> expected 12", }, { name: "ActivationIntervalLengthFail", extraParams: "cgrates.org;Attr_1;*sessions&*chargers;FLTR_ATTR_1&FLTR_ATTR_2;2014-07-29T15:00:00Z&&;AttrFltr_1&AttrFltr2;*req.Subject;*constant;SUPPLIER1;true;10;key:value", expectedErr: utils.ErrUnsupportedFormat.Error(), }, { name: "ActivationIntervalBadStringFail", extraParams: "cgrates.org;Attr_1;*sessions&*chargers;FLTR_ATTR_1&FLTR_ATTR_2;bad String;AttrFltr_1&AttrFltr2;*req.Subject;*constant;SUPPLIER1;true;10;key:value", expectedErr: `Unsupported time format`, }, { name: "ActivationIntervalBadStringFail2", extraParams: "cgrates.org;Attr_1;*sessions&*chargers;FLTR_ATTR_1&FLTR_ATTR_2;2014-07-29T15:00:00Z&bad String;AttrFltr_1&AttrFltr2;*req.Subject;*constant;SUPPLIER1;true;10;key:value", expectedErr: `Unsupported time format`, }, { name: "AttributesValueFail", extraParams: "cgrates.org;Attr_1;*sessions&*chargers;FLTR_ATTR_1&FLTR_ATTR_2;2014-07-29T15:00:00Z;AttrFltr_1&AttrFltr2;*req.Subject;*constant;&&&&;true;10;key:value", expectedErr: `emtpy RSRParser in rule: <>`, }, { name: "BlockerFail", extraParams: "cgrates.org;Attr_1;*sessions&*chargers;FLTR_ATTR_1&FLTR_ATTR_2;2014-07-29T15:00:00Z;AttrFltr_1&AttrFltr2;*req.Subject;*constant;SUPPLIER1;BadString;10;key:value", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "WeightFail", extraParams: "cgrates.org;Attr_1;*sessions&*chargers;FLTR_ATTR_1&FLTR_ATTR_2;2014-07-29T15:00:00Z;AttrFltr_1&AttrFltr2;*req.Subject;*constant;SUPPLIER1;true;BadString;key:value", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, { name: "InvalidOptsMap", extraParams: "cgrates.org;Attr_1;*sessions&*chargers;FLTR_ATTR_1&FLTR_ATTR_2;2014-07-29T15:00:00Z;AttrFltr_1&AttrFltr2;*req.Subject;*constant;SUPPLIER1;true;10;opt", expectedErr: "invalid key-value pair: opt", }, } for i, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { apwo = nil }) err := dynamicAttribute(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(apwo, tc.expApwo) { if i != 1 { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expApwo), utils.ToJSON(apwo)) } else { // Get the absolute difference between the times diff := apwo.ActivationInterval.ActivationTime.Sub(tc.expApwo.ActivationInterval.ActivationTime) if diff < 0 { diff = -diff // Make sure it's positive } // Check if difference is less than or equal to 1 second if diff <= time.Second { tc.expApwo.ActivationInterval.ActivationTime = apwo.ActivationInterval.ActivationTime if !reflect.DeepEqual(apwo, tc.expApwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expApwo), utils.ToJSON(apwo)) } } else { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expApwo), utils.ToJSON(apwo)) } } } }) } } func TestDynamicActionPlan(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var ap *AttrSetActionPlan ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetActionPlan: func(ctx *context.Context, args, reply any) error { var canCast bool if ap, canCast = args.(*AttrSetActionPlan); !canCast { return fmt.Errorf("couldnt cast AttrSetActionPlan") } return nil }, utils.APIerSv1GetActions: func(ctx *context.Context, args2, reply any) error { return nil }, utils.APIerSv1GetTiming: func(ctx *context.Context, args3, reply any) error { var canCast bool if args3, canCast = args3.(*utils.ArgsGetTimingID); !canCast { return fmt.Errorf("couldnt cast ArgsGetTimingID") } var exp *utils.TPTiming if utils.ToJSON(args3) == utils.ToJSON(&utils.ArgsGetTimingID{ID: "Timing_1"}) { exp = &utils.TPTiming{ ID: "Timing_1", Years: utils.Years{2025}, Months: utils.Months{2}, MonthDays: utils.MonthDays{3}, WeekDays: utils.WeekDays{1, 2, 3}, StartTime: "00:12:12", EndTime: "12:12:12", } } if utils.ToJSON(args3) == utils.ToJSON(&utils.ArgsGetTimingID{ID: "*asap"}) { exp = &utils.TPTiming{ ID: "*asap", Years: utils.Years{2025}, Months: utils.Months{}, MonthDays: utils.MonthDays{}, WeekDays: utils.WeekDays{}, StartTime: "*asap", } } if exp == nil { return utils.ErrNotFound } *reply.(*utils.TPTiming) = *exp return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expAp *AttrSetActionPlan expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expAp: &AttrSetActionPlan{ Id: "ActPl_1", ActionPlan: []*AttrActionPlan{ { ActionsId: "Action_1", TimingID: "Timing_1", Years: "2025", Months: "2", MonthDays: "3", WeekDays: "1;2;3", Time: "00:12:12;12:12:12", Weight: 10, }, }, Overwrite: true, ReloadScheduler: true, }, extraParams: "ActPl_1;Action_1;Timing_1;10;true", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expAp: &AttrSetActionPlan{ Id: "ActPl_1", ActionPlan: []*AttrActionPlan{ { ActionsId: "Action_1001", TimingID: "*asap", Years: "2025", Months: "*any", MonthDays: "*any", WeekDays: "*any", Time: "*asap", Weight: 10, }, }, Overwrite: false, ReloadScheduler: true, }, extraParams: "ActPl_1;Action_<~*req.Account>;*asap;10;", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expAp: &AttrSetActionPlan{ Id: "ActPl_1", ActionPlan: []*AttrActionPlan{ { ActionsId: "Action_1", TimingID: "", Weight: 0, }, }, Overwrite: false, ReloadScheduler: true, }, extraParams: "ActPl_1;Action_1;;;", }, { name: "MissingConns", extraParams: "ActPl_1;Action_1;*asap;10;", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "ActPl_1;Action_1;*asap;10;;;", expectedErr: "invalid number of parameters <7> expected 5", }, { name: "ActionIdEmptyFail", extraParams: "ActPl_1;;*asap;10;", expectedErr: `empty ActionsId for dynamic_action_plan`, }, { name: "WeightFail", connIDs: []string{connID}, extraParams: "ActPl_1;Action_1;*asap;BadString;", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { ap = nil }) err := dynamicActionPlan(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if utils.ToJSON(ap) != utils.ToJSON(tc.expAp) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAp), utils.ToJSON(ap)) } }) } } func TestDynamicActionPlanAccount(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var ap *AttrSetActionPlanAccounts ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetActionPlanAccounts: func(ctx *context.Context, args, reply any) error { var canCast bool if ap, canCast = args.(*AttrSetActionPlanAccounts); !canCast { return fmt.Errorf("couldnt cast AttrSetActionPlanAccounts") } return nil }, utils.APIerSv1GetActions: func(ctx *context.Context, args2, reply any) error { return nil }, utils.APIerSv1GetTiming: func(ctx *context.Context, args3, reply any) error { var canCast bool if args3, canCast = args3.(*utils.ArgsGetTimingID); !canCast { return fmt.Errorf("couldnt cast ArgsGetTimingID") } var exp *utils.TPTiming if utils.ToJSON(args3) == utils.ToJSON(&utils.ArgsGetTimingID{ID: "Timing_1"}) { exp = &utils.TPTiming{ ID: "Timing_1", Years: utils.Years{2025}, Months: utils.Months{2}, MonthDays: utils.MonthDays{3}, WeekDays: utils.WeekDays{1, 2, 3}, StartTime: "00:12:12", EndTime: "12:12:12", } } if utils.ToJSON(args3) == utils.ToJSON(&utils.ArgsGetTimingID{ID: "*asap"}) { exp = &utils.TPTiming{ ID: "*asap", Years: utils.Years{2025}, Months: utils.Months{}, MonthDays: utils.MonthDays{}, WeekDays: utils.WeekDays{}, StartTime: "*asap", } } if exp == nil { return utils.ErrNotFound } *reply.(*utils.TPTiming) = *exp return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expAp *AttrSetActionPlanAccounts expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expAp: &AttrSetActionPlanAccounts{ Id: "ActPl_1", ActionPlan: []*AttrActionPlan{ { ActionsId: "Action_1", TimingID: "Timing_1", Years: "2025", Months: "2", MonthDays: "3", WeekDays: "1;2;3", Time: "00:12:12;12:12:12", Weight: 10, }, }, AccountIDs: []string{"cgrates.org:1001"}, Overwrite: true, ReloadScheduler: true, }, extraParams: "ActPl_1;Action_1;Timing_1;10;true;cgrates.org:1001", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expAp: &AttrSetActionPlanAccounts{ Id: "ActPl_1", ActionPlan: []*AttrActionPlan{ { ActionsId: "Action_1001", TimingID: "*asap", Years: "2025", Months: "*any", MonthDays: "*any", WeekDays: "*any", Time: "*asap", Weight: 10, }, }, AccountIDs: []string{"cgrates.org:1001"}, Overwrite: false, ReloadScheduler: true, }, extraParams: "ActPl_1;Action_<~*req.Account>;*asap;10;;<*tenant+:+~*req.Account>", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expAp: &AttrSetActionPlanAccounts{ Id: "ActPl_1", ActionPlan: []*AttrActionPlan{ { ActionsId: "Action_1", TimingID: "", Weight: 0, }, }, Overwrite: false, ReloadScheduler: true, }, extraParams: "ActPl_1;Action_1;;;;", }, { name: "MissingConns", extraParams: "ActPl_1;Action_1;*asap;10;;", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "ActPl_1;Action_1;*asap;10;;;", expectedErr: "invalid number of parameters <7> expected 6", }, { name: "ActionIdEmptyFail", extraParams: "ActPl_1;;*asap;10;;", expectedErr: `empty ActionsId for dynamic_action_plan`, }, { name: "WeightFail", connIDs: []string{connID}, extraParams: "ActPl_1;Action_1;*asap;BadString;;", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { ap = nil }) err := dynamicActionPlanAccount(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if utils.ToJSON(ap) != utils.ToJSON(tc.expAp) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAp), utils.ToJSON(ap)) } }) } } func TestDynamicActionAll(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var a *utils.AttrSetActions ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv2SetActions: func(ctx *context.Context, args, reply any) error { var canCast bool if a, canCast = args.(*utils.AttrSetActions); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expA *utils.AttrSetActions expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expA: &utils.AttrSetActions{ ActionsId: "Alter_Session_10", Actions: []*utils.TPAction{ { Identifier: utils.MetaAlterSessions, ExtraParameters: "cgrates.org;*string:~*req.Account:1001;1;*radCoATemplate:mycoa;CustomFilter:mycustomvalue", Filters: "*string:~*req.Account:1001;filter2", BalanceId: "badID", BalanceType: utils.MetaMonetary, Categories: "call;data", DestinationIds: "1002;1003", RatingSubject: "SPECIAL_1002", SharedGroups: "SHARED_A;SHARED_B", ExpiryTime: "*unlimited", TimingTags: "weekdays;offpeak", Units: "10", BalanceWeight: "10", BalanceBlocker: "true", BalanceDisabled: "true", Weight: 10, }, }, Overwrite: false, }, extraParams: "Alter_Session_10;*alter_sessions;\fcgrates.org;*string:~*req.Account:1001;1;*radCoATemplate:mycoa;CustomFilter:mycustomvalue\f;*string:~*req.Account:1001&filter2;badID;*monetary;call&data;1002&1003;SPECIAL_1002;SHARED_A&SHARED_B;*unlimited;weekdays&offpeak;10;10;true;true;10;false", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expA: &utils.AttrSetActions{ ActionsId: "CDR_Log_1001", Actions: []*utils.TPAction{ { Identifier: utils.CDRLog, ExtraParameters: "{\"Account\":\"1001\",\"RequestType\":\"*pseudoprepaid\",\"Subject\":\"DifferentThanAccount\", \"ToR\":\"~ActionType:s/^\\*(.*)$/did_$1/\"}", Filters: "*string:~*req.Account:1001;filter2", BalanceId: "badID", BalanceType: utils.MetaMonetary, Categories: "call;data", DestinationIds: "1002;1003", RatingSubject: "SPECIAL_1002", SharedGroups: "SHARED_A;SHARED_B", ExpiryTime: "*unlimited", TimingTags: "weekdays;offpeak", Units: "10", BalanceWeight: "10", BalanceBlocker: "true", BalanceDisabled: "true", Weight: 10, }, }, Overwrite: true, }, extraParams: "CDR_Log_<~*req.Account>;*cdrlog;\f{\"Account\":\"<~*req.Account>\",\"RequestType\":\"*pseudoprepaid\",\"Subject\":\"DifferentThanAccount\", \"ToR\":\"~ActionType:s/^\\*(.*)$/did_$1/\"}\f;*string:~*req.Account:<~*req.Account>&filter2;badID;*monetary;call&data;1002&1003;SPECIAL_1002;SHARED_A&SHARED_B;*unlimited;weekdays&offpeak;10;10;true;true;10;true", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expA: &utils.AttrSetActions{ ActionsId: "DISABLE_ACC", Actions: []*utils.TPAction{ { Identifier: utils.MetaDisableAccount, }, }, }, extraParams: "DISABLE_ACC;*disable_account;;;;;;;;;;;;;;;;", }, { name: "MissingConns", extraParams: "TOPUP_MONETARY_10;*topup;;*string:~*req.Account:1001&filter2;badID;*monetary;call&data;1002&1003;SPECIAL_1002;SHARED_A&SHARED_B;*unlimited;weekdays&offpeak;10;10;true;true;10;", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "TOPUP_MONETARY_10;*topup;;*string:~*req.Account:1001&filter2;badID;*monetary;call&data;1002&1003;SPECIAL_1002;SHARED_A&SHARED_B;*unlimited;weekdays&offpeak;10;10;true;true;10;;;", expectedErr: "invalid number of parameters <20> expected 18", }, { name: "ActionIdEmptyFail", extraParams: "DISABLE_ACC;;;;;;;;;;;;;;;;;", expectedErr: `empty Action for dynamic_action`, }, { name: "ActionIdEmptyFail", extraParams: ";;;;;;;;;;;;;;;;;", expectedErr: `empty ActionsId for dynamic_action`, }, { name: "WeightFail", extraParams: "TOPUP_MONETARY_10;*topup;;*string:~*req.Account:1001&filter2;badID;*monetary;call&data;1002&1003;SPECIAL_1002;SHARED_A&SHARED_B;*unlimited;weekdays&offpeak;10;10;true;true;BadString;", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { a = nil }) err := dynamicAction(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if utils.ToJSON(a) != utils.ToJSON(tc.expA) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expA), utils.ToJSON(a)) } }) } } func TestDynamicDestination(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var dest *utils.AttrSetDestination ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetDestination: func(ctx *context.Context, args, reply any) error { var canCast bool if dest, canCast = args.(*utils.AttrSetDestination); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expDest *utils.AttrSetDestination expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expDest: &utils.AttrSetDestination{ Id: "DST_1002&1003", Prefixes: []string{"1002", "1003"}, Overwrite: false, }, extraParams: "DST_1002&1003;1002&1003", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expDest: &utils.AttrSetDestination{ Id: "DST_1001&1003", Prefixes: []string{"1001", "1003"}, Overwrite: false, }, extraParams: "DST_1001&1003;<~*req.Account>&1003", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expDest: &utils.AttrSetDestination{ Id: "DST_Any", Overwrite: false, }, extraParams: "DST_Any;", }, { name: "MissingConns", extraParams: "DST_1002&1003;1002&1003", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "DST_1002&1003;1002&1003;;;", expectedErr: "invalid number of parameters <5> expected 2", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { dest = nil }) err := dynamicDestination(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if utils.ToJSON(dest) != utils.ToJSON(tc.expDest) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expDest), utils.ToJSON(dest)) } }) } } func TestDynamicFilter(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var fwo *FilterWithAPIOpts ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetFilter: func(ctx *context.Context, args, reply any) error { var canCast bool if fwo, canCast = args.(*FilterWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expFwo *FilterWithAPIOpts expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expFwo: &FilterWithAPIOpts{ Filter: &Filter{ Tenant: "cgrates.org", ID: "Fltr_1", Rules: []*FilterRule{ { Type: "*string", Element: "~*req.Account", Values: []string{"1001", "1002"}, }, }, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Date(2014, 7, 29, 15, 0, 0, 0, time.UTC), }, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "cgrates.org;Fltr_1;*string;~*req.Account;1001&1002;2014-07-29T15:00:00Z;key:value", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expFwo: &FilterWithAPIOpts{ Filter: &Filter{ Tenant: "cgrates.org", ID: "Fltr_1001", Rules: []*FilterRule{ { Type: "*string", Element: "~*req.Account", Values: []string{"1001", "1002"}, }, }, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Now(), ExpiryTime: time.Date(3000, 7, 29, 15, 0, 0, 0, time.UTC), }, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "*tenant;Fltr_<~*req.Account>;*string;~*req.<~*req.ExtraInfo>;<~*req.Account>&1002;*now&3000-07-29T15:00:00Z;key:value", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expFwo: &FilterWithAPIOpts{ Filter: &Filter{ Tenant: "cgrates.org", ID: "Fltr_1001", Rules: []*FilterRule{ { Type: "", Element: "", Values: nil, }, }, ActivationInterval: &utils.ActivationInterval{}, }, APIOpts: map[string]any{}, }, extraParams: "*tenant;Fltr_1001;;;;;", }, { name: "MissingConns", extraParams: "cgrates.org;Fltr_1;*string;~*req.Account;1001&1002;2014-07-29T15:00:00Z;key:value", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "tenant;;;", expectedErr: "invalid number of parameters <4> expected 7", }, { name: "ActivationIntervalLengthFail", extraParams: "cgrates.org;Fltr_1;*string;~*req.Account;1001&1002;2014-07-29T15:00:00Z&&;key:value", expectedErr: utils.ErrUnsupportedFormat.Error(), }, { name: "ActivationIntervalBadStringFail", extraParams: "cgrates.org;Fltr_1;*string;~*req.Account;1001&1002;bad String;key:value", expectedErr: `Unsupported time format`, }, { name: "ActivationIntervalBadStringFail2", extraParams: "cgrates.org;Fltr_1;*string;~*req.Account;1001&1002;2014-07-29T15:00:00Z&bad String;key:value", expectedErr: `Unsupported time format`, }, { name: "InvalidOptsMap", extraParams: "cgrates.org;Fltr_1;*string;~*req.Account;1001&1002;2014-07-29T15:00:00Z;opt", expectedErr: "invalid key-value pair: opt", }, } for i, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", utils.ExtraInfo: utils.AccountField, }, } t.Cleanup(func() { fwo = nil }) err := dynamicFilter(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(fwo, tc.expFwo) { if i != 1 { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expFwo), utils.ToJSON(fwo)) } else { // Get the absolute difference between the times diff := fwo.ActivationInterval.ActivationTime.Sub(tc.expFwo.ActivationInterval.ActivationTime) if diff < 0 { diff = -diff // Make sure it's positive } // Check if difference is less than or equal to 1 second if diff <= time.Second { tc.expFwo.ActivationInterval.ActivationTime = fwo.ActivationInterval.ActivationTime if !reflect.DeepEqual(fwo, tc.expFwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expFwo), utils.ToJSON(fwo)) } } else { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expFwo), utils.ToJSON(fwo)) } } } }) } } func TestDynamicRoute(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var rwo *RouteWithAPIOpts ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetRouteProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if rwo, canCast = args.(*RouteWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } return nil }, utils.APIerSv1GetRouteProfile: func(ctx *context.Context, args2, reply any) error { return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expRwo *RouteWithAPIOpts expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expRwo: &RouteWithAPIOpts{ RouteProfile: &RouteProfile{ Tenant: "cgrates.org", ID: "RTP_ACNT_1001", FilterIDs: []string{"*string:~*req.Account:1001", "*string:~*req.Account:1002"}, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Date(2014, 7, 29, 15, 0, 0, 0, time.UTC), }, SortingParameters: []string{"*acd", "*tcc"}, Routes: []*Route{ { ID: "route1", FilterIDs: []string{"*string:~*req.Account:1001", "*string:~*req.Account:1002"}, AccountIDs: []string{"1001", "1002"}, RatingPlanIDs: []string{"RP1", "RP2"}, ResourceIDs: []string{"RS1", "RS2"}, StatIDs: []string{"Stat_1", "Stat_1_1"}, Weight: 10, Blocker: true, RouteParameters: "param", }, }, Sorting: utils.MetaWeight, Weight: 10, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "cgrates.org;RTP_ACNT_1001;*string:~*req.Account:1001&*string:~*req.Account:1002;2014-07-29T15:00:00Z;*weight;*acd&*tcc;route1;*string:~*req.Account:1001&*string:~*req.Account:1002;1001&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;true;param;10;key:value", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expRwo: &RouteWithAPIOpts{ RouteProfile: &RouteProfile{ Tenant: "cgrates.org", ID: "RTP_ACNT_1001", FilterIDs: []string{"*string:~*req.Account:1001", "*string:~*req.Account:1002"}, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Now(), ExpiryTime: time.Date(3000, 7, 29, 15, 0, 0, 0, time.UTC), }, SortingParameters: []string{"*acd", "*tcc"}, Routes: []*Route{ { ID: "route1", FilterIDs: []string{"*string:~*req.Account:1001", "*string:~*req.Account:1002"}, AccountIDs: []string{"1001", "1002"}, RatingPlanIDs: []string{"RP1", "RP2"}, ResourceIDs: []string{"RS1", "RS2"}, StatIDs: []string{"Stat_1", "Stat_1_1"}, Weight: 10, Blocker: true, RouteParameters: "param", }, }, Sorting: utils.MetaWeight, Weight: 10, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "*tenant;route1@RTP_ACNT_1001;*string:~*req.Account:<~*req.Account>&*string:~*req.Account:1002;*now&3000-07-29T15:00:00Z;*weight;*acd&*tcc;route1;*string:~*req.Account:<~*req.Account>&*string:~*req.Account:1002;<~*req.Account>&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;true;param;10;key:value", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expRwo: &RouteWithAPIOpts{ RouteProfile: &RouteProfile{ Tenant: "cgrates.org", ID: "RTP_ACNT_1001", FilterIDs: nil, ActivationInterval: nil, SortingParameters: nil, Routes: []*Route{ { ID: "route1", FilterIDs: nil, AccountIDs: nil, RatingPlanIDs: nil, ResourceIDs: nil, StatIDs: nil, Weight: 0, Blocker: false, RouteParameters: "", }, }, Sorting: "", Weight: 0, }, APIOpts: map[string]any{}, }, extraParams: "cgrates.org;RTP_ACNT_1001;;;;;route1;;;;;;;;;;", }, { name: "MissingConns", extraParams: "cgrates.org;RTP_ACNT_1001;*string:~*req.Account:1001&*string:~*req.Account:1002;2014-07-29T15:00:00Z;*weight;*acd&*tcc;route1;*string:~*req.Account:1001&*string:~*req.Account:1002;1001&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;true;param;10;key:value", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "tenant;RTP1;;", expectedErr: "invalid number of parameters <4> expected 17", }, { name: "RouteProfileIDFail", extraParams: "cgrates.org;@@@;*string:~*req.Account:1001&*string:~*req.Account:1002;2014-07-29T15:00:00Z;*weight;*acd&*tcc;route1;*string:~*req.Account:1001&*string:~*req.Account:1002;1001&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;true;param;10;key:value", expectedErr: `more than 1 "@" character for RouteProfileID: <@@@>`, }, { name: "ActivationIntervalLengthFail", extraParams: "cgrates.org;RTP_ACNT_1001;*string:~*req.Account:1001&*string:~*req.Account:1002;2014-07-29T15:00:00Z&&;*weight;*acd&*tcc;route1;*string:~*req.Account:1001&*string:~*req.Account:1002;1001&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;true;param;10;key:value", expectedErr: utils.ErrUnsupportedFormat.Error(), }, { name: "ActivationIntervalBadStringFail", extraParams: "cgrates.org;RTP_ACNT_1001;*string:~*req.Account:1001&*string:~*req.Account:1002;bad String;*weight;*acd&*tcc;route1;*string:~*req.Account:1001&*string:~*req.Account:1002;1001&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;true;param;10;key:value", expectedErr: `Unsupported time format`, }, { name: "ActivationIntervalBadStringFail2", extraParams: "cgrates.org;RTP_ACNT_1001;*string:~*req.Account:1001&*string:~*req.Account:1002;2014-07-29T15:00:00Z&bad String;*weight;*acd&*tcc;route1;*string:~*req.Account:1001&*string:~*req.Account:1002;1001&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;true;param;10;key:value", expectedErr: `Unsupported time format`, }, { name: "RouteWeightFail", extraParams: "cgrates.org;RTP_ACNT_1001;*string:~*req.Account:1001&*string:~*req.Account:1002;2014-07-29T15:00:00Z;*weight;*acd&*tcc;route1;*string:~*req.Account:1001&*string:~*req.Account:1002;1001&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;BadString;true;param;10;key:value", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, { name: "RouteBlockerFail", extraParams: "cgrates.org;RTP_ACNT_1001;*string:~*req.Account:1001&*string:~*req.Account:1002;2014-07-29T15:00:00Z;*weight;*acd&*tcc;route1;*string:~*req.Account:1001&*string:~*req.Account:1002;1001&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;BadString;param;10;key:value", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "WeightFail", extraParams: "cgrates.org;RTP_ACNT_1001;*string:~*req.Account:1001&*string:~*req.Account:1002;2014-07-29T15:00:00Z;*weight;*acd&*tcc;route1;*string:~*req.Account:1001&*string:~*req.Account:1002;1001&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;true;param;BadString;key:value", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, { name: "InvalidOptsMap", extraParams: "cgrates.org;RTP_ACNT_1001;*string:~*req.Account:1001&*string:~*req.Account:1002;2014-07-29T15:00:00Z;*weight;*acd&*tcc;route1;*string:~*req.Account:1001&*string:~*req.Account:1002;1001&1002;RP1&RP2;RS1&RS2;Stat_1&Stat_1_1;10;true;param;10;opt", expectedErr: "invalid key-value pair: opt", }, } for i, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { rwo = nil }) err := dynamicRoute(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(rwo, tc.expRwo) { if i != 1 { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRwo), utils.ToJSON(rwo)) } else { // Get the absolute difference between the times diff := rwo.ActivationInterval.ActivationTime.Sub(tc.expRwo.ActivationInterval.ActivationTime) if diff < 0 { diff = -diff // Make sure it's positive } // Check if difference is less than or equal to 1 second if diff <= time.Second { tc.expRwo.ActivationInterval.ActivationTime = rwo.ActivationInterval.ActivationTime if !reflect.DeepEqual(rwo, tc.expRwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRwo), utils.ToJSON(rwo)) } } else { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRwo), utils.ToJSON(rwo)) } } } }) } } func TestDynamicRanking(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var rpwo *RankingProfileWithAPIOpts ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetRankingProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if rpwo, canCast = args.(*RankingProfileWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expRpwo *RankingProfileWithAPIOpts expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expRpwo: &RankingProfileWithAPIOpts{ RankingProfile: &RankingProfile{ Tenant: "cgrates.org", ID: "RKNG_ACNT_1001", Schedule: "@every 15m", StatIDs: []string{"Stats2", "Stats3", "Stats4"}, MetricIDs: []string{"Metric1", "Metric3"}, Sorting: "*asc", SortingParameters: []string{"metricA:true", "metricB:false"}, Stored: true, ThresholdIDs: []string{"THD1", "THD2"}, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "cgrates.org;RKNG_ACNT_1001;@every 15m;Stats2&Stats3&Stats4;Metric1&Metric3;*asc;metricA:true&metricB:false;true;THD1&THD2;key:value", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expRpwo: &RankingProfileWithAPIOpts{ RankingProfile: &RankingProfile{ Tenant: "cgrates.org", ID: "RKNG_ACNT_1001", Schedule: "@every 15m", StatIDs: []string{"Stats2", "Stats3", "Stats4"}, MetricIDs: []string{"Metric1", "Metric3"}, Sorting: "*asc", SortingParameters: []string{"metricA:true", "metricB:false"}, Stored: true, ThresholdIDs: []string{"THD1", "THD2"}, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "*tenant;RKNG_ACNT_<~*req.Account>;@every 15m;Stats2&Stats3&Stats4;Metric1&Metric3;*asc;metricA:true&metricB:false;true;THD1&THD2;key:value", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expRpwo: &RankingProfileWithAPIOpts{ RankingProfile: &RankingProfile{ Tenant: "cgrates.org", ID: "RKNG_ACNT_1001", Schedule: "@every 15m", StatIDs: nil, MetricIDs: nil, Sorting: "", SortingParameters: nil, Stored: false, ThresholdIDs: nil, }, APIOpts: map[string]any{}, }, extraParams: "cgrates.org;RKNG_ACNT_1001;@every 15m;;;;;;;", }, { name: "MissingConns", extraParams: "cgrates.org;RKNG_ACNT_1001;@every 15m;Stats2&Stats3&Stats4;Metric1&Metric3;*asc;metricA:true&metricB:false;true;THD1&THD2;key:value", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "tenant;RKNG;;", expectedErr: "invalid number of parameters <4> expected 10", }, { name: "RankingStoredFail", extraParams: "cgrates.org;RKNG_ACNT_1001;@every 15m;Stats2&Stats3&Stats4;Metric1&Metric3;*asc;metricA:true&metricB:false;BadString;THD1&THD2;key:value", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "InvalidOptsMap", extraParams: "cgrates.org;RKNG_ACNT_1001;@every 15m;Stats2&Stats3&Stats4;Metric1&Metric3;*asc;metricA:true&metricB:false;true;THD1&THD2;opt", expectedErr: "invalid key-value pair: opt", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { rpwo = nil }) err := dynamicRanking(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(rpwo, tc.expRpwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpwo), utils.ToJSON(rpwo)) } }) } } func TestDynamicRatingProfile(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var rpwo *utils.AttrSetRatingProfile ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetRatingProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if rpwo, canCast = args.(*utils.AttrSetRatingProfile); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expRpwo *utils.AttrSetRatingProfile expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expRpwo: &utils.AttrSetRatingProfile{ Tenant: "cgrates.org", Category: "call", Subject: utils.MetaAny, RatingPlanActivations: []*utils.TPRatingActivation{ { ActivationTime: "2014-07-29T15:00:00Z", RatingPlanId: "RP_TESTIT1", FallbackSubjects: "RP_TEST", }, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "cgrates.org;call;*any;2014-07-29T15:00:00Z;RP_TESTIT1;RP_TEST;key:value", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expRpwo: &utils.AttrSetRatingProfile{ Tenant: "cgrates.org", Category: "call", Subject: "1001", RatingPlanActivations: []*utils.TPRatingActivation{ { ActivationTime: "*now", RatingPlanId: "RP_TEST1001", FallbackSubjects: "RP_TEST", }, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "*tenant;call;~*req.Account;*now;RP_TEST<~*req.Account>;RP_TEST;key:value", }, { name: "MissingConns", extraParams: "cgrates.org;call;*any;2014-07-29T15:00:00Z;RP_TESTIT1;RP_TEST;key:value", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "cgrates.org;;;", expectedErr: "invalid number of parameters <4> expected 7", }, { name: "InvalidOptsMap", extraParams: "cgrates.org;call;*any;2014-07-29T15:00:00Z;RP_TESTIT1;RP_TEST;opt", expectedErr: "invalid key-value pair: opt", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { rpwo = nil }) err := dynamicRatingProfile(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(rpwo, tc.expRpwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpwo), utils.ToJSON(rpwo)) } }) } } func TestDynamicTrend(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var tpwo *TrendProfileWithAPIOpts ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetTrendProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if tpwo, canCast = args.(*TrendProfileWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expTpwo *TrendProfileWithAPIOpts expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expTpwo: &TrendProfileWithAPIOpts{ TrendProfile: &TrendProfile{ Tenant: "cgrates.org", ID: "TREND1", Schedule: "0 12 * * *", StatID: "Stats2", Metrics: []string{"*acc", "*tcc"}, TTL: -1, QueueLength: -1, MinItems: 1, CorrelationType: "*average", Tolerance: 2.1, Stored: true, ThresholdIDs: []string{"TD1", "TD2"}, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;-1;1;*average;2.1;true;TD1&TD2;key:value", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expTpwo: &TrendProfileWithAPIOpts{ TrendProfile: &TrendProfile{ Tenant: "cgrates.org", ID: "TREND_1001", Schedule: "0 12 * * *", StatID: "Stats2", Metrics: []string{"*acc", "*tcc"}, TTL: -1, QueueLength: -1, MinItems: 1, CorrelationType: "*average", Tolerance: 2.1, Stored: true, ThresholdIDs: []string{"TD1", "TD2"}, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "*tenant;TREND_<~*req.Account>;0 12 * * *;Stats2;*acc&*tcc;-1;-1;1;*average;2.1;true;TD1&TD2;key:value", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expTpwo: &TrendProfileWithAPIOpts{ TrendProfile: &TrendProfile{ Tenant: "cgrates.org", ID: "TREND_1001", Schedule: "0 12 * * *", StatID: "", Metrics: nil, TTL: 0, QueueLength: 0, MinItems: 0, CorrelationType: "", Tolerance: 0, Stored: false, ThresholdIDs: nil, }, APIOpts: map[string]any{}, }, extraParams: "cgrates.org;TREND_1001;0 12 * * *;;;;;;;;;;", }, { name: "MissingConns", extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;-1;1;*average;2.1;true;TD1&TD2;key:value", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "tenant;TREND1;;", expectedErr: "invalid number of parameters <4> expected 13", }, { name: "TTLFail", extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;BadString;-1;1;*average;2.1;true;TD1&TD2;key:value", expectedErr: `time: invalid duration "BadString"`, }, { name: "QueueLengthFail", extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;BadString;1;*average;2.1;true;TD1&TD2;key:value", expectedErr: `strconv.Atoi: parsing "BadString": invalid syntax`, }, { name: "MinItemsFail", extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;-1;BadString;*average;2.1;true;TD1&TD2;key:value", expectedErr: `strconv.Atoi: parsing "BadString": invalid syntax`, }, { name: "ToleranceFail", extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;-1;1;*average;BadString;true;TD1&TD2;key:value", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, { name: "StoredFail", extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;-1;1;*average;2.1;BadString;TD1&TD2;key:value", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "InvalidOptsMap", extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;-1;1;*average;2.1;true;TD1&TD2;opt", expectedErr: "invalid key-value pair: opt", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { tpwo = nil }) err := dynamicTrend(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(tpwo, tc.expTpwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expTpwo), utils.ToJSON(tpwo)) } }) } } func TestDynamicResource(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var rpwo *ResourceProfileWithAPIOpts ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetResourceProfile: func(ctx *context.Context, args, reply any) error { var canCast bool if rpwo, canCast = args.(*ResourceProfileWithAPIOpts); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expRpwo *ResourceProfileWithAPIOpts expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expRpwo: &ResourceProfileWithAPIOpts{ ResourceProfile: &ResourceProfile{ Tenant: "cgrates.org", ID: "RES_ACNT_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Date(2014, 7, 29, 15, 0, 0, 0, time.UTC), }, UsageTTL: time.Hour, Limit: 1, AllocationMessage: "msg_1001", Blocker: true, Stored: true, Weight: 10, ThresholdIDs: []string{"TD1", "TD2"}, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;1;msg_1001;true;true;10;TD1&TD2;key:value", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expRpwo: &ResourceProfileWithAPIOpts{ ResourceProfile: &ResourceProfile{ Tenant: "cgrates.org", ID: "RES_ACNT_1001", FilterIDs: []string{"*string:~*req.Account:1001"}, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Now(), ExpiryTime: time.Date(3000, 7, 29, 15, 0, 0, 0, time.UTC), }, UsageTTL: time.Hour, Limit: 1, AllocationMessage: "msg_1001", Blocker: true, Stored: true, Weight: 10, ThresholdIDs: []string{"TD1", "TD2"}, }, APIOpts: map[string]any{ "key": "value", }, }, extraParams: "*tenant;RES_ACNT_<~*req.Account>;*string:~*req.Account:<~*req.Account>;*now&3000-07-29T15:00:00Z;1h;1;msg_<~*req.Account>;true;true;10;TD1&TD2;key:value", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expRpwo: &ResourceProfileWithAPIOpts{ ResourceProfile: &ResourceProfile{ Tenant: "cgrates.org", ID: "RES_ACNT_1001", FilterIDs: nil, ActivationInterval: &utils.ActivationInterval{}, UsageTTL: 0, Limit: 0, AllocationMessage: "", Blocker: false, Stored: false, Weight: 0, ThresholdIDs: nil, }, APIOpts: map[string]any{}, }, extraParams: "cgrates.org;RES_ACNT_1001;;;;;;;;;;", }, { name: "MissingConns", extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;1;msg_1001;true;true;10;TD1&TD2;key:value", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "tenant;RSC;;", expectedErr: "invalid number of parameters <4> expected 12", }, { name: "ActivationIntervalLengthFail", extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z&&;1h;1;msg_1001;true;true;10;TD1&TD2;key:value", expectedErr: utils.ErrUnsupportedFormat.Error(), }, { name: "ActivationIntervalBadStringFail", extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;bad String;1h;1;msg_1001;true;true;10;TD1&TD2;key:value", expectedErr: `Unsupported time format`, }, { name: "ActivationIntervalBadStringFail2", extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z&bad String;1h;1;msg_1001;true;true;10;TD1&TD2;key:value", expectedErr: `Unsupported time format`, }, { name: "TTLFail", extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;BadString;1;msg_1001;true;true;10;TD1&TD2;key:value", expectedErr: `time: invalid duration "BadString"`, }, { name: "LimitFail", extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;BadString;msg_1001;true;true;10;TD1&TD2;key:value", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, { name: "BlockerFail", extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;1;msg_1001;BadString;true;10;TD1&TD2;key:value", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "StoredFail", extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;1;msg_1001;true;BadString;10;TD1&TD2;key:value", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "WeightFail", extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;1;msg_1001;true;true;BadString;TD1&TD2;key:value", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, { name: "InvalidOptsMap", extraParams: "cgrates.org;RES_ACNT_1001;*string:~*req.Account:1001;2014-07-29T15:00:00Z;1h;1;msg_1001;true;true;10;TD1&TD2;opt", expectedErr: "invalid key-value pair: opt", }, } for i, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { rpwo = nil }) err := dynamicResource(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(rpwo, tc.expRpwo) { if i != 1 { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpwo), utils.ToJSON(rpwo)) } else { // Get the absolute difference between the times diff := rpwo.ActivationInterval.ActivationTime.Sub(tc.expRpwo.ActivationInterval.ActivationTime) if diff < 0 { diff = -diff // Make sure it's positive } // Check if difference is less than or equal to 1 second if diff <= time.Second { tc.expRpwo.ActivationInterval.ActivationTime = rpwo.ActivationInterval.ActivationTime if !reflect.DeepEqual(rpwo, tc.expRpwo) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpwo), utils.ToJSON(rpwo)) } } else { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expRpwo), utils.ToJSON(rpwo)) } } } }) } } func TestDynamicActionTrigger(t *testing.T) { tempConn := connMgr tmpDm := dm tmpCache := Cache defer func() { config.SetCgrConfig(config.NewDefaultCGRConfig()) SetConnManager(tempConn) dm = tmpDm Cache = tmpCache }() Cache.Clear(nil) var at *AttrSetActionTrigger ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.APIerSv1SetActionTrigger: func(ctx *context.Context, args, reply any) error { var canCast bool if at, canCast = args.(*AttrSetActionTrigger); !canCast { return fmt.Errorf("couldnt cast") } return nil }, }, } connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) clientconn := make(chan birpc.ClientConnector, 1) clientconn <- ccMock NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ connID: clientconn, }) testcases := []struct { name string extraParams string connIDs []string expAt *AttrSetActionTrigger expectedErr string }{ { name: "SuccessfulRequest", connIDs: []string{connID}, expAt: &AttrSetActionTrigger{ GroupID: "STANDARD_TRIGGERS", UniqueID: "uid", ActionTrigger: map[string]any{ utils.ThresholdType: "*max_balance", utils.ThresholdValue: float64(20), utils.Recurrent: true, utils.MinSleep: time.Second, utils.ActivationDate: time.Date(2014, 7, 29, 15, 0, 0, 0, time.UTC), utils.BalanceID: "*default", utils.BalanceType: "*monetary", utils.BalanceCategories: []string{utils.Call, "data"}, utils.BalanceDestinationIds: []string{"DST1", "DST2"}, utils.BalanceRatingSubject: "SPECIAL_1002", utils.BalanceSharedGroups: []string{"SHRGroup1", "SHRGroup2"}, utils.BalanceExpirationDate: time.Date(2030, 7, 29, 15, 0, 0, 0, time.UTC), utils.BalanceTimingTags: []string{"*asap"}, utils.BalanceWeight: float64(10), utils.BalanceBlocker: true, utils.BalanceDisabled: true, utils.ActionsID: "ACT_1001", utils.Weight: float64(20), }, }, extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", }, { name: "SuccessfulRequestWithDynamicPaths", connIDs: []string{connID}, expAt: &AttrSetActionTrigger{ GroupID: "STANDARD_TRIGGERS_1001", UniqueID: "uid_1001", ActionTrigger: map[string]any{ utils.ThresholdType: "*max_balance", utils.ThresholdValue: float64(20), utils.Recurrent: true, utils.MinSleep: time.Second, utils.ExpirationDate: time.Now(), utils.ActivationDate: time.Now(), utils.BalanceID: "*default", utils.BalanceType: "*monetary", utils.BalanceCategories: []string{utils.Call, "data"}, utils.BalanceDestinationIds: []string{"DST_1001", "DST2"}, utils.BalanceRatingSubject: "SPECIAL_1001", utils.BalanceSharedGroups: []string{"SHRGroup_1001", "SHRGroup2"}, utils.BalanceExpirationDate: time.Now(), utils.BalanceTimingTags: []string{"*asap"}, utils.BalanceWeight: float64(10), utils.BalanceBlocker: true, utils.BalanceDisabled: true, utils.ActionsID: "ACT_1001", utils.Weight: float64(20), }, }, extraParams: "STANDARD_TRIGGERS_<~*req.Account>;uid_<~*req.Account>;*max_balance;20;true;1s;*now;*now;*default;*monetary;call&data;DST_<~*req.Account>&DST2;SPECIAL_<~*req.Account>;SHRGroup_<~*req.Account>&SHRGroup2;*now;*asap;10;true;true;ACT_<~*req.Account>;20", }, { name: "SuccessfulRequestEmptyFields", connIDs: []string{connID}, expAt: &AttrSetActionTrigger{ GroupID: "STANDARD_TRIGGERS", UniqueID: "uid", ActionTrigger: map[string]any{ utils.ThresholdType: "*max_balance", utils.ThresholdValue: float64(20), }, }, extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;;;;;;;;;;;;;;;;;", }, { name: "MissingConns", extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", expectedErr: "MANDATORY_IE_MISSING: [connIDs]", }, { name: "WrongNumberOfParams", extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20", expectedErr: "invalid number of parameters <4> expected 21", }, { name: "ThresholdValueFail", extraParams: "STANDARD_TRIGGERS;uid;*max_balance;BadString;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, { name: "RecurrentFail", extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;BadString;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "MinSleepFail", extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;BadString;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", expectedErr: `time: invalid duration "BadString"`, }, { name: "ExpirationDateBadStringFail", extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;bad String;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", expectedErr: `Unsupported time format`, }, { name: "ActivationDateBadStringFail", extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;bad String;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;20", expectedErr: `Unsupported time format`, }, { name: "BalanceExpirationDateBadStringFail", extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;bad String;*asap;10;true;true;ACT_1001;20", expectedErr: `Unsupported time format`, }, { name: "BalanceWeightFail", extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;BadString;true;true;ACT_1001;20", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, { name: "BalanceBlockerFail", extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;BadString;true;ACT_1001;20", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "BalanceDisabledFail", extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;BadString;ACT_1001;20", expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, }, { name: "WeightFail", extraParams: "STANDARD_TRIGGERS;uid;*max_balance;20;true;1s;;2014-07-29T15:00:00Z;*default;*monetary;call&data;DST1&DST2;SPECIAL_1002;SHRGroup1&SHRGroup2;2030-07-29T15:00:00Z;*asap;10;true;true;ACT_1001;BadString", expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, }, } for i, tc := range testcases { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} ev := &utils.CGREvent{ Tenant: "cgrates.org", ID: "evID", Time: &time.Time{}, Event: map[string]any{ utils.AccountField: "1001", }, } t.Cleanup(func() { at = nil }) err := dynamicActionTrigger(nil, action, nil, nil, ev, SharedActionsData{}, ActionConnCfg{ ConnIDs: tc.connIDs, }) if tc.expectedErr != "" { if err == nil || err.Error() != tc.expectedErr { t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) } } else if err != nil { t.Error(err) } else if !reflect.DeepEqual(at, tc.expAt) { if i != 1 { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAt), utils.ToJSON(at)) } else { // Get the absolute difference between the times rcvExpDate, err := utils.IfaceAsTime(at.ActionTrigger[utils.ExpirationDate], config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { t.Fatal(err) } expExpDate, err := utils.IfaceAsTime(tc.expAt.ActionTrigger[utils.ExpirationDate], config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { t.Fatal(err) } diff := rcvExpDate.Sub(expExpDate) if diff < 0 { diff = -diff // Make sure it's positive } // Check if difference is less than or equal to 1 second if diff > time.Second { t.Fatalf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAt), utils.ToJSON(at)) } tc.expAt.ActionTrigger[utils.ExpirationDate] = rcvExpDate // Get the absolute difference between the times rcvExpDate, err = utils.IfaceAsTime(at.ActionTrigger[utils.ActivationDate], config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { t.Fatal(err) } expExpDate, err = utils.IfaceAsTime(tc.expAt.ActionTrigger[utils.ActivationDate], config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { t.Fatal(err) } diff = rcvExpDate.Sub(expExpDate) if diff < 0 { diff = -diff // Make sure it's positive } // Check if difference is less than or equal to 1 second if diff > time.Second { t.Fatalf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAt), utils.ToJSON(at)) } tc.expAt.ActionTrigger[utils.ActivationDate] = rcvExpDate // Get the absolute difference between the times rcvExpDate, err = utils.IfaceAsTime(at.ActionTrigger[utils.BalanceExpirationDate], config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { t.Fatal(err) } expExpDate, err = utils.IfaceAsTime(tc.expAt.ActionTrigger[utils.BalanceExpirationDate], config.CgrConfig().GeneralCfg().DefaultTimezone) if err != nil { t.Fatal(err) } diff = rcvExpDate.Sub(expExpDate) if diff < 0 { diff = -diff // Make sure it's positive } // Check if difference is less than or equal to 1 second if diff > time.Second { t.Fatalf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAt), utils.ToJSON(at)) } tc.expAt.ActionTrigger[utils.BalanceExpirationDate] = rcvExpDate if !reflect.DeepEqual(at, tc.expAt) { t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expAt), utils.ToJSON(at)) } } } }) } } func TestUnsetRecurrentAction(t *testing.T) { err := unsetRecurrentAction(nil, &Action{Id: "act1"}, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if err == nil || err.Error() != "nil account" { t.Errorf("expected error 'nil account', got %v", err) } trigger1 := &ActionTrigger{ ID: "act1", Recurrent: true, Balance: &BalanceFilter{Type: utils.StringPointer("data")}, } trigger2 := &ActionTrigger{ ID: "act2", Recurrent: true, Balance: &BalanceFilter{Type: utils.StringPointer("sms")}, } account := &Account{ ID: "user1", ActionTriggers: ActionTriggers{trigger1, trigger2}, } action := &Action{ Id: "act1", Balance: &BalanceFilter{Type: utils.StringPointer("data")}, ExtraParameters: func() string { tp := struct { GroupID string UniqueID string ThresholdType string }{} data, _ := json.Marshal(tp) return string(data) }(), } err = unsetRecurrentAction(account, action, nil, nil, nil, SharedActionsData{}, ActionConnCfg{}) if err != nil { t.Errorf("unexpected error: %v", err) } if trigger1.Recurrent { t.Errorf("expected trigger1.Recurrent to be false after unsetRecurrentAction") } if !trigger2.Recurrent { t.Errorf("expected trigger2.Recurrent to remain true") } } func TestAccountActions(t *testing.T) { for actType := range accountActions { a := &Action{ ActionType: actType, Balance: &BalanceFilter{ Type: utils.StringPointer("test"), Value: &utils.ValueFormula{Static: 1.1}, }, } at := &ActionTiming{ actions: []*Action{a}, } err := at.Execute(nil, "") if err != nil { t.Errorf("Got error for action account type %s, error %v ", actType, err) } } }