Files
cgrates/actions/actions_test.go

619 lines
20 KiB
Go

/*
Real-time Online/Offline Charging System (OerS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package actions
import (
"bytes"
"context"
"log"
"os"
"reflect"
"strings"
"testing"
"time"
"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) {
defaultCfg := config.NewDefaultCGRConfig()
data := engine.NewInternalDB(nil, nil, true)
dm := engine.NewDataManager(data, config.CgrConfig().CacheCfg(), nil)
filters := engine.NewFilterS(defaultCfg, nil, dm)
acts := NewActionS(defaultCfg, 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: "test_id1",
FilterIDs: []string{"*string:~*req.Account:1001;1002;1003", "*prefix:~*req.Destination:10"},
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: "*topup",
Path: "~*balance.TestBalance.Value",
Value: config.NewRSRParsersMustCompile("10", defaultCfg.GeneralCfg().RSRSep),
},
},
}
if err := acts.dm.SetActionProfile(actPrf, true); err != nil {
t.Error(err)
}
expActionPrf := engine.ActionProfiles{actPrf}
if rcv, err := acts.matchingActionProfilesForEvent("cgrates.org", evNM, utils.TimePointer(time.Now()), []string{}); 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("cgrates.org", evNM, utils.TimePointer(time.Now()), []string{}); 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("cgrates.org", evNM, utils.TimePointer(time.Now()), actPrfIDs); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
actPrfIDs = []string{"test_id1"}
if _, err := acts.matchingActionProfilesForEvent("cgrates.org", evNM, utils.TimePointer(time.Now()), actPrfIDs); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
actPrf.ActivationInterval = &utils.ActivationInterval{
ActivationTime: time.Date(2012, 7, 21, 0, 0, 0, 0, time.UTC),
ExpiryTime: time.Date(2012, 8, 21, 0, 0, 0, 0, time.UTC),
}
//this event is not active in this interval time
if _, err := acts.matchingActionProfilesForEvent("cgrates.org", evNM, utils.TimePointer(time.Date(2012, 6, 21, 0, 0, 0, 0, time.UTC)), actPrfIDs); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
actPrf.ActivationInterval = nil
//when dataManager is nil, it won't be able to get ActionsProfile from database
acts.dm = nil
if _, err := acts.matchingActionProfilesForEvent("INVALID_TENANT", evNM, utils.TimePointer(time.Now()), actPrfIDs); err == nil || err != utils.ErrNoDatabaseConn {
t.Errorf("Expected %+v, received %+v", utils.ErrNoDatabaseConn, err)
}
acts.dm = engine.NewDataManager(data, config.CgrConfig().CacheCfg(), nil)
actPrf.FilterIDs = []string{"invalid_filters"}
//Set in database and invalid filter, so it won t pass
if err := acts.dm.SetActionProfile(actPrf, false); err != nil {
t.Error(err)
}
expected := "NOT_FOUND:invalid_filters"
if _, err := acts.matchingActionProfilesForEvent("cgrates.org", evNM, utils.TimePointer(time.Now()), actPrfIDs); err == nil || err.Error() != expected {
t.Errorf("Expected %+v, received %+v", expected, err)
}
if err := acts.dm.RemoveActionProfile(actPrf.Tenant, actPrf.ID, utils.NonTransactional, false); err != nil {
t.Error(err)
}
}
func TestScheduledActions(t *testing.T) {
defaultCfg := config.NewDefaultCGRConfig()
data := engine.NewInternalDB(nil, nil, true)
dm := engine.NewDataManager(data, config.CgrConfig().CacheCfg(), nil)
filters := engine.NewFilterS(defaultCfg, nil, dm)
acts := NewActionS(defaultCfg, 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: "test_id1",
FilterIDs: []string{"*string:~*req.Account:1001;1002;1003", "*prefix:~*req.Destination:10"},
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: utils.MetaLog,
Path: "~*balance.TestBalance.Value",
Value: config.NewRSRParsersMustCompile("10", defaultCfg.GeneralCfg().RSRSep),
},
},
}
if err := acts.dm.SetActionProfile(actPrf, true); err != nil {
t.Error(err)
}
if rcv, err := acts.scheduledActions(cgrEv.Tenant, cgrEv, []string{}, false); err != nil {
t.Error(err)
} else {
expSchedActs := newScheduledActs(cgrEv.Tenant, cgrEv.ID, utils.MetaNone, utils.EmptyString,
utils.EmptyString, context.Background(), 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(cgrEv.Tenant, cgrEv, []string{}, false); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
cgrEv = &utils.CGREvent{
Tenant: "cgrates.org",
ID: "test_id1",
Event: map[string]interface{}{
utils.AccountField: "1001",
utils.Destination: 1002,
},
}
actPrf.Actions[0].Type = "*topup"
if err := acts.dm.SetActionProfile(actPrf, true); err != nil {
t.Error(err)
}
if _, err := acts.scheduledActions(cgrEv.Tenant, cgrEv, []string{}, false); err == nil || err != utils.ErrPartiallyExecuted {
t.Errorf("Expected %+v, received %+v", utils.ErrPartiallyExecuted, err)
}
}
func TestScheduleAction(t *testing.T) {
defaultCfg := config.NewDefaultCGRConfig()
data := engine.NewInternalDB(nil, nil, true)
dm := engine.NewDataManager(data, config.CgrConfig().CacheCfg(), nil)
filters := engine.NewFilterS(defaultCfg, nil, dm)
acts := NewActionS(defaultCfg, 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: "test_id1",
FilterIDs: []string{"*string:~*req.Account:1001;1002;1003", "*prefix:~*req.Destination:10"},
Schedule: "* * * * *",
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: utils.MetaLog,
Path: "~*balance.TestBalance.Value",
Value: config.NewRSRParsersMustCompile("10", defaultCfg.GeneralCfg().RSRSep),
},
},
}
if err := acts.dm.SetActionProfile(actPrf, true); err != nil {
t.Error(err)
}
if err := acts.scheduleActions(cgrEv, []string{}, true); err != nil {
t.Error(err)
}
//Cannot schedule an action if the ID is invalid
if err := acts.scheduleActions(cgrEv, []string{"INVALID_ID1"}, 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(actPrf, true); err != nil {
t.Error(err)
}
if err := acts.scheduleActions(cgrEv, []string{}, true); err != nil {
t.Error(err)
}
//Cannot execute the action if the cron is invalid
actPrf.Schedule = "* * * *"
if err := acts.dm.SetActionProfile(actPrf, true); err != nil {
t.Error(err)
}
if err := acts.scheduleActions(cgrEv, []string{}, true); err == nil || err != utils.ErrPartiallyExecuted {
t.Error(err)
}
}
func TestAsapExecuteActions(t *testing.T) {
newData := &dataDBMockError{}
dm := engine.NewDataManager(newData, config.CgrConfig().CacheCfg(), nil)
defaultCfg := config.NewDefaultCGRConfig()
filters := engine.NewFilterS(defaultCfg, nil, dm)
acts := NewActionS(defaultCfg, 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(cgrEv[0].Tenant, cgrEv[0].ID, utils.MetaNone, utils.EmptyString,
utils.EmptyString, context.Background(), evNM, nil)
if err := acts.asapExecuteActions(expSchedActs); err == nil || err != utils.ErrNoDatabaseConn {
t.Errorf("Expected %+v, received %+v", utils.ErrNoDatabaseConn, err)
}
data := engine.NewInternalDB(nil, nil, true)
acts.dm = engine.NewDataManager(data, config.CgrConfig().CacheCfg(), nil)
expSchedActs = newScheduledActs(cgrEv[0].Tenant, "another_id", utils.MetaNone, utils.EmptyString,
utils.EmptyString, context.Background(), evNM, nil)
if err := acts.asapExecuteActions(expSchedActs); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
}
func TestActionSListenAndServe(t *testing.T) {
newData := &dataDBMockError{}
dm := engine.NewDataManager(newData, config.CgrConfig().CacheCfg(), nil)
defaultCfg := config.NewDefaultCGRConfig()
defaultCfg.ActionSCfg().Tenants = &[]string{"cgrates1.org", "cgrates.org2"}
filters := engine.NewFilterS(defaultCfg, nil, dm)
acts := NewActionS(defaultCfg, filters, dm, nil)
stopChan := make(chan struct{}, 1)
cfgRld := make(chan struct{}, 1)
cfgRld <- struct{}{}
go func() {
time.Sleep(10)
stopChan <- struct{}{}
}()
acts.ListenAndServe(stopChan, cfgRld)
}
func TestV1ScheduleActions(t *testing.T) {
data := engine.NewInternalDB(nil, nil, true)
dm := engine.NewDataManager(data, config.CgrConfig().CacheCfg(), nil)
defaultCfg := config.NewDefaultCGRConfig()
filters := engine.NewFilterS(defaultCfg, nil, dm)
acts := NewActionS(defaultCfg, filters, dm, nil)
var reply string
newArgs := &utils.ArgActionSv1ScheduleActions{
ActionProfileIDs: []string{},
CGREvent: &utils.CGREvent{
Tenant: "cgrates.org",
ID: "test_id1",
Event: map[string]interface{}{
utils.AccountField: "1001",
utils.Destination: 1002,
},
},
}
actPrf := &engine.ActionProfile{
Tenant: "cgrates.org",
ID: "test_id1",
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,
Path: "~*balance.TestBalance.Value",
Value: config.NewRSRParsersMustCompile("10", defaultCfg.GeneralCfg().RSRSep),
},
},
}
if err := acts.dm.SetActionProfile(actPrf, true); err != nil {
t.Error(err)
}
if err := acts.V1ScheduleActions(newArgs, &reply); err != nil {
t.Error(err)
} else if reply != utils.OK {
t.Errorf("Unexpected reply %+v", reply)
}
newArgs.ActionProfileIDs = []string{"invalid_id"}
if err := acts.V1ScheduleActions(newArgs, &reply); err == nil || err != utils.ErrPartiallyExecuted {
t.Errorf("Expected %+v, received %+v", utils.ErrPartiallyExecuted, err)
}
if err := acts.dm.RemoveActionProfile(actPrf.Tenant, actPrf.ID, utils.NonTransactional, true); err != nil {
t.Error(err)
}
}
func TestV1ExecuteActions(t *testing.T) {
data := engine.NewInternalDB(nil, nil, true)
dm := engine.NewDataManager(data, config.CgrConfig().CacheCfg(), nil)
defaultCfg := config.NewDefaultCGRConfig()
filters := engine.NewFilterS(defaultCfg, nil, dm)
acts := NewActionS(defaultCfg, filters, dm, nil)
var reply string
newArgs := &utils.ArgActionSv1ScheduleActions{
ActionProfileIDs: []string{},
CGREvent: &utils.CGREvent{
Tenant: "cgrates.org",
ID: "test_id1",
Event: map[string]interface{}{
utils.AccountField: "1001",
utils.Destination: 1002,
},
},
}
actPrf := &engine.ActionProfile{
Tenant: "cgrates.org",
ID: "test_id1",
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,
Path: "~*balance.TestBalance.Value",
Value: config.NewRSRParsersMustCompile("10", defaultCfg.GeneralCfg().RSRSep),
},
},
}
if err := acts.dm.SetActionProfile(actPrf, true); err != nil {
t.Error(err)
}
if err := acts.V1ExecuteActions(newArgs, &reply); err != nil {
t.Error(err)
} else if reply != utils.OK {
t.Errorf("Unexpected reply %+v", reply)
}
newArgs.ActionProfileIDs = []string{"invalid_id"}
if err := acts.V1ExecuteActions(newArgs, &reply); err == nil || err != utils.ErrNotFound {
t.Errorf("Expected %+v, received %+v", utils.ErrNotFound, err)
}
newData := &dataDBMockError{}
newDm := engine.NewDataManager(newData, config.CgrConfig().CacheCfg(), nil)
newActs := NewActionS(defaultCfg, filters, newDm, nil)
newArgs.ActionProfileIDs = []string{}
if err := newActs.V1ExecuteActions(newArgs, &reply); err == nil || err != utils.ErrPartiallyExecuted {
t.Errorf("Expected %+v, received %+v", utils.ErrPartiallyExecuted, err)
}
if err := acts.dm.RemoveActionProfile(actPrf.Tenant, actPrf.ID, utils.NonTransactional, true); err != nil {
t.Error(err)
}
}
func TestActionSCall(t *testing.T) {
acts := new(ActionS)
if err := acts.Call("UnsupportedServiceMethod", "args", "rply"); err == nil || err != rpcclient.ErrUnsupporteServiceMethod {
t.Errorf("Expected %+q, received %+q", rpcclient.ErrUnsupporteServiceMethod, err)
}
}
func TestActionShutDown(t *testing.T) {
data := engine.NewInternalDB(nil, nil, true)
dm := engine.NewDataManager(data, config.CgrConfig().CacheCfg(), nil)
defaultCfg := config.NewDefaultCGRConfig()
filters := engine.NewFilterS(defaultCfg, nil, dm)
acts := NewActionS(defaultCfg, filters, dm, nil)
acts.crn = &cron.Cron{}
if err := acts.Shutdown(); err != nil {
t.Error(err)
}
}
type dataDBMockError struct {
*engine.DataDBMock
}
func (dbM *dataDBMockError) GetActionProfileDrv(string, string) (*engine.ActionProfile, error) {
return &engine.ActionProfile{
Tenant: "cgrates.org",
ID: "test_id1",
FilterIDs: []string{"*string:~*req.Account:1001;1002;1003", "*prefix:~*req.Destination:10"},
Actions: []*engine.APAction{
{
ID: "TOPUP",
FilterIDs: []string{},
Type: utils.MetaLog,
Path: "~*balance.TestBalance.Value",
Value: config.NewRSRParsersMustCompile("10", ","),
},
},
}, nil
}
func (dbM *dataDBMockError) SetActionProfileDrv(*engine.ActionProfile) error {
return utils.ErrNoDatabaseConn
}
func TestLogActionExecute(t *testing.T) {
output := new(bytes.Buffer)
log.SetOutput(output)
loggertype := utils.MetaStdLog
id := "Engine1"
if newLogger, err := utils.Newlogger(loggertype, id); err != nil {
t.Error(err)
} else {
newLogger.SetLogLevel(7)
utils.Logger = newLogger
}
evNM := utils.MapStorage{
utils.MetaReq: map[string]interface{}{
utils.AccountField: "10",
},
utils.MetaOpts: map[string]interface{}{},
}
logAction := actLog{}
if err := logAction.execute(nil, evNM); err != nil {
t.Error(err)
}
expected := "CGRateS <Engine1> [INFO] LOG Event: {\"*opts\":{},\"*req\":{\"Account\":\"10\"}}"
if rcv := output.String(); !strings.Contains(rcv, expected) {
t.Errorf("Expected %+v, received %+v", expected, rcv)
}
log.SetOutput(os.Stderr)
}
type testMockCDRsConn struct {
calls map[string]func(arg interface{}, rply interface{}) error
}
func (s *testMockCDRsConn) Call(method string, arg interface{}, rply interface{}) error {
if call, has := s.calls[method]; !has {
return rpcclient.ErrUnsupporteServiceMethod
} else {
return call(arg, rply)
}
}
func TestCDRLogActionExecute(t *testing.T) {
sMock := &testMockCDRsConn{
calls: map[string]func(arg interface{}, rply interface{}) error{
utils.CDRsV1ProcessEvent: func(arg interface{}, rply interface{}) error {
argConv, can := arg.(*engine.ArgV1ProcessEvent)
if !can {
t.Errorf("Wrong argument type: %T", arg)
}
if !reflect.DeepEqual(argConv.Flags, []string{utils.ConcatenatedKey(utils.MetaChargers, "false")}) {
t.Errorf("Expected %+v, received %+v", []string{utils.ConcatenatedKey(utils.MetaChargers, "false")}, argConv.Flags)
}
if val, has := argConv.CGREvent.Event[utils.Subject]; !has {
t.Error("missing Subject")
} else if strVal := utils.IfaceAsString(val); strVal != "10" {
t.Errorf("Expected %+v, received %+v", "10", strVal)
}
if val, has := argConv.CGREvent.Event[utils.Cost]; !has {
t.Error("missing Cost")
} else if strVal := utils.IfaceAsString(val); strVal != "0.15" {
t.Errorf("Expected %+v, received %+v", "0.15", strVal)
}
if val, has := argConv.CGREvent.Event[utils.RequestType]; !has {
t.Error("missing RequestType")
} else if strVal := utils.IfaceAsString(val); strVal != utils.MetaNone {
t.Errorf("Expected %+v, received %+v", utils.MetaNone, strVal)
}
if val, has := argConv.CGREvent.Event[utils.RunID]; !has {
t.Error("missing RunID")
} else if strVal := utils.IfaceAsString(val); strVal != utils.MetaTopUp {
t.Errorf("Expected %+v, received %+v", utils.MetaNone, strVal)
}
return nil
},
},
}
internalCDRsChann := make(chan rpcclient.ClientConnector, 1)
internalCDRsChann <- sMock
cfg := config.NewDefaultCGRConfig()
cfg.ActionSCfg().CDRsConns = []string{utils.ConcatenatedKey(utils.MetaInternal, utils.MetaCDRs)}
data := engine.NewInternalDB(nil, nil, true)
dm := engine.NewDataManager(data, config.CgrConfig().CacheCfg(), nil)
filterS := engine.NewFilterS(cfg, nil, dm)
connMgr := engine.NewConnManager(config.CgrConfig(), map[string]chan rpcclient.ClientConnector{
utils.ConcatenatedKey(utils.MetaInternal, utils.MetaCDRs): 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(nil, evNM); err != nil {
t.Error(err)
}
}