/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see
*/
package actions
import (
"bytes"
"fmt"
"reflect"
"strings"
"testing"
"time"
"github.com/cgrates/birpc"
"github.com/cgrates/birpc/context"
"github.com/cgrates/cron"
"github.com/cgrates/rpcclient"
"github.com/cgrates/cgrates/config"
"github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/utils"
)
func TestMatchingActionProfilesForEvent(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
data := engine.NewInternalDB(nil, nil, cfg.DataDbCfg().Items)
dm := engine.NewDataManager(data, cfg.CacheCfg(), nil)
filters := engine.NewFilterS(cfg, nil, dm)
acts := NewActionS(cfg, filters, dm, nil)
evNM := utils.MapStorage{
utils.MetaReq: map[string]interface{}{
utils.AccountField: "1001",
utils.Destination: 1002,
},
utils.MetaOpts: map[string]interface{}{},
}
actPrf := &engine.ActionProfile{
Tenant: "cgrates.org",
ID: "AP1",
FilterIDs: []string{"*string:~*req.Account:1001|1002|1003", "*prefix:~*req.Destination:10"},
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: "*topup",
Diktats: []*engine.APDiktat{{
Path: "~*balance.TestBalance.Value",
Value: "10",
}},
},
},
}
if err := acts.dm.SetActionProfile(context.Background(), actPrf, true); err != nil {
t.Error(err)
}
expActionPrf := engine.ActionProfiles{actPrf}
if rcv, err := acts.matchingActionProfilesForEvent(context.Background(), "cgrates.org",
evNM, []string{}, false); err != nil {
t.Error(err)
} else if !reflect.DeepEqual(rcv, expActionPrf) {
t.Errorf("Expected %+v, received %+v", utils.ToJSON(expActionPrf), utils.ToJSON(rcv))
}
evNM = utils.MapStorage{
utils.MetaReq: map[string]interface{}{
utils.AccountField: "10",
},
utils.MetaOpts: map[string]interface{}{},
}
//This Event is not matching with our filter
if _, err := acts.matchingActionProfilesForEvent(context.Background(), "cgrates.org",
evNM, []string{}, false); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
evNM = utils.MapStorage{
utils.MetaReq: map[string]interface{}{
utils.AccountField: "1001",
},
utils.MetaOpts: map[string]interface{}{},
}
actPrfIDs := []string{"inexisting_id"}
//Unable to get from database an ActionProfile if the ID won't match
if _, err := acts.matchingActionProfilesForEvent(context.Background(), "cgrates.org",
evNM, actPrfIDs, false); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
actPrfIDs = []string{"AP1"}
if _, err := acts.matchingActionProfilesForEvent(context.Background(), "cgrates.org",
evNM, actPrfIDs, false); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
actPrf.FilterIDs = append(actPrf.FilterIDs, "*ai:~*req.AnswerTime:2012-07-21T00:00:00Z|2012-08-21T00:00:00Z")
//this event is not active in this interval time
if _, err := acts.matchingActionProfilesForEvent(context.Background(), "cgrates.org",
evNM, actPrfIDs, false); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
//when dataManager is nil, it won't be able to get ActionsProfile from database
acts.dm = nil
if _, err := acts.matchingActionProfilesForEvent(context.Background(), "INVALID_TENANT",
evNM, actPrfIDs, false); err == nil || err != utils.ErrNoDatabaseConn {
t.Errorf("Expected %+v, received %+v", utils.ErrNoDatabaseConn, err)
}
acts.dm = engine.NewDataManager(data, cfg.CacheCfg(), nil)
actPrf.FilterIDs = []string{"invalid_filters"}
//Set in database and invalid filter, so it won t pass
if err := acts.dm.SetActionProfile(context.Background(), actPrf, false); err != nil {
t.Error(err)
}
expected := "NOT_FOUND:invalid_filters"
if _, err := acts.matchingActionProfilesForEvent(context.Background(), "cgrates.org",
evNM, actPrfIDs, false); err == nil || err.Error() != expected {
t.Errorf("Expected %+v, received %+v", expected, err)
}
if err := acts.dm.RemoveActionProfile(context.Background(), actPrf.Tenant,
actPrf.ID, false); err != nil {
t.Error(err)
}
}
func TestScheduledActions(t *testing.T) {
engine.Cache.Clear(nil)
cfg := config.NewDefaultCGRConfig()
data := engine.NewInternalDB(nil, nil, cfg.DataDbCfg().Items)
dm := engine.NewDataManager(data, cfg.CacheCfg(), nil)
filters := engine.NewFilterS(cfg, nil, dm)
acts := NewActionS(cfg, filters, dm, nil)
cgrEv := &utils.CGREvent{
Tenant: "cgrates.org",
ID: "TEST_ACTIONS1",
Event: map[string]interface{}{
utils.AccountField: "1001",
utils.Destination: 1002,
},
}
evNM := utils.MapStorage{
utils.MetaReq: cgrEv.Event,
utils.MetaOpts: map[string]interface{}{},
}
actPrf := &engine.ActionProfile{
Tenant: "cgrates.org",
ID: "AP2",
FilterIDs: []string{"*string:~*req.Account:1001|1002|1003", "*prefix:~*req.Destination:10"},
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: utils.MetaLog,
Diktats: []*engine.APDiktat{{
Path: "~*balance.TestBalance.Value",
Value: "10",
}},
},
},
}
if err := acts.dm.SetActionProfile(context.Background(), actPrf, true); err != nil {
t.Error(err)
}
if rcv, err := acts.scheduledActions(context.Background(), cgrEv.Tenant, cgrEv, []string{}, false, false); err != nil {
t.Error(err)
} else {
expSchedActs := newScheduledActs(context.Background(), cgrEv.Tenant, cgrEv.ID, utils.MetaNone, utils.EmptyString,
utils.EmptyString, evNM, rcv[0].acts)
if reflect.DeepEqual(expSchedActs, rcv) {
t.Errorf("Expected %+v, received %+v", expSchedActs, rcv)
}
}
cgrEv = &utils.CGREvent{
Tenant: "cgrates.org",
ID: "TEST_ACTIONS1",
Event: map[string]interface{}{
utils.Accounts: "10",
},
}
if _, err := acts.scheduledActions(context.Background(), cgrEv.Tenant, cgrEv, []string{}, false, false); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
}
func TestScheduleAction(t *testing.T) {
engine.Cache.Clear(nil)
cfg := config.NewDefaultCGRConfig()
data := engine.NewInternalDB(nil, nil, cfg.DataDbCfg().Items)
dm := engine.NewDataManager(data, cfg.CacheCfg(), nil)
filters := engine.NewFilterS(cfg, nil, dm)
acts := NewActionS(cfg, filters, dm, nil)
cgrEv := []*utils.CGREvent{
{
Tenant: "cgrates.org",
ID: "TEST_ACTIONS1",
Event: map[string]interface{}{
utils.AccountField: "1001",
utils.Destination: 1002,
},
},
}
actPrf := &engine.ActionProfile{
Tenant: "cgrates.org",
ID: "AP3",
FilterIDs: []string{"*string:~*req.Account:1001|1002|1003", "*prefix:~*req.Destination:10"},
Schedule: "* * * * *",
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: utils.MetaLog,
Diktats: []*engine.APDiktat{{
Path: "~*balance.TestBalance.Value",
Value: "10",
}},
},
},
}
if err := acts.dm.SetActionProfile(context.Background(), actPrf, true); err != nil {
t.Error(err)
}
if err := acts.scheduleActions(context.Background(), cgrEv, []string{}, false, true); err != nil {
t.Error(err)
}
//Cannot schedule an action if the ID is invalid
if err := acts.scheduleActions(context.Background(), cgrEv, []string{"INVALID_ID1"}, false, true); err == nil || err != utils.ErrPartiallyExecuted {
t.Errorf("Expected %+v, received %+v", utils.ErrPartiallyExecuted, err)
}
//When schedule is "*asap", the action will execute immediately
actPrf.Schedule = utils.MetaASAP
if err := acts.dm.SetActionProfile(context.Background(), actPrf, true); err != nil {
t.Error(err)
}
if err := acts.scheduleActions(context.Background(), cgrEv, []string{}, false, true); err != nil {
t.Error(err)
}
//Cannot execute the action if the cron is invalid
actPrf.Schedule = "* * * *"
if err := acts.dm.SetActionProfile(context.Background(), actPrf, true); err != nil {
t.Error(err)
}
if err := acts.scheduleActions(context.Background(), cgrEv, []string{}, false, true); err == nil || err != utils.ErrPartiallyExecuted {
t.Error(err)
}
}
func TestAsapExecuteActions(t *testing.T) {
newData := &dataDBMockError{}
cfg := config.NewDefaultCGRConfig()
dm := engine.NewDataManager(newData, cfg.CacheCfg(), nil)
filters := engine.NewFilterS(cfg, nil, dm)
acts := NewActionS(cfg, filters, dm, nil)
cgrEv := []*utils.CGREvent{
{
Tenant: "cgrates.org",
ID: "CHANGED_ID",
Event: map[string]interface{}{
utils.AccountField: "1001",
utils.Destination: 1002,
},
},
}
evNM := utils.MapStorage{
utils.MetaReq: cgrEv[0].Event,
utils.MetaOpts: map[string]interface{}{},
}
expSchedActs := newScheduledActs(context.Background(), cgrEv[0].Tenant, cgrEv[0].ID, utils.MetaNone, utils.EmptyString,
utils.EmptyString, evNM, nil)
if err := acts.asapExecuteActions(context.Background(), expSchedActs); err == nil || err != utils.ErrNoDatabaseConn {
t.Errorf("Expected %+v, received %+v", utils.ErrNoDatabaseConn, err)
}
data := engine.NewInternalDB(nil, nil, cfg.DataDbCfg().Items)
acts.dm = engine.NewDataManager(data, cfg.CacheCfg(), nil)
expSchedActs = newScheduledActs(context.Background(), cgrEv[0].Tenant, "another_id", utils.MetaNone, utils.EmptyString,
utils.EmptyString, evNM, nil)
if err := acts.asapExecuteActions(context.Background(), expSchedActs); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
}
func TestActionSListenAndServe(t *testing.T) {
newData := &dataDBMockError{}
cfg := config.NewDefaultCGRConfig()
dm := engine.NewDataManager(newData, cfg.CacheCfg(), nil)
cfg.ActionSCfg().Tenants = &[]string{"cgrates1.org", "cgrates.org2"}
filters := engine.NewFilterS(cfg, nil, dm)
acts := NewActionS(cfg, filters, dm, nil)
stopChan := make(chan struct{}, 1)
cfgRld := make(chan struct{}, 1)
cfgRld <- struct{}{}
go func() {
time.Sleep(10)
stopChan <- struct{}{}
}()
tmpLogger := utils.Logger
defer func() {
utils.Logger = tmpLogger
}()
var buf bytes.Buffer
utils.Logger = utils.NewStdLoggerWithWriter(&buf, "", 7)
acts.ListenAndServe(stopChan, cfgRld)
expString := "CGRateS <> [INFO] starting "
if rcv := buf.String(); !strings.Contains(rcv, expString) {
t.Errorf("Expected %+v, received %+v", expString, rcv)
}
}
func TestV1ScheduleActions(t *testing.T) {
engine.Cache.Clear(nil)
cfg := config.NewDefaultCGRConfig()
data := engine.NewInternalDB(nil, nil, cfg.DataDbCfg().Items)
dm := engine.NewDataManager(data, cfg.CacheCfg(), nil)
filters := engine.NewFilterS(cfg, nil, dm)
acts := NewActionS(cfg, filters, dm, nil)
var reply string
ev := &utils.CGREvent{
Tenant: "cgrates.org",
Event: map[string]interface{}{
utils.AccountField: "1001",
utils.Destination: 1002,
},
APIOpts: map[string]interface{}{},
}
actPrf := &engine.ActionProfile{
Tenant: "cgrates.org",
ID: "AP4",
FilterIDs: []string{"*string:~*req.Account:1001|1002|1003", "*prefix:~*req.Destination:10"},
Schedule: utils.MetaASAP,
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: utils.MetaLog,
Diktats: []*engine.APDiktat{{
Path: "~*balance.TestBalance.Value",
Value: "10",
}},
},
},
}
if err := acts.dm.SetActionProfile(context.Background(), actPrf, true); err != nil {
t.Error(err)
}
if err := acts.V1ScheduleActions(context.Background(), ev, &reply); err != nil {
t.Error(err)
} else if reply != utils.OK {
t.Errorf("Unexpected reply %+v", reply)
}
ev.APIOpts[utils.OptsActionsProfileIDs] = []string{"invalid_id"}
if err := acts.V1ScheduleActions(context.Background(), ev, &reply); err == nil || err != utils.ErrPartiallyExecuted {
t.Errorf("Expected %+v, received %+v", utils.ErrPartiallyExecuted, err)
}
if err := acts.dm.RemoveActionProfile(context.Background(), actPrf.Tenant, actPrf.ID, true); err != nil {
t.Error(err)
}
}
func TestV1ExecuteActions(t *testing.T) {
engine.Cache.Clear(nil)
cfg := config.NewDefaultCGRConfig()
data := engine.NewInternalDB(nil, nil, cfg.DataDbCfg().Items)
dm := engine.NewDataManager(data, cfg.CacheCfg(), nil)
filters := engine.NewFilterS(cfg, nil, dm)
acts := NewActionS(cfg, filters, dm, nil)
var reply string
ev := &utils.CGREvent{
Tenant: "cgrates.org",
Event: map[string]interface{}{
utils.AccountField: "1001",
utils.Destination: 1002,
},
APIOpts: map[string]interface{}{},
}
actPrf := &engine.ActionProfile{
Tenant: "cgrates.org",
ID: "AP5",
FilterIDs: []string{"*string:~*req.Account:1001|1002|1003", "*prefix:~*req.Destination:10"},
Schedule: utils.MetaASAP,
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: utils.MetaLog,
Diktats: []*engine.APDiktat{{
Path: "~*balance.TestBalance.Value",
Value: "10",
}},
},
},
}
if err := acts.dm.SetActionProfile(context.Background(), actPrf, true); err != nil {
t.Error(err)
}
if err := acts.V1ExecuteActions(context.Background(), ev, &reply); err != nil {
t.Error(err)
} else if reply != utils.OK {
t.Errorf("Unexpected reply %+v", reply)
}
ev.APIOpts[utils.OptsActionsProfileIDs] = []string{"invalid_id"}
if err := acts.V1ExecuteActions(context.Background(), ev, &reply); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
newData := &dataDBMockError{}
newDm := engine.NewDataManager(newData, cfg.CacheCfg(), nil)
newActs := NewActionS(cfg, filters, newDm, nil)
ev.APIOpts[utils.OptsActionsProfileIDs] = []string{}
if err := newActs.V1ExecuteActions(context.Background(), ev, &reply); err == nil || err != utils.ErrPartiallyExecuted {
t.Errorf("Expected %+v, received %+v", utils.ErrPartiallyExecuted, err)
}
if err := acts.dm.RemoveActionProfile(context.Background(), actPrf.Tenant, actPrf.ID, true); err != nil {
t.Error(err)
}
}
func TestActionShutDown(t *testing.T) {
engine.Cache.Clear(nil)
cfg := config.NewDefaultCGRConfig()
data := engine.NewInternalDB(nil, nil, cfg.DataDbCfg().Items)
dm := engine.NewDataManager(data, cfg.CacheCfg(), nil)
filters := engine.NewFilterS(cfg, nil, dm)
acts := NewActionS(cfg, filters, dm, nil)
acts.crn = &cron.Cron{}
tmpLogger := utils.Logger
defer func() {
utils.Logger = tmpLogger
}()
var buf bytes.Buffer
utils.Logger = utils.NewStdLoggerWithWriter(&buf, "", 7)
acts.Shutdown()
expBuff := "CGRateS <> [INFO] shutdown "
if rcv := buf.String(); !strings.Contains(rcv, expBuff) {
t.Errorf("Expected %+v, received %+v", expBuff, rcv)
}
}
type dataDBMockError struct {
engine.DataDBMock
}
func (dbM *dataDBMockError) GetActionProfileDrv(*context.Context, string, string) (*engine.ActionProfile, error) {
return &engine.ActionProfile{
Tenant: "cgrates.org",
ID: "AP6",
FilterIDs: []string{"*string:~*req.Account:1001|1002|1003", "*prefix:~*req.Destination:10"},
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: utils.MetaLog,
Diktats: []*engine.APDiktat{{
Path: "~*balance.TestBalance.Value",
Value: "10",
}},
},
},
}, nil
}
func (dbM *dataDBMockError) SetActionProfileDrv(*context.Context, *engine.ActionProfile) error {
return utils.ErrNoDatabaseConn
}
func (dbM *dataDBMockError) GetIndexesDrv(ctx *context.Context, idxItmType, tntCtx, idxKey, transactionID string) (indexes map[string]utils.StringSet, err error) {
return nil, nil
}
func TestLogActionExecute(t *testing.T) {
evNM := utils.MapStorage{
utils.MetaReq: map[string]interface{}{
utils.AccountField: "10",
},
utils.MetaOpts: map[string]interface{}{},
}
tmpLogger := utils.Logger
defer func() {
utils.Logger = tmpLogger
}()
var buf bytes.Buffer
utils.Logger = utils.NewStdLoggerWithWriter(&buf, "Engine1", 7)
logAction := actLog{}
if err := logAction.execute(context.Background(), evNM, utils.MetaNone); err != nil {
t.Error(err)
}
expected := "CGRateS [INFO] LOG Event: {\"*opts\":{},\"*req\":{\"Account\":\"10\"}}"
if rcv := buf.String(); !strings.Contains(rcv, expected) {
t.Errorf("Expected %+v, received %+v", expected, rcv)
}
}
type testMockCDRsConn struct {
calls map[string]func(_ *context.Context, _, _ interface{}) error
}
func (s *testMockCDRsConn) Call(ctx *context.Context, method string, arg, rply interface{}) error {
call, has := s.calls[method]
if !has {
return rpcclient.ErrUnsupporteServiceMethod
}
return call(ctx, arg, rply)
}
func TestCDRLogActionExecute(t *testing.T) {
sMock := &testMockCDRsConn{
calls: map[string]func(_ *context.Context, _, _ interface{}) error{
utils.CDRsV1ProcessEvent: func(_ *context.Context, arg, rply interface{}) error {
argConv, can := arg.(*utils.CGREvent)
if !can {
return fmt.Errorf("Wrong argument type: %T", arg)
}
if argConv.APIOpts[utils.MetaChargers].(bool) {
return fmt.Errorf("Expected false, received %+v", argConv.APIOpts[utils.MetaChargers])
}
if val, has := argConv.Event[utils.Subject]; !has {
return fmt.Errorf("missing Subject")
} else if strVal := utils.IfaceAsString(val); strVal != "10" {
return fmt.Errorf("Expected %+v, received %+v", "10", strVal)
}
if val, has := argConv.Event[utils.Cost]; !has {
return fmt.Errorf("missing Cost")
} else if strVal := utils.IfaceAsString(val); strVal != "0.15" {
return fmt.Errorf("Expected %+v, received %+v", "0.15", strVal)
}
if val, has := argConv.Event[utils.RequestType]; !has {
return fmt.Errorf("missing RequestType")
} else if strVal := utils.IfaceAsString(val); strVal != utils.MetaNone {
return fmt.Errorf("Expected %+v, received %+v", utils.MetaNone, strVal)
}
if val, has := argConv.Event[utils.RunID]; !has {
return fmt.Errorf("missing RunID")
} else if strVal := utils.IfaceAsString(val); strVal != utils.MetaTopUp {
return fmt.Errorf("Expected %+v, received %+v", utils.MetaNone, strVal)
}
return nil
},
},
}
internalCDRsChann := make(chan birpc.ClientConnector, 1)
internalCDRsChann <- sMock
cfg := config.NewDefaultCGRConfig()
cfg.ActionSCfg().CDRsConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaCDRs)}
data := engine.NewInternalDB(nil, nil, cfg.DataDbCfg().Items)
dm := engine.NewDataManager(data, cfg.CacheCfg(), nil)
filterS := engine.NewFilterS(cfg, nil, dm)
connMgr := engine.NewConnManager(config.CgrConfig())
connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaCDRs), utils.CDRsV1, internalCDRsChann)
apA := &engine.APAction{
ID: "ACT_CDRLOG",
Type: utils.MetaCdrLog,
}
cdrLogAction := &actCDRLog{
config: cfg,
filterS: filterS,
connMgr: connMgr,
aCfg: apA,
}
evNM := utils.MapStorage{
utils.MetaReq: map[string]interface{}{
utils.AccountField: "10",
utils.Tenant: "cgrates.org",
utils.BalanceType: utils.MetaConcrete,
utils.Cost: 0.15,
utils.ActionType: utils.MetaTopUp,
},
utils.MetaOpts: map[string]interface{}{},
}
if err := cdrLogAction.execute(context.Background(), evNM, utils.MetaNone); err != nil {
t.Error(err)
}
}
func TestCDRLogActionWithOpts(t *testing.T) {
// Clear cache because connManager sets the internal connection in cache
engine.Cache.Clear([]string{utils.CacheRPCConnections})
sMock2 := &testMockCDRsConn{
calls: map[string]func(_ *context.Context, _, _ interface{}) error{
utils.CDRsV1ProcessEvent: func(_ *context.Context, arg, rply interface{}) error {
argConv, can := arg.(*utils.CGREvent)
if !can {
return fmt.Errorf("Wrong argument type: %T", arg)
}
if argConv.APIOpts[utils.MetaChargers].(bool) {
return fmt.Errorf("Expected false, received %+v", argConv.APIOpts[utils.MetaChargers])
}
if val, has := argConv.Event[utils.Tenant]; !has {
return fmt.Errorf("missing Tenant")
} else if strVal := utils.IfaceAsString(val); strVal != "cgrates.org" {
return fmt.Errorf("Expected %+v, received %+v", "cgrates.org", strVal)
}
if val, has := argConv.APIOpts["EventFieldOpt"]; !has {
return fmt.Errorf("missing EventFieldOpt from Opts")
} else if strVal := utils.IfaceAsString(val); strVal != "eventValue" {
return fmt.Errorf("Expected %+v, received %+v", "eventValue", strVal)
}
if val, has := argConv.APIOpts["Option1"]; !has {
return fmt.Errorf("missing Option1 from Opts")
} else if strVal := utils.IfaceAsString(val); strVal != "Value1" {
return fmt.Errorf("Expected %+v, received %+v", "Value1", strVal)
}
if val, has := argConv.APIOpts["Option3"]; !has {
return fmt.Errorf("missing Option3 from Opts")
} else if strVal := utils.IfaceAsString(val); strVal != "eventValue" {
return fmt.Errorf("Expected %+v, received %+v", "eventValue", strVal)
}
return nil
},
},
}
internalCDRsChann := make(chan birpc.ClientConnector, 1)
internalCDRsChann <- sMock2
cfg := config.NewDefaultCGRConfig()
cfg.ActionSCfg().CDRsConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaCDRs)}
cfg.TemplatesCfg()["CustomTemplate"] = []*config.FCTemplate{
{
Tag: "Tenant",
Type: "*constant",
Path: "*cdr.Tenant",
Value: config.NewRSRParsersMustCompile("cgrates.org", utils.InfieldSep),
Layout: time.RFC3339,
},
{
Tag: "Opt1",
Type: "*constant",
Path: "*opts.Option1",
Value: config.NewRSRParsersMustCompile("Value1", utils.InfieldSep),
Layout: time.RFC3339,
},
{
Tag: "Opt2",
Type: "*constant",
Path: "*opts.Option2",
Value: config.NewRSRParsersMustCompile("Value2", utils.InfieldSep),
Layout: time.RFC3339,
},
{
Tag: "Opt3",
Type: "*variable",
Path: "*opts.Option3",
Value: config.NewRSRParsersMustCompile("~*opts.EventFieldOpt", utils.InfieldSep),
Layout: time.RFC3339,
},
}
for _, tpl := range cfg.TemplatesCfg()["CustomTemplate"] {
tpl.ComputePath()
}
data := engine.NewInternalDB(nil, nil, cfg.DataDbCfg().Items)
dm := engine.NewDataManager(data, cfg.CacheCfg(), nil)
filterS := engine.NewFilterS(cfg, nil, dm)
connMgr := engine.NewConnManager(config.CgrConfig())
connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaCDRs), utils.CDRsV1, internalCDRsChann)
apA := &engine.APAction{
ID: "ACT_CDRLOG2",
Type: utils.MetaCdrLog,
Opts: map[string]interface{}{
utils.MetaTemplateID: "CustomTemplate",
},
}
cdrLogAction := &actCDRLog{
config: cfg,
filterS: filterS,
connMgr: connMgr,
aCfg: apA,
}
evNM := utils.MapStorage{
utils.MetaReq: map[string]interface{}{
utils.AccountField: "10",
utils.Tenant: "cgrates.org",
utils.BalanceType: utils.MetaConcrete,
utils.Cost: 0.15,
utils.ActionType: utils.MetaTopUp,
},
utils.MetaOpts: map[string]interface{}{
"EventFieldOpt": "eventValue",
},
}
if err := cdrLogAction.execute(context.Background(), evNM, utils.MetaNone); err != nil {
t.Error(err)
}
}
func TestExportAction(t *testing.T) {
// Clear cache because connManager sets the internal connection in cache
engine.Cache.Clear([]string{utils.CacheRPCConnections})
sMock2 := &testMockCDRsConn{
calls: map[string]func(_ *context.Context, _, _ interface{}) error{
utils.EeSv1ProcessEvent: func(_ *context.Context, arg, rply interface{}) error {
argConv, can := arg.(*utils.CGREventWithEeIDs)
if !can {
return fmt.Errorf("Wrong argument type: %T", arg)
}
if argConv.Tenant != "cgrates.org" {
return fmt.Errorf("Expected %+v, received %+v", "cgrates.org", argConv.Tenant)
}
return nil
},
},
}
internalCDRsChann := make(chan birpc.ClientConnector, 1)
internalCDRsChann <- sMock2
cfg := config.NewDefaultCGRConfig()
cfg.ActionSCfg().EEsConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaEEs)}
connMgr := engine.NewConnManager(config.CgrConfig())
connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaEEs), utils.EeSv1, internalCDRsChann)
apA := &engine.APAction{
ID: "ACT_CDRLOG2",
Type: utils.MetaExport,
}
exportAction := &actExport{
tnt: "cgrates.org",
config: cfg,
connMgr: connMgr,
aCfg: apA,
}
evNM := utils.MapStorage{
utils.MetaReq: map[string]interface{}{
utils.AccountField: "10",
utils.Tenant: "cgrates.org",
utils.BalanceType: utils.MetaConcrete,
utils.Cost: 0.15,
utils.ActionType: utils.MetaTopUp,
},
utils.MetaOpts: map[string]interface{}{
"EventFieldOpt": "eventValue",
},
}
if err := exportAction.execute(context.Background(), evNM, utils.MetaNone); err != nil {
t.Error(err)
}
}
func TestExportActionWithEeIDs(t *testing.T) {
// Clear cache because connManager sets the internal connection in cache
engine.Cache.Clear([]string{utils.CacheRPCConnections})
sMock2 := &testMockCDRsConn{
calls: map[string]func(_ *context.Context, _, _ interface{}) error{
utils.EeSv1ProcessEvent: func(_ *context.Context, arg, rply interface{}) error {
argConv, can := arg.(*utils.CGREventWithEeIDs)
if !can {
return fmt.Errorf("Wrong argument type: %T", arg)
}
if argConv.Tenant != "cgrates.org" {
return fmt.Errorf("Expected %+v, received %+v", "cgrates.org", argConv.Tenant)
}
if !reflect.DeepEqual(argConv.EeIDs, []string{"Exporter1", "Exporter2", "Exporter3"}) {
return fmt.Errorf("Expected %+v, received %+v", []string{"Exporter1", "Exporter2", "Exporter3"}, argConv.EeIDs)
}
return nil
},
},
}
internalCDRsChann := make(chan birpc.ClientConnector, 1)
internalCDRsChann <- sMock2
cfg := config.NewDefaultCGRConfig()
cfg.ActionSCfg().EEsConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaEEs)}
connMgr := engine.NewConnManager(config.CgrConfig())
connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaEEs), utils.EeSv1, internalCDRsChann)
apA := &engine.APAction{
ID: "ACT_CDRLOG2",
Type: utils.MetaExport,
Opts: map[string]interface{}{
utils.MetaExporterIDs: "Exporter1;Exporter2;Exporter3",
},
}
exportAction := &actExport{
tnt: "cgrates.org",
config: cfg,
connMgr: connMgr,
aCfg: apA,
}
evNM := utils.MapStorage{
utils.MetaReq: map[string]interface{}{
utils.AccountField: "10",
utils.Tenant: "cgrates.org",
utils.BalanceType: utils.MetaConcrete,
utils.Cost: 0.15,
utils.ActionType: utils.MetaTopUp,
},
utils.MetaOpts: map[string]interface{}{
"EventFieldOpt": "eventValue",
},
}
if err := exportAction.execute(context.Background(), evNM, utils.MetaNone); err != nil {
t.Error(err)
}
}
func TestExportActionResetThresholdStaticTenantID(t *testing.T) {
// Clear cache because connManager sets the internal connection in cache
engine.Cache.Clear([]string{utils.CacheRPCConnections})
sMock2 := &testMockCDRsConn{
calls: map[string]func(_ *context.Context, _, _ interface{}) error{
utils.ThresholdSv1ResetThreshold: func(_ *context.Context, arg, rply interface{}) error {
argConv, can := arg.(*utils.TenantIDWithAPIOpts)
if !can {
return fmt.Errorf("Wrong argument type: %T", arg)
}
if argConv.Tenant != "cgrates.org" {
return fmt.Errorf("Expected %+v, received %+v", "cgrates.org", argConv.Tenant)
}
if argConv.ID != "TH1" {
return fmt.Errorf("Expected %+v, received %+v", "TH1", argConv.ID)
}
return nil
},
},
}
internalChann := make(chan birpc.ClientConnector, 1)
internalChann <- sMock2
cfg := config.NewDefaultCGRConfig()
cfg.ActionSCfg().ThresholdSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaThresholds)}
connMgr := engine.NewConnManager(config.CgrConfig())
connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaThresholds), utils.ThresholdSv1, internalChann)
apA := &engine.APAction{
ID: "ACT_RESET_TH",
Type: utils.MetaResetThreshold,
Diktats: []*engine.APDiktat{{}},
}
exportAction := &actResetThreshold{
tnt: "cgrates.org",
config: cfg,
connMgr: connMgr,
aCfg: apA,
}
evNM := utils.MapStorage{
utils.MetaOpts: map[string]interface{}{},
}
if err := exportAction.execute(context.Background(), evNM, "cgrates.org:TH1"); err != nil {
t.Error(err)
}
}
func TestExportActionResetThresholdStaticID(t *testing.T) {
// Clear cache because connManager sets the internal connection in cache
engine.Cache.Clear([]string{utils.CacheRPCConnections})
sMock2 := &testMockCDRsConn{
calls: map[string]func(_ *context.Context, _, _ interface{}) error{
utils.ThresholdSv1ResetThreshold: func(_ *context.Context, arg, rply interface{}) error {
argConv, can := arg.(*utils.TenantIDWithAPIOpts)
if !can {
return fmt.Errorf("Wrong argument type: %T", arg)
}
if argConv.Tenant != "cgrates.org" {
return fmt.Errorf("Expected %+v, received %+v", "cgrates.org", argConv.Tenant)
}
if argConv.ID != "TH1" {
return fmt.Errorf("Expected %+v, received %+v", "TH1", argConv.ID)
}
return nil
},
},
}
internalChann := make(chan birpc.ClientConnector, 1)
internalChann <- sMock2
cfg := config.NewDefaultCGRConfig()
cfg.ActionSCfg().ThresholdSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaThresholds)}
connMgr := engine.NewConnManager(config.CgrConfig())
connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaThresholds), utils.ThresholdSv1, internalChann)
apA := &engine.APAction{
ID: "ACT_RESET_TH",
Type: utils.MetaResetThreshold,
Diktats: []*engine.APDiktat{{}},
}
exportAction := &actResetThreshold{
tnt: "cgrates.org",
config: cfg,
connMgr: connMgr,
aCfg: apA,
}
evNM := utils.MapStorage{
utils.MetaOpts: map[string]interface{}{},
}
if err := exportAction.execute(context.Background(), evNM, "TH1"); err != nil {
t.Error(err)
}
}
func TestExportActionResetStatStaticTenantID(t *testing.T) {
// Clear cache because connManager sets the internal connection in cache
engine.Cache.Clear([]string{utils.CacheRPCConnections})
sMock2 := &testMockCDRsConn{
calls: map[string]func(_ *context.Context, _, _ interface{}) error{
utils.StatSv1ResetStatQueue: func(_ *context.Context, arg, rply interface{}) error {
argConv, can := arg.(*utils.TenantIDWithAPIOpts)
if !can {
return fmt.Errorf("Wrong argument type: %T", arg)
}
if argConv.Tenant != "cgrates.org" {
return fmt.Errorf("Expected %+v, received %+v", "cgrates.org", argConv.Tenant)
}
if argConv.ID != "ST1" {
return fmt.Errorf("Expected %+v, received %+v", "TH1", argConv.ID)
}
return nil
},
},
}
internalChann := make(chan birpc.ClientConnector, 1)
internalChann <- sMock2
cfg := config.NewDefaultCGRConfig()
cfg.ActionSCfg().StatSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaStats)}
connMgr := engine.NewConnManager(config.CgrConfig())
connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaStats), utils.StatSv1, internalChann)
apA := &engine.APAction{
ID: "ACT_RESET_ST",
Type: utils.MetaResetStatQueue,
Diktats: []*engine.APDiktat{{}},
}
exportAction := &actResetStat{
tnt: "cgrates.org",
config: cfg,
connMgr: connMgr,
aCfg: apA,
}
evNM := utils.MapStorage{
utils.MetaOpts: map[string]interface{}{},
}
if err := exportAction.execute(context.Background(), evNM, "cgrates.org:ST1"); err != nil {
t.Error(err)
}
}
func TestExportActionResetStatStaticID(t *testing.T) {
// Clear cache because connManager sets the internal connection in cache
engine.Cache.Clear([]string{utils.CacheRPCConnections})
sMock2 := &testMockCDRsConn{
calls: map[string]func(_ *context.Context, _, _ interface{}) error{
utils.StatSv1ResetStatQueue: func(_ *context.Context, arg, rply interface{}) error {
argConv, can := arg.(*utils.TenantIDWithAPIOpts)
if !can {
return fmt.Errorf("Wrong argument type: %T", arg)
}
if argConv.Tenant != "cgrates.org" {
return fmt.Errorf("Expected %+v, received %+v", "cgrates.org", argConv.Tenant)
}
if argConv.ID != "ST1" {
return fmt.Errorf("Expected %+v, received %+v", "TH1", argConv.ID)
}
return nil
},
},
}
internalChann := make(chan birpc.ClientConnector, 1)
internalChann <- sMock2
cfg := config.NewDefaultCGRConfig()
cfg.ActionSCfg().StatSConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaStats)}
connMgr := engine.NewConnManager(config.CgrConfig())
connMgr.AddInternalConn(utils.ConcatenatedKey(utils.MetaInternal, utils.MetaStats), utils.StatSv1, internalChann)
apA := &engine.APAction{
ID: "ACT_RESET_ST",
Type: utils.MetaResetStatQueue,
Diktats: []*engine.APDiktat{{
Value: "ST1",
}},
}
exportAction := &actResetStat{
tnt: "cgrates.org",
config: cfg,
connMgr: connMgr,
aCfg: apA,
}
evNM := utils.MapStorage{
utils.MetaOpts: map[string]interface{}{},
}
if err := exportAction.execute(context.Background(), evNM, "ST1"); err != nil {
t.Error(err)
}
}
func TestACScheduledActions(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
data := engine.NewInternalDB(nil, nil, cfg.DataDbCfg().Items)
dm := engine.NewDataManager(data, cfg.CacheCfg(), nil)
fltrs := engine.NewFilterS(cfg, nil, dm)
actPrf := &engine.ActionProfile{
Tenant: "cgrates.org",
ID: "TestACScheduledActions",
FilterIDs: []string{"*string:~*req.Destination:1005"},
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: "inexistent_type",
Diktats: []*engine.APDiktat{{
Path: "~*balance.TestBalance.Value",
Value: "10",
}},
},
},
}
if err := dm.SetActionProfile(context.Background(), actPrf, true); err != nil {
t.Error(err)
}
cgrEv := &utils.CGREvent{
Tenant: "cgrates.org",
Event: map[string]interface{}{
utils.Destination: "1005",
},
}
tmpLogger := utils.Logger
defer func() {
utils.Logger = tmpLogger
}()
var buf bytes.Buffer
utils.Logger = utils.NewStdLoggerWithWriter(&buf, "", 7)
acts := NewActionS(cfg, fltrs, dm, nil)
expected := "WARNING] ignoring ActionProfile with id: creating action: , error: >"
if _, err := acts.scheduledActions(context.Background(), "cgrates.org", cgrEv, []string{}, false, true); err != nil {
t.Error(err)
} else if rcv := buf.String(); !strings.Contains(rcv, expected) {
t.Errorf("Expected %+v, received %+v", expected, rcv)
}
actPrf.Actions[0].Type = utils.MetaResetStatQueue
actPrf.Targets = map[string]utils.StringSet{
utils.MetaStats: map[string]struct{}{
"ID_TEST": {},
},
}
if err := dm.SetActionProfile(context.Background(), actPrf, true); err != nil {
t.Error(err)
}
mapStorage := utils.MapStorage{
utils.MetaReq: cgrEv.Event,
utils.MetaOpts: cgrEv.APIOpts,
}
expectedSChed := []*scheduledActs{
{
tenant: "cgrates.org",
apID: "TestACScheduledActions",
trgTyp: utils.MetaStats,
trgID: "ID_TEST",
schedule: utils.EmptyString,
ctx: context.Background(),
data: mapStorage,
},
}
var err error
var schedActs []*scheduledActs
if schedActs, err = acts.scheduledActions(context.Background(), "cgrates.org", cgrEv, []string{}, false, true); err != nil {
t.Error(err)
} else {
}
//execute asap the actions
schedActs[0].trgID = "invalid_type"
if err := acts.asapExecuteActions(context.Background(), schedActs[0]); err == nil || err != utils.ErrPartiallyExecuted {
t.Errorf("Expected %+v, received %+v", utils.ErrPartiallyExecuted, err)
}
schedActs[0].trgID = "ID_TEST"
schedActs[0].acts = nil
schedActs[0].cch = nil
if !reflect.DeepEqual(schedActs, expectedSChed) {
t.Errorf("Expected %+v, received %+v", expectedSChed, schedActs)
}
}
func TestV1ScheduleActionsProfileIgnoreFilters(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
cfg.ActionSCfg().Opts.ProfileIgnoreFilters = []*utils.DynamicBoolOpt{
{
Value: true,
},
}
data := engine.NewInternalDB(nil, nil, cfg.DataDbCfg().Items)
dm := engine.NewDataManager(data, cfg.CacheCfg(), nil)
filters := engine.NewFilterS(cfg, nil, dm)
acts := NewActionS(cfg, filters, dm, nil)
var reply string
ev := &utils.CGREvent{
Tenant: "cgrates.org",
Event: map[string]interface{}{
utils.AccountField: "1001",
utils.Destination: 1002,
},
APIOpts: map[string]interface{}{
utils.OptsActionsProfileIDs: []string{"AP7"},
utils.MetaProfileIgnoreFilters: true,
"testFieldIgnore": "testValue",
},
}
actPrf := &engine.ActionProfile{
Tenant: "cgrates.org",
ID: "AP7",
FilterIDs: []string{"*string:~*req.Account:1001|1002|1003", "*prefix:~*req.Destination:10", "*prefix:~*opts.testFieldIgnore:testValue1"},
Schedule: utils.MetaASAP,
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: utils.MetaLog,
Diktats: []*engine.APDiktat{{
Path: "~*balance.TestBalance.Value",
Value: "10",
}},
},
},
}
if err := acts.dm.SetActionProfile(context.Background(), actPrf, true); err != nil {
t.Error(err)
}
if err := acts.V1ScheduleActions(context.Background(), ev, &reply); err != nil {
t.Error(err)
} else if reply != utils.OK {
t.Errorf("Unexpected reply %+v", reply)
}
}
func TestV1ExecuteActionsProfileIgnoreFilters(t *testing.T) {
cfg := config.NewDefaultCGRConfig()
cfg.ActionSCfg().Opts.ProfileIgnoreFilters = []*utils.DynamicBoolOpt{
{
Value: true,
},
}
data := engine.NewInternalDB(nil, nil, cfg.DataDbCfg().Items)
dm := engine.NewDataManager(data, cfg.CacheCfg(), nil)
filters := engine.NewFilterS(cfg, nil, dm)
acts := NewActionS(cfg, filters, dm, nil)
var reply string
ev := &utils.CGREvent{
Tenant: "cgrates.org",
Event: map[string]interface{}{
utils.AccountField: "1001",
utils.Destination: 1002,
},
APIOpts: map[string]interface{}{
utils.OptsActionsProfileIDs: []string{"AP8"},
utils.MetaProfileIgnoreFilters: true,
"testFieldIgnore": "testValue",
},
}
actPrf := &engine.ActionProfile{
Tenant: "cgrates.org",
ID: "AP8",
FilterIDs: []string{"*string:~*req.Account:1001|1002|1003", "*prefix:~*req.Destination:10", "*prefix:~*opts.testFieldIgnore:testValue1"},
Schedule: utils.MetaASAP,
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: utils.MetaLog,
Diktats: []*engine.APDiktat{{
Path: "~*balance.TestBalance.Value",
Value: "10",
}},
},
},
}
if err := acts.dm.SetActionProfile(context.Background(), actPrf, true); err != nil {
t.Error(err)
}
if err := acts.V1ExecuteActions(context.Background(), ev, &reply); err != nil {
t.Error(err)
} else if reply != utils.OK {
t.Errorf("Unexpected reply %+v", reply)
}
}