From 825eea99076c400a311e51361c405a5c9b2e64e3 Mon Sep 17 00:00:00 2001 From: DanB Date: Wed, 3 Feb 2021 20:12:57 +0100 Subject: [PATCH] AccountS - integration test for AccountSv1AccountProfileForEvent API --- accounts/accounts.go | 54 ++++- apier/v1/accountprofiles.go | 18 ++ apier/v1/accountsv1_it_test.go | 208 ++++++++++++++++++ .../samples/accounts_internal/cgrates.json | 43 ++++ .../tutaccounts/AccountProfiles.csv | 6 +- utils/consts.go | 7 +- 6 files changed, 322 insertions(+), 14 deletions(-) create mode 100644 apier/v1/accountsv1_it_test.go create mode 100644 data/conf/samples/accounts_internal/cgrates.json diff --git a/accounts/accounts.go b/accounts/accounts.go index 253586d05..0739272b0 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -24,6 +24,7 @@ import ( "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/guardian" "github.com/cgrates/cgrates/utils" "github.com/ericlagergren/decimal" ) @@ -65,7 +66,10 @@ func (aS *AccountS) Call(serviceMethod string, args interface{}, reply interface } // matchingAccountForEvent returns the matched Account for the given event -func (aS *AccountS) matchingAccountForEvent(tnt string, cgrEv *utils.CGREvent, acntIDs []string) (acnt *utils.AccountProfile, err error) { +// if lked option is passed, the AccountProfile will be also locked +// so it becomes responsibility of upper layers to release the lock +func (aS *AccountS) matchingAccountForEvent(tnt string, cgrEv *utils.CGREvent, + acntIDs []string, lked bool) (acnt *utils.AccountProfile, lkID string, err error) { evNm := utils.MapStorage{ utils.MetaReq: cgrEv.Event, utils.MetaOpts: cgrEv.Opts, @@ -78,7 +82,7 @@ func (aS *AccountS) matchingAccountForEvent(tnt string, cgrEv *utils.CGREvent, a aS.cfg.AccountSCfg().PrefixIndexedFields, aS.cfg.AccountSCfg().SuffixIndexedFields, aS.dm, - utils.CacheActionProfilesFilterIndexes, + utils.CacheAccountProfilesFilterIndexes, tnt, aS.cfg.AccountSCfg().IndexedSelects, aS.cfg.AccountSCfg().NestedFields, @@ -88,9 +92,18 @@ func (aS *AccountS) matchingAccountForEvent(tnt string, cgrEv *utils.CGREvent, a acntIDs = actIDsMp.AsSlice() } for _, acntID := range acntIDs { + var refID string + if lked { + cacheKey := utils.ConcatenatedKey(utils.CacheAccountProfiles, acntID) + refID = guardian.Guardian.GuardIDs("", + aS.cfg.GeneralCfg().LockingTimeout, cacheKey) // RPC caching needs to be atomic + } var qAcnt *utils.AccountProfile if qAcnt, err = aS.dm.GetAccountProfile(tnt, acntID, true, true, utils.NonTransactional); err != nil { + if lked { + guardian.Guardian.UnguardIDs(refID) + } if err == utils.ErrNotFound { err = nil continue @@ -98,24 +111,41 @@ func (aS *AccountS) matchingAccountForEvent(tnt string, cgrEv *utils.CGREvent, a return } if _, isDisabled := qAcnt.Opts[utils.Disabled]; isDisabled { + if lked { + guardian.Guardian.UnguardIDs(refID) + } continue } if qAcnt.ActivationInterval != nil && cgrEv.Time != nil && !qAcnt.ActivationInterval.IsActiveAtTime(*cgrEv.Time) { // not active + if lked { + guardian.Guardian.UnguardIDs(refID) + } continue } var pass bool if pass, err = aS.fltrS.Pass(tnt, qAcnt.FilterIDs, evNm); err != nil { + if lked { + guardian.Guardian.UnguardIDs(refID) + } return } else if !pass { + if lked { + guardian.Guardian.UnguardIDs(refID) + } continue } if acnt == nil || acnt.Weight < qAcnt.Weight { acnt = qAcnt + if lked { + lkID = refID + } + } else if lked { + guardian.Guardian.UnguardIDs(refID) } } if acnt == nil { - return nil, utils.ErrNotFound + return nil, "", utils.ErrNotFound } return } @@ -167,27 +197,29 @@ func (aS *AccountS) accountProcessEvent(acnt *utils.AccountProfile, // V1AccountProfileForEvent returns the matching AccountProfile for Event func (aS *AccountS) V1AccountProfileForEvent(args *utils.ArgsAccountForEvent, ap *utils.AccountProfile) (err error) { var acnt *utils.AccountProfile - if acnt, err = aS.matchingAccountForEvent(args.CGREvent.Tenant, - args.CGREvent, args.AccountIDs); err != nil { + if acnt, _, err = aS.matchingAccountForEvent(args.CGREvent.Tenant, + args.CGREvent, args.AccountIDs, false); err != nil { if err != utils.ErrNotFound { err = utils.NewErrServerError(err) } return } - *ap = *acnt // Make sure we clone in RPC + *ap = *acnt // ToDo: make sure we clone in RPC return } // V1MaxUsage returns the maximum usage for the event, based on matching Account func (aS *AccountS) V1MaxUsage(args *utils.ArgsAccountForEvent, ec *utils.EventCharges) (err error) { var acnt *utils.AccountProfile - if acnt, err = aS.matchingAccountForEvent(args.CGREvent.Tenant, - args.CGREvent, args.AccountIDs); err != nil { + var lkID string + if acnt, lkID, err = aS.matchingAccountForEvent(args.CGREvent.Tenant, + args.CGREvent, args.AccountIDs, true); err != nil { if err != utils.ErrNotFound { err = utils.NewErrServerError(err) } return } + defer guardian.Guardian.UnguardIDs(lkID) var procEC *utils.EventCharges if procEC, err = aS.accountProcessEvent(acnt, args.CGREvent); err != nil { @@ -201,13 +233,15 @@ func (aS *AccountS) V1MaxUsage(args *utils.ArgsAccountForEvent, ec *utils.EventC // V1DebitUsage performs debit for the provided event func (aS *AccountS) V1DebitUsage(args *utils.ArgsAccountForEvent, ec *utils.EventCharges) (err error) { var acnt *utils.AccountProfile - if acnt, err = aS.matchingAccountForEvent(args.CGREvent.Tenant, - args.CGREvent, args.AccountIDs); err != nil { + var lkID string + if acnt, lkID, err = aS.matchingAccountForEvent(args.CGREvent.Tenant, + args.CGREvent, args.AccountIDs, true); err != nil { if err != utils.ErrNotFound { err = utils.NewErrServerError(err) } return } + defer guardian.Guardian.UnguardIDs(lkID) var procEC *utils.EventCharges if procEC, err = aS.accountProcessEvent(acnt, args.CGREvent); err != nil { diff --git a/apier/v1/accountprofiles.go b/apier/v1/accountprofiles.go index 5ca7294aa..23e1dbb7f 100644 --- a/apier/v1/accountprofiles.go +++ b/apier/v1/accountprofiles.go @@ -157,3 +157,21 @@ func (aSv1 *AccountSv1) Ping(ign *utils.CGREvent, reply *string) error { *reply = utils.Pong return nil } + +// AccountProfileForEvent returns the matching AccountProfile for Event +func (aSv1 *AccountSv1) AccountProfileForEvent(args *utils.ArgsAccountForEvent, + ap *utils.AccountProfile) (err error) { + return aSv1.aS.V1AccountProfileForEvent(args, ap) +} + +// MaxUsage returns the maximum usage for the event, based on matching Account +func (aSv1 *AccountSv1) MaxUsage(args *utils.ArgsAccountForEvent, + ec *utils.EventCharges) (err error) { + return aSv1.aS.V1MaxUsage(args, ec) +} + +// DebitUsage performs debit for the provided event +func (aSv1 *AccountSv1) DebitUsage(args *utils.ArgsAccountForEvent, + ec *utils.EventCharges) (err error) { + return aSv1.aS.V1DebitUsage(args, ec) +} diff --git a/apier/v1/accountsv1_it_test.go b/apier/v1/accountsv1_it_test.go new file mode 100644 index 000000000..9fe4a5175 --- /dev/null +++ b/apier/v1/accountsv1_it_test.go @@ -0,0 +1,208 @@ +// +build integration + +/* +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 v1 + +import ( + "net/rpc" + "path" + "reflect" + "testing" + "time" + + "github.com/cgrates/cgrates/config" + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" + "github.com/ericlagergren/decimal" +) + +var ( + acntSConfigDIR string //run tests for specific configuration + acntSCfgPath string + acntSCfg *config.CGRConfig + acntSRPC *rpc.Client + acntSDataDir = "/usr/share/cgrates" +) + +//Test start here +func TestAccountSv1IT(t *testing.T) { + sTestsAccountS := []func(t *testing.T){ + testAccountSv1InitCfg, + testAccountSv1InitDataDb, + testAccountSv1ResetStorDb, + testAccountSv1StartEngine, + testAccountSv1RPCConn, + testAccountSv1LoadFromFolder, + testAccountSv1AccountProfileForEvent, + } + switch *dbType { + case utils.MetaInternal: + acntSConfigDIR = "accounts_internal" + case utils.MetaMySQL: + t.SkipNow() + case utils.MetaMongo: + t.SkipNow() + case utils.MetaPostgres: + t.SkipNow() + default: + t.Fatalf("unknown Database type <%s>", *dbType) + } + for _, stest := range sTestsAccountS { + t.Run(acntSConfigDIR, stest) + } +} + +func testAccountSv1InitCfg(t *testing.T) { + var err error + acntSCfgPath = path.Join(acntSDataDir, "conf", "samples", acntSConfigDIR) + acntSCfg, err = config.NewCGRConfigFromPath(acntSCfgPath) + if err != nil { + t.Error(err) + } +} + +func testAccountSv1InitDataDb(t *testing.T) { + if err := engine.InitDataDb(acntSCfg); err != nil { + t.Fatal(err) + } +} + +// Wipe out the cdr database +func testAccountSv1ResetStorDb(t *testing.T) { + if err := engine.InitStorDb(acntSCfg); err != nil { + t.Fatal(err) + } +} + +// Start CGR Engine +func testAccountSv1StartEngine(t *testing.T) { + if _, err := engine.StopStartEngine(acntSCfgPath, *waitRater); err != nil { + t.Fatal(err) + } +} + +// Connect rpc client to rater +func testAccountSv1RPCConn(t *testing.T) { + var err error + acntSRPC, err = newRPCClient(acntSCfg.ListenCfg()) // We connect over JSON so we can also troubleshoot if needed + if err != nil { + t.Fatal(err) + } +} + +func testAccountSv1LoadFromFolder(t *testing.T) { + var reply string + attrs := &utils.AttrLoadTpFromFolder{FolderPath: path.Join(*dataDir, "tariffplans", "tutaccounts")} + if err := acntSRPC.Call(utils.APIerSv1LoadTariffPlanFromFolder, attrs, &reply); err != nil { + t.Error(err) + } + time.Sleep(100 * time.Millisecond) +} + +func testAccountSv1AccountProfileForEvent(t *testing.T) { + eAcnt := &utils.AccountProfile{ + Tenant: "cgrates.org", + ID: "1001", + FilterIDs: []string{"*string:~*req.Account:1001"}, + Balances: map[string]*utils.Balance{ + "GenericBalance1": &utils.Balance{ + ID: "GenericBalance1", + FilterIDs: []string{}, + Weight: 20, + Type: utils.MetaAbstract, + CostIncrements: []*utils.CostIncrement{ + &utils.CostIncrement{ + FilterIDs: []string{"*string:~*req.ToR:*voice"}, + Increment: &utils.Decimal{decimal.New(int64(time.Second), 0)}, + FixedFee: &utils.Decimal{decimal.New(0, 0)}, + RecurrentFee: &utils.Decimal{decimal.New(1, 2)}, + }, + &utils.CostIncrement{ + FilterIDs: []string{"*string:~*req.ToR:*data"}, + Increment: &utils.Decimal{decimal.New(1024, 0)}, + FixedFee: &utils.Decimal{decimal.New(0, 0)}, + RecurrentFee: &utils.Decimal{decimal.New(1, 2)}, + }, + }, + AttributeIDs: []string{}, + RateProfileIDs: []string{}, + UnitFactors: []*utils.UnitFactor{ + &utils.UnitFactor{ + FilterIDs: []string{"*string:~*req.ToR:*voice"}, + Factor: &utils.Decimal{decimal.New(int64(time.Second), 0)}, + }, + &utils.UnitFactor{ + FilterIDs: []string{"*string:~*req.ToR:*data"}, + Factor: &utils.Decimal{decimal.New(int64(1024*time.Second), 0)}, + }, + }, + Units: &utils.Decimal{decimal.New(int64(time.Hour), 0)}, + }, + "MonetaryBalance1": &utils.Balance{ + ID: "MonetaryBalance1", + FilterIDs: []string{}, + Weight: 30, + Type: utils.MetaConcrete, + CostIncrements: []*utils.CostIncrement{ + &utils.CostIncrement{ + FilterIDs: []string{"*string:~*req.ToR:*voice"}, + Increment: &utils.Decimal{decimal.New(int64(time.Second), 0)}, + FixedFee: &utils.Decimal{decimal.New(0, 0)}, + RecurrentFee: &utils.Decimal{decimal.New(1, 2)}, + }, + &utils.CostIncrement{ + FilterIDs: []string{"*string:~*req.ToR:*data"}, + Increment: &utils.Decimal{decimal.New(1024, 0)}, + FixedFee: &utils.Decimal{decimal.New(0, 0)}, + RecurrentFee: &utils.Decimal{decimal.New(1, 2)}, + }, + }, + AttributeIDs: []string{}, + RateProfileIDs: []string{}, + UnitFactors: []*utils.UnitFactor{}, + Units: &utils.Decimal{decimal.New(5, 0)}, + }, + "MonetaryBalance2": &utils.Balance{ + ID: "MonetaryBalance2", + FilterIDs: []string{}, + Weight: 10, + Type: utils.MetaConcrete, + CostIncrements: []*utils.CostIncrement{}, + AttributeIDs: []string{}, + RateProfileIDs: []string{}, + UnitFactors: []*utils.UnitFactor{}, + Units: &utils.Decimal{decimal.New(3, 0)}, + }, + }, + ThresholdIDs: []string{utils.MetaNone}, + } + var acnt *utils.AccountProfile + if err := acntSRPC.Call(utils.AccountSv1AccountProfileForEvent, + &utils.ArgsAccountForEvent{CGREvent: &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "testAccountSv1AccountProfileForEvent", + Event: map[string]interface{}{ + utils.AccountField: "1001", + }}}, &acnt); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(eAcnt, acnt) { + t.Errorf("Expecting : %s \n received: %s", utils.ToJSON(eAcnt), utils.ToJSON(acnt)) + } +} diff --git a/data/conf/samples/accounts_internal/cgrates.json b/data/conf/samples/accounts_internal/cgrates.json new file mode 100644 index 000000000..bd71f7b44 --- /dev/null +++ b/data/conf/samples/accounts_internal/cgrates.json @@ -0,0 +1,43 @@ +{ + +// CGRateS sample configuration file +// Copyright (C) ITsysCOM GmbH +// + + + +"data_db": { + "db_type": "*internal", +}, + + +"stor_db": { + "db_type": "*internal", +}, + + +"attributes": { + "enabled": true, + +}, + + +"rates": { + "enabled": true, +}, + + + +"accounts": { + "enabled": true, + "attributes_conns": ["*localhost"], + "rates_conns": ["*localhost"], +}, + + +"apiers": { + "enabled": true, +}, + +} + diff --git a/data/tariffplans/tutaccounts/AccountProfiles.csv b/data/tariffplans/tutaccounts/AccountProfiles.csv index be7b53136..87413f880 100644 --- a/data/tariffplans/tutaccounts/AccountProfiles.csv +++ b/data/tariffplans/tutaccounts/AccountProfiles.csv @@ -1,3 +1,5 @@ #Tenant,ID,FilterIDs,ActivationInterval,Weight,BalanceID,BalanceFilterIDs,BalanceWeight,BalanceBlocker,BalanceType,BalanceOpts,BalanceCostIncrements,BalanceAttributeIDs,BalanceRateProfileIDs,BalanceUnitFactors,BalanceUnits,ThresholdIDs -cgrates.org,1001,,,20,MonetaryBalance,,10,,*monetary,,fltr1&fltr2;1.3;2.3;3.3,attr1;attr2,,fltr1&fltr2;100;fltr3;200,14,*none -cgrates.org,1001,,,,VoiceBalance,,10,,*voice,,,,,,3600000000000, \ No newline at end of file +cgrates.org,1001,*string:~*req.Account:1001,,,MonetaryBalance1,,30,,*concrete,,*string:~*req.ToR:*voice;1000000000;0;0.01;*string:~*req.ToR:*data;1024;0;0.01,,,,5,*none +cgrates.org,1001,,,,GenericBalance1,,20,,*abstract,,*string:~*req.ToR:*voice;1000000000;0;0.01;*string:~*req.ToR:*data;1024;0;0.01,,,*string:~*req.ToR:*voice;1000000000;*string:~*req.ToR:*data;1024000000000,3600000000000, +cgrates.org,1001,,,,MonetaryBalance2,,10,,*concrete,,,,,,3, +cgrates.org,1002,*string:~*req.Account:1002,,10,MonetaryBalance1,,,,*concrete,,*string:~*req.ToR:*voice;1000000000;0;0.01;;1;0;1,,,,10,*none diff --git a/utils/consts.go b/utils/consts.go index f1a5ce95b..2f025a662 100755 --- a/utils/consts.go +++ b/utils/consts.go @@ -1564,8 +1564,11 @@ const ( ) const ( - AccountSv1 = "AccountSv1" - AccountSv1Ping = "AccountSv1.Ping" + AccountSv1 = "AccountSv1" + AccountSv1Ping = "AccountSv1.Ping" + AccountSv1AccountProfileForEvent = "AccountSv1.AccountProfileForEvent" + AccountSv1MaxUsage = "AccountSv1.MaxUsage" + AccountSv1DebitUsage = "AccountSv1.DebitUsage" ) const (