From d42072ef77ff406c178874fd7b21b0680e5a4355 Mon Sep 17 00:00:00 2001 From: Arkady Skvoretsky Date: Thu, 2 Jan 2025 18:16:40 +0400 Subject: [PATCH] Unit tests --- .golangci.yaml | 11 + cmd/service/main.go | 8 +- internal/discord/commands/responses/cast.go | 11 + internal/discord/router.go | 8 + internal/services/addQuote/service.go | 10 +- internal/services/addQuote/service_test.go | 130 ++++++ internal/services/diceRoll/service_test.go | 3 +- internal/services/lockVoiceChan/service.go | 4 +- .../services/lockVoiceChan/service_test.go | 372 ++++++++++++++++++ internal/services/mentionVoiceChan/service.go | 5 +- .../services/mentionVoiceChan/service_test.go | 263 +++++++++++++ .../services/unlockVoiceChan/service_test.go | 1 - 12 files changed, 811 insertions(+), 15 deletions(-) create mode 100644 internal/services/addQuote/service_test.go create mode 100644 internal/services/lockVoiceChan/service_test.go create mode 100644 internal/services/mentionVoiceChan/service_test.go diff --git a/.golangci.yaml b/.golangci.yaml index d471919..ebe7f04 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -53,6 +53,7 @@ linters: - wastedassign - whitespace - wrapcheck + linters-settings: copyloopvar: check-alias: true @@ -97,6 +98,8 @@ linters-settings: - anon lll: line-length: 80 + nilnil: + detect-opposite: true nolintlint: require-explanation: true require-specific: true @@ -178,3 +181,11 @@ linters-settings: yaml: kebab testifylint: enable-all: true + +issues: + exclude-rules: + - path: _test\.go + linters: + - dupl + - path: _test\.go + text: "add-constant:" diff --git a/cmd/service/main.go b/cmd/service/main.go index 545362d..d6aaa0a 100644 --- a/cmd/service/main.go +++ b/cmd/service/main.go @@ -46,6 +46,9 @@ func main() { func run() (code int) { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + logger.Info("starting") + defer logger.Info("shutdown complete") + defer func() { if err := recover(); err != nil { logger.Error("panic", "error", err) @@ -58,9 +61,6 @@ func run() (code int) { ) defer cancel() - logger.Info("starting") - defer logger.Info("shutdown complete") - cfg, err := cfgpkg.ReadConfig[config]() if err != nil { logger.ErrorContext(ctx, "failed to read config", "error", err) @@ -75,6 +75,7 @@ func run() (code int) { logger.ErrorContext(ctx, "failed to connect to db", "error", err) return codeErr } + defer dbConn.Close() dbProbe := db.NewProbe(dbConn.DB) @@ -131,7 +132,6 @@ func run() (code int) { logger.ErrorContext(ctx, "failed to set commands", "error", err) return codeErr } - defer dcRouter.UnsetCommands() //nolint:errcheck // shutdown dcProbe := discord.NewProbe(dcRouter.Session()) diff --git a/internal/discord/commands/responses/cast.go b/internal/discord/commands/responses/cast.go index 8c206cf..2913062 100644 --- a/internal/discord/commands/responses/cast.go +++ b/internal/discord/commands/responses/cast.go @@ -6,11 +6,22 @@ import ( "github.com/bwmarrin/discordgo" ) +func CastNoChannel() *discordgo.InteractionResponse { + return &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Either specify a channel or join one", + Flags: discordgo.MessageFlagsEphemeral, + }, + } +} + func CastNoUsers() *discordgo.InteractionResponse { return &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: "There are no users in the voice channel", + Flags: discordgo.MessageFlagsEphemeral, }, } } diff --git a/internal/discord/router.go b/internal/discord/router.go index deb9ee3..b2931cf 100644 --- a/internal/discord/router.go +++ b/internal/discord/router.go @@ -54,6 +54,10 @@ func NewRouter(cfg Config, opts ...RouterOpt) (*Router, error) { } func (r *Router) Close() error { + if err := r.UnsetCommands(); err != nil { + return fmt.Errorf("failed to unset commands: %w", err) + } + if err := r.session.Close(); err != nil { return fmt.Errorf("failed to close Discord session: %w", err) } @@ -66,6 +70,10 @@ func (r *Router) Session() *discordgo.Session { } func (r *Router) SetCommands(commands ...Command) error { + if err := r.UnsetCommands(); err != nil { + return fmt.Errorf("failed to unset previous commands: %w", err) + } + for _, cmd := range commands { name := cmd.Description.Name diff --git a/internal/services/addQuote/service.go b/internal/services/addQuote/service.go index 4d18f3c..bcdb6e8 100644 --- a/internal/services/addQuote/service.go +++ b/internal/services/addQuote/service.go @@ -3,6 +3,7 @@ package addQuote import ( "context" "fmt" + "time" "github.com/bwmarrin/discordgo" @@ -37,10 +38,11 @@ func (s *Service) Exec( } quote := &models.Quote{ - Author: author, - Text: text, - GuildID: models.ID(i.GuildID), - AddedBy: i.Member, + Author: author, + Text: text, + GuildID: models.ID(i.GuildID), + AddedBy: i.Member, + Timestamp: time.Now(), } err = s.db.AddUserQuoteOnGuild(ctx, quote) if err != nil { diff --git a/internal/services/addQuote/service_test.go b/internal/services/addQuote/service_test.go new file mode 100644 index 0000000..7a86706 --- /dev/null +++ b/internal/services/addQuote/service_test.go @@ -0,0 +1,130 @@ +package addQuote_test + +import ( + "context" + "errors" + "testing" + + "github.com/bwmarrin/discordgo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + mocks "github.com/nijeti/cinema-keeper/internal/generated/mocks/services/addQuote" + "github.com/nijeti/cinema-keeper/internal/models" + "github.com/nijeti/cinema-keeper/internal/services/addQuote" +) + +func TestService_Exec(t *testing.T) { + t.Parallel() + + ctx := context.Background() + i := &discordgo.Interaction{ + GuildID: "1", + Member: &discordgo.Member{ + User: &discordgo.User{ + ID: "2", + }, + Nick: "addedBy", + Avatar: "avatar", + }, + } + author := &discordgo.Member{ + User: &discordgo.User{ + ID: "1", + }, + Nick: "author", + Avatar: "avatar", + } + const text = "text" + + type setup func(t *testing.T, err error) *addQuote.Service + + tests := map[string]struct { + err error + setup setup + }{ + "guild_member_error": { + err: errors.New("guild member error"), + setup: func(t *testing.T, err error) *addQuote.Service { + d := mocks.NewMockDiscord(t) + db := mocks.NewMockDb(t) + + d.EXPECT().GuildMember( + ctx, models.ID(i.GuildID), models.ID(author.User.ID), + ).Return(nil, err) + + return addQuote.New(d, db) + }, + }, + "db_error": { + err: errors.New("db error"), + setup: func(t *testing.T, err error) *addQuote.Service { + d := mocks.NewMockDiscord(t) + db := mocks.NewMockDb(t) + + d.EXPECT().GuildMember( + ctx, models.ID(i.GuildID), models.ID(author.User.ID), + ).Return(author, nil) + + db.EXPECT().AddUserQuoteOnGuild(ctx, mock.Anything).Return(err) + + return addQuote.New(d, db) + }, + }, + "respond_error": { + err: errors.New("respond error"), + setup: func(t *testing.T, err error) *addQuote.Service { + d := mocks.NewMockDiscord(t) + db := mocks.NewMockDb(t) + + d.EXPECT().GuildMember( + ctx, models.ID(i.GuildID), models.ID(author.User.ID), + ).Return(author, nil) + + db.EXPECT().AddUserQuoteOnGuild(ctx, mock.Anything).Return(nil) + + d.EXPECT().Respond(ctx, i, mock.Anything).Return(err) + + return addQuote.New(d, db) + }, + }, + "success": { + err: nil, + setup: func(t *testing.T, _ error) *addQuote.Service { + d := mocks.NewMockDiscord(t) + db := mocks.NewMockDb(t) + + d.EXPECT().GuildMember( + ctx, models.ID(i.GuildID), models.ID(author.User.ID), + ).Return(author, nil) + + db.EXPECT().AddUserQuoteOnGuild( + ctx, mock.Anything, + ).RunAndReturn( + func(_ context.Context, quote *models.Quote) error { + assert.Equal(t, author, quote.Author) + assert.Equal(t, text, quote.Text) + assert.Equal(t, i.GuildID, quote.GuildID.String()) + assert.Equal(t, i.Member, quote.AddedBy) + + return nil + }, + ) + + d.EXPECT().Respond(ctx, i, mock.Anything).Return(nil) + + return addQuote.New(d, db) + }, + }, + } + + for name, tt := range tests { + t.Run( + name, func(t *testing.T) { + s := tt.setup(t, tt.err) + err := s.Exec(ctx, i, models.ID(author.User.ID), text) + assert.ErrorIs(t, err, tt.err) + }, + ) + } +} diff --git a/internal/services/diceRoll/service_test.go b/internal/services/diceRoll/service_test.go index 8fc9f79..21022b6 100644 --- a/internal/services/diceRoll/service_test.go +++ b/internal/services/diceRoll/service_test.go @@ -1,4 +1,3 @@ -//nolint:dupl // table-driven tests package diceRoll_test import ( @@ -106,7 +105,7 @@ func TestService_Exec(t *testing.T) { "multiple_d100_roll": { args: args{ size: dice.D100, - count: 10, //nolint:revive // random value + count: 10, }, err: nil, setup: func(t *testing.T, args args, err error) *diceRoll.Service { diff --git a/internal/services/lockVoiceChan/service.go b/internal/services/lockVoiceChan/service.go index e8d8b84..0161010 100644 --- a/internal/services/lockVoiceChan/service.go +++ b/internal/services/lockVoiceChan/service.go @@ -63,9 +63,7 @@ func (s *Service) Exec( } err = s.discord.EditChannel( - ctx, - models.ID(voiceState.ChannelID), - &discordgo.ChannelEdit{UserLimit: *limit}, + ctx, models.ID(channel.ID), &discordgo.ChannelEdit{UserLimit: *limit}, ) if err != nil { return fmt.Errorf("failed to edit channel user limit: %w", err) diff --git a/internal/services/lockVoiceChan/service_test.go b/internal/services/lockVoiceChan/service_test.go new file mode 100644 index 0000000..4495332 --- /dev/null +++ b/internal/services/lockVoiceChan/service_test.go @@ -0,0 +1,372 @@ +package lockVoiceChan_test + +import ( + "context" + "errors" + "testing" + + "github.com/bwmarrin/discordgo" + "github.com/stretchr/testify/assert" + + "github.com/nijeti/cinema-keeper/internal/discord/commands" + "github.com/nijeti/cinema-keeper/internal/discord/commands/responses" + mocks "github.com/nijeti/cinema-keeper/internal/generated/mocks/services/lockVoiceChan" + "github.com/nijeti/cinema-keeper/internal/models" + "github.com/nijeti/cinema-keeper/internal/pkg/ptr" + "github.com/nijeti/cinema-keeper/internal/services/lockVoiceChan" +) + +func TestService_Exec(t *testing.T) { + t.Parallel() + + ctx := context.Background() + i := &discordgo.Interaction{ + GuildID: "1", + Member: &discordgo.Member{ + User: &discordgo.User{ + ID: "2", + }, + }, + } + + type args struct { + limit *int + } + type setup func(t *testing.T, args args, err error) *lockVoiceChan.Service + + tests := map[string]struct { + args args + err error + setup setup + }{ + "voice_state_error": { + args: args{ + limit: nil, + }, + err: errors.New("voice state error"), + setup: func( + t *testing.T, _ args, err error, + ) *lockVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(nil, err) + + return lockVoiceChan.New(d) + }, + }, + "voice_state_nil_response_error": { + args: args{ + limit: nil, + }, + err: errors.New("response error"), + setup: func( + t *testing.T, _ args, err error, + ) *lockVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(nil, nil) + + d.EXPECT().Respond( + ctx, i, responses.UserNotInVoiceChannel(), + ).Return(err) + + return lockVoiceChan.New(d) + }, + }, + "voice_channel_users_error": { + args: args{ + limit: nil, + }, + err: errors.New("voice channel users error"), + setup: func( + t *testing.T, _ args, err error, + ) *lockVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), models.ID(vs.ChannelID), + ).Return(nil, err) + + return lockVoiceChan.New(d) + }, + }, + "channel_error": { + args: args{ + limit: nil, + }, + err: errors.New("channel error"), + setup: func( + t *testing.T, _ args, err error, + ) *lockVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + users := make([]*discordgo.Member, 1) + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), models.ID(vs.ChannelID), + ).Return(users, nil) + + d.EXPECT().Channel( + ctx, models.ID(vs.ChannelID), + ).Return(nil, err) + + return lockVoiceChan.New(d) + }, + }, + "too_many_users_response_error": { + args: args{ + limit: nil, + }, + err: errors.New("response error"), + setup: func( + t *testing.T, _ args, err error, + ) *lockVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + users := make( + []*discordgo.Member, commands.LockOptionLimitMaxValue+1, + ) + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), models.ID(vs.ChannelID), + ).Return(users, nil) + + c := &discordgo.Channel{ID: vs.ChannelID} + d.EXPECT().Channel( + ctx, models.ID(vs.ChannelID), + ).Return(c, nil) + + d.EXPECT().Respond( + ctx, i, responses.LockSpecifyLimit(c), + ).Return(err) + + return lockVoiceChan.New(d) + }, + }, + "edit_channel_error": { + args: args{ + limit: nil, + }, + err: errors.New("edit channel error"), + setup: func( + t *testing.T, _ args, err error, + ) *lockVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + users := make([]*discordgo.Member, 1) + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), models.ID(vs.ChannelID), + ).Return(users, nil) + + c := &discordgo.Channel{ID: vs.ChannelID} + d.EXPECT().Channel( + ctx, models.ID(vs.ChannelID), + ).Return(c, nil) + + d.EXPECT().EditChannel( + ctx, + models.ID(c.ID), + &discordgo.ChannelEdit{UserLimit: len(users)}, + ).Return(err) + + return lockVoiceChan.New(d) + }, + }, + "response_error": { + args: args{ + limit: nil, + }, + err: errors.New("response error"), + setup: func( + t *testing.T, _ args, err error, + ) *lockVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + users := make([]*discordgo.Member, 1) + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), models.ID(vs.ChannelID), + ).Return(users, nil) + + c := &discordgo.Channel{ID: vs.ChannelID} + d.EXPECT().Channel( + ctx, models.ID(vs.ChannelID), + ).Return(c, nil) + + d.EXPECT().EditChannel( + ctx, + models.ID(c.ID), + &discordgo.ChannelEdit{UserLimit: len(users)}, + ).Return(nil) + + d.EXPECT().Respond( + ctx, i, responses.LockDone(c, len(users)), + ).Return(err) + + return lockVoiceChan.New(d) + }, + }, + "voice_state_nil": { + args: args{ + limit: nil, + }, + err: nil, + setup: func( + t *testing.T, _ args, _ error, + ) *lockVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(nil, nil) + + d.EXPECT().Respond( + ctx, i, responses.UserNotInVoiceChannel(), + ).Return(nil) + + return lockVoiceChan.New(d) + }, + }, + "too_many_users": { + args: args{ + limit: nil, + }, + err: nil, + setup: func( + t *testing.T, _ args, _ error, + ) *lockVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + users := make( + []*discordgo.Member, commands.LockOptionLimitMaxValue+1, + ) + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), models.ID(vs.ChannelID), + ).Return(users, nil) + + c := &discordgo.Channel{ID: vs.ChannelID} + d.EXPECT().Channel( + ctx, models.ID(vs.ChannelID), + ).Return(c, nil) + + d.EXPECT().Respond( + ctx, i, responses.LockSpecifyLimit(c), + ).Return(nil) + + return lockVoiceChan.New(d) + }, + }, + "success_limit_nil": { + args: args{ + limit: nil, + }, + err: nil, + setup: func( + t *testing.T, _ args, _ error, + ) *lockVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + users := make([]*discordgo.Member, 1) + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), models.ID(vs.ChannelID), + ).Return(users, nil) + + c := &discordgo.Channel{ID: vs.ChannelID} + d.EXPECT().Channel( + ctx, models.ID(vs.ChannelID), + ).Return(c, nil) + + d.EXPECT().EditChannel( + ctx, + models.ID(c.ID), + &discordgo.ChannelEdit{UserLimit: len(users)}, + ).Return(nil) + + d.EXPECT().Respond( + ctx, i, responses.LockDone(c, len(users)), + ).Return(nil) + + return lockVoiceChan.New(d) + }, + }, + "success": { + args: args{ + limit: ptr.To(1), + }, + err: nil, + setup: func( + t *testing.T, args args, _ error, + ) *lockVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + c := &discordgo.Channel{ID: vs.ChannelID} + d.EXPECT().Channel( + ctx, models.ID(vs.ChannelID), + ).Return(c, nil) + + d.EXPECT().EditChannel( + ctx, + models.ID(c.ID), + &discordgo.ChannelEdit{UserLimit: *args.limit}, + ).Return(nil) + + d.EXPECT().Respond( + ctx, i, responses.LockDone(c, *args.limit), + ).Return(nil) + + return lockVoiceChan.New(d) + }, + }, + } + + for name, tt := range tests { + t.Run( + name, func(t *testing.T) { + s := tt.setup(t, tt.args, tt.err) + err := s.Exec(ctx, i, tt.args.limit) + assert.ErrorIs(t, err, tt.err) + }, + ) + } +} diff --git a/internal/services/mentionVoiceChan/service.go b/internal/services/mentionVoiceChan/service.go index 9ba7d3a..4cf8945 100644 --- a/internal/services/mentionVoiceChan/service.go +++ b/internal/services/mentionVoiceChan/service.go @@ -32,7 +32,7 @@ func (s *Service) Exec( } if channelID == nil { - err = s.discord.Respond(ctx, i, responses.UserNotInVoiceChannel()) + err = s.discord.Respond(ctx, i, responses.CastNoChannel()) if err != nil { return fmt.Errorf("failed to respond: %w", err) } @@ -77,5 +77,8 @@ func (s *Service) verifyChannelID( return nil, fmt.Errorf("failed to get user voice channel: %w", err) } + if voiceState == nil { + return nil, nil //nolint:nilnil // nil is a valid value + } return ptr.To(models.ID(voiceState.ChannelID)), nil } diff --git a/internal/services/mentionVoiceChan/service_test.go b/internal/services/mentionVoiceChan/service_test.go new file mode 100644 index 0000000..ec939aa --- /dev/null +++ b/internal/services/mentionVoiceChan/service_test.go @@ -0,0 +1,263 @@ +package mentionVoiceChan_test + +import ( + "context" + "errors" + "testing" + + "github.com/bwmarrin/discordgo" + "github.com/stretchr/testify/assert" + + "github.com/nijeti/cinema-keeper/internal/discord/commands/responses" + mocks "github.com/nijeti/cinema-keeper/internal/generated/mocks/services/mentionVoiceChan" + "github.com/nijeti/cinema-keeper/internal/models" + "github.com/nijeti/cinema-keeper/internal/pkg/ptr" + "github.com/nijeti/cinema-keeper/internal/services/mentionVoiceChan" +) + +func TestService_Exec(t *testing.T) { + t.Parallel() + + ctx := context.Background() + i := &discordgo.Interaction{ + GuildID: "1", + Member: &discordgo.Member{ + User: &discordgo.User{ + ID: "2", + }, + }, + } + + type args struct { + channelID *models.ID + } + type setup func( + t *testing.T, args args, err error, + ) *mentionVoiceChan.Service + + tests := map[string]struct { + args args + err error + setup setup + }{ + "voice_state_error": { + args: args{ + channelID: nil, + }, + err: errors.New("voice state error"), + setup: func( + t *testing.T, _ args, err error, + ) *mentionVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(nil, err) + + return mentionVoiceChan.New(d) + }, + }, + "no_channel_response_error": { + args: args{ + channelID: nil, + }, + err: errors.New("response error"), + setup: func( + t *testing.T, _ args, err error, + ) *mentionVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(nil, nil) + + d.EXPECT().Respond( + ctx, i, responses.CastNoChannel(), + ).Return(err) + + return mentionVoiceChan.New(d) + }, + }, + "voice_channel_users_error": { + args: args{ + channelID: nil, + }, + err: errors.New("voice channel users error"), + setup: func( + t *testing.T, _ args, err error, + ) *mentionVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), models.ID(vs.ChannelID), + ).Return(nil, err) + + return mentionVoiceChan.New(d) + }, + }, + "response_error": { + args: args{ + channelID: nil, + }, + err: errors.New("response error"), + setup: func( + t *testing.T, _ args, err error, + ) *mentionVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), models.ID(vs.ChannelID), + ).Return(nil, nil) + + d.EXPECT().Respond(ctx, i, responses.CastNoUsers()).Return(err) + + return mentionVoiceChan.New(d) + }, + }, + "no_channel": { + args: args{ + channelID: nil, + }, + err: nil, + setup: func( + t *testing.T, _ args, _ error, + ) *mentionVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(nil, nil) + + d.EXPECT().Respond( + ctx, i, responses.CastNoChannel(), + ).Return(nil) + + return mentionVoiceChan.New(d) + }, + }, + "no_users": { + args: args{ + channelID: nil, + }, + err: nil, + setup: func( + t *testing.T, _ args, _ error, + ) *mentionVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), models.ID(vs.ChannelID), + ).Return(nil, nil) + + d.EXPECT().Respond(ctx, i, responses.CastNoUsers()).Return(nil) + + return mentionVoiceChan.New(d) + }, + }, + "self_only": { + args: args{ + channelID: nil, + }, + err: nil, + setup: func( + t *testing.T, _ args, _ error, + ) *mentionVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + users := []*discordgo.Member{i.Member} + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), models.ID(vs.ChannelID), + ).Return(users, nil) + + d.EXPECT().Respond(ctx, i, responses.CastNoUsers()).Return(nil) + + return mentionVoiceChan.New(d) + }, + }, + "success_channel_id_nil": { + args: args{ + channelID: nil, + }, + err: nil, + setup: func( + t *testing.T, _ args, _ error, + ) *mentionVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + vs := &discordgo.VoiceState{ChannelID: "3"} + d.EXPECT().UserVoiceState( + ctx, models.ID(i.GuildID), models.ID(i.Member.User.ID), + ).Return(vs, nil) + + users := []*discordgo.Member{ + i.Member, + {User: &discordgo.User{ID: "4"}}, + } + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), models.ID(vs.ChannelID), + ).Return(users, nil) + + d.EXPECT().Respond( + ctx, i, responses.CastUsers(i.Member, users), + ).Return(nil) + + return mentionVoiceChan.New(d) + }, + }, + "success": { + args: args{ + channelID: ptr.To(models.ID("3")), + }, + err: nil, + setup: func( + t *testing.T, args args, _ error, + ) *mentionVoiceChan.Service { + d := mocks.NewMockDiscord(t) + + users := []*discordgo.Member{ + i.Member, + {User: &discordgo.User{ID: "4"}}, + } + d.EXPECT().VoiceChannelUsers( + ctx, models.ID(i.GuildID), *args.channelID, + ).Return(users, nil) + + d.EXPECT().Respond( + ctx, i, responses.CastUsers(i.Member, users), + ).Return(nil) + + return mentionVoiceChan.New(d) + }, + }, + } + + for name, tt := range tests { + t.Run( + name, func(t *testing.T) { + s := tt.setup(t, tt.args, tt.err) + err := s.Exec(ctx, i, tt.args.channelID) + assert.ErrorIs(t, err, tt.err) + }, + ) + } +} diff --git a/internal/services/unlockVoiceChan/service_test.go b/internal/services/unlockVoiceChan/service_test.go index 653d3fc..f1ea8f9 100644 --- a/internal/services/unlockVoiceChan/service_test.go +++ b/internal/services/unlockVoiceChan/service_test.go @@ -1,4 +1,3 @@ -//nolint:dupl // table-driven tests package unlockVoiceChan_test import (