From fe10038737f48b8d229d97595d2ae16459dc0486 Mon Sep 17 00:00:00 2001 From: Matty Evans Date: Thu, 19 Dec 2024 16:58:07 +1000 Subject: [PATCH] test: Add test cases for TUI components --- .../commands/install/page_10_welcome_test.go | 63 ++++++++++ .../commands/install/page_20_network_test.go | 82 ++++++++++++ .../install/page_30_beacon_node_test.go | 74 +++++++++++ .../install/page_40_output_server_test.go | 90 +++++++++++++ .../page_50_output_server_credentials_test.go | 119 ++++++++++++++++++ .../commands/install/page_60_finished_test.go | 70 +++++++++++ internal/service/docker.go | 6 +- 7 files changed, 502 insertions(+), 2 deletions(-) create mode 100644 cmd/cli/commands/install/page_10_welcome_test.go create mode 100644 cmd/cli/commands/install/page_20_network_test.go create mode 100644 cmd/cli/commands/install/page_30_beacon_node_test.go create mode 100644 cmd/cli/commands/install/page_40_output_server_test.go create mode 100644 cmd/cli/commands/install/page_50_output_server_credentials_test.go create mode 100644 cmd/cli/commands/install/page_60_finished_test.go diff --git a/cmd/cli/commands/install/page_10_welcome_test.go b/cmd/cli/commands/install/page_10_welcome_test.go new file mode 100644 index 0000000..2aff433 --- /dev/null +++ b/cmd/cli/commands/install/page_10_welcome_test.go @@ -0,0 +1,63 @@ +package install + +import ( + "testing" + + "github.com/ethpandaops/contributoor-installer/internal/tui" + "github.com/rivo/tview" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +// This is about the best we can do re testing TUI components. +// They're heavily dependent on the terminal state. +func TestWelcomePage(t *testing.T) { + tests := []struct { + name string + buttonIndex int + buttonLabel string + expectStop bool + expectNewPage bool + }{ + { + name: "clicks next button", + buttonIndex: 0, + buttonLabel: tui.ButtonNext, + expectStop: false, + expectNewPage: true, + }, + { + name: "clicks close button", + buttonIndex: 1, + buttonLabel: tui.ButtonClose, + expectStop: true, + expectNewPage: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup. + app := tview.NewApplication() + + // Create a new welcome page with our mock display. + mockDisplay := &InstallDisplay{ + app: app, + log: logrus.New(), + networkConfigPage: &NetworkConfigPage{ + page: &tui.Page{ID: "network-config"}, + }, + } + page := NewWelcomePage(mockDisplay) + + // Verify the page was created. + assert.NotNil(t, page, "page should be created") + assert.NotNil(t, page.content, "page content should be set") + assert.IsType(t, &tview.Modal{}, page.content, "content should be a modal") + + // Verify the page ID and title. + assert.Equal(t, "install-welcome", page.page.ID) + assert.Equal(t, "Welcome", page.page.Title) + }) + } +} diff --git a/cmd/cli/commands/install/page_20_network_test.go b/cmd/cli/commands/install/page_20_network_test.go new file mode 100644 index 0000000..205799a --- /dev/null +++ b/cmd/cli/commands/install/page_20_network_test.go @@ -0,0 +1,82 @@ +package install + +import ( + "testing" + + "github.com/ethpandaops/contributoor-installer/internal/service" + "github.com/ethpandaops/contributoor-installer/internal/service/mock" + "github.com/ethpandaops/contributoor-installer/internal/tui" + "github.com/rivo/tview" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// This is about the best we can do re testing TUI components. +// They're heavily dependent on the terminal state. +func TestNetworkConfigPage(t *testing.T) { + setupMockDisplay := func(ctrl *gomock.Controller, cfg *service.ContributoorConfig) *InstallDisplay { + mockConfig := mock.NewMockConfigManager(ctrl) + mockConfig.EXPECT().Get().Return(cfg).AnyTimes() + mockConfig.EXPECT().Update(gomock.Any()).Return(nil).AnyTimes() + + return &InstallDisplay{ + app: tview.NewApplication(), + log: logrus.New(), + configService: mockConfig, + } + } + + t.Run("creates page with correct structure", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDisplay := setupMockDisplay(ctrl, &service.ContributoorConfig{}) + + // Create the page. + page := NewNetworkConfigPage(mockDisplay) + + // Verify the page was created. + assert.NotNil(t, page, "page should be created") + assert.NotNil(t, page.content, "page content should be set") + assert.IsType(t, &tview.Grid{}, page.content, "content should be a grid") + + // Verify the page ID and title. + assert.Equal(t, "install-network", page.page.ID) + assert.Equal(t, "Network Selection", page.page.Title) + }) + + t.Run("has correct network options", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDisplay := setupMockDisplay(ctrl, &service.ContributoorConfig{}) + + // Create the page. + page := NewNetworkConfigPage(mockDisplay) + + // Verify we have network options. + assert.NotNil(t, page.content, "content should be set") + assert.IsType(t, &tview.Grid{}, page.content, "content should be a grid") + + // Verify we have the correct number of networks available. + assert.Greater(t, len(tui.AvailableNetworks), 0, "should have available networks") + }) + + t.Run("has correct styling", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDisplay := setupMockDisplay(ctrl, &service.ContributoorConfig{}) + + // Create the page. + page := NewNetworkConfigPage(mockDisplay) + + // Verify basic styling. + assert.NotNil(t, page.content, "content should be set") + assert.IsType(t, &tview.Grid{}, page.content, "content should be a grid") + }) +} diff --git a/cmd/cli/commands/install/page_30_beacon_node_test.go b/cmd/cli/commands/install/page_30_beacon_node_test.go new file mode 100644 index 0000000..3d9e675 --- /dev/null +++ b/cmd/cli/commands/install/page_30_beacon_node_test.go @@ -0,0 +1,74 @@ +package install + +import ( + "testing" + + "github.com/ethpandaops/contributoor-installer/internal/service" + "github.com/ethpandaops/contributoor-installer/internal/service/mock" + "github.com/ethpandaops/contributoor-installer/internal/tui" + "github.com/rivo/tview" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// This is about the best we can do re testing TUI components. +// They're heavily dependent on the terminal state. +func TestBeaconNodePage(t *testing.T) { + setupMockDisplay := func(ctrl *gomock.Controller, cfg *service.ContributoorConfig) *InstallDisplay { + mockConfig := mock.NewMockConfigManager(ctrl) + mockConfig.EXPECT().Get().Return(cfg).AnyTimes() + + return &InstallDisplay{ + app: tview.NewApplication(), + log: logrus.New(), + configService: mockConfig, + networkConfigPage: &NetworkConfigPage{ + page: &tui.Page{ID: "network-config"}, + }, + } + } + + t.Run("creates page with correct structure", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDisplay := setupMockDisplay(ctrl, &service.ContributoorConfig{ + BeaconNodeAddress: "http://localhost:5052", + }) + + // Create the page. + page := NewBeaconNodePage(mockDisplay) + + // Verify the page was created. + assert.NotNil(t, page, "page should be created") + assert.NotNil(t, page.content, "page content should be set") + assert.IsType(t, &tview.Grid{}, page.content, "content should be a grid") + assert.NotNil(t, page.form, "form should be set") + + // Verify the page ID and title. + assert.Equal(t, "install-beacon", page.page.ID) + assert.Equal(t, "Beacon Node", page.page.Title) + + // Verify form structure. + assert.Equal(t, 1, page.form.GetFormItemCount(), "should have one form item initially") + assert.NotNil(t, page.form.GetButton(0), "should have Next button") + }) + + t.Run("has correct parent page", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDisplay := setupMockDisplay(ctrl, &service.ContributoorConfig{ + BeaconNodeAddress: "http://localhost:5052", + }) + + // Create the page. + page := NewBeaconNodePage(mockDisplay) + + // Verify parent page is set correctly. + assert.Equal(t, "network-config", page.page.Parent.ID) + }) +} diff --git a/cmd/cli/commands/install/page_40_output_server_test.go b/cmd/cli/commands/install/page_40_output_server_test.go new file mode 100644 index 0000000..6154e67 --- /dev/null +++ b/cmd/cli/commands/install/page_40_output_server_test.go @@ -0,0 +1,90 @@ +package install + +import ( + "testing" + + "github.com/ethpandaops/contributoor-installer/internal/service" + "github.com/ethpandaops/contributoor-installer/internal/service/mock" + "github.com/ethpandaops/contributoor-installer/internal/tui" + "github.com/rivo/tview" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// This is about the best we can do re testing TUI components. +// They're heavily dependent on the terminal state. +func TestOutputServerPage(t *testing.T) { + setupMockDisplay := func(ctrl *gomock.Controller, cfg *service.ContributoorConfig) *InstallDisplay { + if cfg.OutputServer == nil { + cfg.OutputServer = &service.OutputServerConfig{} + } + + mockConfig := mock.NewMockConfigManager(ctrl) + mockConfig.EXPECT().Get().Return(cfg).AnyTimes() + mockConfig.EXPECT().Update(gomock.Any()).Return(nil).AnyTimes() + + return &InstallDisplay{ + app: tview.NewApplication(), + log: logrus.New(), + configService: mockConfig, + beaconPage: &BeaconNodePage{ + page: &tui.Page{ID: "beacon-node"}, + }, + } + } + + t.Run("creates page with correct structure", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDisplay := setupMockDisplay(ctrl, &service.ContributoorConfig{}) + + // Create the page. + page := NewOutputServerPage(mockDisplay) + + // Verify the page was created. + assert.NotNil(t, page, "page should be created") + assert.NotNil(t, page.content, "page content should be set") + assert.IsType(t, &tview.Grid{}, page.content, "content should be a grid") + assert.NotNil(t, page.form, "form should be set") + + // Verify the page ID and title. + assert.Equal(t, "install-output", page.page.ID) + assert.Equal(t, "Output Server", page.page.Title) + + // Verify form structure - initially just has dropdown and button + assert.Equal(t, 1, page.form.GetFormItemCount(), "should have one form item initially") + assert.NotNil(t, page.form.GetButton(0), "should have Next button") + }) + + t.Run("has correct parent page", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDisplay := setupMockDisplay(ctrl, &service.ContributoorConfig{}) + + // Create the page. + page := NewOutputServerPage(mockDisplay) + + // Verify parent page is set correctly. + assert.Equal(t, "beacon-node", page.page.Parent.ID) + }) + + t.Run("initializes with default output server config", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDisplay := setupMockDisplay(ctrl, &service.ContributoorConfig{}) + + // Create the page. + page := NewOutputServerPage(mockDisplay) + + // Verify the page was created with default config. + assert.NotNil(t, page, "page should be created") + assert.NotNil(t, page.form, "form should be set") + }) +} diff --git a/cmd/cli/commands/install/page_50_output_server_credentials_test.go b/cmd/cli/commands/install/page_50_output_server_credentials_test.go new file mode 100644 index 0000000..314cb0d --- /dev/null +++ b/cmd/cli/commands/install/page_50_output_server_credentials_test.go @@ -0,0 +1,119 @@ +package install + +import ( + "testing" + + "github.com/ethpandaops/contributoor-installer/internal/service" + "github.com/ethpandaops/contributoor-installer/internal/service/mock" + "github.com/ethpandaops/contributoor-installer/internal/tui" + "github.com/rivo/tview" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// This is about the best we can do re testing TUI components. +// They're heavily dependent on the terminal state. +func TestOutputServerCredentialsPage(t *testing.T) { + setupMockDisplay := func(ctrl *gomock.Controller, cfg *service.ContributoorConfig) *InstallDisplay { + if cfg.OutputServer == nil { + cfg.OutputServer = &service.OutputServerConfig{} + } + + mockConfig := mock.NewMockConfigManager(ctrl) + mockConfig.EXPECT().Get().Return(cfg).AnyTimes() + mockConfig.EXPECT().Update(gomock.Any()).Return(nil).AnyTimes() + + return &InstallDisplay{ + app: tview.NewApplication(), + log: logrus.New(), + configService: mockConfig, + outputPage: &OutputServerPage{ + page: &tui.Page{ID: "output-server"}, + }, + } + } + + t.Run("creates page with correct structure", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDisplay := setupMockDisplay(ctrl, &service.ContributoorConfig{}) + + // Create the page. + page := NewOutputServerCredentialsPage(mockDisplay) + + // Verify the page was created. + assert.NotNil(t, page, "page should be created") + assert.NotNil(t, page.content, "page content should be set") + assert.IsType(t, &tview.Grid{}, page.content, "content should be a grid") + assert.NotNil(t, page.form, "form should be set") + + // Verify the page ID and title. + assert.Equal(t, "install-credentials", page.page.ID) + assert.Equal(t, "Output Server Credentials", page.page.Title) + + // Verify form structure. + assert.Equal(t, 2, page.form.GetFormItemCount(), "should have two form items") // Username and Password fields + assert.NotNil(t, page.form.GetButton(0), "should have Next button") + }) + + t.Run("has correct parent page", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDisplay := setupMockDisplay(ctrl, &service.ContributoorConfig{}) + + // Create the page. + page := NewOutputServerCredentialsPage(mockDisplay) + + // Verify parent page is set correctly. + assert.Equal(t, "output-server", page.page.Parent.ID) + }) + + t.Run("loads existing credentials", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create config with existing credentials. + cfg := &service.ContributoorConfig{ + OutputServer: &service.OutputServerConfig{ + Credentials: "dGVzdHVzZXI6dGVzdHBhc3M=", // base64 encoded "testuser:testpass" + }, + } + + mockDisplay := setupMockDisplay(ctrl, cfg) + + // Create the page. + page := NewOutputServerCredentialsPage(mockDisplay) + + // Verify credentials were loaded. + assert.Equal(t, "testuser", page.username, "should load existing username") + assert.Equal(t, "testpass", page.password, "should load existing password") + }) + + t.Run("handles invalid credentials gracefully", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create config with invalid credentials. + cfg := &service.ContributoorConfig{ + OutputServer: &service.OutputServerConfig{ + Credentials: "invalid-base64", + }, + } + + mockDisplay := setupMockDisplay(ctrl, cfg) + + // Create the page. + page := NewOutputServerCredentialsPage(mockDisplay) + + // Verify invalid credentials don't cause issues. + assert.Empty(t, page.username, "should have empty username for invalid credentials") + assert.Empty(t, page.password, "should have empty password for invalid credentials") + }) +} diff --git a/cmd/cli/commands/install/page_60_finished_test.go b/cmd/cli/commands/install/page_60_finished_test.go new file mode 100644 index 0000000..e20d97c --- /dev/null +++ b/cmd/cli/commands/install/page_60_finished_test.go @@ -0,0 +1,70 @@ +package install + +import ( + "testing" + + "github.com/ethpandaops/contributoor-installer/internal/service" + "github.com/ethpandaops/contributoor-installer/internal/service/mock" + "github.com/ethpandaops/contributoor-installer/internal/tui" + "github.com/rivo/tview" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +// This is about the best we can do re testing TUI components. +// They're heavily dependent on the terminal state. +func TestFinishedPage(t *testing.T) { + setupMockDisplay := func(ctrl *gomock.Controller) *InstallDisplay { + mockConfig := mock.NewMockConfigManager(ctrl) + mockConfig.EXPECT().Get().Return(&service.ContributoorConfig{}).AnyTimes() + + return &InstallDisplay{ + app: tview.NewApplication(), + log: logrus.New(), + configService: mockConfig, + outputServerCredentialsPage: &OutputServerCredentialsPage{ + page: &tui.Page{ID: "output-server-credentials"}, + }, + } + } + + t.Run("creates page with correct structure", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDisplay := setupMockDisplay(ctrl) + + // Create the page. + page := NewFinishedPage(mockDisplay) + + // Verify the page was created. + assert.NotNil(t, page, "page should be created") + assert.NotNil(t, page.content, "page content should be set") + assert.IsType(t, &tview.Grid{}, page.content, "content should be a grid") + assert.NotNil(t, page.form, "form should be set") + + // Verify the page ID and title. + assert.Equal(t, "install-finished", page.page.ID) + assert.Equal(t, "Installation Complete", page.page.Title) + + // Verify form structure. + assert.Equal(t, 0, page.form.GetFormItemCount(), "should have no form items") + assert.NotNil(t, page.form.GetButton(0), "should have Close button") + }) + + t.Run("has correct parent page", func(t *testing.T) { + // Setup. + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockDisplay := setupMockDisplay(ctrl) + + // Create the page. + page := NewFinishedPage(mockDisplay) + + // Verify parent page is set correctly. + assert.Equal(t, "output-server-credentials", page.page.Parent.ID) + }) +} diff --git a/internal/service/docker.go b/internal/service/docker.go index b073335..f836f44 100644 --- a/internal/service/docker.go +++ b/internal/service/docker.go @@ -46,7 +46,8 @@ func NewDockerService(logger *logrus.Logger, configService ConfigManager) (Docke // Start starts the docker container using docker-compose. func (s *dockerService) Start() error { - cmd := exec.Command("docker", "compose", "-f", s.composePath, "up", "-d", "--pull", "always") //nolint:gosec // validateComposePath() and filepath.Clean() in-use. + //nolint:gosec // validateComposePath() and filepath.Clean() in-use. + cmd := exec.Command("docker", "compose", "-f", s.composePath, "up", "-d", "--pull", "always") cmd.Env = s.getComposeEnv() if output, err := cmd.CombinedOutput(); err != nil { @@ -80,7 +81,8 @@ func (s *dockerService) Stop() error { // IsRunning checks if the docker container is running. func (s *dockerService) IsRunning() (bool, error) { - cmd := exec.Command("docker", "compose", "-f", s.composePath, "ps", "--format", "{{.State}}") //nolint:gosec // validateComposePath() and filepath.Clean() in-use. + //nolint:gosec // validateComposePath() and filepath.Clean() in-use. + cmd := exec.Command("docker", "compose", "-f", s.composePath, "ps", "--format", "{{.State}}") cmd.Env = s.getComposeEnv() output, err := cmd.Output()