Files
cgrates/sessions/basics_it_test.go
ionutboangiu 6557c13b61 remove unused chargers conn in test
to prevent errors due to chargers not being enabled
2025-05-26 08:22:23 +02:00

424 lines
12 KiB
Go

//go: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 <http://www.gnu.org/licenses/>
*/
package sessions
import (
"fmt"
"testing"
"time"
"github.com/cgrates/birpc/context"
"github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/utils"
)
func TestSessionBasics(t *testing.T) {
switch *utils.DBType {
case utils.MetaInternal:
case utils.MetaMySQL, utils.MetaMongo, utils.MetaPostgres:
t.SkipNow()
default:
t.Fatal("unsupported dbtype value")
}
ng := engine.TestEngine{
ConfigJSON: `{
"logger": {
"type": "*stdout"
},
"sessions": {
"enabled": true,
"accounts_conns": ["*internal"],
"rates_conns": ["*internal"],
"cdrs_conns": ["*internal"]
},
"cdrs": {
"enabled": true,
"accounts_conns": ["*internal"],
"rates_conns": ["*internal"]
},
"accounts": {
"enabled": true,
"rates_conns": ["*internal"]
},
"admins": {
"enabled": true
},
"rates": {
"enabled": true
}
}`,
TpFiles: map[string]string{
utils.RatesCsv: `
#Tenant,ID,FilterIDs,Weights,MinCost,MaxCost,MaxCostStrategy,RateID,RateFilterIDs,RateActivationStart,RateWeights,RateBlocker,RateIntervalStart,RateFixedFee,RateRecurrentFee,RateUnit,RateIncrement
cgrates.org,RP_STANDARD,,;10,,,,RT_STANDARD,*string:~*req.Destination:1002,"* * * * *",;10,false,0s,1,1,1m,1m
cgrates.org,RP_STANDARD,,,,,,RT_STANDARD,,,,,1m,0,0.6,1m,1s
cgrates.org,RP_FALLBACK,,;0,,,,RT_FALLBACK,*string:~*req.Destination:1002,"* * * * *",;0,false,0s,0,0.01,1s,1s`,
},
DBCfg: engine.InternalDBCfg,
Encoding: *utils.Encoding,
// LogBuffer: new(bytes.Buffer),
}
// t.Cleanup(func() { fmt.Println(ng.LogBuffer) })
client, _ := ng.Run(t)
// account helpers
setAccount := func(t *testing.T, id string, balances []*utils.Balance) {
t.Helper()
acnt := &utils.AccountWithAPIOpts{
Account: &utils.Account{
Tenant: "cgrates.org",
ID: id,
FilterIDs: []string{
fmt.Sprintf("*string:~*req.Account:%s", id),
},
},
}
acnt.Balances = make(map[string]*utils.Balance)
for _, bal := range balances {
acnt.Balances[bal.ID] = bal
}
var replySet string
if err := client.Call(context.Background(), utils.AdminSv1SetAccount,
acnt, &replySet); err != nil {
t.Error(err)
}
}
checkAccountBalances := func(t *testing.T, acntID string, wantBalances map[string]float64) {
t.Helper()
var acnt utils.Account
if err := client.Call(context.Background(), utils.AdminSv1GetAccount,
&utils.TenantIDWithAPIOpts{
TenantID: &utils.TenantID{
Tenant: "cgrates.org",
ID: "1001",
},
}, &acnt); err != nil {
t.Fatal(err)
}
for blncID, val := range wantBalances {
gotUnits := acnt.Balances[blncID].Units
wantUnits := utils.NewDecimalFromFloat64(val)
if gotUnits.Compare(wantUnits) != 0 {
t.Errorf("acnt %q balance %q units=%s, want %s",
acntID, blncID, gotUnits.String(), wantUnits.String())
}
}
}
// cdr helpers
cdrNo := 0
processCDR := func(t *testing.T, acnt, dest, usage string, flags ...string) *utils.CDR {
t.Helper()
cdrNo++
originID := fmt.Sprintf("processCDR%d", cdrNo)
cgrEv := &utils.CGREvent{
Tenant: "cgrates.org",
Event: map[string]any{
utils.AccountField: acnt,
utils.Destination: dest,
utils.AnswerTime: "2018-01-07T17:00:00Z",
},
APIOpts: map[string]any{
utils.MetaOriginID: originID,
utils.MetaUsage: usage,
},
}
for _, flag := range flags {
cgrEv.APIOpts[flag] = true
}
var rplyProcCDR string
if err := client.Call(context.Background(), utils.SessionSv1ProcessCDR,
cgrEv, &rplyProcCDR); err != nil {
t.Error(err)
}
var cdrs []*utils.CDR
if err := client.Call(context.Background(), utils.AdminSv1GetCDRs,
&utils.CDRFilters{
FilterIDs: []string{
fmt.Sprintf("*string:~*opts.*originID:%s", originID),
},
}, &cdrs); err != nil {
t.Fatal(err)
}
if len(cdrs) != 1 {
t.Fatalf("%s received %d cdrs, want exactly one", utils.AdminSv1GetCDRs, len(cdrs))
}
return cdrs[0]
}
getCostDetails := func(t *testing.T, cdr *utils.CDR, field string) map[string]any {
t.Helper()
v, has := cdr.Opts[field]
if !has {
t.Fatalf("missing %q field in CDR opts", field)
}
costDetails, ok := v.(map[string]any)
if !ok {
t.Fatalf("cdr field %q of wrong type %T, want map[string]any", field, v)
}
return costDetails
}
checkCDR := func(t *testing.T, cdr *utils.CDR, wantCosts map[string]float64) {
t.Helper()
var got float64
for costKey, want := range wantCosts {
switch costKey {
case utils.Abstracts, utils.Concretes:
cd := getCostDetails(t, cdr, utils.MetaAccountSCost)
got = cd[costKey].(float64)
case utils.Cost:
cd := getCostDetails(t, cdr, utils.MetaRateSCost)
got = cd[costKey].(float64)
case utils.MetaCost:
got = cdr.Opts[utils.MetaCost].(float64)
default:
t.Fatalf("invalid cdr cost key: %q", costKey)
}
if got != want {
t.Errorf("cdr %s = %g, want %g", costKey, got, want)
}
}
}
// session helpers
authEvent := func(t *testing.T, wantUsage, wantErr string) {
t.Helper()
var reply V1AuthorizeReply
err := client.Call(context.Background(), utils.SessionSv1AuthorizeEvent,
&utils.CGREvent{
Tenant: "cgrates.org",
APIOpts: map[string]any{
utils.MetaAccounts: true,
},
Event: map[string]any{
utils.AccountField: "1001",
utils.Destination: "1002",
utils.SetupTime: "2018-01-07T17:00:00Z",
},
}, &reply)
assertError(t, utils.SessionSv1AuthorizeEvent, err, wantErr)
if err == nil {
wantDecimal := utils.NewDecimalFromUsageIgnoreErr(wantUsage)
if reply.MaxUsage.Compare(wantDecimal) != 0 {
t.Errorf("%s reply.MaxUsage=%s, want %s",
utils.SessionSv1AuthorizeEvent, reply.MaxUsage, wantDecimal)
t.Logf("%s reply: %s", utils.SessionSv1AuthorizeEvent, utils.ToJSON(reply))
}
}
}
authEventWithDigest := func(t *testing.T, wantUsage time.Duration, wantErr string) {
t.Helper()
var reply V1AuthorizeReplyWithDigest
err := client.Call(context.Background(), utils.SessionSv1AuthorizeEventWithDigest,
&utils.CGREvent{
Tenant: "cgrates.org",
APIOpts: map[string]any{
utils.MetaAccounts: true,
},
Event: map[string]any{
utils.AccountField: "1001",
utils.Destination: "1002",
utils.SetupTime: "2018-01-07T17:00:00Z",
},
}, &reply)
assertError(t, utils.SessionSv1AuthorizeEventWithDigest, err, wantErr)
if err == nil {
if got := time.Duration(wantUsage).Nanoseconds(); got != reply.MaxUsage {
t.Errorf("%s reply.MaxUsage=%d, want %d", utils.SessionSv1AuthorizeEventWithDigest, reply.MaxUsage, got)
t.Logf("%s reply: %s", utils.SessionSv1AuthorizeEvent, utils.ToJSON(reply))
}
}
}
t.Run("auth and cdr", func(t *testing.T) {
// Account requested not found, should fail here with error
authEvent(t, "", "ACCOUNTS_ERROR:NOT_FOUND")
// Available less than requested(1m)
setAccount(t, "1001", []*utils.Balance{
{
ID: "ABSTRACT1",
Type: utils.MetaAbstract,
Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 20.0}},
CostIncrements: []*utils.CostIncrement{
{
Increment: utils.NewDecimalFromUsageIgnoreErr("1s"),
RecurrentFee: utils.NewDecimalFromFloat64(0.01),
},
},
Units: utils.NewDecimalFromUsageIgnoreErr("1m"),
},
{
ID: "CONCRETE1",
Type: utils.MetaConcrete,
Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 10.0}},
CostIncrements: []*utils.CostIncrement{
{
Increment: utils.NewDecimalFromUsageIgnoreErr("1s"),
RecurrentFee: utils.NewDecimalFromFloat64(0.01),
},
},
Units: utils.NewDecimalFromFloat64(0.5),
},
})
authEvent(t, "50s", "")
authEventWithDigest(t, 50*time.Second, "")
setAccount(t, "1001", []*utils.Balance{
{
ID: "CONCRETE1",
Type: utils.MetaConcrete,
Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 10.0}},
CostIncrements: []*utils.CostIncrement{
{
Increment: utils.NewDecimalFromUsageIgnoreErr("1s"),
RecurrentFee: utils.NewDecimalFromFloat64(0.01),
},
},
Units: utils.NewDecimalFromFloat64(10),
},
})
authEvent(t, "1m", "")
authEventWithDigest(t, time.Minute, "")
// accounting via CostIncrements
cdr := processCDR(t, "1001", "1002", "1m30s", utils.MetaAccounts)
checkCDR(t, cdr,
map[string]float64{
utils.Abstracts: 90000000000.0,
utils.Concretes: 0.9,
utils.MetaCost: 0.9,
})
checkAccountBalances(t, "1001", map[string]float64{
"CONCRETE1": 9.1,
})
})
t.Run("rates accounting", func(t *testing.T) {
setAccount(t, "1001", []*utils.Balance{
{
ID: "CONCRETE1",
Type: utils.MetaConcrete,
Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 10.0}},
Units: utils.NewDecimalFromFloat64(10),
},
})
cdr := processCDR(t, "1001", "1002", "2m30s", utils.MetaAccounts)
checkCDR(t, cdr,
map[string]float64{
utils.Abstracts: float64(150 * time.Second),
utils.Concretes: 2.9,
utils.MetaCost: 2.9,
})
checkAccountBalances(t, "1001", map[string]float64{
"CONCRETE1": 7.1,
})
})
t.Run("rating", func(t *testing.T) {
setAccount(t, "1001", []*utils.Balance{
{
ID: "CONCRETE1",
Type: utils.MetaConcrete,
Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 10.0}},
Units: utils.NewDecimalFromFloat64(10),
},
})
cdr := processCDR(t, "1001", "1002", "2m30s", utils.MetaRates)
checkCDR(t, cdr,
map[string]float64{
utils.Cost: 2.9,
utils.MetaCost: 2.9,
})
})
t.Run("rates accounting with fallback", func(t *testing.T) {
t.Skip("looping through all max_increments inside maxDebitAbstractsFromConcretes")
setAccount(t, "1001", []*utils.Balance{
{
ID: "CONCRETE1",
Type: utils.MetaConcrete,
Weights: utils.DynamicWeights{&utils.DynamicWeight{Weight: 10.0}},
Units: utils.NewDecimalFromFloat64(2.9), // balance only enough for 2m30s usage
},
})
cdr := processCDR(t, "1001", "1002", "3m15s", utils.MetaAccounts)
checkCDR(t, cdr,
map[string]float64{
utils.Abstracts: float64(150 * time.Second),
utils.Concretes: 2.9,
utils.MetaCost: 2.9,
})
checkAccountBalances(t, "1001", map[string]float64{
"CONCRETE1": 7.1,
})
})
t.Run("rating and accounting", func(t *testing.T) {
setAccount(t, "1001", []*utils.Balance{
{
ID: "CONCRETE1",
Type: utils.MetaConcrete,
CostIncrements: []*utils.CostIncrement{
{
Increment: utils.NewDecimalFromUsageIgnoreErr("1s"),
RecurrentFee: utils.NewDecimalFromFloat64(0.01),
},
},
Units: utils.NewDecimalFromFloat64(10),
},
})
cdr := processCDR(t, "1001", "1002", "2m30s", utils.MetaAccounts, utils.MetaRates)
checkCDR(t, cdr,
map[string]float64{
utils.Abstracts: float64(150 * time.Second),
utils.Concretes: 1.5,
utils.MetaCost: 1.5,
utils.Cost: 2.9,
})
checkAccountBalances(t, "1001", map[string]float64{
"CONCRETE1": 8.5,
})
})
}
func assertError(t *testing.T, method string, err error, wantErr string) {
t.Helper()
if wantErr == "" {
if err != nil {
t.Fatalf("%s: unexpected error: got %v, want none", method, err)
}
} else {
if err == nil {
t.Fatalf("%s: expected error %q, got none", method, wantErr)
}
if err.Error() != wantErr {
t.Fatalf("%s: error mismatch: got %q, want %q", method, err.Error(), wantErr)
}
}
}