From dd12ff619db8425d566c66dea9b62835a29e8b28 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 13:11:06 +0200 Subject: [PATCH 01/22] fix(CreateLoot): pg throw error if raid doesnt exist --- internal/usecase/loot.go | 3 +++ internal/usecase/loot_test.go | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/internal/usecase/loot.go b/internal/usecase/loot.go index b7821bbf..08874bd8 100644 --- a/internal/usecase/loot.go +++ b/internal/usecase/loot.go @@ -26,6 +26,9 @@ func (puc LootUseCase) CreateLoot(ctx context.Context, lootName string, raidID i if err != nil { return fmt.Errorf("CreateLoot - backend.ReadRaid: %w", err) } + if raid.ID == 0 { + return fmt.Errorf("raid not found") + } player, err := puc.backend.SearchPlayer(ctx, -1, playerName, "") if err != nil { diff --git a/internal/usecase/loot_test.go b/internal/usecase/loot_test.go index b3e5a1d2..5edda5f9 100644 --- a/internal/usecase/loot_test.go +++ b/internal/usecase/loot_test.go @@ -164,3 +164,7 @@ func TestLootUseCase_DeleteLoot(t *testing.T) { mockBackend.AssertExpectations(t) }) } + +func TestLootUseCase_CreateLoot(t *testing.T) { + +} From 286ff05efb0cb9a9e6739b24204f50ed9b002779 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 13:11:32 +0200 Subject: [PATCH 02/22] chore: bug cannot be reproduce --- TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 29ce586a..7c3c84c6 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,6 @@ - [ ] Add more logs for each level - [ ] Add tracing for each request - [ ] Add API as controller -- [ ] Fix In UpdatePlayer: placeholder $1 already has type int, cannot assign varchar - [ ] Add notes to players (for example: player cannot play on wednesday) ### In Progress @@ -23,3 +22,4 @@ - [X] Add In CreatePlayer id to player entity - [X] Add Usecase: player name is not discord name. Must implement a way to link them - [X] Fix in SearchPlayer: player strikes must be import there not in usecase +- [X] Fix In UpdatePlayer: placeholder $1 already has type int, cannot assign varchar From ea4b293cc641a8a11929cdfeb66f80affa1364a4 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 13:32:18 +0200 Subject: [PATCH 03/22] feat: add support to delete command when shutdown --- config/config.go | 5 +++-- config/config.yml | 4 ++++ internal/app/app.go | 3 ++- pkg/discord/options.go | 6 ++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/config/config.go b/config/config.go index f571f0fb..bcf4e16a 100644 --- a/config/config.go +++ b/config/config.go @@ -26,8 +26,9 @@ type ( // Discord -. Discord struct { - Token string `env:"DISCORD_TOKEN" env-required:"true" yaml:"token"` - GuildID int `env:"DISCORD_GUILD_ID" env-required:"true" yaml:"guild_id"` + Token string `env:"DISCORD_TOKEN" env-required:"true" yaml:"token"` + GuildID int `env:"DISCORD_GUILD_ID" env-required:"true" yaml:"guild_id"` + DeleteCommands bool `env:"DISCORD_DELETE_COMMANDS" env-required:"true" yaml:"delete_commands"` } // Log -. diff --git a/config/config.yml b/config/config.yml index 6a53c535..b7ae388b 100644 --- a/config/config.yml +++ b/config/config.yml @@ -9,7 +9,11 @@ logger: metrics: port: 2213 +discord: + delete_commands: true + postgres: pool_max: 10 conn_attempts: 10 conn_timeout: 2s + url: user=coven-bot dbname=coven password=JcJXS6FFsRjHD60vgbajSQ host=coven-bot-9798.8nj.cockroachlabs.cloud port=26257 sslmode=verify-full diff --git a/internal/app/app.go b/internal/app/app.go index 64d90b24..e4f43cf8 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -76,7 +76,8 @@ func Run(ctx context.Context, cfg *config.Config) { discord.CommandHandlers(mapHandler), discord.Token(cfg.Discord.Token), discord.Command(handlers), - discord.GuildID(cfg.Discord.GuildID)) + discord.GuildID(cfg.Discord.GuildID), + discord.DeleteCommands(cfg.Discord.DeleteCommands)) zap.L().Info("starting to serve to discord webhooks") err = serve.Run(ctx) diff --git a/pkg/discord/options.go b/pkg/discord/options.go index 22899ccf..d559e879 100644 --- a/pkg/discord/options.go +++ b/pkg/discord/options.go @@ -34,3 +34,9 @@ func Command(m []*discordgo.ApplicationCommand) Option { d.commands = m } } + +func DeleteCommands(b bool) Option { + return func(d *Discord) { + d.DeleteCommands = b + } +} From 062c251c2d89241ff1cd05de9379bf3388b3860e Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 13:33:24 +0200 Subject: [PATCH 04/22] fix: raid-id should be an integer --- internal/controller/discord/loot.go | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/internal/controller/discord/loot.go b/internal/controller/discord/loot.go index d87d8467..72726250 100644 --- a/internal/controller/discord/loot.go +++ b/internal/controller/discord/loot.go @@ -22,9 +22,9 @@ var LootDescriptors = []discordgo.ApplicationCommand{ Required: true, }, { - Type: discordgo.ApplicationCommandOptionString, + Type: discordgo.ApplicationCommandOptionInteger, Name: "raid-id", - Description: "ex: Milowenn", + Description: "ex: 4488766425", Required: true, }, { @@ -100,20 +100,10 @@ func (d Discord) AttributeLootHandler( } lootName := optionMap["loot-name"].StringValue() - raidID, err := strconv.Atoi(optionMap["raid-id"].StringValue()) - if err != nil { - msg := "Erreur lors de l'attribution du loot: " + err.Error() - _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: msg, - }, - }) - return fmt.Errorf("discord - AttributeLootHandler - strconv.Atoi: %w", err) - } + raidID := optionMap["raid-id"].IntValue() playerName := optionMap["player-name"].StringValue() - err = d.LootUseCase.CreateLoot(ctx, lootName, raidID, playerName) + err := d.LootUseCase.CreateLoot(ctx, lootName, int(raidID), playerName) if err != nil { msg := "Erreur lors de l'attribution du loot: " + err.Error() _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ From 30b425acf93d211baa5db93dd9a61eb19c95e3ae Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 13:33:35 +0200 Subject: [PATCH 05/22] chore(tests): add tests on usecase/CreateLoot --- internal/usecase/loot_test.go | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/internal/usecase/loot_test.go b/internal/usecase/loot_test.go index 5edda5f9..00e44c76 100644 --- a/internal/usecase/loot_test.go +++ b/internal/usecase/loot_test.go @@ -166,5 +166,55 @@ func TestLootUseCase_DeleteLoot(t *testing.T) { } func TestLootUseCase_CreateLoot(t *testing.T) { + t.Parallel() + + t.Run("context is done", func(t *testing.T) { + t.Parallel() + + mockBackend := mocks.NewBackend(t) + + LootUseCase := usecase.NewLootUseCase(mockBackend) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := LootUseCase.CreateLoot(ctx, "lootone", 1, "gilbert") + assert.Error(t, err) + mockBackend.AssertExpectations(t) + }) + + t.Run("Success", func(t *testing.T) { + t.Parallel() + + mockBackend := mocks.NewBackend(t) + LootUseCase := usecase.NewLootUseCase(mockBackend) + + mockBackend.On("ReadRaid", mock.Anything, mock.Anything).Return(entity.Raid{ID: 1}, nil) + mockBackend.On("SearchPlayer", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return([]entity.Player{{ID: 1}}, nil) + mockBackend.On("CreateLoot", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(entity.Loot{}, nil) + + err := LootUseCase.CreateLoot(context.Background(), "lootone", 1, "gilbert") + assert.NoError(t, err) + mockBackend.AssertExpectations(t) + }) + + t.Run("Backend Error", func(t *testing.T) { + t.Parallel() + + mockBackend := mocks.NewBackend(t) + + LootUseCase := usecase.NewLootUseCase(mockBackend) + + mockBackend.On("ReadRaid", mock.Anything, mock.Anything).Return(entity.Raid{ID: 1}, nil) + mockBackend.On("SearchPlayer", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return([]entity.Player{{ID: 1}}, nil) + mockBackend.On("CreateLoot", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(entity.Loot{}, errors.New("Backend Error")) + + err := LootUseCase.CreateLoot(context.Background(), "lootone", 1, "gilbert") + assert.Error(t, err) + mockBackend.AssertExpectations(t) + }) } From 7c5d2352a2d4e51383e074d6923d5c38bb2cf5f5 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 13:46:14 +0200 Subject: [PATCH 06/22] chore: make discord error more human readable --- internal/controller/discord/absence.go | 4 +-- internal/controller/discord/loot.go | 8 ++--- internal/controller/discord/main.go | 6 ++++ internal/controller/discord/main_test.go | 39 ++++++++++++++++++++++++ internal/controller/discord/player.go | 10 +++--- internal/controller/discord/raid.go | 6 ++-- internal/controller/discord/strike.go | 8 ++--- 7 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 internal/controller/discord/main_test.go diff --git a/internal/controller/discord/absence.go b/internal/controller/discord/absence.go index f6b77d1a..f72b8df2 100644 --- a/internal/controller/discord/absence.go +++ b/internal/controller/discord/absence.go @@ -70,12 +70,12 @@ func (d Discord) ListAbsenceHandler( var msg string dates, err := parseDate(optionMap["date"].StringValue()) if err != nil { - msg = "Erreur lors de la récupération des absences: " + err.Error() + msg = "Erreur lors de la récupération des absences: " + HumanReadableError(err) } else { msg = "Absences pour le " + dates[0].Format("02-01-2006") + ":\n" absences, err := d.ListAbsence(ctx, dates[0]) if err != nil { - msg = "Erreur lors de la récupération des absences: " + err.Error() + msg = "Erreur lors de la récupération des absences: " + HumanReadableError(err) } else { for _, absence := range absences { msg += absence.Player.Name + "\n" diff --git a/internal/controller/discord/loot.go b/internal/controller/discord/loot.go index 72726250..7e3fdc45 100644 --- a/internal/controller/discord/loot.go +++ b/internal/controller/discord/loot.go @@ -105,7 +105,7 @@ func (d Discord) AttributeLootHandler( err := d.LootUseCase.CreateLoot(ctx, lootName, int(raidID), playerName) if err != nil { - msg := "Erreur lors de l'attribution du loot: " + err.Error() + msg := "Erreur lors de l'attribution du loot: " + HumanReadableError(err) _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ @@ -138,7 +138,7 @@ func (d Discord) ListLootsOnPlayerHandler( lootList, err := d.LootUseCase.ListLootOnPLayer(ctx, playerName) if err != nil { - msg := "Erreur lors de la récupération des loots: " + err.Error() + msg := "Erreur lors de la récupération des loots: " + HumanReadableError(err) _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ @@ -180,7 +180,7 @@ func (d Discord) DeleteLootHandler( err = d.LootUseCase.DeleteLoot(ctx, id) if err != nil { - msg := "Erreur lors de la suppression du loot: " + err.Error() + msg := "Erreur lors de la suppression du loot: " + HumanReadableError(err) _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ @@ -217,7 +217,7 @@ func (d Discord) LootCounterCheckerHandler( player, err := d.LootUseCase.SelectPlayerToAssign(ctx, playerNames, difficulty) if err != nil { - msg := "Erreur lors de l'assignation du loot: " + err.Error() + msg := "Erreur lors de l'assignation du loot: " + HumanReadableError(err) _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ diff --git a/internal/controller/discord/main.go b/internal/controller/discord/main.go index b878e44d..26468007 100644 --- a/internal/controller/discord/main.go +++ b/internal/controller/discord/main.go @@ -16,6 +16,12 @@ type Discord struct { *usecase.RaidUseCase } +// HumanReadableError returns the error message without the package name. +func HumanReadableError(err error) string { + // output only what is after ": " in the error message. If multiple :, give all after the first one. + return strings.Split(err.Error(), ": ")[1] +} + func parseDate(dateStr string) ([]time.Time, error) { dateStr = strings.TrimSpace(dateStr) dateParts := strings.Split(dateStr, " au ") diff --git a/internal/controller/discord/main_test.go b/internal/controller/discord/main_test.go new file mode 100644 index 00000000..3f9f344b --- /dev/null +++ b/internal/controller/discord/main_test.go @@ -0,0 +1,39 @@ +package discordhandler_test + +import ( + "fmt" + "testing" + + discordHandler "github.com/antony-ramos/guildops/internal/controller/discord" +) + +func TestHumanReadableError(t *testing.T) { + t.Parallel() + type args struct { + err error + } + tests := []struct { + name string + args args + want string + }{ + { + "Success", + args{ + err: fmt.Errorf("discord - parseDate - time.Parse: parsing time \"01/01/21\" month out of range"), + }, + "parsing time \"01/01/21\" month out of range", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + want := tt.want + args := tt.args + t.Parallel() + if got := discordHandler.HumanReadableError(args.err); got != want { + t.Errorf("HumanReadableError() = %v, want %v", got, want) + } + }) + } +} diff --git a/internal/controller/discord/player.go b/internal/controller/discord/player.go index e32353d2..00408845 100644 --- a/internal/controller/discord/player.go +++ b/internal/controller/discord/player.go @@ -89,7 +89,7 @@ func (d Discord) PlayerHandler( if interaction.ApplicationCommandData().Name == "coven-player-create" { id, err := d.CreatePlayer(ctx, name) if err != nil { - msg = "Erreur lors de la création du joueur: " + err.Error() + msg = "Erreur lors de la création du joueur: " + HumanReadableError(err) returnErr = err } else { msg = "Joueur " + name + " créé avec succès : ID " + strconv.Itoa(id) @@ -97,7 +97,7 @@ func (d Discord) PlayerHandler( } else { err := d.DeletePlayer(ctx, name) if err != nil { - msg = "Erreur lors de la suppression du joueur: " + err.Error() + msg = "Erreur lors de la suppression du joueur: " + HumanReadableError(err) returnErr = err } else { msg = "Joueur " + name + " supprimé avec succès" @@ -131,7 +131,7 @@ func (d Discord) GetPlayerHandler( player, err := d.ReadPlayer(ctx, name) // Show on string all info about player if err != nil { - msg = "Erreur lors de la récupération du joueur: " + err.Error() + msg = "Erreur lors de la récupération du joueur: " + HumanReadableError(err) _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ @@ -210,7 +210,7 @@ func (d Discord) LinkPlayerHandler( player, err := d.ReadPlayer(ctx, name) // Show on string all info about player if err != nil { - msg = "Erreur lors de la récupération du joueur: " + err.Error() + msg = "Erreur lors de la récupération du joueur: " + HumanReadableError(err) _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ @@ -222,7 +222,7 @@ func (d Discord) LinkPlayerHandler( err = d.LinkPlayer(ctx, player.Name, interaction.Member.User.Username) if err != nil { - msg = "Erreur lors de la liaison avec le joueur : " + err.Error() + msg = "Erreur lors de la liaison avec le joueur : " + HumanReadableError(err) _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ diff --git a/internal/controller/discord/raid.go b/internal/controller/discord/raid.go index 22ddcb0f..c708f1a9 100644 --- a/internal/controller/discord/raid.go +++ b/internal/controller/discord/raid.go @@ -93,7 +93,7 @@ func (d Discord) CreateRaidHandler( name := optionMap["name"].StringValue() date, err := parseDate(optionMap["date"].StringValue()) if err != nil { - msg = "Erreur lors de la création du raid: " + err.Error() + msg = "Erreur lors de la création du raid: " + HumanReadableError(err) _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ @@ -106,7 +106,7 @@ func (d Discord) CreateRaidHandler( raid, err := d.CreateRaid(ctx, name, difficulty, date[0]) if err != nil { - msg = "Erreur lors de la création du raid: " + err.Error() + msg = "Erreur lors de la création du raid: " + HumanReadableError(err) returnErr = err } else { msg = "Raid " + strconv.Itoa(raid.ID) + " créé avec succès" @@ -144,7 +144,7 @@ func (d Discord) DeleteRaidHandler( err = d.DeleteRaid(ctx, raidID) if err != nil { - msg = "Erreur lors de la suppression du joueur: " + err.Error() + msg = "Erreur lors de la suppression du joueur: " + HumanReadableError(err) returnErr = err } else { msg = "Joueur " + strconv.Itoa(raidID) + " supprimé avec succès" diff --git a/internal/controller/discord/strike.go b/internal/controller/discord/strike.go index 0ab9d2b7..05fffae5 100644 --- a/internal/controller/discord/strike.go +++ b/internal/controller/discord/strike.go @@ -82,7 +82,7 @@ func (d Discord) StrikeOnPlayerHandler( err := d.CreateStrike(ctx, reason, name) returnErr := error(nil) if err != nil { - msg = "Erreurs lors de la création du strike: " + err.Error() + msg = "Erreurs lors de la création du strike: " + HumanReadableError(err) returnErr = err } else { msg = "Strike créé avec succès" @@ -115,7 +115,7 @@ func (d Discord) ListStrikesOnPlayerHandler( strikes, err := d.ReadStrikes(ctx, playerName) if err != nil { - msg = "Erreurs lors de la récupération des strikes: " + err.Error() + msg = "Erreurs lors de la récupération des strikes: " + HumanReadableError(err) _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ @@ -157,12 +157,12 @@ func (d Discord) DeleteStrikeHandler( strikeID, err := strconv.ParseInt(idString, 10, 64) returnErr := error(nil) if err != nil { - msg = "Erreurs lors de la suppression du strike: " + err.Error() + msg = "Erreurs lors de la suppression du strike: " + HumanReadableError(err) returnErr = err } else { err = d.DeleteStrike(ctx, int(strikeID)) if err != nil { - msg = "Erreurs lors de la suppression du strike: " + err.Error() + msg = "Erreurs lors de la suppression du strike: " + HumanReadableError(err) returnErr = err } else { msg = "Strike supprimé avec succès" From 962263c31ef811f5ec4d8c7303952a94ae364d12 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 13:48:08 +0200 Subject: [PATCH 07/22] chore(lint): config should be align --- config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index bcf4e16a..5571bebd 100644 --- a/config/config.go +++ b/config/config.go @@ -26,8 +26,8 @@ type ( // Discord -. Discord struct { - Token string `env:"DISCORD_TOKEN" env-required:"true" yaml:"token"` - GuildID int `env:"DISCORD_GUILD_ID" env-required:"true" yaml:"guild_id"` + Token string `env:"DISCORD_TOKEN" env-required:"true" yaml:"token"` + GuildID int `env:"DISCORD_GUILD_ID" env-required:"true" yaml:"guild_id"` DeleteCommands bool `env:"DISCORD_DELETE_COMMANDS" env-required:"true" yaml:"delete_commands"` } From bc4644effa6bcd42018aa219ee0d5b0e1073cf16 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 14:33:56 +0200 Subject: [PATCH 08/22] refact(ctrl/discord): make it testable --- internal/controller/discord/absence.go | 145 +++++++++--------- internal/controller/discord/absence_test.go | 126 +++++++++++++++ internal/controller/discord/main.go | 20 ++- .../discord/mocks/AbsenceUseCase.go | 86 +++++++++++ 4 files changed, 305 insertions(+), 72 deletions(-) create mode 100644 internal/controller/discord/absence_test.go create mode 100644 internal/controller/discord/mocks/AbsenceUseCase.go diff --git a/internal/controller/discord/absence.go b/internal/controller/discord/absence.go index f72b8df2..60e8bde8 100644 --- a/internal/controller/discord/absence.go +++ b/internal/controller/discord/absence.go @@ -2,11 +2,10 @@ package discordhandler import ( "context" - "sync" + "fmt" "time" "github.com/bwmarrin/discordgo" - "go.uber.org/zap" ) var AbsenceDescriptor = []discordgo.ApplicationCommand{ @@ -57,31 +56,42 @@ func (d Discord) InitAbsence() map[string]func( } } +func (d Discord) GenerateListAbsenceHandlerMsg(ctx context.Context, date string) (string, error) { + errorMsg := "Error while getting absence list: " + + select { + case <-ctx.Done(): + return "Error because request took too much time to complete", + fmt.Errorf("discord - GenerateListAbsenceHandlerMsg - ctx.Done: %w", ctx.Err()) + default: + var msg string + dates, err := parseDate(date) + if err != nil { + msg = errorMsg + HumanReadableError(err) + } else { + msg = "Absences pour le " + dates[0].Format("02-01-2006") + ":\n" + absences, err := d.ListAbsence(ctx, dates[0]) + if err != nil { + msg = errorMsg + HumanReadableError(err) + } else { + for _, absence := range absences { + msg += "* " + absence.Player.Name + "\n" + } + } + } + return msg, err + } +} + func (d Discord) ListAbsenceHandler( ctx context.Context, session *discordgo.Session, interaction *discordgo.InteractionCreate, ) error { options := interaction.ApplicationCommandData().Options optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) - for _, opt := range options { optionMap[opt.Name] = opt } - - var msg string - dates, err := parseDate(optionMap["date"].StringValue()) - if err != nil { - msg = "Erreur lors de la récupération des absences: " + HumanReadableError(err) - } else { - msg = "Absences pour le " + dates[0].Format("02-01-2006") + ":\n" - absences, err := d.ListAbsence(ctx, dates[0]) - if err != nil { - msg = "Erreur lors de la récupération des absences: " + HumanReadableError(err) - } else { - for _, absence := range absences { - msg += absence.Player.Name + "\n" - } - } - } + msg, err := d.GenerateListAbsenceHandlerMsg(ctx, optionMap["date"].StringValue()) _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ @@ -91,6 +101,44 @@ func (d Discord) ListAbsenceHandler( return err } +func (d Discord) GenerateAbsenceHandlerMsg( + ctx context.Context, user string, dates string, created bool, +) (string, error) { + errorMsg := "Error while creating absence: " + msg := "Absence(s) créée(s) pour le(s) :\n" + if !created { + errorMsg = "Error while deleting absence: " + msg = "Absence(s) supprimée(s) pour le(s) :\n" + } + select { + case <-ctx.Done(): + return "Error because request took too much time to complete", + fmt.Errorf("discord - GenerateAbsenceHandlerMsg - ctx.Done: %w", ctx.Err()) + default: + dates, err := parseDate(dates) + if err != nil { + return errorMsg + HumanReadableError(err), err + } + for _, date := range dates { + date := date + if !created { + err = d.DeleteAbsence(ctx, user, date) + if err != nil { + return errorMsg + HumanReadableError(err), err + } + msg += "* " + date.Format("02-01-2006") + "\n" + } else { + err = d.CreateAbsence(ctx, user, date) + if err != nil { + return errorMsg + HumanReadableError(err), err + } + msg += "* " + date.Format("02-01-2006") + "\n" + } + } + } + return msg, nil +} + func (d Discord) AbsenceHandler( ctx context.Context, session *discordgo.Session, interaction *discordgo.InteractionCreate, ) error { @@ -111,54 +159,13 @@ func (d Discord) AbsenceHandler( user = interaction.User.Username } - dates, err := parseDate(optionMap["date"].StringValue()) - var waitGroup sync.WaitGroup - for _, date := range dates { - date := date - waitGroup.Add(1) - go func() { - defer waitGroup.Done() - if interaction.ApplicationCommandData().Name == "coven-absence-delete" { - err = d.DeleteAbsence(ctx, user, date) - if err != nil { - err = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Erreur lors de la suppression de l'absence pour le " + date.Format("02-01-2006"), - }, - }) - } - err = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Absence supprimée pour le " + date.Format("02-01-2006"), - }, - }) - if err != nil { - zap.L().Error("discord - AbsenceHandler - session.InteractionRespond", zap.Error(err)) - } - } else { - err = d.CreateAbsence(ctx, user, date) - if err != nil { - err = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Erreur lors de la suppression de l'absence pour le " + date.Format("02-01-2006"), - }, - }) - } - err = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Absence créée pour le " + date.Format("02-01-2006"), - }, - }) - if err != nil { - zap.L().Error("discord - AbsenceHandler - session.InteractionRespond", zap.Error(err)) - } - } - }() - } - waitGroup.Wait() - return nil + msg, err := d.GenerateAbsenceHandlerMsg( + ctx, user, optionMap["date"].StringValue(), interaction.ApplicationCommandData().Name == "coven-absence-create") + _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: msg, + }, + }) + return err } diff --git a/internal/controller/discord/absence_test.go b/internal/controller/discord/absence_test.go new file mode 100644 index 00000000..4b7e0305 --- /dev/null +++ b/internal/controller/discord/absence_test.go @@ -0,0 +1,126 @@ +package discordhandler_test + +import ( + "context" + "errors" + discordHandler "github.com/antony-ramos/guildops/internal/controller/discord" + "testing" + + "github.com/antony-ramos/guildops/internal/controller/discord/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestDiscord_GenerateAbsenceHandlerMsg(t *testing.T) { + t.Parallel() + + t.Run("context is done", func(t *testing.T) { + t.Parallel() + + mockAbsenceUseCase := mocks.NewAbsenceUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: mockAbsenceUseCase, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := discord.GenerateAbsenceHandlerMsg(ctx, "playerone", "01/01/21", true) + + assert.Error(t, err) + mockAbsenceUseCase.AssertExpectations(t) + }) + + t.Run("Create Absence", func(t *testing.T) { + t.Parallel() + + mockAbsenceUseCase := mocks.NewAbsenceUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: mockAbsenceUseCase, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + mockAbsenceUseCase.On("CreateAbsence", mock.Anything, "playerone", mock.Anything).Return(nil) + + msg, err := discord.GenerateAbsenceHandlerMsg(context.Background(), "playerone", "01/01/21", true) + + assert.NoError(t, err) + assert.Equal(t, "Absence(s) créée(s) pour le(s) :\n* 01-01-2021\n", msg) + mockAbsenceUseCase.AssertExpectations(t) + }) + + t.Run("Delete Absence", func(t *testing.T) { + t.Parallel() + + mockAbsenceUseCase := mocks.NewAbsenceUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: mockAbsenceUseCase, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + mockAbsenceUseCase.On("DeleteAbsence", mock.Anything, "playerone", mock.Anything).Return(nil) + + msg, err := discord.GenerateAbsenceHandlerMsg(context.Background(), "playerone", "01/01/21", false) + + assert.NoError(t, err) + assert.Equal(t, "Absence(s) supprimée(s) pour le(s) :\n* 01-01-2021\n", msg) + mockAbsenceUseCase.AssertExpectations(t) + }) + + t.Run("Backend Error Create", func(t *testing.T) { + t.Parallel() + + mockAbsenceUseCase := mocks.NewAbsenceUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: mockAbsenceUseCase, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + mockAbsenceUseCase.On("CreateAbsence", mock.Anything, "playerone", mock.Anything).Return(errors.New("Backend Error")) + + msg, err := discord.GenerateAbsenceHandlerMsg(context.Background(), "playerone", "01/01/21", true) + + assert.Error(t, err) + assert.Equal(t, "Error while creating absence: Backend Error", msg) + mockAbsenceUseCase.AssertExpectations(t) + }) + + t.Run("Backend Error Delete", func(t *testing.T) { + t.Parallel() + + mockAbsenceUseCase := mocks.NewAbsenceUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: mockAbsenceUseCase, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + mockAbsenceUseCase.On("DeleteAbsence", mock.Anything, "playerone", mock.Anything).Return(errors.New("Backend Error")) + + msg, err := discord.GenerateAbsenceHandlerMsg(context.Background(), "playerone", "01/01/21", false) + + assert.Error(t, err) + assert.Equal(t, "Error while deleting absence: Backend Error", msg) + mockAbsenceUseCase.AssertExpectations(t) + }) +} diff --git a/internal/controller/discord/main.go b/internal/controller/discord/main.go index 26468007..e96a0581 100644 --- a/internal/controller/discord/main.go +++ b/internal/controller/discord/main.go @@ -1,25 +1,39 @@ package discordhandler import ( + "context" "fmt" "strings" "time" + "github.com/antony-ramos/guildops/internal/entity" + "github.com/antony-ramos/guildops/internal/usecase" ) type Discord struct { - *usecase.AbsenceUseCase + AbsenceUseCase *usecase.PlayerUseCase *usecase.StrikeUseCase *usecase.LootUseCase *usecase.RaidUseCase } +type AbsenceUseCase interface { + CreateAbsence(ctx context.Context, playerName string, date time.Time) error + DeleteAbsence(ctx context.Context, playerName string, date time.Time) error + ListAbsence(ctx context.Context, date time.Time) ([]entity.Absence, error) +} + // HumanReadableError returns the error message without the package name. func HumanReadableError(err error) string { - // output only what is after ": " in the error message. If multiple :, give all after the first one. - return strings.Split(err.Error(), ": ")[1] + str := strings.Split(err.Error(), ": ") + if len(str) == 1 { + return str[0] + } else if len(str) > 1 { + return strings.Join(str[1:], ": ") + } + return err.Error() } func parseDate(dateStr string) ([]time.Time, error) { diff --git a/internal/controller/discord/mocks/AbsenceUseCase.go b/internal/controller/discord/mocks/AbsenceUseCase.go new file mode 100644 index 00000000..e8f47f08 --- /dev/null +++ b/internal/controller/discord/mocks/AbsenceUseCase.go @@ -0,0 +1,86 @@ +// Code generated by mockery v2.33.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + entity "github.com/antony-ramos/guildops/internal/entity" + + mock "github.com/stretchr/testify/mock" + + time "time" +) + +// AbsenceUseCase is an autogenerated mock type for the AbsenceUseCase type +type AbsenceUseCase struct { + mock.Mock +} + +// CreateAbsence provides a mock function with given fields: ctx, playerName, date +func (_m *AbsenceUseCase) CreateAbsence(ctx context.Context, playerName string, date time.Time) error { + ret := _m.Called(ctx, playerName, date) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, time.Time) error); ok { + r0 = rf(ctx, playerName, date) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteAbsence provides a mock function with given fields: ctx, playerName, date +func (_m *AbsenceUseCase) DeleteAbsence(ctx context.Context, playerName string, date time.Time) error { + ret := _m.Called(ctx, playerName, date) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, time.Time) error); ok { + r0 = rf(ctx, playerName, date) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ListAbsence provides a mock function with given fields: ctx, date +func (_m *AbsenceUseCase) ListAbsence(ctx context.Context, date time.Time) ([]entity.Absence, error) { + ret := _m.Called(ctx, date) + + var r0 []entity.Absence + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, time.Time) ([]entity.Absence, error)); ok { + return rf(ctx, date) + } + if rf, ok := ret.Get(0).(func(context.Context, time.Time) []entity.Absence); ok { + r0 = rf(ctx, date) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]entity.Absence) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, time.Time) error); ok { + r1 = rf(ctx, date) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewAbsenceUseCase creates a new instance of AbsenceUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewAbsenceUseCase(t interface { + mock.TestingT + Cleanup(func()) +}) *AbsenceUseCase { + mock := &AbsenceUseCase{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} From c466749090b8272caf85c6ab75712acbf75ab13e Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 14:44:20 +0200 Subject: [PATCH 09/22] chore(tests): add tests on discord/absenceHandler --- internal/controller/discord/absence.go | 6 +- internal/controller/discord/absence_test.go | 124 +++++++++++++++++++- 2 files changed, 127 insertions(+), 3 deletions(-) diff --git a/internal/controller/discord/absence.go b/internal/controller/discord/absence.go index 60e8bde8..d51762a9 100644 --- a/internal/controller/discord/absence.go +++ b/internal/controller/discord/absence.go @@ -57,7 +57,8 @@ func (d Discord) InitAbsence() map[string]func( } func (d Discord) GenerateListAbsenceHandlerMsg(ctx context.Context, date string) (string, error) { - errorMsg := "Error while getting absence list: " + errorMsg := "Error while listing absences" + + ": " select { case <-ctx.Done(): @@ -69,10 +70,11 @@ func (d Discord) GenerateListAbsenceHandlerMsg(ctx context.Context, date string) if err != nil { msg = errorMsg + HumanReadableError(err) } else { - msg = "Absences pour le " + dates[0].Format("02-01-2006") + ":\n" + msg = "Absence(s) pour le " + dates[0].Format("02-01-2006") + " :\n" absences, err := d.ListAbsence(ctx, dates[0]) if err != nil { msg = errorMsg + HumanReadableError(err) + return msg, err } else { for _, absence := range absences { msg += "* " + absence.Player.Name + "\n" diff --git a/internal/controller/discord/absence_test.go b/internal/controller/discord/absence_test.go index 4b7e0305..fb82093e 100644 --- a/internal/controller/discord/absence_test.go +++ b/internal/controller/discord/absence_test.go @@ -3,8 +3,11 @@ package discordhandler_test import ( "context" "errors" - discordHandler "github.com/antony-ramos/guildops/internal/controller/discord" "testing" + "time" + + discordHandler "github.com/antony-ramos/guildops/internal/controller/discord" + "github.com/antony-ramos/guildops/internal/entity" "github.com/antony-ramos/guildops/internal/controller/discord/mocks" "github.com/stretchr/testify/assert" @@ -36,6 +39,25 @@ func TestDiscord_GenerateAbsenceHandlerMsg(t *testing.T) { mockAbsenceUseCase.AssertExpectations(t) }) + t.Run("Invalid date", func(t *testing.T) { + t.Parallel() + + mockAbsenceUseCase := mocks.NewAbsenceUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: mockAbsenceUseCase, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + _, err := discord.GenerateAbsenceHandlerMsg(context.Background(), "playerone", "01-01-2021", true) + + assert.Error(t, err) + mockAbsenceUseCase.AssertExpectations(t) + }) + t.Run("Create Absence", func(t *testing.T) { t.Parallel() @@ -124,3 +146,103 @@ func TestDiscord_GenerateAbsenceHandlerMsg(t *testing.T) { mockAbsenceUseCase.AssertExpectations(t) }) } + +func TestDiscord_GenerateListAbsenceHandlerMsg(t *testing.T) { + t.Parallel() + + t.Run("context is done", func(t *testing.T) { + t.Parallel() + + mockAbsenceUseCase := mocks.NewAbsenceUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: mockAbsenceUseCase, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := discord.GenerateListAbsenceHandlerMsg(ctx, "01/01/21") + + assert.Error(t, err) + mockAbsenceUseCase.AssertExpectations(t) + }) + + t.Run("Invalid date", func(t *testing.T) { + t.Parallel() + + mockAbsenceUseCase := mocks.NewAbsenceUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: mockAbsenceUseCase, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + _, err := discord.GenerateListAbsenceHandlerMsg(context.Background(), "01-01-2021") + + assert.Error(t, err) + mockAbsenceUseCase.AssertExpectations(t) + }) + + t.Run("Success", func(t *testing.T) { + t.Parallel() + + mockAbsenceUseCase := mocks.NewAbsenceUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: mockAbsenceUseCase, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + mockAbsenceUseCase.On("ListAbsence", mock.Anything, mock.Anything).Return([]entity.Absence{ + { + Player: &entity.Player{ + Name: "playerone", + }, + Raid: &entity.Raid{ + Name: "raidname", + Difficulty: "normal", + Date: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + }, + }, + }, nil) + + msg, err := discord.GenerateListAbsenceHandlerMsg(context.Background(), "01/01/21") + + assert.NoError(t, err) + assert.Equal(t, "Absence(s) pour le 01-01-2021 :\n* playerone\n", msg) + mockAbsenceUseCase.AssertExpectations(t) + }) + + t.Run("Backend Error", func(t *testing.T) { + t.Parallel() + + mockAbsenceUseCase := mocks.NewAbsenceUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: mockAbsenceUseCase, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + mockAbsenceUseCase.On("ListAbsence", mock.Anything, mock.Anything).Return(nil, errors.New("Backend Error")) + + msg, err := discord.GenerateListAbsenceHandlerMsg(context.Background(), "01/01/21") + + assert.Error(t, err) + assert.Equal(t, "Error while listing absences: Backend Error", msg) + mockAbsenceUseCase.AssertExpectations(t) + }) +} From 5ca880fc59386751eaaf05cb1f520f80b9bb57f5 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 15:16:51 +0200 Subject: [PATCH 10/22] chore(tests): add tests on discord/linkPlayer --- internal/controller/discord/absence.go | 4 +- internal/controller/discord/main.go | 11 +- .../controller/discord/mocks/PlayerUseCase.go | 106 ++++++++++++++++ internal/controller/discord/player.go | 65 +++++----- internal/controller/discord/player_test.go | 115 ++++++++++++++++++ 5 files changed, 267 insertions(+), 34 deletions(-) create mode 100644 internal/controller/discord/mocks/PlayerUseCase.go create mode 100644 internal/controller/discord/player_test.go diff --git a/internal/controller/discord/absence.go b/internal/controller/discord/absence.go index d51762a9..03541528 100644 --- a/internal/controller/discord/absence.go +++ b/internal/controller/discord/absence.go @@ -62,7 +62,7 @@ func (d Discord) GenerateListAbsenceHandlerMsg(ctx context.Context, date string) select { case <-ctx.Done(): - return "Error because request took too much time to complete", + return ctxError, fmt.Errorf("discord - GenerateListAbsenceHandlerMsg - ctx.Done: %w", ctx.Err()) default: var msg string @@ -114,7 +114,7 @@ func (d Discord) GenerateAbsenceHandlerMsg( } select { case <-ctx.Done(): - return "Error because request took too much time to complete", + return ctxError, fmt.Errorf("discord - GenerateAbsenceHandlerMsg - ctx.Done: %w", ctx.Err()) default: dates, err := parseDate(dates) diff --git a/internal/controller/discord/main.go b/internal/controller/discord/main.go index e96a0581..4b92e0f6 100644 --- a/internal/controller/discord/main.go +++ b/internal/controller/discord/main.go @@ -13,18 +13,27 @@ import ( type Discord struct { AbsenceUseCase - *usecase.PlayerUseCase + PlayerUseCase *usecase.StrikeUseCase *usecase.LootUseCase *usecase.RaidUseCase } +var ctxError = "Error because request took too much time to complete" + type AbsenceUseCase interface { CreateAbsence(ctx context.Context, playerName string, date time.Time) error DeleteAbsence(ctx context.Context, playerName string, date time.Time) error ListAbsence(ctx context.Context, date time.Time) ([]entity.Absence, error) } +type PlayerUseCase interface { + CreatePlayer(ctx context.Context, playerName string) (int, error) + DeletePlayer(ctx context.Context, playerName string) error + ReadPlayer(ctx context.Context, playerName string) (entity.Player, error) + LinkPlayer(ctx context.Context, playerName string, discordID string) error +} + // HumanReadableError returns the error message without the package name. func HumanReadableError(err error) string { str := strings.Split(err.Error(), ": ") diff --git a/internal/controller/discord/mocks/PlayerUseCase.go b/internal/controller/discord/mocks/PlayerUseCase.go new file mode 100644 index 00000000..6321d4c7 --- /dev/null +++ b/internal/controller/discord/mocks/PlayerUseCase.go @@ -0,0 +1,106 @@ +// Code generated by mockery v2.33.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + entity "github.com/antony-ramos/guildops/internal/entity" + + mock "github.com/stretchr/testify/mock" +) + +// PlayerUseCase is an autogenerated mock type for the PlayerUseCase type +type PlayerUseCase struct { + mock.Mock +} + +// CreatePlayer provides a mock function with given fields: ctx, playerName +func (_m *PlayerUseCase) CreatePlayer(ctx context.Context, playerName string) (int, error) { + ret := _m.Called(ctx, playerName) + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (int, error)); ok { + return rf(ctx, playerName) + } + if rf, ok := ret.Get(0).(func(context.Context, string) int); ok { + r0 = rf(ctx, playerName) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, playerName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeletePlayer provides a mock function with given fields: ctx, playerName +func (_m *PlayerUseCase) DeletePlayer(ctx context.Context, playerName string) error { + ret := _m.Called(ctx, playerName) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, playerName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// LinkPlayer provides a mock function with given fields: ctx, playerName, discordID +func (_m *PlayerUseCase) LinkPlayer(ctx context.Context, playerName string, discordID string) error { + ret := _m.Called(ctx, playerName, discordID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, playerName, discordID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ReadPlayer provides a mock function with given fields: ctx, playerName +func (_m *PlayerUseCase) ReadPlayer(ctx context.Context, playerName string) (entity.Player, error) { + ret := _m.Called(ctx, playerName) + + var r0 entity.Player + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (entity.Player, error)); ok { + return rf(ctx, playerName) + } + if rf, ok := ret.Get(0).(func(context.Context, string) entity.Player); ok { + r0 = rf(ctx, playerName) + } else { + r0 = ret.Get(0).(entity.Player) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, playerName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewPlayerUseCase creates a new instance of PlayerUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPlayerUseCase(t interface { + mock.TestingT + Cleanup(func()) +}) *PlayerUseCase { + mock := &PlayerUseCase{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/controller/discord/player.go b/internal/controller/discord/player.go index 00408845..9cd0104c 100644 --- a/internal/controller/discord/player.go +++ b/internal/controller/discord/player.go @@ -192,6 +192,36 @@ func (d Discord) GetPlayerHandler( return nil } +func (d Discord) GenerateLinkPlayerMsg(ctx context.Context, discordName, playerName string) (string, error) { + select { + case <-ctx.Done(): + return ctxError, + fmt.Errorf("discord - GenerateLinkPlayerMsg - ctx.Done: %w", ctx.Err()) + default: + var msg string + + player, err := d.ReadPlayer(ctx, playerName) + if err != nil { + msg = "Error while reading player: " + HumanReadableError(err) + return msg, fmt.Errorf("discord - LinkPlayerHandler - r.ReadPlayer: %w", err) + } + + err = d.LinkPlayer(ctx, player.Name, discordName) + if err != nil { + msg = "Error while linking player: " + HumanReadableError(err) + return msg, fmt.Errorf("discord - LinkPlayerHandler - r.LinkPlayer: %w", err) + } + + msg += "Vous êtes maintenant lié à ce joueur : \n" + msg += "Name : **" + player.Name + "**\n" + msg += "ID : **" + strconv.Itoa(player.ID) + "**\n" + if player.DiscordName != "" { + msg += "Discord ID : **" + discordName + "**\n" + } + return msg, nil + } +} + func (d Discord) LinkPlayerHandler( ctx context.Context, session *discordgo.Session, interaction *discordgo.InteractionCreate, ) error { @@ -206,43 +236,16 @@ func (d Discord) LinkPlayerHandler( } var msg string - name := optionMap["name"].StringValue() - player, err := d.ReadPlayer(ctx, name) - // Show on string all info about player - if err != nil { - msg = "Erreur lors de la récupération du joueur: " + HumanReadableError(err) - _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: msg, - }, - }) - return fmt.Errorf("discord - LinkPlayerHandler - r.ReadPlayer: %w", err) - } + playerName := optionMap["name"].StringValue() + discordName := interaction.Member.User.Username - err = d.LinkPlayer(ctx, player.Name, interaction.Member.User.Username) - if err != nil { - msg = "Erreur lors de la liaison avec le joueur : " + HumanReadableError(err) - _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: msg, - }, - }) - return fmt.Errorf("discord - LinkPlayerHandler - r.LinkPlayer: %w", err) - } + msg, err := d.GenerateLinkPlayerMsg(ctx, discordName, playerName) - msg += "Vous êtes maintenant lié à ce joueur : \n" - msg += "Name : **" + player.Name + "**\n" - msg += "ID : **" + strconv.Itoa(player.ID) + "**\n" - if player.DiscordName != "" { - msg += "Discord ID : **" + interaction.Member.User.ID + "**\n" - } _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: msg, }, }) - return nil + return err } diff --git a/internal/controller/discord/player_test.go b/internal/controller/discord/player_test.go new file mode 100644 index 00000000..5b8b121a --- /dev/null +++ b/internal/controller/discord/player_test.go @@ -0,0 +1,115 @@ +package discordhandler_test + +import ( + "context" + "errors" + "testing" + + discordHandler "github.com/antony-ramos/guildops/internal/controller/discord" + "github.com/antony-ramos/guildops/internal/controller/discord/mocks" + "github.com/antony-ramos/guildops/internal/entity" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestDiscord_GenerateLinkPlayerMsg(t *testing.T) { + t.Parallel() + + t.Run("context is done", func(t *testing.T) { + t.Parallel() + + mockPlayerUseCase := mocks.NewPlayerUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: nil, + PlayerUseCase: mockPlayerUseCase, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + msg, err := discord.GenerateLinkPlayerMsg(ctx, "playerone", "playerone") + + assert.Error(t, err) + assert.Equal(t, "Error because request took too much time to complete", msg) + mockPlayerUseCase.AssertExpectations(t) + }) + + t.Run("Player doesnt exist", func(t *testing.T) { + t.Parallel() + + mockPlayerUseCase := mocks.NewPlayerUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: nil, + PlayerUseCase: mockPlayerUseCase, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + mockPlayerUseCase.On("ReadPlayer", mock.Anything, "playerone"). + Return(entity.Player{}, errors.New("Player doesnt exist")) + + msg, err := discord.GenerateLinkPlayerMsg(context.Background(), "playerone", "playerone") + + assert.Error(t, err) + assert.Equal(t, "Error while reading player: Player doesnt exist", msg) + mockPlayerUseCase.AssertExpectations(t) + }) + + t.Run("Link Successfully", func(t *testing.T) { + t.Parallel() + + mockPlayerUseCase := mocks.NewPlayerUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: nil, + PlayerUseCase: mockPlayerUseCase, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + mockPlayerUseCase.On("ReadPlayer", mock.Anything, "playerone").Return(entity.Player{ + Name: "playerone", + }, nil) + + mockPlayerUseCase.On("LinkPlayer", mock.Anything, "playerone", mock.Anything).Return(nil) + + msg, err := discord.GenerateLinkPlayerMsg(context.Background(), "playerone", "playerone") + + assert.NoError(t, err) + assert.Equal(t, "Vous êtes maintenant lié à ce joueur : \nName : **playerone**\nID : **0**\n", msg) + mockPlayerUseCase.AssertExpectations(t) + }) + + t.Run("Link Failed", func(t *testing.T) { + t.Parallel() + + mockPlayerUseCase := mocks.NewPlayerUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: nil, + PlayerUseCase: mockPlayerUseCase, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + } + + mockPlayerUseCase.On("ReadPlayer", mock.Anything, "playerone").Return(entity.Player{ + Name: "playerone", + }, nil) + + mockPlayerUseCase.On("LinkPlayer", mock.Anything, "playerone", mock.Anything).Return(errors.New("Link Failed")) + + msg, err := discord.GenerateLinkPlayerMsg(context.Background(), "playerone", "playerone") + + assert.Error(t, err) + assert.Equal(t, "Error while linking player: Link Failed", msg) + mockPlayerUseCase.AssertExpectations(t) + }) +} From 35486e72c5c70bda6498680c004aeab80a041af8 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 15:26:40 +0200 Subject: [PATCH 11/22] fix(controller/discord): raid id should be a integer --- internal/controller/discord/raid.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/controller/discord/raid.go b/internal/controller/discord/raid.go index c708f1a9..599d9ceb 100644 --- a/internal/controller/discord/raid.go +++ b/internal/controller/discord/raid.go @@ -65,9 +65,9 @@ var RaidDescriptors = []discordgo.ApplicationCommand{ Description: "Supprimer un raid", Options: []*discordgo.ApplicationCommandOption{ { - Type: discordgo.ApplicationCommandOptionString, + Type: discordgo.ApplicationCommandOptionInteger, Name: "id", - Description: "ex: 4444-4444-4444", + Description: "ex: 4546646", Required: true, }, }, @@ -137,17 +137,14 @@ func (d Discord) DeleteRaidHandler( optionMap[opt.Name] = opt } - raidID, err := strconv.Atoi(optionMap["raidID"].StringValue()) - if err != nil { - return fmt.Errorf("discord - DeleteRaidHandler - strconv.Atoi: %w", err) - } + raidID := optionMap["raidID"].IntValue() - err = d.DeleteRaid(ctx, raidID) + err := d.DeleteRaid(ctx, int(raidID)) if err != nil { msg = "Erreur lors de la suppression du joueur: " + HumanReadableError(err) returnErr = err } else { - msg = "Joueur " + strconv.Itoa(raidID) + " supprimé avec succès" + msg = "Joueur " + strconv.Itoa(int(raidID)) + " supprimé avec succès" } _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ From d76af86c45f1ff81b5c6a3e2739df1b63af31ea7 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 15:30:10 +0200 Subject: [PATCH 12/22] chore: create interfaces for usecases --- internal/controller/discord/main.go | 28 ++++- .../controller/discord/mocks/LootUseCase.go | 108 ++++++++++++++++++ .../controller/discord/mocks/RaidUseCase.go | 70 ++++++++++++ .../controller/discord/mocks/StrikeUseCase.go | 84 ++++++++++++++ 4 files changed, 285 insertions(+), 5 deletions(-) create mode 100644 internal/controller/discord/mocks/LootUseCase.go create mode 100644 internal/controller/discord/mocks/RaidUseCase.go create mode 100644 internal/controller/discord/mocks/StrikeUseCase.go diff --git a/internal/controller/discord/main.go b/internal/controller/discord/main.go index 4b92e0f6..ea573449 100644 --- a/internal/controller/discord/main.go +++ b/internal/controller/discord/main.go @@ -7,16 +7,14 @@ import ( "time" "github.com/antony-ramos/guildops/internal/entity" - - "github.com/antony-ramos/guildops/internal/usecase" ) type Discord struct { AbsenceUseCase PlayerUseCase - *usecase.StrikeUseCase - *usecase.LootUseCase - *usecase.RaidUseCase + StrikeUseCase + LootUseCase + RaidUseCase } var ctxError = "Error because request took too much time to complete" @@ -34,6 +32,26 @@ type PlayerUseCase interface { LinkPlayer(ctx context.Context, playerName string, discordID string) error } +type RaidUseCase interface { + CreateRaid(ctx context.Context, raidName, difficulty string, date time.Time) (entity.Raid, error) + DeleteRaid(ctx context.Context, raidID int) error +} + +type StrikeUseCase interface { + CreateStrike(ctx context.Context, strikeReason, playerName string) error + DeleteStrike(ctx context.Context, id int) error + ReadStrikes(ctx context.Context, playerName string) ([]entity.Strike, error) +} + +type LootUseCase interface { + CreateLoot(ctx context.Context, lootName string, raidID int, playerName string) error + ListLootOnPLayer(ctx context.Context, playerName string) ([]entity.Loot, error) + SelectPlayerToAssign( + ctx context.Context, playerNames []string, difficulty string, + ) (entity.Player, error) + DeleteLoot(ctx context.Context, lootID int) error +} + // HumanReadableError returns the error message without the package name. func HumanReadableError(err error) string { str := strings.Split(err.Error(), ": ") diff --git a/internal/controller/discord/mocks/LootUseCase.go b/internal/controller/discord/mocks/LootUseCase.go new file mode 100644 index 00000000..cfaf9e3d --- /dev/null +++ b/internal/controller/discord/mocks/LootUseCase.go @@ -0,0 +1,108 @@ +// Code generated by mockery v2.33.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + entity "github.com/antony-ramos/guildops/internal/entity" + + mock "github.com/stretchr/testify/mock" +) + +// LootUseCase is an autogenerated mock type for the LootUseCase type +type LootUseCase struct { + mock.Mock +} + +// CreateLoot provides a mock function with given fields: ctx, lootName, raidID, playerName +func (_m *LootUseCase) CreateLoot(ctx context.Context, lootName string, raidID int, playerName string) error { + ret := _m.Called(ctx, lootName, raidID, playerName) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, int, string) error); ok { + r0 = rf(ctx, lootName, raidID, playerName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteLoot provides a mock function with given fields: ctx, lootID +func (_m *LootUseCase) DeleteLoot(ctx context.Context, lootID int) error { + ret := _m.Called(ctx, lootID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { + r0 = rf(ctx, lootID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ListLootOnPLayer provides a mock function with given fields: ctx, playerName +func (_m *LootUseCase) ListLootOnPLayer(ctx context.Context, playerName string) ([]entity.Loot, error) { + ret := _m.Called(ctx, playerName) + + var r0 []entity.Loot + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]entity.Loot, error)); ok { + return rf(ctx, playerName) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []entity.Loot); ok { + r0 = rf(ctx, playerName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]entity.Loot) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, playerName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SelectPlayerToAssign provides a mock function with given fields: ctx, playerNames, difficulty +func (_m *LootUseCase) SelectPlayerToAssign(ctx context.Context, playerNames []string, difficulty string) (entity.Player, error) { + ret := _m.Called(ctx, playerNames, difficulty) + + var r0 entity.Player + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string, string) (entity.Player, error)); ok { + return rf(ctx, playerNames, difficulty) + } + if rf, ok := ret.Get(0).(func(context.Context, []string, string) entity.Player); ok { + r0 = rf(ctx, playerNames, difficulty) + } else { + r0 = ret.Get(0).(entity.Player) + } + + if rf, ok := ret.Get(1).(func(context.Context, []string, string) error); ok { + r1 = rf(ctx, playerNames, difficulty) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewLootUseCase creates a new instance of LootUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewLootUseCase(t interface { + mock.TestingT + Cleanup(func()) +}) *LootUseCase { + mock := &LootUseCase{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/controller/discord/mocks/RaidUseCase.go b/internal/controller/discord/mocks/RaidUseCase.go new file mode 100644 index 00000000..2b0068ed --- /dev/null +++ b/internal/controller/discord/mocks/RaidUseCase.go @@ -0,0 +1,70 @@ +// Code generated by mockery v2.33.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + entity "github.com/antony-ramos/guildops/internal/entity" + + mock "github.com/stretchr/testify/mock" + + time "time" +) + +// RaidUseCase is an autogenerated mock type for the RaidUseCase type +type RaidUseCase struct { + mock.Mock +} + +// CreateRaid provides a mock function with given fields: ctx, raidName, raidDate +func (_m *RaidUseCase) CreateRaid(ctx context.Context, raidName string, raidDate time.Time) (entity.Raid, error) { + ret := _m.Called(ctx, raidName, raidDate) + + var r0 entity.Raid + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, time.Time) (entity.Raid, error)); ok { + return rf(ctx, raidName, raidDate) + } + if rf, ok := ret.Get(0).(func(context.Context, string, time.Time) entity.Raid); ok { + r0 = rf(ctx, raidName, raidDate) + } else { + r0 = ret.Get(0).(entity.Raid) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, time.Time) error); ok { + r1 = rf(ctx, raidName, raidDate) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteRaid provides a mock function with given fields: ctx, raidID +func (_m *RaidUseCase) DeleteRaid(ctx context.Context, raidID int) error { + ret := _m.Called(ctx, raidID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { + r0 = rf(ctx, raidID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewRaidUseCase creates a new instance of RaidUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRaidUseCase(t interface { + mock.TestingT + Cleanup(func()) +}) *RaidUseCase { + mock := &RaidUseCase{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/controller/discord/mocks/StrikeUseCase.go b/internal/controller/discord/mocks/StrikeUseCase.go new file mode 100644 index 00000000..34df2a98 --- /dev/null +++ b/internal/controller/discord/mocks/StrikeUseCase.go @@ -0,0 +1,84 @@ +// Code generated by mockery v2.33.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + entity "github.com/antony-ramos/guildops/internal/entity" + + mock "github.com/stretchr/testify/mock" +) + +// StrikeUseCase is an autogenerated mock type for the StrikeUseCase type +type StrikeUseCase struct { + mock.Mock +} + +// CreateStrike provides a mock function with given fields: ctx, strikeReason, playerName +func (_m *StrikeUseCase) CreateStrike(ctx context.Context, strikeReason string, playerName string) error { + ret := _m.Called(ctx, strikeReason, playerName) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, strikeReason, playerName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteStrike provides a mock function with given fields: ctx, id +func (_m *StrikeUseCase) DeleteStrike(ctx context.Context, id int) error { + ret := _m.Called(ctx, id) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ReadStrikes provides a mock function with given fields: ctx, playerName +func (_m *StrikeUseCase) ReadStrikes(ctx context.Context, playerName string) ([]entity.Strike, error) { + ret := _m.Called(ctx, playerName) + + var r0 []entity.Strike + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]entity.Strike, error)); ok { + return rf(ctx, playerName) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []entity.Strike); ok { + r0 = rf(ctx, playerName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]entity.Strike) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, playerName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewStrikeUseCase creates a new instance of StrikeUseCase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewStrikeUseCase(t interface { + mock.TestingT + Cleanup(func()) +}) *StrikeUseCase { + mock := &StrikeUseCase{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} From 1c45a694155b494c4107fd8efa36414766e11e22 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 17:14:14 +0200 Subject: [PATCH 13/22] fix: coven-absence-list was always empty --- internal/usecase/postgresbackend/absence.go | 2 +- notes | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 notes diff --git a/internal/usecase/postgresbackend/absence.go b/internal/usecase/postgresbackend/absence.go index 4a24ab74..4f7b633d 100644 --- a/internal/usecase/postgresbackend/absence.go +++ b/internal/usecase/postgresbackend/absence.go @@ -66,7 +66,7 @@ func (pg *PG) SearchAbsence( return nil, err } absences = append(absences, a...) - case playerID != -1 && playerName != "" && !date.IsZero(): + case playerID == -1 && playerName == "" && !date.IsZero(): a, err := pg.searchAbsenceOnParam(ctx, "date", date) if err != nil { return nil, err diff --git a/notes b/notes new file mode 100644 index 00000000..e69de29b From b37142b5fbd3dad401f5618e84c9588ef964149c Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 17:15:55 +0200 Subject: [PATCH 14/22] fix: coven-raid-list is not implemented --- internal/controller/discord/raid.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/internal/controller/discord/raid.go b/internal/controller/discord/raid.go index 599d9ceb..5da33aa2 100644 --- a/internal/controller/discord/raid.go +++ b/internal/controller/discord/raid.go @@ -42,24 +42,6 @@ var RaidDescriptors = []discordgo.ApplicationCommand{ }, }, }, - { - Name: "coven-raid-list", - Description: "Lister les raids", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "name", - Description: "ex: Milowenn", - Required: false, - }, - { - Type: discordgo.ApplicationCommandOptionString, - Name: "date", - Description: "ex: Milowenn", - Required: false, - }, - }, - }, { Name: "coven-raid-del", Description: "Supprimer un raid", From 6fccecfd79b4454e9f2d7d08aa14f959da4dc8eb Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 17:28:23 +0200 Subject: [PATCH 15/22] chore: change output format for strikes --- internal/controller/discord/main.go | 2 +- internal/controller/discord/player.go | 2 +- internal/controller/discord/strike.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/controller/discord/main.go b/internal/controller/discord/main.go index ea573449..8d5dbbae 100644 --- a/internal/controller/discord/main.go +++ b/internal/controller/discord/main.go @@ -72,7 +72,7 @@ func parseDate(dateStr string) ([]time.Time, error) { for _, datePart := range dateParts { date, err := time.Parse("02/01/06", datePart) if err != nil { - return nil, fmt.Errorf("discord - parseDate - time.Parse: %w", err) + return nil, fmt.Errorf("date should be in format dd/mm/yy or \"dd/mm/yy au dd/mm/yy\"") } dates = append(dates, date) } diff --git a/internal/controller/discord/player.go b/internal/controller/discord/player.go index 9cd0104c..1ebdb854 100644 --- a/internal/controller/discord/player.go +++ b/internal/controller/discord/player.go @@ -162,7 +162,7 @@ func (d Discord) GetPlayerHandler( msg += "**Strikes (" + strconv.Itoa(len(player.Strikes)) + ") :** \n" for _, strike := range player.Strikes { msg += " " + strike.Reason + - " | " + strike.Date.Format("02/01/06") + " | " + strike.Season + " | " + strconv.Itoa(strike.ID) + "\n" + " | " + strike.Date.Format("02/01/2006") + " | " + strike.Season + " | " + strconv.Itoa(strike.ID) + "\n" } } if len(player.MissedRaids) > 0 { diff --git a/internal/controller/discord/strike.go b/internal/controller/discord/strike.go index 05fffae5..cdf48a81 100644 --- a/internal/controller/discord/strike.go +++ b/internal/controller/discord/strike.go @@ -125,9 +125,9 @@ func (d Discord) ListStrikesOnPlayerHandler( return fmt.Errorf("database - ListStrikesOnPlayerHandler - r.ReadStrikes: %w", err) } - msg = "Strikes de " + playerName + ":\n" + msg = "Strikes de " + playerName + " (" + strconv.Itoa(len(strikes)) + ") :\n" for _, strike := range strikes { - msg += strike.Date.String() + " | " + strike.Reason + "\n" + msg += "* " + strike.Date.Format("02/01/2006") + " | " + strike.Reason + "\n" } _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ From 6adc7c20a09fb0b845d147e6fae4aa5eb1ab3431 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 17:29:15 +0200 Subject: [PATCH 16/22] chore: change output format for raid already exists --- internal/usecase/postgresbackend/raid.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/usecase/postgresbackend/raid.go b/internal/usecase/postgresbackend/raid.go index 7526b136..403eed87 100644 --- a/internal/usecase/postgresbackend/raid.go +++ b/internal/usecase/postgresbackend/raid.go @@ -97,7 +97,7 @@ func (pg *PG) CreateRaid(ctx context.Context, raid entity.Raid) (entity.Raid, er } defer rows.Close() if rows.Next() { - return entity.Raid{}, fmt.Errorf("database - CreateRaid - raid already exists") + return entity.Raid{}, fmt.Errorf("raid already exists") } sql, _, errInsert := pg.Builder. Insert("raids"). From 0eafcdc78a833029f917f9a635e1c713e5738a1c Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 17:35:35 +0200 Subject: [PATCH 17/22] chore: add logs to track discord command registration and deletion --- pkg/discord/discord.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/discord/discord.go b/pkg/discord/discord.go index 43e68244..8c6d5577 100644 --- a/pkg/discord/discord.go +++ b/pkg/discord/discord.go @@ -64,12 +64,14 @@ func (d *Discord) Run(ctx context.Context) error { case <-stopCh: return // S'arrête immédiatement si un autre goroutine a signalé une erreur default: + zap.L().Info("Registering command " + command.Name) cmd, err := d.s.ApplicationCommandCreate(d.s.State.User.ID, strconv.Itoa(d.guildID), command) if err != nil { errCh <- err close(stopCh) // Ferme le canal pour signaler aux autres goroutines de s'arrêter } registeredCommands[commandName] = cmd + zap.L().Info("Command " + command.Name + " registered") } }() } @@ -85,11 +87,13 @@ func (d *Discord) Run(ctx context.Context) error { <-ctx.Done() if d.DeleteCommands { - for _, v := range registeredCommands { - err := d.s.ApplicationCommandDelete(d.s.State.User.ID, strconv.Itoa(d.guildID), v.ID) + for _, value := range registeredCommands { + zap.L().Info("Deleting command " + value.Name) + err := d.s.ApplicationCommandDelete(d.s.State.User.ID, strconv.Itoa(d.guildID), value.ID) if err != nil { return fmt.Errorf("discord - Run - d.s.ApplicationCommandDelete: %w", err) } + zap.L().Info("Command " + value.Name + " deleted") } } return nil From 0f09ec2076b273554c23ca95824454165420408b Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 18:15:07 +0200 Subject: [PATCH 18/22] feat: add tracing --- TODO.md | 6 ++--- cmd/guildops/main.go | 7 +++-- internal/usecase/player.go | 11 ++++++++ internal/usecase/postgresbackend/player.go | 31 ++++++++++++++++++++++ pkg/discord/discord.go | 12 ++++++--- 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/TODO.md b/TODO.md index 7c3c84c6..b0e7714c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,5 @@ ### Todo -- [ ] Better error management, errors send to Discord are too descriptive -- [ ] Add more logs for each level -- [ ] Add tracing for each request - [ ] Add API as controller - [ ] Add notes to players (for example: player cannot play on wednesday) @@ -10,6 +7,8 @@ - [ ] Add comments to every functions - [ ] Add tests to every functions +- [ ] Add tracing for each request +- [ ] Add more logs for each level ### Done ✓ - [X] Create CI linter @@ -23,3 +22,4 @@ - [X] Add Usecase: player name is not discord name. Must implement a way to link them - [X] Fix in SearchPlayer: player strikes must be import there not in usecase - [X] Fix In UpdatePlayer: placeholder $1 already has type int, cannot assign varchar +- [X] Better error management, errors send to Discord are too descriptive diff --git a/cmd/guildops/main.go b/cmd/guildops/main.go index c9a53a90..11115448 100644 --- a/cmd/guildops/main.go +++ b/cmd/guildops/main.go @@ -13,7 +13,6 @@ import ( "github.com/antony-ramos/guildops/pkg/tracing" "github.com/prometheus/client_golang/prometheus/promhttp" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -82,7 +81,7 @@ func main() { log.Fatal(err) } }() - ctx, span := otel.Tracer(spanName).Start(ctx, "main", trace.WithTimestamp(time.Now())) + ctx, span := otel.Tracer(spanName).Start(ctx, "main") // Metrics zap.L().Info(fmt.Sprintf("Starting metrics server on port %s", cfg.Metrics.Port)) @@ -96,12 +95,12 @@ func main() { if err != nil { span.RecordError(err) zap.L().Fatal(err.Error()) - span.End(trace.WithTimestamp(time.Now())) + span.End() } }() // Run zap.L().Info("Starting app") app.Run(ctx, cfg) - span.End(trace.WithTimestamp(time.Now())) + span.End() } diff --git a/internal/usecase/player.go b/internal/usecase/player.go index 234d929f..9ab27b35 100644 --- a/internal/usecase/player.go +++ b/internal/usecase/player.go @@ -5,6 +5,9 @@ import ( "fmt" "time" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "github.com/antony-ramos/guildops/internal/entity" ) @@ -17,6 +20,9 @@ func NewPlayerUseCase(bk Backend) *PlayerUseCase { } func (puc PlayerUseCase) CreatePlayer(ctx context.Context, playerName string) (int, error) { + ctx, span := otel.Tracer("UseCase").Start(ctx, "PlayerUseCase/CreatePlayer") + span.SetAttributes(attribute.String("playerName", playerName)) + defer span.End() select { case <-ctx.Done(): return -1, fmt.Errorf("PlayerUseCase - CreatePlayer - ctx.Done: %w", ctx.Err()) @@ -24,7 +30,12 @@ func (puc PlayerUseCase) CreatePlayer(ctx context.Context, playerName string) (i player := entity.Player{ Name: playerName, } + + _, spanValidate := otel.Tracer("Entity").Start(ctx, "Player/Validate") + span.SetAttributes(attribute.String("playerName", playerName)) err := player.Validate() + spanValidate.End() + if err != nil { return -1, fmt.Errorf("database - CreatePlayer - r.Validate: %w", err) } diff --git a/internal/usecase/postgresbackend/player.go b/internal/usecase/postgresbackend/player.go index f29ee421..8cb6d6f4 100644 --- a/internal/usecase/postgresbackend/player.go +++ b/internal/usecase/postgresbackend/player.go @@ -6,6 +6,10 @@ import ( "strconv" "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "github.com/jackc/pgx/v4" "github.com/antony-ramos/guildops/internal/entity" @@ -13,6 +17,13 @@ import ( // SearchPlayer is a function which call backend to Search a Player Object. func (pg *PG) SearchPlayer(ctx context.Context, playerID int, name, discordName string) ([]entity.Player, error) { + ctx, span := otel.Tracer("postgresbackend").Start(ctx, "SearchPlayer") + span.SetAttributes( + attribute.String("playerName", name), + attribute.String("discordName", discordName), + attribute.Int("playerID", playerID)) + defer span.End() + select { case <-ctx.Done(): return nil, fmt.Errorf("database - SearchPlayer - ctx.Done: %w", ctx.Err()) @@ -149,6 +160,9 @@ func (pg *PG) SearchPlayer(ctx context.Context, playerID int, name, discordName // CreatePlayer is a function which call backend to Create a Player Object. func (pg *PG) CreatePlayer(ctx context.Context, player entity.Player) (entity.Player, error) { + ctx, span := otel.Tracer("postgresbackend").Start(ctx, "CreatePlayer") + span.SetAttributes(attribute.String("playerName", player.Name)) + defer span.End() select { case <-ctx.Done(): return entity.Player{}, fmt.Errorf("database - CreatePlayer - ctx.Done: %w", ctx.Err()) @@ -191,6 +205,11 @@ func (pg *PG) CreatePlayer(ctx context.Context, player entity.Player) (entity.Pl // ReadPlayer is a function which call backend to Read a Player Object. func (pg *PG) ReadPlayer(ctx context.Context, playerID int) (entity.Player, error) { + ctx, span := otel.Tracer("postgresbackend").Start(ctx, "ReadPlayer") + span.SetAttributes( + attribute.Int("playerID", playerID)) + defer span.End() + select { case <-ctx.Done(): return entity.Player{}, fmt.Errorf("database - ReadPlayer - ctx.Done: %w", ctx.Err()) @@ -218,6 +237,13 @@ func (pg *PG) ReadPlayer(ctx context.Context, playerID int) (entity.Player, erro // UpdatePlayer is a function which call backend to Update a Player Object. func (pg *PG) UpdatePlayer(ctx context.Context, player entity.Player) error { + ctx, span := otel.Tracer("postgresbackend").Start(ctx, "UpdatePlayer") + span.SetAttributes( + attribute.String("playerName", player.Name), + attribute.String("discordName", player.DiscordName), + attribute.Int("playerID", player.ID)) + defer span.End() + select { case <-ctx.Done(): return fmt.Errorf("database - UpdatePlayer - ctx.Done: %w", ctx.Err()) @@ -242,6 +268,11 @@ func (pg *PG) UpdatePlayer(ctx context.Context, player entity.Player) error { // DeletePlayer is a function which call backend to Delete a Player Object. func (pg *PG) DeletePlayer(ctx context.Context, playerID int) error { + ctx, span := otel.Tracer("postgresbackend").Start(ctx, "DeletePlayer") + span.SetAttributes( + attribute.Int("playerID", playerID)) + defer span.End() + select { case <-ctx.Done(): return fmt.Errorf("database - DeletePlayer - ctx.Done: %w", ctx.Err()) diff --git a/pkg/discord/discord.go b/pkg/discord/discord.go index 8c6d5577..4f96bc2a 100644 --- a/pkg/discord/discord.go +++ b/pkg/discord/discord.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/bwmarrin/discordgo" + "go.opentelemetry.io/otel" "go.uber.org/zap" ) @@ -41,11 +42,14 @@ func (d *Discord) Run(ctx context.Context) error { return fmt.Errorf("discord - Run - d.s.Open: %w", err) } - d.s.AddHandler(func(session *discordgo.Session, i *discordgo.InteractionCreate) { - if h, ok := d.commandHandlers[i.ApplicationCommandData().Name]; ok { - err := h(ctx, session, i) + d.s.AddHandler(func(session *discordgo.Session, interaction *discordgo.InteractionCreate) { + if h, ok := d.commandHandlers[interaction.ApplicationCommandData().Name]; ok { + ctx := context.Background() + ctx, span := otel.Tracer("discordHandler").Start(ctx, interaction.ApplicationCommandData().Name) + defer span.End() + err := h(ctx, session, interaction) if err != nil { - zap.L().Error(fmt.Sprintf("Error while handling command %s : %s", i.ApplicationCommandData().Name, err.Error())) + zap.L().Error(fmt.Sprintf("Error while handling command %s : %s", interaction.ApplicationCommandData().Name, err.Error())) } } }) From e5f0373e15312a26912d55cc0e854ea797aa5dfc Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 18:15:58 +0200 Subject: [PATCH 19/22] chore: add more outputs on makefile --- Makefile | 4 ++++ internal/usecase/postgresbackend/player.go | 4 +--- pkg/discord/discord.go | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index f3309928..9293ba4e 100644 --- a/Makefile +++ b/Makefile @@ -18,10 +18,14 @@ help: ## Display this message ## artifact: ## Compile app from sources (linux) + @echo "### Building artifact ..." @CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o ${BINARY_NAME} ./cmd/${BINARY_NAME} + @echo "### Artifact built successfully" artifact.osx: ## Compile app from sources (osx) + @echo "### Building artifact ..." @CGO_ENABLED=0 GOARCH=amd64 GOOS=darwin go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o ${BINARY_NAME} ./cmd/${BINARY_NAME} + @echo "### Artifact built successfully" image-ci: ## Build an image for CI Test Helm docker build . --tag "ghcr.io/antony-ramos/${BINARY_NAME}:ci" diff --git a/internal/usecase/postgresbackend/player.go b/internal/usecase/postgresbackend/player.go index 8cb6d6f4..b33613df 100644 --- a/internal/usecase/postgresbackend/player.go +++ b/internal/usecase/postgresbackend/player.go @@ -6,12 +6,10 @@ import ( "strconv" "time" - + "github.com/jackc/pgx/v4" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "github.com/jackc/pgx/v4" - "github.com/antony-ramos/guildops/internal/entity" ) diff --git a/pkg/discord/discord.go b/pkg/discord/discord.go index 4f96bc2a..38a08734 100644 --- a/pkg/discord/discord.go +++ b/pkg/discord/discord.go @@ -49,7 +49,8 @@ func (d *Discord) Run(ctx context.Context) error { defer span.End() err := h(ctx, session, interaction) if err != nil { - zap.L().Error(fmt.Sprintf("Error while handling command %s : %s", interaction.ApplicationCommandData().Name, err.Error())) + zap.L().Error( + fmt.Sprintf("Error while handling command %s : %s", interaction.ApplicationCommandData().Name, err.Error())) } } }) From 1aa09985bd0bca3f2c1f76412989dfad6945f3f6 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 18:47:56 +0200 Subject: [PATCH 20/22] chore: add tests to discord/lootHandler --- internal/app/app.go | 1 + internal/controller/discord/loot.go | 20 ++++--- internal/controller/discord/loot_test.go | 68 ++++++++++++++++++++++++ internal/controller/discord/main.go | 2 + 4 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 internal/controller/discord/loot_test.go diff --git a/internal/app/app.go b/internal/app/app.go index e4f43cf8..6a2d8e5f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -47,6 +47,7 @@ func Run(ctx context.Context, cfg *config.Config) { LootUseCase: luc, RaidUseCase: ruc, StrikeUseCase: suc, + Fake: false, } var inits []func() map[string]func( diff --git a/internal/controller/discord/loot.go b/internal/controller/discord/loot.go index 7e3fdc45..e943caf8 100644 --- a/internal/controller/discord/loot.go +++ b/internal/controller/discord/loot.go @@ -106,21 +106,25 @@ func (d Discord) AttributeLootHandler( err := d.LootUseCase.CreateLoot(ctx, lootName, int(raidID), playerName) if err != nil { msg := "Erreur lors de l'attribution du loot: " + HumanReadableError(err) + if !d.Fake { + _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: msg, + }, + }) + } + return fmt.Errorf("discord - AttributeLootHandler - d.LootUseCase.CreateLoot: %w", err) + } + msg := "Loot attribué avec succès" + if !d.Fake { _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: msg, }, }) - return fmt.Errorf("discord - AttributeLootHandler - d.LootUseCase.CreateLoot: %w", err) } - msg := "Loot attribué avec succès" - _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: msg, - }, - }) return nil } diff --git a/internal/controller/discord/loot_test.go b/internal/controller/discord/loot_test.go new file mode 100644 index 00000000..dec6a7ef --- /dev/null +++ b/internal/controller/discord/loot_test.go @@ -0,0 +1,68 @@ +package discordhandler_test + +import ( + "context" + "testing" + + discordHandler "github.com/antony-ramos/guildops/internal/controller/discord" + "github.com/antony-ramos/guildops/internal/controller/discord/mocks" + "github.com/bwmarrin/discordgo" + "github.com/stretchr/testify/mock" +) + +func TestDiscord_AttributeLootHandler(t *testing.T) { + t.Parallel() + + t.Run("Success", func(t *testing.T) { + t.Parallel() + mockLootUseCase := mocks.NewLootUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: nil, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: mockLootUseCase, + RaidUseCase: nil, + Fake: true, + } + + mockLootUseCase.On("CreateLoot", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + + session := &discordgo.Session{StateEnabled: true, State: discordgo.NewState()} + interaction := &discordgo.InteractionCreate{ + Interaction: &discordgo.Interaction{ + Type: discordgo.InteractionApplicationCommand, + Data: discordgo.ApplicationCommandInteractionData{ + ID: "mock", + Name: "mock", + TargetID: "mock", + Resolved: &discordgo.ApplicationCommandInteractionDataResolved{}, + Options: []*discordgo.ApplicationCommandInteractionDataOption{ + { + Name: "loot-name", + Type: discordgo.ApplicationCommandOptionString, + Value: "TestLoot", + }, + { + Name: "raid-id", + Type: discordgo.ApplicationCommandOptionInteger, + Value: float64(123), + }, + { + Name: "player-name", + Type: discordgo.ApplicationCommandOptionString, + Value: "TestPlayer", + }, + }, + }, + }, + } + + err := discord.AttributeLootHandler(context.Background(), session, interaction) + if err != nil { + return + } + mockLootUseCase.AssertExpectations(t) + }) +} diff --git a/internal/controller/discord/main.go b/internal/controller/discord/main.go index 8d5dbbae..177655b0 100644 --- a/internal/controller/discord/main.go +++ b/internal/controller/discord/main.go @@ -15,6 +15,8 @@ type Discord struct { StrikeUseCase LootUseCase RaidUseCase + + Fake bool // Used for testing } var ctxError = "Error because request took too much time to complete" From 8e1327fb20dc41e400ddcc5124329bf38217794e Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 20:06:26 +0200 Subject: [PATCH 21/22] chore: add tests to discord/player --- install/guildops/DEBIAN/postinst | 27 --- internal/controller/discord/player.go | 43 ++--- internal/controller/discord/player_test.go | 190 +++++++++++++++++++++ 3 files changed, 214 insertions(+), 46 deletions(-) delete mode 100755 install/guildops/DEBIAN/postinst diff --git a/install/guildops/DEBIAN/postinst b/install/guildops/DEBIAN/postinst deleted file mode 100755 index e7e0d78f..00000000 --- a/install/guildops/DEBIAN/postinst +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -sudo groupadd guildops -sudo useradd -g guildops guildops -sudo chown guildops /usr/bin/guildops -sudo chown guildops /etc/guildops - -cat <<EOL > /etc/systemd/system/guildops.service -[Unit] -Description=GuildOps - -[Service] -ExecStart=/usr/bin/guildops -Restart=always -User=guildops -Group=guildops -WorkingDirectory=/etc/guildops - -[Install] -WantedBy=multi-user.target -EOL - -systemctl daemon-reload - -systemctl enable guildops.service - -systemctl start guildops.service diff --git a/internal/controller/discord/player.go b/internal/controller/discord/player.go index 1ebdb854..35bb9ae5 100644 --- a/internal/controller/discord/player.go +++ b/internal/controller/discord/player.go @@ -104,12 +104,14 @@ func (d Discord) PlayerHandler( } } - _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: msg, - }, - }) + if !d.Fake { + _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: msg, + }, + }) + } return returnErr } @@ -132,12 +134,14 @@ func (d Discord) GetPlayerHandler( // Show on string all info about player if err != nil { msg = "Erreur lors de la récupération du joueur: " + HumanReadableError(err) - _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: msg, - }, - }) + if !d.Fake { + _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: msg, + }, + }) + } return fmt.Errorf("database - GetPlayerHandler - r.ReadPlayer: %w", err) } msg += "Name : **" + player.Name + "**\n" @@ -182,13 +186,14 @@ func (d Discord) GetPlayerHandler( " | " + loot.Name + "\n" } } - - _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: msg, - }, - }) + if !d.Fake { + _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: msg, + }, + }) + } return nil } diff --git a/internal/controller/discord/player_test.go b/internal/controller/discord/player_test.go index 5b8b121a..06a4f738 100644 --- a/internal/controller/discord/player_test.go +++ b/internal/controller/discord/player_test.go @@ -4,6 +4,9 @@ import ( "context" "errors" "testing" + "time" + + "github.com/bwmarrin/discordgo" discordHandler "github.com/antony-ramos/guildops/internal/controller/discord" "github.com/antony-ramos/guildops/internal/controller/discord/mocks" @@ -113,3 +116,190 @@ func TestDiscord_GenerateLinkPlayerMsg(t *testing.T) { mockPlayerUseCase.AssertExpectations(t) }) } + +func TestDiscord_PlayerHandler(t *testing.T) { + t.Parallel() + + t.Run("Success Create Player", func(t *testing.T) { + t.Parallel() + mockPlayerUseCase := mocks.NewPlayerUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: nil, + PlayerUseCase: mockPlayerUseCase, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + Fake: true, + } + + mockPlayerUseCase.On("CreatePlayer", mock.Anything, mock.Anything). + Return(1, nil) + + session := &discordgo.Session{StateEnabled: true, State: discordgo.NewState()} + interaction := &discordgo.InteractionCreate{ + Interaction: &discordgo.Interaction{ + Type: discordgo.InteractionApplicationCommand, + Data: discordgo.ApplicationCommandInteractionData{ + ID: "mock", + Name: "coven-player-create", + TargetID: "mock", + Resolved: &discordgo.ApplicationCommandInteractionDataResolved{}, + Options: []*discordgo.ApplicationCommandInteractionDataOption{ + { + Name: "name", + Type: discordgo.ApplicationCommandOptionString, + Value: "TestPlayer", + }, + }, + }, + }, + } + + err := discord.PlayerHandler(context.Background(), session, interaction) + if err != nil { + return + } + mockPlayerUseCase.AssertExpectations(t) + assert.NoError(t, err) + }) + + t.Run("Success Delete Player", func(t *testing.T) { + t.Parallel() + mockPlayerUseCase := mocks.NewPlayerUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: nil, + PlayerUseCase: mockPlayerUseCase, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + Fake: true, + } + + mockPlayerUseCase.On("DeletePlayer", mock.Anything, mock.Anything). + Return(nil) + + session := &discordgo.Session{StateEnabled: true, State: discordgo.NewState()} + interaction := &discordgo.InteractionCreate{ + Interaction: &discordgo.Interaction{ + Type: discordgo.InteractionApplicationCommand, + Data: discordgo.ApplicationCommandInteractionData{ + ID: "mock", + Name: "coven-player-delete", + TargetID: "mock", + Resolved: &discordgo.ApplicationCommandInteractionDataResolved{}, + Options: []*discordgo.ApplicationCommandInteractionDataOption{ + { + Name: "name", + Type: discordgo.ApplicationCommandOptionString, + Value: "TestPlayer", + }, + }, + }, + }, + } + + err := discord.PlayerHandler(context.Background(), session, interaction) + if err != nil { + return + } + mockPlayerUseCase.AssertExpectations(t) + assert.NoError(t, err) + }) +} + +func TestDiscord_GetPlayerHandler(t *testing.T) { + t.Parallel() + + t.Run("Success Get Player", func(t *testing.T) { + t.Parallel() + mockPlayerUseCase := mocks.NewPlayerUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: nil, + PlayerUseCase: mockPlayerUseCase, + StrikeUseCase: nil, + LootUseCase: nil, + RaidUseCase: nil, + Fake: true, + } + + player := entity.Player{ + Name: "TestPlayer", + ID: 1, + } + + strikes := []entity.Strike{ + { + ID: 1, + Reason: "TestReason", + Season: "DF/S2", + Date: time.Now(), + }, + { + ID: 1, + Reason: "TestReason", + Season: "DF/S2", + Date: time.Now(), + }, + } + player.Strikes = strikes + + loots := []entity.Loot{ + { + ID: 1, + Name: "TestLoot", + Raid: &entity.Raid{ + ID: 1, + Name: "TestRaid", + Difficulty: "TestDifficulty", + Date: time.Now(), + }, + }, + } + + player.Loots = loots + + missedRaids := []entity.Raid{ + { + ID: 1, + Name: "TestRaid", + Difficulty: "TestDifficulty", + Date: time.Now(), + }, + } + + player.MissedRaids = missedRaids + + mockPlayerUseCase.On("ReadPlayer", mock.Anything, mock.Anything). + Return(player, nil) + + session := &discordgo.Session{StateEnabled: true, State: discordgo.NewState()} + interaction := &discordgo.InteractionCreate{ + Interaction: &discordgo.Interaction{ + Type: discordgo.InteractionApplicationCommand, + Data: discordgo.ApplicationCommandInteractionData{ + ID: "mock", + Name: "coven-player-create", + TargetID: "mock", + Resolved: &discordgo.ApplicationCommandInteractionDataResolved{}, + Options: []*discordgo.ApplicationCommandInteractionDataOption{ + { + Name: "name", + Type: discordgo.ApplicationCommandOptionString, + Value: "TestPlayer", + }, + }, + }, + }, + } + + err := discord.GetPlayerHandler(context.Background(), session, interaction) + if err != nil { + return + } + mockPlayerUseCase.AssertExpectations(t) + assert.NoError(t, err) + }) +} From 68f6589ca24dd47942d459c9d1ac7e2043acf148 Mon Sep 17 00:00:00 2001 From: Antony Ramos <antony.ramos@radiofrance.com> Date: Sun, 24 Sep 2023 20:19:17 +0200 Subject: [PATCH 22/22] chore: add tests to discord/loot --- internal/controller/discord/loot.go | 46 +++++++----- internal/controller/discord/loot_test.go | 94 ++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 19 deletions(-) diff --git a/internal/controller/discord/loot.go b/internal/controller/discord/loot.go index e943caf8..3660fc3b 100644 --- a/internal/controller/discord/loot.go +++ b/internal/controller/discord/loot.go @@ -143,24 +143,28 @@ func (d Discord) ListLootsOnPlayerHandler( lootList, err := d.LootUseCase.ListLootOnPLayer(ctx, playerName) if err != nil { msg := "Erreur lors de la récupération des loots: " + HumanReadableError(err) + if !d.Fake { + _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: msg, + }, + }) + } + return fmt.Errorf("discord - ListLootsOnPlayerHandler - d.LootUseCase.ListLootOnPLayer: %w", err) + } + msg := "Tous les loots de " + playerName + ":\n" + for _, loot := range lootList { + msg += loot.Name + " " + loot.Raid.Date.String() + " " + loot.Raid.Difficulty + "\n" + } + if !d.Fake { _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: msg, }, }) - return fmt.Errorf("discord - ListLootsOnPlayerHandler - d.LootUseCase.ListLootOnPLayer: %w", err) } - msg := "Tous les loots de " + playerName + ":\n" - for _, loot := range lootList { - msg += loot.Name + " " + loot.Raid.Date.String() + " " + loot.Raid.Difficulty + "\n" - } - _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: msg, - }, - }) return nil } @@ -185,21 +189,25 @@ func (d Discord) DeleteLootHandler( err = d.LootUseCase.DeleteLoot(ctx, id) if err != nil { msg := "Erreur lors de la suppression du loot: " + HumanReadableError(err) + if !d.Fake { + _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: msg, + }, + }) + } + return fmt.Errorf("discord - DeleteLootHandler - d.LootUseCase.DeleteLoot: %w", err) + } + msg := "Loot supprimé avec succès" + if !d.Fake { _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: msg, }, }) - return fmt.Errorf("discord - DeleteLootHandler - d.LootUseCase.DeleteLoot: %w", err) } - msg := "Loot supprimé avec succès" - _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: msg, - }, - }) return nil } diff --git a/internal/controller/discord/loot_test.go b/internal/controller/discord/loot_test.go index dec6a7ef..e1419498 100644 --- a/internal/controller/discord/loot_test.go +++ b/internal/controller/discord/loot_test.go @@ -7,6 +7,7 @@ import ( discordHandler "github.com/antony-ramos/guildops/internal/controller/discord" "github.com/antony-ramos/guildops/internal/controller/discord/mocks" "github.com/bwmarrin/discordgo" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -66,3 +67,96 @@ func TestDiscord_AttributeLootHandler(t *testing.T) { mockLootUseCase.AssertExpectations(t) }) } + +func TestDiscord_ListLootsOnPlayerHandler(t *testing.T) { + t.Parallel() + + t.Run("Success", func(t *testing.T) { + t.Parallel() + mockLootUseCase := mocks.NewLootUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: nil, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: mockLootUseCase, + RaidUseCase: nil, + Fake: true, + } + + mockLootUseCase.On("ListLootOnPLayer", mock.Anything, mock.Anything). + Return(nil, nil) + + session := &discordgo.Session{StateEnabled: true, State: discordgo.NewState()} + interaction := &discordgo.InteractionCreate{ + Interaction: &discordgo.Interaction{ + Type: discordgo.InteractionApplicationCommand, + Data: discordgo.ApplicationCommandInteractionData{ + ID: "mock", + Name: "mock", + TargetID: "mock", + Resolved: &discordgo.ApplicationCommandInteractionDataResolved{}, + Options: []*discordgo.ApplicationCommandInteractionDataOption{ + { + Name: "player-name", + Type: discordgo.ApplicationCommandOptionString, + Value: "TestPlayer", + }, + }, + }, + }, + } + + err := discord.ListLootsOnPlayerHandler(context.Background(), session, interaction) + if err != nil { + return + } + mockLootUseCase.AssertExpectations(t) + }) +} + +// DeleteLootHandler. +func TestDiscord_DeleteLootHandler(t *testing.T) { + t.Parallel() + + t.Run("Success", func(t *testing.T) { + t.Parallel() + mockLootUseCase := mocks.NewLootUseCase(t) + + discord := discordHandler.Discord{ + AbsenceUseCase: nil, + PlayerUseCase: nil, + StrikeUseCase: nil, + LootUseCase: mockLootUseCase, + RaidUseCase: nil, + Fake: true, + } + + mockLootUseCase.On("DeleteLoot", mock.Anything, mock.Anything). + Return(nil) + + session := &discordgo.Session{StateEnabled: true, State: discordgo.NewState()} + interaction := &discordgo.InteractionCreate{ + Interaction: &discordgo.Interaction{ + Type: discordgo.InteractionApplicationCommand, + Data: discordgo.ApplicationCommandInteractionData{ + ID: "mock", + Name: "mock", + TargetID: "mock", + Resolved: &discordgo.ApplicationCommandInteractionDataResolved{}, + Options: []*discordgo.ApplicationCommandInteractionDataOption{ + { + Name: "id", + Type: discordgo.ApplicationCommandOptionString, + Value: "1", + }, + }, + }, + }, + } + + err := discord.DeleteLootHandler(context.Background(), session, interaction) + assert.NoError(t, err) + mockLootUseCase.AssertExpectations(t) + }) +}