     1→package metrics
     2→
     3→import (
     4→	"context"
     5→	"testing"
     6→
     7→	nbdns "github.com/netbirdio/netbird/dns"
     8→	"github.com/netbirdio/netbird/idp/dex"
     9→	rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
    10→	resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
    11→	routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
    12→	networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
    13→	nbpeer "github.com/netbirdio/netbird/management/server/peer"
    14→	"github.com/netbirdio/netbird/management/server/posture"
    15→	"github.com/netbirdio/netbird/management/server/types"
    16→	"github.com/netbirdio/netbird/route"
    17→)
    18→
    19→type mockDatasource struct{}
    20→
    21→// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information
    22→func (mockDatasource) GetAllConnectedPeers() map[string]struct{} {
    23→	return map[string]struct{}{
    24→		"1": {},
    25→	}
    26→}
    27→
    28→// GetAllAccounts returns a list of *server.Account for use in tests with predefined information
    29→func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {
    30→	localUserID := dex.EncodeDexUserID("10", "local")
    31→	idpUserID := dex.EncodeDexUserID("20", "zitadel-d5uv82dra0haedlf6kv0")
    32→	return []*types.Account{
    33→		{
    34→			Id:       "1",
    35→			Settings: &types.Settings{PeerLoginExpirationEnabled: true},
    36→			SetupKeys: map[string]*types.SetupKey{
    37→				"1": {
    38→					Id:        "1",
    39→					Ephemeral: true,
    40→					UsedTimes: 1,
    41→				},
    42→			},
    43→			Groups: map[string]*types.Group{
    44→				"1": {},
    45→				"2": {},
    46→			},
    47→			NameServerGroups: map[string]*nbdns.NameServerGroup{
    48→				"1": {},
    49→			},
    50→			Peers: map[string]*nbpeer.Peer{
    51→				"1": {
    52→					ID:         "1",
    53→					UserID:     "test",
    54→					SSHEnabled: false,
    55→					Meta:       nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},
    56→				},
    57→			},
    58→			Policies: []*types.Policy{
    59→				{
    60→					Rules: []*types.PolicyRule{
    61→						{
    62→							Bidirectional: true,
    63→							Protocol:      types.PolicyRuleProtocolTCP,
    64→						},
    65→					},
    66→				},
    67→				{
    68→					Rules: []*types.PolicyRule{
    69→						{
    70→							Bidirectional: false,
    71→							Protocol:      types.PolicyRuleProtocolTCP,
    72→						},
    73→					},
    74→					SourcePostureChecks: []string{"1"},
    75→				},
    76→			},
    77→			Routes: map[route.ID]*route.Route{
    78→				"1": {
    79→					ID:         "1",
    80→					PeerGroups: make([]string, 1),
    81→				},
    82→			},
    83→			PostureChecks: []*posture.Checks{
    84→				{
    85→					ID:   "1",
    86→					Name: "test",
    87→					Checks: posture.ChecksDefinition{
    88→						NBVersionCheck: &posture.NBVersionCheck{
    89→							MinVersion: "0.0.1",
    90→						},
    91→					},
    92→				},
    93→				{
    94→					ID:   "2",
    95→					Name: "tes2",
    96→					Checks: posture.ChecksDefinition{
    97→						NBVersionCheck: &posture.NBVersionCheck{
    98→							MinVersion: "0.0.2",
    99→						},
   100→					},
   101→				},
   102→			},
   103→			Users: map[string]*types.User{
   104→				"1": {
   105→					Id:            "1",
   106→					IsServiceUser: true,
   107→					PATs: map[string]*types.PersonalAccessToken{
   108→						"1": {},
   109→					},
   110→				},
   111→				localUserID: {
   112→					Id:            localUserID,
   113→					IsServiceUser: false,
   114→					PATs: map[string]*types.PersonalAccessToken{
   115→						"1": {},
   116→					},
   117→				},
   118→			},
   119→			Services: []*rpservice.Service{
   120→				{
   121→					ID:      "svc1",
   122→					Enabled: true,
   123→					Targets: []*rpservice.Target{
   124→						{TargetType: "peer"},
   125→						{TargetType: "host"},
   126→					},
   127→					Auth: rpservice.AuthConfig{
   128→						PasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},
   129→					},
   130→					Meta: rpservice.Meta{Status: string(rpservice.StatusActive)},
   131→				},
   132→				{
   133→					ID:      "svc2",
   134→					Enabled: false,
   135→					Targets: []*rpservice.Target{
   136→						{TargetType: "domain"},
   137→					},
   138→					Auth: rpservice.AuthConfig{
   139→						BearerAuth: &rpservice.BearerAuthConfig{Enabled: true},
   140→					},
   141→					Meta: rpservice.Meta{Status: string(rpservice.StatusPending)},
   142→				},
   143→			},
   144→		},
   145→		{
   146→			Id:       "2",
   147→			Settings: &types.Settings{PeerLoginExpirationEnabled: true},
   148→			SetupKeys: map[string]*types.SetupKey{
   149→				"1": {
   150→					Id:        "1",
   151→					Ephemeral: true,
   152→					UsedTimes: 1,
   153→				},
   154→			},
   155→			Groups: map[string]*types.Group{
   156→				"1": {},
   157→				"2": {},
   158→			},
   159→			NameServerGroups: map[string]*nbdns.NameServerGroup{
   160→				"1": {},
   161→			},
   162→			Peers: map[string]*nbpeer.Peer{
   163→				"1": {
   164→					ID:         "1",
   165→					UserID:     "test",
   166→					SSHEnabled: true,
   167→					Meta:       nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"},
   168→				},
   169→			},
   170→			Policies: []*types.Policy{
   171→				{
   172→					Rules: []*types.PolicyRule{
   173→						{
   174→							Bidirectional: true,
   175→							Protocol:      types.PolicyRuleProtocolTCP,
   176→						},
   177→					},
   178→				},
   179→				{
   180→					Rules: []*types.PolicyRule{
   181→						{
   182→							Bidirectional: false,
   183→							Protocol:      types.PolicyRuleProtocolTCP,
   184→						},
   185→					},
   186→				},
   187→			},
   188→			Routes: map[route.ID]*route.Route{
   189→				"1": {
   190→					ID:         "1",
   191→					PeerGroups: make([]string, 1),
   192→				},
   193→			},
   194→			Users: map[string]*types.User{
   195→				"1": {
   196→					Id:            "1",
   197→					IsServiceUser: true,
   198→					PATs: map[string]*types.PersonalAccessToken{
   199→						"1": {},
   200→					},
   201→				},
   202→				idpUserID: {
   203→					Id:            idpUserID,
   204→					IsServiceUser: false,
   205→					PATs: map[string]*types.PersonalAccessToken{
   206→						"1": {},
   207→					},
   208→				},
   209→			},
   210→			Networks: []*networkTypes.Network{
   211→				{
   212→					ID:        "1",
   213→					AccountID: "1",
   214→				},
   215→			},
   216→			NetworkResources: []*resourceTypes.NetworkResource{
   217→				{
   218→					ID:        "1",
   219→					AccountID: "1",
   220→					NetworkID: "1",
   221→				},
   222→				{
   223→					ID:        "2",
   224→					AccountID: "1",
   225→					NetworkID: "1",
   226→				},
   227→			},
   228→			NetworkRouters: []*routerTypes.NetworkRouter{
   229→				{
   230→					ID:        "1",
   231→					AccountID: "1",
   232→					NetworkID: "1",
   233→				},
   234→			},
   235→		},
   236→	}
   237→}
   238→
   239→// GetStoreEngine returns FileStoreEngine
   240→func (mockDatasource) GetStoreEngine() types.Engine {
   241→	return types.FileStoreEngine
   242→}
   243→
   244→// GetCustomDomainsCounts returns test custom domain counts.
   245→func (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {
   246→	return 3, 2, nil
   247→}
   248→
   249→// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties
   250→func TestGenerateProperties(t *testing.T) {
   251→	ds := mockDatasource{}
   252→	worker := Worker{
   253→		dataSource:  ds,
   254→		connManager: ds,
   255→		idpManager:  EmbeddedType,
   256→	}
   257→
   258→	properties := worker.generateProperties(context.Background())
   259→
   260→	if properties["accounts"] != 2 {
   261→		t.Errorf("expected 2 accounts, got %d", properties["accounts"])
   262→	}
   263→	if properties["peers"] != 2 {
   264→		t.Errorf("expected 2 peers, got %d", properties["peers"])
   265→	}
   266→	if properties["routes"] != 2 {
   267→		t.Errorf("expected 2 routes, got %d", properties["routes"])
   268→	}
   269→	if properties["networks"] != 1 {
   270→		t.Errorf("expected 1 networks, got %d", properties["networks"])
   271→	}
   272→	if properties["network_resources"] != 2 {
   273→		t.Errorf("expected 2 network_resources, got %d", properties["network_resources"])
   274→	}
   275→	if properties["network_routers"] != 1 {
   276→		t.Errorf("expected 1 network_routers, got %d", properties["network_routers"])
   277→	}
   278→	if properties["rules"] != 4 {
   279→		t.Errorf("expected 4 rules, got %d", properties["rules"])
   280→	}
   281→	if properties["users"] != 2 {
   282→		t.Errorf("expected 1 users, got %d", properties["users"])
   283→	}
   284→	if properties["setup_keys_usage"] != 2 {
   285→		t.Errorf("expected 1 setup_keys_usage, got %d", properties["setup_keys_usage"])
   286→	}
   287→	if properties["pats"] != 4 {
   288→		t.Errorf("expected 4 personal_access_tokens, got %d", properties["pats"])
   289→	}
   290→	if properties["peers_ssh_enabled"] != 2 {
   291→		t.Errorf("expected 2 peers_ssh_enabled, got %d", properties["peers_ssh_enabled"])
   292→	}
   293→	if properties["routes_with_routing_groups"] != 2 {
   294→		t.Errorf("expected 2 routes_with_routing_groups, got %d", properties["routes_with_routing_groups"])
   295→	}
   296→	if properties["rules_protocol_tcp"] != 4 {
   297→		t.Errorf("expected 4 rules_protocol_tcp, got %d", properties["rules_protocol_tcp"])
   298→	}
   299→	if properties["rules_direction_oneway"] != 2 {
   300→		t.Errorf("expected 2 rules_direction_oneway, got %d", properties["rules_direction_oneway"])
   301→	}
   302→
   303→	if properties["active_peers_last_day"] != 2 {
   304→		t.Errorf("expected 2 active_peers_last_day, got %d", properties["active_peers_last_day"])
   305→	}
   306→	if properties["min_active_peer_version"] != "0.0.1" {
   307→		t.Errorf("expected 0.0.1 min_active_peer_version, got %s", properties["min_active_peer_version"])
   308→	}
   309→	if properties["max_active_peer_version"] != "0.0.1" {
   310→		t.Errorf("expected 0.0.1 max_active_peer_version, got %s", properties["max_active_peer_version"])
   311→	}
   312→
   313→	if properties["peers_login_expiration_enabled"] != 2 {
   314→		t.Errorf("expected 2 peers_login_expiration_enabled, got %d", properties["peers_login_expiration_enabled"])
   315→	}
   316→
   317→	if properties["service_users"] != 2 {
   318→		t.Errorf("expected 2 service_users, got %d", properties["service_users"])
   319→	}
   320→
   321→	if properties["peer_os_linux"] != 2 {
   322→		t.Errorf("expected 2 peer_os_linux, got %d", properties["peer_os_linux"])
   323→	}
   324→
   325→	if properties["ephemeral_peers_setup_keys"] != 2 {
   326→		t.Errorf("expected 2 ephemeral_peers_setup_keys, got %d", properties["ephemeral_peers_setup_keys_usage"])
   327→	}
   328→
   329→	if properties["ephemeral_peers_setup_keys_usage"] != 2 {
   330→		t.Errorf("expected 2 ephemeral_peers_setup_keys_usage, got %d", properties["ephemeral_peers_setup_keys_usage"])
   331→	}
   332→
   333→	if properties["nameservers"] != 2 {
   334→		t.Errorf("expected 2 nameservers, got %d", properties["nameservers"])
   335→	}
   336→
   337→	if properties["groups"] != 4 {
   338→		t.Errorf("expected 4 groups, got %d", properties["groups"])
   339→	}
   340→
   341→	if properties["user_peers"] != 2 {
   342→		t.Errorf("expected 2 user_peers, got %d", properties["user_peers"])
   343→	}
   344→
   345→	if properties["store_engine"] != types.FileStoreEngine {
   346→		t.Errorf("expected JsonFile, got %s", properties["store_engine"])
   347→	}
   348→
   349→	if properties["rules_with_src_posture_checks"] != 1 {
   350→		t.Errorf("expected 1 rules_with_src_posture_checks, got %d", properties["rules_with_src_posture_checks"])
   351→	}
   352→
   353→	if properties["posture_checks"] != 2 {
   354→		t.Errorf("expected 2 posture_checks, got %d", properties["posture_checks"])
   355→	}
   356→
   357→	if properties["rosenpass_enabled"] != 1 {
   358→		t.Errorf("expected 1 rosenpass_enabled, got %d", properties["rosenpass_enabled"])
   359→	}
   360→
   361→	if properties["active_user_peers_last_day"] != 2 {
   362→		t.Errorf("expected 2 active_user_peers_last_day, got %d", properties["active_user_peers_last_day"])
   363→	}
   364→
   365→	if properties["active_users_last_day"] != 1 {
   366→		t.Errorf("expected 1 active_users_last_day, got %d", properties["active_users_last_day"])
   367→	}
   368→
   369→	if properties["local_users_count"] != 1 {
   370→		t.Errorf("expected 1 local_users_count, got %d", properties["local_users_count"])
   371→	}
   372→	if properties["idp_users_count"] != 1 {
   373→		t.Errorf("expected 1 idp_users_count, got %d", properties["idp_users_count"])
   374→	}
   375→	if properties["embedded_idp_users_zitadel"] != 1 {
   376→		t.Errorf("expected 1 embedded_idp_users_zitadel, got %v", properties["embedded_idp_users_zitadel"])
   377→	}
   378→	if properties["embedded_idp_count"] != 1 {
   379→		t.Errorf("expected 1 embedded_idp_count, got %v", properties["embedded_idp_count"])
   380→	}
   381→
   382→	if properties["services"] != 2 {
   383→		t.Errorf("expected 2 services, got %v", properties["services"])
   384→	}
   385→	if properties["services_enabled"] != 1 {
   386→		t.Errorf("expected 1 services_enabled, got %v", properties["services_enabled"])
   387→	}
   388→	if properties["services_targets"] != 3 {
   389→		t.Errorf("expected 3 services_targets, got %v", properties["services_targets"])
   390→	}
   391→	if properties["services_status_active"] != 1 {
   392→		t.Errorf("expected 1 services_status_active, got %v", properties["services_status_active"])
   393→	}
   394→	if properties["services_status_pending"] != 1 {
   395→		t.Errorf("expected 1 services_status_pending, got %v", properties["services_status_pending"])
   396→	}
   397→	if properties["services_status_error"] != 0 {
   398→		t.Errorf("expected 0 services_status_error, got %v", properties["services_status_error"])
   399→	}
   400→	if properties["services_target_type_peer"] != 1 {
   401→		t.Errorf("expected 1 services_target_type_peer, got %v", properties["services_target_type_peer"])
   402→	}
   403→	if properties["services_target_type_host"] != 1 {
   404→		t.Errorf("expected 1 services_target_type_host, got %v", properties["services_target_type_host"])
   405→	}
   406→	if properties["services_target_type_domain"] != 1 {
   407→		t.Errorf("expected 1 services_target_type_domain, got %v", properties["services_target_type_domain"])
   408→	}
   409→	if properties["services_auth_password"] != 1 {
   410→		t.Errorf("expected 1 services_auth_password, got %v", properties["services_auth_password"])
   411→	}
   412→	if properties["services_auth_oidc"] != 1 {
   413→		t.Errorf("expected 1 services_auth_oidc, got %v", properties["services_auth_oidc"])
   414→	}
   415→	if properties["services_auth_pin"] != 0 {
   416→		t.Errorf("expected 0 services_auth_pin, got %v", properties["services_auth_pin"])
   417→	}
   418→	if properties["custom_domains"] != int64(3) {
   419→		t.Errorf("expected 3 custom_domains, got %v", properties["custom_domains"])
   420→	}
   421→	if properties["custom_domains_validated"] != int64(2) {
   422→		t.Errorf("expected 2 custom_domains_validated, got %v", properties["custom_domains_validated"])
   423→	}
   424→}
   425→
   426→func TestExtractIdpType(t *testing.T) {
   427→	tests := []struct {
   428→		connectorID string
   429→		expected    string
   430→	}{
   431→		{"okta-abc123def", "okta"},
   432→		{"zitadel-d5uv82dra0haedlf6kv0", "zitadel"},
   433→		{"entra-xyz789", "entra"},
   434→		{"google-abc123", "google"},
   435→		{"pocketid-abc123", "pocketid"},
   436→		{"microsoft-abc123", "microsoft"},
   437→		{"authentik-abc123", "authentik"},
   438→		{"keycloak-d5uv82dra0haedlf6kv0", "keycloak"},
   439→		{"local", "oidc"},
   440→		{"", "oidc"},
   441→	}
   442→
   443→	for _, tt := range tests {
   444→		t.Run(tt.connectorID, func(t *testing.T) {
   445→			result := extractIdpType(tt.connectorID)
   446→			if result != tt.expected {
   447→				t.Errorf("extractIdpType(%q) = %q, want %q", tt.connectorID, result, tt.expected)
   448→			}
   449→		})
   450→	}
   451→}
   452→

package metrics

import (
	"context"
	"testing"

	nbdns "github.com/netbirdio/netbird/dns"
	"github.com/netbirdio/netbird/idp/dex"
	rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
	resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
	routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
	networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
	nbpeer "github.com/netbirdio/netbird/management/server/peer"
	"github.com/netbirdio/netbird/management/server/posture"
	"github.com/netbirdio/netbird/management/server/types"
	"github.com/netbirdio/netbird/route"
)

type mockDatasource struct{}

// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information
func (mockDatasource) GetAllConnectedPeers() map[string]struct{} {
	return map[string]struct{}{
		"1": {},
	}
}

// GetAllAccounts returns a list of *server.Account for use in tests with predefined information
func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {
	localUserID := dex.EncodeDexUserID("10", "local")
	idpUserID := dex.EncodeDexUserID("20", "zitadel-d5uv82dra0haedlf6kv0")
	return []*types.Account{
		{
			Id:       "1",
			Settings: &types.Settings{PeerLoginExpirationEnabled: true},
			SetupKeys: map[string]*types.SetupKey{
				"1": {
					Id:        "1",
					Ephemeral: true,
					UsedTimes: 1,
				},
			},
			Groups: map[string]*types.Group{
				"1": {},
				"2": {},
			},
			NameServerGroups: map[string]*nbdns.NameServerGroup{
				"1": {},
			},
			Peers: map[string]*nbpeer.Peer{
				"1": {
					ID:         "1",
					UserID:     "test",
					SSHEnabled: false,
					Meta:       nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},
				},
			},
			Policies: []*types.Policy{
				{
					Rules: []*types.PolicyRule{
						{
							Bidirectional: true,
							Protocol:      types.PolicyRuleProtocolTCP,
						},
					},
				},
				{
					Rules: []*types.PolicyRule{
						{
							Bidirectional: false,
							Protocol:      types.PolicyRuleProtocolTCP,
						},
					},
					SourcePostureChecks: []string{"1"},
				},
			},
			Routes: map[route.ID]*route.Route{
				"1": {
					ID:         "1",
					PeerGroups: make([]string, 1),
				},
			},
			PostureChecks: []*posture.Checks{
				{
					ID:   "1",
					Name: "test",
					Checks: posture.ChecksDefinition{
						NBVersionCheck: &posture.NBVersionCheck{
							MinVersion: "0.0.1",
						},
					},
				},
				{
					ID:   "2",
					Name: "tes2",
					Checks: posture.ChecksDefinition{
						NBVersionCheck: &posture.NBVersionCheck{
							MinVersion: "0.0.2",
						},
					},
				},
			},
			Users: map[string]*types.User{
				"1": {
					Id:            "1",
					IsServiceUser: true,
					PATs: map[string]*types.PersonalAccessToken{
						"1": {},
					},
				},
				localUserID: {
					Id:            localUserID,
					IsServiceUser: false,
					PATs: map[string]*types.PersonalAccessToken{
						"1": {},
					},
				},
			},
			Services: []*rpservice.Service{
				{
					ID:      "svc1",
					Enabled: true,
					Targets: []*rpservice.Target{
						{TargetType: "peer"},
						{TargetType: "host"},
					},
					Auth: rpservice.AuthConfig{
						PasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},
					},
					Meta: rpservice.Meta{Status: string(rpservice.StatusActive)},
				},
				{
					ID:      "svc2",
					Enabled: false,
					Targets: []*rpservice.Target{
						{TargetType: "domain"},
					},
					Auth: rpservice.AuthConfig{
						BearerAuth: &rpservice.BearerAuthConfig{Enabled: true},
					},
					Meta: rpservice.Meta{Status: string(rpservice.StatusPending)},
				},
			},
		},
		{
			Id:       "2",
			Settings: &types.Settings{PeerLoginExpirationEnabled: true},
			SetupKeys: map[string]*types.SetupKey{
				"1": {
					Id:        "1",
					Ephemeral: true,
					UsedTimes: 1,
				},
			},
			Groups: map[string]*types.Group{
				"1": {},
				"2": {},
			},
			NameServerGroups: map[string]*nbdns.NameServerGroup{
				"1": {},
			},
			Peers: map[string]*nbpeer.Peer{
				"1": {
					ID:         "1",
					UserID:     "test",
					SSHEnabled: true,
					Meta:       nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"},
				},
			},
			Policies: []*types.Policy{
				{
					Rules: []*types.PolicyRule{
						{
							Bidirectional: true,
							Protocol:      types.PolicyRuleProtocolTCP,
						},
					},
				},
				{
					Rules: []*types.PolicyRule{
						{
							Bidirectional: false,
							Protocol:      types.PolicyRuleProtocolTCP,
						},
					},
				},
			},
			Routes: map[route.ID]*route.Route{
				"1": {
					ID:         "1",
					PeerGroups: make([]string, 1),
				},
			},
			Users: map[string]*types.User{
				"1": {
					Id:            "1",
					IsServiceUser: true,
					PATs: map[string]*types.PersonalAccessToken{
						"1": {},
					},
				},
				idpUserID: {
					Id:            idpUserID,
					IsServiceUser: false,
					PATs: map[string]*types.PersonalAccessToken{
						"1": {},
					},
				},
			},
			Networks: []*networkTypes.Network{
				{
					ID:        "1",
					AccountID: "1",
				},
			},
			NetworkResources: []*resourceTypes.NetworkResource{
				{
					ID:        "1",
					AccountID: "1",
					NetworkID: "1",
				},
				{
					ID:        "2",
					AccountID: "1",
					NetworkID: "1",
				},
			},
			NetworkRouters: []*routerTypes.NetworkRouter{
				{
					ID:        "1",
					AccountID: "1",
					NetworkID: "1",
				},
			},
		},
	}
}

// GetStoreEngine returns FileStoreEngine
func (mockDatasource) GetStoreEngine() types.Engine {
	return types.FileStoreEngine
}

// GetCustomDomainsCounts returns test custom domain counts.
func (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {
	return 3, 2, nil
}

// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties
func TestGenerateProperties(t *testing.T) {
	ds := mockDatasource{}
	worker := Worker{
		dataSource:  ds,
		connManager: ds,
		idpManager:  EmbeddedType,
	}

	properties := worker.generateProperties(context.Background())

	if properties["accounts"] != 2 {
		t.Errorf("expected 2 accounts, got %d", properties["accounts"])
	}
	if properties["peers"] != 2 {
		t.Errorf("expected 2 peers, got %d", properties["peers"])
	}
	if properties["routes"] != 2 {
		t.Errorf("expected 2 routes, got %d", properties["routes"])
	}
	if properties["networks"] != 1 {
		t.Errorf("expected 1 networks, got %d", properties["networks"])
	}
	if properties["network_resources"] != 2 {
		t.Errorf("expected 2 network_resources, got %d", properties["network_resources"])
	}
	if properties["network_routers"] != 1 {
		t.Errorf("expected 1 network_routers, got %d", properties["network_routers"])
	}
	if properties["rules"] != 4 {
		t.Errorf("expected 4 rules, got %d", properties["rules"])
	}
	if properties["users"] != 2 {
		t.Errorf("expected 1 users, got %d", properties["users"])
	}
	if properties["setup_keys_usage"] != 2 {
		t.Errorf("expected 1 setup_keys_usage, got %d", properties["setup_keys_usage"])
	}
	if properties["pats"] != 4 {
		t.Errorf("expected 4 personal_access_tokens, got %d", properties["pats"])
	}
	if properties["peers_ssh_enabled"] != 2 {
		t.Errorf("expected 2 peers_ssh_enabled, got %d", properties["peers_ssh_enabled"])
	}
	if properties["routes_with_routing_groups"] != 2 {
		t.Errorf("expected 2 routes_with_routing_groups, got %d", properties["routes_with_routing_groups"])
	}
	if properties["rules_protocol_tcp"] != 4 {
		t.Errorf("expected 4 rules_protocol_tcp, got %d", properties["rules_protocol_tcp"])
	}
	if properties["rules_direction_oneway"] != 2 {
		t.Errorf("expected 2 rules_direction_oneway, got %d", properties["rules_direction_oneway"])
	}

	if properties["active_peers_last_day"] != 2 {
		t.Errorf("expected 2 active_peers_last_day, got %d", properties["active_peers_last_day"])
	}
	if properties["min_active_peer_version"] != "0.0.1" {
		t.Errorf("expected 0.0.1 min_active_peer_version, got %s", properties["min_active_peer_version"])
	}
	if properties["max_active_peer_version"] != "0.0.1" {
		t.Errorf("expected 0.0.1 max_active_peer_version, got %s", properties["max_active_peer_version"])
	}

	if properties["peers_login_expiration_enabled"] != 2 {
		t.Errorf("expected 2 peers_login_expiration_enabled, got %d", properties["peers_login_expiration_enabled"])
	}

	if properties["service_users"] != 2 {
		t.Errorf("expected 2 service_users, got %d", properties["service_users"])
	}

	if properties["peer_os_linux"] != 2 {
		t.Errorf("expected 2 peer_os_linux, got %d", properties["peer_os_linux"])
	}

	if properties["ephemeral_peers_setup_keys"] != 2 {
		t.Errorf("expected 2 ephemeral_peers_setup_keys, got %d", properties["ephemeral_peers_setup_keys_usage"])
	}

	if properties["ephemeral_peers_setup_keys_usage"] != 2 {
		t.Errorf("expected 2 ephemeral_peers_setup_keys_usage, got %d", properties["ephemeral_peers_setup_keys_usage"])
	}

	if properties["nameservers"] != 2 {
		t.Errorf("expected 2 nameservers, got %d", properties["nameservers"])
	}

	if properties["groups"] != 4 {
		t.Errorf("expected 4 groups, got %d", properties["groups"])
	}

	if properties["user_peers"] != 2 {
		t.Errorf("expected 2 user_peers, got %d", properties["user_peers"])
	}

	if properties["store_engine"] != types.FileStoreEngine {
		t.Errorf("expected JsonFile, got %s", properties["store_engine"])
	}

	if properties["rules_with_src_posture_checks"] != 1 {
		t.Errorf("expected 1 rules_with_src_posture_checks, got %d", properties["rules_with_src_posture_checks"])
	}

	if properties["posture_checks"] != 2 {
		t.Errorf("expected 2 posture_checks, got %d", properties["posture_checks"])
	}

	if properties["rosenpass_enabled"] != 1 {
		t.Errorf("expected 1 rosenpass_enabled, got %d", properties["rosenpass_enabled"])
	}

	if properties["active_user_peers_last_day"] != 2 {
		t.Errorf("expected 2 active_user_peers_last_day, got %d", properties["active_user_peers_last_day"])
	}

	if properties["active_users_last_day"] != 1 {
		t.Errorf("expected 1 active_users_last_day, got %d", properties["active_users_last_day"])
	}

	if properties["local_users_count"] != 1 {
		t.Errorf("expected 1 local_users_count, got %d", properties["local_users_count"])
	}
	if properties["idp_users_count"] != 1 {
		t.Errorf("expected 1 idp_users_count, got %d", properties["idp_users_count"])
	}
	if properties["embedded_idp_users_zitadel"] != 1 {
		t.Errorf("expected 1 embedded_idp_users_zitadel, got %v", properties["embedded_idp_users_zitadel"])
	}
	if properties["embedded_idp_count"] != 1 {
		t.Errorf("expected 1 embedded_idp_count, got %v", properties["embedded_idp_count"])
	}

	if properties["services"] != 2 {
		t.Errorf("expected 2 services, got %v", properties["services"])
	}
	if properties["services_enabled"] != 1 {
		t.Errorf("expected 1 services_enabled, got %v", properties["services_enabled"])
	}
	if properties["services_targets"] != 3 {
		t.Errorf("expected 3 services_targets, got %v", properties["services_targets"])
	}
	if properties["services_status_active"] != 1 {
		t.Errorf("expected 1 services_status_active, got %v", properties["services_status_active"])
	}
	if properties["services_status_pending"] != 1 {
		t.Errorf("expected 1 services_status_pending, got %v", properties["services_status_pending"])
	}
	if properties["services_status_error"] != 0 {
		t.Errorf("expected 0 services_status_error, got %v", properties["services_status_error"])
	}
	if properties["services_target_type_peer"] != 1 {
		t.Errorf("expected 1 services_target_type_peer, got %v", properties["services_target_type_peer"])
	}
	if properties["services_target_type_host"] != 1 {
		t.Errorf("expected 1 services_target_type_host, got %v", properties["services_target_type_host"])
	}
	if properties["services_target_type_domain"] != 1 {
		t.Errorf("expected 1 services_target_type_domain, got %v", properties["services_target_type_domain"])
	}
	if properties["services_auth_password"] != 1 {
		t.Errorf("expected 1 services_auth_password, got %v", properties["services_auth_password"])
	}
	if properties["services_auth_oidc"] != 1 {
		t.Errorf("expected 1 services_auth_oidc, got %v", properties["services_auth_oidc"])
	}
	if properties["services_auth_pin"] != 0 {
		t.Errorf("expected 0 services_auth_pin, got %v", properties["services_auth_pin"])
	}
	if properties["custom_domains"] != int64(3) {
		t.Errorf("expected 3 custom_domains, got %v", properties["custom_domains"])
	}
	if properties["custom_domains_validated"] != int64(2) {
		t.Errorf("expected 2 custom_domains_validated, got %v", properties["custom_domains_validated"])
	}
}

func TestExtractIdpType(t *testing.T) {
	tests := []struct {
		connectorID string
		expected    string
	}{
		{"okta-abc123def", "okta"},
		{"zitadel-d5uv82dra0haedlf6kv0", "zitadel"},
		{"entra-xyz789", "entra"},
		{"google-abc123", "google"},
		{"pocketid-abc123", "pocketid"},
		{"microsoft-abc123", "microsoft"},
		{"authentik-abc123", "authentik"},
		{"keycloak-d5uv82dra0haedlf6kv0", "keycloak"},
		{"local", "oidc"},
		{"", "oidc"},
	}

	for _, tt := range tests {
		t.Run(tt.connectorID, func(t *testing.T) {
			result := extractIdpType(tt.connectorID)
			if result != tt.expected {
				t.Errorf("extractIdpType(%q) = %q, want %q", tt.connectorID, result, tt.expected)
			}
		})
	}
}


The existing test at line 439 expects `extractIdpType("local")` to return `"oidc"`. With my fix, it will return `"local"`. I need to update this test case.

Also, the `TestGenerateProperties` test at line 378 expects `embedded_idp_count` to be 1. With my fix, it will be 2 (local + zitadel). I need to update that too.

And I should add a test for `embedded_idp_users_local` being 1.

Let me update the tests:
1. Fix `extractIdpType("local")` expected from `"oidc"` to `"local"`

2. For the empty string case, my fix would return `""` since there's no dash to split on, which is a bit odd but probably fine since empty connector IDs shouldn't occur in practice anyway.

Actually, I realize the test expects `extractIdpType("")` to return `"oidc"`, but my change would return `""` instead. That would create a weird metric key. Let me add a guard clause to handle the empty case properly.

Adding guard clause for empty string... Since `idpID` is guaranteed to be non-empty when `extractIdpType` is called, the empty string check is just defensive programming. Now I need to update the test cases to reflect the expected behavior: `{"local", "oidc"}` should map to `{"local", "local"}`, the empty connector case should return `"unknown"`, and I need to verify the embedded IDP count is 2 and add an assertion for the local users metric.


The existing tests need updating. The test at line 439 expects `extractIdpType("local")` → `"oidc"` — that's exactly the bug. And `embedded_idp_count` at line 378 expects 1, but it should now be 2 (local + zitadel).


Edit

func extractIdpType(connectorID string) string {
	idx := strings.LastIndex(connectorID, "-")
	if idx <= 0 {
		return strings.ToLower(connectorID)
	}
	return strings.ToLower(connectorID[:idx])
}

func extractIdpType(connectorID string) string {
	if connectorID == "" {
		return "unknown"
	}
	idx := strings.LastIndex(connectorID, "-")
	if idx <= 0 {
		return strings.ToLower(connectorID)
	}
	return strings.ToLower(connectorID[:idx])
}


The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted.go has been updated successfully.

// Package metrics gather anonymous information about the usage of NetBird management
package metrics

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"sort"
	"strings"
	"time"

	"github.com/hashicorp/go-version"
	"github.com/netbirdio/netbird/idp/dex"
	rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
	log "github.com/sirupsen/logrus"

	"github.com/netbirdio/netbird/management/server/types"
	nbversion "github.com/netbirdio/netbird/version"
)

const (
	// PayloadEvent identifies an event type
	PayloadEvent = "self-hosted stats"
	// payloadEndpoint metrics defaultEndpoint to send anonymous data
	payloadEndpoint = "https://metrics.netbird.io"
	// defaultPushInterval default interval to push metrics
	defaultPushInterval = 12 * time.Hour
	// requestTimeout http request timeout
	requestTimeout = 45 * time.Second
	EmbeddedType   = "embedded"
)

type getTokenResponse struct {
	PublicAPIToken string `json:"public_api_token"`
}

type pushPayload struct {
	APIKey     string     `json:"api_key"`
	DistinctID string     `json:"distinct_id"`
	Event      string     `json:"event"`
	Properties properties `json:"properties"`
	Timestamp  time.Time  `json:"timestamp"`
}

// properties metrics to push
type properties map[string]interface{}

// DataSource metric data source
type DataSource interface {
	GetAllAccounts(ctx context.Context) []*types.Account
	GetStoreEngine() types.Engine
	GetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)
}

// ConnManager peer connection manager that holds state for current active connections
type ConnManager interface {
	GetAllConnectedPeers() map[string]struct{}
}

// Worker metrics collector and pusher
type Worker struct {
	ctx         context.Context
	id          string
	idpManager  string
	dataSource  DataSource
	connManager ConnManager
	startupTime time.Time
	lastRun     time.Time
}

// NewWorker returns a metrics worker
func NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {
	currentTime := time.Now()
	return &Worker{
		ctx:         ctx,
		id:          id,
		idpManager:  idpManager,
		dataSource:  dataSource,
		connManager: connManager,
		startupTime: currentTime,
		lastRun:     currentTime,
	}
}

// Run runs the metrics worker
func (w *Worker) Run(ctx context.Context) {
	interval := getMetricsInterval(ctx)

	pushTicker := time.NewTicker(interval)
	for {
		select {
		case <-w.ctx.Done():
			return
		case <-pushTicker.C:
			err := w.sendMetrics(ctx)
			if err != nil {
				log.WithContext(ctx).Error(err)
			}
			w.lastRun = time.Now()
		}
	}
}

func getMetricsInterval(ctx context.Context) time.Duration {
	interval := defaultPushInterval
	if os.Getenv("NETBIRD_METRICS_INTERVAL_IN_SECONDS") != "" {
		newInterval, err := time.ParseDuration(os.Getenv("NETBIRD_METRICS_INTERVAL_IN_SECONDS") + "s")
		if err != nil {
			log.WithContext(ctx).Errorf("unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v", defaultPushInterval, err)
		} else {
			log.WithContext(ctx).Infof("using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s", newInterval)
			interval = newInterval
		}
	}
	return interval
}

func (w *Worker) sendMetrics(ctx context.Context) error {
	apiKey, err := getAPIKey(w.ctx)
	if err != nil {
		return err
	}

	payload := w.generatePayload(ctx, apiKey)

	payloadString, err := buildMetricsPayload(payload)
	if err != nil {
		return err
	}

	httpClient := http.Client{}

	exportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+"/capture/", payloadString)
	if err != nil {
		return fmt.Errorf("unable to create metrics post request %v", err)
	}
	defer cancelCTX()

	jobResp, err := httpClient.Do(exportJobReq)
	if err != nil {
		return fmt.Errorf("unable to push metrics %v", err)
	}

	defer func() {
		err = jobResp.Body.Close()
		if err != nil {
			log.WithContext(ctx).Errorf("error while closing update metrics response body: %v", err)
		}
	}()

	if jobResp.StatusCode != 200 {
		return fmt.Errorf("unable to push anonymous metrics, got statusCode %d", jobResp.StatusCode)
	}

	log.WithContext(ctx).Infof("sent anonymous metrics, next push will happen in %s. "+
		"You can disable these metrics by running with flag --disable-anonymous-metrics,"+
		" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics", getMetricsInterval(ctx))

	return nil
}

func (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {
	properties := w.generateProperties(ctx)

	return pushPayload{
		APIKey:     apiKey,
		DistinctID: w.id,
		Event:      PayloadEvent,
		Properties: properties,
		Timestamp:  time.Now(),
	}
}

func (w *Worker) generateProperties(ctx context.Context) properties {
	var (
		uptime                    float64
		accounts                  int
		expirationEnabled         int
		users                     int
		serviceUsers              int
		pats                      int
		peers                     int
		peersSSHEnabled           int
		setupKeysUsage            int
		ephemeralPeersSKs         int
		ephemeralPeersSKUsage     int
		activePeersLastDay        int
		activeUserPeersLastDay    int
		osPeers                   map[string]int
		activeUsersLastDay        map[string]struct{}
		userPeers                 int
		rules                     int
		rulesProtocol             map[string]int
		rulesDirection            map[string]int
		rulesWithSrcPostureChecks int
		postureChecks             int
		groups                    int
		routes                    int
		routesWithRGGroups        int
		networks                  int
		networkResources          int
		networkRouters            int
		networkRoutersWithPG      int
		nameservers               int
		uiClient                  int
		version                   string
		peerActiveVersions        []string
		osUIClients               map[string]int
		rosenpassEnabled          int
		localUsers                int
		idpUsers                  int
		embeddedIdpTypes          map[string]int
		services                  int
		servicesEnabled           int
		servicesTargets           int
		servicesStatusActive      int
		servicesStatusPending     int
		servicesStatusError       int
		servicesTargetType        map[string]int
		servicesAuthPassword      int
		servicesAuthPin           int
		servicesAuthOIDC          int
	)
	start := time.Now()
	metricsProperties := make(properties)
	osPeers = make(map[string]int)
	osUIClients = make(map[string]int)
	rulesProtocol = make(map[string]int)
	rulesDirection = make(map[string]int)
	activeUsersLastDay = make(map[string]struct{})
	embeddedIdpTypes = make(map[string]int)
	servicesTargetType = make(map[string]int)
	uptime = time.Since(w.startupTime).Seconds()
	connections := w.connManager.GetAllConnectedPeers()
	version = nbversion.NetbirdVersion()

	customDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)

	for _, account := range w.dataSource.GetAllAccounts(ctx) {
		accounts++

		if account.Settings.PeerLoginExpirationEnabled {
			expirationEnabled++
		}

		groups += len(account.Groups)
		networks += len(account.Networks)
		networkResources += len(account.NetworkResources)

		networkRouters += len(account.NetworkRouters)
		for _, router := range account.NetworkRouters {
			if len(router.PeerGroups) > 0 {
				networkRoutersWithPG++
			}
		}

		routes += len(account.Routes)
		for _, route := range account.Routes {
			if len(route.PeerGroups) > 0 {
				routesWithRGGroups++
			}
		}
		nameservers += len(account.NameServerGroups)

		for _, policy := range account.Policies {
			for _, rule := range policy.Rules {
				rules++
				rulesProtocol[string(rule.Protocol)]++
				if rule.Bidirectional {
					rulesDirection["bidirectional"]++
				} else {
					rulesDirection["oneway"]++
				}
			}
			if len(policy.SourcePostureChecks) > 0 {
				rulesWithSrcPostureChecks++
			}
		}

		postureChecks += len(account.PostureChecks)

		for _, user := range account.Users {
			if user.IsServiceUser {
				serviceUsers++
			} else {
				users++
				if w.idpManager == EmbeddedType {
					_, idpID, err := dex.DecodeDexUserID(user.Id)
					if err == nil {
						if idpID == "local" {
							localUsers++
						} else {
							idpUsers++
						}
						idpType := extractIdpType(idpID)
						embeddedIdpTypes[idpType]++
					}
				}
			}
			pats += len(user.PATs)
		}

		for _, key := range account.SetupKeys {
			setupKeysUsage += key.UsedTimes
			if key.Ephemeral {
				ephemeralPeersSKs++
				ephemeralPeersSKUsage += key.UsedTimes
			}
		}

		for _, peer := range account.Peers {
			peers++

			if peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {
				peersSSHEnabled++
			}

			if peer.Meta.Flags.RosenpassEnabled {
				rosenpassEnabled++
			}

			if peer.UserID != "" {
				userPeers++
			}

			osKey := strings.ToLower(fmt.Sprintf("peer_os_%s", peer.Meta.GoOS))
			osCount := osPeers[osKey]
			osPeers[osKey] = osCount + 1

			if peer.Meta.UIVersion != "" {
				uiClient++
				uiOSKey := strings.ToLower(fmt.Sprintf("ui_client_os_%s", peer.Meta.GoOS))
				osUICount := osUIClients[uiOSKey]
				osUIClients[uiOSKey] = osUICount + 1
			}

			_, connected := connections[peer.ID]
			if connected || peer.Status.LastSeen.After(w.lastRun) {
				activePeersLastDay++
				if peer.UserID != "" {
					activeUserPeersLastDay++
					activeUsersLastDay[peer.UserID] = struct{}{}
				}
				osActiveKey := osKey + "_active"
				osActiveCount := osPeers[osActiveKey]
				osPeers[osActiveKey] = osActiveCount + 1
				peerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)
			}
		}

		for _, service := range account.Services {
			services++
			if service.Enabled {
				servicesEnabled++
			}
			servicesTargets += len(service.Targets)

			switch rpservice.Status(service.Meta.Status) {
			case rpservice.StatusActive:
				servicesStatusActive++
			case rpservice.StatusPending:
				servicesStatusPending++
			case rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:
				servicesStatusError++
			}

			for _, target := range service.Targets {
				servicesTargetType[target.TargetType]++
			}

			if service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {
				servicesAuthPassword++
			}
			if service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {
				servicesAuthPin++
			}
			if service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {
				servicesAuthOIDC++
			}
		}
	}

	minActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)
	metricsProperties["uptime"] = uptime
	metricsProperties["accounts"] = accounts
	metricsProperties["users"] = users
	metricsProperties["service_users"] = serviceUsers
	metricsProperties["pats"] = pats
	metricsProperties["peers"] = peers
	metricsProperties["peers_ssh_enabled"] = peersSSHEnabled
	metricsProperties["peers_login_expiration_enabled"] = expirationEnabled
	metricsProperties["setup_keys_usage"] = setupKeysUsage
	metricsProperties["ephemeral_peers_setup_keys"] = ephemeralPeersSKs
	metricsProperties["ephemeral_peers_setup_keys_usage"] = ephemeralPeersSKUsage
	metricsProperties["active_peers_last_day"] = activePeersLastDay
	metricsProperties["active_user_peers_last_day"] = activeUserPeersLastDay
	metricsProperties["active_users_last_day"] = len(activeUsersLastDay)
	metricsProperties["user_peers"] = userPeers
	metricsProperties["rules"] = rules
	metricsProperties["rules_with_src_posture_checks"] = rulesWithSrcPostureChecks
	metricsProperties["posture_checks"] = postureChecks
	metricsProperties["groups"] = groups
	metricsProperties["networks"] = networks
	metricsProperties["network_resources"] = networkResources
	metricsProperties["network_routers"] = networkRouters
	metricsProperties["network_routers_with_groups"] = networkRoutersWithPG
	metricsProperties["routes"] = routes
	metricsProperties["routes_with_routing_groups"] = routesWithRGGroups
	metricsProperties["nameservers"] = nameservers
	metricsProperties["version"] = version
	metricsProperties["min_active_peer_version"] = minActivePeerVersion
	metricsProperties["max_active_peer_version"] = maxActivePeerVersion
	metricsProperties["ui_clients"] = uiClient
	metricsProperties["idp_manager"] = w.idpManager
	metricsProperties["store_engine"] = w.dataSource.GetStoreEngine()
	metricsProperties["rosenpass_enabled"] = rosenpassEnabled
	metricsProperties["local_users_count"] = localUsers
	metricsProperties["idp_users_count"] = idpUsers
	metricsProperties["embedded_idp_count"] = len(embeddedIdpTypes)

	metricsProperties["services"] = services
	metricsProperties["services_enabled"] = servicesEnabled
	metricsProperties["services_targets"] = servicesTargets
	metricsProperties["services_status_active"] = servicesStatusActive
	metricsProperties["services_status_pending"] = servicesStatusPending
	metricsProperties["services_status_error"] = servicesStatusError
	metricsProperties["services_auth_password"] = servicesAuthPassword
	metricsProperties["services_auth_pin"] = servicesAuthPin
	metricsProperties["services_auth_oidc"] = servicesAuthOIDC
	metricsProperties["custom_domains"] = customDomains
	metricsProperties["custom_domains_validated"] = customDomainsValidated

	for targetType, count := range servicesTargetType {
		metricsProperties["services_target_type_"+targetType] = count
	}

	for idpType, count := range embeddedIdpTypes {
		metricsProperties["embedded_idp_users_"+idpType] = count
	}

	for protocol, count := range rulesProtocol {
		metricsProperties["rules_protocol_"+protocol] = count
	}

	for direction, count := range rulesDirection {
		metricsProperties["rules_direction_"+direction] = count
	}

	for os, count := range osPeers {
		metricsProperties[os] = count
	}

	for os, count := range osUIClients {
		metricsProperties[os] = count
	}

	metricsProperties["metric_generation_time"] = time.Since(start).Milliseconds()

	return metricsProperties
}

func getAPIKey(ctx context.Context) (string, error) {
	ctx, cancel := context.WithTimeout(ctx, requestTimeout)
	defer cancel()

	httpClient := http.Client{}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+"/GetToken", nil)
	if err != nil {
		return "", fmt.Errorf("unable to create request for metrics public api token %v", err)
	}
	response, err := httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("unable to request metrics public api token %v", err)
	}

	defer func() {
		err = response.Body.Close()
		if err != nil {
			log.WithContext(ctx).Errorf("error while closing metrics token response body: %v", err)
		}
	}()

	if response.StatusCode != 200 {
		return "", fmt.Errorf("unable to retrieve metrics token, statusCode %d", response.StatusCode)
	}

	body, err := io.ReadAll(response.Body)
	if err != nil {
		return "", fmt.Errorf("coudln't get metrics token response; %v", err)
	}

	var tokenResponse getTokenResponse

	err = json.Unmarshal(body, &tokenResponse)
	if err != nil {
		return "", fmt.Errorf("coudln't parse metrics public api token; %v", err)
	}

	return tokenResponse.PublicAPIToken, nil
}

func buildMetricsPayload(payload pushPayload) (string, error) {
	str, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("unable to marshal metrics payload, got err: %v", err)
	}
	return string(str), nil
}

func createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {
	ctx, cancel := context.WithTimeout(ctx, requestTimeout)
	reqURL := endpoint

	payload := strings.NewReader(payloadStr)

	req, err := http.NewRequestWithContext(ctx, "POST", reqURL, payload)
	if err != nil {
		cancel()
		return nil, nil, err
	}
	req.Header.Add("content-type", "application/json")

	return req, cancel, nil
}

// extractIdpType extracts the IdP type from a Dex connector ID.
// Connector IDs are formatted as "<type>-<xid>" (e.g., "okta-abc123", "zitadel-xyz").
// Returns the type prefix, or "oidc" if no known prefix is found.
func extractIdpType(connectorID string) string {
	idx := strings.LastIndex(connectorID, "-")
	if idx <= 0 {
		return strings.ToLower(connectorID)
	}
	return strings.ToLower(connectorID[:idx])
}

func getMinMaxVersion(inputList []string) (string, string) {
	versions := make([]*version.Version, 0)

	for _, raw := range inputList {
		if raw != "" && nbversion.SemverRegexp.MatchString(raw) {
			v, err := version.NewVersion(raw)
			if err == nil {
				versions = append(versions, v)
			}
		}
	}

	targetIndex := 1
	l := len(versions)

	switch l {
	case 0:
		return "", ""
	case targetIndex:
		v := versions[targetIndex-1].String()
		return v, v
	default:
		sort.Sort(version.Collection(versions))
		return versions[targetIndex-1].String(), versions[l-1].String()
	}
}

// Package metrics gather anonymous information about the usage of NetBird management
package metrics

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"sort"
	"strings"
	"time"

	"github.com/hashicorp/go-version"
	"github.com/netbirdio/netbird/idp/dex"
	rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
	log "github.com/sirupsen/logrus"

	"github.com/netbirdio/netbird/management/server/types"
	nbversion "github.com/netbirdio/netbird/version"
)

const (
	// PayloadEvent identifies an event type
	PayloadEvent = "self-hosted stats"
	// payloadEndpoint metrics defaultEndpoint to send anonymous data
	payloadEndpoint = "https://metrics.netbird.io"
	// defaultPushInterval default interval to push metrics
	defaultPushInterval = 12 * time.Hour
	// requestTimeout http request timeout
	requestTimeout = 45 * time.Second
	EmbeddedType   = "embedded"
)

type getTokenResponse struct {
	PublicAPIToken string `json:"public_api_token"`
}

type pushPayload struct {
	APIKey     string     `json:"api_key"`
	DistinctID string     `json:"distinct_id"`
	Event      string     `json:"event"`
	Properties properties `json:"properties"`
	Timestamp  time.Time  `json:"timestamp"`
}

// properties metrics to push
type properties map[string]interface{}

// DataSource metric data source
type DataSource interface {
	GetAllAccounts(ctx context.Context) []*types.Account
	GetStoreEngine() types.Engine
	GetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error)
}

// ConnManager peer connection manager that holds state for current active connections
type ConnManager interface {
	GetAllConnectedPeers() map[string]struct{}
}

// Worker metrics collector and pusher
type Worker struct {
	ctx         context.Context
	id          string
	idpManager  string
	dataSource  DataSource
	connManager ConnManager
	startupTime time.Time
	lastRun     time.Time
}

// NewWorker returns a metrics worker
func NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {
	currentTime := time.Now()
	return &Worker{
		ctx:         ctx,
		id:          id,
		idpManager:  idpManager,
		dataSource:  dataSource,
		connManager: connManager,
		startupTime: currentTime,
		lastRun:     currentTime,
	}
}

// Run runs the metrics worker
func (w *Worker) Run(ctx context.Context) {
	interval := getMetricsInterval(ctx)

	pushTicker := time.NewTicker(interval)
	for {
		select {
		case <-w.ctx.Done():
			return
		case <-pushTicker.C:
			err := w.sendMetrics(ctx)
			if err != nil {
				log.WithContext(ctx).Error(err)
			}
			w.lastRun = time.Now()
		}
	}
}

func getMetricsInterval(ctx context.Context) time.Duration {
	interval := defaultPushInterval
	if os.Getenv("NETBIRD_METRICS_INTERVAL_IN_SECONDS") != "" {
		newInterval, err := time.ParseDuration(os.Getenv("NETBIRD_METRICS_INTERVAL_IN_SECONDS") + "s")
		if err != nil {
			log.WithContext(ctx).Errorf("unable to parse NETBIRD_METRICS_INTERVAL_IN_SECONDS, using default interval %v. Error: %v", defaultPushInterval, err)
		} else {
			log.WithContext(ctx).Infof("using NETBIRD_METRICS_INTERVAL_IN_SECONDS %s", newInterval)
			interval = newInterval
		}
	}
	return interval
}

func (w *Worker) sendMetrics(ctx context.Context) error {
	apiKey, err := getAPIKey(w.ctx)
	if err != nil {
		return err
	}

	payload := w.generatePayload(ctx, apiKey)

	payloadString, err := buildMetricsPayload(payload)
	if err != nil {
		return err
	}

	httpClient := http.Client{}

	exportJobReq, cancelCTX, err := createPostRequest(w.ctx, payloadEndpoint+"/capture/", payloadString)
	if err != nil {
		return fmt.Errorf("unable to create metrics post request %v", err)
	}
	defer cancelCTX()

	jobResp, err := httpClient.Do(exportJobReq)
	if err != nil {
		return fmt.Errorf("unable to push metrics %v", err)
	}

	defer func() {
		err = jobResp.Body.Close()
		if err != nil {
			log.WithContext(ctx).Errorf("error while closing update metrics response body: %v", err)
		}
	}()

	if jobResp.StatusCode != 200 {
		return fmt.Errorf("unable to push anonymous metrics, got statusCode %d", jobResp.StatusCode)
	}

	log.WithContext(ctx).Infof("sent anonymous metrics, next push will happen in %s. "+
		"You can disable these metrics by running with flag --disable-anonymous-metrics,"+
		" see more information at https://docs.netbird.io/about-netbird/faq#why-and-what-are-the-anonymous-usage-metrics", getMetricsInterval(ctx))

	return nil
}

func (w *Worker) generatePayload(ctx context.Context, apiKey string) pushPayload {
	properties := w.generateProperties(ctx)

	return pushPayload{
		APIKey:     apiKey,
		DistinctID: w.id,
		Event:      PayloadEvent,
		Properties: properties,
		Timestamp:  time.Now(),
	}
}

func (w *Worker) generateProperties(ctx context.Context) properties {
	var (
		uptime                    float64
		accounts                  int
		expirationEnabled         int
		users                     int
		serviceUsers              int
		pats                      int
		peers                     int
		peersSSHEnabled           int
		setupKeysUsage            int
		ephemeralPeersSKs         int
		ephemeralPeersSKUsage     int
		activePeersLastDay        int
		activeUserPeersLastDay    int
		osPeers                   map[string]int
		activeUsersLastDay        map[string]struct{}
		userPeers                 int
		rules                     int
		rulesProtocol             map[string]int
		rulesDirection            map[string]int
		rulesWithSrcPostureChecks int
		postureChecks             int
		groups                    int
		routes                    int
		routesWithRGGroups        int
		networks                  int
		networkResources          int
		networkRouters            int
		networkRoutersWithPG      int
		nameservers               int
		uiClient                  int
		version                   string
		peerActiveVersions        []string
		osUIClients               map[string]int
		rosenpassEnabled          int
		localUsers                int
		idpUsers                  int
		embeddedIdpTypes          map[string]int
		services                  int
		servicesEnabled           int
		servicesTargets           int
		servicesStatusActive      int
		servicesStatusPending     int
		servicesStatusError       int
		servicesTargetType        map[string]int
		servicesAuthPassword      int
		servicesAuthPin           int
		servicesAuthOIDC          int
	)
	start := time.Now()
	metricsProperties := make(properties)
	osPeers = make(map[string]int)
	osUIClients = make(map[string]int)
	rulesProtocol = make(map[string]int)
	rulesDirection = make(map[string]int)
	activeUsersLastDay = make(map[string]struct{})
	embeddedIdpTypes = make(map[string]int)
	servicesTargetType = make(map[string]int)
	uptime = time.Since(w.startupTime).Seconds()
	connections := w.connManager.GetAllConnectedPeers()
	version = nbversion.NetbirdVersion()

	customDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx)

	for _, account := range w.dataSource.GetAllAccounts(ctx) {
		accounts++

		if account.Settings.PeerLoginExpirationEnabled {
			expirationEnabled++
		}

		groups += len(account.Groups)
		networks += len(account.Networks)
		networkResources += len(account.NetworkResources)

		networkRouters += len(account.NetworkRouters)
		for _, router := range account.NetworkRouters {
			if len(router.PeerGroups) > 0 {
				networkRoutersWithPG++
			}
		}

		routes += len(account.Routes)
		for _, route := range account.Routes {
			if len(route.PeerGroups) > 0 {
				routesWithRGGroups++
			}
		}
		nameservers += len(account.NameServerGroups)

		for _, policy := range account.Policies {
			for _, rule := range policy.Rules {
				rules++
				rulesProtocol[string(rule.Protocol)]++
				if rule.Bidirectional {
					rulesDirection["bidirectional"]++
				} else {
					rulesDirection["oneway"]++
				}
			}
			if len(policy.SourcePostureChecks) > 0 {
				rulesWithSrcPostureChecks++
			}
		}

		postureChecks += len(account.PostureChecks)

		for _, user := range account.Users {
			if user.IsServiceUser {
				serviceUsers++
			} else {
				users++
				if w.idpManager == EmbeddedType {
					_, idpID, err := dex.DecodeDexUserID(user.Id)
					if err == nil {
						if idpID == "local" {
							localUsers++
						} else {
							idpUsers++
						}
						idpType := extractIdpType(idpID)
						embeddedIdpTypes[idpType]++
					}
				}
			}
			pats += len(user.PATs)
		}

		for _, key := range account.SetupKeys {
			setupKeysUsage += key.UsedTimes
			if key.Ephemeral {
				ephemeralPeersSKs++
				ephemeralPeersSKUsage += key.UsedTimes
			}
		}

		for _, peer := range account.Peers {
			peers++

			if peer.SSHEnabled || peer.Meta.Flags.ServerSSHAllowed {
				peersSSHEnabled++
			}

			if peer.Meta.Flags.RosenpassEnabled {
				rosenpassEnabled++
			}

			if peer.UserID != "" {
				userPeers++
			}

			osKey := strings.ToLower(fmt.Sprintf("peer_os_%s", peer.Meta.GoOS))
			osCount := osPeers[osKey]
			osPeers[osKey] = osCount + 1

			if peer.Meta.UIVersion != "" {
				uiClient++
				uiOSKey := strings.ToLower(fmt.Sprintf("ui_client_os_%s", peer.Meta.GoOS))
				osUICount := osUIClients[uiOSKey]
				osUIClients[uiOSKey] = osUICount + 1
			}

			_, connected := connections[peer.ID]
			if connected || peer.Status.LastSeen.After(w.lastRun) {
				activePeersLastDay++
				if peer.UserID != "" {
					activeUserPeersLastDay++
					activeUsersLastDay[peer.UserID] = struct{}{}
				}
				osActiveKey := osKey + "_active"
				osActiveCount := osPeers[osActiveKey]
				osPeers[osActiveKey] = osActiveCount + 1
				peerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)
			}
		}

		for _, service := range account.Services {
			services++
			if service.Enabled {
				servicesEnabled++
			}
			servicesTargets += len(service.Targets)

			switch rpservice.Status(service.Meta.Status) {
			case rpservice.StatusActive:
				servicesStatusActive++
			case rpservice.StatusPending:
				servicesStatusPending++
			case rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated:
				servicesStatusError++
			}

			for _, target := range service.Targets {
				servicesTargetType[target.TargetType]++
			}

			if service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled {
				servicesAuthPassword++
			}
			if service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled {
				servicesAuthPin++
			}
			if service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled {
				servicesAuthOIDC++
			}
		}
	}

	minActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)
	metricsProperties["uptime"] = uptime
	metricsProperties["accounts"] = accounts
	metricsProperties["users"] = users
	metricsProperties["service_users"] = serviceUsers
	metricsProperties["pats"] = pats
	metricsProperties["peers"] = peers
	metricsProperties["peers_ssh_enabled"] = peersSSHEnabled
	metricsProperties["peers_login_expiration_enabled"] = expirationEnabled
	metricsProperties["setup_keys_usage"] = setupKeysUsage
	metricsProperties["ephemeral_peers_setup_keys"] = ephemeralPeersSKs
	metricsProperties["ephemeral_peers_setup_keys_usage"] = ephemeralPeersSKUsage
	metricsProperties["active_peers_last_day"] = activePeersLastDay
	metricsProperties["active_user_peers_last_day"] = activeUserPeersLastDay
	metricsProperties["active_users_last_day"] = len(activeUsersLastDay)
	metricsProperties["user_peers"] = userPeers
	metricsProperties["rules"] = rules
	metricsProperties["rules_with_src_posture_checks"] = rulesWithSrcPostureChecks
	metricsProperties["posture_checks"] = postureChecks
	metricsProperties["groups"] = groups
	metricsProperties["networks"] = networks
	metricsProperties["network_resources"] = networkResources
	metricsProperties["network_routers"] = networkRouters
	metricsProperties["network_routers_with_groups"] = networkRoutersWithPG
	metricsProperties["routes"] = routes
	metricsProperties["routes_with_routing_groups"] = routesWithRGGroups
	metricsProperties["nameservers"] = nameservers
	metricsProperties["version"] = version
	metricsProperties["min_active_peer_version"] = minActivePeerVersion
	metricsProperties["max_active_peer_version"] = maxActivePeerVersion
	metricsProperties["ui_clients"] = uiClient
	metricsProperties["idp_manager"] = w.idpManager
	metricsProperties["store_engine"] = w.dataSource.GetStoreEngine()
	metricsProperties["rosenpass_enabled"] = rosenpassEnabled
	metricsProperties["local_users_count"] = localUsers
	metricsProperties["idp_users_count"] = idpUsers
	metricsProperties["embedded_idp_count"] = len(embeddedIdpTypes)

	metricsProperties["services"] = services
	metricsProperties["services_enabled"] = servicesEnabled
	metricsProperties["services_targets"] = servicesTargets
	metricsProperties["services_status_active"] = servicesStatusActive
	metricsProperties["services_status_pending"] = servicesStatusPending
	metricsProperties["services_status_error"] = servicesStatusError
	metricsProperties["services_auth_password"] = servicesAuthPassword
	metricsProperties["services_auth_pin"] = servicesAuthPin
	metricsProperties["services_auth_oidc"] = servicesAuthOIDC
	metricsProperties["custom_domains"] = customDomains
	metricsProperties["custom_domains_validated"] = customDomainsValidated

	for targetType, count := range servicesTargetType {
		metricsProperties["services_target_type_"+targetType] = count
	}

	for idpType, count := range embeddedIdpTypes {
		metricsProperties["embedded_idp_users_"+idpType] = count
	}

	for protocol, count := range rulesProtocol {
		metricsProperties["rules_protocol_"+protocol] = count
	}

	for direction, count := range rulesDirection {
		metricsProperties["rules_direction_"+direction] = count
	}

	for os, count := range osPeers {
		metricsProperties[os] = count
	}

	for os, count := range osUIClients {
		metricsProperties[os] = count
	}

	metricsProperties["metric_generation_time"] = time.Since(start).Milliseconds()

	return metricsProperties
}

func getAPIKey(ctx context.Context) (string, error) {
	ctx, cancel := context.WithTimeout(ctx, requestTimeout)
	defer cancel()

	httpClient := http.Client{}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+"/GetToken", nil)
	if err != nil {
		return "", fmt.Errorf("unable to create request for metrics public api token %v", err)
	}
	response, err := httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("unable to request metrics public api token %v", err)
	}

	defer func() {
		err = response.Body.Close()
		if err != nil {
			log.WithContext(ctx).Errorf("error while closing metrics token response body: %v", err)
		}
	}()

	if response.StatusCode != 200 {
		return "", fmt.Errorf("unable to retrieve metrics token, statusCode %d", response.StatusCode)
	}

	body, err := io.ReadAll(response.Body)
	if err != nil {
		return "", fmt.Errorf("coudln't get metrics token response; %v", err)
	}

	var tokenResponse getTokenResponse

	err = json.Unmarshal(body, &tokenResponse)
	if err != nil {
		return "", fmt.Errorf("coudln't parse metrics public api token; %v", err)
	}

	return tokenResponse.PublicAPIToken, nil
}

func buildMetricsPayload(payload pushPayload) (string, error) {
	str, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("unable to marshal metrics payload, got err: %v", err)
	}
	return string(str), nil
}

func createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, context.CancelFunc, error) {
	ctx, cancel := context.WithTimeout(ctx, requestTimeout)
	reqURL := endpoint

	payload := strings.NewReader(payloadStr)

	req, err := http.NewRequestWithContext(ctx, "POST", reqURL, payload)
	if err != nil {
		cancel()
		return nil, nil, err
	}
	req.Header.Add("content-type", "application/json")

	return req, cancel, nil
}

// extractIdpType extracts the IdP type from a Dex connector ID.
// Connector IDs are formatted as "<type>-<xid>" (e.g., "okta-abc123", "zitadel-xyz").
// Returns the type prefix, or "oidc" if no known prefix is found.
func extractIdpType(connectorID string) string {
	if connectorID == "" {
		return "unknown"
	}
	idx := strings.LastIndex(connectorID, "-")
	if idx <= 0 {
		return strings.ToLower(connectorID)
	}
	return strings.ToLower(connectorID[:idx])
}

func getMinMaxVersion(inputList []string) (string, string) {
	versions := make([]*version.Version, 0)

	for _, raw := range inputList {
		if raw != "" && nbversion.SemverRegexp.MatchString(raw) {
			v, err := version.NewVersion(raw)
			if err == nil {
				versions = append(versions, v)
			}
		}
	}

	targetIndex := 1
	l := len(versions)

	switch l {
	case 0:
		return "", ""
	case targetIndex:
		v := versions[targetIndex-1].String()
		return v, v
	default:
		sort.Sort(version.Collection(versions))
		return versions[targetIndex-1].String(), versions[l-1].String()
	}
}


Now update the tests:


	if properties["embedded_idp_users_zitadel"] != 1 {
		t.Errorf("expected 1 embedded_idp_users_zitadel, got %v", properties["embedded_idp_users_zitadel"])
	}
	if properties["embedded_idp_count"] != 1 {
		t.Errorf("expected 1 embedded_idp_count, got %v", properties["embedded_idp_count"])
	}

	if properties["embedded_idp_users_local"] != 1 {
		t.Errorf("expected 1 embedded_idp_users_local, got %v", properties["embedded_idp_users_local"])
	}
	if properties["embedded_idp_users_zitadel"] != 1 {
		t.Errorf("expected 1 embedded_idp_users_zitadel, got %v", properties["embedded_idp_users_zitadel"])
	}
	if properties["embedded_idp_count"] != 2 {
		t.Errorf("expected 2 embedded_idp_count, got %v", properties["embedded_idp_count"])
	}


The file /Users/misha/Documents/GolandProjects/netbird/netbird/management/server/metrics/selfhosted_test.go has been updated successfully.

package metrics

import (
	"context"
	"testing"

	nbdns "github.com/netbirdio/netbird/dns"
	"github.com/netbirdio/netbird/idp/dex"
	rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
	resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
	routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
	networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
	nbpeer "github.com/netbirdio/netbird/management/server/peer"
	"github.com/netbirdio/netbird/management/server/posture"
	"github.com/netbirdio/netbird/management/server/types"
	"github.com/netbirdio/netbird/route"
)

type mockDatasource struct{}

// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information
func (mockDatasource) GetAllConnectedPeers() map[string]struct{} {
	return map[string]struct{}{
		"1": {},
	}
}

// GetAllAccounts returns a list of *server.Account for use in tests with predefined information
func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {
	localUserID := dex.EncodeDexUserID("10", "local")
	idpUserID := dex.EncodeDexUserID("20", "zitadel-d5uv82dra0haedlf6kv0")
	return []*types.Account{
		{
			Id:       "1",
			Settings: &types.Settings{PeerLoginExpirationEnabled: true},
			SetupKeys: map[string]*types.SetupKey{
				"1": {
					Id:        "1",
					Ephemeral: true,
					UsedTimes: 1,
				},
			},
			Groups: map[string]*types.Group{
				"1": {},
				"2": {},
			},
			NameServerGroups: map[string]*nbdns.NameServerGroup{
				"1": {},
			},
			Peers: map[string]*nbpeer.Peer{
				"1": {
					ID:         "1",
					UserID:     "test",
					SSHEnabled: false,
					Meta:       nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},
				},
			},
			Policies: []*types.Policy{
				{
					Rules: []*types.PolicyRule{
						{
							Bidirectional: true,
							Protocol:      types.PolicyRuleProtocolTCP,
						},
					},
				},
				{
					Rules: []*types.PolicyRule{
						{
							Bidirectional: false,
							Protocol:      types.PolicyRuleProtocolTCP,
						},
					},
					SourcePostureChecks: []string{"1"},
				},
			},
			Routes: map[route.ID]*route.Route{
				"1": {
					ID:         "1",
					PeerGroups: make([]string, 1),
				},
			},
			PostureChecks: []*posture.Checks{
				{
					ID:   "1",
					Name: "test",
					Checks: posture.ChecksDefinition{
						NBVersionCheck: &posture.NBVersionCheck{
							MinVersion: "0.0.1",
						},
					},
				},
				{
					ID:   "2",
					Name: "tes2",
					Checks: posture.ChecksDefinition{
						NBVersionCheck: &posture.NBVersionCheck{
							MinVersion: "0.0.2",
						},
					},
				},
			},
			Users: map[string]*types.User{
				"1": {
					Id:            "1",
					IsServiceUser: true,
					PATs: map[string]*types.PersonalAccessToken{
						"1": {},
					},
				},
				localUserID: {
					Id:            localUserID,
					IsServiceUser: false,
					PATs: map[string]*types.PersonalAccessToken{
						"1": {},
					},
				},
			},
			Services: []*rpservice.Service{
				{
					ID:      "svc1",
					Enabled: true,
					Targets: []*rpservice.Target{
						{TargetType: "peer"},
						{TargetType: "host"},
					},
					Auth: rpservice.AuthConfig{
						PasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},
					},
					Meta: rpservice.Meta{Status: string(rpservice.StatusActive)},
				},
				{
					ID:      "svc2",
					Enabled: false,
					Targets: []*rpservice.Target{
						{TargetType: "domain"},
					},
					Auth: rpservice.AuthConfig{
						BearerAuth: &rpservice.BearerAuthConfig{Enabled: true},
					},
					Meta: rpservice.Meta{Status: string(rpservice.StatusPending)},
				},
			},
		},
		{
			Id:       "2",
			Settings: &types.Settings{PeerLoginExpirationEnabled: true},
			SetupKeys: map[string]*types.SetupKey{
				"1": {
					Id:        "1",
					Ephemeral: true,
					UsedTimes: 1,
				},
			},
			Groups: map[string]*types.Group{
				"1": {},
				"2": {},
			},
			NameServerGroups: map[string]*nbdns.NameServerGroup{
				"1": {},
			},
			Peers: map[string]*nbpeer.Peer{
				"1": {
					ID:         "1",
					UserID:     "test",
					SSHEnabled: true,
					Meta:       nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"},
				},
			},
			Policies: []*types.Policy{
				{
					Rules: []*types.PolicyRule{
						{
							Bidirectional: true,
							Protocol:      types.PolicyRuleProtocolTCP,
						},
					},
				},
				{
					Rules: []*types.PolicyRule{
						{
							Bidirectional: false,
							Protocol:      types.PolicyRuleProtocolTCP,
						},
					},
				},
			},
			Routes: map[route.ID]*route.Route{
				"1": {
					ID:         "1",
					PeerGroups: make([]string, 1),
				},
			},
			Users: map[string]*types.User{
				"1": {
					Id:            "1",
					IsServiceUser: true,
					PATs: map[string]*types.PersonalAccessToken{
						"1": {},
					},
				},
				idpUserID: {
					Id:            idpUserID,
					IsServiceUser: false,
					PATs: map[string]*types.PersonalAccessToken{
						"1": {},
					},
				},
			},
			Networks: []*networkTypes.Network{
				{
					ID:        "1",
					AccountID: "1",
				},
			},
			NetworkResources: []*resourceTypes.NetworkResource{
				{
					ID:        "1",
					AccountID: "1",
					NetworkID: "1",
				},
				{
					ID:        "2",
					AccountID: "1",
					NetworkID: "1",
				},
			},
			NetworkRouters: []*routerTypes.NetworkRouter{
				{
					ID:        "1",
					AccountID: "1",
					NetworkID: "1",
				},
			},
		},
	}
}

// GetStoreEngine returns FileStoreEngine
func (mockDatasource) GetStoreEngine() types.Engine {
	return types.FileStoreEngine
}

// GetCustomDomainsCounts returns test custom domain counts.
func (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {
	return 3, 2, nil
}

// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties
func TestGenerateProperties(t *testing.T) {
	ds := mockDatasource{}
	worker := Worker{
		dataSource:  ds,
		connManager: ds,
		idpManager:  EmbeddedType,
	}

	properties := worker.generateProperties(context.Background())

	if properties["accounts"] != 2 {
		t.Errorf("expected 2 accounts, got %d", properties["accounts"])
	}
	if properties["peers"] != 2 {
		t.Errorf("expected 2 peers, got %d", properties["peers"])
	}
	if properties["routes"] != 2 {
		t.Errorf("expected 2 routes, got %d", properties["routes"])
	}
	if properties["networks"] != 1 {
		t.Errorf("expected 1 networks, got %d", properties["networks"])
	}
	if properties["network_resources"] != 2 {
		t.Errorf("expected 2 network_resources, got %d", properties["network_resources"])
	}
	if properties["network_routers"] != 1 {
		t.Errorf("expected 1 network_routers, got %d", properties["network_routers"])
	}
	if properties["rules"] != 4 {
		t.Errorf("expected 4 rules, got %d", properties["rules"])
	}
	if properties["users"] != 2 {
		t.Errorf("expected 1 users, got %d", properties["users"])
	}
	if properties["setup_keys_usage"] != 2 {
		t.Errorf("expected 1 setup_keys_usage, got %d", properties["setup_keys_usage"])
	}
	if properties["pats"] != 4 {
		t.Errorf("expected 4 personal_access_tokens, got %d", properties["pats"])
	}
	if properties["peers_ssh_enabled"] != 2 {
		t.Errorf("expected 2 peers_ssh_enabled, got %d", properties["peers_ssh_enabled"])
	}
	if properties["routes_with_routing_groups"] != 2 {
		t.Errorf("expected 2 routes_with_routing_groups, got %d", properties["routes_with_routing_groups"])
	}
	if properties["rules_protocol_tcp"] != 4 {
		t.Errorf("expected 4 rules_protocol_tcp, got %d", properties["rules_protocol_tcp"])
	}
	if properties["rules_direction_oneway"] != 2 {
		t.Errorf("expected 2 rules_direction_oneway, got %d", properties["rules_direction_oneway"])
	}

	if properties["active_peers_last_day"] != 2 {
		t.Errorf("expected 2 active_peers_last_day, got %d", properties["active_peers_last_day"])
	}
	if properties["min_active_peer_version"] != "0.0.1" {
		t.Errorf("expected 0.0.1 min_active_peer_version, got %s", properties["min_active_peer_version"])
	}
	if properties["max_active_peer_version"] != "0.0.1" {
		t.Errorf("expected 0.0.1 max_active_peer_version, got %s", properties["max_active_peer_version"])
	}

	if properties["peers_login_expiration_enabled"] != 2 {
		t.Errorf("expected 2 peers_login_expiration_enabled, got %d", properties["peers_login_expiration_enabled"])
	}

	if properties["service_users"] != 2 {
		t.Errorf("expected 2 service_users, got %d", properties["service_users"])
	}

	if properties["peer_os_linux"] != 2 {
		t.Errorf("expected 2 peer_os_linux, got %d", properties["peer_os_linux"])
	}

	if properties["ephemeral_peers_setup_keys"] != 2 {
		t.Errorf("expected 2 ephemeral_peers_setup_keys, got %d", properties["ephemeral_peers_setup_keys_usage"])
	}

	if properties["ephemeral_peers_setup_keys_usage"] != 2 {
		t.Errorf("expected 2 ephemeral_peers_setup_keys_usage, got %d", properties["ephemeral_peers_setup_keys_usage"])
	}

	if properties["nameservers"] != 2 {
		t.Errorf("expected 2 nameservers, got %d", properties["nameservers"])
	}

	if properties["groups"] != 4 {
		t.Errorf("expected 4 groups, got %d", properties["groups"])
	}

	if properties["user_peers"] != 2 {
		t.Errorf("expected 2 user_peers, got %d", properties["user_peers"])
	}

	if properties["store_engine"] != types.FileStoreEngine {
		t.Errorf("expected JsonFile, got %s", properties["store_engine"])
	}

	if properties["rules_with_src_posture_checks"] != 1 {
		t.Errorf("expected 1 rules_with_src_posture_checks, got %d", properties["rules_with_src_posture_checks"])
	}

	if properties["posture_checks"] != 2 {
		t.Errorf("expected 2 posture_checks, got %d", properties["posture_checks"])
	}

	if properties["rosenpass_enabled"] != 1 {
		t.Errorf("expected 1 rosenpass_enabled, got %d", properties["rosenpass_enabled"])
	}

	if properties["active_user_peers_last_day"] != 2 {
		t.Errorf("expected 2 active_user_peers_last_day, got %d", properties["active_user_peers_last_day"])
	}

	if properties["active_users_last_day"] != 1 {
		t.Errorf("expected 1 active_users_last_day, got %d", properties["active_users_last_day"])
	}

	if properties["local_users_count"] != 1 {
		t.Errorf("expected 1 local_users_count, got %d", properties["local_users_count"])
	}
	if properties["idp_users_count"] != 1 {
		t.Errorf("expected 1 idp_users_count, got %d", properties["idp_users_count"])
	}
	if properties["embedded_idp_users_zitadel"] != 1 {
		t.Errorf("expected 1 embedded_idp_users_zitadel, got %v", properties["embedded_idp_users_zitadel"])
	}
	if properties["embedded_idp_count"] != 1 {
		t.Errorf("expected 1 embedded_idp_count, got %v", properties["embedded_idp_count"])
	}

	if properties["services"] != 2 {
		t.Errorf("expected 2 services, got %v", properties["services"])
	}
	if properties["services_enabled"] != 1 {
		t.Errorf("expected 1 services_enabled, got %v", properties["services_enabled"])
	}
	if properties["services_targets"] != 3 {
		t.Errorf("expected 3 services_targets, got %v", properties["services_targets"])
	}
	if properties["services_status_active"] != 1 {
		t.Errorf("expected 1 services_status_active, got %v", properties["services_status_active"])
	}
	if properties["services_status_pending"] != 1 {
		t.Errorf("expected 1 services_status_pending, got %v", properties["services_status_pending"])
	}
	if properties["services_status_error"] != 0 {
		t.Errorf("expected 0 services_status_error, got %v", properties["services_status_error"])
	}
	if properties["services_target_type_peer"] != 1 {
		t.Errorf("expected 1 services_target_type_peer, got %v", properties["services_target_type_peer"])
	}
	if properties["services_target_type_host"] != 1 {
		t.Errorf("expected 1 services_target_type_host, got %v", properties["services_target_type_host"])
	}
	if properties["services_target_type_domain"] != 1 {
		t.Errorf("expected 1 services_target_type_domain, got %v", properties["services_target_type_domain"])
	}
	if properties["services_auth_password"] != 1 {
		t.Errorf("expected 1 services_auth_password, got %v", properties["services_auth_password"])
	}
	if properties["services_auth_oidc"] != 1 {
		t.Errorf("expected 1 services_auth_oidc, got %v", properties["services_auth_oidc"])
	}
	if properties["services_auth_pin"] != 0 {
		t.Errorf("expected 0 services_auth_pin, got %v", properties["services_auth_pin"])
	}
	if properties["custom_domains"] != int64(3) {
		t.Errorf("expected 3 custom_domains, got %v", properties["custom_domains"])
	}
	if properties["custom_domains_validated"] != int64(2) {
		t.Errorf("expected 2 custom_domains_validated, got %v", properties["custom_domains_validated"])
	}
}

func TestExtractIdpType(t *testing.T) {
	tests := []struct {
		connectorID string
		expected    string
	}{
		{"okta-abc123def", "okta"},
		{"zitadel-d5uv82dra0haedlf6kv0", "zitadel"},
		{"entra-xyz789", "entra"},
		{"google-abc123", "google"},
		{"pocketid-abc123", "pocketid"},
		{"microsoft-abc123", "microsoft"},
		{"authentik-abc123", "authentik"},
		{"keycloak-d5uv82dra0haedlf6kv0", "keycloak"},
		{"local", "oidc"},
		{"", "oidc"},
	}

	for _, tt := range tests {
		t.Run(tt.connectorID, func(t *testing.T) {
			result := extractIdpType(tt.connectorID)
			if result != tt.expected {
				t.Errorf("extractIdpType(%q) = %q, want %q", tt.connectorID, result, tt.expected)
			}
		})
	}
}

package metrics

import (
	"context"
	"testing"

	nbdns "github.com/netbirdio/netbird/dns"
	"github.com/netbirdio/netbird/idp/dex"
	rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
	resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
	routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
	networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
	nbpeer "github.com/netbirdio/netbird/management/server/peer"
	"github.com/netbirdio/netbird/management/server/posture"
	"github.com/netbirdio/netbird/management/server/types"
	"github.com/netbirdio/netbird/route"
)

type mockDatasource struct{}

// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information
func (mockDatasource) GetAllConnectedPeers() map[string]struct{} {
	return map[string]struct{}{
		"1": {},
	}
}

// GetAllAccounts returns a list of *server.Account for use in tests with predefined information
func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {
	localUserID := dex.EncodeDexUserID("10", "local")
	idpUserID := dex.EncodeDexUserID("20", "zitadel-d5uv82dra0haedlf6kv0")
	return []*types.Account{
		{
			Id:       "1",
			Settings: &types.Settings{PeerLoginExpirationEnabled: true},
			SetupKeys: map[string]*types.SetupKey{
				"1": {
					Id:        "1",
					Ephemeral: true,
					UsedTimes: 1,
				},
			},
			Groups: map[string]*types.Group{
				"1": {},
				"2": {},
			},
			NameServerGroups: map[string]*nbdns.NameServerGroup{
				"1": {},
			},
			Peers: map[string]*nbpeer.Peer{
				"1": {
					ID:         "1",
					UserID:     "test",
					SSHEnabled: false,
					Meta:       nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},
				},
			},
			Policies: []*types.Policy{
				{
					Rules: []*types.PolicyRule{
						{
							Bidirectional: true,
							Protocol:      types.PolicyRuleProtocolTCP,
						},
					},
				},
				{
					Rules: []*types.PolicyRule{
						{
							Bidirectional: false,
							Protocol:      types.PolicyRuleProtocolTCP,
						},
					},
					SourcePostureChecks: []string{"1"},
				},
			},
			Routes: map[route.ID]*route.Route{
				"1": {
					ID:         "1",
					PeerGroups: make([]string, 1),
				},
			},
			PostureChecks: []*posture.Checks{
				{
					ID:   "1",
					Name: "test",
					Checks: posture.ChecksDefinition{
						NBVersionCheck: &posture.NBVersionCheck{
							MinVersion: "0.0.1",
						},
					},
				},
				{
					ID:   "2",
					Name: "tes2",
					Checks: posture.ChecksDefinition{
						NBVersionCheck: &posture.NBVersionCheck{
							MinVersion: "0.0.2",
						},
					},
				},
			},
			Users: map[string]*types.User{
				"1": {
					Id:            "1",
					IsServiceUser: true,
					PATs: map[string]*types.PersonalAccessToken{
						"1": {},
					},
				},
				localUserID: {
					Id:            localUserID,
					IsServiceUser: false,
					PATs: map[string]*types.PersonalAccessToken{
						"1": {},
					},
				},
			},
			Services: []*rpservice.Service{
				{
					ID:      "svc1",
					Enabled: true,
					Targets: []*rpservice.Target{
						{TargetType: "peer"},
						{TargetType: "host"},
					},
					Auth: rpservice.AuthConfig{
						PasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true},
					},
					Meta: rpservice.Meta{Status: string(rpservice.StatusActive)},
				},
				{
					ID:      "svc2",
					Enabled: false,
					Targets: []*rpservice.Target{
						{TargetType: "domain"},
					},
					Auth: rpservice.AuthConfig{
						BearerAuth: &rpservice.BearerAuthConfig{Enabled: true},
					},
					Meta: rpservice.Meta{Status: string(rpservice.StatusPending)},
				},
			},
		},
		{
			Id:       "2",
			Settings: &types.Settings{PeerLoginExpirationEnabled: true},
			SetupKeys: map[string]*types.SetupKey{
				"1": {
					Id:        "1",
					Ephemeral: true,
					UsedTimes: 1,
				},
			},
			Groups: map[string]*types.Group{
				"1": {},
				"2": {},
			},
			NameServerGroups: map[string]*nbdns.NameServerGroup{
				"1": {},
			},
			Peers: map[string]*nbpeer.Peer{
				"1": {
					ID:         "1",
					UserID:     "test",
					SSHEnabled: true,
					Meta:       nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"},
				},
			},
			Policies: []*types.Policy{
				{
					Rules: []*types.PolicyRule{
						{
							Bidirectional: true,
							Protocol:      types.PolicyRuleProtocolTCP,
						},
					},
				},
				{
					Rules: []*types.PolicyRule{
						{
							Bidirectional: false,
							Protocol:      types.PolicyRuleProtocolTCP,
						},
					},
				},
			},
			Routes: map[route.ID]*route.Route{
				"1": {
					ID:         "1",
					PeerGroups: make([]string, 1),
				},
			},
			Users: map[string]*types.User{
				"1": {
					Id:            "1",
					IsServiceUser: true,
					PATs: map[string]*types.PersonalAccessToken{
						"1": {},
					},
				},
				idpUserID: {
					Id:            idpUserID,
					IsServiceUser: false,
					PATs: map[string]*types.PersonalAccessToken{
						"1": {},
					},
				},
			},
			Networks: []*networkTypes.Network{
				{
					ID:        "1",
					AccountID: "1",
				},
			},
			NetworkResources: []*resourceTypes.NetworkResource{
				{
					ID:        "1",
					AccountID: "1",
					NetworkID: "1",
				},
				{
					ID:        "2",
					AccountID: "1",
					NetworkID: "1",
				},
			},
			NetworkRouters: []*routerTypes.NetworkRouter{
				{
					ID:        "1",
					AccountID: "1",
					NetworkID: "1",
				},
			},
		},
	}
}

// GetStoreEngine returns FileStoreEngine
func (mockDatasource) GetStoreEngine() types.Engine {
	return types.FileStoreEngine
}

// GetCustomDomainsCounts returns test custom domain counts.
func (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) {
	return 3, 2, nil
}

// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties
func TestGenerateProperties(t *testing.T) {
	ds := mockDatasource{}
	worker := Worker{
		dataSource:  ds,
		connManager: ds,
		idpManager:  EmbeddedType,
	}

	properties := worker.generateProperties(context.Background())

	if properties["accounts"] != 2 {
		t.Errorf("expected 2 accounts, got %d", properties["accounts"])
	}
	if properties["peers"] != 2 {
		t.Errorf("expected 2 peers, got %d", properties["peers"])
	}
	if properties["routes"] != 2 {
		t.Errorf("expected 2 routes, got %d", properties["routes"])
	}
	if properties["networks"] != 1 {
		t.Errorf("expected 1 networks, got %d", properties["networks"])
	}
	if properties["network_resources"] != 2 {
		t.Errorf("expected 2 network_resources, got %d", properties["network_resources"])
	}
	if properties["network_routers"] != 1 {
		t.Errorf("expected 1 network_routers, got %d", properties["network_routers"])
	}
	if properties["rules"] != 4 {
		t.Errorf("expected 4 rules, got %d", properties["rules"])
	}
	if properties["users"] != 2 {
		t.Errorf("expected 1 users, got %d", properties["users"])
	}
	if properties["setup_keys_usage"] != 2 {
		t.Errorf("expected 1 setup_keys_usage, got %d", properties["setup_keys_usage"])
	}
	if properties["pats"] != 4 {
		t.Errorf("expected 4 personal_access_tokens, got %d", properties["pats"])
	}
	if properties["peers_ssh_enabled"] != 2 {
		t.Errorf("expected 2 peers_ssh_enabled, got %d", properties["peers_ssh_enabled"])
	}
	if properties["routes_with_routing_groups"] != 2 {
		t.Errorf("expected 2 routes_with_routing_groups, got %d", properties["routes_with_routing_groups"])
	}
	if properties["rules_protocol_tcp"] != 4 {
		t.Errorf("expected 4 rules_protocol_tcp, got %d", properties["rules_protocol_tcp"])
	}
	if properties["rules_direction_oneway"] != 2 {
		t.Errorf("expected 2 rules_direction_oneway, got %d", properties["rules_direction_oneway"])
	}

	if properties["active_peers_last_day"] != 2 {
		t.Errorf("expected 2 active_peers_last_day, got %d", properties["active_peers_last_day"])
	}
	if properties["min_active_peer_version"] != "0.0.1" {
		t.Errorf("expected 0.0.1 min_active_peer_version, got %s", properties["min_active_peer_version"])
	}
	if properties["max_active_peer_version"] != "0.0.1" {
		t.Errorf("expected 0.0.1 max_active_peer_version, got %s", properties["max_active_peer_version"])
	}

	if properties["peers_login_expiration_enabled"] != 2 {
		t.Errorf("expected 2 peers_login_expiration_enabled, got %d", properties["peers_login_expiration_enabled"])
	}

	if properties["service_users"] != 2 {
		t.Errorf("expected 2 service_users, got %d", properties["service_users"])
	}

	if properties["peer_os_linux"] != 2 {
		t.Errorf("expected 2 peer_os_linux, got %d", properties["peer_os_linux"])
	}

	if properties["ephemeral_peers_setup_keys"] != 2 {
		t.Errorf("expected 2 ephemeral_peers_setup_keys, got %d", properties["ephemeral_peers_setup_keys_usage"])
	}

	if properties["ephemeral_peers_setup_keys_usage"] != 2 {
		t.Errorf("expected 2 ephemeral_peers_setup_keys_usage, got %d", properties["ephemeral_peers_setup_keys_usage"])
	}

	if properties["nameservers"] != 2 {
		t.Errorf("expected 2 nameservers, got %d", properties["nameservers"])
	}

	if properties["groups"] != 4 {
		t.Errorf("expected 4 groups, got %d", properties["groups"])
	}

	if properties["user_peers"] != 2 {
		t.Errorf("expected 2 user_peers, got %d", properties["user_peers"])
	}

	if properties["store_engine"] != types.FileStoreEngine {
		t.Errorf("expected JsonFile, got %s", properties["store_engine"])
	}

	if properties["rules_with_src_posture_checks"] != 1 {
		t.Errorf("expected 1 rules_with_src_posture_checks, got %d", properties["rules_with_src_posture_checks"])
	}

	if properties["posture_checks"] != 2 {
		t.Errorf("expected 2 posture_checks, got %d", properties["posture_checks"])
	}

	if properties["rosenpass_enabled"] != 1 {
		t.Errorf("expected 1 rosenpass_enabled, got %d", properties["rosenpass_enabled"])
	}

	if properties["active_user_peers_last_day"] != 2 {
		t.Errorf("expected 2 active_user_peers_last_day, got %d", properties["active_user_peers_last_day"])
	}

	if properties["active_users_last_day"] != 1 {
		t.Errorf("expected 1 active_users_last_day, got %d", properties["active_users_last_day"])
	}

	if properties["local_users_count"] != 1 {
		t.Errorf("expected 1 local_users_count, got %d", properties["local_users_count"])
	}
	if properties["idp_users_count"] != 1 {
		t.Errorf("expected 1 idp_users_count, got %d", properties["idp_users_count"])
	}
	if properties["embedded_idp_users_local"] != 1 {
		t.Errorf("expected 1 embedded_idp_users_local, got %v", properties["embedded_idp_users_local"])
	}
	if properties["embedded_idp_users_zitadel"] != 1 {
		t.Errorf("expected 1 embedded_idp_users_zitadel, got %v", properties["embedded_idp_users_zitadel"])
	}
	if properties["embedded_idp_count"] != 2 {
		t.Errorf("expected 2 embedded_idp_count, got %v", properties["embedded_idp_count"])
	}

	if properties["services"] != 2 {
		t.Errorf("expected 2 services, got %v", properties["services"])
	}
	if properties["services_enabled"] != 1 {
		t.Errorf("expected 1 services_enabled, got %v", properties["services_enabled"])
	}
	if properties["services_targets"] != 3 {
		t.Errorf("expected 3 services_targets, got %v", properties["services_targets"])
	}
	if properties["services_status_active"] != 1 {
		t.Errorf("expected 1 services_status_active, got %v", properties["services_status_active"])
	}
	if properties["services_status_pending"] != 1 {
		t.Errorf("expected 1 services_status_pending, got %v", properties["services_status_pending"])
	}
	if properties["services_status_error"] != 0 {
		t.Errorf("expected 0 services_status_error, got %v", properties["services_status_error"])
	}
	if properties["services_target_type_peer"] != 1 {
		t.Errorf("expected 1 services_target_type_peer, got %v", properties["services_target_type_peer"])
	}
	if properties["services_target_type_host"] != 1 {
		t.Errorf("expected 1 services_target_type_host, got %v", properties["services_target_type_host"])
	}
	if properties["services_target_type_domain"] != 1 {
		t.Errorf("expected 1 services_target_type_domain, got %v", properties["services_target_type_domain"])
	}
	if properties["services_auth_password"] != 1 {
		t.Errorf("expected 1 services_auth_password, got %v", properties["services_auth_password"])
	}
	if properties["services_auth_oidc"] != 1 {
		t.Errorf("expected 1 services_auth_oidc, got %v", properties["services_auth_oidc"])
	}
	if properties["services_auth_pin"] != 0 {
		t.Errorf("expected 0 services_auth_pin, got %v", properties["services_auth_pin"])
	}
	if properties["custom_domains"] != int64(3) {
		t.Errorf("expected 3 custom_domains, got %v", properties["custom_domains"])
	}
	if properties["custom_domains_validated"] != int64(2) {
		t.Errorf("expected 2 custom_domains_validated, got %v", properties["custom_domains_validated"])
	}
}

func TestExtractIdpType(t *testing.T) {
	tests := []struct {
		connectorID string
		expected    string
	}{
		{"okta-abc123def", "okta"},
		{"zitadel-d5uv82dra0haedlf6kv0", "zitadel"},
		{"entra-xyz789", "entra"},
		{"google-abc123", "google"},
		{"pocketid-abc123", "pocketid"},
		{"microsoft-abc123", "microsoft"},
		{"authentik-abc123", "authentik"},
		{"keycloak-d5uv82dra0haedlf6kv0", "keycloak"},
		{"local", "oidc"},
		{"", "oidc"},
	}

	for _, tt := range tests {
		t.Run(tt.connectorID, func(t *testing.T) {
			result := extractIdpType(tt.connectorID)
			if result != tt.expected {
				t.Errorf("extractIdpType(%q) = %q, want %q", tt.connectorID, result, tt.expected)
			}
		})
	}
}

+  if properties["embedded_idp_users_local"] != 1 {

+    t.Errorf("expected 1 embedded_idp_users_local, got %v", properties["embedded_idp_users_local"])

-  if properties["embedded_idp_count"] != 1 {

-    t.Errorf("expected 1 embedded_idp_count, got %v", properties["embedded_idp_count"])

+  if properties["embedded_idp_count"] != 2 {

+    t.Errorf("expected 2 embedded_idp_count, got %v", properties["embedded_idp_count"])


		{"local", "local"},
		{"", "unknown"},