mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
Ensure CostDetails is of *EventCost type when parsing dynamicDP
CGREventWithEeIDs has also been optimized and properly tested. Comments have been added explaining the process. When sending a request to AttributeS from EEs, CostDetails from the reply will now be overwritten by the original CostDetails to preserve its type. The downside is that we are assuming that CostDetails was not altered by AttributeS. We might consider adding a type check against *engine.EventCost to at least stay backwards compatible with *gob and *internal connections. general_tests/ees_it_test.go has been updated to ensure changes are working properly.
This commit is contained in:
committed by
Dan Christian Bogos
parent
221f6e2c91
commit
49d6b8d565
@@ -116,6 +116,14 @@ func (eeS *EventExporterS) attrSProcessEvent(cgrEv *utils.CGREvent, attrIDs []st
|
||||
return nil, err
|
||||
}
|
||||
if len(rplyEv.AlteredFields) != 0 {
|
||||
|
||||
// Restore original CostDetails in reply to preserve its type, which is lost after
|
||||
// calling AttributeS via a *json connection. Assumes AttributeS does not modify
|
||||
// CostDetails. If it does, we should probably add a type check against *engine.EventCost
|
||||
// before to stay backwards compatible with *gob and *internal connections.
|
||||
if _, has := cgrEv.Event[utils.CostDetails]; has {
|
||||
rplyEv.Event[utils.CostDetails] = cgrEv.Event[utils.CostDetails]
|
||||
}
|
||||
return rplyEv.CGREvent, nil
|
||||
}
|
||||
return cgrEv, nil
|
||||
|
||||
@@ -103,7 +103,9 @@ func TestAttrSProcessEvent(t *testing.T) {
|
||||
utils.AttributeSv1ProcessEvent: func(args, reply any) error {
|
||||
rplyEv := &engine.AttrSProcessEventReply{
|
||||
AlteredFields: []string{"testcase"},
|
||||
CGREvent: &utils.CGREvent{Event: map[string]any{"testcase": 1}},
|
||||
CGREvent: &utils.CGREvent{
|
||||
Event: map[string]any{"testcase": 1},
|
||||
},
|
||||
}
|
||||
*reply.(*engine.AttrSProcessEventReply) = *rplyEv
|
||||
return nil
|
||||
@@ -131,7 +133,7 @@ func TestAttrSProcessEvent(t *testing.T) {
|
||||
if rplyEv, err := eeS.attrSProcessEvent(cgrEv, []string{}, utils.EmptyString); err != nil {
|
||||
t.Error(err)
|
||||
} else if !reflect.DeepEqual(exp, rplyEv) {
|
||||
t.Errorf("Expected %v but received %v", utils.ToJSON(exp), utils.ToJSON(cgrEv))
|
||||
t.Errorf("Expected %v but received %v", utils.ToJSON(exp), utils.ToJSON(rplyEv))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,11 @@ import (
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
// CGREventWithEeIDs struct is moved in engine due to importing ciclying packages in order to unmarshalling properly for our EventCost type. This is the API struct argument
|
||||
|
||||
// CGREventWithEeIDs is CGREvent with EventExporterIDs
|
||||
// CGREventWithEeIDs is CGREvent with EventExporterIDs. This
|
||||
// struct is used as an API argument. It has been moved into
|
||||
// the engine package to avoid import cycling issues that were
|
||||
// encountered when trying to properly handle unmarshalling
|
||||
// for our EventCost type.
|
||||
type CGREventWithEeIDs struct {
|
||||
EeIDs []string
|
||||
*utils.CGREvent
|
||||
@@ -54,49 +56,62 @@ func (attr *CGREventWithEeIDs) RPCClone() (any, error) {
|
||||
return attr.Clone(), nil
|
||||
}
|
||||
|
||||
func (cgr *CGREventWithEeIDs) UnmarshalJSON(data []byte) (err error) {
|
||||
// firstly, we will unamrshall the entire data into raw bytes
|
||||
ids := make(map[string]json.RawMessage)
|
||||
if err = json.Unmarshal(data, &ids); err != nil {
|
||||
return
|
||||
// UnmarshalJSON decodes the JSON data into a CGREventWithEeIDs, while
|
||||
// ensuring that the CostDetails key of the embedded CGREvent is of
|
||||
// type *engine.EventCost.
|
||||
func (cgr *CGREventWithEeIDs) UnmarshalJSON(data []byte) error {
|
||||
|
||||
// Define a temporary struct with the same
|
||||
// structure as CGREventWithEeIDs.
|
||||
var temp struct {
|
||||
EeIDs []string
|
||||
*utils.CGREvent
|
||||
}
|
||||
// populate eeids in case of it's existance
|
||||
eeIDs := make([]string, len(ids[utils.EeIDs]))
|
||||
if err = json.Unmarshal(ids[utils.EeIDs], &eeIDs); err != nil {
|
||||
return
|
||||
|
||||
// Unmarshal JSON data into the temporary struct,
|
||||
// using the default unmarshaler.
|
||||
err := json.Unmarshal(data, &temp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cgr.EeIDs = eeIDs
|
||||
// populate the entire CGRevent struct in case of it's existance
|
||||
var cgrEv *utils.CGREvent
|
||||
if err = json.Unmarshal(data, &cgrEv); err != nil {
|
||||
return
|
||||
}
|
||||
cgr.CGREvent = cgrEv
|
||||
// check if we have CostDetails and modify it's type (by default it was map[string]any by unrmarshaling, now it will be EventCost)
|
||||
if ecEv, has := cgrEv.Event[utils.CostDetails]; has {
|
||||
var bts []byte
|
||||
switch ecEv.(type) {
|
||||
|
||||
if ecEv, has := temp.Event[utils.CostDetails]; has {
|
||||
var ecBytes []byte
|
||||
|
||||
// CostDetails value can either be a JSON string (which is
|
||||
// the marshaled form of an EventCost) or a map representing
|
||||
// the EventCost directly.
|
||||
switch v := ecEv.(type) {
|
||||
case string:
|
||||
btsToStr, err := json.Marshal(ecEv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var toString string
|
||||
if err = json.Unmarshal(btsToStr, &toString); err != nil {
|
||||
return err
|
||||
}
|
||||
bts = []byte(toString)
|
||||
// If string, it's assumed to be the JSON
|
||||
// representation of EventCost.
|
||||
ecBytes = []byte(v)
|
||||
default:
|
||||
bts, err = json.Marshal(ecEv)
|
||||
// Otherwise we assume it's a map and we marshal
|
||||
// it back to JSON to prepare for unmarshalling
|
||||
// into EventCost.
|
||||
ecBytes, err = json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ec := new(EventCost)
|
||||
if err = json.Unmarshal(bts, &ec); err != nil {
|
||||
|
||||
// Unmarshal the JSON (either directly from the string case
|
||||
// or from the marshaled map) into an EventCost struct.
|
||||
var ec EventCost
|
||||
if err := json.Unmarshal(ecBytes, &ec); err != nil {
|
||||
return err
|
||||
}
|
||||
cgr.Event[utils.CostDetails] = ec
|
||||
|
||||
// Update the Event map with the unmarshalled EventCost,
|
||||
// ensuring the type of CostDetails is *EventCost.
|
||||
temp.Event[utils.CostDetails] = &ec
|
||||
}
|
||||
return
|
||||
|
||||
// Assign the extracted EeIDs and CGREvent
|
||||
// to the main struct fields.
|
||||
cgr.EeIDs = temp.EeIDs
|
||||
cgr.CGREvent = temp.CGREvent
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
130
engine/argees_test.go
Normal file
130
engine/argees_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
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 engine
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cgrates/cgrates/utils"
|
||||
)
|
||||
|
||||
func TestArgEEsUnmarshalJSON(t *testing.T) {
|
||||
var testEC = &EventCost{
|
||||
CGRID: "164b0422fdc6a5117031b427439482c6a4f90e41",
|
||||
RunID: utils.MetaDefault,
|
||||
StartTime: time.Date(2017, 1, 9, 16, 18, 21, 0, time.UTC),
|
||||
AccountSummary: &AccountSummary{
|
||||
Tenant: "cgrates.org",
|
||||
ID: "dan",
|
||||
BalanceSummaries: []*BalanceSummary{
|
||||
{
|
||||
UUID: "8c54a9e9-d610-4c82-bcb5-a315b9a65010",
|
||||
ID: "BALANCE_1",
|
||||
Type: utils.MetaMonetary,
|
||||
Value: 50,
|
||||
Initial: 60,
|
||||
Disabled: false,
|
||||
},
|
||||
},
|
||||
AllowNegative: false,
|
||||
Disabled: false,
|
||||
},
|
||||
}
|
||||
cdBytes, err := json.Marshal(testEC)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cgrEvWithIDs := CGREventWithEeIDs{
|
||||
EeIDs: []string{"id1", "id2"},
|
||||
CGREvent: &utils.CGREvent{
|
||||
Tenant: "cgrates.org",
|
||||
ID: "ev1",
|
||||
Event: map[string]any{
|
||||
utils.AccountField: "1001",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("UnmarshalFromMap", func(t *testing.T) {
|
||||
var cdMap map[string]any
|
||||
if err = json.Unmarshal(cdBytes, &cdMap); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cgrEvWithIDs.Event[utils.CostDetails] = cdMap
|
||||
|
||||
cgrEvBytes, err := json.Marshal(cgrEvWithIDs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var rcvCGREv CGREventWithEeIDs
|
||||
if err = json.Unmarshal(cgrEvBytes, &rcvCGREv); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedType := "*engine.EventCost"
|
||||
if cdType := fmt.Sprintf("%T", rcvCGREv.Event[utils.CostDetails]); cdType != expectedType {
|
||||
t.Fatalf("expected type to be %v, received %v", expectedType, cdType)
|
||||
}
|
||||
|
||||
cgrEvWithIDs.Event[utils.CostDetails] = testEC
|
||||
if !reflect.DeepEqual(rcvCGREv, cgrEvWithIDs) {
|
||||
t.Errorf("expected: %v,\nreceived: %v",
|
||||
utils.ToJSON(cgrEvWithIDs), utils.ToJSON(rcvCGREv))
|
||||
}
|
||||
})
|
||||
t.Run("UnmarshalFromString", func(t *testing.T) {
|
||||
cdStringBytes, err := json.Marshal(string(cdBytes))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cdString string
|
||||
if err = json.Unmarshal(cdStringBytes, &cdString); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cgrEvWithIDs.Event[utils.CostDetails] = cdString
|
||||
|
||||
cgrEvBytes, err := json.Marshal(cgrEvWithIDs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var rcvCGREv CGREventWithEeIDs
|
||||
if err = json.Unmarshal(cgrEvBytes, &rcvCGREv); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedType := "*engine.EventCost"
|
||||
if cdType := fmt.Sprintf("%T", rcvCGREv.Event[utils.CostDetails]); cdType != expectedType {
|
||||
t.Fatalf("expected type to be %v, received %v", expectedType, cdType)
|
||||
}
|
||||
|
||||
cgrEvWithIDs.Event[utils.CostDetails] = testEC
|
||||
if !reflect.DeepEqual(rcvCGREv, cgrEvWithIDs) {
|
||||
t.Errorf("expected: %v,\nreceived: %v",
|
||||
utils.ToJSON(cgrEvWithIDs), utils.ToJSON(rcvCGREv))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
package engine
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/nyaruka/phonenumbers"
|
||||
@@ -71,6 +72,45 @@ func (dDP *dynamicDP) FieldAsInterface(fldPath []string) (val any, err error) {
|
||||
if len(fldPath) == 0 {
|
||||
return nil, utils.ErrNotFound
|
||||
}
|
||||
|
||||
// Parsing deeper than the first level in *req.CostDetails requires it to be
|
||||
// of type *EventCost. If not, we serialize and deserialize into an *EventCost.
|
||||
if len(fldPath) > 3 &&
|
||||
fldPath[0] == utils.MetaReq && fldPath[1] == utils.CostDetails {
|
||||
if mp, canCast := dDP.initialDP.(utils.MapStorage); canCast {
|
||||
if event, canCast := mp[utils.MetaReq].(map[string]any); canCast {
|
||||
if cd, has := event[utils.CostDetails]; has {
|
||||
var cdBytes []byte
|
||||
switch cd := cd.(type) {
|
||||
case *EventCost:
|
||||
// Directly proceed if already *EventCost.
|
||||
return cd.FieldAsInterface(fldPath[2:])
|
||||
case string:
|
||||
// Convert string to bytes for unmarshalling
|
||||
// if it's a serialized *EventCost.
|
||||
cdBytes = []byte(cd)
|
||||
default:
|
||||
// Marshal non-string types to JSON bytes
|
||||
// for unmarshalling into *EventCost.
|
||||
cdBytes, err = json.Marshal(cd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var ec EventCost
|
||||
if err = json.Unmarshal(cdBytes, &ec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update CostDetails with the unmarshalled *EventCost
|
||||
// to avoid repetitive serialization.
|
||||
event[utils.CostDetails] = &ec
|
||||
return ec.FieldAsInterface(fldPath[2:])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if initialDPPrefixes.Has(fldPath[0]) {
|
||||
return dDP.initialDP.FieldAsInterface(fldPath)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,10 @@ func TestEEsExportEventChanges(t *testing.T) {
|
||||
|
||||
content := `{
|
||||
|
||||
"general": {
|
||||
"log_level": 7
|
||||
},
|
||||
|
||||
"data_db": {
|
||||
"db_type": "*internal"
|
||||
},
|
||||
@@ -69,7 +73,7 @@ func TestEEsExportEventChanges(t *testing.T) {
|
||||
|
||||
"ees": {
|
||||
"enabled": true,
|
||||
"attributes_conns":["*internal"],
|
||||
"attributes_conns":["*localhost"],
|
||||
"exporters": [
|
||||
{
|
||||
"id": "exporter1",
|
||||
@@ -82,6 +86,9 @@ func TestEEsExportEventChanges(t *testing.T) {
|
||||
"fields":[
|
||||
{"tag": "CGRID", "path": "*uch.CGRID1", "type": "*variable", "value": "~*req.CGRID"},
|
||||
{"tag": "RequestType", "path": "*uch.RequestType1", "type": "*variable", "value": "~*req.RequestType"},
|
||||
{"tag": "BalanceID", "path": "*uch.BalanceID", "type": "*variable", "value": "~*req.CostDetails.Charges[0].Increments[0].Accounting.Balance.ID"},
|
||||
{"tag": "BalanceType", "path": "*uch.BalanceType", "type": "*variable", "value": "~*req.CostDetails.Charges[0].Increments[0].Accounting.Balance.Type"},
|
||||
{"tag": "BalanceFound", "path": "*uch.BalanceFound", "type": "*variable", "value": "~*req.BalanceFound"},
|
||||
{"tag": "ExporterID", "path": "*uch.ExporterID1", "type": "*variable", "value": "~*opts.*exporterID"}
|
||||
],
|
||||
},
|
||||
@@ -136,6 +143,19 @@ func TestEEsExportEventChanges(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
FilterIDs: []string{
|
||||
"*string:~*req.CostDetails.Charges[0].Increments[0].Accounting.Balance.ID:BALANCE_TEST",
|
||||
"*string:~*req.CostDetails.Charges[0].Increments[0].Accounting.Balance.Type:*voice",
|
||||
},
|
||||
Path: "*req.BalanceFound",
|
||||
Type: utils.MetaVariable,
|
||||
Value: config.RSRParsers{
|
||||
&config.RSRParser{
|
||||
Rules: utils.TrueStr,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Blocker: false,
|
||||
Weight: 10,
|
||||
@@ -157,6 +177,31 @@ func TestEEsExportEventChanges(t *testing.T) {
|
||||
Event: map[string]any{
|
||||
utils.CGRID: "TEST",
|
||||
utils.RequestType: utils.MetaRated,
|
||||
utils.CostDetails: &engine.EventCost{
|
||||
Charges: []*engine.ChargingInterval{
|
||||
{
|
||||
Increments: []*engine.ChargingIncrement{
|
||||
{
|
||||
AccountingID: "ACCOUNTING_TEST",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Accounting: engine.Accounting{
|
||||
"ACCOUNTING_TEST": &engine.BalanceCharge{
|
||||
BalanceUUID: "123456",
|
||||
},
|
||||
},
|
||||
AccountSummary: &engine.AccountSummary{
|
||||
BalanceSummaries: engine.BalanceSummaries{
|
||||
{
|
||||
ID: "BALANCE_TEST",
|
||||
Type: utils.MetaVoice,
|
||||
UUID: "123456",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -220,4 +265,43 @@ func TestEEsExportEventChanges(t *testing.T) {
|
||||
t.Errorf("expected %v, received %v", "exporter2", exporterID2)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CheckAttributesAlteredFields", func(t *testing.T) {
|
||||
var balanceID any
|
||||
if err = client.Call(context.Background(), utils.CacheSv1GetItem, &utils.ArgsGetCacheItemWithAPIOpts{
|
||||
Tenant: "cgrates.org",
|
||||
ArgsGetCacheItem: utils.ArgsGetCacheItem{
|
||||
CacheID: utils.CacheUCH,
|
||||
ItemID: "BalanceID",
|
||||
},
|
||||
}, &balanceID); err != nil {
|
||||
t.Error(err)
|
||||
} else if balanceID != "BALANCE_TEST" {
|
||||
t.Errorf("expected %v, received %v", "BALANCE_TEST", balanceID)
|
||||
}
|
||||
var balanceType any
|
||||
if err = client.Call(context.Background(), utils.CacheSv1GetItem, &utils.ArgsGetCacheItemWithAPIOpts{
|
||||
Tenant: "cgrates.org",
|
||||
ArgsGetCacheItem: utils.ArgsGetCacheItem{
|
||||
CacheID: utils.CacheUCH,
|
||||
ItemID: "BalanceType",
|
||||
},
|
||||
}, &balanceType); err != nil {
|
||||
t.Error(err)
|
||||
} else if balanceType != utils.MetaVoice {
|
||||
t.Errorf("expected %v, received %v", "BALANCE_TEST", balanceType)
|
||||
}
|
||||
var balanceFound any
|
||||
if err = client.Call(context.Background(), utils.CacheSv1GetItem, &utils.ArgsGetCacheItemWithAPIOpts{
|
||||
Tenant: "cgrates.org",
|
||||
ArgsGetCacheItem: utils.ArgsGetCacheItem{
|
||||
CacheID: utils.CacheUCH,
|
||||
ItemID: "BalanceFound",
|
||||
},
|
||||
}, &balanceFound); err != nil {
|
||||
t.Error(err)
|
||||
} else if balanceFound != "true" {
|
||||
t.Errorf("expected %v, received %v", "true", balanceFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user