Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add early cleanup for Slack E2E tests #1463

Merged
merged 6 commits into from
Jun 24, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions test/cloud-slack-dev-e2e/botkube_page_helpers_test.go
Original file line number Diff line number Diff line change
@@ -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)
185 changes: 112 additions & 73 deletions test/cloud-slack-dev-e2e/cloud_slack_dev_e2e_test.go
Original file line number Diff line number Diff line change
@@ -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),
2 changes: 1 addition & 1 deletion test/cloud-slack-dev-e2e/page_helpers_test.go
Original file line number Diff line number Diff line change
@@ -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
}

13 changes: 12 additions & 1 deletion test/cloud-slack-dev-e2e/slack_page_helpers_test.go
Original file line number Diff line number Diff line change
@@ -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,10 +85,21 @@ 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")
}

t.Log("Waiting for page auto-close...")
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)
}