Files
cgrates/sessions/basics_it_test.go
2026-01-14 10:31:50 +01:00

795 lines
21 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 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 <https://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) {
var dbcfg engine.DBCfg
switch *utils.DBType {
case utils.MetaInternal:
dbcfg = engine.InternalDBCfg
case utils.MetaRedis:
dbcfg = engine.DBCfg{
DB: &engine.DBParams{
DBConns: map[string]engine.DBConn{
utils.MetaDefault: {
Type: utils.StringPointer(utils.MetaRedis),
Host: utils.StringPointer("127.0.0.1"),
Port: utils.IntPointer(6379),
Name: utils.StringPointer("10"),
User: utils.StringPointer(utils.CGRateSLwr),
},
},
},
}
case utils.MetaMySQL:
dbcfg = engine.MySQLDBCfg
case utils.MetaMongo:
dbcfg = engine.MongoDBCfg
case utils.MetaPostgres:
dbcfg = engine.PostgresDBCfg
default:
t.Fatal("unsupported dbtype value")
}
ng := engine.TestEngine{
ConfigJSON: `{
"logger": {
"type": "*stdout"
},
"db": {
"db_conns": {
"*default": {
"db_type": "*internal",
"opts":{
"internalDBRewriteInterval": "0s",
"internalDBDumpInterval": "0s"
}
}
},
},
"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: dbcfg,
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),
"*exists:~*opts.*originID:",
"*notexists:~*req.NonExistentField:",
"*notempty:~*opts.*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)
if cd == nil {
t.Fatalf("Nil costDetails")
}
var canCast bool
got, canCast = cd[costKey].(float64)
if !canCast {
t.Fatalf("Could not cast cdr.Opts[utils.MetaCost] to float64")
}
case utils.Cost:
cd := getCostDetails(t, cdr, utils.MetaRateSCost)
if cd == nil {
t.Fatalf("Nil costDetails")
}
var canCast bool
got, canCast = cd[costKey].(float64)
if !canCast {
t.Fatalf("Could not cast cdr.Opts[utils.MetaCost] to float64")
}
case utils.MetaCost:
var canCast bool
got, canCast = cdr.Opts[utils.MetaCost].(float64)
if !canCast {
t.Fatalf("Could not cast cdr.Opts[utils.MetaCost] to float64")
}
default:
t.Fatalf("invalid cdr cost key: %q", costKey)
}
if got != want {
t.Errorf("cdr %s = %g, want %g", costKey, got, want)
}
}
}
checkCDRMongo := func(t *testing.T, cdr *utils.CDR, wantCosts map[string]float64) {
t.Helper()
var got float64
for costKey, want := range wantCosts {
switch costKey {
case "abstracts", "concretes":
cd := getCostDetails(t, cdr, utils.MetaAccountsCost)
if cd == nil {
t.Fatalf("Nil costDetails")
}
got = cd[costKey].(float64)
case "cost":
cd := getCostDetails(t, cdr, utils.MetaRateSCost)
if cd == nil {
t.Fatalf("Nil costDetails")
}
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)
}
}
}
checkCDRRedis := func(t *testing.T, cdr *utils.CDR, wantCosts map[string]string) {
t.Helper()
var got string
for costKey, want := range wantCosts {
switch costKey {
case utils.Abstracts, utils.Concretes:
cd := getCostDetails(t, cdr, utils.MetaAccountsCost)
if cd == nil {
t.Fatalf("Nil costDetails")
}
var canCast bool
got, canCast = cd[costKey].(string)
if !canCast {
t.Fatalf("Could not cast cdr.Opts[utils.MetaCost] to string")
}
case utils.Cost:
cd := getCostDetails(t, cdr, utils.MetaRateSCost)
if cd == nil {
t.Fatalf("Nil costDetails")
}
var canCast bool
got, canCast = cd[costKey].(string)
if !canCast {
t.Fatalf("Could not cast cdr.Opts[utils.MetaCost] to string")
}
case utils.MetaCost:
var canCast bool
got, canCast = cdr.Opts[utils.MetaCost].(string)
if !canCast {
t.Fatalf("Could not cast cdr.Opts[utils.MetaCost] to string")
}
default:
t.Fatalf("invalid cdr cost key: %q", costKey)
}
if got != want {
t.Errorf("cdr %s = %v, want %v", 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)
switch *utils.DBType {
case utils.MetaMongo: // field names are lowercase on mongo
checkCDRMongo(t, cdr,
map[string]float64{
"abstracts": 90000000000.0,
"concretes": 0.9,
utils.MetaCost: 0.9,
})
case utils.MetaRedis:
checkCDRRedis(t, cdr,
map[string]string{
utils.Abstracts: "90000000000",
utils.Concretes: "0.9",
utils.MetaCost: "0.9",
})
default:
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)
switch *utils.DBType {
case utils.MetaMongo: // field names are lowercase on mongo
checkCDRMongo(t, cdr,
map[string]float64{
"abstracts": float64(150 * time.Second),
"concretes": 2.9,
utils.MetaCost: 2.9,
})
case utils.MetaRedis:
checkCDRRedis(t, cdr,
map[string]string{
utils.Abstracts: "150000000000",
utils.Concretes: "2.90",
utils.MetaCost: "2.90",
})
default:
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)
switch *utils.DBType {
case utils.MetaMongo: // field names are lowercase on mongo
checkCDRMongo(t, cdr,
map[string]float64{
"cost": 2.9,
utils.MetaCost: 2.9,
})
case utils.MetaRedis:
checkCDRRedis(t, cdr,
map[string]string{
utils.Cost: "2.90",
utils.MetaCost: "2.90",
})
default:
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)
if *utils.DBType == utils.MetaMongo { // field names are lowercase on mongo
checkCDRMongo(t, cdr,
map[string]float64{
"abstracts": float64(150 * time.Second),
"concretes": 2.9,
utils.MetaCost: 2.9,
})
} else {
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)
switch *utils.DBType {
case utils.MetaMongo: // field names are lowercase on mongo
checkCDRMongo(t, cdr,
map[string]float64{
"abstracts": float64(150 * time.Second),
"concretes": 1.5,
utils.MetaCost: 1.5,
"cost": 2.9,
})
case utils.MetaRedis:
checkCDRRedis(t, cdr,
map[string]string{
utils.Abstracts: "150000000000",
utils.Concretes: "1.5",
utils.MetaCost: "1.5",
utils.Cost: "2.90",
})
default:
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)
}
}
}
func TestSessionLifecycle(t *testing.T) {
var dbcfg engine.DBCfg
switch *utils.DBType {
case utils.MetaInternal:
dbcfg = engine.InternalDBCfg
case utils.MetaRedis:
dbcfg = engine.RedisDBCfg
case utils.MetaMySQL:
dbcfg = engine.MySQLDBCfg
case utils.MetaMongo:
dbcfg = engine.MongoDBCfg
case utils.MetaPostgres:
dbcfg = engine.PostgresDBCfg
default:
t.Fatal("unsupported dbtype value")
}
ng := engine.TestEngine{
ConfigJSON: `{
"db": {
"db_conns": {
"*default": {
"db_type": "*internal",
"opts":{
"internalDBRewriteInterval": "0s",
"internalDBDumpInterval": "0s"
}
}
},
},
"sessions": {
"enabled": true,
"chargers_conns": ["*localhost"],
"alterable_fields": ["AlterableField"],
"terminate_attempts": 1
},
"chargers": {
"enabled": true
},
"admins": {
"enabled": true
}
}`,
DBCfg: dbcfg,
Encoding: *utils.Encoding,
// LogBuffer: new(bytes.Buffer),
}
// t.Cleanup(func() { fmt.Println(ng.LogBuffer) })
client, _ := ng.Run(t)
initSession := func(t *testing.T, originID string) {
t.Helper()
var reply V1InitSessionReply
err := client.Call(context.Background(), utils.SessionSv1InitiateSession,
&utils.CGREvent{
Tenant: "cgrates.org",
Event: map[string]any{
utils.AccountField: "1001",
utils.Destination: "1002",
utils.SetupTime: "2018-01-07T17:00:00Z",
"AlterableField": "test_val",
},
APIOpts: map[string]any{
utils.MetaOriginID: originID,
utils.MetaInitiate: true,
utils.MetaChargers: true,
},
}, &reply)
if err != nil {
t.Fatalf("failed to initiate session %s: %v", originID, err)
}
}
updateSession := func(t *testing.T, originID string) {
t.Helper()
var reply V1UpdateSessionReply
err := client.Call(context.Background(), utils.SessionSv1UpdateSession,
&utils.CGREvent{
Tenant: "cgrates.org",
Event: map[string]any{
utils.AccountField: "1001",
utils.Destination: "1002",
utils.SetupTime: "2018-01-07T17:00:00Z",
"AlterableField": "new_val",
},
APIOpts: map[string]any{
utils.MetaOriginID: originID,
utils.MetaUpdate: true,
},
}, &reply)
if err != nil {
t.Fatalf("failed to update session %s: %v", originID, err)
}
}
termSession := func(t *testing.T, originID string) {
t.Helper()
var reply string
err := client.Call(context.Background(), utils.SessionSv1TerminateSession,
&utils.CGREvent{
Tenant: "cgrates.org",
Event: map[string]any{
utils.AccountField: "1001",
utils.Destination: "1002",
utils.SetupTime: "2018-01-07T17:00:00Z",
},
APIOpts: map[string]any{
utils.MetaOriginID: originID,
utils.MetaTerminate: true,
},
}, &reply)
if err != nil {
t.Fatalf("failed to terminate session %s: %v", originID, err)
}
}
checkActiveSessions := func(t *testing.T, wantCount int, filters ...string) []*ExternalSession {
t.Helper()
var sessions []*ExternalSession
if err := client.Call(context.Background(), utils.SessionSv1GetActiveSessions,
&utils.SessionFilter{
Filters: filters,
}, &sessions); err != nil {
if wantCount == 0 && err.Error() == utils.ErrNotFound.Error() {
t.Logf("no active sessions found (expected)")
return nil
}
t.Fatalf("failed to get active sessions: %v", err)
}
if len(sessions) != wantCount {
t.Fatalf("%s received %d sessions, want exactly %d",
utils.SessionSv1GetActiveSessions, len(sessions), wantCount)
}
t.Logf("%s reply: %s", utils.SessionSv1GetActiveSessions, utils.ToIJSON(sessions))
return sessions
}
var replySetCharger string
if err := client.Call(context.Background(), utils.AdminSv1SetChargerProfile,
&utils.ChargerProfileWithAPIOpts{
ChargerProfile: &utils.ChargerProfile{
Tenant: "cgrates.org",
ID: "DEFAULT",
RunID: utils.MetaDefault,
AttributeIDs: []string{utils.MetaNone},
},
}, &replySetCharger); err != nil {
t.Fatal(err)
}
checkActiveSessions(t, 0)
sessionID := "test-session-123"
initSession(t, sessionID)
sessions := checkActiveSessions(t, 1)
want := "test_val"
if sessions[0].CGREvent.Event["AlterableField"] != want {
t.Errorf("after init, AlterableField = %v, want %s", sessions[0].CGREvent.Event["AlterableField"], want)
}
updateSession(t, sessionID)
sessions = checkActiveSessions(t, 1)
want = "new_val"
if sessions[0].CGREvent.Event["AlterableField"] != want {
t.Errorf("after update, AlterableField = %v, want %s", sessions[0].CGREvent.Event["AlterableField"], want)
}
termSession(t, sessionID)
checkActiveSessions(t, 0)
}