Skip to content

Commit

Permalink
Add integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
PrasadG193 committed Aug 26, 2019
1 parent e4eea76 commit c834d0b
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 50 deletions.
7 changes: 3 additions & 4 deletions pkg/execute/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
Expand Down Expand Up @@ -323,12 +322,12 @@ func makeFiltersList() string {

func findBotKubeVersion() (versions string) {
args := []string{"-c", fmt.Sprintf("%s version --short=true | grep Server", kubectlBinary)}
cmd := exec.Command("sh", args...)
runner := NewCommandRunner("sh", args)
// Returns "Server Version: xxxx"
k8sVersion, err := cmd.CombinedOutput()
k8sVersion, err := runner.Run()
if err != nil {
log.Logger.Warn(fmt.Sprintf("Failed to get Kubernetes version: %s", err.Error()))
k8sVersion = []byte("Server Version: Unknown\n")
k8sVersion = "Server Version: Unknown\n"
}

botkubeVersion := os.Getenv("BOTKUBE_VERSION")
Expand Down
4 changes: 4 additions & 0 deletions pkg/execute/fake_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
package execute

import (
"fmt"
"strings"
)

const K8sVersion = "v1.15.3"

// KubectlResponse map for fake Kubectl responses
var KubectlResponse = map[string]string{
"-n default get pods": "NAME READY STATUS RESTARTS AGE\n" +
"nginx-xxxxxxx-yyyyyyy 1/1 Running 1 1d",
"-c " + kubectlBinary + " version --short=true | grep Server": fmt.Sprintf("Server Version: %s\n", K8sVersion),
}

// FakeRunner mocks Run
Expand Down
151 changes: 151 additions & 0 deletions test/e2e/command/botkube.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package command

import (
"encoding/json"
"fmt"
"os"
"strings"
"testing"
"time"

"github.com/infracloudio/botkube/pkg/config"
"github.com/infracloudio/botkube/pkg/execute"
"github.com/infracloudio/botkube/test/e2e/utils"
"github.com/nlopes/slack"
"github.com/stretchr/testify/assert"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type botkubeCommand struct {
command string
expected string
}

// Send botkube command via Slack message and check if BotKube returns correct response
func (c *context) testBotkubeCommand(t *testing.T) {
botkubeVersion := os.Getenv("BOTKUBE_VERSION")
// Test cases
tests := map[string]botkubeCommand{
"BotKube ping": {
command: "ping",
expected: fmt.Sprintf("```pong from cluster '%s'\n\nK8s Server Version: %s\nBotKube version: %s```", c.Config.Settings.ClusterName, execute.K8sVersion, botkubeVersion),
},
"BotKube filters list": {
command: "filters list",
expected: "FILTER ENABLED DESCRIPTION\n" +
"IngressValidator true Checks if services and tls secrets used in ingress specs are available.\n" +
"JobStatusChecker true Sends notifications only when job succeeds and ignores other job update events.\n" +
"NodeEventsChecker true Sends notifications on node level critical events.\n" +
"PodLabelChecker true Checks and adds recommedations if labels are missing in the pod specs.\n" +
"ImageTagChecker true Checks and adds recommendation if 'latest' image tag is used for container image.\n",
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
// Send message to a channel
c.SlackServer.SendMessageToBot(c.Config.Communications.Slack.Channel, test.command)

// Get last seen slack message
time.Sleep(time.Second)
lastSeenMsg := c.GetLastSeenSlackMessage()

// Convert text message into Slack message structure
m := slack.Message{}
err := json.Unmarshal([]byte(lastSeenMsg), &m)
assert.NoError(t, err, "message should decode properly")
assert.Equal(t, c.Config.Communications.Slack.Channel, m.Channel)
switch test.command {
case "filters list":
fl := compareFilters(strings.Split(test.expected, "\n"), strings.Split(strings.Trim(m.Text, "```"), "\n"))
assert.Equal(t, fl, true)
default:
assert.Equal(t, test.expected, m.Text)
}
})
}
}

func compareFilters(expected, actual []string) bool {
for _, a := range actual {
found := false
for _, e := range expected {
if a == e {
found = true
break
}
}
if !found {
return false
}
}
return true
}

// Test disable notification with BotKube notifier command
// - disable notifier with '@BotKube notifier stop'
// - create pod and verify BotKube doesn't send notification
// - enable notifier with '@BotKube notifier start'
func (c *context) testNotifierCommand(t *testing.T) {
// Disable notifier with @BotKube notifier stop
t.Run("disable notifier", func(t *testing.T) {
// Send message to a channel
c.SlackServer.SendMessageToBot(c.Config.Communications.Slack.Channel, "notifier stop")

// Get last seen slack message
time.Sleep(time.Second)
lastSeenMsg := c.GetLastSeenSlackMessage()

// Convert text message into Slack message structure
m := slack.Message{}
err := json.Unmarshal([]byte(lastSeenMsg), &m)
assert.NoError(t, err, "message should decode properly")
assert.Equal(t, c.Config.Communications.Slack.Channel, m.Channel)
assert.Equal(t, fmt.Sprintf("```Sure! I won't send you notifications from cluster '%s' anymore.```", c.Config.Settings.ClusterName), m.Text)
assert.Equal(t, config.Notify, false)
})

// Create pod and verify that BotKube is not sending notifications
pod := utils.CreateObjects{
Kind: "pod",
Namespace: "test",
Specs: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "test-pod-notifier"}},
Expected: utils.SlackMessage{
Attachments: []slack.Attachment{{Color: "good", Fields: []slack.AttachmentField{{Title: "Pod create", Value: "Pod `test-pod` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\nRecommendations:\n- pod 'test-pod' creation without labels should be avoided.\n```", Short: false}}, Footer: "BotKube"}},
},
}
t.Run("create resource", func(t *testing.T) {
// Inject an event into the fake client.
utils.CreateResource(t, pod)

// Get last seen slack message
time.Sleep(time.Second)
lastSeenMsg := c.GetLastSeenSlackMessage()

// Convert text message into Slack message structure
m := slack.Message{}
err := json.Unmarshal([]byte(lastSeenMsg), &m)
assert.NoError(t, err, "message should decode properly")
assert.Equal(t, c.Config.Communications.Slack.Channel, m.Channel)
assert.NotEqual(t, pod.Expected.Attachments, m.Attachments)
})

// Revert and Enable notifier
t.Run("Enable notifier", func(t *testing.T) {
// Send message to a channel
c.SlackServer.SendMessageToBot(c.Config.Communications.Slack.Channel, "notifier start")

// Get last seen slack message
time.Sleep(time.Second)
lastSeenMsg := c.GetLastSeenSlackMessage()

// Convert text message into Slack message structure
m := slack.Message{}
err := json.Unmarshal([]byte(lastSeenMsg), &m)
assert.NoError(t, err, "message should decode properly")
assert.Equal(t, c.Config.Communications.Slack.Channel, m.Channel)
assert.Equal(t, fmt.Sprintf("```Brace yourselves, notifications are coming from cluster '%s'.```", c.Config.Settings.ClusterName), m.Text)
assert.Equal(t, config.Notify, true)
})
}
28 changes: 17 additions & 11 deletions test/e2e/command/kubectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type kubectlCommand struct {
}

type context struct {
Env *env.TestEnv
*env.TestEnv
}

// Send kubectl command via Slack message and check if BotKube returns correct response
Expand All @@ -27,34 +27,40 @@ func (c *context) testKubectlCommand(t *testing.T) {
tests := map[string]kubectlCommand{
"BotKube get pods": {
command: "get pods",
expected: fmt.Sprintf("```Cluster: %s\n%s```", c.Env.Config.Settings.ClusterName, execute.KubectlResponse["-n default get pods"]),
expected: fmt.Sprintf("```Cluster: %s\n%s```", c.Config.Settings.ClusterName, execute.KubectlResponse["-n default get pods"]),
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
// Send message to a channel
c.Env.SlackServer.SendMessageToBot(c.Env.Config.Communications.Slack.Channel, test.command)
c.SlackServer.SendMessageToBot(c.Config.Communications.Slack.Channel, test.command)

// Get last seen slack message
time.Sleep(time.Second)
lastSeenMsg := c.Env.GetLastSeenSlackMessage()
lastSeenMsg := c.GetLastSeenSlackMessage()

// Convert text message into Slack message structure
m := slack.Message{}
err := json.Unmarshal([]byte(lastSeenMsg), &m)
assert.NoError(t, err, "message should decode properly")
assert.Equal(t, c.Env.Config.Communications.Slack.Channel, m.Channel)
assert.Equal(t, c.Config.Communications.Slack.Channel, m.Channel)
assert.Equal(t, test.expected, m.Text)
})
}
}

// E2ETests runs create notification tests
func E2ETests(testEnv *env.TestEnv) func(*testing.T) {
ctx := &context{
Env: testEnv,
}
// Run tests
func (c *context) Run(t *testing.T) {
// Run kubectl tests
t.Run("Test Kubectl command", c.testKubectlCommand)
t.Run("Test BotKube command", c.testBotkubeCommand)
t.Run("Test disable notifier", c.testNotifierCommand)
}

return ctx.testKubectlCommand
// E2ETests runs command execution tests
func E2ETests(testEnv *env.TestEnv) env.E2ETest {
return &context{
testEnv,
}
}
7 changes: 3 additions & 4 deletions test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,16 @@ func TestRun(t *testing.T) {
time.Sleep(time.Second)

// Make test suite
suite := map[string]func(*testing.T){
suite := map[string]env.E2ETest{
"notifier": create.E2ETests(testEnv),
"command": command.E2ETests(testEnv),
"command": command.E2ETests(testEnv),
"filters": filters.E2ETests(testEnv),
}

// Run test suite
for name, test := range suite {
t.Run(name, test)
t.Run(name, test.Run)
}

}

func StartFakeSlackBot(testenv *env.TestEnv) {
Expand Down
13 changes: 10 additions & 3 deletions test/e2e/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,33 @@ package env
import (
"fmt"
"log"
"os"
"testing"

"github.com/infracloudio/botkube/pkg/config"
"github.com/nlopes/slack"
"github.com/nlopes/slack/slacktest"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
//"github.com/infracloudio/botkube/pkg/controller"
)

// TestEnv to store objects required for e2e testing
// K8sClient : Fake K8s client to mock resource creation
// SlackServer : Fake Slack server
// SlackRTM : Stores RTM object to interact with fake Slack server
// SlackMessages: Channel to store incoming Slack messages from BotKube
// Config : BotKube config provided with config.yaml
type TestEnv struct {
K8sClient kubernetes.Interface
SlackRTM *slack.RTM
SlackServer *slacktest.Server
SlackMessages chan (*slack.MessageEvent)
Config *config.Config
}

// E2ETest interface to run tests
type E2ETest interface {
Run(*testing.T)
}

// New creates TestEnv and populate required objects
func New() *TestEnv {
testEnv := &TestEnv{}
Expand All @@ -45,6 +49,9 @@ func New() *TestEnv {
testEnv.Config.Settings.ClusterName = "test-cluster-1"
testEnv.Config.Settings.AllowKubectl = true

// Set fake BotKube version
os.Setenv("BOTKUBE_VERSION", "v9.99.9")

testEnv.K8sClient = fake.NewSimpleClientset()
testEnv.SlackMessages = make(chan (*slack.MessageEvent), 1)
testEnv.SetupFakeSlack()
Expand Down
36 changes: 25 additions & 11 deletions test/e2e/filters/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import (
"github.com/nlopes/slack"
"github.com/stretchr/testify/assert"
"k8s.io/api/core/v1"
extV1beta1 "k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)

type context struct {
Env *env.TestEnv
*env.TestEnv
}

// Test if BotKube sends notification when a resource is created
Expand All @@ -26,7 +28,7 @@ func (c *context) testFilters(t *testing.T) {
Namespace: "test",
Specs: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "nginx-pod", Labels: map[string]string{"env": "test"}}, Spec: v1.PodSpec{Containers: []v1.Container{{Name: "nginx", Image: "nginx:latest"}}}},
Expected: utils.SlackMessage{
Attachments: []slack.Attachment{{Color: "good", Fields: []slack.AttachmentField{{Title: "Pod create", Value: "Pod `nginx-pod` of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\n\nRecommendations: :latest tag used in image 'nginx:latest' of Container 'nginx' should be avoided.\n```", Short: false}}, Footer: "BotKube"}},
Attachments: []slack.Attachment{{Color: "good", Fields: []slack.AttachmentField{{Title: "Pod create", Value: "Pod `nginx-pod` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\nRecommendations:\n- :latest tag used in image 'nginx:latest' of Container 'nginx' should be avoided.\n```", Short: false}}, Footer: "BotKube"}},
},
},

Expand All @@ -35,7 +37,16 @@ func (c *context) testFilters(t *testing.T) {
Namespace: "test",
Specs: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod-wo-label"}},
Expected: utils.SlackMessage{
Attachments: []slack.Attachment{{Color: "good", Fields: []slack.AttachmentField{{Title: "Pod create", Value: "Pod `pod-wo-label` of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\n\nRecommendations: pod 'pod-wo-label' creation without labels should be avoided.\n```", Short: false}}, Footer: "BotKube"}},
Attachments: []slack.Attachment{{Color: "good", Fields: []slack.AttachmentField{{Title: "Pod create", Value: "Pod `pod-wo-label` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\nRecommendations:\n- pod 'pod-wo-label' creation without labels should be avoided.\n```", Short: false}}, Footer: "BotKube"}},
},
},

"test IngressValidator filter": {
Kind: "ingress",
Namespace: "test",
Specs: &extV1beta1.Ingress{ObjectMeta: metav1.ObjectMeta{Name: "ingress-with-service"}, Spec: extV1beta1.IngressSpec{Rules: []extV1beta1.IngressRule{{IngressRuleValue: extV1beta1.IngressRuleValue{HTTP: &extV1beta1.HTTPIngressRuleValue{Paths: []extV1beta1.HTTPIngressPath{{Path: "testpath", Backend: extV1beta1.IngressBackend{ServiceName: "test-service", ServicePort: intstr.FromInt(80)}}}}}}}}},
Expected: utils.SlackMessage{
Attachments: []slack.Attachment{{Color: "good", Fields: []slack.AttachmentField{{Title: "Ingress create", Value: "Ingress `ingress-with-service` in of cluster `test-cluster-1`, namespace `test` has been created:\n```Resource created\nWarnings:\n- Service 'test-service' used in ingress 'ingress-with-service' config does not exist or port '80' not exposed\n```", Short: false}}, Footer: "BotKube"}},
},
},
}
Expand All @@ -47,23 +58,26 @@ func (c *context) testFilters(t *testing.T) {

// Get last seen slack message
time.Sleep(time.Second)
lastSeenMsg := c.Env.GetLastSeenSlackMessage()
lastSeenMsg := c.GetLastSeenSlackMessage()

// Convert text message into Slack message structure
m := slack.Message{}
err := json.Unmarshal([]byte(lastSeenMsg), &m)
assert.NoError(t, err, "message should decode properly")
assert.Equal(t, c.Env.Config.Communications.Slack.Channel, m.Channel)
assert.Equal(t, c.Config.Communications.Slack.Channel, m.Channel)
assert.Equal(t, test.Expected.Attachments, m.Attachments)
})
}
}

// E2ETests test ImageTagChecker filter
func E2ETests(testEnv *env.TestEnv) func(*testing.T) {
ctx := &context{
Env: testEnv,
}
// Run tests
func (c *context) Run(t *testing.T) {
t.Run("test filters", c.testFilters)
}

return ctx.testFilters
// E2ETests runs filter tests
func E2ETests(testEnv *env.TestEnv) env.E2ETest {
return &context{
testEnv,
}
}
Loading

0 comments on commit c834d0b

Please sign in to comment.