diff --git a/data/conf/samples/radius_ipam/acct.json b/data/conf/samples/radius_ipam/acct.json new file mode 100644 index 000000000..bcabe6cd7 --- /dev/null +++ b/data/conf/samples/radius_ipam/acct.json @@ -0,0 +1,446 @@ +{ + "radius_agent": { + "request_processors": [ + { + "id": "IPSAccountingStart", + "filters": [ + "*string:~*req.Acct-Status-Type:Start" + ], + "flags": [ + "*initiate", + "*ips" + ], + "request_fields": [ + { + "tag": "Category", + "path": "*cgreq.Category", + "type": "*constant", + "value": "ips" + }, + { + "tag": "RequestType", + "path": "*cgreq.RequestType", + "type": "*constant", + "value": "*prepaid", + "mandatory": true + }, + { + "tag": "OriginID", + "path": "*opts.*originID", + "type": "*variable", + "value": "~*req.Acct-Session-Id", + "mandatory": true + }, + { + "tag": "OriginHost", + "path": "*cgreq.OriginHost", + "type": "*variable", + "value": "~*req.NAS-IP-Address", + "mandatory": true + }, + { + "tag": "Account", + "path": "*cgreq.Account", + "type": "*variable", + "value": "~*req.User-Name", + "mandatory": true + }, + { + "tag": "Subject", + "path": "*cgreq.Subject", + "type": "*variable", + "value": "~*req.User-Name", + "mandatory": true + }, + { + "tag": "Destination", + "path": "*cgreq.Destination", + "type": "*variable", + "value": "~*req.Called-Station-Id", + "mandatory": true + }, + { + "tag": "SetupTime", + "path": "*cgreq.SetupTime", + "type": "*variable", + "value": "~*req.Event-Timestamp", + "mandatory": true + }, + { + "tag": "AnswerTime", + "path": "*cgreq.AnswerTime", + "type": "*variable", + "value": "~*req.Event-Timestamp", + "mandatory": true + }, + { + "tag": "IMSI", + "path": "*cgreq.IMSI", + "type": "*variable", + "value": "~*req.User-Name", + "mandatory": true + }, + { + "tag": "APN", + "path": "*cgreq.APN", + "type": "*variable", + "value": "~*req.Called-Station-Id", + "mandatory": true + }, + { + "tag": "CallingStationId", + "path": "*cgreq.CallingStationId", + "type": "*variable", + "value": "~*req.Calling-Station-Id", + "mandatory": true + }, + { + "tag": "NAS-Identifier", + "path": "*cgreq.NAS-Identifier", + "type": "*variable", + "value": "~*req.NAS-Identifier", + "mandatory": true + }, + { + "tag": "ServiceType", + "path": "*cgreq.ServiceType", + "type": "*variable", + "value": "~*req.Service-Type", + "mandatory": true + }, + { + "tag": "FramedProtocol", + "path": "*cgreq.FramedProtocol", + "type": "*variable", + "value": "~*req.Framed-Protocol", + "mandatory": true + }, + { + "tag": "NASPortType", + "path": "*cgreq.NASPortType", + "type": "*variable", + "value": "~*req.NAS-Port-Type", + "mandatory": true + }, + { + "tag": "AcctAuthentic", + "path": "*cgreq.AcctAuthentic", + "type": "*variable", + "value": "~*req.Acct-Authentic", + "mandatory": true + }, + { + "tag": "AcctDelayTime", + "path": "*cgreq.AcctDelayTime", + "type": "*variable", + "value": "~*req.Acct-Delay-Time", + "mandatory": true + }, + { + "tag": "AllocatedIP", + "path": "*cgreq.AllocatedIP", + "type": "*variable", + "value": "~*req.Framed-IP-Address", + "mandatory": true + }, + { + "tag": "SessionID", + "path": "*vars.*sessionID", + "type": "*variable", + "value": "~*req.Acct-Session-Id" + }, + { + "tag": "RemoteAddr", + "path": "*cgreq.RemoteAddr", + "type": "*variable", + "value": "~*vars.RemoteHost:s/(.*):\\d+/${1}/" + } + ], + "reply_fields": [ + { + "tag": "ProxyState", + "path": "*rep.Proxy-State", + "type": "*variable", + "value": "~*req.Proxy-State", + "mandatory": true + } + ] + }, + { + "id": "IPSAccountingAlive", + "filters": [ + "*string:~*req.Acct-Status-Type:Alive" + ], + "flags": [ + "*update" + ], + "request_fields": [ + { + "tag": "Category", + "path": "*cgreq.Category", + "type": "*constant", + "value": "ips" + }, + { + "tag": "RequestType", + "path": "*cgreq.RequestType", + "type": "*constant", + "value": "*prepaid", + "mandatory": true + }, + { + "tag": "OriginID", + "path": "*opts.*originID", + "type": "*variable", + "value": "~*req.Acct-Session-Id", + "mandatory": true + }, + { + "tag": "Account", + "path": "*cgreq.Account", + "type": "*variable", + "value": "~*req.User-Name", + "mandatory": true + }, + { + "tag": "Subject", + "path": "*cgreq.Subject", + "type": "*variable", + "value": "~*req.User-Name", + "mandatory": true + }, + { + "tag": "Destination", + "path": "*cgreq.Destination", + "type": "*variable", + "value": "~*req.Called-Station-Id", + "mandatory": true + }, + { + "tag": "SetupTime", + "path": "*cgreq.SetupTime", + "type": "*variable", + "value": "~*req.Event-Timestamp", + "mandatory": true + }, + { + "tag": "IMSI", + "path": "*cgreq.IMSI", + "type": "*variable", + "value": "~*req.User-Name", + "mandatory": true + }, + { + "tag": "APN", + "path": "*cgreq.APN", + "type": "*variable", + "value": "~*req.Called-Station-Id", + "mandatory": true + }, + { + "tag": "CallingStationId", + "path": "*cgreq.CallingStationId", + "type": "*variable", + "value": "~*req.Calling-Station-Id", + "mandatory": true + }, + { + "tag": "NAS-Identifier", + "path": "*cgreq.NAS-Identifier", + "type": "*variable", + "value": "~*req.NAS-Identifier", + "mandatory": true + }, + { + "tag": "ServiceType", + "path": "*cgreq.ServiceType", + "type": "*variable", + "value": "~*req.Service-Type", + "mandatory": true + }, + { + "tag": "FramedProtocol", + "path": "*cgreq.FramedProtocol", + "type": "*variable", + "value": "~*req.Framed-Protocol", + "mandatory": true + }, + { + "tag": "AcctInputOctets", + "path": "*cgreq.AcctInputOctets", + "type": "*variable", + "value": "~*req.Acct-Input-Octets", + "mandatory": true + }, + { + "tag": "AcctOutputOctets", + "path": "*cgreq.AcctOutputOctets", + "type": "*variable", + "value": "~*req.Acct-Output-Octets", + "mandatory": true + }, + { + "tag": "SessionID", + "path": "*vars.*sessionID", + "type": "*variable", + "value": "~*req.Acct-Session-Id" + } + ], + "reply_fields": [ + { + "tag": "ProxyState", + "path": "*rep.Proxy-State", + "type": "*variable", + "value": "~*req.Proxy-State", + "mandatory": true + } + ] + }, + { + "id": "IPSAccountingStop", + "filters": [ + "*string:~*req.Acct-Status-Type:Stop" + ], + "flags": [ + "*terminate", + "*ips" + ], + "request_fields": [ + { + "tag": "Category", + "path": "*cgreq.Category", + "type": "*constant", + "value": "ips" + }, + { + "tag": "RequestType", + "path": "*cgreq.RequestType", + "type": "*constant", + "value": "*prepaid", + "mandatory": true + }, + { + "tag": "OriginID", + "path": "*opts.*originID", + "type": "*variable", + "value": "~*req.Acct-Session-Id", + "mandatory": true + }, + { + "tag": "Account", + "path": "*cgreq.Account", + "type": "*variable", + "value": "~*req.User-Name", + "mandatory": true + }, + { + "tag": "Subject", + "path": "*cgreq.Subject", + "type": "*variable", + "value": "~*req.User-Name", + "mandatory": true + }, + { + "tag": "Destination", + "path": "*cgreq.Destination", + "type": "*variable", + "value": "~*req.Called-Station-Id", + "mandatory": true + }, + { + "tag": "SetupTime", + "path": "*cgreq.SetupTime", + "type": "*variable", + "value": "~*req.Event-Timestamp", + "mandatory": true + }, + { + "tag": "Usage", + "path": "*cgreq.Usage", + "type": "*variable", + "value": "~*req.Event-Timestamp", + "mandatory": true + }, + { + "tag": "IMSI", + "path": "*cgreq.IMSI", + "type": "*variable", + "value": "~*req.User-Name", + "mandatory": true + }, + { + "tag": "APN", + "path": "*cgreq.APN", + "type": "*variable", + "value": "~*req.Called-Station-Id", + "mandatory": true + }, + { + "tag": "CallingStationId", + "path": "*cgreq.CallingStationId", + "type": "*variable", + "value": "~*req.Calling-Station-Id", + "mandatory": true + }, + { + "tag": "NAS-Identifier", + "path": "*cgreq.NAS-Identifier", + "type": "*variable", + "value": "~*req.NAS-Identifier", + "mandatory": true + }, + { + "tag": "ServiceType", + "path": "*cgreq.ServiceType", + "type": "*variable", + "value": "~*req.Service-Type", + "mandatory": true + }, + { + "tag": "FramedProtocol", + "path": "*cgreq.FramedProtocol", + "type": "*variable", + "value": "~*req.Framed-Protocol", + "mandatory": true + }, + { + "tag": "AcctInputOctets", + "path": "*cgreq.AcctInputOctets", + "type": "*variable", + "value": "~*req.Acct-Input-Octets", + "mandatory": true + }, + { + "tag": "AcctOutputOctets", + "path": "*cgreq.AcctOutputOctets", + "type": "*variable", + "value": "~*req.Acct-Output-Octets", + "mandatory": true + }, + { + "tag": "AcctTerminateCause", + "path": "*cgreq.AcctTerminateCause", + "type": "*variable", + "value": "~*req.Acct-Terminate-Cause", + "mandatory": true + }, + { + "tag": "SessionID", + "path": "*vars.*sessionID", + "type": "*variable", + "value": "~*req.Acct-Session-Id" + } + ], + "reply_fields": [ + { + "tag": "ProxyState", + "path": "*rep.Proxy-State", + "type": "*variable", + "value": "~*req.Proxy-State", + "mandatory": true + } + ] + } + ] + } +} diff --git a/data/conf/samples/radius_ipam/auth.json b/data/conf/samples/radius_ipam/auth.json new file mode 100644 index 000000000..dc2788c50 --- /dev/null +++ b/data/conf/samples/radius_ipam/auth.json @@ -0,0 +1,172 @@ +{ + "radius_agent": { + "request_processors": [ + { + "id": "IPSAuthorization", + "filters": [ + "*string:~*vars.*radReqType:*radAuth" + ], + "flags": [ + "*authorize", + "*ips", + "*continue" + ], + "request_fields": [ + { + "tag": "Category", + "path": "*cgreq.Category", + "type": "*constant", + "value": "ips" + }, + { + "tag": "RequestType", + "path": "*cgreq.RequestType", + "type": "*constant", + "value": "*prepaid", + "mandatory": true + }, + { + "tag": "OriginID", + "path": "*opts.*originID", + "type": "*variable", + "value": "~*req.Acct-Session-Id", + "mandatory": true + }, + { + "tag": "Account", + "path": "*cgreq.Account", + "type": "*variable", + "value": "~*req.User-Name", + "mandatory": true + }, + { + "tag": "Subject", + "path": "*cgreq.Subject", + "type": "*variable", + "value": "~*req.User-Name", + "mandatory": true + }, + { + "tag": "Destination", + "path": "*cgreq.Destination", + "type": "*variable", + "value": "~*req.Called-Station-Id", + "mandatory": true + }, + { + "tag": "SetupTime", + "path": "*cgreq.SetupTime", + "type": "*variable", + "value": "~*req.Event-Timestamp", + "mandatory": true + }, + { + "tag": "AnswerTime", + "path": "*cgreq.AnswerTime", + "type": "*variable", + "value": "~*req.Event-Timestamp", + "mandatory": true + }, + { + "tag": "IMSI", + "path": "*cgreq.IMSI", + "type": "*variable", + "value": "~*req.User-Name", + "mandatory": true + }, + { + "tag": "APN", + "path": "*cgreq.APN", + "type": "*variable", + "value": "~*req.Called-Station-Id", + "mandatory": true + }, + { + "tag": "CallingStationId", + "path": "*cgreq.CallingStationId", + "type": "*variable", + "value": "~*req.Calling-Station-Id", + "mandatory": true + }, + { + "tag": "NAS-Identifier", + "path": "*cgreq.NAS-Identifier", + "type": "*variable", + "value": "~*req.NAS-Identifier", + "mandatory": true + }, + { + "tag": "FramedPool", + "path": "*cgreq.FramedPool", + "type": "*variable", + "value": "~*req.Framed-Pool", + "mandatory": true + }, + { + "tag": "ServiceType", + "path": "*cgreq.ServiceType", + "type": "*variable", + "value": "~*req.Service-Type", + "mandatory": true + }, + { + "tag": "FramedProtocol", + "path": "*cgreq.FramedProtocol", + "type": "*variable", + "value": "~*req.Framed-Protocol", + "mandatory": true + }, + { + "tag": "NAS-IP", + "path": "*cgreq.NAS-IP", + "type": "*variable", + "value": "~*req.NAS-IP-Address", + "mandatory": true + } + ], + "reply_fields": [ + { + "tag": "FramedIPAddress", + "path": "*rep.Framed-IP-Address", + "type": "*variable", + "value": "~*cgrep.AllocatedIP.Address", + "mandatory": true + }, + { + "tag": "FramedProtocol", + "path": "*rep.Framed-Protocol", + "type": "*constant", + "value": "PPP", + "mandatory": true + }, + { + "tag": "ServiceType", + "path": "*rep.Service-Type", + "type": "*constant", + "value": "Framed", + "mandatory": true + }, + { + "tag": "ReplyMessage", + "path": "*rep.Reply-Message", + "type": "*variable", + "value": "Pool:;~*cgrep.AllocatedIP.PoolID; - ;~*cgrep.AllocatedIP.Message", + }, + { + "tag": "Class", + "path": "*rep.Class", + "type": "*variable", + "value": "~*cgrep.AllocatedIP.ProfileID", + }, + { + "tag": "ProxyState", + "path": "*rep.Proxy-State", + "type": "*variable", + "value": "~*req.Proxy-State", + "mandatory": true + } + ] + } + ] + } +} diff --git a/data/conf/samples/radius_ipam/cgrates.json b/data/conf/samples/radius_ipam/cgrates.json new file mode 100644 index 000000000..5320ca691 --- /dev/null +++ b/data/conf/samples/radius_ipam/cgrates.json @@ -0,0 +1,45 @@ +{ + "sessions": { + "enabled": true, + "ips_conns": [ + "*localhost" + ], + "opts": { + "*ipsAuthorize": [ + { + "Value": true + } + ], + "*ipsAllocate": [ + { + "Value": true + } + ], + "*ipsRelease": [ + { + "Value": true + } + ] + } + }, + "ips": { + "enabled": true, + "store_interval": "-1" + }, + "radius_agent": { + "enabled": true, + "sessions_conns": [ + "*bijson_localhost" + ], + "listeners": [ + { + "network": "udp", + "auth_address": "127.0.0.1:1812", + "acct_address": "127.0.0.1:1813" + } + ] + }, + "admins": { + "enabled": true + } +} diff --git a/general_tests/radius_ipam_it_test.go b/general_tests/radius_ipam_it_test.go new file mode 100644 index 000000000..264b6091a --- /dev/null +++ b/general_tests/radius_ipam_it_test.go @@ -0,0 +1,362 @@ +//go:build integration +// +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 general_tests + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/cgrates/birpc" + "github.com/cgrates/birpc/context" + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" + "github.com/cgrates/radigo" +) + +func TestRadiusIPAM(t *testing.T) { + switch *utils.DBType { + case utils.MetaInternal: + case utils.MetaMySQL, utils.MetaMongo, utils.MetaPostgres: + t.SkipNow() + default: + t.Fatal("unsupported dbtype value") + } + + var testRadiusDict = ` +ATTRIBUTE Framed-Pool 88 string + +VALUE Service-Type Framed 2 +VALUE Framed-Protocol PPP 1 +VALUE Framed-Protocol GPRS-PDP-Context 7 +VALUE NAS-Port-Type Virtual 5 +VALUE Acct-Status-Type Start 1 +VALUE Acct-Status-Type Stop 2 +VALUE Acct-Status-Type Alive 3 +VALUE Acct-Authentic RADIUS 1 +VALUE Acct-Terminate-Cause User-Request 1 +` + + dictDir := t.TempDir() + dictPath := filepath.Join(dictDir, "dictionary.test") + if err := os.WriteFile(dictPath, []byte(testRadiusDict), 0644); err != nil { + t.Fatal(err) + } + + ng := engine.TestEngine{ + ConfigPath: filepath.Join(*utils.DataDir, "conf", "samples", "radius_ipam"), + ConfigJSON: fmt.Sprintf(`{ +"radius_agent": { + "client_dictionaries": { + "*default": [ + %q + ] + } +} +}`, dictDir+"/"), + DBCfg: engine.InternalDBCfg, + Encoding: *utils.Encoding, + // LogBuffer: &bytes.Buffer{}, + } + // t.Cleanup(func() { fmt.Println(ng.LogBuffer) }) + client, cfg := ng.Run(t) + + var replySet string + if err := client.Call(context.Background(), utils.AdminSv1SetIPProfile, + &utils.IPProfileWithAPIOpts{ + IPProfile: &utils.IPProfile{ + Tenant: "cgrates.org", + ID: "IPsAPI", + FilterIDs: []string{"*string:~*req.Account:123456789012345"}, + Weights: utils.DynamicWeights{ + { + Weight: 15, + }, + }, + TTL: -1, + Stored: false, + + // Pool selection logic: + // POOL_A (10.100.0.1): weight 50, blocks (APN=internet.test.apn) + // POOL_B (10.100.0.2): weight 30, gets removed + // POOL_C (10.100.0.3): weight 100 should win + Pools: []*utils.IPPool{ + { + ID: "POOL_A", + FilterIDs: []string{}, + Type: "*ipv4", + Range: "10.100.0.1/32", + Strategy: "*ascending", + Message: "Pool A message", + Weights: utils.DynamicWeights{ + { + FilterIDs: []string{}, + Weight: 50, + }, + }, + Blockers: utils.DynamicBlockers{ + { + FilterIDs: []string{"*string:~*req.APN:internet.test.apn"}, + Blocker: true, + }, + }, + }, + { + ID: "POOL_B", + FilterIDs: []string{}, + Type: "*ipv4", + Range: "10.100.0.2/32", + Strategy: "*ascending", + Message: "Pool B message", + Weights: utils.DynamicWeights{ + { + FilterIDs: []string{}, + Weight: 30, + }, + }, + }, + { + ID: "POOL_C", + FilterIDs: []string{}, + Type: "*ipv4", + Range: "10.100.0.3/32", + Strategy: "*ascending", + Message: "Pool C message", + Weights: utils.DynamicWeights{ + { + FilterIDs: []string{"*string:~*req.APN:internet.test.apn"}, + Weight: 100, + }, + { + FilterIDs: []string{}, + Weight: 10, + }, + }, + }, + }, + }, + }, &replySet); err != nil { + t.Error(err) + } + + imsi := "123456789012345" + msisdn := "987654321098765" + apn := "internet.test.apn" + + nasID := "test-nas-server-1" + nasIP := "192.168.1.10" + poolName := "test-pool-primary" + + passwd := "CGRateSPassword1" + currentTimestamp := fmt.Sprintf("%d", time.Now().Unix()) + + authSessionID := "auth-session-12345-67890" + acctSessionID := "acct-session-abcdef-123456" + + proxyAuth := "4829" + proxyAcctStart := "4830" + proxyAcctAlive := "4831" + proxyAcctStop := "4832" + + // Step 1: Access-Request (should not allocate) + dictRad := radigo.RFC2865Dictionary() + dictRad.ParseFromReader(strings.NewReader(testRadiusDict)) + secret := cfg.RadiusAgentCfg().ClientSecrets[utils.MetaDefault] + net := cfg.RadiusAgentCfg().Listeners[0].Network + authAddr := cfg.RadiusAgentCfg().Listeners[0].AuthAddr + clientAuth, err := radigo.NewClient(net, authAddr, secret, dictRad, 1, nil, utils.Logger) + if err != nil { + t.Fatal(err) + } + + reply := sendRadReq(t, clientAuth, radigo.AccessRequest, 1, + map[string]string{ + "User-Name": imsi, + "Service-Type": "Framed", + "Framed-Protocol": "GPRS-PDP-Context", + "Called-Station-Id": apn, + "Calling-Station-Id": msisdn, + "NAS-Identifier": nasID, + "Acct-Session-Id": authSessionID, + "Framed-Pool": poolName, + "User-Password": passwd, + "Event-Timestamp": currentTimestamp, + "NAS-IP-Address": nasIP, + "Proxy-State": proxyAuth, + }, + ) + checkAllocs(t, client, "IPsAPI") + + // retrieve allocatedIP (to be used in Accounting-Request Start) + var allocatedIP string + for _, avp := range reply.AVPs { + if avp.Number == 8 { // Framed-IP-Address + if len(avp.RawValue) == 4 { + allocatedIP = fmt.Sprintf("%d.%d.%d.%d", + avp.RawValue[0], avp.RawValue[1], + avp.RawValue[2], avp.RawValue[3]) + break + } + } + } + if allocatedIP != "10.100.0.3" { + t.Errorf("expected IP from POOL_C (10.100.0.3), got %s", allocatedIP) + } + + // Step 2: Accounting-Request Start (should allocate) + acctAddr := cfg.RadiusAgentCfg().Listeners[0].AcctAddr + clientAcct, err := radigo.NewClient(net, acctAddr, secret, dictRad, 1, nil, utils.Logger) + if err != nil { + t.Fatal(err) + } + + sendRadReq(t, clientAcct, radigo.AccountingRequest, 2, + map[string]string{ + "User-Name": imsi, + "Acct-Status-Type": "Start", + "NAS-Identifier": "test-nas-server-2", // Different NAS for accounting + "Called-Station-Id": apn, + "Framed-Protocol": "GPRS-PDP-Context", + "Service-Type": "Framed", + "NAS-Port-Type": "Virtual", + "Calling-Station-Id": msisdn, + "Acct-Authentic": "RADIUS", + "Acct-Delay-Time": "0", + "Acct-Session-Id": acctSessionID, + "Framed-IP-Address": allocatedIP, + "NAS-IP-Address": "192.168.1.11", // Different NAS IP for accounting + "Event-Timestamp": currentTimestamp, + "Proxy-State": proxyAcctStart, + }, + ) + checkAllocs(t, client, "IPsAPI", acctSessionID) + + // Step 3: Accounting-Request Alive (should maintain allocation) + time.Sleep(100 * time.Millisecond) + aliveTimestamp := fmt.Sprintf("%d", time.Now().Unix()) + sendRadReq(t, clientAcct, radigo.AccountingRequest, 3, + map[string]string{ + "User-Name": imsi, + "Acct-Status-Type": "Alive", + "Service-Type": "Framed", + "Acct-Session-Id": acctSessionID, + "Framed-Protocol": "GPRS-PDP-Context", + "Called-Station-Id": apn, + "Calling-Station-Id": msisdn, + "NAS-Identifier": "test-nas-server-3", + "Acct-Input-Octets": "1234567", + "Acct-Output-Octets": "7654321", + "NAS-IP-Address": "192.168.1.12", + "Event-Timestamp": aliveTimestamp, + "Proxy-State": proxyAcctAlive, + }, + ) + checkAllocs(t, client, "IPsAPI", acctSessionID) + + // Step 4: Accounting-Request Stop (should release) + time.Sleep(100 * time.Millisecond) + stopTimestamp := fmt.Sprintf("%d", time.Now().Unix()) + sendRadReq(t, clientAcct, radigo.AccountingRequest, 4, + map[string]string{ + "User-Name": imsi, + "Acct-Status-Type": "Stop", + "Service-Type": "Framed", + "Acct-Session-Id": acctSessionID, + "Framed-Protocol": "GPRS-PDP-Context", + "Called-Station-Id": apn, + "Calling-Station-Id": msisdn, + "NAS-Identifier": "test-nas-server-3", + "Acct-Input-Octets": "9876543", + "Acct-Output-Octets": "1234567", + "Acct-Terminate-Cause": "User-Request", + "NAS-IP-Address": "192.168.1.12", + "Event-Timestamp": stopTimestamp, + "Proxy-State": proxyAcctStop, + }, + ) + checkAllocs(t, client, "IPsAPI") +} + +func sendRadReq(t *testing.T, client *radigo.Client, code radigo.PacketCode, id uint8, avps map[string]string) *radigo.Packet { + t.Helper() + req := client.NewRequest(code, id) + + for attr, val := range avps { + if err := req.AddAVPWithName(attr, val, ""); err != nil { + t.Fatal(err) + } + if code == radigo.AccessRequest && attr == "User-Password" { + secret := []byte("CGRateS.org") + for i := len(req.AVPs) - 1; i >= 0; i-- { + if req.AVPs[i].Name == "User-Password" { + req.AVPs[i].RawValue = radigo.EncodeUserPassword([]byte(val), secret, req.Authenticator[:]) + break + } + } + } + } + + replyPacket, err := client.SendRequest(req) + if err != nil { + t.Fatal(err) + } + + var failed bool + switch code { + case radigo.AccessRequest: + failed = replyPacket.Code != radigo.AccessAccept + case radigo.AccountingRequest: + failed = replyPacket.Code != radigo.AccountingResponse + } + if failed { + t.Errorf("unexpected reply received to %s: %+v", code.String(), utils.ToJSON(replyPacket)) + } + + return replyPacket +} + +func checkAllocs(t *testing.T, client *birpc.Client, id string, wantAllocs ...string) { + t.Helper() + var allocs utils.IPAllocations + if err := client.Call(context.Background(), utils.IPsV1GetIPAllocations, + &utils.TenantIDWithAPIOpts{ + TenantID: &utils.TenantID{ + Tenant: "cgrates.org", + ID: id, + }, + }, &allocs); err != nil { + t.Error(err) + } + if len(allocs.Allocations) != len(wantAllocs) { + t.Errorf("%s unexpected result: %s", utils.IPsV1GetIPAllocations, utils.ToJSON(allocs)) + } + + for _, allocID := range wantAllocs { + if _, exists := allocs.Allocations[allocID]; !exists { + t.Errorf("%s unexpected result: %s", utils.IPsV1GetIPAllocations, utils.ToJSON(allocs)) + return + } + } +}