From c6845687fa42e4fc5b21ef4a419827bf3c97fb88 Mon Sep 17 00:00:00 2001 From: porosnicuadrian Date: Wed, 28 Jul 2021 17:17:19 +0300 Subject: [PATCH] Fixed MIssingStructField panic + index health test --- engine/z_libindex_health_test.go | 447 ++++++++++++++++++++++++++++++- utils/struct.go | 39 ++- utils/struct_test.go | 31 +++ 3 files changed, 497 insertions(+), 20 deletions(-) diff --git a/engine/z_libindex_health_test.go b/engine/z_libindex_health_test.go index e36bb9e25..d5d6c3a2a 100644 --- a/engine/z_libindex_health_test.go +++ b/engine/z_libindex_health_test.go @@ -22,6 +22,7 @@ import ( "reflect" "sort" "testing" + "time" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/utils" @@ -507,12 +508,12 @@ func TestHealthIndexCharger(t *testing.T) { // we will set this charger but without indexing chPrf := &ChargerProfile{ - Tenant: "cgrates.org", - ID: "Raw", - FilterIDs: []string{ + Tenant: "cgrates.org", + ID: "Raw", + FilterIDs: []string{ "*string:~*opts.*eventType:ChargerAccountUpdate", "*string:~*req.*Account:1234", - "*string:~*asm.ID:1002", // *asm will not be indexing + "*string:~*asm.ID:1002", // *asm will not be indexing "*suffix:BrokenFilter:Invalid"}, RunID: "raw", AttributeIDs: []string{"*constant:*req.RequestType:*none"}, @@ -526,7 +527,7 @@ func TestHealthIndexCharger(t *testing.T) { exp := &FilterIHReply{ MissingIndexes: map[string][]string{ "cgrates.org:*string:*opts.*eventType:ChargerAccountUpdate": {"Raw"}, - "cgrates.org:*string:*req.*Account:1234": {"Raw"}, + "cgrates.org:*string:*req.*Account:1234": {"Raw"}, }, BrokenIndexes: map[string][]string{}, MissingFilters: map[string][]string{}, @@ -559,7 +560,7 @@ func TestHealthIndexCharger(t *testing.T) { MissingObjects: []string{"cgrates.org:InexistingCharger"}, MissingIndexes: map[string][]string{ "cgrates.org:*string:*opts.*eventType:ChargerAccountUpdate": {"Raw"}, - "cgrates.org:*string:*req.*Account:1234": {"Raw"}, + "cgrates.org:*string:*req.*Account:1234": {"Raw"}, }, BrokenIndexes: map[string][]string{ "cgrates.org:*prefix:req.Destination:+10": {"Raw"}, @@ -578,14 +579,14 @@ func TestHealthIndexCharger(t *testing.T) { //we will use an inexisting Filter(not inline) for the same ChargerProfile chPrf = &ChargerProfile{ - Tenant: "cgrates.org", - ID: "Raw", - FilterIDs: []string{ + Tenant: "cgrates.org", + ID: "Raw", + FilterIDs: []string{ "*string:~*opts.*eventType:ChargerAccountUpdate", "*string:~*req.*Account:1234", - "*string:~*asm.ID:1002", // *asm will not be indexing + "*string:~*asm.ID:1002", // *asm will not be indexing "*suffix:BrokenFilter:Invalid", - "FLTR_1_DOES_NOT_EXIST_CHRGR"}, + "FLTR_1_DOES_NOT_EXIST_CHRGR"}, RunID: "raw", AttributeIDs: []string{"*constant:*req.RequestType:*none"}, Weight: 20, @@ -597,7 +598,7 @@ func TestHealthIndexCharger(t *testing.T) { MissingObjects: []string{"cgrates.org:InexistingCharger"}, MissingIndexes: map[string][]string{ "cgrates.org:*string:*opts.*eventType:ChargerAccountUpdate": {"Raw"}, - "cgrates.org:*string:*req.*Account:1234": {"Raw"}, + "cgrates.org:*string:*req.*Account:1234": {"Raw"}, }, BrokenIndexes: map[string][]string{ "cgrates.org:*prefix:req.Destination:+10": {"Raw"}, @@ -616,3 +617,425 @@ func TestHealthIndexCharger(t *testing.T) { t.Errorf("Expected %+v, received %+v", utils.ToJSON(exp), utils.ToJSON(rply)) } } + +func TestHealthIndexResources(t *testing.T) { + Cache.Clear(nil) + cfg := config.NewDefaultCGRConfig() + db := NewInternalDB(nil, nil, true) + dm := NewDataManager(db, cfg.CacheCfg(), nil) + + // we will set this resource but without indexing + rsPrf := &ResourceProfile{ + Tenant: "tenant.custom", + ID: "RES_GRP1", + FilterIDs: []string{ + "*string:~*opts.*eventType:ResourceAccountUpdate", + "*string:~*req.RequestType:*rated", + "*prefix:~*accounts.RES_GRP1.Available:10", // *accounts will not be indexing + "*suffix:BrokenFilter:Invalid", + }, + UsageTTL: 10 * time.Microsecond, + Limit: 10, + AllocationMessage: "MessageAllocation", + Blocker: true, + Stored: true, + Weight: 20, + } + if err := dm.SetResourceProfile(rsPrf, false); err != nil { + t.Error(err) + } + + args := &IndexHealthArgsWith3Ch{} + exp := &FilterIHReply{ + MissingIndexes: map[string][]string{ + "tenant.custom:*string:*opts.*eventType:ResourceAccountUpdate": {"RES_GRP1"}, + "tenant.custom:*string:*req.RequestType:*rated": {"RES_GRP1"}, + }, + BrokenIndexes: map[string][]string{}, + MissingFilters: map[string][]string{}, + } + if rply, err := GetFltrIdxHealth(dm, + ltcache.NewCache(args.FilterCacheLimit, args.FilterCacheTTL, args.FilterCacheStaticTTL, nil), + ltcache.NewCache(args.IndexCacheLimit, args.IndexCacheTTL, args.IndexCacheStaticTTL, nil), + ltcache.NewCache(args.ObjectCacheLimit, args.ObjectCacheTTL, args.ObjectCacheStaticTTL, nil), + utils.CacheResourceFilterIndexes); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(exp, rply) { + t.Errorf("Expected %+v, received %+v", utils.ToJSON(exp), utils.ToJSON(rply)) + } + + indexes := map[string]utils.StringSet{ + "*suffix:*req.Destination:+10": { // obj exist but the index don't + "RES_GRP1": {}, + }, + "*string:*req.CGRID:not_an_id": { // index is valid but the obj does not exist + "InexistingResource": {}, + }, + } + + // we will set manually some indexes that points to an nil object or index is valid but the obj is missing + if err := dm.SetIndexes(utils.CacheResourceFilterIndexes, "tenant.custom", + indexes, true, utils.NonTransactional); err != nil { + t.Error(err) + } + exp = &FilterIHReply{ + MissingObjects: []string{"tenant.custom:InexistingResource"}, + MissingIndexes: map[string][]string{ + "tenant.custom:*string:*opts.*eventType:ResourceAccountUpdate": {"RES_GRP1"}, + "tenant.custom:*string:*req.RequestType:*rated": {"RES_GRP1"}, + }, + BrokenIndexes: map[string][]string{ + "tenant.custom:*suffix:*req.Destination:+10": {"RES_GRP1"}, + }, + MissingFilters: map[string][]string{}, + } + if rply, err := GetFltrIdxHealth(dm, + ltcache.NewCache(args.FilterCacheLimit, args.FilterCacheTTL, args.FilterCacheStaticTTL, nil), + ltcache.NewCache(args.IndexCacheLimit, args.IndexCacheTTL, args.IndexCacheStaticTTL, nil), + ltcache.NewCache(args.ObjectCacheLimit, args.ObjectCacheTTL, args.ObjectCacheStaticTTL, nil), + utils.CacheResourceFilterIndexes); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(exp, rply) { + t.Errorf("Expected %+v, received %+v", utils.ToJSON(exp), utils.ToJSON(rply)) + } + + //we will use an inexisting Filter(not inline) for the same ResourceProfile + rsPrf = &ResourceProfile{ + Tenant: "tenant.custom", + ID: "RES_GRP1", + FilterIDs: []string{ + "*string:~*opts.*eventType:ResourceAccountUpdate", + "*string:~*req.RequestType:*rated", + "*prefix:~*accounts.RES_GRP1.Available:10", // *asm will not be indexing + "*suffix:BrokenFilter:Invalid", + "FLTR_1_NOT_EXIST", + }, + UsageTTL: 10 * time.Microsecond, + Limit: 10, + AllocationMessage: "MessageAllocation", + Blocker: true, + Stored: true, + Weight: 20, + } + if err := dm.SetResourceProfile(rsPrf, false); err != nil { + t.Error(err) + } + exp = &FilterIHReply{ + MissingObjects: []string{"tenant.custom:InexistingResource"}, + MissingIndexes: map[string][]string{ + "tenant.custom:*string:*opts.*eventType:ResourceAccountUpdate": {"RES_GRP1"}, + "tenant.custom:*string:*req.RequestType:*rated": {"RES_GRP1"}, + }, + BrokenIndexes: map[string][]string{ + "tenant.custom:*suffix:*req.Destination:+10": {"RES_GRP1"}, + }, + MissingFilters: map[string][]string{ + "tenant.custom:FLTR_1_NOT_EXIST": {"RES_GRP1"}, + }, + } + if rply, err := GetFltrIdxHealth(dm, + ltcache.NewCache(args.FilterCacheLimit, args.FilterCacheTTL, args.FilterCacheStaticTTL, nil), + ltcache.NewCache(args.IndexCacheLimit, args.IndexCacheTTL, args.IndexCacheStaticTTL, nil), + ltcache.NewCache(args.ObjectCacheLimit, args.ObjectCacheTTL, args.ObjectCacheStaticTTL, nil), + utils.CacheResourceFilterIndexes); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(exp, rply) { + t.Errorf("Expected %+v, received %+v", utils.ToJSON(exp), utils.ToJSON(rply)) + } +} + +func TestHealthIndexStats(t *testing.T) { + Cache.Clear(nil) + cfg := config.NewDefaultCGRConfig() + db := NewInternalDB(nil, nil, true) + dm := NewDataManager(db, cfg.CacheCfg(), nil) + + // we will set this statQueue but without indexing + sqPrf := &StatQueueProfile{ + Tenant: "cgrates.org", + ID: "Stat_1", + FilterIDs: []string{ + "*string:~*opts.*apikey:sts1234", + "*string:~*req.RequestType:*postpaid", + "*prefix:~*resources.RES_GRP1.Available:10", // *resources will not be indexing + "*suffix:BrokenFilter:Invalid", + }, + Weight: 30, + QueueLength: 100, + TTL: 10 * time.Second, + MinItems: 0, + Metrics: []*MetricWithFilters{ + { + MetricID: "*tcd", + }, + { + MetricID: "*asr", + }, + { + MetricID: "*acd", + }, + }, + Blocker: true, + ThresholdIDs: []string{utils.MetaNone}, + } + if err := dm.SetStatQueueProfile(sqPrf, false); err != nil { + t.Error(err) + } + + args := &IndexHealthArgsWith3Ch{} + exp := &FilterIHReply{ + MissingIndexes: map[string][]string{ + "cgrates.org:*string:*opts.*apikey:sts1234": {"Stat_1"}, + "cgrates.org:*string:*req.RequestType:*postpaid": {"Stat_1"}, + }, + BrokenIndexes: map[string][]string{}, + MissingFilters: map[string][]string{}, + } + if rply, err := GetFltrIdxHealth(dm, + ltcache.NewCache(args.FilterCacheLimit, args.FilterCacheTTL, args.FilterCacheStaticTTL, nil), + ltcache.NewCache(args.IndexCacheLimit, args.IndexCacheTTL, args.IndexCacheStaticTTL, nil), + ltcache.NewCache(args.ObjectCacheLimit, args.ObjectCacheTTL, args.ObjectCacheStaticTTL, nil), + utils.CacheStatFilterIndexes); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(exp, rply) { + t.Errorf("Expected %+v, received %+v", utils.ToJSON(exp), utils.ToJSON(rply)) + } + + indexes := map[string]utils.StringSet{ + "*suffix:*req.Destination:+60": { // obj exist but the index don't + "Stat_1": {}, + }, + "*string:*req.ExtraField:Usage": { // index is valid but the obj does not exist + "InexistingStats": {}, + }, + } + + // we will set manually some indexes that points to an nil object or index is valid but the obj is missing + if err := dm.SetIndexes(utils.CacheStatFilterIndexes, "cgrates.org", + indexes, true, utils.NonTransactional); err != nil { + t.Error(err) + } + exp = &FilterIHReply{ + MissingObjects: []string{"cgrates.org:InexistingStats"}, + MissingIndexes: map[string][]string{ + "cgrates.org:*string:*opts.*apikey:sts1234": {"Stat_1"}, + "cgrates.org:*string:*req.RequestType:*postpaid": {"Stat_1"}, + }, + BrokenIndexes: map[string][]string{ + "cgrates.org:*suffix:*req.Destination:+60": {"Stat_1"}, + }, + MissingFilters: map[string][]string{}, + } + if rply, err := GetFltrIdxHealth(dm, + ltcache.NewCache(args.FilterCacheLimit, args.FilterCacheTTL, args.FilterCacheStaticTTL, nil), + ltcache.NewCache(args.IndexCacheLimit, args.IndexCacheTTL, args.IndexCacheStaticTTL, nil), + ltcache.NewCache(args.ObjectCacheLimit, args.ObjectCacheTTL, args.ObjectCacheStaticTTL, nil), + utils.CacheStatFilterIndexes); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(exp, rply) { + t.Errorf("Expected %+v, received %+v", utils.ToJSON(exp), utils.ToJSON(rply)) + } + + //we will use an inexisting Filter(not inline) for the same StatQueueProfile + sqPrf = &StatQueueProfile{ + Tenant: "cgrates.org", + ID: "Stat_1", + FilterIDs: []string{ + "*string:~*opts.*apikey:sts1234", + "*string:~*req.RequestType:*postpaid", + "*prefix:~*resources.RES_GRP1.Available:10", // *resources will not be indexing + "*suffix:BrokenFilter:Invalid", + "FLTR_1_NOT_EXIST", + }, + Weight: 30, + QueueLength: 100, + TTL: 10 * time.Second, + MinItems: 0, + Metrics: []*MetricWithFilters{ + { + MetricID: "*tcd", + }, + { + MetricID: "*asr", + }, + { + MetricID: "*acd", + }, + }, + Blocker: true, + ThresholdIDs: []string{utils.MetaNone}, + } + if err := dm.SetStatQueueProfile(sqPrf, false); err != nil { + t.Error(err) + } + exp = &FilterIHReply{ + MissingObjects: []string{"cgrates.org:InexistingStats"}, + MissingIndexes: map[string][]string{ + "cgrates.org:*string:*opts.*apikey:sts1234": {"Stat_1"}, + "cgrates.org:*string:*req.RequestType:*postpaid": {"Stat_1"}, + }, + BrokenIndexes: map[string][]string{ + "cgrates.org:*suffix:*req.Destination:+60": {"Stat_1"}, + }, + MissingFilters: map[string][]string{ + "cgrates.org:FLTR_1_NOT_EXIST": {"Stat_1"}, + }, + } + if rply, err := GetFltrIdxHealth(dm, + ltcache.NewCache(args.FilterCacheLimit, args.FilterCacheTTL, args.FilterCacheStaticTTL, nil), + ltcache.NewCache(args.IndexCacheLimit, args.IndexCacheTTL, args.IndexCacheStaticTTL, nil), + ltcache.NewCache(args.ObjectCacheLimit, args.ObjectCacheTTL, args.ObjectCacheStaticTTL, nil), + utils.CacheStatFilterIndexes); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(exp, rply) { + t.Errorf("Expected %+v, received %+v", utils.ToJSON(exp), utils.ToJSON(rply)) + } +} + +func TestHealthIndexRoutes(t *testing.T) { + Cache.Clear(nil) + cfg := config.NewDefaultCGRConfig() + db := NewInternalDB(nil, nil, true) + dm := NewDataManager(db, cfg.CacheCfg(), nil) + + // we will set this statQueue but without indexing + sqPrf := &StatQueueProfile{ + Tenant: "cgrates.org", + ID: "Stat_1", + FilterIDs: []string{ + "*string:~*opts.*apikey:sts1234", + "*string:~*req.RequestType:*postpaid", + "*prefix:~*resources.RES_GRP1.Available:10", // *resources will not be indexing + "*suffix:BrokenFilter:Invalid", + }, + Weight: 30, + QueueLength: 100, + TTL: 10 * time.Second, + MinItems: 0, + Metrics: []*MetricWithFilters{ + { + MetricID: "*tcd", + }, + { + MetricID: "*asr", + }, + { + MetricID: "*acd", + }, + }, + Blocker: true, + ThresholdIDs: []string{utils.MetaNone}, + } + if err := dm.SetStatQueueProfile(sqPrf, false); err != nil { + t.Error(err) + } + + args := &IndexHealthArgsWith3Ch{} + exp := &FilterIHReply{ + MissingIndexes: map[string][]string{ + "cgrates.org:*string:*opts.*apikey:sts1234": {"Stat_1"}, + "cgrates.org:*string:*req.RequestType:*postpaid": {"Stat_1"}, + }, + BrokenIndexes: map[string][]string{}, + MissingFilters: map[string][]string{}, + } + if rply, err := GetFltrIdxHealth(dm, + ltcache.NewCache(args.FilterCacheLimit, args.FilterCacheTTL, args.FilterCacheStaticTTL, nil), + ltcache.NewCache(args.IndexCacheLimit, args.IndexCacheTTL, args.IndexCacheStaticTTL, nil), + ltcache.NewCache(args.ObjectCacheLimit, args.ObjectCacheTTL, args.ObjectCacheStaticTTL, nil), + utils.CacheStatFilterIndexes); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(exp, rply) { + t.Errorf("Expected %+v, received %+v", utils.ToJSON(exp), utils.ToJSON(rply)) + } + + indexes := map[string]utils.StringSet{ + "*suffix:*req.Destination:+60": { // obj exist but the index don't + "Stat_1": {}, + }, + "*string:*req.ExtraField:Usage": { // index is valid but the obj does not exist + "InexistingStats": {}, + }, + } + + // we will set manually some indexes that points to an nil object or index is valid but the obj is missing + if err := dm.SetIndexes(utils.CacheStatFilterIndexes, "cgrates.org", + indexes, true, utils.NonTransactional); err != nil { + t.Error(err) + } + exp = &FilterIHReply{ + MissingObjects: []string{"cgrates.org:InexistingStats"}, + MissingIndexes: map[string][]string{ + "cgrates.org:*string:*opts.*apikey:sts1234": {"Stat_1"}, + "cgrates.org:*string:*req.RequestType:*postpaid": {"Stat_1"}, + }, + BrokenIndexes: map[string][]string{ + "cgrates.org:*suffix:*req.Destination:+60": {"Stat_1"}, + }, + MissingFilters: map[string][]string{}, + } + if rply, err := GetFltrIdxHealth(dm, + ltcache.NewCache(args.FilterCacheLimit, args.FilterCacheTTL, args.FilterCacheStaticTTL, nil), + ltcache.NewCache(args.IndexCacheLimit, args.IndexCacheTTL, args.IndexCacheStaticTTL, nil), + ltcache.NewCache(args.ObjectCacheLimit, args.ObjectCacheTTL, args.ObjectCacheStaticTTL, nil), + utils.CacheStatFilterIndexes); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(exp, rply) { + t.Errorf("Expected %+v, received %+v", utils.ToJSON(exp), utils.ToJSON(rply)) + } + + //we will use an inexisting Filter(not inline) for the same StatQueueProfile + sqPrf = &StatQueueProfile{ + Tenant: "cgrates.org", + ID: "Stat_1", + FilterIDs: []string{ + "*string:~*opts.*apikey:sts1234", + "*string:~*req.RequestType:*postpaid", + "*prefix:~*resources.RES_GRP1.Available:10", // *resources will not be indexing + "*suffix:BrokenFilter:Invalid", + "FLTR_1_NOT_EXIST", + }, + Weight: 30, + QueueLength: 100, + TTL: 10 * time.Second, + MinItems: 0, + Metrics: []*MetricWithFilters{ + { + MetricID: "*tcd", + }, + { + MetricID: "*asr", + }, + { + MetricID: "*acd", + }, + }, + Blocker: true, + ThresholdIDs: []string{utils.MetaNone}, + } + if err := dm.SetStatQueueProfile(sqPrf, false); err != nil { + t.Error(err) + } + exp = &FilterIHReply{ + MissingObjects: []string{"cgrates.org:InexistingStats"}, + MissingIndexes: map[string][]string{ + "cgrates.org:*string:*opts.*apikey:sts1234": {"Stat_1"}, + "cgrates.org:*string:*req.RequestType:*postpaid": {"Stat_1"}, + }, + BrokenIndexes: map[string][]string{ + "cgrates.org:*suffix:*req.Destination:+60": {"Stat_1"}, + }, + MissingFilters: map[string][]string{ + "cgrates.org:FLTR_1_NOT_EXIST": {"Stat_1"}, + }, + } + if rply, err := GetFltrIdxHealth(dm, + ltcache.NewCache(args.FilterCacheLimit, args.FilterCacheTTL, args.FilterCacheStaticTTL, nil), + ltcache.NewCache(args.IndexCacheLimit, args.IndexCacheTTL, args.IndexCacheStaticTTL, nil), + ltcache.NewCache(args.ObjectCacheLimit, args.ObjectCacheTTL, args.ObjectCacheStaticTTL, nil), + utils.CacheStatFilterIndexes); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(exp, rply) { + t.Errorf("Expected %+v, received %+v", utils.ToJSON(exp), utils.ToJSON(rply)) + } +} diff --git a/utils/struct.go b/utils/struct.go index d60c7d161..8bf6528df 100644 --- a/utils/struct.go +++ b/utils/struct.go @@ -24,18 +24,41 @@ import ( "strings" ) +func fieldByIndexIsEmpty(v reflect.Value, index []int) bool { + if len(index) == 1 { + return valueIsEmpty(v.Field(index[0])) + } + for i, x := range index { + if i > 0 { + if v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct { + if v.IsNil() { + return true + } + v = v.Elem() + } + } + v = v.Field(x) + } + return valueIsEmpty(v) +} + +func valueIsEmpty(fld reflect.Value) bool { + if fld.Kind() == reflect.String && fld.CanSet() { + fld.SetString(strings.TrimSpace(fld.String())) + } + return (fld.Kind() == reflect.String && fld.String() == EmptyString) || + ((fld.Kind() == reflect.Slice || fld.Kind() == reflect.Map) && fld.Len() == 0) || + (fld.Kind() == reflect.Int && fld.Int() == 0) +} + // Detects missing field values based on mandatory field names, s should be a pointer to a struct func MissingStructFields(s interface{}, mandatories []string) []string { missing := []string{} + sValue := reflect.ValueOf(s).Elem() + sType := sValue.Type() for _, fieldName := range mandatories { - fld := reflect.ValueOf(s).Elem().FieldByName(fieldName) - // sanitize the string fields before checking - if fld.Kind() == reflect.String && fld.CanSet() { - fld.SetString(strings.TrimSpace(fld.String())) - } - if (fld.Kind() == reflect.String && fld.String() == "") || - ((fld.Kind() == reflect.Slice || fld.Kind() == reflect.Map) && fld.Len() == 0) || - (fld.Kind() == reflect.Int && fld.Int() == 0) { + fldStr, ok := sType.FieldByName(fieldName) + if !ok || fieldByIndexIsEmpty(sValue, fldStr.Index){ missing = append(missing, fieldName) } } diff --git a/utils/struct_test.go b/utils/struct_test.go index cbb887247..1cc4f5c2a 100644 --- a/utils/struct_test.go +++ b/utils/struct_test.go @@ -37,6 +37,37 @@ func TestMissingStructFieldsCorrect(t *testing.T) { } } +func TestMissingStructFieldsNilCorporate(t *testing.T) { + tst := &TenantIDWithAPIOpts{ + APIOpts: map[string]interface{}{ + OptsAPIKey: "attr1234", + }, + } + if missing := MissingStructFields(tst, + []string{Tenant}); len(missing) != 1 { + t.Errorf("TenantIDWithAPIOpts is missing from my struct: %v", missing) + } +} + +func TestMissingStructFieldsNilCorporateTwoStructs(t *testing.T) { + tst := &struct { + APIOpts map[string]interface{} + *TenantID + *TenantAccount + }{ + APIOpts: map[string]interface{}{ + OptsAPIKey: "attr1234", + }, + TenantID: &TenantID{ + Tenant: "cgrates.org", + }, + } + if missing := MissingStructFields(tst, + []string{Tenant}); len(missing) != 1 { + t.Errorf("TenantIDWithAPIOpts is missing from my struct: %v", missing) + } +} + func TestUpdateStructWithIfaceMap(t *testing.T) { type myStruct struct { String string