Skip to content

Commit

Permalink
fix up tests
Browse files Browse the repository at this point in the history
  • Loading branch information
wass3r committed Aug 24, 2022
1 parent ec7a7c3 commit dbb2c93
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 63 deletions.
1 change: 1 addition & 0 deletions core/matcher.go
Expand Up @@ -77,6 +77,7 @@ func getProccessedInputAndHitValue(messageInput, ruleRespondValue, ruleHearValue
}

// handleChatServiceRule handles the processing logic for a rule that came from either the chat application or CLI remote.
//
//nolint:gocyclo // refactor candidate
func handleChatServiceRule(outputMsgs chan<- models.Message, message models.Message, hitRule chan<- models.Rule, rule models.Rule, processedInput string, hit bool, bot *models.Bot) (bool, bool) {
match, stopSearch := false, false
Expand Down
20 changes: 13 additions & 7 deletions core/remotes.go
Expand Up @@ -23,15 +23,21 @@ import (
// 'Message' object and pass it along to the Matcher function (see '/core/matcher.go') for processing.
// Currently, we support 3 types of remotes: chat applications, CLI, and Scheduler.
// Remote 1: Chat applications
// This remote allows us to read messages from various chat application platforms, e.g. Slack, Discord, etc.
// We typically read the messages from these chat applications using their respective APIs.
// * Note: right now we only support reading from one chat application at a time.
//
// This remote allows us to read messages from various chat application platforms, e.g. Slack, Discord, etc.
// We typically read the messages from these chat applications using their respective APIs.
// * Note: right now we only support reading from one chat application at a time.
//
// Remote 2: CLI
// This remote is enabled when 'CLI mode' is set to true in the bot.yml configuration.
// Messages from this remote are read from the user's input via the terminal.
//
// This remote is enabled when 'CLI mode' is set to true in the bot.yml configuration.
// Messages from this remote are read from the user's input via the terminal.
//
// Remote 3: Scheduler
// This remote allows us to read messages being sent internally by a running cronjob
// created by a schedule type rule, e.g. see '/config/rules/schedule.yml'.
//
// This remote allows us to read messages being sent internally by a running cronjob
// created by a schedule type rule, e.g. see '/config/rules/schedule.yml'.
//
// TODO: Refactor to keep remote specific stuff in remote/.
func Remotes(inputMsgs chan<- models.Message, rules map[string]models.Rule, bot *models.Bot) {
// Run a chat application
Expand Down
52 changes: 41 additions & 11 deletions handlers/script.go
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
Expand Down Expand Up @@ -53,18 +54,47 @@ func ScriptExec(args models.Action, msg *models.Message) (*models.ScriptResponse

// run command and capture stdout/stderr
out, err := cmd.CombinedOutput()

// handle timeouts
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
result.Output = "Hmm, the command timed out. Please try again."

return result, fmt.Errorf("timeout reached, exec process for action %#q canceled", args.Name)
}

// deal with non-zero exit codes
if err != nil {
result.Status = cmd.ProcessState.ExitCode()
result.Output = strings.Trim(string(out), " \n")
// handle timeouts
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
result.Output = "Hmm, the command timed out. Please try again."

return result, fmt.Errorf("timeout reached, exec process for action %#q canceled", args.Name)
}

// check if file couldn't be found
errFileNotFoundMsg := "file not found: %s"
if os.IsNotExist(err) {
result.Output = fmt.Sprintf(errFileNotFoundMsg, args.Cmd)
}

// check other variations that might
// indicate that a file couldn't be found
// whether called directly,
// or indirectly
outAsLower := strings.ToLower(string(out))
if strings.Contains(outAsLower, "no such file") ||
strings.Contains(outAsLower, "can't open") ||
strings.Contains(err.Error(), "file not found") {
result.Output = fmt.Sprintf("file not found: %s", args.Cmd)
}

// grab the statuscode of the process
exitCode := cmd.ProcessState.ExitCode()

// this might be -1 if process didn't exit
// or was terminated otherwise
//
// only pass on "real" error codes
if exitCode > 0 {
result.Status = exitCode
}

// if we had an error not covered above,
// attempt to grab the output
if result.Output == "" {
result.Output = strings.Trim(string(out), " \n")
}

return result, err
}
Expand Down
163 changes: 118 additions & 45 deletions handlers/script_test.go
Expand Up @@ -6,7 +6,6 @@ package handlers

import (
"reflect"
"regexp"
"testing"

"github.com/target/flottbot/models"
Expand All @@ -23,8 +22,8 @@ func newExecAction(cmd string) models.Action {

func TestScriptExec(t *testing.T) {
type args struct {
args models.Action
msg *models.Message
action models.Action
msg *models.Message
}

simpleScriptMessage := models.NewMessage()
Expand All @@ -42,23 +41,132 @@ func TestScriptExec(t *testing.T) {

msgBeforeExit := newExecAction(`/bin/sh ../testdata/fail.sh`)

notExists := newExecAction(`./trap.sh`)

notExistsInPath := newExecAction(`trap.sh`)

// assumes existence of /bin/sh
fileNotFound := newExecAction(`/bin/sh ./this/is/a/trap.sh`)

tests := []struct {
name string
args args
want *models.ScriptResponse
wantErr bool
}{
{"Simple Script", args{args: simpleScriptAction, msg: &simpleScriptMessage}, &models.ScriptResponse{Status: 0, Output: "hi there"}, false},
{"Slow Script", args{args: slowScriptAction, msg: &simpleScriptMessage}, &models.ScriptResponse{Status: 1, Output: "Hmm, the command timed out. Please try again."}, true},
{"Error Script", args{args: errorScriptAction, msg: &simpleScriptMessage}, &models.ScriptResponse{Status: 1, Output: ""}, true},
{"Existing Var Script", args{args: varExistsScriptAction, msg: &simpleScriptMessage}, &models.ScriptResponse{Status: 0, Output: "echo"}, false},
{"Missing Var Script", args{args: varMissingScriptAction, msg: &simpleScriptMessage}, &models.ScriptResponse{Status: 1, Output: ""}, true},
{"StdOut before exit code 1", args{args: msgBeforeExit, msg: &simpleScriptMessage}, &models.ScriptResponse{Status: 1, Output: "error is coming"}, true},
{
"Simple Script",
args{
action: simpleScriptAction,
msg: &simpleScriptMessage,
},
&models.ScriptResponse{
Status: 0,
Output: "hi there",
},
false,
},
{
"Slow Script",
args{
action: slowScriptAction,
msg: &simpleScriptMessage,
},
&models.ScriptResponse{
Status: 1,
Output: "Hmm, the command timed out. Please try again.",
},
true,
},
{
"Error Script",
args{
action: errorScriptAction,
msg: &simpleScriptMessage,
},
&models.ScriptResponse{
Status: 1,
Output: "",
},
true,
},
{
"Existing Var Script",
args{
action: varExistsScriptAction,
msg: &simpleScriptMessage,
},
&models.ScriptResponse{
Status: 0,
Output: "echo",
},
false,
},
{
"Missing Var Script",
args{
action: varMissingScriptAction,
msg: &simpleScriptMessage,
},
&models.ScriptResponse{
Status: 1,
Output: "",
},
true,
},
{
"StdOut before exit code 1",
args{
action: msgBeforeExit,
msg: &simpleScriptMessage,
},
&models.ScriptResponse{
Status: 1,
Output: "error is coming",
},
true,
},
{
"Script doesn't exist",
args{
action: notExists,
msg: &simpleScriptMessage,
},
&models.ScriptResponse{
Status: 1,
Output: "file not found: ./trap.sh",
},
true,
},
{
"Script doesn't exist in PATH",
args{
action: notExistsInPath,
msg: &simpleScriptMessage,
},
&models.ScriptResponse{
Status: 1,
Output: "file not found: trap.sh",
},
true,
},
{
"Calling file doesn't exist",
args{
action: fileNotFound,
msg: &simpleScriptMessage,
},
&models.ScriptResponse{
Status: 127,
Output: "file not found: /bin/sh ./this/is/a/trap.sh",
},
true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ScriptExec(tt.args.args, tt.args.msg)
got, err := ScriptExec(tt.args.action, tt.args.msg)
if (err != nil) != tt.wantErr {
t.Errorf("ScriptExec() error = %v, wantErr %v", err, tt.wantErr)
return
Expand All @@ -69,38 +177,3 @@ func TestScriptExec(t *testing.T) {
})
}
}

func TestScriptExecWithRegex(t *testing.T) {
type args struct {
args models.Action
msg *models.Message
}

simpleScriptMessage := models.NewMessage()

cmdNotFound := newExecAction(`/bin/sh ./this/is/a/trap.sh`)

tests := []struct {
name string
args args
want *models.ScriptResponse
wantErr bool
wantRegexp *regexp.Regexp
}{
{"Script does not exist", args{args: cmdNotFound, msg: &simpleScriptMessage}, &models.ScriptResponse{Status: 2}, true, regexp.MustCompile(`No such file`)},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ScriptExec(tt.args.args, tt.args.msg)
if (err != nil) != tt.wantErr {
t.Errorf("ScriptExec() error = %v, wantErr %v", err, tt.wantErr)
return
}

if !tt.wantRegexp.MatchString(got.Output) {
t.Errorf("ScriptExec() = %v, want %v", got.Output, "Regexp(`"+tt.wantRegexp.String()+"`)")
}
})
}
}
1 change: 1 addition & 0 deletions remote/slack/helper.go
Expand Up @@ -678,6 +678,7 @@ func readFromEventsAPI(api *slack.Client, vToken string, inputMsgs chan<- models
// readFromSocketMode reads messages from Slack's Socket Mode
//
// https://api.slack.com/apis/connections/socket
//
//nolint:gocyclo,funlen // needs refactor
func readFromSocketMode(sm *slack.Client, inputMsgs chan<- models.Message, bot *models.Bot) {
// setup the client
Expand Down

0 comments on commit dbb2c93

Please sign in to comment.