diff --git a/agents/agentreq_test.go b/agents/agentreq_test.go index 15dfb1e94..f5005d120 100644 --- a/agents/agentreq_test.go +++ b/agents/agentreq_test.go @@ -3070,3 +3070,203 @@ func TestCacheRadiusPacket(t *testing.T) { } }) } + +func TestGigawordsCalculateTotalOctets(t *testing.T) { // Renamed for clarity + var gigawordMultiplier int64 = 4294967296 + configTemplate := ` +{ + "diameter_agent": { + "request_processors": [ + { + "id": "test_proc", + "filters": [], + "flags": [], + "request_fields": [ + %s + ] + } + ] + } +}` + + inputFieldConfig := `{ + "tag": "TotalUsageInput", + "path": "*cgreq.TotalUsageInput", + "type": "*sum", + "value": "~*req.Acct-Input-Gigawords{*gigawords};~*req.Acct-Input-Octets" + }` + outputFieldConfig := `{ + "tag": "TotalUsageOutput", + "path": "*cgreq.TotalUsageOutput", + "type": "*sum", + "value": "~*req.Acct-Output-Gigawords{*gigawords};~*req.Acct-Output-Octets" + }` + + testCases := []struct { + name string + fieldConfig string + inputDP utils.MapStorage + expectedValue int64 + expectErr bool + err error + }{ + + { + name: "Input: Zero GW, Zero Octets", + fieldConfig: inputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Input-Gigawords": 0, + "Acct-Input-Octets": 0, + }, + expectedValue: 0, + }, + { + name: "Input: Zero GW, Some Octets", + fieldConfig: inputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Input-Gigawords": 0, + "Acct-Input-Octets": 12345, + }, + expectedValue: 12345, + }, + { + name: "Input: Some GW (2), Zero Octets", + fieldConfig: inputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Input-Gigawords": 2, + "Acct-Input-Octets": 0, + }, + expectedValue: 2 * gigawordMultiplier, + }, + { + name: "Input: Some GW (3), Some Octets (1000)", + fieldConfig: inputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Input-Gigawords": 3, + "Acct-Input-Octets": 1000, + }, + expectedValue: (3 * gigawordMultiplier) + 1000, + }, + { + name: "Input: Missing GW, Some Octets", + fieldConfig: inputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Input-Octets": 50000, + }, + expectedValue: 0, + expectErr: true, + err: utils.ErrNotFound, + }, + { + name: "Input: Some GW, Missing Octets", + fieldConfig: inputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Input-Gigawords": 1, + }, + expectErr: true, + err: utils.ErrNotFound, + }, + { + name: "Input: Both GW and Octets Missing", + fieldConfig: inputFieldConfig, + inputDP: utils.MapStorage{}, + expectedValue: 0, + expectErr: true, + err: utils.ErrNotFound, + }, + { + name: "Input: GW as String '2', Octets as String '333'", + fieldConfig: inputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Input-Gigawords": "2", + "Acct-Input-Octets": "333", + }, + expectedValue: (2 * gigawordMultiplier) + 333, + }, + + { + name: "Output: Zero GW, Zero Octets", + fieldConfig: outputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Output-Gigawords": 0, + "Acct-Output-Octets": 0, + }, + expectedValue: 0, + }, + { + name: "Output: Zero GW, Some Octets", + fieldConfig: outputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Output-Gigawords": 0, + "Acct-Output-Octets": 98765, + }, + expectedValue: 98765, + }, + { + name: "Output: Some GW (1), Zero Octets", + fieldConfig: outputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Output-Gigawords": 1, + "Acct-Output-Octets": 0, + }, + expectedValue: 1 * gigawordMultiplier, + }, + { + name: "Output: Some GW (4), Some Octets (20000)", + fieldConfig: outputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Output-Gigawords": 4, + "Acct-Output-Octets": 20000, + }, + expectedValue: (4 * gigawordMultiplier) + 20000, + }, + { + name: "Output: Missing GW, Some Octets", + fieldConfig: outputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Output-Octets": 777, + }, + expectedValue: 0, + expectErr: true, + err: utils.ErrNotFound, + }, + { + name: "Output: Some GW, Missing Octets", + fieldConfig: outputFieldConfig, + inputDP: utils.MapStorage{ + "Acct-Output-Gigawords": 2, + }, + expectErr: true, + err: utils.ErrNotFound, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fullConf := fmt.Sprintf(configTemplate, tc.fieldConfig) + cgrconf, err := config.NewCGRConfigFromJSONStringWithDefaults(fullConf) + if err != nil { + t.Fatalf("Config parsing failed: %v", err) + } + agReq := NewAgentRequest(tc.inputDP, nil, nil, nil, nil, nil, "cgrates.org", "", nil, nil) + fieldDef := cgrconf.DiameterAgentCfg().RequestProcessors[0].RequestFields[0] + out, err := agReq.ParseField(fieldDef) + if tc.expectErr { + if err == nil || err.Error() != tc.err.Error() { + t.Fatalf("Expected error <%v>, but got <%v>", tc.err, err) + } + return + } + if err != nil { + t.Fatalf("ParseField failed unexpectedly: %v", err) + } + resultInt64, ok := out.(int64) + if !ok { + t.Fatal("ParseField result type is not int64") + } else { + if resultInt64 != tc.expectedValue { + t.Errorf("Expected result %d, but got %d", tc.expectedValue, resultInt64) + } + } + }) + } +} diff --git a/utils/consts.go b/utils/consts.go index 5a3415d21..669d0105b 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1165,6 +1165,7 @@ const ( MetaTpDispatchers = "*tp_dispatchers" MetaDurationSeconds = "*duration_seconds" MetaDurationNanoseconds = "*duration_nanoseconds" + MetaGigawords = "*gigawords" CapAttributes = "Attributes" CapResourceAllocation = "ResourceAllocation" CapMaxUsage = "MaxUsage" diff --git a/utils/dataconverter.go b/utils/dataconverter.go index e01af861d..875a16001 100644 --- a/utils/dataconverter.go +++ b/utils/dataconverter.go @@ -23,6 +23,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "math/rand" "net" "net/url" @@ -129,6 +130,8 @@ func NewDataConverter(params string) (conv DataConverter, err error) { return NewRandomConverter(params[len(MetaRandom)+1:]) case strings.HasPrefix(params, MetaStrip): return NewStripConverter(params) + case strings.HasPrefix(params, MetaGigawords): + return new(GigawordsConverter), nil default: return nil, fmt.Errorf("unsupported converter definition: <%s>", params) } @@ -787,3 +790,15 @@ func (URLEncodeConverter) Convert(in any) (any, error) { } return parsedURL.String(), nil } + +// GigawordsConverter converts a value in Gigawords to octects +type GigawordsConverter struct{} + +func (GigawordsConverter) Convert(in any) (any, error) { + gigawordsValue, err := IfaceAsInt64(in) + if err != nil { + return nil, err + } + totalOctects := (gigawordsValue * int64(math.Pow(2, 32))) // 2^32 + return totalOctects, nil +} diff --git a/utils/dataconverter_test.go b/utils/dataconverter_test.go index a69c22ba8..7cd0ba451 100644 --- a/utils/dataconverter_test.go +++ b/utils/dataconverter_test.go @@ -22,6 +22,7 @@ import ( "math" "net" "reflect" + "strings" "testing" "time" @@ -1814,3 +1815,82 @@ func TestStripConverterConvert(t *testing.T) { }) } } +func TestGigaWordsConverter(t *testing.T) { + converter := GigawordsConverter{} + multiplier := int64(4294967296) + testCases := []struct { + name string + input any + expectedValue int64 + expectError bool + errorContains string + }{ + { + name: "Input Zero (int)", + input: int(0), + expectedValue: 0, + expectError: false, + }, + { + name: "Input Zero (int32)", + input: int32(0), + expectedValue: 0, + expectError: false, + }, + { + name: "Input One (int)", + input: int(1), + expectedValue: multiplier, + expectError: false, + }, + { + name: "Input Two (int64)", + input: int64(2), + expectedValue: 2 * multiplier, + expectError: false, + }, + { + name: "Input Three (string)", + input: "3", + expectedValue: 3 * multiplier, + expectError: false, + }, + { + name: "Input Nil", + input: nil, + expectedValue: 0, + expectError: true, + errorContains: "cannot convert", + }, + { + name: "Input Invalid String", + input: "abc", + expectedValue: 0, + expectError: true, + errorContains: "strconv.ParseInt: parsing \"abc\": invalid syntax", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output, err := converter.Convert(tc.input) + if tc.expectError { + if err == nil || !strings.Contains(err.Error(), tc.errorContains) { + t.Errorf("Expected error '%s', but got '%v'", tc.errorContains, err) + } + return + } + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + + outputInt64, ok := output.(int64) + if !ok { + t.Fatalf("Expected output type int64, but got %T (%v)", output, output) + } + if outputInt64 != tc.expectedValue { + t.Errorf("Expected output %d, but got %d", tc.expectedValue, outputInt64) + } + }) + } +}