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)
+	})
+}