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/TODO.md b/TODO.md index 29ce586a..b0e7714c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,16 +1,14 @@ ### 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 -- [ ] 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 - [ ] 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 +21,5 @@ - [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 +- [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/config/config.go b/config/config.go index f571f0fb..5571bebd 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/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 < /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/app/app.go b/internal/app/app.go index 64d90b24..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( @@ -76,7 +77,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/internal/controller/discord/absence.go b/internal/controller/discord/absence.go index f6b77d1a..03541528 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,44 @@ func (d Discord) InitAbsence() map[string]func( } } +func (d Discord) GenerateListAbsenceHandlerMsg(ctx context.Context, date string) (string, error) { + errorMsg := "Error while listing absences" + + ": " + + select { + case <-ctx.Done(): + return ctxError, + 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 = "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" + } + } + } + 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: " + err.Error() - } 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() - } 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 +103,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 ctxError, + 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 +161,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..fb82093e --- /dev/null +++ b/internal/controller/discord/absence_test.go @@ -0,0 +1,248 @@ +package discordhandler_test + +import ( + "context" + "errors" + "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" + "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("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() + + 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) + }) +} + +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) + }) +} diff --git a/internal/controller/discord/loot.go b/internal/controller/discord/loot.go index d87d8467..3660fc3b 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,37 +100,31 @@ 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() + 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 } @@ -148,25 +142,29 @@ 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) + 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 } @@ -190,22 +188,26 @@ 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) + 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 } @@ -227,7 +229,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/loot_test.go b/internal/controller/discord/loot_test.go new file mode 100644 index 00000000..e1419498 --- /dev/null +++ b/internal/controller/discord/loot_test.go @@ -0,0 +1,162 @@ +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/assert" + "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) + }) +} + +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) + }) +} diff --git a/internal/controller/discord/main.go b/internal/controller/discord/main.go index b878e44d..177655b0 100644 --- a/internal/controller/discord/main.go +++ b/internal/controller/discord/main.go @@ -1,19 +1,68 @@ package discordhandler import ( + "context" "fmt" "strings" "time" - "github.com/antony-ramos/guildops/internal/usecase" + "github.com/antony-ramos/guildops/internal/entity" ) type Discord struct { - *usecase.AbsenceUseCase - *usecase.PlayerUseCase - *usecase.StrikeUseCase - *usecase.LootUseCase - *usecase.RaidUseCase + AbsenceUseCase + PlayerUseCase + StrikeUseCase + LootUseCase + RaidUseCase + + Fake bool // Used for testing +} + +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 +} + +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(), ": ") + 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) { @@ -25,7 +74,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/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/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 +} 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/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/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 +} diff --git a/internal/controller/discord/player.go b/internal/controller/discord/player.go index e32353d2..35bb9ae5 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,19 +97,21 @@ 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" } } - _ = 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 } @@ -131,13 +133,15 @@ 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() - _ = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: msg, - }, - }) + msg = "Erreur lors de la récupération du joueur: " + HumanReadableError(err) + 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" @@ -162,7 +166,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 { @@ -182,16 +186,47 @@ 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 } +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 +241,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: " + err.Error() - _ = 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 : " + err.Error() - _ = 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..06a4f738 --- /dev/null +++ b/internal/controller/discord/player_test.go @@ -0,0 +1,305 @@ +package discordhandler_test + +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" + "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) + }) +} + +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) + }) +} diff --git a/internal/controller/discord/raid.go b/internal/controller/discord/raid.go index 22ddcb0f..5da33aa2 100644 --- a/internal/controller/discord/raid.go +++ b/internal/controller/discord/raid.go @@ -42,32 +42,14 @@ 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", Options: []*discordgo.ApplicationCommandOption{ { - Type: discordgo.ApplicationCommandOptionString, + Type: discordgo.ApplicationCommandOptionInteger, Name: "id", - Description: "ex: 4444-4444-4444", + Description: "ex: 4546646", Required: true, }, }, @@ -93,7 +75,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 +88,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" @@ -137,17 +119,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: " + err.Error() + 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{ diff --git a/internal/controller/discord/strike.go b/internal/controller/discord/strike.go index 0ab9d2b7..cdf48a81 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{ @@ -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{ @@ -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" 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..00e44c76 100644 --- a/internal/usecase/loot_test.go +++ b/internal/usecase/loot_test.go @@ -164,3 +164,57 @@ func TestLootUseCase_DeleteLoot(t *testing.T) { mockBackend.AssertExpectations(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) + }) +} 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/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/internal/usecase/postgresbackend/player.go b/internal/usecase/postgresbackend/player.go index f29ee421..b33613df 100644 --- a/internal/usecase/postgresbackend/player.go +++ b/internal/usecase/postgresbackend/player.go @@ -7,12 +7,21 @@ import ( "time" "github.com/jackc/pgx/v4" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" "github.com/antony-ramos/guildops/internal/entity" ) // 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 +158,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 +203,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 +235,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 +266,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/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"). diff --git a/notes b/notes new file mode 100644 index 00000000..e69de29b diff --git a/pkg/discord/discord.go b/pkg/discord/discord.go index 43e68244..38a08734 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,15 @@ 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())) } } }) @@ -64,12 +69,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 +92,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 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 + } +}