/
assertplugin.go
209 lines (170 loc) · 8.56 KB
/
assertplugin.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
// Package assertplugin provides testing functions to validate a plugin's overall functionality.
// This package is designed to play well but not require the assertanswer package for validation
// of answers
//
// Note that all commands and hearActions are evaluated by assertplugin's driver but this is a
// simplified version of how slackscot actually drives plugins and aims to provide the minimal
// processing required to allow a plugin to test functionality given an incoming message.
// Users should take special care to use include <@botUserID> with the same botUserID with which the
// plugin driver has been instantiated in the message text inputs to test commands (or include a
// channel name that starts with D for direct channel testing)
//
// Example:
// func TestPlugin(t *testing.T) {
// assertplugin := assertplugin.New(t, "bot")
// yourPlugin := newPlugin()
//
// assertplugin.AnswersAndReacts(yourPlugin, &slack.Msg{Text: "are you up?"}, func(t *testing.T, answers []*slackscot.Answer, emojis []string) bool {
// return assert.Len(t, answers, 1) && assertanswer.HasText(t, answers[0], "I'm 😴, you?")
// }))
// }
package assertplugin
import (
"fmt"
"github.com/alexandre-normand/slackscot"
"github.com/alexandre-normand/slackscot/schedule"
"github.com/alexandre-normand/slackscot/test/capture"
"github.com/nlopes/slack"
"github.com/stretchr/testify/assert"
"log"
"strings"
"testing"
)
// Asserter represents a plugin driver/asserter and holds the bot identifier that tests are using when
// sending test messages for processing
type Asserter struct {
botUserID string
t *testing.T
logger *log.Logger
}
// New creates a new asserter with the given botUserId
// (only include the id without the '@' prefix).
// The botUserId is used in order to detect commands formed with
// <@botUserId>
func New(t *testing.T, botUserID string, options ...Option) (a *Asserter) {
a = new(Asserter)
a.botUserID = botUserID
a.t = t
for _, option := range options {
option(a)
}
return a
}
// Option defines an option for the Asserter
type Option func(*Asserter)
// OptionLog sets a logger for the asserter such that this logger is attached to the plugin when driven by
// the asserter
func OptionLog(logger *log.Logger) Option {
return func(a *Asserter) {
a.logger = logger
}
}
// ResultValidator is a function to do further validation of the answers and emoji reactions resulting from
// a plugin processing of all of its commands and hear actions. The return value is meant to be true if validation
// is successful and false otherwise (following the testify convention)
type ResultValidator func(t *testing.T, answers []*slackscot.Answer, emojis []string) bool
// ResultWithUploadsValidator is a function to do further validation of the answers, emoji reactions and file uploads
// resulting from a plugin processing of all of its commands and hear actions. The return value is meant to be true
// if validation is successful and false otherwise (following the testify convention)
type ResultWithUploadsValidator func(t *testing.T, answers []*slackscot.Answer, emojis []string, fileUploads []slack.FileUploadParameters) bool
// ScheduleResultValidator is a function to do further validation of the messages potentially sent by a
// slackscot.ScheduledAction as well as files uploaded. The messages sent during the execution of
// scheduled actions is given as a map of channel IDs to messages sent on that channel.
//
// The return value is meant to be true if validation is successful and false otherwise
// (following the testify convention)
type ScheduleResultValidator func(t *testing.T, sentMessagesByChannelID map[string][]string, fileUploads []slack.FileUploadParameters) bool
// AnswersAndReacts drives a plugin and collects Answers as well as emoji reactions. Once all of those have been collected,
// it passes handling to a validator to assert the expected answers and emoji reactions. It follows the style of
// github.com/stretchr/testify/assert as far as returning true/false to indicate success for further nested testing.
func (a *Asserter) AnswersAndReacts(p *slackscot.Plugin, m *slack.Msg, validate ResultValidator) (valid bool) {
answers, emojis, _ := a.injectServicesAndRun(p, m)
return validate(a.t, answers, emojis)
}
// AnswersAndReactsWithUploads drives a plugin and collects Answers as well as emoji reactions and file uploads.
// Once all of those have been collected, it passes handling to a validator to assert the expected answers,
// emoji reactions and file uploads. It follows the style of github.com/stretchr/testify/assert as far as
// returning true/false to indicate success for further nested testing.
func (a *Asserter) AnswersAndReactsWithUploads(p *slackscot.Plugin, m *slack.Msg, validate ResultWithUploadsValidator) (valid bool) {
answers, emojis, fileUploads := a.injectServicesAndRun(p, m)
return validate(a.t, answers, emojis, fileUploads)
}
// RunsOnSchedule drives a plugin's scheduled actions that match the schedule definition being passed in (i.e. "Every 1 hour" will
// run all actions scheduled to run every hour) and collects all the sent messages. Once all have been collected,
// the results are passed to the ScheduleResultValidator as a map[string][]string where the key is the channel id
// and the value holds the messages sent to that channel
func (a *Asserter) RunsOnSchedule(p *slackscot.Plugin, expected schedule.Definition, validate ScheduleResultValidator) (valid bool) {
_, fileUploadCaptor, rtmSender := a.injectServices(p)
didOneRun := false
schedules := make([]schedule.Definition, 0)
for _, action := range p.ScheduledActions {
schedules = append(schedules, action.Schedule)
if action.Schedule == expected {
action.Action()
didOneRun = true
}
}
return assert.Truef(a.t, didOneRun, "Expected at least one action to run on schedule [%s] but none did. Actual plugin action schedules: %s", expected, schedules) && validate(a.t, rtmSender.SentMessages, fileUploadCaptor.FileUploads)
}
// DoesNotRunOnSchedule drives a plugin's scheduled actions and validate that none of the
// ScheduledActions run on the specified schedule
func (a *Asserter) DoesNotRunOnSchedule(p *slackscot.Plugin, schedule schedule.Definition) (valid bool) {
a.injectServices(p)
for _, action := range p.ScheduledActions {
if action.Schedule == schedule {
action.Action()
return assert.Falsef(a.t, true, "Expected no action to run for schedule [%s] but [%s] did run", schedule, action.Description)
}
}
// No action ran so we can assert that it was indeed false
return assert.False(a.t, false)
}
// injectServicesAndRun injects services in the plugin, drives all of its actions and returns the answers and captured data
// from the execution
func (a *Asserter) injectServicesAndRun(p *slackscot.Plugin, m *slack.Msg) (answers []*slackscot.Answer, emojis []string, fileUploads []slack.FileUploadParameters) {
emojiCaptor, fileUploadCaptor, _ := a.injectServices(p)
answers = a.driveActions(p, m)
return answers, emojiCaptor.Emojis, fileUploadCaptor.FileUploads
}
func (a *Asserter) injectServices(p *slackscot.Plugin) (emojiCaptor *capture.EmojiReactionCaptor, fileUploadCaptor *capture.FileUploadCaptor, rtmSenderCaptor *capture.RealTimeSenderCaptor) {
emojiCaptor = capture.NewEmojiReactor()
p.EmojiReactor = emojiCaptor
fileUploadCaptor = capture.NewFileUploader()
p.FileUploader = slackscot.NewFileUploader(fileUploadCaptor)
p.Logger = slackscot.NewSLogger(getLogger(a), true)
rtmSender := capture.NewRealTimeSender()
p.RealTimeMsgSender = rtmSender
return emojiCaptor, fileUploadCaptor, rtmSender
}
func getLogger(a *Asserter) (logger *log.Logger) {
if a.logger != nil {
return a.logger
}
var b strings.Builder
return log.New(&b, "", 0)
}
func (a *Asserter) driveActions(p *slackscot.Plugin, m *slack.Msg) (answers []*slackscot.Answer) {
botMentionPrefix := fmt.Sprintf("<@%s> ", a.botUserID)
if strings.HasPrefix(m.Text, botMentionPrefix) {
normalizedText := strings.TrimPrefix(m.Text, botMentionPrefix)
inMsg := slackscot.IncomingMessage{NormalizedText: normalizedText, Msg: *m}
return runActions(p.Commands, &inMsg)
}
inMsg := slackscot.IncomingMessage{NormalizedText: m.Text, Msg: *m}
if strings.HasPrefix(m.Channel, "D") {
return runActions(p.Commands, &inMsg)
}
return runActions(p.HearActions, &inMsg)
}
func runActions(actions []slackscot.ActionDefinition, m *slackscot.IncomingMessage) (answers []*slackscot.Answer) {
answers = make([]*slackscot.Answer, 0)
for _, action := range actions {
if action.Match(m) {
a := action.Answer(m)
if a != nil {
answers = append(answers, a)
}
}
}
return answers
}