package server import ( "fmt" "net" "net/netip" "path/filepath" "runtime" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" route2 "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/management/server/status" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/util" ) func TestSqlite_NewStore(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } store := newSqliteStore(t) if len(store.GetAllAccounts()) != 0 { t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") } } func TestSqlite_SaveAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } store := newSqliteStore(t) account := newAccountWithId("account_id", "testuser", "") setupKey := GenerateDefaultSetupKey() account.SetupKeys[setupKey.Key] = setupKey account.Peers["testpeer"] = &nbpeer.Peer{ Key: "peerkey", SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, } err := store.SaveAccount(account) require.NoError(t, err) account2 := newAccountWithId("account_id2", "testuser2", "") setupKey = GenerateDefaultSetupKey() account2.SetupKeys[setupKey.Key] = setupKey account2.Peers["testpeer2"] = &nbpeer.Peer{ Key: "peerkey2", SetupKey: "peerkeysetupkey2", IP: net.IP{127, 0, 0, 2}, Meta: nbpeer.PeerSystemMeta{}, Name: "peer name 2", Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, } err = store.SaveAccount(account2) require.NoError(t, err) if len(store.GetAllAccounts()) != 2 { t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") } a, err := store.GetAccount(account.Id) if a == nil { t.Errorf("expecting Account to be stored after SaveAccount(): %v", err) } if a != nil && len(a.Policies) != 1 { t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies)) } if a != nil && len(a.Policies[0].Rules) != 1 { t.Errorf("expecting Account to have one policy rule stored after SaveAccount(), got %d", len(a.Policies[0].Rules)) return } if a, err := store.GetAccountByPeerPubKey("peerkey"); a == nil { t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) } if a, err := store.GetAccountByUser("testuser"); a == nil { t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) } if a, err := store.GetAccountByPeerID("testpeer"); a == nil { t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) } if a, err := store.GetAccountBySetupKey(setupKey.Key); a == nil { t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) } } func TestSqlite_DeleteAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } store := newSqliteStore(t) testUserID := "testuser" user := NewAdminUser(testUserID) user.PATs = map[string]*PersonalAccessToken{"testtoken": { ID: "testtoken", Name: "test token", }} account := newAccountWithId("account_id", testUserID, "") setupKey := GenerateDefaultSetupKey() account.SetupKeys[setupKey.Key] = setupKey account.Peers["testpeer"] = &nbpeer.Peer{ Key: "peerkey", SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, } account.Users[testUserID] = user err := store.SaveAccount(account) require.NoError(t, err) if len(store.GetAllAccounts()) != 1 { t.Errorf("expecting 1 Accounts to be stored after SaveAccount()") } err = store.DeleteAccount(account) require.NoError(t, err) if len(store.GetAllAccounts()) != 0 { t.Errorf("expecting 0 Accounts to be stored after DeleteAccount()") } _, err = store.GetAccountByPeerPubKey("peerkey") require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer public key") _, err = store.GetAccountByUser("testuser") require.Error(t, err, "expecting error after removing DeleteAccount when getting account by user") _, err = store.GetAccountByPeerID("testpeer") require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer id") _, err = store.GetAccountBySetupKey(setupKey.Key) require.Error(t, err, "expecting error after removing DeleteAccount when getting account by setup key") _, err = store.GetAccount(account.Id) require.Error(t, err, "expecting error after removing DeleteAccount when getting account by id") for _, policy := range account.Policies { var rules []*PolicyRule err = store.db.Model(&PolicyRule{}).Find(&rules, "policy_id = ?", policy.ID).Error require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for policy rules") require.Len(t, rules, 0, "expecting no policy rules to be found after removing DeleteAccount") } for _, accountUser := range account.Users { var pats []*PersonalAccessToken err = store.db.Model(&PersonalAccessToken{}).Find(&pats, "user_id = ?", accountUser.Id).Error require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for personal access token") require.Len(t, pats, 0, "expecting no personal access token to be found after removing DeleteAccount") } } func TestSqlite_GetAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } store := newSqliteStoreFromFile(t, "testdata/store.json") id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" account, err := store.GetAccount(id) require.NoError(t, err) require.Equal(t, id, account.Id, "account id should match") _, err = store.GetAccount("non-existing-account") assert.Error(t, err) parsedErr, ok := status.FromError(err) require.True(t, ok) require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } func TestSqlite_SavePeerStatus(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } store := newSqliteStoreFromFile(t, "testdata/store.json") account, err := store.GetAccount("bf1c8084-ba50-4ce7-9439-34653001fc3b") require.NoError(t, err) // save status of non-existing peer newStatus := nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()} err = store.SavePeerStatus(account.Id, "non-existing-peer", newStatus) assert.Error(t, err) parsedErr, ok := status.FromError(err) require.True(t, ok) require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") // save new status of existing peer account.Peers["testpeer"] = &nbpeer.Peer{ Key: "peerkey", ID: "testpeer", SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", Status: &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now().UTC()}, } err = store.SaveAccount(account) require.NoError(t, err) err = store.SavePeerStatus(account.Id, "testpeer", newStatus) require.NoError(t, err) account, err = store.GetAccount(account.Id) require.NoError(t, err) actual := account.Peers["testpeer"].Status assert.Equal(t, newStatus, *actual) } func TestSqlite_SavePeerLocation(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } store := newSqliteStoreFromFile(t, "testdata/store.json") account, err := store.GetAccount("bf1c8084-ba50-4ce7-9439-34653001fc3b") require.NoError(t, err) peer := &nbpeer.Peer{ AccountID: account.Id, ID: "testpeer", Location: nbpeer.Location{ ConnectionIP: net.ParseIP("0.0.0.0"), CountryCode: "YY", CityName: "City", GeoNameID: 1, }, Meta: nbpeer.PeerSystemMeta{}, } // error is expected as peer is not in store yet err = store.SavePeerLocation(account.Id, peer) assert.Error(t, err) account.Peers[peer.ID] = peer err = store.SaveAccount(account) require.NoError(t, err) peer.Location.ConnectionIP = net.ParseIP("35.1.1.1") peer.Location.CountryCode = "DE" peer.Location.CityName = "Berlin" peer.Location.GeoNameID = 2950159 err = store.SavePeerLocation(account.Id, account.Peers[peer.ID]) assert.NoError(t, err) account, err = store.GetAccount(account.Id) require.NoError(t, err) actual := account.Peers[peer.ID].Location assert.Equal(t, peer.Location, actual) peer.ID = "non-existing-peer" err = store.SavePeerLocation(account.Id, peer) assert.Error(t, err) parsedErr, ok := status.FromError(err) require.True(t, ok) require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } func TestSqlite_TestGetAccountByPrivateDomain(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } store := newSqliteStoreFromFile(t, "testdata/store.json") existingDomain := "test.com" account, err := store.GetAccountByPrivateDomain(existingDomain) require.NoError(t, err, "should found account") require.Equal(t, existingDomain, account.Domain, "domains should match") _, err = store.GetAccountByPrivateDomain("missing-domain.com") require.Error(t, err, "should return error on domain lookup") parsedErr, ok := status.FromError(err) require.True(t, ok) require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } func TestSqlite_GetTokenIDByHashedToken(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } store := newSqliteStoreFromFile(t, "testdata/store.json") hashed := "SoMeHaShEdToKeN" id := "9dj38s35-63fb-11ec-90d6-0242ac120003" token, err := store.GetTokenIDByHashedToken(hashed) require.NoError(t, err) require.Equal(t, id, token) _, err = store.GetTokenIDByHashedToken("non-existing-hash") require.Error(t, err) parsedErr, ok := status.FromError(err) require.True(t, ok) require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } func TestSqlite_GetUserByTokenID(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } store := newSqliteStoreFromFile(t, "testdata/store.json") id := "9dj38s35-63fb-11ec-90d6-0242ac120003" user, err := store.GetUserByTokenID(id) require.NoError(t, err) require.Equal(t, id, user.PATs[id].ID) _, err = store.GetUserByTokenID("non-existing-id") require.Error(t, err) parsedErr, ok := status.FromError(err) require.True(t, ok) require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } func TestMigrate(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } store := newSqliteStore(t) err := migrate(store.db) require.NoError(t, err, "Migration should not fail on empty db") _, ipnet, err := net.ParseCIDR("10.0.0.0/24") require.NoError(t, err, "Failed to parse CIDR") type network struct { Network Net net.IPNet `gorm:"serializer:gob"` } type account struct { Account Network *network `gorm:"embedded;embeddedPrefix:network_"` } act := &account{ Network: &network{ Net: *ipnet, }, } err = store.db.Save(act).Error require.NoError(t, err, "Failed to insert Gob data") type route struct { route2.Route Network netip.Prefix `gorm:"serializer:gob"` PeerGroups []string `gorm:"serializer:gob"` } prefix := netip.MustParsePrefix("11.0.0.0/24") rt := &route{ Network: prefix, PeerGroups: []string{"group1", "group2"}, } err = store.db.Save(rt).Error require.NoError(t, err, "Failed to insert Gob data") err = migrate(store.db) require.NoError(t, err, "Migration should not fail on gob populated db") err = migrate(store.db) require.NoError(t, err, "Migration should not fail on migrated db") } func newSqliteStore(t *testing.T) *SqliteStore { t.Helper() store, err := NewSqliteStore(t.TempDir(), nil) require.NoError(t, err) require.NotNil(t, store) return store } func newSqliteStoreFromFile(t *testing.T, filename string) *SqliteStore { t.Helper() storeDir := t.TempDir() err := util.CopyFileContents(filename, filepath.Join(storeDir, "store.json")) require.NoError(t, err) fStore, err := NewFileStore(storeDir, nil) require.NoError(t, err) store, err := NewSqliteStoreFromFileStore(fStore, storeDir, nil) require.NoError(t, err) require.NotNil(t, store) return store } func newAccount(store Store, id int) error { str := fmt.Sprintf("%s-%d", uuid.New().String(), id) account := newAccountWithId(str, str+"-testuser", "example.com") setupKey := GenerateDefaultSetupKey() account.SetupKeys[setupKey.Key] = setupKey account.Peers["p"+str] = &nbpeer.Peer{ Key: "peerkey" + str, SetupKey: "peerkeysetupkey", IP: net.IP{127, 0, 0, 1}, Meta: nbpeer.PeerSystemMeta{}, Name: "peer name", Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, } return store.SaveAccount(account) }