From 5b8636cb771e3f2c15287843e31f9b52af0f1a6e Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Fri, 17 Jan 2025 12:37:15 +0100 Subject: [PATCH 01/27] refactor ginkgo test and add cancel to sync client test --- management/client/client_test.go | 5 +- management/server/management_suite_test.go | 13 - management/server/management_test.go | 998 ++++++++++++--------- 3 files changed, 578 insertions(+), 438 deletions(-) delete mode 100644 management/server/management_suite_test.go diff --git a/management/client/client_test.go b/management/client/client_test.go index 8bd8af8d2aa..d0fcacfad13 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -258,8 +258,11 @@ func TestClient_Sync(t *testing.T) { ch := make(chan *mgmtProto.SyncResponse, 1) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { - err = client.Sync(context.Background(), info, func(msg *mgmtProto.SyncResponse) error { + err = client.Sync(ctx, info, func(msg *mgmtProto.SyncResponse) error { ch <- msg return nil }) diff --git a/management/server/management_suite_test.go b/management/server/management_suite_test.go deleted file mode 100644 index cc99624a07e..00000000000 --- a/management/server/management_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package server_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestManagement(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Management Service Suite") -} diff --git a/management/server/management_test.go b/management/server/management_test.go index cfa2c138f37..04456de8338 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -6,12 +6,11 @@ import ( "net" "os" "runtime" - sync2 "sync" + "sync" + "testing" "time" pb "github.com/golang/protobuf/proto" //nolint - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc" @@ -30,424 +29,73 @@ import ( const ( ValidSetupKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB" - AccountKey = "bf1c8084-ba50-4ce7-9439-34653001fc3b" ) -var _ = Describe("Management service", func() { - var ( - addr string - s *grpc.Server - dataDir string - client mgmtProto.ManagementServiceClient - serverPubKey wgtypes.Key - conn *grpc.ClientConn - ) - - BeforeEach(func() { - level, _ := log.ParseLevel("Debug") - log.SetLevel(level) - var err error - dataDir, err = os.MkdirTemp("", "wiretrustee_mgmt_test_tmp_*") - Expect(err).NotTo(HaveOccurred()) - - var listener net.Listener - - config := &server.Config{} - _, err = util.ReadJson("testdata/management.json", config) - Expect(err).NotTo(HaveOccurred()) - config.Datadir = dataDir - - s, listener = startServer(config, dataDir, "testdata/store.sql") - addr = listener.Addr().String() - client, conn = createRawClient(addr) - - // s public key - resp, err := client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) - Expect(err).NotTo(HaveOccurred()) - serverPubKey, err = wgtypes.ParseKey(resp.Key) - Expect(err).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - s.Stop() - err := conn.Close() - Expect(err).NotTo(HaveOccurred()) - os.RemoveAll(dataDir) - }) - - Context("when calling IsHealthy endpoint", func() { - Specify("a non-error result is returned", func() { - healthy, err := client.IsHealthy(context.TODO(), &mgmtProto.Empty{}) - - Expect(err).NotTo(HaveOccurred()) - Expect(healthy).ToNot(BeNil()) - }) - }) - - Context("when calling Sync endpoint", func() { - Context("when there is a new peer registered", func() { - Specify("a proper configuration is returned", func() { - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - - syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} - encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, syncReq) - Expect(err).NotTo(HaveOccurred()) - - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp) - Expect(err).NotTo(HaveOccurred()) - - expectedSignalConfig := &mgmtProto.HostConfig{ - Uri: "signal.wiretrustee.com:10000", - Protocol: mgmtProto.HostConfig_HTTP, - } - expectedStunsConfig := &mgmtProto.HostConfig{ - Uri: "stun:stun.wiretrustee.com:3468", - Protocol: mgmtProto.HostConfig_UDP, - } - expectedTRUNHost := &mgmtProto.HostConfig{ - Uri: "turn:stun.wiretrustee.com:3468", - Protocol: mgmtProto.HostConfig_UDP, - } - - Expect(resp.WiretrusteeConfig.Signal).To(BeEquivalentTo(expectedSignalConfig)) - Expect(resp.WiretrusteeConfig.Stuns).To(ConsistOf(expectedStunsConfig)) - // TURN validation is special because credentials are dynamically generated - Expect(resp.WiretrusteeConfig.Turns).To(HaveLen(1)) - actualTURN := resp.WiretrusteeConfig.Turns[0] - Expect(len(actualTURN.User) > 0).To(BeTrue()) - Expect(actualTURN.HostConfig).To(BeEquivalentTo(expectedTRUNHost)) - Expect(len(resp.NetworkMap.OfflinePeers) == 0).To(BeTrue()) - }) - }) - - Context("when there are 3 peers registered under one account", func() { - Specify("a list containing other 2 peers is returned", func() { - key, _ := wgtypes.GenerateKey() - key1, _ := wgtypes.GenerateKey() - key2, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - loginPeerWithValidSetupKey(serverPubKey, key1, client) - loginPeerWithValidSetupKey(serverPubKey, key2, client) - - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - Expect(err).NotTo(HaveOccurred()) - - Expect(resp.GetRemotePeers()).To(HaveLen(2)) - peers := []string{resp.GetRemotePeers()[0].WgPubKey, resp.GetRemotePeers()[1].WgPubKey} - Expect(peers).To(ContainElements(key1.PublicKey().String(), key2.PublicKey().String())) - }) - }) - - Context("when there is a new peer registered", func() { - Specify("an update is returned", func() { - // register only a single peer - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - - // after the initial sync call we have 0 peer updates - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - resp := &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - Expect(resp.GetRemotePeers()).To(HaveLen(0)) - - wg := sync2.WaitGroup{} - wg.Add(1) - - // continue listening on updates for a peer - go func() { - err = sync.RecvMsg(encryptedResponse) - - decryptedBytes, err = encryption.Decrypt(encryptedResponse.Body, serverPubKey, key) - Expect(err).NotTo(HaveOccurred()) - resp = &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - wg.Done() - }() - - // register a new peer - key1, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key1, client) - - wg.Wait() - - Expect(err).NotTo(HaveOccurred()) - Expect(resp.GetRemotePeers()).To(HaveLen(1)) - Expect(resp.GetRemotePeers()[0].WgPubKey).To(BeEquivalentTo(key1.PublicKey().String())) - }) - }) - }) - - Context("when calling GetServerKey endpoint", func() { - Specify("a public Wireguard key of the service is returned", func() { - resp, err := client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) - - Expect(err).NotTo(HaveOccurred()) - Expect(resp).ToNot(BeNil()) - Expect(resp.Key).ToNot(BeNil()) - Expect(resp.ExpiresAt).ToNot(BeNil()) - - // check if the key is a valid Wireguard key - key, err := wgtypes.ParseKey(resp.Key) - Expect(err).NotTo(HaveOccurred()) - Expect(key).ToNot(BeNil()) - }) - }) - - Context("when calling Login endpoint", func() { - Context("with an invalid setup key", func() { - Specify("an error is returned", func() { - key, _ := wgtypes.GenerateKey() - message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: "invalid setup key", - Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - - resp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: message, - }) - - Expect(err).To(HaveOccurred()) - Expect(resp).To(BeNil()) - }) - }) - - Context("with a valid setup key", func() { - It("a non error result is returned", func() { - key, _ := wgtypes.GenerateKey() - resp := loginPeerWithValidSetupKey(serverPubKey, key, client) - - Expect(resp).ToNot(BeNil()) - }) - }) - - Context("with a registered peer", func() { - It("a non error result is returned", func() { - key, _ := wgtypes.GenerateKey() - regResp := loginPeerWithValidSetupKey(serverPubKey, key, client) - Expect(regResp).NotTo(BeNil()) - - // just login without registration - message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - loginResp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: message, - }) - - Expect(err).NotTo(HaveOccurred()) - - decryptedResp := &mgmtProto.LoginResponse{} - err = encryption.DecryptMessage(serverPubKey, key, loginResp.Body, decryptedResp) - Expect(err).NotTo(HaveOccurred()) - - expectedSignalConfig := &mgmtProto.HostConfig{ - Uri: "signal.wiretrustee.com:10000", - Protocol: mgmtProto.HostConfig_HTTP, - } - expectedStunsConfig := &mgmtProto.HostConfig{ - Uri: "stun:stun.wiretrustee.com:3468", - Protocol: mgmtProto.HostConfig_UDP, - } - expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{ - HostConfig: &mgmtProto.HostConfig{ - Uri: "turn:stun.wiretrustee.com:3468", - Protocol: mgmtProto.HostConfig_UDP, - }, - User: "some_user", - Password: "some_password", - } - - Expect(decryptedResp.GetWiretrusteeConfig().Signal).To(BeEquivalentTo(expectedSignalConfig)) - Expect(decryptedResp.GetWiretrusteeConfig().Stuns).To(ConsistOf(expectedStunsConfig)) - Expect(decryptedResp.GetWiretrusteeConfig().Turns).To(ConsistOf(expectedTurnsConfig)) - }) - }) - }) +type testSuite struct { + t *testing.T + addr string + grpcServer *grpc.Server + dataDir string + client mgmtProto.ManagementServiceClient + serverPubKey wgtypes.Key + conn *grpc.ClientConn +} - Context("when there are 10 peers registered under one account", func() { - Context("when there are 10 more peers registered under the same account", func() { - Specify("all of the 10 peers will get updates of 10 newly registered peers", func() { - initialPeers := 10 - additionalPeers := 10 - - var peers []wgtypes.Key - for i := 0; i < initialPeers; i++ { - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - peers = append(peers, key) - } +func setupTest(t *testing.T) *testSuite { + level, _ := log.ParseLevel("Debug") + log.SetLevel(level) - wg := sync2.WaitGroup{} - wg.Add(initialPeers + initialPeers*additionalPeers) - - var clients []mgmtProto.ManagementService_SyncClient - for _, peer := range peers { - messageBytes, err := pb.Marshal(&mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}}) - Expect(err).NotTo(HaveOccurred()) - encryptedBytes, err := encryption.Encrypt(messageBytes, serverPubKey, peer) - Expect(err).NotTo(HaveOccurred()) - - // open stream - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: peer.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - clients = append(clients, sync) - - // receive stream - peer := peer - go func() { - for { - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - if err != nil { - break - } - decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, serverPubKey, peer) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = pb.Unmarshal(decryptedBytes, resp) - Expect(err).NotTo(HaveOccurred()) - if len(resp.GetRemotePeers()) > 0 { - // only consider peer updates - wg.Done() - } - } - }() - } + ts := &testSuite{t: t} - time.Sleep(1 * time.Second) - for i := 0; i < additionalPeers; i++ { - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - r := rand.New(rand.NewSource(time.Now().UnixNano())) - n := r.Intn(200) - time.Sleep(time.Duration(n) * time.Millisecond) - } + var err error + ts.dataDir, err = os.MkdirTemp("", "wiretrustee_mgmt_test_tmp_*") + if err != nil { + t.Fatalf("failed to create temp directory: %v", err) + } - wg.Wait() + config := &server.Config{} + _, err = util.ReadJson("testdata/management.json", config) + if err != nil { + t.Fatalf("failed to read management.json: %v", err) + } + config.Datadir = ts.dataDir - for _, syncClient := range clients { - err := syncClient.CloseSend() - Expect(err).NotTo(HaveOccurred()) - } - }) - }) - }) + var listener net.Listener + ts.grpcServer, listener = startServer(t, config, ts.dataDir, "testdata/store.sql") + ts.addr = listener.Addr().String() - Context("when there are peers registered under one account concurrently", func() { - Specify("then there are no duplicate IPs", func() { - initialPeers := 30 - - ipChannel := make(chan string, 20) - for i := 0; i < initialPeers; i++ { - go func() { - defer GinkgoRecover() - key, _ := wgtypes.GenerateKey() - loginPeerWithValidSetupKey(serverPubKey, key, client) - syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} - encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, syncReq) - Expect(err).NotTo(HaveOccurred()) - - // open stream - sync, err := client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ - WgPubKey: key.PublicKey().String(), - Body: encryptedBytes, - }) - Expect(err).NotTo(HaveOccurred()) - encryptedResponse := &mgmtProto.EncryptedMessage{} - err = sync.RecvMsg(encryptedResponse) - Expect(err).NotTo(HaveOccurred()) - - resp := &mgmtProto.SyncResponse{} - err = encryption.DecryptMessage(serverPubKey, key, encryptedResponse.Body, resp) - Expect(err).NotTo(HaveOccurred()) - - ipChannel <- resp.GetPeerConfig().Address - }() - } + ts.client, ts.conn = createRawClient(t, ts.addr) - ips := make(map[string]struct{}) - for ip := range ipChannel { - if _, ok := ips[ip]; ok { - Fail("found duplicate IP: " + ip) - } - ips[ip] = struct{}{} - if len(ips) == initialPeers { - break - } - } - close(ipChannel) - }) - }) - - Context("after login two peers", func() { - Specify("then they receive the same network", func() { - key, _ := wgtypes.GenerateKey() - firstLogin := loginPeerWithValidSetupKey(serverPubKey, key, client) - key, _ = wgtypes.GenerateKey() - secondLogin := loginPeerWithValidSetupKey(serverPubKey, key, client) + resp, err := ts.client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) + if err != nil { + t.Fatalf("failed to get server key: %v", err) + } - _, firstLoginNetwork, err := net.ParseCIDR(firstLogin.GetPeerConfig().GetAddress()) - Expect(err).NotTo(HaveOccurred()) - _, secondLoginNetwork, err := net.ParseCIDR(secondLogin.GetPeerConfig().GetAddress()) - Expect(err).NotTo(HaveOccurred()) + serverKey, err := wgtypes.ParseKey(resp.Key) + if err != nil { + t.Fatalf("failed to parse server key: %v", err) + } + ts.serverPubKey = serverKey - Expect(secondLoginNetwork.String()).To(BeEquivalentTo(firstLoginNetwork.String())) - }) - }) -}) + return ts +} -func loginPeerWithValidSetupKey(serverPubKey wgtypes.Key, key wgtypes.Key, client mgmtProto.ManagementServiceClient) *mgmtProto.LoginResponse { - defer GinkgoRecover() +func tearDownTest(t *testing.T, ts *testSuite) { + ts.grpcServer.Stop() + if err := ts.conn.Close(); err != nil { + t.Fatalf("failed to close client connection: %v", err) + } + if err := os.RemoveAll(ts.dataDir); err != nil { + t.Fatalf("failed to remove data directory %s: %v", ts.dataDir, err) + } +} +func loginPeerWithValidSetupKey( + t *testing.T, + serverPubKey wgtypes.Key, + key wgtypes.Key, + client mgmtProto.ManagementServiceClient, +) *mgmtProto.LoginResponse { meta := &mgmtProto.PeerSystemMeta{ Hostname: key.PublicKey().String(), GoOS: runtime.GOOS, @@ -457,23 +105,29 @@ func loginPeerWithValidSetupKey(serverPubKey wgtypes.Key, key wgtypes.Key, clien Kernel: "kernel", WiretrusteeVersion: "", } - message, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.LoginRequest{SetupKey: ValidSetupKey, Meta: meta}) - Expect(err).NotTo(HaveOccurred()) + msgToEncrypt := &mgmtProto.LoginRequest{SetupKey: ValidSetupKey, Meta: meta} + message, err := encryption.EncryptMessage(serverPubKey, key, msgToEncrypt) + if err != nil { + t.Fatalf("failed to encrypt login request: %v", err) + } resp, err := client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ WgPubKey: key.PublicKey().String(), Body: message, }) - - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("login request failed: %v", err) + } loginResp := &mgmtProto.LoginResponse{} err = encryption.DecryptMessage(serverPubKey, key, resp.Body, loginResp) - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("failed to decrypt login response: %v", err) + } return loginResp } -func createRawClient(addr string) (mgmtProto.ManagementServiceClient, *grpc.ClientConn) { +func createRawClient(t *testing.T, addr string) (mgmtProto.ManagementServiceClient, *grpc.ClientConn) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -484,17 +138,26 @@ func createRawClient(addr string) (mgmtProto.ManagementServiceClient, *grpc.Clie Time: 10 * time.Second, Timeout: 2 * time.Second, })) - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("failed to dial gRPC server: %v", err) + } return mgmtProto.NewManagementServiceClient(conn), conn } -func startServer(config *server.Config, dataDir string, testFile string) (*grpc.Server, net.Listener) { +func startServer( + t *testing.T, + config *server.Config, + dataDir string, + testFile string, +) (*grpc.Server, net.Listener) { lis, err := net.Listen("tcp", ":0") - Expect(err).NotTo(HaveOccurred()) + if err != nil { + t.Fatalf("failed to listen on a random port: %v", err) + } s := grpc.NewServer() - store, _, err := store.NewTestStoreFromSQL(context.Background(), testFile, dataDir) + str, _, err := store.NewTestStoreFromSQL(context.Background(), testFile, dataDir) if err != nil { log.Fatalf("failed creating a store: %s: %v", config.Datadir, err) } @@ -504,23 +167,510 @@ func startServer(config *server.Config, dataDir string, testFile string) (*grpc. metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) if err != nil { - log.Fatalf("failed creating metrics: %v", err) + t.Fatalf("failed creating metrics: %v", err) } - accountManager, err := server.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, server.MocIntegratedValidator{}, metrics) + accountManager, err := server.BuildManager( + context.Background(), + str, + peersUpdateManager, + nil, + "", + "netbird.selfhosted", + eventStore, + nil, + false, + server.MocIntegratedValidator{}, + metrics, + ) if err != nil { - log.Fatalf("failed creating a manager: %v", err) + t.Fatalf("failed creating an account manager: %v", err) } secretsManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay) - mgmtServer, err := server.NewServer(context.Background(), config, accountManager, settings.NewManager(store), peersUpdateManager, secretsManager, nil, nil) - Expect(err).NotTo(HaveOccurred()) + mgmtServer, err := server.NewServer( + context.Background(), + config, + accountManager, + settings.NewManager(str), + peersUpdateManager, + secretsManager, + nil, + nil, + ) + if err != nil { + t.Fatalf("failed creating management server: %v", err) + } + mgmtProto.RegisterManagementServiceServer(s, mgmtServer) + go func() { if err := s.Serve(lis); err != nil { - Expect(err).NotTo(HaveOccurred()) + t.Fatalf("failed to serve gRPC: %v", err) } }() return s, lis } + +func TestIsHealthy(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + healthy, err := ts.client.IsHealthy(context.TODO(), &mgmtProto.Empty{}) + if err != nil { + t.Fatalf("IsHealthy call returned an error: %v", err) + } + if healthy == nil { + t.Fatal("IsHealthy returned a nil response") + } +} + +func TestSyncNewPeerConfiguration(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedBytes, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, syncReq) + if err != nil { + t.Fatalf("failed to encrypt sync request: %v", err) + } + + syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync: %v", err) + } + + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Fatalf("failed to receive sync response message: %v", err) + } + + resp := &mgmtProto.SyncResponse{} + err = encryption.DecryptMessage(ts.serverPubKey, peerKey, encryptedResponse.Body, resp) + if err != nil { + t.Fatalf("failed to decrypt sync response: %v", err) + } + + if resp.WiretrusteeConfig == nil { + t.Fatal("WiretrusteeConfig is nil in SyncResponse") + } + if resp.WiretrusteeConfig.Signal == nil { + t.Fatal("Expected Signal config in SyncResponse, got nil") + } + if len(resp.WiretrusteeConfig.Stuns) == 0 { + t.Fatal("Expected at least one STUN config in SyncResponse, got none") + } + if len(resp.WiretrusteeConfig.Turns) == 0 { + t.Fatal("Expected at least one TURN config in SyncResponse, got none") + } + if len(resp.NetworkMap.OfflinePeers) != 0 { + t.Fatalf("Expected 0 offline peers, got %d", len(resp.NetworkMap.OfflinePeers)) + } +} + +func TestSyncThreePeers(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + peerKey1, _ := wgtypes.GenerateKey() + peerKey2, _ := wgtypes.GenerateKey() + + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey1, ts.client) + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey2, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + syncBytes, err := pb.Marshal(syncReq) + if err != nil { + t.Fatalf("failed to marshal sync request: %v", err) + } + encryptedBytes, err := encryption.Encrypt(syncBytes, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to encrypt sync request: %v", err) + } + + syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync: %v", err) + } + + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Fatalf("failed to receive sync response: %v", err) + } + + decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to decrypt sync response: %v", err) + } + + resp := &mgmtProto.SyncResponse{} + err = pb.Unmarshal(decryptedBytes, resp) + if err != nil { + t.Fatalf("failed to unmarshal sync response: %v", err) + } + + if len(resp.GetRemotePeers()) != 2 { + t.Fatalf("expected 2 remote peers, got %d", len(resp.GetRemotePeers())) + } + + var found1, found2 bool + for _, rp := range resp.GetRemotePeers() { + if rp.WgPubKey == peerKey1.PublicKey().String() { + found1 = true + } else if rp.WgPubKey == peerKey2.PublicKey().String() { + found2 = true + } + } + if !found1 || !found2 { + t.Fatalf("did not find the expected peer keys %s, %s among %v", + peerKey1.PublicKey().String(), + peerKey2.PublicKey().String(), + resp.GetRemotePeers()) + } +} + +func TestSyncNewPeerUpdate(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + syncBytes, err := pb.Marshal(syncReq) + if err != nil { + t.Fatalf("failed to marshal sync request: %v", err) + } + + encryptedBytes, err := encryption.Encrypt(syncBytes, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to encrypt sync request: %v", err) + } + + syncStream, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync: %v", err) + } + + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Fatalf("failed to receive first sync response: %v", err) + } + + decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey) + if err != nil { + t.Fatalf("failed to decrypt first sync response: %v", err) + } + + resp := &mgmtProto.SyncResponse{} + if err := pb.Unmarshal(decryptedBytes, resp); err != nil { + t.Fatalf("failed to unmarshal first sync response: %v", err) + } + + if len(resp.GetRemotePeers()) != 0 { + t.Fatalf("expected 0 remote peers at first sync, got %d", len(resp.GetRemotePeers())) + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + encryptedResponse := &mgmtProto.EncryptedMessage{} + err = syncStream.RecvMsg(encryptedResponse) + if err != nil { + t.Errorf("failed to receive second sync response: %v", err) + return + } + + decryptedBytes, err := encryption.Decrypt(encryptedResponse.Body, ts.serverPubKey, peerKey) + if err != nil { + t.Errorf("failed to decrypt second sync response: %v", err) + return + } + err = pb.Unmarshal(decryptedBytes, resp) + if err != nil { + t.Errorf("failed to unmarshal second sync response: %v", err) + return + } + }() + + newPeerKey, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, newPeerKey, ts.client) + + wg.Wait() + + if len(resp.GetRemotePeers()) != 1 { + t.Fatalf("expected exactly 1 remote peer update, got %d", len(resp.GetRemotePeers())) + } + if resp.GetRemotePeers()[0].WgPubKey != newPeerKey.PublicKey().String() { + t.Fatalf("expected new peer key %s, got %s", + newPeerKey.PublicKey().String(), + resp.GetRemotePeers()[0].WgPubKey) + } +} + +func TestGetServerKey(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + resp, err := ts.client.GetServerKey(context.TODO(), &mgmtProto.Empty{}) + if err != nil { + t.Fatalf("GetServerKey returned error: %v", err) + } + if resp == nil { + t.Fatal("GetServerKey returned nil response") + } + if resp.Key == "" { + t.Fatal("GetServerKey returned empty key") + } + if resp.ExpiresAt.AsTime().IsZero() { + t.Fatal("GetServerKey returned 0 for ExpiresAt") + } + + _, err = wgtypes.ParseKey(resp.Key) + if err != nil { + t.Fatalf("GetServerKey returned an invalid WG key: %v", err) + } +} + +func TestLoginInvalidSetupKey(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + request := &mgmtProto.LoginRequest{ + SetupKey: "invalid setup key", + Meta: &mgmtProto.PeerSystemMeta{}, + } + encryptedMsg, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, request) + if err != nil { + t.Fatalf("failed to encrypt login request: %v", err) + } + + resp, err := ts.client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedMsg, + }) + if err == nil { + t.Fatal("expected error for invalid setup key but got nil") + } + if resp != nil { + t.Fatalf("expected nil response for invalid setup key but got: %+v", resp) + } +} + +func TestLoginValidSetupKey(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + resp := loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + if resp == nil { + t.Fatal("loginPeerWithValidSetupKey returned nil, expected a valid response") + } +} + +func TestLoginRegisteredPeer(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + peerKey, _ := wgtypes.GenerateKey() + regResp := loginPeerWithValidSetupKey(t, ts.serverPubKey, peerKey, ts.client) + if regResp == nil { + t.Fatal("registration with valid setup key failed") + } + + loginReq := &mgmtProto.LoginRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedLogin, err := encryption.EncryptMessage(ts.serverPubKey, peerKey, loginReq) + if err != nil { + t.Fatalf("failed to encrypt login request: %v", err) + } + loginRespEnc, err := ts.client.Login(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: peerKey.PublicKey().String(), + Body: encryptedLogin, + }) + if err != nil { + t.Fatalf("login call returned an error: %v", err) + } + + loginResp := &mgmtProto.LoginResponse{} + err = encryption.DecryptMessage(ts.serverPubKey, peerKey, loginRespEnc.Body, loginResp) + if err != nil { + t.Fatalf("failed to decrypt login response: %v", err) + } + + if loginResp.GetWiretrusteeConfig() == nil { + t.Fatal("WiretrusteeConfig is nil in login response") + } + if len(loginResp.GetWiretrusteeConfig().Stuns) == 0 { + t.Fatal("Expected STUN servers in login response, got none") + } + if len(loginResp.GetWiretrusteeConfig().Turns) == 0 { + t.Fatal("Expected TURN servers in login response, got none") + } +} + +func TestSync10PeersGetUpdates(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + initialPeers := 10 + additionalPeers := 10 + + var peers []wgtypes.Key + for i := 0; i < initialPeers; i++ { + key, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client) + peers = append(peers, key) + } + + var wg sync.WaitGroup + wg.Add(initialPeers + initialPeers*additionalPeers) + + var syncClients []mgmtProto.ManagementService_SyncClient + for _, pk := range peers { + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + msgBytes, err := pb.Marshal(syncReq) + if err != nil { + t.Fatalf("failed to marshal SyncRequest: %v", err) + } + encBytes, err := encryption.Encrypt(msgBytes, ts.serverPubKey, pk) + if err != nil { + t.Fatalf("failed to encrypt SyncRequest: %v", err) + } + + s, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: pk.PublicKey().String(), + Body: encBytes, + }) + if err != nil { + t.Fatalf("failed to call Sync for peer: %v", err) + } + syncClients = append(syncClients, s) + + go func(pk wgtypes.Key, syncStream mgmtProto.ManagementService_SyncClient) { + for { + encMsg := &mgmtProto.EncryptedMessage{} + err := syncStream.RecvMsg(encMsg) + if err != nil { + return + } + decryptedBytes, decErr := encryption.Decrypt(encMsg.Body, ts.serverPubKey, pk) + if decErr != nil { + t.Errorf("failed to decrypt SyncResponse for peer %s: %v", pk.PublicKey().String(), decErr) + return + } + resp := &mgmtProto.SyncResponse{} + umErr := pb.Unmarshal(decryptedBytes, resp) + if umErr != nil { + t.Errorf("failed to unmarshal SyncResponse for peer %s: %v", pk.PublicKey().String(), umErr) + return + } + // We only count if there's a new peer update + if len(resp.GetRemotePeers()) > 0 { + wg.Done() + } + } + }(pk, s) + } + + time.Sleep(500 * time.Millisecond) + for i := 0; i < additionalPeers; i++ { + key, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client) + r := rand.New(rand.NewSource(time.Now().UnixNano())) + n := r.Intn(200) + time.Sleep(time.Duration(n) * time.Millisecond) + } + + wg.Wait() + + for _, sc := range syncClients { + err := sc.CloseSend() + if err != nil { + t.Fatalf("failed to close sync client: %v", err) + } + } +} + +func TestConcurrentPeersNoDuplicateIPs(t *testing.T) { + ts := setupTest(t) + defer tearDownTest(t, ts) + + initialPeers := 30 + ipChan := make(chan string, initialPeers) + + var wg sync.WaitGroup + wg.Add(initialPeers) + + for i := 0; i < initialPeers; i++ { + go func() { + defer wg.Done() + key, _ := wgtypes.GenerateKey() + loginPeerWithValidSetupKey(t, ts.serverPubKey, key, ts.client) + + syncReq := &mgmtProto.SyncRequest{Meta: &mgmtProto.PeerSystemMeta{}} + encryptedBytes, err := encryption.EncryptMessage(ts.serverPubKey, key, syncReq) + if err != nil { + t.Errorf("failed to encrypt sync request: %v", err) + return + } + + s, err := ts.client.Sync(context.TODO(), &mgmtProto.EncryptedMessage{ + WgPubKey: key.PublicKey().String(), + Body: encryptedBytes, + }) + if err != nil { + t.Errorf("failed to call Sync: %v", err) + return + } + + encResp := &mgmtProto.EncryptedMessage{} + if err = s.RecvMsg(encResp); err != nil { + t.Errorf("failed to receive sync response: %v", err) + return + } + + resp := &mgmtProto.SyncResponse{} + if err = encryption.DecryptMessage(ts.serverPubKey, key, encResp.Body, resp); err != nil { + t.Errorf("failed to decrypt sync response: %v", err) + return + } + ipChan <- resp.GetPeerConfig().Address + }() + } + + wg.Wait() + close(ipChan) + + ipMap := make(map[string]bool) + for ip := range ipChan { + if ipMap[ip] { + t.Fatalf("found duplicate IP: %s", ip) + } + ipMap[ip] = true + } + + // Ensure we collected all peers + if len(ipMap) != initialPeers { + t.Fatalf("expected %d unique IPs, got %d", initialPeers, len(ipMap)) + } +} From 3662872a391d12fb7246107ae3e5b45ff2a7d8e2 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Fri, 17 Jan 2025 13:03:59 +0100 Subject: [PATCH 02/27] mark test helper functions --- management/server/management_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/management/server/management_test.go b/management/server/management_test.go index 04456de8338..f791d813c6b 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -42,6 +42,7 @@ type testSuite struct { } func setupTest(t *testing.T) *testSuite { + t.Helper() level, _ := log.ParseLevel("Debug") log.SetLevel(level) @@ -81,6 +82,7 @@ func setupTest(t *testing.T) *testSuite { } func tearDownTest(t *testing.T, ts *testSuite) { + t.Helper() ts.grpcServer.Stop() if err := ts.conn.Close(); err != nil { t.Fatalf("failed to close client connection: %v", err) @@ -96,6 +98,7 @@ func loginPeerWithValidSetupKey( key wgtypes.Key, client mgmtProto.ManagementServiceClient, ) *mgmtProto.LoginResponse { + t.Helper() meta := &mgmtProto.PeerSystemMeta{ Hostname: key.PublicKey().String(), GoOS: runtime.GOOS, @@ -128,6 +131,7 @@ func loginPeerWithValidSetupKey( } func createRawClient(t *testing.T, addr string) (mgmtProto.ManagementServiceClient, *grpc.ClientConn) { + t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -151,6 +155,7 @@ func startServer( dataDir string, testFile string, ) (*grpc.Server, net.Listener) { + t.Helper() lis, err := net.Listen("tcp", ":0") if err != nil { t.Fatalf("failed to listen on a random port: %v", err) @@ -206,7 +211,8 @@ func startServer( go func() { if err := s.Serve(lis); err != nil { - t.Fatalf("failed to serve gRPC: %v", err) + t.Errorf("failed to serve gRPC: %v", err) + return } }() From 801ce510086d7a4dbb3765fe49b6141d7a4cb543 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sat, 18 Jan 2025 18:22:10 +0100 Subject: [PATCH 03/27] except input dsn an run testcontainer only once --- management/server/store/sql_store_test.go | 26 ++++++++++ management/server/store/store.go | 58 +++++++++++++++++++---- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 5928b45baa7..19c21085663 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -24,6 +24,7 @@ import ( routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/testutil" "github.com/netbirdio/netbird/management/server/types" route2 "github.com/netbirdio/netbird/route" @@ -761,6 +762,30 @@ func newAccount(store Store, id int) error { return store.SaveAccount(context.Background(), account) } +var cleanUp func() + +func TestMain(m *testing.M) { + + // Start container once for the entire test suite in this package + var err error + cleanUp, err = testutil.CreatePostgresTestContainer() + if err != nil { + // If container fails to start, stop testing immediately + os.Exit(1) + } + + // run all tests + code := m.Run() + + // stop container + if cleanUp != nil { + cleanUp() + } + + // exit with test code + os.Exit(code) +} + func TestPostgresql_NewStore(t *testing.T) { if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { t.Skip("skip CI tests on darwin and windows") @@ -2370,6 +2395,7 @@ func TestSqlStore_GetNetworkRouterByID(t *testing.T) { } func TestSqlStore_SaveNetworkRouter(t *testing.T) { + t.Setenv("NETBIRD_STORE_ENGINE", string(PostgresStoreEngine)) store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) t.Cleanup(cleanup) require.NoError(t, err) diff --git a/management/server/store/store.go b/management/server/store/store.go index 91ae93c7c34..3a483096b28 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -4,8 +4,10 @@ import ( "context" "errors" "fmt" + "math/rand" "net" "net/netip" + "net/url" "os" "path" "path/filepath" @@ -14,6 +16,7 @@ import ( "time" log "github.com/sirupsen/logrus" + "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" @@ -334,9 +337,13 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) { if kind == PostgresStoreEngine { - cleanUp, err := testutil.CreatePostgresTestContainer() - if err != nil { - return nil, nil, err + var cleanUp func() + if envDsn, ok := os.LookupEnv(postgresDsnEnv); !ok || envDsn == "" { + var err error + cleanUp, err = testutil.CreatePostgresTestContainer() + if err != nil { + return nil, nil, err + } } dsn, ok := os.LookupEnv(postgresDsnEnv) @@ -344,18 +351,51 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv) } - store, err = NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil) + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, nil, fmt.Errorf("failed to open postgres connection: %v", err) + } + + rand.Seed(time.Now().UnixNano()) + dbName := fmt.Sprintf("test_db_%d", rand.Intn(1e6)) + + if err := db.Exec(fmt.Sprintf("CREATE DATABASE %q", dbName)).Error; err != nil { + return nil, nil, fmt.Errorf("failed to create database: %v", err) + } + + u, err := url.Parse(dsn) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse DSN: %v", err) + } + + u.Path = dbName + + dsn = u.String() + + store, err := NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil) if err != nil { return nil, nil, err } - return store, cleanUp, nil + cleanup := func() { + db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)) + if cleanUp != nil { + os.Setenv(postgresDsnEnv, "") + cleanUp() + } + } + + return store, cleanup, nil } if kind == MysqlStoreEngine { - cleanUp, err := testutil.CreateMysqlTestContainer() - if err != nil { - return nil, nil, err + var cleanUp func() + if _, ok := os.LookupEnv(mysqlDsnEnv); !ok { + var err error + cleanUp, err = testutil.CreateMysqlTestContainer() + if err != nil { + return nil, nil, err + } } dsn, ok := os.LookupEnv(mysqlDsnEnv) @@ -363,7 +403,7 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv) } - store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil) + store, err := NewMysqlStoreFromSqlStore(ctx, store, dsn, nil) if err != nil { return nil, nil, err } From 9e09de1e75d29944a6395f43cc20102cf0225c04 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sat, 18 Jan 2025 18:26:59 +0100 Subject: [PATCH 04/27] start postgres container from github actions --- .github/workflows/golang-test-linux.yml | 26 ++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index ba5f66746e7..106b1bf5890 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -144,6 +144,22 @@ jobs: arch: [ '386','amd64' ] store: [ 'sqlite', 'postgres', 'mysql' ] runs-on: ubuntu-22.04 + + services: + postgres: + image: postgres:15-alpine + env: + POSTGRES_USER: root + POSTGRES_PASSWORD: netbird + POSTGRES_DB: netbird + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U root -d netbird" + --health-interval 5s + --health-timeout 5s + --health-retries 5 + steps: - name: Install Go uses: actions/setup-go@v5 @@ -193,8 +209,16 @@ jobs: if: matrix.store == 'mysql' run: docker pull mlsmaycon/warmed-mysql:8 + - name: Wait for Postgres + run: | + for i in {1..10}; do + nc -z localhost 5432 && break + echo "Waiting for Postgres..." + sleep 3 + done + - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=devcert -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management) + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} NETBIRD_STORE_ENGINE_POSTGRES_DSN="postgres://root:netbird@localhost:5432/netbird?sslmode=disable" CI=true go test -tags=devcert -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN' -timeout 10m $(go list ./... | grep /management) benchmark: needs: [ build-cache ] From f2d800a7af5ab197a319c337641788115c56dceb Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sat, 18 Jan 2025 19:10:11 +0100 Subject: [PATCH 05/27] run store test for all engines example --- management/server/store/sql_store_test.go | 730 +++++++++++----------- 1 file changed, 357 insertions(+), 373 deletions(-) diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 19c21085663..23060e49c6a 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -35,40 +35,75 @@ import ( nbroute "github.com/netbirdio/netbird/route" ) -func TestSqlite_NewStore(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("The SQLite store is not properly supported by Windows yet") +var cleanUpPostgres func() +var cleanUpMysql func() + +func TestMain(m *testing.M) { + + if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_POSTGRES_DSN"); !ok || dsn == "" { + var err error + cleanUpPostgres, err = testutil.CreatePostgresTestContainer() + if err != nil { + os.Exit(1) + } } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_MYSQL_DSN"); !ok || dsn == "" { + var err error + cleanUpMysql, err = testutil.CreateMysqlTestContainer() + if err != nil { + os.Exit(1) + } + } - if len(store.GetAllAccounts(context.Background())) != 0 { - t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") + code := m.Run() + + if cleanUpPostgres != nil { + cleanUpPostgres() } + if cleanUpMysql != nil { + cleanUpMysql() + } + + os.Exit(code) } -func TestSqlite_SaveAccount_Large(t *testing.T) { - if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { - t.Skip("skip CI tests on darwin and windows") - } +var engines = []Engine{PostgresStoreEngine, SqliteStoreEngine} - t.Run("SQLite", func(t *testing.T) { +func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) { + t.Helper() + for _, engine := range engines { + if os.Getenv("NETBIRD_STORE_ENGINE") != "" && os.Getenv("NETBIRD_STORE_ENGINE") != string(engine) { + continue + } t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), testDataFile, t.TempDir()) t.Cleanup(cleanUp) assert.NoError(t, err) - runLargeTest(t, store) + t.Run(string(engine), func(t *testing.T) { + f(t, store) + }) + } +} + +func Test_NewStore(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + if len(store.GetAllAccounts(context.Background())) != 0 { + t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") + } }) +} - // create store outside to have a better time counter for the test - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - t.Run("PostgreSQL", func(t *testing.T) { +func TestSqlite_SaveAccount_Large(t *testing.T) { + if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { + t.Skip("skip CI tests on darwin and windows") + } + + runTestForAllEngines(t, "", func(t *testing.T, store Store) { runLargeTest(t, store) }) } @@ -213,443 +248,416 @@ func randomIPv4() net.IP { return net.IP(b) } -func TestSqlite_SaveAccount(t *testing.T) { +func Test_SaveAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - account := newAccountWithId(context.Background(), "account_id", "testuser", "") - setupKey, _ := types.GenerateDefaultSetupKey() - account.SetupKeys[setupKey.Key] = setupKey - account.Peers["testpeer"] = &nbpeer.Peer{ - Key: "peerkey", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + account := newAccountWithId(context.Background(), "account_id", "testuser", "") + setupKey, _ := types.GenerateDefaultSetupKey() + account.SetupKeys[setupKey.Key] = setupKey + account.Peers["testpeer"] = &nbpeer.Peer{ + Key: "peerkey", + 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(context.Background(), account) - require.NoError(t, err) + err := store.SaveAccount(context.Background(), account) + require.NoError(t, err) - account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "") - setupKey, _ = types.GenerateDefaultSetupKey() - account2.SetupKeys[setupKey.Key] = setupKey - account2.Peers["testpeer2"] = &nbpeer.Peer{ - Key: "peerkey2", - IP: net.IP{127, 0, 0, 2}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name 2", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "") + setupKey, _ = types.GenerateDefaultSetupKey() + account2.SetupKeys[setupKey.Key] = setupKey + account2.Peers["testpeer2"] = &nbpeer.Peer{ + Key: "peerkey2", + 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(context.Background(), account2) - require.NoError(t, err) + err = store.SaveAccount(context.Background(), account2) + require.NoError(t, err) - if len(store.GetAllAccounts(context.Background())) != 2 { - t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") - } + if len(store.GetAllAccounts(context.Background())) != 2 { + t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") + } - a, err := store.GetAccount(context.Background(), account.Id) - if a == nil { - t.Errorf("expecting Account to be stored after SaveAccount(): %v", err) - } + a, err := store.GetAccount(context.Background(), 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) != 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 != 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(context.Background(), "peerkey"); a == nil { - t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByPeerPubKey(context.Background(), "peerkey"); a == nil { + t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil { - t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil { + t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil { - t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil { + t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil { - t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil { + t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) + } + }) } -func TestSqlite_DeleteAccount(t *testing.T) { +func Test_DeleteAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + testUserID := "testuser" + user := types.NewAdminUser(testUserID) + user.PATs = map[string]*types.PersonalAccessToken{"testtoken": { + ID: "testtoken", + Name: "test token", + }} - testUserID := "testuser" - user := types.NewAdminUser(testUserID) - user.PATs = map[string]*types.PersonalAccessToken{"testtoken": { - ID: "testtoken", - Name: "test token", - }} + account := newAccountWithId(context.Background(), "account_id", testUserID, "") + setupKey, _ := types.GenerateDefaultSetupKey() + account.SetupKeys[setupKey.Key] = setupKey + account.Peers["testpeer"] = &nbpeer.Peer{ + Key: "peerkey", + 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 + account.Networks = []*networkTypes.Network{ + { + ID: "network_id", + AccountID: account.Id, + Name: "network name", + Description: "network description", + }, + } + account.NetworkRouters = []*routerTypes.NetworkRouter{ + { + ID: "router_id", + NetworkID: account.Networks[0].ID, + AccountID: account.Id, + PeerGroups: []string{"group_id"}, + Masquerade: true, + Metric: 1, + }, + } + account.NetworkResources = []*resourceTypes.NetworkResource{ + { + ID: "resource_id", + NetworkID: account.Networks[0].ID, + AccountID: account.Id, + Name: "Name", + Description: "Description", + Type: "Domain", + Address: "example.com", + }, + } - account := newAccountWithId(context.Background(), "account_id", testUserID, "") - setupKey, _ := types.GenerateDefaultSetupKey() - account.SetupKeys[setupKey.Key] = setupKey - account.Peers["testpeer"] = &nbpeer.Peer{ - Key: "peerkey", - 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 - account.Networks = []*networkTypes.Network{ - { - ID: "network_id", - AccountID: account.Id, - Name: "network name", - Description: "network description", - }, - } - account.NetworkRouters = []*routerTypes.NetworkRouter{ - { - ID: "router_id", - NetworkID: account.Networks[0].ID, - AccountID: account.Id, - PeerGroups: []string{"group_id"}, - Masquerade: true, - Metric: 1, - }, - } - account.NetworkResources = []*resourceTypes.NetworkResource{ - { - ID: "resource_id", - NetworkID: account.Networks[0].ID, - AccountID: account.Id, - Name: "Name", - Description: "Description", - Type: "Domain", - Address: "example.com", - }, - } + err := store.SaveAccount(context.Background(), account) + require.NoError(t, err) - err = store.SaveAccount(context.Background(), account) - require.NoError(t, err) + if len(store.GetAllAccounts(context.Background())) != 1 { + t.Errorf("expecting 1 Accounts to be stored after SaveAccount()") + } - if len(store.GetAllAccounts(context.Background())) != 1 { - t.Errorf("expecting 1 Accounts to be stored after SaveAccount()") - } + err = store.DeleteAccount(context.Background(), account) + require.NoError(t, err) - err = store.DeleteAccount(context.Background(), account) - require.NoError(t, err) + if len(store.GetAllAccounts(context.Background())) != 0 { + t.Errorf("expecting 0 Accounts to be stored after DeleteAccount()") + } - if len(store.GetAllAccounts(context.Background())) != 0 { - t.Errorf("expecting 0 Accounts to be stored after DeleteAccount()") - } + _, err = store.GetAccountByPeerPubKey(context.Background(), "peerkey") + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer public key") - _, err = store.GetAccountByPeerPubKey(context.Background(), "peerkey") - require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer public key") + _, err = store.GetAccountByUser(context.Background(), "testuser") + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by user") - _, err = store.GetAccountByUser(context.Background(), "testuser") - require.Error(t, err, "expecting error after removing DeleteAccount when getting account by user") + _, err = store.GetAccountByPeerID(context.Background(), "testpeer") + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer id") - _, err = store.GetAccountByPeerID(context.Background(), "testpeer") - require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer id") + _, err = store.GetAccountBySetupKey(context.Background(), setupKey.Key) + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by setup key") - _, err = store.GetAccountBySetupKey(context.Background(), setupKey.Key) - require.Error(t, err, "expecting error after removing DeleteAccount when getting account by setup key") + _, err = store.GetAccount(context.Background(), account.Id) + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by id") - _, err = store.GetAccount(context.Background(), account.Id) - require.Error(t, err, "expecting error after removing DeleteAccount when getting account by id") + for _, policy := range account.Policies { + var rules []*types.PolicyRule + err = store.(*SqlStore).db.Model(&types.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 _, policy := range account.Policies { - var rules []*types.PolicyRule - err = store.(*SqlStore).db.Model(&types.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 []*types.PersonalAccessToken + err = store.(*SqlStore).db.Model(&types.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") - for _, accountUser := range account.Users { - var pats []*types.PersonalAccessToken - err = store.(*SqlStore).db.Model(&types.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") - - } + } - for _, network := range account.Networks { - routers, err := store.GetNetworkRoutersByNetID(context.Background(), LockingStrengthShare, account.Id, network.ID) - require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for network routers") - require.Len(t, routers, 0, "expecting no network routers to be found after DeleteAccount") + for _, network := range account.Networks { + routers, err := store.GetNetworkRoutersByNetID(context.Background(), LockingStrengthShare, account.Id, network.ID) + require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for network routers") + require.Len(t, routers, 0, "expecting no network routers to be found after DeleteAccount") - resources, err := store.GetNetworkResourcesByNetID(context.Background(), LockingStrengthShare, account.Id, network.ID) - require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for network resources") - require.Len(t, resources, 0, "expecting no network resources to be found after DeleteAccount") - } + resources, err := store.GetNetworkResourcesByNetID(context.Background(), LockingStrengthShare, account.Id, network.ID) + require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for network resources") + require.Len(t, resources, 0, "expecting no network resources to be found after DeleteAccount") + } + }) } -func TestSqlite_GetAccount(t *testing.T) { +func Test_GetAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - account, err := store.GetAccount(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, account.Id, "account id should match") + account, err := store.GetAccount(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, account.Id, "account id should match") - _, err = store.GetAccount(context.Background(), "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") + _, err = store.GetAccount(context.Background(), "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_SavePeer(t *testing.T) { +func Test_SavePeer(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") - require.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") + require.NoError(t, err) - // save status of non-existing peer - peer := &nbpeer.Peer{ - Key: "peerkey", - ID: "testpeer", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{Hostname: "testingpeer"}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } - ctx := context.Background() - err = store.SavePeer(ctx, 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") + // save status of non-existing peer + peer := &nbpeer.Peer{ + Key: "peerkey", + ID: "testpeer", + IP: net.IP{127, 0, 0, 1}, + Meta: nbpeer.PeerSystemMeta{Hostname: "testingpeer"}, + Name: "peer name", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } + ctx := context.Background() + err = store.SavePeer(ctx, 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") - // save new status of existing peer - account.Peers[peer.ID] = peer + // save new status of existing peer + account.Peers[peer.ID] = peer - err = store.SaveAccount(context.Background(), account) - require.NoError(t, err) + err = store.SaveAccount(context.Background(), account) + require.NoError(t, err) - updatedPeer := peer.Copy() - updatedPeer.Status.Connected = false - updatedPeer.Meta.Hostname = "updatedpeer" + updatedPeer := peer.Copy() + updatedPeer.Status.Connected = false + updatedPeer.Meta.Hostname = "updatedpeer" - err = store.SavePeer(ctx, account.Id, updatedPeer) - require.NoError(t, err) + err = store.SavePeer(ctx, account.Id, updatedPeer) + require.NoError(t, err) - account, err = store.GetAccount(context.Background(), account.Id) - require.NoError(t, err) + account, err = store.GetAccount(context.Background(), account.Id) + require.NoError(t, err) - actual := account.Peers[peer.ID] - assert.Equal(t, updatedPeer.Status, actual.Status) - assert.Equal(t, updatedPeer.Meta, actual.Meta) + actual := account.Peers[peer.ID] + assert.Equal(t, updatedPeer.Status, actual.Status) + assert.Equal(t, updatedPeer.Meta, actual.Meta) + }) } -func TestSqlite_SavePeerStatus(t *testing.T) { +func Test_SavePeerStatus(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") - require.NoError(t, err) - - // save status of non-existing peer - newStatus := nbpeer.PeerStatus{Connected: false, 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") + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") + require.NoError(t, err) - // save new status of existing peer - account.Peers["testpeer"] = &nbpeer.Peer{ - Key: "peerkey", - ID: "testpeer", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + // save status of non-existing peer + newStatus := nbpeer.PeerStatus{Connected: false, 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", + 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(context.Background(), account) - require.NoError(t, err) + err = store.SaveAccount(context.Background(), account) + require.NoError(t, err) - err = store.SavePeerStatus(account.Id, "testpeer", newStatus) - require.NoError(t, err) + err = store.SavePeerStatus(account.Id, "testpeer", newStatus) + require.NoError(t, err) - account, err = store.GetAccount(context.Background(), account.Id) - require.NoError(t, err) + account, err = store.GetAccount(context.Background(), account.Id) + require.NoError(t, err) - actual := account.Peers["testpeer"].Status - assert.Equal(t, newStatus, *actual) + actual := account.Peers["testpeer"].Status + assert.Equal(t, newStatus, *actual) - newStatus.Connected = true + newStatus.Connected = true - err = store.SavePeerStatus(account.Id, "testpeer", newStatus) - require.NoError(t, err) + err = store.SavePeerStatus(account.Id, "testpeer", newStatus) + require.NoError(t, err) - account, err = store.GetAccount(context.Background(), account.Id) - require.NoError(t, err) + account, err = store.GetAccount(context.Background(), account.Id) + require.NoError(t, err) - actual = account.Peers["testpeer"].Status - assert.Equal(t, newStatus, *actual) + actual = account.Peers["testpeer"].Status + assert.Equal(t, newStatus, *actual) + }) } -func TestSqlite_SavePeerLocation(t *testing.T) { +func Test_SavePeerLocation(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") - require.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + account, err := store.GetAccount(context.Background(), "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) + 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(context.Background(), account) - require.NoError(t, err) + account.Peers[peer.ID] = peer + err = store.SaveAccount(context.Background(), 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 + 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) + err = store.SavePeerLocation(account.Id, account.Peers[peer.ID]) + assert.NoError(t, err) - account, err = store.GetAccount(context.Background(), account.Id) - require.NoError(t, err) + account, err = store.GetAccount(context.Background(), account.Id) + require.NoError(t, err) - actual := account.Peers[peer.ID].Location - assert.Equal(t, peer.Location, actual) + 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") + 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) { +func Test_TestGetAccountByPrivateDomain(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + existingDomain := "test.com" - existingDomain := "test.com" + account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) + require.NoError(t, err, "should found account") + require.Equal(t, existingDomain, account.Domain, "domains should match") - account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) - require.NoError(t, err, "should found account") - require.Equal(t, existingDomain, account.Domain, "domains should match") - - _, err = store.GetAccountByPrivateDomain(context.Background(), "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") + _, err = store.GetAccountByPrivateDomain(context.Background(), "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) { +func Test_GetTokenIDByHashedToken(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - hashed := "SoMeHaShEdToKeN" - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + hashed := "SoMeHaShEdToKeN" + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - token, err := store.GetTokenIDByHashedToken(context.Background(), hashed) - require.NoError(t, err) - require.Equal(t, id, token) + token, err := store.GetTokenIDByHashedToken(context.Background(), hashed) + require.NoError(t, err) + require.Equal(t, id, token) - _, err = store.GetTokenIDByHashedToken(context.Background(), "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") + _, err = store.GetTokenIDByHashedToken(context.Background(), "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) { +func Test_GetUserByTokenID(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - - user, err := store.GetUserByTokenID(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, user.PATs[id].ID) + user, err := store.GetUserByTokenID(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, user.PATs[id].ID) - _, err = store.GetUserByTokenID(context.Background(), "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") + _, err = store.GetUserByTokenID(context.Background(), "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) { @@ -762,30 +770,6 @@ func newAccount(store Store, id int) error { return store.SaveAccount(context.Background(), account) } -var cleanUp func() - -func TestMain(m *testing.M) { - - // Start container once for the entire test suite in this package - var err error - cleanUp, err = testutil.CreatePostgresTestContainer() - if err != nil { - // If container fails to start, stop testing immediately - os.Exit(1) - } - - // run all tests - code := m.Run() - - // stop container - if cleanUp != nil { - cleanUp() - } - - // exit with test code - os.Exit(code) -} - func TestPostgresql_NewStore(t *testing.T) { if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { t.Skip("skip CI tests on darwin and windows") From 293bf6e593563040d95dc8b08e67b6991edba418 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sat, 18 Jan 2025 20:30:12 +0100 Subject: [PATCH 06/27] fix local container startup --- management/server/store/sql_store_test.go | 166 ++++++++++++---------- management/server/store/store.go | 76 ++++++---- management/server/testutil/store.go | 2 + 3 files changed, 142 insertions(+), 102 deletions(-) diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 23060e49c6a..180445f23a9 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -62,13 +62,15 @@ func TestMain(m *testing.M) { cleanUpPostgres() } if cleanUpMysql != nil { + os.Unsetenv("NETBIRD_STORE_ENGINE_MYSQL_DSN") + os.Unsetenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN") cleanUpMysql() } os.Exit(code) } -var engines = []Engine{PostgresStoreEngine, SqliteStoreEngine} +var engines = []Engine{SqliteStoreEngine, PostgresStoreEngine} func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) { t.Helper() @@ -76,13 +78,14 @@ func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T if os.Getenv("NETBIRD_STORE_ENGINE") != "" && os.Getenv("NETBIRD_STORE_ENGINE") != string(engine) { continue } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + t.Setenv("NETBIRD_STORE_ENGINE", string(engine)) store, cleanUp, err := NewTestStoreFromSQL(context.Background(), testDataFile, t.TempDir()) t.Cleanup(cleanUp) assert.NoError(t, err) t.Run(string(engine), func(t *testing.T) { f(t, store) }) + os.Unsetenv("NETBIRD_STORE_ENGINE") } } @@ -92,13 +95,16 @@ func Test_NewStore(t *testing.T) { } runTestForAllEngines(t, "", func(t *testing.T, store Store) { + if store == nil { + t.Errorf("expected to create a new Store") + } if len(store.GetAllAccounts(context.Background())) != 0 { t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") } }) } -func TestSqlite_SaveAccount_Large(t *testing.T) { +func Test_SaveAccount_Large(t *testing.T) { if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { t.Skip("skip CI tests on darwin and windows") } @@ -449,103 +455,113 @@ func Test_GetAccount(t *testing.T) { }) } -func Test_SavePeer(t *testing.T) { +func TestSqlite_SavePeerStatus(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { - account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") - require.NoError(t, err) + t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) - // save status of non-existing peer - peer := &nbpeer.Peer{ - Key: "peerkey", - ID: "testpeer", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{Hostname: "testingpeer"}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } - ctx := context.Background() - err = store.SavePeer(ctx, 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") + account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") + require.NoError(t, err) - // save new status of existing peer - account.Peers[peer.ID] = peer + // save status of non-existing peer + newStatus := nbpeer.PeerStatus{Connected: false, 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") - err = store.SaveAccount(context.Background(), account) - require.NoError(t, err) + // save new status of existing peer + account.Peers["testpeer"] = &nbpeer.Peer{ + Key: "peerkey", + ID: "testpeer", + IP: net.IP{127, 0, 0, 1}, + Meta: nbpeer.PeerSystemMeta{}, + Name: "peer name", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } - updatedPeer := peer.Copy() - updatedPeer.Status.Connected = false - updatedPeer.Meta.Hostname = "updatedpeer" + err = store.SaveAccount(context.Background(), account) + require.NoError(t, err) - err = store.SavePeer(ctx, account.Id, updatedPeer) - require.NoError(t, err) + err = store.SavePeerStatus(account.Id, "testpeer", newStatus) + require.NoError(t, err) - account, err = store.GetAccount(context.Background(), account.Id) - require.NoError(t, err) + account, err = store.GetAccount(context.Background(), account.Id) + require.NoError(t, err) - actual := account.Peers[peer.ID] - assert.Equal(t, updatedPeer.Status, actual.Status) - assert.Equal(t, updatedPeer.Meta, actual.Meta) - }) + actual := account.Peers["testpeer"].Status + assert.Equal(t, newStatus, *actual) + + newStatus.Connected = true + + err = store.SavePeerStatus(account.Id, "testpeer", newStatus) + require.NoError(t, err) + + account, err = store.GetAccount(context.Background(), account.Id) + require.NoError(t, err) + + actual = account.Peers["testpeer"].Status + assert.Equal(t, newStatus, *actual) } -func Test_SavePeerStatus(t *testing.T) { +func TestSqlite_SavePeerLocation(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { - account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") - require.NoError(t, err) - - // save status of non-existing peer - newStatus := nbpeer.PeerStatus{Connected: false, 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", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) - err = store.SaveAccount(context.Background(), account) - require.NoError(t, err) + account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") + require.NoError(t, err) - err = store.SavePeerStatus(account.Id, "testpeer", newStatus) - 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, err = store.GetAccount(context.Background(), account.Id) - require.NoError(t, err) + account.Peers[peer.ID] = peer + err = store.SaveAccount(context.Background(), account) + require.NoError(t, err) - actual := account.Peers["testpeer"].Status - assert.Equal(t, newStatus, *actual) + peer.Location.ConnectionIP = net.ParseIP("35.1.1.1") + peer.Location.CountryCode = "DE" + peer.Location.CityName = "Berlin" + peer.Location.GeoNameID = 2950159 - newStatus.Connected = true + err = store.SavePeerLocation(account.Id, account.Peers[peer.ID]) + assert.NoError(t, err) - err = store.SavePeerStatus(account.Id, "testpeer", newStatus) - require.NoError(t, err) + account, err = store.GetAccount(context.Background(), account.Id) + require.NoError(t, err) - account, err = store.GetAccount(context.Background(), account.Id) - require.NoError(t, err) + actual := account.Peers[peer.ID].Location + assert.Equal(t, peer.Location, actual) - actual = account.Peers["testpeer"].Status - assert.Equal(t, newStatus, *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 Test_SavePeerLocation(t *testing.T) { diff --git a/management/server/store/store.go b/management/server/store/store.go index 3a483096b28..4a1ab85205b 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -16,6 +16,7 @@ import ( "time" log "github.com/sirupsen/logrus" + "gorm.io/driver/mysql" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" @@ -338,51 +339,34 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) { if kind == PostgresStoreEngine { var cleanUp func() + removeContainer := false if envDsn, ok := os.LookupEnv(postgresDsnEnv); !ok || envDsn == "" { var err error cleanUp, err = testutil.CreatePostgresTestContainer() if err != nil { return nil, nil, err } + removeContainer = true } dsn, ok := os.LookupEnv(postgresDsnEnv) if !ok { - return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv) + return nil, cleanUp, fmt.Errorf("%s is not set", postgresDsnEnv) } db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { - return nil, nil, fmt.Errorf("failed to open postgres connection: %v", err) + return nil, cleanUp, fmt.Errorf("failed to open postgres connection: %v", err) } - rand.Seed(time.Now().UnixNano()) - dbName := fmt.Sprintf("test_db_%d", rand.Intn(1e6)) - - if err := db.Exec(fmt.Sprintf("CREATE DATABASE %q", dbName)).Error; err != nil { - return nil, nil, fmt.Errorf("failed to create database: %v", err) - } - - u, err := url.Parse(dsn) + newDsn, cleanup, err := createRandomDB(dsn, db, cleanUp, removeContainer) if err != nil { - return nil, nil, fmt.Errorf("failed to parse DSN: %v", err) + return nil, cleanup, err } - u.Path = dbName - - dsn = u.String() - - store, err := NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil) + store, err := NewPostgresqlStoreFromSqlStore(ctx, store, newDsn, nil) if err != nil { - return nil, nil, err - } - - cleanup := func() { - db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)) - if cleanUp != nil { - os.Setenv(postgresDsnEnv, "") - cleanUp() - } + return nil, cleanup, err } return store, cleanup, nil @@ -390,12 +374,14 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store if kind == MysqlStoreEngine { var cleanUp func() + removeContainer := false if _, ok := os.LookupEnv(mysqlDsnEnv); !ok { var err error cleanUp, err = testutil.CreateMysqlTestContainer() if err != nil { return nil, nil, err } + removeContainer = true } dsn, ok := os.LookupEnv(mysqlDsnEnv) @@ -403,12 +389,22 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv) } - store, err := NewMysqlStoreFromSqlStore(ctx, store, dsn, nil) + db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig()) + if err != nil { + return nil, cleanUp, fmt.Errorf("failed to open mysql connection: %v", err) + } + + newDsn, cleanup, err := createRandomDB(dsn, db, cleanUp, removeContainer) + if err != nil { + return nil, cleanup, err + } + + store, err := NewMysqlStoreFromSqlStore(ctx, store, newDsn, nil) if err != nil { return nil, nil, err } - return store, cleanUp, nil + return store, cleanup, nil } closeConnection := func() { @@ -418,6 +414,32 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return store, closeConnection, nil } +func createRandomDB(dsn string, db *gorm.DB, cleanUp func(), removeContainer bool) (string, func(), error) { + rand.Seed(time.Now().UnixNano()) + dbName := fmt.Sprintf("test_db_%d", rand.Intn(1e6)) + + if err := db.Exec(fmt.Sprintf("CREATE DATABASE %s", dbName)).Error; err != nil { + return "", cleanUp, fmt.Errorf("failed to create database: %v", err) + } + + u, err := url.Parse(dsn) + if err != nil { + return "", cleanUp, fmt.Errorf("failed to parse DSN: %v", err) + } + + u.Path = dbName + + cleanup := func() { + db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)) + if cleanUp != nil && removeContainer { + cleanUp() + } + } + + return u.String(), cleanup, nil + +} + func loadSQL(db *gorm.DB, filepath string) error { sqlContent, err := os.ReadFile(filepath) if err != nil { diff --git a/management/server/testutil/store.go b/management/server/testutil/store.go index 16438cab8f0..382b5fb3d0f 100644 --- a/management/server/testutil/store.go +++ b/management/server/testutil/store.go @@ -34,6 +34,7 @@ func CreateMysqlTestContainer() (func(), error) { } cleanup := func() { + os.Unsetenv("NETBIRD_STORE_ENGINE_MYSQL_DSN") timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second) defer cancelFunc() if err = myContainer.Terminate(timeoutCtx); err != nil { @@ -68,6 +69,7 @@ func CreatePostgresTestContainer() (func(), error) { } cleanup := func() { + os.Unsetenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN") timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second) defer cancelFunc() if err = pgContainer.Terminate(timeoutCtx); err != nil { From 70bd33f9e7ebd5dbe4a4df1abe5f3d3db5daafa2 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sat, 18 Jan 2025 21:29:03 +0100 Subject: [PATCH 07/27] explicitly start postgres on github --- .github/workflows/golang-test-linux.yml | 32 +++++---- management/server/route_test.go | 5 +- management/server/store/sql_store_test.go | 82 ++++------------------- management/server/store/store.go | 2 +- 4 files changed, 32 insertions(+), 89 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 106b1bf5890..c32282b71e0 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -144,22 +144,6 @@ jobs: arch: [ '386','amd64' ] store: [ 'sqlite', 'postgres', 'mysql' ] runs-on: ubuntu-22.04 - - services: - postgres: - image: postgres:15-alpine - env: - POSTGRES_USER: root - POSTGRES_PASSWORD: netbird - POSTGRES_DB: netbird - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U root -d netbird" - --health-interval 5s - --health-timeout 5s - --health-retries 5 - steps: - name: Install Go uses: actions/setup-go@v5 @@ -209,10 +193,24 @@ jobs: if: matrix.store == 'mysql' run: docker pull mlsmaycon/warmed-mysql:8 + - name: Start Postgres + if: matrix.store == 'postgres' + run: | + docker run -d \ + -e POSTGRES_USER=root \ + -e POSTGRES_PASSWORD=netbird \ + -e POSTGRES_DB=netbird \ + -p 5432:5432 \ + --name my-postgres \ + postgres:15-alpine + - name: Wait for Postgres + if: matrix.store == 'postgres' run: | for i in {1..10}; do - nc -z localhost 5432 && break + if nc -z localhost 5432; then + break + fi echo "Waiting for Postgres..." sleep 3 done diff --git a/management/server/route_test.go b/management/server/route_test.go index 1c5c56f6069..40e0f41b029 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -13,12 +13,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/netbirdio/netbird/management/domain" + "github.com/netbirdio/netbird/management/server/activity" 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" - - "github.com/netbirdio/netbird/management/domain" - "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/telemetry" diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 180445f23a9..acb33a70ec6 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -35,10 +35,9 @@ import ( nbroute "github.com/netbirdio/netbird/route" ) -var cleanUpPostgres func() -var cleanUpMysql func() - func TestMain(m *testing.M) { + var cleanUpPostgres func() + // var cleanUpMysql func() if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_POSTGRES_DSN"); !ok || dsn == "" { var err error @@ -47,30 +46,28 @@ func TestMain(m *testing.M) { os.Exit(1) } } - - if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_MYSQL_DSN"); !ok || dsn == "" { - var err error - cleanUpMysql, err = testutil.CreateMysqlTestContainer() - if err != nil { - os.Exit(1) - } - } + // + // if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_MYSQL_DSN"); !ok || dsn == "" { + // var err error + // cleanUpMysql, err = testutil.CreateMysqlTestContainer() + // if err != nil { + // os.Exit(1) + // } + // } code := m.Run() if cleanUpPostgres != nil { cleanUpPostgres() } - if cleanUpMysql != nil { - os.Unsetenv("NETBIRD_STORE_ENGINE_MYSQL_DSN") - os.Unsetenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN") - cleanUpMysql() - } + // if cleanUpMysql != nil { + // cleanUpMysql() + // } os.Exit(code) } -var engines = []Engine{SqliteStoreEngine, PostgresStoreEngine} +var engines = []Engine{SqliteStoreEngine, PostgresStoreEngine, MysqlStoreEngine} func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) { t.Helper() @@ -564,57 +561,6 @@ func TestSqlite_SavePeerLocation(t *testing.T) { require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } -func Test_SavePeerLocation(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("The SQLite store is not properly supported by Windows yet") - } - - runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { - account, err := store.GetAccount(context.Background(), "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(context.Background(), 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(context.Background(), 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 Test_TestGetAccountByPrivateDomain(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") diff --git a/management/server/store/store.go b/management/server/store/store.go index 4a1ab85205b..0c176815866 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -375,7 +375,7 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store if kind == MysqlStoreEngine { var cleanUp func() removeContainer := false - if _, ok := os.LookupEnv(mysqlDsnEnv); !ok { + if envDsn, ok := os.LookupEnv(mysqlDsnEnv); !ok || envDsn == "" { var err error cleanUp, err = testutil.CreateMysqlTestContainer() if err != nil { From 009e74216653314f943016e73080f35929068ddc Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sat, 18 Jan 2025 22:14:48 +0100 Subject: [PATCH 08/27] revert tests to old version --- management/server/store/sql_store_test.go | 536 ++++++++++++---------- 1 file changed, 282 insertions(+), 254 deletions(-) diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index acb33a70ec6..5928b45baa7 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -24,7 +24,6 @@ import ( routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/management/server/testutil" "github.com/netbirdio/netbird/management/server/types" route2 "github.com/netbirdio/netbird/route" @@ -35,78 +34,40 @@ import ( nbroute "github.com/netbirdio/netbird/route" ) -func TestMain(m *testing.M) { - var cleanUpPostgres func() - // var cleanUpMysql func() - - if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_POSTGRES_DSN"); !ok || dsn == "" { - var err error - cleanUpPostgres, err = testutil.CreatePostgresTestContainer() - if err != nil { - os.Exit(1) - } +func TestSqlite_NewStore(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") } - // - // if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_MYSQL_DSN"); !ok || dsn == "" { - // var err error - // cleanUpMysql, err = testutil.CreateMysqlTestContainer() - // if err != nil { - // os.Exit(1) - // } - // } - code := m.Run() + t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) - if cleanUpPostgres != nil { - cleanUpPostgres() + if len(store.GetAllAccounts(context.Background())) != 0 { + t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") } - // if cleanUpMysql != nil { - // cleanUpMysql() - // } - - os.Exit(code) } -var engines = []Engine{SqliteStoreEngine, PostgresStoreEngine, MysqlStoreEngine} +func TestSqlite_SaveAccount_Large(t *testing.T) { + if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { + t.Skip("skip CI tests on darwin and windows") + } -func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) { - t.Helper() - for _, engine := range engines { - if os.Getenv("NETBIRD_STORE_ENGINE") != "" && os.Getenv("NETBIRD_STORE_ENGINE") != string(engine) { - continue - } - t.Setenv("NETBIRD_STORE_ENGINE", string(engine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), testDataFile, t.TempDir()) + t.Run("SQLite", func(t *testing.T) { + t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) t.Cleanup(cleanUp) assert.NoError(t, err) - t.Run(string(engine), func(t *testing.T) { - f(t, store) - }) - os.Unsetenv("NETBIRD_STORE_ENGINE") - } -} - -func Test_NewStore(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("The SQLite store is not properly supported by Windows yet") - } - - runTestForAllEngines(t, "", func(t *testing.T, store Store) { - if store == nil { - t.Errorf("expected to create a new Store") - } - if len(store.GetAllAccounts(context.Background())) != 0 { - t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") - } + runLargeTest(t, store) }) -} - -func Test_SaveAccount_Large(t *testing.T) { - if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { - t.Skip("skip CI tests on darwin and windows") - } - runTestForAllEngines(t, "", func(t *testing.T, store Store) { + // create store outside to have a better time counter for the test + t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) + t.Run("PostgreSQL", func(t *testing.T) { runLargeTest(t, store) }) } @@ -251,205 +212,264 @@ func randomIPv4() net.IP { return net.IP(b) } -func Test_SaveAccount(t *testing.T) { +func TestSqlite_SaveAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - runTestForAllEngines(t, "", func(t *testing.T, store Store) { - account := newAccountWithId(context.Background(), "account_id", "testuser", "") - setupKey, _ := types.GenerateDefaultSetupKey() - account.SetupKeys[setupKey.Key] = setupKey - account.Peers["testpeer"] = &nbpeer.Peer{ - Key: "peerkey", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) - err := store.SaveAccount(context.Background(), account) - require.NoError(t, err) + account := newAccountWithId(context.Background(), "account_id", "testuser", "") + setupKey, _ := types.GenerateDefaultSetupKey() + account.SetupKeys[setupKey.Key] = setupKey + account.Peers["testpeer"] = &nbpeer.Peer{ + Key: "peerkey", + IP: net.IP{127, 0, 0, 1}, + Meta: nbpeer.PeerSystemMeta{}, + Name: "peer name", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } - account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "") - setupKey, _ = types.GenerateDefaultSetupKey() - account2.SetupKeys[setupKey.Key] = setupKey - account2.Peers["testpeer2"] = &nbpeer.Peer{ - Key: "peerkey2", - 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(context.Background(), account) + require.NoError(t, err) - err = store.SaveAccount(context.Background(), account2) - require.NoError(t, err) + account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "") + setupKey, _ = types.GenerateDefaultSetupKey() + account2.SetupKeys[setupKey.Key] = setupKey + account2.Peers["testpeer2"] = &nbpeer.Peer{ + Key: "peerkey2", + IP: net.IP{127, 0, 0, 2}, + Meta: nbpeer.PeerSystemMeta{}, + Name: "peer name 2", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } - if len(store.GetAllAccounts(context.Background())) != 2 { - t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") - } + err = store.SaveAccount(context.Background(), account2) + require.NoError(t, err) - a, err := store.GetAccount(context.Background(), account.Id) - if a == nil { - t.Errorf("expecting Account to be stored after SaveAccount(): %v", err) - } + if len(store.GetAllAccounts(context.Background())) != 2 { + t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") + } - if a != nil && len(a.Policies) != 1 { - t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies)) - } + a, err := store.GetAccount(context.Background(), account.Id) + if a == nil { + t.Errorf("expecting Account to be stored after SaveAccount(): %v", err) + } - 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 != nil && len(a.Policies) != 1 { + t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies)) + } - if a, err := store.GetAccountByPeerPubKey(context.Background(), "peerkey"); a == nil { - t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) - } + 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.GetAccountByUser(context.Background(), "testuser"); a == nil { - t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByPeerPubKey(context.Background(), "peerkey"); a == nil { + t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil { - t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil { + t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil { - t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) - } - }) + if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil { + t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) + } + + if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil { + t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) + } } -func Test_DeleteAccount(t *testing.T) { +func TestSqlite_DeleteAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - runTestForAllEngines(t, "", func(t *testing.T, store Store) { - testUserID := "testuser" - user := types.NewAdminUser(testUserID) - user.PATs = map[string]*types.PersonalAccessToken{"testtoken": { - ID: "testtoken", - Name: "test token", - }} + t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) - account := newAccountWithId(context.Background(), "account_id", testUserID, "") - setupKey, _ := types.GenerateDefaultSetupKey() - account.SetupKeys[setupKey.Key] = setupKey - account.Peers["testpeer"] = &nbpeer.Peer{ - Key: "peerkey", - 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 - account.Networks = []*networkTypes.Network{ - { - ID: "network_id", - AccountID: account.Id, - Name: "network name", - Description: "network description", - }, - } - account.NetworkRouters = []*routerTypes.NetworkRouter{ - { - ID: "router_id", - NetworkID: account.Networks[0].ID, - AccountID: account.Id, - PeerGroups: []string{"group_id"}, - Masquerade: true, - Metric: 1, - }, - } - account.NetworkResources = []*resourceTypes.NetworkResource{ - { - ID: "resource_id", - NetworkID: account.Networks[0].ID, - AccountID: account.Id, - Name: "Name", - Description: "Description", - Type: "Domain", - Address: "example.com", - }, - } + testUserID := "testuser" + user := types.NewAdminUser(testUserID) + user.PATs = map[string]*types.PersonalAccessToken{"testtoken": { + ID: "testtoken", + Name: "test token", + }} - err := store.SaveAccount(context.Background(), account) - require.NoError(t, err) + account := newAccountWithId(context.Background(), "account_id", testUserID, "") + setupKey, _ := types.GenerateDefaultSetupKey() + account.SetupKeys[setupKey.Key] = setupKey + account.Peers["testpeer"] = &nbpeer.Peer{ + Key: "peerkey", + 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 + account.Networks = []*networkTypes.Network{ + { + ID: "network_id", + AccountID: account.Id, + Name: "network name", + Description: "network description", + }, + } + account.NetworkRouters = []*routerTypes.NetworkRouter{ + { + ID: "router_id", + NetworkID: account.Networks[0].ID, + AccountID: account.Id, + PeerGroups: []string{"group_id"}, + Masquerade: true, + Metric: 1, + }, + } + account.NetworkResources = []*resourceTypes.NetworkResource{ + { + ID: "resource_id", + NetworkID: account.Networks[0].ID, + AccountID: account.Id, + Name: "Name", + Description: "Description", + Type: "Domain", + Address: "example.com", + }, + } - if len(store.GetAllAccounts(context.Background())) != 1 { - t.Errorf("expecting 1 Accounts to be stored after SaveAccount()") - } + err = store.SaveAccount(context.Background(), account) + require.NoError(t, err) - err = store.DeleteAccount(context.Background(), account) - require.NoError(t, err) + if len(store.GetAllAccounts(context.Background())) != 1 { + t.Errorf("expecting 1 Accounts to be stored after SaveAccount()") + } - if len(store.GetAllAccounts(context.Background())) != 0 { - t.Errorf("expecting 0 Accounts to be stored after DeleteAccount()") - } + err = store.DeleteAccount(context.Background(), account) + require.NoError(t, err) - _, err = store.GetAccountByPeerPubKey(context.Background(), "peerkey") - require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer public key") + if len(store.GetAllAccounts(context.Background())) != 0 { + t.Errorf("expecting 0 Accounts to be stored after DeleteAccount()") + } - _, err = store.GetAccountByUser(context.Background(), "testuser") - require.Error(t, err, "expecting error after removing DeleteAccount when getting account by user") + _, err = store.GetAccountByPeerPubKey(context.Background(), "peerkey") + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer public key") - _, err = store.GetAccountByPeerID(context.Background(), "testpeer") - require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer id") + _, err = store.GetAccountByUser(context.Background(), "testuser") + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by user") - _, err = store.GetAccountBySetupKey(context.Background(), setupKey.Key) - require.Error(t, err, "expecting error after removing DeleteAccount when getting account by setup key") + _, err = store.GetAccountByPeerID(context.Background(), "testpeer") + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by peer id") - _, err = store.GetAccount(context.Background(), account.Id) - require.Error(t, err, "expecting error after removing DeleteAccount when getting account by id") + _, err = store.GetAccountBySetupKey(context.Background(), setupKey.Key) + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by setup key") - for _, policy := range account.Policies { - var rules []*types.PolicyRule - err = store.(*SqlStore).db.Model(&types.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") + _, err = store.GetAccount(context.Background(), account.Id) + require.Error(t, err, "expecting error after removing DeleteAccount when getting account by id") - } + for _, policy := range account.Policies { + var rules []*types.PolicyRule + err = store.(*SqlStore).db.Model(&types.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 []*types.PersonalAccessToken - err = store.(*SqlStore).db.Model(&types.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") + } - } + for _, accountUser := range account.Users { + var pats []*types.PersonalAccessToken + err = store.(*SqlStore).db.Model(&types.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") - for _, network := range account.Networks { - routers, err := store.GetNetworkRoutersByNetID(context.Background(), LockingStrengthShare, account.Id, network.ID) - require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for network routers") - require.Len(t, routers, 0, "expecting no network routers to be found after DeleteAccount") + } - resources, err := store.GetNetworkResourcesByNetID(context.Background(), LockingStrengthShare, account.Id, network.ID) - require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for network resources") - require.Len(t, resources, 0, "expecting no network resources to be found after DeleteAccount") - } - }) + for _, network := range account.Networks { + routers, err := store.GetNetworkRoutersByNetID(context.Background(), LockingStrengthShare, account.Id, network.ID) + require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for network routers") + require.Len(t, routers, 0, "expecting no network routers to be found after DeleteAccount") + + resources, err := store.GetNetworkResourcesByNetID(context.Background(), LockingStrengthShare, account.Id, network.ID) + require.NoError(t, err, "expecting no error after removing DeleteAccount when searching for network resources") + require.Len(t, resources, 0, "expecting no network resources to be found after DeleteAccount") + } } -func Test_GetAccount(t *testing.T) { +func TestSqlite_GetAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { - id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" + t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) + + id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - account, err := store.GetAccount(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, account.Id, "account id should match") + account, err := store.GetAccount(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, account.Id, "account id should match") - _, err = store.GetAccount(context.Background(), "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") - }) + _, err = store.GetAccount(context.Background(), "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_SavePeer(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) + + account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") + require.NoError(t, err) + + // save status of non-existing peer + peer := &nbpeer.Peer{ + Key: "peerkey", + ID: "testpeer", + IP: net.IP{127, 0, 0, 1}, + Meta: nbpeer.PeerSystemMeta{Hostname: "testingpeer"}, + Name: "peer name", + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } + ctx := context.Background() + err = store.SavePeer(ctx, 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") + + // save new status of existing peer + account.Peers[peer.ID] = peer + + err = store.SaveAccount(context.Background(), account) + require.NoError(t, err) + + updatedPeer := peer.Copy() + updatedPeer.Status.Connected = false + updatedPeer.Meta.Hostname = "updatedpeer" + + err = store.SavePeer(ctx, account.Id, updatedPeer) + require.NoError(t, err) + + account, err = store.GetAccount(context.Background(), account.Id) + require.NoError(t, err) + + actual := account.Peers[peer.ID] + assert.Equal(t, updatedPeer.Status, actual.Status) + assert.Equal(t, updatedPeer.Meta, actual.Meta) } func TestSqlite_SavePeerStatus(t *testing.T) { @@ -561,65 +581,74 @@ func TestSqlite_SavePeerLocation(t *testing.T) { require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } -func Test_TestGetAccountByPrivateDomain(t *testing.T) { +func TestSqlite_TestGetAccountByPrivateDomain(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { - existingDomain := "test.com" + t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) + + existingDomain := "test.com" - account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) - require.NoError(t, err, "should found account") - require.Equal(t, existingDomain, account.Domain, "domains should match") + account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) + require.NoError(t, err, "should found account") + require.Equal(t, existingDomain, account.Domain, "domains should match") - _, err = store.GetAccountByPrivateDomain(context.Background(), "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") - }) + _, err = store.GetAccountByPrivateDomain(context.Background(), "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 Test_GetTokenIDByHashedToken(t *testing.T) { +func TestSqlite_GetTokenIDByHashedToken(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { - hashed := "SoMeHaShEdToKeN" - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) - token, err := store.GetTokenIDByHashedToken(context.Background(), hashed) - require.NoError(t, err) - require.Equal(t, id, token) + hashed := "SoMeHaShEdToKeN" + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - _, err = store.GetTokenIDByHashedToken(context.Background(), "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") - }) + token, err := store.GetTokenIDByHashedToken(context.Background(), hashed) + require.NoError(t, err) + require.Equal(t, id, token) + + _, err = store.GetTokenIDByHashedToken(context.Background(), "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 Test_GetUserByTokenID(t *testing.T) { +func TestSqlite_GetUserByTokenID(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) + t.Cleanup(cleanUp) + assert.NoError(t, err) - user, err := store.GetUserByTokenID(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, user.PATs[id].ID) + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - _, err = store.GetUserByTokenID(context.Background(), "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") - }) + user, err := store.GetUserByTokenID(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, user.PATs[id].ID) + + _, err = store.GetUserByTokenID(context.Background(), "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) { @@ -2341,7 +2370,6 @@ func TestSqlStore_GetNetworkRouterByID(t *testing.T) { } func TestSqlStore_SaveNetworkRouter(t *testing.T) { - t.Setenv("NETBIRD_STORE_ENGINE", string(PostgresStoreEngine)) store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) t.Cleanup(cleanup) require.NoError(t, err) From c2834929b54a933447423fd3e53fc7673296e434 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sat, 18 Jan 2025 22:26:37 +0100 Subject: [PATCH 09/27] update workflow --- .github/workflows/golang-test-linux.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index c32282b71e0..c941b0b8696 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -216,7 +216,21 @@ jobs: done - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} NETBIRD_STORE_ENGINE_POSTGRES_DSN="postgres://root:netbird@localhost:5432/netbird?sslmode=disable" CI=true go test -tags=devcert -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN' -timeout 10m $(go list ./... | grep /management) + run: | + # For Postgres: pass DSN, etc. + if [ "${{ matrix.store }}" = "postgres" ]; then + export NETBIRD_STORE_ENGINE_POSTGRES_DSN="postgres://root:netbird@localhost:5432/netbird?sslmode=disable" + fi + + # For MySQL: maybe do `docker run ...` similarly or skip entirely + # For SQLite: no container + + # run tests + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} \ + go test -tags=devcert -p 1 \ + -exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN" \ + -timeout 10m $(go list ./... | grep /management) benchmark: needs: [ build-cache ] From 6644de942d59cea8b6f5768015d394a843d34d01 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sat, 18 Jan 2025 22:42:24 +0100 Subject: [PATCH 10/27] remove drop with force --- management/server/store/store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/server/store/store.go b/management/server/store/store.go index 0c176815866..02048546a00 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -430,7 +430,7 @@ func createRandomDB(dsn string, db *gorm.DB, cleanUp func(), removeContainer boo u.Path = dbName cleanup := func() { - db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)) + db.Exec(fmt.Sprintf("DROP DATABASE %s", dbName)) if cleanUp != nil && removeContainer { cleanUp() } From 9258adb3785512f52b4093ab243140291c3ced44 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 00:00:22 +0100 Subject: [PATCH 11/27] close db connection --- management/server/store/store.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/management/server/store/store.go b/management/server/store/store.go index 02048546a00..1f4f270d748 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -430,7 +430,9 @@ func createRandomDB(dsn string, db *gorm.DB, cleanUp func(), removeContainer boo u.Path = dbName cleanup := func() { - db.Exec(fmt.Sprintf("DROP DATABASE %s", dbName)) + sqlDB, _ := db.DB() + _ = sqlDB.Close() + db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)) if cleanUp != nil && removeContainer { cleanUp() } From 5cffe78ae068ca3d72b8aca28df0796ba45bd180 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 00:05:49 +0100 Subject: [PATCH 12/27] add mysql container and fix close --- .github/workflows/golang-test-linux.yml | 32 ++++++++++++++++++++----- management/server/store/store.go | 2 +- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index c941b0b8696..48390c0d787 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -215,21 +215,41 @@ jobs: sleep 3 done + - name: Start MySQL + if: matrix.store == 'mysql' + run: | + docker run -d \ + -e MYSQL_ROOT_PASSWORD=netbird \ + -e MYSQL_DATABASE=netbird \ + -p 3306:3306 \ + --name my-mysql \ + mlsmaycon/warmed-mysql:8 + + - name: Wait for MySQL + if: matrix.store == 'mysql' + run: | + for i in {1..10}; do + if nc -z localhost 3306; then + break + fi + echo "Waiting for MySQL..." + sleep 3 + done + - name: Test run: | - # For Postgres: pass DSN, etc. if [ "${{ matrix.store }}" = "postgres" ]; then export NETBIRD_STORE_ENGINE_POSTGRES_DSN="postgres://root:netbird@localhost:5432/netbird?sslmode=disable" fi + + if [ "${{ matrix.store }}" = "mysql" ]; then + export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:netbird@tcp(localhost:3306)/netbird?parseTime=True" + fi - # For MySQL: maybe do `docker run ...` similarly or skip entirely - # For SQLite: no container - - # run tests CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ NETBIRD_STORE_ENGINE=${{ matrix.store }} \ go test -tags=devcert -p 1 \ - -exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN" \ + -exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN, NETBIRD_STORE_ENGINE_MYSQL_DSN" \ -timeout 10m $(go list ./... | grep /management) benchmark: diff --git a/management/server/store/store.go b/management/server/store/store.go index 1f4f270d748..82d7806202e 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -430,9 +430,9 @@ func createRandomDB(dsn string, db *gorm.DB, cleanUp func(), removeContainer boo u.Path = dbName cleanup := func() { + db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)) sqlDB, _ := db.DB() _ = sqlDB.Close() - db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)) if cleanUp != nil && removeContainer { cleanUp() } From 58e00bb25e7e725d1753d630d12852ccaf2a5393 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 00:11:25 +0100 Subject: [PATCH 13/27] fix typo --- .github/workflows/golang-test-linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 48390c0d787..5be7b0e7605 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -249,7 +249,7 @@ jobs: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ NETBIRD_STORE_ENGINE=${{ matrix.store }} \ go test -tags=devcert -p 1 \ - -exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN, NETBIRD_STORE_ENGINE_MYSQL_DSN" \ + -exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN" \ -timeout 10m $(go list ./... | grep /management) benchmark: From 5eb5e37b238c7926fe8bcafcf9bfc66ae103fbff Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 00:23:17 +0100 Subject: [PATCH 14/27] enable for other workflows --- .github/workflows/golang-test-linux.yml | 137 +++++++++++++++++++++++- management/server/store/store.go | 2 +- 2 files changed, 134 insertions(+), 5 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 5be7b0e7605..21997c282df 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -309,8 +309,51 @@ jobs: if: matrix.store == 'mysql' run: docker pull mlsmaycon/warmed-mysql:8 + - name: Start Postgres + if: matrix.store == 'postgres' + run: | + docker run -d \ + -e POSTGRES_USER=root \ + -e POSTGRES_PASSWORD=netbird \ + -e POSTGRES_DB=netbird \ + -p 5432:5432 \ + --name my-postgres \ + postgres:15-alpine + + - name: Wait for Postgres + if: matrix.store == 'postgres' + run: | + for i in {1..10}; do + if nc -z localhost 5432; then + break + fi + echo "Waiting for Postgres..." + sleep 3 + done + + - name: Start MySQL + if: matrix.store == 'mysql' + run: | + docker run -d \ + -e MYSQL_ROOT_PASSWORD=netbird \ + -e MYSQL_DATABASE=netbird \ + -p 3306:3306 \ + --name my-mysql \ + mlsmaycon/warmed-mysql:8 + + - name: Wait for MySQL + if: matrix.store == 'mysql' + run: | + for i in {1..10}; do + if nc -z localhost 3306; then + break + fi + echo "Waiting for MySQL..." + sleep 3 + done + - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags devcert -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 20m ./... + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags devcert -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' -timeout 20m ./... api_benchmark: needs: [ build-cache ] @@ -368,9 +411,52 @@ jobs: - name: download mysql image if: matrix.store == 'mysql' run: docker pull mlsmaycon/warmed-mysql:8 - + + - name: Start Postgres + if: matrix.store == 'postgres' + run: | + docker run -d \ + -e POSTGRES_USER=root \ + -e POSTGRES_PASSWORD=netbird \ + -e POSTGRES_DB=netbird \ + -p 5432:5432 \ + --name my-postgres \ + postgres:15-alpine + + - name: Wait for Postgres + if: matrix.store == 'postgres' + run: | + for i in {1..10}; do + if nc -z localhost 5432; then + break + fi + echo "Waiting for Postgres..." + sleep 3 + done + + - name: Start MySQL + if: matrix.store == 'mysql' + run: | + docker run -d \ + -e MYSQL_ROOT_PASSWORD=netbird \ + -e MYSQL_DATABASE=netbird \ + -p 3306:3306 \ + --name my-mysql \ + mlsmaycon/warmed-mysql:8 + + - name: Wait for MySQL + if: matrix.store == 'mysql' + run: | + for i in {1..10}; do + if nc -z localhost 3306; then + break + fi + echo "Waiting for MySQL..." + sleep 3 + done + - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=benchmark -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management) + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=benchmark -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' -timeout 10m $(go list ./... | grep /management) api_integration_test: needs: [ build-cache ] @@ -418,8 +504,51 @@ jobs: - name: check git status run: git --no-pager diff --exit-code + - name: Start Postgres + if: matrix.store == 'postgres' + run: | + docker run -d \ + -e POSTGRES_USER=root \ + -e POSTGRES_PASSWORD=netbird \ + -e POSTGRES_DB=netbird \ + -p 5432:5432 \ + --name my-postgres \ + postgres:15-alpine + + - name: Wait for Postgres + if: matrix.store == 'postgres' + run: | + for i in {1..10}; do + if nc -z localhost 5432; then + break + fi + echo "Waiting for Postgres..." + sleep 3 + done + + - name: Start MySQL + if: matrix.store == 'mysql' + run: | + docker run -d \ + -e MYSQL_ROOT_PASSWORD=netbird \ + -e MYSQL_DATABASE=netbird \ + -p 3306:3306 \ + --name my-mysql \ + mlsmaycon/warmed-mysql:8 + + - name: Wait for MySQL + if: matrix.store == 'mysql' + run: | + for i in {1..10}; do + if nc -z localhost 3306; then + break + fi + echo "Waiting for MySQL..." + sleep 3 + done + - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m -tags=integration $(go list ./... | grep /management) + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' -timeout 10m -tags=integration $(go list ./... | grep /management) test_client_on_docker: needs: [ build-cache ] diff --git a/management/server/store/store.go b/management/server/store/store.go index 82d7806202e..d59f5ccc41e 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -389,7 +389,7 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv) } - db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig()) + db, err := gorm.Open(mysql.Open(dsn), getGormConfig()) if err != nil { return nil, cleanUp, fmt.Errorf("failed to open mysql connection: %v", err) } From de920b38e898e94b354e4f6388caeb5940ca099e Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 00:39:30 +0100 Subject: [PATCH 15/27] grant permissions --- .github/workflows/golang-test-linux.yml | 49 ++++++++++++++++++++++++- management/server/store/store.go | 1 - 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 21997c282df..e7b4997865e 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -230,6 +230,9 @@ jobs: run: | for i in {1..10}; do if nc -z localhost 3306; then + docker exec my-mysql \ + mysql -u root -pnetbird -e \ + "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'netbird' WITH GRANT OPTION; FLUSH PRIVILEGES;" break fi echo "Waiting for MySQL..." @@ -353,7 +356,20 @@ jobs: done - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags devcert -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' -timeout 20m ./... + run: | + if [ "${{ matrix.store }}" = "postgres" ]; then + export NETBIRD_STORE_ENGINE_POSTGRES_DSN="postgres://root:netbird@localhost:5432/netbird?sslmode=disable" + fi + + if [ "${{ matrix.store }}" = "mysql" ]; then + export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:netbird@tcp(localhost:3306)/netbird?parseTime=True" + fi + + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ + go test -tags devcert -run=^$ -bench=. \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' \ + -timeout 10m ./... api_benchmark: needs: [ build-cache ] @@ -458,6 +474,22 @@ jobs: - name: Test run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=benchmark -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' -timeout 10m $(go list ./... | grep /management) + - name: Test + run: | + if [ "${{ matrix.store }}" = "postgres" ]; then + export NETBIRD_STORE_ENGINE_POSTGRES_DSN="postgres://root:netbird@localhost:5432/netbird?sslmode=disable" + fi + + if [ "${{ matrix.store }}" = "mysql" ]; then + export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:netbird@tcp(localhost:3306)/netbird?parseTime=True" + fi + + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ + go test -tags=benchmark -run=^$ -bench=. \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' \ + -timeout 10m $(go list ./... | grep /management) + api_integration_test: needs: [ build-cache ] strategy: @@ -549,6 +581,21 @@ jobs: - name: Test run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' -timeout 10m -tags=integration $(go list ./... | grep /management) + - name: Test + run: | + if [ "${{ matrix.store }}" = "postgres" ]; then + export NETBIRD_STORE_ENGINE_POSTGRES_DSN="postgres://root:netbird@localhost:5432/netbird?sslmode=disable" + fi + + if [ "${{ matrix.store }}" = "mysql" ]; then + export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:netbird@tcp(localhost:3306)/netbird?parseTime=True" + fi + + CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ + NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ + go test -p 1 \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' \ + -timeout 10m -tags=integration $(go list ./... | grep /management) test_client_on_docker: needs: [ build-cache ] diff --git a/management/server/store/store.go b/management/server/store/store.go index d59f5ccc41e..dc38c9e928a 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -415,7 +415,6 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store } func createRandomDB(dsn string, db *gorm.DB, cleanUp func(), removeContainer bool) (string, func(), error) { - rand.Seed(time.Now().UnixNano()) dbName := fmt.Sprintf("test_db_%d", rand.Intn(1e6)) if err := db.Exec(fmt.Sprintf("CREATE DATABASE %s", dbName)).Error; err != nil { From cd4e191e367c9875307fe03ed17649949c02e0a4 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 00:48:33 +0100 Subject: [PATCH 16/27] wait for mysql to be ready --- .github/workflows/golang-test-linux.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index e7b4997865e..b5bfae8be35 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -229,9 +229,9 @@ jobs: if: matrix.store == 'mysql' run: | for i in {1..10}; do - if nc -z localhost 3306; then + if docker exec my-mysql mysqladmin -uroot -pnetbird ping &>/dev/null; then docker exec my-mysql \ - mysql -u root -pnetbird -e \ + mysql -h127.0.0.1 -P3306 -uroot -pnetbird -e \ "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'netbird' WITH GRANT OPTION; FLUSH PRIVILEGES;" break fi From 5c50f06c2ff0910ece2a2ef9907b44eebad6654e Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 00:54:04 +0100 Subject: [PATCH 17/27] update grant privileges --- .github/workflows/golang-test-linux.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index b5bfae8be35..d9fc49260dc 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -221,6 +221,7 @@ jobs: docker run -d \ -e MYSQL_ROOT_PASSWORD=netbird \ -e MYSQL_DATABASE=netbird \ + -e MYSQL_ROOT_HOST=% \ -p 3306:3306 \ --name my-mysql \ mlsmaycon/warmed-mysql:8 @@ -230,9 +231,10 @@ jobs: run: | for i in {1..10}; do if docker exec my-mysql mysqladmin -uroot -pnetbird ping &>/dev/null; then + # Connect via socket (no -h, no -P) docker exec my-mysql \ - mysql -h127.0.0.1 -P3306 -uroot -pnetbird -e \ - "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'netbird' WITH GRANT OPTION; FLUSH PRIVILEGES;" + mysql -uroot -pnetbird \ + -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'netbird' WITH GRANT OPTION; FLUSH PRIVILEGES;" break fi echo "Waiting for MySQL..." From 8849b40a5b9cb1d09485b6f4585367ca56a53b41 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 01:02:19 +0100 Subject: [PATCH 18/27] mysql without a password --- .github/workflows/golang-test-linux.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index d9fc49260dc..7ae1ce8d7d8 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -219,9 +219,7 @@ jobs: if: matrix.store == 'mysql' run: | docker run -d \ - -e MYSQL_ROOT_PASSWORD=netbird \ - -e MYSQL_DATABASE=netbird \ - -e MYSQL_ROOT_HOST=% \ + -e MYSQL_ALLOW_EMPTY_PASSWORD=true \ -p 3306:3306 \ --name my-mysql \ mlsmaycon/warmed-mysql:8 @@ -230,11 +228,7 @@ jobs: if: matrix.store == 'mysql' run: | for i in {1..10}; do - if docker exec my-mysql mysqladmin -uroot -pnetbird ping &>/dev/null; then - # Connect via socket (no -h, no -P) - docker exec my-mysql \ - mysql -uroot -pnetbird \ - -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'netbird' WITH GRANT OPTION; FLUSH PRIVILEGES;" + if docker exec my-mysql mysqladmin -uroot ping &>/dev/null; then break fi echo "Waiting for MySQL..." @@ -248,7 +242,7 @@ jobs: fi if [ "${{ matrix.store }}" = "mysql" ]; then - export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:netbird@tcp(localhost:3306)/netbird?parseTime=True" + export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:@tcp(localhost:3306)/netbird?parseTime=True" fi CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ From 352583759a9ccdb487f942384a35e0219b08b79f Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 01:14:50 +0100 Subject: [PATCH 19/27] use proper credentials for mysql root --- .github/workflows/golang-test-linux.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 7ae1ce8d7d8..c27f38a693c 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -228,7 +228,7 @@ jobs: if: matrix.store == 'mysql' run: | for i in {1..10}; do - if docker exec my-mysql mysqladmin -uroot ping &>/dev/null; then + if nc -z localhost 3306; then break fi echo "Waiting for MySQL..." @@ -242,7 +242,7 @@ jobs: fi if [ "${{ matrix.store }}" = "mysql" ]; then - export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:@tcp(localhost:3306)/netbird?parseTime=True" + export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:testing@tcp(localhost:3306)/testing?parseTime=True" fi CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ @@ -334,8 +334,6 @@ jobs: if: matrix.store == 'mysql' run: | docker run -d \ - -e MYSQL_ROOT_PASSWORD=netbird \ - -e MYSQL_DATABASE=netbird \ -p 3306:3306 \ --name my-mysql \ mlsmaycon/warmed-mysql:8 @@ -558,8 +556,6 @@ jobs: if: matrix.store == 'mysql' run: | docker run -d \ - -e MYSQL_ROOT_PASSWORD=netbird \ - -e MYSQL_DATABASE=netbird \ -p 3306:3306 \ --name my-mysql \ mlsmaycon/warmed-mysql:8 @@ -584,7 +580,7 @@ jobs: fi if [ "${{ matrix.store }}" = "mysql" ]; then - export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:netbird@tcp(localhost:3306)/netbird?parseTime=True" + export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:testing@tcp(localhost:3306)/testing?parseTime=True" fi CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ From 229610a2c0aec1ba5c67a1d3514c7e91c7b7f670 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 01:24:42 +0100 Subject: [PATCH 20/27] fix other workflows --- .github/workflows/golang-test-linux.yml | 11 ++++------- management/server/store/store.go | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index c27f38a693c..3e64b8bd711 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -219,7 +219,6 @@ jobs: if: matrix.store == 'mysql' run: | docker run -d \ - -e MYSQL_ALLOW_EMPTY_PASSWORD=true \ -p 3306:3306 \ --name my-mysql \ mlsmaycon/warmed-mysql:8 @@ -242,7 +241,7 @@ jobs: fi if [ "${{ matrix.store }}" = "mysql" ]; then - export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:testing@tcp(localhost:3306)/testing?parseTime=True" + export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:testing@tcp(localhost:3306)/testing" fi CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ @@ -356,7 +355,7 @@ jobs: fi if [ "${{ matrix.store }}" = "mysql" ]; then - export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:netbird@tcp(localhost:3306)/netbird?parseTime=True" + export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:testing@tcp(localhost:3306)/testing" fi CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ @@ -448,8 +447,6 @@ jobs: if: matrix.store == 'mysql' run: | docker run -d \ - -e MYSQL_ROOT_PASSWORD=netbird \ - -e MYSQL_DATABASE=netbird \ -p 3306:3306 \ --name my-mysql \ mlsmaycon/warmed-mysql:8 @@ -475,7 +472,7 @@ jobs: fi if [ "${{ matrix.store }}" = "mysql" ]; then - export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:netbird@tcp(localhost:3306)/netbird?parseTime=True" + export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:testing@tcp(localhost:3306)/testing" fi CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ @@ -580,7 +577,7 @@ jobs: fi if [ "${{ matrix.store }}" = "mysql" ]; then - export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:testing@tcp(localhost:3306)/testing?parseTime=True" + export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:testing@tcp(localhost:3306)/testing" fi CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ diff --git a/management/server/store/store.go b/management/server/store/store.go index dc38c9e928a..12534b50282 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -389,7 +389,7 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv) } - db, err := gorm.Open(mysql.Open(dsn), getGormConfig()) + db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig()) if err != nil { return nil, cleanUp, fmt.Errorf("failed to open mysql connection: %v", err) } From 469787935d8b11c719f95b5f0fd309e9be2d6abc Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 01:40:31 +0100 Subject: [PATCH 21/27] remove other test runs --- .github/workflows/golang-test-linux.yml | 5 ----- management/server/store/store.go | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 3e64b8bd711..c884301b0ee 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -462,9 +462,6 @@ jobs: sleep 3 done - - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=benchmark -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' -timeout 10m $(go list ./... | grep /management) - - name: Test run: | if [ "${{ matrix.store }}" = "postgres" ]; then @@ -568,8 +565,6 @@ jobs: sleep 3 done - - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' -timeout 10m -tags=integration $(go list ./... | grep /management) - name: Test run: | if [ "${{ matrix.store }}" = "postgres" ]; then diff --git a/management/server/store/store.go b/management/server/store/store.go index 12534b50282..ab6c753459c 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -389,7 +389,7 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv) } - db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig()) + db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), &gorm.Config{}) if err != nil { return nil, cleanUp, fmt.Errorf("failed to open mysql connection: %v", err) } From 83c1070e65554bccf818464de00cf91012ac4f1f Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 01:46:03 +0100 Subject: [PATCH 22/27] remove with force when dropping database --- management/server/store/store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/server/store/store.go b/management/server/store/store.go index ab6c753459c..ebea8c14a64 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -429,7 +429,7 @@ func createRandomDB(dsn string, db *gorm.DB, cleanUp func(), removeContainer boo u.Path = dbName cleanup := func() { - db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)) + db.Exec(fmt.Sprintf("DROP DATABASE %s", dbName)) sqlDB, _ := db.DB() _ = sqlDB.Close() if cleanUp != nil && removeContainer { From 441313de3d618dc661f629c04ffa82cbc03f5a21 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 02:19:02 +0100 Subject: [PATCH 23/27] allow force for postgres --- management/server/account_test.go | 33 ++ management/server/store/sql_store_test.go | 355 ++++++++++------------ management/server/store/store.go | 13 +- management/server/testutil/store.go | 2 +- 4 files changed, 208 insertions(+), 195 deletions(-) diff --git a/management/server/account_test.go b/management/server/account_test.go index e4f079507d2..9bdc627f316 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -17,6 +17,7 @@ import ( "github.com/golang-jwt/jwt" + "github.com/netbirdio/netbird/management/server/testutil" "github.com/netbirdio/netbird/management/server/util" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" @@ -39,6 +40,38 @@ import ( "github.com/netbirdio/netbird/route" ) +func TestMain(m *testing.M) { + var cleanUpPostgres func() + var cleanUpMysql func() + + if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_POSTGRES_DSN"); !ok || dsn == "" { + var err error + cleanUpPostgres, err = testutil.CreatePostgresTestContainer() + if err != nil { + os.Exit(1) + } + } + + if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_MYSQL_DSN"); !ok || dsn == "" { + var err error + cleanUpMysql, err = testutil.CreateMysqlTestContainer() + if err != nil { + os.Exit(1) + } + } + + code := m.Run() + + if cleanUpPostgres != nil { + cleanUpPostgres() + } + if cleanUpMysql != nil { + cleanUpMysql() + } + + os.Exit(code) +} + func verifyCanAddPeerToAccount(t *testing.T, manager AccountManager, account *types.Account, userID string) { t.Helper() peer := &nbpeer.Peer{ diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 5928b45baa7..7b42cf55059 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -24,6 +24,7 @@ import ( routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/testutil" "github.com/netbirdio/netbird/management/server/types" route2 "github.com/netbirdio/netbird/route" @@ -34,40 +35,78 @@ import ( nbroute "github.com/netbirdio/netbird/route" ) -func TestSqlite_NewStore(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("The SQLite store is not properly supported by Windows yet") +func TestMain(m *testing.M) { + var cleanUpPostgres func() + var cleanUpMysql func() + + if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_POSTGRES_DSN"); !ok || dsn == "" { + var err error + cleanUpPostgres, err = testutil.CreatePostgresTestContainer() + if err != nil { + os.Exit(1) + } } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) + if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_MYSQL_DSN"); !ok || dsn == "" { + var err error + cleanUpMysql, err = testutil.CreateMysqlTestContainer() + if err != nil { + os.Exit(1) + } + } - if len(store.GetAllAccounts(context.Background())) != 0 { - t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") + code := m.Run() + + if cleanUpPostgres != nil { + cleanUpPostgres() + } + if cleanUpMysql != nil { + cleanUpMysql() } + + os.Exit(code) } -func TestSqlite_SaveAccount_Large(t *testing.T) { - if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { - t.Skip("skip CI tests on darwin and windows") - } +var engines = []Engine{SqliteStoreEngine, PostgresStoreEngine, MysqlStoreEngine} - t.Run("SQLite", func(t *testing.T) { - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) +func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) { + t.Helper() + for _, engine := range engines { + if os.Getenv("NETBIRD_STORE_ENGINE") != "" && os.Getenv("NETBIRD_STORE_ENGINE") != string(engine) { + continue + } + t.Setenv("NETBIRD_STORE_ENGINE", string(engine)) + store, cleanUp, err := NewTestStoreFromSQL(context.Background(), testDataFile, t.TempDir()) t.Cleanup(cleanUp) assert.NoError(t, err) - runLargeTest(t, store) + t.Run(string(engine), func(t *testing.T) { + f(t, store) + }) + os.Unsetenv("NETBIRD_STORE_ENGINE") + } +} + +func Test_NewStore(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + if store == nil { + t.Errorf("expected to create a new Store") + } + if len(store.GetAllAccounts(context.Background())) != 0 { + t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") + } }) +} - // create store outside to have a better time counter for the test - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - t.Run("PostgreSQL", func(t *testing.T) { +func Test_SaveAccount_Large(t *testing.T) { + if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" { + t.Skip("skip CI tests on darwin and windows") + } + + runTestForAllEngines(t, "", func(t *testing.T, store Store) { runLargeTest(t, store) }) } @@ -212,77 +251,74 @@ func randomIPv4() net.IP { return net.IP(b) } -func TestSqlite_SaveAccount(t *testing.T) { +func Test_SaveAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - account := newAccountWithId(context.Background(), "account_id", "testuser", "") - setupKey, _ := types.GenerateDefaultSetupKey() - account.SetupKeys[setupKey.Key] = setupKey - account.Peers["testpeer"] = &nbpeer.Peer{ - Key: "peerkey", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + runTestForAllEngines(t, "", func(t *testing.T, store Store) { + account := newAccountWithId(context.Background(), "account_id", "testuser", "") + setupKey, _ := types.GenerateDefaultSetupKey() + account.SetupKeys[setupKey.Key] = setupKey + account.Peers["testpeer"] = &nbpeer.Peer{ + Key: "peerkey", + 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(context.Background(), account) - require.NoError(t, err) + err := store.SaveAccount(context.Background(), account) + require.NoError(t, err) - account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "") - setupKey, _ = types.GenerateDefaultSetupKey() - account2.SetupKeys[setupKey.Key] = setupKey - account2.Peers["testpeer2"] = &nbpeer.Peer{ - Key: "peerkey2", - IP: net.IP{127, 0, 0, 2}, - Meta: nbpeer.PeerSystemMeta{}, - Name: "peer name 2", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } + account2 := newAccountWithId(context.Background(), "account_id2", "testuser2", "") + setupKey, _ = types.GenerateDefaultSetupKey() + account2.SetupKeys[setupKey.Key] = setupKey + account2.Peers["testpeer2"] = &nbpeer.Peer{ + Key: "peerkey2", + 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(context.Background(), account2) - require.NoError(t, err) + err = store.SaveAccount(context.Background(), account2) + require.NoError(t, err) - if len(store.GetAllAccounts(context.Background())) != 2 { - t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") - } + if len(store.GetAllAccounts(context.Background())) != 2 { + t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") + } - a, err := store.GetAccount(context.Background(), account.Id) - if a == nil { - t.Errorf("expecting Account to be stored after SaveAccount(): %v", err) - } + a, err := store.GetAccount(context.Background(), 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) != 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 != 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(context.Background(), "peerkey"); a == nil { - t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByPeerPubKey(context.Background(), "peerkey"); a == nil { + t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil { - t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByUser(context.Background(), "testuser"); a == nil { + t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil { - t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountByPeerID(context.Background(), "testpeer"); a == nil { + t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) + } - if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil { - t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) - } + if a, err := store.GetAccountBySetupKey(context.Background(), setupKey.Key); a == nil { + t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) + } + }) } func TestSqlite_DeleteAccount(t *testing.T) { @@ -399,77 +435,24 @@ func TestSqlite_DeleteAccount(t *testing.T) { } } -func TestSqlite_GetAccount(t *testing.T) { +func Test_GetAccount(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - - account, err := store.GetAccount(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, account.Id, "account id should match") - - _, err = store.GetAccount(context.Background(), "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_SavePeer(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("The SQLite store is not properly supported by Windows yet") - } - - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - account, err := store.GetAccount(context.Background(), "bf1c8084-ba50-4ce7-9439-34653001fc3b") - require.NoError(t, err) - - // save status of non-existing peer - peer := &nbpeer.Peer{ - Key: "peerkey", - ID: "testpeer", - IP: net.IP{127, 0, 0, 1}, - Meta: nbpeer.PeerSystemMeta{Hostname: "testingpeer"}, - Name: "peer name", - Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, - } - ctx := context.Background() - err = store.SavePeer(ctx, 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") - - // save new status of existing peer - account.Peers[peer.ID] = peer - - err = store.SaveAccount(context.Background(), account) - require.NoError(t, err) - - updatedPeer := peer.Copy() - updatedPeer.Status.Connected = false - updatedPeer.Meta.Hostname = "updatedpeer" - - err = store.SavePeer(ctx, account.Id, updatedPeer) - require.NoError(t, err) + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + id := "bf1c8084-ba50-4ce7-9439-34653001fc3b" - account, err = store.GetAccount(context.Background(), account.Id) - require.NoError(t, err) + account, err := store.GetAccount(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, account.Id, "account id should match") - actual := account.Peers[peer.ID] - assert.Equal(t, updatedPeer.Status, actual.Status) - assert.Equal(t, updatedPeer.Meta, actual.Meta) + _, err = store.GetAccount(context.Background(), "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) { @@ -581,74 +564,65 @@ func TestSqlite_SavePeerLocation(t *testing.T) { require.Equal(t, status.NotFound, parsedErr.Type(), "should return not found error") } -func TestSqlite_TestGetAccountByPrivateDomain(t *testing.T) { +func Test_TestGetAccountByPrivateDomain(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - existingDomain := "test.com" + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + existingDomain := "test.com" - account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) - require.NoError(t, err, "should found account") - require.Equal(t, existingDomain, account.Domain, "domains should match") + account, err := store.GetAccountByPrivateDomain(context.Background(), existingDomain) + require.NoError(t, err, "should found account") + require.Equal(t, existingDomain, account.Domain, "domains should match") - _, err = store.GetAccountByPrivateDomain(context.Background(), "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") + _, err = store.GetAccountByPrivateDomain(context.Background(), "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) { +func Test_GetTokenIDByHashedToken(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - hashed := "SoMeHaShEdToKeN" - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + hashed := "SoMeHaShEdToKeN" + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - token, err := store.GetTokenIDByHashedToken(context.Background(), hashed) - require.NoError(t, err) - require.Equal(t, id, token) + token, err := store.GetTokenIDByHashedToken(context.Background(), hashed) + require.NoError(t, err) + require.Equal(t, id, token) - _, err = store.GetTokenIDByHashedToken(context.Background(), "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") + _, err = store.GetTokenIDByHashedToken(context.Background(), "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) { +func Test_GetUserByTokenID(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("The SQLite store is not properly supported by Windows yet") } - t.Setenv("NETBIRD_STORE_ENGINE", string(SqliteStoreEngine)) - store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) - t.Cleanup(cleanUp) - assert.NoError(t, err) - - id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) { + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" - user, err := store.GetUserByTokenID(context.Background(), id) - require.NoError(t, err) - require.Equal(t, id, user.PATs[id].ID) + user, err := store.GetUserByTokenID(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, user.PATs[id].ID) - _, err = store.GetUserByTokenID(context.Background(), "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") + _, err = store.GetUserByTokenID(context.Background(), "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) { @@ -2370,6 +2344,7 @@ func TestSqlStore_GetNetworkRouterByID(t *testing.T) { } func TestSqlStore_SaveNetworkRouter(t *testing.T) { + t.Setenv("NETBIRD_STORE_ENGINE", string(PostgresStoreEngine)) store, cleanup, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir()) t.Cleanup(cleanup) require.NoError(t, err) diff --git a/management/server/store/store.go b/management/server/store/store.go index ebea8c14a64..70c925a9968 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -359,7 +359,7 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return nil, cleanUp, fmt.Errorf("failed to open postgres connection: %v", err) } - newDsn, cleanup, err := createRandomDB(dsn, db, cleanUp, removeContainer) + newDsn, cleanup, err := createRandomDB(dsn, db, cleanUp, kind, removeContainer) if err != nil { return nil, cleanup, err } @@ -394,7 +394,7 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return nil, cleanUp, fmt.Errorf("failed to open mysql connection: %v", err) } - newDsn, cleanup, err := createRandomDB(dsn, db, cleanUp, removeContainer) + newDsn, cleanup, err := createRandomDB(dsn, db, cleanUp, kind, removeContainer) if err != nil { return nil, cleanup, err } @@ -414,7 +414,7 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return store, closeConnection, nil } -func createRandomDB(dsn string, db *gorm.DB, cleanUp func(), removeContainer bool) (string, func(), error) { +func createRandomDB(dsn string, db *gorm.DB, cleanUp func(), engine Engine, removeContainer bool) (string, func(), error) { dbName := fmt.Sprintf("test_db_%d", rand.Intn(1e6)) if err := db.Exec(fmt.Sprintf("CREATE DATABASE %s", dbName)).Error; err != nil { @@ -429,7 +429,12 @@ func createRandomDB(dsn string, db *gorm.DB, cleanUp func(), removeContainer boo u.Path = dbName cleanup := func() { - db.Exec(fmt.Sprintf("DROP DATABASE %s", dbName)) + switch engine { + case PostgresStoreEngine: + db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)) + case MysqlStoreEngine: + db.Exec(fmt.Sprintf("DROP DATABASE %s", dbName)) + } sqlDB, _ := db.DB() _ = sqlDB.Close() if cleanUp != nil && removeContainer { diff --git a/management/server/testutil/store.go b/management/server/testutil/store.go index 382b5fb3d0f..8672efa7ff4 100644 --- a/management/server/testutil/store.go +++ b/management/server/testutil/store.go @@ -22,7 +22,7 @@ func CreateMysqlTestContainer() (func(), error) { myContainer, err := mysql.RunContainer(ctx, testcontainers.WithImage("mlsmaycon/warmed-mysql:8"), mysql.WithDatabase("testing"), - mysql.WithUsername("testing"), + mysql.WithUsername("root"), mysql.WithPassword("testing"), testcontainers.WithWaitStrategy( wait.ForLog("/usr/sbin/mysqld: ready for connections"). From 3c820a2cbb47170994f1ae361fd42d88f7f0bfee Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 12:58:32 +0100 Subject: [PATCH 24/27] don't cleanup containers after tests --- .github/workflows/golang-test-linux.yml | 53 +------------------ management/server/store/sql_store_test.go | 33 ------------ management/server/store/store.go | 63 ++++++++++++++++------- 3 files changed, 47 insertions(+), 102 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index c884301b0ee..a6b376b5a28 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -193,61 +193,12 @@ jobs: if: matrix.store == 'mysql' run: docker pull mlsmaycon/warmed-mysql:8 - - name: Start Postgres - if: matrix.store == 'postgres' - run: | - docker run -d \ - -e POSTGRES_USER=root \ - -e POSTGRES_PASSWORD=netbird \ - -e POSTGRES_DB=netbird \ - -p 5432:5432 \ - --name my-postgres \ - postgres:15-alpine - - - name: Wait for Postgres - if: matrix.store == 'postgres' - run: | - for i in {1..10}; do - if nc -z localhost 5432; then - break - fi - echo "Waiting for Postgres..." - sleep 3 - done - - - name: Start MySQL - if: matrix.store == 'mysql' - run: | - docker run -d \ - -p 3306:3306 \ - --name my-mysql \ - mlsmaycon/warmed-mysql:8 - - - name: Wait for MySQL - if: matrix.store == 'mysql' - run: | - for i in {1..10}; do - if nc -z localhost 3306; then - break - fi - echo "Waiting for MySQL..." - sleep 3 - done - - name: Test - run: | - if [ "${{ matrix.store }}" = "postgres" ]; then - export NETBIRD_STORE_ENGINE_POSTGRES_DSN="postgres://root:netbird@localhost:5432/netbird?sslmode=disable" - fi - - if [ "${{ matrix.store }}" = "mysql" ]; then - export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:testing@tcp(localhost:3306)/testing" - fi - + run: | CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ NETBIRD_STORE_ENGINE=${{ matrix.store }} \ go test -tags=devcert -p 1 \ - -exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN" \ + -exec "sudo --preserve-env=CI,NETBIRD_STORE_ENGINE" \ -timeout 10m $(go list ./... | grep /management) benchmark: diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 7b42cf55059..5f7b5801d58 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -24,7 +24,6 @@ import ( routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" "github.com/netbirdio/netbird/management/server/posture" - "github.com/netbirdio/netbird/management/server/testutil" "github.com/netbirdio/netbird/management/server/types" route2 "github.com/netbirdio/netbird/route" @@ -35,38 +34,6 @@ import ( nbroute "github.com/netbirdio/netbird/route" ) -func TestMain(m *testing.M) { - var cleanUpPostgres func() - var cleanUpMysql func() - - if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_POSTGRES_DSN"); !ok || dsn == "" { - var err error - cleanUpPostgres, err = testutil.CreatePostgresTestContainer() - if err != nil { - os.Exit(1) - } - } - - if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_MYSQL_DSN"); !ok || dsn == "" { - var err error - cleanUpMysql, err = testutil.CreateMysqlTestContainer() - if err != nil { - os.Exit(1) - } - } - - code := m.Run() - - if cleanUpPostgres != nil { - cleanUpPostgres() - } - if cleanUpMysql != nil { - cleanUpMysql() - } - - os.Exit(code) -} - var engines = []Engine{SqliteStoreEngine, PostgresStoreEngine, MysqlStoreEngine} func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) { diff --git a/management/server/store/store.go b/management/server/store/store.go index 70c925a9968..f5049fb7468 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -338,28 +338,25 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) { if kind == PostgresStoreEngine { - var cleanUp func() - removeContainer := false if envDsn, ok := os.LookupEnv(postgresDsnEnv); !ok || envDsn == "" { var err error - cleanUp, err = testutil.CreatePostgresTestContainer() + _, err = testutil.CreatePostgresTestContainer() if err != nil { return nil, nil, err } - removeContainer = true } dsn, ok := os.LookupEnv(postgresDsnEnv) if !ok { - return nil, cleanUp, fmt.Errorf("%s is not set", postgresDsnEnv) + return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv) } db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { - return nil, cleanUp, fmt.Errorf("failed to open postgres connection: %v", err) + return nil, nil, fmt.Errorf("failed to open postgres connection: %v", err) } - newDsn, cleanup, err := createRandomDB(dsn, db, cleanUp, kind, removeContainer) + newDsn, cleanup, err := createRandomDB(dsn, db, kind) if err != nil { return nil, cleanup, err } @@ -374,14 +371,12 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store if kind == MysqlStoreEngine { var cleanUp func() - removeContainer := false if envDsn, ok := os.LookupEnv(mysqlDsnEnv); !ok || envDsn == "" { var err error cleanUp, err = testutil.CreateMysqlTestContainer() if err != nil { return nil, nil, err } - removeContainer = true } dsn, ok := os.LookupEnv(mysqlDsnEnv) @@ -394,7 +389,7 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return nil, cleanUp, fmt.Errorf("failed to open mysql connection: %v", err) } - newDsn, cleanup, err := createRandomDB(dsn, db, cleanUp, kind, removeContainer) + newDsn, cleanup, err := createRandomDB(dsn, db, kind) if err != nil { return nil, cleanup, err } @@ -414,16 +409,16 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return store, closeConnection, nil } -func createRandomDB(dsn string, db *gorm.DB, cleanUp func(), engine Engine, removeContainer bool) (string, func(), error) { +func createRandomDB(dsn string, db *gorm.DB, engine Engine) (string, func(), error) { dbName := fmt.Sprintf("test_db_%d", rand.Intn(1e6)) if err := db.Exec(fmt.Sprintf("CREATE DATABASE %s", dbName)).Error; err != nil { - return "", cleanUp, fmt.Errorf("failed to create database: %v", err) + return "", nil, fmt.Errorf("failed to create database: %v", err) } u, err := url.Parse(dsn) if err != nil { - return "", cleanUp, fmt.Errorf("failed to parse DSN: %v", err) + return "", nil, fmt.Errorf("failed to parse DSN: %v", err) } u.Path = dbName @@ -431,21 +426,53 @@ func createRandomDB(dsn string, db *gorm.DB, cleanUp func(), engine Engine, remo cleanup := func() { switch engine { case PostgresStoreEngine: - db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)) + err = db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)).Error case MysqlStoreEngine: - db.Exec(fmt.Sprintf("DROP DATABASE %s", dbName)) + err = killMySQLConnections(dsn, dbName) + } + if err != nil { + log.Errorf("failed to drop database %s: %v", dbName, err) } sqlDB, _ := db.DB() _ = sqlDB.Close() - if cleanUp != nil && removeContainer { - cleanUp() - } } return u.String(), cleanup, nil } +func killMySQLConnections(dsn, targetDB string) error { + u, err := url.Parse(dsn) + if err != nil { + return fmt.Errorf("failed to parse DSN: %v", err) + } + + u.Path = "testing" + + ctrlDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + return fmt.Errorf("failed to open mysql connection: %v", err) + } + + var procs []struct { + ID int + } + query := "SELECT ID FROM INFORMATION_SCHEMA.PROCESSLIST WHERE DB = ?" + if err := ctrlDB.Raw(query, targetDB).Scan(&procs).Error; err != nil { + return fmt.Errorf("failed to get processes: %v", err) + } + + for _, p := range procs { + killStmt := fmt.Sprintf("KILL %d", p.ID) + if err := ctrlDB.Exec(killStmt).Error; err != nil { + return fmt.Errorf("failed to kill process %d: %v", p.ID, err) + } + } + + dropStmt := fmt.Sprintf("DROP DATABASE `%s`", targetDB) + return ctrlDB.Exec(dropStmt).Error +} + func loadSQL(db *gorm.DB, filepath string) error { sqlContent, err := os.ReadFile(filepath) if err != nil { From 46ff4f4ae3efbe947cef7e098efad9bcef606524 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 13:04:55 +0100 Subject: [PATCH 25/27] remove container startup from workflow files --- .github/workflows/golang-test-linux.yml | 153 +----------------------- 1 file changed, 3 insertions(+), 150 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index a6b376b5a28..e4ab80b766f 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -258,61 +258,12 @@ jobs: if: matrix.store == 'mysql' run: docker pull mlsmaycon/warmed-mysql:8 - - name: Start Postgres - if: matrix.store == 'postgres' - run: | - docker run -d \ - -e POSTGRES_USER=root \ - -e POSTGRES_PASSWORD=netbird \ - -e POSTGRES_DB=netbird \ - -p 5432:5432 \ - --name my-postgres \ - postgres:15-alpine - - - name: Wait for Postgres - if: matrix.store == 'postgres' - run: | - for i in {1..10}; do - if nc -z localhost 5432; then - break - fi - echo "Waiting for Postgres..." - sleep 3 - done - - - name: Start MySQL - if: matrix.store == 'mysql' - run: | - docker run -d \ - -p 3306:3306 \ - --name my-mysql \ - mlsmaycon/warmed-mysql:8 - - - name: Wait for MySQL - if: matrix.store == 'mysql' - run: | - for i in {1..10}; do - if nc -z localhost 3306; then - break - fi - echo "Waiting for MySQL..." - sleep 3 - done - - name: Test run: | - if [ "${{ matrix.store }}" = "postgres" ]; then - export NETBIRD_STORE_ENGINE_POSTGRES_DSN="postgres://root:netbird@localhost:5432/netbird?sslmode=disable" - fi - - if [ "${{ matrix.store }}" = "mysql" ]; then - export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:testing@tcp(localhost:3306)/testing" - fi - CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ go test -tags devcert -run=^$ -bench=. \ - -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ -timeout 10m ./... api_benchmark: @@ -372,61 +323,12 @@ jobs: if: matrix.store == 'mysql' run: docker pull mlsmaycon/warmed-mysql:8 - - name: Start Postgres - if: matrix.store == 'postgres' - run: | - docker run -d \ - -e POSTGRES_USER=root \ - -e POSTGRES_PASSWORD=netbird \ - -e POSTGRES_DB=netbird \ - -p 5432:5432 \ - --name my-postgres \ - postgres:15-alpine - - - name: Wait for Postgres - if: matrix.store == 'postgres' - run: | - for i in {1..10}; do - if nc -z localhost 5432; then - break - fi - echo "Waiting for Postgres..." - sleep 3 - done - - - name: Start MySQL - if: matrix.store == 'mysql' - run: | - docker run -d \ - -p 3306:3306 \ - --name my-mysql \ - mlsmaycon/warmed-mysql:8 - - - name: Wait for MySQL - if: matrix.store == 'mysql' - run: | - for i in {1..10}; do - if nc -z localhost 3306; then - break - fi - echo "Waiting for MySQL..." - sleep 3 - done - - name: Test run: | - if [ "${{ matrix.store }}" = "postgres" ]; then - export NETBIRD_STORE_ENGINE_POSTGRES_DSN="postgres://root:netbird@localhost:5432/netbird?sslmode=disable" - fi - - if [ "${{ matrix.store }}" = "mysql" ]; then - export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:testing@tcp(localhost:3306)/testing" - fi - CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ go test -tags=benchmark -run=^$ -bench=. \ - -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ -timeout 10m $(go list ./... | grep /management) api_integration_test: @@ -475,61 +377,12 @@ jobs: - name: check git status run: git --no-pager diff --exit-code - - name: Start Postgres - if: matrix.store == 'postgres' - run: | - docker run -d \ - -e POSTGRES_USER=root \ - -e POSTGRES_PASSWORD=netbird \ - -e POSTGRES_DB=netbird \ - -p 5432:5432 \ - --name my-postgres \ - postgres:15-alpine - - - name: Wait for Postgres - if: matrix.store == 'postgres' - run: | - for i in {1..10}; do - if nc -z localhost 5432; then - break - fi - echo "Waiting for Postgres..." - sleep 3 - done - - - name: Start MySQL - if: matrix.store == 'mysql' - run: | - docker run -d \ - -p 3306:3306 \ - --name my-mysql \ - mlsmaycon/warmed-mysql:8 - - - name: Wait for MySQL - if: matrix.store == 'mysql' - run: | - for i in {1..10}; do - if nc -z localhost 3306; then - break - fi - echo "Waiting for MySQL..." - sleep 3 - done - - name: Test run: | - if [ "${{ matrix.store }}" = "postgres" ]; then - export NETBIRD_STORE_ENGINE_POSTGRES_DSN="postgres://root:netbird@localhost:5432/netbird?sslmode=disable" - fi - - if [ "${{ matrix.store }}" = "mysql" ]; then - export NETBIRD_STORE_ENGINE_MYSQL_DSN="root:testing@tcp(localhost:3306)/testing" - fi - CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \ NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true \ go test -p 1 \ - -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,NETBIRD_STORE_ENGINE_POSTGRES_DSN,NETBIRD_STORE_ENGINE_MYSQL_DSN' \ + -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \ -timeout 10m -tags=integration $(go list ./... | grep /management) test_client_on_docker: From b00d6333bb5771782411476226e188d05f2508b3 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Sun, 19 Jan 2025 13:19:09 +0100 Subject: [PATCH 26/27] use basic drop table again --- management/server/account_test.go | 33 ------------------------------- management/server/store/store.go | 6 +++--- 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/management/server/account_test.go b/management/server/account_test.go index 9bdc627f316..e4f079507d2 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -17,7 +17,6 @@ import ( "github.com/golang-jwt/jwt" - "github.com/netbirdio/netbird/management/server/testutil" "github.com/netbirdio/netbird/management/server/util" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" @@ -40,38 +39,6 @@ import ( "github.com/netbirdio/netbird/route" ) -func TestMain(m *testing.M) { - var cleanUpPostgres func() - var cleanUpMysql func() - - if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_POSTGRES_DSN"); !ok || dsn == "" { - var err error - cleanUpPostgres, err = testutil.CreatePostgresTestContainer() - if err != nil { - os.Exit(1) - } - } - - if dsn, ok := os.LookupEnv("NETBIRD_STORE_ENGINE_MYSQL_DSN"); !ok || dsn == "" { - var err error - cleanUpMysql, err = testutil.CreateMysqlTestContainer() - if err != nil { - os.Exit(1) - } - } - - code := m.Run() - - if cleanUpPostgres != nil { - cleanUpPostgres() - } - if cleanUpMysql != nil { - cleanUpMysql() - } - - os.Exit(code) -} - func verifyCanAddPeerToAccount(t *testing.T, manager AccountManager, account *types.Account, userID string) { t.Helper() peer := &nbpeer.Peer{ diff --git a/management/server/store/store.go b/management/server/store/store.go index f5049fb7468..427921db3bd 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -428,7 +428,8 @@ func createRandomDB(dsn string, db *gorm.DB, engine Engine) (string, func(), err case PostgresStoreEngine: err = db.Exec(fmt.Sprintf("DROP DATABASE %s WITH (FORCE)", dbName)).Error case MysqlStoreEngine: - err = killMySQLConnections(dsn, dbName) + // err = killMySQLConnections(dsn, dbName) + err = db.Exec(fmt.Sprintf("DROP DATABASE %s", dbName)).Error } if err != nil { log.Errorf("failed to drop database %s: %v", dbName, err) @@ -469,8 +470,7 @@ func killMySQLConnections(dsn, targetDB string) error { } } - dropStmt := fmt.Sprintf("DROP DATABASE `%s`", targetDB) - return ctrlDB.Exec(dropStmt).Error + return nil } func loadSQL(db *gorm.DB, filepath string) error { From 63281122a3e480243fbcac7266393b1226e834d0 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Mon, 20 Jan 2025 11:01:38 +0100 Subject: [PATCH 27/27] extract supported engines --- management/server/dns_test.go | 2 +- management/server/store/sql_store_test.go | 4 +--- management/server/store/store.go | 26 ++++++++++++----------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/management/server/dns_test.go b/management/server/dns_test.go index 6fb9f6a29a9..150953f51c7 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -129,7 +129,7 @@ func TestSaveDNSSettings(t *testing.T) { account, err := initTestDNSAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Errorf("failed to init testing account: %v", err) } err = am.SaveDNSSettings(context.Background(), account.Id, testCase.userID, testCase.inputSettings) diff --git a/management/server/store/sql_store_test.go b/management/server/store/sql_store_test.go index 5f7b5801d58..f8e9bf0c0df 100644 --- a/management/server/store/sql_store_test.go +++ b/management/server/store/sql_store_test.go @@ -34,11 +34,9 @@ import ( nbroute "github.com/netbirdio/netbird/route" ) -var engines = []Engine{SqliteStoreEngine, PostgresStoreEngine, MysqlStoreEngine} - func runTestForAllEngines(t *testing.T, testDataFile string, f func(t *testing.T, store Store)) { t.Helper() - for _, engine := range engines { + for _, engine := range supportedEngines { if os.Getenv("NETBIRD_STORE_ENGINE") != "" && os.Getenv("NETBIRD_STORE_ENGINE") != string(engine) { continue } diff --git a/management/server/store/store.go b/management/server/store/store.go index 427921db3bd..c3959f88f98 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -12,6 +12,7 @@ import ( "path" "path/filepath" "runtime" + "slices" "strings" "time" @@ -181,6 +182,8 @@ const ( mysqlDsnEnv = "NETBIRD_STORE_ENGINE_MYSQL_DSN" ) +var supportedEngines = []Engine{SqliteStoreEngine, PostgresStoreEngine, MysqlStoreEngine} + func getStoreEngineFromEnv() Engine { // NETBIRD_STORE_ENGINE supposed to be used in tests. Otherwise, rely on the config file. kind, ok := os.LookupEnv("NETBIRD_STORE_ENGINE") @@ -189,7 +192,7 @@ func getStoreEngineFromEnv() Engine { } value := Engine(strings.ToLower(kind)) - if value == SqliteStoreEngine || value == PostgresStoreEngine || value == MysqlStoreEngine { + if slices.Contains(supportedEngines, value) { return value } @@ -337,6 +340,7 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) ( } func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store, func(), error) { + var cleanup func() if kind == PostgresStoreEngine { if envDsn, ok := os.LookupEnv(postgresDsnEnv); !ok || envDsn == "" { var err error @@ -356,24 +360,21 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store return nil, nil, fmt.Errorf("failed to open postgres connection: %v", err) } - newDsn, cleanup, err := createRandomDB(dsn, db, kind) + dsn, cleanup, err = createRandomDB(dsn, db, kind) if err != nil { return nil, cleanup, err } - store, err := NewPostgresqlStoreFromSqlStore(ctx, store, newDsn, nil) + store, err = NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil) if err != nil { return nil, cleanup, err } - - return store, cleanup, nil } if kind == MysqlStoreEngine { - var cleanUp func() if envDsn, ok := os.LookupEnv(mysqlDsnEnv); !ok || envDsn == "" { var err error - cleanUp, err = testutil.CreateMysqlTestContainer() + _, err = testutil.CreateMysqlTestContainer() if err != nil { return nil, nil, err } @@ -386,23 +387,23 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), &gorm.Config{}) if err != nil { - return nil, cleanUp, fmt.Errorf("failed to open mysql connection: %v", err) + return nil, nil, fmt.Errorf("failed to open mysql connection: %v", err) } - newDsn, cleanup, err := createRandomDB(dsn, db, kind) + dsn, cleanup, err = createRandomDB(dsn, db, kind) if err != nil { return nil, cleanup, err } - store, err := NewMysqlStoreFromSqlStore(ctx, store, newDsn, nil) + store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil) if err != nil { return nil, nil, err } - return store, cleanup, nil } closeConnection := func() { + cleanup() store.Close(ctx) } @@ -410,7 +411,7 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind Engine) (Store } func createRandomDB(dsn string, db *gorm.DB, engine Engine) (string, func(), error) { - dbName := fmt.Sprintf("test_db_%d", rand.Intn(1e6)) + dbName := fmt.Sprintf("test_db_%d", rand.Intn(1e9)) if err := db.Exec(fmt.Sprintf("CREATE DATABASE %s", dbName)).Error; err != nil { return "", nil, fmt.Errorf("failed to create database: %v", err) @@ -433,6 +434,7 @@ func createRandomDB(dsn string, db *gorm.DB, engine Engine) (string, func(), err } if err != nil { log.Errorf("failed to drop database %s: %v", dbName, err) + panic(err) } sqlDB, _ := db.DB() _ = sqlDB.Close()