Skip to content

Commit

Permalink
Merge pull request #3137 from target/slack-usergroup-smoke
Browse files Browse the repository at this point in the history
feat: add happy path smoke test for slack user groups
  • Loading branch information
allending313 committed Jul 5, 2023
2 parents 71c59ce + ab44da7 commit d4dd18d
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 7 deletions.
3 changes: 3 additions & 0 deletions devtools/mockslack/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ type Message struct {
Actions []Action
}

// UserGroup represents a Slack user group.
type UserGroup struct {
ID string `json:"id"`
Name string `json:"name"`
Handle string `json:"handle"`

IsUserGroup bool `json:"is_usergroup"`

Users []string `json:"users"`
}
18 changes: 18 additions & 0 deletions devtools/mockslack/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func NewServer() *Server {
srv.mux.HandleFunc("/api/groups.create", srv.ServeGroupsCreate)
srv.mux.HandleFunc("/api/team.info", srv.ServeTeamInfo)
srv.mux.HandleFunc("/api/usergroups.list", srv.ServeUserGroupList)
srv.mux.HandleFunc("/api/usergroups.users.update", srv.ServeUserGroupsUsersUpdate)
// TODO: history, leave, join

srv.mux.HandleFunc("/stats", func(w http.ResponseWriter, req *http.Request) {
Expand Down Expand Up @@ -209,6 +210,7 @@ func (st *state) NewChannel(name string) ChannelInfo {
return info
}

// UserGroupInfo contains information about a newly created Slack user group.
type UserGroupInfo struct {
ID, Name, Handle string
}
Expand All @@ -233,6 +235,22 @@ func (st *state) NewUserGroup(name string) UserGroupInfo {
return info
}

// UserGroupUserIDs will return all users from a given user group.
func (st *state) UserGroupUserIDs(ugID string) []string {
st.mx.Lock()
defer st.mx.Unlock()

ug := st.usergroups[ugID]
if ug == nil {
return nil
}

users := make([]string, len(ug.Users))
copy(users, ug.Users)

return users
}

// Messages will return all messages from a given channel/group.
func (st *state) Messages(chanID string) []Message {
st.mx.Lock()
Expand Down
61 changes: 61 additions & 0 deletions devtools/mockslack/usergroupsusersupdate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package mockslack

import (
"context"
"net/http"
"strings"
)

type UserGroupsUsersUpdateOptions struct {
Usergroup string
Users []string
}

// UserGroupsUsersUpdate updates the list of users within a user group and returns the user group.
func (st *API) UserGroupsUsersUpdate(ctx context.Context, opts UserGroupsUsersUpdateOptions) (*UserGroup, error) {
err := checkPermission(ctx, "bot", "usergroups:write")
if err != nil {
return nil, err
}

st.mx.Lock()
defer st.mx.Unlock()

ug := st.usergroups[opts.Usergroup]
if ug == nil {
return nil, &response{Err: "subteam_not_found"}
}

if len(opts.Users) == 0 {
return nil, &response{Err: "no_users_provided"}
}

for _, u := range opts.Users {
if st.users[u] == nil {
return nil, &response{Err: "invalid_users"}
}
}

ug.Users = opts.Users

return &ug.UserGroup, nil
}

// ServeUserGroupsUsersUpdate serves a request to the `usergroups.users.update` API call.
//
// https://api.slack.com/methods/usergroups.users.update
func (s *Server) ServeUserGroupsUsersUpdate(w http.ResponseWriter, req *http.Request) {
ug, err := s.API().UserGroupsUsersUpdate(req.Context(), UserGroupsUsersUpdateOptions{Usergroup: req.FormValue("usergroup"), Users: strings.Split(req.FormValue("users"), ",")})
if respondErr(w, err) {
return
}

var resp struct {
response
UserGroup *UserGroup `json:"usergroup"`
}

resp.UserGroup = ug

respondWith(w, resp)
}
15 changes: 8 additions & 7 deletions test/smoke/harness/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,13 +359,14 @@ func (h *Harness) execQuery(sql string, data interface{}) {
h.t.Helper()
t := template.New("sql")
t.Funcs(template.FuncMap{
"uuidJSON": func(id string) string { return fmt.Sprintf(`"%s"`, h.uuidG.Get(id)) },
"uuid": func(id string) string { return fmt.Sprintf("'%s'", h.uuidG.Get(id)) },
"phone": func(id string) string { return fmt.Sprintf("'%s'", h.phoneCCG.Get(id)) },
"email": func(id string) string { return fmt.Sprintf("'%s'", h.emailG.Get(id)) },
"phoneCC": func(cc, id string) string { return fmt.Sprintf("'%s'", h.phoneCCG.GetWithArg(cc, id)) },
"slackChannelID": func(name string) string { return fmt.Sprintf("'%s'", h.Slack().Channel(name).ID()) },
"slackUserID": func(name string) string { return fmt.Sprintf("'%s'", h.Slack().User(name).ID()) },
"uuidJSON": func(id string) string { return fmt.Sprintf(`"%s"`, h.uuidG.Get(id)) },
"uuid": func(id string) string { return fmt.Sprintf("'%s'", h.uuidG.Get(id)) },
"phone": func(id string) string { return fmt.Sprintf("'%s'", h.phoneCCG.Get(id)) },
"email": func(id string) string { return fmt.Sprintf("'%s'", h.emailG.Get(id)) },
"phoneCC": func(cc, id string) string { return fmt.Sprintf("'%s'", h.phoneCCG.GetWithArg(cc, id)) },
"slackChannelID": func(name string) string { return fmt.Sprintf("'%s'", h.Slack().Channel(name).ID()) },
"slackUserID": func(name string) string { return fmt.Sprintf("'%s'", h.Slack().User(name).ID()) },
"slackUserGroupID": func(name string) string { return fmt.Sprintf("'%s'", h.Slack().UserGroup(name).ID()) },
})
_, err := t.Parse(sql)
if err != nil {
Expand Down
62 changes: 62 additions & 0 deletions test/smoke/harness/slack.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package harness

import (
"fmt"
"net/http/httptest"
"sort"
"strings"
Expand All @@ -18,6 +19,7 @@ const (
type SlackServer interface {
Channel(string) SlackChannel
User(string) SlackUser
UserGroup(string) SlackUserGroup

WaitAndAssert()
}
Expand All @@ -37,6 +39,15 @@ type SlackChannel interface {
ExpectEphemeralMessage(keywords ...string) SlackMessage
}

type SlackUserGroup interface {
ID() string
Name() string
ErrorChannel() SlackChannel

ExpectUsers(names ...string)
ExpectUserIDs(ids ...string)
}

type SlackMessageState interface {
// AssertText asserts that the message contains the given keywords.
AssertText(keywords ...string)
Expand Down Expand Up @@ -76,6 +87,7 @@ type slackServer struct {
*mockslack.Server
hasFailure bool
channels map[string]*slackChannel
ug map[string]*slackUserGroup
}

type slackChannel struct {
Expand Down Expand Up @@ -197,6 +209,55 @@ func (s *slackServer) Channel(name string) SlackChannel {
return ch
}

type slackUserGroup struct {
h *Harness
name string
ugID string
channel SlackChannel
}

func (s *slackServer) UserGroup(name string) SlackUserGroup {
ug := s.ug[name]
if ug != nil {
return ug
}

mUG := s.NewUserGroup(name)
ch := s.Channel("ug:" + name)

ug = &slackUserGroup{h: s.h, name: fmt.Sprintf("@%s (%s)", name, ch.Name()), ugID: mUG.ID, channel: ch}

s.ug[name] = ug

return ug
}

func (ug *slackUserGroup) ID() string { return ug.ugID + ":" + ug.channel.ID() }
func (ug *slackUserGroup) Name() string { return ug.name }
func (ug *slackUserGroup) ErrorChannel() SlackChannel { return ug.channel }

func (ug *slackUserGroup) ExpectUsers(names ...string) {
ug.h.t.Helper()

var ids []string
for _, name := range names {
ids = append(ids, ug.h.Slack().User(name).ID())
}
ug.ExpectUserIDs(ids...)
}

func (ug *slackUserGroup) ExpectUserIDs(ids ...string) {
ug.h.t.Helper()

require.EventuallyWithT(ug.h.t, func(t *assert.CollectT) {
if assert.ElementsMatch(t, ug.h.slack.UserGroupUserIDs(ug.ugID), ids, "List A = expected; List B = actual") {
return
}

ug.h.Trigger()
}, 15*time.Second, time.Millisecond, "UserGroup Users should match")
}

func (ch *slackChannel) ID() string { return ch.id }
func (ch *slackChannel) Name() string { return ch.name }

Expand Down Expand Up @@ -324,6 +385,7 @@ func (h *Harness) initSlack() {
h.slack = &slackServer{
h: h,
channels: make(map[string]*slackChannel),
ug: make(map[string]*slackUserGroup),
Server: mockslack.NewServer(),
}
h.slackS = httptest.NewServer(h.slack)
Expand Down
52 changes: 52 additions & 0 deletions test/smoke/slackusergroup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package smoke

import (
"testing"
"time"

"github.com/target/goalert/expflag"
"github.com/target/goalert/test/smoke/harness"
)

// TestSlackUserGroups tests that the configured notification rule sends the intended notification to the slack user group.
func TestSlackUserGroups(t *testing.T) {
t.Parallel()

sql := `
insert into users (id, name, email)
values
({{uuid "uid"}}, 'bob', 'bob@example.com');
insert into user_contact_methods (id, user_id, name, type, value, pending)
values
({{uuid "cm1"}}, {{uuid "uid"}}, 'personal', 'SLACK_DM', {{slackUserID "bob"}}, false);
insert into schedules (id, name, time_zone)
values
({{uuid "sid"}}, 'testschedule', 'UTC');
insert into schedule_rules (id, schedule_id, sunday, monday, tuesday, wednesday, thursday, friday, saturday, start_time, end_time, tgt_user_id)
values
({{uuid "ruleID"}}, {{uuid "sid"}}, true, true, true, true, true, true, true, '00:00:00', '00:00:00', {{uuid "uid"}});
insert into notification_channels (id, type, name, value)
values
({{uuid "ug"}}, 'SLACK_USER_GROUP', '@testug (#test1)', {{slackUserGroupID "test2"}});
insert into schedule_data (schedule_id, data)
values
({{uuid "sid"}}, '{"V1":{"OnCallNotificationRules": [{"ChannelID": {{uuidJSON "ug"}}, "Time": "00:00" }]}}');
`
h := harness.NewHarnessWithFlags(t, sql, "slack-ug", expflag.FlagSet{expflag.SlackUserGroups})

defer h.Close()

h.Trigger()

// Passing in no arguments to assert empty
h.Slack().UserGroup("test2").ExpectUsers()

h.FastForward(24 * time.Hour)

h.Slack().UserGroup("test2").ExpectUsers("bob")
}

0 comments on commit d4dd18d

Please sign in to comment.