diff --git a/test/cloud-slack-dev-e2e/botkube_page_helpers_test.go b/test/cloud-slack-dev-e2e/botkube_page_helpers_test.go index cf367f704..953861d71 100644 --- a/test/cloud-slack-dev-e2e/botkube_page_helpers_test.go +++ b/test/cloud-slack-dev-e2e/botkube_page_helpers_test.go @@ -3,6 +3,7 @@ package cloud_slack_dev_e2e import ( + "botkube.io/botube/test/cloud_graphql" "fmt" "net/http" "net/url" @@ -33,6 +34,7 @@ type BotkubeCloudPage struct { AuthHeaderValue string GQLEndpoint string ConnectedDeploy *gqlModel.Deployment + AlreadyDeleted bool } func NewBotkubeCloudPage(t *testing.T, cfg E2ESlackConfig) *BotkubeCloudPage { @@ -63,7 +65,7 @@ func (p *BotkubeCloudPage) HideCookieBanner(t *testing.T) { p.page.Screenshot() } -func (p *BotkubeCloudPage) CaptureBearerToken(t *testing.T, browser *rod.Browser) func() { +func (p *BotkubeCloudPage) InterceptBearerToken(t *testing.T, browser *rod.Browser) func() { t.Logf("Starting hijacking requests to %q to get the bearer token...", p.GQLEndpoint) router := browser.HijackRequests() @@ -80,6 +82,7 @@ func (p *BotkubeCloudPage) CaptureBearerToken(t *testing.T, browser *rod.Browser require.NotNil(t, ctx.Request) p.AuthHeaderValue = ctx.Request.Header(authHeaderName) + t.Log("Bearer token intercepted") ctx.ContinueRequest(&proto.FetchContinueRequest{}) }) go router.Run() @@ -90,8 +93,10 @@ func (p *BotkubeCloudPage) CreateNewInstance(t *testing.T, name string) { t.Log("Create new Botkube Instance") p.page.MustElement("h6#create-instance").MustClick() + time.Sleep(3 * time.Second) + p.page.Screenshot("after-clicking-create-instance") p.page.MustElement(`input[name="name"]`).MustSelectAllText().MustInput(name) - p.page.Screenshot() + p.page.Screenshot("after-filling-in-instance-name") // persist connected deploy info _, id, _ := strings.Cut(p.page.MustInfo().URL, "add/") @@ -152,22 +157,23 @@ func (p *BotkubeCloudPage) VerifyDeploymentStatus(t *testing.T, status string) { func (p *BotkubeCloudPage) SetupSlackWorkspace(t *testing.T, channel string) { t.Logf("Selecting newly connected %q Slack Workspace", p.cfg.Slack.WorkspaceName) - + time.Sleep(3 * time.Second) + p.page.Screenshot("before-selecting-workspace") p.page.MustElement(`input[type="search"]`). MustInput(p.cfg.Slack.WorkspaceName). MustType(input.Enter) - p.page.Screenshot() + p.page.Screenshot("after-selecting-workspace") // filter by channel, to make sure that it's visible on the first table page, in order to select it in the next step t.Log("Filtering by channel name") p.page.Mouse.MustScroll(10, 5000) // scroll bottom, as the footer collides with selecting filter - p.page.Screenshot() + p.page.Screenshot("before-filtering-channel") p.page.MustElement("table th:nth-child(3) span.ant-dropdown-trigger.ant-table-filter-trigger").MustClick() t.Log("Selecting channel checkbox") p.page.MustElement("input#name-channel").MustInput(channel).MustType(input.Enter) p.page.MustElement(fmt.Sprintf(`input[type="checkbox"][name="%s"]`, channel)).MustClick() - p.page.Screenshot() + p.page.Screenshot("after-selecting-channel") } func (p *BotkubeCloudPage) FinishWizard(t *testing.T) { @@ -193,6 +199,7 @@ func (p *BotkubeCloudPage) FinishWizard(t *testing.T) { p.page.Screenshot("after-second-next") t.Log("Submitting changes") + p.page.Mouse.MustMoveTo(0, 0) time.Sleep(3 * time.Second) p.page.MustElementR("button", "/^Deploy changes$/i"). MustWaitEnabled(). @@ -216,10 +223,11 @@ func (p *BotkubeCloudPage) UpdateKubectlNamespace(t *testing.T) { t.Log("Moving to top left corner of the page") p.page.Mouse.MustMoveTo(0, 0) - p.page.Screenshot("after-moving-to-top-left") + time.Sleep(3 * time.Second) t.Log("Submitting changes") p.page.MustWaitStable() + p.page.Screenshot("before-deploying-plugin-changes") p.page.MustElementR("button", "/Deploy changes/i").MustClick() p.page.Screenshot("after-deploying-plugin-changes") } @@ -247,6 +255,22 @@ func (p *BotkubeCloudPage) openKubectlUpdateForm() { p.page.Screenshot("after-selecting-kubectl-cfg-form") } +func (p *BotkubeCloudPage) Cleanup(t *testing.T, gqlCli *cloud_graphql.Client) { + if p.AlreadyDeleted { + return + } + + t.Log("Cleaning up Botkube instance on test failure...") + + if p.ConnectedDeploy == nil { + t.Log("No deployment to delete") + return + } + + deleteDeployment(t, gqlCli, p.ConnectedDeploy.ID, "connected") + p.AlreadyDeleted = true +} + func appendOrgIDQueryParam(t *testing.T, inURL, orgID string) string { parsedURL, err := url.Parse(inURL) require.NoError(t, err) diff --git a/test/cloud-slack-dev-e2e/cloud_slack_dev_e2e_test.go b/test/cloud-slack-dev-e2e/cloud_slack_dev_e2e_test.go index 3ae3b18d2..0ff67e360 100644 --- a/test/cloud-slack-dev-e2e/cloud_slack_dev_e2e_test.go +++ b/test/cloud-slack-dev-e2e/cloud_slack_dev_e2e_test.go @@ -72,7 +72,7 @@ type BotkubeCloudConfig struct { } func TestCloudSlackE2E(t *testing.T) { - t.Log("Loading configuration...") + t.Log("1. Loading configuration...") var cfg E2ESlackConfig err := envconfig.Init(&cfg) require.NoError(t, err) @@ -122,97 +122,82 @@ func TestCloudSlackE2E(t *testing.T) { botkubeCloudPage := NewBotkubeCloudPage(t, cfg) slackPage := NewSlackPage(t, cfg) - t.Run("Creating Botkube Instance with newly added Slack Workspace", func(t *testing.T) { - t.Log("Setting up browser...") + t.Log("2. Creating Botkube Instance with newly added Slack Workspace") - launcher := launcher.New().Headless(true) - isHeadless := launcher.Has(flags.Headless) - t.Cleanup(launcher.Cleanup) + t.Log("Setting up browser...") + launcher := launcher.New().Headless(true) + isHeadless := launcher.Has(flags.Headless) + t.Cleanup(launcher.Cleanup) - browser := rod.New().Trace(cfg.DebugMode).ControlURL(launcher.MustLaunch()).MustConnect() - t.Cleanup(func() { - err := browser.Close() - if err != nil { - t.Logf("Failed to close browser: %v", err) - } - }) + browser := rod.New().Trace(cfg.DebugMode).ControlURL(launcher.MustLaunch()).MustConnect() + t.Cleanup(func() { + err := browser.Close() + if err != nil { + t.Logf("Failed to close browser: %v", err) + } + }) - page := newBrowserPage(t, browser, cfg) - t.Cleanup(func() { - closePage(t, "page", page) - }) + page := newBrowserPage(t, browser, cfg) + t.Cleanup(func() { + closePage(t, "page", page) + }) - botkubeCloudPage.NavigateAndLogin(t, page) - botkubeCloudPage.HideCookieBanner(t) + stopRouter := botkubeCloudPage.InterceptBearerToken(t, browser) + defer stopRouter() - stopRouter := botkubeCloudPage.CaptureBearerToken(t, browser) - defer stopRouter() + botkubeCloudPage.NavigateAndLogin(t, page) + botkubeCloudPage.HideCookieBanner(t) - botkubeCloudPage.CreateNewInstance(t, channel.Name()) - botkubeCloudPage.InstallAgentInCluster(t, cfg.BotkubeCliBinaryPath) - botkubeCloudPage.OpenSlackAppIntegrationPage(t) + botkubeCloudPage.CreateNewInstance(t, channel.Name()) + t.Cleanup(func() { + // Delete Botkube instance. + // Cleanup is skipped if the instance was already deleted. + // This cleanup is needed if there's a fail between instance creation and Slack workspace connection. + gqlCli := createGQLCli(t, cfg, botkubeCloudPage) + botkubeCloudPage.Cleanup(t, gqlCli) + }) + botkubeCloudPage.InstallAgentInCluster(t, cfg.BotkubeCliBinaryPath) + botkubeCloudPage.OpenSlackAppIntegrationPage(t) - slackPage.ConnectWorkspace(t, browser) + slackPage.ConnectWorkspace(t, browser) + t.Cleanup(func() { + // Disconnect Slack workspace. + gqlCli := createGQLCli(t, cfg, botkubeCloudPage) + slackPage.Cleanup(t, gqlCli) + }) + t.Cleanup(func() { + // Delete Botkube instance. + // The code is repeated on purpose: we want to make sure the instance is cleaned up before the Slack workspace. + // t.Cleanup functions are called in last added, first called order. + gqlCli := createGQLCli(t, cfg, botkubeCloudPage) + botkubeCloudPage.Cleanup(t, gqlCli) + }) - botkubeCloudPage.ReAddSlackPlatformIfShould(t, isHeadless) - botkubeCloudPage.SetupSlackWorkspace(t, channel.Name()) - botkubeCloudPage.FinishWizard(t) - botkubeCloudPage.VerifyDeploymentStatus(t, "Connected") + botkubeCloudPage.ReAddSlackPlatformIfShould(t, isHeadless) + botkubeCloudPage.SetupSlackWorkspace(t, channel.Name()) + botkubeCloudPage.FinishWizard(t) + botkubeCloudPage.VerifyDeploymentStatus(t, "Connected") - botkubeCloudPage.UpdateKubectlNamespace(t) - botkubeCloudPage.VerifyDeploymentStatus(t, "Updating") - botkubeCloudPage.VerifyDeploymentStatus(t, "Connected") - botkubeCloudPage.VerifyUpdatedKubectlNamespace(t) - }) + botkubeCloudPage.UpdateKubectlNamespace(t) + botkubeCloudPage.VerifyDeploymentStatus(t, "Updating") + botkubeCloudPage.VerifyDeploymentStatus(t, "Connected") + botkubeCloudPage.VerifyUpdatedKubectlNamespace(t) t.Run("Run E2E tests with deployment", func(t *testing.T) { + gqlCli := createGQLCli(t, cfg, botkubeCloudPage) + connectedDeploy := botkubeCloudPage.ConnectedDeploy require.NotNil(t, connectedDeploy, "Previous subtest needs to pass to get connected deployment information") - require.NotEmpty(t, botkubeCloudPage.AuthHeaderValue, "Previous subtest needs to pass to get authorization header value") + // cleanup is done in the upper test function - t.Logf("Using Organization ID %q and Authorization header starting with %q", cfg.BotkubeCloud.TeamOrganizationID, - stringsutil.ShortenString(botkubeCloudPage.AuthHeaderValue, 15)) - - gqlCli := cloud_graphql.NewClientForAuthAndOrg(botkubeCloudPage.GQLEndpoint, cfg.BotkubeCloud.TeamOrganizationID, botkubeCloudPage.AuthHeaderValue) - - t.Logf("Getting connected Slack workspace...") - slackWorkspaces := gqlCli.MustListSlackWorkspacesForOrg(t, cfg.BotkubeCloud.TeamOrganizationID) - require.Len(t, slackWorkspaces, 1) - slackWorkspace := slackWorkspaces[0] + slackWorkspace := findConnectedSlackWorkspace(t, cfg, gqlCli) require.NotNil(t, slackWorkspace) - t.Cleanup(func() { - if !cfg.Slack.DisconnectWorkspaceAfterTests { - return - } - t.Log("Disconnecting Slack workspace...") - err = retryOperation(func() error { - return gqlCli.DeleteSlackWorkspace(t, cfg.BotkubeCloud.TeamOrganizationID, slackWorkspace.ID) - }) - if err != nil { - t.Logf("Failed to disconnect Slack workspace: %s", err.Error()) - } - }) + // cleanup is done in the upper test function t.Log("Creating a second deployment to test not connected flow...") notConnectedDeploy := gqlCli.MustCreateBasicDeploymentWithCloudSlack(t, fmt.Sprintf("%s-2", channel.Name()), slackWorkspace.TeamID, channel.Name()) t.Cleanup(func() { - t.Log("Deleting second deployment...") - err = retryOperation(func() error { - return gqlCli.DeleteDeployment(t, graphql.ID(notConnectedDeploy.ID)) - }) - if err != nil { - t.Logf("Failed to delete second deployment: %s", err.Error()) - } - }) - - t.Cleanup(func() { - t.Log("Deleting first deployment...") - err = retryOperation(func() error { - return gqlCli.DeleteDeployment(t, graphql.ID(connectedDeploy.ID)) - }) - if err != nil { - t.Logf("Failed to delete first deployment: %s", err.Error()) - } + deleteDeployment(t, gqlCli, notConnectedDeploy.ID, "second (not connected)") }) t.Log("Waiting for help message...") @@ -511,6 +496,60 @@ func createK8sCli(t *testing.T, kubeconfigPath string) *kubernetes.Clientset { return k8sCli } +func createGQLCli(t *testing.T, cfg E2ESlackConfig, botkubeCloudPage *BotkubeCloudPage) *cloud_graphql.Client { + require.NotEmpty(t, botkubeCloudPage.AuthHeaderValue, "Authorization header value should be set") + + t.Logf("Using Organization ID %q and Authorization header starting with %q", cfg.BotkubeCloud.TeamOrganizationID, + stringsutil.ShortenString(botkubeCloudPage.AuthHeaderValue, 15)) + return cloud_graphql.NewClientForAuthAndOrg(botkubeCloudPage.GQLEndpoint, cfg.BotkubeCloud.TeamOrganizationID, botkubeCloudPage.AuthHeaderValue) +} + +func findConnectedSlackWorkspace(t *testing.T, cfg E2ESlackConfig, gqlCli *cloud_graphql.Client) *gqlModel.SlackWorkspace { + t.Logf("Finding connected Slack workspace...") + slackWorkspaces := gqlCli.MustListSlackWorkspacesForOrg(t, cfg.BotkubeCloud.TeamOrganizationID) + if len(slackWorkspaces) == 0 { + return nil + } + + if len(slackWorkspaces) > 1 { + t.Logf("Found multiple connected Slack workspaces: %v", slackWorkspaces) + return nil + } + + slackWorkspace := slackWorkspaces[0] + return slackWorkspace +} + +func disconnectConnectedSlackWorkspace(t *testing.T, cfg E2ESlackConfig, gqlCli *cloud_graphql.Client, slackWorkspace *gqlModel.SlackWorkspace) { + if slackWorkspace == nil { + t.Log("Skipping disconnecting Slack workspace as it is nil") + return + } + + if !cfg.Slack.DisconnectWorkspaceAfterTests { + t.Log("Skipping disconnecting Slack workspace...") + return + } + + t.Log("Disconnecting Slack workspace...") + err := retryOperation(func() error { + return gqlCli.DeleteSlackWorkspace(t, cfg.BotkubeCloud.TeamOrganizationID, slackWorkspace.ID) + }) + if err != nil { + t.Logf("Failed to disconnect Slack workspace: %s", err.Error()) + } +} + +func deleteDeployment(t *testing.T, gqlCli *cloud_graphql.Client, deployID string, label string) { + t.Logf("Deleting %s deployment...", label) + err := retryOperation(func() error { + return gqlCli.DeleteDeployment(t, graphql.ID(deployID)) + }) + if err != nil { + t.Logf("Failed to delete first deployment: %s", err.Error()) + } +} + func retryOperation(fn func() error) error { return retry.Do(fn, retry.Attempts(cleanupRetryAttempts), diff --git a/test/cloud-slack-dev-e2e/page_helpers_test.go b/test/cloud-slack-dev-e2e/page_helpers_test.go index 00fb184b3..666cb3909 100644 --- a/test/cloud-slack-dev-e2e/page_helpers_test.go +++ b/test/cloud-slack-dev-e2e/page_helpers_test.go @@ -74,7 +74,7 @@ func closePage(t *testing.T, name string, page *rod.Page) { t.Helper() err := page.Close() if err != nil { - if errors.Is(err, context.Canceled) { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return } diff --git a/test/cloud-slack-dev-e2e/slack_page_helpers_test.go b/test/cloud-slack-dev-e2e/slack_page_helpers_test.go index f6397885d..46a6d3dab 100644 --- a/test/cloud-slack-dev-e2e/slack_page_helpers_test.go +++ b/test/cloud-slack-dev-e2e/slack_page_helpers_test.go @@ -3,6 +3,7 @@ package cloud_slack_dev_e2e import ( + "botkube.io/botube/test/cloud_graphql" "context" "errors" "github.com/stretchr/testify/assert" @@ -15,7 +16,6 @@ import ( const ( slackBaseURL = "slack.com" waitTime = 10 * time.Second - contextTimeout = 30 * time.Second shorterContextTimeout = 10 * time.Second ) @@ -85,6 +85,7 @@ func (p *SlackPage) ConnectWorkspace(t *testing.T, browser *rod.Browser) { time.Sleep(waitTime) p.page.Screenshot("before-workspace-connect") p.page.MustElement("button#slack-workspace-connect").MustClick() + time.Sleep(1 * time.Second) p.page.Screenshot("after-workspace-connect") } @@ -92,3 +93,13 @@ func (p *SlackPage) ConnectWorkspace(t *testing.T, browser *rod.Browser) { err = p.page.WaitIdle(waitTime) // wait for auto-close assert.NoError(t, err) } + +func (p *SlackPage) Cleanup(t *testing.T, gqlCli *cloud_graphql.Client) { + t.Log("Cleaning up Slack workspace on test failure...") + if !p.cfg.Slack.DisconnectWorkspaceAfterTests { + return + } + + slackWorkspace := findConnectedSlackWorkspace(t, p.cfg, gqlCli) + disconnectConnectedSlackWorkspace(t, p.cfg, gqlCli, slackWorkspace) +}