From 8260b08c4b974bb87e0ce1a4d8d7150272dec540 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Wed, 20 Sep 2023 16:52:37 +0200 Subject: [PATCH] Migrate Cloud Slack Dev E2E tests and fix migration test --- .github/workflows/branch-build.yml | 58 - .github/workflows/cli-migration-e2e.yaml | 104 ++ .github/workflows/cloud-slack-dev-e2e.yaml | 73 + Makefile | 6 + go.mod | 6 + go.sum | 16 + test/README.md | 53 + test/cloud-slack-dev-e2e/e2e_test.go | 681 +++++++++ test/cloud-slack-dev-e2e/gql.go | 98 ++ test/cloud_graphql/graphql_client.go | 539 ++++++++ .../model/connected_platforms.go | 8 + test/cloud_graphql/model/doc.go | 3 + test/cloud_graphql/model/models_gen.go | 1223 +++++++++++++++++ test/cloud_graphql/model/platforms.go | 14 + test/cloud_graphql/model/usage.go | 10 + test/commplatform/discord_tester.go | 15 + test/commplatform/generic.go | 2 + test/commplatform/slack_tester.go | 40 +- test/e2e/migration_test.go | 6 +- 19 files changed, 2888 insertions(+), 67 deletions(-) create mode 100644 .github/workflows/cli-migration-e2e.yaml create mode 100644 .github/workflows/cloud-slack-dev-e2e.yaml create mode 100644 test/cloud-slack-dev-e2e/e2e_test.go create mode 100644 test/cloud-slack-dev-e2e/gql.go create mode 100644 test/cloud_graphql/graphql_client.go create mode 100644 test/cloud_graphql/model/connected_platforms.go create mode 100644 test/cloud_graphql/model/doc.go create mode 100644 test/cloud_graphql/model/models_gen.go create mode 100644 test/cloud_graphql/model/platforms.go create mode 100644 test/cloud_graphql/model/usage.go diff --git a/.github/workflows/branch-build.yml b/.github/workflows/branch-build.yml index 7aa3e0ea02..93f4436924 100644 --- a/.github/workflows/branch-build.yml +++ b/.github/workflows/branch-build.yml @@ -4,8 +4,6 @@ on: push: branches: - main - repository_dispatch: - types: [ trigger-e2e-tests ] env: HELM_VERSION: v3.9.0 @@ -53,62 +51,6 @@ jobs: with: version: ${{ env.HELM_VERSION }} - migration-e2e-test: - name: Migration e2e test - runs-on: ubuntu-latest - needs: [ build ] - concurrency: - group: e2e-migration - cancel-in-progress: false - permissions: - contents: read - packages: read - strategy: - fail-fast: false - matrix: - e2e: - - discord - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Install GoReleaser - uses: goreleaser/goreleaser-action@v5 - with: - install-only: true - version: latest - - name: Setup Go - uses: actions/setup-go@v4 - with: - go-version-file: 'go.mod' - cache: true - - name: Run GoReleaser - run: make release-snapshot-cli - - name: Add botkube alias - run: | - echo BOTKUBE_BINARY_PATH="$PWD/dist/botkube-cli_linux_amd64_v1/botkube" >> $GITHUB_ENV - - name: Install Helm - uses: azure/setup-helm@v1 - with: - version: ${{ env.HELM_VERSION }} - - name: Download k3d - run: "wget -q -O - https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | TAG=${K3D_VERSION} bash" - - name: Create k3d cluster - run: "k3d cluster create migration-test-cluster --wait --timeout=5m" - - name: Run e2e tests for botkube client - env: - DISCORD_BOT_ID: ${{ secrets.DISCORD_BOT_ID }} - DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} - DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }} - DISCORD_TESTER_APP_TOKEN: ${{ secrets.DISCORD_TESTER_APP_TOKEN }} - BOTKUBE_CLOUD_DEV_GQL_ENDPOINT: ${{ secrets.BOTKUBE_CLOUD_DEV_GQL_ENDPOINT }} - BOTKUBE_CLOUD_DEV_REFRESH_TOKEN: ${{ secrets.BOTKUBE_CLOUD_DEV_REFRESH_TOKEN }} - BOTKUBE_CLOUD_DEV_AUTH0_CLIENT_ID: ${{ secrets.BOTKUBE_CLOUD_DEV_AUTH0_CLIENT_ID }} - run: | - KUBECONFIG=$(k3d kubeconfig write migration-test-cluster) \ - make test-migration-tool - integration-tests: name: Integration tests runs-on: ubuntu-latest diff --git a/.github/workflows/cli-migration-e2e.yaml b/.github/workflows/cli-migration-e2e.yaml new file mode 100644 index 0000000000..6926609e65 --- /dev/null +++ b/.github/workflows/cli-migration-e2e.yaml @@ -0,0 +1,104 @@ +name: CLI Migration E2E tests + +concurrency: + group: cli-migration-e2e + cancel-in-progress: false + +on: + push: + branches: + - 'main' # TODO: Ensure it runs after branch build + - 'cloud-slack-dev-e2e' # TODO: Remove before merge + repository_dispatch: + types: [ trigger-e2e-tests ] + +env: + HELM_VERSION: v3.9.0 + K3D_VERSION: v5.4.6 + IMAGE_REGISTRY: "ghcr.io" + IMAGE_REPOSITORY: "kubeshop/botkube" + IMAGE_TAG: v9.99.9-dev # TODO: Use commit hash tag to make the predictable builds for each commit on branch + +jobs: + migration-e2e-test: + name: Migration e2e test + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + strategy: + fail-fast: false + matrix: + e2e: + - discord + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + install-only: true + version: latest + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version-file: 'go.mod' + cache: true + - name: Run GoReleaser + run: make release-snapshot-cli + - name: Add botkube alias + run: | + echo BOTKUBE_BINARY_PATH="$PWD/dist/botkube-cli_linux_amd64_v1/botkube" >> $GITHUB_ENV + - name: Install Helm + uses: azure/setup-helm@v1 + with: + version: ${{ env.HELM_VERSION }} + - name: Download k3d + run: "wget -q -O - https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | TAG=${K3D_VERSION} bash" + - name: Create k3d cluster + run: "k3d cluster create migration-test-cluster --wait --timeout=5m" + - name: Run e2e tests for botkube client + env: + DISCORD_BOT_ID: ${{ secrets.DISCORD_BOT_ID }} + DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} + DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }} + DISCORD_TESTER_APP_TOKEN: ${{ secrets.DISCORD_TESTER_APP_TOKEN }} + BOTKUBE_CLOUD_DEV_GQL_ENDPOINT: ${{ secrets.BOTKUBE_CLOUD_DEV_GQL_ENDPOINT }} + BOTKUBE_CLOUD_DEV_REFRESH_TOKEN: ${{ secrets.BOTKUBE_CLOUD_DEV_REFRESH_TOKEN }} + BOTKUBE_CLOUD_DEV_AUTH0_CLIENT_ID: ${{ secrets.BOTKUBE_CLOUD_DEV_AUTH0_CLIENT_ID }} + run: | + KUBECONFIG=$(k3d kubeconfig write migration-test-cluster) \ + make test-migration-tool + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + if: ${{ always() }} + with: + name: screenshots_dump_${{github.sha}} + path: ${{ runner.temp }}/screenshots + retention-days: 5 + + - name: Dump cluster state + if: ${{ failure() }} + uses: ./.github/actions/dump-cluster + +# TODO: Uncomment +# slackNotification: +# name: Slack Notification +# runs-on: ubuntu-latest +# needs: [ e2e ] +# if: failure() +# steps: +# - name: Slack Notification +# uses: rtCamp/action-slack-notify@v2 +# env: +# SLACK_USERNAME: Botkube Cloud CI +# SLACK_COLOR: 'red' +# SLACK_TITLE: 'Message' +# SLACK_CHANNEL: 'botkube-cloud-ci-alerts' +# SLACK_MESSAGE: 'CLI Migration E2E tests failed :scream:' +# SLACK_ICON_EMOJI: ':this-is-fine-fire:' +# SLACK_FOOTER: "Fingers crossed it's just an outdated/flaky test..." +# SLACK_WEBHOOK: ${{ secrets.SLACK_CI_ALERTS_WEBHOOK }} diff --git a/.github/workflows/cloud-slack-dev-e2e.yaml b/.github/workflows/cloud-slack-dev-e2e.yaml new file mode 100644 index 0000000000..4515f3f4cc --- /dev/null +++ b/.github/workflows/cloud-slack-dev-e2e.yaml @@ -0,0 +1,73 @@ +name: Botkube Cloud Slack Dev E2E + +concurrency: + group: cloud-slack-dev-e2e + cancel-in-progress: false + +on: + push: + branches: + - 'main' # TODO: Ensure it runs after branch build + - 'cloud-slack-dev-e2e' # TODO: Remove before merge + repository_dispatch: + types: [ trigger-e2e-tests ] + +jobs: + e2e: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Kubernetes + uses: medyagh/setup-minikube@latest + + - name: Setup Helm + uses: azure/setup-helm@v3 + + - name: Run e2e tests + env: + SLACK_WORKSPACE_NAME: ${{ secrets.E2E_DEV_SLACK_WORKSPACE_NAME }} + SLACK_EMAIL: ${{ secrets.E2E_DEV_SLACK_EMAIL }} + SLACK_PASSWORD: ${{ secrets.E2E_DEV_SLACK_USER_PASSWORD }} + SLACK_TESTER_TESTER_BOT_TOKEN: ${{ secrets.E2E_DEV_SLACK_TESTER_BOT_TOKEN }} + SLACK_TESTER_BOT_NAME: botkubedev + BOTKUBE_CLOUD_EMAIL: ${{ secrets.E2E_DEV_BOTKUBE_CLOUD_EMAIL }} + BOTKUBE_CLOUD_PASSWORD: ${{ secrets.E2E_DEV_BOTKUBE_CLOUD_PASSWORD }} + BOTKUBE_CLOUD_TEAM_ORGANIZATION_ID: ${{ secrets.E2E_DEV_BOTKUBE_CLOUD_TEAM_ORGANIZATION_ID }} + BOTKUBE_CLOUD_FREE_ORGANIZATION_ID: ${{ secrets.E2E_DEV_BOTKUBE_CLOUD_FREE_ORGANIZATION_ID }} + SCREENSHOTS_DIR: ${{ runner.temp }}/screenshots + DEBUG_MODE: true + run: + make test-cloud-slack-dev-e2e + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + if: ${{ always() }} + with: + name: screenshots_dump_${{github.sha}} + path: ${{ runner.temp }}/screenshots + retention-days: 5 + + - name: Dump cluster state + if: ${{ failure() }} + uses: ./.github/actions/dump-cluster + + # TODO: Uncomment +# slackNotification: +# name: Slack Notification +# runs-on: ubuntu-latest +# needs: [ e2e ] +# if: failure() +# steps: +# - name: Slack Notification +# uses: rtCamp/action-slack-notify@v2 +# env: +# SLACK_USERNAME: Botkube Cloud CI +# SLACK_COLOR: 'red' +# SLACK_TITLE: 'Message' +# SLACK_CHANNEL: 'botkube-cloud-ci-alerts' +# SLACK_MESSAGE: 'Cloud Slack Dev E2E tests failed :scream:' +# SLACK_ICON_EMOJI: ':this-is-fine-fire:' +# SLACK_FOOTER: "Fingers crossed it's just an outdated/flaky test..." +# SLACK_WEBHOOK: ${{ secrets.SLACK_CI_ALERTS_WEBHOOK }} diff --git a/Makefile b/Makefile index 0ac176722d..92f158b32b 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,12 @@ test-integration-discord: system-check test-migration-tool: system-check @go test -v -tags=migration -race -count=1 ./test/e2e/... +test-cloud-slack-dev-e2e: system-check + @go test -tags=cloud_slack_dev_e2e -race -p 1 -v -timeout 30m ./test/cloud-slack-dev-e2e/... + +test-cloud-slack-dev-e2e-show-browser: system-check + @go test -tags=cloud_slack_dev_e2e -race -p 1 -v -timeout 30m -rod=show ./test/cloud-slack-dev-e2e/... + # Build the binary build: pre-build @cd cmd/botkube-agent;GOOS_VAL=$(shell go env GOOS) CGO_ENABLED=0 GOARCH_VAL=$(shell go env GOARCH) go build -o $(shell go env GOPATH)/bin/botkube diff --git a/go.mod b/go.mod index 230967f5c0..cc2a7894ab 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/go-playground/locales v0.14.0 github.com/go-playground/universal-translator v0.18.0 github.com/go-playground/validator/v10 v10.11.0 + github.com/go-rod/rod v0.113.3 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 github.com/google/go-github/v53 v53.2.0 github.com/google/go-querystring v1.1.0 @@ -236,6 +237,11 @@ require ( github.com/xlab/treeprint v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect + github.com/ysmood/fetchup v0.2.3 // indirect + github.com/ysmood/goob v0.4.0 // indirect + github.com/ysmood/got v0.34.1 // indirect + github.com/ysmood/gson v0.7.3 // indirect + github.com/ysmood/leakless v0.8.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.36.4 // indirect go.opentelemetry.io/otel v1.16.0 // indirect diff --git a/go.sum b/go.sum index ae2f39ae0f..fb089b82a4 100644 --- a/go.sum +++ b/go.sum @@ -513,6 +513,8 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-rod/rod v0.113.3 h1:oLiKZW721CCMwA5g7977cWfcAKQ+FuosP47Zf1QiDrA= +github.com/go-rod/rod v0.113.3/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -1267,6 +1269,20 @@ github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEAB github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= +github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/gop v0.0.2 h1:VuWweTmXK+zedLqYufJdh3PlxDNBOfFHjIZlPT2T5nw= +github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= +github.com/ysmood/got v0.34.1 h1:IrV2uWLs45VXNvZqhJ6g2nIhY+pgIG1CUoOcqfXFl1s= +github.com/ysmood/got v0.34.1/go.mod h1:yddyjq/PmAf08RMLSwDjPyCvHvYed+WjHnQxpH851LM= +github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= +github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.8.0 h1:BzLrVoiwxikpgEQR0Lk8NyBN5Cit2b1z+u0mgL4ZJak= +github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/test/README.md b/test/README.md index 27ac4a8ac2..00f3e134c0 100644 --- a/test/README.md +++ b/test/README.md @@ -198,3 +198,56 @@ kubectl delete cm botkube-system -n botkube # or the namespace where Botkube is ``` If you don't remove the ConfigMap, any e2e tests looking to verify that a help message is displayed will error. This also stops the rest of the e2e tests from running. + +## Testing Cloud Slack end-to-end with Botkube Cloud + +You can test Botkube Cloud Slack with Botkube Cloud. Follow the instructions below to set up the test environment and run the tests. + +### Setting up test environment + +1. Create Slack user for testing purposes with access to a given Slack workspace. +1. Create Botkube Cloud user with two organizations: FREE and TEAM one (with Cloud Slack quota). +1. Follow the [Testing Slack](#testing-slack) instructions to set up "Tester" bot. + +### Running tests + +To run the tests, get all the noted data from previous steps and export them as environment variables. + +```bash +# Required +export SLACK_WORKSPACE_NAME="" # e.g. my-workspace +# The test log ins to the Slack workspace using the credentials below +export SLACK_EMAIL="" # e.g. my-email@example.com +export SLACK_PASSWORD="" +export SLACK_TESTER_BOT_NAME="" # e.g. botkubedev +export SLACK_TESTER_TESTER_BOT_TOKEN="" # e.g. xoxb-... +export BOTKUBE_CLOUD_EMAIL="" # e.g. my-email@example.com +export BOTKUBE_CLOUD_PASSWORD="" +export BOTKUBE_CLOUD_TEAM_ORGANIZATION_ID="" # e.g. 204a2d86-265c-4ae2-89a8-928f823ec4da +export BOTKUBE_CLOUD_FREE_ORGANIZATION_ID="" # e.g. c03bd605-7b8d-490f-b4d5-57c8a0560e83 + +# Optional - useful for running tests locally +export KUBECONFIG="" # path to your kubeconfig +export BOTKUBE_CLOUD_API_BASE_URL="" e.g. http://localhost:8080 +export BOTKUBE_CLOUD_API_SLACK_APP_INSTALLATION_BASE_URL_OVERRIDE="" # provide if necessary e.g. using ngrok: https://d5ac-194-33-77-250.ngrok-free.app +export SLACK_BOT_DISPLAY_NAME="" # e.g. BotkubeDev +export SLACK_WORKSPACE_ALREADY_CONNECTED="true" +export SLACK_DISCONNECT_WORKSPACE_AFTER_TESTS="false" +export PAGE_TIMEOUT="1m" +export SCREENSHOTS_ENABLED="false" # disable screenshots +``` + +To run the test in headless mode (without browser window), run: + +```shell +make test-cloud-slack-dev-e2e +``` + +To run the tests with Chromium browser window visible, run: + +```shell +test-cloud-slack-dev-e2e-show-browser +``` + +Refer to the `E2ESlackConfig` (`./cloud-slack-dev-e2e/e2e_test.go`) for all possible environment variables. + diff --git a/test/cloud-slack-dev-e2e/e2e_test.go b/test/cloud-slack-dev-e2e/e2e_test.go new file mode 100644 index 0000000000..23045e4148 --- /dev/null +++ b/test/cloud-slack-dev-e2e/e2e_test.go @@ -0,0 +1,681 @@ +//go:build cloud_slack_dev_e2e + +package cloud_slack_dev_e2e + +import ( + "context" + "fmt" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/MakeNowJust/heredoc" + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/proto" + "github.com/hasura/go-graphql-client" + "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" + "k8s.io/client-go/util/homedir" + stringsutil "k8s.io/utils/strings" + + "github.com/kubeshop/botkube/pkg/api" + "github.com/kubeshop/botkube/pkg/bot/interactive" + "github.com/kubeshop/botkube/test/cloud_graphql" + gqlModel "github.com/kubeshop/botkube/test/cloud_graphql/model" + "github.com/kubeshop/botkube/test/commplatform" + "github.com/kubeshop/botkube/test/helmx" +) + +const ( + // Chromium is not supported by Slack web app for some reason + chromeUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + authHeaderName = "Authorization" +) + +type E2ESlackConfig struct { + Slack SlackConfig + BotkubeCloud BotkubeCloudConfig + + PageTimeout time.Duration `envconfig:"default=1m"` + ScreenshotsDir string `envconfig:"optional"` + DebugMode bool `envconfig:"default=false"` + + ClusterNamespace string `envconfig:"default=default"` + Kubeconfig string `envconfig:"optional"` + DefaultWaitTime time.Duration `envconfig:"default=10s"` +} + +type SlackConfig struct { + WorkspaceName string + Email string + Password string + BotDisplayName string `envconfig:"default=BotkubeDev"` + ConversationWithBotURL string `envconfig:"default=https://app.slack.com/client/"` + + // The properties won't be needed once we fix: https://github.com/kubeshop/botkube-cloud/issues/487 + WorkspaceAlreadyConnected bool `envconfig:"default=true"` + DisconnectWorkspaceAfterTests bool `envconfig:"default=false"` + + Tester commplatform.SlackConfig +} + +type BotkubeCloudConfig struct { + APIBaseURL string `envconfig:"default=https://api-dev.botkube.io"` + APIGraphQLEndpoint string `envconfig:"default=graphql"` + APISlackAppInstallationBaseURLOverride string `envconfig:"optional"` + APISlackAppInstallationEndpoint string `envconfig:"default=routers/slack/v1/install"` + Email string + Password string + + TeamOrganizationID string + FreeOrganizationID string +} + +func TestCloudSlackE2E(t *testing.T) { + t.Log("Loading configuration...") + var cfg E2ESlackConfig + err := envconfig.Init(&cfg) + require.NoError(t, err) + + authHeaderValue := "" + firstPageClosed := false + helmChartUninstalled := false + gqlEndpoint := fmt.Sprintf("%s/%s", cfg.BotkubeCloud.APIBaseURL, cfg.BotkubeCloud.APIGraphQLEndpoint) + + if cfg.ScreenshotsDir != "" { + t.Logf("Screenshots enabled. They will be saved to %s", cfg.ScreenshotsDir) + err = os.MkdirAll(cfg.ScreenshotsDir, os.ModePerm) + require.NoError(t, err) + } else { + t.Log("Screenshots disabled.") + } + + t.Run("Connecting app", func(t *testing.T) { + t.Log("Setting up browser...") + + launcher := launcher.New() + t.Cleanup(launcher.Cleanup) + + browser := rod.New().Trace(cfg.DebugMode).ControlURL(launcher.MustLaunch()).MustConnect() + t.Cleanup(func() { + err := browser.Close() + if err != nil { + t.Logf("Failed to close browser: %v", err) + } + }) + + page := newBrowserPage(t, browser, cfg) + t.Cleanup(func() { + if firstPageClosed { + return + } + + err := page.Close() + if err != nil { + t.Logf("Failed to close page: %v", err) + } + }) + + var slackAppInstallationURL string + if cfg.BotkubeCloud.APISlackAppInstallationBaseURLOverride == "" { + slackAppInstallationURL = fmt.Sprintf("%s/%s", cfg.BotkubeCloud.APIBaseURL, cfg.BotkubeCloud.APISlackAppInstallationEndpoint) + } else { + slackAppInstallationURL = fmt.Sprintf("%s/%s", cfg.BotkubeCloud.APISlackAppInstallationBaseURLOverride, cfg.BotkubeCloud.APISlackAppInstallationEndpoint) + } + page.MustNavigate(slackAppInstallationURL).MustWaitStable() + screenshotIfShould(t, cfg, page) + + isNgrok := strings.Contains(slackAppInstallationURL, "ngrok") + if isNgrok { + t.Log("ngrok host detected. Skipping the warning page...") + page.MustElement("button.ant-btn").MustClick() + screenshotIfShould(t, cfg, page) + } + + t.Log("Logging in to Slack...") + page.MustElement("input#domain").MustInput(cfg.Slack.WorkspaceName) + screenshotIfShould(t, cfg, page) + page.MustElementR("button", "Continue").MustClick() + screenshotIfShould(t, cfg, page) + page.MustElementR("a", "sign in with a password instead").MustClick() + screenshotIfShould(t, cfg, page) + page.MustElement("input#email").MustInput(cfg.Slack.Email) + page.MustElement("input#password").MustInput(cfg.Slack.Password) + screenshotIfShould(t, cfg, page) + page.MustElementR("button", "^Sign In$").MustClick() + screenshotIfShould(t, cfg, page) + + t.Log("Installing Slack app...") + time.Sleep(cfg.DefaultWaitTime) // ensure the screenshots shows a page after "Sign in" click + screenshotIfShould(t, cfg, page) + page.MustElementR("button.c-button:not(.c-button--disabled)", "Allow").MustClick() + screenshotIfShould(t, cfg, page) + page.MustElementR("a", "open this link in your browser") + page.MustClose() + firstPageClosed = true + + t.Log("Opening new window...") + // Workaround for the Slack protocol handler modal which cannot be closed programmatically + slackPage := newBrowserPage(t, browser, cfg) + t.Cleanup(func() { + err := slackPage.Close() + if err != nil { + t.Logf("Failed to close Slack page: %v", err) + } + }) + + t.Logf("Navigating to the conversation with %q bot...", cfg.Slack.BotDisplayName) + slackPage.MustNavigate(cfg.Slack.ConversationWithBotURL).MustWaitLoad() + screenshotIfShould(t, cfg, slackPage) + + // sometimes it shows up - not sure if that really helps as I didn't see it later ¯\_(ツ)_/¯ We need to test it + shortTimeoutPage := slackPage.Timeout(cfg.DefaultWaitTime) + elem, _ := shortTimeoutPage.Element("button.p-download_modal__not_now") + if elem != nil { + t.Log("Closing the 'Download the Slack app' modal...") + elem.MustClick() + time.Sleep(cfg.DefaultWaitTime) // to ensure the additional screenshot we do below shows closed modal + screenshotIfShould(t, cfg, slackPage) + } + + t.Log("Selecting the conversation with the bot...") + screenshotIfShould(t, cfg, slackPage) + slackPage.MustElementR(".c-scrollbar__child .c-virtual_list__scroll_container .p-channel_sidebar__static_list__item .p-channel_sidebar__name span", fmt.Sprintf("^%s$", cfg.Slack.BotDisplayName)).MustParent().MustParent().MustParent().MustClick() + screenshotIfShould(t, cfg, slackPage) + + t.Log("Clicking 'Connect' button...") + slackPage.MustElement(".p-actions_block__action button.c-button") // workaround for `MustElements` not having built-in retry + screenshotIfShould(t, cfg, slackPage) + elems := slackPage.MustElements(`.p-actions_block__action button.c-button`) + require.NotEmpty(t, elems) + t.Logf("Got %d buttons, using the last one...", len(elems)) + wait := slackPage.MustWaitOpen() + elems[len(elems)-1].MustClick() + botkubePage := wait() + t.Cleanup(func() { + err := botkubePage.Close() + if err != nil { + t.Logf("Failed to close Botkube page: %v", err) + } + }) + + t.Logf("Signing in to Botkube Cloud as %q...", cfg.BotkubeCloud.Email) + screenshotIfShould(t, cfg, botkubePage) + botkubePage.MustElement("input#username").MustInput(cfg.BotkubeCloud.Email) + botkubePage.MustElement("input#password").MustInput(cfg.BotkubeCloud.Password) + screenshotIfShould(t, cfg, botkubePage) + botkubePage.MustElementR("form button[name='action'][data-action-button-primary='true']", "Continue").MustClick() + screenshotIfShould(t, cfg, botkubePage) + + t.Logf("Starting hijacking requests to %q to get the bearer token...", gqlEndpoint) + router := browser.HijackRequests() + router.MustAdd(gqlEndpoint, func(ctx *rod.Hijack) { + if authHeaderValue != "" { + ctx.ContinueRequest(&proto.FetchContinueRequest{}) + return + } + + if ctx.Request != nil && ctx.Request.Method() != http.MethodPost { + ctx.ContinueRequest(&proto.FetchContinueRequest{}) + return + } + + require.NotNil(t, ctx.Request) + authHeaderValue = ctx.Request.Header(authHeaderName) + ctx.ContinueRequest(&proto.FetchContinueRequest{}) + }) + go router.Run() + defer router.MustStop() + + t.Log("Ensuring proper organizaton is selected") + botkubePage.MustWaitOpen() + screenshotIfShould(t, cfg, botkubePage) + botkubePage.MustElement("a.logo-link") + + pageURL := botkubePage.MustInfo().URL + urlWithOrgID := appendOrgIDQueryParam(t, pageURL, cfg.BotkubeCloud.TeamOrganizationID) + + botkubePage.MustNavigate(urlWithOrgID).MustWaitLoad() + screenshotIfShould(t, cfg, botkubePage) + botkubePage.MustElement("a.logo-link") + screenshotIfShould(t, cfg, botkubePage) + + t.Log("Finalizing Slack workspace connection...") + if cfg.Slack.WorkspaceAlreadyConnected { + botkubePage.MustElementR("div.ant-result-title", "Organization Already Connected!") + } else { + t.Log("Finalizing connection...") + botkubePage.MustElementR("button > span", "Connect").MustParent().MustClick() + // detect homepage + screenshotIfShould(t, cfg, botkubePage) + botkubePage.MustElementR(".ant-layout-content p", "All Botkube installations managed by Botkube Cloud.") + } + }) + + t.Run("Run E2E tests with deployment", func(t *testing.T) { + require.NotEmpty(t, authHeaderValue, "Previous subtest needs to pass to get authorization header value") + + t.Log("Initializing Slack...") + tester, err := commplatform.NewSlackTester(cfg.Slack.Tester) + require.NoError(t, err) + + t.Log("Initializing users...") + tester.InitUsers(t) + + t.Log("Creating channel...") + channel, createChannelCallback := tester.CreateChannel(t, "e2e-test") + t.Cleanup(func() { createChannelCallback(t) }) + + t.Log("Inviting Bot to the channel...") + tester.InviteBotToChannel(t, channel.ID()) + + t.Logf("Using Organization ID %q and Authorization header starting with %q", cfg.BotkubeCloud.TeamOrganizationID, + stringsutil.ShortenString(authHeaderValue, 15)) + + gqlCli := cloud_graphql.NewClientForAuthAndOrg(gqlEndpoint, cfg.BotkubeCloud.TeamOrganizationID, authHeaderValue) + + slackWorkspaces := gqlCli.MustListSlackWorkspacesForOrg(t, cfg.BotkubeCloud.TeamOrganizationID) + require.Len(t, slackWorkspaces, 1) + slackWorkspace := slackWorkspaces[0] + require.NotNil(t, slackWorkspace) + + t.Cleanup(func() { + if !cfg.Slack.DisconnectWorkspaceAfterTests { + return + } + gqlCli.MustDeleteSlackWorkspace(t, cfg.BotkubeCloud.TeamOrganizationID, slackWorkspace.ID) + }) + + t.Log("Creating deployment...") + deployment := gqlCli.MustCreateBasicDeploymentWithCloudSlack(t, channel.Name(), slackWorkspace.TeamID, channel.Name()) + t.Cleanup(func() { + // We have a glitch on backend side and the logic below is a workaround for that. + // Tl;dr uninstalling Helm chart reports "DISCONNECTED" status, and deplyment deletion reports "DELETED" status. + // If we do these two things too quickly, we'll run into resource version mismatch in repository logic. + // Read more here: https://github.com/kubeshop/botkube-cloud/pull/486#issuecomment-1604333794 + + for !helmChartUninstalled { + t.Log("Waiting for Helm chart uninstallation, in order to proceed with deleting the first deployment...") + time.Sleep(1 * time.Second) + } + + t.Log("Helm chart uninstalled. Waiting a bit...") + time.Sleep(3 * time.Second) // ugly, but at least we will be pretty sure we won't run into the resource version mismatch + + t.Log("Deleting first deployment...") + gqlCli.MustDeleteDeployment(t, graphql.ID(deployment.ID)) + }) + + t.Log("Creating a second deployment...") + deployment2 := gqlCli.MustCreateBasicDeploymentWithCloudSlack(t, fmt.Sprintf("%s-2", channel.Name()), slackWorkspace.TeamID, channel.Name()) + t.Cleanup(func() { + t.Log("Deleting second deployment...") + gqlCli.MustDeleteDeployment(t, graphql.ID(deployment2.ID)) + }) + + params := helmx.InstallChartParams{ + // TODO(https://github.com/kubeshop/botkube/issues/1203): Update Helm chart version to the latest released one + RepoURL: "https://charts.botkube.io", + RepoName: "botkube", + Name: "botkube", + Namespace: "botkube", + Command: *deployment.HelmCommand, + } + helmInstallCallback := helmx.InstallChart(t, params) + t.Cleanup(func() { + t.Log("Uninstalling Helm chart...") + helmInstallCallback(t) + helmChartUninstalled = true + }) + + t.Log("Waiting for help message...") + assertionFn := func(msg string) (bool, int, string) { + return strings.Contains(msg, fmt.Sprintf("Botkube instance %q is now active.", deployment.Name)), 0, "" + } + err = tester.WaitForMessagePosted(tester.BotUserID(), channel.ID(), 3, assertionFn) + + t.Run("Check basic commands", func(t *testing.T) { + t.Log("Testing ping with --cluster-name") + command := fmt.Sprintf("ping --cluster-name %s", deployment.Name) + expectedMessage := fmt.Sprintf("`%s` on `%s`\n```\npong", command, deployment.Name) + tester.PostMessageToBot(t, channel.ID(), command) + err = tester.WaitForLastMessageContains(tester.BotUserID(), channel.ID(), expectedMessage) + require.NoError(t, err) + + t.Log("Testing ping for not connected deployment #2") + command = "ping" + expectedBlockMessage := notConnectedMessage(deployment2.Name, deployment2.ID) + tester.PostMessageToBot(t, channel.ID(), fmt.Sprintf("%s --cluster-name %s", command, deployment2.Name)) + + renderedMsg := interactive.RenderMessage(tester.MDFormatter(), expectedBlockMessage) + renderedMsg = strings.Replace(renderedMsg, "\n", " ", -1) + renderedMsg = strings.TrimSuffix(renderedMsg, " ") + err = tester.WaitForLastInteractiveMessagePostedEqualWithCustomRender(tester.BotUserID(), channel.ID(), renderedMsg) + require.NoError(t, err) + + t.Log("Testing ping for not existing deployment") + command = "ping" + deployName := "non-existing-deployment" + expectedMessage = "*Instance not found* The cluster non-existing-deployment does not exist." + tester.PostMessageToBot(t, channel.ID(), fmt.Sprintf("%s --cluster-name %s", command, deployName)) + err = tester.WaitForLastMessageContains(tester.BotUserID(), channel.ID(), expectedMessage) + require.NoError(t, err) + + t.Log("Setting cluster as default") + tester.PostMessageToBot(t, channel.ID(), fmt.Sprintf("cloud set default-instance %s", deployment.ID)) + t.Log("Waiting for confirmation message...") + expectedClusterDefaultMsg := fmt.Sprintf(":white_check_mark: Instance %s was successfully selected as the default cluster for this channel.", deployment.Name) + err = tester.WaitForLastMessageEqual(tester.BotUserID(), channel.ID(), expectedClusterDefaultMsg) + require.NoError(t, err) + + t.Log("Testing getting all deployments") + command = "kubectl get deployments -A" + assertionFn := func(msg string) (bool, int, string) { + return strings.Contains(msg, heredoc.Doc(fmt.Sprintf("`%s` on `%s`", command, deployment.Name))) && + strings.Contains(msg, "coredns") && + strings.Contains(msg, "botkube"), 0, "" + } + tester.PostMessageToBot(t, channel.ID(), command) + err = tester.WaitForMessagePosted(tester.BotUserID(), channel.ID(), 1, assertionFn) + require.NoError(t, err) + }) + + t.Run("Get notifications", func(t *testing.T) { + t.Log("Creating K8s client...") + k8sCli := createK8sCli(t, cfg.Kubeconfig) + + t.Log("Creating Pod which should trigger recommendations") + podCli := k8sCli.CoreV1().Pods(cfg.ClusterNamespace) + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: channel.Name(), + Namespace: cfg.ClusterNamespace, + }, + 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) }) + + assertionFn := func(msg string) (bool, int, string) { + expStrings := []string{ + "*:large_green_circle: v1/pods created*", + fmt.Sprintf("*Name:* %s", pod.Name), + fmt.Sprintf("*Namespace:* %s", pod.Namespace), + fmt.Sprintf("*Cluster:* %s", deployment.Name), + "*Recommendations*", + 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), + fmt.Sprintf("The 'latest' tag used in 'nginx:latest' image of Pod '%s/%s' container 'nginx' should be avoided.", pod.Namespace, pod.Name), + } + + var result = true + for _, str := range expStrings { + if !strings.Contains(msg, str) { + result = false + t.Logf("Expected string not found in message: %s", str) + } + } + return result, 0, "" + } + err = tester.WaitForMessagePosted(tester.BotUserID(), channel.ID(), 1, assertionFn) + require.NoError(t, err) + }) + + t.Run("Botkube Deployment -> Cloud sync", func(t *testing.T) { + t.Log("Disabling notification...") + tester.PostMessageToBot(t, channel.ID(), "disable notifications") + t.Log("Waiting for config reload message...") + expectedReloadMsg := fmt.Sprintf(":arrows_counterclockwise: Configuration reload requested for cluster '%s'. Hold on a sec...", deployment.Name) + err = tester.WaitForMessagePostedRecentlyEqual(tester.BotUserID(), channel.ID(), expectedReloadMsg) + require.NoError(t, err) + + t.Log("Waiting for watch begin message...") + expectedWatchBeginMsg := fmt.Sprintf("My watch begins for cluster '%s'! :crossed_swords:", deployment.Name) + err = tester.WaitForLastMessageEqual(tester.BotUserID(), channel.ID(), expectedWatchBeginMsg) + require.NoError(t, err) + + t.Log("Verifying disabled notification on Cloud...") + deploy := gqlCli.MustGetDeployment(t, graphql.ID(deployment.ID)) + require.True(t, *deploy.Platforms.CloudSlacks[0].Channels[0].NotificationsDisabled) + }) + + t.Run("Cloud -> Botkube Deployment sync", func(t *testing.T) { + t.Log("Removing source binding from Slack platform & add actions") + d := gqlCli.MustGetDeployment(t, graphql.ID(deployment.ID)) // Get final resource version + deployment = removeSlackSourcesFromDeployment(t, gqlCli.Client, &d) + + t.Log("Waiting for config reload message...") + expectedReloadMsg := fmt.Sprintf(":arrows_counterclockwise: Configuration reload requested for cluster '%s'. Hold on a sec...", deployment.Name) + err = tester.WaitForMessagePostedRecentlyEqual(tester.BotUserID(), channel.ID(), expectedReloadMsg) + require.NoError(t, err) + + t.Log("Waiting for watch begin message...") + expectedWatchBeginMsg := fmt.Sprintf("My watch begins for cluster '%s'! :crossed_swords:", deployment.Name) + err = tester.WaitForLastMessageEqual(tester.BotUserID(), channel.ID(), expectedWatchBeginMsg) + require.NoError(t, err) + tester.PostMessageToBot(t, channel.ID(), "list sources") + + t.Log("Waiting for empty source list...") + expectedSourceListMsg := fmt.Sprintf("`list sources` on `%s`\n```\nSOURCE ENABLED\n```", deployment.Name) + err = tester.WaitForLastMessageEqual(tester.BotUserID(), channel.ID(), expectedSourceListMsg) + require.NoError(t, err) + tester.PostMessageToBot(t, channel.ID(), "list actions") + t.Log("Waiting for actions list...") + expectedActionsListMsg := fmt.Sprintf("`list actions` on `%s`\n```\nACTION ENABLED DISPLAY NAME\naction_xxx22 true Action Name\n```", deployment.Name) + err = tester.WaitForLastMessageEqual(tester.BotUserID(), channel.ID(), expectedActionsListMsg) + require.NoError(t, err) + }) + + t.Run("Executed commands and events are audited", func(t *testing.T) { + var auditPage struct { + Audits AuditEventPage `graphql:"auditEvents(filter: $filter, offset: $offset, limit: $limit)"` + } + variables := map[string]interface{}{ + "offset": 0, + "limit": 10, + "filter": gqlModel.AuditEventFilter{ + DeploymentID: &deployment.ID, + }, + } + + err := gqlCli.Query(context.Background(), &auditPage, variables) + require.NoError(t, err) + require.NotEmpty(t, auditPage.Audits.Data) + + t.Log("Asserting command executed events...") + botPlatform := gqlModel.BotPlatformSLACk + want := ExpectedCommandExecutedEvents([]string{ + "kubectl get deployments -A", + fmt.Sprintf("ping --cluster-name %s", deployment.Name), + "disable notifications", + "list sources", + "list actions", + }, &botPlatform, channel.Name()) + + got := CommandExecutedEventsFromAuditResponse(auditPage.Audits) + require.ElementsMatch(t, want, got) + + t.Log("Asserting source emitted events...") + wantSrcEvents := []gqlModel.SourceEventEmittedEvent{ + { + Source: &gqlModel.SourceEventDetails{ + Name: "kubernetes_config", + DisplayName: "Kubernetes Info", + }, + PluginName: "botkube/kubernetes", + }, + } + gotSrcEvents := SourceEmittedEventsFromAuditResponse(auditPage.Audits) + require.ElementsMatch(t, wantSrcEvents, gotSrcEvents) + }) + + t.Run("Try to create deployment with Cloud Slack in free tier", func(t *testing.T) { + gqlCli := cloud_graphql.NewClientForAuthAndOrg(gqlEndpoint, cfg.BotkubeCloud.FreeOrganizationID, authHeaderValue) + _, err := gqlCli.CreateBasicDeploymentWithCloudSlack(t, "it won't be created anyway", slackWorkspace.TeamID, channel.Name()) + require.Error(t, err) + require.Contains(t, err.Error(), "you cannot use Cloud Slack within your plan") + }) + }) +} + +func newBrowserPage(t *testing.T, browser *rod.Browser, cfg E2ESlackConfig) *rod.Page { + t.Helper() + + page, err := browser.Page(proto.TargetCreateTarget{URL: ""}) + require.NoError(t, err) + t.Cleanup(func() { + err := page.Close() + if err != nil { + t.Logf("Failed to close page: %s", err.Error()) + } + }) + page.MustSetUserAgent(&proto.NetworkSetUserAgentOverride{ + UserAgent: chromeUserAgent, + }) + page = page.Timeout(cfg.PageTimeout) + page.MustSetViewport(1200, 1080, 1, false) + return page +} + +func removeSlackSourcesFromDeployment(t *testing.T, gql *graphql.Client, existingDeployment *gqlModel.Deployment) *gqlModel.Deployment { + var updateInput struct { + UpdateDeployment gqlModel.Deployment `graphql:"updateDeployment(id: $id, input: $input)"` + } + var updatePluginGroup []*gqlModel.PluginConfigurationGroupUpdateInput + for _, createdPlugin := range existingDeployment.Plugins { + var pluginConfigs []*gqlModel.PluginConfigurationUpdateInput + pluginConfig := gqlModel.PluginConfigurationUpdateInput{ + Name: createdPlugin.ConfigurationName, + Configuration: createdPlugin.Configuration, + } + pluginConfigs = append(pluginConfigs, &pluginConfig) + plugin := gqlModel.PluginConfigurationGroupUpdateInput{ + ID: &createdPlugin.ID, + Name: createdPlugin.Name, + Type: createdPlugin.Type, + DisplayName: createdPlugin.DisplayName, + Configurations: pluginConfigs, + } + updatePluginGroup = append(updatePluginGroup, &plugin) + } + var updatePlugins []*gqlModel.PluginsUpdateInput + updatePlugin := gqlModel.PluginsUpdateInput{ + Groups: updatePluginGroup, + } + updatePlugins = append(updatePlugins, &updatePlugin) + + platforms := gqlModel.PlatformsUpdateInput{} + + for _, slack := range existingDeployment.Platforms.CloudSlacks { + var channelUpdateInputs []*gqlModel.ChannelBindingsByNameUpdateInput + for _, channel := range slack.Channels { + channelUpdateInputs = append(channelUpdateInputs, &gqlModel.ChannelBindingsByNameUpdateInput{ + Name: channel.Name, + Bindings: &gqlModel.BotBindingsUpdateInput{ + Sources: []*string{}, + Executors: []*string{&channel.Bindings.Executors[0]}, + }, + }) + } + platforms.CloudSlacks = append(platforms.CloudSlacks, &gqlModel.CloudSlackUpdateInput{ + ID: &slack.ID, + Name: slack.Name, + TeamID: slack.TeamID, + Channels: channelUpdateInputs, + }) + } + + updateVariables := map[string]interface{}{ + "id": graphql.ID(existingDeployment.ID), + "input": gqlModel.DeploymentUpdateInput{ + Name: existingDeployment.Name, + ResourceVersion: existingDeployment.ResourceVersion, + Plugins: updatePlugins, + Platforms: &platforms, + Actions: CreateActionUpdateInput(), + }, + } + err := gql.Mutate(context.Background(), &updateInput, updateVariables) + require.NoError(t, err) + return &updateInput.UpdateDeployment +} + +func screenshotIfShould(t *testing.T, cfg E2ESlackConfig, page *rod.Page) { + t.Helper() + if cfg.ScreenshotsDir == "" { + return + } + + pathParts := strings.Split(cfg.ScreenshotsDir, "/") + pathParts = append(pathParts) + + filePath := filepath.Join(cfg.ScreenshotsDir, fmt.Sprintf("%d.png", time.Now().UnixNano())) + + logMsg := fmt.Sprintf("Saving screenshot to %q", filePath) + if cfg.DebugMode { + logMsg += fmt.Sprintf(" for URL %q", page.MustInfo().URL) + } + t.Log(logMsg) + data := page.MustScreenshot() + err := os.WriteFile(filePath, data, 0644) + require.NoError(t, err) +} + +func appendOrgIDQueryParam(t *testing.T, inURL, orgID string) string { + parsedURL, err := url.Parse(inURL) + require.NoError(t, err) + queryValues := parsedURL.Query() + queryValues.Set("organizationId", orgID) + parsedURL.RawQuery = queryValues.Encode() + + return parsedURL.String() +} + +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) +} + +func createK8sCli(t *testing.T, kubeconfigPath string) *kubernetes.Clientset { + if kubeconfigPath == "" { + home := homedir.HomeDir() + kubeconfigPath = filepath.Join(home, ".kube", "config") + } + k8sConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) + require.NoError(t, err) + k8sCli, err := kubernetes.NewForConfig(k8sConfig) + require.NoError(t, err) + return k8sCli +} + +func notConnectedMessage(name, id string) interactive.CoreMessage { + return interactive.CoreMessage{ + Message: api.Message{ + Sections: []api.Section{ + { + Base: api.Base{ + Header: "Instance not connected", + Description: fmt.Sprintf("The cluster %s (id: %s) is not connected.", name, id), + }, + }, + }, + }, + } +} diff --git a/test/cloud-slack-dev-e2e/gql.go b/test/cloud-slack-dev-e2e/gql.go new file mode 100644 index 0000000000..ff04471747 --- /dev/null +++ b/test/cloud-slack-dev-e2e/gql.go @@ -0,0 +1,98 @@ +//go:build cloud_slack_dev_e2e + +package cloud_slack_dev_e2e + +import gqlModel "github.com/kubeshop/botkube/test/cloud_graphql/model" + +// AuditEvent represents audit event. +type AuditEvent struct { + CommandExecutedEvent `graphql:"... on CommandExecutedEvent"` + SourceEventEmittedEvent `graphql:"... on SourceEventEmittedEvent"` + Type gqlModel.AuditEventType `json:"type"` + PluginName string `json:"pluginName"` +} + +// CommandExecutedEvent represents command executed event. +type CommandExecutedEvent struct { + Command string `json:"command"` + BotPlatform *gqlModel.BotPlatform `json:"botPlatform"` + Channel string `json:"channel"` + PluginName string `json:"pluginName"` +} + +// SourceEventEmittedEvent represents source event emitted event. +type SourceEventEmittedEvent struct { + Source *gqlModel.SourceEventDetails `json:"source"` +} + +// AuditEventPage represents audit event page. +type AuditEventPage struct { + Data []AuditEvent + TotalCount int +} + +// CreateActionUpdateInput returns action create update input. +func CreateActionUpdateInput() []*gqlModel.ActionCreateUpdateInput { + var actions []*gqlModel.ActionCreateUpdateInput + source1 := "kubernetes_config" + executor1 := "kubectl_config" + actions = append(actions, &gqlModel.ActionCreateUpdateInput{ + Name: "action_xxx22", + DisplayName: "Action Name", + Enabled: true, + Command: "kc get pods", + Bindings: &gqlModel.ActionCreateUpdateInputBindings{ + Sources: []string{source1}, + Executors: []string{executor1}, + }, + }) + + return actions +} + +// ExpectedCommandExecutedEvents returns expected command executed events. +func ExpectedCommandExecutedEvents(commands []string, botPlatform *gqlModel.BotPlatform, channel string) []gqlModel.CommandExecutedEvent { + var out = make([]gqlModel.CommandExecutedEvent, 0, len(commands)) + for _, c := range commands { + out = append(out, gqlModel.CommandExecutedEvent{ + Command: c, + BotPlatform: botPlatform, + Channel: channel, + }) + } + + return out +} + +// CommandExecutedEventsFromAuditResponse returns command executed events from audit response. +func CommandExecutedEventsFromAuditResponse(auditPage AuditEventPage) []gqlModel.CommandExecutedEvent { + var out = make([]gqlModel.CommandExecutedEvent, 0, auditPage.TotalCount) + for _, a := range auditPage.Data { + if a.Type != gqlModel.AuditEventTypeCommandExecuted { + continue + } + out = append(out, gqlModel.CommandExecutedEvent{ + Command: a.Command, + BotPlatform: a.BotPlatform, + Channel: a.Channel, + }) + } + + return out +} + +// SourceEmittedEventsFromAuditResponse returns source emitted events from audit response. +func SourceEmittedEventsFromAuditResponse(auditPage AuditEventPage) []gqlModel.SourceEventEmittedEvent { + var out = make([]gqlModel.SourceEventEmittedEvent, 0, auditPage.TotalCount) + for _, a := range auditPage.Data { + if a.Type != gqlModel.AuditEventTypeSourceEventEmitted { + continue + } + out = append(out, gqlModel.SourceEventEmittedEvent{ + Source: a.Source, + PluginName: a.PluginName, + }) + } + + return out +} diff --git a/test/cloud_graphql/graphql_client.go b/test/cloud_graphql/graphql_client.go new file mode 100644 index 0000000000..3340d19380 --- /dev/null +++ b/test/cloud_graphql/graphql_client.go @@ -0,0 +1,539 @@ +//go:build cloud_slack_dev_e2e + +package cloud_graphql + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/hasura/go-graphql-client" + "github.com/stretchr/testify/require" + + "github.com/kubeshop/botkube/internal/ptr" + "github.com/kubeshop/botkube/test/cloud_graphql/model" +) + +const ( + botkubeOrganizationHeaderName = "X-Botkube-Organization-Id" + botkubeAuthorizationHeaderName = "Authorization" + //nolint:gosec // G101: Potential hardcoded credentials + botkubeAPIKeyHeaderName = "X-API-Key" +) + +// Client provides helper functions for queries and mutations that across different test cases. +// It simplifies setting up a given test prerequisites that are not a part of the test itself. +type Client struct { + *graphql.Client +} + +// MustCreateEmptyDeployment create empty deployment (without platform, plugins, etc.) +func (c *Client) MustCreateEmptyDeployment(t *testing.T) *model.Deployment { + t.Helper() + + var mutation struct { + CreateDeployment struct { + ID string `json:"id"` + Name string `json:"name"` + Status *model.DeploymentStatus `json:"status"` + APIKey *model.APIKey `json:"apiKey"` + YamlConfig *string `json:"yamlConfig"` + HelmCommand *string `json:"helmCommand"` + InstallUpgradeInstructions []*model.InstallUpgradeInstructionsForPlatform `json:"installUpgradeInstructions"` + ResourceVersion int `json:"resourceVersion"` + Heartbeat *model.Heartbeat `json:"heartbeat"` + } `graphql:"createDeployment(input: $input)"` + } + + err := c.Client.Mutate(context.Background(), &mutation, map[string]interface{}{ + "input": model.DeploymentCreateInput{ + Name: fmt.Sprintf("test/%s", t.Name()), + Platforms: &model.PlatformsCreateInput{}, + }, + }) + require.NoError(t, err) + + return &model.Deployment{ + ID: mutation.CreateDeployment.ID, + Name: mutation.CreateDeployment.Name, + Status: mutation.CreateDeployment.Status, + APIKey: mutation.CreateDeployment.APIKey, + YamlConfig: mutation.CreateDeployment.YamlConfig, + HelmCommand: mutation.CreateDeployment.HelmCommand, + InstallUpgradeInstructions: mutation.CreateDeployment.InstallUpgradeInstructions, + ResourceVersion: mutation.CreateDeployment.ResourceVersion, + Heartbeat: mutation.CreateDeployment.Heartbeat, + } +} + +// MustCreateBasicDeployment create deployment with Slack platform and three plugins. +func (c *Client) MustCreateBasicDeployment(t *testing.T) *model.Deployment { + t.Helper() + + var mutation struct { + CreateDeployment model.Deployment `graphql:"createDeployment(input: $input)"` + } + + err := c.Client.Mutate(context.Background(), &mutation, map[string]interface{}{ + "input": model.DeploymentCreateInput{ + Name: fmt.Sprintf("test/%s", t.Name()), + AttachDefaultAliases: ptr.FromType(true), + AttachDefaultActions: ptr.FromType(true), + Plugins: []*model.PluginsCreateInput{ + { + Groups: []*model.PluginConfigurationGroupInput{ + { + Name: "botkube/kubernetes", + DisplayName: "Kubernetes Info", + Type: model.PluginTypeSource, + Configurations: []*model.PluginConfigurationInput{ + { + Name: "kubernetes_config", + Configuration: "{\"recommendations\":{\"pod\":{\"noLatestImageTag\":true,\"labelsSet\":true},\"ingress\":{\"backendServiceValid\":true,\"tlsSecretValid\":true}}}", + }, + }, + }, + { + Name: "botkube/kubernetes", + DisplayName: "Kubernetes Info2", + Type: model.PluginTypeSource, + Configurations: []*model.PluginConfigurationInput{ + { + Name: "kubernetes_config2", + Configuration: "{\"recommendations\":{\"pod\":{\"noLatestImageTag\":true,\"labelsSet\":true},\"ingress\":{\"backendServiceValid\":true,\"tlsSecretValid\":true}}}", + }, + }, + }, + { + Name: "botkube/kubectl", + DisplayName: "Kubectl", + Type: model.PluginTypeExecutor, + Configurations: []*model.PluginConfigurationInput{ + { + Name: "kubectl_config", + Configuration: "{\"recommendations\":{\"pod\":{\"noLatestImageTag\":true,\"labelsSet\":true},\"ingress\":{\"backendServiceValid\":true,\"tlsSecretValid\":true}}}", + }, + }, + }, + }, + }, + }, + Platforms: &model.PlatformsCreateInput{ + SocketSlacks: []*model.SocketSlackCreateInput{ + { + Name: "slack", + AppToken: "app token", + BotToken: "bot token", + Channels: []*model.ChannelBindingsByNameCreateInput{ + { + Name: "foo", + Bindings: &model.BotBindingsCreateInput{ + Sources: []*string{ptr.FromType("kubernetes_config")}, + Executors: []*string{ptr.FromType("kubectl_config")}, + }, + NotificationsDisabled: ptr.FromType(true), + }, + { + Name: "bar", + Bindings: &model.BotBindingsCreateInput{ + Sources: []*string{ptr.FromType("kubernetes_config2")}, + Executors: []*string{}, + }, + NotificationsDisabled: ptr.FromType(true), + }, + }, + }, + }, + }, + }, + }) + require.NoError(t, err) + + return &mutation.CreateDeployment +} + +// CreateBasicDeploymentWithCloudSlack create deployment with Slack platform and three plugins. +func (c *Client) CreateBasicDeploymentWithCloudSlack(t *testing.T, clusterName, slackTeamID, channelName string) (*model.Deployment, error) { + t.Helper() + + var mutation struct { + CreateDeployment *model.Deployment `graphql:"createDeployment(input: $input)"` + } + + err := c.Client.Mutate(context.Background(), &mutation, map[string]interface{}{ + "input": model.DeploymentCreateInput{ + Name: clusterName, + Plugins: []*model.PluginsCreateInput{ + { + Groups: []*model.PluginConfigurationGroupInput{ + { + Name: "botkube/kubernetes", + DisplayName: "Kubernetes Info", + Type: model.PluginTypeSource, + Configurations: []*model.PluginConfigurationInput{ + { + Name: "kubernetes_config", + Configuration: "{\"recommendations\":{\"pod\":{\"noLatestImageTag\":true,\"labelsSet\":true},\"ingress\":{\"backendServiceValid\":true,\"tlsSecretValid\":true}},\"namespaces\":{\"include\":[\"default\"],\"exclude\":[]},\"event\":{\"types\":[\"create\"]},\"resources\":[{\"type\":\"v1/pods\"},{\"type\":\"v1/services\"},{\"type\":\"networking.k8s.io/v1/ingresses\"},{\"type\":\"v1/nodes\"},{\"type\":\"v1/namespaces\"},{\"type\":\"v1/persistentvolumes\"},{\"type\":\"v1/persistentvolumeclaims\"},{\"type\":\"v1/configmaps\"},{\"type\":\"rbac.authorization.k8s.io/v1/roles\"},{\"type\":\"rbac.authorization.k8s.io/v1/rolebindings\"},{\"type\":\"rbac.authorization.k8s.io/v1/clusterrolebindings\"},{\"type\":\"rbac.authorization.k8s.io/v1/clusterroles\"},{\"type\":\"apps/v1/deployments\"},{\"type\":\"apps/v1/statefulsets\"},{\"type\":\"apps/v1/daemonsets\"},{\"type\":\"batch/v1/jobs\"}],\"commands\":{\"verbs\":[\"api-resources\",\"api-versions\",\"cluster-info\",\"describe\",\"explain\",\"get\",\"logs\",\"top\"],\"resources\":[\"deployments\",\"pods\",\"namespaces\",\"daemonsets\",\"statefulsets\",\"storageclasses\",\"nodes\",\"configmaps\",\"services\",\"ingresses\"]},\"filters\":{\"objectAnnotationChecker\":true,\"nodeEventsChecker\":true},\"informerResyncPeriod\":\"30m\",\"log\":{\"level\":\"info\",\"disableColors\":false}}", + }, + }, + }, + { + Name: "botkube/kubernetes", + DisplayName: "Kubernetes Info2", + Type: model.PluginTypeSource, + Configurations: []*model.PluginConfigurationInput{ + { + Name: "kubernetes_config2", + Configuration: "{\"recommendations\":{\"pod\":{\"noLatestImageTag\":true,\"labelsSet\":true},\"ingress\":{\"backendServiceValid\":true,\"tlsSecretValid\":true}}}", + }, + }, + }, + { + Name: "botkube/kubectl", + DisplayName: "Kubectl", + Type: model.PluginTypeExecutor, + Configurations: []*model.PluginConfigurationInput{ + { + Name: "kubectl_config", + Configuration: "{\"recommendations\":{\"pod\":{\"noLatestImageTag\":true,\"labelsSet\":true},\"ingress\":{\"backendServiceValid\":true,\"tlsSecretValid\":true}}}", + }, + }, + }, + }, + }, + }, + Platforms: &model.PlatformsCreateInput{ + CloudSlacks: []*model.CloudSlackCreateInput{ + { + Name: "Cloud Slack", + TeamID: slackTeamID, + Channels: []*model.ChannelBindingsByNameCreateInput{ + { + Name: channelName, + Bindings: &model.BotBindingsCreateInput{ + Sources: []*string{ptr.FromType("kubernetes_config")}, + Executors: []*string{ptr.FromType("kubectl_config")}, + }, + NotificationsDisabled: nil, + }, + }, + }, + }, + }, + }, + }) + return mutation.CreateDeployment, err +} + +// MustCreateBasicDeploymentWithCloudSlack is like CreateBasicDeploymentWithCloudSlack but fails on error. +func (c *Client) MustCreateBasicDeploymentWithCloudSlack(t *testing.T, clusterName, slackTeamID, channelName string) *model.Deployment { + t.Helper() + deployment, err := c.CreateBasicDeploymentWithCloudSlack(t, clusterName, slackTeamID, channelName) + require.NoError(t, err) + return deployment +} + +type ( + // Organization is a custom model that allow us to skip the 'connectedPlatforms.slack' field. Otherwise, we get such error: + // + // Field "slack" argument "id" of type "ID!" is required, but it was not provided. + Organization struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Subscription *model.OrganizationSubscription `json:"subscription"` + ConnectedPlatforms *OrganizationConnectedPlatforms `json:"connectedPlatforms"` + OwnerID string `json:"ownerId"` + Owner *model.User `json:"owner"` + Members []*model.User `json:"members"` + Quota *model.Quota `json:"quota"` + BillingHistoryAvailable bool `json:"billingHistoryAvailable"` + UpdateOperations *model.OrganizationUpdateOperations `json:"updateOperations"` + Usage *model.Usage `json:"usage"` + } + // Organizations holds organization collection. + Organizations []Organization + + // OrganizationConnectedPlatforms skips the 'slack' field. + OrganizationConnectedPlatforms struct { + Slacks []*model.SlackWorkspace `json:"slacks"` + } +) + +// ToModel returns official gql model. +func (o Organization) ToModel() model.Organization { + return model.Organization{ + ID: o.ID, + DisplayName: o.DisplayName, + Subscription: o.Subscription, + ConnectedPlatforms: &model.OrganizationConnectedPlatforms{ + Slacks: o.ConnectedPlatforms.Slacks, + }, + OwnerID: o.OwnerID, + Owner: o.Owner, + Members: o.Members, + Quota: o.Quota, + BillingHistoryAvailable: o.BillingHistoryAvailable, + UpdateOperations: o.UpdateOperations, + Usage: o.Usage, + } +} + +// ToModel returns official gql model. +func (o Organizations) ToModel() []model.Organization { + var out []model.Organization + for _, item := range o { + out = append(out, item.ToModel()) + } + return out +} + +// MustCreateOrganization creates organization. +func (c *Client) MustCreateOrganization(t *testing.T) model.Organization { + t.Helper() + + var mutation struct { + CreateOrganization Organization `graphql:"createOrganization(input: $input)"` + } + + err := c.Client.Mutate(context.Background(), &mutation, map[string]interface{}{ + "input": model.OrganizationCreateInput{ + DisplayName: fmt.Sprintf("My %s organization:%s", t.Name(), uuid.NewString()), + }, + }) + require.NoError(t, err) + + return mutation.CreateOrganization.ToModel() +} + +// MustGetOrganization gets organization. +func (c *Client) MustGetOrganization(t *testing.T, id graphql.ID) model.Organization { + t.Helper() + + var query struct { + Organization Organization `graphql:"organization(id: $id)"` + } + + err := c.Client.Query(context.Background(), &query, map[string]interface{}{ + "id": id, + }) + require.NoError(t, err) + + return query.Organization.ToModel() +} + +// MustAddMember adds member to organization. +func (c *Client) MustAddMember(t *testing.T, input model.AddMemberForOrganizationInput) model.Organization { + t.Helper() + + var mutation struct { + AddMember Organization `graphql:"addMemberForOrganization(input: $input)"` + } + + err := c.Client.Mutate(context.Background(), &mutation, map[string]interface{}{ + "input": input, + }) + require.NoError(t, err) + + return mutation.AddMember.ToModel() +} + +// MustRemoveMember removes member from organization. +func (c *Client) MustRemoveMember(t *testing.T, input model.RemoveMemberFromOrganizationInput) model.Organization { + t.Helper() + + var mutation struct { + RemoveMember Organization `graphql:"removeMemberFromOrganization(input: $input)"` + } + + err := c.Client.Mutate(context.Background(), &mutation, map[string]interface{}{ + "input": input, + }) + require.NoError(t, err) + + return mutation.RemoveMember.ToModel() +} + +// MustListAliases returns all aliases scoped to a given user. +func (c *Client) MustListAliases(t *testing.T) []*model.Alias { + t.Helper() + + var page struct { + Aliases model.AliasPage `graphql:"aliases(offset: $offset, limit: $limit)"` + } + + err := c.Client.Query(context.Background(), &page, c.pagingVariables()) + require.NoError(t, err) + return page.Aliases.Data +} + +// MustGetDeployment returns a given deployment scoped to a given user. +func (c *Client) MustGetDeployment(t *testing.T, id graphql.ID) model.Deployment { + t.Helper() + + var query struct { + Deployment model.Deployment `graphql:"deployment(id: $id)"` + } + + err := c.Client.Query(context.Background(), &query, map[string]interface{}{"id": id}) + require.NoError(t, err) + return query.Deployment +} + +// MustDeleteDeployment is like DeleteDeployment but panics on error. +func (c *Client) MustDeleteDeployment(t *testing.T, id graphql.ID) { + err := c.DeleteDeployment(t, id) + require.NoError(t, err) +} + +// DeleteDeployment deletes a given deployment scoped to a given user. +func (c *Client) DeleteDeployment(t *testing.T, id graphql.ID) error { + t.Helper() + + var mutation struct { + Deployment bool `graphql:"deleteDeployment(id: $id)"` + } + + return c.Client.Mutate(context.Background(), &mutation, map[string]interface{}{"id": id}) +} + +// MustListDeployments returns all deployments scoped to a given user. +func (c *Client) MustListDeployments(t *testing.T) []*model.Deployment { + t.Helper() + + var page struct { + Deployments model.DeploymentPage `graphql:"deployments(offset: $offset, limit: $limit)"` + } + + err := c.Client.Query(context.Background(), &page, c.pagingVariables()) + require.NoError(t, err) + return page.Deployments.Data +} + +// MustListAudits returns all audits scoped to a given user. +func (c *Client) MustListAudits(t *testing.T) []model.AuditEvent { + t.Helper() + + var page struct { + Audits model.AuditEventPage `graphql:"auditEvents(offset: $offset, limit: $limit)"` + } + + err := c.Client.Query(context.Background(), &page, c.pagingVariables()) + require.NoError(t, err) + return page.Audits.Data +} + +// MustReportDeploymentHeartbeat sends heartbeat info. +func (c *Client) MustReportDeploymentHeartbeat(t *testing.T, deploymentId string, nodeCount int) bool { + t.Helper() + + var mutation struct { + ReportDeploymentHeartbeat bool `graphql:"reportDeploymentHeartbeat(id: $id, in: $in)"` + } + + err := c.Client.Mutate(context.Background(), &mutation, map[string]interface{}{ + "id": graphql.ID(deploymentId), + "in": model.DeploymentHeartbeatInput{ + NodeCount: nodeCount, + }, + }) + require.NoError(t, err) + + return mutation.ReportDeploymentHeartbeat +} + +// MustReportDeploymentStartup sends startup report. +func (c *Client) MustReportDeploymentStartup(t *testing.T, deploymentId string) bool { + t.Helper() + + var mutation struct { + ReportDeploymentStartup bool `graphql:"reportDeploymentStartup(id: $id, resourceVersion: $in)"` + } + + err := c.Client.Mutate(context.Background(), &mutation, map[string]interface{}{ + "id": graphql.ID(deploymentId), + "in": 1, + }) + require.NoError(t, err) + + return mutation.ReportDeploymentStartup +} + +// MustDeleteSlackWorkspace deletes a slack workspace. +func (c *Client) MustDeleteSlackWorkspace(t *testing.T, orgID, slackWorkspaceID string) { + t.Helper() + + type Identifiable struct { + ID string `graphql:"id"` + } + + var mutation struct { + RemovePlatformFromOrganization Identifiable `graphql:"removePlatformFromOrganization(input: $input)"` + } + + err := c.Client.Mutate(context.Background(), &mutation, map[string]interface{}{ + "input": model.RemovePlatformFromOrganizationInput{ + OrganizationID: orgID, + Slack: &model.RemoveSlackFromOrganizationInput{ + ID: slackWorkspaceID, + }, + }, + }) + require.NoError(t, err) +} + +// MustListSlackWorkspacesForOrg returns all slack workspaces scoped to a given organization. +func (c *Client) MustListSlackWorkspacesForOrg(t *testing.T, orgID string) []*model.SlackWorkspace { + t.Helper() + + var query struct { + Organization Organization `graphql:"organization(id: $id)"` + } + + err := c.Client.Query(context.Background(), &query, map[string]interface{}{ + "id": graphql.ID(orgID), + }) + require.NoError(t, err) + + require.NotNil(t, query.Organization.ConnectedPlatforms) + require.NotEmpty(t, query.Organization.ConnectedPlatforms.Slacks) + return query.Organization.ConnectedPlatforms.Slacks +} + +// NewClientForOrganization returns new GraphQL client with organization header. +func (c *Client) NewClientForOrganization(id string) *Client { + return &Client{ + Client: c.Client.WithRequestModifier(func(request *http.Request) { + request.Header.Set(botkubeOrganizationHeaderName, id) + }), + } +} + +// NewClientForAuthAndOrg returns new GraphQL client with organization and authorization headers. +func NewClientForAuthAndOrg(apiEndpoint, orgID, authValue string) *Client { + gqLCli := graphql.NewClient(apiEndpoint, nil) + + return &Client{ + Client: gqLCli.WithRequestModifier(func(request *http.Request) { + request.Header.Set(botkubeOrganizationHeaderName, orgID) + request.Header.Set(botkubeAuthorizationHeaderName, authValue) + }), + } +} + +// NewClientForAPIKey returns new GraphQL client with API Key header. +func (c *Client) NewClientForAPIKey(key string) *Client { + return &Client{ + Client: c.Client.WithRequestModifier(func(request *http.Request) { + request.Header.Set(botkubeAPIKeyHeaderName, key) + }), + } +} + +func (c *Client) pagingVariables() map[string]interface{} { + return map[string]interface{}{ + "offset": 0, + "limit": 100, + } +} diff --git a/test/cloud_graphql/model/connected_platforms.go b/test/cloud_graphql/model/connected_platforms.go new file mode 100644 index 0000000000..c42727d86b --- /dev/null +++ b/test/cloud_graphql/model/connected_platforms.go @@ -0,0 +1,8 @@ +package model + +// OrganizationConnectedPlatforms represents connected platforms. +type OrganizationConnectedPlatforms struct { + OrganizationID string `graphql:"-"` + Slacks []*SlackWorkspace `json:"slacks"` + Slack *SlackWorkspace `json:"slack"` +} diff --git a/test/cloud_graphql/model/doc.go b/test/cloud_graphql/model/doc.go new file mode 100644 index 0000000000..68f1d8a681 --- /dev/null +++ b/test/cloud_graphql/model/doc.go @@ -0,0 +1,3 @@ +// Package model contains copied GraphQL models generated by gqlgen for Botkube Cloud GraphQL API. +// Apart from the generated `models_gen.go` file, other files contain copied, customized models. +package model diff --git a/test/cloud_graphql/model/models_gen.go b/test/cloud_graphql/model/models_gen.go new file mode 100644 index 0000000000..f574ed1967 --- /dev/null +++ b/test/cloud_graphql/model/models_gen.go @@ -0,0 +1,1223 @@ +// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. + +package model + +import ( + "fmt" + "io" + "strconv" +) + +type AuditEvent interface { + IsAuditEvent() + GetID() string + GetType() *AuditEventType + GetDeploymentID() string + GetCreatedAt() string + GetPluginName() string + GetDeployment() *Deployment +} + +type Pageable interface { + IsPageable() + GetPageInfo() *PageInfo + GetTotalCount() int +} + +type Action struct { + ID string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"displayName"` + Enabled bool `json:"enabled"` + Command string `json:"command"` + Bindings *ActionBindings `json:"bindings"` +} + +type ActionBindings struct { + Sources []string `json:"sources"` + Executors []string `json:"executors"` +} + +type ActionCreateUpdateInput struct { + ID *string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"displayName"` + Enabled bool `json:"enabled"` + Command string `json:"command"` + Bindings *ActionCreateUpdateInputBindings `json:"bindings"` +} + +type ActionCreateUpdateInputBindings struct { + Sources []string `json:"sources"` + Executors []string `json:"executors"` +} + +type ActionPatchDeploymentConfigInput struct { + Name string `json:"name"` + Enabled *bool `json:"enabled"` +} + +type AddMemberForOrganizationInput struct { + OrgID string `json:"orgId"` + UserID *string `json:"userId"` + UserEmail *string `json:"userEmail"` +} + +type AddPlatformToOrganizationInput struct { + OrganizationID string `json:"organizationId"` + Slack *AddSlackToOrganizationInput `json:"slack"` +} + +type AddSlackToOrganizationInput struct { + Token string `json:"token"` +} + +type Alias struct { + ID string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"displayName"` + Command string `json:"command"` + Deployments []*DeploymentInfo `json:"deployments"` +} + +type AliasCreateInput struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Command string `json:"command"` + DeploymentIds []string `json:"deploymentIds"` +} + +type AliasPage struct { + Data []*Alias `json:"data"` + PageInfo *PageInfo `json:"pageInfo"` + TotalCount int `json:"totalCount"` + TotalPages int `json:"totalPages"` +} + +func (AliasPage) IsPageable() {} +func (this AliasPage) GetPageInfo() *PageInfo { return this.PageInfo } +func (this AliasPage) GetTotalCount() int { return this.TotalCount } + +type AliasUpdateInput struct { + Name *string `json:"name"` + DisplayName *string `json:"displayName"` + Command *string `json:"command"` + DeploymentIds []string `json:"deploymentIds"` +} + +type APICallEvent struct { + ID string `json:"id"` + Type *AuditEventType `json:"type"` + DeploymentID string `json:"deploymentId"` + CreatedAt string `json:"createdAt"` + PluginName string `json:"pluginName"` + Deployment *Deployment `json:"deployment"` + User string `json:"user"` + GqlType APICallEventQglType `json:"gqlType"` + GqlName string `json:"gqlName"` + RequestBody string `json:"requestBody"` + ResponseBody string `json:"responseBody"` +} + +func (APICallEvent) IsAuditEvent() {} +func (this APICallEvent) GetID() string { return this.ID } +func (this APICallEvent) GetType() *AuditEventType { return this.Type } +func (this APICallEvent) GetDeploymentID() string { return this.DeploymentID } +func (this APICallEvent) GetCreatedAt() string { return this.CreatedAt } +func (this APICallEvent) GetPluginName() string { return this.PluginName } +func (this APICallEvent) GetDeployment() *Deployment { return this.Deployment } + +type APIKey struct { + Name string `json:"name"` + Value string `json:"value"` +} + +type AuditEventCommandCreateInput struct { + PlatformUser string `json:"platformUser"` + Channel string `json:"channel"` + BotPlatform *BotPlatform `json:"botPlatform"` + Command string `json:"command"` +} + +type AuditEventCreateInput struct { + Type AuditEventType `json:"type"` + CreatedAt string `json:"createdAt"` + DeploymentID string `json:"deploymentId"` + PluginName string `json:"pluginName"` + SourceEventEmitted *AuditEventSourceCreateInput `json:"sourceEventEmitted"` + CommandExecuted *AuditEventCommandCreateInput `json:"commandExecuted"` +} + +type AuditEventFilter struct { + DeploymentID *string `json:"deploymentId"` + StartDate *string `json:"startDate"` + EndDate *string `json:"endDate"` +} + +type AuditEventPage struct { + Data []AuditEvent `json:"data"` + PageInfo *PageInfo `json:"pageInfo"` + TotalCount int `json:"totalCount"` + TotalPages int `json:"totalPages"` +} + +func (AuditEventPage) IsPageable() {} +func (this AuditEventPage) GetPageInfo() *PageInfo { return this.PageInfo } +func (this AuditEventPage) GetTotalCount() int { return this.TotalCount } + +type AuditEventSourceCreateInput struct { + Event string `json:"event"` + Source *AuditEventSourceDetailsInput `json:"source"` +} + +type AuditEventSourceDetailsInput struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` +} + +type BotBindings struct { + Sources []string `json:"sources"` + Executors []string `json:"executors"` +} + +type BotBindingsCreateInput struct { + Sources []*string `json:"sources"` + Executors []*string `json:"executors"` +} + +type BotBindingsUpdateInput struct { + Sources []*string `json:"sources"` + Executors []*string `json:"executors"` +} + +type ChannelBindingsByID struct { + ID string `json:"id"` + Bindings *BotBindings `json:"bindings"` + NotificationsDisabled *bool `json:"notificationsDisabled"` +} + +type ChannelBindingsByIDCreateInput struct { + ID string `json:"id"` + Bindings *BotBindingsCreateInput `json:"bindings"` + NotificationsDisabled *bool `json:"notificationsDisabled"` +} + +type ChannelBindingsByIDUpdateInput struct { + ID string `json:"id"` + Bindings *BotBindingsUpdateInput `json:"bindings"` +} + +type ChannelBindingsByName struct { + Name string `json:"name"` + Bindings *BotBindings `json:"bindings"` + NotificationsDisabled *bool `json:"notificationsDisabled"` +} + +type ChannelBindingsByNameCreateInput struct { + Name string `json:"name"` + Bindings *BotBindingsCreateInput `json:"bindings"` + NotificationsDisabled *bool `json:"notificationsDisabled"` +} + +type ChannelBindingsByNameUpdateInput struct { + Name string `json:"name"` + Bindings *BotBindingsUpdateInput `json:"bindings"` +} + +type CloudSlack struct { + ID string `json:"id"` + Name string `json:"name"` + TeamID string `json:"teamId"` + BotToken string `json:"botToken"` + Channels []*ChannelBindingsByName `json:"channels"` +} + +type CloudSlackCreateInput struct { + Name string `json:"name"` + TeamID string `json:"teamId"` + Channels []*ChannelBindingsByNameCreateInput `json:"channels"` +} + +type CloudSlackUpdateInput struct { + ID *string `json:"id"` + Name string `json:"name"` + TeamID string `json:"teamId"` + Channels []*ChannelBindingsByNameUpdateInput `json:"channels"` +} + +type CommandExecutedEvent struct { + ID string `json:"id"` + Type *AuditEventType `json:"type"` + PlatformUser *string `json:"platformUser"` + DeploymentID string `json:"deploymentId"` + Deployment *Deployment `json:"deployment"` + CreatedAt string `json:"createdAt"` + Command string `json:"command"` + BotPlatform *BotPlatform `json:"botPlatform"` + Channel string `json:"channel"` + PluginName string `json:"pluginName"` +} + +func (CommandExecutedEvent) IsAuditEvent() {} +func (this CommandExecutedEvent) GetID() string { return this.ID } +func (this CommandExecutedEvent) GetType() *AuditEventType { return this.Type } +func (this CommandExecutedEvent) GetDeploymentID() string { return this.DeploymentID } +func (this CommandExecutedEvent) GetCreatedAt() string { return this.CreatedAt } +func (this CommandExecutedEvent) GetPluginName() string { return this.PluginName } +func (this CommandExecutedEvent) GetDeployment() *Deployment { return this.Deployment } + +type ConnectedPlatforms struct { + Slack *SlackWorkspace `json:"slack"` +} + +type Coupon struct { + Name string `json:"name"` + AmountOff *int `json:"amountOff"` + PercentOff *float64 `json:"percentOff"` + Duration StripeCouponDuration `json:"duration"` + DurationInMonths *int `json:"durationInMonths"` +} + +type DeleteByIDInput struct { + ID string `json:"ID"` +} + +type DeletePlatformInput struct { + SocketSlack *DeleteByIDInput `json:"socketSlack"` + CloudSlack *DeleteByIDInput `json:"cloudSlack"` + Discord *DeleteByIDInput `json:"discord"` + Mattermost *DeleteByIDInput `json:"mattermost"` + Webhook *DeleteByIDInput `json:"webhook"` + MsTeams *DeleteByIDInput `json:"msTeams"` + Elasticsearch *DeleteByIDInput `json:"elasticsearch"` +} + +type Deployment struct { + ID string `json:"id"` + Name string `json:"name"` + Actions []*Action `json:"actions"` + Plugins []*Plugin `json:"plugins"` + Platforms *Platforms `json:"platforms"` + Status *DeploymentStatus `json:"status"` + APIKey *APIKey `json:"apiKey"` + YamlConfig *string `json:"yamlConfig"` + Aliases []*Alias `json:"aliases"` + HelmCommand *string `json:"helmCommand"` + ResourceVersion int `json:"resourceVersion"` + Heartbeat *Heartbeat `json:"heartbeat"` + InstallUpgradeInstructions []*InstallUpgradeInstructionsForPlatform `json:"installUpgradeInstructions"` +} + +type DeploymentConfig struct { + ResourceVersion int `json:"resourceVersion"` + Value interface{} `json:"value"` +} + +type DeploymentCreateInput struct { + Name string `json:"name"` + Plugins []*PluginsCreateInput `json:"plugins"` + Platforms *PlatformsCreateInput `json:"platforms"` + Actions []*ActionCreateUpdateInput `json:"actions"` + AttachDefaultAliases *bool `json:"attachDefaultAliases"` + AttachDefaultActions *bool `json:"attachDefaultActions"` +} + +type DeploymentFailureInput struct { + ResourceVersion int `json:"resourceVersion"` + Message string `json:"message"` +} + +type DeploymentHeartbeatInput struct { + NodeCount int `json:"nodeCount"` +} + +type DeploymentInfo struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type DeploymentPage struct { + Data []*Deployment `json:"data"` + PageInfo *PageInfo `json:"pageInfo"` + TotalCount int `json:"totalCount"` +} + +func (DeploymentPage) IsPageable() {} +func (this DeploymentPage) GetPageInfo() *PageInfo { return this.PageInfo } +func (this DeploymentPage) GetTotalCount() int { return this.TotalCount } + +type DeploymentStatus struct { + Phase DeploymentStatusPhase `json:"phase"` + Message *string `json:"message"` + BotkubeVersion *string `json:"botkubeVersion"` + Upgrade *DeploymentUpgradeStatus `json:"upgrade"` + LastTransitionTime *string `json:"lastTransitionTime"` +} + +type DeploymentStatusInput struct { + Message *string `json:"message"` + Phase *DeploymentStatusPhase `json:"phase"` +} + +type DeploymentUpdateInput struct { + Name string `json:"name"` + Platforms *PlatformsUpdateInput `json:"platforms"` + Plugins []*PluginsUpdateInput `json:"plugins"` + Actions []*ActionCreateUpdateInput `json:"actions"` + ResourceVersion int `json:"resourceVersion"` +} + +type DeploymentUpgradeStatus struct { + NeedsUpgrade bool `json:"needsUpgrade"` + TargetBotkubeVersion string `json:"targetBotkubeVersion"` +} + +type Discord struct { + ID string `json:"id"` + Name string `json:"name"` + Token string `json:"token"` + BotID string `json:"botId"` + Channels []*ChannelBindingsByID `json:"channels"` +} + +type DiscordCreateInput struct { + Name string `json:"name"` + Token string `json:"token"` + BotID string `json:"botId"` + Channels []*ChannelBindingsByIDCreateInput `json:"channels"` +} + +type DiscordUpdateInput struct { + ID *string `json:"id"` + Name string `json:"name"` + Token string `json:"token"` + BotID string `json:"botId"` + Channels []*ChannelBindingsByIDUpdateInput `json:"channels"` +} + +type Elasticsearch struct { + ID string `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + Password string `json:"password"` + Server string `json:"server"` + SkipTLSVerify bool `json:"skipTlsVerify"` + AwsSigningRegion *string `json:"awsSigningRegion"` + AwsSigningRoleArn *string `json:"awsSigningRoleArn"` + Indices []*ElasticsearchIndex `json:"indices"` +} + +type ElasticsearchCreateInput struct { + Name string `json:"name"` + Username string `json:"username"` + Password string `json:"password"` + Server string `json:"server"` + SkipTLSVerify bool `json:"skipTlsVerify"` + AwsSigningRegion *string `json:"awsSigningRegion"` + AwsSigningRoleArn *string `json:"awsSigningRoleArn"` + Indices []*ElasticsearchIndexCreateInput `json:"indices"` +} + +type ElasticsearchIndex struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Shards int `json:"shards"` + Replicas int `json:"replicas"` + Bindings *SinkBindings `json:"bindings"` +} + +type ElasticsearchIndexCreateInput struct { + Name string `json:"name"` + Type string `json:"type"` + Shards int `json:"shards"` + Replicas int `json:"replicas"` + Bindings *SinkBindingsCreateInput `json:"bindings"` +} + +type ElasticsearchIndexUpdateInput struct { + Name string `json:"name"` + Type string `json:"type"` + Shards int `json:"shards"` + Replicas int `json:"replicas"` + Bindings *SinkBindingsUpdateInput `json:"bindings"` +} + +type ElasticsearchUpdateInput struct { + ID *string `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + Password string `json:"password"` + Server string `json:"server"` + SkipTLSVerify bool `json:"skipTlsVerify"` + AwsSigningRegion *string `json:"awsSigningRegion"` + AwsSigningRoleArn *string `json:"awsSigningRoleArn"` + Indices []*ElasticsearchIndexUpdateInput `json:"indices"` +} + +type GroupPolicySubject struct { + Type PolicySubjectType `json:"type"` + Static *GroupStaticSubject `json:"static"` + Prefix *string `json:"prefix"` +} + +type GroupPolicySubjectInput struct { + Type PolicySubjectType `json:"type"` + Static *GroupStaticSubjectInput `json:"static"` + Prefix *string `json:"prefix"` +} + +type GroupStaticSubject struct { + Values []string `json:"values"` +} + +type GroupStaticSubjectInput struct { + Values []string `json:"values"` +} + +type Heartbeat struct { + NodeCount *int `json:"nodeCount"` +} + +type HubSpotIdentificationToken struct { + Token string `json:"token"` +} + +type HubspotIdentificationTokenInput struct { + Email string `json:"email"` + FirstName *string `json:"firstName"` + LastName *string `json:"lastName"` +} + +type InstallUpgradeInstructionsForPlatform struct { + PlatformName string `json:"platformName"` + Prerequisites []*InstallUpgradePrerequisite `json:"prerequisites"` + InstallUpgradeCommand string `json:"installUpgradeCommand"` +} + +type InstallUpgradePrerequisite struct { + Description *string `json:"description"` + Command *string `json:"command"` +} + +type Invoice struct { + IsOnTrial bool `json:"isOnTrial"` + UpcomingAmount int `json:"upcomingAmount"` + Currency string `json:"currency"` + EndOfBillingCycleDate *string `json:"endOfBillingCycleDate"` + EndOfTrialDate *string `json:"endOfTrialDate"` + Items []*InvoiceItem `json:"items"` + Coupon *Coupon `json:"coupon"` +} + +type InvoiceItem struct { + Amount int `json:"amount"` + PriceUnitAmount string `json:"priceUnitAmount"` + Currency string `json:"currency"` + Description *string `json:"description"` +} + +type Mattermost struct { + ID string `json:"id"` + Name string `json:"name"` + BotName string `json:"botName"` + URL string `json:"url"` + Token string `json:"token"` + Team string `json:"team"` + Channels []*ChannelBindingsByName `json:"channels"` +} + +type MattermostCreateInput struct { + Name string `json:"name"` + BotName string `json:"botName"` + URL string `json:"url"` + Token string `json:"token"` + Team string `json:"team"` + Channels []*ChannelBindingsByNameCreateInput `json:"channels"` +} + +type MattermostUpdateInput struct { + ID *string `json:"id"` + Name string `json:"name"` + BotName string `json:"botName"` + URL string `json:"url"` + Token string `json:"token"` + Team string `json:"team"` + Channels []*ChannelBindingsByNameUpdateInput `json:"channels"` +} + +type MsTeams struct { + ID string `json:"id"` + Name string `json:"name"` + BotName string `json:"botName"` + AppID string `json:"appId"` + AppPassword string `json:"appPassword"` + Port string `json:"port"` + MessagePath string `json:"messagePath"` + NotificationsDisabled *bool `json:"notificationsDisabled"` + Bindings *BotBindings `json:"bindings"` +} + +type MsTeamsCreateInput struct { + Name string `json:"name"` + BotName string `json:"botName"` + AppID string `json:"appId"` + AppPassword string `json:"appPassword"` + Port string `json:"port"` + MessagePath string `json:"messagePath"` + NotificationsDisabled *bool `json:"notificationsDisabled"` + Bindings *BotBindingsCreateInput `json:"bindings"` +} + +type MsTeamsUpdateInput struct { + ID *string `json:"id"` + Name string `json:"name"` + BotName string `json:"botName"` + AppID string `json:"appId"` + AppPassword string `json:"appPassword"` + Port string `json:"port"` + MessagePath string `json:"messagePath"` + Bindings *BotBindingsUpdateInput `json:"bindings"` +} + +type NotificationPatchDeploymentConfigInput struct { + CommunicationGroupName string `json:"communicationGroupName"` + Platform BotPlatform `json:"platform"` + ChannelAlias string `json:"channelAlias"` + Disabled bool `json:"disabled"` +} + +type Organization struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Subscription *OrganizationSubscription `json:"subscription"` + ConnectedPlatforms *OrganizationConnectedPlatforms `json:"connectedPlatforms"` + OwnerID string `json:"ownerId"` + Owner *User `json:"owner"` + Members []*User `json:"members"` + Quota *Quota `json:"quota"` + BillingHistoryAvailable bool `json:"billingHistoryAvailable"` + UpdateOperations *OrganizationUpdateOperations `json:"updateOperations"` + Usage *Usage `json:"usage"` +} + +type OrganizationCreateInput struct { + DisplayName string `json:"displayName"` +} + +type OrganizationSubscription struct { + PlanName string `json:"planName"` + CustomerID *string `json:"customerId"` + SubscriptionID *string `json:"subscriptionId"` + PlanDisplayName *string `json:"planDisplayName"` + IsDefaultPlan *bool `json:"isDefaultPlan"` + TrialConsumed bool `json:"trialConsumed"` + Invoice *Invoice `json:"invoice"` +} + +type OrganizationUpdateInput struct { + DisplayName string `json:"displayName"` +} + +type OrganizationUpdateOperations struct { + Blocked bool `json:"blocked"` + Reasons []string `json:"reasons"` +} + +type PageInfo struct { + Limit int `json:"limit"` + Offset int `json:"offset"` + HasNextPage bool `json:"hasNextPage"` +} + +type PatchDeploymentConfigInput struct { + ResourceVersion int `json:"resourceVersion"` + Notification *NotificationPatchDeploymentConfigInput `json:"notification"` + SourceBinding *SourceBindingPatchDeploymentConfigInput `json:"sourceBinding"` + Action *ActionPatchDeploymentConfigInput `json:"action"` +} + +type PlatformsCreateInput struct { + Discords []*DiscordCreateInput `json:"discords"` + SocketSlacks []*SocketSlackCreateInput `json:"socketSlacks"` + CloudSlacks []*CloudSlackCreateInput `json:"cloudSlacks"` + Mattermosts []*MattermostCreateInput `json:"mattermosts"` + Webhooks []*WebhookCreateInput `json:"webhooks"` + MsTeams []*MsTeamsCreateInput `json:"msTeams"` + Elasticsearches []*ElasticsearchCreateInput `json:"elasticsearches"` +} + +type PlatformsUpdateInput struct { + SocketSlacks []*SocketSlackUpdateInput `json:"socketSlacks"` + CloudSlacks []*CloudSlackUpdateInput `json:"cloudSlacks"` + Discords []*DiscordUpdateInput `json:"discords"` + Mattermosts []*MattermostUpdateInput `json:"mattermosts"` + Webhooks []*WebhookUpdateInput `json:"webhooks"` + MsTeams []*MsTeamsUpdateInput `json:"msTeams"` + Elasticsearches []*ElasticsearchUpdateInput `json:"elasticsearches"` +} + +type Plugin struct { + ID string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"displayName"` + Type PluginType `json:"type"` + ConfigurationName string `json:"configurationName"` + Configuration string `json:"configuration"` + Rbac *Rbac `json:"rbac"` +} + +type PluginConfigurationGroupInput struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Type PluginType `json:"type"` + Configurations []*PluginConfigurationInput `json:"configurations"` +} + +type PluginConfigurationGroupUpdateInput struct { + ID *string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"displayName"` + Type PluginType `json:"type"` + Configurations []*PluginConfigurationUpdateInput `json:"configurations"` +} + +type PluginConfigurationInput struct { + Name string `json:"name"` + Configuration string `json:"configuration"` + Rbac *RBACInput `json:"rbac"` +} + +type PluginConfigurationUpdateInput struct { + ID *string `json:"id"` + Name string `json:"name"` + Configuration string `json:"configuration"` + Rbac *RBACUpdateInput `json:"rbac"` +} + +type PluginPage struct { + Data []*Plugin `json:"data"` + PageInfo *PageInfo `json:"pageInfo"` + TotalCount int `json:"totalCount"` +} + +func (PluginPage) IsPageable() {} +func (this PluginPage) GetPageInfo() *PageInfo { return this.PageInfo } +func (this PluginPage) GetTotalCount() int { return this.TotalCount } + +type PluginTemplate struct { + Name string `json:"name"` + Title string `json:"title"` + Description string `json:"description"` + Type PluginType `json:"type"` + Schema interface{} `json:"schema"` +} + +type PluginTemplatePage struct { + Data []*PluginTemplate `json:"data"` +} + +type PluginsCreateInput struct { + Groups []*PluginConfigurationGroupInput `json:"groups"` +} + +type PluginsUpdateInput struct { + Groups []*PluginConfigurationGroupUpdateInput `json:"groups"` +} + +type Quota struct { + DeploymentCount *int `json:"deploymentCount"` + AuditRetentionPeriod *int `json:"auditRetentionPeriod"` + MemberCount *int `json:"memberCount"` + NodeCount *int `json:"nodeCount"` + CloudSlackUseCount *int `json:"cloudSlackUseCount"` +} + +type Rbac struct { + ID string `json:"id"` + User *UserPolicySubject `json:"user"` + Group *GroupPolicySubject `json:"group"` +} + +type RBACInput struct { + User *UserPolicySubjectInput `json:"user"` + Group *GroupPolicySubjectInput `json:"group"` +} + +type RBACUpdateInput struct { + ID string `json:"id"` + User *UserPolicySubjectInput `json:"user"` + Group *GroupPolicySubjectInput `json:"group"` +} + +type RemoveMemberFromOrganizationInput struct { + OrgID string `json:"orgId"` + UserID string `json:"userId"` +} + +type RemovePlatformFromOrganizationInput struct { + OrganizationID string `json:"organizationId"` + Slack *RemoveSlackFromOrganizationInput `json:"slack"` +} + +type RemoveSlackFromOrganizationInput struct { + ID string `json:"ID"` +} + +type SinkBindings struct { + Sources []string `json:"sources"` +} + +type SinkBindingsCreateInput struct { + Sources []*string `json:"sources"` +} + +type SinkBindingsUpdateInput struct { + Sources []*string `json:"sources"` +} + +type SlackWorkspace struct { + ID string `json:"id"` + Name string `json:"name"` + TeamID string `json:"teamId"` + URL string `json:"url"` + Channels []*SlackWorkspaceChannel `json:"channels"` + IsReinstallRequired bool `json:"isReinstallRequired"` + ConnectedOrganizations []*SlackWorkspaceConnectedOrganizations `json:"connectedOrganizations"` +} + +type SlackWorkspaceChannel struct { + Name string `json:"name"` + IsPrivate bool `json:"isPrivate"` + IsMember bool `json:"isMember"` + Topic *string `json:"topic"` + Purpose *string `json:"purpose"` +} + +type SlackWorkspaceConnectedOrganizations struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type SocketSlack struct { + ID string `json:"id"` + Name string `json:"name"` + AppToken string `json:"appToken"` + BotToken string `json:"botToken"` + Channels []*ChannelBindingsByName `json:"channels"` +} + +type SocketSlackCreateInput struct { + Name string `json:"name"` + AppToken string `json:"appToken"` + BotToken string `json:"botToken"` + Channels []*ChannelBindingsByNameCreateInput `json:"channels"` +} + +type SocketSlackUpdateInput struct { + ID *string `json:"id"` + Name string `json:"name"` + AppToken string `json:"appToken"` + BotToken string `json:"botToken"` + Channels []*ChannelBindingsByNameUpdateInput `json:"channels"` +} + +type SourceBindingPatchDeploymentConfigInput struct { + CommunicationGroupName string `json:"communicationGroupName"` + Platform BotPlatform `json:"platform"` + ChannelAlias string `json:"channelAlias"` + SourceBindings []string `json:"sourceBindings"` +} + +type SourceEventDetails struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` +} + +type SourceEventEmittedEvent struct { + ID string `json:"id"` + Type AuditEventType `json:"type"` + DeploymentID string `json:"deploymentId"` + Deployment *Deployment `json:"deployment"` + CreatedAt string `json:"createdAt"` + Event interface{} `json:"event"` + Source *SourceEventDetails `json:"source"` + PluginName string `json:"pluginName"` +} + +func (SourceEventEmittedEvent) IsAuditEvent() {} +func (this SourceEventEmittedEvent) GetID() string { return this.ID } +func (this SourceEventEmittedEvent) GetType() *AuditEventType { return &this.Type } +func (this SourceEventEmittedEvent) GetDeploymentID() string { return this.DeploymentID } +func (this SourceEventEmittedEvent) GetCreatedAt() string { return this.CreatedAt } +func (this SourceEventEmittedEvent) GetPluginName() string { return this.PluginName } +func (this SourceEventEmittedEvent) GetDeployment() *Deployment { return this.Deployment } + +type SubscriptionPlan struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + IsDefault bool `json:"isDefault"` + DisplayUnitPrice int `json:"displayUnitPrice"` + TrialPeriodDays int `json:"trialPeriodDays"` +} + +type UpdateCurrentUserInput struct { + FirstLoginPageVisitedIn bool `json:"firstLoginPageVisitedIn"` +} + +type User struct { + ID string `json:"id"` + Email string `json:"email"` + FirstLoginPageVisitedIn bool `json:"firstLoginPageVisitedIn"` +} + +type UserPolicySubject struct { + Type PolicySubjectType `json:"type"` + Static *UserStaticSubject `json:"static"` + Prefix *string `json:"prefix"` +} + +type UserPolicySubjectInput struct { + Type PolicySubjectType `json:"type"` + Static *UserStaticSubjectInput `json:"static"` + Prefix *string `json:"prefix"` +} + +type UserStaticSubject struct { + Value string `json:"value"` +} + +type UserStaticSubjectInput struct { + Value string `json:"value"` +} + +type Webhook struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Bindings *SinkBindings `json:"bindings"` +} + +type WebhookCreateInput struct { + Name string `json:"name"` + URL string `json:"url"` + Bindings *SinkBindingsCreateInput `json:"bindings"` +} + +type WebhookUpdateInput struct { + ID *string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Bindings *SinkBindingsUpdateInput `json:"bindings"` +} + +type APICallEventQglType string + +const ( + APICallEventQglTypeQuery APICallEventQglType = "QUERY" + APICallEventQglTypeMutation APICallEventQglType = "MUTATION" +) + +var AllAPICallEventQglType = []APICallEventQglType{ + APICallEventQglTypeQuery, + APICallEventQglTypeMutation, +} + +func (e APICallEventQglType) IsValid() bool { + switch e { + case APICallEventQglTypeQuery, APICallEventQglTypeMutation: + return true + } + return false +} + +func (e APICallEventQglType) String() string { + return string(e) +} + +func (e *APICallEventQglType) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = APICallEventQglType(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid ApiCallEventQglType", str) + } + return nil +} + +func (e APICallEventQglType) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +type AuditEventType string + +const ( + AuditEventTypeCommandExecuted AuditEventType = "COMMAND_EXECUTED" + AuditEventTypeSourceEventEmitted AuditEventType = "SOURCE_EVENT_EMITTED" + AuditEventTypeAPICall AuditEventType = "API_CALL" +) + +var AllAuditEventType = []AuditEventType{ + AuditEventTypeCommandExecuted, + AuditEventTypeSourceEventEmitted, + AuditEventTypeAPICall, +} + +func (e AuditEventType) IsValid() bool { + switch e { + case AuditEventTypeCommandExecuted, AuditEventTypeSourceEventEmitted, AuditEventTypeAPICall: + return true + } + return false +} + +func (e AuditEventType) String() string { + return string(e) +} + +func (e *AuditEventType) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = AuditEventType(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid AuditEventType", str) + } + return nil +} + +func (e AuditEventType) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +type BotPlatform string + +const ( + BotPlatformSLACk BotPlatform = "SLACK" + BotPlatformDiscord BotPlatform = "DISCORD" + BotPlatformMattermost BotPlatform = "MATTERMOST" + BotPlatformMsTeams BotPlatform = "MS_TEAMS" + BotPlatformUnknown BotPlatform = "UNKNOWN" +) + +var AllBotPlatform = []BotPlatform{ + BotPlatformSLACk, + BotPlatformDiscord, + BotPlatformMattermost, + BotPlatformMsTeams, + BotPlatformUnknown, +} + +func (e BotPlatform) IsValid() bool { + switch e { + case BotPlatformSLACk, BotPlatformDiscord, BotPlatformMattermost, BotPlatformMsTeams, BotPlatformUnknown: + return true + } + return false +} + +func (e BotPlatform) String() string { + return string(e) +} + +func (e *BotPlatform) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = BotPlatform(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid BotPlatform", str) + } + return nil +} + +func (e BotPlatform) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +type DeploymentStatusPhase string + +const ( + DeploymentStatusPhaseConnecting DeploymentStatusPhase = "CONNECTING" + DeploymentStatusPhaseConnected DeploymentStatusPhase = "CONNECTED" + DeploymentStatusPhaseDisconnected DeploymentStatusPhase = "DISCONNECTED" + DeploymentStatusPhaseFailed DeploymentStatusPhase = "FAILED" + DeploymentStatusPhaseCreating DeploymentStatusPhase = "CREATING" + DeploymentStatusPhaseUpdating DeploymentStatusPhase = "UPDATING" + DeploymentStatusPhaseDeleted DeploymentStatusPhase = "DELETED" +) + +var AllDeploymentStatusPhase = []DeploymentStatusPhase{ + DeploymentStatusPhaseConnecting, + DeploymentStatusPhaseConnected, + DeploymentStatusPhaseDisconnected, + DeploymentStatusPhaseFailed, + DeploymentStatusPhaseCreating, + DeploymentStatusPhaseUpdating, + DeploymentStatusPhaseDeleted, +} + +func (e DeploymentStatusPhase) IsValid() bool { + switch e { + case DeploymentStatusPhaseConnecting, DeploymentStatusPhaseConnected, DeploymentStatusPhaseDisconnected, DeploymentStatusPhaseFailed, DeploymentStatusPhaseCreating, DeploymentStatusPhaseUpdating, DeploymentStatusPhaseDeleted: + return true + } + return false +} + +func (e DeploymentStatusPhase) String() string { + return string(e) +} + +func (e *DeploymentStatusPhase) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = DeploymentStatusPhase(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid DeploymentStatusPhase", str) + } + return nil +} + +func (e DeploymentStatusPhase) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +type PluginType string + +const ( + PluginTypeSource PluginType = "SOURCE" + PluginTypeExecutor PluginType = "EXECUTOR" +) + +var AllPluginType = []PluginType{ + PluginTypeSource, + PluginTypeExecutor, +} + +func (e PluginType) IsValid() bool { + switch e { + case PluginTypeSource, PluginTypeExecutor: + return true + } + return false +} + +func (e PluginType) String() string { + return string(e) +} + +func (e *PluginType) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = PluginType(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid PluginType", str) + } + return nil +} + +func (e PluginType) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +type PolicySubjectType string + +const ( + PolicySubjectTypeStatic PolicySubjectType = "STATIC" + PolicySubjectTypeChannelName PolicySubjectType = "CHANNEL_NAME" + PolicySubjectTypeEmpty PolicySubjectType = "EMPTY" +) + +var AllPolicySubjectType = []PolicySubjectType{ + PolicySubjectTypeStatic, + PolicySubjectTypeChannelName, + PolicySubjectTypeEmpty, +} + +func (e PolicySubjectType) IsValid() bool { + switch e { + case PolicySubjectTypeStatic, PolicySubjectTypeChannelName, PolicySubjectTypeEmpty: + return true + } + return false +} + +func (e PolicySubjectType) String() string { + return string(e) +} + +func (e *PolicySubjectType) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = PolicySubjectType(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid PolicySubjectType", str) + } + return nil +} + +func (e PolicySubjectType) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +type StripeCouponDuration string + +const ( + StripeCouponDurationForever StripeCouponDuration = "FOREVER" + StripeCouponDurationOnce StripeCouponDuration = "ONCE" + StripeCouponDurationRepeating StripeCouponDuration = "REPEATING" + StripeCouponDurationUnknown StripeCouponDuration = "UNKNOWN" +) + +var AllStripeCouponDuration = []StripeCouponDuration{ + StripeCouponDurationForever, + StripeCouponDurationOnce, + StripeCouponDurationRepeating, + StripeCouponDurationUnknown, +} + +func (e StripeCouponDuration) IsValid() bool { + switch e { + case StripeCouponDurationForever, StripeCouponDurationOnce, StripeCouponDurationRepeating, StripeCouponDurationUnknown: + return true + } + return false +} + +func (e StripeCouponDuration) String() string { + return string(e) +} + +func (e *StripeCouponDuration) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = StripeCouponDuration(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid StripeCouponDuration", str) + } + return nil +} + +func (e StripeCouponDuration) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} diff --git a/test/cloud_graphql/model/platforms.go b/test/cloud_graphql/model/platforms.go new file mode 100644 index 0000000000..e8526e3c37 --- /dev/null +++ b/test/cloud_graphql/model/platforms.go @@ -0,0 +1,14 @@ +package model + +// Platforms is used by a specific platform field resolvers to +// return only those that are connected with a given deployment ID. +type Platforms struct { + DeploymentID string `graphql:"-"` + SocketSlacks []*SocketSlack `json:"socketSlacks"` + CloudSlacks []*CloudSlack `json:"cloudSlacks"` + Discords []*Discord `json:"discords"` + Mattermosts []*Mattermost `json:"mattermosts"` + Webhooks []*Webhook `json:"webhooks"` + MsTeams []*MsTeams `json:"msTeams"` + Elasticsearches []*Elasticsearch `json:"elasticsearches"` +} diff --git a/test/cloud_graphql/model/usage.go b/test/cloud_graphql/model/usage.go new file mode 100644 index 0000000000..db3e3a49ec --- /dev/null +++ b/test/cloud_graphql/model/usage.go @@ -0,0 +1,10 @@ +package model + +// Usage describes organization usage statistics. +type Usage struct { + OrganizationID string `graphql:"-"` + DeploymentCount *int `json:"deploymentCount"` + MemberCount *int `json:"memberCount"` + NodeCount *int `json:"nodeCount"` + CloudSlackUseCount *int `json:"cloudSlackUseCount"` +} diff --git a/test/commplatform/discord_tester.go b/test/commplatform/discord_tester.go index 9a07b3c269..29aa8dcb84 100644 --- a/test/commplatform/discord_tester.go +++ b/test/commplatform/discord_tester.go @@ -93,6 +93,10 @@ func (d *DiscordTester) ThirdChannel() Channel { return d.thirdChannel } +func (d *DiscordTester) MDFormatter() interactive.MDFormatter { + return d.mdFormatter +} + func (d *DiscordTester) InitUsers(t *testing.T) { t.Helper() @@ -427,6 +431,17 @@ func (d *DiscordTester) WaitForLastInteractiveMessagePostedEqual(userID, channel }) } +func (d *DiscordTester) WaitForLastInteractiveMessagePostedEqualWithCustomRender(userID, channelID string, renderedMsg string) error { + return d.WaitForMessagePosted(userID, channelID, 1, func(msg string) (bool, int, string) { + if !strings.EqualFold(renderedMsg, msg) { + count := diff.CountMatchBlock(renderedMsg, msg) + msgDiff := diff.Diff(renderedMsg, msg) + return false, count, msgDiff + } + return true, 0, "" + }) +} + func (d *DiscordTester) findUserID(t *testing.T, name string) string { t.Logf("Getting user %q...", name) res, err := d.cli.GuildMembersSearch(d.cfg.GuildID, name, 50) diff --git a/test/commplatform/generic.go b/test/commplatform/generic.go index 9e74e23ff6..7ed8a54fc1 100644 --- a/test/commplatform/generic.go +++ b/test/commplatform/generic.go @@ -43,8 +43,10 @@ type BotDriver interface { BotName() string BotUserID() string TesterUserID() string + MDFormatter() interactive.MDFormatter WaitForInteractiveMessagePostedRecentlyEqual(userID string, channelID string, message interactive.CoreMessage) error WaitForLastInteractiveMessagePostedEqual(userID string, channelID string, message interactive.CoreMessage) error + WaitForLastInteractiveMessagePostedEqualWithCustomRender(userID, channelID string, renderedMsg string) error } type MessageAssertion func(content string) (bool, int, string) diff --git a/test/commplatform/slack_tester.go b/test/commplatform/slack_tester.go index a96bc82583..dc497a3e9e 100644 --- a/test/commplatform/slack_tester.go +++ b/test/commplatform/slack_tester.go @@ -22,11 +22,12 @@ import ( ) type SlackConfig struct { - BotName string `envconfig:"default=botkube"` - TesterName string `envconfig:"default=tester"` - AdditionalContextMessage string `envconfig:"optional"` - TesterAppToken string - CloudTesterAppToken string + BotName string `envconfig:"default=botkube"` + TesterName string `envconfig:"default=tester"` + AdditionalContextMessage string `envconfig:"optional"` + TesterAppToken string `envconfig:"optional"` + TesterBotToken string `envconfig:"optional"` + CloudTesterAppToken string `envconfig:"optional"` RecentMessagesLimit int `envconfig:"default=6"` MessageWaitTimeout time.Duration `envconfig:"default=30s"` } @@ -57,7 +58,18 @@ type SlackTester struct { } func NewSlackTester(slackCfg SlackConfig) (BotDriver, error) { - slackCli := slack.New(slackCfg.TesterAppToken) + var token string + if slackCfg.TesterAppToken == "" && slackCfg.TesterBotToken == "" { + return nil, errors.New("slack tester token is not set") + } + if slackCfg.TesterAppToken != "" { + token = slackCfg.TesterAppToken + } + if slackCfg.TesterBotToken != "" { + token = slackCfg.TesterBotToken + } + + slackCli := slack.New(token) _, err := slackCli.AuthTest() if err != nil { return nil, err @@ -122,6 +134,10 @@ func (s *SlackTester) ThirdChannel() Channel { return s.thirdChannel } +func (s *SlackTester) MDFormatter() interactive.MDFormatter { + return s.mdFormatter +} + func (s *SlackTester) PostInitialMessage(t *testing.T, channelName string) { t.Helper() t.Log("Posting welcome message...") @@ -349,6 +365,18 @@ func (s *SlackTester) WaitForLastInteractiveMessagePostedEqual(userID, channelID }) } +func (s *SlackTester) WaitForLastInteractiveMessagePostedEqualWithCustomRender(userID, channelID string, renderedMsg string) error { + return s.WaitForMessagePosted(userID, channelID, 1, func(msg string) (bool, int, string) { + msg = strings.NewReplacer("\n", "\n").Replace(msg) + if !strings.EqualFold(renderedMsg, msg) { + count := diff.CountMatchBlock(renderedMsg, msg) + msgDiff := diff.Diff(renderedMsg, msg) + return false, count, msgDiff + } + return true, 0, "" + }) +} + func (s *SlackTester) findUserID(t *testing.T, name string) string { t.Log("Getting users...") res, err := s.cli.GetUsers() diff --git a/test/e2e/migration_test.go b/test/e2e/migration_test.go index 92f3f7c21b..281a50d859 100644 --- a/test/e2e/migration_test.go +++ b/test/e2e/migration_test.go @@ -50,7 +50,7 @@ var ( User: &gqlModel.UserPolicySubject{ Type: gqlModel.PolicySubjectTypeEmpty, Static: &gqlModel.UserStaticSubject{}, - Prefix: ptr.FromType(""), + Prefix: nil, }, Group: &gqlModel.GroupPolicySubject{ Type: gqlModel.PolicySubjectTypeStatic, @@ -368,7 +368,7 @@ func assertPlugins(t *testing.T, actual []*gqlModel.Plugin) { DisplayName: "Kubernetes Info", Type: "SOURCE", ConfigurationName: "k8s-all-events", - Configuration: "{\"annotations\":{},\"event\":{\"message\":{\"exclude\":[],\"include\":[]},\"reason\":{\"exclude\":[],\"include\":[]},\"types\":[\"create\",\"delete\",\"error\"]},\"filters\":{\"nodeEventsChecker\":true,\"objectAnnotationChecker\":true},\"labels\":{},\"namespaces\":{\"include\":[\".*\"]},\"resources\":[{\"type\":\"v1/pods\"},{\"type\":\"v1/services\"},{\"type\":\"networking.k8s.io/v1/ingresses\"},{\"type\":\"v1/nodes\"},{\"type\":\"v1/namespaces\"},{\"type\":\"v1/persistentvolumes\"},{\"type\":\"v1/persistentvolumeclaims\"},{\"type\":\"v1/configmaps\"},{\"type\":\"rbac.authorization.k8s.io/v1/roles\"},{\"type\":\"rbac.authorization.k8s.io/v1/rolebindings\"},{\"type\":\"rbac.authorization.k8s.io/v1/clusterrolebindings\"},{\"type\":\"rbac.authorization.k8s.io/v1/clusterroles\"},{\"event\":{\"types\":[\"create\",\"update\",\"delete\",\"error\"]},\"type\":\"apps/v1/daemonsets\",\"updateSetting\":{\"fields\":[\"spec.template.spec.containers[*].image\",\"status.numberReady\"],\"includeDiff\":true}},{\"event\":{\"types\":[\"create\",\"update\",\"delete\",\"error\"]},\"type\":\"batch/v1/jobs\",\"updateSetting\":{\"fields\":[\"spec.template.spec.containers[*].image\",\"status.conditions[*].type\"],\"includeDiff\":true}},{\"event\":{\"types\":[\"create\",\"update\",\"delete\",\"error\"]},\"type\":\"apps/v1/deployments\",\"updateSetting\":{\"fields\":[\"spec.template.spec.containers[*].image\",\"status.availableReplicas\"],\"includeDiff\":true}},{\"event\":{\"types\":[\"create\",\"update\",\"delete\",\"error\"]},\"type\":\"apps/v1/statefulsets\",\"updateSetting\":{\"fields\":[\"spec.template.spec.containers[*].image\",\"status.readyReplicas\"],\"includeDiff\":true}}]}", + Configuration: "{\"annotations\":{},\"event\":{\"message\":{\"exclude\":[],\"include\":[]},\"reason\":{\"exclude\":[],\"include\":[]},\"types\":[\"create\",\"delete\",\"error\"]},\"filters\":{\"nodeEventsChecker\":true,\"objectAnnotationChecker\":true},\"labels\":{},\"namespaces\":{\"include\":[\".*\"]},\"resources\":[{\"type\":\"v1/pods\"},{\"type\":\"v1/services\"},{\"type\":\"networking.k8s.io/v1/ingresses\"},{\"event\":{\"message\":{\"exclude\":[\".*nf_conntrack_buckets.*\"]}},\"type\":\"v1/nodes\"},{\"type\":\"v1/namespaces\"},{\"type\":\"v1/persistentvolumes\"},{\"type\":\"v1/persistentvolumeclaims\"},{\"type\":\"v1/configmaps\"},{\"type\":\"rbac.authorization.k8s.io/v1/roles\"},{\"type\":\"rbac.authorization.k8s.io/v1/rolebindings\"},{\"type\":\"rbac.authorization.k8s.io/v1/clusterrolebindings\"},{\"type\":\"rbac.authorization.k8s.io/v1/clusterroles\"},{\"event\":{\"types\":[\"create\",\"update\",\"delete\",\"error\"]},\"type\":\"apps/v1/daemonsets\",\"updateSetting\":{\"fields\":[\"spec.template.spec.containers[*].image\",\"status.numberReady\"],\"includeDiff\":true}},{\"event\":{\"types\":[\"create\",\"update\",\"delete\",\"error\"]},\"type\":\"batch/v1/jobs\",\"updateSetting\":{\"fields\":[\"spec.template.spec.containers[*].image\",\"status.conditions[*].type\"],\"includeDiff\":true}},{\"event\":{\"types\":[\"create\",\"update\",\"delete\",\"error\"]},\"type\":\"apps/v1/deployments\",\"updateSetting\":{\"fields\":[\"spec.template.spec.containers[*].image\",\"status.availableReplicas\"],\"includeDiff\":true}},{\"event\":{\"types\":[\"create\",\"update\",\"delete\",\"error\"]},\"type\":\"apps/v1/statefulsets\",\"updateSetting\":{\"fields\":[\"spec.template.spec.containers[*].image\",\"status.readyReplicas\"],\"includeDiff\":true}}]}", Rbac: defaultRBAC, }, { @@ -376,7 +376,7 @@ func assertPlugins(t *testing.T, actual []*gqlModel.Plugin) { DisplayName: "Kubernetes Errors", Type: "SOURCE", ConfigurationName: "k8s-err-events", - Configuration: "{\"event\":{\"types\":[\"error\"]},\"namespaces\":{\"include\":[\".*\"]},\"resources\":[{\"type\":\"v1/pods\"},{\"type\":\"v1/services\"},{\"type\":\"networking.k8s.io/v1/ingresses\"},{\"type\":\"v1/nodes\"},{\"type\":\"v1/namespaces\"},{\"type\":\"v1/persistentvolumes\"},{\"type\":\"v1/persistentvolumeclaims\"},{\"type\":\"v1/configmaps\"},{\"type\":\"rbac.authorization.k8s.io/v1/roles\"},{\"type\":\"rbac.authorization.k8s.io/v1/rolebindings\"},{\"type\":\"rbac.authorization.k8s.io/v1/clusterrolebindings\"},{\"type\":\"rbac.authorization.k8s.io/v1/clusterroles\"},{\"type\":\"apps/v1/deployments\"},{\"type\":\"apps/v1/statefulsets\"},{\"type\":\"apps/v1/daemonsets\"},{\"type\":\"batch/v1/jobs\"}]}", + Configuration: "{\"event\":{\"types\":[\"error\"]},\"namespaces\":{\"include\":[\".*\"]},\"resources\":[{\"type\":\"v1/pods\"},{\"type\":\"v1/services\"},{\"type\":\"networking.k8s.io/v1/ingresses\"},{\"event\":{\"message\":{\"exclude\":[\".*nf_conntrack_buckets.*\"]}},\"type\":\"v1/nodes\"},{\"type\":\"v1/namespaces\"},{\"type\":\"v1/persistentvolumes\"},{\"type\":\"v1/persistentvolumeclaims\"},{\"type\":\"v1/configmaps\"},{\"type\":\"rbac.authorization.k8s.io/v1/roles\"},{\"type\":\"rbac.authorization.k8s.io/v1/rolebindings\"},{\"type\":\"rbac.authorization.k8s.io/v1/clusterrolebindings\"},{\"type\":\"rbac.authorization.k8s.io/v1/clusterroles\"},{\"type\":\"apps/v1/deployments\"},{\"type\":\"apps/v1/statefulsets\"},{\"type\":\"apps/v1/daemonsets\"},{\"type\":\"batch/v1/jobs\"}]}", Rbac: defaultRBAC, }, {