/* 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 */ package engine import ( "reflect" "slices" "testing" "time" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/utils" ) func TestIPProfileClone(t *testing.T) { activation := time.Date(2025, 7, 21, 10, 0, 0, 0, time.UTC) expiry := time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC) ip := &IPProfile{ Tenant: "cgrates.org", ID: "ip_profile_1", FilterIDs: []string{"flt1", "flt2"}, ActivationInterval: &utils.ActivationInterval{ ActivationTime: activation, ExpiryTime: expiry, }, TTL: 10 * time.Minute, Type: "ipv4", AddressPool: "main_pool", Allocation: "dynamic", Stored: true, Weight: 2.5, } cloned := ip.Clone() if cloned == ip { t.Error("Clone returned the same reference as original") } if cloned.Tenant != ip.Tenant { t.Error("Tenant mismatch") } if cloned.ID != ip.ID { t.Error("ID mismatch") } if cloned.Type != ip.Type { t.Error("Type mismatch") } if cloned.AddressPool != ip.AddressPool { t.Error("AddressPool mismatch") } if cloned.Allocation != ip.Allocation { t.Error("Allocation mismatch") } if cloned.Stored != ip.Stored { t.Error("Stored mismatch") } if cloned.Weight != ip.Weight { t.Error("Weight mismatch") } if cloned.TTL != ip.TTL { t.Error("TTL mismatch") } if cloned.ActivationInterval == ip.ActivationInterval { t.Error("ActivationInterval not deeply cloned") } if cloned.ActivationInterval.ActivationTime != ip.ActivationInterval.ActivationTime { t.Error("ActivationTime mismatch") } if cloned.ActivationInterval.ExpiryTime != ip.ActivationInterval.ExpiryTime { t.Error("ExpiryTime mismatch") } if len(cloned.FilterIDs) != len(ip.FilterIDs) { t.Error("FilterIDs length mismatch") } for i := range cloned.FilterIDs { if cloned.FilterIDs[i] != ip.FilterIDs[i] { t.Errorf("FilterIDs[%d] mismatch", i) } } cloned.FilterIDs[0] = "changed" if ip.FilterIDs[0] == "changed" { t.Error("Original FilterIDs changed after modifying clone") } var nilIP *IPProfile clonedNil := nilIP.Clone() if clonedNil != nil { t.Error("Clone of nil IPProfile should return nil") } } func TestIPProfileCacheClone(t *testing.T) { ip := &IPProfile{ Tenant: "cgrates.org", ID: "ip_cache_test", FilterIDs: []string{"fltA"}, ActivationInterval: &utils.ActivationInterval{ ActivationTime: time.Date(2025, 7, 21, 0, 0, 0, 0, time.UTC), ExpiryTime: time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC), }, Type: "ipv6", TTL: 5 * time.Minute, AddressPool: "test_pool", Allocation: "static", Stored: true, Weight: 1.0, } clonedAny := ip.CacheClone() cloned, ok := clonedAny.(*IPProfile) if !ok { t.Error("CacheClone did not return *IPProfile") } if cloned == ip { t.Error("CacheClone returned the same reference instead of a clone") } if cloned.ID != ip.ID || cloned.Tenant != ip.Tenant { t.Error("Cloned fields do not match original") } cloned.FilterIDs[0] = "modified" if ip.FilterIDs[0] == "modified" { t.Error("Original FilterIDs modified") } var nilIP *IPProfile clonedNil := nilIP.CacheClone() if clonedNil != nil { if _, ok := clonedNil.(*IPProfile); !ok { t.Error("CacheClone on nil receiver should return nil") } } } func TestIPProfileLockKey(t *testing.T) { tenant := "cgrates.org" id := "1001" got := ipProfileLockKey(tenant, id) want := utils.CacheIPProfiles + ":" + tenant + ":" + id if got != want { t.Errorf("ipProfileLockKey() = %q; want %q", got, want) } got = ipProfileLockKey("", "") want = utils.CacheIPProfiles + "::" if got != want { t.Errorf("ipProfileLockKey() with empty strings = %q; want %q", got, want) } } func TestIPProfilelock(t *testing.T) { ip := &IPProfile{ Tenant: "cgrates.org", ID: "profile123", } ip.lock("IDlock") if ip.lkID != "IDlock" { t.Errorf("expected lkID 'IDlock', got %q", ip.lkID) } ip.lock(utils.EmptyString) if ip.lkID == "" { t.Error("expected lkID to be set by guardian but got empty string") } } func TestIPProfileUnlock(t *testing.T) { ip := &IPProfile{} ip.lkID = utils.EmptyString ip.unlock() if ip.lkID != utils.EmptyString { t.Errorf("expected lkID to remain empty, got %q", ip.lkID) } ip.lkID = "Id" ip.unlock() if ip.lkID != utils.EmptyString { t.Errorf("expected lkID to be cleared, got %q", ip.lkID) } } func TestIPUsageTenantID(t *testing.T) { u := &IPUsage{ Tenant: "cgrates.org", ID: "usage01", } got := u.TenantID() want := "cgrates.org:usage01" if got != want { t.Errorf("TenantID() = %q; want %q", got, want) } u.Tenant = "" u.ID = "" got = u.TenantID() want = ":" if got != want { t.Errorf("TenantID() with empty = %q; want %q", got, want) } } func TestIPUsageIsActive(t *testing.T) { now := time.Now() u := &IPUsage{ ExpiryTime: now.Add(1 * time.Hour), } if !u.isActive(now) { t.Errorf("Expected active usage, got inactive") } u.ExpiryTime = now.Add(-1 * time.Hour) if u.isActive(now) { t.Errorf("Expected inactive usage, got active") } u.ExpiryTime = time.Time{} if !u.isActive(now) { t.Errorf("Expected active usage for zero expiry time, got inactive") } } func TestIPTotalUsage(t *testing.T) { ip := &IP{ Usages: map[string]*IPUsage{ "u1": {Units: 1.5}, "u2": {Units: 2.0}, "u3": {Units: 0.5}, }, } expected := 4.0 got := ip.TotalUsage() if got != expected { t.Errorf("TotalUsage() = %v, want %v", got, expected) } gotCached := ip.TotalUsage() if gotCached != expected { t.Errorf("TotalUsage() after cache = %v, want %v", gotCached, expected) } } func TestIPUsageClone(t *testing.T) { original := &IPUsage{ Tenant: "cgrates.org", ID: "ID1001", ExpiryTime: time.Now().Add(24 * time.Hour), } cloned := original.Clone() if cloned == nil { t.Fatal("expected clone not to be nil") } if cloned == original { t.Error("expected clone to be a different instance") } if *cloned != *original { t.Errorf("expected clone to have same content, got %+v, want %+v", cloned, original) } var nilUsage *IPUsage nilClone := nilUsage.Clone() if nilClone != nil { t.Error("expected nil clone for nil input") } } func TestIPClone(t *testing.T) { ttl := 5 * time.Minute tUsage := 123.45 dirty := true expTime := time.Now().Add(time.Hour) original := &IP{ Tenant: "cgrates.org", ID: "ip01", TTLIdx: []string{"idx1", "idx2"}, cfg: &IPProfile{ Tenant: "cgrates.org", ID: "profile1", FilterIDs: []string{"f1", "f2"}, TTL: time.Minute * 10, Type: "dynamic", }, Usages: map[string]*IPUsage{ "u1": { Tenant: "cgrates.org", ID: "u1", ExpiryTime: expTime, Units: 50.0, }, }, ttl: &ttl, tUsage: &tUsage, dirty: &dirty, } cloned := original.Clone() t.Run("nil IP returns nil", func(t *testing.T) { var ip *IP = nil cloned := ip.Clone() if cloned != nil { t.Errorf("expected nil, got %+v", cloned) } }) if !reflect.DeepEqual(original, cloned) { t.Errorf("Expected clone to be deeply equal to original, got difference:\noriginal: %+v\nclone: %+v", original, cloned) } if cloned == original { t.Error("Clone should return a different pointer than the original") } if cloned.cfg == original.cfg { t.Error("cfg field was not deeply cloned") } if cloned.Usages["u1"] == original.Usages["u1"] { t.Error("Usages map content was not deeply cloned") } if cloned.ttl == original.ttl { t.Error("ttl pointer was not deeply cloned") } if cloned.tUsage == original.tUsage { t.Error("tUsage pointer was not deeply cloned") } if cloned.dirty == original.dirty { t.Error("dirty pointer was not deeply cloned") } } func TestIpLockKey(t *testing.T) { tnt := "cgrates.org" id := "192.168.0.1" expected := utils.ConcatenatedKey(utils.CacheIPs, tnt, id) got := ipLockKey(tnt, id) if got != expected { t.Errorf("Expected %s, got %s", expected, got) } } func TestIPlock(t *testing.T) { ip := &IP{Tenant: "cgrates.org", ID: "1001"} ip.lock("customLockID") if ip.lkID != "customLockID" { t.Errorf("Expected lkID to be 'customLockID', got %s", ip.lkID) } ip2 := &IP{Tenant: "cgrates.org2", ID: "1002"} ip2.lock(utils.EmptyString) if ip2.lkID == utils.EmptyString { t.Error("Expected lkID to be set by Guardian.GuardIDs, got empty string") } } func TestIPUunlock(t *testing.T) { ip := &IP{lkID: "LockID"} ip.unlock() if ip.lkID != utils.EmptyString { t.Errorf("Expected lkID to be cleared, got %s", ip.lkID) } ip.unlock() } func TestIPRemoveExpiredUnits(t *testing.T) { now := time.Now() expiredID := "expired" activeID := "active" ip := &IP{ ID: "ip-test", Usages: map[string]*IPUsage{}, TTLIdx: []string{expiredID, activeID}, tUsage: utils.Float64Pointer(30.0), } ip.Usages[expiredID] = &IPUsage{ ID: expiredID, ExpiryTime: now.Add(-10 * time.Minute), Units: 10.0, } ip.Usages[activeID] = &IPUsage{ ID: activeID, ExpiryTime: now.Add(10 * time.Minute), Units: 20.0, } ip.removeExpiredUnits() if _, ok := ip.Usages[expiredID]; ok { t.Errorf("Expected expired usage to be removed") } if _, ok := ip.Usages[activeID]; !ok { t.Errorf("Expected active usage to be retained") } if ip.tUsage != nil { t.Errorf("Expected tUsage to be set to nil after recalculation") } if len(ip.TTLIdx) != 1 || ip.TTLIdx[0] != activeID { t.Errorf("Expected TTLIdx to only contain activeID") } } func TestIPRecordUsage(t *testing.T) { t.Run("record new usage with ttl set", func(t *testing.T) { ttl := 10 * time.Minute ip := &IP{ Usages: make(map[string]*IPUsage), TTLIdx: []string{}, ttl: &ttl, tUsage: new(float64), } usage := &IPUsage{Tenant: "cgrates.org", ID: "usage1", Units: 5} err := ip.recordUsage(usage) if err != nil { t.Fatalf("unexpected error: %v", err) } if got, want := len(ip.Usages), 1; got != want { t.Fatalf("unexpected number of usages: got %d, want %d", got, want) } if _, ok := ip.Usages[usage.ID]; !ok { t.Fatal("usage not recorded") } if len(ip.TTLIdx) != 1 || ip.TTLIdx[0] != usage.ID { t.Fatal("TTLIdx not updated properly") } if ip.tUsage == nil || *ip.tUsage != usage.Units { t.Fatalf("tUsage not updated properly, got %v", ip.tUsage) } }) t.Run("duplicate usage id", func(t *testing.T) { ip := &IP{ Usages: map[string]*IPUsage{ "usage1": {Tenant: "cgrates.org", ID: "usage1", Units: 5}, }, } usage := &IPUsage{Tenant: "cgrates.org", ID: "usage1", Units: 10} err := ip.recordUsage(usage) if err == nil { t.Fatal("expected error on duplicate usage id, got nil") } }) t.Run("ttl zero disables expiry setting", func(t *testing.T) { zeroTTL := time.Duration(0) ip := &IP{ Usages: make(map[string]*IPUsage), ttl: &zeroTTL, } usage := &IPUsage{Tenant: "cgrates.org", ID: "noexpiry", Units: 2} err := ip.recordUsage(usage) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(ip.Usages) != 0 { t.Fatal("usage should NOT be recorded when ttl is 0") } }) } func TestIPClearUsage(t *testing.T) { t.Run("usage not found", func(t *testing.T) { ip := &IP{Usages: make(map[string]*IPUsage)} err := ip.clearUsage("missing") if err == nil { t.Fatal("expected error, got nil") } expected := "cannot find usage record with id: missing" if err.Error() != expected { t.Errorf("expected error %q, got %q", expected, err.Error()) } }) t.Run("usage found with zero expiry time", func(t *testing.T) { totalUsage := 10.0 ip := &IP{ Usages: map[string]*IPUsage{ "id1": {Units: 5.0}, }, tUsage: &totalUsage, TTLIdx: []string{"id2"}, } err := ip.clearUsage("id1") if err != nil { t.Fatalf("unexpected error: %v", err) } if _, exists := ip.Usages["id1"]; exists { t.Error("expected id1 to be deleted from Usages") } if *ip.tUsage != 5.0 { t.Errorf("expected tUsage=5.0, got %v", *ip.tUsage) } if !slices.Equal(ip.TTLIdx, []string{"id2"}) { t.Errorf("TTLIdx changed unexpectedly: %v", ip.TTLIdx) } }) t.Run("usage found with non-zero expiry time", func(t *testing.T) { totalUsage := 20.0 expTime := time.Now().Add(time.Hour) ip := &IP{ Usages: map[string]*IPUsage{ "id2": {ExpiryTime: expTime, Units: 7.0}, }, TTLIdx: []string{"id2", "other"}, tUsage: &totalUsage, } err := ip.clearUsage("id2") if err != nil { t.Fatalf("unexpected error: %v", err) } if _, exists := ip.Usages["id2"]; exists { t.Error("expected id2 to be deleted from Usages") } if *ip.tUsage != 13.0 { t.Errorf("expected tUsage=13.0, got %v", *ip.tUsage) } if slices.Contains(ip.TTLIdx, "id2") { t.Errorf("expected id2 to be removed from TTLIdx, got %v", ip.TTLIdx) } }) } func TestIPsSort(t *testing.T) { ip1 := &IP{cfg: &IPProfile{Weight: 1.0}} ip2 := &IP{cfg: &IPProfile{Weight: 3.0}} ip3 := &IP{cfg: &IPProfile{Weight: 2.0}} ips := IPs{ip1, ip2, ip3} ips.Sort() expected := IPs{ip2, ip3, ip1} for i := range ips { if ips[i] != expected[i] { t.Errorf("expected index %d to be %+v, got %+v", i, expected[i], ips[i]) } } } func TestIPsUnlock(t *testing.T) { ip1 := &IP{lkID: "lock1", cfg: &IPProfile{lkID: "cfgLock1"}} ip2 := &IP{lkID: "lock2", cfg: &IPProfile{lkID: "cfgLock2"}} ip3 := &IP{lkID: "lock3", cfg: nil} ips := IPs{ip1, ip2, ip3} ips.unlock() if ip1.lkID != "" { t.Errorf("expected ip1.lkID to be empty, got %q", ip1.lkID) } if ip2.lkID != "" { t.Errorf("expected ip2.lkID to be empty, got %q", ip2.lkID) } if ip3.lkID != "" { t.Errorf("expected ip3.lkID to be empty, got %q", ip3.lkID) } if ip1.cfg.lkID != "" { t.Errorf("expected ip1.cfg.lkID to be empty, got %q", ip1.cfg.lkID) } if ip2.cfg.lkID != "" { t.Errorf("expected ip2.cfg.lkID to be empty, got %q", ip2.cfg.lkID) } } func TestIPsIds(t *testing.T) { ip1 := &IP{ID: "ip1"} ip2 := &IP{ID: "ip2"} ip3 := &IP{ID: "ip3"} ips := IPs{ip1, ip2, ip3} got := ips.ids() if len(got) != 3 { t.Errorf("expected 3 IDs in set, got %d", len(got)) } expectedIDs := []string{"ip1", "ip2", "ip3"} for _, id := range expectedIDs { if _, exists := got[id]; !exists { t.Errorf("expected ID %q in set, but it was missing", id) } } } func TestIPCacheClone(t *testing.T) { ttl := 10 * time.Minute tUsage := 123.45 dirty := true original := &IP{ Tenant: "cgrates.org", ID: "ip01", TTLIdx: []string{"idx1", "idx2"}, Usages: map[string]*IPUsage{ "u1": {Tenant: "cgrates.org", ID: "u1", Units: 50.0}, }, ttl: &ttl, tUsage: &tUsage, dirty: &dirty, cfg: &IPProfile{ Tenant: "cgrates.org", ID: "profile1", Weight: 1.5, }, } cloneAny := original.CacheClone() if cloneAny == nil { t.Fatal("CacheClone returned nil") } clone, ok := cloneAny.(*IP) if !ok { t.Fatalf("expected type *IP, got %T", cloneAny) } if clone == original { t.Error("expected clone to be a different pointer than original") } if !reflect.DeepEqual(clone, original) { t.Errorf("expected clone to be deeply equal to original:\noriginal: %+v\nclone: %+v", original, clone) } } func TestNewIPService(t *testing.T) { dm := &DataManager{} cgrcfg := &config.CGRConfig{} filterS := &FilterS{} connMgr := &ConnManager{} service := NewIPService(dm, cgrcfg, filterS, connMgr) if service == nil { t.Fatal("expected NewIPService to return non-nil") } if service.dm != dm { t.Errorf("expected dm=%+v, got %+v", dm, service.dm) } if service.cfg != cgrcfg { t.Errorf("expected cfg=%+v, got %+v", cgrcfg, service.cfg) } if service.fs != filterS { t.Errorf("expected fs=%+v, got %+v", filterS, service.fs) } if service.cm != connMgr { t.Errorf("expected cm=%+v, got %+v", connMgr, service.cm) } if service.storedIPs == nil { t.Error("expected storedIPs map to be initialized") } if service.loopStopped == nil { t.Error("expected loopStopped channel to be initialized") } if service.stopBackup == nil { t.Error("expected stopBackup channel to be initialized") } } func TestIPServiceStoreMatchedIPs(t *testing.T) { cfg := config.NewDefaultCGRConfig() data, err := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items) if err != nil { t.Error(err) } dm := NewDataManager(data, config.CgrConfig().CacheCfg(), nil) t.Run("StoreInterval=0", func(t *testing.T) { cfg.IPsCfg().StoreInterval = 0 svc := &IPService{ cfg: cfg, dm: dm, storedIPs: make(utils.StringSet), } dirty := false ip := &IP{Tenant: "cgrates.org", ID: "ip01", dirty: &dirty} err := svc.storeMatchedIPs(IPs{ip}) if err != nil { t.Fatalf("unexpected error: %v", err) } if dirty { t.Errorf("expected dirty=false, got true") } if len(svc.storedIPs) != 0 { t.Errorf("expected storedIPs empty, got %+v", svc.storedIPs) } }) t.Run("StoreInterval>0 marks dirty and schedules for backup", func(t *testing.T) { cfg.IPsCfg().StoreInterval = 10 svc := &IPService{ cfg: cfg, dm: dm, storedIPs: make(utils.StringSet), } dirty := false ip := &IP{Tenant: "cgrates.org", ID: "ip02", dirty: &dirty} err := svc.storeMatchedIPs(IPs{ip}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !dirty { t.Errorf("expected dirty=true, got false") } if _, exists := svc.storedIPs[ip.TenantID()]; !exists { t.Errorf("expected ip02 in storedIPs, got %+v", svc.storedIPs) } }) t.Run("StoreInterval<0 stores immediately", func(t *testing.T) { cfg.IPsCfg().StoreInterval = -1 svc := &IPService{ cfg: cfg, dm: dm, storedIPs: make(utils.StringSet), } dirty := true ip := &IP{Tenant: "cgrates.org", ID: "ip03", dirty: &dirty} Cache.Set(utils.CacheIPs, ip.TenantID(), ip, nil, true, utils.NonTransactional) err := svc.storeMatchedIPs(IPs{ip}) if err != nil { t.Fatalf("unexpected error: %v", err) } if dirty { t.Errorf("expected dirty=false after storeIP, got true") } }) } func TestIPServiceMatchingIPsForEvent(t *testing.T) { cfg := config.NewDefaultCGRConfig() data, err := NewInternalDB(nil, nil, true, nil, cfg.DataDbCfg().Items) if err != nil { t.Error(err) } dm := NewDataManager(data, cfg.CacheCfg(), nil) fs := NewFilterS(cfg, nil, dm) svc := &IPService{ cfg: cfg, dm: dm, fs: fs, } profile := &IPProfile{ Tenant: "cgrates.org", ID: "ip01", Stored: true, TTL: 5 * time.Minute, } if err := dm.SetIPProfile(profile, true); err != nil { t.Fatalf("failed to set IPProfile: %v", err) } ip := &IP{ Tenant: "cgrates.org", ID: "ip01", } Cache.Set(utils.CacheIPs, ip.TenantID(), ip, nil, true, utils.NonTransactional) if err := dm.SetIP(ip); err != nil { t.Fatalf("failed to set IP: %v", err) } ev := &utils.CGREvent{ Event: map[string]interface{}{ "type": "testEvent", }, Time: func() *time.Time { t := time.Now(); return &t }(), } evUUID := "event-uuid-001" ips, err := svc.matchingIPsForEvent("cgrates.org", ev, evUUID, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(ips) != 1 { t.Errorf("expected 1 IP, got %d", len(ips)) } if ips[0].ID != "ip01" { t.Errorf("expected IP ID 'ip01', got '%s'", ips[0].ID) } if ips[0].ttl == nil || *ips[0].ttl != profile.TTL { t.Errorf("expected IP TTL %v, got %v", profile.TTL, ips[0].ttl) } cached, ok := Cache.Get(utils.CacheEventIPs, evUUID) if !ok || cached == nil { t.Errorf("expected cached IPIDs for event UUID") } }