     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→	resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
    10→	routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
    11→	networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
    12→	nbpeer "github.com/netbirdio/netbird/management/server/peer"
    13→	"github.com/netbirdio/netbird/management/server/posture"
    14→	"github.com/netbirdio/netbird/management/server/types"
    15→	"github.com/netbirdio/netbird/route"
    16→)
    17→
    18→type mockDatasource struct{}
    19→
    20→// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information
    21→func (mockDatasource) GetAllConnectedPeers() map[string]struct{} {
    22→	return map[string]struct{}{
    23→		"1": {},
    24→	}
    25→}
    26→
    27→// GetAllAccounts returns a list of *server.Account for use in tests with predefined information
    28→func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {
    29→	localUserID := dex.EncodeDexUserID("10", "local")
    30→	idpUserID := dex.EncodeDexUserID("20", "zitadel")
    31→	return []*types.Account{
    32→		{
    33→			Id:       "1",
    34→			Settings: &types.Settings{PeerLoginExpirationEnabled: true},
    35→			SetupKeys: map[string]*types.SetupKey{
    36→				"1": {
    37→					Id:        "1",
    38→					Ephemeral: true,
    39→					UsedTimes: 1,
    40→				},
    41→			},
    42→			Groups: map[string]*types.Group{
    43→				"1": {},
    44→				"2": {},
    45→			},
    46→			NameServerGroups: map[string]*nbdns.NameServerGroup{
    47→				"1": {},
    48→			},
    49→			Peers: map[string]*nbpeer.Peer{
    50→				"1": {
    51→					ID:         "1",
    52→					UserID:     "test",
    53→					SSHEnabled: false,
    54→					Meta:       nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1", Flags: nbpeer.Flags{ServerSSHAllowed: true, RosenpassEnabled: true}},
    55→				},
    56→			},
    57→			Policies: []*types.Policy{
    58→				{
    59→					Rules: []*types.PolicyRule{
    60→						{
    61→							Bidirectional: true,
    62→							Protocol:      types.PolicyRuleProtocolTCP,
    63→						},
    64→					},
    65→				},
    66→				{
    67→					Rules: []*types.PolicyRule{
    68→						{
    69→							Bidirectional: false,
    70→							Protocol:      types.PolicyRuleProtocolTCP,
    71→						},
    72→					},
    73→					SourcePostureChecks: []string{"1"},
    74→				},
    75→			},
    76→			Routes: map[route.ID]*route.Route{
    77→				"1": {
    78→					ID:         "1",
    79→					PeerGroups: make([]string, 1),
    80→				},
    81→			},
    82→			PostureChecks: []*posture.Checks{
    83→				{
    84→					ID:   "1",
    85→					Name: "test",
    86→					Checks: posture.ChecksDefinition{
    87→						NBVersionCheck: &posture.NBVersionCheck{
    88→							MinVersion: "0.0.1",
    89→						},
    90→					},
    91→				},
    92→				{
    93→					ID:   "2",
    94→					Name: "tes2",
    95→					Checks: posture.ChecksDefinition{
    96→						NBVersionCheck: &posture.NBVersionCheck{
    97→							MinVersion: "0.0.2",
    98→						},
    99→					},
   100→				},
   101→			},
   102→			Users: map[string]*types.User{
   103→				"1": {
   104→					Id:            "1",
   105→					IsServiceUser: true,
   106→					PATs: map[string]*types.PersonalAccessToken{
   107→						"1": {},
   108→					},
   109→				},
   110→				localUserID: {
   111→					Id:            localUserID,
   112→					IsServiceUser: false,
   113→					PATs: map[string]*types.PersonalAccessToken{
   114→						"1": {},
   115→					},
   116→				},
   117→			},
   118→		},
   119→		{
   120→			Id:       "2",
   121→			Settings: &types.Settings{PeerLoginExpirationEnabled: true},
   122→			SetupKeys: map[string]*types.SetupKey{
   123→				"1": {
   124→					Id:        "1",
   125→					Ephemeral: true,
   126→					UsedTimes: 1,
   127→				},
   128→			},
   129→			Groups: map[string]*types.Group{
   130→				"1": {},
   131→				"2": {},
   132→			},
   133→			NameServerGroups: map[string]*nbdns.NameServerGroup{
   134→				"1": {},
   135→			},
   136→			Peers: map[string]*nbpeer.Peer{
   137→				"1": {
   138→					ID:         "1",
   139→					UserID:     "test",
   140→					SSHEnabled: true,
   141→					Meta:       nbpeer.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"},
   142→				},
   143→			},
   144→			Policies: []*types.Policy{
   145→				{
   146→					Rules: []*types.PolicyRule{
   147→						{
   148→							Bidirectional: true,
   149→							Protocol:      types.PolicyRuleProtocolTCP,
   150→						},
   151→					},
   152→				},
   153→				{
   154→					Rules: []*types.PolicyRule{
   155→						{
   156→							Bidirectional: false,
   157→							Protocol:      types.PolicyRuleProtocolTCP,
   158→						},
   159→					},
   160→				},
   161→			},
   162→			Routes: map[route.ID]*route.Route{
   163→				"1": {
   164→					ID:         "1",
   165→					PeerGroups: make([]string, 1),
   166→				},
   167→			},
   168→			Users: map[string]*types.User{
   169→				"1": {
   170→					Id:            "1",
   171→					IsServiceUser: true,
   172→					PATs: map[string]*types.PersonalAccessToken{
   173→						"1": {},
   174→					},
   175→				},
   176→				idpUserID: {
   177→					Id:            idpUserID,
   178→					IsServiceUser: false,
   179→					PATs: map[string]*types.PersonalAccessToken{
   180→						"1": {},
   181→					},
   182→				},
   183→			},
   184→			Networks: []*networkTypes.Network{
   185→				{
   186→					ID:        "1",
   187→					AccountID: "1",
   188→				},
   189→			},
   190→			NetworkResources: []*resourceTypes.NetworkResource{
   191→				{
   192→					ID:        "1",
   193→					AccountID: "1",
   194→					NetworkID: "1",
   195→				},
   196→				{
   197→					ID:        "2",
   198→					AccountID: "1",
   199→					NetworkID: "1",
   200→				},
   201→			},
   202→			NetworkRouters: []*routerTypes.NetworkRouter{
   203→				{
   204→					ID:        "1",
   205→					AccountID: "1",
   206→					NetworkID: "1",
   207→				},
   208→			},
   209→		},
   210→	}
   211→}
   212→
   213→// GetStoreEngine returns FileStoreEngine
   214→func (mockDatasource) GetStoreEngine() types.Engine {
   215→	return types.FileStoreEngine
   216→}
   217→
   218→// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties
   219→func TestGenerateProperties(t *testing.T) {
   220→	ds := mockDatasource{}
   221→	worker := Worker{
   222→		dataSource:  ds,
   223→		connManager: ds,
   224→		idpManager:  EmbeddedType,
   225→	}
   226→
   227→	properties := worker.generateProperties(context.Background())
   228→
   229→	if properties["accounts"] != 2 {
   230→		t.Errorf("expected 2 accounts, got %d", properties["accounts"])
   231→	}
   232→	if properties["peers"] != 2 {
   233→		t.Errorf("expected 2 peers, got %d", properties["peers"])
   234→	}
   235→	if properties["routes"] != 2 {
   236→		t.Errorf("expected 2 routes, got %d", properties["routes"])
   237→	}
   238→	if properties["networks"] != 1 {
   239→		t.Errorf("expected 1 networks, got %d", properties["networks"])
   240→	}
   241→	if properties["network_resources"] != 2 {
   242→		t.Errorf("expected 2 network_resources, got %d", properties["network_resources"])
   243→	}
   244→	if properties["network_routers"] != 1 {
   245→		t.Errorf("expected 1 network_routers, got %d", properties["network_routers"])
   246→	}
   247→	if properties["rules"] != 4 {
   248→		t.Errorf("expected 4 rules, got %d", properties["rules"])
   249→	}
   250→	if properties["users"] != 2 {
   251→		t.Errorf("expected 1 users, got %d", properties["users"])
   252→	}
   253→	if properties["setup_keys_usage"] != 2 {
   254→		t.Errorf("expected 1 setup_keys_usage, got %d", properties["setup_keys_usage"])
   255→	}
   256→	if properties["pats"] != 4 {
   257→		t.Errorf("expected 4 personal_access_tokens, got %d", properties["pats"])
   258→	}
   259→	if properties["peers_ssh_enabled"] != 2 {
   260→		t.Errorf("expected 2 peers_ssh_enabled, got %d", properties["peers_ssh_enabled"])
   261→	}
   262→	if properties["routes_with_routing_groups"] != 2 {
   263→		t.Errorf("expected 2 routes_with_routing_groups, got %d", properties["routes_with_routing_groups"])
   264→	}
   265→	if properties["rules_protocol_tcp"] != 4 {
   266→		t.Errorf("expected 4 rules_protocol_tcp, got %d", properties["rules_protocol_tcp"])
   267→	}
   268→	if properties["rules_direction_oneway"] != 2 {
   269→		t.Errorf("expected 2 rules_direction_oneway, got %d", properties["rules_direction_oneway"])
   270→	}
   271→
   272→	if properties["active_peers_last_day"] != 2 {
   273→		t.Errorf("expected 2 active_peers_last_day, got %d", properties["active_peers_last_day"])
   274→	}
   275→	if properties["min_active_peer_version"] != "0.0.1" {
   276→		t.Errorf("expected 0.0.1 min_active_peer_version, got %s", properties["min_active_peer_version"])
   277→	}
   278→	if properties["max_active_peer_version"] != "0.0.1" {
   279→		t.Errorf("expected 0.0.1 max_active_peer_version, got %s", properties["max_active_peer_version"])
   280→	}
   281→
   282→	if properties["peers_login_expiration_enabled"] != 2 {
   283→		t.Errorf("expected 2 peers_login_expiration_enabled, got %d", properties["peers_login_expiration_enabled"])
   284→	}
   285→
   286→	if properties["service_users"] != 2 {
   287→		t.Errorf("expected 2 service_users, got %d", properties["service_users"])
   288→	}
   289→
   290→	if properties["peer_os_linux"] != 2 {
   291→		t.Errorf("expected 2 peer_os_linux, got %d", properties["peer_os_linux"])
   292→	}
   293→
   294→	if properties["ephemeral_peers_setup_keys"] != 2 {
   295→		t.Errorf("expected 2 ephemeral_peers_setup_keys, got %d", properties["ephemeral_peers_setup_keys_usage"])
   296→	}
   297→
   298→	if properties["ephemeral_peers_setup_keys_usage"] != 2 {
   299→		t.Errorf("expected 2 ephemeral_peers_setup_keys_usage, got %d", properties["ephemeral_peers_setup_keys_usage"])
   300→	}
   301→
   302→	if properties["nameservers"] != 2 {
   303→		t.Errorf("expected 2 nameservers, got %d", properties["nameservers"])
   304→	}
   305→
   306→	if properties["groups"] != 4 {
   307→		t.Errorf("expected 4 groups, got %d", properties["groups"])
   308→	}
   309→
   310→	if properties["user_peers"] != 2 {
   311→		t.Errorf("expected 2 user_peers, got %d", properties["user_peers"])
   312→	}
   313→
   314→	if properties["store_engine"] != types.FileStoreEngine {
   315→		t.Errorf("expected JsonFile, got %s", properties["store_engine"])
   316→	}
   317→
   318→	if properties["rules_with_src_posture_checks"] != 1 {
   319→		t.Errorf("expected 1 rules_with_src_posture_checks, got %d", properties["rules_with_src_posture_checks"])
   320→	}
   321→
   322→	if properties["posture_checks"] != 2 {
   323→		t.Errorf("expected 2 posture_checks, got %d", properties["posture_checks"])
   324→	}
   325→
   326→	if properties["rosenpass_enabled"] != 1 {
   327→		t.Errorf("expected 1 rosenpass_enabled, got %d", properties["rosenpass_enabled"])
   328→	}
   329→
   330→	if properties["active_user_peers_last_day"] != 2 {
   331→		t.Errorf("expected 2 active_user_peers_last_day, got %d", properties["active_user_peers_last_day"])
   332→	}
   333→
   334→	if properties["active_users_last_day"] != 1 {
   335→		t.Errorf("expected 1 active_users_last_day, got %d", properties["active_users_last_day"])
   336→	}
   337→
   338→	if properties["local_users_count"] != 1 {
   339→		t.Errorf("expected 1 local_users_count, got %d", properties["local_users_count"])
   340→	}
   341→	if properties["idp_users_count"] != 1 {
   342→		t.Errorf("expected 1 idp_users_count, got %d", properties["idp_users_count"])
   343→	}
   344→}
   345→

package metrics

import (
	"context"
	"testing"

	nbdns "github.com/netbirdio/netbird/dns"
	"github.com/netbirdio/netbird/idp/dex"
	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")
	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": {},
					},
				},
			},
		},
		{
			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
}

// 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"])
	}
}


The test data already has a `"zitadel"` connector user (line 30). I need to:
1. Add assertions for the new `embedded_idp_users_*` metric
2. Add a test for `extractIdpType`


Edit

	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["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"])
	}
}

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 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"
	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")
	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": {},
					},
				},
			},
		},
		{
			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
}

// 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"])
	}
}

package metrics

import (
	"context"
	"testing"

	nbdns "github.com/netbirdio/netbird/dns"
	"github.com/netbirdio/netbird/idp/dex"
	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")
	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": {},
					},
				},
			},
		},
		{
			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
}

// 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"])
	}
}

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_zitadel"] != 1 {

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

+func TestExtractIdpType(t *testing.T) {

+    {"zitadel-d5uv82dra0haedlf6kv0", "zitadel"},

+    {"keycloak-d5uv82dra0haedlf6kv0", "keycloak"},

+    t.Run(tt.connectorID, func(t *testing.T) {

+      result := extractIdpType(tt.connectorID)

+        t.Errorf("extractIdpType(%q) = %q, want %q", tt.connectorID, result, tt.expected)


Bash

go test ./management/server/metrics/ -v -run "TestGenerateProperties|TestExtractIdpType" 2>&1

Run the metrics tests


Run the metrics tests


Output:
    --- PASS: TestExtractIdpType/#00 (0.00s)
FAIL
FAIL	github.com/netbirdio/netbird/management/server/metrics	2.342s
FAIL

=== RUN   TestGenerateProperties
    selfhosted_test.go:345: expected 1 embedded_idp_users_zitadel, got <nil>
--- FAIL: TestGenerateProperties (0.00s)
=== RUN   TestExtractIdpType
=== RUN   TestExtractIdpType/okta-abc123def
=== RUN   REDACTED
=== RUN   TestExtractIdpType/entra-xyz789
=== RUN   TestExtractIdpType/google-abc123
=== RUN   TestExtractIdpType/pocketid-abc123
=== RUN   TestExtractIdpType/microsoft-abc123
=== RUN   TestExtractIdpType/authentik-abc123
=== RUN   REDACTED
=== RUN   TestExtractIdpType/local
=== RUN   TestExtractIdpType/#00
--- PASS: TestExtractIdpType (0.00s)
    --- PASS: TestExtractIdpType/okta-abc123def (0.00s)
    --- PASS: REDACTED (0.00s)
    --- PASS: TestExtractIdpType/entra-xyz789 (0.00s)
    --- PASS: TestExtractIdpType/google-abc123 (0.00s)
    --- PASS: TestExtractIdpType/pocketid-abc123 (0.00s)
    --- PASS: TestExtractIdpType/microsoft-abc123 (0.00s)
    --- PASS: TestExtractIdpType/authentik-abc123 (0.00s)
    --- PASS: REDACTED (0.00s)
    --- PASS: TestExtractIdpType/local (0.00s)
    --- PASS: TestExtractIdpType/#00 (0.00s)
FAIL
FAIL	github.com/netbirdio/netbird/management/server/metrics	2.342s
FAIL


Error: Exit code 1
=== RUN   TestGenerateProperties
    selfhosted_test.go:345: expected 1 embedded_idp_users_zitadel, got <nil>
--- FAIL: TestGenerateProperties (0.00s)
=== RUN   TestExtractIdpType
=== RUN   TestExtractIdpType/okta-abc123def
=== RUN   REDACTED
=== RUN   TestExtractIdpType/entra-xyz789
=== RUN   TestExtractIdpType/google-abc123
=== RUN   TestExtractIdpType/pocketid-abc123
=== RUN   TestExtractIdpType/microsoft-abc123
=== RUN   TestExtractIdpType/authentik-abc123
=== RUN   REDACTED
=== RUN   TestExtractIdpType/local
=== RUN   TestExtractIdpType/#00
--- PASS: TestExtractIdpType (0.00s)
    --- PASS: TestExtractIdpType/okta-abc123def (0.00s)
    --- PASS: REDACTED (0.00s)
    --- PASS: TestExtractIdpType/entra-xyz789 (0.00s)
    --- PASS: TestExtractIdpType/google-abc123 (0.00s)
    --- PASS: TestExtractIdpType/pocketid-abc123 (0.00s)
    --- PASS: TestExtractIdpType/microsoft-abc123 (0.00s)
    --- PASS: TestExtractIdpType/authentik-abc123 (0.00s)
    --- PASS: REDACTED (0.00s)
    --- PASS: TestExtractIdpType/local (0.00s)
    --- PASS: TestExtractIdpType/#00 (0.00s)
FAIL
FAIL	github.com/netbirdio/netbird/management/server/metrics	2.342s
FAIL

=== RUN   TestGenerateProperties
    selfhosted_test.go:345: expected 1 embedded_idp_users_zitadel, got <nil>
--- FAIL: TestGenerateProperties (0.00s)
=== RUN   TestExtractIdpType
=== RUN   TestExtractIdpType/okta-abc123def
=== RUN   REDACTED
=== RUN   TestExtractIdpType/entra-xyz789
=== RUN   TestExtractIdpType/google-abc123
=== RUN   TestExtractIdpType/pocketid-abc123
=== RUN   TestExtractIdpType/microsoft-abc123
=== RUN   TestExtractIdpType/authentik-abc123
=== RUN   REDACTED
=== RUN   TestExtractIdpType/local
=== RUN   TestExtractIdpType/#00
--- PASS: TestExtractIdpType (0.00s)
    --- PASS: TestExtractIdpType/okta-abc123def (0.00s)
    --- PASS: REDACTED (0.00s)
    --- PASS: TestExtractIdpType/entra-xyz789 (0.00s)
    --- PASS: TestExtractIdpType/google-abc123 (0.00s)
    --- PASS: TestExtractIdpType/pocketid-abc123 (0.00s)
    --- PASS: TestExtractIdpType/microsoft-abc123 (0.00s)
    --- PASS: TestExtractIdpType/authentik-abc123 (0.00s)
    --- PASS: REDACTED (0.00s)
    --- PASS: TestExtractIdpType/local (0.00s)
    --- PASS: TestExtractIdpType/#00 (0.00s)
FAIL
FAIL	github.com/netbirdio/netbird/management/server/metrics	2.342s
FAIL


`TestExtractIdpType` passes. The `TestGenerateProperties` fails because the mock data uses `"zitadel"` as the raw connector ID (not the real format like `"zitadel-abc123"`). Let me check what the mock produces:

The mock at line 30 does `dex.EncodeDexUserID("20", "zitadel")` — the connector ID is just `"zitadel"` (no `-xid` suffix). So `extractIdpType("zitadel")` returns `"oidc"` since there's no `-` separator. I need to update the mock to use a realistic connector ID.


Edit

idpUserID := dex.EncodeDexUserID("20", "zitadel")

idpUserID := dex.EncodeDexUserID("20", "zitadel-d5uv82dra0haedlf6kv0")


package metrics

import (
	"context"
	"testing"

	nbdns "github.com/netbirdio/netbird/dns"
	"github.com/netbirdio/netbird/idp/dex"
	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")
	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": {},
					},
				},
			},
		},
		{
			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
}

// 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"])
	}
}

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"
	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": {},
					},
				},
			},
		},
		{
			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
}

// 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"])
	}
}

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)
			}
		})
	}
}

-  idpUserID := dex.EncodeDexUserID("20", "zitadel")

+  idpUserID := dex.EncodeDexUserID("20", "zitadel-d5uv82dra0haedlf6kv0")


Bash

go test ./management/server/metrics/ -v -run "TestGenerateProperties|TestExtractIdpType" 2>&1

Run the metrics tests again


Run the metrics tests again


Output:
    --- PASS: TestExtractIdpType/local (0.00s)
    --- PASS: TestExtractIdpType/#00 (0.00s)
PASS
ok  	github.com/netbirdio/netbird/management/server/metrics	2.392s

=== RUN   TestGenerateProperties
--- PASS: TestGenerateProperties (0.00s)
=== RUN   TestExtractIdpType
=== RUN   TestExtractIdpType/okta-abc123def
=== RUN   REDACTED
=== RUN   TestExtractIdpType/entra-xyz789
=== RUN   TestExtractIdpType/google-abc123
=== RUN   TestExtractIdpType/pocketid-abc123
=== RUN   TestExtractIdpType/microsoft-abc123
=== RUN   TestExtractIdpType/authentik-abc123
=== RUN   REDACTED
=== RUN   TestExtractIdpType/local
=== RUN   TestExtractIdpType/#00
--- PASS: TestExtractIdpType (0.00s)
    --- PASS: TestExtractIdpType/okta-abc123def (0.00s)
    --- PASS: REDACTED (0.00s)
    --- PASS: TestExtractIdpType/entra-xyz789 (0.00s)
    --- PASS: TestExtractIdpType/google-abc123 (0.00s)
    --- PASS: TestExtractIdpType/pocketid-abc123 (0.00s)
    --- PASS: TestExtractIdpType/microsoft-abc123 (0.00s)
    --- PASS: TestExtractIdpType/authentik-abc123 (0.00s)
    --- PASS: REDACTED (0.00s)
    --- PASS: TestExtractIdpType/local (0.00s)
    --- PASS: TestExtractIdpType/#00 (0.00s)
PASS
ok  	github.com/netbirdio/netbird/management/server/metrics	2.392s


=== RUN   TestGenerateProperties
--- PASS: TestGenerateProperties (0.00s)
=== RUN   TestExtractIdpType
=== RUN   TestExtractIdpType/okta-abc123def
=== RUN   REDACTED
=== RUN   TestExtractIdpType/entra-xyz789
=== RUN   TestExtractIdpType/google-abc123
=== RUN   TestExtractIdpType/pocketid-abc123
=== RUN   TestExtractIdpType/microsoft-abc123
=== RUN   TestExtractIdpType/authentik-abc123
=== RUN   REDACTED
=== RUN   TestExtractIdpType/local
=== RUN   TestExtractIdpType/#00
--- PASS: TestExtractIdpType (0.00s)
    --- PASS: TestExtractIdpType/okta-abc123def (0.00s)
    --- PASS: REDACTED (0.00s)
    --- PASS: TestExtractIdpType/entra-xyz789 (0.00s)
    --- PASS: TestExtractIdpType/google-abc123 (0.00s)
    --- PASS: TestExtractIdpType/pocketid-abc123 (0.00s)
    --- PASS: TestExtractIdpType/microsoft-abc123 (0.00s)
    --- PASS: TestExtractIdpType/authentik-abc123 (0.00s)
    --- PASS: REDACTED (0.00s)
    --- PASS: TestExtractIdpType/local (0.00s)
    --- PASS: TestExtractIdpType/#00 (0.00s)
PASS
ok  	github.com/netbirdio/netbird/management/server/metrics	2.392s


All tests pass. Added:

1. **`embedded_idp_users_zitadel` assertion** in `TestGenerateProperties` — verifies the new metric is emitted correctly (also fixed the mock connector ID to use realistic format `"zitadel-d5uv82dra0haedlf6kv0"`).

2. **`TestExtractIdpType`** — table-driven test covering all IdP types (okta, zitadel, entra, google, pocketid, microsoft, authentik, keycloak) plus edge cases (`"local"` and empty string both return `"oidc"`).


add new metric - number of idps configured when embedded idp is enabled


The user wants to add a metric that counts the number of identity providers configured when the embedded IdP is enabled. Since the metrics worker doesn't have access to the EmbeddedIdPManager, I need to think about how to get this count.

Looking at the existing code, the worker already decodes Dex user IDs to get connector IDs. I could count the unique connector IDs (excluding "local") to get the number of configured IdPs. But that would only count IdPs that have users — not IdPs that are configured but have no users yet.

Alternatively, I could add a new method to the DataSource interface or pass the count directly. But that's more invasive.

Actually, the simplest approach that's consistent with the existing pattern: count the number of unique IdP types (unique connector ID prefixes) from the user data.

This gives "IdPs with users" which is still useful, though the user asked for "number of idps configured" which would include all configured ones, even without users. I could count unique connector IDs from the user data — each connector has a unique ID like "zitadel-abc123" — which would give me the number of IdPs that have at least one user. That's reasonably close to "configured" since an IdP without any users isn't really in active use. I notice there's already an `embeddedIdpTypes` map that counts users per IdP type, but I need to think through whether multiple connectors can share the same type.

The cleanest solution is to track unique non-local connector IDs in a set, where the count represents the number of external IdPs configured with users. I can add a simple counter for this without changing the DataSource interface.


   210→		rosenpassEnabled          int
   211→		localUsers                int
   212→		idpUsers                  int
   213→		embeddedIdpTypes          map[string]int
   214→	)
   215→	start := time.Now()
   216→	metricsProperties := make(properties)
   217→	osPeers = make(map[string]int)
   218→	osUIClients = make(map[string]int)
   219→	rulesProtocol = make(map[string]int)
   220→	rulesDirection = make(map[string]int)
   221→	activeUsersLastDay = make(map[string]struct{})
   222→	embeddedIdpTypes = make(map[string]int)
   223→	uptime = time.Since(w.startupTime).Seconds()
   224→	connections := w.connManager.GetAllConnectedPeers()

		rosenpassEnabled          int
		localUsers                int
		idpUsers                  int
		embeddedIdpTypes          map[string]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)
	uptime = time.Since(w.startupTime).Seconds()
	connections := w.connManager.GetAllConnectedPeers()


   273→			} else {
   274→				users++
   275→				if w.idpManager == EmbeddedType {
   276→					_, idpID, err := dex.DecodeDexUserID(user.Id)
   277→					if err == nil {
   278→						if idpID == "local" {
   279→							localUsers++
   280→						} else {
   281→							idpUsers++
   282→							idpType := extractIdpType(idpID)
   283→							embeddedIdpTypes[idpType]++
   284→						}
   285→					}
   286→				}
   287→			}
   288→			pats += len(user.PATs)
   289→		}
   290→
   291→		for _, key := range account.SetupKeys {
   292→			setupKeysUsage += key.UsedTimes

			} 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


I'll track unique connector IDs (not types — since you could have two Keycloak connectors):


Edit

		embeddedIdpTypes          map[string]int
	)

		embeddedIdpTypes          map[string]int
		embeddedIdpConnectors     map[string]struct{}
	)