|
| 1 | +//go:build cloud_slack_dev_e2e |
| 2 | + |
| 3 | +package cloud_slack_dev_e2e |
| 4 | + |
| 5 | +import ( |
| 6 | + "fmt" |
| 7 | + "net/http" |
| 8 | + "net/url" |
| 9 | + "os/exec" |
| 10 | + "strings" |
| 11 | + "testing" |
| 12 | + "time" |
| 13 | + |
| 14 | + "github.com/go-rod/rod" |
| 15 | + "github.com/go-rod/rod/lib/input" |
| 16 | + "github.com/go-rod/rod/lib/proto" |
| 17 | + "github.com/mattn/go-shellwords" |
| 18 | + "github.com/stretchr/testify/require" |
| 19 | + |
| 20 | + gqlModel "github.com/kubeshop/botkube-cloud/botkube-cloud-backend/pkg/graphql" |
| 21 | +) |
| 22 | + |
| 23 | +const ( |
| 24 | + authHeaderName = "Authorization" |
| 25 | + awaitInstanceStatusChange = 2 * time.Minute |
| 26 | + orgQueryParam = "organizationId" |
| 27 | +) |
| 28 | + |
| 29 | +type BotkubeCloudPage struct { |
| 30 | + cfg E2ESlackConfig |
| 31 | + page *Page |
| 32 | + |
| 33 | + AuthHeaderValue string |
| 34 | + GQLEndpoint string |
| 35 | + ConnectedDeploy *gqlModel.Deployment |
| 36 | +} |
| 37 | + |
| 38 | +func NewBotkubeCloudPage(t *testing.T, cfg E2ESlackConfig) *BotkubeCloudPage { |
| 39 | + return &BotkubeCloudPage{ |
| 40 | + page: &Page{t: t, cfg: cfg}, |
| 41 | + cfg: cfg, |
| 42 | + GQLEndpoint: fmt.Sprintf("%s/%s", cfg.BotkubeCloud.APIBaseURL, cfg.BotkubeCloud.APIGraphQLEndpoint), |
| 43 | + } |
| 44 | +} |
| 45 | + |
| 46 | +func (p *BotkubeCloudPage) NavigateAndLogin(t *testing.T, page *rod.Page) { |
| 47 | + t.Log("Log into Botkube Cloud Dashboard") |
| 48 | + |
| 49 | + p.page.Page = page |
| 50 | + |
| 51 | + p.page.MustNavigate(appendOrgIDQueryParam(t, p.cfg.BotkubeCloud.UIBaseURL, p.cfg.BotkubeCloud.TeamOrganizationID)) |
| 52 | + p.page.MustWaitNavigation() |
| 53 | + |
| 54 | + p.page.MustElement(`input[name="username"]`).MustInput(p.cfg.BotkubeCloud.Email) |
| 55 | + p.page.MustElement(`input[name="password"]`).MustInput(p.cfg.BotkubeCloud.Password) |
| 56 | + p.page.MustElementR("button", "^Continue$").MustClick() |
| 57 | + p.page.Screenshot() |
| 58 | +} |
| 59 | + |
| 60 | +func (p *BotkubeCloudPage) HideCookieBanner(t *testing.T) { |
| 61 | + t.Log("Hide Botkube cookie banner") |
| 62 | + p.page.MustElementR("button", "^Decline$").MustClick() |
| 63 | + p.page.Screenshot() |
| 64 | +} |
| 65 | + |
| 66 | +func (p *BotkubeCloudPage) CaptureBearerToken(t *testing.T, browser *rod.Browser) func() { |
| 67 | + t.Logf("Starting hijacking requests to %q to get the bearer token...", p.GQLEndpoint) |
| 68 | + |
| 69 | + router := browser.HijackRequests() |
| 70 | + router.MustAdd(p.GQLEndpoint, func(ctx *rod.Hijack) { |
| 71 | + if p.AuthHeaderValue != "" { |
| 72 | + ctx.ContinueRequest(&proto.FetchContinueRequest{}) |
| 73 | + return |
| 74 | + } |
| 75 | + |
| 76 | + if ctx.Request != nil && ctx.Request.Method() != http.MethodPost { |
| 77 | + ctx.ContinueRequest(&proto.FetchContinueRequest{}) |
| 78 | + return |
| 79 | + } |
| 80 | + |
| 81 | + require.NotNil(t, ctx.Request) |
| 82 | + p.AuthHeaderValue = ctx.Request.Header(authHeaderName) |
| 83 | + ctx.ContinueRequest(&proto.FetchContinueRequest{}) |
| 84 | + }) |
| 85 | + go router.Run() |
| 86 | + return router.MustStop |
| 87 | +} |
| 88 | + |
| 89 | +func (p *BotkubeCloudPage) CreateNewInstance(t *testing.T, name string) { |
| 90 | + t.Log("Create new Botkube Instance") |
| 91 | + |
| 92 | + p.page.MustElement("h6#create-instance").MustClick() |
| 93 | + p.page.MustElement(`input[name="name"]`).MustSelectAllText().MustInput(name) |
| 94 | + p.page.Screenshot() |
| 95 | + |
| 96 | + // persist connected deploy info |
| 97 | + _, id, _ := strings.Cut(p.page.MustInfo().URL, "add/") |
| 98 | + p.ConnectedDeploy = &gqlModel.Deployment{ |
| 99 | + Name: name, |
| 100 | + ID: id, |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +func (p *BotkubeCloudPage) InstallAgentInCluster(t *testing.T, botkubeBinary string) { |
| 105 | + t.Log("Installing Botkube using Botkube CLI") |
| 106 | + |
| 107 | + installCmd := p.page.MustElement("div#install-upgrade-cmd > kbd").MustText() |
| 108 | + |
| 109 | + args, err := shellwords.Parse(installCmd) |
| 110 | + args = append(args, "--auto-approve") |
| 111 | + require.NoError(t, err) |
| 112 | + |
| 113 | + cmd := exec.Command(botkubeBinary, args[1:]...) |
| 114 | + installOutput, err := cmd.CombinedOutput() |
| 115 | + t.Log(string(installOutput)) |
| 116 | + require.NoError(t, err) |
| 117 | + |
| 118 | + p.page.MustElement("button#cluster-connected").MustClick() |
| 119 | +} |
| 120 | + |
| 121 | +func (p *BotkubeCloudPage) OpenSlackAppIntegrationPage(t *testing.T) { |
| 122 | + p.page.MustElement(`button[aria-label="Add tab"]`).MustClick() |
| 123 | + p.page.MustWaitStable() |
| 124 | + p.page.MustElementR("button", "^Slack$").MustClick() |
| 125 | + p.page.MustWaitStable() |
| 126 | + p.page.Screenshot() |
| 127 | + |
| 128 | + p.page.MustElementR("a", "Add to Slack").MustClick() |
| 129 | +} |
| 130 | + |
| 131 | +// ReAddSlackPlatformIfShould add the slack platform again as the page was often not refreshed with a newly connected Slack Workspace. |
| 132 | +// It only occurs with headless mode. |
| 133 | +// TODO(@pkosiec): Do you have a better idea how to fix it? |
| 134 | +func (p *BotkubeCloudPage) ReAddSlackPlatformIfShould(t *testing.T, isHeadless bool) { |
| 135 | + if !isHeadless { |
| 136 | + return |
| 137 | + } |
| 138 | + |
| 139 | + t.Log("Re-adding Slack platform") |
| 140 | + |
| 141 | + p.page.MustActivate() |
| 142 | + p.page.MustElement(`button[aria-label="remove"]`).MustClick() |
| 143 | + p.page.MustElement(`button[aria-label="Add tab"]`).MustClick() |
| 144 | + p.page.MustElementR("button", "^Slack$").MustClick() |
| 145 | + p.page.Screenshot() |
| 146 | +} |
| 147 | + |
| 148 | +func (p *BotkubeCloudPage) VerifyDeploymentStatus(t *testing.T, status string) { |
| 149 | + t.Logf("Waiting for status '%s'", status) |
| 150 | + p.page.Timeout(awaitInstanceStatusChange).MustElementR("div#deployment-status", status) |
| 151 | +} |
| 152 | + |
| 153 | +func (p *BotkubeCloudPage) SetupSlackWorkspace(t *testing.T, channel string) { |
| 154 | + t.Logf("Selecting newly connected %q Slack Workspace", p.cfg.Slack.WorkspaceName) |
| 155 | + |
| 156 | + p.page.MustElement(`input[type="search"]`). |
| 157 | + MustInput(p.cfg.Slack.WorkspaceName). |
| 158 | + MustType(input.Enter) |
| 159 | + p.page.Screenshot() |
| 160 | + |
| 161 | + // filter by channel, to make sure that it's visible on the first table page, in order to select it in the next step |
| 162 | + t.Log("Filtering by channel name") |
| 163 | + p.page.Keyboard.MustType(input.End) // scroll bottom, as the footer collides with selecting filter |
| 164 | + p.page.MustElement("table th:nth-child(3) span.ant-dropdown-trigger.ant-table-filter-trigger").MustFocus().MustClick() |
| 165 | + |
| 166 | + t.Log("Selecting channel checkbox") |
| 167 | + p.page.MustElement("input#name-channel").MustInput(channel).MustType(input.Enter) |
| 168 | + p.page.MustElement(fmt.Sprintf(`input[type="checkbox"][name="%s"]`, channel)).MustClick() |
| 169 | + p.page.Screenshot() |
| 170 | +} |
| 171 | + |
| 172 | +func (p *BotkubeCloudPage) FinishWizard(t *testing.T) { |
| 173 | + t.Log("Navigating to plugin selection") |
| 174 | + p.page.MustElementR("button", "/^Next$/i").MustClick().MustWaitStable() |
| 175 | + p.page.Screenshot() |
| 176 | + |
| 177 | + t.Log("Using pre-selected plugins. Navigating to wizard summary") |
| 178 | + p.page.MustElementR("button", "/^Next$/i").MustClick().MustWaitStable() |
| 179 | + p.page.Screenshot() |
| 180 | + |
| 181 | + t.Log("Submitting changes") |
| 182 | + p.page.MustElementR("button", "/^Deploy changes$/i").MustClick().MustWaitStable() |
| 183 | + p.page.Screenshot() |
| 184 | +} |
| 185 | + |
| 186 | +func (p *BotkubeCloudPage) UpdateKubectlNamespace(t *testing.T) { |
| 187 | + t.Log("Updating 'kubectl' namespace property") |
| 188 | + p.page.MustElementR(`div[role="tab"]`, "Plugins").MustClick() |
| 189 | + p.page.MustElement(`button[id^="botkube/kubectl_"]`).MustClick() |
| 190 | + p.page.MustElement(`div[data-node-key="ui-form"]`).MustClick() |
| 191 | + p.page.MustElementR("input#root_defaultNamespace", "default").MustSelectAllText().MustInput("kube-system") |
| 192 | + p.page.MustElementR("button", "/^Update$/i").MustClick() |
| 193 | + |
| 194 | + t.Log("Submitting changes") |
| 195 | + p.page.MustElementR("button", "/^Deploy changes$/i").MustClick().MustWaitStable() // use the case-insensitive flag "i" |
| 196 | +} |
| 197 | + |
| 198 | +func (p *BotkubeCloudPage) VerifyUpdatedKubectlNamespace(t *testing.T) { |
| 199 | + t.Log("Verifying that the 'namespace' value was updated and persisted properly") |
| 200 | + |
| 201 | + p.page.MustElementR(`div[role="tab"]`, "Plugins").MustClick() |
| 202 | + p.page.MustElement(`button[id^="botkube/kubectl_"]`).MustClick() |
| 203 | + p.page.MustElement(`div[data-node-key="ui-form"]`).MustClick() |
| 204 | + p.page.MustElementR("input#root_defaultNamespace", "kube-system") |
| 205 | +} |
| 206 | + |
| 207 | +func appendOrgIDQueryParam(t *testing.T, inURL, orgID string) string { |
| 208 | + parsedURL, err := url.Parse(inURL) |
| 209 | + require.NoError(t, err) |
| 210 | + queryValues := parsedURL.Query() |
| 211 | + queryValues.Set(orgQueryParam, orgID) |
| 212 | + parsedURL.RawQuery = queryValues.Encode() |
| 213 | + |
| 214 | + return parsedURL.String() |
| 215 | +} |
0 commit comments