From 5f8f944bf582dcd9747f128b95570dad3e5ea539 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Tue, 6 Sep 2022 10:10:01 +0100 Subject: [PATCH 01/20] Added Discord e2e tests while minding differences with existing slack tests. --- Makefile | 8 +- hack/goreleaser.sh | 6 +- helm/botkube/e2e-test-values.yaml | 38 + helm/botkube/values.yaml | 13 + test/e2e/bots_test.go | 1059 +++++++++++++++++ ...ack_tester_test.go => bots_tester_test.go} | 157 +++ test/e2e/k8s_helpers_test.go | 26 +- test/e2e/slack_test.go | 568 --------- 8 files changed, 1293 insertions(+), 582 deletions(-) create mode 100644 test/e2e/bots_test.go rename test/e2e/{slack_tester_test.go => bots_tester_test.go} (51%) delete mode 100644 test/e2e/slack_test.go diff --git a/Makefile b/Makefile index 86c4790b2..17f7ea9ff 100644 --- a/Makefile +++ b/Makefile @@ -60,11 +60,11 @@ container-image-single: pre-build @./hack/goreleaser.sh build_single @echo "Single target docker image build successful" -# Build the test image -container-image-test-single: pre-build +# Build the e2e test image +container-image-single-e2e: pre-build @echo "Building single target docker image for e2e test" - @./hack/goreleaser.sh build_test_single - @echo "Single target docker image build for e2e test successful" + @./hack/goreleaser.sh build_single_e2e + @echo "Single target docker image build for e2e tests successful" # Publish release using goreleaser gorelease: diff --git a/hack/goreleaser.sh b/hack/goreleaser.sh index 27aa4b965..a83c6b05d 100755 --- a/hack/goreleaser.sh +++ b/hack/goreleaser.sh @@ -114,7 +114,7 @@ build_single() { rm "$PWD/botkube" } -build_test_single() { +build_single_e2e(){ export GORELEASER_CURRENT_TAG=v9.99.9-dev docker run --rm --privileged \ -v "$PWD":/go/src/github.com/kubeshop/botkube \ @@ -156,8 +156,8 @@ case "${1}" in build_single) build_single ;; - build_test_single) - build_test_single + build_single_e2e) + build_single_e2e ;; release) release diff --git a/helm/botkube/e2e-test-values.yaml b/helm/botkube/e2e-test-values.yaml index 47c16f1d1..a8c389212 100644 --- a/helm/botkube/e2e-test-values.yaml +++ b/helm/botkube/e2e-test-values.yaml @@ -1,3 +1,9 @@ +## Parameters for anonymous analytics collection. +analytics: + # -- If true, sending anonymous analytics is disabled. To learn what date we collect, + # see [Privacy Policy](https://botkube.io/privacy#privacy-policy). + disable: true + communications: 'default-group': slack: @@ -21,6 +27,30 @@ communications: - kubectl-read-only sources: - k8s-updates + discord: + enabled: false # Tests will override this temporarily + token: "" # Provide a valid token for BotKube app + botID: "" # Provide a valid Application Client ID for BotKube app + channels: + 'default': + id: "" # Tests will override this channel ID temporarily + bindings: + executors: + - kubectl-read-only + - kubectl-wait-cmd + - kubectl-exec-cmd + - kubectl-allow-all + sources: + - k8s-events + 'secondary': + id: "" # Tests will override this channel ID temporarily + bindings: + # -- Executors configuration for a given channel. + executors: + - kubectl-read-only + # -- Notification sources configuration for a given channel. + sources: + - k8s-updates sources: 'k8s-events': @@ -103,6 +133,9 @@ executors: settings: clusterName: sample upgradeNotifier: false + log: + level: debug + disableColors: false extraAnnotations: botkube.io/disable: "true" @@ -114,3 +147,8 @@ e2eTest: slack: testerAppToken: "" # Provide a valid token for BotKube tester app additionalContextMessage: "" # Optional additional context + discord: + testerAppToken: "" # Provide a valid token for BotKube tester app + testerAppBotID: "" # Provide a valid token for BotKube tester app + guildID: "" + additionalContextMessage: "" # Optional additional context diff --git a/helm/botkube/values.yaml b/helm/botkube/values.yaml index 98119619e..fc2071aee 100644 --- a/helm/botkube/values.yaml +++ b/helm/botkube/values.yaml @@ -647,3 +647,16 @@ e2eTest: additionalContextMessage: "" # -- Message wait timeout. It defines how long we wait to ensure that notification were not sent when disabled. messageWaitTimeout: 1m + discord: + # -- Name of the BotKube bot to interact with during the e2e tests. + botName: "botkube" + # -- Name of the BotKube Tester bot that sends messages during the e2e tests. + testerName: "botkube_tester" + # -- Discord tester application token that interacts with BotKube bot. + testerAppToken: "" + # -- Discord tester application bot ID that interacts with BotKube tester bot. + testerAppBotID: "" + # -- Additional message that is sent by Tester. You can pass e.g. pull request number or source link where these tests are run from. + additionalContextMessage: "" + # -- Message wait timeout. It defines how long we wait to ensure that notification were not sent when disabled. + messageWaitTimeout: 1m diff --git a/test/e2e/bots_test.go b/test/e2e/bots_test.go new file mode 100644 index 000000000..0701cba0c --- /dev/null +++ b/test/e2e/bots_test.go @@ -0,0 +1,1059 @@ +//go:build integration + +package e2e + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/MakeNowJust/heredoc" + "github.com/bwmarrin/discordgo" + "github.com/kubeshop/botkube/pkg/filterengine/filters" + "github.com/slack-go/slack" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vrischmann/envconfig" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/clientcmd" +) + +type Config struct { + KubeconfigPath string `envconfig:"optional,KUBECONFIG"` + Deployment struct { + Name string `envconfig:"default=botkube"` + Namespace string `envconfig:"default=botkube"` + ContainerName string `envconfig:"default=botkube"` + WaitTimeout time.Duration `envconfig:"default=3m"` + Envs struct { + SlackEnabledName string `envconfig:"default=BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_SLACK_ENABLED"` + DefaultSlackChannelIDName string `envconfig:"default=BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_SLACK_CHANNELS_DEFAULT_NAME"` + SecondarySlackChannelIDName string `envconfig:"default=BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_SLACK_CHANNELS_SECONDARY_NAME"` + DiscordEnabledName string `envconfig:"default=BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_DISCORD_ENABLED"` + DefaultDiscordChannelIDName string `envconfig:"default=BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_DISCORD_CHANNELS_DEFAULT_ID"` + SecondaryDiscordChannelIDName string `envconfig:"default=BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_DISCORD_CHANNELS_SECONDARY_ID"` + } + } + ClusterName string `envconfig:"default=sample"` + Slack SlackConfig + Discord DiscordConfig +} + +type SlackConfig struct { + BotName string `envconfig:"default=botkube"` + TesterName string `envconfig:"default=tester"` + AdditionalContextMessage string `envconfig:"optional"` + TesterAppToken string + MessageWaitTimeout time.Duration `envconfig:"default=10s"` +} + +type DiscordConfig struct { + BotName string `envconfig:"default=botkube"` + TesterName string `envconfig:"default=tester"` + AdditionalContextMessage string `envconfig:"optional"` + GuildID string + TesterAppToken string + MessageWaitTimeout time.Duration `envconfig:"default=10s"` +} + +const ( + channelNamePrefix = "test" + welcomeText = "Let the tests begin 🤞" + pollInterval = time.Second +) + +func TestSlack(t *testing.T) { + t.Log("Loading configuration...") + var appCfg Config + err := envconfig.Init(&appCfg) + require.NoError(t, err) + + t.Log("Creating Slack API client with provided token...") + slackTester, err := newSlackTester(appCfg.Slack) + require.NoError(t, err) + + t.Log("Creating K8s client...") + k8sConfig, err := clientcmd.BuildConfigFromFlags("", appCfg.KubeconfigPath) + require.NoError(t, err) + k8sCli, err := kubernetes.NewForConfig(k8sConfig) + require.NoError(t, err) + + t.Log("Setting up test Slack setup...") + botUserID := slackTester.FindUserIDForBot(t) + testerUserID := slackTester.FindUserIDForTester(t) + + channel, cleanupChannelFn := slackTester.CreateChannel(t) + t.Cleanup(func() { cleanupChannelFn(t) }) + secondChannel, cleanupSecondChannelFn := slackTester.CreateChannel(t) + t.Cleanup(func() { cleanupSecondChannelFn(t) }) + + channels := map[string]*slack.Channel{ + appCfg.Deployment.Envs.DefaultSlackChannelIDName: channel, + appCfg.Deployment.Envs.SecondarySlackChannelIDName: secondChannel, + } + for _, currentChannel := range channels { + slackTester.PostInitialMessage(t, currentChannel.Name) + slackTester.InviteBotToChannel(t, botUserID, currentChannel.ID) + } + + t.Log("Patching Deployment with test env variables...") + deployNsCli := k8sCli.AppsV1().Deployments(appCfg.Deployment.Namespace) + revertDeployFn := setTestEnvsForDeploy(t, appCfg, deployNsCli, channels, nil) + t.Cleanup(func() { revertDeployFn(t) }) + + t.Log("Waiting for Deployment") + err = waitForDeploymentReady(deployNsCli, appCfg.Deployment.Name, appCfg.Deployment.WaitTimeout) + require.NoError(t, err) + + t.Log("Waiting for Bot message on channel...") + err = slackTester.WaitForMessagePostedRecentlyEqual(botUserID, channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) + require.NoError(t, err) + + t.Log("Running actual test cases") + + t.Run("Ping", func(t *testing.T) { + command := "ping" + expectedMessage := fmt.Sprintf("pong from cluster '%s'", appCfg.ClusterName) + + slackTester.PostMessageToBot(t, channel.Name, command) + err := slackTester.WaitForLastMessageContains(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Filters list", func(t *testing.T) { + command := "filters list" + expectedMessage := codeBlock(heredoc.Doc(` + FILTER ENABLED DESCRIPTION + NodeEventsChecker true Sends notifications on node level critical events. + ObjectAnnotationChecker true Checks if annotations present in object specs and filters them.`)) + + slackTester.PostMessageToBot(t, channel.Name, command) + err := slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Commands list", func(t *testing.T) { + command := "commands list" + expectedMessage := codeBlock(heredoc.Doc(` + Enabled executors: + kubectl: + kubectl-allow-all: + namespaces: + include: + - .* + enabled: true + commands: + verbs: + - get + resources: + - deployments + kubectl-read-only: + namespaces: + include: + - botkube + - default + enabled: true + commands: + verbs: + - api-resources + - api-versions + - cluster-info + - describe + - diff + - explain + - get + - logs + - top + - auth + resources: + - deployments + - pods + - namespaces + - daemonsets + - statefulsets + - storageclasses + - nodes + - configmaps + defaultNamespace: default + restrictAccess: false + kubectl-wait-cmd: + namespaces: + include: + - botkube + - default + enabled: true + commands: + verbs: + - wait + resources: [] + restrictAccess: false`)) + + t.Run("With default cluster", func(t *testing.T) { + slackTester.PostMessageToBot(t, channel.Name, command) + err := slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("With custom cluster name", func(t *testing.T) { + command := fmt.Sprintf("commands list --cluster-name %s", appCfg.ClusterName) + + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("With unknown cluster name", func(t *testing.T) { + command := "commands list --cluster-name non-existing" + + slackTester.PostMessageToBot(t, channel.Name, command) + t.Log("Ensuring bot didn't write anything new...") + time.Sleep(appCfg.Slack.MessageWaitTimeout) + // Same expected message as before + err = slackTester.WaitForLastMessageContains(testerUserID, channel.ID, command) + assert.NoError(t, err) + }) + }) + + t.Run("Executor", func(t *testing.T) { + t.Run("Get Deployment", func(t *testing.T) { + command := fmt.Sprintf("get deploy -n %s %s", appCfg.Deployment.Namespace, appCfg.Deployment.Name) + assertionFn := func(msg slack.Message) bool { + return strings.Contains(msg.Text, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg.Text, "botkube") + } + + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + assert.NoError(t, err) + }) + + t.Run("Get Configmap", func(t *testing.T) { + command := fmt.Sprintf("get configmap -n %s", appCfg.Deployment.Namespace) + assertionFn := func(msg slack.Message) bool { + return strings.Contains(msg.Text, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg.Text, "kube-root-ca.crt") && + strings.Contains(msg.Text, "botkube-global-config") + } + + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + assert.NoError(t, err) + }) + + t.Run("Get forbidden resource", func(t *testing.T) { + command := "get ingress" + expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'ingress' resources in the 'default' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) + + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Specify unknown command", func(t *testing.T) { + command := "unknown" + expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") + + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Specify invalid command", func(t *testing.T) { + command := "get" + expectedMessage := codeBlock(heredoc.Docf(`Cluster: %s + You must specify the type of resource to get. Use "kubectl api-resources" for a complete list of supported resources. + + error: Required resource not specified. + Use "kubectl explain <resource>" for a detailed description of that resource (e.g. kubectl explain pods). + See 'kubectl get -h' for help and examples + exit status 1`, appCfg.ClusterName)) + + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Specify forbidden namespace", func(t *testing.T) { + command := "get po --namespace team-b" + expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'po' resources in the 'team-b' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) + + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Based on other bindings", func(t *testing.T) { + t.Run("Wait for Deployment (the 2st binding)", func(t *testing.T) { + command := fmt.Sprintf("wait deployment -n %s %s --for condition=Available=True", appCfg.Deployment.Namespace, appCfg.Deployment.Name) + assertionFn := func(msg slack.Message) bool { + return strings.Contains(msg.Text, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg.Text, "deployment.apps/botkube condition met") + } + + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + assert.NoError(t, err) + }) + + t.Run("Exec (the 3rd binding which is disabled)", func(t *testing.T) { + command := "exec" + expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") + + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Get all Pods (the 4th binding)", func(t *testing.T) { + command := "get pods -A" + expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'pods' resources for all Namespaces on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) + + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Get all Deployments (the 4th binding)", func(t *testing.T) { + command := "get deploy -A" + assertionFn := func(msg slack.Message) bool { + return strings.Contains(msg.Text, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg.Text, "local-path-provisioner") && + strings.Contains(msg.Text, "coredns") && + strings.Contains(msg.Text, "botkube") + } + + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + assert.NoError(t, err) + }) + }) + }) + + t.Run("Multi-channel notifications", func(t *testing.T) { + cfgMapCli := k8sCli.CoreV1().ConfigMaps(appCfg.Deployment.Namespace) + var channelIDs []string + for _, channel := range channels { + channelIDs = append(channelIDs, channel.ID) + } + + t.Log("Creating ConfigMap...") + var cfgMapAlreadyDeleted bool + cfgMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: channel.Name, + Namespace: appCfg.Deployment.Namespace, + }, + } + cfgMap, err = cfgMapCli.Create(context.Background(), cfgMap, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { cleanupCreatedCfgMapIfShould(t, cfgMapCli, cfgMap.Name, &cfgMapAlreadyDeleted) }) + + t.Log("Expecting bot message in first channel...") + assertionFn := func(msg slack.Message) bool { + return doesSlackMessageContainExactlyOneAttachment( + msg, + "v1/configmaps created", + "2eb886", + fmt.Sprintf("ConfigMap *%s/%s* has been created in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), + ) + } + err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + require.NoError(t, err) + + t.Log("Expecting no bot message in second channel...") + expectedMessage := fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName) + time.Sleep(appCfg.Slack.MessageWaitTimeout) + err = slackTester.WaitForLastMessageEqual(botUserID, secondChannel.ID, expectedMessage) + require.NoError(t, err) + + t.Log("Updating ConfigMap...") + cfgMap.Data = map[string]string{ + "operation": "update", + } + cfgMap, err = cfgMapCli.Update(context.Background(), cfgMap, metav1.UpdateOptions{}) + require.NoError(t, err) + + t.Log("Expecting bot message in all channels...") + assertionFn = func(msg slack.Message) bool { + return doesSlackMessageContainExactlyOneAttachment( + msg, + "v1/configmaps updated", + "daa038", + fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), + ) + } + err = slackTester.WaitForMessagesPostedOnChannels(botUserID, channelIDs, 1, assertionFn) + require.NoError(t, err) + + t.Log("Stopping notifier...") + command := "notifier stop" + expectedMessage = codeBlock(fmt.Sprintf("Sure! I won't send you notifications from cluster '%s' here.", appCfg.ClusterName)) + + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + + t.Log("Getting notifier status from second channel...") + command = "notifier status" + expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are enabled here.", appCfg.ClusterName)) + slackTester.PostMessageToBot(t, secondChannel.Name, command) + err = slackTester.WaitForLastMessageEqual(botUserID, secondChannel.ID, expectedMessage) + assert.NoError(t, err) + + t.Log("Getting notifier status from first channel...") + command = "notifier status" + expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are disabled here.", appCfg.ClusterName)) + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + + t.Log("Updating ConfigMap once again...") + cfgMap.Data = map[string]string{ + "operation": "update-second", + } + _, err = cfgMapCli.Update(context.Background(), cfgMap, metav1.UpdateOptions{}) + require.NoError(t, err) + + t.Log("Ensuring bot didn't write anything new on first channel...") + time.Sleep(appCfg.Slack.MessageWaitTimeout) + // Same expected message as before + err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + require.NoError(t, err) + + t.Log("Expecting bot message on second channel...") + assertionFn = func(msg slack.Message) bool { + return doesSlackMessageContainExactlyOneAttachment( + msg, + "v1/configmaps updated", + "daa038", + fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), + ) + } + err = slackTester.WaitForMessagePosted(botUserID, secondChannel.ID, 1, assertionFn) + + t.Log("Starting notifier") + command = "notifier start" + expectedMessage = codeBlock(fmt.Sprintf("Brace yourselves, incoming notifications from cluster '%s'.", appCfg.ClusterName)) + slackTester.PostMessageToBot(t, channel.Name, command) + err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + require.NoError(t, err) + + t.Log("Creating and deleting ignored ConfigMap") + ignoredCfgMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-ignored", channel.Name), + Namespace: appCfg.Deployment.Namespace, + Annotations: map[string]string{ + filters.DisableAnnotation: "true", + }, + }, + } + _, err = cfgMapCli.Create(context.Background(), ignoredCfgMap, metav1.CreateOptions{}) + require.NoError(t, err) + err = cfgMapCli.Delete(context.Background(), ignoredCfgMap.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + + t.Log("Ensuring bot didn't write anything new...") + time.Sleep(appCfg.Slack.MessageWaitTimeout) + err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + require.NoError(t, err) + + t.Log("Deleting ConfigMap") + err = cfgMapCli.Delete(context.Background(), cfgMap.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + cfgMapAlreadyDeleted = true + + t.Log("Expecting bot message on first channel...") + assertionFn = func(msg slack.Message) bool { + return doesSlackMessageContainExactlyOneAttachment( + msg, + "v1/configmaps deleted", + "a30200", + fmt.Sprintf("ConfigMap *%s/%s* has been deleted in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), + ) + } + err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + require.NoError(t, err) + + t.Log("Ensuring bot didn't write anything new on second channel...") + time.Sleep(appCfg.Slack.MessageWaitTimeout) + assertionFn = func(msg slack.Message) bool { + return doesSlackMessageContainExactlyOneAttachment( + msg, + "v1/configmaps updated", + "daa038", + fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), + ) + } + err = slackTester.WaitForMessagePosted(botUserID, secondChannel.ID, 1, assertionFn) + require.NoError(t, err) + }) + + t.Run("Recommendations", func(t *testing.T) { + podCli := k8sCli.CoreV1().Pods(appCfg.Deployment.Namespace) + + t.Log("Creating Pod...") + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: channel.Name, + Namespace: appCfg.Deployment.Namespace, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + {Name: "nginx", Image: "nginx:latest"}, + }, + }, + } + require.Len(t, pod.Spec.Containers, 1) + pod, err = podCli.Create(context.Background(), pod, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { cleanupCreatedPod(t, podCli, pod.Name) }) + + t.Log("Expecting bot message...") + assertionFn := func(msg slack.Message) bool { + if len(msg.Attachments) != 1 { + return false + } + + attachment := msg.Attachments[0] + title := attachment.Title + + if len(attachment.Fields) != 1 { + return false + } + + fieldMessage := attachment.Fields[0].Value + return title == "v1/pods created" && + strings.Contains(fieldMessage, "Recommendations:") && + strings.Contains(fieldMessage, fmt.Sprintf("- Pod '%s/%s' created without labels. Consider defining them, to be able to use them as a selector e.g. in Service.", pod.Namespace, pod.Name)) && + strings.Contains(fieldMessage, fmt.Sprintf("- The 'latest' tag used in '%s' image of Pod '%s/%s' container '%s' should be avoided.", pod.Spec.Containers[0].Image, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)) + } + err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + require.NoError(t, err) + }) +} + +func TestDiscord(t *testing.T) { + t.Log("Loading configuration...") + var appCfg Config + err := envconfig.Init(&appCfg) + require.NoError(t, err) + + t.Log("Creating Discord API client with provided token...") + discordTester, err := newDiscordTester(appCfg.Discord) + require.NoError(t, err) + + t.Log("Creating K8s client...") + k8sConfig, err := clientcmd.BuildConfigFromFlags("", appCfg.KubeconfigPath) + require.NoError(t, err) + k8sCli, err := kubernetes.NewForConfig(k8sConfig) + require.NoError(t, err) + + t.Log("Setting up test Slack setup...") + botUserID := discordTester.FindUserIDForBot(t) + testerUserID := discordTester.FindUserIDForTester(t) + t.Logf("Just loaded botUserID...: %+v", botUserID) + + channel, cleanupChannelFn := discordTester.CreateChannel(t) + t.Cleanup(func() { cleanupChannelFn(t) }) + secondChannel, cleanupSecondChannelFn := discordTester.CreateChannel(t) + t.Cleanup(func() { cleanupSecondChannelFn(t) }) + + channels := map[string]*discordgo.Channel{ + appCfg.Deployment.Envs.DefaultDiscordChannelIDName: channel, + appCfg.Deployment.Envs.SecondaryDiscordChannelIDName: secondChannel, + } + + for _, currentChannel := range channels { + discordTester.PostInitialMessage(t, currentChannel.ID) + discordTester.InviteBotToChannel(t, botUserID, currentChannel.ID) + } + + t.Log("Patching Deployment with test env variables...") + deployNsCli := k8sCli.AppsV1().Deployments(appCfg.Deployment.Namespace) + revertDeployFn := setTestEnvsForDeploy(t, appCfg, deployNsCli, nil, channels) + t.Cleanup(func() { revertDeployFn(t) }) + + t.Log("Waiting for Deployment") + err = waitForDeploymentReady(deployNsCli, appCfg.Deployment.Name, appCfg.Deployment.WaitTimeout) + require.NoError(t, err) + + t.Log("Waiting for Bot message on channel from user") + err = discordTester.WaitForMessagePostedRecentlyEqual(botUserID, channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) + require.NoError(t, err) + + t.Log("Running actual test cases") + + t.Run("Ping", func(t *testing.T) { + command := "ping" + expectedMessage := fmt.Sprintf("pong from cluster '%s'", appCfg.ClusterName) + + //discordTester.PostMessageToBot(t, channel.ID, command) + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err := discordTester.WaitForLastMessageContains(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Filters list", func(t *testing.T) { + command := "filters list" + expectedMessage := codeBlock(heredoc.Doc(` + FILTER ENABLED DESCRIPTION + NodeEventsChecker true Sends notifications on node level critical events. + ObjectAnnotationChecker true Checks if annotations botkube.io/* present in object specs and filters them.`)) + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err := discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Commands list", func(t *testing.T) { + command := "commands list" + expectedMessage := codeBlock(heredoc.Doc(` + Enabled executors: + kubectl: + kubectl-allow-all: + namespaces: + include: + - .* + enabled: true + commands: + verbs: + - get + resources: + - deployments + kubectl-read-only: + namespaces: + include: + - botkube + - default + enabled: true + commands: + verbs: + - api-resources + - api-versions + - cluster-info + - describe + - diff + - explain + - get + - logs + - top + - auth + resources: + - deployments + - pods + - namespaces + - daemonsets + - statefulsets + - storageclasses + - nodes + - configmaps + defaultNamespace: default + restrictAccess: false + kubectl-wait-cmd: + namespaces: + include: + - botkube + - default + enabled: true + commands: + verbs: + - wait + resources: [] + restrictAccess: false`)) + + t.Run("With default cluster", func(t *testing.T) { + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err := discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("With custom cluster name", func(t *testing.T) { + command := fmt.Sprintf("commands list --cluster-name %s", appCfg.ClusterName) + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("With unknown cluster name", func(t *testing.T) { + command := "commands list --cluster-name non-existing" + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + t.Log("Ensuring bot didn't write anything new...") + time.Sleep(appCfg.Discord.MessageWaitTimeout) + // Same expected message as before + err = discordTester.WaitForLastMessageContains(testerUserID, channel.ID, command) + assert.NoError(t, err) + }) + }) + + t.Run("Executor", func(t *testing.T) { + t.Run("Get Deployment", func(t *testing.T) { + command := fmt.Sprintf("get deploy -n %s %s", appCfg.Deployment.Namespace, appCfg.Deployment.Name) + assertionFn := func(msg *discordgo.Message) bool { + return strings.Contains(msg.Content, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg.Content, "botkube") + } + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + assert.NoError(t, err) + }) + + t.Run("Get Configmap", func(t *testing.T) { + command := fmt.Sprintf("get configmap -n %s", appCfg.Deployment.Namespace) + assertionFn := func(msg *discordgo.Message) bool { + return strings.Contains(msg.Content, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg.Content, "kube-root-ca.crt") && + strings.Contains(msg.Content, "botkube-global-config") + } + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + assert.NoError(t, err) + }) + + t.Run("Get forbidden resource", func(t *testing.T) { + command := "get ingress" + expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'ingress' resources in the 'default' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Specify unknown command", func(t *testing.T) { + command := "unknown" + expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Specify invalid command", func(t *testing.T) { + command := "get" + expectedMessage := codeBlock(fmt.Sprintf("Cluster: %s\nYou must specify the type of resource to get. Use \"kubectl api-resources\" for a complete list of supported resources.\n\nerror: Required resource not specified.\nUse \"kubectl explain \" for a detailed description of that resource (e.g. kubectl explain pods).\nSee 'kubectl get -h' for help and examples\nexit status 1", appCfg.ClusterName)) + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Specify forbidden namespace", func(t *testing.T) { + command := "get po --namespace team-b" + expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'po' resources in the 'team-b' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Based on other bindings", func(t *testing.T) { + t.Run("Wait for Deployment (the 2st binding)", func(t *testing.T) { + command := fmt.Sprintf("wait deployment -n %s %s --for condition=Available=True", appCfg.Deployment.Namespace, appCfg.Deployment.Name) + assertionFn := func(msg *discordgo.Message) bool { + return strings.Contains(msg.Content, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg.Content, "deployment.apps/botkube condition met") + } + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + assert.NoError(t, err) + }) + + t.Run("Exec (the 3rd binding which is disabled)", func(t *testing.T) { + command := "exec" + expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Get all Pods (the 4th binding)", func(t *testing.T) { + command := "get pods -A" + expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'pods' resources for all Namespaces on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + }) + + t.Run("Get all Deployments (the 4th binding)", func(t *testing.T) { + command := "get deploy -A" + assertionFn := func(msg *discordgo.Message) bool { + return strings.Contains(msg.Content, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg.Content, "local-path-provisioner") && + strings.Contains(msg.Content, "coredns") && + strings.Contains(msg.Content, "botkube") + } + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + assert.NoError(t, err) + }) + }) + }) + + t.Run("Multi-channel notifications", func(t *testing.T) { + cfgMapCli := k8sCli.CoreV1().ConfigMaps(appCfg.Deployment.Namespace) + var channelIDs []string + for _, channel := range channels { + channelIDs = append(channelIDs, channel.ID) + } + + t.Log("Creating ConfigMap...") + var cfgMapAlreadyDeleted bool + cfgMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: channel.Name, + Namespace: appCfg.Deployment.Namespace, + }, + } + cfgMap, err = cfgMapCli.Create(context.Background(), cfgMap, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { cleanupCreatedCfgMapIfShould(t, cfgMapCli, cfgMap.Name, &cfgMapAlreadyDeleted) }) + + t.Log("Expecting bot message in first channel...") + assertionFn := func(msg *discordgo.Message) bool { + return doesDiscordMessageContainExactlyOneEmbed( + msg, + "v1/configmaps created", + 8311585, + fmt.Sprintf("ConfigMap *%s/%s* has been created in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), + ) + } + err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + require.NoError(t, err) + + t.Log("Expecting no bot message in second channel...") + expectedMessage := fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName) + time.Sleep(appCfg.Discord.MessageWaitTimeout) + err = discordTester.WaitForLastMessageEqual(botUserID, secondChannel.ID, expectedMessage) + require.NoError(t, err) + + t.Log("Updating ConfigMap...") + cfgMap.Data = map[string]string{ + "operation": "update", + } + cfgMap, err = cfgMapCli.Update(context.Background(), cfgMap, metav1.UpdateOptions{}) + require.NoError(t, err) + + t.Log("Expecting bot message in all channels...") + assertionFn = func(msg *discordgo.Message) bool { + return doesDiscordMessageContainExactlyOneEmbed( + msg, + "v1/configmaps updated", + 16312092, + fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), + ) + } + err = discordTester.WaitForMessagesPostedOnChannels(botUserID, channelIDs, 1, assertionFn) + require.NoError(t, err) + + t.Log("Stopping notifier...") + command := "notifier stop" + expectedMessage = codeBlock(fmt.Sprintf("Sure! I won't send you notifications from cluster '%s' here.", appCfg.ClusterName)) + + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + + t.Log("Getting notifier status from second channel...") + command = "notifier status" + expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are enabled here.", appCfg.ClusterName)) + discordTester.PostMessageToBot(t, botUserID, secondChannel.ID, command) + err = discordTester.WaitForLastMessageEqual(botUserID, secondChannel.ID, expectedMessage) + assert.NoError(t, err) + + t.Log("Getting notifier status from first channel...") + command = "notifier status" + expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are disabled here.", appCfg.ClusterName)) + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + assert.NoError(t, err) + + t.Log("Updating ConfigMap once again...") + cfgMap.Data = map[string]string{ + "operation": "update-second", + } + _, err = cfgMapCli.Update(context.Background(), cfgMap, metav1.UpdateOptions{}) + require.NoError(t, err) + + t.Log("Ensuring bot didn't write anything new on first channel...") + time.Sleep(appCfg.Slack.MessageWaitTimeout) + // Same expected message as before + err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + require.NoError(t, err) + + t.Log("Expecting bot message on second channel...") + assertionFn = func(msg *discordgo.Message) bool { + return doesDiscordMessageContainExactlyOneEmbed( + msg, + "v1/configmaps updated", + 16312092, + fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), + ) + } + err = discordTester.WaitForMessagePosted(botUserID, secondChannel.ID, 1, assertionFn) + require.NoError(t, err) + + t.Log("Starting notifier") + command = "notifier start" + expectedMessage = codeBlock(fmt.Sprintf("Brace yourselves, incoming notifications from cluster '%s'.", appCfg.ClusterName)) + discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + require.NoError(t, err) + + t.Log("Creating and deleting ignored ConfigMap") + ignoredCfgMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-ignored", channel.Name), + Namespace: appCfg.Deployment.Namespace, + Annotations: map[string]string{ + filters.DisableAnnotation: "true", + }, + }, + } + _, err = cfgMapCli.Create(context.Background(), ignoredCfgMap, metav1.CreateOptions{}) + require.NoError(t, err) + err = cfgMapCli.Delete(context.Background(), ignoredCfgMap.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + + t.Log("Ensuring bot didn't write anything new...") + time.Sleep(appCfg.Slack.MessageWaitTimeout) + err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + require.NoError(t, err) + + t.Log("Deleting ConfigMap") + err = cfgMapCli.Delete(context.Background(), cfgMap.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + cfgMapAlreadyDeleted = true + + t.Log("Expecting bot message on first channel...") + assertionFn = func(msg *discordgo.Message) bool { + return doesDiscordMessageContainExactlyOneEmbed( + msg, + "v1/configmaps deleted", + 13632027, + fmt.Sprintf("ConfigMap *%s/%s* has been deleted in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), + ) + } + err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + require.NoError(t, err) + + t.Log("Ensuring bot didn't write anything new on second channel...") + time.Sleep(appCfg.Slack.MessageWaitTimeout) + assertionFn = func(msg *discordgo.Message) bool { + return doesDiscordMessageContainExactlyOneEmbed( + msg, + "v1/configmaps updated", + 16312092, + fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), + ) + } + err = discordTester.WaitForMessagePosted(botUserID, secondChannel.ID, 1, assertionFn) + require.NoError(t, err) + }) + + t.Run("Recommendations", func(t *testing.T) { + podCli := k8sCli.CoreV1().Pods(appCfg.Deployment.Namespace) + + t.Log("Creating Pod...") + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: channel.Name, + Namespace: appCfg.Deployment.Namespace, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + {Name: "nginx", Image: "nginx:latest"}, + }, + }, + } + require.Len(t, pod.Spec.Containers, 1) + pod, err = podCli.Create(context.Background(), pod, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { cleanupCreatedPod(t, podCli, pod.Name) }) + + t.Log("Expecting bot message...") + assertionFn := func(msg *discordgo.Message) bool { + if len(msg.Embeds) != 1 { + return false + } + + embed := msg.Embeds[0] + title := embed.Title + fieldMessage := embed.Description + + return title == "v1/pods created" && + strings.Contains(fieldMessage, "Recommendations:") && + strings.Contains(fieldMessage, fmt.Sprintf("- Pod '%s/%s' created without labels. Consider defining them, to be able to use them as a selector e.g. in Service.", pod.Namespace, pod.Name)) && + strings.Contains(fieldMessage, fmt.Sprintf("- The 'latest' tag used in '%s' image of Pod '%s/%s' container '%s' should be avoided.", pod.Spec.Containers[0].Image, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)) + } + err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + require.NoError(t, err) + }) +} + +func codeBlock(in string) string { + return fmt.Sprintf("```\n%s\n```", in) +} + +func doesSlackMessageContainExactlyOneAttachment(msg slack.Message, expectedTitle, expectedColor, expectedFieldMessage string) bool { + if len(msg.Attachments) != 1 { + return false + } + + attachment := msg.Attachments[0] + title := attachment.Title + color := attachment.Color + + if len(attachment.Fields) != 1 { + return false + } + + fieldMessage := attachment.Fields[0].Value + + return title == expectedTitle && + color == expectedColor && + fieldMessage == expectedFieldMessage +} + +func doesDiscordMessageContainExactlyOneEmbed(msg *discordgo.Message, expectedTitle string, expectedColor int, expectedFieldMessage string) bool { + if len(msg.Embeds) != 1 { + return false + } + + embed := msg.Embeds[0] + return embed.Title == expectedTitle && + embed.Color == expectedColor && + embed.Description == expectedFieldMessage +} + +func cleanupCreatedCfgMapIfShould(t *testing.T, cfgMapCli corev1.ConfigMapInterface, name string, cfgMapAlreadyDeleted *bool) { + if cfgMapAlreadyDeleted != nil && *cfgMapAlreadyDeleted { + return + } + + t.Log("Cleaning up created ConfigMap...") + err := cfgMapCli.Delete(context.Background(), name, metav1.DeleteOptions{}) + assert.NoError(t, err) +} + +func cleanupCreatedPod(t *testing.T, podCli corev1.PodInterface, name string) { + t.Log("Cleaning up created Pod...") + err := podCli.Delete(context.Background(), name, metav1.DeleteOptions{}) + assert.NoError(t, err) +} diff --git a/test/e2e/slack_tester_test.go b/test/e2e/bots_tester_test.go similarity index 51% rename from test/e2e/slack_tester_test.go rename to test/e2e/bots_tester_test.go index 14caddf9a..6b0c3335c 100644 --- a/test/e2e/slack_tester_test.go +++ b/test/e2e/bots_tester_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/bwmarrin/discordgo" "github.com/google/uuid" "github.com/slack-go/slack" "github.com/stretchr/testify/assert" @@ -25,6 +26,11 @@ type slackTester struct { cfg SlackConfig } +type discordTester struct { + cli *discordgo.Session + cfg DiscordConfig +} + func newSlackTester(slackCfg SlackConfig) (*slackTester, error) { slackCli := slack.New(slackCfg.TesterAppToken) _, err := slackCli.AuthTest() @@ -35,6 +41,157 @@ func newSlackTester(slackCfg SlackConfig) (*slackTester, error) { return &slackTester{cli: slackCli, cfg: slackCfg}, nil } +func newDiscordTester(discordCfg DiscordConfig) (*discordTester, error) { + discordCli, err := discordgo.New("Bot " + discordCfg.TesterAppToken) + if err != nil { + return nil, fmt.Errorf("while creating Discord session: %w", err) + } + return &discordTester{cli: discordCli, cfg: discordCfg}, nil +} + +func (d *discordTester) CreateChannel(t *testing.T) (*discordgo.Channel, func(t *testing.T)) { + t.Helper() + randomID := uuid.New() + channelName := fmt.Sprintf("%s-%s", channelNamePrefix, randomID.String()) + + t.Logf("Creating channel %q...", channelName) + channel, err := d.cli.GuildChannelCreate(d.cfg.GuildID, channelName, discordgo.ChannelTypeGuildText) + require.NoError(t, err) + + t.Logf("Channel %q (ID: %q) created", channelName, channel.ID) + + cleanupFn := func(t *testing.T) { + t.Helper() + t.Logf("Deleting channel %q...", channel.Name) + // We cannot archive a channel: https://support.discord.com/hc/en-us/community/posts/360042842012-Archive-old-chat-channels + _, err := d.cli.ChannelDelete(channel.ID) + assert.NoError(t, err) + } + + return channel, cleanupFn +} + +func (d *discordTester) FindUserIDForBot(t *testing.T) string { + return d.FindUserID(t, d.cfg.BotName) +} + +func (d *discordTester) FindUserIDForTester(t *testing.T) string { + return d.FindUserID(t, d.cfg.TesterName) +} + +func (d *discordTester) FindUserID(t *testing.T, name string) string { + t.Log("Getting users...") + res, err := d.cli.GuildMembersSearch(d.cfg.GuildID, name, 5) + require.NoError(t, err) + + t.Logf("Finding user ID by name %q...", name) + for _, m := range res { + if !strings.EqualFold(name, m.User.Username) { + continue + } + return m.User.ID + } + + return "" +} + +func (d *discordTester) PostInitialMessage(t *testing.T, channelID string) { + t.Helper() + t.Logf("Posting welcome message for channel: %s...", channelID) + + var additionalContextMsg string + if d.cfg.AdditionalContextMessage != "" { + additionalContextMsg = fmt.Sprintf("%s\n", d.cfg.AdditionalContextMessage) + } + message := fmt.Sprintf("Hello!\n%s%s", additionalContextMsg, welcomeText) + _, err := d.cli.ChannelMessageSend(channelID, message) + require.NoError(t, err) +} + +func (d *discordTester) InviteBotToChannel(_ *testing.T, _, _ string) { + // This is not required in Discord. + // Bots can't "join" text channels because when you join a server you're already in every text channel. + // See: https://stackoverflow.com/questions/60990748/making-discord-bot-join-leave-a-channel +} + +func (d *discordTester) WaitForMessagePostedRecentlyEqual(userID, channelID, expectedMsg string) error { + return d.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg *discordgo.Message) bool { + return strings.EqualFold(msg.Content, expectedMsg) + }) +} + +func (d *discordTester) WaitForLastMessageContains(userID, channelID string, expectedMsgSubstring string) error { + return d.WaitForMessagePosted(userID, channelID, 1, func(msg *discordgo.Message) bool { + return strings.Contains(msg.Content, expectedMsgSubstring) + }) +} + +func (d *discordTester) WaitForLastMessageEqual(userID, channelID string, expectedMsg string) error { + return d.WaitForMessagePosted(userID, channelID, 1, func(msg *discordgo.Message) bool { + return msg.Content == expectedMsg + }) +} + +func (d *discordTester) PostMessageToBot(t *testing.T, userID, channelID, command string) { + message := fmt.Sprintf("<@%s> %s", userID, command) + _, err := d.cli.ChannelMessageSend(channelID, message) + require.NoError(t, err) +} + +func (d *discordTester) WaitForMessagePosted(userID, channelID string, limitMessages int, msgAssertFn func(msg *discordgo.Message) bool) error { + // To always receive message content: + // ensure you enable the MESSAGE CONTENT INTENT for the tester bot on the developer portal. + // Applications ↦ Settings ↦ Bot ↦ Privileged Gateway Intents + // This setting has been enforced from August 31, 2022 + + var fetchedMessages []*discordgo.Message + var lastErr error + + err := wait.Poll(pollInterval, d.cfg.MessageWaitTimeout, func() (done bool, err error) { + messages, err := d.cli.ChannelMessages(channelID, limitMessages, "", "", "") + if err != nil { + lastErr = err + return false, nil + } + + fetchedMessages = messages + for _, msg := range messages { + if msg.Author.ID != userID { + continue + } + + if !msgAssertFn(msg) { + // different message + continue + } + + return true, nil + } + + return false, nil + }) + if lastErr == nil { + lastErr = errors.New("message assertion function returned false") + } + if err != nil { + if err == wait.ErrWaitTimeout { + return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, structDumper.Sdump(fetchedMessages)) + } + return err + } + + return nil +} + +func (d *discordTester) WaitForMessagesPostedOnChannels(userID string, channelIDs []string, limitMessages int, msgAssertFn func(msg *discordgo.Message) bool) error { + errs := multierror.New() + for _, channelID := range channelIDs { + errs = multierror.Append(errs, d.WaitForMessagePosted(userID, channelID, limitMessages, msgAssertFn)) + } + + return errs.ErrorOrNil() +} + func (s *slackTester) CreateChannel(t *testing.T) (*slack.Channel, func(t *testing.T)) { t.Helper() randomID := uuid.New() diff --git a/test/e2e/k8s_helpers_test.go b/test/e2e/k8s_helpers_test.go index 310f78f96..8262a77b2 100644 --- a/test/e2e/k8s_helpers_test.go +++ b/test/e2e/k8s_helpers_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/bwmarrin/discordgo" "github.com/slack-go/slack" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -20,11 +21,9 @@ import ( deploymentutil "k8s.io/kubectl/pkg/util/deployment" ) -func setTestEnvsForDeploy(t *testing.T, appCfg Config, deployNsCli appsv1cli.DeploymentInterface, channels map[string]*slack.Channel) func(t *testing.T) { +func setTestEnvsForDeploy(t *testing.T, appCfg Config, deployNsCli appsv1cli.DeploymentInterface, slackChannels map[string]*slack.Channel, discordChannels map[string]*discordgo.Channel) func(t *testing.T) { t.Helper() - slackEnabledEnvName := appCfg.Deployment.Envs.SlackEnabledName - deployment, err := deployNsCli.Get(context.Background(), appCfg.Deployment.Name, metav1.GetOptions{}) require.NoError(t, err) require.NotNil(t, deployment) @@ -52,11 +51,24 @@ func setTestEnvsForDeploy(t *testing.T, appCfg Config, deployNsCli appsv1cli.Dep require.NoError(t, err) } - newEnvs := []v1.EnvVar{ - {Name: slackEnabledEnvName, Value: strconv.FormatBool(true)}, + var newEnvs []v1.EnvVar + + if len(slackChannels) > 0 { + slackEnabledEnvName := appCfg.Deployment.Envs.SlackEnabledName + newEnvs = append(newEnvs, v1.EnvVar{Name: slackEnabledEnvName, Value: strconv.FormatBool(true)}) + + for envName, slackChannel := range slackChannels { + newEnvs = append(newEnvs, v1.EnvVar{Name: envName, Value: slackChannel.Name}) + } } - for envName, slackChannel := range channels { - newEnvs = append(newEnvs, v1.EnvVar{Name: envName, Value: slackChannel.Name}) + + if len(discordChannels) > 0 { + discordEnabledEnvName := appCfg.Deployment.Envs.DiscordEnabledName + newEnvs = append(newEnvs, v1.EnvVar{Name: discordEnabledEnvName, Value: strconv.FormatBool(true)}) + + for envName, discordChannel := range discordChannels { + newEnvs = append(newEnvs, v1.EnvVar{Name: envName, Value: discordChannel.ID}) + } } deployment.Spec.Template.Spec.Containers[containerIdx].Env = updateEnv( diff --git a/test/e2e/slack_test.go b/test/e2e/slack_test.go deleted file mode 100644 index b9f625d69..000000000 --- a/test/e2e/slack_test.go +++ /dev/null @@ -1,568 +0,0 @@ -//go:build integration - -package e2e - -import ( - "context" - "fmt" - "strings" - "testing" - "time" - - "github.com/MakeNowJust/heredoc" - "github.com/slack-go/slack" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/vrischmann/envconfig" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/client-go/tools/clientcmd" - - "github.com/kubeshop/botkube/pkg/filterengine/filters" -) - -type Config struct { - KubeconfigPath string `envconfig:"optional,KUBECONFIG"` - Deployment struct { - Name string `envconfig:"default=botkube"` - Namespace string `envconfig:"default=botkube"` - ContainerName string `envconfig:"default=botkube"` - WaitTimeout time.Duration `envconfig:"default=3m"` - Envs struct { - SlackEnabledName string `envconfig:"default=BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_SLACK_ENABLED"` - DefaultSlackChannelIDName string `envconfig:"default=BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_SLACK_CHANNELS_DEFAULT_NAME"` - SecondarySlackChannelIDName string `envconfig:"default=BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_SLACK_CHANNELS_SECONDARY_NAME"` - } - } - ClusterName string `envconfig:"default=sample"` - Slack SlackConfig -} - -type SlackConfig struct { - BotName string `envconfig:"default=botkube"` - TesterName string `envconfig:"default=tester"` - AdditionalContextMessage string `envconfig:"optional"` - TesterAppToken string - MessageWaitTimeout time.Duration `envconfig:"default=10s"` -} - -const ( - channelNamePrefix = "test" - welcomeText = "Let the tests begin 🤞" - pollInterval = time.Second -) - -func TestSlack(t *testing.T) { - t.Log("Loading configuration...") - var appCfg Config - err := envconfig.Init(&appCfg) - require.NoError(t, err) - - t.Log("Creating Slack API client with provided token...") - slackTester, err := newSlackTester(appCfg.Slack) - require.NoError(t, err) - - t.Log("Creating K8s client...") - k8sConfig, err := clientcmd.BuildConfigFromFlags("", appCfg.KubeconfigPath) - require.NoError(t, err) - k8sCli, err := kubernetes.NewForConfig(k8sConfig) - require.NoError(t, err) - - t.Log("Setting up test Slack setup...") - botUserID := slackTester.FindUserIDForBot(t) - testerUserID := slackTester.FindUserIDForTester(t) - - channel, cleanupChannelFn := slackTester.CreateChannel(t) - t.Cleanup(func() { cleanupChannelFn(t) }) - secondChannel, cleanupSecondChannelFn := slackTester.CreateChannel(t) - t.Cleanup(func() { cleanupSecondChannelFn(t) }) - - channels := map[string]*slack.Channel{ - appCfg.Deployment.Envs.DefaultSlackChannelIDName: channel, - appCfg.Deployment.Envs.SecondarySlackChannelIDName: secondChannel, - } - for _, currentChannel := range channels { - slackTester.PostInitialMessage(t, currentChannel.Name) - slackTester.InviteBotToChannel(t, botUserID, currentChannel.ID) - } - - t.Log("Patching Deployment with test env variables...") - deployNsCli := k8sCli.AppsV1().Deployments(appCfg.Deployment.Namespace) - revertDeployFn := setTestEnvsForDeploy(t, appCfg, deployNsCli, channels) - t.Cleanup(func() { revertDeployFn(t) }) - - t.Log("Waiting for Deployment") - err = waitForDeploymentReady(deployNsCli, appCfg.Deployment.Name, appCfg.Deployment.WaitTimeout) - require.NoError(t, err) - - t.Log("Waiting for Bot message on channel...") - err = slackTester.WaitForMessagePostedRecentlyEqual(botUserID, channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) - require.NoError(t, err) - - t.Log("Running actual test cases") - - t.Run("Ping", func(t *testing.T) { - command := "ping" - expectedMessage := fmt.Sprintf("pong from cluster '%s'", appCfg.ClusterName) - - slackTester.PostMessageToBot(t, channel.Name, command) - err := slackTester.WaitForLastMessageContains(botUserID, channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Filters list", func(t *testing.T) { - command := "filters list" - expectedMessage := codeBlock(heredoc.Doc(` - FILTER ENABLED DESCRIPTION - NodeEventsChecker true Sends notifications on node level critical events. - ObjectAnnotationChecker true Checks if annotations present in object specs and filters them.`)) - - slackTester.PostMessageToBot(t, channel.Name, command) - err := slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Commands list", func(t *testing.T) { - command := "commands list" - expectedMessage := codeBlock(heredoc.Doc(` - Enabled executors: - kubectl: - kubectl-allow-all: - namespaces: - include: - - .* - enabled: true - commands: - verbs: - - get - resources: - - deployments - kubectl-read-only: - namespaces: - include: - - botkube - - default - enabled: true - commands: - verbs: - - api-resources - - api-versions - - cluster-info - - describe - - diff - - explain - - get - - logs - - top - - auth - resources: - - deployments - - pods - - namespaces - - daemonsets - - statefulsets - - storageclasses - - nodes - - configmaps - defaultNamespace: default - restrictAccess: false - kubectl-wait-cmd: - namespaces: - include: - - botkube - - default - enabled: true - commands: - verbs: - - wait - resources: [] - restrictAccess: false`)) - - t.Run("With default cluster", func(t *testing.T) { - slackTester.PostMessageToBot(t, channel.Name, command) - err := slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("With custom cluster name", func(t *testing.T) { - command := fmt.Sprintf("commands list --cluster-name %s", appCfg.ClusterName) - - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("With unknown cluster name", func(t *testing.T) { - command := "commands list --cluster-name non-existing" - - slackTester.PostMessageToBot(t, channel.Name, command) - t.Log("Ensuring bot didn't write anything new...") - time.Sleep(appCfg.Slack.MessageWaitTimeout) - // Same expected message as before - err = slackTester.WaitForLastMessageContains(testerUserID, channel.ID, command) - assert.NoError(t, err) - }) - }) - - t.Run("Executor", func(t *testing.T) { - t.Run("Get Deployment", func(t *testing.T) { - command := fmt.Sprintf("get deploy -n %s %s", appCfg.Deployment.Namespace, appCfg.Deployment.Name) - assertionFn := func(msg slack.Message) bool { - return strings.Contains(msg.Text, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg.Text, "botkube") - } - - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) - assert.NoError(t, err) - }) - - t.Run("Get Configmap", func(t *testing.T) { - command := fmt.Sprintf("get configmap -n %s", appCfg.Deployment.Namespace) - assertionFn := func(msg slack.Message) bool { - return strings.Contains(msg.Text, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg.Text, "kube-root-ca.crt") && - strings.Contains(msg.Text, "botkube-global-config") - } - - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) - assert.NoError(t, err) - }) - - t.Run("Get forbidden resource", func(t *testing.T) { - command := "get ingress" - expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'ingress' resources in the 'default' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Specify unknown command", func(t *testing.T) { - command := "unknown" - expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") - - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Specify invalid command", func(t *testing.T) { - command := "get" - expectedMessage := codeBlock(heredoc.Docf(`Cluster: %s - You must specify the type of resource to get. Use "kubectl api-resources" for a complete list of supported resources. - - error: Required resource not specified. - Use "kubectl explain <resource>" for a detailed description of that resource (e.g. kubectl explain pods). - See 'kubectl get -h' for help and examples - exit status 1`, appCfg.ClusterName)) - - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Specify forbidden namespace", func(t *testing.T) { - command := "get po --namespace team-b" - expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'po' resources in the 'team-b' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Based on other bindings", func(t *testing.T) { - t.Run("Wait for Deployment (the 2st binding)", func(t *testing.T) { - command := fmt.Sprintf("wait deployment -n %s %s --for condition=Available=True", appCfg.Deployment.Namespace, appCfg.Deployment.Name) - assertionFn := func(msg slack.Message) bool { - return strings.Contains(msg.Text, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg.Text, "deployment.apps/botkube condition met") - } - - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) - assert.NoError(t, err) - }) - - t.Run("Exec (the 3rd binding which is disabled)", func(t *testing.T) { - command := "exec" - expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") - - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Get all Pods (the 4th binding)", func(t *testing.T) { - command := "get pods -A" - expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'pods' resources for all Namespaces on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Get all Deployments (the 4th binding)", func(t *testing.T) { - command := "get deploy -A" - assertionFn := func(msg slack.Message) bool { - return strings.Contains(msg.Text, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg.Text, "local-path-provisioner") && - strings.Contains(msg.Text, "coredns") && - strings.Contains(msg.Text, "botkube") - } - - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) - assert.NoError(t, err) - }) - }) - }) - - t.Run("Multi-channel notifications", func(t *testing.T) { - cfgMapCli := k8sCli.CoreV1().ConfigMaps(appCfg.Deployment.Namespace) - var channelIDs []string - for _, channel := range channels { - channelIDs = append(channelIDs, channel.ID) - } - - t.Log("Creating ConfigMap...") - var cfgMapAlreadyDeleted bool - cfgMap := &v1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: channel.Name, - Namespace: appCfg.Deployment.Namespace, - }, - } - cfgMap, err = cfgMapCli.Create(context.Background(), cfgMap, metav1.CreateOptions{}) - require.NoError(t, err) - - t.Cleanup(func() { cleanupCreatedCfgMapIfShould(t, cfgMapCli, cfgMap.Name, &cfgMapAlreadyDeleted) }) - - t.Log("Expecting bot message in first channel...") - assertionFn := func(msg slack.Message) bool { - return doesMessageContainExactlyOneAttachment( - msg, - "v1/configmaps created", - "2eb886", - fmt.Sprintf("ConfigMap *%s/%s* has been created in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) - } - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) - require.NoError(t, err) - - t.Log("Expecting no bot message in second channel...") - expectedMessage := fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName) - time.Sleep(appCfg.Slack.MessageWaitTimeout) - err = slackTester.WaitForLastMessageEqual(botUserID, secondChannel.ID, expectedMessage) - require.NoError(t, err) - - t.Log("Updating ConfigMap...") - cfgMap.Data = map[string]string{ - "operation": "update", - } - cfgMap, err = cfgMapCli.Update(context.Background(), cfgMap, metav1.UpdateOptions{}) - require.NoError(t, err) - - t.Log("Expecting bot message in all channels...") - assertionFn = func(msg slack.Message) bool { - return doesMessageContainExactlyOneAttachment( - msg, - "v1/configmaps updated", - "daa038", - fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) - } - err = slackTester.WaitForMessagesPostedOnChannels(botUserID, channelIDs, 1, assertionFn) - require.NoError(t, err) - - t.Log("Stopping notifier...") - command := "notifier stop" - expectedMessage = codeBlock(fmt.Sprintf("Sure! I won't send you notifications from cluster '%s' here.", appCfg.ClusterName)) - - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - assert.NoError(t, err) - - t.Log("Getting notifier status from second channel...") - command = "notifier status" - expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are enabled here.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, secondChannel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, secondChannel.ID, expectedMessage) - assert.NoError(t, err) - - t.Log("Getting notifier status from first channel...") - command = "notifier status" - expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are disabled here.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - assert.NoError(t, err) - - t.Log("Updating ConfigMap once again...") - cfgMap.Data = map[string]string{ - "operation": "update-second", - } - _, err = cfgMapCli.Update(context.Background(), cfgMap, metav1.UpdateOptions{}) - require.NoError(t, err) - - t.Log("Ensuring bot didn't write anything new on first channel...") - time.Sleep(appCfg.Slack.MessageWaitTimeout) - // Same expected message as before - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - require.NoError(t, err) - - t.Log("Expecting bot message on second channel...") - assertionFn = func(msg slack.Message) bool { - return doesMessageContainExactlyOneAttachment( - msg, - "v1/configmaps updated", - "daa038", - fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) - } - err = slackTester.WaitForMessagePosted(botUserID, secondChannel.ID, 1, assertionFn) - - t.Log("Starting notifier") - command = "notifier start" - expectedMessage = codeBlock(fmt.Sprintf("Brace yourselves, incoming notifications from cluster '%s'.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - require.NoError(t, err) - - t.Log("Creating and deleting ignored ConfigMap") - ignoredCfgMap := &v1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-ignored", channel.Name), - Namespace: appCfg.Deployment.Namespace, - Annotations: map[string]string{ - filters.DisableAnnotation: "true", - }, - }, - } - _, err = cfgMapCli.Create(context.Background(), ignoredCfgMap, metav1.CreateOptions{}) - require.NoError(t, err) - err = cfgMapCli.Delete(context.Background(), ignoredCfgMap.Name, metav1.DeleteOptions{}) - require.NoError(t, err) - - t.Log("Ensuring bot didn't write anything new...") - time.Sleep(appCfg.Slack.MessageWaitTimeout) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) - require.NoError(t, err) - - t.Log("Deleting ConfigMap") - err = cfgMapCli.Delete(context.Background(), cfgMap.Name, metav1.DeleteOptions{}) - require.NoError(t, err) - cfgMapAlreadyDeleted = true - - t.Log("Expecting bot message on first channel...") - assertionFn = func(msg slack.Message) bool { - return doesMessageContainExactlyOneAttachment( - msg, - "v1/configmaps deleted", - "a30200", - fmt.Sprintf("ConfigMap *%s/%s* has been deleted in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) - } - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) - require.NoError(t, err) - - t.Log("Ensuring bot didn't write anything new on second channel...") - time.Sleep(appCfg.Slack.MessageWaitTimeout) - assertionFn = func(msg slack.Message) bool { - return doesMessageContainExactlyOneAttachment( - msg, - "v1/configmaps updated", - "daa038", - fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) - } - err = slackTester.WaitForMessagePosted(botUserID, secondChannel.ID, 1, assertionFn) - require.NoError(t, err) - }) - - t.Run("Recommendations", func(t *testing.T) { - podCli := k8sCli.CoreV1().Pods(appCfg.Deployment.Namespace) - - t.Log("Creating Pod...") - pod := &v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: channel.Name, - Namespace: appCfg.Deployment.Namespace, - }, - Spec: v1.PodSpec{ - Containers: []v1.Container{ - {Name: "nginx", Image: "nginx:latest"}, - }, - }, - } - require.Len(t, pod.Spec.Containers, 1) - pod, err = podCli.Create(context.Background(), pod, metav1.CreateOptions{}) - require.NoError(t, err) - - t.Cleanup(func() { cleanupCreatedPod(t, podCli, pod.Name) }) - - t.Log("Expecting bot message...") - assertionFn := func(msg slack.Message) bool { - if len(msg.Attachments) != 1 { - return false - } - - attachment := msg.Attachments[0] - title := attachment.Title - - if len(attachment.Fields) != 1 { - return false - } - - fieldMessage := attachment.Fields[0].Value - return title == "v1/pods created" && - strings.Contains(fieldMessage, "Recommendations:") && - strings.Contains(fieldMessage, fmt.Sprintf("- Pod '%s/%s' created without labels. Consider defining them, to be able to use them as a selector e.g. in Service.", pod.Namespace, pod.Name)) && - strings.Contains(fieldMessage, fmt.Sprintf("- The 'latest' tag used in '%s' image of Pod '%s/%s' container '%s' should be avoided.", pod.Spec.Containers[0].Image, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)) - } - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) - require.NoError(t, err) - }) -} - -func codeBlock(in string) string { - return fmt.Sprintf("```\n%s\n```", in) -} - -func doesMessageContainExactlyOneAttachment(msg slack.Message, expectedTitle, expectedColor, expectedFieldMessage string) bool { - if len(msg.Attachments) != 1 { - return false - } - - attachment := msg.Attachments[0] - title := attachment.Title - color := attachment.Color - - if len(attachment.Fields) != 1 { - return false - } - - fieldMessage := attachment.Fields[0].Value - - return title == expectedTitle && - color == expectedColor && - fieldMessage == expectedFieldMessage -} - -func cleanupCreatedCfgMapIfShould(t *testing.T, cfgMapCli corev1.ConfigMapInterface, name string, cfgMapAlreadyDeleted *bool) { - if cfgMapAlreadyDeleted != nil && *cfgMapAlreadyDeleted { - return - } - - t.Log("Cleaning up created ConfigMap...") - err := cfgMapCli.Delete(context.Background(), name, metav1.DeleteOptions{}) - assert.NoError(t, err) -} - -func cleanupCreatedPod(t *testing.T, podCli corev1.PodInterface, name string) { - t.Log("Cleaning up created Pod...") - err := podCli.Delete(context.Background(), name, metav1.DeleteOptions{}) - assert.NoError(t, err) -} From 6276993070e3b22af799be98baa25d1307065475 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Tue, 6 Sep 2022 15:05:46 +0100 Subject: [PATCH 02/20] Updated YAML config with required fields. --- helm/botkube/e2e-test-values.yaml | 3 +-- helm/botkube/templates/tests/e2e-test.yaml | 18 ++++++++++++++++++ helm/botkube/values.yaml | 4 ++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/helm/botkube/e2e-test-values.yaml b/helm/botkube/e2e-test-values.yaml index a8c389212..75468f61a 100644 --- a/helm/botkube/e2e-test-values.yaml +++ b/helm/botkube/e2e-test-values.yaml @@ -148,7 +148,6 @@ e2eTest: testerAppToken: "" # Provide a valid token for BotKube tester app additionalContextMessage: "" # Optional additional context discord: + guildID: "" # Provide the Guild ID (discord server ID) used to run e2e tests testerAppToken: "" # Provide a valid token for BotKube tester app - testerAppBotID: "" # Provide a valid token for BotKube tester app - guildID: "" additionalContextMessage: "" # Optional additional context diff --git a/helm/botkube/templates/tests/e2e-test.yaml b/helm/botkube/templates/tests/e2e-test.yaml index bed1f00d4..67d53238a 100644 --- a/helm/botkube/templates/tests/e2e-test.yaml +++ b/helm/botkube/templates/tests/e2e-test.yaml @@ -36,16 +36,34 @@ spec: value: "BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_SLACK_CHANNELS_DEFAULT_NAME" - name: DEPLOYMENT_ENVS_SECONDARY_SLACK_CHANNEL_ID_NAME value: "BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_SLACK_CHANNELS_SECONDARY_NAME" + - name: DEPLOYMENT_ENVS_DISCORD_ENABLED_NAME + value: "BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_DISCORD_ENABLED" + - name: DEPLOYMENT_ENVS_DEFAULT_DISCORD_CHANNEL_ID_NAME + value: "BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_DISCORD_CHANNELS_DEFAULT_ID" + - name: DEPLOYMENT_ENVS_SECONDARY_DISCORD_CHANNEL_ID_NAME + value: "BOTKUBE_COMMUNICATIONS_DEFAULT-GROUP_DISCORD_CHANNELS_SECONDARY_ID" - name: CLUSTER_NAME value: "{{ .Values.settings.clusterName }}" - name: SLACK_BOT_NAME value: "{{ .Values.e2eTest.slack.botName }}" + - name: DISCORD_BOT_NAME + value: "{{ .Values.e2eTest.discord.botName }}" - name: SLACK_TESTER_NAME value: "{{ .Values.e2eTest.slack.testerName }}" + - name: DISCORD_TESTER_NAME + value: "{{ .Values.e2eTest.discord.testerName }}" - name: SLACK_ADDITIONAL_CONTEXT_MESSAGE value: "{{ .Values.e2eTest.slack.additionalContextMessage }}" + - name: DISCORD_ADDITIONAL_CONTEXT_MESSAGE + value: "{{ .Values.e2eTest.discord.additionalContextMessage }}" - name: SLACK_TESTER_APP_TOKEN value: "{{ .Values.e2eTest.slack.testerAppToken }}" + - name: DISCORD_TESTER_APP_TOKEN + value: "{{ .Values.e2eTest.discord.testerAppToken }}" + - name: DISCORD_GUILD_ID + value: "{{ .Values.e2eTest.discord.guildID }}" - name: SLACK_MESSAGE_WAIT_TIMEOUT value: "{{ .Values.e2eTest.slack.messageWaitTimeout }}" + - name: DISCORD_MESSAGE_WAIT_TIMEOUT + value: "{{ .Values.e2eTest.discord.messageWaitTimeout }}" restartPolicy: Never diff --git a/helm/botkube/values.yaml b/helm/botkube/values.yaml index fc2071aee..495beb476 100644 --- a/helm/botkube/values.yaml +++ b/helm/botkube/values.yaml @@ -652,10 +652,10 @@ e2eTest: botName: "botkube" # -- Name of the BotKube Tester bot that sends messages during the e2e tests. testerName: "botkube_tester" + # -- Discord Guild ID (discord server ID) used to run e2e tests + guildID: "" # -- Discord tester application token that interacts with BotKube bot. testerAppToken: "" - # -- Discord tester application bot ID that interacts with BotKube tester bot. - testerAppBotID: "" # -- Additional message that is sent by Tester. You can pass e.g. pull request number or source link where these tests are run from. additionalContextMessage: "" # -- Message wait timeout. It defines how long we wait to ensure that notification were not sent when disabled. From 7a904b2b712cc8bcea2ee3e3f84f62ba87f8add5 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Wed, 7 Sep 2022 08:30:04 +0100 Subject: [PATCH 03/20] First stab at unifying Slack and Discord interfaces. --- test/e2e/bots_test.go | 408 ++++++++++++++--------------------- test/e2e/bots_tester_test.go | 245 ++++++++++++++++----- 2 files changed, 349 insertions(+), 304 deletions(-) diff --git a/test/e2e/bots_test.go b/test/e2e/bots_test.go index 0701cba0c..02784c654 100644 --- a/test/e2e/bots_test.go +++ b/test/e2e/bots_test.go @@ -58,7 +58,7 @@ type DiscordConfig struct { AdditionalContextMessage string `envconfig:"optional"` GuildID string TesterAppToken string - MessageWaitTimeout time.Duration `envconfig:"default=10s"` + MessageWaitTimeout time.Duration `envconfig:"default=15s"` } const ( @@ -84,8 +84,9 @@ func TestSlack(t *testing.T) { require.NoError(t, err) t.Log("Setting up test Slack setup...") - botUserID := slackTester.FindUserIDForBot(t) - testerUserID := slackTester.FindUserIDForTester(t) + slackTester.InitUsers(t) + //botUserID := slackTester.FindUserIDForBot(t) + //testerUserID := slackTester.FindUserIDForTester(t) channel, cleanupChannelFn := slackTester.CreateChannel(t) t.Cleanup(func() { cleanupChannelFn(t) }) @@ -98,7 +99,7 @@ func TestSlack(t *testing.T) { } for _, currentChannel := range channels { slackTester.PostInitialMessage(t, currentChannel.Name) - slackTester.InviteBotToChannel(t, botUserID, currentChannel.ID) + slackTester.InviteBotToChannel(t, currentChannel.ID) } t.Log("Patching Deployment with test env variables...") @@ -111,7 +112,7 @@ func TestSlack(t *testing.T) { require.NoError(t, err) t.Log("Waiting for Bot message on channel...") - err = slackTester.WaitForMessagePostedRecentlyEqual(botUserID, channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) + err = slackTester.WaitForMessagePostedRecentlyEqual(slackTester.botUserID, channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) require.NoError(t, err) t.Log("Running actual test cases") @@ -121,7 +122,7 @@ func TestSlack(t *testing.T) { expectedMessage := fmt.Sprintf("pong from cluster '%s'", appCfg.ClusterName) slackTester.PostMessageToBot(t, channel.Name, command) - err := slackTester.WaitForLastMessageContains(botUserID, channel.ID, expectedMessage) + err := slackTester.WaitForLastMessageContains(slackTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -133,7 +134,7 @@ func TestSlack(t *testing.T) { ObjectAnnotationChecker true Checks if annotations present in object specs and filters them.`)) slackTester.PostMessageToBot(t, channel.Name, command) - err := slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err := slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -195,7 +196,7 @@ func TestSlack(t *testing.T) { t.Run("With default cluster", func(t *testing.T) { slackTester.PostMessageToBot(t, channel.Name, command) - err := slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err := slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -203,7 +204,7 @@ func TestSlack(t *testing.T) { command := fmt.Sprintf("commands list --cluster-name %s", appCfg.ClusterName) slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -214,7 +215,7 @@ func TestSlack(t *testing.T) { t.Log("Ensuring bot didn't write anything new...") time.Sleep(appCfg.Slack.MessageWaitTimeout) // Same expected message as before - err = slackTester.WaitForLastMessageContains(testerUserID, channel.ID, command) + err = slackTester.WaitForLastMessageContains(slackTester.testerUserID, channel.ID, command) assert.NoError(t, err) }) }) @@ -222,26 +223,26 @@ func TestSlack(t *testing.T) { t.Run("Executor", func(t *testing.T) { t.Run("Get Deployment", func(t *testing.T) { command := fmt.Sprintf("get deploy -n %s %s", appCfg.Deployment.Namespace, appCfg.Deployment.Name) - assertionFn := func(msg slack.Message) bool { - return strings.Contains(msg.Text, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg.Text, "botkube") + assertionFn := func(msg string) bool { + return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg, "botkube") } slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + err = slackTester.WaitForMessagePosted(slackTester.botUserID, channel.ID, 1, assertionFn) assert.NoError(t, err) }) t.Run("Get Configmap", func(t *testing.T) { command := fmt.Sprintf("get configmap -n %s", appCfg.Deployment.Namespace) - assertionFn := func(msg slack.Message) bool { - return strings.Contains(msg.Text, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg.Text, "kube-root-ca.crt") && - strings.Contains(msg.Text, "botkube-global-config") + assertionFn := func(msg string) bool { + return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg, "kube-root-ca.crt") && + strings.Contains(msg, "botkube-global-config") } slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + err = slackTester.WaitForMessagePosted(slackTester.botUserID, channel.ID, 1, assertionFn) assert.NoError(t, err) }) @@ -250,7 +251,7 @@ func TestSlack(t *testing.T) { expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'ingress' resources in the 'default' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -259,7 +260,7 @@ func TestSlack(t *testing.T) { expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -274,7 +275,7 @@ func TestSlack(t *testing.T) { exit status 1`, appCfg.ClusterName)) slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -283,20 +284,20 @@ func TestSlack(t *testing.T) { expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'po' resources in the 'team-b' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) t.Run("Based on other bindings", func(t *testing.T) { t.Run("Wait for Deployment (the 2st binding)", func(t *testing.T) { command := fmt.Sprintf("wait deployment -n %s %s --for condition=Available=True", appCfg.Deployment.Namespace, appCfg.Deployment.Name) - assertionFn := func(msg slack.Message) bool { - return strings.Contains(msg.Text, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg.Text, "deployment.apps/botkube condition met") + assertionFn := func(msg string) bool { + return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg, "deployment.apps/botkube condition met") } slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + err = slackTester.WaitForMessagePosted(slackTester.botUserID, channel.ID, 1, assertionFn) assert.NoError(t, err) }) @@ -305,7 +306,7 @@ func TestSlack(t *testing.T) { expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -314,21 +315,21 @@ func TestSlack(t *testing.T) { expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'pods' resources for all Namespaces on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) t.Run("Get all Deployments (the 4th binding)", func(t *testing.T) { command := "get deploy -A" - assertionFn := func(msg slack.Message) bool { - return strings.Contains(msg.Text, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg.Text, "local-path-provisioner") && - strings.Contains(msg.Text, "coredns") && - strings.Contains(msg.Text, "botkube") + assertionFn := func(msg string) bool { + return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg, "local-path-provisioner") && + strings.Contains(msg, "coredns") && + strings.Contains(msg, "botkube") } slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + err = slackTester.WaitForMessagePosted(slackTester.botUserID, channel.ID, 1, assertionFn) assert.NoError(t, err) }) }) @@ -355,21 +356,18 @@ func TestSlack(t *testing.T) { t.Cleanup(func() { cleanupCreatedCfgMapIfShould(t, cfgMapCli, cfgMap.Name, &cfgMapAlreadyDeleted) }) t.Log("Expecting bot message in first channel...") - assertionFn := func(msg slack.Message) bool { - return doesSlackMessageContainExactlyOneAttachment( - msg, - "v1/configmaps created", - "2eb886", - fmt.Sprintf("ConfigMap *%s/%s* has been created in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) + attachAssertionFn := func(title, color, msg string) bool { + return title == "v1/configmaps created" && + color == "2eb886" && + msg == fmt.Sprintf("ConfigMap *%s/%s* has been created in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, channel.ID, attachAssertionFn) require.NoError(t, err) t.Log("Expecting no bot message in second channel...") expectedMessage := fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName) time.Sleep(appCfg.Slack.MessageWaitTimeout) - err = slackTester.WaitForLastMessageEqual(botUserID, secondChannel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, secondChannel.ID, expectedMessage) require.NoError(t, err) t.Log("Updating ConfigMap...") @@ -380,15 +378,12 @@ func TestSlack(t *testing.T) { require.NoError(t, err) t.Log("Expecting bot message in all channels...") - assertionFn = func(msg slack.Message) bool { - return doesSlackMessageContainExactlyOneAttachment( - msg, - "v1/configmaps updated", - "daa038", - fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) + attachAssertionFn = func(title, color, msg string) bool { + return title == "v1/configmaps updated" && + color == "daa038" && + msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagesPostedOnChannels(botUserID, channelIDs, 1, assertionFn) + err = slackTester.WaitForMessagesPostedOnChannelsWithAttachment(slackTester.botUserID, channelIDs, attachAssertionFn) require.NoError(t, err) t.Log("Stopping notifier...") @@ -396,21 +391,21 @@ func TestSlack(t *testing.T) { expectedMessage = codeBlock(fmt.Sprintf("Sure! I won't send you notifications from cluster '%s' here.", appCfg.ClusterName)) slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) t.Log("Getting notifier status from second channel...") command = "notifier status" expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are enabled here.", appCfg.ClusterName)) slackTester.PostMessageToBot(t, secondChannel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, secondChannel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, secondChannel.ID, expectedMessage) assert.NoError(t, err) t.Log("Getting notifier status from first channel...") command = "notifier status" expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are disabled here.", appCfg.ClusterName)) slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) t.Log("Updating ConfigMap once again...") @@ -423,25 +418,22 @@ func TestSlack(t *testing.T) { t.Log("Ensuring bot didn't write anything new on first channel...") time.Sleep(appCfg.Slack.MessageWaitTimeout) // Same expected message as before - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) require.NoError(t, err) t.Log("Expecting bot message on second channel...") - assertionFn = func(msg slack.Message) bool { - return doesSlackMessageContainExactlyOneAttachment( - msg, - "v1/configmaps updated", - "daa038", - fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) + attachAssertionFn = func(title, color, msg string) bool { + return title == "v1/configmaps updated" && + color == "daa038" && + msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagePosted(botUserID, secondChannel.ID, 1, assertionFn) + err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, secondChannel.ID, attachAssertionFn) t.Log("Starting notifier") command = "notifier start" expectedMessage = codeBlock(fmt.Sprintf("Brace yourselves, incoming notifications from cluster '%s'.", appCfg.ClusterName)) slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) require.NoError(t, err) t.Log("Creating and deleting ignored ConfigMap") @@ -461,7 +453,7 @@ func TestSlack(t *testing.T) { t.Log("Ensuring bot didn't write anything new...") time.Sleep(appCfg.Slack.MessageWaitTimeout) - err = slackTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) require.NoError(t, err) t.Log("Deleting ConfigMap") @@ -470,28 +462,22 @@ func TestSlack(t *testing.T) { cfgMapAlreadyDeleted = true t.Log("Expecting bot message on first channel...") - assertionFn = func(msg slack.Message) bool { - return doesSlackMessageContainExactlyOneAttachment( - msg, - "v1/configmaps deleted", - "a30200", - fmt.Sprintf("ConfigMap *%s/%s* has been deleted in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) + attachAssertionFn = func(title, color, msg string) bool { + return title == "v1/configmaps deleted" && + color == "a30200" && + msg == fmt.Sprintf("ConfigMap *%s/%s* has been deleted in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, channel.ID, attachAssertionFn) require.NoError(t, err) t.Log("Ensuring bot didn't write anything new on second channel...") time.Sleep(appCfg.Slack.MessageWaitTimeout) - assertionFn = func(msg slack.Message) bool { - return doesSlackMessageContainExactlyOneAttachment( - msg, - "v1/configmaps updated", - "daa038", - fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) + attachAssertionFn = func(title, color, msg string) bool { + return title == "v1/configmaps updated" && + color == "daa038" && + msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagePosted(botUserID, secondChannel.ID, 1, assertionFn) + err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, secondChannel.ID, attachAssertionFn) require.NoError(t, err) }) @@ -517,25 +503,13 @@ func TestSlack(t *testing.T) { t.Cleanup(func() { cleanupCreatedPod(t, podCli, pod.Name) }) t.Log("Expecting bot message...") - assertionFn := func(msg slack.Message) bool { - if len(msg.Attachments) != 1 { - return false - } - - attachment := msg.Attachments[0] - title := attachment.Title - - if len(attachment.Fields) != 1 { - return false - } - - fieldMessage := attachment.Fields[0].Value + assertionFn := func(title, color, msg string) bool { return title == "v1/pods created" && - strings.Contains(fieldMessage, "Recommendations:") && - strings.Contains(fieldMessage, fmt.Sprintf("- Pod '%s/%s' created without labels. Consider defining them, to be able to use them as a selector e.g. in Service.", pod.Namespace, pod.Name)) && - strings.Contains(fieldMessage, fmt.Sprintf("- The 'latest' tag used in '%s' image of Pod '%s/%s' container '%s' should be avoided.", pod.Spec.Containers[0].Image, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)) + strings.Contains(msg, "Recommendations:") && + strings.Contains(msg, fmt.Sprintf("- Pod '%s/%s' created without labels. Consider defining them, to be able to use them as a selector e.g. in Service.", pod.Namespace, pod.Name)) && + strings.Contains(msg, fmt.Sprintf("- The 'latest' tag used in '%s' image of Pod '%s/%s' container '%s' should be avoided.", pod.Spec.Containers[0].Image, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)) } - err = slackTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, channel.ID, assertionFn) require.NoError(t, err) }) } @@ -556,10 +530,8 @@ func TestDiscord(t *testing.T) { k8sCli, err := kubernetes.NewForConfig(k8sConfig) require.NoError(t, err) - t.Log("Setting up test Slack setup...") - botUserID := discordTester.FindUserIDForBot(t) - testerUserID := discordTester.FindUserIDForTester(t) - t.Logf("Just loaded botUserID...: %+v", botUserID) + t.Log("Setting up test Discord setup...") + discordTester.InitUsers(t) channel, cleanupChannelFn := discordTester.CreateChannel(t) t.Cleanup(func() { cleanupChannelFn(t) }) @@ -573,7 +545,7 @@ func TestDiscord(t *testing.T) { for _, currentChannel := range channels { discordTester.PostInitialMessage(t, currentChannel.ID) - discordTester.InviteBotToChannel(t, botUserID, currentChannel.ID) + discordTester.InviteBotToChannel(t, currentChannel.ID) } t.Log("Patching Deployment with test env variables...") @@ -586,7 +558,7 @@ func TestDiscord(t *testing.T) { require.NoError(t, err) t.Log("Waiting for Bot message on channel from user") - err = discordTester.WaitForMessagePostedRecentlyEqual(botUserID, channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) + err = discordTester.WaitForMessagePostedRecentlyEqual(discordTester.botUserID, channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) require.NoError(t, err) t.Log("Running actual test cases") @@ -596,8 +568,8 @@ func TestDiscord(t *testing.T) { expectedMessage := fmt.Sprintf("pong from cluster '%s'", appCfg.ClusterName) //discordTester.PostMessageToBot(t, channel.ID, command) - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err := discordTester.WaitForLastMessageContains(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err := discordTester.WaitForLastMessageContains(discordTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -608,8 +580,8 @@ func TestDiscord(t *testing.T) { NodeEventsChecker true Sends notifications on node level critical events. ObjectAnnotationChecker true Checks if annotations botkube.io/* present in object specs and filters them.`)) - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err := discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err := discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -670,27 +642,27 @@ func TestDiscord(t *testing.T) { restrictAccess: false`)) t.Run("With default cluster", func(t *testing.T) { - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err := discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err := discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) t.Run("With custom cluster name", func(t *testing.T) { command := fmt.Sprintf("commands list --cluster-name %s", appCfg.ClusterName) - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) t.Run("With unknown cluster name", func(t *testing.T) { command := "commands list --cluster-name non-existing" - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) + discordTester.PostMessageToBot(t, channel.ID, command) t.Log("Ensuring bot didn't write anything new...") time.Sleep(appCfg.Discord.MessageWaitTimeout) // Same expected message as before - err = discordTester.WaitForLastMessageContains(testerUserID, channel.ID, command) + err = discordTester.WaitForLastMessageContains(discordTester.testerUserID, channel.ID, command) assert.NoError(t, err) }) }) @@ -698,26 +670,26 @@ func TestDiscord(t *testing.T) { t.Run("Executor", func(t *testing.T) { t.Run("Get Deployment", func(t *testing.T) { command := fmt.Sprintf("get deploy -n %s %s", appCfg.Deployment.Namespace, appCfg.Deployment.Name) - assertionFn := func(msg *discordgo.Message) bool { - return strings.Contains(msg.Content, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg.Content, "botkube") + assertionFn := func(msg string) bool { + return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg, "botkube") } - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForMessagePosted(discordTester.botUserID, channel.ID, 1, assertionFn) assert.NoError(t, err) }) t.Run("Get Configmap", func(t *testing.T) { command := fmt.Sprintf("get configmap -n %s", appCfg.Deployment.Namespace) - assertionFn := func(msg *discordgo.Message) bool { - return strings.Contains(msg.Content, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg.Content, "kube-root-ca.crt") && - strings.Contains(msg.Content, "botkube-global-config") + assertionFn := func(msg string) bool { + return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg, "kube-root-ca.crt") && + strings.Contains(msg, "botkube-global-config") } - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForMessagePosted(discordTester.botUserID, channel.ID, 1, assertionFn) assert.NoError(t, err) }) @@ -725,8 +697,8 @@ func TestDiscord(t *testing.T) { command := "get ingress" expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'ingress' resources in the 'default' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -734,8 +706,8 @@ func TestDiscord(t *testing.T) { command := "unknown" expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -743,8 +715,8 @@ func TestDiscord(t *testing.T) { command := "get" expectedMessage := codeBlock(fmt.Sprintf("Cluster: %s\nYou must specify the type of resource to get. Use \"kubectl api-resources\" for a complete list of supported resources.\n\nerror: Required resource not specified.\nUse \"kubectl explain \" for a detailed description of that resource (e.g. kubectl explain pods).\nSee 'kubectl get -h' for help and examples\nexit status 1", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -752,21 +724,21 @@ func TestDiscord(t *testing.T) { command := "get po --namespace team-b" expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'po' resources in the 'team-b' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) t.Run("Based on other bindings", func(t *testing.T) { t.Run("Wait for Deployment (the 2st binding)", func(t *testing.T) { command := fmt.Sprintf("wait deployment -n %s %s --for condition=Available=True", appCfg.Deployment.Namespace, appCfg.Deployment.Name) - assertionFn := func(msg *discordgo.Message) bool { - return strings.Contains(msg.Content, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg.Content, "deployment.apps/botkube condition met") + assertionFn := func(msg string) bool { + return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg, "deployment.apps/botkube condition met") } - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForMessagePosted(discordTester.botUserID, channel.ID, 1, assertionFn) assert.NoError(t, err) }) @@ -774,8 +746,8 @@ func TestDiscord(t *testing.T) { command := "exec" expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -783,22 +755,22 @@ func TestDiscord(t *testing.T) { command := "get pods -A" expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'pods' resources for all Namespaces on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) }) t.Run("Get all Deployments (the 4th binding)", func(t *testing.T) { command := "get deploy -A" - assertionFn := func(msg *discordgo.Message) bool { - return strings.Contains(msg.Content, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg.Content, "local-path-provisioner") && - strings.Contains(msg.Content, "coredns") && - strings.Contains(msg.Content, "botkube") + assertionFn := func(msg string) bool { + return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && + strings.Contains(msg, "local-path-provisioner") && + strings.Contains(msg, "coredns") && + strings.Contains(msg, "botkube") } - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForMessagePosted(discordTester.botUserID, channel.ID, 1, assertionFn) assert.NoError(t, err) }) }) @@ -825,21 +797,18 @@ func TestDiscord(t *testing.T) { t.Cleanup(func() { cleanupCreatedCfgMapIfShould(t, cfgMapCli, cfgMap.Name, &cfgMapAlreadyDeleted) }) t.Log("Expecting bot message in first channel...") - assertionFn := func(msg *discordgo.Message) bool { - return doesDiscordMessageContainExactlyOneEmbed( - msg, - "v1/configmaps created", - 8311585, - fmt.Sprintf("ConfigMap *%s/%s* has been created in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) + assertionFn := func(title, color, msg string) bool { + return title == "v1/configmaps created" && + color == "8311585" && + msg == fmt.Sprintf("ConfigMap *%s/%s* has been created in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, channel.ID, assertionFn) require.NoError(t, err) t.Log("Expecting no bot message in second channel...") expectedMessage := fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName) time.Sleep(appCfg.Discord.MessageWaitTimeout) - err = discordTester.WaitForLastMessageEqual(botUserID, secondChannel.ID, expectedMessage) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, secondChannel.ID, expectedMessage) require.NoError(t, err) t.Log("Updating ConfigMap...") @@ -850,37 +819,34 @@ func TestDiscord(t *testing.T) { require.NoError(t, err) t.Log("Expecting bot message in all channels...") - assertionFn = func(msg *discordgo.Message) bool { - return doesDiscordMessageContainExactlyOneEmbed( - msg, - "v1/configmaps updated", - 16312092, - fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) + assertionFn = func(title, color, msg string) bool { + return title == "v1/configmaps updated" && + color == "16312092" && + msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = discordTester.WaitForMessagesPostedOnChannels(botUserID, channelIDs, 1, assertionFn) + err = discordTester.WaitForMessagesPostedOnChannelsWithAttachment(discordTester.botUserID, channelIDs, assertionFn) require.NoError(t, err) t.Log("Stopping notifier...") command := "notifier stop" expectedMessage = codeBlock(fmt.Sprintf("Sure! I won't send you notifications from cluster '%s' here.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) t.Log("Getting notifier status from second channel...") command = "notifier status" expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are enabled here.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, botUserID, secondChannel.ID, command) - err = discordTester.WaitForLastMessageEqual(botUserID, secondChannel.ID, expectedMessage) + discordTester.PostMessageToBot(t, secondChannel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, secondChannel.ID, expectedMessage) assert.NoError(t, err) t.Log("Getting notifier status from first channel...") command = "notifier status" expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are disabled here.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) assert.NoError(t, err) t.Log("Updating ConfigMap once again...") @@ -893,26 +859,23 @@ func TestDiscord(t *testing.T) { t.Log("Ensuring bot didn't write anything new on first channel...") time.Sleep(appCfg.Slack.MessageWaitTimeout) // Same expected message as before - err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) require.NoError(t, err) t.Log("Expecting bot message on second channel...") - assertionFn = func(msg *discordgo.Message) bool { - return doesDiscordMessageContainExactlyOneEmbed( - msg, - "v1/configmaps updated", - 16312092, - fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) + attachmentAssertionFn := func(title, color, msg string) bool { + return title == "v1/configmaps updated" && + color == "16312092" && + msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = discordTester.WaitForMessagePosted(botUserID, secondChannel.ID, 1, assertionFn) + err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, secondChannel.ID, attachmentAssertionFn) require.NoError(t, err) t.Log("Starting notifier") command = "notifier start" expectedMessage = codeBlock(fmt.Sprintf("Brace yourselves, incoming notifications from cluster '%s'.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, botUserID, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) require.NoError(t, err) t.Log("Creating and deleting ignored ConfigMap") @@ -932,7 +895,7 @@ func TestDiscord(t *testing.T) { t.Log("Ensuring bot didn't write anything new...") time.Sleep(appCfg.Slack.MessageWaitTimeout) - err = discordTester.WaitForLastMessageEqual(botUserID, channel.ID, expectedMessage) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) require.NoError(t, err) t.Log("Deleting ConfigMap") @@ -941,28 +904,22 @@ func TestDiscord(t *testing.T) { cfgMapAlreadyDeleted = true t.Log("Expecting bot message on first channel...") - assertionFn = func(msg *discordgo.Message) bool { - return doesDiscordMessageContainExactlyOneEmbed( - msg, - "v1/configmaps deleted", - 13632027, - fmt.Sprintf("ConfigMap *%s/%s* has been deleted in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) + assertionFn = func(title, color, msg string) bool { + return title == "v1/configmaps deleted" && + color == "13632027" && + msg == fmt.Sprintf("ConfigMap *%s/%s* has been deleted in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, channel.ID, assertionFn) require.NoError(t, err) t.Log("Ensuring bot didn't write anything new on second channel...") time.Sleep(appCfg.Slack.MessageWaitTimeout) - assertionFn = func(msg *discordgo.Message) bool { - return doesDiscordMessageContainExactlyOneEmbed( - msg, - "v1/configmaps updated", - 16312092, - fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName), - ) + assertionFn = func(title, color, msg string) bool { + return title == "v1/configmaps updated" && + color == "16312092" && + msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = discordTester.WaitForMessagePosted(botUserID, secondChannel.ID, 1, assertionFn) + err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, secondChannel.ID, assertionFn) require.NoError(t, err) }) @@ -988,21 +945,13 @@ func TestDiscord(t *testing.T) { t.Cleanup(func() { cleanupCreatedPod(t, podCli, pod.Name) }) t.Log("Expecting bot message...") - assertionFn := func(msg *discordgo.Message) bool { - if len(msg.Embeds) != 1 { - return false - } - - embed := msg.Embeds[0] - title := embed.Title - fieldMessage := embed.Description - + assertionFn := func(title, _, msg string) bool { return title == "v1/pods created" && - strings.Contains(fieldMessage, "Recommendations:") && - strings.Contains(fieldMessage, fmt.Sprintf("- Pod '%s/%s' created without labels. Consider defining them, to be able to use them as a selector e.g. in Service.", pod.Namespace, pod.Name)) && - strings.Contains(fieldMessage, fmt.Sprintf("- The 'latest' tag used in '%s' image of Pod '%s/%s' container '%s' should be avoided.", pod.Spec.Containers[0].Image, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)) + strings.Contains(msg, "Recommendations:") && + strings.Contains(msg, fmt.Sprintf("- Pod '%s/%s' created without labels. Consider defining them, to be able to use them as a selector e.g. in Service.", pod.Namespace, pod.Name)) && + strings.Contains(msg, fmt.Sprintf("- The 'latest' tag used in '%s' image of Pod '%s/%s' container '%s' should be avoided.", pod.Spec.Containers[0].Image, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)) } - err = discordTester.WaitForMessagePosted(botUserID, channel.ID, 1, assertionFn) + err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, channel.ID, assertionFn) require.NoError(t, err) }) } @@ -1011,37 +960,6 @@ func codeBlock(in string) string { return fmt.Sprintf("```\n%s\n```", in) } -func doesSlackMessageContainExactlyOneAttachment(msg slack.Message, expectedTitle, expectedColor, expectedFieldMessage string) bool { - if len(msg.Attachments) != 1 { - return false - } - - attachment := msg.Attachments[0] - title := attachment.Title - color := attachment.Color - - if len(attachment.Fields) != 1 { - return false - } - - fieldMessage := attachment.Fields[0].Value - - return title == expectedTitle && - color == expectedColor && - fieldMessage == expectedFieldMessage -} - -func doesDiscordMessageContainExactlyOneEmbed(msg *discordgo.Message, expectedTitle string, expectedColor int, expectedFieldMessage string) bool { - if len(msg.Embeds) != 1 { - return false - } - - embed := msg.Embeds[0] - return embed.Title == expectedTitle && - embed.Color == expectedColor && - embed.Description == expectedFieldMessage -} - func cleanupCreatedCfgMapIfShould(t *testing.T, cfgMapCli corev1.ConfigMapInterface, name string, cfgMapAlreadyDeleted *bool) { if cfgMapAlreadyDeleted != nil && *cfgMapAlreadyDeleted { return diff --git a/test/e2e/bots_tester_test.go b/test/e2e/bots_tester_test.go index 6b0c3335c..b4cb8e0d7 100644 --- a/test/e2e/bots_tester_test.go +++ b/test/e2e/bots_tester_test.go @@ -5,11 +5,14 @@ package e2e import ( "errors" "fmt" + "regexp" + "strconv" "strings" "testing" "github.com/bwmarrin/discordgo" "github.com/google/uuid" + "github.com/sanity-io/litter" "github.com/slack-go/slack" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,14 +24,30 @@ import ( const recentMessagesLimit = 5 +// structDumper provides an option to print the struct in more readable way. +var structDumper = litter.Options{ + HidePrivateFields: true, + HideZeroValues: true, + StripPackageNames: false, + FieldExclusions: regexp.MustCompile(`^(XXX_.*)$`), // XXX_ is a prefix of fields generated by protoc-gen-go + Separator: " ", +} + +type MessageAssertion func(content string) bool +type AttachmentAssertion func(title, color, msg string) bool + type slackTester struct { - cli *slack.Client - cfg SlackConfig + cli *slack.Client + cfg SlackConfig + botUserID string + testerUserID string } type discordTester struct { - cli *discordgo.Session - cfg DiscordConfig + cli *discordgo.Session + cfg DiscordConfig + botUserID string + testerUserID string } func newSlackTester(slackCfg SlackConfig) (*slackTester, error) { @@ -41,6 +60,12 @@ func newSlackTester(slackCfg SlackConfig) (*slackTester, error) { return &slackTester{cli: slackCli, cfg: slackCfg}, nil } +func (s *slackTester) InitUsers(t *testing.T) { + t.Helper() + s.botUserID = s.FindUserID(t, s.cfg.BotName) + s.testerUserID = s.FindUserID(t, s.cfg.TesterName) +} + func newDiscordTester(discordCfg DiscordConfig) (*discordTester, error) { discordCli, err := discordgo.New("Bot " + discordCfg.TesterAppToken) if err != nil { @@ -49,6 +74,12 @@ func newDiscordTester(discordCfg DiscordConfig) (*discordTester, error) { return &discordTester{cli: discordCli, cfg: discordCfg}, nil } +func (d *discordTester) InitUsers(t *testing.T) { + t.Helper() + d.botUserID = d.FindUserID(t, d.cfg.BotName) + d.testerUserID = d.FindUserID(t, d.cfg.TesterName) +} + func (d *discordTester) CreateChannel(t *testing.T) (*discordgo.Channel, func(t *testing.T)) { t.Helper() randomID := uuid.New() @@ -71,6 +102,25 @@ func (d *discordTester) CreateChannel(t *testing.T) (*discordgo.Channel, func(t return channel, cleanupFn } +func (d *discordTester) PostInitialMessage(t *testing.T, channelID string) { + t.Helper() + t.Logf("Posting welcome message for channel: %s...", channelID) + + var additionalContextMsg string + if d.cfg.AdditionalContextMessage != "" { + additionalContextMsg = fmt.Sprintf("%s\n", d.cfg.AdditionalContextMessage) + } + message := fmt.Sprintf("Hello!\n%s%s", additionalContextMsg, welcomeText) + _, err := d.cli.ChannelMessageSend(channelID, message) + require.NoError(t, err) +} + +func (d *discordTester) PostMessageToBot(t *testing.T, channel, command string) { + message := fmt.Sprintf("<@%s> %s", d.botUserID, command) + _, err := d.cli.ChannelMessageSend(channel, message) + require.NoError(t, err) +} + func (d *discordTester) FindUserIDForBot(t *testing.T) string { return d.FindUserID(t, d.cfg.BotName) } @@ -95,50 +145,76 @@ func (d *discordTester) FindUserID(t *testing.T, name string) string { return "" } -func (d *discordTester) PostInitialMessage(t *testing.T, channelID string) { - t.Helper() - t.Logf("Posting welcome message for channel: %s...", channelID) - - var additionalContextMsg string - if d.cfg.AdditionalContextMessage != "" { - additionalContextMsg = fmt.Sprintf("%s\n", d.cfg.AdditionalContextMessage) - } - message := fmt.Sprintf("Hello!\n%s%s", additionalContextMsg, welcomeText) - _, err := d.cli.ChannelMessageSend(channelID, message) - require.NoError(t, err) -} - -func (d *discordTester) InviteBotToChannel(_ *testing.T, _, _ string) { +func (d *discordTester) InviteBotToChannel(_ *testing.T, _ string) { // This is not required in Discord. // Bots can't "join" text channels because when you join a server you're already in every text channel. // See: https://stackoverflow.com/questions/60990748/making-discord-bot-join-leave-a-channel } func (d *discordTester) WaitForMessagePostedRecentlyEqual(userID, channelID, expectedMsg string) error { - return d.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg *discordgo.Message) bool { - return strings.EqualFold(msg.Content, expectedMsg) + return d.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg string) bool { + return strings.EqualFold(msg, expectedMsg) }) } -func (d *discordTester) WaitForLastMessageContains(userID, channelID string, expectedMsgSubstring string) error { - return d.WaitForMessagePosted(userID, channelID, 1, func(msg *discordgo.Message) bool { - return strings.Contains(msg.Content, expectedMsgSubstring) +func (d *discordTester) WaitForLastMessageContains(userID, channelID, expectedMsgSubstring string) error { + return d.WaitForMessagePosted(userID, channelID, 1, func(msg string) bool { + return strings.Contains(msg, expectedMsgSubstring) }) } -func (d *discordTester) WaitForLastMessageEqual(userID, channelID string, expectedMsg string) error { - return d.WaitForMessagePosted(userID, channelID, 1, func(msg *discordgo.Message) bool { - return msg.Content == expectedMsg +func (d *discordTester) WaitForLastMessageEqual(userID, channelID, expectedMsg string) error { + return d.WaitForMessagePosted(userID, channelID, 1, func(msg string) bool { + return msg == expectedMsg }) } -func (d *discordTester) PostMessageToBot(t *testing.T, userID, channelID, command string) { - message := fmt.Sprintf("<@%s> %s", userID, command) - _, err := d.cli.ChannelMessageSend(channelID, message) - require.NoError(t, err) +func (d *discordTester) WaitForMessagePosted(userID, channelID string, limitMessages int, assertFn MessageAssertion) error { + // To always receive message content: + // ensure you enable the MESSAGE CONTENT INTENT for the tester bot on the developer portal. + // Applications ↦ Settings ↦ Bot ↦ Privileged Gateway Intents + // This setting has been enforced from August 31, 2022 + + var fetchedMessages []*discordgo.Message + var lastErr error + + err := wait.Poll(pollInterval, d.cfg.MessageWaitTimeout, func() (done bool, err error) { + messages, err := d.cli.ChannelMessages(channelID, limitMessages, "", "", "") + if err != nil { + lastErr = err + return false, nil + } + + fetchedMessages = messages + for _, msg := range messages { + if msg.Author.ID != userID { + continue + } + + if !assertFn(msg.Content) { + // different message + continue + } + + return true, nil + } + + return false, nil + }) + if lastErr == nil { + lastErr = errors.New("message assertion function returned false") + } + if err != nil { + if err == wait.ErrWaitTimeout { + return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, structDumper.Sdump(fetchedMessages)) + } + return err + } + + return nil } -func (d *discordTester) WaitForMessagePosted(userID, channelID string, limitMessages int, msgAssertFn func(msg *discordgo.Message) bool) error { +func (d *discordTester) WaitForMessagePostedWithAttachment(userID, channelID string, assertFn AttachmentAssertion) error { // To always receive message content: // ensure you enable the MESSAGE CONTENT INTENT for the tester bot on the developer portal. // Applications ↦ Settings ↦ Bot ↦ Privileged Gateway Intents @@ -148,7 +224,7 @@ func (d *discordTester) WaitForMessagePosted(userID, channelID string, limitMess var lastErr error err := wait.Poll(pollInterval, d.cfg.MessageWaitTimeout, func() (done bool, err error) { - messages, err := d.cli.ChannelMessages(channelID, limitMessages, "", "", "") + messages, err := d.cli.ChannelMessages(channelID, 1, "", "", "") if err != nil { lastErr = err return false, nil @@ -160,7 +236,14 @@ func (d *discordTester) WaitForMessagePosted(userID, channelID string, limitMess continue } - if !msgAssertFn(msg) { + if len(msg.Embeds) != 1 { + lastErr = err + return false, nil + } + + embed := msg.Embeds[0] + + if !assertFn(embed.Title, strconv.Itoa(embed.Color), embed.Description) { // different message continue } @@ -183,10 +266,10 @@ func (d *discordTester) WaitForMessagePosted(userID, channelID string, limitMess return nil } -func (d *discordTester) WaitForMessagesPostedOnChannels(userID string, channelIDs []string, limitMessages int, msgAssertFn func(msg *discordgo.Message) bool) error { +func (d *discordTester) WaitForMessagesPostedOnChannelsWithAttachment(userID string, channelIDs []string, assertFn AttachmentAssertion) error { errs := multierror.New() for _, channelID := range channelIDs { - errs = multierror.Append(errs, d.WaitForMessagePosted(userID, channelID, limitMessages, msgAssertFn)) + errs = multierror.Append(errs, d.WaitForMessagePostedWithAttachment(userID, channelID, assertFn)) } return errs.ErrorOrNil() @@ -230,9 +313,9 @@ func (s *slackTester) PostInitialMessage(t *testing.T, channelName string) { require.NoError(t, err) } -func (s *slackTester) PostMessageToBot(t *testing.T, channelName, command string) { +func (s *slackTester) PostMessageToBot(t *testing.T, channel, command string) { message := fmt.Sprintf("<@%s> %s", s.cfg.BotName, command) - _, _, err := s.cli.PostMessage(channelName, slack.MsgOptionText(message, false)) + _, _, err := s.cli.PostMessage(channel, slack.MsgOptionText(message, false)) require.NoError(t, err) } @@ -260,37 +343,31 @@ func (s *slackTester) FindUserID(t *testing.T, name string) string { return "" } -func (s *slackTester) InviteBotToChannel(t *testing.T, botID, channelID string) { - t.Logf("Inviting bot with ID %q to the channel with ID %q", botID, channelID) - _, err := s.cli.InviteUsersToConversation(channelID, botID) +func (s *slackTester) InviteBotToChannel(t *testing.T, channelID string) { + t.Logf("Inviting bot with ID %q to the channel with ID %q", s.botUserID, channelID) + _, err := s.cli.InviteUsersToConversation(channelID, s.botUserID) require.NoError(t, err) } -func (s *slackTester) WaitForMessagePostedRecentlyEqual(userID, channelID string, expectedMsg string) error { - return s.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg slack.Message) bool { - return strings.EqualFold(msg.Text, expectedMsg) +func (s *slackTester) WaitForMessagePostedRecentlyEqual(userID, channelID, expectedMsg string) error { + return s.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg string) bool { + return strings.EqualFold(msg, expectedMsg) }) } -func (s *slackTester) WaitForLastMessageContains(userID, channelID string, expectedMsgSubstring string) error { - return s.WaitForMessagePosted(userID, channelID, 1, func(msg slack.Message) bool { - return strings.Contains(msg.Text, expectedMsgSubstring) +func (s *slackTester) WaitForLastMessageContains(userID, channelID, expectedMsgSubstring string) error { + return s.WaitForMessagePosted(userID, channelID, 1, func(msg string) bool { + return strings.Contains(msg, expectedMsgSubstring) }) } -func (s *slackTester) WaitForLastMessageEqual(userID, channelID string, expectedMsg string) error { - return s.WaitForMessagePosted(userID, channelID, 1, func(msg slack.Message) bool { - return msg.Text == expectedMsg +func (s *slackTester) WaitForLastMessageEqual(userID, channelID, expectedMsg string) error { + return s.WaitForMessagePosted(userID, channelID, 1, func(msg string) bool { + return msg == expectedMsg }) } -func (s *slackTester) WaitForLastMessageEqualOnChannels(userID string, channelIDs []string, expectedMsg string) error { - return s.WaitForMessagesPostedOnChannels(userID, channelIDs, 1, func(msg slack.Message) bool { - return msg.Text == expectedMsg - }) -} - -func (s *slackTester) WaitForMessagePosted(userID, channelID string, limitMessages int, msgAssertFn func(msg slack.Message) bool) error { +func (s *slackTester) WaitForMessagePosted(userID, channelID string, limitMessages int, assertFn MessageAssertion) error { var fetchedMessages []slack.Message var lastErr error err := wait.Poll(pollInterval, s.cfg.MessageWaitTimeout, func() (done bool, err error) { @@ -308,7 +385,7 @@ func (s *slackTester) WaitForMessagePosted(userID, channelID string, limitMessag continue } - if !msgAssertFn(msg) { + if !assertFn(msg.Text) { // different message continue } @@ -331,10 +408,60 @@ func (s *slackTester) WaitForMessagePosted(userID, channelID string, limitMessag return nil } -func (s *slackTester) WaitForMessagesPostedOnChannels(userID string, channelIDs []string, limitMessages int, msgAssertFn func(msg slack.Message) bool) error { +func (s *slackTester) WaitForMessagePostedWithAttachment(userID, channelID string, assertFn AttachmentAssertion) error { + var fetchedMessages []slack.Message + var lastErr error + err := wait.Poll(pollInterval, s.cfg.MessageWaitTimeout, func() (done bool, err error) { + historyRes, err := s.cli.GetConversationHistory(&slack.GetConversationHistoryParameters{ + ChannelID: channelID, Limit: 1, + }) + if err != nil { + lastErr = err + return false, nil + } + + fetchedMessages = historyRes.Messages + for _, msg := range historyRes.Messages { + if msg.User != userID { + continue + } + + if len(msg.Attachments) != 1 { + return false, nil + } + + attachment := msg.Attachments[0] + if len(attachment.Fields) != 1 { + return false, nil + } + + if !assertFn(attachment.Title, attachment.Color, attachment.Fields[0].Value) { + // different message + return false, nil + } + + return true, nil + } + + return false, nil + }) + if lastErr == nil { + lastErr = errors.New("message assertion function returned false") + } + if err != nil { + if err == wait.ErrWaitTimeout { + return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, structDumper.Sdump(fetchedMessages)) + } + return err + } + + return nil +} + +func (s *slackTester) WaitForMessagesPostedOnChannelsWithAttachment(userID string, channelIDs []string, assertFn AttachmentAssertion) error { errs := multierror.New() for _, channelID := range channelIDs { - errs = multierror.Append(errs, s.WaitForMessagePosted(userID, channelID, limitMessages, msgAssertFn)) + errs = multierror.Append(errs, s.WaitForMessagePostedWithAttachment(userID, channelID, assertFn)) } return errs.ErrorOrNil() From 872410a97c8dcd37ef8719eea58f4a8d4b93a399 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Wed, 7 Sep 2022 11:51:18 +0100 Subject: [PATCH 04/20] Unified channel initialisation for Slack and Discord. --- test/e2e/bots_test.go | 229 +++++++++++++++++------------------ test/e2e/bots_tester_test.go | 50 ++++++-- 2 files changed, 152 insertions(+), 127 deletions(-) diff --git a/test/e2e/bots_test.go b/test/e2e/bots_test.go index 02784c654..04e39ed15 100644 --- a/test/e2e/bots_test.go +++ b/test/e2e/bots_test.go @@ -85,17 +85,14 @@ func TestSlack(t *testing.T) { t.Log("Setting up test Slack setup...") slackTester.InitUsers(t) - //botUserID := slackTester.FindUserIDForBot(t) - //testerUserID := slackTester.FindUserIDForTester(t) - - channel, cleanupChannelFn := slackTester.CreateChannel(t) - t.Cleanup(func() { cleanupChannelFn(t) }) - secondChannel, cleanupSecondChannelFn := slackTester.CreateChannel(t) - t.Cleanup(func() { cleanupSecondChannelFn(t) }) + cleanUpFns := slackTester.InitChannels(t) + for _, fn := range cleanUpFns { + t.Cleanup(fn) + } channels := map[string]*slack.Channel{ - appCfg.Deployment.Envs.DefaultSlackChannelIDName: channel, - appCfg.Deployment.Envs.SecondarySlackChannelIDName: secondChannel, + appCfg.Deployment.Envs.DefaultSlackChannelIDName: slackTester.channel, + appCfg.Deployment.Envs.SecondarySlackChannelIDName: slackTester.secondChannel, } for _, currentChannel := range channels { slackTester.PostInitialMessage(t, currentChannel.Name) @@ -112,7 +109,7 @@ func TestSlack(t *testing.T) { require.NoError(t, err) t.Log("Waiting for Bot message on channel...") - err = slackTester.WaitForMessagePostedRecentlyEqual(slackTester.botUserID, channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) + err = slackTester.WaitForMessagePostedRecentlyEqual(slackTester.botUserID, slackTester.channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) require.NoError(t, err) t.Log("Running actual test cases") @@ -121,8 +118,8 @@ func TestSlack(t *testing.T) { command := "ping" expectedMessage := fmt.Sprintf("pong from cluster '%s'", appCfg.ClusterName) - slackTester.PostMessageToBot(t, channel.Name, command) - err := slackTester.WaitForLastMessageContains(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err := slackTester.WaitForLastMessageContains(slackTester.botUserID, slackTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -133,8 +130,8 @@ func TestSlack(t *testing.T) { NodeEventsChecker true Sends notifications on node level critical events. ObjectAnnotationChecker true Checks if annotations present in object specs and filters them.`)) - slackTester.PostMessageToBot(t, channel.Name, command) - err := slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err := slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -195,27 +192,27 @@ func TestSlack(t *testing.T) { restrictAccess: false`)) t.Run("With default cluster", func(t *testing.T) { - slackTester.PostMessageToBot(t, channel.Name, command) - err := slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err := slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) assert.NoError(t, err) }) t.Run("With custom cluster name", func(t *testing.T) { command := fmt.Sprintf("commands list --cluster-name %s", appCfg.ClusterName) - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) assert.NoError(t, err) }) t.Run("With unknown cluster name", func(t *testing.T) { command := "commands list --cluster-name non-existing" - slackTester.PostMessageToBot(t, channel.Name, command) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) t.Log("Ensuring bot didn't write anything new...") time.Sleep(appCfg.Slack.MessageWaitTimeout) // Same expected message as before - err = slackTester.WaitForLastMessageContains(slackTester.testerUserID, channel.ID, command) + err = slackTester.WaitForLastMessageContains(slackTester.testerUserID, slackTester.channel.ID, command) assert.NoError(t, err) }) }) @@ -228,8 +225,8 @@ func TestSlack(t *testing.T) { strings.Contains(msg, "botkube") } - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForMessagePosted(slackTester.botUserID, channel.ID, 1, assertionFn) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForMessagePosted(slackTester.botUserID, slackTester.channel.ID, 1, assertionFn) assert.NoError(t, err) }) @@ -241,8 +238,8 @@ func TestSlack(t *testing.T) { strings.Contains(msg, "botkube-global-config") } - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForMessagePosted(slackTester.botUserID, channel.ID, 1, assertionFn) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForMessagePosted(slackTester.botUserID, slackTester.channel.ID, 1, assertionFn) assert.NoError(t, err) }) @@ -250,8 +247,8 @@ func TestSlack(t *testing.T) { command := "get ingress" expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'ingress' resources in the 'default' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -259,8 +256,8 @@ func TestSlack(t *testing.T) { command := "unknown" expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -274,8 +271,8 @@ func TestSlack(t *testing.T) { See 'kubectl get -h' for help and examples exit status 1`, appCfg.ClusterName)) - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -283,8 +280,8 @@ func TestSlack(t *testing.T) { command := "get po --namespace team-b" expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'po' resources in the 'team-b' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -296,8 +293,8 @@ func TestSlack(t *testing.T) { strings.Contains(msg, "deployment.apps/botkube condition met") } - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForMessagePosted(slackTester.botUserID, channel.ID, 1, assertionFn) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForMessagePosted(slackTester.botUserID, slackTester.channel.ID, 1, assertionFn) assert.NoError(t, err) }) @@ -305,8 +302,8 @@ func TestSlack(t *testing.T) { command := "exec" expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -314,8 +311,8 @@ func TestSlack(t *testing.T) { command := "get pods -A" expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'pods' resources for all Namespaces on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -328,8 +325,8 @@ func TestSlack(t *testing.T) { strings.Contains(msg, "botkube") } - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForMessagePosted(slackTester.botUserID, channel.ID, 1, assertionFn) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForMessagePosted(slackTester.botUserID, slackTester.channel.ID, 1, assertionFn) assert.NoError(t, err) }) }) @@ -346,7 +343,7 @@ func TestSlack(t *testing.T) { var cfgMapAlreadyDeleted bool cfgMap := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: channel.Name, + Name: slackTester.channel.Name, Namespace: appCfg.Deployment.Namespace, }, } @@ -361,13 +358,13 @@ func TestSlack(t *testing.T) { color == "2eb886" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been created in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, channel.ID, attachAssertionFn) + err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, slackTester.channel.ID, attachAssertionFn) require.NoError(t, err) t.Log("Expecting no bot message in second channel...") expectedMessage := fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName) time.Sleep(appCfg.Slack.MessageWaitTimeout) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, secondChannel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.secondChannel.ID, expectedMessage) require.NoError(t, err) t.Log("Updating ConfigMap...") @@ -390,22 +387,22 @@ func TestSlack(t *testing.T) { command := "notifier stop" expectedMessage = codeBlock(fmt.Sprintf("Sure! I won't send you notifications from cluster '%s' here.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) assert.NoError(t, err) t.Log("Getting notifier status from second channel...") command = "notifier status" expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are enabled here.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, secondChannel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, secondChannel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.secondChannel.Name, command) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.secondChannel.ID, expectedMessage) assert.NoError(t, err) t.Log("Getting notifier status from first channel...") command = "notifier status" expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are disabled here.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) assert.NoError(t, err) t.Log("Updating ConfigMap once again...") @@ -418,7 +415,7 @@ func TestSlack(t *testing.T) { t.Log("Ensuring bot didn't write anything new on first channel...") time.Sleep(appCfg.Slack.MessageWaitTimeout) // Same expected message as before - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) require.NoError(t, err) t.Log("Expecting bot message on second channel...") @@ -427,19 +424,19 @@ func TestSlack(t *testing.T) { color == "daa038" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, secondChannel.ID, attachAssertionFn) + err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, slackTester.secondChannel.ID, attachAssertionFn) t.Log("Starting notifier") command = "notifier start" expectedMessage = codeBlock(fmt.Sprintf("Brace yourselves, incoming notifications from cluster '%s'.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) require.NoError(t, err) t.Log("Creating and deleting ignored ConfigMap") ignoredCfgMap := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-ignored", channel.Name), + Name: fmt.Sprintf("%s-ignored", slackTester.channel.Name), Namespace: appCfg.Deployment.Namespace, Annotations: map[string]string{ filters.DisableAnnotation: "true", @@ -453,7 +450,7 @@ func TestSlack(t *testing.T) { t.Log("Ensuring bot didn't write anything new...") time.Sleep(appCfg.Slack.MessageWaitTimeout) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, channel.ID, expectedMessage) + err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) require.NoError(t, err) t.Log("Deleting ConfigMap") @@ -467,7 +464,7 @@ func TestSlack(t *testing.T) { color == "a30200" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been deleted in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, channel.ID, attachAssertionFn) + err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, slackTester.channel.ID, attachAssertionFn) require.NoError(t, err) t.Log("Ensuring bot didn't write anything new on second channel...") @@ -477,7 +474,7 @@ func TestSlack(t *testing.T) { color == "daa038" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, secondChannel.ID, attachAssertionFn) + err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, slackTester.secondChannel.ID, attachAssertionFn) require.NoError(t, err) }) @@ -487,7 +484,7 @@ func TestSlack(t *testing.T) { t.Log("Creating Pod...") pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Name: channel.Name, + Name: slackTester.channel.Name, Namespace: appCfg.Deployment.Namespace, }, Spec: v1.PodSpec{ @@ -509,7 +506,7 @@ func TestSlack(t *testing.T) { strings.Contains(msg, fmt.Sprintf("- Pod '%s/%s' created without labels. Consider defining them, to be able to use them as a selector e.g. in Service.", pod.Namespace, pod.Name)) && strings.Contains(msg, fmt.Sprintf("- The 'latest' tag used in '%s' image of Pod '%s/%s' container '%s' should be avoided.", pod.Spec.Containers[0].Image, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)) } - err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, channel.ID, assertionFn) + err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, slackTester.channel.ID, assertionFn) require.NoError(t, err) }) } @@ -532,15 +529,14 @@ func TestDiscord(t *testing.T) { t.Log("Setting up test Discord setup...") discordTester.InitUsers(t) - - channel, cleanupChannelFn := discordTester.CreateChannel(t) - t.Cleanup(func() { cleanupChannelFn(t) }) - secondChannel, cleanupSecondChannelFn := discordTester.CreateChannel(t) - t.Cleanup(func() { cleanupSecondChannelFn(t) }) + cleanUpFns := discordTester.InitChannels(t) + for _, fn := range cleanUpFns { + t.Cleanup(fn) + } channels := map[string]*discordgo.Channel{ - appCfg.Deployment.Envs.DefaultDiscordChannelIDName: channel, - appCfg.Deployment.Envs.SecondaryDiscordChannelIDName: secondChannel, + appCfg.Deployment.Envs.DefaultDiscordChannelIDName: discordTester.channel, + appCfg.Deployment.Envs.SecondaryDiscordChannelIDName: discordTester.secondChannel, } for _, currentChannel := range channels { @@ -558,7 +554,7 @@ func TestDiscord(t *testing.T) { require.NoError(t, err) t.Log("Waiting for Bot message on channel from user") - err = discordTester.WaitForMessagePostedRecentlyEqual(discordTester.botUserID, channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) + err = discordTester.WaitForMessagePostedRecentlyEqual(discordTester.botUserID, discordTester.channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) require.NoError(t, err) t.Log("Running actual test cases") @@ -567,9 +563,8 @@ func TestDiscord(t *testing.T) { command := "ping" expectedMessage := fmt.Sprintf("pong from cluster '%s'", appCfg.ClusterName) - //discordTester.PostMessageToBot(t, channel.ID, command) - discordTester.PostMessageToBot(t, channel.ID, command) - err := discordTester.WaitForLastMessageContains(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err := discordTester.WaitForLastMessageContains(discordTester.botUserID, discordTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -580,8 +575,8 @@ func TestDiscord(t *testing.T) { NodeEventsChecker true Sends notifications on node level critical events. ObjectAnnotationChecker true Checks if annotations botkube.io/* present in object specs and filters them.`)) - discordTester.PostMessageToBot(t, channel.ID, command) - err := discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err := discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -642,27 +637,27 @@ func TestDiscord(t *testing.T) { restrictAccess: false`)) t.Run("With default cluster", func(t *testing.T) { - discordTester.PostMessageToBot(t, channel.ID, command) - err := discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err := discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) assert.NoError(t, err) }) t.Run("With custom cluster name", func(t *testing.T) { command := fmt.Sprintf("commands list --cluster-name %s", appCfg.ClusterName) - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) assert.NoError(t, err) }) t.Run("With unknown cluster name", func(t *testing.T) { command := "commands list --cluster-name non-existing" - discordTester.PostMessageToBot(t, channel.ID, command) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) t.Log("Ensuring bot didn't write anything new...") time.Sleep(appCfg.Discord.MessageWaitTimeout) // Same expected message as before - err = discordTester.WaitForLastMessageContains(discordTester.testerUserID, channel.ID, command) + err = discordTester.WaitForLastMessageContains(discordTester.testerUserID, discordTester.channel.ID, command) assert.NoError(t, err) }) }) @@ -675,8 +670,8 @@ func TestDiscord(t *testing.T) { strings.Contains(msg, "botkube") } - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForMessagePosted(discordTester.botUserID, channel.ID, 1, assertionFn) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForMessagePosted(discordTester.botUserID, discordTester.channel.ID, 1, assertionFn) assert.NoError(t, err) }) @@ -688,8 +683,8 @@ func TestDiscord(t *testing.T) { strings.Contains(msg, "botkube-global-config") } - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForMessagePosted(discordTester.botUserID, channel.ID, 1, assertionFn) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForMessagePosted(discordTester.botUserID, discordTester.channel.ID, 1, assertionFn) assert.NoError(t, err) }) @@ -697,8 +692,8 @@ func TestDiscord(t *testing.T) { command := "get ingress" expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'ingress' resources in the 'default' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -706,8 +701,8 @@ func TestDiscord(t *testing.T) { command := "unknown" expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -715,8 +710,8 @@ func TestDiscord(t *testing.T) { command := "get" expectedMessage := codeBlock(fmt.Sprintf("Cluster: %s\nYou must specify the type of resource to get. Use \"kubectl api-resources\" for a complete list of supported resources.\n\nerror: Required resource not specified.\nUse \"kubectl explain \" for a detailed description of that resource (e.g. kubectl explain pods).\nSee 'kubectl get -h' for help and examples\nexit status 1", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -724,8 +719,8 @@ func TestDiscord(t *testing.T) { command := "get po --namespace team-b" expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'po' resources in the 'team-b' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -737,8 +732,8 @@ func TestDiscord(t *testing.T) { strings.Contains(msg, "deployment.apps/botkube condition met") } - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForMessagePosted(discordTester.botUserID, channel.ID, 1, assertionFn) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForMessagePosted(discordTester.botUserID, discordTester.channel.ID, 1, assertionFn) assert.NoError(t, err) }) @@ -746,8 +741,8 @@ func TestDiscord(t *testing.T) { command := "exec" expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -755,8 +750,8 @@ func TestDiscord(t *testing.T) { command := "get pods -A" expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'pods' resources for all Namespaces on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) assert.NoError(t, err) }) @@ -769,8 +764,8 @@ func TestDiscord(t *testing.T) { strings.Contains(msg, "botkube") } - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForMessagePosted(discordTester.botUserID, channel.ID, 1, assertionFn) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForMessagePosted(discordTester.botUserID, discordTester.channel.ID, 1, assertionFn) assert.NoError(t, err) }) }) @@ -787,7 +782,7 @@ func TestDiscord(t *testing.T) { var cfgMapAlreadyDeleted bool cfgMap := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: channel.Name, + Name: discordTester.channel.Name, Namespace: appCfg.Deployment.Namespace, }, } @@ -802,13 +797,13 @@ func TestDiscord(t *testing.T) { color == "8311585" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been created in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, channel.ID, assertionFn) + err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, discordTester.channel.ID, assertionFn) require.NoError(t, err) t.Log("Expecting no bot message in second channel...") expectedMessage := fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName) time.Sleep(appCfg.Discord.MessageWaitTimeout) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, secondChannel.ID, expectedMessage) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.secondChannel.ID, expectedMessage) require.NoError(t, err) t.Log("Updating ConfigMap...") @@ -831,22 +826,22 @@ func TestDiscord(t *testing.T) { command := "notifier stop" expectedMessage = codeBlock(fmt.Sprintf("Sure! I won't send you notifications from cluster '%s' here.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) assert.NoError(t, err) t.Log("Getting notifier status from second channel...") command = "notifier status" expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are enabled here.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, secondChannel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, secondChannel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.secondChannel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.secondChannel.ID, expectedMessage) assert.NoError(t, err) t.Log("Getting notifier status from first channel...") command = "notifier status" expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are disabled here.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) assert.NoError(t, err) t.Log("Updating ConfigMap once again...") @@ -859,7 +854,7 @@ func TestDiscord(t *testing.T) { t.Log("Ensuring bot didn't write anything new on first channel...") time.Sleep(appCfg.Slack.MessageWaitTimeout) // Same expected message as before - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) require.NoError(t, err) t.Log("Expecting bot message on second channel...") @@ -868,20 +863,20 @@ func TestDiscord(t *testing.T) { color == "16312092" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, secondChannel.ID, attachmentAssertionFn) + err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, discordTester.secondChannel.ID, attachmentAssertionFn) require.NoError(t, err) t.Log("Starting notifier") command = "notifier start" expectedMessage = codeBlock(fmt.Sprintf("Brace yourselves, incoming notifications from cluster '%s'.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + discordTester.PostMessageToBot(t, discordTester.channel.ID, command) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) require.NoError(t, err) t.Log("Creating and deleting ignored ConfigMap") ignoredCfgMap := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-ignored", channel.Name), + Name: fmt.Sprintf("%s-ignored", discordTester.channel.Name), Namespace: appCfg.Deployment.Namespace, Annotations: map[string]string{ filters.DisableAnnotation: "true", @@ -895,7 +890,7 @@ func TestDiscord(t *testing.T) { t.Log("Ensuring bot didn't write anything new...") time.Sleep(appCfg.Slack.MessageWaitTimeout) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, channel.ID, expectedMessage) + err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) require.NoError(t, err) t.Log("Deleting ConfigMap") @@ -909,7 +904,7 @@ func TestDiscord(t *testing.T) { color == "13632027" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been deleted in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, channel.ID, assertionFn) + err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, discordTester.channel.ID, assertionFn) require.NoError(t, err) t.Log("Ensuring bot didn't write anything new on second channel...") @@ -919,7 +914,7 @@ func TestDiscord(t *testing.T) { color == "16312092" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, secondChannel.ID, assertionFn) + err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, discordTester.secondChannel.ID, assertionFn) require.NoError(t, err) }) @@ -929,7 +924,7 @@ func TestDiscord(t *testing.T) { t.Log("Creating Pod...") pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Name: channel.Name, + Name: discordTester.channel.Name, Namespace: appCfg.Deployment.Namespace, }, Spec: v1.PodSpec{ @@ -951,7 +946,7 @@ func TestDiscord(t *testing.T) { strings.Contains(msg, fmt.Sprintf("- Pod '%s/%s' created without labels. Consider defining them, to be able to use them as a selector e.g. in Service.", pod.Namespace, pod.Name)) && strings.Contains(msg, fmt.Sprintf("- The 'latest' tag used in '%s' image of Pod '%s/%s' container '%s' should be avoided.", pod.Spec.Containers[0].Image, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)) } - err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, channel.ID, assertionFn) + err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, discordTester.channel.ID, assertionFn) require.NoError(t, err) }) } diff --git a/test/e2e/bots_tester_test.go b/test/e2e/bots_tester_test.go index b4cb8e0d7..57984ef4f 100644 --- a/test/e2e/bots_tester_test.go +++ b/test/e2e/bots_tester_test.go @@ -37,17 +37,21 @@ type MessageAssertion func(content string) bool type AttachmentAssertion func(title, color, msg string) bool type slackTester struct { - cli *slack.Client - cfg SlackConfig - botUserID string - testerUserID string + cli *slack.Client + cfg SlackConfig + botUserID string + testerUserID string + channel *slack.Channel + secondChannel *slack.Channel } type discordTester struct { - cli *discordgo.Session - cfg DiscordConfig - botUserID string - testerUserID string + cli *discordgo.Session + cfg DiscordConfig + botUserID string + testerUserID string + channel *discordgo.Channel + secondChannel *discordgo.Channel } func newSlackTester(slackCfg SlackConfig) (*slackTester, error) { @@ -66,6 +70,19 @@ func (s *slackTester) InitUsers(t *testing.T) { s.testerUserID = s.FindUserID(t, s.cfg.TesterName) } +func (s *slackTester) InitChannels(t *testing.T) []func() { + channel, cleanupChannelFn := s.createChannel(t) + s.channel = channel + + secondChannel, cleanupSecondChannelFn := s.createChannel(t) + s.secondChannel = secondChannel + + return []func(){ + func() { cleanupChannelFn(t) }, + func() { cleanupSecondChannelFn(t) }, + } +} + func newDiscordTester(discordCfg DiscordConfig) (*discordTester, error) { discordCli, err := discordgo.New("Bot " + discordCfg.TesterAppToken) if err != nil { @@ -80,7 +97,20 @@ func (d *discordTester) InitUsers(t *testing.T) { d.testerUserID = d.FindUserID(t, d.cfg.TesterName) } -func (d *discordTester) CreateChannel(t *testing.T) (*discordgo.Channel, func(t *testing.T)) { +func (d *discordTester) InitChannels(t *testing.T) []func() { + channel, cleanupChannelFn := d.createChannel(t) + d.channel = channel + + secondChannel, cleanupSecondChannelFn := d.createChannel(t) + d.secondChannel = secondChannel + + return []func(){ + func() { cleanupChannelFn(t) }, + func() { cleanupSecondChannelFn(t) }, + } +} + +func (d *discordTester) createChannel(t *testing.T) (*discordgo.Channel, func(t *testing.T)) { t.Helper() randomID := uuid.New() channelName := fmt.Sprintf("%s-%s", channelNamePrefix, randomID.String()) @@ -275,7 +305,7 @@ func (d *discordTester) WaitForMessagesPostedOnChannelsWithAttachment(userID str return errs.ErrorOrNil() } -func (s *slackTester) CreateChannel(t *testing.T) (*slack.Channel, func(t *testing.T)) { +func (s *slackTester) createChannel(t *testing.T) (*slack.Channel, func(t *testing.T)) { t.Helper() randomID := uuid.New() channelName := fmt.Sprintf("%s-%s", channelNamePrefix, randomID.String()) From c45bc66bb2488c98dbf16f655c77992de2ddf196 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Wed, 7 Sep 2022 13:19:32 +0100 Subject: [PATCH 05/20] Slack and Discord drivers can interchangeably run the same tests. --- test/e2e/bots_test.go | 659 +++++++---------------------------- test/e2e/bots_tester_test.go | 151 ++++++-- test/e2e/k8s_helpers_test.go | 16 +- 3 files changed, 257 insertions(+), 569 deletions(-) diff --git a/test/e2e/bots_test.go b/test/e2e/bots_test.go index 04e39ed15..61eb7f28e 100644 --- a/test/e2e/bots_test.go +++ b/test/e2e/bots_test.go @@ -10,9 +10,7 @@ import ( "time" "github.com/MakeNowJust/heredoc" - "github.com/bwmarrin/discordgo" "github.com/kubeshop/botkube/pkg/filterengine/filters" - "github.com/slack-go/slack" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vrischmann/envconfig" @@ -58,13 +56,23 @@ type DiscordConfig struct { AdditionalContextMessage string `envconfig:"optional"` GuildID string TesterAppToken string - MessageWaitTimeout time.Duration `envconfig:"default=15s"` + MessageWaitTimeout time.Duration `envconfig:"default=10s"` } const ( - channelNamePrefix = "test" - welcomeText = "Let the tests begin 🤞" - pollInterval = time.Second + channelNamePrefix = "test" + welcomeText = "Let the tests begin 🤞" + pollInterval = time.Second + slackAnnotation = "" + discordAnnotation = "botkube.io/*" + slackInvalidCmdTemplate = `Cluster: %s + You must specify the type of resource to get. Use "kubectl api-resources" for a complete list of supported resources. + + error: Required resource not specified. + Use "kubectl explain <resource>" for a detailed description of that resource (e.g. kubectl explain pods). + See 'kubectl get -h' for help and examples + exit status 1` + discordInvalidCmdTemplate = "Cluster: %s\nYou must specify the type of resource to get. Use \"kubectl api-resources\" for a complete list of supported resources.\n\nerror: Required resource not specified.\nUse \"kubectl explain \" for a detailed description of that resource (e.g. kubectl explain pods).\nSee 'kubectl get -h' for help and examples\nexit status 1" ) func TestSlack(t *testing.T) { @@ -73,8 +81,52 @@ func TestSlack(t *testing.T) { err := envconfig.Init(&appCfg) require.NoError(t, err) - t.Log("Creating Slack API client with provided token...") - slackTester, err := newSlackTester(appCfg.Slack) + runBotTest(t, + appCfg, + SlackBot, + slackAnnotation, + slackInvalidCmdTemplate, + appCfg.Deployment.Envs.DefaultSlackChannelIDName, + appCfg.Deployment.Envs.SecondarySlackChannelIDName, + ) +} + +func TestDiscord(t *testing.T) { + t.Log("Loading configuration...") + var appCfg Config + err := envconfig.Init(&appCfg) + require.NoError(t, err) + + runBotTest(t, + appCfg, + DiscordBot, + discordAnnotation, + discordInvalidCmdTemplate, + appCfg.Deployment.Envs.DefaultDiscordChannelIDName, + appCfg.Deployment.Envs.SecondaryDiscordChannelIDName, + ) +} + +func newBotDriver(cfg Config, driverType BotType) (BotDriver, error) { + switch driverType { + case SlackBot: + return newSlackTester(cfg.Slack) + case DiscordBot: + return newDiscordTester(cfg.Discord) + } + return nil, nil +} + +func runBotTest(t *testing.T, + appCfg Config, + driverType BotType, + annotation, + invalidCmdTemplate, + deployEnvChannelIDName, + deployEnvSecondaryChannelIDName string, +) { + t.Logf("Creating API client with provided token for %s...", driverType) + botDriver, err := newBotDriver(appCfg, driverType) require.NoError(t, err) t.Log("Creating K8s client...") @@ -83,33 +135,34 @@ func TestSlack(t *testing.T) { k8sCli, err := kubernetes.NewForConfig(k8sConfig) require.NoError(t, err) - t.Log("Setting up test Slack setup...") - slackTester.InitUsers(t) - cleanUpFns := slackTester.InitChannels(t) + t.Logf("Setting up test %s setup...", driverType) + botDriver.InitUsers(t) + cleanUpFns := botDriver.InitChannels(t) for _, fn := range cleanUpFns { t.Cleanup(fn) } - channels := map[string]*slack.Channel{ - appCfg.Deployment.Envs.DefaultSlackChannelIDName: slackTester.channel, - appCfg.Deployment.Envs.SecondarySlackChannelIDName: slackTester.secondChannel, + channels := map[string]Channel{ + deployEnvChannelIDName: botDriver.Channel(), + deployEnvSecondaryChannelIDName: botDriver.SecondChannel(), } + for _, currentChannel := range channels { - slackTester.PostInitialMessage(t, currentChannel.Name) - slackTester.InviteBotToChannel(t, currentChannel.ID) + botDriver.PostInitialMessage(t, currentChannel.Identifier()) + botDriver.InviteBotToChannel(t, currentChannel.ID()) } t.Log("Patching Deployment with test env variables...") deployNsCli := k8sCli.AppsV1().Deployments(appCfg.Deployment.Namespace) - revertDeployFn := setTestEnvsForDeploy(t, appCfg, deployNsCli, channels, nil) + revertDeployFn := setTestEnvsForDeploy(t, appCfg, deployNsCli, botDriver.Type(), channels) t.Cleanup(func() { revertDeployFn(t) }) t.Log("Waiting for Deployment") err = waitForDeploymentReady(deployNsCli, appCfg.Deployment.Name, appCfg.Deployment.WaitTimeout) require.NoError(t, err) - t.Log("Waiting for Bot message on channel...") - err = slackTester.WaitForMessagePostedRecentlyEqual(slackTester.botUserID, slackTester.channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) + t.Log("Waiting for Bot message on channel from user") + err = botDriver.WaitForMessagePostedRecentlyEqual(botDriver.BotUserID(), botDriver.Channel().ID(), fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) require.NoError(t, err) t.Log("Running actual test cases") @@ -118,20 +171,20 @@ func TestSlack(t *testing.T) { command := "ping" expectedMessage := fmt.Sprintf("pong from cluster '%s'", appCfg.ClusterName) - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err := slackTester.WaitForLastMessageContains(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err := botDriver.WaitForLastMessageContains(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) }) t.Run("Filters list", func(t *testing.T) { command := "filters list" - expectedMessage := codeBlock(heredoc.Doc(` + expectedMessage := codeBlock(heredoc.Doc(fmt.Sprintf(` FILTER ENABLED DESCRIPTION NodeEventsChecker true Sends notifications on node level critical events. - ObjectAnnotationChecker true Checks if annotations present in object specs and filters them.`)) + ObjectAnnotationChecker true Checks if annotations %s present in object specs and filters them.`, annotation))) - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err := slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err := botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) }) @@ -192,27 +245,27 @@ func TestSlack(t *testing.T) { restrictAccess: false`)) t.Run("With default cluster", func(t *testing.T) { - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err := slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err := botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) }) t.Run("With custom cluster name", func(t *testing.T) { command := fmt.Sprintf("commands list --cluster-name %s", appCfg.ClusterName) - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) }) t.Run("With unknown cluster name", func(t *testing.T) { command := "commands list --cluster-name non-existing" - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) t.Log("Ensuring bot didn't write anything new...") time.Sleep(appCfg.Slack.MessageWaitTimeout) // Same expected message as before - err = slackTester.WaitForLastMessageContains(slackTester.testerUserID, slackTester.channel.ID, command) + err = botDriver.WaitForLastMessageContains(botDriver.TesterUserID(), botDriver.Channel().ID(), command) assert.NoError(t, err) }) }) @@ -225,8 +278,8 @@ func TestSlack(t *testing.T) { strings.Contains(msg, "botkube") } - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForMessagePosted(slackTester.botUserID, slackTester.channel.ID, 1, assertionFn) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForMessagePosted(botDriver.BotUserID(), botDriver.Channel().ID(), 1, assertionFn) assert.NoError(t, err) }) @@ -238,8 +291,8 @@ func TestSlack(t *testing.T) { strings.Contains(msg, "botkube-global-config") } - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForMessagePosted(slackTester.botUserID, slackTester.channel.ID, 1, assertionFn) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForMessagePosted(botDriver.BotUserID(), botDriver.Channel().ID(), 1, assertionFn) assert.NoError(t, err) }) @@ -247,8 +300,8 @@ func TestSlack(t *testing.T) { command := "get ingress" expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'ingress' resources in the 'default' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) }) @@ -256,23 +309,17 @@ func TestSlack(t *testing.T) { command := "unknown" expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) }) t.Run("Specify invalid command", func(t *testing.T) { command := "get" - expectedMessage := codeBlock(heredoc.Docf(`Cluster: %s - You must specify the type of resource to get. Use "kubectl api-resources" for a complete list of supported resources. - - error: Required resource not specified. - Use "kubectl explain <resource>" for a detailed description of that resource (e.g. kubectl explain pods). - See 'kubectl get -h' for help and examples - exit status 1`, appCfg.ClusterName)) + expectedMessage := codeBlock(heredoc.Docf(invalidCmdTemplate, appCfg.ClusterName)) - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) }) @@ -280,8 +327,8 @@ func TestSlack(t *testing.T) { command := "get po --namespace team-b" expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'po' resources in the 'team-b' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) }) @@ -293,8 +340,8 @@ func TestSlack(t *testing.T) { strings.Contains(msg, "deployment.apps/botkube condition met") } - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForMessagePosted(slackTester.botUserID, slackTester.channel.ID, 1, assertionFn) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForMessagePosted(botDriver.BotUserID(), botDriver.Channel().ID(), 1, assertionFn) assert.NoError(t, err) }) @@ -302,8 +349,8 @@ func TestSlack(t *testing.T) { command := "exec" expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) }) @@ -311,8 +358,8 @@ func TestSlack(t *testing.T) { command := "get pods -A" expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'pods' resources for all Namespaces on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) }) @@ -325,8 +372,8 @@ func TestSlack(t *testing.T) { strings.Contains(msg, "botkube") } - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForMessagePosted(slackTester.botUserID, slackTester.channel.ID, 1, assertionFn) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForMessagePosted(botDriver.BotUserID(), botDriver.Channel().ID(), 1, assertionFn) assert.NoError(t, err) }) }) @@ -336,14 +383,14 @@ func TestSlack(t *testing.T) { cfgMapCli := k8sCli.CoreV1().ConfigMaps(appCfg.Deployment.Namespace) var channelIDs []string for _, channel := range channels { - channelIDs = append(channelIDs, channel.ID) + channelIDs = append(channelIDs, channel.ID()) } t.Log("Creating ConfigMap...") var cfgMapAlreadyDeleted bool cfgMap := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: slackTester.channel.Name, + Name: botDriver.Channel().Name(), Namespace: appCfg.Deployment.Namespace, }, } @@ -353,18 +400,17 @@ func TestSlack(t *testing.T) { t.Cleanup(func() { cleanupCreatedCfgMapIfShould(t, cfgMapCli, cfgMap.Name, &cfgMapAlreadyDeleted) }) t.Log("Expecting bot message in first channel...") - attachAssertionFn := func(title, color, msg string) bool { + attachAssertionFn := func(title, _, msg string) bool { return title == "v1/configmaps created" && - color == "2eb886" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been created in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, slackTester.channel.ID, attachAssertionFn) + err = botDriver.WaitForMessagePostedWithAttachment(botDriver.BotUserID(), botDriver.Channel().ID(), attachAssertionFn) require.NoError(t, err) t.Log("Expecting no bot message in second channel...") expectedMessage := fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName) time.Sleep(appCfg.Slack.MessageWaitTimeout) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.secondChannel.ID, expectedMessage) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.SecondChannel().ID(), expectedMessage) require.NoError(t, err) t.Log("Updating ConfigMap...") @@ -375,34 +421,33 @@ func TestSlack(t *testing.T) { require.NoError(t, err) t.Log("Expecting bot message in all channels...") - attachAssertionFn = func(title, color, msg string) bool { + attachAssertionFn = func(title, _, msg string) bool { return title == "v1/configmaps updated" && - color == "daa038" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagesPostedOnChannelsWithAttachment(slackTester.botUserID, channelIDs, attachAssertionFn) + err = botDriver.WaitForMessagesPostedOnChannelsWithAttachment(botDriver.BotUserID(), channelIDs, attachAssertionFn) require.NoError(t, err) t.Log("Stopping notifier...") command := "notifier stop" expectedMessage = codeBlock(fmt.Sprintf("Sure! I won't send you notifications from cluster '%s' here.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) t.Log("Getting notifier status from second channel...") command = "notifier status" expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are enabled here.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, slackTester.secondChannel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.secondChannel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.SecondChannel().Identifier(), command) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.SecondChannel().ID(), expectedMessage) assert.NoError(t, err) t.Log("Getting notifier status from first channel...") command = "notifier status" expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are disabled here.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) assert.NoError(t, err) t.Log("Updating ConfigMap once again...") @@ -415,28 +460,27 @@ func TestSlack(t *testing.T) { t.Log("Ensuring bot didn't write anything new on first channel...") time.Sleep(appCfg.Slack.MessageWaitTimeout) // Same expected message as before - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) require.NoError(t, err) t.Log("Expecting bot message on second channel...") - attachAssertionFn = func(title, color, msg string) bool { + attachAssertionFn = func(title, _, msg string) bool { return title == "v1/configmaps updated" && - color == "daa038" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, slackTester.secondChannel.ID, attachAssertionFn) + err = botDriver.WaitForMessagePostedWithAttachment(botDriver.BotUserID(), botDriver.SecondChannel().ID(), attachAssertionFn) t.Log("Starting notifier") command = "notifier start" expectedMessage = codeBlock(fmt.Sprintf("Brace yourselves, incoming notifications from cluster '%s'.", appCfg.ClusterName)) - slackTester.PostMessageToBot(t, slackTester.channel.Name, command) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + botDriver.PostMessageToBot(t, botDriver.Channel().Identifier(), command) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) require.NoError(t, err) t.Log("Creating and deleting ignored ConfigMap") ignoredCfgMap := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-ignored", slackTester.channel.Name), + Name: fmt.Sprintf("%s-ignored", botDriver.Channel().Name()), Namespace: appCfg.Deployment.Namespace, Annotations: map[string]string{ filters.DisableAnnotation: "true", @@ -450,7 +494,7 @@ func TestSlack(t *testing.T) { t.Log("Ensuring bot didn't write anything new...") time.Sleep(appCfg.Slack.MessageWaitTimeout) - err = slackTester.WaitForLastMessageEqual(slackTester.botUserID, slackTester.channel.ID, expectedMessage) + err = botDriver.WaitForLastMessageEqual(botDriver.BotUserID(), botDriver.Channel().ID(), expectedMessage) require.NoError(t, err) t.Log("Deleting ConfigMap") @@ -459,22 +503,20 @@ func TestSlack(t *testing.T) { cfgMapAlreadyDeleted = true t.Log("Expecting bot message on first channel...") - attachAssertionFn = func(title, color, msg string) bool { + attachAssertionFn = func(title, _, msg string) bool { return title == "v1/configmaps deleted" && - color == "a30200" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been deleted in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, slackTester.channel.ID, attachAssertionFn) + err = botDriver.WaitForMessagePostedWithAttachment(botDriver.BotUserID(), botDriver.Channel().ID(), attachAssertionFn) require.NoError(t, err) t.Log("Ensuring bot didn't write anything new on second channel...") time.Sleep(appCfg.Slack.MessageWaitTimeout) - attachAssertionFn = func(title, color, msg string) bool { + attachAssertionFn = func(title, _, msg string) bool { return title == "v1/configmaps updated" && - color == "daa038" && msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) } - err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, slackTester.secondChannel.ID, attachAssertionFn) + err = botDriver.WaitForMessagePostedWithAttachment(botDriver.BotUserID(), botDriver.SecondChannel().ID(), attachAssertionFn) require.NoError(t, err) }) @@ -484,7 +526,7 @@ func TestSlack(t *testing.T) { t.Log("Creating Pod...") pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Name: slackTester.channel.Name, + Name: botDriver.Channel().Name(), Namespace: appCfg.Deployment.Namespace, }, Spec: v1.PodSpec{ @@ -506,449 +548,10 @@ func TestSlack(t *testing.T) { strings.Contains(msg, fmt.Sprintf("- Pod '%s/%s' created without labels. Consider defining them, to be able to use them as a selector e.g. in Service.", pod.Namespace, pod.Name)) && strings.Contains(msg, fmt.Sprintf("- The 'latest' tag used in '%s' image of Pod '%s/%s' container '%s' should be avoided.", pod.Spec.Containers[0].Image, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)) } - err = slackTester.WaitForMessagePostedWithAttachment(slackTester.botUserID, slackTester.channel.ID, assertionFn) + err = botDriver.WaitForMessagePostedWithAttachment(botDriver.BotUserID(), botDriver.Channel().ID(), assertionFn) require.NoError(t, err) }) -} -func TestDiscord(t *testing.T) { - t.Log("Loading configuration...") - var appCfg Config - err := envconfig.Init(&appCfg) - require.NoError(t, err) - - t.Log("Creating Discord API client with provided token...") - discordTester, err := newDiscordTester(appCfg.Discord) - require.NoError(t, err) - - t.Log("Creating K8s client...") - k8sConfig, err := clientcmd.BuildConfigFromFlags("", appCfg.KubeconfigPath) - require.NoError(t, err) - k8sCli, err := kubernetes.NewForConfig(k8sConfig) - require.NoError(t, err) - - t.Log("Setting up test Discord setup...") - discordTester.InitUsers(t) - cleanUpFns := discordTester.InitChannels(t) - for _, fn := range cleanUpFns { - t.Cleanup(fn) - } - - channels := map[string]*discordgo.Channel{ - appCfg.Deployment.Envs.DefaultDiscordChannelIDName: discordTester.channel, - appCfg.Deployment.Envs.SecondaryDiscordChannelIDName: discordTester.secondChannel, - } - - for _, currentChannel := range channels { - discordTester.PostInitialMessage(t, currentChannel.ID) - discordTester.InviteBotToChannel(t, currentChannel.ID) - } - - t.Log("Patching Deployment with test env variables...") - deployNsCli := k8sCli.AppsV1().Deployments(appCfg.Deployment.Namespace) - revertDeployFn := setTestEnvsForDeploy(t, appCfg, deployNsCli, nil, channels) - t.Cleanup(func() { revertDeployFn(t) }) - - t.Log("Waiting for Deployment") - err = waitForDeploymentReady(deployNsCli, appCfg.Deployment.Name, appCfg.Deployment.WaitTimeout) - require.NoError(t, err) - - t.Log("Waiting for Bot message on channel from user") - err = discordTester.WaitForMessagePostedRecentlyEqual(discordTester.botUserID, discordTester.channel.ID, fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) - require.NoError(t, err) - - t.Log("Running actual test cases") - - t.Run("Ping", func(t *testing.T) { - command := "ping" - expectedMessage := fmt.Sprintf("pong from cluster '%s'", appCfg.ClusterName) - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err := discordTester.WaitForLastMessageContains(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Filters list", func(t *testing.T) { - command := "filters list" - expectedMessage := codeBlock(heredoc.Doc(` - FILTER ENABLED DESCRIPTION - NodeEventsChecker true Sends notifications on node level critical events. - ObjectAnnotationChecker true Checks if annotations botkube.io/* present in object specs and filters them.`)) - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err := discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Commands list", func(t *testing.T) { - command := "commands list" - expectedMessage := codeBlock(heredoc.Doc(` - Enabled executors: - kubectl: - kubectl-allow-all: - namespaces: - include: - - .* - enabled: true - commands: - verbs: - - get - resources: - - deployments - kubectl-read-only: - namespaces: - include: - - botkube - - default - enabled: true - commands: - verbs: - - api-resources - - api-versions - - cluster-info - - describe - - diff - - explain - - get - - logs - - top - - auth - resources: - - deployments - - pods - - namespaces - - daemonsets - - statefulsets - - storageclasses - - nodes - - configmaps - defaultNamespace: default - restrictAccess: false - kubectl-wait-cmd: - namespaces: - include: - - botkube - - default - enabled: true - commands: - verbs: - - wait - resources: [] - restrictAccess: false`)) - - t.Run("With default cluster", func(t *testing.T) { - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err := discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("With custom cluster name", func(t *testing.T) { - command := fmt.Sprintf("commands list --cluster-name %s", appCfg.ClusterName) - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("With unknown cluster name", func(t *testing.T) { - command := "commands list --cluster-name non-existing" - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - t.Log("Ensuring bot didn't write anything new...") - time.Sleep(appCfg.Discord.MessageWaitTimeout) - // Same expected message as before - err = discordTester.WaitForLastMessageContains(discordTester.testerUserID, discordTester.channel.ID, command) - assert.NoError(t, err) - }) - }) - - t.Run("Executor", func(t *testing.T) { - t.Run("Get Deployment", func(t *testing.T) { - command := fmt.Sprintf("get deploy -n %s %s", appCfg.Deployment.Namespace, appCfg.Deployment.Name) - assertionFn := func(msg string) bool { - return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg, "botkube") - } - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForMessagePosted(discordTester.botUserID, discordTester.channel.ID, 1, assertionFn) - assert.NoError(t, err) - }) - - t.Run("Get Configmap", func(t *testing.T) { - command := fmt.Sprintf("get configmap -n %s", appCfg.Deployment.Namespace) - assertionFn := func(msg string) bool { - return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg, "kube-root-ca.crt") && - strings.Contains(msg, "botkube-global-config") - } - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForMessagePosted(discordTester.botUserID, discordTester.channel.ID, 1, assertionFn) - assert.NoError(t, err) - }) - - t.Run("Get forbidden resource", func(t *testing.T) { - command := "get ingress" - expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'ingress' resources in the 'default' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Specify unknown command", func(t *testing.T) { - command := "unknown" - expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Specify invalid command", func(t *testing.T) { - command := "get" - expectedMessage := codeBlock(fmt.Sprintf("Cluster: %s\nYou must specify the type of resource to get. Use \"kubectl api-resources\" for a complete list of supported resources.\n\nerror: Required resource not specified.\nUse \"kubectl explain \" for a detailed description of that resource (e.g. kubectl explain pods).\nSee 'kubectl get -h' for help and examples\nexit status 1", appCfg.ClusterName)) - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Specify forbidden namespace", func(t *testing.T) { - command := "get po --namespace team-b" - expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'po' resources in the 'team-b' Namespace on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Based on other bindings", func(t *testing.T) { - t.Run("Wait for Deployment (the 2st binding)", func(t *testing.T) { - command := fmt.Sprintf("wait deployment -n %s %s --for condition=Available=True", appCfg.Deployment.Namespace, appCfg.Deployment.Name) - assertionFn := func(msg string) bool { - return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg, "deployment.apps/botkube condition met") - } - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForMessagePosted(discordTester.botUserID, discordTester.channel.ID, 1, assertionFn) - assert.NoError(t, err) - }) - - t.Run("Exec (the 3rd binding which is disabled)", func(t *testing.T) { - command := "exec" - expectedMessage := codeBlock("Command not supported. Please run /botkubehelp to see supported commands.") - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Get all Pods (the 4th binding)", func(t *testing.T) { - command := "get pods -A" - expectedMessage := codeBlock(fmt.Sprintf("Sorry, the kubectl command is not authorized to work with 'pods' resources for all Namespaces on cluster '%s'. Use 'commands list' to see allowed commands.", appCfg.ClusterName)) - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - assert.NoError(t, err) - }) - - t.Run("Get all Deployments (the 4th binding)", func(t *testing.T) { - command := "get deploy -A" - assertionFn := func(msg string) bool { - return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("Cluster: %s", appCfg.ClusterName))) && - strings.Contains(msg, "local-path-provisioner") && - strings.Contains(msg, "coredns") && - strings.Contains(msg, "botkube") - } - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForMessagePosted(discordTester.botUserID, discordTester.channel.ID, 1, assertionFn) - assert.NoError(t, err) - }) - }) - }) - - t.Run("Multi-channel notifications", func(t *testing.T) { - cfgMapCli := k8sCli.CoreV1().ConfigMaps(appCfg.Deployment.Namespace) - var channelIDs []string - for _, channel := range channels { - channelIDs = append(channelIDs, channel.ID) - } - - t.Log("Creating ConfigMap...") - var cfgMapAlreadyDeleted bool - cfgMap := &v1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: discordTester.channel.Name, - Namespace: appCfg.Deployment.Namespace, - }, - } - cfgMap, err = cfgMapCli.Create(context.Background(), cfgMap, metav1.CreateOptions{}) - require.NoError(t, err) - - t.Cleanup(func() { cleanupCreatedCfgMapIfShould(t, cfgMapCli, cfgMap.Name, &cfgMapAlreadyDeleted) }) - - t.Log("Expecting bot message in first channel...") - assertionFn := func(title, color, msg string) bool { - return title == "v1/configmaps created" && - color == "8311585" && - msg == fmt.Sprintf("ConfigMap *%s/%s* has been created in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) - } - err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, discordTester.channel.ID, assertionFn) - require.NoError(t, err) - - t.Log("Expecting no bot message in second channel...") - expectedMessage := fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName) - time.Sleep(appCfg.Discord.MessageWaitTimeout) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.secondChannel.ID, expectedMessage) - require.NoError(t, err) - - t.Log("Updating ConfigMap...") - cfgMap.Data = map[string]string{ - "operation": "update", - } - cfgMap, err = cfgMapCli.Update(context.Background(), cfgMap, metav1.UpdateOptions{}) - require.NoError(t, err) - - t.Log("Expecting bot message in all channels...") - assertionFn = func(title, color, msg string) bool { - return title == "v1/configmaps updated" && - color == "16312092" && - msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) - } - err = discordTester.WaitForMessagesPostedOnChannelsWithAttachment(discordTester.botUserID, channelIDs, assertionFn) - require.NoError(t, err) - - t.Log("Stopping notifier...") - command := "notifier stop" - expectedMessage = codeBlock(fmt.Sprintf("Sure! I won't send you notifications from cluster '%s' here.", appCfg.ClusterName)) - - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - assert.NoError(t, err) - - t.Log("Getting notifier status from second channel...") - command = "notifier status" - expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are enabled here.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, discordTester.secondChannel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.secondChannel.ID, expectedMessage) - assert.NoError(t, err) - - t.Log("Getting notifier status from first channel...") - command = "notifier status" - expectedMessage = codeBlock(fmt.Sprintf("Notifications from cluster '%s' are disabled here.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - assert.NoError(t, err) - - t.Log("Updating ConfigMap once again...") - cfgMap.Data = map[string]string{ - "operation": "update-second", - } - _, err = cfgMapCli.Update(context.Background(), cfgMap, metav1.UpdateOptions{}) - require.NoError(t, err) - - t.Log("Ensuring bot didn't write anything new on first channel...") - time.Sleep(appCfg.Slack.MessageWaitTimeout) - // Same expected message as before - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - require.NoError(t, err) - - t.Log("Expecting bot message on second channel...") - attachmentAssertionFn := func(title, color, msg string) bool { - return title == "v1/configmaps updated" && - color == "16312092" && - msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) - } - err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, discordTester.secondChannel.ID, attachmentAssertionFn) - require.NoError(t, err) - - t.Log("Starting notifier") - command = "notifier start" - expectedMessage = codeBlock(fmt.Sprintf("Brace yourselves, incoming notifications from cluster '%s'.", appCfg.ClusterName)) - discordTester.PostMessageToBot(t, discordTester.channel.ID, command) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - require.NoError(t, err) - - t.Log("Creating and deleting ignored ConfigMap") - ignoredCfgMap := &v1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-ignored", discordTester.channel.Name), - Namespace: appCfg.Deployment.Namespace, - Annotations: map[string]string{ - filters.DisableAnnotation: "true", - }, - }, - } - _, err = cfgMapCli.Create(context.Background(), ignoredCfgMap, metav1.CreateOptions{}) - require.NoError(t, err) - err = cfgMapCli.Delete(context.Background(), ignoredCfgMap.Name, metav1.DeleteOptions{}) - require.NoError(t, err) - - t.Log("Ensuring bot didn't write anything new...") - time.Sleep(appCfg.Slack.MessageWaitTimeout) - err = discordTester.WaitForLastMessageEqual(discordTester.botUserID, discordTester.channel.ID, expectedMessage) - require.NoError(t, err) - - t.Log("Deleting ConfigMap") - err = cfgMapCli.Delete(context.Background(), cfgMap.Name, metav1.DeleteOptions{}) - require.NoError(t, err) - cfgMapAlreadyDeleted = true - - t.Log("Expecting bot message on first channel...") - assertionFn = func(title, color, msg string) bool { - return title == "v1/configmaps deleted" && - color == "13632027" && - msg == fmt.Sprintf("ConfigMap *%s/%s* has been deleted in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) - } - err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, discordTester.channel.ID, assertionFn) - require.NoError(t, err) - - t.Log("Ensuring bot didn't write anything new on second channel...") - time.Sleep(appCfg.Slack.MessageWaitTimeout) - assertionFn = func(title, color, msg string) bool { - return title == "v1/configmaps updated" && - color == "16312092" && - msg == fmt.Sprintf("ConfigMap *%s/%s* has been updated in *%s* cluster", cfgMap.Namespace, cfgMap.Name, appCfg.ClusterName) - } - err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, discordTester.secondChannel.ID, assertionFn) - require.NoError(t, err) - }) - - t.Run("Recommendations", func(t *testing.T) { - podCli := k8sCli.CoreV1().Pods(appCfg.Deployment.Namespace) - - t.Log("Creating Pod...") - pod := &v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: discordTester.channel.Name, - Namespace: appCfg.Deployment.Namespace, - }, - Spec: v1.PodSpec{ - Containers: []v1.Container{ - {Name: "nginx", Image: "nginx:latest"}, - }, - }, - } - require.Len(t, pod.Spec.Containers, 1) - pod, err = podCli.Create(context.Background(), pod, metav1.CreateOptions{}) - require.NoError(t, err) - - t.Cleanup(func() { cleanupCreatedPod(t, podCli, pod.Name) }) - - t.Log("Expecting bot message...") - assertionFn := func(title, _, msg string) bool { - return title == "v1/pods created" && - strings.Contains(msg, "Recommendations:") && - strings.Contains(msg, fmt.Sprintf("- Pod '%s/%s' created without labels. Consider defining them, to be able to use them as a selector e.g. in Service.", pod.Namespace, pod.Name)) && - strings.Contains(msg, fmt.Sprintf("- The 'latest' tag used in '%s' image of Pod '%s/%s' container '%s' should be avoided.", pod.Spec.Containers[0].Image, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name)) - } - err = discordTester.WaitForMessagePostedWithAttachment(discordTester.botUserID, discordTester.channel.ID, assertionFn) - require.NoError(t, err) - }) } func codeBlock(in string) string { diff --git a/test/e2e/bots_tester_test.go b/test/e2e/bots_tester_test.go index 57984ef4f..dca592c94 100644 --- a/test/e2e/bots_tester_test.go +++ b/test/e2e/bots_tester_test.go @@ -36,13 +36,96 @@ var structDumper = litter.Options{ type MessageAssertion func(content string) bool type AttachmentAssertion func(title, color, msg string) bool +type Channel interface { + ID() string + Name() string + Identifier() string +} + +type SlackChannel struct { + *slack.Channel +} + +func (s *SlackChannel) ID() string { + return s.Channel.ID +} +func (s *SlackChannel) Name() string { + return s.Channel.Name +} +func (s *SlackChannel) Identifier() string { + return s.Channel.Name +} + +type DiscordChannel struct { + *discordgo.Channel +} + +func (s *DiscordChannel) ID() string { + return s.Channel.ID +} +func (s *DiscordChannel) Name() string { + return s.Channel.Name +} +func (s *DiscordChannel) Identifier() string { + return s.Channel.ID +} + +// BotType to instrument +type BotType string + +const ( + // CreateEvent when resource is created + SlackBot BotType = "slack" + // UpdateEvent when resource is updated + DiscordBot BotType = "discord" +) + +type BotDriver interface { + Type() BotType + InitUsers(t *testing.T) + InitChannels(t *testing.T) []func() + PostInitialMessage(t *testing.T, channel string) + PostMessageToBot(t *testing.T, channel, command string) + InviteBotToChannel(t *testing.T, channel string) + WaitForMessagePostedRecentlyEqual(userID, channelID, expectedMsg string) error + WaitForLastMessageContains(userID, channel, expectedMsgSubstring string) error + WaitForLastMessageEqual(userID, channel, expectedMsg string) error + WaitForMessagePosted(userID, channel string, limitMessages int, assertFn MessageAssertion) error + WaitForMessagePostedWithAttachment(userID, channel string, assertFn AttachmentAssertion) error + WaitForMessagesPostedOnChannelsWithAttachment(userID string, channelIDs []string, assertFn AttachmentAssertion) error + Channel() Channel + SecondChannel() Channel + BotUserID() string + TesterUserID() string +} + type slackTester struct { cli *slack.Client cfg SlackConfig botUserID string testerUserID string - channel *slack.Channel - secondChannel *slack.Channel + channel Channel + secondChannel Channel +} + +func (s *slackTester) Type() BotType { + return SlackBot +} + +func (s *slackTester) BotUserID() string { + return s.botUserID +} + +func (s *slackTester) TesterUserID() string { + return s.testerUserID +} + +func (s *slackTester) Channel() Channel { + return s.channel +} + +func (s *slackTester) SecondChannel() Channel { + return s.secondChannel } type discordTester struct { @@ -50,11 +133,31 @@ type discordTester struct { cfg DiscordConfig botUserID string testerUserID string - channel *discordgo.Channel - secondChannel *discordgo.Channel + channel Channel + secondChannel Channel +} + +func (d *discordTester) Type() BotType { + return DiscordBot +} + +func (d *discordTester) BotUserID() string { + return d.botUserID } -func newSlackTester(slackCfg SlackConfig) (*slackTester, error) { +func (d *discordTester) TesterUserID() string { + return d.testerUserID +} + +func (d *discordTester) Channel() Channel { + return d.channel +} + +func (d *discordTester) SecondChannel() Channel { + return d.secondChannel +} + +func newSlackTester(slackCfg SlackConfig) (BotDriver, error) { slackCli := slack.New(slackCfg.TesterAppToken) _, err := slackCli.AuthTest() if err != nil { @@ -66,16 +169,16 @@ func newSlackTester(slackCfg SlackConfig) (*slackTester, error) { func (s *slackTester) InitUsers(t *testing.T) { t.Helper() - s.botUserID = s.FindUserID(t, s.cfg.BotName) - s.testerUserID = s.FindUserID(t, s.cfg.TesterName) + s.botUserID = s.findUserID(t, s.cfg.BotName) + s.testerUserID = s.findUserID(t, s.cfg.TesterName) } func (s *slackTester) InitChannels(t *testing.T) []func() { channel, cleanupChannelFn := s.createChannel(t) - s.channel = channel + s.channel = &SlackChannel{Channel: channel} secondChannel, cleanupSecondChannelFn := s.createChannel(t) - s.secondChannel = secondChannel + s.secondChannel = &SlackChannel{Channel: secondChannel} return []func(){ func() { cleanupChannelFn(t) }, @@ -83,7 +186,7 @@ func (s *slackTester) InitChannels(t *testing.T) []func() { } } -func newDiscordTester(discordCfg DiscordConfig) (*discordTester, error) { +func newDiscordTester(discordCfg DiscordConfig) (BotDriver, error) { discordCli, err := discordgo.New("Bot " + discordCfg.TesterAppToken) if err != nil { return nil, fmt.Errorf("while creating Discord session: %w", err) @@ -93,16 +196,16 @@ func newDiscordTester(discordCfg DiscordConfig) (*discordTester, error) { func (d *discordTester) InitUsers(t *testing.T) { t.Helper() - d.botUserID = d.FindUserID(t, d.cfg.BotName) - d.testerUserID = d.FindUserID(t, d.cfg.TesterName) + d.botUserID = d.findUserID(t, d.cfg.BotName) + d.testerUserID = d.findUserID(t, d.cfg.TesterName) } func (d *discordTester) InitChannels(t *testing.T) []func() { channel, cleanupChannelFn := d.createChannel(t) - d.channel = channel + d.channel = &DiscordChannel{Channel: channel} secondChannel, cleanupSecondChannelFn := d.createChannel(t) - d.secondChannel = secondChannel + d.secondChannel = &DiscordChannel{Channel: secondChannel} return []func(){ func() { cleanupChannelFn(t) }, @@ -151,15 +254,7 @@ func (d *discordTester) PostMessageToBot(t *testing.T, channel, command string) require.NoError(t, err) } -func (d *discordTester) FindUserIDForBot(t *testing.T) string { - return d.FindUserID(t, d.cfg.BotName) -} - -func (d *discordTester) FindUserIDForTester(t *testing.T) string { - return d.FindUserID(t, d.cfg.TesterName) -} - -func (d *discordTester) FindUserID(t *testing.T, name string) string { +func (d *discordTester) findUserID(t *testing.T, name string) string { t.Log("Getting users...") res, err := d.cli.GuildMembersSearch(d.cfg.GuildID, name, 5) require.NoError(t, err) @@ -349,15 +444,7 @@ func (s *slackTester) PostMessageToBot(t *testing.T, channel, command string) { require.NoError(t, err) } -func (s *slackTester) FindUserIDForBot(t *testing.T) string { - return s.FindUserID(t, s.cfg.BotName) -} - -func (s *slackTester) FindUserIDForTester(t *testing.T) string { - return s.FindUserID(t, s.cfg.TesterName) -} - -func (s *slackTester) FindUserID(t *testing.T, name string) string { +func (s *slackTester) findUserID(t *testing.T, name string) string { t.Log("Getting users...") res, err := s.cli.GetUsers() require.NoError(t, err) diff --git a/test/e2e/k8s_helpers_test.go b/test/e2e/k8s_helpers_test.go index 8262a77b2..e7fa82d14 100644 --- a/test/e2e/k8s_helpers_test.go +++ b/test/e2e/k8s_helpers_test.go @@ -9,8 +9,6 @@ import ( "testing" "time" - "github.com/bwmarrin/discordgo" - "github.com/slack-go/slack" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" @@ -21,7 +19,7 @@ import ( deploymentutil "k8s.io/kubectl/pkg/util/deployment" ) -func setTestEnvsForDeploy(t *testing.T, appCfg Config, deployNsCli appsv1cli.DeploymentInterface, slackChannels map[string]*slack.Channel, discordChannels map[string]*discordgo.Channel) func(t *testing.T) { +func setTestEnvsForDeploy(t *testing.T, appCfg Config, deployNsCli appsv1cli.DeploymentInterface, botType BotType, channels map[string]Channel) func(t *testing.T) { t.Helper() deployment, err := deployNsCli.Get(context.Background(), appCfg.Deployment.Name, metav1.GetOptions{}) @@ -53,21 +51,21 @@ func setTestEnvsForDeploy(t *testing.T, appCfg Config, deployNsCli appsv1cli.Dep var newEnvs []v1.EnvVar - if len(slackChannels) > 0 { + if len(channels) > 0 && botType == SlackBot { slackEnabledEnvName := appCfg.Deployment.Envs.SlackEnabledName newEnvs = append(newEnvs, v1.EnvVar{Name: slackEnabledEnvName, Value: strconv.FormatBool(true)}) - for envName, slackChannel := range slackChannels { - newEnvs = append(newEnvs, v1.EnvVar{Name: envName, Value: slackChannel.Name}) + for envName, channel := range channels { + newEnvs = append(newEnvs, v1.EnvVar{Name: envName, Value: channel.Identifier()}) } } - if len(discordChannels) > 0 { + if len(channels) > 0 && botType == DiscordBot { discordEnabledEnvName := appCfg.Deployment.Envs.DiscordEnabledName newEnvs = append(newEnvs, v1.EnvVar{Name: discordEnabledEnvName, Value: strconv.FormatBool(true)}) - for envName, discordChannel := range discordChannels { - newEnvs = append(newEnvs, v1.EnvVar{Name: envName, Value: discordChannel.ID}) + for envName, channels := range channels { + newEnvs = append(newEnvs, v1.EnvVar{Name: envName, Value: channels.Identifier()}) } } From 37efaac33dc39dc89dd66c6afa21eed6a1b20878 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Thu, 8 Sep 2022 11:50:04 +0100 Subject: [PATCH 06/20] E2E test rework is complete. --- Makefile | 9 +- test/e2e/bots_test.go | 8 +- test/e2e/bots_tester_test.go | 536 +------------------------------- test/e2e/discord_driver_test.go | 273 ++++++++++++++++ test/e2e/k8s_helpers_test.go | 6 +- test/e2e/slack_driver_test.go | 271 ++++++++++++++++ 6 files changed, 562 insertions(+), 541 deletions(-) create mode 100644 test/e2e/discord_driver_test.go create mode 100644 test/e2e/slack_driver_test.go diff --git a/Makefile b/Makefile index 17f7ea9ff..46356dd98 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ TAG=$(shell cut -d'=' -f2- .release) .DEFAULT_GOAL := build -.PHONY: release git-tag check-git-status container-image test test-integration build pre-build publish lint lint-fix go-import-fmt system-check save-images load-and-push-images +.PHONY: release git-tag check-git-status container-image test test-integration-slack test-integration-discord build pre-build publish lint lint-fix go-import-fmt system-check save-images load-and-push-images # Show this help. help: @@ -40,8 +40,11 @@ go-import-fmt: test: system-check @go test -v -race ./... -test-integration: system-check - @go test -v -tags=integration -race -count=1 ./test/... +test-integration-slack: system-check + @go test -v -tags=integration -race -count=1 ./test/... -run "TestSlack" + +test-integration-discord: system-check + @go test -v -tags=integration -race -count=1 ./test/... -run "TestDiscord" # Build the binary build: pre-build diff --git a/test/e2e/bots_test.go b/test/e2e/bots_test.go index 61eb7f28e..c120e029a 100644 --- a/test/e2e/bots_test.go +++ b/test/e2e/bots_test.go @@ -107,19 +107,19 @@ func TestDiscord(t *testing.T) { ) } -func newBotDriver(cfg Config, driverType BotType) (BotDriver, error) { +func newBotDriver(cfg Config, driverType DriverType) (BotDriver, error) { switch driverType { case SlackBot: - return newSlackTester(cfg.Slack) + return newSlackDriver(cfg.Slack) case DiscordBot: - return newDiscordTester(cfg.Discord) + return newDiscordDriver(cfg.Discord) } return nil, nil } func runBotTest(t *testing.T, appCfg Config, - driverType BotType, + driverType DriverType, annotation, invalidCmdTemplate, deployEnvChannelIDName, diff --git a/test/e2e/bots_tester_test.go b/test/e2e/bots_tester_test.go index dca592c94..966c7b08f 100644 --- a/test/e2e/bots_tester_test.go +++ b/test/e2e/bots_tester_test.go @@ -3,23 +3,10 @@ package e2e import ( - "errors" - "fmt" "regexp" - "strconv" - "strings" "testing" - "github.com/bwmarrin/discordgo" - "github.com/google/uuid" "github.com/sanity-io/litter" - "github.com/slack-go/slack" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/util/wait" - - "github.com/kubeshop/botkube/pkg/multierror" - "github.com/kubeshop/botkube/pkg/utils" ) const recentMessagesLimit = 5 @@ -42,46 +29,18 @@ type Channel interface { Identifier() string } -type SlackChannel struct { - *slack.Channel -} - -func (s *SlackChannel) ID() string { - return s.Channel.ID -} -func (s *SlackChannel) Name() string { - return s.Channel.Name -} -func (s *SlackChannel) Identifier() string { - return s.Channel.Name -} - -type DiscordChannel struct { - *discordgo.Channel -} - -func (s *DiscordChannel) ID() string { - return s.Channel.ID -} -func (s *DiscordChannel) Name() string { - return s.Channel.Name -} -func (s *DiscordChannel) Identifier() string { - return s.Channel.ID -} - -// BotType to instrument -type BotType string +// DriverType to instrument +type DriverType string const ( // CreateEvent when resource is created - SlackBot BotType = "slack" + SlackBot DriverType = "slack" // UpdateEvent when resource is updated - DiscordBot BotType = "discord" + DiscordBot DriverType = "discord" ) type BotDriver interface { - Type() BotType + Type() DriverType InitUsers(t *testing.T) InitChannels(t *testing.T) []func() PostInitialMessage(t *testing.T, channel string) @@ -98,488 +57,3 @@ type BotDriver interface { BotUserID() string TesterUserID() string } - -type slackTester struct { - cli *slack.Client - cfg SlackConfig - botUserID string - testerUserID string - channel Channel - secondChannel Channel -} - -func (s *slackTester) Type() BotType { - return SlackBot -} - -func (s *slackTester) BotUserID() string { - return s.botUserID -} - -func (s *slackTester) TesterUserID() string { - return s.testerUserID -} - -func (s *slackTester) Channel() Channel { - return s.channel -} - -func (s *slackTester) SecondChannel() Channel { - return s.secondChannel -} - -type discordTester struct { - cli *discordgo.Session - cfg DiscordConfig - botUserID string - testerUserID string - channel Channel - secondChannel Channel -} - -func (d *discordTester) Type() BotType { - return DiscordBot -} - -func (d *discordTester) BotUserID() string { - return d.botUserID -} - -func (d *discordTester) TesterUserID() string { - return d.testerUserID -} - -func (d *discordTester) Channel() Channel { - return d.channel -} - -func (d *discordTester) SecondChannel() Channel { - return d.secondChannel -} - -func newSlackTester(slackCfg SlackConfig) (BotDriver, error) { - slackCli := slack.New(slackCfg.TesterAppToken) - _, err := slackCli.AuthTest() - if err != nil { - return nil, err - } - - return &slackTester{cli: slackCli, cfg: slackCfg}, nil -} - -func (s *slackTester) InitUsers(t *testing.T) { - t.Helper() - s.botUserID = s.findUserID(t, s.cfg.BotName) - s.testerUserID = s.findUserID(t, s.cfg.TesterName) -} - -func (s *slackTester) InitChannels(t *testing.T) []func() { - channel, cleanupChannelFn := s.createChannel(t) - s.channel = &SlackChannel{Channel: channel} - - secondChannel, cleanupSecondChannelFn := s.createChannel(t) - s.secondChannel = &SlackChannel{Channel: secondChannel} - - return []func(){ - func() { cleanupChannelFn(t) }, - func() { cleanupSecondChannelFn(t) }, - } -} - -func newDiscordTester(discordCfg DiscordConfig) (BotDriver, error) { - discordCli, err := discordgo.New("Bot " + discordCfg.TesterAppToken) - if err != nil { - return nil, fmt.Errorf("while creating Discord session: %w", err) - } - return &discordTester{cli: discordCli, cfg: discordCfg}, nil -} - -func (d *discordTester) InitUsers(t *testing.T) { - t.Helper() - d.botUserID = d.findUserID(t, d.cfg.BotName) - d.testerUserID = d.findUserID(t, d.cfg.TesterName) -} - -func (d *discordTester) InitChannels(t *testing.T) []func() { - channel, cleanupChannelFn := d.createChannel(t) - d.channel = &DiscordChannel{Channel: channel} - - secondChannel, cleanupSecondChannelFn := d.createChannel(t) - d.secondChannel = &DiscordChannel{Channel: secondChannel} - - return []func(){ - func() { cleanupChannelFn(t) }, - func() { cleanupSecondChannelFn(t) }, - } -} - -func (d *discordTester) createChannel(t *testing.T) (*discordgo.Channel, func(t *testing.T)) { - t.Helper() - randomID := uuid.New() - channelName := fmt.Sprintf("%s-%s", channelNamePrefix, randomID.String()) - - t.Logf("Creating channel %q...", channelName) - channel, err := d.cli.GuildChannelCreate(d.cfg.GuildID, channelName, discordgo.ChannelTypeGuildText) - require.NoError(t, err) - - t.Logf("Channel %q (ID: %q) created", channelName, channel.ID) - - cleanupFn := func(t *testing.T) { - t.Helper() - t.Logf("Deleting channel %q...", channel.Name) - // We cannot archive a channel: https://support.discord.com/hc/en-us/community/posts/360042842012-Archive-old-chat-channels - _, err := d.cli.ChannelDelete(channel.ID) - assert.NoError(t, err) - } - - return channel, cleanupFn -} - -func (d *discordTester) PostInitialMessage(t *testing.T, channelID string) { - t.Helper() - t.Logf("Posting welcome message for channel: %s...", channelID) - - var additionalContextMsg string - if d.cfg.AdditionalContextMessage != "" { - additionalContextMsg = fmt.Sprintf("%s\n", d.cfg.AdditionalContextMessage) - } - message := fmt.Sprintf("Hello!\n%s%s", additionalContextMsg, welcomeText) - _, err := d.cli.ChannelMessageSend(channelID, message) - require.NoError(t, err) -} - -func (d *discordTester) PostMessageToBot(t *testing.T, channel, command string) { - message := fmt.Sprintf("<@%s> %s", d.botUserID, command) - _, err := d.cli.ChannelMessageSend(channel, message) - require.NoError(t, err) -} - -func (d *discordTester) findUserID(t *testing.T, name string) string { - t.Log("Getting users...") - res, err := d.cli.GuildMembersSearch(d.cfg.GuildID, name, 5) - require.NoError(t, err) - - t.Logf("Finding user ID by name %q...", name) - for _, m := range res { - if !strings.EqualFold(name, m.User.Username) { - continue - } - return m.User.ID - } - - return "" -} - -func (d *discordTester) InviteBotToChannel(_ *testing.T, _ string) { - // This is not required in Discord. - // Bots can't "join" text channels because when you join a server you're already in every text channel. - // See: https://stackoverflow.com/questions/60990748/making-discord-bot-join-leave-a-channel -} - -func (d *discordTester) WaitForMessagePostedRecentlyEqual(userID, channelID, expectedMsg string) error { - return d.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg string) bool { - return strings.EqualFold(msg, expectedMsg) - }) -} - -func (d *discordTester) WaitForLastMessageContains(userID, channelID, expectedMsgSubstring string) error { - return d.WaitForMessagePosted(userID, channelID, 1, func(msg string) bool { - return strings.Contains(msg, expectedMsgSubstring) - }) -} - -func (d *discordTester) WaitForLastMessageEqual(userID, channelID, expectedMsg string) error { - return d.WaitForMessagePosted(userID, channelID, 1, func(msg string) bool { - return msg == expectedMsg - }) -} - -func (d *discordTester) WaitForMessagePosted(userID, channelID string, limitMessages int, assertFn MessageAssertion) error { - // To always receive message content: - // ensure you enable the MESSAGE CONTENT INTENT for the tester bot on the developer portal. - // Applications ↦ Settings ↦ Bot ↦ Privileged Gateway Intents - // This setting has been enforced from August 31, 2022 - - var fetchedMessages []*discordgo.Message - var lastErr error - - err := wait.Poll(pollInterval, d.cfg.MessageWaitTimeout, func() (done bool, err error) { - messages, err := d.cli.ChannelMessages(channelID, limitMessages, "", "", "") - if err != nil { - lastErr = err - return false, nil - } - - fetchedMessages = messages - for _, msg := range messages { - if msg.Author.ID != userID { - continue - } - - if !assertFn(msg.Content) { - // different message - continue - } - - return true, nil - } - - return false, nil - }) - if lastErr == nil { - lastErr = errors.New("message assertion function returned false") - } - if err != nil { - if err == wait.ErrWaitTimeout { - return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, structDumper.Sdump(fetchedMessages)) - } - return err - } - - return nil -} - -func (d *discordTester) WaitForMessagePostedWithAttachment(userID, channelID string, assertFn AttachmentAssertion) error { - // To always receive message content: - // ensure you enable the MESSAGE CONTENT INTENT for the tester bot on the developer portal. - // Applications ↦ Settings ↦ Bot ↦ Privileged Gateway Intents - // This setting has been enforced from August 31, 2022 - - var fetchedMessages []*discordgo.Message - var lastErr error - - err := wait.Poll(pollInterval, d.cfg.MessageWaitTimeout, func() (done bool, err error) { - messages, err := d.cli.ChannelMessages(channelID, 1, "", "", "") - if err != nil { - lastErr = err - return false, nil - } - - fetchedMessages = messages - for _, msg := range messages { - if msg.Author.ID != userID { - continue - } - - if len(msg.Embeds) != 1 { - lastErr = err - return false, nil - } - - embed := msg.Embeds[0] - - if !assertFn(embed.Title, strconv.Itoa(embed.Color), embed.Description) { - // different message - continue - } - - return true, nil - } - - return false, nil - }) - if lastErr == nil { - lastErr = errors.New("message assertion function returned false") - } - if err != nil { - if err == wait.ErrWaitTimeout { - return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, structDumper.Sdump(fetchedMessages)) - } - return err - } - - return nil -} - -func (d *discordTester) WaitForMessagesPostedOnChannelsWithAttachment(userID string, channelIDs []string, assertFn AttachmentAssertion) error { - errs := multierror.New() - for _, channelID := range channelIDs { - errs = multierror.Append(errs, d.WaitForMessagePostedWithAttachment(userID, channelID, assertFn)) - } - - return errs.ErrorOrNil() -} - -func (s *slackTester) createChannel(t *testing.T) (*slack.Channel, func(t *testing.T)) { - t.Helper() - randomID := uuid.New() - channelName := fmt.Sprintf("%s-%s", channelNamePrefix, randomID.String()) - - t.Logf("Creating channel %q...", channelName) - // > There’s no limit to how many unique channels you can have in Slack — go ahead, create as many as you’d like! - // Sure, thanks Slack! - // Source: https://slack.com/help/articles/201402297-Create-a-channel - channel, err := s.cli.CreateConversation(channelName, false) - require.NoError(t, err) - - t.Logf("Channel %q (ID: %q) created", channelName, channel.ID) - - cleanupFn := func(t *testing.T) { - t.Helper() - t.Logf("Archiving channel %q...", channel.Name) - // We cannot delete channel: https://stackoverflow.com/questions/46807744/delete-channel-in-slack-api - err = s.cli.ArchiveConversation(channel.ID) - assert.NoError(t, err) - } - - return channel, cleanupFn -} - -func (s *slackTester) PostInitialMessage(t *testing.T, channelName string) { - t.Helper() - t.Log("Posting welcome message...") - - var additionalContextMsg string - if s.cfg.AdditionalContextMessage != "" { - additionalContextMsg = fmt.Sprintf("%s\n", s.cfg.AdditionalContextMessage) - } - message := fmt.Sprintf("Hello!\n%s%s", additionalContextMsg, welcomeText) - _, _, err := s.cli.PostMessage(channelName, slack.MsgOptionText(message, false)) - require.NoError(t, err) -} - -func (s *slackTester) PostMessageToBot(t *testing.T, channel, command string) { - message := fmt.Sprintf("<@%s> %s", s.cfg.BotName, command) - _, _, err := s.cli.PostMessage(channel, slack.MsgOptionText(message, false)) - require.NoError(t, err) -} - -func (s *slackTester) findUserID(t *testing.T, name string) string { - t.Log("Getting users...") - res, err := s.cli.GetUsers() - require.NoError(t, err) - - t.Logf("Finding user ID by name %q...", name) - for _, u := range res { - if u.Name != name { - continue - } - return u.ID - } - - return "" -} - -func (s *slackTester) InviteBotToChannel(t *testing.T, channelID string) { - t.Logf("Inviting bot with ID %q to the channel with ID %q", s.botUserID, channelID) - _, err := s.cli.InviteUsersToConversation(channelID, s.botUserID) - require.NoError(t, err) -} - -func (s *slackTester) WaitForMessagePostedRecentlyEqual(userID, channelID, expectedMsg string) error { - return s.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg string) bool { - return strings.EqualFold(msg, expectedMsg) - }) -} - -func (s *slackTester) WaitForLastMessageContains(userID, channelID, expectedMsgSubstring string) error { - return s.WaitForMessagePosted(userID, channelID, 1, func(msg string) bool { - return strings.Contains(msg, expectedMsgSubstring) - }) -} - -func (s *slackTester) WaitForLastMessageEqual(userID, channelID, expectedMsg string) error { - return s.WaitForMessagePosted(userID, channelID, 1, func(msg string) bool { - return msg == expectedMsg - }) -} - -func (s *slackTester) WaitForMessagePosted(userID, channelID string, limitMessages int, assertFn MessageAssertion) error { - var fetchedMessages []slack.Message - var lastErr error - err := wait.Poll(pollInterval, s.cfg.MessageWaitTimeout, func() (done bool, err error) { - historyRes, err := s.cli.GetConversationHistory(&slack.GetConversationHistoryParameters{ - ChannelID: channelID, Limit: limitMessages, - }) - if err != nil { - lastErr = err - return false, nil - } - - fetchedMessages = historyRes.Messages - for _, msg := range historyRes.Messages { - if msg.User != userID { - continue - } - - if !assertFn(msg.Text) { - // different message - continue - } - - return true, nil - } - - return false, nil - }) - if lastErr == nil { - lastErr = errors.New("message assertion function returned false") - } - if err != nil { - if err == wait.ErrWaitTimeout { - return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, utils.StructDumper().Sdump(fetchedMessages)) - } - return err - } - - return nil -} - -func (s *slackTester) WaitForMessagePostedWithAttachment(userID, channelID string, assertFn AttachmentAssertion) error { - var fetchedMessages []slack.Message - var lastErr error - err := wait.Poll(pollInterval, s.cfg.MessageWaitTimeout, func() (done bool, err error) { - historyRes, err := s.cli.GetConversationHistory(&slack.GetConversationHistoryParameters{ - ChannelID: channelID, Limit: 1, - }) - if err != nil { - lastErr = err - return false, nil - } - - fetchedMessages = historyRes.Messages - for _, msg := range historyRes.Messages { - if msg.User != userID { - continue - } - - if len(msg.Attachments) != 1 { - return false, nil - } - - attachment := msg.Attachments[0] - if len(attachment.Fields) != 1 { - return false, nil - } - - if !assertFn(attachment.Title, attachment.Color, attachment.Fields[0].Value) { - // different message - return false, nil - } - - return true, nil - } - - return false, nil - }) - if lastErr == nil { - lastErr = errors.New("message assertion function returned false") - } - if err != nil { - if err == wait.ErrWaitTimeout { - return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, structDumper.Sdump(fetchedMessages)) - } - return err - } - - return nil -} - -func (s *slackTester) WaitForMessagesPostedOnChannelsWithAttachment(userID string, channelIDs []string, assertFn AttachmentAssertion) error { - errs := multierror.New() - for _, channelID := range channelIDs { - errs = multierror.Append(errs, s.WaitForMessagePostedWithAttachment(userID, channelID, assertFn)) - } - - return errs.ErrorOrNil() -} diff --git a/test/e2e/discord_driver_test.go b/test/e2e/discord_driver_test.go new file mode 100644 index 000000000..5f051d08a --- /dev/null +++ b/test/e2e/discord_driver_test.go @@ -0,0 +1,273 @@ +package e2e + +import ( + "errors" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/bwmarrin/discordgo" + "github.com/google/uuid" + "github.com/kubeshop/botkube/pkg/multierror" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/wait" +) + +type DiscordChannel struct { + *discordgo.Channel +} + +func (s *DiscordChannel) ID() string { + return s.Channel.ID +} +func (s *DiscordChannel) Name() string { + return s.Channel.Name +} +func (s *DiscordChannel) Identifier() string { + return s.Channel.ID +} + +type discordTester struct { + cli *discordgo.Session + cfg DiscordConfig + botUserID string + testerUserID string + channel Channel + secondChannel Channel +} + +func newDiscordDriver(discordCfg DiscordConfig) (BotDriver, error) { + discordCli, err := discordgo.New("Bot " + discordCfg.TesterAppToken) + if err != nil { + return nil, fmt.Errorf("while creating Discord session: %w", err) + } + return &discordTester{cli: discordCli, cfg: discordCfg}, nil +} + +func (d *discordTester) Type() DriverType { + return DiscordBot +} + +func (d *discordTester) BotUserID() string { + return d.botUserID +} + +func (d *discordTester) TesterUserID() string { + return d.testerUserID +} + +func (d *discordTester) Channel() Channel { + return d.channel +} + +func (d *discordTester) SecondChannel() Channel { + return d.secondChannel +} + +func (d *discordTester) InitUsers(t *testing.T) { + t.Helper() + d.botUserID = d.findUserID(t, d.cfg.BotName) + d.testerUserID = d.findUserID(t, d.cfg.TesterName) +} + +func (d *discordTester) InitChannels(t *testing.T) []func() { + channel, cleanupChannelFn := d.createChannel(t) + d.channel = &DiscordChannel{Channel: channel} + + secondChannel, cleanupSecondChannelFn := d.createChannel(t) + d.secondChannel = &DiscordChannel{Channel: secondChannel} + + return []func(){ + func() { cleanupChannelFn(t) }, + func() { cleanupSecondChannelFn(t) }, + } +} + +func (d *discordTester) PostInitialMessage(t *testing.T, channelID string) { + t.Helper() + t.Logf("Posting welcome message for channel: %s...", channelID) + + var additionalContextMsg string + if d.cfg.AdditionalContextMessage != "" { + additionalContextMsg = fmt.Sprintf("%s\n", d.cfg.AdditionalContextMessage) + } + message := fmt.Sprintf("Hello!\n%s%s", additionalContextMsg, welcomeText) + _, err := d.cli.ChannelMessageSend(channelID, message) + require.NoError(t, err) +} + +func (d *discordTester) PostMessageToBot(t *testing.T, channel, command string) { + message := fmt.Sprintf("<@%s> %s", d.botUserID, command) + _, err := d.cli.ChannelMessageSend(channel, message) + require.NoError(t, err) +} + +func (d *discordTester) InviteBotToChannel(_ *testing.T, _ string) { + // This is not required in Discord. + // Bots can't "join" text channels because when you join a server you're already in every text channel. + // See: https://stackoverflow.com/questions/60990748/making-discord-bot-join-leave-a-channel +} + +func (d *discordTester) WaitForMessagePostedRecentlyEqual(userID, channelID, expectedMsg string) error { + return d.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg string) bool { + return strings.EqualFold(msg, expectedMsg) + }) +} + +func (d *discordTester) WaitForLastMessageContains(userID, channelID, expectedMsgSubstring string) error { + return d.WaitForMessagePosted(userID, channelID, 1, func(msg string) bool { + return strings.Contains(msg, expectedMsgSubstring) + }) +} + +func (d *discordTester) WaitForLastMessageEqual(userID, channelID, expectedMsg string) error { + return d.WaitForMessagePosted(userID, channelID, 1, func(msg string) bool { + return msg == expectedMsg + }) +} + +func (d *discordTester) WaitForMessagePosted(userID, channelID string, limitMessages int, assertFn MessageAssertion) error { + // To always receive message content: + // ensure you enable the MESSAGE CONTENT INTENT for the tester bot on the developer portal. + // Applications ↦ Settings ↦ Bot ↦ Privileged Gateway Intents + // This setting has been enforced from August 31, 2022 + + var fetchedMessages []*discordgo.Message + var lastErr error + + err := wait.Poll(pollInterval, d.cfg.MessageWaitTimeout, func() (done bool, err error) { + messages, err := d.cli.ChannelMessages(channelID, limitMessages, "", "", "") + if err != nil { + lastErr = err + return false, nil + } + + fetchedMessages = messages + for _, msg := range messages { + if msg.Author.ID != userID { + continue + } + + if !assertFn(msg.Content) { + // different message + continue + } + + return true, nil + } + + return false, nil + }) + if lastErr == nil { + lastErr = errors.New("message assertion function returned false") + } + if err != nil { + if err == wait.ErrWaitTimeout { + return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, structDumper.Sdump(fetchedMessages)) + } + return err + } + + return nil +} + +func (d *discordTester) WaitForMessagePostedWithAttachment(userID, channelID string, assertFn AttachmentAssertion) error { + // To always receive message content: + // ensure you enable the MESSAGE CONTENT INTENT for the tester bot on the developer portal. + // Applications ↦ Settings ↦ Bot ↦ Privileged Gateway Intents + // This setting has been enforced from August 31, 2022 + + var fetchedMessages []*discordgo.Message + var lastErr error + + err := wait.Poll(pollInterval, d.cfg.MessageWaitTimeout, func() (done bool, err error) { + messages, err := d.cli.ChannelMessages(channelID, 1, "", "", "") + if err != nil { + lastErr = err + return false, nil + } + + fetchedMessages = messages + for _, msg := range messages { + if msg.Author.ID != userID { + continue + } + + if len(msg.Embeds) != 1 { + lastErr = err + return false, nil + } + + embed := msg.Embeds[0] + + if !assertFn(embed.Title, strconv.Itoa(embed.Color), embed.Description) { + // different message + continue + } + + return true, nil + } + + return false, nil + }) + if lastErr == nil { + lastErr = errors.New("message assertion function returned false") + } + if err != nil { + if err == wait.ErrWaitTimeout { + return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, structDumper.Sdump(fetchedMessages)) + } + return err + } + + return nil +} + +func (d *discordTester) WaitForMessagesPostedOnChannelsWithAttachment(userID string, channelIDs []string, assertFn AttachmentAssertion) error { + errs := multierror.New() + for _, channelID := range channelIDs { + errs = multierror.Append(errs, d.WaitForMessagePostedWithAttachment(userID, channelID, assertFn)) + } + + return errs.ErrorOrNil() +} + +func (d *discordTester) findUserID(t *testing.T, name string) string { + t.Log("Getting users...") + res, err := d.cli.GuildMembersSearch(d.cfg.GuildID, name, 5) + require.NoError(t, err) + + t.Logf("Finding user ID by name %q...", name) + for _, m := range res { + if !strings.EqualFold(name, m.User.Username) { + continue + } + return m.User.ID + } + + return "" +} + +func (d *discordTester) createChannel(t *testing.T) (*discordgo.Channel, func(t *testing.T)) { + t.Helper() + randomID := uuid.New() + channelName := fmt.Sprintf("%s-%s", channelNamePrefix, randomID.String()) + + t.Logf("Creating channel %q...", channelName) + channel, err := d.cli.GuildChannelCreate(d.cfg.GuildID, channelName, discordgo.ChannelTypeGuildText) + require.NoError(t, err) + + t.Logf("Channel %q (ID: %q) created", channelName, channel.ID) + + cleanupFn := func(t *testing.T) { + t.Helper() + t.Logf("Deleting channel %q...", channel.Name) + // We cannot archive a channel: https://support.discord.com/hc/en-us/community/posts/360042842012-Archive-old-chat-channels + _, err := d.cli.ChannelDelete(channel.ID) + assert.NoError(t, err) + } + + return channel, cleanupFn +} diff --git a/test/e2e/k8s_helpers_test.go b/test/e2e/k8s_helpers_test.go index e7fa82d14..1f2191e0c 100644 --- a/test/e2e/k8s_helpers_test.go +++ b/test/e2e/k8s_helpers_test.go @@ -19,7 +19,7 @@ import ( deploymentutil "k8s.io/kubectl/pkg/util/deployment" ) -func setTestEnvsForDeploy(t *testing.T, appCfg Config, deployNsCli appsv1cli.DeploymentInterface, botType BotType, channels map[string]Channel) func(t *testing.T) { +func setTestEnvsForDeploy(t *testing.T, appCfg Config, deployNsCli appsv1cli.DeploymentInterface, driverType DriverType, channels map[string]Channel) func(t *testing.T) { t.Helper() deployment, err := deployNsCli.Get(context.Background(), appCfg.Deployment.Name, metav1.GetOptions{}) @@ -51,7 +51,7 @@ func setTestEnvsForDeploy(t *testing.T, appCfg Config, deployNsCli appsv1cli.Dep var newEnvs []v1.EnvVar - if len(channels) > 0 && botType == SlackBot { + if len(channels) > 0 && driverType == SlackBot { slackEnabledEnvName := appCfg.Deployment.Envs.SlackEnabledName newEnvs = append(newEnvs, v1.EnvVar{Name: slackEnabledEnvName, Value: strconv.FormatBool(true)}) @@ -60,7 +60,7 @@ func setTestEnvsForDeploy(t *testing.T, appCfg Config, deployNsCli appsv1cli.Dep } } - if len(channels) > 0 && botType == DiscordBot { + if len(channels) > 0 && driverType == DiscordBot { discordEnabledEnvName := appCfg.Deployment.Envs.DiscordEnabledName newEnvs = append(newEnvs, v1.EnvVar{Name: discordEnabledEnvName, Value: strconv.FormatBool(true)}) diff --git a/test/e2e/slack_driver_test.go b/test/e2e/slack_driver_test.go new file mode 100644 index 000000000..041ef947d --- /dev/null +++ b/test/e2e/slack_driver_test.go @@ -0,0 +1,271 @@ +package e2e + +import ( + "errors" + "fmt" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/kubeshop/botkube/pkg/multierror" + "github.com/slack-go/slack" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/wait" +) + +type slackChannel struct { + *slack.Channel +} + +func (s *slackChannel) ID() string { + return s.Channel.ID +} +func (s *slackChannel) Name() string { + return s.Channel.Name +} +func (s *slackChannel) Identifier() string { + return s.Channel.Name +} + +type slackTester struct { + cli *slack.Client + cfg SlackConfig + botUserID string + testerUserID string + channel Channel + secondChannel Channel +} + +func newSlackDriver(slackCfg SlackConfig) (BotDriver, error) { + slackCli := slack.New(slackCfg.TesterAppToken) + _, err := slackCli.AuthTest() + if err != nil { + return nil, err + } + + return &slackTester{cli: slackCli, cfg: slackCfg}, nil +} + +func (s *slackTester) InitUsers(t *testing.T) { + t.Helper() + s.botUserID = s.findUserID(t, s.cfg.BotName) + s.testerUserID = s.findUserID(t, s.cfg.TesterName) +} + +func (s *slackTester) InitChannels(t *testing.T) []func() { + channel, cleanupChannelFn := s.createChannel(t) + s.channel = &slackChannel{Channel: channel} + + secondChannel, cleanupSecondChannelFn := s.createChannel(t) + s.secondChannel = &slackChannel{Channel: secondChannel} + + return []func(){ + func() { cleanupChannelFn(t) }, + func() { cleanupSecondChannelFn(t) }, + } +} + +func (s *slackTester) Type() DriverType { + return SlackBot +} + +func (s *slackTester) BotUserID() string { + return s.botUserID +} + +func (s *slackTester) TesterUserID() string { + return s.testerUserID +} + +func (s *slackTester) Channel() Channel { + return s.channel +} + +func (s *slackTester) SecondChannel() Channel { + return s.secondChannel +} + +func (s *slackTester) PostInitialMessage(t *testing.T, channelName string) { + t.Helper() + t.Log("Posting welcome message...") + + var additionalContextMsg string + if s.cfg.AdditionalContextMessage != "" { + additionalContextMsg = fmt.Sprintf("%s\n", s.cfg.AdditionalContextMessage) + } + message := fmt.Sprintf("Hello!\n%s%s", additionalContextMsg, welcomeText) + _, _, err := s.cli.PostMessage(channelName, slack.MsgOptionText(message, false)) + require.NoError(t, err) +} + +func (s *slackTester) PostMessageToBot(t *testing.T, channel, command string) { + message := fmt.Sprintf("<@%s> %s", s.cfg.BotName, command) + _, _, err := s.cli.PostMessage(channel, slack.MsgOptionText(message, false)) + require.NoError(t, err) +} + +func (s *slackTester) InviteBotToChannel(t *testing.T, channelID string) { + t.Logf("Inviting bot with ID %q to the channel with ID %q", s.botUserID, channelID) + _, err := s.cli.InviteUsersToConversation(channelID, s.botUserID) + require.NoError(t, err) +} + +func (s *slackTester) WaitForMessagePostedRecentlyEqual(userID, channelID, expectedMsg string) error { + return s.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg string) bool { + return strings.EqualFold(msg, expectedMsg) + }) +} + +func (s *slackTester) WaitForLastMessageContains(userID, channelID, expectedMsgSubstring string) error { + return s.WaitForMessagePosted(userID, channelID, 1, func(msg string) bool { + return strings.Contains(msg, expectedMsgSubstring) + }) +} + +func (s *slackTester) WaitForLastMessageEqual(userID, channelID, expectedMsg string) error { + return s.WaitForMessagePosted(userID, channelID, 1, func(msg string) bool { + return msg == expectedMsg + }) +} + +func (s *slackTester) WaitForMessagePosted(userID, channelID string, limitMessages int, assertFn MessageAssertion) error { + var fetchedMessages []slack.Message + var lastErr error + err := wait.Poll(pollInterval, s.cfg.MessageWaitTimeout, func() (done bool, err error) { + historyRes, err := s.cli.GetConversationHistory(&slack.GetConversationHistoryParameters{ + ChannelID: channelID, Limit: limitMessages, + }) + if err != nil { + lastErr = err + return false, nil + } + + fetchedMessages = historyRes.Messages + for _, msg := range historyRes.Messages { + if msg.User != userID { + continue + } + + if !assertFn(msg.Text) { + // different message + continue + } + + return true, nil + } + + return false, nil + }) + if lastErr == nil { + lastErr = errors.New("message assertion function returned false") + } + if err != nil { + if err == wait.ErrWaitTimeout { + return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, structDumper.Sdump(fetchedMessages)) + } + return err + } + + return nil +} + +func (s *slackTester) WaitForMessagePostedWithAttachment(userID, channelID string, assertFn AttachmentAssertion) error { + var fetchedMessages []slack.Message + var lastErr error + err := wait.Poll(pollInterval, s.cfg.MessageWaitTimeout, func() (done bool, err error) { + historyRes, err := s.cli.GetConversationHistory(&slack.GetConversationHistoryParameters{ + ChannelID: channelID, Limit: 1, + }) + if err != nil { + lastErr = err + return false, nil + } + + fetchedMessages = historyRes.Messages + for _, msg := range historyRes.Messages { + if msg.User != userID { + continue + } + + if len(msg.Attachments) != 1 { + return false, nil + } + + attachment := msg.Attachments[0] + if len(attachment.Fields) != 1 { + return false, nil + } + + if !assertFn(attachment.Title, attachment.Color, attachment.Fields[0].Value) { + // different message + return false, nil + } + + return true, nil + } + + return false, nil + }) + if lastErr == nil { + lastErr = errors.New("message assertion function returned false") + } + if err != nil { + if err == wait.ErrWaitTimeout { + return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, structDumper.Sdump(fetchedMessages)) + } + return err + } + + return nil +} + +func (s *slackTester) WaitForMessagesPostedOnChannelsWithAttachment(userID string, channelIDs []string, assertFn AttachmentAssertion) error { + errs := multierror.New() + for _, channelID := range channelIDs { + errs = multierror.Append(errs, s.WaitForMessagePostedWithAttachment(userID, channelID, assertFn)) + } + + return errs.ErrorOrNil() +} + +func (s *slackTester) findUserID(t *testing.T, name string) string { + t.Log("Getting users...") + res, err := s.cli.GetUsers() + require.NoError(t, err) + + t.Logf("Finding user ID by name %q...", name) + for _, u := range res { + if u.Name != name { + continue + } + return u.ID + } + + return "" +} + +func (s *slackTester) createChannel(t *testing.T) (*slack.Channel, func(t *testing.T)) { + t.Helper() + randomID := uuid.New() + channelName := fmt.Sprintf("%s-%s", channelNamePrefix, randomID.String()) + + t.Logf("Creating channel %q...", channelName) + // > There’s no limit to how many unique channels you can have in Slack — go ahead, create as many as you’d like! + // Sure, thanks Slack! + // Source: https://slack.com/help/articles/201402297-Create-a-channel + channel, err := s.cli.CreateConversation(channelName, false) + require.NoError(t, err) + + t.Logf("Channel %q (ID: %q) created", channelName, channel.ID) + + cleanupFn := func(t *testing.T) { + t.Helper() + t.Logf("Archiving channel %q...", channel.Name) + // We cannot delete channel: https://stackoverflow.com/questions/46807744/delete-channel-in-slack-api + err = s.cli.ArchiveConversation(channel.ID) + assert.NoError(t, err) + } + + return channel, cleanupFn +} From 6cf06afa4dd23cec85107f9431f82145af894b6a Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Thu, 8 Sep 2022 11:56:12 +0100 Subject: [PATCH 07/20] Linter fixes. --- test/e2e/bots_test.go | 4 ++-- test/e2e/discord_driver_test.go | 3 ++- test/e2e/slack_driver_test.go | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/test/e2e/bots_test.go b/test/e2e/bots_test.go index c120e029a..8506cb978 100644 --- a/test/e2e/bots_test.go +++ b/test/e2e/bots_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/MakeNowJust/heredoc" - "github.com/kubeshop/botkube/pkg/filterengine/filters" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vrischmann/envconfig" @@ -19,6 +18,8 @@ import ( "k8s.io/client-go/kubernetes" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/clientcmd" + + "github.com/kubeshop/botkube/pkg/filterengine/filters" ) type Config struct { @@ -551,7 +552,6 @@ func runBotTest(t *testing.T, err = botDriver.WaitForMessagePostedWithAttachment(botDriver.BotUserID(), botDriver.Channel().ID(), assertionFn) require.NoError(t, err) }) - } func codeBlock(in string) string { diff --git a/test/e2e/discord_driver_test.go b/test/e2e/discord_driver_test.go index 5f051d08a..03c74219f 100644 --- a/test/e2e/discord_driver_test.go +++ b/test/e2e/discord_driver_test.go @@ -9,10 +9,11 @@ import ( "github.com/bwmarrin/discordgo" "github.com/google/uuid" - "github.com/kubeshop/botkube/pkg/multierror" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/wait" + + "github.com/kubeshop/botkube/pkg/multierror" ) type DiscordChannel struct { diff --git a/test/e2e/slack_driver_test.go b/test/e2e/slack_driver_test.go index 041ef947d..315259ba2 100644 --- a/test/e2e/slack_driver_test.go +++ b/test/e2e/slack_driver_test.go @@ -7,11 +7,12 @@ import ( "testing" "github.com/google/uuid" - "github.com/kubeshop/botkube/pkg/multierror" "github.com/slack-go/slack" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/wait" + + "github.com/kubeshop/botkube/pkg/multierror" ) type slackChannel struct { From e6a031ce26ccb56441fcf2484b6fbd26eec3b083 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Thu, 8 Sep 2022 14:41:41 +0100 Subject: [PATCH 08/20] TesterName use botkube_tester by default. --- test/e2e/bots_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/bots_test.go b/test/e2e/bots_test.go index 8506cb978..308634d40 100644 --- a/test/e2e/bots_test.go +++ b/test/e2e/bots_test.go @@ -53,7 +53,7 @@ type SlackConfig struct { type DiscordConfig struct { BotName string `envconfig:"default=botkube"` - TesterName string `envconfig:"default=tester"` + TesterName string `envconfig:"default=botkube_tester"` AdditionalContextMessage string `envconfig:"optional"` GuildID string TesterAppToken string From 0382af0cc735480bafc40d6c1b885b98dd086293 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Fri, 9 Sep 2022 10:39:44 +0100 Subject: [PATCH 09/20] Setup e2e tests to run with helm test and CI. --- .github/workflows/branch-build.yml | 8 +++++++- hack/goreleaser.sh | 4 ++-- helm/botkube/e2e-test-values.yaml | 6 ------ helm/botkube/templates/tests/e2e-test.yaml | 2 -- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/branch-build.yml b/.github/workflows/branch-build.yml index e2364dc7d..4da60b377 100644 --- a/.github/workflows/branch-build.yml +++ b/.github/workflows/branch-build.yml @@ -82,7 +82,11 @@ jobs: - name: Install BotKube env: SLACK_BOT_TOKEN: ${{ secrets.SOCKET_SLACK_BOT_TOKEN }} + DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} + DISCORD_BOT_ID: ${{ secrets.DISCORD_BOT_ID }} SLACK_TESTER_APP_TOKEN: ${{ secrets.SLACK_TESTER_APP_TOKEN }} + DISCORD_TESTER_APP_TOKEN: ${{ secrets.DISCORD_TESTER_APP_TOKEN }} + DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }} run: | helm install botkube --namespace botkube ./helm/botkube --wait --create-namespace \ -f ./helm/botkube/e2e-test-values.yaml \ @@ -95,7 +99,9 @@ jobs: --set e2eTest.image.tag="${IMAGE_TAG}" \ --set e2eTest.slack.testerAppToken="${SLACK_TESTER_APP_TOKEN}" \ --set e2eTest.slack.additionalContextMessage="Branch test - commit SHA: ${GITHUB_SHA} - https://github.com/kubeshop/botkube/commit/${GITHUB_SHA}" \ - --set e2eTest.slack.botName="botkubev2" + --set e2eTest.discord.testerAppToken="${DISCORD_TESTER_APP_TOKEN}" \ + --set e2eTest.discord.guildID="${DISCORD_GUILD_ID}" \ + --set e2eTest.discord.additionalContextMessage="Branch test - commit SHA: ${GITHUB_SHA} - https://github.com/kubeshop/botkube/commit/${GITHUB_SHA}" - name: Run tests run: "helm test botkube --namespace botkube --timeout=$INTEGRATION_TESTS_TIMEOUT --logs" diff --git a/hack/goreleaser.sh b/hack/goreleaser.sh index a83c6b05d..78776b93c 100755 --- a/hack/goreleaser.sh +++ b/hack/goreleaser.sh @@ -122,8 +122,8 @@ build_single_e2e(){ -w /go/src/github.com/kubeshop/botkube \ -e GORELEASER_CURRENT_TAG=${GORELEASER_CURRENT_TAG} \ goreleaser/goreleaser build --single-target --rm-dist --snapshot --id botkube-test -o "./botkube-e2e.test" - docker build -f "$PWD/build/test.Dockerfile" --platform "${IMAGE_PLATFORM}" -t "${IMAGE_REGISTRY}/${TEST_IMAGE_REPOSITORY}:${GORELEASER_CURRENT_TAG}" --build-arg TEST_NAME=botkube-e2e.test . - rm "$PWD/botkube-test" + docker build -f "$PWD/build/test.Dockerfile" --build-arg=TEST_NAME=botkube-e2e.test --platform "${IMAGE_PLATFORM}" -t "${TEST_IMAGE_REPOSITORY}:${GORELEASER_CURRENT_TAG}" . + rm "$PWD/botkube-e2e.test" } release() { diff --git a/helm/botkube/e2e-test-values.yaml b/helm/botkube/e2e-test-values.yaml index 75468f61a..a0f8352f0 100644 --- a/helm/botkube/e2e-test-values.yaml +++ b/helm/botkube/e2e-test-values.yaml @@ -1,7 +1,4 @@ -## Parameters for anonymous analytics collection. analytics: - # -- If true, sending anonymous analytics is disabled. To learn what date we collect, - # see [Privacy Policy](https://botkube.io/privacy#privacy-policy). disable: true communications: @@ -140,9 +137,6 @@ settings: extraAnnotations: botkube.io/disable: "true" -analytics: - disable: true - e2eTest: slack: testerAppToken: "" # Provide a valid token for BotKube tester app diff --git a/helm/botkube/templates/tests/e2e-test.yaml b/helm/botkube/templates/tests/e2e-test.yaml index 67d53238a..4d953075d 100644 --- a/helm/botkube/templates/tests/e2e-test.yaml +++ b/helm/botkube/templates/tests/e2e-test.yaml @@ -20,8 +20,6 @@ spec: env: - name: CONFIG_PATH value: "/config/" - - name: ADDITIONAL_CONTEXT_MESSAGE - value: "{{ .Values.e2eTest.additionalContextMessage }}" - name: DEPLOYMENT_NAME value: "{{ include "botkube.fullname" . }}" - name: DEPLOYMENT_NAMESPACE From 07aa0b223040d748a1f4de585b854a4226f9988a Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Fri, 9 Sep 2022 10:48:41 +0100 Subject: [PATCH 10/20] Updated chart readme to include new e2e discord settings. --- helm/botkube/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helm/botkube/README.md b/helm/botkube/README.md index 5f5a8aade..9885d61b5 100644 --- a/helm/botkube/README.md +++ b/helm/botkube/README.md @@ -139,6 +139,12 @@ Controller for the BotKube Slack app which helps you monitor your Kubernetes clu | [e2eTest.slack.testerAppToken](./values.yaml#L645) | string | `""` | Slack tester application token that interacts with BotKube bot. | | [e2eTest.slack.additionalContextMessage](./values.yaml#L647) | string | `""` | Additional message that is sent by Tester. You can pass e.g. pull request number or source link where these tests are run from. | | [e2eTest.slack.messageWaitTimeout](./values.yaml#L649) | string | `"1m"` | Message wait timeout. It defines how long we wait to ensure that notification were not sent when disabled. | +| [e2eTest.discord.botName](./values.yaml#L652) | string | `"botkube"` | Name of the BotKube bot to interact with during the e2e tests. | +| [e2eTest.discord.testerName](./values.yaml#L654) | string | `"botkube_tester"` | Name of the BotKube Tester bot that sends messages during the e2e tests. | +| [e2eTest.discord.guildID](./values.yaml#L656) | string | `""` | Discord Guild ID (discord server ID) used to run e2e tests | +| [e2eTest.discord.testerAppToken](./values.yaml#L658) | string | `""` | Discord tester application token that interacts with BotKube bot. | +| [e2eTest.discord.additionalContextMessage](./values.yaml#L660) | string | `""` | Additional message that is sent by Tester. You can pass e.g. pull request number or source link where these tests are run from. | +| [e2eTest.discord.messageWaitTimeout](./values.yaml#L662) | string | `"1m"` | Message wait timeout. It defines how long we wait to ensure that notification were not sent when disabled. | ### AWS IRSA on EKS support From 7cf5e96e6647214896379bdd5a7fb0454f599822 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Fri, 9 Sep 2022 12:24:14 +0100 Subject: [PATCH 11/20] Updated E2E tests documentation. --- test/README.md | 99 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 84 insertions(+), 15 deletions(-) diff --git a/test/README.md b/test/README.md index b4150624d..29995d705 100644 --- a/test/README.md +++ b/test/README.md @@ -1,13 +1,27 @@ # E2E Tests -This directory contains E2E tests which are run against BotKube installed on Kubernetes cluster and Slack API. +This directory contains E2E tests. The tests instrument both Slack and Discord using a tester app and tester bot respectively. + +Basically, our testers listen to events sent from BotKube in a test cluster. And, the testers also trigger commands for BotKube to execute. + +On Kubernetes, the E2E tests are self-contained. They just require a BotKube installation on a cluster as highlighted in the instructions below. ## Prerequisites - Kubernetes cluster (e.g. local one created with `k3d`) -- BotKube bot app configured for a given Slack workspace according to the [instruction](https://botkube.io/installation/slack/) + +### Slack + +- BotKube bot app configured for a Slack workspace according to the [instruction](https://botkube.io/docs/installation/slack/) - BotKube tester app configured according to the [instruction](#configure-tester-slack-application) +### Discord + +- A Discord server available, [create one if required](https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server-). +- BotKube bot app configured for a Discord server according to the [instruction](https://botkube.io/docs/installation/discord/#install-botkube-to-the-discord-server) + > **NOTE:** Please name the app `botkube` and skip step 11 as it's not required. +- BotKube tester bot app configured according to the [instruction](#configure-tester-discord-bot-application) + ### Configure Tester Slack application > **NOTE:** This is something you need to do only once. Once the tester app is configured, you can use its token for running integration tests as many times as you want. @@ -47,55 +61,110 @@ This directory contains E2E tests which are run against BotKube installed on Kub export SLACK_TESTER_APP_TOKEN="{BotKube tester app token} ``` +### Configure Tester Discord bot application + +> **NOTE:** This is something you need to do only once. Once the tester app is configured, you can use its token for running integration tests as many times as you want. + +1. Create a new Discord app [here](https://discordapp.com/developers/applications). +2. Name the app: `botkube_tester`. +3. Navigate to the Bot page and Click Add Bot to add a Discord Bot to your application. +4. Click the Reset Token button. +5. Copy the Token and export it as the `DISCORD_TESTER_APP_TOKEN` environment variable. + ```bash + export DISCORD_TESTER_APP_TOKEN="{BotKube Discord tester app bot token} + ``` +6. Go to the OAuth2 page. +7. Select `SCOPES` as `bot`. +8. Select `BOT PERMISSIONS` as: + - Manage Channels. + - Read Messages/View Channels. + - Send Messages. + - Manage Messages. + - Embed Links. + - Attach Files. + - Read Message History. + - Mention Everyone. +9. Generate the URL using the OAuth2 URL Generator available under the OAuth2 section to add bot to your Discord server. +10. Copy and Paste the generated URL in a new tab, select the discord server to which you want to add the bot, click Continue and Authorize Bot addition. +11. Go back to the Discord screen and navigate to the Bot page. +12. Toggle `PUBLIC BOT` to OFF and `MESSAGE CONTENT INTENT` to ON. +13. Navigate back to the Discord server where you've installed tester app bot. +14. Find the name of the server in the top left. +15. Right click and select `CopyID` to copy the server ID. This is the `DISCORD_GUILD_ID` that we'll need to run tests against the server. + ```bash + export DISCORD_GUILD_ID="{BotKube Discord tester guildID} + ``` + ## Install BotKube 1. Export required environment variables: ```bash - export SLACK_BOT_TOKEN="{token for your configured BotKube app}" # WARNING: It is token for BotKube Slack bot, not the Tester! + export SLACK_BOT_TOKEN="{token for your configured Slack BotKube app}" # WARNING: Token for BotKube Slack bot, not the Tester! + + export DISCORD_BOT_ID="{BotKube Discord bot ClientID}" # WARNING: ClientID for BotKube Discord bot, not the Tester bot! + export DISCORD_BOT_TOKEN="{token for your configured Discord BotKube bot}" # WARNING: Token for BotKube Discord bot, not the Tester! + export IMAGE_REGISTRY="ghcr.io" export IMAGE_REPOSITORY="kubeshop/botkube" export IMAGE_TAG="v9.99.9-dev" # - # The following environmental variables are required only when running integration tests via Helm: + # Environment variables for running integration tests via Helm: # - export SLACK_TESTER_APP_TOKEN="{BotKube tester app token}" # WARNING: This is a token for Tester, not the BotKube Slack bot! export TEST_IMAGE_REGISTRY="ghcr.io" export TEST_IMAGE_REPOSITORY="kubeshop/botkube-test" export TEST_IMAGE_TAG="v9.99.9-dev" + + # + # Environment variables for running integration tests both LOCALLY and via Helm: + # + export SLACK_TESTER_APP_TOKEN="{BotKube Slack tester app token}" # WARNING: Token for Tester, not the BotKube Slack bot! + export DISCORD_TESTER_APP_TOKEN="{BotKube Discord tester app token}" # WARNING: Token for Tester, not the BotKube Discord bot! + export DISCORD_GUILD_ID="{Discord server ID}" # Where the tests will + + # + # Optional: environment variables for running integration tests LOCALLY using make: + # + export SLACK_TESTER_NAME="{Name of BotKube tester app}" # WARNING: tester name defaults to `tester` when a name is not provided for local test runs! ``` -1. Install BotKube using Helm chart: +2. Install BotKube using Helm chart: ```bash helm install botkube --namespace botkube ./helm/botkube --wait --create-namespace \ -f ./helm/botkube/e2e-test-values.yaml \ --set communications.default-group.slack.token="${SLACK_BOT_TOKEN}" \ + --set communications.default-group.discord.token="${DISCORD_BOT_TOKEN}" \ + --set communications.default-group.discord.botID="${DISCORD_BOT_ID}" \ --set image.registry="${IMAGE_REGISTRY}" \ --set image.repository="${IMAGE_REPOSITORY}" \ --set image.tag="${IMAGE_TAG}" \ --set e2eTest.image.registry="${TEST_IMAGE_REGISTRY}" \ --set e2eTest.image.repository="${TEST_IMAGE_REPOSITORY}" \ --set e2eTest.image.tag="${TEST_IMAGE_TAG}" \ - --set e2eTest.slack.testerAppToken="${SLACK_TESTER_APP_TOKEN}" + --set e2eTest.slack.testerAppToken="${SLACK_TESTER_APP_TOKEN}" \ + --set e2eTest.discord.testerAppToken="${DISCORD_TESTER_APP_TOKEN}" \ + --set e2eTest.discord.guildID="${DISCORD_GUILD_ID}" ``` ## Run tests locally -1. Export required environment variables: +1. Ensure these environment variables are exported: ```bash - export SLACK_TESTER_APP_TOKEN="{BotKube tester app token}" # WARNING: This is a token for Tester, not the BotKube Slack bot. + export SLACK_TESTER_APP_TOKEN="{BotKube Slack tester app token}" # WARNING: Token for Tester, not the BotKube Slack bot! + export DISCORD_TESTER_APP_TOKEN="{BotKube Discord tester app token}" # WARNING: Token for Tester, not the BotKube Discord bot! + export DISCORD_GUILD_ID="{Discord server ID}" # Where the tests will export KUBECONFIG=/Users/$USER/.kube/config # set custom path if necessary ``` -1. Run the tests and wait for the result: +2. Run the tests for Slack and Discord in parallel : ```bash - make test-integration + make test-integration-slack & make test-integration-discord & ``` - + ## Run Helm test Follow these steps to run integration tests via Helm: @@ -103,12 +172,12 @@ Follow these steps to run integration tests via Helm: 1. Run the tests: ```bash - helm test botkube --namespace botkube --timeout=10m --logs + helm test botkube --namespace botkube --timeout=30m --logs ``` -1. Wait for the results. The logs will be printed when the `helm test` command exits. + Wait for the results. The logs will be printed when the `helm test` command exits. - If you would like to see the logs in the real time, run: +2. If you would like to see the logs in real time, run: ```bash kubectl logs -n botkube botkube-e2e-test -f From 304c022529ce66078abf56ebb108c282c3630aa6 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Thu, 15 Sep 2022 10:51:04 +0100 Subject: [PATCH 12/20] Ensured branch-build and pr-build GitHub actions are configured correctly. --- .github/workflows/branch-build.yml | 3 ++- .github/workflows/pr-build.yaml | 14 +++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/branch-build.yml b/.github/workflows/branch-build.yml index 4da60b377..2ccb6b82b 100644 --- a/.github/workflows/branch-build.yml +++ b/.github/workflows/branch-build.yml @@ -8,7 +8,7 @@ on: env: HELM_VERSION: v3.9.0 K3D_VERSION: v5.4.3 - INTEGRATION_TESTS_TIMEOUT: 10m + INTEGRATION_TESTS_TIMEOUT: 30m IMAGE_REGISTRY: "ghcr.io" IMAGE_REPOSITORY: "kubeshop/botkube" TEST_IMAGE_REPOSITORY: "kubeshop/botkube-test" @@ -98,6 +98,7 @@ jobs: --set e2eTest.image.repository="${TEST_IMAGE_REPOSITORY}" \ --set e2eTest.image.tag="${IMAGE_TAG}" \ --set e2eTest.slack.testerAppToken="${SLACK_TESTER_APP_TOKEN}" \ + --set e2eTest.slack.botName="botkubev2" \ --set e2eTest.slack.additionalContextMessage="Branch test - commit SHA: ${GITHUB_SHA} - https://github.com/kubeshop/botkube/commit/${GITHUB_SHA}" \ --set e2eTest.discord.testerAppToken="${DISCORD_TESTER_APP_TOKEN}" \ --set e2eTest.discord.guildID="${DISCORD_GUILD_ID}" \ diff --git a/.github/workflows/pr-build.yaml b/.github/workflows/pr-build.yaml index 0495ee7d6..b4ea304f3 100644 --- a/.github/workflows/pr-build.yaml +++ b/.github/workflows/pr-build.yaml @@ -25,7 +25,7 @@ env: HELM_VERSION: v3.9.0 K3D_VERSION: v5.4.3 PR_NUMBER: ${{ github.event.pull_request.number }} - INTEGRATION_TESTS_TIMEOUT: 10m + INTEGRATION_TESTS_TIMEOUT: 30m IMAGE_REGISTRY: "ghcr.io" IMAGE_REPOSITORY: "kubeshop/pr/botkube" TEST_IMAGE_REPOSITORY: "kubeshop/pr/botkube-test" @@ -154,8 +154,12 @@ jobs: - name: Install BotKube env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_BOT_TOKEN: ${{ secrets.SOCKET_SLACK_BOT_TOKEN }} + DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} + DISCORD_BOT_ID: ${{ secrets.DISCORD_BOT_ID }} SLACK_TESTER_APP_TOKEN: ${{ secrets.SLACK_TESTER_APP_TOKEN }} + DISCORD_TESTER_APP_TOKEN: ${{ secrets.DISCORD_TESTER_APP_TOKEN }} + DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }} run: | helm install botkube --namespace botkube ./helm/botkube --wait --create-namespace \ -f ./helm/botkube/e2e-test-values.yaml \ @@ -167,7 +171,11 @@ jobs: --set e2eTest.image.repository="${TEST_IMAGE_REPOSITORY}" \ --set e2eTest.image.tag="${IMAGE_TAG}" \ --set e2eTest.slack.testerAppToken="${SLACK_TESTER_APP_TOKEN}" \ - --set e2eTest.slack.additionalContextMessage="Pull request: ${PR_NUMBER} - https://github.com/kubeshop/botkube/pull/${PR_NUMBER}" + --set e2eTest.slack.additionalContextMessage="Pull request: ${PR_NUMBER} - https://github.com/kubeshop/botkube/pull/${PR_NUMBER}" \ + --set e2eTest.slack.botName="botkubev2" \ + --set e2eTest.discord.testerAppToken="${DISCORD_TESTER_APP_TOKEN}" \ + --set e2eTest.discord.guildID="${DISCORD_GUILD_ID}" \ + --set e2eTest.discord.additionalContextMessage="Branch test - commit SHA: ${GITHUB_SHA} - https://github.com/kubeshop/botkube/commit/${GITHUB_SHA}" - name: Run tests run: "helm test botkube --namespace botkube --timeout=$INTEGRATION_TESTS_TIMEOUT --logs" From 1d1a9f51e0827504f7b201ec83162077fb3a66d5 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Fri, 16 Sep 2022 09:00:30 +0100 Subject: [PATCH 13/20] Remove debug log level setting to avoid noisy e2e tests. --- helm/botkube/e2e-test-values.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/helm/botkube/e2e-test-values.yaml b/helm/botkube/e2e-test-values.yaml index a0f8352f0..e5e68075c 100644 --- a/helm/botkube/e2e-test-values.yaml +++ b/helm/botkube/e2e-test-values.yaml @@ -130,9 +130,6 @@ executors: settings: clusterName: sample upgradeNotifier: false - log: - level: debug - disableColors: false extraAnnotations: botkube.io/disable: "true" From fbf3524a8a900bd0d0050c67b93520be73ed05ca Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:07:55 +0100 Subject: [PATCH 14/20] Driver based E2E tests now support interactive message checks. --- test/e2e/bots_test.go | 9 +- test/e2e/bots_tester_test.go | 3 + test/e2e/discord_driver_test.go | 11 + test/e2e/slack_driver_test.go | 126 +++++++++- test/e2e/slack_tester_test.go | 420 +++++++++++++++----------------- 5 files changed, 337 insertions(+), 232 deletions(-) diff --git a/test/e2e/bots_test.go b/test/e2e/bots_test.go index 96e5b2c4c..9d4b69d0a 100644 --- a/test/e2e/bots_test.go +++ b/test/e2e/bots_test.go @@ -50,7 +50,7 @@ type SlackConfig struct { TesterName string `envconfig:"default=tester"` AdditionalContextMessage string `envconfig:"optional"` TesterAppToken string - MessageWaitTimeout time.Duration `envconfig:"default=30s"` + MessageWaitTimeout time.Duration `envconfig:"default=35s"` } type DiscordConfig struct { @@ -59,7 +59,7 @@ type DiscordConfig struct { AdditionalContextMessage string `envconfig:"optional"` GuildID string TesterAppToken string - MessageWaitTimeout time.Duration `envconfig:"default=10s"` + MessageWaitTimeout time.Duration `envconfig:"default=30s"` } const ( @@ -165,7 +165,10 @@ func runBotTest(t *testing.T, require.NoError(t, err) t.Log("Waiting for Bot message on channel...") - err = slackTester.WaitForInteractiveMessagePostedRecentlyEqual(botDriver.BotUserID(), botDriver.Channel().ID(), interactive.Help(config.SlackCommPlatformIntegration, appCfg.ClusterName, fmt.Sprintf("<@%s>", botDriver.BotUserID()))) + err = botDriver.WaitForInteractiveMessagePostedRecentlyEqual(botDriver.BotUserID(), + botDriver.Channel().ID(), + interactive.Help(config.CommPlatformIntegration(SlackBot), appCfg.ClusterName, fmt.Sprintf("<@%s>", botDriver.BotUserID())), + ) require.NoError(t, err) err = botDriver.WaitForMessagePostedRecentlyEqual(botDriver.BotUserID(), botDriver.Channel().ID(), fmt.Sprintf("...and now my watch begins for cluster '%s'! :crossed_swords:", appCfg.ClusterName)) require.NoError(t, err) diff --git a/test/e2e/bots_tester_test.go b/test/e2e/bots_tester_test.go index 966c7b08f..5f4755a54 100644 --- a/test/e2e/bots_tester_test.go +++ b/test/e2e/bots_tester_test.go @@ -6,6 +6,7 @@ import ( "regexp" "testing" + "github.com/kubeshop/botkube/pkg/bot/interactive" "github.com/sanity-io/litter" ) @@ -50,10 +51,12 @@ type BotDriver interface { WaitForLastMessageContains(userID, channel, expectedMsgSubstring string) error WaitForLastMessageEqual(userID, channel, expectedMsg string) error WaitForMessagePosted(userID, channel string, limitMessages int, assertFn MessageAssertion) error + WaitForInteractiveMessagePosted(userID, channelID string, limitMessages int, assertFn MessageAssertion) error WaitForMessagePostedWithAttachment(userID, channel string, assertFn AttachmentAssertion) error WaitForMessagesPostedOnChannelsWithAttachment(userID string, channelIDs []string, assertFn AttachmentAssertion) error Channel() Channel SecondChannel() Channel BotUserID() string TesterUserID() string + WaitForInteractiveMessagePostedRecentlyEqual(userID string, channelID string, message interactive.Message) error } diff --git a/test/e2e/discord_driver_test.go b/test/e2e/discord_driver_test.go index 03c74219f..b672296e3 100644 --- a/test/e2e/discord_driver_test.go +++ b/test/e2e/discord_driver_test.go @@ -9,6 +9,7 @@ import ( "github.com/bwmarrin/discordgo" "github.com/google/uuid" + "github.com/kubeshop/botkube/pkg/bot/interactive" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/wait" @@ -174,6 +175,10 @@ func (d *discordTester) WaitForMessagePosted(userID, channelID string, limitMess return nil } +func (d *discordTester) WaitForInteractiveMessagePosted(userID, channelID string, limitMessages int, assertFn MessageAssertion) error { + return d.WaitForMessagePosted(userID, channelID, limitMessages, assertFn) +} + func (d *discordTester) WaitForMessagePostedWithAttachment(userID, channelID string, assertFn AttachmentAssertion) error { // To always receive message content: // ensure you enable the MESSAGE CONTENT INTENT for the tester bot on the developer portal. @@ -235,6 +240,12 @@ func (d *discordTester) WaitForMessagesPostedOnChannelsWithAttachment(userID str return errs.ErrorOrNil() } +func (d *discordTester) WaitForInteractiveMessagePostedRecentlyEqual(userID, channelID string, _ interactive.Message) error { + return d.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg string) bool { + return true + }) +} + func (d *discordTester) findUserID(t *testing.T, name string) string { t.Log("Getting users...") res, err := d.cli.GuildMembersSearch(d.cfg.GuildID, name, 5) diff --git a/test/e2e/slack_driver_test.go b/test/e2e/slack_driver_test.go index 315259ba2..19b9c5548 100644 --- a/test/e2e/slack_driver_test.go +++ b/test/e2e/slack_driver_test.go @@ -7,6 +7,9 @@ import ( "testing" "github.com/google/uuid" + "github.com/kubeshop/botkube/pkg/bot" + "github.com/kubeshop/botkube/pkg/bot/interactive" + "github.com/kubeshop/botkube/pkg/config" "github.com/slack-go/slack" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -15,17 +18,17 @@ import ( "github.com/kubeshop/botkube/pkg/multierror" ) -type slackChannel struct { +type SlackChannel struct { *slack.Channel } -func (s *slackChannel) ID() string { +func (s *SlackChannel) ID() string { return s.Channel.ID } -func (s *slackChannel) Name() string { +func (s *SlackChannel) Name() string { return s.Channel.Name } -func (s *slackChannel) Identifier() string { +func (s *SlackChannel) Identifier() string { return s.Channel.Name } @@ -56,10 +59,10 @@ func (s *slackTester) InitUsers(t *testing.T) { func (s *slackTester) InitChannels(t *testing.T) []func() { channel, cleanupChannelFn := s.createChannel(t) - s.channel = &slackChannel{Channel: channel} + s.channel = &SlackChannel{Channel: channel} secondChannel, cleanupSecondChannelFn := s.createChannel(t) - s.secondChannel = &slackChannel{Channel: secondChannel} + s.secondChannel = &SlackChannel{Channel: secondChannel} return []func(){ func() { cleanupChannelFn(t) }, @@ -171,6 +174,51 @@ func (s *slackTester) WaitForMessagePosted(userID, channelID string, limitMessag return nil } +func (s *slackTester) WaitForInteractiveMessagePosted(userID, channelID string, limitMessages int, assertFn MessageAssertion) error { + var fetchedMessages []slack.Message + var lastErr error + err := wait.Poll(pollInterval, s.cfg.MessageWaitTimeout, func() (done bool, err error) { + historyRes, err := s.cli.GetConversationHistory(&slack.GetConversationHistoryParameters{ + ChannelID: channelID, Limit: limitMessages, + }) + if err != nil { + lastErr = err + return false, nil + } + + fetchedMessages = historyRes.Messages + for _, msg := range historyRes.Messages { + if msg.User != userID { + continue + } + + if len(msg.Blocks.BlockSet) == 0 { + continue + } + + if !assertFn(sPrintBlocks(s.normalizeSlackBlockSet(msg.Blocks))) { + // different message + continue + } + + return true, nil + } + + return false, nil + }) + if lastErr == nil { + lastErr = errors.New("message assertion function returned false") + } + if err != nil { + if err == wait.ErrWaitTimeout { + return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, structDumper.Sdump(fetchedMessages)) + } + return err + } + + return nil +} + func (s *slackTester) WaitForMessagePostedWithAttachment(userID, channelID string, assertFn AttachmentAssertion) error { var fetchedMessages []slack.Message var lastErr error @@ -230,6 +278,72 @@ func (s *slackTester) WaitForMessagesPostedOnChannelsWithAttachment(userID strin return errs.ErrorOrNil() } +func (s *slackTester) WaitForInteractiveMessagePostedRecentlyEqual(userID, channelID string, msg interactive.Message) error { + //expectedBlocks := bot.NewSlackRenderer(config.Notification{}).RenderAsSlackBlocks(msg) + printedBlocks := sPrintBlocks(bot.NewSlackRenderer(config.Notification{}).RenderAsSlackBlocks(msg)) + return s.WaitForInteractiveMessagePosted(userID, channelID, recentMessagesLimit, func(msg string) bool { + return strings.EqualFold(msg, printedBlocks) + }) +} + +func sPrintBlocks(blocks []slack.Block) string { + var builder strings.Builder + + for _, block := range blocks { + switch block.BlockType() { + case slack.MBTSection: + section := block.(*slack.SectionBlock) + builder.WriteString("::::") + builder.WriteString(fmt.Sprintf("section: %s", section.Text.Text)) + case slack.MBTDivider: + builder.WriteString("::::") + builder.WriteString("divider") + case slack.MBTAction: + action := block.(*slack.ActionBlock) + builder.WriteString("::::") + for _, element := range action.Elements.ElementSet { + switch element.ElementType() { + case slack.METButton: + button := element.(*slack.ButtonBlockElement) + builder.WriteString(fmt.Sprintf("action::button: %s <> %s <> %s", + button.Text.Text, + button.Value, + button.ActionID, + )) + } + } + } + } + builder.WriteString("::::") + return builder.String() +} + +func (s *slackTester) normalizeSlackBlockSet(got slack.Blocks) []slack.Block { + for idx, item := range got.BlockSet { + switch item.BlockType() { + case slack.MBTSection: + item := item.(*slack.SectionBlock) + item.BlockID = "" // it's generated by SDK, so we don't compare it. + if item.Text != nil { + // slack add the < > to all links. For example: + // 'Learn more at https://botkube.io/filters' is converted to 'Learn more at ' + item.Text.Text = removeSlackLinksIndicators(item.Text.Text) + } + + got.BlockSet[idx] = item + case slack.MBTDivider: + item := item.(*slack.DividerBlock) + item.BlockID = "" // it's generated by SDK, so we don't compare it. + got.BlockSet[idx] = item + case slack.MBTAction: + item := item.(*slack.ActionBlock) + item.BlockID = "" // it's generated by SDK, so we don't compare it. + got.BlockSet[idx] = item + } + } + return got.BlockSet +} + func (s *slackTester) findUserID(t *testing.T, name string) string { t.Log("Getting users...") res, err := s.cli.GetUsers() diff --git a/test/e2e/slack_tester_test.go b/test/e2e/slack_tester_test.go index cee1c6052..b6af46772 100644 --- a/test/e2e/slack_tester_test.go +++ b/test/e2e/slack_tester_test.go @@ -2,226 +2,200 @@ package e2e -import ( - "errors" - "fmt" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/slack-go/slack" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/util/wait" - - "github.com/kubeshop/botkube/pkg/bot" - "github.com/kubeshop/botkube/pkg/bot/interactive" - "github.com/kubeshop/botkube/pkg/config" - "github.com/kubeshop/botkube/pkg/multierror" - "github.com/kubeshop/botkube/pkg/utils" -) - -const recentMessagesLimit = 5 - -type slackTester struct { - cli *slack.Client - cfg SlackConfig -} - -func newSlackTester(slackCfg SlackConfig) (*slackTester, error) { - slackCli := slack.New(slackCfg.TesterAppToken) - _, err := slackCli.AuthTest() - if err != nil { - return nil, err - } - - return &slackTester{cli: slackCli, cfg: slackCfg}, nil -} - -func (s *slackTester) CreateChannel(t *testing.T) (*slack.Channel, func(t *testing.T)) { - t.Helper() - randomID := uuid.New() - channelName := fmt.Sprintf("%s-%s", channelNamePrefix, randomID.String()) - - t.Logf("Creating channel %q...", channelName) - // > There’s no limit to how many unique channels you can have in Slack — go ahead, create as many as you’d like! - // Sure, thanks Slack! - // Source: https://slack.com/help/articles/201402297-Create-a-channel - channel, err := s.cli.CreateConversation(channelName, false) - require.NoError(t, err) - - t.Logf("Channel %q (ID: %q) created", channelName, channel.ID) - - cleanupFn := func(t *testing.T) { - t.Helper() - t.Logf("Archiving channel %q...", channel.Name) - // We cannot delete channel: https://stackoverflow.com/questions/46807744/delete-channel-in-slack-api - err = s.cli.ArchiveConversation(channel.ID) - assert.NoError(t, err) - } - - return channel, cleanupFn -} - -func (s *slackTester) PostInitialMessage(t *testing.T, channelName string) { - t.Helper() - t.Log("Posting welcome message...") - - var additionalContextMsg string - if s.cfg.AdditionalContextMessage != "" { - additionalContextMsg = fmt.Sprintf("%s\n", s.cfg.AdditionalContextMessage) - } - message := fmt.Sprintf("Hello!\n%s%s", additionalContextMsg, welcomeText) - _, _, err := s.cli.PostMessage(channelName, slack.MsgOptionText(message, false)) - require.NoError(t, err) -} - -func (s *slackTester) PostMessageToBot(t *testing.T, channelName, command string) { - message := fmt.Sprintf("<@%s> %s", s.cfg.BotName, command) - _, _, err := s.cli.PostMessage(channelName, slack.MsgOptionText(message, false)) - require.NoError(t, err) -} - -func (s *slackTester) FindUserIDForBot(t *testing.T) string { - return s.FindUserID(t, s.cfg.BotName) -} - -func (s *slackTester) FindUserIDForTester(t *testing.T) string { - return s.FindUserID(t, s.cfg.TesterName) -} - -func (s *slackTester) FindUserID(t *testing.T, name string) string { - t.Log("Getting users...") - res, err := s.cli.GetUsers() - require.NoError(t, err) - - t.Logf("Finding user ID by name %q...", name) - for _, u := range res { - if u.Name != name { - continue - } - return u.ID - } - - return "" -} - -func (s *slackTester) InviteBotToChannel(t *testing.T, botID, channelID string) { - t.Logf("Inviting bot with ID %q to the channel with ID %q", botID, channelID) - _, err := s.cli.InviteUsersToConversation(channelID, botID) - require.NoError(t, err) -} - -func (s *slackTester) WaitForMessagePostedRecentlyEqual(userID, channelID string, expectedMsg string) error { - return s.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg slack.Message) bool { - return strings.EqualFold(msg.Text, expectedMsg) - }) -} - -func (s *slackTester) WaitForLastMessageContains(userID, channelID string, expectedMsgSubstring string) error { - return s.WaitForMessagePosted(userID, channelID, 1, func(msg slack.Message) bool { - return strings.Contains(msg.Text, expectedMsgSubstring) - }) -} - -func (s *slackTester) WaitForLastMessageEqual(userID, channelID string, expectedMsg string) error { - return s.WaitForMessagePosted(userID, channelID, 1, func(msg slack.Message) bool { - return msg.Text == expectedMsg - }) -} - -func (s *slackTester) WaitForLastMessageEqualOnChannels(userID string, channelIDs []string, expectedMsg string) error { - return s.WaitForMessagesPostedOnChannels(userID, channelIDs, 1, func(msg slack.Message) bool { - return msg.Text == expectedMsg - }) -} - -func (s *slackTester) WaitForMessagePosted(userID, channelID string, limitMessages int, msgAssertFn func(msg slack.Message) bool) error { - var fetchedMessages []slack.Message - var lastErr error - err := wait.Poll(pollInterval, s.cfg.MessageWaitTimeout, func() (done bool, err error) { - historyRes, err := s.cli.GetConversationHistory(&slack.GetConversationHistoryParameters{ - ChannelID: channelID, Limit: limitMessages, - }) - if err != nil { - lastErr = err - return false, nil - } - - fetchedMessages = historyRes.Messages - for _, msg := range historyRes.Messages { - if msg.User != userID { - continue - } - - if !msgAssertFn(msg) { - // different message - continue - } - - return true, nil - } - - return false, nil - }) - if lastErr == nil { - lastErr = errors.New("message assertion function returned false") - } - if err != nil { - if err == wait.ErrWaitTimeout { - return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, utils.StructDumper().Sdump(fetchedMessages)) - } - return err - } - - return nil -} - -func (s *slackTester) WaitForMessagesPostedOnChannels(userID string, channelIDs []string, limitMessages int, msgAssertFn func(msg slack.Message) bool) error { - errs := multierror.New() - for _, channelID := range channelIDs { - errs = multierror.Append(errs, s.WaitForMessagePosted(userID, channelID, limitMessages, msgAssertFn)) - } - - return errs.ErrorOrNil() -} - -func (s *slackTester) WaitForInteractiveMessagePostedRecentlyEqual(userID, channelID string, msg interactive.Message) error { - expectedBlocks := bot.NewSlackRenderer(config.Notification{}).RenderAsSlackBlocks(msg) - return s.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg slack.Message) bool { - got := msg.Blocks - - if len(got.BlockSet) == 0 { - return false - } - - normalizedGot := s.normalizeSlackBlockSet(got) - return assert.ObjectsAreEqualValues(expectedBlocks, normalizedGot) - }) -} - -func (s *slackTester) normalizeSlackBlockSet(got slack.Blocks) []slack.Block { - for idx, item := range got.BlockSet { - switch item.BlockType() { - case slack.MBTSection: - item := item.(*slack.SectionBlock) - item.BlockID = "" // it's generated by SDK, so we don't compare it. - if item.Text != nil { - // slack add the < > to all links. For example: - // 'Learn more at https://botkube.io/filters' is converted to 'Learn more at ' - item.Text.Text = removeSlackLinksIndicators(item.Text.Text) - } - - got.BlockSet[idx] = item - case slack.MBTDivider: - item := item.(*slack.DividerBlock) - item.BlockID = "" // it's generated by SDK, so we don't compare it. - got.BlockSet[idx] = item - case slack.MBTAction: - item := item.(*slack.ActionBlock) - item.BlockID = "" // it's generated by SDK, so we don't compare it. - got.BlockSet[idx] = item - } - } - return got.BlockSet -} +//func newSlackTester(slackCfg SlackConfig) (*slackTester, error) { +// slackCli := slack.New(slackCfg.TesterAppToken) +// _, err := slackCli.AuthTest() +// if err != nil { +// return nil, err +// } +// +// return &slackTester{cli: slackCli, cfg: slackCfg}, nil +//} + +//func (s *slackTester) CreateChannel(t *testing.T) (*slack.Channel, func(t *testing.T)) { +// t.Helper() +// randomID := uuid.New() +// channelName := fmt.Sprintf("%s-%s", channelNamePrefix, randomID.String()) +// +// t.Logf("Creating channel %q...", channelName) +// // > There’s no limit to how many unique channels you can have in Slack — go ahead, create as many as you’d like! +// // Sure, thanks Slack! +// // Source: https://slack.com/help/articles/201402297-Create-a-channel +// channel, err := s.cli.CreateConversation(channelName, false) +// require.NoError(t, err) +// +// t.Logf("Channel %q (ID: %q) created", channelName, channel.ID) +// +// cleanupFn := func(t *testing.T) { +// t.Helper() +// t.Logf("Archiving channel %q...", channel.Name) +// // We cannot delete channel: https://stackoverflow.com/questions/46807744/delete-channel-in-slack-api +// err = s.cli.ArchiveConversation(channel.ID) +// assert.NoError(t, err) +// } +// +// return channel, cleanupFn +//} + +//func (s *slackTester) PostInitialMessage(t *testing.T, channelName string) { +// t.Helper() +// t.Log("Posting welcome message...") +// +// var additionalContextMsg string +// if s.cfg.AdditionalContextMessage != "" { +// additionalContextMsg = fmt.Sprintf("%s\n", s.cfg.AdditionalContextMessage) +// } +// message := fmt.Sprintf("Hello!\n%s%s", additionalContextMsg, welcomeText) +// _, _, err := s.cli.PostMessage(channelName, slack.MsgOptionText(message, false)) +// require.NoError(t, err) +//} + +//func (s *slackTester) PostMessageToBot(t *testing.T, channelName, command string) { +// message := fmt.Sprintf("<@%s> %s", s.cfg.BotName, command) +// _, _, err := s.cli.PostMessage(channelName, slack.MsgOptionText(message, false)) +// require.NoError(t, err) +//} + +//func (s *slackTester) FindUserIDForBot(t *testing.T) string { +// return s.FindUserID(t, s.cfg.BotName) +//} + +//func (s *slackTester) FindUserIDForTester(t *testing.T) string { +// return s.FindUserID(t, s.cfg.TesterName) +//} + +//func (s *slackTester) FindUserID(t *testing.T, name string) string { +// t.Log("Getting users...") +// res, err := s.cli.GetUsers() +// require.NoError(t, err) +// +// t.Logf("Finding user ID by name %q...", name) +// for _, u := range res { +// if u.Name != name { +// continue +// } +// return u.ID +// } +// +// return "" +//} + +//func (s *slackTester) InviteBotToChannel(t *testing.T, botID, channelID string) { +// t.Logf("Inviting bot with ID %q to the channel with ID %q", botID, channelID) +// _, err := s.cli.InviteUsersToConversation(channelID, botID) +// require.NoError(t, err) +//} + +//func (s *slackTester) WaitForMessagePostedRecentlyEqual(userID, channelID string, expectedMsg string) error { +// return s.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg slack.Message) bool { +// return strings.EqualFold(msg.Text, expectedMsg) +// }) +//} + +//func (s *slackTester) WaitForLastMessageContains(userID, channelID string, expectedMsgSubstring string) error { +// return s.WaitForMessagePosted(userID, channelID, 1, func(msg slack.Message) bool { +// return strings.Contains(msg.Text, expectedMsgSubstring) +// }) +//} + +//func (s *slackTester) WaitForLastMessageEqual(userID, channelID string, expectedMsg string) error { +// return s.WaitForMessagePosted(userID, channelID, 1, func(msg slack.Message) bool { +// return msg.Text == expectedMsg +// }) +//} + +//func (s *slackTester) WaitForLastMessageEqualOnChannels(userID string, channelIDs []string, expectedMsg string) error { +// return s.WaitForMessagesPostedOnChannels(userID, channelIDs, 1, func(msg slack.Message) bool { +// return msg.Text == expectedMsg +// }) +//} + +//func (s *slackTester) WaitForMessagePosted(userID, channelID string, limitMessages int, msgAssertFn func(msg slack.Message) bool) error { +// var fetchedMessages []slack.Message +// var lastErr error +// err := wait.Poll(pollInterval, s.cfg.MessageWaitTimeout, func() (done bool, err error) { +// historyRes, err := s.cli.GetConversationHistory(&slack.GetConversationHistoryParameters{ +// ChannelID: channelID, Limit: limitMessages, +// }) +// if err != nil { +// lastErr = err +// return false, nil +// } +// +// fetchedMessages = historyRes.Messages +// for _, msg := range historyRes.Messages { +// if msg.User != userID { +// continue +// } +// +// if !msgAssertFn(msg) { +// // different message +// continue +// } +// +// return true, nil +// } +// +// return false, nil +// }) +// if lastErr == nil { +// lastErr = errors.New("message assertion function returned false") +// } +// if err != nil { +// if err == wait.ErrWaitTimeout { +// return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, utils.StructDumper().Sdump(fetchedMessages)) +// } +// return err +// } +// +// return nil +//} + +//func (s *slackTester) WaitForMessagesPostedOnChannels(userID string, channelIDs []string, limitMessages int, msgAssertFn func(msg slack.Message) bool) error { +// errs := multierror.New() +// for _, channelID := range channelIDs { +// errs = multierror.Append(errs, s.WaitForMessagePosted(userID, channelID, limitMessages, msgAssertFn)) +// } +// +// return errs.ErrorOrNil() +//} + +//func (s *slackTester) WaitForInteractiveMessagePostedRecentlyEqual(userID, channelID string, msg interactive.Message) error { +// expectedBlocks := bot.NewSlackRenderer(config.Notification{}).RenderAsSlackBlocks(msg) +// return s.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg slack.Message) bool { +// got := msg.Blocks +// +// if len(got.BlockSet) == 0 { +// return false +// } +// +// normalizedGot := s.normalizeSlackBlockSet(got) +// return assert.ObjectsAreEqualValues(expectedBlocks, normalizedGot) +// }) +//} + +//func (s *slackTester) normalizeSlackBlockSet(got slack.Blocks) []slack.Block { +// for idx, item := range got.BlockSet { +// switch item.BlockType() { +// case slack.MBTSection: +// item := item.(*slack.SectionBlock) +// item.BlockID = "" // it's generated by SDK, so we don't compare it. +// if item.Text != nil { +// // slack add the < > to all links. For example: +// // 'Learn more at https://botkube.io/filters' is converted to 'Learn more at ' +// item.Text.Text = removeSlackLinksIndicators(item.Text.Text) +// } +// +// got.BlockSet[idx] = item +// case slack.MBTDivider: +// item := item.(*slack.DividerBlock) +// item.BlockID = "" // it's generated by SDK, so we don't compare it. +// got.BlockSet[idx] = item +// case slack.MBTAction: +// item := item.(*slack.ActionBlock) +// item.BlockID = "" // it's generated by SDK, so we don't compare it. +// got.BlockSet[idx] = item +// } +// } +// return got.BlockSet +//} From d11c5089a0122add31a1a0a2b0ddf9bb9f7a6ca5 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:26:41 +0100 Subject: [PATCH 15/20] Updated Slack test message wait time out. --- test/e2e/bots_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/bots_test.go b/test/e2e/bots_test.go index 9d4b69d0a..97ccb5c9d 100644 --- a/test/e2e/bots_test.go +++ b/test/e2e/bots_test.go @@ -50,7 +50,7 @@ type SlackConfig struct { TesterName string `envconfig:"default=tester"` AdditionalContextMessage string `envconfig:"optional"` TesterAppToken string - MessageWaitTimeout time.Duration `envconfig:"default=35s"` + MessageWaitTimeout time.Duration `envconfig:"default=30s"` } type DiscordConfig struct { From dbca94f52e9bc184539db9631642566d907fa82f Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:30:07 +0100 Subject: [PATCH 16/20] Updated e2e tests README per review comments. --- test/README.md | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/test/README.md b/test/README.md index 29995d705..6a492c902 100644 --- a/test/README.md +++ b/test/README.md @@ -6,22 +6,17 @@ Basically, our testers listen to events sent from BotKube in a test cluster. And On Kubernetes, the E2E tests are self-contained. They just require a BotKube installation on a cluster as highlighted in the instructions below. -## Prerequisites +## General prerequisites - Kubernetes cluster (e.g. local one created with `k3d`) -### Slack +## Testing Slack + +### Prerequisites - BotKube bot app configured for a Slack workspace according to the [instruction](https://botkube.io/docs/installation/slack/) - BotKube tester app configured according to the [instruction](#configure-tester-slack-application) -### Discord - -- A Discord server available, [create one if required](https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server-). -- BotKube bot app configured for a Discord server according to the [instruction](https://botkube.io/docs/installation/discord/#install-botkube-to-the-discord-server) - > **NOTE:** Please name the app `botkube` and skip step 11 as it's not required. -- BotKube tester bot app configured according to the [instruction](#configure-tester-discord-bot-application) - ### Configure Tester Slack application > **NOTE:** This is something you need to do only once. Once the tester app is configured, you can use its token for running integration tests as many times as you want. @@ -61,6 +56,15 @@ On Kubernetes, the E2E tests are self-contained. They just require a BotKube ins export SLACK_TESTER_APP_TOKEN="{BotKube tester app token} ``` +## Testing Discord + +### Prerequisites + +- A Discord server available, [create one if required](https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server-). +- BotKube bot app configured for a Discord server according to the [instruction](https://botkube.io/docs/installation/discord/#install-botkube-to-the-discord-server) + > **NOTE:** Please name the app `botkube` and skip step 11 as it's not required. +- BotKube tester bot app configured according to the [instruction](#configure-tester-discord-bot-application) + ### Configure Tester Discord bot application > **NOTE:** This is something you need to do only once. Once the tester app is configured, you can use its token for running integration tests as many times as you want. @@ -97,6 +101,10 @@ On Kubernetes, the E2E tests are self-contained. They just require a BotKube ins ## Install BotKube +Use environment vars for the specific platform (Slack or Discord or both) when running your E2E tests. + +For example, if you're only running Discord tests, you can omit env var prefixed with `SLACK_..`. + 1. Export required environment variables: ```bash @@ -126,10 +134,19 @@ On Kubernetes, the E2E tests are self-contained. They just require a BotKube ins # # Optional: environment variables for running integration tests LOCALLY using make: # - export SLACK_TESTER_NAME="{Name of BotKube tester app}" # WARNING: tester name defaults to `tester` when a name is not provided for local test runs! + export SLACK_TESTER_NAME="{Name of BotKube SLACK tester app}" # WARNING: tester name defaults to `tester` when a name is not provided for local test runs! + export DISCORD_TESTER_NAME="{Name of BotKube DISCORD tester app}" # WARNING: tester name defaults to `tester` when a name is not provided for local test runs! ``` 2. Install BotKube using Helm chart: + + Again, you can omit a platform your E2E tests by only adding `--set` directives for the target platform. + + For example, if you're only intending to test SLACK, remove: + - `--set communications.default-group.discord.token="${DISCORD_BOT_TOKEN}"` + - `--set communications.default-group.discord.botID="${DISCORD_BOT_ID}"` + - `--set e2eTest.discord.testerAppToken="${DISCORD_TESTER_APP_TOKEN}"` + - `--set e2eTest.discord.guildID="${DISCORD_GUILD_ID}"` ```bash helm install botkube --namespace botkube ./helm/botkube --wait --create-namespace \ @@ -150,7 +167,7 @@ On Kubernetes, the E2E tests are self-contained. They just require a BotKube ins ## Run tests locally -1. Ensure these environment variables are exported: +1. Ensure the environment variables for your target platforms are exported: ```bash export SLACK_TESTER_APP_TOKEN="{BotKube Slack tester app token}" # WARNING: Token for Tester, not the BotKube Slack bot! From 14fc98fac675b88e48cf84a97cd99aecea23ef47 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:40:41 +0100 Subject: [PATCH 17/20] Added missing compiler directives. --- test/e2e/discord_driver_test.go | 2 ++ test/e2e/slack_driver_test.go | 2 ++ test/e2e/slack_helpers_test.go | 2 ++ 3 files changed, 6 insertions(+) diff --git a/test/e2e/discord_driver_test.go b/test/e2e/discord_driver_test.go index b672296e3..932ae671e 100644 --- a/test/e2e/discord_driver_test.go +++ b/test/e2e/discord_driver_test.go @@ -1,3 +1,5 @@ +//go:build integration + package e2e import ( diff --git a/test/e2e/slack_driver_test.go b/test/e2e/slack_driver_test.go index 19b9c5548..04d750355 100644 --- a/test/e2e/slack_driver_test.go +++ b/test/e2e/slack_driver_test.go @@ -1,3 +1,5 @@ +//go:build integration + package e2e import ( diff --git a/test/e2e/slack_helpers_test.go b/test/e2e/slack_helpers_test.go index 9e3d31c09..b6af1b7fa 100644 --- a/test/e2e/slack_helpers_test.go +++ b/test/e2e/slack_helpers_test.go @@ -1,3 +1,5 @@ +//go:build integration + package e2e import ( From 3ecda162f20a3603d50280b5e9df7b2003f304de Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:40:56 +0100 Subject: [PATCH 18/20] Removed legacy tests. --- test/e2e/slack_tester_test.go | 201 ---------------------------------- 1 file changed, 201 deletions(-) delete mode 100644 test/e2e/slack_tester_test.go diff --git a/test/e2e/slack_tester_test.go b/test/e2e/slack_tester_test.go deleted file mode 100644 index b6af46772..000000000 --- a/test/e2e/slack_tester_test.go +++ /dev/null @@ -1,201 +0,0 @@ -//go:build integration - -package e2e - -//func newSlackTester(slackCfg SlackConfig) (*slackTester, error) { -// slackCli := slack.New(slackCfg.TesterAppToken) -// _, err := slackCli.AuthTest() -// if err != nil { -// return nil, err -// } -// -// return &slackTester{cli: slackCli, cfg: slackCfg}, nil -//} - -//func (s *slackTester) CreateChannel(t *testing.T) (*slack.Channel, func(t *testing.T)) { -// t.Helper() -// randomID := uuid.New() -// channelName := fmt.Sprintf("%s-%s", channelNamePrefix, randomID.String()) -// -// t.Logf("Creating channel %q...", channelName) -// // > There’s no limit to how many unique channels you can have in Slack — go ahead, create as many as you’d like! -// // Sure, thanks Slack! -// // Source: https://slack.com/help/articles/201402297-Create-a-channel -// channel, err := s.cli.CreateConversation(channelName, false) -// require.NoError(t, err) -// -// t.Logf("Channel %q (ID: %q) created", channelName, channel.ID) -// -// cleanupFn := func(t *testing.T) { -// t.Helper() -// t.Logf("Archiving channel %q...", channel.Name) -// // We cannot delete channel: https://stackoverflow.com/questions/46807744/delete-channel-in-slack-api -// err = s.cli.ArchiveConversation(channel.ID) -// assert.NoError(t, err) -// } -// -// return channel, cleanupFn -//} - -//func (s *slackTester) PostInitialMessage(t *testing.T, channelName string) { -// t.Helper() -// t.Log("Posting welcome message...") -// -// var additionalContextMsg string -// if s.cfg.AdditionalContextMessage != "" { -// additionalContextMsg = fmt.Sprintf("%s\n", s.cfg.AdditionalContextMessage) -// } -// message := fmt.Sprintf("Hello!\n%s%s", additionalContextMsg, welcomeText) -// _, _, err := s.cli.PostMessage(channelName, slack.MsgOptionText(message, false)) -// require.NoError(t, err) -//} - -//func (s *slackTester) PostMessageToBot(t *testing.T, channelName, command string) { -// message := fmt.Sprintf("<@%s> %s", s.cfg.BotName, command) -// _, _, err := s.cli.PostMessage(channelName, slack.MsgOptionText(message, false)) -// require.NoError(t, err) -//} - -//func (s *slackTester) FindUserIDForBot(t *testing.T) string { -// return s.FindUserID(t, s.cfg.BotName) -//} - -//func (s *slackTester) FindUserIDForTester(t *testing.T) string { -// return s.FindUserID(t, s.cfg.TesterName) -//} - -//func (s *slackTester) FindUserID(t *testing.T, name string) string { -// t.Log("Getting users...") -// res, err := s.cli.GetUsers() -// require.NoError(t, err) -// -// t.Logf("Finding user ID by name %q...", name) -// for _, u := range res { -// if u.Name != name { -// continue -// } -// return u.ID -// } -// -// return "" -//} - -//func (s *slackTester) InviteBotToChannel(t *testing.T, botID, channelID string) { -// t.Logf("Inviting bot with ID %q to the channel with ID %q", botID, channelID) -// _, err := s.cli.InviteUsersToConversation(channelID, botID) -// require.NoError(t, err) -//} - -//func (s *slackTester) WaitForMessagePostedRecentlyEqual(userID, channelID string, expectedMsg string) error { -// return s.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg slack.Message) bool { -// return strings.EqualFold(msg.Text, expectedMsg) -// }) -//} - -//func (s *slackTester) WaitForLastMessageContains(userID, channelID string, expectedMsgSubstring string) error { -// return s.WaitForMessagePosted(userID, channelID, 1, func(msg slack.Message) bool { -// return strings.Contains(msg.Text, expectedMsgSubstring) -// }) -//} - -//func (s *slackTester) WaitForLastMessageEqual(userID, channelID string, expectedMsg string) error { -// return s.WaitForMessagePosted(userID, channelID, 1, func(msg slack.Message) bool { -// return msg.Text == expectedMsg -// }) -//} - -//func (s *slackTester) WaitForLastMessageEqualOnChannels(userID string, channelIDs []string, expectedMsg string) error { -// return s.WaitForMessagesPostedOnChannels(userID, channelIDs, 1, func(msg slack.Message) bool { -// return msg.Text == expectedMsg -// }) -//} - -//func (s *slackTester) WaitForMessagePosted(userID, channelID string, limitMessages int, msgAssertFn func(msg slack.Message) bool) error { -// var fetchedMessages []slack.Message -// var lastErr error -// err := wait.Poll(pollInterval, s.cfg.MessageWaitTimeout, func() (done bool, err error) { -// historyRes, err := s.cli.GetConversationHistory(&slack.GetConversationHistoryParameters{ -// ChannelID: channelID, Limit: limitMessages, -// }) -// if err != nil { -// lastErr = err -// return false, nil -// } -// -// fetchedMessages = historyRes.Messages -// for _, msg := range historyRes.Messages { -// if msg.User != userID { -// continue -// } -// -// if !msgAssertFn(msg) { -// // different message -// continue -// } -// -// return true, nil -// } -// -// return false, nil -// }) -// if lastErr == nil { -// lastErr = errors.New("message assertion function returned false") -// } -// if err != nil { -// if err == wait.ErrWaitTimeout { -// return fmt.Errorf("while waiting for condition: last error: %w; fetched messages: %s", lastErr, utils.StructDumper().Sdump(fetchedMessages)) -// } -// return err -// } -// -// return nil -//} - -//func (s *slackTester) WaitForMessagesPostedOnChannels(userID string, channelIDs []string, limitMessages int, msgAssertFn func(msg slack.Message) bool) error { -// errs := multierror.New() -// for _, channelID := range channelIDs { -// errs = multierror.Append(errs, s.WaitForMessagePosted(userID, channelID, limitMessages, msgAssertFn)) -// } -// -// return errs.ErrorOrNil() -//} - -//func (s *slackTester) WaitForInteractiveMessagePostedRecentlyEqual(userID, channelID string, msg interactive.Message) error { -// expectedBlocks := bot.NewSlackRenderer(config.Notification{}).RenderAsSlackBlocks(msg) -// return s.WaitForMessagePosted(userID, channelID, recentMessagesLimit, func(msg slack.Message) bool { -// got := msg.Blocks -// -// if len(got.BlockSet) == 0 { -// return false -// } -// -// normalizedGot := s.normalizeSlackBlockSet(got) -// return assert.ObjectsAreEqualValues(expectedBlocks, normalizedGot) -// }) -//} - -//func (s *slackTester) normalizeSlackBlockSet(got slack.Blocks) []slack.Block { -// for idx, item := range got.BlockSet { -// switch item.BlockType() { -// case slack.MBTSection: -// item := item.(*slack.SectionBlock) -// item.BlockID = "" // it's generated by SDK, so we don't compare it. -// if item.Text != nil { -// // slack add the < > to all links. For example: -// // 'Learn more at https://botkube.io/filters' is converted to 'Learn more at ' -// item.Text.Text = removeSlackLinksIndicators(item.Text.Text) -// } -// -// got.BlockSet[idx] = item -// case slack.MBTDivider: -// item := item.(*slack.DividerBlock) -// item.BlockID = "" // it's generated by SDK, so we don't compare it. -// got.BlockSet[idx] = item -// case slack.MBTAction: -// item := item.(*slack.ActionBlock) -// item.BlockID = "" // it's generated by SDK, so we don't compare it. -// got.BlockSet[idx] = item -// } -// } -// return got.BlockSet -//} From e903647559aa3d858b33c8ed8679e72d25da7f4a Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:43:22 +0100 Subject: [PATCH 19/20] Linter fixes. --- test/e2e/bots_tester_test.go | 3 ++- test/e2e/discord_driver_test.go | 2 +- test/e2e/slack_driver_test.go | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/test/e2e/bots_tester_test.go b/test/e2e/bots_tester_test.go index 5f4755a54..5b4dc1252 100644 --- a/test/e2e/bots_tester_test.go +++ b/test/e2e/bots_tester_test.go @@ -6,8 +6,9 @@ import ( "regexp" "testing" - "github.com/kubeshop/botkube/pkg/bot/interactive" "github.com/sanity-io/litter" + + "github.com/kubeshop/botkube/pkg/bot/interactive" ) const recentMessagesLimit = 5 diff --git a/test/e2e/discord_driver_test.go b/test/e2e/discord_driver_test.go index 932ae671e..96390c81c 100644 --- a/test/e2e/discord_driver_test.go +++ b/test/e2e/discord_driver_test.go @@ -11,11 +11,11 @@ import ( "github.com/bwmarrin/discordgo" "github.com/google/uuid" - "github.com/kubeshop/botkube/pkg/bot/interactive" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/wait" + "github.com/kubeshop/botkube/pkg/bot/interactive" "github.com/kubeshop/botkube/pkg/multierror" ) diff --git a/test/e2e/slack_driver_test.go b/test/e2e/slack_driver_test.go index 04d750355..a51a99ae5 100644 --- a/test/e2e/slack_driver_test.go +++ b/test/e2e/slack_driver_test.go @@ -9,14 +9,14 @@ import ( "testing" "github.com/google/uuid" - "github.com/kubeshop/botkube/pkg/bot" - "github.com/kubeshop/botkube/pkg/bot/interactive" - "github.com/kubeshop/botkube/pkg/config" "github.com/slack-go/slack" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/wait" + "github.com/kubeshop/botkube/pkg/bot" + "github.com/kubeshop/botkube/pkg/bot/interactive" + "github.com/kubeshop/botkube/pkg/config" "github.com/kubeshop/botkube/pkg/multierror" ) From 7b2378f2f2e56aa0e31e626e40e7d4b2d0246553 Mon Sep 17 00:00:00 2001 From: Ezo Saleh <105397+ezodude@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:59:34 +0100 Subject: [PATCH 20/20] Added missing registry setting when building single e2e container. --- hack/goreleaser.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/goreleaser.sh b/hack/goreleaser.sh index 78776b93c..c0a46cfc2 100755 --- a/hack/goreleaser.sh +++ b/hack/goreleaser.sh @@ -122,7 +122,7 @@ build_single_e2e(){ -w /go/src/github.com/kubeshop/botkube \ -e GORELEASER_CURRENT_TAG=${GORELEASER_CURRENT_TAG} \ goreleaser/goreleaser build --single-target --rm-dist --snapshot --id botkube-test -o "./botkube-e2e.test" - docker build -f "$PWD/build/test.Dockerfile" --build-arg=TEST_NAME=botkube-e2e.test --platform "${IMAGE_PLATFORM}" -t "${TEST_IMAGE_REPOSITORY}:${GORELEASER_CURRENT_TAG}" . + docker build -f "$PWD/build/test.Dockerfile" --build-arg=TEST_NAME=botkube-e2e.test --platform "${IMAGE_PLATFORM}" -t "${IMAGE_REGISTRY}/${TEST_IMAGE_REPOSITORY}:${GORELEASER_CURRENT_TAG}" . rm "$PWD/botkube-e2e.test" }