ips: improve integration tests

This commit is contained in:
ionutboangiu
2025-08-07 18:09:07 +03:00
committed by Dan Christian Bogos
parent 707826359b
commit d76759ea5b

View File

@@ -21,22 +21,100 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
package ips
import (
"bytes"
"fmt"
"net/netip"
"reflect"
"testing"
"time"
"github.com/cgrates/birpc"
"github.com/cgrates/birpc/context"
"github.com/cgrates/cgrates/config"
"github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/utils"
)
// NOTE: this test is incomplete. Currently used only for the API samples.
// Expected IP profiles for testing
var (
// Profile created via SetIPProfile API
expectedIPsAPIProfile = &utils.IPProfile{
Tenant: "cgrates.org",
ID: "IPsAPI",
FilterIDs: []string{"*string:~*req.Account:1001"},
Weights: utils.DynamicWeights{{Weight: 15}},
TTL: -1,
Stored: false,
Pools: []*utils.IPPool{
{
ID: "API_POOL",
FilterIDs: []string{},
Type: "*ipv4",
Range: "10.100.0.1/32",
Strategy: "*ascending",
Message: "API created pool",
Weights: utils.DynamicWeights{{Weight: 15}},
Blockers: utils.DynamicBlockers{{Blocker: false}},
},
},
}
// Profile from CSV test data (IPs1)
expectedIPs1Profile = &utils.IPProfile{
Tenant: "cgrates.org",
ID: "IPs1",
FilterIDs: []string{"*string:~*req.Account:1001"},
Weights: utils.DynamicWeights{{Weight: 10}},
TTL: time.Second,
Stored: true,
Pools: []*utils.IPPool{
{
ID: "POOL1",
FilterIDs: []string{"*string:~*req.Destination:2001"},
Type: "*ipv4",
Range: "172.16.1.1/32",
Strategy: "*ascending",
Message: "alloc_success",
Weights: utils.DynamicWeights{{Weight: 15}, {FilterIDs: []string{"*exists:~*req.NeedMoreWeight:"}, Weight: 50}},
Blockers: utils.DynamicBlockers{{FilterIDs: []string{"*exists:~*req.ShouldBlock:"}, Blocker: true}},
},
{
ID: "POOL2",
FilterIDs: []string{"*string:~*req.Destination:2002"},
Type: "*ipv4",
Range: "192.168.122.1/32",
Strategy: "*random",
Message: "alloc_new",
Weights: utils.DynamicWeights{{Weight: 25}},
Blockers: utils.DynamicBlockers{{Blocker: true}},
},
},
}
// Profile from CSV test data (IPs2)
expectedIPs2Profile = &utils.IPProfile{
Tenant: "cgrates.org",
ID: "IPs2",
FilterIDs: []string{"*string:~*req.Account:1002"},
Weights: utils.DynamicWeights{{Weight: 20}},
TTL: 2 * time.Second,
Stored: false,
Pools: []*utils.IPPool{
{
ID: "POOL1",
FilterIDs: []string{"*string:~*req.Destination:3001"},
Type: "*ipv4",
Range: "127.0.0.1/32",
Strategy: "*descending",
Message: "alloc_msg",
Weights: utils.DynamicWeights{{Weight: 35}},
Blockers: utils.DynamicBlockers{{Blocker: true}},
},
},
}
)
// TODO: move anything sessions related to sessions once ips implementation
// is complete.
func TestIPsIT(t *testing.T) {
t.Skip("ips test currently incomplete, skipping...")
var dbCfg engine.DBCfg
switch *utils.DBType {
case utils.MetaInternal:
@@ -117,106 +195,126 @@ cgrates.org,IPs1,,,,,POOL1,,,,,,*exists:~*req.NeedMoreWeight:;50,*exists:~*req.S
cgrates.org,IPs1,,,,,POOL2,*string:~*req.Destination:2002,*ipv4,192.168.122.1/32,*random,alloc_new,;25,;true
cgrates.org,IPs2,*string:~*req.Account:1002,;20,2s,false,POOL1,*string:~*req.Destination:3001,*ipv4,127.0.0.1/32,*descending,alloc_msg,;35,;true`,
},
DBCfg: dbCfg,
Encoding: *utils.Encoding,
LogBuffer: new(bytes.Buffer),
GracefulShutdown: true,
DBCfg: dbCfg,
Encoding: *utils.Encoding,
// LogBuffer: new(bytes.Buffer),
// GracefulShutdown: true,
}
t.Cleanup(func() { fmt.Println(ng.LogBuffer) })
// t.Cleanup(func() { fmt.Println(ng.LogBuffer) })
client, _ := ng.Run(t)
t.Run("admins apis", func(t *testing.T) {
var reply string
if err := client.Call(context.Background(), utils.AdminSv1SetIPProfile,
err := client.Call(context.Background(), utils.AdminSv1SetIPProfile,
&utils.IPProfileWithAPIOpts{
IPProfile: &utils.IPProfile{
Tenant: "cgrates.org",
ID: "IPsAPI",
FilterIDs: []string{"*string:~*req.Account:1001"},
Weights: utils.DynamicWeights{
{
Weight: 15,
},
},
TTL: -1,
Stored: false,
Weights: utils.DynamicWeights{{Weight: 15}},
TTL: -1,
Stored: false,
Pools: []*utils.IPPool{
{
ID: "FIRST_POOL",
ID: "API_POOL",
FilterIDs: []string{},
Type: "*ipv4",
Range: "192.168.122.1/32",
Range: "10.100.0.1/32",
Strategy: "*ascending",
Message: "Some message",
Weights: utils.DynamicWeights{
{
Weight: 15,
},
},
Blockers: utils.DynamicBlockers{
{
Blocker: false,
},
},
Message: "API created pool",
Weights: utils.DynamicWeights{{Weight: 15}},
Blockers: utils.DynamicBlockers{{Blocker: false}},
},
},
},
}, &reply); err != nil {
t.Error(err)
}, &reply)
if err != nil {
t.Fatal(err)
}
var ipp utils.IPProfile
if err := client.Call(context.Background(), utils.AdminSv1GetIPProfile,
utils.TenantID{
Tenant: "cgrates.org",
ID: "IPsAPI",
}, &ipp); err != nil {
t.Error(err)
}
var ipps []*utils.IPProfile
if err := client.Call(context.Background(), utils.AdminSv1GetIPProfiles,
&utils.ArgsItemIDs{
Tenant: "cgrates.org",
ItemsPrefix: "IPs",
}, &ipps); err != nil {
t.Error(err)
}
if err := client.Call(context.Background(), utils.AdminSv1RemoveIPProfile,
var profile utils.IPProfile
err = client.Call(context.Background(), utils.AdminSv1GetIPProfile,
&utils.TenantIDWithAPIOpts{
TenantID: &utils.TenantID{
Tenant: "cgrates.org",
ID: "IPsAPI",
},
}, &reply); err != nil {
t.Error(err)
}, &profile)
if err != nil {
t.Fatal(err)
}
verifyIPProfile(t, &profile, expectedIPsAPIProfile)
var ipps []*utils.IPProfile
err = client.Call(context.Background(), utils.AdminSv1GetIPProfiles,
&utils.ArgsItemIDs{
Tenant: "cgrates.org",
ItemsPrefix: "IPs",
}, &ipps)
if err != nil {
t.Fatal(err)
}
if len(ipps) != 3 { // IPs1, IPs2 from CSV + IPsAPI from SetIPProfile
t.Fatalf("want exactly 3 profiles, got %d", len(ipps))
}
var no int
if err := client.Call(context.Background(), utils.AdminSv1GetIPProfilesCount,
// Verify each expected profile exists and matches exactly
expectedProfiles := []*utils.IPProfile{
expectedIPs1Profile,
expectedIPs2Profile,
expectedIPsAPIProfile,
}
for _, expected := range expectedProfiles {
found := findProfileByID(ipps, expected.ID)
if found == nil {
t.Fatalf("profile %q not found in GetIPProfiles results", expected.ID)
}
verifyIPProfile(t, found, expected)
}
var count int
err = client.Call(context.Background(), utils.AdminSv1GetIPProfilesCount,
&utils.TenantWithAPIOpts{
Tenant: "cgrates.org",
}, &no); err != nil {
t.Error(err)
}, &count)
if err != nil {
t.Fatal(err)
}
if count != 3 {
t.Fatalf("want exactly 3 profiles in count, got %d", count)
}
})
t.Run("ips apis", func(t *testing.T) {
var ip utils.IPAllocations
if err := client.Call(context.Background(), utils.IPsV1GetIPAllocations,
err = client.Call(context.Background(), utils.AdminSv1RemoveIPProfile,
&utils.TenantIDWithAPIOpts{
TenantID: &utils.TenantID{
Tenant: "cgrates.org",
ID: "IPs1",
ID: "IPsAPI",
},
}, &ip); err != nil {
t.Error(err)
}, &reply)
if err != nil {
t.Fatal(err)
}
allocID := "api_allocation"
var evIP utils.IPAllocations
if err := client.Call(context.Background(), utils.IPsV1GetIPAllocationForEvent,
// Verify profile is gone
err = client.Call(context.Background(), utils.AdminSv1GetIPProfile,
&utils.TenantIDWithAPIOpts{
TenantID: &utils.TenantID{
Tenant: "cgrates.org",
ID: "IPsAPI",
},
}, &utils.IPProfile{})
verifySpecificError(t, err, utils.ErrNotFound.Error())
})
t.Run("ips core functionality", func(t *testing.T) {
// Account "1001" + Destination "2001" should match IPs1/POOL1 -> 172.16.1.1
allocID := "test_allocation"
// no allocs yet
verifyAllocations(t, client, "IPs1")
var eventAllocs utils.IPAllocations
err := client.Call(context.Background(), utils.IPsV1GetIPAllocationForEvent,
&utils.CGREvent{
Tenant: "cgrates.org",
ID: "GetIPsForEvent1",
@@ -227,12 +325,22 @@ cgrates.org,IPs2,*string:~*req.Account:1002,;20,2s,false,POOL1,*string:~*req.Des
APIOpts: map[string]any{
utils.OptsIPsAllocationID: allocID,
},
}, &evIP); err != nil {
t.Error(err)
}, &eventAllocs)
if err != nil {
t.Fatal(err)
}
if eventAllocs.ID != "IPs1" {
t.Fatalf("want profile IPs1, got %s", eventAllocs.ID)
}
var allocIP utils.AllocatedIP
if err := client.Call(context.Background(), utils.IPsV1AuthorizeIP,
expectedAuthorizedIP := &utils.AllocatedIP{
ProfileID: "IPs1",
PoolID: "POOL1",
Message: "alloc_success",
Address: netip.MustParseAddr("172.16.1.1"),
}
var authorizedIP utils.AllocatedIP
err = client.Call(context.Background(), utils.IPsV1AuthorizeIP,
&utils.CGREvent{
Tenant: "cgrates.org",
ID: "AuthorizeIP1",
@@ -243,11 +351,23 @@ cgrates.org,IPs2,*string:~*req.Account:1002,;20,2s,false,POOL1,*string:~*req.Des
APIOpts: map[string]any{
utils.OptsIPsAllocationID: allocID,
},
}, &allocIP); err != nil {
t.Error(err)
}, &authorizedIP)
if err != nil {
t.Fatal(err)
}
verifyAllocatedIP(t, &authorizedIP, expectedAuthorizedIP)
if err := client.Call(context.Background(), utils.IPsV1AllocateIP,
// verify allocation doesn't exist yet (authorize is dry run).
verifyAllocations(t, client, "IPs1")
expectedAllocatedIP := &utils.AllocatedIP{
ProfileID: "IPs1",
PoolID: "POOL1",
Message: "alloc_success",
Address: netip.MustParseAddr("172.16.1.1"),
}
var allocatedIP utils.AllocatedIP
err = client.Call(context.Background(), utils.IPsV1AllocateIP,
&utils.CGREvent{
Tenant: "cgrates.org",
ID: "AllocateIP1",
@@ -258,12 +378,34 @@ cgrates.org,IPs2,*string:~*req.Account:1002,;20,2s,false,POOL1,*string:~*req.Des
APIOpts: map[string]any{
utils.OptsIPsAllocationID: allocID,
},
}, &allocIP); err != nil {
t.Error(err)
}, &allocatedIP)
if err != nil {
t.Fatal(err)
}
verifyAllocatedIP(t, &allocatedIP, expectedAllocatedIP)
// verify allocation exists now
verifyAllocations(t, client, "IPs1", allocID)
verifyIPAllocation(t, client, "IPs1", allocID, expectedAuthorizedIP.Address.String())
// double allocation should fail
err = client.Call(context.Background(), utils.IPsV1AllocateIP,
&utils.CGREvent{
Tenant: "cgrates.org",
ID: "AllocateIP2",
Event: map[string]any{
utils.AccountField: "1001",
utils.Destination: "2001",
},
APIOpts: map[string]any{
utils.OptsIPsAllocationID: "different_alloc",
},
}, &utils.AllocatedIP{})
verifySpecificError(t, err,
`allocation failed for pool "POOL1", IP "172.16.1.1": IP_ALREADY_ALLOCATED (allocated to "test_allocation")`)
var reply string
if err := client.Call(context.Background(), utils.IPsV1ReleaseIP,
err = client.Call(context.Background(), utils.IPsV1ReleaseIP,
&utils.CGREvent{
Tenant: "cgrates.org",
ID: "ReleaseIP1",
@@ -274,59 +416,211 @@ cgrates.org,IPs2,*string:~*req.Account:1002,;20,2s,false,POOL1,*string:~*req.Des
APIOpts: map[string]any{
utils.OptsIPsAllocationID: allocID,
},
}, &reply); err != nil {
t.Error(err)
}, &reply)
if err != nil {
t.Fatal(err)
}
if err := client.Call(context.Background(), utils.IPsV1ClearIPAllocations,
// allocation should be gone.
verifyAllocations(t, client, "IPs1")
// test ClearAllocations API
alloc1, alloc2 := "test_alloc1", "test_alloc2"
// allocate first IP (should work after release)
err = client.Call(context.Background(), utils.IPsV1AllocateIP,
&utils.CGREvent{
Tenant: "cgrates.org",
ID: "AllocateForClear1",
Event: map[string]any{
utils.AccountField: "1001",
utils.Destination: "2001",
},
APIOpts: map[string]any{
utils.OptsIPsAllocationID: alloc1,
},
}, &utils.AllocatedIP{})
if err != nil {
t.Fatal(err)
}
verifyAllocations(t, client, "IPs1", alloc1)
// try second allocation on different pool (should get POOL2)
err = client.Call(context.Background(), utils.IPsV1AllocateIP,
&utils.CGREvent{
Tenant: "cgrates.org",
ID: "AllocateForClear2",
Event: map[string]any{
utils.AccountField: "1001",
utils.Destination: "2002", // different destination for POOL2
},
APIOpts: map[string]any{
utils.OptsIPsAllocationID: alloc2,
},
}, &utils.AllocatedIP{})
if err != nil {
t.Fatal(err)
}
verifyAllocations(t, client, "IPs1", alloc1, alloc2)
// clear only second allocation
err = client.Call(context.Background(), utils.IPsV1ClearIPAllocations,
&utils.ClearIPAllocationsArgs{
Tenant: "cgrates.org",
ID: "IPs1",
AllocationIDs: []string{alloc2},
}, &reply)
if err != nil {
t.Fatal(err)
}
verifyAllocations(t, client, "IPs1", alloc1)
// clear the rest (by not specifying AllocationIDs)
err = client.Call(context.Background(), utils.IPsV1ClearIPAllocations,
&utils.ClearIPAllocationsArgs{
Tenant: "cgrates.org",
ID: "IPs1",
// AllocationIDs: []string{allocID},
}, &reply); err != nil {
t.Error(err)
}, &reply)
if err != nil {
t.Fatal(err)
}
verifyAllocations(t, client, "IPs1")
})
t.Run("sessions ips apis", func(t *testing.T) {
t.Run("sessions integration", func(t *testing.T) {
// NOTE: reply is of type any to avoid having to import sessions just for
// this test in order to prevent future cyclic imports. Any sessions
// related test should be moved to sessions when ips implementation is
// complete.
var reply any
if err := client.Call(context.Background(), utils.SessionSv1AuthorizeEvent,
var authReply any
err := client.Call(context.Background(), utils.SessionSv1AuthorizeEvent,
&utils.CGREvent{
Tenant: "cgrates.org",
APIOpts: map[string]any{
utils.MetaIPs: true,
utils.MetaOriginID: "session1",
utils.MetaOriginID: "session_auth_test",
},
Event: map[string]any{
utils.AccountField: "1001",
utils.Destination: "2001",
utils.SetupTime: "2018-01-07T17:00:00Z",
},
}, &reply); err != nil {
t.Error(err)
}, &authReply)
if err != nil {
t.Fatal(err)
}
if err := client.Call(context.Background(), utils.SessionSv1InitiateSession,
if authReply == nil {
t.Fatal("SessionSv1AuthorizeEvent returned nil reply")
}
var initReply any
err = client.Call(context.Background(), utils.SessionSv1InitiateSession,
&utils.CGREvent{
Tenant: "cgrates.org",
APIOpts: map[string]any{
utils.MetaIPs: true,
utils.MetaOriginID: "session1",
utils.MetaOriginID: "session_init_test",
},
Event: map[string]any{
utils.AccountField: "1001",
utils.Destination: "2001",
utils.SetupTime: "2018-01-07T17:00:00Z",
},
}, &reply); err != nil {
t.Error(err)
}, &initReply)
if err != nil {
t.Fatal(err)
}
if initReply == nil {
t.Fatal("SessionSv1InitiateSession returned nil reply")
}
})
}
// Helper functions for testing
func getIPAllocations(t *testing.T, client *birpc.Client, profileID string) *utils.IPAllocations {
t.Helper()
var allocs utils.IPAllocations
if err := client.Call(context.Background(), utils.IPsV1GetIPAllocations,
&utils.TenantIDWithAPIOpts{
TenantID: &utils.TenantID{
Tenant: "cgrates.org",
ID: profileID,
},
}, &allocs); err != nil {
t.Fatal(err)
}
return &allocs
}
func verifyAllocatedIP(t *testing.T, got, expected *utils.AllocatedIP) {
t.Helper()
if !reflect.DeepEqual(got, expected) {
t.Fatalf("AllocatedIP mismatch:\nExpected: %s\nGot: %s",
utils.ToIJSON(expected), utils.ToIJSON(got))
}
}
func verifySpecificError(t *testing.T, got error, expected string) {
t.Helper()
if got == nil {
t.Fatalf("want error %v, got nil", expected)
}
if got.Error() != expected {
t.Fatalf("want error %v, got %v", expected, got)
}
}
func verifyAllocations(t *testing.T, client *birpc.Client, profileID string, wantAllocs ...string) {
t.Helper()
allocs := getIPAllocations(t, client, profileID)
if len(allocs.Allocations) != len(wantAllocs) {
t.Fatalf("Expected %d allocations, got %d: %s",
len(wantAllocs), len(allocs.Allocations), utils.ToJSON(allocs))
}
for _, allocID := range wantAllocs {
if _, exists := allocs.Allocations[allocID]; !exists {
t.Fatalf("Allocation %s not found in profile %s: %s",
allocID, profileID, utils.ToJSON(allocs))
}
}
}
func verifyIPAllocation(t *testing.T, client *birpc.Client, profileID, allocID, expectedIP string) {
t.Helper()
allocs := getIPAllocations(t, client, profileID)
alloc, exists := allocs.Allocations[allocID]
if !exists {
t.Fatalf("Allocation %s not found in profile %s", allocID, profileID)
}
if alloc.Address.String() != expectedIP {
t.Fatalf("Expected IP %s, got %s for allocation %s",
expectedIP, alloc.Address.String(), allocID)
}
}
func verifyIPProfile(t *testing.T, got, expected *utils.IPProfile) {
t.Helper()
if !reflect.DeepEqual(got, expected) {
t.Fatalf("Profile mismatch:\nwant: %s\ngot: %s",
utils.ToIJSON(expected), utils.ToIJSON(got))
}
}
func findProfileByID(profiles []*utils.IPProfile, id string) *utils.IPProfile {
for _, p := range profiles {
if p.ID == id {
return p
}
}
return nil
}
func BenchmarkIPsAuthorize(b *testing.B) {
cfg := config.NewDefaultCGRConfig()
dataDB, _ := engine.NewInternalDB(nil, nil, nil, cfg.DataDbCfg().Items)