From 55bd20da0e2ba8191cfbe100a7f42eab9db15555 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Mon, 9 Feb 2026 03:52:14 +0900 Subject: [PATCH 01/24] Feat: Add contest thumbnail upload API endpoint (#59) Add POST /api/contests/:id/thumbnail endpoint that uploads a thumbnail image to R2 storage and updates the contest's thumbnail URL and banner_key fields in a single operation. Only contest leaders can upload. Co-Authored-By: Claude Opus 4.6 --- cmd/server.go | 5 ++ .../contest/application/contest_service.go | 76 +++++++++++++++++++ .../contest/application/dto/contest_dto.go | 9 +++ .../application/port/contest_storage_port.go | 12 +++ .../presentation/contest_controller.go | 61 +++++++++++++++ internal/storage/domain/storage.go | 16 ++-- internal/storage/provider.go | 3 + 7 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 internal/contest/application/port/contest_storage_port.go diff --git a/cmd/server.go b/cmd/server.go index 6f8875e..99fa1db 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -152,6 +152,11 @@ func main() { // Storage module - provides R2 storage integration for images storageDeps := storage.ProvideStorageDependencies(appRouter) + // Set storage port for contest thumbnail upload + if storageDeps != nil { + contestDeps.ContestService.SetStoragePort(storageDeps.StoragePort) + } + // Banner module - provides main banner management for homepage bannerDeps := banner.ProvideBannerDependencies(db, appRouter) diff --git a/internal/contest/application/contest_service.go b/internal/contest/application/contest_service.go index 963e4fd..e6eb9e6 100644 --- a/internal/contest/application/contest_service.go +++ b/internal/contest/application/contest_service.go @@ -10,10 +10,15 @@ import ( commonDto "github.com/FOR-GAMERS/GAMERS-BE/internal/global/common/dto" "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" oauth2Port "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application/port" + storageDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/domain" "context" "errors" + "fmt" "log" + "mime/multipart" "time" + + "github.com/google/uuid" ) // TournamentGeneratorPort defines the interface for tournament generation @@ -32,6 +37,7 @@ type ContestService struct { tournamentGenerator TournamentGeneratorPort teamDBPort gamePort.TeamDatabasePort gameTeamDBPort gamePort.GameTeamDatabasePort + storagePort port.ContestStoragePort } func NewContestService( @@ -94,6 +100,11 @@ func NewContestServiceFull( } } +// SetStoragePort sets the storage port for file upload operations +func (c *ContestService) SetStoragePort(storagePort port.ContestStoragePort) { + c.storagePort = storagePort +} + func (c *ContestService) SaveContest(req *dto.CreateContestRequest, userId int64) (*domain.Contest, *dto.DiscordLinkRequiredResponse, error) { // Check if user has linked Discord account discordAccount, err := c.oauth2Repository.FindDiscordAccountByUserId(userId) @@ -470,3 +481,68 @@ func (c *ContestService) GetDiscordTextChannels(guildID string) ([]port.DiscordC } return c.discordValidator.GetGuildTextChannels(guildID) } + +// UploadThumbnail uploads a thumbnail image for a contest and updates the contest record +func (c *ContestService) UploadThumbnail(ctx context.Context, contestId, userId int64, file *multipart.FileHeader) (*dto.ThumbnailUploadResponse, error) { + if c.storagePort == nil { + return nil, exception.ErrStorageUploadFailed + } + + // Check leader permission + if err := c.checkLeaderPermission(contestId, userId); err != nil { + return nil, err + } + + // Validate file + if err := storageDomain.ValidateFile(file, storageDomain.UploadTypeContestThumbnail); err != nil { + return nil, err + } + + // Generate storage key + mimeType := file.Header.Get("Content-Type") + ext := storageDomain.GetExtensionFromMimeType(mimeType) + key := fmt.Sprintf("%s/%d/%s%s", storageDomain.UploadTypeContestThumbnail, contestId, uuid.New().String(), ext) + + // Upload file to storage + src, err := file.Open() + if err != nil { + return nil, exception.ErrStorageUploadFailed + } + defer src.Close() + + if err := c.storagePort.Upload(ctx, key, src, file.Size, mimeType); err != nil { + return nil, exception.ErrStorageUploadFailed + } + + // Get public URL and update contest record + url := c.storagePort.GetPublicURL(key) + + contest, err := c.repository.GetContestById(contestId) + if err != nil { + return nil, err + } + + // Delete old thumbnail from storage if exists + if contest.BannerKey != nil && *contest.BannerKey != "" { + go func() { + if delErr := c.storagePort.Delete(context.Background(), *contest.BannerKey); delErr != nil { + log.Printf("[UploadThumbnail] Failed to delete old thumbnail %s: %v", *contest.BannerKey, delErr) + } + }() + } + + contest.Thumbnail = &url + contest.BannerKey = &key + + if err := c.repository.UpdateContest(contest); err != nil { + return nil, err + } + + return &dto.ThumbnailUploadResponse{ + Key: key, + URL: url, + Size: file.Size, + MimeType: mimeType, + UploadedAt: time.Now(), + }, nil +} diff --git a/internal/contest/application/dto/contest_dto.go b/internal/contest/application/dto/contest_dto.go index 2d017a7..f9d73c7 100644 --- a/internal/contest/application/dto/contest_dto.go +++ b/internal/contest/application/dto/contest_dto.go @@ -269,6 +269,15 @@ func ToMyContestResponses(contests []*port.ContestWithMembership) []*MyContestRe return responses } +// ThumbnailUploadResponse represents the response after uploading a contest thumbnail +type ThumbnailUploadResponse struct { + Key string `json:"key"` + URL string `json:"url"` + Size int64 `json:"size"` + MimeType string `json:"mime_type"` + UploadedAt time.Time `json:"uploaded_at"` +} + // ChangeMemberRoleRequest represents the request to change a member's role type ChangeMemberRoleRequest struct { MemberType domain.MemberType `json:"member_type" binding:"required"` diff --git a/internal/contest/application/port/contest_storage_port.go b/internal/contest/application/port/contest_storage_port.go new file mode 100644 index 0000000..02c7e69 --- /dev/null +++ b/internal/contest/application/port/contest_storage_port.go @@ -0,0 +1,12 @@ +package port + +import ( + "context" + "io" +) + +type ContestStoragePort interface { + Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) error + Delete(ctx context.Context, key string) error + GetPublicURL(key string) string +} diff --git a/internal/contest/presentation/contest_controller.go b/internal/contest/presentation/contest_controller.go index e8c6ab5..f886af3 100644 --- a/internal/contest/presentation/contest_controller.go +++ b/internal/contest/presentation/contest_controller.go @@ -37,6 +37,7 @@ func (c *ContestController) RegisterRoute() { privateGroup.GET("/me", c.GetMyContests) privateGroup.PATCH("/:id", c.UpdateContest) privateGroup.DELETE("/:id", c.DeleteContest) + privateGroup.POST("/:id/thumbnail", c.UploadThumbnail) privateGroup.POST("/:id/start", c.StartContest) privateGroup.POST("/:id/stop", c.StopContest) @@ -263,6 +264,43 @@ func (c *ContestController) StopContest(ctx *gin.Context) { c.helper.RespondOK(ctx, contest, err, "contest stopped successfully") } +// UploadThumbnail godoc +// @Summary Upload a contest thumbnail image +// @Description Upload a thumbnail image for a contest. Maximum file size is 5MB. Allowed formats: jpeg, png, webp. Only the contest leader can upload. +// @Tags contests +// @Accept multipart/form-data +// @Produce json +// @Security BearerAuth +// @Param id path int true "Contest ID" +// @Param file formData file true "Image file (max 5MB, jpeg/png/webp)" +// @Success 201 {object} response.Response{data=dto.ThumbnailUploadResponse} +// @Failure 400 {object} response.Response +// @Failure 401 {object} response.Response +// @Failure 403 {object} response.Response +// @Router /api/contests/{id}/thumbnail [post] +func (c *ContestController) UploadThumbnail(ctx *gin.Context) { + id, err := strconv.ParseInt(ctx.Param("id"), 10, 64) + if err != nil { + response.JSON(ctx, response.BadRequest("invalid contest id")) + return + } + + userId, ok := middleware.GetUserIdFromContext(ctx) + if !ok { + response.JSON(ctx, response.Error(401, "user not authenticated")) + return + } + + file, err := ctx.FormFile("file") + if err != nil { + response.JSON(ctx, response.BadRequest("file is required")) + return + } + + result, err := c.service.UploadThumbnail(ctx.Request.Context(), id, userId, file) + c.helper.RespondCreated(ctx, result, err, "thumbnail uploaded successfully") +} + // GetMyContests godoc // @Summary Get contests I have joined // @Description Get all contests that the authenticated user has joined with pagination, sorting, and filtering support @@ -421,3 +459,26 @@ func HandleStopContest(ctx *gin.Context, service *application.ContestService, he contest, err := service.StopContest(ctx.Request.Context(), id, userId) helper.RespondOK(ctx, contest, err, "contest stopped successfully") } + +func HandleUploadThumbnail(ctx *gin.Context, service *application.ContestService, helper *handler.ControllerHelper) { + id, err := strconv.ParseInt(ctx.Param("id"), 10, 64) + if err != nil { + response.JSON(ctx, response.BadRequest("invalid contest id")) + return + } + + userId, ok := middleware.GetUserIdFromContext(ctx) + if !ok { + response.JSON(ctx, response.Error(401, "user not authenticated")) + return + } + + file, err := ctx.FormFile("file") + if err != nil { + response.JSON(ctx, response.BadRequest("file is required")) + return + } + + result, err := service.UploadThumbnail(ctx.Request.Context(), id, userId, file) + helper.RespondCreated(ctx, result, err, "thumbnail uploaded successfully") +} diff --git a/internal/storage/domain/storage.go b/internal/storage/domain/storage.go index 7a4c569..7dbb577 100644 --- a/internal/storage/domain/storage.go +++ b/internal/storage/domain/storage.go @@ -11,15 +11,17 @@ import ( type UploadType string const ( - UploadTypeContestBanner UploadType = "contest-banners" - UploadTypeUserProfile UploadType = "user-profiles" - UploadTypeMainBanner UploadType = "main-banners" + UploadTypeContestBanner UploadType = "contest-banners" + UploadTypeContestThumbnail UploadType = "contest-thumbnails" + UploadTypeUserProfile UploadType = "user-profiles" + UploadTypeMainBanner UploadType = "main-banners" ) const ( - MaxContestBannerSize = 5 * 1024 * 1024 // 5MB - MaxUserProfileSize = 2 * 1024 * 1024 // 2MB - MaxMainBannerSize = 5 * 1024 * 1024 // 5MB + MaxContestBannerSize = 5 * 1024 * 1024 // 5MB + MaxContestThumbnailSize = 5 * 1024 * 1024 // 5MB + MaxUserProfileSize = 2 * 1024 * 1024 // 2MB + MaxMainBannerSize = 5 * 1024 * 1024 // 5MB ) var AllowedMimeTypes = map[string]bool{ @@ -74,6 +76,8 @@ func getMaxSize(uploadType UploadType) int64 { switch uploadType { case UploadTypeContestBanner: return MaxContestBannerSize + case UploadTypeContestThumbnail: + return MaxContestThumbnailSize case UploadTypeUserProfile: return MaxUserProfileSize case UploadTypeMainBanner: diff --git a/internal/storage/provider.go b/internal/storage/provider.go index e153fdf..d9743c4 100644 --- a/internal/storage/provider.go +++ b/internal/storage/provider.go @@ -4,6 +4,7 @@ import ( "github.com/FOR-GAMERS/GAMERS-BE/internal/global/common/handler" "github.com/FOR-GAMERS/GAMERS-BE/internal/global/common/router" "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/application/port" "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/infra" "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/presentation" "log" @@ -12,6 +13,7 @@ import ( type Dependencies struct { Controller *presentation.StorageController StorageService *application.StorageService + StoragePort port.StoragePort } func ProvideStorageDependencies(router *router.Router) *Dependencies { @@ -31,5 +33,6 @@ func ProvideStorageDependencies(router *router.Router) *Dependencies { return &Dependencies{ Controller: storageController, StorageService: storageService, + StoragePort: storageAdapter, } } From 84ff7f3de08d6d30c1185c59951b6cdb54bb7212 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:08:19 +0900 Subject: [PATCH 02/24] Fix: Handle Discord OAuth2 denial with redirect instead of 500 error (#9) When users click "Deny" on Discord consent screen, the callback receives an error query parameter instead of a code. Previously this caused a 500 error due to the required binding on code field. Co-Authored-By: Claude Opus 4.6 --- internal/oauth2/application/dto/oauth2_dto.go | 4 +++- internal/oauth2/presentation/discord_controller.go | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/oauth2/application/dto/oauth2_dto.go b/internal/oauth2/application/dto/oauth2_dto.go index c9ab3ba..e804c95 100644 --- a/internal/oauth2/application/dto/oauth2_dto.go +++ b/internal/oauth2/application/dto/oauth2_dto.go @@ -1,7 +1,9 @@ package dto type DiscordCallbackRequest struct { - Code string `form:"code" binding:"required"` + Code string `form:"code"` + Error string `form:"error"` + ErrorDescription string `form:"error_description"` } type DiscordUserInfo struct { diff --git a/internal/oauth2/presentation/discord_controller.go b/internal/oauth2/presentation/discord_controller.go index 242e74d..d5aab54 100644 --- a/internal/oauth2/presentation/discord_controller.go +++ b/internal/oauth2/presentation/discord_controller.go @@ -75,6 +75,19 @@ func (c *DiscordController) DiscordCallback(ctx *gin.Context) { return } + // User denied Discord OAuth2 consent + if req.Error != "" { + redirectURL := c.webURL + "/login?error=" + req.Error + ctx.Redirect(http.StatusFound, redirectURL) + return + } + + if req.Code == "" { + redirectURL := c.webURL + "/login?error=missing_code" + ctx.Redirect(http.StatusFound, redirectURL) + return + } + result, err := c.oauth2Service.HandleDiscordCallback(&req) if err != nil { ctx.Error(err) From e44dafbe0d096c5174add0c3917fae896336285e Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:20:28 +0900 Subject: [PATCH 03/24] Docs: Regenerate Swagger docs for Discord OAuth2 callback changes Co-Authored-By: Claude Opus 4.6 --- docs/docs.go | 1117 ++++++++++++++++++++++++-------------------- docs/swagger.json | 1119 +++++++++++++++++++++++++-------------------- docs/swagger.yaml | 1067 ++++++++++++++++++++++-------------------- 3 files changed, 1841 insertions(+), 1462 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index d4e691f..988ded6 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -44,13 +44,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.BannerListResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerListResponse" } } } @@ -60,13 +60,13 @@ const docTemplate = `{ "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -95,7 +95,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.CreateBannerRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.CreateBannerRequest" } } ], @@ -105,13 +105,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.BannerResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerResponse" } } } @@ -121,25 +121,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Maximum banner limit exceeded", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -176,19 +176,19 @@ const docTemplate = `{ "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -224,7 +224,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.UpdateBannerRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.UpdateBannerRequest" } } ], @@ -234,13 +234,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.BannerResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerResponse" } } } @@ -250,25 +250,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -294,7 +294,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_auth_application_dto.LoginRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.LoginRequest" } } ], @@ -302,19 +302,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -340,7 +340,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_auth_application_dto.LogoutRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.LogoutRequest" } } ], @@ -351,7 +351,7 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -377,7 +377,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_auth_application_dto.RefreshRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.RefreshRequest" } } ], @@ -385,19 +385,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -419,13 +419,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.BannerListResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerListResponse" } } } @@ -486,13 +486,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_global_common_dto.PaginationResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse" } } } @@ -502,7 +502,7 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -531,7 +531,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.CreateContestRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.CreateContestRequest" } } ], @@ -541,13 +541,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse" } } } @@ -557,13 +557,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { @@ -571,13 +571,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse" } } } @@ -643,13 +643,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_global_common_dto.PaginationResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse" } } } @@ -659,13 +659,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -702,19 +702,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -749,19 +749,19 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { @@ -769,13 +769,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse" } } } @@ -785,7 +785,7 @@ const docTemplate = `{ "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -822,25 +822,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -877,25 +877,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -932,31 +932,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1000,19 +1000,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1056,19 +1056,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1110,25 +1110,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1167,13 +1167,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.ScheduleGameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameResponse" } } } @@ -1183,7 +1183,7 @@ const docTemplate = `{ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1222,13 +1222,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.MatchResultResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultResponse" } } } @@ -1238,7 +1238,7 @@ const docTemplate = `{ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1282,7 +1282,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.ManualResultRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ManualResultRequest" } } ], @@ -1292,13 +1292,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.MatchResultResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultResponse" } } } @@ -1308,19 +1308,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1359,13 +1359,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.MatchResultResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultResponse" } } } @@ -1375,7 +1375,7 @@ const docTemplate = `{ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1421,7 +1421,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.ScheduleGameRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameRequest" } } ], @@ -1431,13 +1431,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.ScheduleGameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameResponse" } } } @@ -1447,19 +1447,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1527,13 +1527,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_global_common_dto.PaginationResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse" } } } @@ -1543,19 +1543,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1600,7 +1600,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleRequest" } } ], @@ -1610,13 +1610,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleResponse" } } } @@ -1626,25 +1626,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1683,13 +1683,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.UserContestStatusResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.UserContestStatusResponse" } } } @@ -1699,19 +1699,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1745,13 +1745,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse" } } } @@ -1761,13 +1761,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1805,19 +1805,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1853,7 +1853,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.UpdateContestRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.UpdateContestRequest" } } ], @@ -1863,13 +1863,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse" } } } @@ -1879,19 +1879,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1954,13 +1954,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_global_common_dto.PaginationResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse" } } } @@ -1970,13 +1970,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2012,7 +2012,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_comment_application_dto.CreateCommentRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CreateCommentRequest" } } ], @@ -2022,13 +2022,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_comment_application_dto.CommentResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CommentResponse" } } } @@ -2038,19 +2038,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2091,13 +2091,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_comment_application_dto.CommentResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CommentResponse" } } } @@ -2107,13 +2107,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2155,31 +2155,31 @@ const docTemplate = `{ "204": { "description": "No Content", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2222,7 +2222,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_comment_application_dto.UpdateCommentRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.UpdateCommentRequest" } } ], @@ -2232,13 +2232,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_comment_application_dto.CommentResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CommentResponse" } } } @@ -2248,25 +2248,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2300,7 +2300,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", @@ -2308,7 +2308,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -2319,7 +2319,7 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2351,13 +2351,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.ContestResultResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ContestResultResponse" } } } @@ -2367,13 +2367,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2412,13 +2412,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse" } } } @@ -2428,25 +2428,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2485,13 +2485,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse" } } } @@ -2501,25 +2501,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2556,25 +2556,25 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2609,7 +2609,7 @@ const docTemplate = `{ "name": "request", "in": "body", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.CreateTeamRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateTeamRequest" } } ], @@ -2617,25 +2617,25 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2673,25 +2673,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2728,31 +2728,31 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2790,7 +2790,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.InviteMemberRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.InviteMemberRequest" } } ], @@ -2800,13 +2800,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.TeamInviteResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TeamInviteResponse" } } } @@ -2816,31 +2816,31 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2871,6 +2871,15 @@ const docTemplate = `{ "name": "id", "in": "path", "required": true + }, + { + "description": "Accept invite request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.AcceptInviteRequest" + } } ], "responses": { @@ -2879,13 +2888,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.CachedMemberResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CachedMemberResponse" } } } @@ -2895,25 +2904,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2944,6 +2953,15 @@ const docTemplate = `{ "name": "id", "in": "path", "required": true + }, + { + "description": "Reject invite request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.RejectInviteRequest" + } } ], "responses": { @@ -2953,19 +2971,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3003,7 +3021,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.KickMemberRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.KickMemberRequest" } } ], @@ -3014,25 +3032,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3072,25 +3090,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3103,7 +3121,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Get all members of a contest team", + "description": "Get all members of the contest team that the authenticated user belongs to", "consumes": [ "application/json" ], @@ -3129,7 +3147,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", @@ -3137,7 +3155,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.CachedMemberResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CachedMemberResponse" } } } @@ -3148,19 +3166,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3173,7 +3191,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Get a specific member of a contest team by user ID", + "description": "Get a specific member of the authenticated user's team by user ID", "consumes": [ "application/json" ], @@ -3206,13 +3224,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.CachedMemberResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CachedMemberResponse" } } } @@ -3222,19 +3240,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3272,7 +3296,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.TransferLeadershipRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TransferLeadershipRequest" } } ], @@ -3280,31 +3304,105 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + } + } + } + } + }, + "/api/contests/{id}/thumbnail": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Upload a thumbnail image for a contest. Maximum file size is 5MB. Allowed formats: jpeg, png, webp. Only the contest leader can upload.", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "contests" + ], + "summary": "Upload a contest thumbnail image", + "parameters": [ + { + "type": "integer", + "description": "Contest ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "Image file (max 5MB, jpeg/png/webp)", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ThumbnailUploadResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3350,13 +3448,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_valorant_application_dto.ContestPointResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ContestPointResponse" } } } @@ -3366,19 +3464,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3409,7 +3507,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.CreateGameTeamRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateGameTeamRequest" } } ], @@ -3419,13 +3517,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameTeamResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResponse" } } } @@ -3435,25 +3533,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3487,13 +3585,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameTeamResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResponse" } } } @@ -3503,13 +3601,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3547,19 +3645,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3590,7 +3688,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.CreateGameRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateGameRequest" } } ], @@ -3600,13 +3698,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -3616,13 +3714,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3656,13 +3754,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -3672,13 +3770,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3716,25 +3814,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3770,7 +3868,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.UpdateGameRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.UpdateGameRequest" } } ], @@ -3780,13 +3878,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -3796,25 +3894,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3853,13 +3951,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -3869,25 +3967,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3926,13 +4024,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -3942,25 +4040,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3994,7 +4092,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", @@ -4002,7 +4100,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameTeamResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResponse" } } } @@ -4013,7 +4111,7 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4052,13 +4150,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -4068,25 +4166,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4133,13 +4231,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_notification_application_dto.NotificationListResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_notification_application_dto.NotificationListResponse" } } } @@ -4149,7 +4247,7 @@ const docTemplate = `{ "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4174,13 +4272,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4211,7 +4309,7 @@ const docTemplate = `{ "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4245,19 +4343,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4301,13 +4399,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4356,7 +4454,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.CreateUserRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.CreateUserRequest" } } ], @@ -4366,13 +4464,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.UserResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UserResponse" } } } @@ -4382,13 +4480,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4418,13 +4516,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.MyUserResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.MyUserResponse" } } } @@ -4434,13 +4532,13 @@ const docTemplate = `{ "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4470,13 +4568,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse" } } } @@ -4486,19 +4584,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4527,7 +4625,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_valorant_application_dto.RegisterValorantRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.RegisterValorantRequest" } } ], @@ -4537,13 +4635,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse" } } } @@ -4553,31 +4651,31 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "502": { "description": "Bad Gateway", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4606,19 +4704,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4648,13 +4746,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse" } } } @@ -4664,25 +4762,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "502": { "description": "Bad Gateway", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4721,13 +4819,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.UserResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UserResponse" } } } @@ -4737,19 +4835,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4785,7 +4883,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.UpdateUserInfoRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UpdateUserInfoRequest" } } ], @@ -4795,13 +4893,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.MyUserResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.MyUserResponse" } } } @@ -4811,25 +4909,25 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4867,19 +4965,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4915,7 +5013,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.UpdateUserRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UpdateUserRequest" } } ], @@ -4925,13 +5023,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.UserResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UserResponse" } } } @@ -4941,19 +5039,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4999,13 +5097,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_storage_application_dto.UploadResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_storage_application_dto.UploadResponse" } } } @@ -5015,13 +5113,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5060,13 +5158,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_storage_application_dto.UploadResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_storage_application_dto.UploadResponse" } } } @@ -5076,13 +5174,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5107,7 +5205,7 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", @@ -5115,7 +5213,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse" } } } @@ -5149,7 +5247,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_point_application_dto.CreateValorantScoreTableDto" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.CreateValorantScoreTableDto" } } ], @@ -5159,13 +5257,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse" } } } @@ -5175,13 +5273,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5215,13 +5313,13 @@ const docTemplate = `{ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse" } } } @@ -5231,13 +5329,13 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5275,19 +5373,19 @@ const docTemplate = `{ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5317,20 +5415,20 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_discord_application_dto.DiscordGuild" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordGuild" } } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5369,32 +5467,32 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_discord_application_dto.DiscordChannel" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordChannel" } } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5402,7 +5500,7 @@ const docTemplate = `{ } }, "definitions": { - "GAMERS-BE_internal_auth_application_dto.LoginRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.LoginRequest": { "type": "object", "properties": { "email": { @@ -5413,7 +5511,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_auth_application_dto.LogoutRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.LogoutRequest": { "type": "object", "required": [ "access_token", @@ -5428,7 +5526,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_auth_application_dto.RefreshRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.RefreshRequest": { "type": "object", "required": [ "refresh_token" @@ -5439,13 +5537,13 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_banner_application_dto.BannerListResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerListResponse": { "type": "object", "properties": { "banners": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.BannerResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerResponse" } }, "total": { @@ -5453,7 +5551,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_banner_application_dto.BannerResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerResponse": { "type": "object", "properties": { "created_at": { @@ -5482,7 +5580,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_banner_application_dto.CreateBannerRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.CreateBannerRequest": { "type": "object", "required": [ "image_key" @@ -5506,7 +5604,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_banner_application_dto.UpdateBannerRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.UpdateBannerRequest": { "type": "object", "properties": { "display_order": { @@ -5527,7 +5625,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_comment_application_dto.AuthorResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.AuthorResponse": { "type": "object", "properties": { "avatar": { @@ -5544,11 +5642,11 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_comment_application_dto.CommentResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CommentResponse": { "type": "object", "properties": { "author": { - "$ref": "#/definitions/GAMERS-BE_internal_comment_application_dto.AuthorResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.AuthorResponse" }, "comment_id": { "type": "integer" @@ -5567,7 +5665,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_comment_application_dto.CreateCommentRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CreateCommentRequest": { "type": "object", "required": [ "content" @@ -5579,7 +5677,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_comment_application_dto.UpdateCommentRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.UpdateCommentRequest": { "type": "object", "required": [ "content" @@ -5591,35 +5689,35 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleRequest": { "type": "object", "required": [ "member_type" ], "properties": { "member_type": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.MemberType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.MemberType" } } }, - "GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleResponse": { "type": "object", "properties": { "contest_id": { "type": "integer" }, "leader_type": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.LeaderType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.LeaderType" }, "member_type": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.MemberType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.MemberType" }, "user_id": { "type": "integer" } } }, - "GAMERS-BE_internal_contest_application_dto.ContestResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse": { "type": "object", "properties": { "auto_start": { @@ -5629,10 +5727,10 @@ const docTemplate = `{ "type": "integer" }, "contest_status": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.ContestStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestStatus" }, "contest_type": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.ContestType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestType" }, "created_at": { "type": "string" @@ -5653,7 +5751,7 @@ const docTemplate = `{ "type": "integer" }, "game_type": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameType" }, "max_team_count": { "type": "integer" @@ -5678,7 +5776,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_contest_application_dto.CreateContestRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.CreateContestRequest": { "type": "object", "required": [ "contest_type", @@ -5689,7 +5787,7 @@ const docTemplate = `{ "type": "boolean" }, "contest_type": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.ContestType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestType" }, "description": { "type": "string" @@ -5707,7 +5805,7 @@ const docTemplate = `{ "type": "integer" }, "game_type": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameType" }, "max_team_count": { "type": "integer" @@ -5729,7 +5827,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse": { "type": "object", "properties": { "message": { @@ -5740,17 +5838,37 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_contest_application_dto.UpdateContestRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ThumbnailUploadResponse": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "mime_type": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "uploaded_at": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.UpdateContestRequest": { "type": "object", "properties": { "auto_start": { "type": "boolean" }, "contest_status": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.ContestStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestStatus" }, "contest_type": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.ContestType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestType" }, "description": { "type": "string" @@ -5768,7 +5886,7 @@ const docTemplate = `{ "type": "integer" }, "game_type": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameType" }, "max_team_count": { "type": "integer" @@ -5790,11 +5908,11 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_contest_application_dto.UserContestStatusResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.UserContestStatusResponse": { "type": "object", "properties": { "application_status": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_port.ApplicationStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_port.ApplicationStatus" }, "has_applied": { "type": "boolean" @@ -5810,7 +5928,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_contest_application_port.ApplicationStatus": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_port.ApplicationStatus": { "type": "string", "enum": [ "PENDING", @@ -5823,7 +5941,7 @@ const docTemplate = `{ "ApplicationStatusRejected" ] }, - "GAMERS-BE_internal_contest_domain.ContestStatus": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestStatus": { "type": "string", "enum": [ "PENDING", @@ -5838,7 +5956,7 @@ const docTemplate = `{ "ContestStatusCancelled" ] }, - "GAMERS-BE_internal_contest_domain.ContestType": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestType": { "type": "string", "enum": [ "TOURNAMENT", @@ -5851,7 +5969,7 @@ const docTemplate = `{ "ContestTypeCasual" ] }, - "GAMERS-BE_internal_contest_domain.LeaderType": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.LeaderType": { "type": "string", "enum": [ "LEADER", @@ -5862,7 +5980,7 @@ const docTemplate = `{ "LeaderTypeMember" ] }, - "GAMERS-BE_internal_contest_domain.MemberType": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.MemberType": { "type": "string", "enum": [ "STAFF", @@ -5873,7 +5991,7 @@ const docTemplate = `{ "MemberTypeNormal" ] }, - "GAMERS-BE_internal_discord_application_dto.DiscordChannel": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordChannel": { "type": "object", "properties": { "guild_id": { @@ -5896,7 +6014,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_discord_application_dto.DiscordGuild": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordGuild": { "type": "object", "properties": { "icon": { @@ -5916,7 +6034,18 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.CachedMemberResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.AcceptInviteRequest": { + "type": "object", + "required": [ + "team_id" + ], + "properties": { + "team_id": { + "type": "integer" + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CachedMemberResponse": { "type": "object", "properties": { "contest_id": { @@ -5942,11 +6071,11 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.ContestResultResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ContestResultResponse": { "type": "object", "properties": { "champion": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.TeamSummary" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TeamSummary" }, "contest_id": { "type": "integer" @@ -5957,7 +6086,7 @@ const docTemplate = `{ "rounds": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.RoundResult" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.RoundResult" } }, "title": { @@ -5968,7 +6097,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.CreateGameRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateGameRequest": { "type": "object", "required": [ "contest_id", @@ -5982,14 +6111,14 @@ const docTemplate = `{ "type": "string" }, "game_team_type": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameTeamType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameTeamType" }, "started_at": { "type": "string" } } }, - "GAMERS-BE_internal_game_application_dto.CreateGameTeamRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateGameTeamRequest": { "type": "object", "required": [ "game_id", @@ -6007,7 +6136,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.CreateTeamRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateTeamRequest": { "type": "object", "properties": { "team_name": { @@ -6015,7 +6144,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.GameResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse": { "type": "object", "properties": { "contest_id": { @@ -6031,10 +6160,10 @@ const docTemplate = `{ "type": "integer" }, "game_status": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameStatus" }, "game_team_type": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameTeamType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameTeamType" }, "modified_at": { "type": "string" @@ -6044,7 +6173,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.GameResult": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResult": { "type": "object", "properties": { "detection_status": { @@ -6060,17 +6189,17 @@ const docTemplate = `{ "type": "integer" }, "match_result": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.MatchResultSummary" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultSummary" }, "teams": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameTeamResult" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResult" } } } }, - "GAMERS-BE_internal_game_application_dto.GameTeamResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResponse": { "type": "object", "properties": { "game_id": { @@ -6087,7 +6216,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.GameTeamResult": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResult": { "type": "object", "properties": { "grade": { @@ -6101,7 +6230,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.InviteMemberRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.InviteMemberRequest": { "type": "object", "required": [ "user_id" @@ -6112,7 +6241,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.KickMemberRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.KickMemberRequest": { "type": "object", "required": [ "user_id" @@ -6123,7 +6252,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.ManualResultRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ManualResultRequest": { "type": "object", "required": [ "loserScore", @@ -6145,11 +6274,11 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.MatchResultResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultResponse": { "type": "object", "properties": { "detectionStatus": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.DetectionStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.DetectionStatus" }, "gameDuration": { "type": "integer" @@ -6175,7 +6304,7 @@ const docTemplate = `{ "playerStats": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.PlayerStatResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.PlayerStatResponse" } }, "roundsPlayed": { @@ -6192,7 +6321,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.MatchResultSummary": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultSummary": { "type": "object", "properties": { "loser_score": { @@ -6212,7 +6341,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.PlayerStatResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.PlayerStatResponse": { "type": "object", "properties": { "agentName": { @@ -6247,13 +6376,24 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.RoundResult": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.RejectInviteRequest": { + "type": "object", + "required": [ + "team_id" + ], + "properties": { + "team_id": { + "type": "integer" + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.RoundResult": { "type": "object", "properties": { "games": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResult" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResult" } }, "round": { @@ -6264,7 +6404,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.ScheduleGameRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameRequest": { "type": "object", "required": [ "scheduledStartTime" @@ -6278,14 +6418,14 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.ScheduleGameResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameResponse": { "type": "object", "properties": { "contestId": { "type": "integer" }, "detectionStatus": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.DetectionStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.DetectionStatus" }, "detectionWindowMinutes": { "type": "integer" @@ -6294,7 +6434,7 @@ const docTemplate = `{ "type": "integer" }, "gameStatus": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameStatus" }, "matchNumber": { "type": "integer" @@ -6307,7 +6447,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.TeamInviteResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TeamInviteResponse": { "type": "object", "properties": { "contest_id": { @@ -6327,10 +6467,13 @@ const docTemplate = `{ }, "status": { "type": "string" + }, + "team_id": { + "type": "integer" } } }, - "GAMERS-BE_internal_game_application_dto.TeamSummary": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TeamSummary": { "type": "object", "properties": { "team_id": { @@ -6341,7 +6484,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.TransferLeadershipRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TransferLeadershipRequest": { "type": "object", "required": [ "new_leader_user_id" @@ -6352,24 +6495,24 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_game_application_dto.UpdateGameRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.UpdateGameRequest": { "type": "object", "properties": { "ended_at": { "type": "string" }, "game_status": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameStatus" }, "game_team_type": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameTeamType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameTeamType" }, "started_at": { "type": "string" } } }, - "GAMERS-BE_internal_game_domain.DetectionStatus": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.DetectionStatus": { "type": "string", "enum": [ "NONE", @@ -6386,7 +6529,7 @@ const docTemplate = `{ "DetectionStatusManual" ] }, - "GAMERS-BE_internal_game_domain.GameStatus": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameStatus": { "type": "string", "enum": [ "PENDING", @@ -6401,7 +6544,7 @@ const docTemplate = `{ "GameStatusCancelled" ] }, - "GAMERS-BE_internal_game_domain.GameTeamType": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameTeamType": { "type": "string", "enum": [ "SINGLE", @@ -6432,7 +6575,7 @@ const docTemplate = `{ "GameTeamTypeHurupa" ] }, - "GAMERS-BE_internal_game_domain.GameType": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameType": { "type": "string", "enum": [ "VALORANT", @@ -6443,7 +6586,7 @@ const docTemplate = `{ "GameTypeLOL" ] }, - "GAMERS-BE_internal_global_common_dto.PaginationResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse": { "type": "object", "properties": { "data": {}, @@ -6461,7 +6604,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_global_response.Response": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response": { "type": "object", "properties": { "data": {}, @@ -6473,13 +6616,13 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_notification_application_dto.NotificationListResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_notification_application_dto.NotificationListResponse": { "type": "object", "properties": { "notifications": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_notification_application_dto.NotificationResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_notification_application_dto.NotificationResponse" } }, "total": { @@ -6490,7 +6633,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_notification_application_dto.NotificationResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_notification_application_dto.NotificationResponse": { "type": "object", "properties": { "created_at": { @@ -6513,11 +6656,11 @@ const docTemplate = `{ "type": "string" }, "type": { - "$ref": "#/definitions/GAMERS-BE_internal_notification_domain.NotificationType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_notification_domain.NotificationType" } } }, - "GAMERS-BE_internal_notification_domain.NotificationType": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_notification_domain.NotificationType": { "type": "string", "enum": [ "TEAM_INVITE_RECEIVED", @@ -6534,7 +6677,7 @@ const docTemplate = `{ "NotificationTypeApplicationRejected" ] }, - "GAMERS-BE_internal_point_application_dto.CreateValorantScoreTableDto": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.CreateValorantScoreTableDto": { "type": "object", "required": [ "ascendant_1", @@ -6641,7 +6784,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse": { "type": "object", "properties": { "ascendant_1": { @@ -6724,7 +6867,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_storage_application_dto.UploadResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_storage_application_dto.UploadResponse": { "type": "object", "properties": { "key": { @@ -6744,7 +6887,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_user_application_dto.CreateUserRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.CreateUserRequest": { "type": "object", "required": [ "email", @@ -6773,7 +6916,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_user_application_dto.MyUserResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.MyUserResponse": { "type": "object", "properties": { "avatar": { @@ -6835,7 +6978,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_user_application_dto.UpdateUserInfoRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UpdateUserInfoRequest": { "type": "object", "properties": { "avatar": { @@ -6852,7 +6995,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_user_application_dto.UpdateUserRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UpdateUserRequest": { "type": "object", "required": [ "password" @@ -6863,7 +7006,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_user_application_dto.UserResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UserResponse": { "type": "object", "properties": { "created_at": { @@ -6880,7 +7023,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_valorant_application_dto.ContestPointResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ContestPointResponse": { "type": "object", "properties": { "current_tier_patched": { @@ -6925,7 +7068,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_valorant_application_dto.RegisterValorantRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.RegisterValorantRequest": { "type": "object", "required": [ "region", @@ -6957,7 +7100,7 @@ const docTemplate = `{ } } }, - "GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse": { "type": "object", "properties": { "current_tier": { diff --git a/docs/swagger.json b/docs/swagger.json index 9b764b0..66148eb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -38,13 +38,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.BannerListResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerListResponse" } } } @@ -54,13 +54,13 @@ "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -89,7 +89,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.CreateBannerRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.CreateBannerRequest" } } ], @@ -99,13 +99,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.BannerResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerResponse" } } } @@ -115,25 +115,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Maximum banner limit exceeded", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -170,19 +170,19 @@ "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -218,7 +218,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.UpdateBannerRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.UpdateBannerRequest" } } ], @@ -228,13 +228,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.BannerResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerResponse" } } } @@ -244,25 +244,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -288,7 +288,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_auth_application_dto.LoginRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.LoginRequest" } } ], @@ -296,19 +296,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -334,7 +334,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_auth_application_dto.LogoutRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.LogoutRequest" } } ], @@ -345,7 +345,7 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -371,7 +371,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_auth_application_dto.RefreshRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.RefreshRequest" } } ], @@ -379,19 +379,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -413,13 +413,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.BannerListResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerListResponse" } } } @@ -480,13 +480,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_global_common_dto.PaginationResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse" } } } @@ -496,7 +496,7 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -525,7 +525,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.CreateContestRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.CreateContestRequest" } } ], @@ -535,13 +535,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse" } } } @@ -551,13 +551,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { @@ -565,13 +565,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse" } } } @@ -637,13 +637,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_global_common_dto.PaginationResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse" } } } @@ -653,13 +653,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -696,19 +696,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -743,19 +743,19 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { @@ -763,13 +763,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse" } } } @@ -779,7 +779,7 @@ "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -816,25 +816,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -871,25 +871,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -926,31 +926,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -994,19 +994,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1050,19 +1050,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1104,25 +1104,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1161,13 +1161,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.ScheduleGameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameResponse" } } } @@ -1177,7 +1177,7 @@ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1216,13 +1216,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.MatchResultResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultResponse" } } } @@ -1232,7 +1232,7 @@ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1276,7 +1276,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.ManualResultRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ManualResultRequest" } } ], @@ -1286,13 +1286,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.MatchResultResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultResponse" } } } @@ -1302,19 +1302,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1353,13 +1353,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.MatchResultResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultResponse" } } } @@ -1369,7 +1369,7 @@ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1415,7 +1415,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.ScheduleGameRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameRequest" } } ], @@ -1425,13 +1425,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.ScheduleGameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameResponse" } } } @@ -1441,19 +1441,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1521,13 +1521,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_global_common_dto.PaginationResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse" } } } @@ -1537,19 +1537,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1594,7 +1594,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleRequest" } } ], @@ -1604,13 +1604,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleResponse" } } } @@ -1620,25 +1620,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1677,13 +1677,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.UserContestStatusResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.UserContestStatusResponse" } } } @@ -1693,19 +1693,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1739,13 +1739,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse" } } } @@ -1755,13 +1755,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1799,19 +1799,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1847,7 +1847,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.UpdateContestRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.UpdateContestRequest" } } ], @@ -1857,13 +1857,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse" } } } @@ -1873,19 +1873,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -1948,13 +1948,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_global_common_dto.PaginationResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse" } } } @@ -1964,13 +1964,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2006,7 +2006,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_comment_application_dto.CreateCommentRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CreateCommentRequest" } } ], @@ -2016,13 +2016,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_comment_application_dto.CommentResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CommentResponse" } } } @@ -2032,19 +2032,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2085,13 +2085,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_comment_application_dto.CommentResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CommentResponse" } } } @@ -2101,13 +2101,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2149,31 +2149,31 @@ "204": { "description": "No Content", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2216,7 +2216,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_comment_application_dto.UpdateCommentRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.UpdateCommentRequest" } } ], @@ -2226,13 +2226,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_comment_application_dto.CommentResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CommentResponse" } } } @@ -2242,25 +2242,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2294,7 +2294,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", @@ -2302,7 +2302,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -2313,7 +2313,7 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2345,13 +2345,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.ContestResultResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ContestResultResponse" } } } @@ -2361,13 +2361,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2406,13 +2406,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse" } } } @@ -2422,25 +2422,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2479,13 +2479,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse" } } } @@ -2495,25 +2495,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2550,25 +2550,25 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2603,7 +2603,7 @@ "name": "request", "in": "body", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.CreateTeamRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateTeamRequest" } } ], @@ -2611,25 +2611,25 @@ "201": { "description": "Created", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2667,25 +2667,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2722,31 +2722,31 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2784,7 +2784,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.InviteMemberRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.InviteMemberRequest" } } ], @@ -2794,13 +2794,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.TeamInviteResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TeamInviteResponse" } } } @@ -2810,31 +2810,31 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2865,6 +2865,15 @@ "name": "id", "in": "path", "required": true + }, + { + "description": "Accept invite request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.AcceptInviteRequest" + } } ], "responses": { @@ -2873,13 +2882,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.CachedMemberResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CachedMemberResponse" } } } @@ -2889,25 +2898,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2938,6 +2947,15 @@ "name": "id", "in": "path", "required": true + }, + { + "description": "Reject invite request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.RejectInviteRequest" + } } ], "responses": { @@ -2947,19 +2965,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -2997,7 +3015,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.KickMemberRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.KickMemberRequest" } } ], @@ -3008,25 +3026,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3066,25 +3084,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3097,7 +3115,7 @@ "BearerAuth": [] } ], - "description": "Get all members of a contest team", + "description": "Get all members of the contest team that the authenticated user belongs to", "consumes": [ "application/json" ], @@ -3123,7 +3141,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", @@ -3131,7 +3149,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.CachedMemberResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CachedMemberResponse" } } } @@ -3142,19 +3160,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3167,7 +3185,7 @@ "BearerAuth": [] } ], - "description": "Get a specific member of a contest team by user ID", + "description": "Get a specific member of the authenticated user's team by user ID", "consumes": [ "application/json" ], @@ -3200,13 +3218,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.CachedMemberResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CachedMemberResponse" } } } @@ -3216,19 +3234,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3266,7 +3290,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.TransferLeadershipRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TransferLeadershipRequest" } } ], @@ -3274,31 +3298,105 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + } + } + } + } + }, + "/api/contests/{id}/thumbnail": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Upload a thumbnail image for a contest. Maximum file size is 5MB. Allowed formats: jpeg, png, webp. Only the contest leader can upload.", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "contests" + ], + "summary": "Upload a contest thumbnail image", + "parameters": [ + { + "type": "integer", + "description": "Contest ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "Image file (max 5MB, jpeg/png/webp)", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ThumbnailUploadResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3344,13 +3442,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_valorant_application_dto.ContestPointResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ContestPointResponse" } } } @@ -3360,19 +3458,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3403,7 +3501,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.CreateGameTeamRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateGameTeamRequest" } } ], @@ -3413,13 +3511,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameTeamResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResponse" } } } @@ -3429,25 +3527,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3481,13 +3579,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameTeamResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResponse" } } } @@ -3497,13 +3595,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3541,19 +3639,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3584,7 +3682,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.CreateGameRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateGameRequest" } } ], @@ -3594,13 +3692,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -3610,13 +3708,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3650,13 +3748,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -3666,13 +3764,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3710,25 +3808,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3764,7 +3862,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.UpdateGameRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.UpdateGameRequest" } } ], @@ -3774,13 +3872,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -3790,25 +3888,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3847,13 +3945,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -3863,25 +3961,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3920,13 +4018,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -3936,25 +4034,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -3988,7 +4086,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", @@ -3996,7 +4094,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameTeamResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResponse" } } } @@ -4007,7 +4105,7 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4046,13 +4144,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse" } } } @@ -4062,25 +4160,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4127,13 +4225,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_notification_application_dto.NotificationListResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_notification_application_dto.NotificationListResponse" } } } @@ -4143,7 +4241,7 @@ "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4168,13 +4266,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4205,7 +4303,7 @@ "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4239,19 +4337,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4295,13 +4393,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4350,23 +4448,23 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.CreateUserRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.CreateUserRequest" } } - ] , + ], "responses": { "201": { "description": "Created", "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.UserResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UserResponse" } } } @@ -4376,13 +4474,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4412,13 +4510,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.MyUserResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.MyUserResponse" } } } @@ -4428,13 +4526,13 @@ "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4464,13 +4562,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse" } } } @@ -4480,19 +4578,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4521,7 +4619,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_valorant_application_dto.RegisterValorantRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.RegisterValorantRequest" } } ], @@ -4531,13 +4629,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse" } } } @@ -4547,31 +4645,31 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "502": { "description": "Bad Gateway", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4600,19 +4698,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4642,13 +4740,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse" } } } @@ -4658,25 +4756,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "502": { "description": "Bad Gateway", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4715,13 +4813,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.UserResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UserResponse" } } } @@ -4731,19 +4829,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4779,7 +4877,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.UpdateUserInfoRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UpdateUserInfoRequest" } } ], @@ -4789,13 +4887,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.MyUserResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.MyUserResponse" } } } @@ -4805,25 +4903,25 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "409": { "description": "Conflict", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4861,19 +4959,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4909,7 +5007,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.UpdateUserRequest" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UpdateUserRequest" } } ], @@ -4919,13 +5017,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_user_application_dto.UserResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UserResponse" } } } @@ -4935,19 +5033,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -4993,13 +5091,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_storage_application_dto.UploadResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_storage_application_dto.UploadResponse" } } } @@ -5009,13 +5107,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5054,13 +5152,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_storage_application_dto.UploadResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_storage_application_dto.UploadResponse" } } } @@ -5070,13 +5168,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5101,7 +5199,7 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", @@ -5109,7 +5207,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse" } } } @@ -5143,7 +5241,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_point_application_dto.CreateValorantScoreTableDto" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.CreateValorantScoreTableDto" } } ], @@ -5153,13 +5251,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse" } } } @@ -5169,13 +5267,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5209,13 +5307,13 @@ "schema": { "allOf": [ { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" }, { "type": "object", "properties": { "data": { - "$ref": "#/definitions/GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse" } } } @@ -5225,13 +5323,13 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5269,19 +5367,19 @@ "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5311,20 +5409,20 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_discord_application_dto.DiscordGuild" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordGuild" } } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5363,32 +5461,32 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_discord_application_dto.DiscordChannel" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordChannel" } } }, "400": { "description": "Bad Request", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "401": { "description": "Unauthorized", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "403": { "description": "Forbidden", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/GAMERS-BE_internal_global_response.Response" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" } } } @@ -5396,7 +5494,7 @@ } }, "definitions": { - "GAMERS-BE_internal_auth_application_dto.LoginRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.LoginRequest": { "type": "object", "properties": { "email": { @@ -5407,7 +5505,7 @@ } } }, - "GAMERS-BE_internal_auth_application_dto.LogoutRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.LogoutRequest": { "type": "object", "required": [ "access_token", @@ -5422,7 +5520,7 @@ } } }, - "GAMERS-BE_internal_auth_application_dto.RefreshRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.RefreshRequest": { "type": "object", "required": [ "refresh_token" @@ -5433,13 +5531,13 @@ } } }, - "GAMERS-BE_internal_banner_application_dto.BannerListResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerListResponse": { "type": "object", "properties": { "banners": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_banner_application_dto.BannerResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerResponse" } }, "total": { @@ -5447,7 +5545,7 @@ } } }, - "GAMERS-BE_internal_banner_application_dto.BannerResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerResponse": { "type": "object", "properties": { "created_at": { @@ -5476,7 +5574,7 @@ } } }, - "GAMERS-BE_internal_banner_application_dto.CreateBannerRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.CreateBannerRequest": { "type": "object", "required": [ "image_key" @@ -5500,7 +5598,7 @@ } } }, - "GAMERS-BE_internal_banner_application_dto.UpdateBannerRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.UpdateBannerRequest": { "type": "object", "properties": { "display_order": { @@ -5521,7 +5619,7 @@ } } }, - "GAMERS-BE_internal_comment_application_dto.AuthorResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.AuthorResponse": { "type": "object", "properties": { "avatar": { @@ -5538,11 +5636,11 @@ } } }, - "GAMERS-BE_internal_comment_application_dto.CommentResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CommentResponse": { "type": "object", "properties": { "author": { - "$ref": "#/definitions/GAMERS-BE_internal_comment_application_dto.AuthorResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.AuthorResponse" }, "comment_id": { "type": "integer" @@ -5561,7 +5659,7 @@ } } }, - "GAMERS-BE_internal_comment_application_dto.CreateCommentRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CreateCommentRequest": { "type": "object", "required": [ "content" @@ -5573,7 +5671,7 @@ } } }, - "GAMERS-BE_internal_comment_application_dto.UpdateCommentRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.UpdateCommentRequest": { "type": "object", "required": [ "content" @@ -5585,35 +5683,35 @@ } } }, - "GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleRequest": { "type": "object", "required": [ "member_type" ], "properties": { "member_type": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.MemberType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.MemberType" } } }, - "GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleResponse": { "type": "object", "properties": { "contest_id": { "type": "integer" }, "leader_type": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.LeaderType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.LeaderType" }, "member_type": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.MemberType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.MemberType" }, "user_id": { "type": "integer" } } }, - "GAMERS-BE_internal_contest_application_dto.ContestResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse": { "type": "object", "properties": { "auto_start": { @@ -5623,10 +5721,10 @@ "type": "integer" }, "contest_status": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.ContestStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestStatus" }, "contest_type": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.ContestType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestType" }, "created_at": { "type": "string" @@ -5647,7 +5745,7 @@ "type": "integer" }, "game_type": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameType" }, "max_team_count": { "type": "integer" @@ -5672,7 +5770,7 @@ } } }, - "GAMERS-BE_internal_contest_application_dto.CreateContestRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.CreateContestRequest": { "type": "object", "required": [ "contest_type", @@ -5683,7 +5781,7 @@ "type": "boolean" }, "contest_type": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.ContestType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestType" }, "description": { "type": "string" @@ -5701,7 +5799,7 @@ "type": "integer" }, "game_type": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameType" }, "max_team_count": { "type": "integer" @@ -5723,7 +5821,7 @@ } } }, - "GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse": { "type": "object", "properties": { "message": { @@ -5734,17 +5832,37 @@ } } }, - "GAMERS-BE_internal_contest_application_dto.UpdateContestRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ThumbnailUploadResponse": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "mime_type": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "uploaded_at": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.UpdateContestRequest": { "type": "object", "properties": { "auto_start": { "type": "boolean" }, "contest_status": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.ContestStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestStatus" }, "contest_type": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_domain.ContestType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestType" }, "description": { "type": "string" @@ -5762,7 +5880,7 @@ "type": "integer" }, "game_type": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameType" }, "max_team_count": { "type": "integer" @@ -5784,11 +5902,11 @@ } } }, - "GAMERS-BE_internal_contest_application_dto.UserContestStatusResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.UserContestStatusResponse": { "type": "object", "properties": { "application_status": { - "$ref": "#/definitions/GAMERS-BE_internal_contest_application_port.ApplicationStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_port.ApplicationStatus" }, "has_applied": { "type": "boolean" @@ -5804,7 +5922,7 @@ } } }, - "GAMERS-BE_internal_contest_application_port.ApplicationStatus": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_port.ApplicationStatus": { "type": "string", "enum": [ "PENDING", @@ -5817,7 +5935,7 @@ "ApplicationStatusRejected" ] }, - "GAMERS-BE_internal_contest_domain.ContestStatus": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestStatus": { "type": "string", "enum": [ "PENDING", @@ -5832,7 +5950,7 @@ "ContestStatusCancelled" ] }, - "GAMERS-BE_internal_contest_domain.ContestType": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestType": { "type": "string", "enum": [ "TOURNAMENT", @@ -5845,7 +5963,7 @@ "ContestTypeCasual" ] }, - "GAMERS-BE_internal_contest_domain.LeaderType": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.LeaderType": { "type": "string", "enum": [ "LEADER", @@ -5856,7 +5974,7 @@ "LeaderTypeMember" ] }, - "GAMERS-BE_internal_contest_domain.MemberType": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.MemberType": { "type": "string", "enum": [ "STAFF", @@ -5867,7 +5985,7 @@ "MemberTypeNormal" ] }, - "GAMERS-BE_internal_discord_application_dto.DiscordChannel": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordChannel": { "type": "object", "properties": { "guild_id": { @@ -5890,7 +6008,7 @@ } } }, - "GAMERS-BE_internal_discord_application_dto.DiscordGuild": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordGuild": { "type": "object", "properties": { "icon": { @@ -5910,7 +6028,18 @@ } } }, - "GAMERS-BE_internal_game_application_dto.CachedMemberResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.AcceptInviteRequest": { + "type": "object", + "required": [ + "team_id" + ], + "properties": { + "team_id": { + "type": "integer" + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CachedMemberResponse": { "type": "object", "properties": { "contest_id": { @@ -5936,11 +6065,11 @@ } } }, - "GAMERS-BE_internal_game_application_dto.ContestResultResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ContestResultResponse": { "type": "object", "properties": { "champion": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.TeamSummary" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TeamSummary" }, "contest_id": { "type": "integer" @@ -5951,7 +6080,7 @@ "rounds": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.RoundResult" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.RoundResult" } }, "title": { @@ -5962,7 +6091,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.CreateGameRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateGameRequest": { "type": "object", "required": [ "contest_id", @@ -5976,14 +6105,14 @@ "type": "string" }, "game_team_type": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameTeamType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameTeamType" }, "started_at": { "type": "string" } } }, - "GAMERS-BE_internal_game_application_dto.CreateGameTeamRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateGameTeamRequest": { "type": "object", "required": [ "game_id", @@ -6001,7 +6130,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.CreateTeamRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateTeamRequest": { "type": "object", "properties": { "team_name": { @@ -6009,7 +6138,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.GameResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse": { "type": "object", "properties": { "contest_id": { @@ -6025,10 +6154,10 @@ "type": "integer" }, "game_status": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameStatus" }, "game_team_type": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameTeamType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameTeamType" }, "modified_at": { "type": "string" @@ -6038,7 +6167,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.GameResult": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResult": { "type": "object", "properties": { "detection_status": { @@ -6054,17 +6183,17 @@ "type": "integer" }, "match_result": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.MatchResultSummary" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultSummary" }, "teams": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameTeamResult" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResult" } } } }, - "GAMERS-BE_internal_game_application_dto.GameTeamResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResponse": { "type": "object", "properties": { "game_id": { @@ -6081,7 +6210,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.GameTeamResult": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResult": { "type": "object", "properties": { "grade": { @@ -6095,7 +6224,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.InviteMemberRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.InviteMemberRequest": { "type": "object", "required": [ "user_id" @@ -6106,7 +6235,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.KickMemberRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.KickMemberRequest": { "type": "object", "required": [ "user_id" @@ -6117,7 +6246,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.ManualResultRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ManualResultRequest": { "type": "object", "required": [ "loserScore", @@ -6139,11 +6268,11 @@ } } }, - "GAMERS-BE_internal_game_application_dto.MatchResultResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultResponse": { "type": "object", "properties": { "detectionStatus": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.DetectionStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.DetectionStatus" }, "gameDuration": { "type": "integer" @@ -6169,7 +6298,7 @@ "playerStats": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.PlayerStatResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.PlayerStatResponse" } }, "roundsPlayed": { @@ -6186,7 +6315,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.MatchResultSummary": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultSummary": { "type": "object", "properties": { "loser_score": { @@ -6206,7 +6335,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.PlayerStatResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.PlayerStatResponse": { "type": "object", "properties": { "agentName": { @@ -6241,13 +6370,24 @@ } } }, - "GAMERS-BE_internal_game_application_dto.RoundResult": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.RejectInviteRequest": { + "type": "object", + "required": [ + "team_id" + ], + "properties": { + "team_id": { + "type": "integer" + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.RoundResult": { "type": "object", "properties": { "games": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_game_application_dto.GameResult" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResult" } }, "round": { @@ -6258,7 +6398,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.ScheduleGameRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameRequest": { "type": "object", "required": [ "scheduledStartTime" @@ -6272,14 +6412,14 @@ } } }, - "GAMERS-BE_internal_game_application_dto.ScheduleGameResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameResponse": { "type": "object", "properties": { "contestId": { "type": "integer" }, "detectionStatus": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.DetectionStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.DetectionStatus" }, "detectionWindowMinutes": { "type": "integer" @@ -6288,7 +6428,7 @@ "type": "integer" }, "gameStatus": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameStatus" }, "matchNumber": { "type": "integer" @@ -6301,7 +6441,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.TeamInviteResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TeamInviteResponse": { "type": "object", "properties": { "contest_id": { @@ -6321,10 +6461,13 @@ }, "status": { "type": "string" + }, + "team_id": { + "type": "integer" } } }, - "GAMERS-BE_internal_game_application_dto.TeamSummary": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TeamSummary": { "type": "object", "properties": { "team_id": { @@ -6335,7 +6478,7 @@ } } }, - "GAMERS-BE_internal_game_application_dto.TransferLeadershipRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TransferLeadershipRequest": { "type": "object", "required": [ "new_leader_user_id" @@ -6346,24 +6489,24 @@ } } }, - "GAMERS-BE_internal_game_application_dto.UpdateGameRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.UpdateGameRequest": { "type": "object", "properties": { "ended_at": { "type": "string" }, "game_status": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameStatus" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameStatus" }, "game_team_type": { - "$ref": "#/definitions/GAMERS-BE_internal_game_domain.GameTeamType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameTeamType" }, "started_at": { "type": "string" } } }, - "GAMERS-BE_internal_game_domain.DetectionStatus": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.DetectionStatus": { "type": "string", "enum": [ "NONE", @@ -6380,7 +6523,7 @@ "DetectionStatusManual" ] }, - "GAMERS-BE_internal_game_domain.GameStatus": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameStatus": { "type": "string", "enum": [ "PENDING", @@ -6395,7 +6538,7 @@ "GameStatusCancelled" ] }, - "GAMERS-BE_internal_game_domain.GameTeamType": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameTeamType": { "type": "string", "enum": [ "SINGLE", @@ -6426,7 +6569,7 @@ "GameTeamTypeHurupa" ] }, - "GAMERS-BE_internal_game_domain.GameType": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameType": { "type": "string", "enum": [ "VALORANT", @@ -6437,7 +6580,7 @@ "GameTypeLOL" ] }, - "GAMERS-BE_internal_global_common_dto.PaginationResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse": { "type": "object", "properties": { "data": {}, @@ -6455,7 +6598,7 @@ } } }, - "GAMERS-BE_internal_global_response.Response": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response": { "type": "object", "properties": { "data": {}, @@ -6467,13 +6610,13 @@ } } }, - "GAMERS-BE_internal_notification_application_dto.NotificationListResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_notification_application_dto.NotificationListResponse": { "type": "object", "properties": { "notifications": { "type": "array", "items": { - "$ref": "#/definitions/GAMERS-BE_internal_notification_application_dto.NotificationResponse" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_notification_application_dto.NotificationResponse" } }, "total": { @@ -6484,7 +6627,7 @@ } } }, - "GAMERS-BE_internal_notification_application_dto.NotificationResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_notification_application_dto.NotificationResponse": { "type": "object", "properties": { "created_at": { @@ -6507,11 +6650,11 @@ "type": "string" }, "type": { - "$ref": "#/definitions/GAMERS-BE_internal_notification_domain.NotificationType" + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_notification_domain.NotificationType" } } }, - "GAMERS-BE_internal_notification_domain.NotificationType": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_notification_domain.NotificationType": { "type": "string", "enum": [ "TEAM_INVITE_RECEIVED", @@ -6528,7 +6671,7 @@ "NotificationTypeApplicationRejected" ] }, - "GAMERS-BE_internal_point_application_dto.CreateValorantScoreTableDto": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.CreateValorantScoreTableDto": { "type": "object", "required": [ "ascendant_1", @@ -6635,7 +6778,7 @@ } } }, - "GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse": { "type": "object", "properties": { "ascendant_1": { @@ -6718,7 +6861,7 @@ } } }, - "GAMERS-BE_internal_storage_application_dto.UploadResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_storage_application_dto.UploadResponse": { "type": "object", "properties": { "key": { @@ -6738,7 +6881,7 @@ } } }, - "GAMERS-BE_internal_user_application_dto.CreateUserRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.CreateUserRequest": { "type": "object", "required": [ "email", @@ -6767,7 +6910,7 @@ } } }, - "GAMERS-BE_internal_user_application_dto.MyUserResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.MyUserResponse": { "type": "object", "properties": { "avatar": { @@ -6829,7 +6972,7 @@ } } }, - "GAMERS-BE_internal_user_application_dto.UpdateUserInfoRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UpdateUserInfoRequest": { "type": "object", "properties": { "avatar": { @@ -6846,7 +6989,7 @@ } } }, - "GAMERS-BE_internal_user_application_dto.UpdateUserRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UpdateUserRequest": { "type": "object", "required": [ "password" @@ -6857,7 +7000,7 @@ } } }, - "GAMERS-BE_internal_user_application_dto.UserResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UserResponse": { "type": "object", "properties": { "created_at": { @@ -6874,7 +7017,7 @@ } } }, - "GAMERS-BE_internal_valorant_application_dto.ContestPointResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ContestPointResponse": { "type": "object", "properties": { "current_tier_patched": { @@ -6919,7 +7062,7 @@ } } }, - "GAMERS-BE_internal_valorant_application_dto.RegisterValorantRequest": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.RegisterValorantRequest": { "type": "object", "required": [ "region", @@ -6951,7 +7094,7 @@ } } }, - "GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse": { + "github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse": { "type": "object", "properties": { "current_tier": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index afeb7b2..1968772 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,13 +1,13 @@ basePath: /api definitions: - GAMERS-BE_internal_auth_application_dto.LoginRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.LoginRequest: properties: email: type: string password: type: string type: object - GAMERS-BE_internal_auth_application_dto.LogoutRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.LogoutRequest: properties: access_token: type: string @@ -17,23 +17,23 @@ definitions: - access_token - refresh_token type: object - GAMERS-BE_internal_auth_application_dto.RefreshRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.RefreshRequest: properties: refresh_token: type: string required: - refresh_token type: object - GAMERS-BE_internal_banner_application_dto.BannerListResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerListResponse: properties: banners: items: - $ref: '#/definitions/GAMERS-BE_internal_banner_application_dto.BannerResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerResponse' type: array total: type: integer type: object - GAMERS-BE_internal_banner_application_dto.BannerResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerResponse: properties: created_at: type: string @@ -52,7 +52,7 @@ definitions: title: type: string type: object - GAMERS-BE_internal_banner_application_dto.CreateBannerRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.CreateBannerRequest: properties: display_order: minimum: 0 @@ -68,7 +68,7 @@ definitions: required: - image_key type: object - GAMERS-BE_internal_banner_application_dto.UpdateBannerRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.UpdateBannerRequest: properties: display_order: minimum: 0 @@ -82,7 +82,7 @@ definitions: title: type: string type: object - GAMERS-BE_internal_comment_application_dto.AuthorResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.AuthorResponse: properties: avatar: type: string @@ -93,10 +93,10 @@ definitions: username: type: string type: object - GAMERS-BE_internal_comment_application_dto.CommentResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CommentResponse: properties: author: - $ref: '#/definitions/GAMERS-BE_internal_comment_application_dto.AuthorResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.AuthorResponse' comment_id: type: integer content: @@ -108,7 +108,7 @@ definitions: modified_at: type: string type: object - GAMERS-BE_internal_comment_application_dto.CreateCommentRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CreateCommentRequest: properties: content: maxLength: 255 @@ -116,7 +116,7 @@ definitions: required: - content type: object - GAMERS-BE_internal_comment_application_dto.UpdateCommentRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.UpdateCommentRequest: properties: content: maxLength: 255 @@ -124,34 +124,34 @@ definitions: required: - content type: object - GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleRequest: properties: member_type: - $ref: '#/definitions/GAMERS-BE_internal_contest_domain.MemberType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.MemberType' required: - member_type type: object - GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleResponse: properties: contest_id: type: integer leader_type: - $ref: '#/definitions/GAMERS-BE_internal_contest_domain.LeaderType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.LeaderType' member_type: - $ref: '#/definitions/GAMERS-BE_internal_contest_domain.MemberType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.MemberType' user_id: type: integer type: object - GAMERS-BE_internal_contest_application_dto.ContestResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse: properties: auto_start: type: boolean contest_id: type: integer contest_status: - $ref: '#/definitions/GAMERS-BE_internal_contest_domain.ContestStatus' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestStatus' contest_type: - $ref: '#/definitions/GAMERS-BE_internal_contest_domain.ContestType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestType' created_at: type: string description: @@ -165,7 +165,7 @@ definitions: game_point_table_id: type: integer game_type: - $ref: '#/definitions/GAMERS-BE_internal_game_domain.GameType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameType' max_team_count: type: integer modified_at: @@ -181,12 +181,12 @@ definitions: total_team_member: type: integer type: object - GAMERS-BE_internal_contest_application_dto.CreateContestRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.CreateContestRequest: properties: auto_start: type: boolean contest_type: - $ref: '#/definitions/GAMERS-BE_internal_contest_domain.ContestType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestType' description: type: string discord_guild_id: @@ -198,7 +198,7 @@ definitions: game_point_table_id: type: integer game_type: - $ref: '#/definitions/GAMERS-BE_internal_game_domain.GameType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameType' max_team_count: type: integer started_at: @@ -215,21 +215,34 @@ definitions: - contest_type - title type: object - GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse: properties: message: type: string oauth_url: type: string type: object - GAMERS-BE_internal_contest_application_dto.UpdateContestRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ThumbnailUploadResponse: + properties: + key: + type: string + mime_type: + type: string + size: + type: integer + uploaded_at: + type: string + url: + type: string + type: object + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.UpdateContestRequest: properties: auto_start: type: boolean contest_status: - $ref: '#/definitions/GAMERS-BE_internal_contest_domain.ContestStatus' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestStatus' contest_type: - $ref: '#/definitions/GAMERS-BE_internal_contest_domain.ContestType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestType' description: type: string discord_guild_id: @@ -241,7 +254,7 @@ definitions: game_point_table_id: type: integer game_type: - $ref: '#/definitions/GAMERS-BE_internal_game_domain.GameType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameType' max_team_count: type: integer started_at: @@ -255,10 +268,10 @@ definitions: total_team_member: type: integer type: object - GAMERS-BE_internal_contest_application_dto.UserContestStatusResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.UserContestStatusResponse: properties: application_status: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_port.ApplicationStatus' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_port.ApplicationStatus' has_applied: type: boolean is_leader: @@ -268,7 +281,7 @@ definitions: member_type: type: string type: object - GAMERS-BE_internal_contest_application_port.ApplicationStatus: + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_port.ApplicationStatus: enum: - PENDING - ACCEPTED @@ -278,7 +291,7 @@ definitions: - ApplicationStatusPending - ApplicationStatusAccepted - ApplicationStatusRejected - GAMERS-BE_internal_contest_domain.ContestStatus: + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestStatus: enum: - PENDING - ACTIVE @@ -290,7 +303,7 @@ definitions: - ContestStatusActive - ContestStatusFinished - ContestStatusCancelled - GAMERS-BE_internal_contest_domain.ContestType: + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ContestType: enum: - TOURNAMENT - LEAGUE @@ -300,7 +313,7 @@ definitions: - ContestTypeTournament - ContestTypeLeague - ContestTypeCasual - GAMERS-BE_internal_contest_domain.LeaderType: + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.LeaderType: enum: - LEADER - MEMBER @@ -308,7 +321,7 @@ definitions: x-enum-varnames: - LeaderTypeLeader - LeaderTypeMember - GAMERS-BE_internal_contest_domain.MemberType: + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.MemberType: enum: - STAFF - NORMAL @@ -316,7 +329,7 @@ definitions: x-enum-varnames: - MemberTypeStaff - MemberTypeNormal - GAMERS-BE_internal_discord_application_dto.DiscordChannel: + github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordChannel: properties: guild_id: type: string @@ -331,7 +344,7 @@ definitions: type: type: integer type: object - GAMERS-BE_internal_discord_application_dto.DiscordGuild: + github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordGuild: properties: icon: type: string @@ -344,7 +357,14 @@ definitions: permissions: type: string type: object - GAMERS-BE_internal_game_application_dto.CachedMemberResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.AcceptInviteRequest: + properties: + team_id: + type: integer + required: + - team_id + type: object + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CachedMemberResponse: properties: contest_id: type: integer @@ -361,38 +381,38 @@ definitions: username: type: string type: object - GAMERS-BE_internal_game_application_dto.ContestResultResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ContestResultResponse: properties: champion: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.TeamSummary' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TeamSummary' contest_id: type: integer contest_status: type: string rounds: items: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.RoundResult' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.RoundResult' type: array title: type: string total_rounds: type: integer type: object - GAMERS-BE_internal_game_application_dto.CreateGameRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateGameRequest: properties: contest_id: type: integer ended_at: type: string game_team_type: - $ref: '#/definitions/GAMERS-BE_internal_game_domain.GameTeamType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameTeamType' started_at: type: string required: - contest_id - game_team_type type: object - GAMERS-BE_internal_game_application_dto.CreateGameTeamRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateGameTeamRequest: properties: game_id: type: integer @@ -404,12 +424,12 @@ definitions: - game_id - team_id type: object - GAMERS-BE_internal_game_application_dto.CreateTeamRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateTeamRequest: properties: team_name: type: string type: object - GAMERS-BE_internal_game_application_dto.GameResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse: properties: contest_id: type: integer @@ -420,15 +440,15 @@ definitions: game_id: type: integer game_status: - $ref: '#/definitions/GAMERS-BE_internal_game_domain.GameStatus' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameStatus' game_team_type: - $ref: '#/definitions/GAMERS-BE_internal_game_domain.GameTeamType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameTeamType' modified_at: type: string started_at: type: string type: object - GAMERS-BE_internal_game_application_dto.GameResult: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResult: properties: detection_status: type: string @@ -439,13 +459,13 @@ definitions: match_number: type: integer match_result: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.MatchResultSummary' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultSummary' teams: items: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.GameTeamResult' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResult' type: array type: object - GAMERS-BE_internal_game_application_dto.GameTeamResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResponse: properties: game_id: type: integer @@ -456,7 +476,7 @@ definitions: team_id: type: integer type: object - GAMERS-BE_internal_game_application_dto.GameTeamResult: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResult: properties: grade: type: integer @@ -465,21 +485,21 @@ definitions: team_name: type: string type: object - GAMERS-BE_internal_game_application_dto.InviteMemberRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.InviteMemberRequest: properties: user_id: type: integer required: - user_id type: object - GAMERS-BE_internal_game_application_dto.KickMemberRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.KickMemberRequest: properties: user_id: type: integer required: - user_id type: object - GAMERS-BE_internal_game_application_dto.ManualResultRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ManualResultRequest: properties: loserScore: type: integer @@ -494,10 +514,10 @@ definitions: - winnerScore - winnerTeamId type: object - GAMERS-BE_internal_game_application_dto.MatchResultResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultResponse: properties: detectionStatus: - $ref: '#/definitions/GAMERS-BE_internal_game_domain.DetectionStatus' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.DetectionStatus' gameDuration: type: integer gameId: @@ -514,7 +534,7 @@ definitions: type: integer playerStats: items: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.PlayerStatResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.PlayerStatResponse' type: array roundsPlayed: type: integer @@ -525,7 +545,7 @@ definitions: winnerTeamId: type: integer type: object - GAMERS-BE_internal_game_application_dto.MatchResultSummary: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultSummary: properties: loser_score: type: integer @@ -538,7 +558,7 @@ definitions: winner_team_id: type: integer type: object - GAMERS-BE_internal_game_application_dto.PlayerStatResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.PlayerStatResponse: properties: agentName: type: string @@ -561,18 +581,25 @@ definitions: userId: type: integer type: object - GAMERS-BE_internal_game_application_dto.RoundResult: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.RejectInviteRequest: + properties: + team_id: + type: integer + required: + - team_id + type: object + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.RoundResult: properties: games: items: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.GameResult' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResult' type: array round: type: integer round_name: type: string type: object - GAMERS-BE_internal_game_application_dto.ScheduleGameRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameRequest: properties: detectionWindowMinutes: type: integer @@ -581,18 +608,18 @@ definitions: required: - scheduledStartTime type: object - GAMERS-BE_internal_game_application_dto.ScheduleGameResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameResponse: properties: contestId: type: integer detectionStatus: - $ref: '#/definitions/GAMERS-BE_internal_game_domain.DetectionStatus' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.DetectionStatus' detectionWindowMinutes: type: integer gameId: type: integer gameStatus: - $ref: '#/definitions/GAMERS-BE_internal_game_domain.GameStatus' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameStatus' matchNumber: type: integer round: @@ -600,7 +627,7 @@ definitions: scheduledStartTime: type: string type: object - GAMERS-BE_internal_game_application_dto.TeamInviteResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TeamInviteResponse: properties: contest_id: type: integer @@ -614,33 +641,35 @@ definitions: type: string status: type: string + team_id: + type: integer type: object - GAMERS-BE_internal_game_application_dto.TeamSummary: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TeamSummary: properties: team_id: type: integer team_name: type: string type: object - GAMERS-BE_internal_game_application_dto.TransferLeadershipRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TransferLeadershipRequest: properties: new_leader_user_id: type: integer required: - new_leader_user_id type: object - GAMERS-BE_internal_game_application_dto.UpdateGameRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.UpdateGameRequest: properties: ended_at: type: string game_status: - $ref: '#/definitions/GAMERS-BE_internal_game_domain.GameStatus' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameStatus' game_team_type: - $ref: '#/definitions/GAMERS-BE_internal_game_domain.GameTeamType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameTeamType' started_at: type: string type: object - GAMERS-BE_internal_game_domain.DetectionStatus: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.DetectionStatus: enum: - NONE - DETECTING @@ -654,7 +683,7 @@ definitions: - DetectionStatusDetected - DetectionStatusFailed - DetectionStatusManual - GAMERS-BE_internal_game_domain.GameStatus: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameStatus: enum: - PENDING - ACTIVE @@ -666,7 +695,7 @@ definitions: - GameStatusActive - GameStatusFinished - GameStatusCancelled - GAMERS-BE_internal_game_domain.GameTeamType: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameTeamType: enum: - SINGLE - DUO @@ -692,7 +721,7 @@ definitions: - GameTeamTypeTrio - GameTeamTypeFull - GameTeamTypeHurupa - GAMERS-BE_internal_game_domain.GameType: + github_com_FOR-GAMERS_GAMERS-BE_internal_game_domain.GameType: enum: - VALORANT - LOL @@ -700,7 +729,7 @@ definitions: x-enum-varnames: - GameTypeValorant - GameTypeLOL - GAMERS-BE_internal_global_common_dto.PaginationResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse: properties: data: {} page: @@ -712,7 +741,7 @@ definitions: total_pages: type: integer type: object - GAMERS-BE_internal_global_response.Response: + github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response: properties: data: {} message: @@ -720,18 +749,18 @@ definitions: status: type: integer type: object - GAMERS-BE_internal_notification_application_dto.NotificationListResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_notification_application_dto.NotificationListResponse: properties: notifications: items: - $ref: '#/definitions/GAMERS-BE_internal_notification_application_dto.NotificationResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_notification_application_dto.NotificationResponse' type: array total: type: integer unread_count: type: integer type: object - GAMERS-BE_internal_notification_application_dto.NotificationResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_notification_application_dto.NotificationResponse: properties: created_at: type: string @@ -747,9 +776,9 @@ definitions: title: type: string type: - $ref: '#/definitions/GAMERS-BE_internal_notification_domain.NotificationType' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_notification_domain.NotificationType' type: object - GAMERS-BE_internal_notification_domain.NotificationType: + github_com_FOR-GAMERS_GAMERS-BE_internal_notification_domain.NotificationType: enum: - TEAM_INVITE_RECEIVED - TEAM_INVITE_ACCEPTED @@ -763,7 +792,7 @@ definitions: - NotificationTypeTeamInviteRejected - NotificationTypeApplicationAccepted - NotificationTypeApplicationRejected - GAMERS-BE_internal_point_application_dto.CreateValorantScoreTableDto: + github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.CreateValorantScoreTableDto: properties: ascendant_1: type: integer @@ -842,7 +871,7 @@ definitions: - silver_2 - silver_3 type: object - GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse: properties: ascendant_1: type: integer @@ -897,7 +926,7 @@ definitions: silver_3: type: integer type: object - GAMERS-BE_internal_storage_application_dto.UploadResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_storage_application_dto.UploadResponse: properties: key: type: string @@ -910,7 +939,7 @@ definitions: url: type: string type: object - GAMERS-BE_internal_user_application_dto.CreateUserRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.CreateUserRequest: properties: avatar: type: string @@ -930,7 +959,7 @@ definitions: - tag - username type: object - GAMERS-BE_internal_user_application_dto.MyUserResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.MyUserResponse: properties: avatar: type: string @@ -971,7 +1000,7 @@ definitions: valorant_updated_at: type: string type: object - GAMERS-BE_internal_user_application_dto.UpdateUserInfoRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UpdateUserInfoRequest: properties: avatar: type: string @@ -982,14 +1011,14 @@ definitions: username: type: string type: object - GAMERS-BE_internal_user_application_dto.UpdateUserRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UpdateUserRequest: properties: password: type: string required: - password type: object - GAMERS-BE_internal_user_application_dto.UserResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UserResponse: properties: created_at: type: string @@ -1000,7 +1029,7 @@ definitions: user_id: type: integer type: object - GAMERS-BE_internal_valorant_application_dto.ContestPointResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ContestPointResponse: properties: current_tier_patched: example: Diamond 1 @@ -1033,7 +1062,7 @@ definitions: example: 123 type: integer type: object - GAMERS-BE_internal_valorant_application_dto.RegisterValorantRequest: + github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.RegisterValorantRequest: properties: region: enum: @@ -1058,7 +1087,7 @@ definitions: - riot_name - riot_tag type: object - GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse: + github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse: properties: current_tier: example: 21 @@ -1117,19 +1146,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_banner_application_dto.BannerListResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerListResponse' type: object "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get all banners (Admin) @@ -1145,7 +1174,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_banner_application_dto.CreateBannerRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.CreateBannerRequest' produces: - application/json responses: @@ -1153,27 +1182,27 @@ paths: description: Created schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_banner_application_dto.BannerResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "409": description: Maximum banner limit exceeded schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Create a new banner (Admin) @@ -1196,15 +1225,15 @@ paths: "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Delete a banner (Admin) @@ -1225,7 +1254,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_banner_application_dto.UpdateBannerRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.UpdateBannerRequest' produces: - application/json responses: @@ -1233,27 +1262,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_banner_application_dto.BannerResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Update a banner (Admin) @@ -1270,22 +1299,22 @@ paths: name: login required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_auth_application_dto.LoginRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.LoginRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: User login tags: - auth @@ -1300,7 +1329,7 @@ paths: name: logout required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_auth_application_dto.LogoutRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.LogoutRequest' produces: - application/json responses: @@ -1309,7 +1338,7 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Logout tags: - auth @@ -1324,22 +1353,22 @@ paths: name: refresh required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_auth_application_dto.RefreshRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_auth_application_dto.RefreshRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Refresh access token tags: - auth @@ -1354,10 +1383,10 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_banner_application_dto.BannerListResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_banner_application_dto.BannerListResponse' type: object summary: Get active banners tags: @@ -1395,15 +1424,15 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_global_common_dto.PaginationResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get all contests with pagination tags: - contests @@ -1417,7 +1446,7 @@ paths: name: contest required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_dto.CreateContestRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.CreateContestRequest' produces: - application/json responses: @@ -1425,27 +1454,27 @@ paths: description: Created schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse' type: object security: - BearerAuth: [] @@ -1469,15 +1498,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get pending applications for a contest @@ -1499,28 +1528,28 @@ paths: "201": description: Created schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.DiscordLinkRequiredResponse' type: object "409": description: Conflict schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Request to participate in a contest @@ -1548,15 +1577,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Accept a contest application @@ -1584,15 +1613,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Reject a contest application @@ -1615,19 +1644,19 @@ paths: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Cancel my pending application @@ -1650,19 +1679,19 @@ paths: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get my application status @@ -1685,23 +1714,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Withdraw from a contest @@ -1727,19 +1756,19 @@ paths: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Manually trigger match detection @@ -1767,15 +1796,15 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.ScheduleGameResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameResponse' type: object "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get match detection status tags: - games @@ -1801,15 +1830,15 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.MatchResultResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultResponse' type: object "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get match result tags: - games @@ -1835,7 +1864,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.ManualResultRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ManualResultRequest' produces: - application/json responses: @@ -1843,23 +1872,23 @@ paths: description: Created schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.MatchResultResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Submit manual game result @@ -1887,15 +1916,15 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.MatchResultResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.MatchResultResponse' type: object "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get match result with player stats tags: - games @@ -1922,7 +1951,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.ScheduleGameRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameRequest' produces: - application/json responses: @@ -1930,23 +1959,23 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.ScheduleGameResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ScheduleGameResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Set game scheduled start time @@ -1993,23 +2022,23 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_global_common_dto.PaginationResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get contest members with pagination @@ -2037,7 +2066,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleRequest' produces: - application/json responses: @@ -2045,27 +2074,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ChangeMemberRoleResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Change a member's role (Leader only) @@ -2090,23 +2119,23 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_dto.UserContestStatusResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.UserContestStatusResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get my status in a contest @@ -2131,15 +2160,15 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Delete a contest @@ -2162,19 +2191,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get a contest by ID tags: - contests @@ -2193,7 +2222,7 @@ paths: name: contest required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_dto.UpdateContestRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.UpdateContestRequest' produces: - application/json responses: @@ -2201,23 +2230,23 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Update a contest @@ -2262,19 +2291,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_global_common_dto.PaginationResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get comments for a contest tags: - contest-comments @@ -2293,7 +2322,7 @@ paths: name: comment required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_comment_application_dto.CreateCommentRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CreateCommentRequest' produces: - application/json responses: @@ -2301,23 +2330,23 @@ paths: description: Created schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_comment_application_dto.CommentResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CommentResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Create a new comment @@ -2345,23 +2374,23 @@ paths: "204": description: No Content schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Delete a comment @@ -2389,19 +2418,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_comment_application_dto.CommentResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CommentResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get a comment by ID tags: - contest-comments @@ -2425,7 +2454,7 @@ paths: name: comment required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_comment_application_dto.UpdateCommentRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.UpdateCommentRequest' produces: - application/json responses: @@ -2433,27 +2462,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_comment_application_dto.CommentResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_comment_application_dto.CommentResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Update a comment @@ -2477,17 +2506,17 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: items: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse' type: array type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get all games for a contest tags: - games @@ -2507,19 +2536,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.ContestResultResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.ContestResultResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get tournament contest result tags: - games @@ -2543,27 +2572,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Start a contest @@ -2588,27 +2617,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_contest_application_dto.ContestResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ContestResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Stop a contest @@ -2633,19 +2662,19 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Delete the entire team @@ -2668,19 +2697,19 @@ paths: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get team information @@ -2700,26 +2729,26 @@ paths: in: body name: request schema: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.CreateTeamRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateTeamRequest' produces: - application/json responses: "201": description: Created schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "409": description: Conflict schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Create a team for a contest @@ -2743,23 +2772,23 @@ paths: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Finalize the team @@ -2782,7 +2811,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.InviteMemberRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.InviteMemberRequest' produces: - application/json responses: @@ -2790,31 +2819,31 @@ paths: description: Created schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.TeamInviteResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TeamInviteResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "409": description: Conflict schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Invite a user to the team @@ -2831,6 +2860,12 @@ paths: name: id required: true type: integer + - description: Accept invite request + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.AcceptInviteRequest' produces: - application/json responses: @@ -2838,27 +2873,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.CachedMemberResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CachedMemberResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "409": description: Conflict schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Accept a team invitation @@ -2875,6 +2910,12 @@ paths: name: id required: true type: integer + - description: Reject invite request + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.RejectInviteRequest' produces: - application/json responses: @@ -2883,15 +2924,15 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Reject a team invitation @@ -2913,7 +2954,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.KickMemberRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.KickMemberRequest' produces: - application/json responses: @@ -2922,19 +2963,19 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Kick a member from the team @@ -2960,19 +3001,19 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Leave the team @@ -2982,7 +3023,8 @@ paths: get: consumes: - application/json - description: Get all members of a contest team + description: Get all members of the contest team that the authenticated user + belongs to parameters: - description: Contest ID in: path @@ -2996,25 +3038,25 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: items: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.CachedMemberResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CachedMemberResponse' type: array type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get all team members @@ -3024,7 +3066,8 @@ paths: get: consumes: - application/json - description: Get a specific member of a contest team by user ID + description: Get a specific member of the authenticated user's team by user + ID parameters: - description: Contest ID in: path @@ -3043,23 +3086,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.CachedMemberResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CachedMemberResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' + "403": + description: Forbidden + schema: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get a specific team member @@ -3081,35 +3128,81 @@ paths: name: request required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.TransferLeadershipRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.TransferLeadershipRequest' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Transfer leadership to another member tags: - teams + /api/contests/{id}/thumbnail: + post: + consumes: + - multipart/form-data + description: 'Upload a thumbnail image for a contest. Maximum file size is 5MB. + Allowed formats: jpeg, png, webp. Only the contest leader can upload.' + parameters: + - description: Contest ID + in: path + name: id + required: true + type: integer + - description: Image file (max 5MB, jpeg/png/webp) + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "201": + description: Created + schema: + allOf: + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' + - properties: + data: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ThumbnailUploadResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' + "403": + description: Forbidden + schema: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' + security: + - BearerAuth: [] + summary: Upload a contest thumbnail image + tags: + - contests /api/contests/{id}/valorant-point: get: consumes: @@ -3134,23 +3227,23 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_valorant_application_dto.ContestPointResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ContestPointResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get contest point @@ -3191,19 +3284,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_global_common_dto.PaginationResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_common_dto.PaginationResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get contests I have joined @@ -3220,7 +3313,7 @@ paths: name: gameTeam required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.CreateGameTeamRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateGameTeamRequest' produces: - application/json responses: @@ -3228,27 +3321,27 @@ paths: description: Created schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.GameTeamResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "409": description: Conflict schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Create a new game-team relationship @@ -3273,15 +3366,15 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Delete a game-team relationship @@ -3304,19 +3397,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.GameTeamResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get a game-team relationship by ID tags: - game-teams @@ -3331,7 +3424,7 @@ paths: name: game required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.CreateGameRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.CreateGameRequest' produces: - application/json responses: @@ -3339,19 +3432,19 @@ paths: description: Created schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Create a new game @@ -3376,19 +3469,19 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Delete a game @@ -3411,19 +3504,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get a game by ID tags: - games @@ -3442,7 +3535,7 @@ paths: name: game required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.UpdateGameRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.UpdateGameRequest' produces: - application/json responses: @@ -3450,27 +3543,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Update a game @@ -3494,27 +3587,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Cancel a game @@ -3538,27 +3631,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Finish a game @@ -3582,17 +3675,17 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: items: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.GameTeamResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameTeamResponse' type: array type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get all game-teams for a game tags: - game-teams @@ -3614,27 +3707,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_game_application_dto.GameResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_game_application_dto.GameResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Start a game @@ -3663,15 +3756,15 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_notification_application_dto.NotificationListResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_notification_application_dto.NotificationListResponse' type: object "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get notifications @@ -3692,15 +3785,15 @@ paths: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Mark notification as read @@ -3715,11 +3808,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Mark all notifications as read @@ -3738,7 +3831,7 @@ paths: "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Subscribe to real-time notifications @@ -3769,11 +3862,11 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "500": description: Internal Server Error schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Discord OAuth2 Callback tags: - oauth2 @@ -3803,7 +3896,7 @@ paths: name: user required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_user_application_dto.CreateUserRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.CreateUserRequest' produces: - application/json responses: @@ -3811,19 +3904,19 @@ paths: description: Created schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_user_application_dto.UserResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UserResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "409": description: Conflict schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Create a new user tags: - users @@ -3846,15 +3939,15 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Delete a user @@ -3877,23 +3970,23 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_user_application_dto.UserResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UserResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get a user by ID @@ -3914,7 +4007,7 @@ paths: name: user required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_user_application_dto.UpdateUserRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UpdateUserRequest' produces: - application/json responses: @@ -3922,23 +4015,23 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_user_application_dto.UserResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UserResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Update a user @@ -3959,7 +4052,7 @@ paths: name: user required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_user_application_dto.UpdateUserInfoRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.UpdateUserInfoRequest' produces: - application/json responses: @@ -3967,27 +4060,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_user_application_dto.MyUserResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.MyUserResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "409": description: Conflict schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Update user information @@ -4005,19 +4098,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_user_application_dto.MyUserResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_user_application_dto.MyUserResponse' type: object "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get my user information @@ -4036,15 +4129,15 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Unlink Valorant account @@ -4061,23 +4154,23 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get Valorant info @@ -4093,7 +4186,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_valorant_application_dto.RegisterValorantRequest' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.RegisterValorantRequest' produces: - application/json responses: @@ -4101,31 +4194,31 @@ paths: description: Created schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "409": description: Conflict schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "502": description: Bad Gateway schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Register Valorant account @@ -4143,27 +4236,27 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_valorant_application_dto.ValorantInfoResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "502": description: Bad Gateway schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Refresh Valorant data @@ -4193,19 +4286,19 @@ paths: description: Created schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_storage_application_dto.UploadResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_storage_application_dto.UploadResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Upload a contest banner image @@ -4230,19 +4323,19 @@ paths: description: Created schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_storage_application_dto.UploadResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_storage_application_dto.UploadResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Upload a user profile image @@ -4260,11 +4353,11 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: items: - $ref: '#/definitions/GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse' type: array type: object summary: Get all Valorant score tables @@ -4280,7 +4373,7 @@ paths: name: scoreTable required: true schema: - $ref: '#/definitions/GAMERS-BE_internal_point_application_dto.CreateValorantScoreTableDto' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.CreateValorantScoreTableDto' produces: - application/json responses: @@ -4288,19 +4381,19 @@ paths: description: Created schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Create a new Valorant score table @@ -4325,15 +4418,15 @@ paths: "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Delete a Valorant score table @@ -4356,19 +4449,19 @@ paths: description: OK schema: allOf: - - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' - properties: data: - $ref: '#/definitions/GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_point_application_dto.ValorantScoreTableResponse' type: object "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "404": description: Not Found schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' summary: Get a Valorant score table by ID tags: - valorant @@ -4385,16 +4478,16 @@ paths: description: OK schema: items: - $ref: '#/definitions/GAMERS-BE_internal_discord_application_dto.DiscordGuild' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordGuild' type: array "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "500": description: Internal Server Error schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get available guilds for contest creation @@ -4419,24 +4512,24 @@ paths: description: OK schema: items: - $ref: '#/definitions/GAMERS-BE_internal_discord_application_dto.DiscordChannel' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordChannel' type: array "400": description: Bad Request schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "401": description: Unauthorized schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "403": description: Forbidden schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' "500": description: Internal Server Error schema: - $ref: '#/definitions/GAMERS-BE_internal_global_response.Response' + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' security: - BearerAuth: [] summary: Get guild's text channels From 102148d349a8bf7187d2abeef524bb6aa30044ec Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:04:50 +0900 Subject: [PATCH 04/24] Refactor: Use domain methods and proper error logging in contest application (#62) - Replace string comparison with domain method (IsPending) - Replace silent error swallowing with log.Printf - Rename ErrContestAlreadyStarted to ErrNotContestLeader - Add ErrContestStartTimePassed, ErrAlreadyContestMemberExists - Use typed BusinessError in UpdateContestRequest validation - Handle duplicate application with 409 Conflict response Co-Authored-By: Claude Opus 4.6 --- .../contest_application_service.go | 37 ++++++++++--------- .../contest/application/contest_service.go | 5 +-- .../contest/application/dto/contest_dto.go | 12 +++--- .../contest_application_controller.go | 16 ++++++++ .../global/exception/contest_error_status.go | 3 +- 5 files changed, 45 insertions(+), 28 deletions(-) diff --git a/internal/contest/application/contest_application_service.go b/internal/contest/application/contest_application_service.go index 629ab9f..008c1c9 100644 --- a/internal/contest/application/contest_application_service.go +++ b/internal/contest/application/contest_application_service.go @@ -64,12 +64,18 @@ func (s *ContestApplicationService) RequestParticipate(ctx context.Context, cont return nil, err } - if contest.ContestStatus != "PENDING" { + if !contest.IsPending() { return nil, exception.ErrCannotAcceptApplication } if !contest.IsBeforeStartTime() { - return nil, exception.ErrContestAlreadyStarted + return nil, exception.ErrContestStartTimePassed + } + + // Check if user is already a contest member or leader + member, memberErr := s.memberRepo.GetByContestAndUser(contestId, userId) + if memberErr == nil && member != nil { + return nil, exception.ErrAlreadyContestMemberExists } // Fetch user info and create sender snapshot @@ -120,7 +126,7 @@ func (s *ContestApplicationService) AcceptApplication(ctx context.Context, conte return err } - if contest.ContestStatus != "PENDING" { + if !contest.IsPending() { return exception.ErrCannotAcceptApplication } @@ -135,9 +141,7 @@ func (s *ContestApplicationService) AcceptApplication(ctx context.Context, conte member := domain.NewContestMember(userId, contestId, domain.MemberTypeNormal, domain.LeaderTypeMember) if err := s.memberRepo.Save(member); err != nil { - // DB 저장 실패 시 Redis 상태 롤백은 하지 않음 (최종적 일관성) - // 추후 MigrateAcceptedApplicationsToDatabase에서 재시도됨 - _ = err + log.Printf("[AcceptApplication] failed to save member (contestId=%d, userId=%d): %v", contestId, userId, err) } go s.publishApplicationAcceptedEvent(context.Background(), contest, userId, leaderUserId) @@ -156,7 +160,7 @@ func (s *ContestApplicationService) RejectApplication(ctx context.Context, conte } // Contest가 PENDING 상태인지 확인 - if contest.ContestStatus != "PENDING" { + if !contest.IsPending() { return exception.ErrCannotAcceptApplication } @@ -278,7 +282,7 @@ func (s *ContestApplicationService) CancelApplication(ctx context.Context, conte } // Verify contest is in PENDING status - if contest.ContestStatus != "PENDING" { + if !contest.IsPending() { return exception.ErrCannotAcceptApplication } @@ -303,7 +307,7 @@ func (s *ContestApplicationService) ChangeMemberRole(contestId, targetUserId, le } // Contest가 PENDING 상태인지 확인 (대회 시작 전에만 역할 변경 가능) - if contest.ContestStatus != "PENDING" { + if !contest.IsPending() { return nil, exception.ErrContestNotPending } @@ -350,7 +354,7 @@ func (s *ContestApplicationService) WithdrawFromContest(contestId, userId int64) } // Contest가 PENDING 상태인지 확인 - if contest.ContestStatus != "PENDING" { + if !contest.IsPending() { return exception.ErrCannotAcceptApplication } @@ -414,7 +418,7 @@ func (s *ContestApplicationService) publishApplicationRequestedEvent( } if err := s.eventPublisher.PublishContestApplicationEvent(ctx, event); err != nil { - _ = err + log.Printf("[PublishEvent] failed to publish application.requested for contest %d, user %d: %v", contest.ContestID, userId, err) } } @@ -445,8 +449,7 @@ func (s *ContestApplicationService) publishApplicationAcceptedEvent( } if err := s.eventPublisher.PublishContestApplicationEvent(ctx, event); err != nil { - // 로그만 남기고 에러는 무시 - _ = err + log.Printf("[PublishEvent] failed to publish application.accepted for contest %d, user %d: %v", contest.ContestID, userId, err) } } @@ -477,8 +480,7 @@ func (s *ContestApplicationService) publishApplicationRejectedEvent( } if err := s.eventPublisher.PublishContestApplicationEvent(ctx, event); err != nil { - // 로그만 남기고 에러는 무시 - _ = err + log.Printf("[PublishEvent] failed to publish application.rejected for contest %d, user %d: %v", contest.ContestID, userId, err) } } @@ -505,8 +507,7 @@ func (s *ContestApplicationService) publishMemberWithdrawnEvent( } if err := s.eventPublisher.PublishContestApplicationEvent(ctx, event); err != nil { - // 로그만 남기고 에러는 무시 - _ = err + log.Printf("[PublishEvent] failed to publish member.withdrawn for contest %d, user %d: %v", contest.ContestID, userId, err) } } @@ -533,7 +534,7 @@ func (s *ContestApplicationService) publishApplicationCancelledEvent( } if err := s.eventPublisher.PublishContestApplicationEvent(ctx, event); err != nil { - _ = err + log.Printf("[PublishEvent] failed to publish application.cancelled for contest %d, user %d: %v", contest.ContestID, userId, err) } } diff --git a/internal/contest/application/contest_service.go b/internal/contest/application/contest_service.go index e6eb9e6..1d8110e 100644 --- a/internal/contest/application/contest_service.go +++ b/internal/contest/application/contest_service.go @@ -272,7 +272,7 @@ func (c *ContestService) checkLeaderPermission(contestId, userId int64) error { return exception.ErrInvalidAccess } if !member.IsLeader() { - return exception.ErrContestAlreadyStarted + return exception.ErrNotContestLeader } return nil @@ -461,8 +461,7 @@ func (c *ContestService) publishContestCreatedEvent( } if err := c.eventPublisher.PublishContestCreatedEvent(ctx, event); err != nil { - // Log error but don't affect contest creation - _ = err + log.Printf("[SaveContest] failed to publish contest created event for contest %d: %v", contest.ContestID, err) } } diff --git a/internal/contest/application/dto/contest_dto.go b/internal/contest/application/dto/contest_dto.go index f9d73c7..5210d78 100644 --- a/internal/contest/application/dto/contest_dto.go +++ b/internal/contest/application/dto/contest_dto.go @@ -4,8 +4,8 @@ import ( "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application/port" "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/domain" gameDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/game/domain" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" "github.com/FOR-GAMERS/GAMERS-BE/internal/global/utils" - "errors" "time" ) @@ -134,24 +134,24 @@ func (req *UpdateContestRequest) HasChanges() bool { func (req *UpdateContestRequest) Validate() error { if req.StartedAt != nil && req.EndedAt != nil { if req.EndedAt.Before(*req.StartedAt) { - return errors.New("end time must be after start time") + return exception.ErrInvalidContestDates } } if req.MaxTeamCount != nil && *req.MaxTeamCount <= 0 { - return errors.New("max team count must be positive") + return exception.ErrInvalidMaxTeamCount } if req.TotalPoint != nil && *req.TotalPoint < 0 { - return errors.New("total point must be non-negative") + return exception.ErrInvalidTotalPoint } if req.TotalTeamMember != nil && *req.TotalTeamMember < 1 { - return errors.New("total team member must be at least 1") + return exception.ErrInvalidTotalTeamMember } if req.GameType != nil && !req.GameType.IsValid() { - return errors.New("invalid game type") + return exception.ErrInvalidGameType } return nil diff --git a/internal/contest/presentation/contest_application_controller.go b/internal/contest/presentation/contest_application_controller.go index cdcb87d..6b7449b 100644 --- a/internal/contest/presentation/contest_application_controller.go +++ b/internal/contest/presentation/contest_application_controller.go @@ -10,6 +10,7 @@ import ( "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" "github.com/FOR-GAMERS/GAMERS-BE/internal/global/response" "errors" + "net/http" "strconv" "github.com/gin-gonic/gin" @@ -88,6 +89,21 @@ func (c *ContestApplicationController) RequestParticipate(ctx *gin.Context) { return } + // Handle already applied — return existing application data + if errors.Is(err, exception.ErrAlreadyApplied) { + existingApp, appErr := c.service.GetMyApplication(ctx.Request.Context(), contestId, userId) + if appErr == nil { + ctx.JSON(http.StatusConflict, response.Response{ + Status: http.StatusConflict, + Message: "already applied to this contest", + Data: existingApp, + }) + } else { + c.helper.HandleError(ctx, err) + } + return + } + c.helper.RespondCreated(ctx, nil, err, "application submitted successfully") } diff --git a/internal/global/exception/contest_error_status.go b/internal/global/exception/contest_error_status.go index aa6428f..0a5ba27 100644 --- a/internal/global/exception/contest_error_status.go +++ b/internal/global/exception/contest_error_status.go @@ -28,7 +28,7 @@ var ( ErrApplicationNotFound = NewNotFoundError("application not found", "CT021") ErrCannotAcceptApplication = NewBadRequestError("contest is not accepting applications", "CT022") ErrInvalidAccess = NewBusinessError(http.StatusForbidden, "you are not a member of this contest", "CT024") - ErrContestAlreadyStarted = NewBusinessError(http.StatusForbidden, "only contest leader can perform this action", "CT025") + ErrNotContestLeader = NewBusinessError(http.StatusForbidden, "only contest leader can perform this action", "CT025") ErrContestNotPending = NewBadRequestError("contest is not in pending status", "CT026") ErrContestCannotStart = NewBadRequestError("contest start time has not arrived yet", "CT027") ErrDiscordTextChannelRequired = NewBadRequestError("discord text channel id is required when guild id is provided", "CT028") @@ -38,4 +38,5 @@ var ( ErrContestNotActive = NewBadRequestError("contest is not in active status", "CT032") ErrCannotChangeLeaderRole = NewBusinessError(http.StatusForbidden, "cannot change leader's role", "CT033") ErrAlreadySameMemberType = NewBadRequestError("member already has the same role", "CT034") + ErrContestStartTimePassed = NewBadRequestError("contest start time has already passed", "CT035") ) From dd9eba831cb64de605c57aae0ca6dcfde2fe0d52 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:05:00 +0900 Subject: [PATCH 05/24] Test: Add unit and integration tests for Auth and Discord OAuth2 login (#63) - Extract OAuth2TokenExchangerPort, DiscordUserInfoPort, OAuth2StatePort interfaces - Create OAuth2TokenExchangerAdapter for production oauth2.Config wrapping - Refactor DiscordService to depend on port interfaces for testability - Add 9 auth service tests (Login, Logout, Refresh) - Add 7 auth controller tests (httptest) - Add 5 Discord service tests (Callback, LoginURL) - Add 5 Discord controller tests (Redirect, Callback scenarios) - Add 4 auth integration tests (testcontainers: MySQL + Redis) Co-Authored-By: Claude Opus 4.6 --- .../oauth2/application/discord_service.go | 30 +- .../port/discord_user_info_port.go | 10 + .../application/port/oauth2_state_port.go | 5 + .../port/oauth2_token_exchanger_port.go | 8 + .../discord/oauth2_token_exchanger_adapter.go | 27 ++ internal/oauth2/provider.go | 5 +- test/auth/application/auth_service_test.go | 378 ++++++++++++++++ .../auth/presentation/auth_controller_test.go | 372 ++++++++++++++++ test/integration/auth_integration_test.go | 242 ++++++++++ .../application/discord_service_test.go | 395 ++++++++++++++++ .../presentation/discord_controller_test.go | 421 ++++++++++++++++++ 11 files changed, 1872 insertions(+), 21 deletions(-) create mode 100644 internal/oauth2/application/port/discord_user_info_port.go create mode 100644 internal/oauth2/application/port/oauth2_token_exchanger_port.go create mode 100644 internal/oauth2/infra/discord/oauth2_token_exchanger_adapter.go create mode 100644 test/auth/application/auth_service_test.go create mode 100644 test/auth/presentation/auth_controller_test.go create mode 100644 test/integration/auth_integration_test.go create mode 100644 test/oauth2/application/discord_service_test.go create mode 100644 test/oauth2/presentation/discord_controller_test.go diff --git a/internal/oauth2/application/discord_service.go b/internal/oauth2/application/discord_service.go index 7023e97..ea8592e 100644 --- a/internal/oauth2/application/discord_service.go +++ b/internal/oauth2/application/discord_service.go @@ -12,21 +12,15 @@ import ( "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application/dto" "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application/port" "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/domain" - "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/infra/discord" - "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/infra/state" userDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/user/domain" - "context" "errors" "fmt" - - "golang.org/x/oauth2" ) type DiscordService struct { - ctx context.Context - config *oauth2.Config - discordClient *discord.Client - stateManager *state.Manager + tokenExchanger port.OAuth2TokenExchangerPort + userInfoClient port.DiscordUserInfoPort + stateManager port.OAuth2StatePort oauth2UserPort port.OAuth2UserPort oauth2DatabasePort port.OAuth2DatabasePort discordTokenPort discordPort.DiscordTokenPort @@ -35,10 +29,9 @@ type DiscordService struct { } func NewOAuth2Service( - ctx context.Context, - config *oauth2.Config, - discordClient *discord.Client, - stateManager *state.Manager, + tokenExchanger port.OAuth2TokenExchangerPort, + userInfoClient port.DiscordUserInfoPort, + stateManager port.OAuth2StatePort, oauth2UserPort port.OAuth2UserPort, oauth2DatabasePort port.OAuth2DatabasePort, discordTokenPort discordPort.DiscordTokenPort, @@ -46,9 +39,8 @@ func NewOAuth2Service( tokenService jwtApplication.TokenService, ) *DiscordService { return &DiscordService{ - ctx: ctx, - config: config, - discordClient: discordClient, + tokenExchanger: tokenExchanger, + userInfoClient: userInfoClient, stateManager: stateManager, oauth2UserPort: oauth2UserPort, oauth2DatabasePort: oauth2DatabasePort, @@ -63,16 +55,16 @@ func (s *DiscordService) GetDiscordLoginURL() (string, error) { if err != nil { return "", err } - return s.config.AuthCodeURL(randomState, oauth2.AccessTypeOnline), nil + return s.tokenExchanger.AuthCodeURL(randomState), nil } func (s *DiscordService) HandleDiscordCallback(req *dto.DiscordCallbackRequest) (*dto.OAuth2LoginResponse, error) { - token, err := s.config.Exchange(s.ctx, req.Code) + token, err := s.tokenExchanger.Exchange(req.Code) if err != nil { return nil, exception.ErrDiscordTokenExchange } - userInfo, err := s.discordClient.GetUserInfo(token) + userInfo, err := s.userInfoClient.GetUserInfo(token) if err != nil { return nil, exception.ErrDiscordCannotGetUserInfo } diff --git a/internal/oauth2/application/port/discord_user_info_port.go b/internal/oauth2/application/port/discord_user_info_port.go new file mode 100644 index 0000000..40e12ce --- /dev/null +++ b/internal/oauth2/application/port/discord_user_info_port.go @@ -0,0 +1,10 @@ +package port + +import ( + "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application/dto" + "golang.org/x/oauth2" +) + +type DiscordUserInfoPort interface { + GetUserInfo(token *oauth2.Token) (*dto.DiscordUserInfo, error) +} diff --git a/internal/oauth2/application/port/oauth2_state_port.go b/internal/oauth2/application/port/oauth2_state_port.go index 53ccc8d..39d1f39 100644 --- a/internal/oauth2/application/port/oauth2_state_port.go +++ b/internal/oauth2/application/port/oauth2_state_port.go @@ -1 +1,6 @@ package port + +type OAuth2StatePort interface { + GenerateState() (string, error) + ValidateState(state string) error +} diff --git a/internal/oauth2/application/port/oauth2_token_exchanger_port.go b/internal/oauth2/application/port/oauth2_token_exchanger_port.go new file mode 100644 index 0000000..a2ec980 --- /dev/null +++ b/internal/oauth2/application/port/oauth2_token_exchanger_port.go @@ -0,0 +1,8 @@ +package port + +import "golang.org/x/oauth2" + +type OAuth2TokenExchangerPort interface { + Exchange(code string) (*oauth2.Token, error) + AuthCodeURL(state string) string +} diff --git a/internal/oauth2/infra/discord/oauth2_token_exchanger_adapter.go b/internal/oauth2/infra/discord/oauth2_token_exchanger_adapter.go new file mode 100644 index 0000000..6072dda --- /dev/null +++ b/internal/oauth2/infra/discord/oauth2_token_exchanger_adapter.go @@ -0,0 +1,27 @@ +package discord + +import ( + "context" + + "golang.org/x/oauth2" +) + +type OAuth2TokenExchangerAdapter struct { + config *oauth2.Config + ctx context.Context +} + +func NewOAuth2TokenExchangerAdapter(config *oauth2.Config, ctx context.Context) *OAuth2TokenExchangerAdapter { + return &OAuth2TokenExchangerAdapter{ + config: config, + ctx: ctx, + } +} + +func (a *OAuth2TokenExchangerAdapter) Exchange(code string) (*oauth2.Token, error) { + return a.config.Exchange(a.ctx, code) +} + +func (a *OAuth2TokenExchangerAdapter) AuthCodeURL(state string) string { + return a.config.AuthCodeURL(state, oauth2.AccessTypeOnline) +} diff --git a/internal/oauth2/provider.go b/internal/oauth2/provider.go index 8024e2c..a7b476b 100644 --- a/internal/oauth2/provider.go +++ b/internal/oauth2/provider.go @@ -38,9 +38,10 @@ func ProvideOAuth2Dependencies(db *gorm.DB, redisClient *redis.Client, ctx *cont refreshTokenCacheAdapter := authAdapter.NewRefreshTokenCacheAdapter(ctx, redisClient) + tokenExchanger := discord.NewOAuth2TokenExchangerAdapter(discordConfig, *ctx) + oauth2Service := application.NewOAuth2Service( - *ctx, - discordConfig, + tokenExchanger, discordClient, stateManager, oauth2UserAdapter, diff --git a/test/auth/application/auth_service_test.go b/test/auth/application/auth_service_test.go new file mode 100644 index 0000000..89b09d9 --- /dev/null +++ b/test/auth/application/auth_service_test.go @@ -0,0 +1,378 @@ +package application_test + +import ( + "errors" + "testing" + "time" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/application/dto" + "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/domain" + jwtApp "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/application" + jwtDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/domain" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/infra" + userDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/user/domain" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// ==================== Mock Definitions ==================== + +type MockAuthUserQueryPort struct { + mock.Mock +} + +func (m *MockAuthUserQueryPort) FindByEmail(email string) (*userDomain.User, error) { + args := m.Called(email) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*userDomain.User), args.Error(1) +} + +type MockRefreshTokenCachePort struct { + mock.Mock +} + +func (m *MockRefreshTokenCachePort) Save(token *domain.RefreshToken, ttl *int64) error { + args := m.Called(token, ttl) + return args.Error(0) +} + +func (m *MockRefreshTokenCachePort) FindByToken(token *string) (*domain.RefreshToken, error) { + args := m.Called(token) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*domain.RefreshToken), args.Error(1) +} + +func (m *MockRefreshTokenCachePort) ExistsByToken(token *string) (bool, error) { + args := m.Called(token) + return args.Bool(0), args.Error(1) +} + +func (m *MockRefreshTokenCachePort) Delete(token *string) error { + args := m.Called(token) + return args.Error(0) +} + +func (m *MockRefreshTokenCachePort) DeleteByUserID(userID *int64) error { + args := m.Called(userID) + return args.Error(0) +} + +type MockPasswordHasher struct { + mock.Mock +} + +func (m *MockPasswordHasher) HashPassword(password string) (string, error) { + args := m.Called(password) + return args.String(0), args.Error(1) +} + +func (m *MockPasswordHasher) ComparePassword(hashedPassword, password string) error { + args := m.Called(hashedPassword, password) + return args.Error(0) +} + +// ==================== Helper ==================== + +func newTestTokenService() jwtApp.TokenService { + config := &jwtDomain.Token{ + SecretKey: "test-secret-key-for-access", + RefreshSecretKey: "test-secret-key-for-refresh", + AccessTokenDuration: 5 * time.Minute, + RefreshTokenDuration: 1 * time.Hour, + Issuer: "test-gamers-api", + } + accessStrategy := infra.NewAccessTokenStrategy(config) + refreshStrategy := infra.NewRefreshTokenStrategy(config) + + tokenService := jwtApp.NewTokenService() + tokenService.RegisterStrategy(accessStrategy) + tokenService.RegisterStrategy(refreshStrategy) + + return *tokenService +} + +func newTestUser() *userDomain.User { + return &userDomain.User{ + Id: 1, + Email: "test@example.com", + Password: "hashed-password", + Username: "testuser", + Tag: "0001", + Role: userDomain.UserRoleUser, + } +} + +// ==================== Login Tests ==================== + +func TestAuthService_Login_Success(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + tokenService := newTestTokenService() + + service := application.NewAuthService(mockUserQuery, mockCache, tokenService, mockHasher) + + user := newTestUser() + req := &dto.LoginRequest{ + Email: "test@example.com", + Password: "raw-password", + } + + mockUserQuery.On("FindByEmail", req.Email).Return(user, nil) + mockHasher.On("ComparePassword", user.Password, req.Password).Return(nil) + mockCache.On("Save", mock.AnythingOfType("*domain.RefreshToken"), mock.AnythingOfType("*int64")).Return(nil) + + // When + result, err := service.Login(req) + + // Then + assert.NoError(t, err) + assert.NotNil(t, result) + assert.NotEmpty(t, result.AccessToken) + assert.NotEmpty(t, result.RefreshToken) + + mockUserQuery.AssertExpectations(t) + mockHasher.AssertExpectations(t) + mockCache.AssertExpectations(t) +} + +func TestAuthService_Login_UserNotFound(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + tokenService := newTestTokenService() + + service := application.NewAuthService(mockUserQuery, mockCache, tokenService, mockHasher) + + req := &dto.LoginRequest{ + Email: "nonexistent@example.com", + Password: "raw-password", + } + + mockUserQuery.On("FindByEmail", req.Email).Return(nil, errors.New("user not found")) + + // When + result, err := service.Login(req) + + // Then + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "user not found") + + mockUserQuery.AssertExpectations(t) +} + +func TestAuthService_Login_WrongPassword(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + tokenService := newTestTokenService() + + service := application.NewAuthService(mockUserQuery, mockCache, tokenService, mockHasher) + + user := newTestUser() + req := &dto.LoginRequest{ + Email: "test@example.com", + Password: "wrong-password", + } + + mockUserQuery.On("FindByEmail", req.Email).Return(user, nil) + mockHasher.On("ComparePassword", user.Password, req.Password).Return(errors.New("password mismatch")) + + // When + result, err := service.Login(req) + + // Then + assert.Error(t, err) + assert.Nil(t, result) + + mockUserQuery.AssertExpectations(t) + mockHasher.AssertExpectations(t) +} + +func TestAuthService_Login_RefreshTokenSaveFail(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + tokenService := newTestTokenService() + + service := application.NewAuthService(mockUserQuery, mockCache, tokenService, mockHasher) + + user := newTestUser() + req := &dto.LoginRequest{ + Email: "test@example.com", + Password: "raw-password", + } + + mockUserQuery.On("FindByEmail", req.Email).Return(user, nil) + mockHasher.On("ComparePassword", user.Password, req.Password).Return(nil) + mockCache.On("Save", mock.AnythingOfType("*domain.RefreshToken"), mock.AnythingOfType("*int64")).Return(errors.New("redis connection failed")) + + // When + result, err := service.Login(req) + + // Then + assert.Error(t, err) + assert.Nil(t, result) + + mockUserQuery.AssertExpectations(t) + mockHasher.AssertExpectations(t) + mockCache.AssertExpectations(t) +} + +// ==================== Logout Tests ==================== + +func TestAuthService_Logout_Success(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + tokenService := newTestTokenService() + + service := application.NewAuthService(mockUserQuery, mockCache, tokenService, mockHasher) + + req := dto.LogoutRequest{ + AccessToken: "some-access-token", + RefreshToken: "some-refresh-token", + } + + mockCache.On("Delete", &req.RefreshToken).Return(nil) + + // When + err := service.Logout(req) + + // Then + assert.NoError(t, err) + mockCache.AssertExpectations(t) +} + +func TestAuthService_Logout_TokenNotFound(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + tokenService := newTestTokenService() + + service := application.NewAuthService(mockUserQuery, mockCache, tokenService, mockHasher) + + req := dto.LogoutRequest{ + AccessToken: "some-access-token", + RefreshToken: "nonexistent-token", + } + + mockCache.On("Delete", &req.RefreshToken).Return(errors.New("token not found")) + + // When + err := service.Logout(req) + + // Then + assert.Error(t, err) + mockCache.AssertExpectations(t) +} + +// ==================== Refresh Tests ==================== + +func TestAuthService_Refresh_Success(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + tokenService := newTestTokenService() + + service := application.NewAuthService(mockUserQuery, mockCache, tokenService, mockHasher) + + // Generate a real refresh token so Validate passes + refreshTokenStr, _ := tokenService.Generate(jwtDomain.TokenTypeRefresh, 1, "USER") + + req := dto.RefreshRequest{ + RefreshToken: refreshTokenStr, + } + + storedToken := &domain.RefreshToken{ + Token: refreshTokenStr, + UserID: 1, + ExpiresAt: time.Now().Add(1 * time.Hour).UnixMilli(), + } + + mockCache.On("FindByToken", &req.RefreshToken).Return(storedToken, nil) + mockCache.On("Delete", &req.RefreshToken).Return(nil) + mockCache.On("Save", mock.AnythingOfType("*domain.RefreshToken"), mock.AnythingOfType("*int64")).Return(nil) + + // When + result, err := service.Refresh(req) + + // Then + assert.NoError(t, err) + assert.NotNil(t, result) + assert.NotEmpty(t, result.AccessToken) + assert.NotEmpty(t, result.RefreshToken) + + mockCache.AssertExpectations(t) +} + +func TestAuthService_Refresh_TokenNotFound(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + tokenService := newTestTokenService() + + service := application.NewAuthService(mockUserQuery, mockCache, tokenService, mockHasher) + + req := dto.RefreshRequest{ + RefreshToken: "nonexistent-refresh-token", + } + + mockCache.On("FindByToken", &req.RefreshToken).Return(nil, errors.New("token not found")) + + // When + result, err := service.Refresh(req) + + // Then + assert.Error(t, err) + assert.Nil(t, result) + + mockCache.AssertExpectations(t) +} + +func TestAuthService_Refresh_InvalidToken(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + tokenService := newTestTokenService() + + service := application.NewAuthService(mockUserQuery, mockCache, tokenService, mockHasher) + + invalidToken := "invalid.jwt.token" + req := dto.RefreshRequest{ + RefreshToken: invalidToken, + } + + storedToken := &domain.RefreshToken{ + Token: invalidToken, + UserID: 1, + ExpiresAt: time.Now().Add(1 * time.Hour).UnixMilli(), + } + + mockCache.On("FindByToken", &req.RefreshToken).Return(storedToken, nil) + + // When + result, err := service.Refresh(req) + + // Then + assert.Error(t, err) + assert.Nil(t, result) + + mockCache.AssertExpectations(t) +} diff --git a/test/auth/presentation/auth_controller_test.go b/test/auth/presentation/auth_controller_test.go new file mode 100644 index 0000000..7dd4493 --- /dev/null +++ b/test/auth/presentation/auth_controller_test.go @@ -0,0 +1,372 @@ +package presentation_test + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/domain" + jwtApp "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/application" + jwtDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/domain" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/infra" + userDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/user/domain" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/presentation" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/common/router" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// ==================== Mock Definitions ==================== + +type MockAuthUserQueryPort struct { + mock.Mock +} + +func (m *MockAuthUserQueryPort) FindByEmail(email string) (*userDomain.User, error) { + args := m.Called(email) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*userDomain.User), args.Error(1) +} + +type MockRefreshTokenCachePort struct { + mock.Mock +} + +func (m *MockRefreshTokenCachePort) Save(token *domain.RefreshToken, ttl *int64) error { + args := m.Called(token, ttl) + return args.Error(0) +} + +func (m *MockRefreshTokenCachePort) FindByToken(token *string) (*domain.RefreshToken, error) { + args := m.Called(token) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*domain.RefreshToken), args.Error(1) +} + +func (m *MockRefreshTokenCachePort) ExistsByToken(token *string) (bool, error) { + args := m.Called(token) + return args.Bool(0), args.Error(1) +} + +func (m *MockRefreshTokenCachePort) Delete(token *string) error { + args := m.Called(token) + return args.Error(0) +} + +func (m *MockRefreshTokenCachePort) DeleteByUserID(userID *int64) error { + args := m.Called(userID) + return args.Error(0) +} + +type MockPasswordHasher struct { + mock.Mock +} + +func (m *MockPasswordHasher) HashPassword(password string) (string, error) { + args := m.Called(password) + return args.String(0), args.Error(1) +} + +func (m *MockPasswordHasher) ComparePassword(hashedPassword, password string) error { + args := m.Called(hashedPassword, password) + return args.Error(0) +} + +// ==================== Helpers ==================== + +func newTestTokenService() jwtApp.TokenService { + config := &jwtDomain.Token{ + SecretKey: "test-secret-key-for-access", + RefreshSecretKey: "test-secret-key-for-refresh", + AccessTokenDuration: 5 * time.Minute, + RefreshTokenDuration: 1 * time.Hour, + Issuer: "test-gamers-api", + } + accessStrategy := infra.NewAccessTokenStrategy(config) + refreshStrategy := infra.NewRefreshTokenStrategy(config) + + tokenService := jwtApp.NewTokenService() + tokenService.RegisterStrategy(accessStrategy) + tokenService.RegisterStrategy(refreshStrategy) + + return *tokenService +} + +func setupTestRouter() *gin.Engine { + gin.SetMode(gin.TestMode) + return gin.New() +} + +func setupAuthController(mockUserQuery *MockAuthUserQueryPort, mockCache *MockRefreshTokenCachePort, mockHasher *MockPasswordHasher) (*gin.Engine, *presentation.AuthController) { + tokenService := newTestTokenService() + authService := application.NewAuthService(mockUserQuery, mockCache, tokenService, mockHasher) + + engine := setupTestRouter() + r := &router.Router{} + + controller := presentation.NewAuthController(r, authService) + + // Register routes directly on test engine + authGroup := engine.Group("/api/auth") + { + authGroup.POST("/login", controller.Login) + authGroup.POST("/logout", controller.Logout) + authGroup.POST("/refresh", controller.Refresh) + } + + return engine, controller +} + +// ==================== Login Tests ==================== + +func TestAuthController_Login_Success(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + + engine, _ := setupAuthController(mockUserQuery, mockCache, mockHasher) + + user := &userDomain.User{ + Id: 1, + Email: "test@example.com", + Password: "hashed-password", + Username: "testuser", + Tag: "0001", + Role: userDomain.UserRoleUser, + } + + mockUserQuery.On("FindByEmail", "test@example.com").Return(user, nil) + mockHasher.On("ComparePassword", "hashed-password", "rawpassword").Return(nil) + mockCache.On("Save", mock.AnythingOfType("*domain.RefreshToken"), mock.AnythingOfType("*int64")).Return(nil) + + body, _ := json.Marshal(map[string]string{ + "email": "test@example.com", + "password": "rawpassword", + }) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/auth/login", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + engine.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusOK, w.Code) + + var response map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &response) + assert.Equal(t, float64(200), response["status"]) + assert.Equal(t, "Login successful", response["message"]) + + data := response["data"].(map[string]interface{}) + assert.NotEmpty(t, data["access_token"]) + assert.NotEmpty(t, data["refresh_token"]) + + mockUserQuery.AssertExpectations(t) + mockHasher.AssertExpectations(t) + mockCache.AssertExpectations(t) +} + +func TestAuthController_Login_InvalidJSON(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + + engine, _ := setupAuthController(mockUserQuery, mockCache, mockHasher) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/auth/login", bytes.NewBufferString("invalid-json")) + req.Header.Set("Content-Type", "application/json") + engine.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusBadRequest, w.Code) + + var response map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &response) + assert.Equal(t, float64(400), response["status"]) +} + +func TestAuthController_Login_WrongCredentials(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + + engine, _ := setupAuthController(mockUserQuery, mockCache, mockHasher) + + user := &userDomain.User{ + Id: 1, + Email: "test@example.com", + Password: "hashed-password", + Username: "testuser", + Tag: "0001", + Role: userDomain.UserRoleUser, + } + + mockUserQuery.On("FindByEmail", "test@example.com").Return(user, nil) + mockHasher.On("ComparePassword", "hashed-password", "wrong-password").Return(errors.New("password mismatch")) + + body, _ := json.Marshal(map[string]string{ + "email": "test@example.com", + "password": "wrong-password", + }) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/auth/login", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + engine.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusUnauthorized, w.Code) + + mockUserQuery.AssertExpectations(t) + mockHasher.AssertExpectations(t) +} + +// ==================== Logout Tests ==================== + +func TestAuthController_Logout_Success(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + + engine, _ := setupAuthController(mockUserQuery, mockCache, mockHasher) + + refreshToken := "some-refresh-token" + mockCache.On("Delete", &refreshToken).Return(nil) + + body, _ := json.Marshal(map[string]string{ + "access_token": "some-access-token", + "refresh_token": refreshToken, + }) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/auth/logout", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + engine.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusNoContent, w.Code) + + mockCache.AssertExpectations(t) +} + +func TestAuthController_Logout_MissingFields(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + + engine, _ := setupAuthController(mockUserQuery, mockCache, mockHasher) + + // Missing required refresh_token + body, _ := json.Marshal(map[string]string{}) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/auth/logout", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + engine.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +// ==================== Refresh Tests ==================== + +func TestAuthController_Refresh_Success(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + + engine, _ := setupAuthController(mockUserQuery, mockCache, mockHasher) + + tokenService := newTestTokenService() + refreshTokenStr, _ := tokenService.Generate(jwtDomain.TokenTypeRefresh, 1, "USER") + + storedToken := &domain.RefreshToken{ + Token: refreshTokenStr, + UserID: 1, + ExpiresAt: time.Now().Add(1 * time.Hour).UnixMilli(), + } + + mockCache.On("FindByToken", &refreshTokenStr).Return(storedToken, nil) + mockCache.On("Delete", &refreshTokenStr).Return(nil) + mockCache.On("Save", mock.AnythingOfType("*domain.RefreshToken"), mock.AnythingOfType("*int64")).Return(nil) + + body, _ := json.Marshal(map[string]string{ + "refresh_token": refreshTokenStr, + }) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/auth/refresh", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + engine.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusOK, w.Code) + + var response map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &response) + assert.Equal(t, float64(200), response["status"]) + + data := response["data"].(map[string]interface{}) + assert.NotEmpty(t, data["access_token"]) + assert.NotEmpty(t, data["refresh_token"]) + + mockCache.AssertExpectations(t) +} + +func TestAuthController_Refresh_InvalidToken(t *testing.T) { + // Given + mockUserQuery := new(MockAuthUserQueryPort) + mockCache := new(MockRefreshTokenCachePort) + mockHasher := new(MockPasswordHasher) + + engine, _ := setupAuthController(mockUserQuery, mockCache, mockHasher) + + invalidToken := "invalid.jwt.token" + storedToken := &domain.RefreshToken{ + Token: invalidToken, + UserID: 1, + ExpiresAt: time.Now().Add(1 * time.Hour).UnixMilli(), + } + + mockCache.On("FindByToken", &invalidToken).Return(storedToken, nil) + + body, _ := json.Marshal(map[string]string{ + "refresh_token": invalidToken, + }) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/auth/refresh", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + engine.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusUnauthorized, w.Code) + + mockCache.AssertExpectations(t) +} diff --git a/test/integration/auth_integration_test.go b/test/integration/auth_integration_test.go new file mode 100644 index 0000000..be1ff80 --- /dev/null +++ b/test/integration/auth_integration_test.go @@ -0,0 +1,242 @@ +package integration_test + +import ( + "context" + "testing" + "time" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/application/dto" + authAdapter "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/infra/persistence/adapter" + jwtApp "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/application" + jwtDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/domain" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/infra" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/password" + userDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/user/domain" + userQuery "github.com/FOR-GAMERS/GAMERS-BE/internal/user/infra/persistence/query" + "github.com/FOR-GAMERS/GAMERS-BE/test/global/support" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/suite" + "gorm.io/gorm" +) + +// ==================== Integration Test Suite ==================== + +type AuthIntegrationTestSuite struct { + suite.Suite + mysqlContainer *support.MySQLContainer + redisContainer *support.RedisContainer + db *gorm.DB + redisClient *redis.Client + authService *application.AuthService + tokenService jwtApp.TokenService + hasher password.Hasher + ctx context.Context +} + +func (s *AuthIntegrationTestSuite) SetupSuite() { + s.ctx = context.Background() + var err error + + // Start MySQL container + s.mysqlContainer, err = support.SetupMySQLContainer(s.ctx) + s.Require().NoError(err, "Failed to setup MySQL container") + s.db = s.mysqlContainer.GetDB() + + // Start Redis container + s.redisContainer, err = support.SetupRedisContainer(s.ctx) + s.Require().NoError(err, "Failed to setup Redis container") + s.redisClient = s.redisContainer.GetClient() + + // Auto-migrate user schema + err = s.db.AutoMigrate(&userDomain.User{}) + s.Require().NoError(err, "Failed to migrate user schema") +} + +func (s *AuthIntegrationTestSuite) TearDownSuite() { + if s.redisContainer != nil { + s.redisContainer.Teardown(s.ctx) + } + if s.mysqlContainer != nil { + s.mysqlContainer.Teardown(s.ctx) + } +} + +func (s *AuthIntegrationTestSuite) SetupTest() { + // Clean up + s.db.Exec("DELETE FROM users") + s.redisClient.FlushAll(s.ctx) + + // Create real dependencies + s.hasher = password.NewBcryptPasswordHasher() + + config := &jwtDomain.Token{ + SecretKey: "test-integration-secret-access", + RefreshSecretKey: "test-integration-secret-refresh", + AccessTokenDuration: 5 * time.Minute, + RefreshTokenDuration: 1 * time.Hour, + Issuer: "test-gamers-api", + } + accessStrategy := infra.NewAccessTokenStrategy(config) + refreshStrategy := infra.NewRefreshTokenStrategy(config) + + tokenService := jwtApp.NewTokenService() + tokenService.RegisterStrategy(accessStrategy) + tokenService.RegisterStrategy(refreshStrategy) + s.tokenService = *tokenService + + authUserQueryAdapter := userQuery.NewAuthUserQueryAdapter(s.db) + refreshTokenCacheAdapter := authAdapter.NewRefreshTokenCacheAdapter(&s.ctx, s.redisClient) + + s.authService = application.NewAuthService( + authUserQueryAdapter, + refreshTokenCacheAdapter, + s.tokenService, + s.hasher, + ) +} + +func (s *AuthIntegrationTestSuite) seedUser(email, rawPassword string) *userDomain.User { + hashed, err := s.hasher.HashPassword(rawPassword) + s.Require().NoError(err) + + user := &userDomain.User{ + Email: email, + Password: hashed, + Username: "testuser", + Tag: "0001", + Role: userDomain.UserRoleUser, + } + + result := s.db.Create(user) + s.Require().NoError(result.Error) + + return user +} + +// ==================== Tests ==================== + +func (s *AuthIntegrationTestSuite) TestFullLoginFlow() { + // Seed user + s.seedUser("flow@test.com", "TestPass1!") + + // 1. Login + loginReq := &dto.LoginRequest{ + Email: "flow@test.com", + Password: "TestPass1!", + } + loginResp, err := s.authService.Login(loginReq) + s.NoError(err) + s.NotNil(loginResp) + s.NotEmpty(loginResp.AccessToken) + s.NotEmpty(loginResp.RefreshToken) + + // 2. Refresh + refreshReq := dto.RefreshRequest{ + RefreshToken: loginResp.RefreshToken, + } + refreshResp, err := s.authService.Refresh(refreshReq) + s.NoError(err) + s.NotNil(refreshResp) + s.NotEmpty(refreshResp.AccessToken) + s.NotEmpty(refreshResp.RefreshToken) + // New tokens should be different from original + s.NotEqual(loginResp.RefreshToken, refreshResp.RefreshToken) + + // 3. Logout (with new refresh token) + logoutReq := dto.LogoutRequest{ + AccessToken: refreshResp.AccessToken, + RefreshToken: refreshResp.RefreshToken, + } + err = s.authService.Logout(logoutReq) + s.NoError(err) + + // 4. Re-refresh should fail (old token was deleted during refresh, new token was deleted during logout) + reRefreshReq := dto.RefreshRequest{ + RefreshToken: refreshResp.RefreshToken, + } + result, err := s.authService.Refresh(reRefreshReq) + s.Error(err) + s.Nil(result) +} + +func (s *AuthIntegrationTestSuite) TestLoginWithWrongPassword() { + // Seed user + s.seedUser("wrong@test.com", "CorrectPass1!") + + loginReq := &dto.LoginRequest{ + Email: "wrong@test.com", + Password: "WrongPass1!", + } + result, err := s.authService.Login(loginReq) + s.Error(err) + s.Nil(result) +} + +func (s *AuthIntegrationTestSuite) TestLoginWithNonExistentUser() { + loginReq := &dto.LoginRequest{ + Email: "nonexistent@test.com", + Password: "AnyPass1!", + } + result, err := s.authService.Login(loginReq) + s.Error(err) + s.Nil(result) +} + +func (s *AuthIntegrationTestSuite) TestRefreshWithExpiredToken() { + // Create a token service with very short refresh TTL + config := &jwtDomain.Token{ + SecretKey: "test-integration-secret-access", + RefreshSecretKey: "test-integration-secret-refresh", + AccessTokenDuration: 5 * time.Minute, + RefreshTokenDuration: 1 * time.Second, // Very short TTL + Issuer: "test-gamers-api", + } + accessStrategy := infra.NewAccessTokenStrategy(config) + refreshStrategy := infra.NewRefreshTokenStrategy(config) + + shortTokenService := jwtApp.NewTokenService() + shortTokenService.RegisterStrategy(accessStrategy) + shortTokenService.RegisterStrategy(refreshStrategy) + + authUserQueryAdapter := userQuery.NewAuthUserQueryAdapter(s.db) + refreshTokenCacheAdapter := authAdapter.NewRefreshTokenCacheAdapter(&s.ctx, s.redisClient) + + shortAuthService := application.NewAuthService( + authUserQueryAdapter, + refreshTokenCacheAdapter, + *shortTokenService, + s.hasher, + ) + + // Seed user and login + s.seedUser("expire@test.com", "TestPass1!") + loginReq := &dto.LoginRequest{ + Email: "expire@test.com", + Password: "TestPass1!", + } + loginResp, err := shortAuthService.Login(loginReq) + s.NoError(err) + s.NotNil(loginResp) + + // Wait for Redis TTL to expire + time.Sleep(2 * time.Second) + + // Try to refresh - should fail because token expired in Redis + refreshReq := dto.RefreshRequest{ + RefreshToken: loginResp.RefreshToken, + } + result, err := shortAuthService.Refresh(refreshReq) + s.Error(err) + s.Nil(result) +} + +// ==================== Run Suite ==================== + +func TestAuthIntegration(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration tests in short mode") + } + suite.Run(t, new(AuthIntegrationTestSuite)) +} diff --git a/test/oauth2/application/discord_service_test.go b/test/oauth2/application/discord_service_test.go new file mode 100644 index 0000000..d6ad020 --- /dev/null +++ b/test/oauth2/application/discord_service_test.go @@ -0,0 +1,395 @@ +package application_test + +import ( + "errors" + "testing" + "time" + + authDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/domain" + discordDto "github.com/FOR-GAMERS/GAMERS-BE/internal/discord/application/dto" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" + jwtApp "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/application" + jwtDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/domain" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/infra" + "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application/dto" + oauth2Domain "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/domain" + userDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/user/domain" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "golang.org/x/oauth2" +) + +// ==================== Mock Definitions ==================== + +type MockTokenExchanger struct { + mock.Mock +} + +func (m *MockTokenExchanger) Exchange(code string) (*oauth2.Token, error) { + args := m.Called(code) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*oauth2.Token), args.Error(1) +} + +func (m *MockTokenExchanger) AuthCodeURL(state string) string { + args := m.Called(state) + return args.String(0) +} + +type MockDiscordUserInfo struct { + mock.Mock +} + +func (m *MockDiscordUserInfo) GetUserInfo(token *oauth2.Token) (*dto.DiscordUserInfo, error) { + args := m.Called(token) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*dto.DiscordUserInfo), args.Error(1) +} + +type MockStateManager struct { + mock.Mock +} + +func (m *MockStateManager) GenerateState() (string, error) { + args := m.Called() + return args.String(0), args.Error(1) +} + +func (m *MockStateManager) ValidateState(state string) error { + args := m.Called(state) + return args.Error(0) +} + +type MockOAuth2DatabasePort struct { + mock.Mock +} + +func (m *MockOAuth2DatabasePort) FindDiscordAccountByDiscordId(discordId string) (*oauth2Domain.DiscordAccount, error) { + args := m.Called(discordId) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*oauth2Domain.DiscordAccount), args.Error(1) +} + +func (m *MockOAuth2DatabasePort) FindDiscordAccountByUserId(userId int64) (*oauth2Domain.DiscordAccount, error) { + args := m.Called(userId) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*oauth2Domain.DiscordAccount), args.Error(1) +} + +func (m *MockOAuth2DatabasePort) CreateDiscordAccount(account *oauth2Domain.DiscordAccount) error { + args := m.Called(account) + return args.Error(0) +} + +func (m *MockOAuth2DatabasePort) UpdateDiscordAccount(account *oauth2Domain.DiscordAccount) error { + args := m.Called(account) + return args.Error(0) +} + +type MockOAuth2UserPort struct { + mock.Mock +} + +func (m *MockOAuth2UserPort) SaveRandomUser(user *userDomain.User) error { + args := m.Called(user) + return args.Error(0) +} + +func (m *MockOAuth2UserPort) FindById(id int64) (*userDomain.User, error) { + args := m.Called(id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*userDomain.User), args.Error(1) +} + +type MockDiscordTokenPort struct { + mock.Mock +} + +func (m *MockDiscordTokenPort) SaveToken(token *discordDto.DiscordToken) error { + args := m.Called(token) + return args.Error(0) +} + +func (m *MockDiscordTokenPort) GetToken(userID int64) (*discordDto.DiscordToken, error) { + args := m.Called(userID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*discordDto.DiscordToken), args.Error(1) +} + +func (m *MockDiscordTokenPort) DeleteToken(userID int64) error { + args := m.Called(userID) + return args.Error(0) +} + +func (m *MockDiscordTokenPort) ExistsToken(userID int64) (bool, error) { + args := m.Called(userID) + return args.Bool(0), args.Error(1) +} + +type MockRefreshTokenCachePort struct { + mock.Mock +} + +func (m *MockRefreshTokenCachePort) Save(token *authDomain.RefreshToken, ttl *int64) error { + args := m.Called(token, ttl) + return args.Error(0) +} + +func (m *MockRefreshTokenCachePort) FindByToken(token *string) (*authDomain.RefreshToken, error) { + args := m.Called(token) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*authDomain.RefreshToken), args.Error(1) +} + +func (m *MockRefreshTokenCachePort) ExistsByToken(token *string) (bool, error) { + args := m.Called(token) + return args.Bool(0), args.Error(1) +} + +func (m *MockRefreshTokenCachePort) Delete(token *string) error { + args := m.Called(token) + return args.Error(0) +} + +func (m *MockRefreshTokenCachePort) DeleteByUserID(userID *int64) error { + args := m.Called(userID) + return args.Error(0) +} + +// ==================== Helper ==================== + +func newTestTokenService() jwtApp.TokenService { + config := &jwtDomain.Token{ + SecretKey: "test-secret-key-for-access", + RefreshSecretKey: "test-secret-key-for-refresh", + AccessTokenDuration: 5 * time.Minute, + RefreshTokenDuration: 1 * time.Hour, + Issuer: "test-gamers-api", + } + accessStrategy := infra.NewAccessTokenStrategy(config) + refreshStrategy := infra.NewRefreshTokenStrategy(config) + + tokenService := jwtApp.NewTokenService() + tokenService.RegisterStrategy(accessStrategy) + tokenService.RegisterStrategy(refreshStrategy) + + return *tokenService +} + +func newTestOAuth2Token() *oauth2.Token { + return &oauth2.Token{ + AccessToken: "discord-access-token", + RefreshToken: "discord-refresh-token", + TokenType: "Bearer", + Expiry: time.Now().Add(1 * time.Hour), + } +} + +func newTestDiscordUserInfo() *dto.DiscordUserInfo { + return &dto.DiscordUserInfo{ + Id: "123456789", + Username: "discorduser", + Avatar: "avatar_hash", + Verified: true, + } +} + +func setupDiscordService() ( + *application.DiscordService, + *MockTokenExchanger, + *MockDiscordUserInfo, + *MockStateManager, + *MockOAuth2DatabasePort, + *MockOAuth2UserPort, + *MockDiscordTokenPort, + *MockRefreshTokenCachePort, +) { + mockExchanger := new(MockTokenExchanger) + mockUserInfo := new(MockDiscordUserInfo) + mockState := new(MockStateManager) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockOAuth2User := new(MockOAuth2UserPort) + mockDiscordToken := new(MockDiscordTokenPort) + mockRefreshCache := new(MockRefreshTokenCachePort) + tokenService := newTestTokenService() + + service := application.NewOAuth2Service( + mockExchanger, + mockUserInfo, + mockState, + mockOAuth2User, + mockOAuth2DB, + mockDiscordToken, + mockRefreshCache, + tokenService, + ) + + return service, mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache +} + +// ==================== HandleDiscordCallback Tests ==================== + +func TestDiscordService_Callback_NewUser(t *testing.T) { + // Given + service, mockExchanger, mockUserInfo, _, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache := setupDiscordService() + + oauthToken := newTestOAuth2Token() + userInfo := newTestDiscordUserInfo() + + req := &dto.DiscordCallbackRequest{Code: "auth-code"} + + mockExchanger.On("Exchange", "auth-code").Return(oauthToken, nil) + mockUserInfo.On("GetUserInfo", oauthToken).Return(userInfo, nil) + mockOAuth2DB.On("FindDiscordAccountByDiscordId", "123456789").Return(nil, exception.ErrDiscordUserCannotFound) + mockOAuth2User.On("SaveRandomUser", mock.AnythingOfType("*domain.User")).Return(nil). + Run(func(args mock.Arguments) { + user := args.Get(0).(*userDomain.User) + user.Id = 1 + user.Role = userDomain.UserRoleUser + }) + mockOAuth2DB.On("CreateDiscordAccount", mock.AnythingOfType("*domain.DiscordAccount")).Return(nil) + mockDiscordToken.On("SaveToken", mock.AnythingOfType("*dto.DiscordToken")).Return(nil) + mockRefreshCache.On("Save", mock.AnythingOfType("*domain.RefreshToken"), mock.AnythingOfType("*int64")).Return(nil) + + // When + result, err := service.HandleDiscordCallback(req) + + // Then + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsNewUser) + assert.NotEmpty(t, result.AccessToken) + assert.NotEmpty(t, result.RefreshToken) + + mockExchanger.AssertExpectations(t) + mockUserInfo.AssertExpectations(t) + mockOAuth2DB.AssertExpectations(t) + mockOAuth2User.AssertExpectations(t) + mockDiscordToken.AssertExpectations(t) + mockRefreshCache.AssertExpectations(t) +} + +func TestDiscordService_Callback_ExistingUser(t *testing.T) { + // Given + service, mockExchanger, mockUserInfo, _, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache := setupDiscordService() + + oauthToken := newTestOAuth2Token() + userInfo := newTestDiscordUserInfo() + + existingAccount := &oauth2Domain.DiscordAccount{ + DiscordId: "123456789", + UserId: 1, + DiscordAvatar: "old_avatar", + DiscordVerified: true, + } + + existingUser := &userDomain.User{ + Id: 1, + Email: "123456789@discord.oauth", + Username: "discorduser", + Role: userDomain.UserRoleUser, + } + + req := &dto.DiscordCallbackRequest{Code: "auth-code"} + + mockExchanger.On("Exchange", "auth-code").Return(oauthToken, nil) + mockUserInfo.On("GetUserInfo", oauthToken).Return(userInfo, nil) + mockOAuth2DB.On("FindDiscordAccountByDiscordId", "123456789").Return(existingAccount, nil) + mockOAuth2DB.On("UpdateDiscordAccount", mock.AnythingOfType("*domain.DiscordAccount")).Return(nil) + mockOAuth2User.On("FindById", int64(1)).Return(existingUser, nil) + mockDiscordToken.On("SaveToken", mock.AnythingOfType("*dto.DiscordToken")).Return(nil) + mockRefreshCache.On("Save", mock.AnythingOfType("*domain.RefreshToken"), mock.AnythingOfType("*int64")).Return(nil) + + // When + result, err := service.HandleDiscordCallback(req) + + // Then + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.IsNewUser) + assert.NotEmpty(t, result.AccessToken) + assert.NotEmpty(t, result.RefreshToken) + + mockExchanger.AssertExpectations(t) + mockUserInfo.AssertExpectations(t) + mockOAuth2DB.AssertExpectations(t) + mockOAuth2User.AssertExpectations(t) +} + +func TestDiscordService_Callback_TokenExchangeFail(t *testing.T) { + // Given + service, mockExchanger, _, _, _, _, _, _ := setupDiscordService() + + req := &dto.DiscordCallbackRequest{Code: "bad-code"} + + mockExchanger.On("Exchange", "bad-code").Return(nil, errors.New("exchange failed")) + + // When + result, err := service.HandleDiscordCallback(req) + + // Then + assert.Error(t, err) + assert.Nil(t, result) + assert.ErrorIs(t, err, exception.ErrDiscordTokenExchange) + + mockExchanger.AssertExpectations(t) +} + +func TestDiscordService_Callback_UserInfoFail(t *testing.T) { + // Given + service, mockExchanger, mockUserInfo, _, _, _, _, _ := setupDiscordService() + + oauthToken := newTestOAuth2Token() + req := &dto.DiscordCallbackRequest{Code: "auth-code"} + + mockExchanger.On("Exchange", "auth-code").Return(oauthToken, nil) + mockUserInfo.On("GetUserInfo", oauthToken).Return(nil, errors.New("discord API error")) + + // When + result, err := service.HandleDiscordCallback(req) + + // Then + assert.Error(t, err) + assert.Nil(t, result) + assert.ErrorIs(t, err, exception.ErrDiscordCannotGetUserInfo) + + mockExchanger.AssertExpectations(t) + mockUserInfo.AssertExpectations(t) +} + +// ==================== GetDiscordLoginURL Tests ==================== + +func TestDiscordService_GetLoginURL_Success(t *testing.T) { + // Given + service, mockExchanger, _, mockState, _, _, _, _ := setupDiscordService() + + mockState.On("GenerateState").Return("random-state", nil) + mockExchanger.On("AuthCodeURL", "random-state").Return("https://discord.com/oauth2/authorize?state=random-state") + + // When + url, err := service.GetDiscordLoginURL() + + // Then + assert.NoError(t, err) + assert.Contains(t, url, "https://discord.com/oauth2/authorize") + assert.Contains(t, url, "random-state") + + mockState.AssertExpectations(t) + mockExchanger.AssertExpectations(t) +} diff --git a/test/oauth2/presentation/discord_controller_test.go b/test/oauth2/presentation/discord_controller_test.go new file mode 100644 index 0000000..f0cbb03 --- /dev/null +++ b/test/oauth2/presentation/discord_controller_test.go @@ -0,0 +1,421 @@ +package presentation_test + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" + + authDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/domain" + discordDto "github.com/FOR-GAMERS/GAMERS-BE/internal/discord/application/dto" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" + jwtApp "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/application" + jwtDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/domain" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/infra" + "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application/dto" + oauth2Domain "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/domain" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/middleware" + "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/presentation" + userDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/user/domain" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/common/router" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "golang.org/x/oauth2" +) + +// ==================== Mock Definitions ==================== + +type MockTokenExchanger struct { + mock.Mock +} + +func (m *MockTokenExchanger) Exchange(code string) (*oauth2.Token, error) { + args := m.Called(code) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*oauth2.Token), args.Error(1) +} + +func (m *MockTokenExchanger) AuthCodeURL(state string) string { + args := m.Called(state) + return args.String(0) +} + +type MockDiscordUserInfo struct { + mock.Mock +} + +func (m *MockDiscordUserInfo) GetUserInfo(token *oauth2.Token) (*dto.DiscordUserInfo, error) { + args := m.Called(token) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*dto.DiscordUserInfo), args.Error(1) +} + +type MockStateManager struct { + mock.Mock +} + +func (m *MockStateManager) GenerateState() (string, error) { + args := m.Called() + return args.String(0), args.Error(1) +} + +func (m *MockStateManager) ValidateState(state string) error { + args := m.Called(state) + return args.Error(0) +} + +type MockOAuth2DatabasePort struct { + mock.Mock +} + +func (m *MockOAuth2DatabasePort) FindDiscordAccountByDiscordId(discordId string) (*oauth2Domain.DiscordAccount, error) { + args := m.Called(discordId) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*oauth2Domain.DiscordAccount), args.Error(1) +} + +func (m *MockOAuth2DatabasePort) FindDiscordAccountByUserId(userId int64) (*oauth2Domain.DiscordAccount, error) { + args := m.Called(userId) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*oauth2Domain.DiscordAccount), args.Error(1) +} + +func (m *MockOAuth2DatabasePort) CreateDiscordAccount(account *oauth2Domain.DiscordAccount) error { + args := m.Called(account) + return args.Error(0) +} + +func (m *MockOAuth2DatabasePort) UpdateDiscordAccount(account *oauth2Domain.DiscordAccount) error { + args := m.Called(account) + return args.Error(0) +} + +type MockOAuth2UserPort struct { + mock.Mock +} + +func (m *MockOAuth2UserPort) SaveRandomUser(user *userDomain.User) error { + args := m.Called(user) + return args.Error(0) +} + +func (m *MockOAuth2UserPort) FindById(id int64) (*userDomain.User, error) { + args := m.Called(id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*userDomain.User), args.Error(1) +} + +type MockDiscordTokenPort struct { + mock.Mock +} + +func (m *MockDiscordTokenPort) SaveToken(token *discordDto.DiscordToken) error { + args := m.Called(token) + return args.Error(0) +} + +func (m *MockDiscordTokenPort) GetToken(userID int64) (*discordDto.DiscordToken, error) { + args := m.Called(userID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*discordDto.DiscordToken), args.Error(1) +} + +func (m *MockDiscordTokenPort) DeleteToken(userID int64) error { + args := m.Called(userID) + return args.Error(0) +} + +func (m *MockDiscordTokenPort) ExistsToken(userID int64) (bool, error) { + args := m.Called(userID) + return args.Bool(0), args.Error(1) +} + +type MockRefreshTokenCachePort struct { + mock.Mock +} + +func (m *MockRefreshTokenCachePort) Save(token *authDomain.RefreshToken, ttl *int64) error { + args := m.Called(token, ttl) + return args.Error(0) +} + +func (m *MockRefreshTokenCachePort) FindByToken(token *string) (*authDomain.RefreshToken, error) { + args := m.Called(token) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*authDomain.RefreshToken), args.Error(1) +} + +func (m *MockRefreshTokenCachePort) ExistsByToken(token *string) (bool, error) { + args := m.Called(token) + return args.Bool(0), args.Error(1) +} + +func (m *MockRefreshTokenCachePort) Delete(token *string) error { + args := m.Called(token) + return args.Error(0) +} + +func (m *MockRefreshTokenCachePort) DeleteByUserID(userID *int64) error { + args := m.Called(userID) + return args.Error(0) +} + +// ==================== Helpers ==================== + +func newTestTokenService() jwtApp.TokenService { + config := &jwtDomain.Token{ + SecretKey: "test-secret-key-for-access", + RefreshSecretKey: "test-secret-key-for-refresh", + AccessTokenDuration: 5 * time.Minute, + RefreshTokenDuration: 1 * time.Hour, + Issuer: "test-gamers-api", + } + accessStrategy := infra.NewAccessTokenStrategy(config) + refreshStrategy := infra.NewRefreshTokenStrategy(config) + + tokenService := jwtApp.NewTokenService() + tokenService.RegisterStrategy(accessStrategy) + tokenService.RegisterStrategy(refreshStrategy) + + return *tokenService +} + +func setupDiscordController( + mockExchanger *MockTokenExchanger, + mockUserInfo *MockDiscordUserInfo, + mockState *MockStateManager, + mockOAuth2DB *MockOAuth2DatabasePort, + mockOAuth2User *MockOAuth2UserPort, + mockDiscordToken *MockDiscordTokenPort, + mockRefreshCache *MockRefreshTokenCachePort, +) *gin.Engine { + gin.SetMode(gin.TestMode) + engine := gin.New() + engine.Use(middleware.GlobalErrorHandler()) + + tokenService := newTestTokenService() + + oauth2Service := application.NewOAuth2Service( + mockExchanger, + mockUserInfo, + mockState, + mockOAuth2User, + mockOAuth2DB, + mockDiscordToken, + mockRefreshCache, + tokenService, + ) + + r := &router.Router{} + webURL := "http://localhost:3000" + cookieDomain := "" + + controller := presentation.NewDiscordController(r, oauth2Service, webURL, cookieDomain) + + // Register routes directly on test engine + oauth2Group := engine.Group("/api/oauth2") + { + oauth2Group.GET("/discord/login", controller.DiscordLogin) + oauth2Group.GET("/discord/callback", controller.DiscordCallback) + } + + return engine +} + +// ==================== DiscordLogin Tests ==================== + +func TestDiscordController_Login_Redirect(t *testing.T) { + // Given + mockExchanger := new(MockTokenExchanger) + mockUserInfo := new(MockDiscordUserInfo) + mockState := new(MockStateManager) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockOAuth2User := new(MockOAuth2UserPort) + mockDiscordToken := new(MockDiscordTokenPort) + mockRefreshCache := new(MockRefreshTokenCachePort) + + engine := setupDiscordController(mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache) + + mockState.On("GenerateState").Return("random-state", nil) + mockExchanger.On("AuthCodeURL", "random-state").Return("https://discord.com/oauth2/authorize?state=random-state&client_id=test") + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/oauth2/discord/login", nil) + engine.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusFound, w.Code) + location := w.Header().Get("Location") + assert.Contains(t, location, "https://discord.com/oauth2/authorize") + + mockState.AssertExpectations(t) + mockExchanger.AssertExpectations(t) +} + +// ==================== DiscordCallback Tests ==================== + +func TestDiscordController_Callback_UserDenied(t *testing.T) { + // Given + mockExchanger := new(MockTokenExchanger) + mockUserInfo := new(MockDiscordUserInfo) + mockState := new(MockStateManager) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockOAuth2User := new(MockOAuth2UserPort) + mockDiscordToken := new(MockDiscordTokenPort) + mockRefreshCache := new(MockRefreshTokenCachePort) + + engine := setupDiscordController(mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache) + + // When - User denied OAuth2 consent + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback?error=access_denied&error_description=The+user+denied+the+request", nil) + engine.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusFound, w.Code) + location := w.Header().Get("Location") + assert.Contains(t, location, "/login?error=access_denied") +} + +func TestDiscordController_Callback_MissingCode(t *testing.T) { + // Given + mockExchanger := new(MockTokenExchanger) + mockUserInfo := new(MockDiscordUserInfo) + mockState := new(MockStateManager) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockOAuth2User := new(MockOAuth2UserPort) + mockDiscordToken := new(MockDiscordTokenPort) + mockRefreshCache := new(MockRefreshTokenCachePort) + + engine := setupDiscordController(mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache) + + // When - No code and no error + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback", nil) + engine.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusFound, w.Code) + location := w.Header().Get("Location") + assert.Contains(t, location, "/login?error=missing_code") +} + +func TestDiscordController_Callback_Success(t *testing.T) { + // Given + mockExchanger := new(MockTokenExchanger) + mockUserInfo := new(MockDiscordUserInfo) + mockState := new(MockStateManager) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockOAuth2User := new(MockOAuth2UserPort) + mockDiscordToken := new(MockDiscordTokenPort) + mockRefreshCache := new(MockRefreshTokenCachePort) + + engine := setupDiscordController(mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache) + + oauthToken := &oauth2.Token{ + AccessToken: "discord-access-token", + RefreshToken: "discord-refresh-token", + TokenType: "Bearer", + Expiry: time.Now().Add(1 * time.Hour), + } + + userInfo := &dto.DiscordUserInfo{ + Id: "123456789", + Username: "discorduser", + Avatar: "avatar_hash", + Verified: true, + } + + existingAccount := &oauth2Domain.DiscordAccount{ + DiscordId: "123456789", + UserId: 1, + DiscordAvatar: "old_avatar", + DiscordVerified: true, + } + + existingUser := &userDomain.User{ + Id: 1, + Email: "123456789@discord.oauth", + Username: "discorduser", + Role: userDomain.UserRoleUser, + } + + mockExchanger.On("Exchange", "valid-code").Return(oauthToken, nil) + mockUserInfo.On("GetUserInfo", oauthToken).Return(userInfo, nil) + mockOAuth2DB.On("FindDiscordAccountByDiscordId", "123456789").Return(existingAccount, nil) + mockOAuth2DB.On("UpdateDiscordAccount", mock.AnythingOfType("*domain.DiscordAccount")).Return(nil) + mockOAuth2User.On("FindById", int64(1)).Return(existingUser, nil) + mockDiscordToken.On("SaveToken", mock.AnythingOfType("*dto.DiscordToken")).Return(nil) + mockRefreshCache.On("Save", mock.AnythingOfType("*domain.RefreshToken"), mock.AnythingOfType("*int64")).Return(nil) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback?code=valid-code", nil) + engine.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusFound, w.Code) + location := w.Header().Get("Location") + assert.Contains(t, location, "/login/success") + + // Verify cookies were set + cookies := w.Result().Cookies() + cookieNames := make(map[string]bool) + for _, c := range cookies { + cookieNames[c.Name] = true + } + assert.True(t, cookieNames["access_token"]) + assert.True(t, cookieNames["refresh_token"]) + assert.True(t, cookieNames["is_new_user"]) + + mockExchanger.AssertExpectations(t) + mockUserInfo.AssertExpectations(t) + mockOAuth2DB.AssertExpectations(t) + mockOAuth2User.AssertExpectations(t) +} + +func TestDiscordController_Callback_ExchangeFail(t *testing.T) { + // Given + mockExchanger := new(MockTokenExchanger) + mockUserInfo := new(MockDiscordUserInfo) + mockState := new(MockStateManager) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockOAuth2User := new(MockOAuth2UserPort) + mockDiscordToken := new(MockDiscordTokenPort) + mockRefreshCache := new(MockRefreshTokenCachePort) + + engine := setupDiscordController(mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache) + + mockExchanger.On("Exchange", "bad-code").Return(nil, errors.New("exchange failed")) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback?code=bad-code", nil) + engine.ServeHTTP(w, req) + + // Then - The controller calls ctx.Error(err) which doesn't set a status by default + // The error is the BusinessError ErrDiscordTokenExchange with status 401 + assert.Equal(t, exception.ErrDiscordTokenExchange.Status, w.Code) + + mockExchanger.AssertExpectations(t) +} From 1a511115cc95c0ff6789b0999f6d987d3f75106e Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:35:35 +0900 Subject: [PATCH 06/24] Fix: Reflects Code Review --- .../oauth2/application/discord_service.go | 11 +++-- internal/oauth2/application/dto/oauth2_dto.go | 1 + .../oauth2/presentation/discord_controller.go | 3 +- .../application/discord_service_test.go | 41 +++++++++++++++---- .../presentation/discord_controller_test.go | 34 +++++++++++++-- 5 files changed, 75 insertions(+), 15 deletions(-) diff --git a/internal/oauth2/application/discord_service.go b/internal/oauth2/application/discord_service.go index ea8592e..cc0e584 100644 --- a/internal/oauth2/application/discord_service.go +++ b/internal/oauth2/application/discord_service.go @@ -1,8 +1,10 @@ package application import ( - authDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/domain" + "errors" + "fmt" authPort "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/application/port" + authDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/domain" discordDto "github.com/FOR-GAMERS/GAMERS-BE/internal/discord/application/dto" discordPort "github.com/FOR-GAMERS/GAMERS-BE/internal/discord/application/port" "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" @@ -13,8 +15,6 @@ import ( "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application/port" "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/domain" userDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/user/domain" - "errors" - "fmt" ) type DiscordService struct { @@ -59,6 +59,11 @@ func (s *DiscordService) GetDiscordLoginURL() (string, error) { } func (s *DiscordService) HandleDiscordCallback(req *dto.DiscordCallbackRequest) (*dto.OAuth2LoginResponse, error) { + // Validate OAuth2 state for CSRF protection + if err := s.stateManager.ValidateState(req.State); err != nil { + return nil, err + } + token, err := s.tokenExchanger.Exchange(req.Code) if err != nil { return nil, exception.ErrDiscordTokenExchange diff --git a/internal/oauth2/application/dto/oauth2_dto.go b/internal/oauth2/application/dto/oauth2_dto.go index e804c95..b8e5169 100644 --- a/internal/oauth2/application/dto/oauth2_dto.go +++ b/internal/oauth2/application/dto/oauth2_dto.go @@ -2,6 +2,7 @@ package dto type DiscordCallbackRequest struct { Code string `form:"code"` + State string `form:"state"` Error string `form:"error"` ErrorDescription string `form:"error_description"` } diff --git a/internal/oauth2/presentation/discord_controller.go b/internal/oauth2/presentation/discord_controller.go index d5aab54..5ce5b9d 100644 --- a/internal/oauth2/presentation/discord_controller.go +++ b/internal/oauth2/presentation/discord_controller.go @@ -6,6 +6,7 @@ import ( "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application" "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application/dto" "net/http" + "net/url" "os" "strings" @@ -77,7 +78,7 @@ func (c *DiscordController) DiscordCallback(ctx *gin.Context) { // User denied Discord OAuth2 consent if req.Error != "" { - redirectURL := c.webURL + "/login?error=" + req.Error + redirectURL := c.webURL + "/login?error=" + url.QueryEscape(req.Error) ctx.Redirect(http.StatusFound, redirectURL) return } diff --git a/test/oauth2/application/discord_service_test.go b/test/oauth2/application/discord_service_test.go index d6ad020..671e0a0 100644 --- a/test/oauth2/application/discord_service_test.go +++ b/test/oauth2/application/discord_service_test.go @@ -247,13 +247,14 @@ func setupDiscordService() ( func TestDiscordService_Callback_NewUser(t *testing.T) { // Given - service, mockExchanger, mockUserInfo, _, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache := setupDiscordService() + service, mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache := setupDiscordService() oauthToken := newTestOAuth2Token() userInfo := newTestDiscordUserInfo() - req := &dto.DiscordCallbackRequest{Code: "auth-code"} + req := &dto.DiscordCallbackRequest{Code: "auth-code", State: "valid-state"} + mockState.On("ValidateState", "valid-state").Return(nil) mockExchanger.On("Exchange", "auth-code").Return(oauthToken, nil) mockUserInfo.On("GetUserInfo", oauthToken).Return(userInfo, nil) mockOAuth2DB.On("FindDiscordAccountByDiscordId", "123456789").Return(nil, exception.ErrDiscordUserCannotFound) @@ -277,6 +278,7 @@ func TestDiscordService_Callback_NewUser(t *testing.T) { assert.NotEmpty(t, result.AccessToken) assert.NotEmpty(t, result.RefreshToken) + mockState.AssertExpectations(t) mockExchanger.AssertExpectations(t) mockUserInfo.AssertExpectations(t) mockOAuth2DB.AssertExpectations(t) @@ -287,7 +289,7 @@ func TestDiscordService_Callback_NewUser(t *testing.T) { func TestDiscordService_Callback_ExistingUser(t *testing.T) { // Given - service, mockExchanger, mockUserInfo, _, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache := setupDiscordService() + service, mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache := setupDiscordService() oauthToken := newTestOAuth2Token() userInfo := newTestDiscordUserInfo() @@ -306,8 +308,9 @@ func TestDiscordService_Callback_ExistingUser(t *testing.T) { Role: userDomain.UserRoleUser, } - req := &dto.DiscordCallbackRequest{Code: "auth-code"} + req := &dto.DiscordCallbackRequest{Code: "auth-code", State: "valid-state"} + mockState.On("ValidateState", "valid-state").Return(nil) mockExchanger.On("Exchange", "auth-code").Return(oauthToken, nil) mockUserInfo.On("GetUserInfo", oauthToken).Return(userInfo, nil) mockOAuth2DB.On("FindDiscordAccountByDiscordId", "123456789").Return(existingAccount, nil) @@ -326,6 +329,7 @@ func TestDiscordService_Callback_ExistingUser(t *testing.T) { assert.NotEmpty(t, result.AccessToken) assert.NotEmpty(t, result.RefreshToken) + mockState.AssertExpectations(t) mockExchanger.AssertExpectations(t) mockUserInfo.AssertExpectations(t) mockOAuth2DB.AssertExpectations(t) @@ -334,10 +338,11 @@ func TestDiscordService_Callback_ExistingUser(t *testing.T) { func TestDiscordService_Callback_TokenExchangeFail(t *testing.T) { // Given - service, mockExchanger, _, _, _, _, _, _ := setupDiscordService() + service, mockExchanger, _, mockState, _, _, _, _ := setupDiscordService() - req := &dto.DiscordCallbackRequest{Code: "bad-code"} + req := &dto.DiscordCallbackRequest{Code: "bad-code", State: "valid-state"} + mockState.On("ValidateState", "valid-state").Return(nil) mockExchanger.On("Exchange", "bad-code").Return(nil, errors.New("exchange failed")) // When @@ -351,13 +356,33 @@ func TestDiscordService_Callback_TokenExchangeFail(t *testing.T) { mockExchanger.AssertExpectations(t) } +func TestDiscordService_Callback_StateValidationFail(t *testing.T) { + // Given + service, _, _, mockState, _, _, _, _ := setupDiscordService() + + req := &dto.DiscordCallbackRequest{Code: "auth-code", State: "invalid-state"} + + mockState.On("ValidateState", "invalid-state").Return(exception.ErrStateNotFound) + + // When + result, err := service.HandleDiscordCallback(req) + + // Then + assert.Error(t, err) + assert.Nil(t, result) + assert.ErrorIs(t, err, exception.ErrStateNotFound) + + mockState.AssertExpectations(t) +} + func TestDiscordService_Callback_UserInfoFail(t *testing.T) { // Given - service, mockExchanger, mockUserInfo, _, _, _, _, _ := setupDiscordService() + service, mockExchanger, mockUserInfo, mockState, _, _, _, _ := setupDiscordService() oauthToken := newTestOAuth2Token() - req := &dto.DiscordCallbackRequest{Code: "auth-code"} + req := &dto.DiscordCallbackRequest{Code: "auth-code", State: "valid-state"} + mockState.On("ValidateState", "valid-state").Return(nil) mockExchanger.On("Exchange", "auth-code").Return(oauthToken, nil) mockUserInfo.On("GetUserInfo", oauthToken).Return(nil, errors.New("discord API error")) diff --git a/test/oauth2/presentation/discord_controller_test.go b/test/oauth2/presentation/discord_controller_test.go index f0cbb03..5e8ac9f 100644 --- a/test/oauth2/presentation/discord_controller_test.go +++ b/test/oauth2/presentation/discord_controller_test.go @@ -10,13 +10,13 @@ import ( authDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/auth/domain" discordDto "github.com/FOR-GAMERS/GAMERS-BE/internal/discord/application/dto" "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/middleware" jwtApp "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/application" jwtDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/domain" "github.com/FOR-GAMERS/GAMERS-BE/internal/global/security/jwt/infra" "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application" "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application/dto" oauth2Domain "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/domain" - "github.com/FOR-GAMERS/GAMERS-BE/internal/global/middleware" "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/presentation" userDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/user/domain" @@ -360,6 +360,7 @@ func TestDiscordController_Callback_Success(t *testing.T) { Role: userDomain.UserRoleUser, } + mockState.On("ValidateState", "test-state").Return(nil) mockExchanger.On("Exchange", "valid-code").Return(oauthToken, nil) mockUserInfo.On("GetUserInfo", oauthToken).Return(userInfo, nil) mockOAuth2DB.On("FindDiscordAccountByDiscordId", "123456789").Return(existingAccount, nil) @@ -370,7 +371,7 @@ func TestDiscordController_Callback_Success(t *testing.T) { // When w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback?code=valid-code", nil) + req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback?code=valid-code&state=test-state", nil) engine.ServeHTTP(w, req) // Then @@ -388,12 +389,38 @@ func TestDiscordController_Callback_Success(t *testing.T) { assert.True(t, cookieNames["refresh_token"]) assert.True(t, cookieNames["is_new_user"]) + mockState.AssertExpectations(t) mockExchanger.AssertExpectations(t) mockUserInfo.AssertExpectations(t) mockOAuth2DB.AssertExpectations(t) mockOAuth2User.AssertExpectations(t) } +func TestDiscordController_Callback_InvalidState(t *testing.T) { + // Given + mockExchanger := new(MockTokenExchanger) + mockUserInfo := new(MockDiscordUserInfo) + mockState := new(MockStateManager) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockOAuth2User := new(MockOAuth2UserPort) + mockDiscordToken := new(MockDiscordTokenPort) + mockRefreshCache := new(MockRefreshTokenCachePort) + + engine := setupDiscordController(mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache) + + mockState.On("ValidateState", "invalid-state").Return(exception.ErrStateNotFound) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback?code=valid-code&state=invalid-state", nil) + engine.ServeHTTP(w, req) + + // Then - State validation fails, returns 404 (ErrStateNotFound) + assert.Equal(t, exception.ErrStateNotFound.Status, w.Code) + + mockState.AssertExpectations(t) +} + func TestDiscordController_Callback_ExchangeFail(t *testing.T) { // Given mockExchanger := new(MockTokenExchanger) @@ -406,11 +433,12 @@ func TestDiscordController_Callback_ExchangeFail(t *testing.T) { engine := setupDiscordController(mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache) + mockState.On("ValidateState", "test-state").Return(nil) mockExchanger.On("Exchange", "bad-code").Return(nil, errors.New("exchange failed")) // When w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback?code=bad-code", nil) + req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback?code=bad-code&state=test-state", nil) engine.ServeHTTP(w, req) // Then - The controller calls ctx.Error(err) which doesn't set a status by default From 3517ef27adfa6312078fa91b113081b5865220c5 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:25:36 +0900 Subject: [PATCH 07/24] Fix: Add CSRF state validation in controller and fix appErr bug (#61) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move OAuth2 state validation to controller layer for all callback paths (error, missing_code, success) - Fix incorrect error variable usage (err → appErr) in contest application controller - Update tests to reflect state validation changes Co-Authored-By: Claude Opus 4.6 --- .../contest_application_controller.go | 2 +- .../oauth2/application/discord_service.go | 9 +++-- .../oauth2/presentation/discord_controller.go | 7 ++++ .../application/discord_service_test.go | 36 +++++++++++-------- .../presentation/discord_controller_test.go | 14 +++++--- 5 files changed, 43 insertions(+), 25 deletions(-) diff --git a/internal/contest/presentation/contest_application_controller.go b/internal/contest/presentation/contest_application_controller.go index 6b7449b..0a819df 100644 --- a/internal/contest/presentation/contest_application_controller.go +++ b/internal/contest/presentation/contest_application_controller.go @@ -99,7 +99,7 @@ func (c *ContestApplicationController) RequestParticipate(ctx *gin.Context) { Data: existingApp, }) } else { - c.helper.HandleError(ctx, err) + c.helper.HandleError(ctx, appErr) } return } diff --git a/internal/oauth2/application/discord_service.go b/internal/oauth2/application/discord_service.go index cc0e584..1acb543 100644 --- a/internal/oauth2/application/discord_service.go +++ b/internal/oauth2/application/discord_service.go @@ -58,12 +58,11 @@ func (s *DiscordService) GetDiscordLoginURL() (string, error) { return s.tokenExchanger.AuthCodeURL(randomState), nil } -func (s *DiscordService) HandleDiscordCallback(req *dto.DiscordCallbackRequest) (*dto.OAuth2LoginResponse, error) { - // Validate OAuth2 state for CSRF protection - if err := s.stateManager.ValidateState(req.State); err != nil { - return nil, err - } +func (s *DiscordService) ValidateState(state string) error { + return s.stateManager.ValidateState(state) +} +func (s *DiscordService) HandleDiscordCallback(req *dto.DiscordCallbackRequest) (*dto.OAuth2LoginResponse, error) { token, err := s.tokenExchanger.Exchange(req.Code) if err != nil { return nil, exception.ErrDiscordTokenExchange diff --git a/internal/oauth2/presentation/discord_controller.go b/internal/oauth2/presentation/discord_controller.go index 5ce5b9d..4794843 100644 --- a/internal/oauth2/presentation/discord_controller.go +++ b/internal/oauth2/presentation/discord_controller.go @@ -76,6 +76,13 @@ func (c *DiscordController) DiscordCallback(ctx *gin.Context) { return } + // Validate OAuth2 state for CSRF protection + if err := c.oauth2Service.ValidateState(req.State); err != nil { + redirectURL := c.webURL + "/login?error=invalid_state" + ctx.Redirect(http.StatusFound, redirectURL) + return + } + // User denied Discord OAuth2 consent if req.Error != "" { redirectURL := c.webURL + "/login?error=" + url.QueryEscape(req.Error) diff --git a/test/oauth2/application/discord_service_test.go b/test/oauth2/application/discord_service_test.go index 671e0a0..a532c80 100644 --- a/test/oauth2/application/discord_service_test.go +++ b/test/oauth2/application/discord_service_test.go @@ -247,14 +247,13 @@ func setupDiscordService() ( func TestDiscordService_Callback_NewUser(t *testing.T) { // Given - service, mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache := setupDiscordService() + service, mockExchanger, mockUserInfo, _, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache := setupDiscordService() oauthToken := newTestOAuth2Token() userInfo := newTestDiscordUserInfo() req := &dto.DiscordCallbackRequest{Code: "auth-code", State: "valid-state"} - mockState.On("ValidateState", "valid-state").Return(nil) mockExchanger.On("Exchange", "auth-code").Return(oauthToken, nil) mockUserInfo.On("GetUserInfo", oauthToken).Return(userInfo, nil) mockOAuth2DB.On("FindDiscordAccountByDiscordId", "123456789").Return(nil, exception.ErrDiscordUserCannotFound) @@ -278,7 +277,6 @@ func TestDiscordService_Callback_NewUser(t *testing.T) { assert.NotEmpty(t, result.AccessToken) assert.NotEmpty(t, result.RefreshToken) - mockState.AssertExpectations(t) mockExchanger.AssertExpectations(t) mockUserInfo.AssertExpectations(t) mockOAuth2DB.AssertExpectations(t) @@ -289,7 +287,7 @@ func TestDiscordService_Callback_NewUser(t *testing.T) { func TestDiscordService_Callback_ExistingUser(t *testing.T) { // Given - service, mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache := setupDiscordService() + service, mockExchanger, mockUserInfo, _, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache := setupDiscordService() oauthToken := newTestOAuth2Token() userInfo := newTestDiscordUserInfo() @@ -310,7 +308,6 @@ func TestDiscordService_Callback_ExistingUser(t *testing.T) { req := &dto.DiscordCallbackRequest{Code: "auth-code", State: "valid-state"} - mockState.On("ValidateState", "valid-state").Return(nil) mockExchanger.On("Exchange", "auth-code").Return(oauthToken, nil) mockUserInfo.On("GetUserInfo", oauthToken).Return(userInfo, nil) mockOAuth2DB.On("FindDiscordAccountByDiscordId", "123456789").Return(existingAccount, nil) @@ -329,7 +326,6 @@ func TestDiscordService_Callback_ExistingUser(t *testing.T) { assert.NotEmpty(t, result.AccessToken) assert.NotEmpty(t, result.RefreshToken) - mockState.AssertExpectations(t) mockExchanger.AssertExpectations(t) mockUserInfo.AssertExpectations(t) mockOAuth2DB.AssertExpectations(t) @@ -338,11 +334,10 @@ func TestDiscordService_Callback_ExistingUser(t *testing.T) { func TestDiscordService_Callback_TokenExchangeFail(t *testing.T) { // Given - service, mockExchanger, _, mockState, _, _, _, _ := setupDiscordService() + service, mockExchanger, _, _, _, _, _, _ := setupDiscordService() req := &dto.DiscordCallbackRequest{Code: "bad-code", State: "valid-state"} - mockState.On("ValidateState", "valid-state").Return(nil) mockExchanger.On("Exchange", "bad-code").Return(nil, errors.New("exchange failed")) // When @@ -356,33 +351,44 @@ func TestDiscordService_Callback_TokenExchangeFail(t *testing.T) { mockExchanger.AssertExpectations(t) } -func TestDiscordService_Callback_StateValidationFail(t *testing.T) { +func TestDiscordService_ValidateState_Fail(t *testing.T) { // Given service, _, _, mockState, _, _, _, _ := setupDiscordService() - req := &dto.DiscordCallbackRequest{Code: "auth-code", State: "invalid-state"} - mockState.On("ValidateState", "invalid-state").Return(exception.ErrStateNotFound) // When - result, err := service.HandleDiscordCallback(req) + err := service.ValidateState("invalid-state") // Then assert.Error(t, err) - assert.Nil(t, result) assert.ErrorIs(t, err, exception.ErrStateNotFound) mockState.AssertExpectations(t) } +func TestDiscordService_ValidateState_Success(t *testing.T) { + // Given + service, _, _, mockState, _, _, _, _ := setupDiscordService() + + mockState.On("ValidateState", "valid-state").Return(nil) + + // When + err := service.ValidateState("valid-state") + + // Then + assert.NoError(t, err) + + mockState.AssertExpectations(t) +} + func TestDiscordService_Callback_UserInfoFail(t *testing.T) { // Given - service, mockExchanger, mockUserInfo, mockState, _, _, _, _ := setupDiscordService() + service, mockExchanger, mockUserInfo, _, _, _, _, _ := setupDiscordService() oauthToken := newTestOAuth2Token() req := &dto.DiscordCallbackRequest{Code: "auth-code", State: "valid-state"} - mockState.On("ValidateState", "valid-state").Return(nil) mockExchanger.On("Exchange", "auth-code").Return(oauthToken, nil) mockUserInfo.On("GetUserInfo", oauthToken).Return(nil, errors.New("discord API error")) diff --git a/test/oauth2/presentation/discord_controller_test.go b/test/oauth2/presentation/discord_controller_test.go index 5e8ac9f..b7f2e2b 100644 --- a/test/oauth2/presentation/discord_controller_test.go +++ b/test/oauth2/presentation/discord_controller_test.go @@ -286,9 +286,11 @@ func TestDiscordController_Callback_UserDenied(t *testing.T) { engine := setupDiscordController(mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache) + mockState.On("ValidateState", "test-state").Return(nil) + // When - User denied OAuth2 consent w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback?error=access_denied&error_description=The+user+denied+the+request", nil) + req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback?error=access_denied&error_description=The+user+denied+the+request&state=test-state", nil) engine.ServeHTTP(w, req) // Then @@ -309,9 +311,11 @@ func TestDiscordController_Callback_MissingCode(t *testing.T) { engine := setupDiscordController(mockExchanger, mockUserInfo, mockState, mockOAuth2DB, mockOAuth2User, mockDiscordToken, mockRefreshCache) + mockState.On("ValidateState", "test-state").Return(nil) + // When - No code and no error w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback", nil) + req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback?state=test-state", nil) engine.ServeHTTP(w, req) // Then @@ -415,8 +419,10 @@ func TestDiscordController_Callback_InvalidState(t *testing.T) { req, _ := http.NewRequest("GET", "/api/oauth2/discord/callback?code=valid-code&state=invalid-state", nil) engine.ServeHTTP(w, req) - // Then - State validation fails, returns 404 (ErrStateNotFound) - assert.Equal(t, exception.ErrStateNotFound.Status, w.Code) + // Then - State validation fails, redirects to login with error + assert.Equal(t, http.StatusFound, w.Code) + location := w.Header().Get("Location") + assert.Contains(t, location, "/login?error=invalid_state") mockState.AssertExpectations(t) } From a1d0743b84593c0d14cc57c022b755390da2f638 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:57:46 +0900 Subject: [PATCH 08/24] Feat/Refactor/Test/Chore: Valorant tier-based point calculation in contest application - [feat #64] Calculate Valorant tier point (avg of current/peak) on RequestParticipate and propagate stored point to ContestMember on AcceptApplication - [feat #64] Add Point, CurrentTier, PeakTier fields to SenderSnapshot and SenderResponse - [feat #64] NewContestMember now accepts a point parameter instead of hardcoding 0 - [feat #64] Wire SetScoreTablePort in cmd/server.go to avoid circular dependency - [refactor #65] Move getTierPoint logic into ValorantScoreTable.GetTierPoint domain method - [feat #66] Add ErrValorantRankNotFound (VAL007) and nil-guard for tier data in CalculateContestPoint - [test #67] Add unit tests for ContestApplicationService point calculation (10 cases) - [test #67] Add integration tests for end-to-end point persistence with testcontainers - [test #67] Add unit tests for ValorantScoreTable.GetTierPoint domain method - [chore #68] Remove K6 load-test infrastructure (replaced by testcontainers integration tests) Co-Authored-By: Claude Sonnet 4.6 --- cmd/server.go | 3 + .../contest_application_service.go | 47 +- .../contest/application/contest_service.go | 4 +- .../application/dto/application_dto.go | 22 +- .../port/contest_application_redis_port.go | 11 +- internal/contest/domain/contest_member.go | 4 +- .../global/exception/valorant_error_status.go | 1 + internal/point/domain/valorant_score_table.go | 64 +- .../application/valorant_user_service.go | 67 +- load-test/docker-compose.yaml | 61 -- .../grafana/dashboards/k6-load-test.json | 860 ------------------ .../provisioning/dashboards/dashboard.yaml | 12 - .../provisioning/datasources/influxdb.yaml | 10 - load-test/mock-mysql/go.mod | 35 - load-test/mock-mysql/go.sum | 425 --------- load-test/mock-mysql/main.go | 59 -- load-test/mock-mysql/schema.go | 241 ----- load-test/run.sh | 121 --- .../contest_application_service_test.go | 663 ++++++++++++++ test/contest/domain/contest_member_test.go | 8 +- .../contest_member_benchmark_test.go | 2 +- .../contest_member_database_adapter_test.go | 24 +- .../contest_application_integration_test.go | 528 +++++++++++ test/global/support/suite_helpers.go | 72 ++ test/global/support/suite_interfaces.go | 28 + .../application/dto/valorant_dto_test.go | 109 +++ .../application/valorant_service_test.go | 290 ++++++ .../point/domain/valorant_score_table_test.go | 108 +++ .../valorant_score_table_adapter_test.go | 215 +++++ 29 files changed, 2171 insertions(+), 1923 deletions(-) delete mode 100644 load-test/docker-compose.yaml delete mode 100644 load-test/grafana/dashboards/k6-load-test.json delete mode 100644 load-test/grafana/provisioning/dashboards/dashboard.yaml delete mode 100644 load-test/grafana/provisioning/datasources/influxdb.yaml delete mode 100644 load-test/mock-mysql/go.mod delete mode 100644 load-test/mock-mysql/go.sum delete mode 100644 load-test/mock-mysql/main.go delete mode 100644 load-test/mock-mysql/schema.go delete mode 100755 load-test/run.sh create mode 100644 test/contest/application/contest_application_service_test.go create mode 100644 test/contest/integration/contest_application_integration_test.go create mode 100644 test/global/support/suite_helpers.go create mode 100644 test/global/support/suite_interfaces.go create mode 100644 test/point/application/dto/valorant_dto_test.go create mode 100644 test/point/application/valorant_service_test.go create mode 100644 test/point/domain/valorant_score_table_test.go create mode 100644 test/point/infra/persistence/valorant_score_table_adapter_test.go diff --git a/cmd/server.go b/cmd/server.go index 99fa1db..67f69f0 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -163,6 +163,9 @@ func main() { // Notification module - provides SSE real-time notifications notificationDeps := notification.ProvideNotificationDependencies(db, appRouter) + // Wire score table port for point calculation during contest application + contestDeps.ApplicationService.SetScoreTablePort(pointDeps.ScoreTableRepository) + // Wire notification handler to contest and game services contestDeps.ApplicationService.SetNotificationHandler(notificationDeps.Service) gameDeps.TeamService.SetNotificationHandler(notificationDeps.Service) diff --git a/internal/contest/application/contest_application_service.go b/internal/contest/application/contest_application_service.go index 629ab9f..e7bbf62 100644 --- a/internal/contest/application/contest_application_service.go +++ b/internal/contest/application/contest_application_service.go @@ -8,10 +8,12 @@ import ( "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" notificationPort "github.com/FOR-GAMERS/GAMERS-BE/internal/notification/application/port" oauth2Port "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/application/port" + pointPort "github.com/FOR-GAMERS/GAMERS-BE/internal/point/application/port" userQueryPort "github.com/FOR-GAMERS/GAMERS-BE/internal/user/application/port/port" "context" "errors" "log" + "math" "time" ) @@ -23,6 +25,7 @@ type ContestApplicationService struct { oauth2Repository oauth2Port.OAuth2DatabasePort userQueryRepo userQueryPort.UserQueryPort notificationHandler notificationPort.NotificationHandlerPort + scoreTableRepo pointPort.ValorantScoreTableDatabasePort } func NewContestApplicationService( @@ -48,6 +51,11 @@ func (s *ContestApplicationService) SetNotificationHandler(handler notificationP s.notificationHandler = handler } +// SetScoreTablePort sets the score table port (to avoid circular dependency) +func (s *ContestApplicationService) SetScoreTablePort(scoreTableRepo pointPort.ValorantScoreTableDatabasePort) { + s.scoreTableRepo = scoreTableRepo +} + // RequestParticipate - Contest 참가 신청 func (s *ContestApplicationService) RequestParticipate(ctx context.Context, contestId, userId int64) (*dto.DiscordLinkRequiredResponse, error) { // Check if user has linked Discord account @@ -78,11 +86,29 @@ func (s *ContestApplicationService) RequestParticipate(ctx context.Context, cont return nil, err } + // Calculate point from score table if contest has GamePointTableId + calculatedPoint := 0 + if contest.GamePointTableId != nil && s.scoreTableRepo != nil { + scoreTable, scoreErr := s.scoreTableRepo.GetByID(*contest.GamePointTableId) + if scoreErr != nil { + log.Printf("[RequestParticipate] failed to get score table (id=%d): %v", *contest.GamePointTableId, scoreErr) + } else if user.HasValorantLinked() { + currentTierPoint := scoreTable.GetTierPoint(user.GetCurrentTierFullName()) + peakTierPoint := scoreTable.GetTierPoint(user.GetPeakTierFullName()) + calculatedPoint = int(math.Round(float64(currentTierPoint+peakTierPoint) / 2)) + } else { + log.Printf("[RequestParticipate] user %d has no Valorant linked, point set to 0", userId) + } + } + senderSnapshot := &port.SenderSnapshot{ - UserID: user.Id, - Username: user.Username, - Tag: user.Tag, - Avatar: user.Avatar, + UserID: user.Id, + Username: user.Username, + Tag: user.Tag, + Avatar: user.Avatar, + Point: calculatedPoint, + CurrentTier: user.GetCurrentTierFullName(), + PeakTier: user.GetPeakTierFullName(), } ttl := time.Until(contest.StartedAt) @@ -128,12 +154,23 @@ func (s *ContestApplicationService) AcceptApplication(ctx context.Context, conte return err } + // Get the application to retrieve stored point before accepting + application, err := s.applicationRepo.GetApplication(ctx, contestId, userId) + if err != nil { + return err + } + err = s.applicationRepo.AcceptRequest(ctx, contestId, userId, leaderUserId) if err != nil { return err } - member := domain.NewContestMember(userId, contestId, domain.MemberTypeNormal, domain.LeaderTypeMember) + memberPoint := 0 + if application != nil && application.Sender != nil { + memberPoint = application.Sender.Point + } + + member := domain.NewContestMember(userId, contestId, domain.MemberTypeNormal, domain.LeaderTypeMember, memberPoint) if err := s.memberRepo.Save(member); err != nil { // DB 저장 실패 시 Redis 상태 롤백은 하지 않음 (최종적 일관성) // 추후 MigrateAcceptedApplicationsToDatabase에서 재시도됨 diff --git a/internal/contest/application/contest_service.go b/internal/contest/application/contest_service.go index e6eb9e6..6222b91 100644 --- a/internal/contest/application/contest_service.go +++ b/internal/contest/application/contest_service.go @@ -329,7 +329,7 @@ func (c *ContestService) startTournamentContest(ctx context.Context, contest *do if m.MemberType == gameDomain.TeamMemberTypeLeader { leaderType = domain.LeaderTypeLeader } - member := domain.NewContestMember(m.UserID, contest.ContestID, domain.MemberTypeNormal, leaderType) + member := domain.NewContestMember(m.UserID, contest.ContestID, domain.MemberTypeNormal, leaderType, 0) members = append(members, member) } } @@ -384,7 +384,7 @@ func (c *ContestService) startNonTournamentContest(ctx context.Context, contest if len(acceptedUserIDs) > 0 { members := make([]*domain.ContestMember, 0, len(acceptedUserIDs)) for _, userID := range acceptedUserIDs { - member := domain.NewContestMember(userID, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember) + member := domain.NewContestMember(userID, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0) members = append(members, member) } diff --git a/internal/contest/application/dto/application_dto.go b/internal/contest/application/dto/application_dto.go index 8ae4197..0f5c8e7 100644 --- a/internal/contest/application/dto/application_dto.go +++ b/internal/contest/application/dto/application_dto.go @@ -6,10 +6,13 @@ import ( ) type SenderResponse struct { - UserID int64 `json:"user_id"` - Username string `json:"username"` - Tag string `json:"tag"` - Avatar string `json:"avatar,omitempty"` + UserID int64 `json:"user_id"` + Username string `json:"username"` + Tag string `json:"tag"` + Avatar string `json:"avatar,omitempty"` + Point int `json:"point"` + CurrentTier string `json:"current_tier,omitempty"` + PeakTier string `json:"peak_tier,omitempty"` } type ApplicationResponse struct { @@ -26,10 +29,13 @@ func ToApplicationResponse(app *port.ContestApplication) *ApplicationResponse { var sender *SenderResponse if app.Sender != nil { sender = &SenderResponse{ - UserID: app.Sender.UserID, - Username: app.Sender.Username, - Tag: app.Sender.Tag, - Avatar: app.Sender.Avatar, + UserID: app.Sender.UserID, + Username: app.Sender.Username, + Tag: app.Sender.Tag, + Avatar: app.Sender.Avatar, + Point: app.Sender.Point, + CurrentTier: app.Sender.CurrentTier, + PeakTier: app.Sender.PeakTier, } } diff --git a/internal/contest/application/port/contest_application_redis_port.go b/internal/contest/application/port/contest_application_redis_port.go index db2cabb..3ceb43b 100644 --- a/internal/contest/application/port/contest_application_redis_port.go +++ b/internal/contest/application/port/contest_application_redis_port.go @@ -15,10 +15,13 @@ const ( // SenderSnapshot stores user information at the time of application type SenderSnapshot struct { - UserID int64 `json:"user_id"` - Username string `json:"username"` - Tag string `json:"tag"` - Avatar string `json:"avatar,omitempty"` + UserID int64 `json:"user_id"` + Username string `json:"username"` + Tag string `json:"tag"` + Avatar string `json:"avatar,omitempty"` + Point int `json:"point"` + CurrentTier string `json:"current_tier,omitempty"` + PeakTier string `json:"peak_tier,omitempty"` } type ContestApplication struct { diff --git a/internal/contest/domain/contest_member.go b/internal/contest/domain/contest_member.go index d2d55e2..4716e13 100644 --- a/internal/contest/domain/contest_member.go +++ b/internal/contest/domain/contest_member.go @@ -46,13 +46,13 @@ func NewContestMemberAsLeader(userID, contestID int64) *ContestMember { } } -func NewContestMember(userID, contestID int64, memberType MemberType, leaderType LeaderType) *ContestMember { +func NewContestMember(userID, contestID int64, memberType MemberType, leaderType LeaderType, point int) *ContestMember { return &ContestMember{ UserID: userID, ContestID: contestID, MemberType: memberType, LeaderType: leaderType, - Point: 0, + Point: point, } } diff --git a/internal/global/exception/valorant_error_status.go b/internal/global/exception/valorant_error_status.go index 662e127..f775bbb 100644 --- a/internal/global/exception/valorant_error_status.go +++ b/internal/global/exception/valorant_error_status.go @@ -9,4 +9,5 @@ var ( ErrValorantAlreadyLinked = NewBusinessError(http.StatusConflict, "Valorant account is already linked", "VAL004") ErrValorantPlayerNotFound = NewNotFoundError("Valorant player not found", "VAL005") ErrValorantApiRateLimit = NewBusinessError(http.StatusTooManyRequests, "Valorant API rate limit exceeded, please try again later", "VAL006") + ErrValorantRankNotFound = NewNotFoundError("Valorant rank information not found. Please refresh your Valorant data", "VAL007") ) diff --git a/internal/point/domain/valorant_score_table.go b/internal/point/domain/valorant_score_table.go index b804b14..d727678 100644 --- a/internal/point/domain/valorant_score_table.go +++ b/internal/point/domain/valorant_score_table.go @@ -1,6 +1,9 @@ package domain -import "time" +import ( + "strings" + "time" +) type ValorantScoreTable struct { ScoreTableID int64 `gorm:"column:score_table_id;primaryKey;autoIncrement" json:"score_table_id"` @@ -37,6 +40,65 @@ func (v *ValorantScoreTable) TableName() string { return "valorant_score_tables" } +// GetTierPoint returns the point value for a given tier full name (e.g., "Diamond 1", "Radiant") +func (v *ValorantScoreTable) GetTierPoint(tierFullName string) int { + tierFullName = strings.ToLower(tierFullName) + switch tierFullName { + case "radiant": + return v.Radiant + case "immortal 3": + return v.Immortal3 + case "immortal 2": + return v.Immortal2 + case "immortal 1": + return v.Immortal1 + case "ascendant 3": + return v.Ascendant3 + case "ascendant 2": + return v.Ascendant2 + case "ascendant 1": + return v.Ascendant1 + case "diamond 3": + return v.Diamond3 + case "diamond 2": + return v.Diamond2 + case "diamond 1": + return v.Diamond1 + case "platinum 3": + return v.Platinum3 + case "platinum 2": + return v.Platinum2 + case "platinum 1": + return v.Platinum1 + case "gold 3": + return v.Gold3 + case "gold 2": + return v.Gold2 + case "gold 1": + return v.Gold1 + case "silver 3": + return v.Silver3 + case "silver 2": + return v.Silver2 + case "silver 1": + return v.Silver1 + case "bronze 3": + return v.Bronze3 + case "bronze 2": + return v.Bronze2 + case "bronze 1": + return v.Bronze1 + case "iron 3": + return v.Iron3 + case "iron 2": + return v.Iron2 + case "iron 1": + return v.Iron1 + default: + return 0 + } +} + func NewValorantScoreTable( radiant int, immortal3, immortal2, immortal1 int, diff --git a/internal/valorant/application/valorant_user_service.go b/internal/valorant/application/valorant_user_service.go index b7129b5..71c5f8b 100644 --- a/internal/valorant/application/valorant_user_service.go +++ b/internal/valorant/application/valorant_user_service.go @@ -2,7 +2,6 @@ package application import ( "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" - pointDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/point/domain" pointPort "github.com/FOR-GAMERS/GAMERS-BE/internal/point/application/port" userCommandPort "github.com/FOR-GAMERS/GAMERS-BE/internal/user/application/port/command" userQueryPort "github.com/FOR-GAMERS/GAMERS-BE/internal/user/application/port/port" @@ -205,6 +204,11 @@ func (s *ValorantUserService) CalculateContestPoint(userId int64, scoreTableId i return nil, exception.ErrValorantNotLinked } + // Check if tier information is available + if user.CurrentTierPatched == nil || user.PeakTierPatched == nil { + return nil, exception.ErrValorantRankNotFound + } + // Get score table scoreTable, err := s.scoreTablePort.GetByID(scoreTableId) if err != nil { @@ -212,8 +216,8 @@ func (s *ValorantUserService) CalculateContestPoint(userId int64, scoreTableId i } // Calculate points - currentTierPoint := getTierPoint(user.GetCurrentTierFullName(), scoreTable) - peakTierPoint := getTierPoint(user.GetPeakTierFullName(), scoreTable) + currentTierPoint := scoreTable.GetTierPoint(user.GetCurrentTierFullName()) + peakTierPoint := scoreTable.GetTierPoint(user.GetPeakTierFullName()) finalPoint := int(math.Round(float64(currentTierPoint+peakTierPoint) / 2)) refreshNeeded := user.IsValorantRefreshNeeded() @@ -248,60 +252,3 @@ func isValidRegion(region string) bool { return validRegions[strings.ToLower(region)] } -func getTierPoint(tierFullName string, scoreTable *pointDomain.ValorantScoreTable) int { - tierFullName = strings.ToLower(tierFullName) - switch tierFullName { - case "radiant": - return scoreTable.Radiant - case "immortal 3": - return scoreTable.Immortal3 - case "immortal 2": - return scoreTable.Immortal2 - case "immortal 1": - return scoreTable.Immortal1 - case "ascendant 3": - return scoreTable.Ascendant3 - case "ascendant 2": - return scoreTable.Ascendant2 - case "ascendant 1": - return scoreTable.Ascendant1 - case "diamond 3": - return scoreTable.Diamond3 - case "diamond 2": - return scoreTable.Diamond2 - case "diamond 1": - return scoreTable.Diamond1 - case "platinum 3": - return scoreTable.Platinum3 - case "platinum 2": - return scoreTable.Platinum2 - case "platinum 1": - return scoreTable.Platinum1 - case "gold 3": - return scoreTable.Gold3 - case "gold 2": - return scoreTable.Gold2 - case "gold 1": - return scoreTable.Gold1 - case "silver 3": - return scoreTable.Silver3 - case "silver 2": - return scoreTable.Silver2 - case "silver 1": - return scoreTable.Silver1 - case "bronze 3": - return scoreTable.Bronze3 - case "bronze 2": - return scoreTable.Bronze2 - case "bronze 1": - return scoreTable.Bronze1 - case "iron 3": - return scoreTable.Iron3 - case "iron 2": - return scoreTable.Iron2 - case "iron 1": - return scoreTable.Iron1 - default: - return 0 - } -} diff --git a/load-test/docker-compose.yaml b/load-test/docker-compose.yaml deleted file mode 100644 index e71c433..0000000 --- a/load-test/docker-compose.yaml +++ /dev/null @@ -1,61 +0,0 @@ -services: - # ─── InfluxDB: k6 결과 저장소 ─── - influxdb: - image: influxdb:1.8 - container_name: gamers-influxdb - ports: - - "8086:8086" - environment: - - INFLUXDB_DB=k6 - - INFLUXDB_HTTP_MAX_BODY_SIZE=0 - volumes: - - influxdb-data:/var/lib/influxdb - networks: - - gamers-network - - # ─── Grafana: 대시보드 시각화 ─── - grafana: - image: grafana/grafana:10.4.0 - container_name: gamers-grafana - ports: - - "3001:3000" - environment: - - GF_SECURITY_ADMIN_USER=admin - - GF_SECURITY_ADMIN_PASSWORD=admin - - GF_USERS_ALLOW_SIGN_UP=false - - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/var/lib/grafana/dashboards/k6-load-test.json - volumes: - - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources - - ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards - - ./grafana/dashboards:/var/lib/grafana/dashboards - - grafana-data:/var/lib/grafana - depends_on: - - influxdb - networks: - - gamers-network - - # ─── k6: 부하 테스트 실행 ─── - k6: - image: grafana/k6:0.50.0 - container_name: gamers-k6 - volumes: - - ./scripts:/scripts - environment: - - K6_OUT=influxdb=http://influxdb:8086/k6 - networks: - - gamers-network - depends_on: - - influxdb - # 기본적으로 team-invite 시나리오 실행. run.sh로 덮어쓸 수 있음 - entrypoint: ["k6"] - command: ["run", "--out", "influxdb=http://influxdb:8086/k6", "/scripts/scenario-team-invite.js"] - profiles: - - run # docker compose --profile run up k6 로 실행 - -volumes: - influxdb-data: - grafana-data: - -networks: - gamers-network: - external: true diff --git a/load-test/grafana/dashboards/k6-load-test.json b/load-test/grafana/dashboards/k6-load-test.json deleted file mode 100644 index a834e9a..0000000 --- a/load-test/grafana/dashboards/k6-load-test.json +++ /dev/null @@ -1,860 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 1, - "id": null, - "links": [], - "panels": [ - { - "gridPos": { "h": 2, "w": 24, "x": 0, "y": 0 }, - "id": 100, - "options": { - "content": "# GAMERS Load Test Dashboard\nReal-time k6 부하 테스트 결과 모니터링", - "mode": "markdown" - }, - "title": "", - "type": "text" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 300 }, - { "color": "red", "value": 500 } - ] - }, - "unit": "ms" - } - }, - "gridPos": { "h": 4, "w": 6, "x": 0, "y": 2 }, - "id": 1, - "options": { - "reduceOptions": { - "calcs": ["mean"], - "fields": "", - "values": false - } - }, - "targets": [ - { - "query": "SELECT mean(\"value\") FROM \"http_req_duration\" WHERE $timeFilter", - "rawQuery": true, - "refId": "A" - } - ], - "title": "Avg Response Time", - "type": "stat" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 500 }, - { "color": "red", "value": 1500 } - ] - }, - "unit": "ms" - } - }, - "gridPos": { "h": 4, "w": 6, "x": 6, "y": 2 }, - "id": 2, - "options": { - "reduceOptions": { - "calcs": ["max"], - "fields": "", - "values": false - } - }, - "targets": [ - { - "query": "SELECT percentile(\"value\", 95) FROM \"http_req_duration\" WHERE $timeFilter", - "rawQuery": true, - "refId": "A" - } - ], - "title": "P95 Response Time", - "type": "stat" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "green", "value": null }, - { "color": "red", "value": 0.05 } - ] - }, - "unit": "percentunit" - } - }, - "gridPos": { "h": 4, "w": 6, "x": 12, "y": 2 }, - "id": 3, - "options": { - "reduceOptions": { - "calcs": ["mean"], - "fields": "", - "values": false - } - }, - "targets": [ - { - "query": "SELECT mean(\"value\") FROM \"http_req_failed\" WHERE $timeFilter", - "rawQuery": true, - "refId": "A" - } - ], - "title": "Error Rate", - "type": "stat" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "blue", "value": null } - ] - }, - "unit": "short" - } - }, - "gridPos": { "h": 4, "w": 6, "x": 18, "y": 2 }, - "id": 4, - "options": { - "reduceOptions": { - "calcs": ["max"], - "fields": "", - "values": false - } - }, - "targets": [ - { - "query": "SELECT max(\"value\") FROM \"vus\" WHERE $timeFilter", - "rawQuery": true, - "refId": "A" - } - ], - "title": "Peak Virtual Users", - "type": "stat" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 10 - }, - "unit": "ms" - }, - "overrides": [ - { - "matcher": { "id": "byName", "options": "p95" }, - "properties": [{ "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byName", "options": "p99" }, - "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byName", "options": "avg" }, - "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] - } - ] - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 }, - "id": 5, - "options": { - "tooltip": { "mode": "multi" } - }, - "targets": [ - { - "alias": "avg", - "query": "SELECT mean(\"value\") FROM \"http_req_duration\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "A" - }, - { - "alias": "p95", - "query": "SELECT percentile(\"value\", 95) FROM \"http_req_duration\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "B" - }, - { - "alias": "p99", - "query": "SELECT percentile(\"value\", 99) FROM \"http_req_duration\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "C" - } - ], - "title": "Response Time (avg / p95 / p99)", - "type": "timeseries" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 15 - }, - "unit": "reqps" - } - }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 }, - "id": 6, - "targets": [ - { - "alias": "RPS", - "query": "SELECT count(\"value\") FROM \"http_reqs\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "A" - } - ], - "title": "Requests Per Second", - "type": "timeseries" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 10 - }, - "unit": "short" - } - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 14 }, - "id": 7, - "targets": [ - { - "alias": "Active VUs", - "query": "SELECT mean(\"value\") FROM \"vus\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "A" - } - ], - "title": "Active Virtual Users", - "type": "timeseries" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 20 - }, - "unit": "percentunit" - } - }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 14 }, - "id": 8, - "targets": [ - { - "alias": "Error Rate", - "query": "SELECT mean(\"value\") FROM \"http_req_failed\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "A" - } - ], - "title": "Error Rate Over Time", - "type": "timeseries" - }, - { - "gridPos": { "h": 2, "w": 24, "x": 0, "y": 22 }, - "id": 101, - "options": { - "content": "## Per-Endpoint Breakdown", - "mode": "markdown" - }, - "title": "", - "type": "text" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 10 - }, - "unit": "ms" - } - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 }, - "id": 9, - "targets": [ - { - "alias": "$tag_name", - "query": "SELECT mean(\"value\") FROM \"http_req_duration\" WHERE $timeFilter GROUP BY time($__interval), \"name\"", - "rawQuery": true, - "refId": "A" - } - ], - "title": "Response Time by Endpoint", - "type": "timeseries" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 10 - }, - "unit": "short" - } - }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 }, - "id": 10, - "targets": [ - { - "alias": "$tag_name", - "query": "SELECT count(\"value\") FROM \"http_reqs\" WHERE $timeFilter GROUP BY time($__interval), \"name\"", - "rawQuery": true, - "refId": "A" - } - ], - "title": "Request Count by Endpoint", - "type": "timeseries" - }, - { - "gridPos": { "h": 2, "w": 24, "x": 0, "y": 32 }, - "id": 102, - "options": { - "content": "## Custom Metrics (Scenario-Specific)", - "mode": "markdown" - }, - "title": "", - "type": "text" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 10 - }, - "unit": "ms" - } - }, - "gridPos": { "h": 8, "w": 8, "x": 0, "y": 34 }, - "id": 11, - "targets": [ - { - "alias": "Invite Latency", - "query": "SELECT mean(\"value\") FROM \"invite_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "A" - }, - { - "alias": "Accept Latency", - "query": "SELECT mean(\"value\") FROM \"accept_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "B" - } - ], - "title": "Team Invite / Accept Latency", - "type": "timeseries" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 10 - }, - "unit": "ms" - } - }, - "gridPos": { "h": 8, "w": 8, "x": 8, "y": 34 }, - "id": 12, - "targets": [ - { - "alias": "Submit Latency", - "query": "SELECT mean(\"value\") FROM \"submit_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "A" - }, - { - "alias": "Application Accept Latency", - "query": "SELECT mean(\"value\") FROM \"accept_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "B" - } - ], - "title": "Application Submit / Accept Latency", - "type": "timeseries" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 10 - }, - "unit": "ms" - } - }, - "gridPos": { "h": 8, "w": 8, "x": 16, "y": 34 }, - "id": 13, - "targets": [ - { - "alias": "Contest Start Latency", - "query": "SELECT mean(\"value\") FROM \"contest_start_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "A" - }, - { - "alias": "Tournament Create Latency", - "query": "SELECT mean(\"value\") FROM \"tournament_create_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "B" - } - ], - "title": "Contest Start / Tournament Create Latency", - "type": "timeseries" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "unit": "short" - } - }, - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 42 }, - "id": 14, - "targets": [ - { - "alias": "Invites Sent", - "query": "SELECT sum(\"value\") FROM \"invite_sent\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "A" - }, - { - "alias": "Invites Accepted", - "query": "SELECT sum(\"value\") FROM \"invite_accepted\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "B" - }, - { - "alias": "Applications Submitted", - "query": "SELECT sum(\"value\") FROM \"application_submitted\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "C" - }, - { - "alias": "Contests Started", - "query": "SELECT sum(\"value\") FROM \"contest_started\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "D" - }, - { - "alias": "Tournaments Created", - "query": "SELECT sum(\"value\") FROM \"tournament_created\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "E" - } - ], - "title": "Business Metrics (Cumulative Counts)", - "type": "timeseries" - }, - { - "gridPos": { "h": 2, "w": 24, "x": 0, "y": 50 }, - "id": 103, - "options": { - "content": "## Team Query (N+1 Performance Test)", - "mode": "markdown" - }, - "title": "", - "type": "text" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 300 }, - { "color": "red", "value": 500 } - ] - }, - "unit": "ms" - } - }, - "gridPos": { "h": 4, "w": 6, "x": 0, "y": 52 }, - "id": 20, - "options": { - "reduceOptions": { - "calcs": ["mean"], - "fields": "", - "values": false - } - }, - "targets": [ - { - "query": "SELECT mean(\"value\") FROM \"team_query_latency\" WHERE $timeFilter", - "rawQuery": true, - "refId": "A" - } - ], - "title": "Avg Team Query Latency", - "type": "stat" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 300 }, - { "color": "red", "value": 500 } - ] - }, - "unit": "ms" - } - }, - "gridPos": { "h": 4, "w": 6, "x": 6, "y": 52 }, - "id": 21, - "options": { - "reduceOptions": { - "calcs": ["mean"], - "fields": "", - "values": false - } - }, - "targets": [ - { - "query": "SELECT mean(\"value\") FROM \"members_query_latency\" WHERE $timeFilter", - "rawQuery": true, - "refId": "A" - } - ], - "title": "Avg Members Query Latency", - "type": "stat" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 500 }, - { "color": "red", "value": 1500 } - ] - }, - "unit": "ms" - } - }, - "gridPos": { "h": 4, "w": 6, "x": 12, "y": 52 }, - "id": 22, - "options": { - "reduceOptions": { - "calcs": ["max"], - "fields": "", - "values": false - } - }, - "targets": [ - { - "query": "SELECT percentile(\"value\", 95) FROM \"team_query_latency\" WHERE $timeFilter", - "rawQuery": true, - "refId": "A" - } - ], - "title": "P95 Team Query Latency", - "type": "stat" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 500 }, - { "color": "red", "value": 1500 } - ] - }, - "unit": "ms" - } - }, - "gridPos": { "h": 4, "w": 6, "x": 18, "y": 52 }, - "id": 23, - "options": { - "reduceOptions": { - "calcs": ["max"], - "fields": "", - "values": false - } - }, - "targets": [ - { - "query": "SELECT percentile(\"value\", 95) FROM \"members_query_latency\" WHERE $timeFilter", - "rawQuery": true, - "refId": "A" - } - ], - "title": "P95 Members Query Latency", - "type": "stat" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 10 - }, - "unit": "ms" - }, - "overrides": [ - { - "matcher": { "id": "byName", "options": "Team Query avg" }, - "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byName", "options": "Team Query p95" }, - "properties": [{ "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byName", "options": "Members Query avg" }, - "properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byName", "options": "Members Query p95" }, - "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] - } - ] - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 56 }, - "id": 24, - "options": { - "tooltip": { "mode": "multi" } - }, - "targets": [ - { - "alias": "Team Query avg", - "query": "SELECT mean(\"value\") FROM \"team_query_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "A" - }, - { - "alias": "Team Query p95", - "query": "SELECT percentile(\"value\", 95) FROM \"team_query_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "B" - }, - { - "alias": "Members Query avg", - "query": "SELECT mean(\"value\") FROM \"members_query_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "C" - }, - { - "alias": "Members Query p95", - "query": "SELECT percentile(\"value\", 95) FROM \"members_query_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "D" - } - ], - "title": "Team Query Latency (avg / p95)", - "type": "timeseries" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 15 - }, - "unit": "short" - }, - "overrides": [ - { - "matcher": { "id": "byName", "options": "Team Queries" }, - "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byName", "options": "Members Queries" }, - "properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }] - } - ] - }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 56 }, - "id": 25, - "options": { - "tooltip": { "mode": "multi" } - }, - "targets": [ - { - "alias": "Team Queries", - "query": "SELECT sum(\"value\") FROM \"team_query_count\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "A" - }, - { - "alias": "Members Queries", - "query": "SELECT sum(\"value\") FROM \"members_query_count\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "B" - } - ], - "title": "Team Query Request Count", - "type": "timeseries" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 20 - }, - "unit": "percentunit" - }, - "overrides": [ - { - "matcher": { "id": "byName", "options": "Team Query Failed" }, - "properties": [{ "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byName", "options": "Members Query Failed" }, - "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] - } - ] - }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 64 }, - "id": 26, - "options": { - "tooltip": { "mode": "multi" } - }, - "targets": [ - { - "alias": "Team Query Failed", - "query": "SELECT mean(\"value\") FROM \"team_query_failed_rate\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "A" - }, - { - "alias": "Members Query Failed", - "query": "SELECT mean(\"value\") FROM \"members_query_failed_rate\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "B" - } - ], - "title": "Team Query Error Rate", - "type": "timeseries" - }, - { - "datasource": "InfluxDB-k6", - "fieldConfig": { - "defaults": { - "custom": { - "lineWidth": 2, - "fillOpacity": 10 - }, - "unit": "ms" - }, - "overrides": [ - { - "matcher": { "id": "byName", "options": "Team Query p99" }, - "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byName", "options": "Members Query p99" }, - "properties": [{ "id": "color", "value": { "fixedColor": "purple", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byName", "options": "Team Query p50" }, - "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] - }, - { - "matcher": { "id": "byName", "options": "Members Query p50" }, - "properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }] - } - ] - }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 64 }, - "id": 27, - "options": { - "tooltip": { "mode": "multi" } - }, - "targets": [ - { - "alias": "Team Query p50", - "query": "SELECT percentile(\"value\", 50) FROM \"team_query_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "A" - }, - { - "alias": "Team Query p99", - "query": "SELECT percentile(\"value\", 99) FROM \"team_query_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "B" - }, - { - "alias": "Members Query p50", - "query": "SELECT percentile(\"value\", 50) FROM \"members_query_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "C" - }, - { - "alias": "Members Query p99", - "query": "SELECT percentile(\"value\", 99) FROM \"members_query_latency\" WHERE $timeFilter GROUP BY time($__interval)", - "rawQuery": true, - "refId": "D" - } - ], - "title": "Team Query Latency Distribution (p50 / p99)", - "type": "timeseries" - } - ], - "refresh": "5s", - "schemaVersion": 39, - "tags": ["k6", "load-test", "gamers"], - "templating": { "list": [] }, - "time": { "from": "now-15m", "to": "now" }, - "timepicker": {}, - "timezone": "Asia/Tokyo", - "title": "GAMERS - Load Test Dashboard", - "uid": "gamers-k6-load-test", - "version": 1 -} diff --git a/load-test/grafana/provisioning/dashboards/dashboard.yaml b/load-test/grafana/provisioning/dashboards/dashboard.yaml deleted file mode 100644 index 0120295..0000000 --- a/load-test/grafana/provisioning/dashboards/dashboard.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: 1 - -providers: - - name: 'k6-load-test' - orgId: 1 - folder: 'Load Tests' - type: file - disableDeletion: false - editable: true - options: - path: /var/lib/grafana/dashboards - foldersFromFilesStructure: false diff --git a/load-test/grafana/provisioning/datasources/influxdb.yaml b/load-test/grafana/provisioning/datasources/influxdb.yaml deleted file mode 100644 index 34a3760..0000000 --- a/load-test/grafana/provisioning/datasources/influxdb.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: 1 - -datasources: - - name: InfluxDB-k6 - type: influxdb - access: proxy - url: http://influxdb:8086 - database: k6 - isDefault: true - editable: true diff --git a/load-test/mock-mysql/go.mod b/load-test/mock-mysql/go.mod deleted file mode 100644 index 70b734a..0000000 --- a/load-test/mock-mysql/go.mod +++ /dev/null @@ -1,35 +0,0 @@ -module mock-mysql - -go 1.23.3 - -require github.com/dolthub/go-mysql-server v0.19.0 - -require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 // indirect - github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 // indirect - github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect - github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 // indirect - github.com/go-kit/kit v0.10.0 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/lestrrat-go/strftime v1.0.4 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/shopspring/decimal v1.3.1 // indirect - github.com/sirupsen/logrus v1.8.1 // indirect - github.com/tetratelabs/wazero v1.8.2 // indirect - go.opentelemetry.io/otel v1.31.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 // indirect - golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/text v0.6.0 // indirect - golang.org/x/tools v0.13.0 // indirect - google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect - google.golang.org/grpc v1.53.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect - gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect -) diff --git a/load-test/mock-mysql/go.sum b/load-test/mock-mysql/go.sum deleted file mode 100644 index 81d7081..0000000 --- a/load-test/mock-mysql/go.sum +++ /dev/null @@ -1,425 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= -github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww= -github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY= -github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90 h1:Sni8jrP0sy/w9ZYXoff4g/ixe+7bFCZlfCqXKJSU+zM= -github.com/dolthub/go-icu-regex v0.0.0-20241215010122-db690dd53c90/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA= -github.com/dolthub/go-mysql-server v0.19.0 h1:NdcXyGt9v7m4sQOahU+ss++iyPy4Q3viuVvbnn3rUTQ= -github.com/dolthub/go-mysql-server v0.19.0/go.mod h1:elfIatfq2fkU5lqTBrTcpL0RcHZOgYPE8EzBD7yQFiY= -github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ= -github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= -github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54 h1:nzBnC0Rt1gFtscJEz4veYd/mazZEdbdmed+tujdaKOo= -github.com/dolthub/vitess v0.0.0-20241211024425-b00987f7ba54/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= -github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d h1:QQP1nE4qh5aHTGvI1LgOFxZYVxYoGeMfbNHikogPyoA= -github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= -github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= -github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= -github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8= -github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= -github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= -github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= -github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= -github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= -github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= -github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/src-d/go-errors.v1 v1.0.0 h1:cooGdZnCjYbeS1zb1s6pVAAimTdKceRrpn7aKOnNIfc= -gopkg.in/src-d/go-errors.v1 v1.0.0/go.mod h1:q1cBlomlw2FnDBDNGlnh6X0jPihy+QxZfMMNxPCbdYg= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/load-test/mock-mysql/main.go b/load-test/mock-mysql/main.go deleted file mode 100644 index feace36..0000000 --- a/load-test/mock-mysql/main.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "fmt" - "log" - - sqle "github.com/dolthub/go-mysql-server" - "github.com/dolthub/go-mysql-server/memory" - "github.com/dolthub/go-mysql-server/server" - "github.com/dolthub/go-mysql-server/sql" - "github.com/dolthub/go-mysql-server/sql/information_schema" -) - -func main() { - dbProvider := memory.NewDBProvider( - memory.NewDatabase("gamers"), - information_schema.NewInformationSchemaDatabase(), - ) - - engine := sqle.NewDefault(dbProvider) - - ctx := sql.NewEmptyContext() - ctx.SetCurrentDatabase("gamers") - - for _, stmt := range createTableStatements { - _, _, _, err := engine.Query(ctx, stmt) - if err != nil { - log.Fatalf("Failed to create table: %v\nSQL: %s", err, stmt) - } - } - - log.Println("All 12 tables created successfully") - - // Insert seed data for load testing - for _, stmt := range seedDataStatements { - _, _, _, err := engine.Query(ctx, stmt) - if err != nil { - log.Printf("Warning: seed data insert failed: %v\nSQL: %s", err, stmt) - } - } - log.Printf("Seed data inserted: %d statements executed", len(seedDataStatements)) - - config := server.Config{ - Protocol: "tcp", - Address: "0.0.0.0:3306", - } - - s, err := server.NewServer(config, engine, memory.NewSessionBuilder(dbProvider), nil) - if err != nil { - log.Fatalf("Failed to create server: %v", err) - } - - fmt.Println("Mock MySQL server listening on 0.0.0.0:3306") - fmt.Println("Database: gamers | User: root (no password)") - - if err := s.Start(); err != nil { - log.Fatalf("Failed to start server: %v", err) - } -} diff --git a/load-test/mock-mysql/schema.go b/load-test/mock-mysql/schema.go deleted file mode 100644 index e619e7e..0000000 --- a/load-test/mock-mysql/schema.go +++ /dev/null @@ -1,241 +0,0 @@ -package main - -import "fmt" - -var seedDataStatements []string - -func init() { - seedDataStatements = generateSeedData() -} - -func generateSeedData() []string { - stmts := []string{} - - // Create users (200 users for team members) - for i := 1; i <= 200; i++ { - stmts = append(stmts, fmt.Sprintf( - `INSERT INTO users (id, email, password, username, tag, role) VALUES (%d, 'user%d@test.com', 'hashed_pw', 'User%d', '%04d', 'USER')`, - i, i, i, i, - )) - } - - // Contest 100: 10 teams x 5 members (finalized, FINISHED status) - stmts = append(stmts, `INSERT INTO contests (contest_id, title, description, max_team_count, contest_type, contest_status, total_team_member) VALUES (100, 'Contest-10Teams', 'Load test contest with 10 teams', 10, 'LEAGUE', 'FINISHED', 5)`) - stmts = append(stmts, generateTeamsAndMembers(100, 10, 5, 1)...) - - // Contest 101: 20 teams x 5 members - stmts = append(stmts, `INSERT INTO contests (contest_id, title, description, max_team_count, contest_type, contest_status, total_team_member) VALUES (101, 'Contest-20Teams', 'Load test contest with 20 teams', 20, 'LEAGUE', 'FINISHED', 5)`) - stmts = append(stmts, generateTeamsAndMembers(101, 20, 5, 51)...) - - // Contest 102: 50 teams x 5 members - stmts = append(stmts, `INSERT INTO contests (contest_id, title, description, max_team_count, contest_type, contest_status, total_team_member) VALUES (102, 'Contest-50Teams', 'Load test contest with 50 teams', 50, 'LEAGUE', 'FINISHED', 5)`) - stmts = append(stmts, generateTeamsAndMembers(102, 50, 5, 151)...) - - return stmts -} - -func generateTeamsAndMembers(contestID, teamCount, membersPerTeam, teamIDStart int) []string { - stmts := []string{} - userID := 1 - - for t := 0; t < teamCount; t++ { - teamID := teamIDStart + t - stmts = append(stmts, fmt.Sprintf( - `INSERT INTO teams (team_id, contest_id, team_name) VALUES (%d, %d, 'Team-%d-%d')`, - teamID, contestID, contestID, t+1, - )) - - for m := 0; m < membersPerTeam; m++ { - memberType := "MEMBER" - if m == 0 { - memberType = "LEADER" - } - stmts = append(stmts, fmt.Sprintf( - `INSERT INTO team_members (team_id, user_id, member_type) VALUES (%d, %d, '%s')`, - teamID, userID, memberType, - )) - userID++ - if userID > 200 { - userID = 1 - } - } - } - - return stmts -} - -var createTableStatements = []string{ - // 1. users - `CREATE TABLE users ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - email VARCHAR(64) NOT NULL, - password VARCHAR(255) NOT NULL, - username VARCHAR(16) NOT NULL, - tag VARCHAR(6) NOT NULL, - bio VARCHAR(255), - avatar TEXT, - profile_key VARCHAR(512), - role VARCHAR(16) NOT NULL DEFAULT 'USER', - riot_name VARCHAR(32), - riot_tag VARCHAR(8), - region VARCHAR(10), - current_tier INT, - current_tier_patched VARCHAR(32), - elo INT, - ranking_in_tier INT, - peak_tier INT, - peak_tier_patched VARCHAR(32), - valorant_updated_at TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - )`, - - // 2. discord_accounts - `CREATE TABLE discord_accounts ( - discord_id VARCHAR(255) PRIMARY KEY, - user_id BIGINT NOT NULL, - discord_avatar VARCHAR(255), - discord_verified BOOLEAN DEFAULT FALSE - )`, - - // 3. contests - `CREATE TABLE contests ( - contest_id BIGINT AUTO_INCREMENT PRIMARY KEY, - title VARCHAR(255) NOT NULL, - description TEXT, - max_team_count INT, - total_point INT DEFAULT 100, - contest_type VARCHAR(16) NOT NULL, - contest_status VARCHAR(16) NOT NULL, - started_at DATETIME, - ended_at DATETIME, - auto_start BOOLEAN DEFAULT FALSE, - game_type VARCHAR(32), - game_point_table_id BIGINT, - total_team_member INT NOT NULL DEFAULT 5, - discord_guild_id VARCHAR(255), - discord_text_channel_id VARCHAR(255), - thumbnail VARCHAR(512), - banner_key VARCHAR(512), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - )`, - - // 4. contests_members - `CREATE TABLE contests_members ( - user_id BIGINT NOT NULL, - contest_id BIGINT NOT NULL, - member_type VARCHAR(16) NOT NULL, - leader_type VARCHAR(8) NOT NULL, - point INT DEFAULT 0, - PRIMARY KEY (user_id, contest_id) - )`, - - // 5. teams - `CREATE TABLE teams ( - team_id BIGINT AUTO_INCREMENT PRIMARY KEY, - contest_id BIGINT NOT NULL, - team_name VARCHAR(50) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - )`, - - // 6. team_members - `CREATE TABLE team_members ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - team_id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - member_type VARCHAR(16) NOT NULL, - joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )`, - - // 7. games - `CREATE TABLE games ( - game_id BIGINT AUTO_INCREMENT PRIMARY KEY, - contest_id BIGINT NOT NULL, - game_status VARCHAR(16) NOT NULL, - game_team_type VARCHAR(16) NOT NULL, - started_at DATETIME, - ended_at DATETIME, - round INT, - match_number INT, - next_game_id BIGINT, - bracket_position INT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - )`, - - // 8. game_teams - `CREATE TABLE game_teams ( - game_team_id BIGINT AUTO_INCREMENT PRIMARY KEY, - game_id BIGINT NOT NULL, - team_id BIGINT NOT NULL, - grade INT - )`, - - // 9. contest_comments - `CREATE TABLE contest_comments ( - comment_id BIGINT AUTO_INCREMENT PRIMARY KEY, - contest_id BIGINT NOT NULL, - user_id BIGINT NOT NULL, - content VARCHAR(255) NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - modified_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - )`, - - // 10. valorant_score_tables - `CREATE TABLE valorant_score_tables ( - score_table_id BIGINT AUTO_INCREMENT PRIMARY KEY, - radiant INT NOT NULL, - immortal_3 INT NOT NULL DEFAULT 0, - immortal_2 INT NOT NULL DEFAULT 0, - immortal_1 INT NOT NULL DEFAULT 0, - ascendant_3 INT NOT NULL DEFAULT 0, - ascendant_2 INT NOT NULL DEFAULT 0, - ascendant_1 INT NOT NULL DEFAULT 0, - diamond_3 INT NOT NULL DEFAULT 0, - diamond_2 INT NOT NULL DEFAULT 0, - diamond_1 INT NOT NULL DEFAULT 0, - platinum_3 INT NOT NULL DEFAULT 0, - platinum_2 INT NOT NULL DEFAULT 0, - platinum_1 INT NOT NULL DEFAULT 0, - gold_3 INT NOT NULL DEFAULT 0, - gold_2 INT NOT NULL DEFAULT 0, - gold_1 INT NOT NULL DEFAULT 0, - silver_3 INT NOT NULL DEFAULT 0, - silver_2 INT NOT NULL DEFAULT 0, - silver_1 INT NOT NULL DEFAULT 0, - bronze_3 INT NOT NULL DEFAULT 0, - bronze_2 INT NOT NULL DEFAULT 0, - bronze_1 INT NOT NULL DEFAULT 0, - iron_3 INT NOT NULL DEFAULT 0, - iron_2 INT NOT NULL DEFAULT 0, - iron_1 INT NOT NULL DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - )`, - - // 11. main_banners - `CREATE TABLE main_banners ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - image_key VARCHAR(512) NOT NULL, - title VARCHAR(255), - link_url VARCHAR(512), - display_order INT NOT NULL DEFAULT 0, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - )`, - - // 12. notifications - `CREATE TABLE notifications ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - user_id BIGINT NOT NULL, - type VARCHAR(50) NOT NULL, - title VARCHAR(255) NOT NULL, - message TEXT, - data JSON, - is_read BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )`, -} diff --git a/load-test/run.sh b/load-test/run.sh deleted file mode 100755 index ffce2e3..0000000 --- a/load-test/run.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# ─── GAMERS Load Test Runner ─── -# Usage: -# ./run.sh # 기본: team-invite 시나리오 -# ./run.sh team-invite # Team 초대 시나리오 -# ./run.sh application # 참가 신청 시나리오 -# ./run.sh contest-start # 대회 시작 시나리오 -# ./run.sh tournament-create # 토너먼트 생성 시나리오 -# ./run.sh full-flow # 전체 플로우 시나리오 -# ./run.sh team-query # 팀 조회 N+1 성능 시나리오 -# ./run.sh dashboard # 대시보드만 실행 (Grafana + InfluxDB) -# ./run.sh clean # 모든 컨테이너 & 볼륨 정리 - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -SCENARIO="${1:-team-invite}" -COMPOSE="docker compose -f docker-compose.yaml" - -# ─── 환경변수 기본값 ─── -export LEADER_TOKEN="${LEADER_TOKEN:-}" -export LOGIN_EMAIL="${LOGIN_EMAIL:-}" -export LOGIN_PASSWORD="${LOGIN_PASSWORD:-}" -export INVITEE_TOKEN="${INVITEE_TOKEN:-your-invitee-jwt-token}" -export CONTEST_ID="${CONTEST_ID:-1}" -export BASE_URL="${BASE_URL:-http://gamers-app:8080/api}" - -# ─── 네트워크 확인 ─── -ensure_network() { - if ! docker network inspect gamers-network >/dev/null 2>&1; then - echo "[INFO] Creating gamers-network..." - docker network create gamers-network - fi -} - -# ─── 대시보드 시작 ─── -start_dashboard() { - echo "============================================" - echo " Starting InfluxDB + Grafana Dashboard" - echo "============================================" - $COMPOSE up -d influxdb grafana - echo "" - echo "[OK] Grafana: http://localhost:3001 (admin / admin)" - echo "[OK] InfluxDB: http://localhost:8086" - echo "" -} - -# ─── k6 시나리오 실행 ─── -run_scenario() { - local script="$1" - echo "============================================" - echo " Running scenario: ${script}" - echo "============================================" - echo " BASE_URL: ${BASE_URL}" - echo " CONTEST_ID: ${CONTEST_ID}" - echo "============================================" - echo "" - - $COMPOSE run --rm \ - -e K6_OUT="influxdb=http://influxdb:8086/k6" \ - -e LEADER_TOKEN="${LEADER_TOKEN}" \ - -e LOGIN_EMAIL="${LOGIN_EMAIL}" \ - -e LOGIN_PASSWORD="${LOGIN_PASSWORD}" \ - -e INVITEE_TOKEN="${INVITEE_TOKEN}" \ - -e CONTEST_ID="${CONTEST_ID}" \ - -e BASE_URL="${BASE_URL}" \ - -e USER_TOKENS="${USER_TOKENS:-}" \ - -e USER_IDS="${USER_IDS:-}" \ - -e CONTEST_IDS="${CONTEST_IDS:-}" \ - -e LEADER_TOKENS="${LEADER_TOKENS:-}" \ - -e INVITEE_IDS="${INVITEE_IDS:-}" \ - k6 run --out "influxdb=http://influxdb:8086/k6" "/scripts/scenario-${script}.js" -} - -# ─── 정리 ─── -cleanup() { - echo "[INFO] Stopping all containers and removing volumes..." - $COMPOSE down -v - echo "[OK] Cleaned up." -} - -# ─── Main ─── -ensure_network - -case "$SCENARIO" in - dashboard) - start_dashboard - echo "Dashboard is running. Press Ctrl+C to stop." - echo "Run tests in another terminal: ./run.sh team-invite" - $COMPOSE logs -f grafana - ;; - clean) - cleanup - ;; - team-invite|application|contest-start|tournament-create|full-flow|team-query) - start_dashboard - sleep 3 # InfluxDB가 준비될 때까지 대기 - run_scenario "$SCENARIO" - echo "" - echo "============================================" - echo " Test Complete!" - echo " View results: http://localhost:3001" - echo "============================================" - ;; - *) - echo "Unknown scenario: $SCENARIO" - echo "" - echo "Available scenarios:" - echo " team-invite Team 초대 부하 테스트" - echo " application 참가 신청 부하 테스트" - echo " contest-start 대회 시작 (배치 INSERT) 테스트" - echo " tournament-create 토너먼트 생성 테스트" - echo " full-flow 전체 플로우 E2E 테스트" - echo " team-query 팀 조회 N+1 성능 테스트" - echo " dashboard 대시보드만 실행" - echo " clean 정리" - exit 1 - ;; -esac diff --git a/test/contest/application/contest_application_service_test.go b/test/contest/application/contest_application_service_test.go new file mode 100644 index 0000000..bd56ddb --- /dev/null +++ b/test/contest/application/contest_application_service_test.go @@ -0,0 +1,663 @@ +package application_test + +import ( + "context" + "testing" + "time" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application/port" + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/domain" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" + oauth2Domain "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/domain" + pointDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/point/domain" + userDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/user/domain" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// ==================== Additional Mock Definitions (not in contest_service_test.go) ==================== + +type MockUserQueryPort struct { + mock.Mock +} + +func (m *MockUserQueryPort) FindById(id int64) (*userDomain.User, error) { + args := m.Called(id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*userDomain.User), args.Error(1) +} + +func (m *MockUserQueryPort) FindByEmail(email string) (*userDomain.User, error) { + args := m.Called(email) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*userDomain.User), args.Error(1) +} + +type MockValorantScoreTableDatabasePort struct { + mock.Mock +} + +func (m *MockValorantScoreTableDatabasePort) Save(scoreTable *pointDomain.ValorantScoreTable) (*pointDomain.ValorantScoreTable, error) { + args := m.Called(scoreTable) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*pointDomain.ValorantScoreTable), args.Error(1) +} + +func (m *MockValorantScoreTableDatabasePort) GetByID(scoreTableID int64) (*pointDomain.ValorantScoreTable, error) { + args := m.Called(scoreTableID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*pointDomain.ValorantScoreTable), args.Error(1) +} + +func (m *MockValorantScoreTableDatabasePort) GetAll() ([]*pointDomain.ValorantScoreTable, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]*pointDomain.ValorantScoreTable), args.Error(1) +} + +func (m *MockValorantScoreTableDatabasePort) Update(scoreTable *pointDomain.ValorantScoreTable) error { + args := m.Called(scoreTable) + return args.Error(0) +} + +func (m *MockValorantScoreTableDatabasePort) Delete(scoreTableID int64) error { + args := m.Called(scoreTableID) + return args.Error(0) +} + +// ==================== Test Fixtures ==================== + +func strPtr(s string) *string { + return &s +} + +func int64Ptr(i int64) *int64 { + return &i +} + +func createTestScoreTable() *pointDomain.ValorantScoreTable { + return &pointDomain.ValorantScoreTable{ + ScoreTableID: 1, + Radiant: 100, + Immortal3: 95, + Immortal2: 90, + Immortal1: 85, + Ascendant3: 80, + Ascendant2: 75, + Ascendant1: 70, + Diamond3: 65, + Diamond2: 60, + Diamond1: 55, + Platinum3: 50, + Platinum2: 45, + Platinum1: 40, + Gold3: 35, + Gold2: 30, + Gold1: 25, + Silver3: 20, + Silver2: 15, + Silver1: 10, + Bronze3: 8, + Bronze2: 6, + Bronze1: 4, + Iron3: 3, + Iron2: 2, + Iron1: 1, + } +} + +func createValorantLinkedUser(id int64, currentTier, peakTier string) *userDomain.User { + riotName := "TestPlayer" + riotTag := "KR1" + region := "kr" + now := time.Now() + return &userDomain.User{ + Id: id, + Email: "test@example.com", + Username: "testuser", + Tag: "0001", + Avatar: "avatar.png", + RiotName: &riotName, + RiotTag: &riotTag, + Region: ®ion, + CurrentTierPatched: strPtr(currentTier), + PeakTierPatched: strPtr(peakTier), + CurrentTier: intPtr(15), + PeakTier: intPtr(18), + Elo: intPtr(100), + RankingInTier: intPtr(50), + ValorantUpdatedAt: &now, + } +} + +func createNonValorantUser(id int64) *userDomain.User { + return &userDomain.User{ + Id: id, + Email: "noval@example.com", + Username: "novaluser", + Tag: "0002", + Avatar: "avatar.png", + } +} + +func createPendingContestWithScoreTable(contestId int64, scoreTableId int64) *domain.Contest { + return &domain.Contest{ + ContestID: contestId, + Title: "Test Contest", + Description: "Test Description", + MaxTeamCount: 8, + TotalPoint: 100, + ContestType: domain.ContestTypeTournament, + ContestStatus: domain.ContestStatusPending, + StartedAt: time.Now().Add(24 * time.Hour), + EndedAt: time.Now().Add(48 * time.Hour), + GamePointTableId: &scoreTableId, + } +} + +func createPendingContestWithoutScoreTable(contestId int64) *domain.Contest { + return &domain.Contest{ + ContestID: contestId, + Title: "Test Contest No ScoreTable", + Description: "Test Description", + MaxTeamCount: 8, + TotalPoint: 100, + ContestType: domain.ContestTypeTournament, + ContestStatus: domain.ContestStatusPending, + StartedAt: time.Now().Add(24 * time.Hour), + EndedAt: time.Now().Add(48 * time.Hour), + } +} + +func createApplicationService( + appRedis *MockContestApplicationRedisPort, + contestDB *MockContestDatabasePort, + memberDB *MockContestMemberDatabasePort, + eventPub *MockEventPublisherPort, + oauth2DB *MockOAuth2DatabasePort, + userQuery *MockUserQueryPort, + scoreTableDB *MockValorantScoreTableDatabasePort, +) *application.ContestApplicationService { + service := application.NewContestApplicationService( + appRedis, + contestDB, + memberDB, + eventPub, + oauth2DB, + userQuery, + ) + if scoreTableDB != nil { + service.SetScoreTablePort(scoreTableDB) + } + return service +} + +// ==================== RequestParticipate Point Calculation Tests ==================== + +func TestRequestParticipate_WithScoreTable_CalculatesPoint(t *testing.T) { + // Given: Contest with score table, user with Diamond 2 current / Immortal 1 peak + mockRedis := new(MockContestApplicationRedisPort) + mockContestDB := new(MockContestDatabasePort) + mockMemberDB := new(MockContestMemberDatabasePort) + mockEventPub := new(MockEventPublisherPort) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockUserQuery := new(MockUserQueryPort) + mockScoreTable := new(MockValorantScoreTableDatabasePort) + + service := createApplicationService(mockRedis, mockContestDB, mockMemberDB, mockEventPub, mockOAuth2DB, mockUserQuery, mockScoreTable) + + ctx := context.Background() + contestId := int64(1) + userId := int64(10) + scoreTableId := int64(1) + + user := createValorantLinkedUser(userId, "Diamond 2", "Immortal 1") + contest := createPendingContestWithScoreTable(contestId, scoreTableId) + scoreTable := createTestScoreTable() + discordAccount := &oauth2Domain.DiscordAccount{DiscordId: "discord_123", UserId: userId} + + mockOAuth2DB.On("FindDiscordAccountByUserId", userId).Return(discordAccount, nil) + mockContestDB.On("GetContestById", contestId).Return(contest, nil) + mockMemberDB.On("GetByContestAndUser", contestId, userId).Return(nil, exception.ErrContestMemberNotFound) + mockUserQuery.On("FindById", userId).Return(user, nil) + mockScoreTable.On("GetByID", scoreTableId).Return(scoreTable, nil) + + // Expected point: (Diamond2=60 + Immortal1=85) / 2 = 72.5 → 73 (rounded) + mockRedis.On("RequestParticipate", ctx, contestId, mock.MatchedBy(func(s *port.SenderSnapshot) bool { + return s.UserID == userId && s.Point == 73 + }), mock.AnythingOfType("time.Duration")).Return(nil) + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + + // When + _, err := service.RequestParticipate(ctx, contestId, userId) + + // Then + assert.NoError(t, err) + mockScoreTable.AssertExpectations(t) + mockRedis.AssertExpectations(t) +} + +func TestRequestParticipate_WithScoreTable_SameTier_CalculatesExactPoint(t *testing.T) { + // Given: User where current tier == peak tier + mockRedis := new(MockContestApplicationRedisPort) + mockContestDB := new(MockContestDatabasePort) + mockMemberDB := new(MockContestMemberDatabasePort) + mockEventPub := new(MockEventPublisherPort) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockUserQuery := new(MockUserQueryPort) + mockScoreTable := new(MockValorantScoreTableDatabasePort) + + service := createApplicationService(mockRedis, mockContestDB, mockMemberDB, mockEventPub, mockOAuth2DB, mockUserQuery, mockScoreTable) + + ctx := context.Background() + contestId := int64(1) + userId := int64(10) + scoreTableId := int64(1) + + user := createValorantLinkedUser(userId, "Gold 1", "Gold 1") + contest := createPendingContestWithScoreTable(contestId, scoreTableId) + scoreTable := createTestScoreTable() + discordAccount := &oauth2Domain.DiscordAccount{DiscordId: "discord_123", UserId: userId} + + mockOAuth2DB.On("FindDiscordAccountByUserId", userId).Return(discordAccount, nil) + mockContestDB.On("GetContestById", contestId).Return(contest, nil) + mockMemberDB.On("GetByContestAndUser", contestId, userId).Return(nil, exception.ErrContestMemberNotFound) + mockUserQuery.On("FindById", userId).Return(user, nil) + mockScoreTable.On("GetByID", scoreTableId).Return(scoreTable, nil) + + // Expected point: (Gold1=25 + Gold1=25) / 2 = 25 + mockRedis.On("RequestParticipate", ctx, contestId, mock.MatchedBy(func(s *port.SenderSnapshot) bool { + return s.Point == 25 + }), mock.AnythingOfType("time.Duration")).Return(nil) + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + + // When + _, err := service.RequestParticipate(ctx, contestId, userId) + + // Then + assert.NoError(t, err) + mockRedis.AssertExpectations(t) +} + +func TestRequestParticipate_WithoutScoreTable_PointIsZero(t *testing.T) { + // Given: Contest WITHOUT GamePointTableId + mockRedis := new(MockContestApplicationRedisPort) + mockContestDB := new(MockContestDatabasePort) + mockMemberDB := new(MockContestMemberDatabasePort) + mockEventPub := new(MockEventPublisherPort) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockUserQuery := new(MockUserQueryPort) + mockScoreTable := new(MockValorantScoreTableDatabasePort) + + service := createApplicationService(mockRedis, mockContestDB, mockMemberDB, mockEventPub, mockOAuth2DB, mockUserQuery, mockScoreTable) + + ctx := context.Background() + contestId := int64(2) + userId := int64(10) + + user := createValorantLinkedUser(userId, "Diamond 2", "Immortal 1") + contest := createPendingContestWithoutScoreTable(contestId) + discordAccount := &oauth2Domain.DiscordAccount{DiscordId: "discord_123", UserId: userId} + + mockOAuth2DB.On("FindDiscordAccountByUserId", userId).Return(discordAccount, nil) + mockContestDB.On("GetContestById", contestId).Return(contest, nil) + mockMemberDB.On("GetByContestAndUser", contestId, userId).Return(nil, exception.ErrContestMemberNotFound) + mockUserQuery.On("FindById", userId).Return(user, nil) + + // Point should be 0 since no GamePointTableId + mockRedis.On("RequestParticipate", ctx, contestId, mock.MatchedBy(func(s *port.SenderSnapshot) bool { + return s.Point == 0 + }), mock.AnythingOfType("time.Duration")).Return(nil) + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + + // When + _, err := service.RequestParticipate(ctx, contestId, userId) + + // Then + assert.NoError(t, err) + mockScoreTable.AssertNotCalled(t, "GetByID", mock.Anything) + mockRedis.AssertExpectations(t) +} + +func TestRequestParticipate_ValorantNotLinked_PointIsZero(t *testing.T) { + // Given: User without Valorant linked, contest WITH score table + mockRedis := new(MockContestApplicationRedisPort) + mockContestDB := new(MockContestDatabasePort) + mockMemberDB := new(MockContestMemberDatabasePort) + mockEventPub := new(MockEventPublisherPort) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockUserQuery := new(MockUserQueryPort) + mockScoreTable := new(MockValorantScoreTableDatabasePort) + + service := createApplicationService(mockRedis, mockContestDB, mockMemberDB, mockEventPub, mockOAuth2DB, mockUserQuery, mockScoreTable) + + ctx := context.Background() + contestId := int64(1) + userId := int64(20) + scoreTableId := int64(1) + + user := createNonValorantUser(userId) + contest := createPendingContestWithScoreTable(contestId, scoreTableId) + scoreTable := createTestScoreTable() + discordAccount := &oauth2Domain.DiscordAccount{DiscordId: "discord_456", UserId: userId} + + mockOAuth2DB.On("FindDiscordAccountByUserId", userId).Return(discordAccount, nil) + mockContestDB.On("GetContestById", contestId).Return(contest, nil) + mockMemberDB.On("GetByContestAndUser", contestId, userId).Return(nil, exception.ErrContestMemberNotFound) + mockUserQuery.On("FindById", userId).Return(user, nil) + mockScoreTable.On("GetByID", scoreTableId).Return(scoreTable, nil) + + // Point should be 0 for non-Valorant user (no error, just 0) + mockRedis.On("RequestParticipate", ctx, contestId, mock.MatchedBy(func(s *port.SenderSnapshot) bool { + return s.Point == 0 + }), mock.AnythingOfType("time.Duration")).Return(nil) + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + + // When + _, err := service.RequestParticipate(ctx, contestId, userId) + + // Then + assert.NoError(t, err) + mockRedis.AssertExpectations(t) +} + +func TestRequestParticipate_ScoreTableNotFound_PointIsZero(t *testing.T) { + // Given: Contest has GamePointTableId but score table doesn't exist in DB + mockRedis := new(MockContestApplicationRedisPort) + mockContestDB := new(MockContestDatabasePort) + mockMemberDB := new(MockContestMemberDatabasePort) + mockEventPub := new(MockEventPublisherPort) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockUserQuery := new(MockUserQueryPort) + mockScoreTable := new(MockValorantScoreTableDatabasePort) + + service := createApplicationService(mockRedis, mockContestDB, mockMemberDB, mockEventPub, mockOAuth2DB, mockUserQuery, mockScoreTable) + + ctx := context.Background() + contestId := int64(1) + userId := int64(10) + scoreTableId := int64(999) + + user := createValorantLinkedUser(userId, "Diamond 2", "Immortal 1") + contest := createPendingContestWithScoreTable(contestId, scoreTableId) + discordAccount := &oauth2Domain.DiscordAccount{DiscordId: "discord_123", UserId: userId} + + mockOAuth2DB.On("FindDiscordAccountByUserId", userId).Return(discordAccount, nil) + mockContestDB.On("GetContestById", contestId).Return(contest, nil) + mockMemberDB.On("GetByContestAndUser", contestId, userId).Return(nil, exception.ErrContestMemberNotFound) + mockUserQuery.On("FindById", userId).Return(user, nil) + mockScoreTable.On("GetByID", scoreTableId).Return(nil, exception.ErrScoreTableNotFound) + + // Point should be 0 due to score table fetch error (graceful degradation) + mockRedis.On("RequestParticipate", ctx, contestId, mock.MatchedBy(func(s *port.SenderSnapshot) bool { + return s.Point == 0 + }), mock.AnythingOfType("time.Duration")).Return(nil) + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + + // When + _, err := service.RequestParticipate(ctx, contestId, userId) + + // Then + assert.NoError(t, err) + mockRedis.AssertExpectations(t) +} + +func TestRequestParticipate_NoScoreTablePortSet_PointIsZero(t *testing.T) { + // Given: scoreTableRepo is nil (not wired) + mockRedis := new(MockContestApplicationRedisPort) + mockContestDB := new(MockContestDatabasePort) + mockMemberDB := new(MockContestMemberDatabasePort) + mockEventPub := new(MockEventPublisherPort) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockUserQuery := new(MockUserQueryPort) + + service := createApplicationService(mockRedis, mockContestDB, mockMemberDB, mockEventPub, mockOAuth2DB, mockUserQuery, nil) + + ctx := context.Background() + contestId := int64(1) + userId := int64(10) + scoreTableId := int64(1) + + user := createValorantLinkedUser(userId, "Diamond 2", "Immortal 1") + contest := createPendingContestWithScoreTable(contestId, scoreTableId) + discordAccount := &oauth2Domain.DiscordAccount{DiscordId: "discord_123", UserId: userId} + + mockOAuth2DB.On("FindDiscordAccountByUserId", userId).Return(discordAccount, nil) + mockContestDB.On("GetContestById", contestId).Return(contest, nil) + mockMemberDB.On("GetByContestAndUser", contestId, userId).Return(nil, exception.ErrContestMemberNotFound) + mockUserQuery.On("FindById", userId).Return(user, nil) + + // Point should be 0 since scoreTableRepo is nil + mockRedis.On("RequestParticipate", ctx, contestId, mock.MatchedBy(func(s *port.SenderSnapshot) bool { + return s.Point == 0 + }), mock.AnythingOfType("time.Duration")).Return(nil) + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + + // When + _, err := service.RequestParticipate(ctx, contestId, userId) + + // Then + assert.NoError(t, err) + mockRedis.AssertExpectations(t) +} + +// ==================== AcceptApplication Point Propagation Tests ==================== + +func TestAcceptApplication_PropagatesPointToMember(t *testing.T) { + // Given: Application stored in Redis has Point=73 + mockRedis := new(MockContestApplicationRedisPort) + mockContestDB := new(MockContestDatabasePort) + mockMemberDB := new(MockContestMemberDatabasePort) + mockEventPub := new(MockEventPublisherPort) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockUserQuery := new(MockUserQueryPort) + + service := createApplicationService(mockRedis, mockContestDB, mockMemberDB, mockEventPub, mockOAuth2DB, mockUserQuery, nil) + + ctx := context.Background() + contestId := int64(1) + userId := int64(10) + leaderId := int64(1) + + contest := &domain.Contest{ + ContestID: contestId, + Title: "Test Contest", + ContestStatus: domain.ContestStatusPending, + StartedAt: time.Now().Add(24 * time.Hour), + EndedAt: time.Now().Add(48 * time.Hour), + } + + leader := &domain.ContestMember{ + UserID: leaderId, + ContestID: contestId, + MemberType: domain.MemberTypeStaff, + LeaderType: domain.LeaderTypeLeader, + } + + storedApplication := &port.ContestApplication{ + UserID: userId, + ContestID: contestId, + Status: port.ApplicationStatusPending, + Sender: &port.SenderSnapshot{ + UserID: userId, + Username: "testuser", + Tag: "0001", + Point: 73, + }, + } + + mockContestDB.On("GetContestById", contestId).Return(contest, nil) + mockMemberDB.On("GetByContestAndUser", contestId, leaderId).Return(leader, nil) + mockRedis.On("GetApplication", ctx, contestId, userId).Return(storedApplication, nil) + mockRedis.On("AcceptRequest", ctx, contestId, userId, leaderId).Return(nil) + + // Verify member is saved with Point=73 + mockMemberDB.On("Save", mock.MatchedBy(func(m *domain.ContestMember) bool { + return m.UserID == userId && + m.ContestID == contestId && + m.Point == 73 && + m.MemberType == domain.MemberTypeNormal && + m.LeaderType == domain.LeaderTypeMember + })).Return(nil) + + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + mockOAuth2DB.On("FindDiscordAccountByUserId", mock.AnythingOfType("int64")).Return(nil, exception.ErrDiscordUserCannotFound).Maybe() + + // When + err := service.AcceptApplication(ctx, contestId, userId, leaderId) + + // Then + assert.NoError(t, err) + mockMemberDB.AssertExpectations(t) + mockRedis.AssertExpectations(t) +} + +func TestAcceptApplication_ZeroPointWhenNoSenderSnapshot(t *testing.T) { + // Given: Application in Redis has nil Sender (edge case) + mockRedis := new(MockContestApplicationRedisPort) + mockContestDB := new(MockContestDatabasePort) + mockMemberDB := new(MockContestMemberDatabasePort) + mockEventPub := new(MockEventPublisherPort) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockUserQuery := new(MockUserQueryPort) + + service := createApplicationService(mockRedis, mockContestDB, mockMemberDB, mockEventPub, mockOAuth2DB, mockUserQuery, nil) + + ctx := context.Background() + contestId := int64(1) + userId := int64(10) + leaderId := int64(1) + + contest := &domain.Contest{ + ContestID: contestId, + Title: "Test Contest", + ContestStatus: domain.ContestStatusPending, + StartedAt: time.Now().Add(24 * time.Hour), + EndedAt: time.Now().Add(48 * time.Hour), + } + + leader := &domain.ContestMember{ + UserID: leaderId, + ContestID: contestId, + MemberType: domain.MemberTypeStaff, + LeaderType: domain.LeaderTypeLeader, + } + + storedApplication := &port.ContestApplication{ + UserID: userId, + ContestID: contestId, + Status: port.ApplicationStatusPending, + Sender: nil, + } + + mockContestDB.On("GetContestById", contestId).Return(contest, nil) + mockMemberDB.On("GetByContestAndUser", contestId, leaderId).Return(leader, nil) + mockRedis.On("GetApplication", ctx, contestId, userId).Return(storedApplication, nil) + mockRedis.On("AcceptRequest", ctx, contestId, userId, leaderId).Return(nil) + + // Point should default to 0 + mockMemberDB.On("Save", mock.MatchedBy(func(m *domain.ContestMember) bool { + return m.Point == 0 + })).Return(nil) + + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + mockOAuth2DB.On("FindDiscordAccountByUserId", mock.AnythingOfType("int64")).Return(nil, exception.ErrDiscordUserCannotFound).Maybe() + + // When + err := service.AcceptApplication(ctx, contestId, userId, leaderId) + + // Then + assert.NoError(t, err) + mockMemberDB.AssertExpectations(t) +} + +// ==================== Point Calculation Edge Cases ==================== + +func TestRequestParticipate_RadiantUser_MaxPoint(t *testing.T) { + mockRedis := new(MockContestApplicationRedisPort) + mockContestDB := new(MockContestDatabasePort) + mockMemberDB := new(MockContestMemberDatabasePort) + mockEventPub := new(MockEventPublisherPort) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockUserQuery := new(MockUserQueryPort) + mockScoreTable := new(MockValorantScoreTableDatabasePort) + + service := createApplicationService(mockRedis, mockContestDB, mockMemberDB, mockEventPub, mockOAuth2DB, mockUserQuery, mockScoreTable) + + ctx := context.Background() + contestId := int64(1) + userId := int64(10) + scoreTableId := int64(1) + + user := createValorantLinkedUser(userId, "Radiant", "Radiant") + contest := createPendingContestWithScoreTable(contestId, scoreTableId) + scoreTable := createTestScoreTable() + discordAccount := &oauth2Domain.DiscordAccount{DiscordId: "discord_123", UserId: userId} + + mockOAuth2DB.On("FindDiscordAccountByUserId", userId).Return(discordAccount, nil) + mockContestDB.On("GetContestById", contestId).Return(contest, nil) + mockMemberDB.On("GetByContestAndUser", contestId, userId).Return(nil, exception.ErrContestMemberNotFound) + mockUserQuery.On("FindById", userId).Return(user, nil) + mockScoreTable.On("GetByID", scoreTableId).Return(scoreTable, nil) + + // Expected: (100 + 100) / 2 = 100 + mockRedis.On("RequestParticipate", ctx, contestId, mock.MatchedBy(func(s *port.SenderSnapshot) bool { + return s.Point == 100 + }), mock.AnythingOfType("time.Duration")).Return(nil) + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + + _, err := service.RequestParticipate(ctx, contestId, userId) + + assert.NoError(t, err) + mockRedis.AssertExpectations(t) +} + +func TestRequestParticipate_Iron1User_MinPoint(t *testing.T) { + mockRedis := new(MockContestApplicationRedisPort) + mockContestDB := new(MockContestDatabasePort) + mockMemberDB := new(MockContestMemberDatabasePort) + mockEventPub := new(MockEventPublisherPort) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockUserQuery := new(MockUserQueryPort) + mockScoreTable := new(MockValorantScoreTableDatabasePort) + + service := createApplicationService(mockRedis, mockContestDB, mockMemberDB, mockEventPub, mockOAuth2DB, mockUserQuery, mockScoreTable) + + ctx := context.Background() + contestId := int64(1) + userId := int64(10) + scoreTableId := int64(1) + + user := createValorantLinkedUser(userId, "Iron 1", "Iron 1") + contest := createPendingContestWithScoreTable(contestId, scoreTableId) + scoreTable := createTestScoreTable() + discordAccount := &oauth2Domain.DiscordAccount{DiscordId: "discord_123", UserId: userId} + + mockOAuth2DB.On("FindDiscordAccountByUserId", userId).Return(discordAccount, nil) + mockContestDB.On("GetContestById", contestId).Return(contest, nil) + mockMemberDB.On("GetByContestAndUser", contestId, userId).Return(nil, exception.ErrContestMemberNotFound) + mockUserQuery.On("FindById", userId).Return(user, nil) + mockScoreTable.On("GetByID", scoreTableId).Return(scoreTable, nil) + + // Expected: (1 + 1) / 2 = 1 + mockRedis.On("RequestParticipate", ctx, contestId, mock.MatchedBy(func(s *port.SenderSnapshot) bool { + return s.Point == 1 + }), mock.AnythingOfType("time.Duration")).Return(nil) + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + + _, err := service.RequestParticipate(ctx, contestId, userId) + + assert.NoError(t, err) + mockRedis.AssertExpectations(t) +} diff --git a/test/contest/domain/contest_member_test.go b/test/contest/domain/contest_member_test.go index bb65e9c..b2f65cf 100644 --- a/test/contest/domain/contest_member_test.go +++ b/test/contest/domain/contest_member_test.go @@ -48,7 +48,7 @@ func TestNewContestMember(t *testing.T) { userID := int64(2) contestID := int64(100) - member := domain.NewContestMember(userID, contestID, domain.MemberTypeNormal, domain.LeaderTypeMember) + member := domain.NewContestMember(userID, contestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0) assert.NotNil(t, member) assert.Equal(t, userID, member.UserID) @@ -62,7 +62,7 @@ func TestNewContestMember(t *testing.T) { userID := int64(3) contestID := int64(100) - member := domain.NewContestMember(userID, contestID, domain.MemberTypeStaff, domain.LeaderTypeMember) + member := domain.NewContestMember(userID, contestID, domain.MemberTypeStaff, domain.LeaderTypeMember, 0) assert.NotNil(t, member) assert.Equal(t, domain.MemberTypeStaff, member.MemberType) @@ -246,14 +246,14 @@ func TestContestMember_LeaderCreation(t *testing.T) { }) t.Run("regular member is not leader and not staff", func(t *testing.T) { - member := domain.NewContestMember(2, 100, domain.MemberTypeNormal, domain.LeaderTypeMember) + member := domain.NewContestMember(2, 100, domain.MemberTypeNormal, domain.LeaderTypeMember, 0) assert.False(t, member.IsLeader()) assert.False(t, member.IsStaff()) }) t.Run("staff member is not leader but is staff", func(t *testing.T) { - member := domain.NewContestMember(3, 100, domain.MemberTypeStaff, domain.LeaderTypeMember) + member := domain.NewContestMember(3, 100, domain.MemberTypeStaff, domain.LeaderTypeMember, 0) assert.False(t, member.IsLeader()) assert.True(t, member.IsStaff()) diff --git a/test/contest/infra/persistence/contest_member_benchmark_test.go b/test/contest/infra/persistence/contest_member_benchmark_test.go index 6f1f850..74a676e 100644 --- a/test/contest/infra/persistence/contest_member_benchmark_test.go +++ b/test/contest/infra/persistence/contest_member_benchmark_test.go @@ -52,7 +52,7 @@ func makeMembers(count int, contestID int64, offset int) []*domain.ContestMember for j := 0; j < count; j++ { members[j] = domain.NewContestMember( int64(offset+j+1), contestID, - domain.MemberTypeNormal, domain.LeaderTypeMember, + domain.MemberTypeNormal, domain.LeaderTypeMember, 0, ) } return members diff --git a/test/contest/infra/persistence/contest_member_database_adapter_test.go b/test/contest/infra/persistence/contest_member_database_adapter_test.go index 5f001ab..6a9e502 100644 --- a/test/contest/infra/persistence/contest_member_database_adapter_test.go +++ b/test/contest/infra/persistence/contest_member_database_adapter_test.go @@ -85,7 +85,7 @@ func (s *ContestMemberDatabaseAdapterTestSuite) TestSave_Success() { func (s *ContestMemberDatabaseAdapterTestSuite) TestSave_NormalMember() { // Given contest := s.createTestContest("Test Contest") - member := domain.NewContestMember(2, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember) + member := domain.NewContestMember(2, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0) // When err := s.memberAdapter.Save(member) @@ -173,10 +173,10 @@ func (s *ContestMemberDatabaseAdapterTestSuite) TestGetMembersByContest_Success( leader := domain.NewContestMemberAsLeader(1, contest.ContestID) s.memberAdapter.Save(leader) - member1 := domain.NewContestMember(2, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember) + member1 := domain.NewContestMember(2, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0) s.memberAdapter.Save(member1) - member2 := domain.NewContestMember(3, contest.ContestID, domain.MemberTypeStaff, domain.LeaderTypeMember) + member2 := domain.NewContestMember(3, contest.ContestID, domain.MemberTypeStaff, domain.LeaderTypeMember, 0) s.memberAdapter.Save(member2) // When @@ -204,7 +204,7 @@ func (s *ContestMemberDatabaseAdapterTestSuite) TestGetMembersByContest_Empty() func (s *ContestMemberDatabaseAdapterTestSuite) TestDeleteById_Success() { // Given contest := s.createTestContest("Test Contest") - member := domain.NewContestMember(10, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember) + member := domain.NewContestMember(10, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0) s.memberAdapter.Save(member) // When @@ -236,9 +236,9 @@ func (s *ContestMemberDatabaseAdapterTestSuite) TestSaveBatch_Success() { // Given contest := s.createTestContest("Batch Contest") members := []*domain.ContestMember{ - domain.NewContestMember(100, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember), - domain.NewContestMember(101, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember), - domain.NewContestMember(102, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember), + domain.NewContestMember(100, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0), + domain.NewContestMember(101, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0), + domain.NewContestMember(102, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0), } // When @@ -265,9 +265,9 @@ func (s *ContestMemberDatabaseAdapterTestSuite) TestSaveBatch_RollbackOnError() // Given contest := s.createTestContest("Batch Contest") members := []*domain.ContestMember{ - domain.NewContestMember(200, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember), + domain.NewContestMember(200, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0), {UserID: 0, ContestID: contest.ContestID, MemberType: domain.MemberTypeNormal, LeaderType: domain.LeaderTypeMember}, // Invalid - domain.NewContestMember(202, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember), + domain.NewContestMember(202, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0), } // When @@ -286,7 +286,7 @@ func (s *ContestMemberDatabaseAdapterTestSuite) TestSaveBatch_RollbackOnError() func (s *ContestMemberDatabaseAdapterTestSuite) TestUpdateMemberType_Success() { // Given contest := s.createTestContest("Update Type Contest") - member := domain.NewContestMember(50, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember) + member := domain.NewContestMember(50, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0) s.memberAdapter.Save(member) // When @@ -315,7 +315,7 @@ func (s *ContestMemberDatabaseAdapterTestSuite) TestUpdateMemberType_NotFound() func (s *ContestMemberDatabaseAdapterTestSuite) TestUpdateMemberType_DemoteStaff() { // Given contest := s.createTestContest("Demote Contest") - member := domain.NewContestMember(60, contest.ContestID, domain.MemberTypeStaff, domain.LeaderTypeMember) + member := domain.NewContestMember(60, contest.ContestID, domain.MemberTypeStaff, domain.LeaderTypeMember, 0) s.memberAdapter.Save(member) // When @@ -339,7 +339,7 @@ func (s *ContestMemberDatabaseAdapterTestSuite) TestMember_InMultipleContests() userID := int64(99) member1 := domain.NewContestMemberAsLeader(userID, contest1.ContestID) - member2 := domain.NewContestMember(userID, contest2.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember) + member2 := domain.NewContestMember(userID, contest2.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0) // When err1 := s.memberAdapter.Save(member1) diff --git a/test/contest/integration/contest_application_integration_test.go b/test/contest/integration/contest_application_integration_test.go new file mode 100644 index 0000000..aa4fcc7 --- /dev/null +++ b/test/contest/integration/contest_application_integration_test.go @@ -0,0 +1,528 @@ +package integration_test + +import ( + "context" + "testing" + "time" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application/port" + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/domain" + contestAdapter "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/infra/persistence/adapter" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" + oauth2Domain "github.com/FOR-GAMERS/GAMERS-BE/internal/oauth2/domain" + pointDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/point/domain" + pointAdapter "github.com/FOR-GAMERS/GAMERS-BE/internal/point/infra/persistence/adapter" + userDomain "github.com/FOR-GAMERS/GAMERS-BE/internal/user/domain" + "github.com/FOR-GAMERS/GAMERS-BE/test/global/support" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "gorm.io/gorm" +) + +// ==================== Mocks for external dependencies ==================== +// Note: MockOAuth2Port is defined in tournament_flow_test.go (same package) + +type MockEventPub struct { + mock.Mock +} + +func (m *MockEventPub) PublishContestApplicationEvent(ctx context.Context, event *port.ContestApplicationEvent) error { + args := m.Called(ctx, event) + return args.Error(0) +} + +func (m *MockEventPub) PublishContestCreatedEvent(ctx context.Context, event *port.ContestCreatedEvent) error { + args := m.Called(ctx, event) + return args.Error(0) +} + +func (m *MockEventPub) Close() error { + args := m.Called() + return args.Error(0) +} + +func (m *MockEventPub) HealthCheck(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +type MockAppRedisPort struct { + mock.Mock +} + +func (m *MockAppRedisPort) RequestParticipate(ctx context.Context, contestId int64, sender *port.SenderSnapshot, ttl time.Duration) error { + args := m.Called(ctx, contestId, sender, ttl) + return args.Error(0) +} + +func (m *MockAppRedisPort) AcceptRequest(ctx context.Context, contestId, userId, processedBy int64) error { + args := m.Called(ctx, contestId, userId, processedBy) + return args.Error(0) +} + +func (m *MockAppRedisPort) RejectRequest(ctx context.Context, contestId, userId, processedBy int64) error { + args := m.Called(ctx, contestId, userId, processedBy) + return args.Error(0) +} + +func (m *MockAppRedisPort) CancelApplication(ctx context.Context, contestId, userId int64) error { + args := m.Called(ctx, contestId, userId) + return args.Error(0) +} + +func (m *MockAppRedisPort) GetApplication(ctx context.Context, contestId, userId int64) (*port.ContestApplication, error) { + args := m.Called(ctx, contestId, userId) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*port.ContestApplication), args.Error(1) +} + +func (m *MockAppRedisPort) GetPendingApplications(ctx context.Context, contestId int64) ([]*port.ContestApplication, error) { + args := m.Called(ctx, contestId) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]*port.ContestApplication), args.Error(1) +} + +func (m *MockAppRedisPort) GetAcceptedApplications(ctx context.Context, contestId int64) ([]int64, error) { + args := m.Called(ctx, contestId) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]int64), args.Error(1) +} + +func (m *MockAppRedisPort) GetRejectedApplications(ctx context.Context, contestId int64) ([]int64, error) { + args := m.Called(ctx, contestId) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]int64), args.Error(1) +} + +func (m *MockAppRedisPort) HasApplied(ctx context.Context, contestId, userId int64) (bool, error) { + args := m.Called(ctx, contestId, userId) + return args.Bool(0), args.Error(1) +} + +func (m *MockAppRedisPort) ExtendTTL(ctx context.Context, contestId int64, newTTL time.Duration) error { + args := m.Called(ctx, contestId, newTTL) + return args.Error(0) +} + +func (m *MockAppRedisPort) ClearApplications(ctx context.Context, contestId int64) error { + args := m.Called(ctx, contestId) + return args.Error(0) +} + +type MockUserQuery struct { + mock.Mock +} + +func (m *MockUserQuery) FindById(id int64) (*userDomain.User, error) { + args := m.Called(id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*userDomain.User), args.Error(1) +} + +func (m *MockUserQuery) FindByEmail(email string) (*userDomain.User, error) { + args := m.Called(email) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*userDomain.User), args.Error(1) +} + +// ==================== Integration Test Suite ==================== + +type ContestApplicationPointIntegrationSuite struct { + suite.Suite + container *support.MySQLContainer + db *gorm.DB + contestAdapter *contestAdapter.ContestDatabaseAdapter + memberAdapter *contestAdapter.ContestMemberDatabaseAdapter + scoreTableAdapter *pointAdapter.ValorantScoreTableDatabaseAdapter +} + +func (s *ContestApplicationPointIntegrationSuite) SetupSuite() { + ctx := context.Background() + var err error + + s.container, err = support.SetupMySQLContainer(ctx) + s.Require().NoError(err, "Failed to setup MySQL container") + + s.db = s.container.GetDB() + + // Auto-migrate schemas + err = s.db.AutoMigrate( + &domain.Contest{}, + &domain.ContestMember{}, + &pointDomain.ValorantScoreTable{}, + ) + s.Require().NoError(err, "Failed to migrate schemas") +} + +func (s *ContestApplicationPointIntegrationSuite) TearDownSuite() { + ctx := context.Background() + if s.container != nil { + s.container.Teardown(ctx) + } +} + +func (s *ContestApplicationPointIntegrationSuite) SetupTest() { + s.db.Exec("DELETE FROM contests_members") + s.db.Exec("DELETE FROM contests") + s.db.Exec("DELETE FROM valorant_score_tables") + + s.contestAdapter = contestAdapter.NewContestDatabaseAdapter(s.db) + s.memberAdapter = contestAdapter.NewContestMemberDatabaseAdapter(s.db) + s.scoreTableAdapter = pointAdapter.NewValorantScoreTableDatabaseAdapter(s.db) +} + +func (s *ContestApplicationPointIntegrationSuite) createTestContest(scoreTableId *int64) *domain.Contest { + contest := &domain.Contest{ + Title: "Point Test Contest", + Description: "Testing point integration", + MaxTeamCount: 8, + TotalPoint: 100, + ContestType: domain.ContestTypeTournament, + ContestStatus: domain.ContestStatusPending, + StartedAt: time.Now().Add(24 * time.Hour), + EndedAt: time.Now().Add(48 * time.Hour), + TotalTeamMember: 5, + GamePointTableId: scoreTableId, + } + saved, err := s.contestAdapter.Save(contest) + s.Require().NoError(err) + return saved +} + +func (s *ContestApplicationPointIntegrationSuite) createTestScoreTable() *pointDomain.ValorantScoreTable { + scoreTable := &pointDomain.ValorantScoreTable{ + Radiant: 100, + Immortal3: 95, + Immortal2: 90, + Immortal1: 85, + Ascendant3: 80, + Ascendant2: 75, + Ascendant1: 70, + Diamond3: 65, + Diamond2: 60, + Diamond1: 55, + Platinum3: 50, + Platinum2: 45, + Platinum1: 40, + Gold3: 35, + Gold2: 30, + Gold1: 25, + Silver3: 20, + Silver2: 15, + Silver1: 10, + Bronze3: 8, + Bronze2: 6, + Bronze1: 4, + Iron3: 3, + Iron2: 2, + Iron1: 1, + } + saved, err := s.scoreTableAdapter.Save(scoreTable) + s.Require().NoError(err) + return saved +} + +func (s *ContestApplicationPointIntegrationSuite) createLeader(contestId, userId int64) { + leader := domain.NewContestMemberAsLeader(userId, contestId) + err := s.memberAdapter.Save(leader) + s.Require().NoError(err) +} + +func strPtr(st string) *string { return &st } +func intPtr(i int) *int { return &i } +func int64Ptr(i int64) *int64 { return &i } + +func createValorantLinkedUser(id int64, currentTier, peakTier string) *userDomain.User { + now := time.Now() + return &userDomain.User{ + Id: id, + Email: "test@example.com", + Username: "testuser", + Tag: "0001", + Avatar: "avatar.png", + RiotName: strPtr("TestPlayer"), + RiotTag: strPtr("KR1"), + Region: strPtr("kr"), + CurrentTierPatched: strPtr(currentTier), + PeakTierPatched: strPtr(peakTier), + CurrentTier: intPtr(15), + PeakTier: intPtr(18), + Elo: intPtr(100), + RankingInTier: intPtr(50), + ValorantUpdatedAt: &now, + } +} + +// ==================== Integration Tests ==================== + +func (s *ContestApplicationPointIntegrationSuite) TestAcceptApplication_PersistsPointInDB() { + // Given: A contest with score table, and a stored application with calculated point + scoreTable := s.createTestScoreTable() + contest := s.createTestContest(&scoreTable.ScoreTableID) + + leaderId := int64(1) + applicantId := int64(10) + s.createLeader(contest.ContestID, leaderId) + + // Setup mocks + mockRedis := new(MockAppRedisPort) + mockOAuth2 := new(MockOAuth2Port) + mockEventPub := new(MockEventPub) + mockUserQuery := new(MockUserQuery) + + service := application.NewContestApplicationService( + mockRedis, + s.contestAdapter, + s.memberAdapter, + mockEventPub, + mockOAuth2, + mockUserQuery, + ) + service.SetScoreTablePort(s.scoreTableAdapter) + + ctx := context.Background() + + // Simulate stored application in Redis with Point=73 (Diamond 2 + Immortal 1) + storedApplication := &port.ContestApplication{ + UserID: applicantId, + ContestID: contest.ContestID, + Status: port.ApplicationStatusPending, + Sender: &port.SenderSnapshot{ + UserID: applicantId, + Username: "testuser", + Tag: "0001", + Point: 73, // Pre-calculated: (Diamond2=60 + Immortal1=85) / 2 = 72.5 → 73 + }, + } + + mockRedis.On("GetApplication", ctx, contest.ContestID, applicantId).Return(storedApplication, nil) + mockRedis.On("AcceptRequest", ctx, contest.ContestID, applicantId, leaderId).Return(nil) + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + mockOAuth2.On("FindDiscordAccountByUserId", mock.AnythingOfType("int64")).Return(nil, exception.ErrDiscordUserCannotFound).Maybe() + + // When + err := service.AcceptApplication(ctx, contest.ContestID, applicantId, leaderId) + + // Then + s.NoError(err) + + // Verify member is persisted in DB with correct point + member, err := s.memberAdapter.GetByContestAndUser(contest.ContestID, applicantId) + s.NoError(err) + s.Equal(applicantId, member.UserID) + s.Equal(contest.ContestID, member.ContestID) + s.Equal(73, member.Point) + s.Equal(domain.MemberTypeNormal, member.MemberType) + s.Equal(domain.LeaderTypeMember, member.LeaderType) +} + +func (s *ContestApplicationPointIntegrationSuite) TestAcceptApplication_ZeroPointPersistedInDB() { + // Given: Application with Point=0 (non-Valorant user) + contest := s.createTestContest(nil) + + leaderId := int64(1) + applicantId := int64(20) + s.createLeader(contest.ContestID, leaderId) + + mockRedis := new(MockAppRedisPort) + mockOAuth2 := new(MockOAuth2Port) + mockEventPub := new(MockEventPub) + mockUserQuery := new(MockUserQuery) + + service := application.NewContestApplicationService( + mockRedis, + s.contestAdapter, + s.memberAdapter, + mockEventPub, + mockOAuth2, + mockUserQuery, + ) + + ctx := context.Background() + + storedApplication := &port.ContestApplication{ + UserID: applicantId, + ContestID: contest.ContestID, + Status: port.ApplicationStatusPending, + Sender: &port.SenderSnapshot{ + UserID: applicantId, + Username: "novaluser", + Tag: "0002", + Point: 0, + }, + } + + mockRedis.On("GetApplication", ctx, contest.ContestID, applicantId).Return(storedApplication, nil) + mockRedis.On("AcceptRequest", ctx, contest.ContestID, applicantId, leaderId).Return(nil) + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + mockOAuth2.On("FindDiscordAccountByUserId", mock.AnythingOfType("int64")).Return(nil, exception.ErrDiscordUserCannotFound).Maybe() + + // When + err := service.AcceptApplication(ctx, contest.ContestID, applicantId, leaderId) + + // Then + s.NoError(err) + + member, err := s.memberAdapter.GetByContestAndUser(contest.ContestID, applicantId) + s.NoError(err) + s.Equal(0, member.Point) +} + +func (s *ContestApplicationPointIntegrationSuite) TestRequestAndAccept_EndToEndPointFlow() { + // Given: Full flow - RequestParticipate calculates point, AcceptApplication persists it + scoreTable := s.createTestScoreTable() + contest := s.createTestContest(&scoreTable.ScoreTableID) + + leaderId := int64(1) + applicantId := int64(30) + s.createLeader(contest.ContestID, leaderId) + + mockRedis := new(MockAppRedisPort) + mockOAuth2 := new(MockOAuth2Port) + mockEventPub := new(MockEventPub) + mockUserQuery := new(MockUserQuery) + + service := application.NewContestApplicationService( + mockRedis, + s.contestAdapter, + s.memberAdapter, + mockEventPub, + mockOAuth2, + mockUserQuery, + ) + service.SetScoreTablePort(s.scoreTableAdapter) + + ctx := context.Background() + + // Phase 1: RequestParticipate - capture the SenderSnapshot + user := createValorantLinkedUser(applicantId, "Platinum 2", "Diamond 3") + discordAccount := &oauth2Domain.DiscordAccount{DiscordId: "discord_30", UserId: applicantId} + + mockOAuth2.On("FindDiscordAccountByUserId", applicantId).Return(discordAccount, nil) + mockUserQuery.On("FindById", applicantId).Return(user, nil) + var capturedSnapshot *port.SenderSnapshot + mockRedis.On("RequestParticipate", ctx, contest.ContestID, mock.MatchedBy(func(s *port.SenderSnapshot) bool { + capturedSnapshot = s + return true + }), mock.AnythingOfType("time.Duration")).Return(nil) + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + + _, err := service.RequestParticipate(ctx, contest.ContestID, applicantId) + s.NoError(err) + s.NotNil(capturedSnapshot) + + // Expected: (Platinum2=45 + Diamond3=65) / 2 = 55 + s.Equal(55, capturedSnapshot.Point) + + // Phase 2: AcceptApplication - use captured snapshot point + storedApplication := &port.ContestApplication{ + UserID: applicantId, + ContestID: contest.ContestID, + Status: port.ApplicationStatusPending, + Sender: capturedSnapshot, + } + + mockRedis.On("GetApplication", ctx, contest.ContestID, applicantId).Return(storedApplication, nil) + mockRedis.On("AcceptRequest", ctx, contest.ContestID, applicantId, leaderId).Return(nil) + mockOAuth2.On("FindDiscordAccountByUserId", mock.AnythingOfType("int64")).Return(nil, exception.ErrDiscordUserCannotFound).Maybe() + + err = service.AcceptApplication(ctx, contest.ContestID, applicantId, leaderId) + s.NoError(err) + + // Verify point persisted correctly in DB + member, err := s.memberAdapter.GetByContestAndUser(contest.ContestID, applicantId) + s.NoError(err) + s.Equal(55, member.Point) + s.Equal(domain.MemberTypeNormal, member.MemberType) +} + +func (s *ContestApplicationPointIntegrationSuite) TestMultipleApplicants_DifferentPoints() { + // Given: Multiple users with different tiers apply and get accepted + scoreTable := s.createTestScoreTable() + contest := s.createTestContest(&scoreTable.ScoreTableID) + + leaderId := int64(1) + s.createLeader(contest.ContestID, leaderId) + + mockRedis := new(MockAppRedisPort) + mockOAuth2 := new(MockOAuth2Port) + mockEventPub := new(MockEventPub) + mockUserQuery := new(MockUserQuery) + + service := application.NewContestApplicationService( + mockRedis, + s.contestAdapter, + s.memberAdapter, + mockEventPub, + mockOAuth2, + mockUserQuery, + ) + service.SetScoreTablePort(s.scoreTableAdapter) + + ctx := context.Background() + + applicants := []struct { + userId int64 + currentTier string + peakTier string + expectedPoint int + }{ + {10, "Iron 1", "Iron 1", 1}, // (1+1)/2 = 1 + {11, "Gold 2", "Platinum 1", 33}, // (30+40)/2 = 35 → wrong, let me recalculate: Gold2=30, Platinum1=40 → (30+40)/2 = 35 + {12, "Diamond 1", "Radiant", 78}, // (55+100)/2 = 77.5 → 78 + {13, "Radiant", "Radiant", 100}, // (100+100)/2 = 100 + } + // Fix: Gold2=30, Platinum1=40 → (30+40)/2 = 35 + applicants[1].expectedPoint = 35 + + for _, a := range applicants { + storedApp := &port.ContestApplication{ + UserID: a.userId, + ContestID: contest.ContestID, + Status: port.ApplicationStatusPending, + Sender: &port.SenderSnapshot{ + UserID: a.userId, + Username: "user", + Tag: "0001", + Point: a.expectedPoint, + }, + } + + mockRedis.On("GetApplication", ctx, contest.ContestID, a.userId).Return(storedApp, nil) + mockRedis.On("AcceptRequest", ctx, contest.ContestID, a.userId, leaderId).Return(nil) + } + mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) + mockOAuth2.On("FindDiscordAccountByUserId", mock.AnythingOfType("int64")).Return(nil, exception.ErrDiscordUserCannotFound).Maybe() + + // When: Accept all applications + for _, a := range applicants { + err := service.AcceptApplication(ctx, contest.ContestID, a.userId, leaderId) + s.NoError(err) + } + + // Then: Verify each member has correct point in DB + for _, a := range applicants { + member, err := s.memberAdapter.GetByContestAndUser(contest.ContestID, a.userId) + s.NoError(err, "Failed to get member for userId=%d", a.userId) + s.Equal(a.expectedPoint, member.Point, "Wrong point for userId=%d (tier=%s/%s)", a.userId, a.currentTier, a.peakTier) + } +} + +// ==================== Run the test suite ==================== + +func TestContestApplicationPointIntegrationSuite(t *testing.T) { + suite.Run(t, new(ContestApplicationPointIntegrationSuite)) +} diff --git a/test/global/support/suite_helpers.go b/test/global/support/suite_helpers.go new file mode 100644 index 0000000..08547a1 --- /dev/null +++ b/test/global/support/suite_helpers.go @@ -0,0 +1,72 @@ +package support + +import ( + "context" + "fmt" +) + +// SetupDatabaseSuite 는 DatabaseTestSuite 구현체의 SetupSuite에서 호출한다. +// MySQL 컨테이너를 시작하고, DB/Container를 Suite에 주입한 뒤, MigrateSchema를 실행한다. +func SetupDatabaseSuite(ctx context.Context, s DatabaseTestSuite) (*MySQLContainer, error) { + container, err := SetupMySQLContainer(ctx) + if err != nil { + return nil, fmt.Errorf("failed to setup MySQL container: %w", err) + } + + db := container.GetDB() + s.SetDB(db) + s.SetMySQLContainer(container) + + if err := s.MigrateSchema(db); err != nil { + container.Teardown(ctx) + return nil, fmt.Errorf("failed to migrate schema: %w", err) + } + + return container, nil +} + +// TeardownDatabaseSuite 는 DatabaseTestSuite 구현체의 TearDownSuite에서 호출한다. +func TeardownDatabaseSuite(ctx context.Context, container *MySQLContainer) { + if container != nil { + container.Teardown(ctx) + } +} + +// SetupRedisSuite 는 RedisTestSuite 구현체의 SetupSuite에서 호출한다. +// Redis 컨테이너를 시작하고, Client/Container를 Suite에 주입한다. +func SetupRedisSuite(ctx context.Context, s RedisTestSuite) (*RedisContainer, error) { + container, err := SetupRedisContainer(ctx) + if err != nil { + return nil, fmt.Errorf("failed to setup Redis container: %w", err) + } + + client := container.GetClient() + s.SetRedisClient(client) + s.SetRedisContainer(container) + + return container, nil +} + +// TeardownRedisSuite 는 RedisTestSuite 구현체의 TearDownSuite에서 호출한다. +func TeardownRedisSuite(ctx context.Context, container *RedisContainer) { + if container != nil { + container.Teardown(ctx) + } +} + +// SetupFullInfraSuite 는 FullInfraTestSuite 구현체의 SetupSuite에서 호출한다. +// MySQL과 Redis 컨테이너를 모두 시작하고, Suite에 주입한다. +func SetupFullInfraSuite(ctx context.Context, s FullInfraTestSuite) (*MySQLContainer, *RedisContainer, error) { + mysqlContainer, err := SetupDatabaseSuite(ctx, s) + if err != nil { + return nil, nil, err + } + + redisContainer, err := SetupRedisSuite(ctx, s) + if err != nil { + TeardownDatabaseSuite(ctx, mysqlContainer) + return nil, nil, fmt.Errorf("failed to setup Redis container: %w", err) + } + + return mysqlContainer, redisContainer, nil +} diff --git a/test/global/support/suite_interfaces.go b/test/global/support/suite_interfaces.go new file mode 100644 index 0000000..fa0ad9f --- /dev/null +++ b/test/global/support/suite_interfaces.go @@ -0,0 +1,28 @@ +package support + +import ( + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +// DatabaseTestSuite - MySQL TestContainer가 필요한 Adapter/Integration 테스트용 인터페이스. +// Suite struct가 이 인터페이스를 구현하면 SetupDatabaseSuite 헬퍼로 컨테이너 셋업을 위임할 수 있다. +type DatabaseTestSuite interface { + SetDB(db *gorm.DB) + SetMySQLContainer(container *MySQLContainer) + MigrateSchema(db *gorm.DB) error // AutoMigrate 대상 엔티티 정의 + CleanupTables(db *gorm.DB) // 테스트 간 테이블 클리닝 +} + +// RedisTestSuite - Redis TestContainer가 필요한 Redis Adapter 테스트용 인터페이스. +type RedisTestSuite interface { + SetRedisClient(client *redis.Client) + SetRedisContainer(container *RedisContainer) + CleanupRedis(client *redis.Client) // FlushAll 등 +} + +// FullInfraTestSuite - MySQL + Redis 모두 필요한 Integration 테스트용 인터페이스. +type FullInfraTestSuite interface { + DatabaseTestSuite + RedisTestSuite +} diff --git a/test/point/application/dto/valorant_dto_test.go b/test/point/application/dto/valorant_dto_test.go new file mode 100644 index 0000000..a321b06 --- /dev/null +++ b/test/point/application/dto/valorant_dto_test.go @@ -0,0 +1,109 @@ +package dto_test + +import ( + "testing" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/point/application/dto" + "github.com/FOR-GAMERS/GAMERS-BE/internal/point/domain" + + "github.com/stretchr/testify/assert" +) + +func createTestScoreTable(id int64) *domain.ValorantScoreTable { + return &domain.ValorantScoreTable{ + ScoreTableID: id, + Radiant: 2500, + Immortal3: 2200, + Immortal2: 2000, + Immortal1: 1800, + Ascendant3: 1600, + Ascendant2: 1400, + Ascendant1: 1200, + Diamond3: 1100, + Diamond2: 1000, + Diamond1: 900, + Platinum3: 800, + Platinum2: 700, + Platinum1: 600, + Gold3: 500, + Gold2: 450, + Gold1: 400, + Silver3: 350, + Silver2: 300, + Silver1: 250, + Bronze3: 200, + Bronze2: 150, + Bronze1: 100, + Iron3: 75, + Iron2: 50, + Iron1: 25, + } +} + +func TestToValorantScoreTableResponse(t *testing.T) { + // Given + table := createTestScoreTable(1) + + // When + response := dto.ToValorantScoreTableResponse(table) + + // Then + assert.NotNil(t, response) + assert.Equal(t, table.ScoreTableID, response.ScoreTableID) + assert.Equal(t, table.Radiant, response.Radiant) + assert.Equal(t, table.Immortal3, response.Immortal3) + assert.Equal(t, table.Immortal2, response.Immortal2) + assert.Equal(t, table.Immortal1, response.Immortal1) + assert.Equal(t, table.Ascendant3, response.Ascendant3) + assert.Equal(t, table.Ascendant2, response.Ascendant2) + assert.Equal(t, table.Ascendant1, response.Ascendant1) + assert.Equal(t, table.Diamond3, response.Diamond3) + assert.Equal(t, table.Diamond2, response.Diamond2) + assert.Equal(t, table.Diamond1, response.Diamond1) + assert.Equal(t, table.Platinum3, response.Platinum3) + assert.Equal(t, table.Platinum2, response.Platinum2) + assert.Equal(t, table.Platinum1, response.Platinum1) + assert.Equal(t, table.Gold3, response.Gold3) + assert.Equal(t, table.Gold2, response.Gold2) + assert.Equal(t, table.Gold1, response.Gold1) + assert.Equal(t, table.Silver3, response.Silver3) + assert.Equal(t, table.Silver2, response.Silver2) + assert.Equal(t, table.Silver1, response.Silver1) + assert.Equal(t, table.Bronze3, response.Bronze3) + assert.Equal(t, table.Bronze2, response.Bronze2) + assert.Equal(t, table.Bronze1, response.Bronze1) + assert.Equal(t, table.Iron3, response.Iron3) + assert.Equal(t, table.Iron2, response.Iron2) + assert.Equal(t, table.Iron1, response.Iron1) +} + +func TestToValorantScoreTableResponse_Nil(t *testing.T) { + // When + response := dto.ToValorantScoreTableResponse(nil) + + // Then + assert.Nil(t, response) +} + +func TestToValorantScoreTableResponseList(t *testing.T) { + // Given + tables := []*domain.ValorantScoreTable{ + createTestScoreTable(1), + createTestScoreTable(2), + createTestScoreTable(3), + } + + // When + responses := dto.ToValorantScoreTableResponseList(tables) + + // Then + assert.Len(t, responses, 3) + assert.Equal(t, int64(1), responses[0].ScoreTableID) + assert.Equal(t, int64(2), responses[1].ScoreTableID) + assert.Equal(t, int64(3), responses[2].ScoreTableID) + + for i, resp := range responses { + assert.Equal(t, tables[i].Radiant, resp.Radiant) + assert.Equal(t, tables[i].Iron1, resp.Iron1) + } +} diff --git a/test/point/application/valorant_service_test.go b/test/point/application/valorant_service_test.go new file mode 100644 index 0000000..00e3595 --- /dev/null +++ b/test/point/application/valorant_service_test.go @@ -0,0 +1,290 @@ +package application_test + +import ( + "errors" + "testing" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" + "github.com/FOR-GAMERS/GAMERS-BE/internal/point/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/point/application/dto" + "github.com/FOR-GAMERS/GAMERS-BE/internal/point/domain" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// ==================== Mock Definitions ==================== + +type MockValorantScoreTableDatabasePort struct { + mock.Mock +} + +func (m *MockValorantScoreTableDatabasePort) Save(scoreTable *domain.ValorantScoreTable) (*domain.ValorantScoreTable, error) { + args := m.Called(scoreTable) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*domain.ValorantScoreTable), args.Error(1) +} + +func (m *MockValorantScoreTableDatabasePort) GetByID(scoreTableID int64) (*domain.ValorantScoreTable, error) { + args := m.Called(scoreTableID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*domain.ValorantScoreTable), args.Error(1) +} + +func (m *MockValorantScoreTableDatabasePort) GetAll() ([]*domain.ValorantScoreTable, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]*domain.ValorantScoreTable), args.Error(1) +} + +func (m *MockValorantScoreTableDatabasePort) Update(scoreTable *domain.ValorantScoreTable) error { + args := m.Called(scoreTable) + return args.Error(0) +} + +func (m *MockValorantScoreTableDatabasePort) Delete(scoreTableID int64) error { + args := m.Called(scoreTableID) + return args.Error(0) +} + +// ==================== Test Fixtures ==================== + +func createValidScoreTableRequest() *dto.CreateValorantScoreTableDto { + return &dto.CreateValorantScoreTableDto{ + Radiant: 2500, + Immortal3: 2200, + Immortal2: 2000, + Immortal1: 1800, + Ascendant3: 1600, + Ascendant2: 1400, + Ascendant1: 1200, + Diamond3: 1100, + Diamond2: 1000, + Diamond1: 900, + Platinum3: 800, + Platinum2: 700, + Platinum1: 600, + Gold3: 500, + Gold2: 450, + Gold1: 400, + Silver3: 350, + Silver2: 300, + Silver1: 250, + Bronze3: 200, + Bronze2: 150, + Bronze1: 100, + Iron3: 75, + Iron2: 50, + Iron1: 25, + } +} + +func createSavedScoreTable(req *dto.CreateValorantScoreTableDto, id int64) *domain.ValorantScoreTable { + return &domain.ValorantScoreTable{ + ScoreTableID: id, + Radiant: req.Radiant, + Immortal3: req.Immortal3, + Immortal2: req.Immortal2, + Immortal1: req.Immortal1, + Ascendant3: req.Ascendant3, + Ascendant2: req.Ascendant2, + Ascendant1: req.Ascendant1, + Diamond3: req.Diamond3, + Diamond2: req.Diamond2, + Diamond1: req.Diamond1, + Platinum3: req.Platinum3, + Platinum2: req.Platinum2, + Platinum1: req.Platinum1, + Gold3: req.Gold3, + Gold2: req.Gold2, + Gold1: req.Gold1, + Silver3: req.Silver3, + Silver2: req.Silver2, + Silver1: req.Silver1, + Bronze3: req.Bronze3, + Bronze2: req.Bronze2, + Bronze1: req.Bronze1, + Iron3: req.Iron3, + Iron2: req.Iron2, + Iron1: req.Iron1, + } +} + +// ==================== CreateScoreTable Tests ==================== + +func TestValorantService_CreateScoreTable_Success(t *testing.T) { + // Given + mockDB := new(MockValorantScoreTableDatabasePort) + service := application.NewValorantService(mockDB) + + req := createValidScoreTableRequest() + saved := createSavedScoreTable(req, 1) + + mockDB.On("Save", mock.AnythingOfType("*domain.ValorantScoreTable")).Return(saved, nil) + + // When + result, err := service.CreateScoreTable(req) + + // Then + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, int64(1), result.ScoreTableID) + assert.Equal(t, req.Radiant, result.Radiant) + assert.Equal(t, req.Iron1, result.Iron1) + + mockDB.AssertExpectations(t) +} + +func TestValorantService_CreateScoreTable_DBError(t *testing.T) { + // Given + mockDB := new(MockValorantScoreTableDatabasePort) + service := application.NewValorantService(mockDB) + + req := createValidScoreTableRequest() + dbErr := errors.New("database connection failed") + + mockDB.On("Save", mock.AnythingOfType("*domain.ValorantScoreTable")).Return(nil, dbErr) + + // When + result, err := service.CreateScoreTable(req) + + // Then + assert.Error(t, err) + assert.Nil(t, result) + assert.Equal(t, dbErr, err) + + mockDB.AssertExpectations(t) +} + +// ==================== GetScoreTable Tests ==================== + +func TestValorantService_GetScoreTable_Success(t *testing.T) { + // Given + mockDB := new(MockValorantScoreTableDatabasePort) + service := application.NewValorantService(mockDB) + + scoreTableID := int64(1) + expected := createSavedScoreTable(createValidScoreTableRequest(), scoreTableID) + + mockDB.On("GetByID", scoreTableID).Return(expected, nil) + + // When + result, err := service.GetScoreTable(scoreTableID) + + // Then + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, scoreTableID, result.ScoreTableID) + assert.Equal(t, expected.Radiant, result.Radiant) + + mockDB.AssertExpectations(t) +} + +func TestValorantService_GetScoreTable_NotFound(t *testing.T) { + // Given + mockDB := new(MockValorantScoreTableDatabasePort) + service := application.NewValorantService(mockDB) + + scoreTableID := int64(999) + + mockDB.On("GetByID", scoreTableID).Return(nil, exception.ErrScoreTableNotFound) + + // When + result, err := service.GetScoreTable(scoreTableID) + + // Then + assert.Error(t, err) + assert.Nil(t, result) + assert.Equal(t, exception.ErrScoreTableNotFound, err) + + mockDB.AssertExpectations(t) +} + +// ==================== GetAllScoreTables Tests ==================== + +func TestValorantService_GetAllScoreTables_Success(t *testing.T) { + // Given + mockDB := new(MockValorantScoreTableDatabasePort) + service := application.NewValorantService(mockDB) + + req := createValidScoreTableRequest() + tables := []*domain.ValorantScoreTable{ + createSavedScoreTable(req, 1), + createSavedScoreTable(req, 2), + } + + mockDB.On("GetAll").Return(tables, nil) + + // When + result, err := service.GetAllScoreTables() + + // Then + assert.NoError(t, err) + assert.Len(t, result, 2) + assert.Equal(t, int64(1), result[0].ScoreTableID) + assert.Equal(t, int64(2), result[1].ScoreTableID) + + mockDB.AssertExpectations(t) +} + +func TestValorantService_GetAllScoreTables_Empty(t *testing.T) { + // Given + mockDB := new(MockValorantScoreTableDatabasePort) + service := application.NewValorantService(mockDB) + + mockDB.On("GetAll").Return([]*domain.ValorantScoreTable{}, nil) + + // When + result, err := service.GetAllScoreTables() + + // Then + assert.NoError(t, err) + assert.Empty(t, result) + + mockDB.AssertExpectations(t) +} + +// ==================== DeleteScoreTable Tests ==================== + +func TestValorantService_DeleteScoreTable_Success(t *testing.T) { + // Given + mockDB := new(MockValorantScoreTableDatabasePort) + service := application.NewValorantService(mockDB) + + scoreTableID := int64(1) + + mockDB.On("Delete", scoreTableID).Return(nil) + + // When + err := service.DeleteScoreTable(scoreTableID) + + // Then + assert.NoError(t, err) + + mockDB.AssertExpectations(t) +} + +func TestValorantService_DeleteScoreTable_NotFound(t *testing.T) { + // Given + mockDB := new(MockValorantScoreTableDatabasePort) + service := application.NewValorantService(mockDB) + + scoreTableID := int64(999) + + mockDB.On("Delete", scoreTableID).Return(exception.ErrScoreTableNotFound) + + // When + err := service.DeleteScoreTable(scoreTableID) + + // Then + assert.Error(t, err) + assert.Equal(t, exception.ErrScoreTableNotFound, err) + + mockDB.AssertExpectations(t) +} diff --git a/test/point/domain/valorant_score_table_test.go b/test/point/domain/valorant_score_table_test.go new file mode 100644 index 0000000..b93f679 --- /dev/null +++ b/test/point/domain/valorant_score_table_test.go @@ -0,0 +1,108 @@ +package domain_test + +import ( + "testing" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/point/domain" + "github.com/stretchr/testify/assert" +) + +func createTestScoreTable() *domain.ValorantScoreTable { + return &domain.ValorantScoreTable{ + ScoreTableID: 1, + Radiant: 100, + Immortal3: 95, + Immortal2: 90, + Immortal1: 85, + Ascendant3: 80, + Ascendant2: 75, + Ascendant1: 70, + Diamond3: 65, + Diamond2: 60, + Diamond1: 55, + Platinum3: 50, + Platinum2: 45, + Platinum1: 40, + Gold3: 35, + Gold2: 30, + Gold1: 25, + Silver3: 20, + Silver2: 15, + Silver1: 10, + Bronze3: 8, + Bronze2: 6, + Bronze1: 4, + Iron3: 3, + Iron2: 2, + Iron1: 1, + } +} + +func TestValorantScoreTable_GetTierPoint(t *testing.T) { + scoreTable := createTestScoreTable() + + tests := []struct { + name string + tierFullName string + expected int + }{ + // Exact match cases + {"Radiant", "Radiant", 100}, + {"Immortal 3", "Immortal 3", 95}, + {"Immortal 2", "Immortal 2", 90}, + {"Immortal 1", "Immortal 1", 85}, + {"Ascendant 3", "Ascendant 3", 80}, + {"Ascendant 2", "Ascendant 2", 75}, + {"Ascendant 1", "Ascendant 1", 70}, + {"Diamond 3", "Diamond 3", 65}, + {"Diamond 2", "Diamond 2", 60}, + {"Diamond 1", "Diamond 1", 55}, + {"Platinum 3", "Platinum 3", 50}, + {"Platinum 2", "Platinum 2", 45}, + {"Platinum 1", "Platinum 1", 40}, + {"Gold 3", "Gold 3", 35}, + {"Gold 2", "Gold 2", 30}, + {"Gold 1", "Gold 1", 25}, + {"Silver 3", "Silver 3", 20}, + {"Silver 2", "Silver 2", 15}, + {"Silver 1", "Silver 1", 10}, + {"Bronze 3", "Bronze 3", 8}, + {"Bronze 2", "Bronze 2", 6}, + {"Bronze 1", "Bronze 1", 4}, + {"Iron 3", "Iron 3", 3}, + {"Iron 2", "Iron 2", 2}, + {"Iron 1", "Iron 1", 1}, + + // Case insensitivity + {"lowercase radiant", "radiant", 100}, + {"UPPERCASE DIAMOND 2", "DIAMOND 2", 60}, + {"mixed case Platinum 1", "platinum 1", 40}, + + // Unknown/invalid tiers return 0 + {"empty string", "", 0}, + {"unknown tier", "Challenger", 0}, + {"partial match", "Diamond", 0}, + {"unranked", "Unranked", 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := scoreTable.GetTierPoint(tt.tierFullName) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestValorantScoreTable_GetTierPoint_DifferentScoreValues(t *testing.T) { + // Verify that different score tables can have different values + scoreTable := &domain.ValorantScoreTable{ + Radiant: 200, + Diamond1: 110, + Iron1: 2, + } + + assert.Equal(t, 200, scoreTable.GetTierPoint("Radiant")) + assert.Equal(t, 110, scoreTable.GetTierPoint("Diamond 1")) + assert.Equal(t, 2, scoreTable.GetTierPoint("Iron 1")) + assert.Equal(t, 0, scoreTable.GetTierPoint("Gold 1")) // zero-value field +} diff --git a/test/point/infra/persistence/valorant_score_table_adapter_test.go b/test/point/infra/persistence/valorant_score_table_adapter_test.go new file mode 100644 index 0000000..a92e7b2 --- /dev/null +++ b/test/point/infra/persistence/valorant_score_table_adapter_test.go @@ -0,0 +1,215 @@ +package persistence_test + +import ( + "context" + "testing" + "time" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" + "github.com/FOR-GAMERS/GAMERS-BE/internal/point/domain" + "github.com/FOR-GAMERS/GAMERS-BE/internal/point/infra/persistence/adapter" + "github.com/FOR-GAMERS/GAMERS-BE/test/global/support" + + "github.com/stretchr/testify/suite" + "gorm.io/gorm" +) + +type ValorantScoreTableAdapterTestSuite struct { + suite.Suite + container *support.MySQLContainer + db *gorm.DB + adapter *adapter.ValorantScoreTableDatabaseAdapter +} + +// DatabaseTestSuite interface implementation +func (s *ValorantScoreTableAdapterTestSuite) SetDB(db *gorm.DB) { + s.db = db +} + +func (s *ValorantScoreTableAdapterTestSuite) SetMySQLContainer(container *support.MySQLContainer) { + s.container = container +} + +func (s *ValorantScoreTableAdapterTestSuite) MigrateSchema(db *gorm.DB) error { + return db.AutoMigrate(&domain.ValorantScoreTable{}) +} + +func (s *ValorantScoreTableAdapterTestSuite) CleanupTables(db *gorm.DB) { + db.Exec("DELETE FROM valorant_score_tables") +} + +func (s *ValorantScoreTableAdapterTestSuite) SetupSuite() { + ctx := context.Background() + _, err := support.SetupDatabaseSuite(ctx, s) + s.Require().NoError(err, "Failed to setup database suite") +} + +func (s *ValorantScoreTableAdapterTestSuite) TearDownSuite() { + ctx := context.Background() + support.TeardownDatabaseSuite(ctx, s.container) +} + +func (s *ValorantScoreTableAdapterTestSuite) SetupTest() { + s.CleanupTables(s.db) + s.adapter = adapter.NewValorantScoreTableDatabaseAdapter(s.db) +} + +func createTestScoreTable() *domain.ValorantScoreTable { + return domain.NewValorantScoreTable( + 2500, + 2200, 2000, 1800, + 1600, 1400, 1200, + 1100, 1000, 900, + 800, 700, 600, + 500, 450, 400, + 350, 300, 250, + 200, 150, 100, + 75, 50, 25, + ) +} + +// ==================== Save Tests ==================== + +func (s *ValorantScoreTableAdapterTestSuite) TestSave_Success() { + // Given + scoreTable := createTestScoreTable() + + // When + saved, err := s.adapter.Save(scoreTable) + + // Then + s.NoError(err) + s.NotNil(saved) + s.Greater(saved.ScoreTableID, int64(0)) + s.Equal(2500, saved.Radiant) + s.Equal(25, saved.Iron1) +} + +// ==================== GetByID Tests ==================== + +func (s *ValorantScoreTableAdapterTestSuite) TestGetByID_Success() { + // Given + scoreTable := createTestScoreTable() + saved, err := s.adapter.Save(scoreTable) + s.NoError(err) + + // When + found, err := s.adapter.GetByID(saved.ScoreTableID) + + // Then + s.NoError(err) + s.NotNil(found) + s.Equal(saved.ScoreTableID, found.ScoreTableID) + s.Equal(saved.Radiant, found.Radiant) + s.Equal(saved.Immortal3, found.Immortal3) + s.Equal(saved.Iron1, found.Iron1) +} + +func (s *ValorantScoreTableAdapterTestSuite) TestGetByID_NotFound() { + // When + found, err := s.adapter.GetByID(999) + + // Then + s.Error(err) + s.Nil(found) + s.Equal(exception.ErrScoreTableNotFound, err) +} + +// ==================== GetAll Tests ==================== + +func (s *ValorantScoreTableAdapterTestSuite) TestGetAll_Success() { + // Given + table1 := createTestScoreTable() + _, err := s.adapter.Save(table1) + s.NoError(err) + + time.Sleep(1 * time.Second) // ensure different created_at + + table2 := createTestScoreTable() + table2.Radiant = 3000 + _, err = s.adapter.Save(table2) + s.NoError(err) + + // When + results, err := s.adapter.GetAll() + + // Then + s.NoError(err) + s.Len(results, 2) + // Ordered by created_at DESC, so table2 should be first + s.Equal(3000, results[0].Radiant) + s.Equal(2500, results[1].Radiant) +} + +func (s *ValorantScoreTableAdapterTestSuite) TestGetAll_Empty() { + // When + results, err := s.adapter.GetAll() + + // Then + s.NoError(err) + s.Empty(results) +} + +// ==================== Update Tests ==================== + +func (s *ValorantScoreTableAdapterTestSuite) TestUpdate_Success() { + // Given + scoreTable := createTestScoreTable() + saved, err := s.adapter.Save(scoreTable) + s.NoError(err) + + // Re-fetch to get proper timestamps from DB + fetched, err := s.adapter.GetByID(saved.ScoreTableID) + s.NoError(err) + + fetched.Radiant = 3000 + fetched.Iron1 = 50 + + // When + err = s.adapter.Update(fetched) + + // Then + s.NoError(err) + + // Verify + found, err := s.adapter.GetByID(fetched.ScoreTableID) + s.NoError(err) + s.Equal(3000, found.Radiant) + s.Equal(50, found.Iron1) +} + +// ==================== Delete Tests ==================== + +func (s *ValorantScoreTableAdapterTestSuite) TestDelete_Success() { + // Given + scoreTable := createTestScoreTable() + saved, err := s.adapter.Save(scoreTable) + s.NoError(err) + + // When + err = s.adapter.Delete(saved.ScoreTableID) + + // Then + s.NoError(err) + + // Verify deletion + found, err := s.adapter.GetByID(saved.ScoreTableID) + s.Error(err) + s.Nil(found) + s.Equal(exception.ErrScoreTableNotFound, err) +} + +func (s *ValorantScoreTableAdapterTestSuite) TestDelete_NotFound() { + // When + err := s.adapter.Delete(999) + + // Then + s.Error(err) + s.Equal(exception.ErrScoreTableNotFound, err) +} + +// ==================== Run the test suite ==================== + +func TestValorantScoreTableAdapterSuite(t *testing.T) { + suite.Run(t, new(ValorantScoreTableAdapterTestSuite)) +} From c7b68b50d58d9eb3dc8c896985c0ed30ee4d3fbe Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:33:18 +0900 Subject: [PATCH 09/24] Fix: Apply code review feedback on PR #69 - [HIGH] Propagate score table fetch error instead of silent fallback to 0: When contest has GamePointTableId but score table is not found, return the error to caller (server-side configuration error) - [SECURITY] Add nil-check for application before AcceptApplication: Return ErrApplicationNotFound when application is nil to prevent unauthorized member addition by contest leaders - [SECURITY] Add status check for application before AcceptApplication: Return ErrApplicationNotPending when application is not in PENDING state - [TEST] Update TestRequestParticipate_ScoreTableNotFound to expect error return - [TEST] Add TestAcceptApplication_NilApplication_ReturnsError - [TEST] Add TestAcceptApplication_NonPendingApplication_ReturnsError - [MEDIUM] Fix confusing test data initialization in integration test: Initialize Gold2+Platinum1 applicant with correct value (35) directly Co-Authored-By: Claude Sonnet 4.6 --- .../contest_application_service.go | 8 +- .../contest_application_service_test.go | 118 ++++++++++++++++-- .../contest_application_integration_test.go | 10 +- 3 files changed, 118 insertions(+), 18 deletions(-) diff --git a/internal/contest/application/contest_application_service.go b/internal/contest/application/contest_application_service.go index e7bbf62..f0c041f 100644 --- a/internal/contest/application/contest_application_service.go +++ b/internal/contest/application/contest_application_service.go @@ -91,7 +91,7 @@ func (s *ContestApplicationService) RequestParticipate(ctx context.Context, cont if contest.GamePointTableId != nil && s.scoreTableRepo != nil { scoreTable, scoreErr := s.scoreTableRepo.GetByID(*contest.GamePointTableId) if scoreErr != nil { - log.Printf("[RequestParticipate] failed to get score table (id=%d): %v", *contest.GamePointTableId, scoreErr) + return nil, scoreErr } else if user.HasValorantLinked() { currentTierPoint := scoreTable.GetTierPoint(user.GetCurrentTierFullName()) peakTierPoint := scoreTable.GetTierPoint(user.GetPeakTierFullName()) @@ -159,6 +159,12 @@ func (s *ContestApplicationService) AcceptApplication(ctx context.Context, conte if err != nil { return err } + if application == nil { + return exception.ErrApplicationNotFound + } + if application.Status != port.ApplicationStatusPending { + return exception.ErrApplicationNotPending + } err = s.applicationRepo.AcceptRequest(ctx, contestId, userId, leaderUserId) if err != nil { diff --git a/test/contest/application/contest_application_service_test.go b/test/contest/application/contest_application_service_test.go index bd56ddb..43e3f50 100644 --- a/test/contest/application/contest_application_service_test.go +++ b/test/contest/application/contest_application_service_test.go @@ -372,8 +372,9 @@ func TestRequestParticipate_ValorantNotLinked_PointIsZero(t *testing.T) { mockRedis.AssertExpectations(t) } -func TestRequestParticipate_ScoreTableNotFound_PointIsZero(t *testing.T) { - // Given: Contest has GamePointTableId but score table doesn't exist in DB +func TestRequestParticipate_ScoreTableNotFound_ReturnsError(t *testing.T) { + // Given: Contest has GamePointTableId but score table doesn't exist in DB. + // This is a server-side configuration error and must be propagated to the caller. mockRedis := new(MockContestApplicationRedisPort) mockContestDB := new(MockContestDatabasePort) mockMemberDB := new(MockContestMemberDatabasePort) @@ -399,18 +400,13 @@ func TestRequestParticipate_ScoreTableNotFound_PointIsZero(t *testing.T) { mockUserQuery.On("FindById", userId).Return(user, nil) mockScoreTable.On("GetByID", scoreTableId).Return(nil, exception.ErrScoreTableNotFound) - // Point should be 0 due to score table fetch error (graceful degradation) - mockRedis.On("RequestParticipate", ctx, contestId, mock.MatchedBy(func(s *port.SenderSnapshot) bool { - return s.Point == 0 - }), mock.AnythingOfType("time.Duration")).Return(nil) - mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) - // When _, err := service.RequestParticipate(ctx, contestId, userId) - // Then - assert.NoError(t, err) - mockRedis.AssertExpectations(t) + // Then: error is propagated — no silent failure + assert.Error(t, err) + assert.ErrorIs(t, err, exception.ErrScoreTableNotFound) + mockRedis.AssertNotCalled(t, "RequestParticipate", mock.Anything, mock.Anything, mock.Anything, mock.Anything) } func TestRequestParticipate_NoScoreTablePortSet_PointIsZero(t *testing.T) { @@ -582,6 +578,106 @@ func TestAcceptApplication_ZeroPointWhenNoSenderSnapshot(t *testing.T) { mockMemberDB.AssertExpectations(t) } +func TestAcceptApplication_NilApplication_ReturnsError(t *testing.T) { + // Given: GetApplication returns nil (no pending application exists for the user) + mockRedis := new(MockContestApplicationRedisPort) + mockContestDB := new(MockContestDatabasePort) + mockMemberDB := new(MockContestMemberDatabasePort) + mockEventPub := new(MockEventPublisherPort) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockUserQuery := new(MockUserQueryPort) + + service := createApplicationService(mockRedis, mockContestDB, mockMemberDB, mockEventPub, mockOAuth2DB, mockUserQuery, nil) + + ctx := context.Background() + contestId := int64(1) + userId := int64(99) // user who never applied + leaderId := int64(1) + + contest := &domain.Contest{ + ContestID: contestId, + Title: "Test Contest", + ContestStatus: domain.ContestStatusPending, + StartedAt: time.Now().Add(24 * time.Hour), + EndedAt: time.Now().Add(48 * time.Hour), + } + + leader := &domain.ContestMember{ + UserID: leaderId, + ContestID: contestId, + MemberType: domain.MemberTypeStaff, + LeaderType: domain.LeaderTypeLeader, + } + + mockContestDB.On("GetContestById", contestId).Return(contest, nil) + mockMemberDB.On("GetByContestAndUser", contestId, leaderId).Return(leader, nil) + mockRedis.On("GetApplication", ctx, contestId, userId).Return(nil, nil) + + // When + err := service.AcceptApplication(ctx, contestId, userId, leaderId) + + // Then: should return ErrApplicationNotFound, not silently create a member + assert.Error(t, err) + assert.ErrorIs(t, err, exception.ErrApplicationNotFound) + mockMemberDB.AssertNotCalled(t, "Save", mock.Anything) + mockRedis.AssertNotCalled(t, "AcceptRequest", mock.Anything, mock.Anything, mock.Anything, mock.Anything) +} + +func TestAcceptApplication_NonPendingApplication_ReturnsError(t *testing.T) { + // Given: Application exists but is already ACCEPTED (not PENDING) + mockRedis := new(MockContestApplicationRedisPort) + mockContestDB := new(MockContestDatabasePort) + mockMemberDB := new(MockContestMemberDatabasePort) + mockEventPub := new(MockEventPublisherPort) + mockOAuth2DB := new(MockOAuth2DatabasePort) + mockUserQuery := new(MockUserQueryPort) + + service := createApplicationService(mockRedis, mockContestDB, mockMemberDB, mockEventPub, mockOAuth2DB, mockUserQuery, nil) + + ctx := context.Background() + contestId := int64(1) + userId := int64(10) + leaderId := int64(1) + + contest := &domain.Contest{ + ContestID: contestId, + Title: "Test Contest", + ContestStatus: domain.ContestStatusPending, + StartedAt: time.Now().Add(24 * time.Hour), + EndedAt: time.Now().Add(48 * time.Hour), + } + + leader := &domain.ContestMember{ + UserID: leaderId, + ContestID: contestId, + MemberType: domain.MemberTypeStaff, + LeaderType: domain.LeaderTypeLeader, + } + + alreadyAccepted := &port.ContestApplication{ + UserID: userId, + ContestID: contestId, + Status: port.ApplicationStatusAccepted, + Sender: &port.SenderSnapshot{ + UserID: userId, + Point: 50, + }, + } + + mockContestDB.On("GetContestById", contestId).Return(contest, nil) + mockMemberDB.On("GetByContestAndUser", contestId, leaderId).Return(leader, nil) + mockRedis.On("GetApplication", ctx, contestId, userId).Return(alreadyAccepted, nil) + + // When + err := service.AcceptApplication(ctx, contestId, userId, leaderId) + + // Then: should reject non-pending applications + assert.Error(t, err) + assert.ErrorIs(t, err, exception.ErrApplicationNotPending) + mockMemberDB.AssertNotCalled(t, "Save", mock.Anything) + mockRedis.AssertNotCalled(t, "AcceptRequest", mock.Anything, mock.Anything, mock.Anything, mock.Anything) +} + // ==================== Point Calculation Edge Cases ==================== func TestRequestParticipate_RadiantUser_MaxPoint(t *testing.T) { diff --git a/test/contest/integration/contest_application_integration_test.go b/test/contest/integration/contest_application_integration_test.go index aa4fcc7..70868d9 100644 --- a/test/contest/integration/contest_application_integration_test.go +++ b/test/contest/integration/contest_application_integration_test.go @@ -480,13 +480,11 @@ func (s *ContestApplicationPointIntegrationSuite) TestMultipleApplicants_Differe peakTier string expectedPoint int }{ - {10, "Iron 1", "Iron 1", 1}, // (1+1)/2 = 1 - {11, "Gold 2", "Platinum 1", 33}, // (30+40)/2 = 35 → wrong, let me recalculate: Gold2=30, Platinum1=40 → (30+40)/2 = 35 - {12, "Diamond 1", "Radiant", 78}, // (55+100)/2 = 77.5 → 78 - {13, "Radiant", "Radiant", 100}, // (100+100)/2 = 100 + {10, "Iron 1", "Iron 1", 1}, // (Iron1=1 + Iron1=1) / 2 = 1 + {11, "Gold 2", "Platinum 1", 35}, // (Gold2=30 + Platinum1=40) / 2 = 35 + {12, "Diamond 1", "Radiant", 78}, // (Diamond1=55 + Radiant=100) / 2 = 77.5 → 78 + {13, "Radiant", "Radiant", 100}, // (Radiant=100 + Radiant=100) / 2 = 100 } - // Fix: Gold2=30, Platinum1=40 → (30+40)/2 = 35 - applicants[1].expectedPoint = 35 for _, a := range applicants { storedApp := &port.ContestApplication{ From 263a9c72c20004eef644ecb6e833e070b6826478 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:40:35 +0900 Subject: [PATCH 10/24] Fix: Resolve flaky integration test caused by same-second JWT generation JWT expiry uses second-precision, so tokens generated within the same second produce identical strings. Added time.Sleep(time.Second) before the Refresh call in TestFullLoginFlow to ensure unique timestamps. Co-Authored-By: Claude Sonnet 4.6 --- test/integration/auth_integration_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integration/auth_integration_test.go b/test/integration/auth_integration_test.go index be1ff80..10d63dd 100644 --- a/test/integration/auth_integration_test.go +++ b/test/integration/auth_integration_test.go @@ -133,6 +133,8 @@ func (s *AuthIntegrationTestSuite) TestFullLoginFlow() { s.NotEmpty(loginResp.RefreshToken) // 2. Refresh + // Sleep 1s to ensure JWT timestamps differ (JWT expiry is second-precision) + time.Sleep(time.Second) refreshReq := dto.RefreshRequest{ RefreshToken: loginResp.RefreshToken, } From 58b0e372b9fc8bc532d47811ea940e726b536ae5 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:53:55 +0900 Subject: [PATCH 11/24] Fix: Member point saved as 0 when contest application is accepted (#71) - Return error from AcceptApplication when memberRepo.Save fails instead of silently logging it, preventing inconsistent state where application is ACCEPTED in Redis but member record is absent in DB - Remove redundant SaveBatch call in startNonTournamentContest that was re-creating members with hardcoded point=0; members are already persisted with correct points during AcceptApplication Co-Authored-By: Claude Sonnet 4.6 --- .../application/contest_application_service.go | 3 ++- .../contest/application/contest_service.go | 18 +----------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/internal/contest/application/contest_application_service.go b/internal/contest/application/contest_application_service.go index f6a9863..ff8e945 100644 --- a/internal/contest/application/contest_application_service.go +++ b/internal/contest/application/contest_application_service.go @@ -12,6 +12,7 @@ import ( userQueryPort "github.com/FOR-GAMERS/GAMERS-BE/internal/user/application/port/port" "context" "errors" + "fmt" "log" "math" "time" @@ -184,7 +185,7 @@ func (s *ContestApplicationService) AcceptApplication(ctx context.Context, conte member := domain.NewContestMember(userId, contestId, domain.MemberTypeNormal, domain.LeaderTypeMember, memberPoint) if err := s.memberRepo.Save(member); err != nil { - log.Printf("[AcceptApplication] failed to save member (contestId=%d, userId=%d): %v", contestId, userId, err) + return fmt.Errorf("[AcceptApplication] failed to save member (contestId=%d, userId=%d): %w", contestId, userId, err) } go s.publishApplicationAcceptedEvent(context.Background(), contest, userId, leaderUserId) diff --git a/internal/contest/application/contest_service.go b/internal/contest/application/contest_service.go index ff7fb69..45f0f90 100644 --- a/internal/contest/application/contest_service.go +++ b/internal/contest/application/contest_service.go @@ -375,24 +375,8 @@ func (c *ContestService) startTournamentContest(ctx context.Context, contest *do } // startNonTournamentContest handles starting a non-tournament contest (individual applications) +// Members are already saved to DB with correct points during AcceptApplication, so no batch-save needed here. func (c *ContestService) startNonTournamentContest(ctx context.Context, contest *domain.Contest) (*domain.Contest, error) { - acceptedUserIDs, err := c.applicationRepository.GetAcceptedApplications(ctx, contest.ContestID) - if err != nil { - return nil, err - } - - if len(acceptedUserIDs) > 0 { - members := make([]*domain.ContestMember, 0, len(acceptedUserIDs)) - for _, userID := range acceptedUserIDs { - member := domain.NewContestMember(userID, contest.ContestID, domain.MemberTypeNormal, domain.LeaderTypeMember, 0) - members = append(members, member) - } - - if err := c.memberRepository.SaveBatch(members); err != nil { - return nil, err - } - } - if err := contest.TransitionTo(domain.ContestStatusActive); err != nil { return nil, err } From 551eca97464943d52e5c353b760afa0a6cd5821a Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:25:54 +0900 Subject: [PATCH 12/24] feat: Add Valorant roles/description to contest application and member list (#73) - Define ValorantRole type (DUELIST, INITIATOR, CONTROLLER, SENTINEL) in domain - Add ValorantRoles (custom JSON type) and Description to ContestMember entity - Extend SenderSnapshot and ContestMemberWithUser with new fields - Accept optional request body in POST /api/contests/:id/applications - Validate description max 64 chars and role deduplication in service layer - Include valorant_roles and description in GET /api/contests/:id/members response - Add DB migration 000025 to add valorant_roles JSON and description VARCHAR(64) columns Co-Authored-By: Claude Sonnet 4.6 --- ...lorant_fields_to_contests_members.down.sql | 3 + ...valorant_fields_to_contests_members.up.sql | 3 + .../contest_application_service.go | 31 ++++++- .../application/dto/application_dto.go | 27 ++++-- .../contest/application/dto/contest_dto.go | 28 +++--- .../port/contest_application_redis_port.go | 12 ++- .../port/contest_member_database_port.go | 30 ++++--- internal/contest/domain/contest_member.go | 86 +++++++++++++++++-- .../contest_member_database_adapter.go | 2 +- .../contest_application_controller.go | 10 ++- .../global/exception/contest_error_status.go | 3 + 11 files changed, 188 insertions(+), 47 deletions(-) create mode 100644 db/migrations/000025_add_valorant_fields_to_contests_members.down.sql create mode 100644 db/migrations/000025_add_valorant_fields_to_contests_members.up.sql diff --git a/db/migrations/000025_add_valorant_fields_to_contests_members.down.sql b/db/migrations/000025_add_valorant_fields_to_contests_members.down.sql new file mode 100644 index 0000000..9d02795 --- /dev/null +++ b/db/migrations/000025_add_valorant_fields_to_contests_members.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE contests_members + DROP COLUMN valorant_roles, + DROP COLUMN description; diff --git a/db/migrations/000025_add_valorant_fields_to_contests_members.up.sql b/db/migrations/000025_add_valorant_fields_to_contests_members.up.sql new file mode 100644 index 0000000..00b3e5b --- /dev/null +++ b/db/migrations/000025_add_valorant_fields_to_contests_members.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE contests_members + ADD COLUMN valorant_roles JSON DEFAULT NULL AFTER point, + ADD COLUMN description VARCHAR(64) DEFAULT '' AFTER valorant_roles; diff --git a/internal/contest/application/contest_application_service.go b/internal/contest/application/contest_application_service.go index 629ab9f..5762534 100644 --- a/internal/contest/application/contest_application_service.go +++ b/internal/contest/application/contest_application_service.go @@ -49,7 +49,20 @@ func (s *ContestApplicationService) SetNotificationHandler(handler notificationP } // RequestParticipate - Contest 참가 신청 -func (s *ContestApplicationService) RequestParticipate(ctx context.Context, contestId, userId int64) (*dto.DiscordLinkRequiredResponse, error) { +func (s *ContestApplicationService) RequestParticipate(ctx context.Context, contestId, userId int64, req *dto.RequestParticipateRequest) (*dto.DiscordLinkRequiredResponse, error) { + // Validate request if provided + if req != nil { + if len(req.Description) > 64 { + return nil, exception.ErrDescriptionTooLong + } + if len(req.ValorantRoles) > 0 { + roles := domain.ValorantRoles(req.ValorantRoles) + if !roles.AreValid() { + return nil, exception.ErrInvalidValorantRole + } + } + } + // Check if user has linked Discord account _, err := s.oauth2Repository.FindDiscordAccountByUserId(userId) if err != nil { @@ -85,6 +98,12 @@ func (s *ContestApplicationService) RequestParticipate(ctx context.Context, cont Avatar: user.Avatar, } + // Add valorant roles and description if provided + if req != nil { + senderSnapshot.ValorantRoles = req.ValorantRoles + senderSnapshot.Description = req.Description + } + ttl := time.Until(contest.StartedAt) if ttl < 0 { ttl = 24 * time.Hour @@ -128,12 +147,22 @@ func (s *ContestApplicationService) AcceptApplication(ctx context.Context, conte return err } + // Get application data before accepting (to preserve valorant roles and description) + application, appErr := s.applicationRepo.GetApplication(ctx, contestId, userId) + err = s.applicationRepo.AcceptRequest(ctx, contestId, userId, leaderUserId) if err != nil { return err } member := domain.NewContestMember(userId, contestId, domain.MemberTypeNormal, domain.LeaderTypeMember) + + // Copy valorant roles and description from application sender snapshot + if appErr == nil && application != nil && application.Sender != nil { + member.ValorantRoles = application.Sender.ValorantRoles + member.Description = application.Sender.Description + } + if err := s.memberRepo.Save(member); err != nil { // DB 저장 실패 시 Redis 상태 롤백은 하지 않음 (최종적 일관성) // 추후 MigrateAcceptedApplicationsToDatabase에서 재시도됨 diff --git a/internal/contest/application/dto/application_dto.go b/internal/contest/application/dto/application_dto.go index 8ae4197..dad5d6f 100644 --- a/internal/contest/application/dto/application_dto.go +++ b/internal/contest/application/dto/application_dto.go @@ -2,14 +2,23 @@ package dto import ( "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application/port" + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/domain" "time" ) +// RequestParticipateRequest represents the request body for contest participation +type RequestParticipateRequest struct { + ValorantRoles []domain.ValorantRole `json:"valorant_roles,omitempty"` + Description string `json:"description,omitempty"` +} + type SenderResponse struct { - UserID int64 `json:"user_id"` - Username string `json:"username"` - Tag string `json:"tag"` - Avatar string `json:"avatar,omitempty"` + UserID int64 `json:"user_id"` + Username string `json:"username"` + Tag string `json:"tag"` + Avatar string `json:"avatar,omitempty"` + ValorantRoles []domain.ValorantRole `json:"valorant_roles,omitempty"` + Description string `json:"description,omitempty"` } type ApplicationResponse struct { @@ -26,10 +35,12 @@ func ToApplicationResponse(app *port.ContestApplication) *ApplicationResponse { var sender *SenderResponse if app.Sender != nil { sender = &SenderResponse{ - UserID: app.Sender.UserID, - Username: app.Sender.Username, - Tag: app.Sender.Tag, - Avatar: app.Sender.Avatar, + UserID: app.Sender.UserID, + Username: app.Sender.Username, + Tag: app.Sender.Tag, + Avatar: app.Sender.Avatar, + ValorantRoles: app.Sender.ValorantRoles, + Description: app.Sender.Description, } } diff --git a/internal/contest/application/dto/contest_dto.go b/internal/contest/application/dto/contest_dto.go index 2d017a7..a6e777b 100644 --- a/internal/contest/application/dto/contest_dto.go +++ b/internal/contest/application/dto/contest_dto.go @@ -159,18 +159,20 @@ func (req *UpdateContestRequest) Validate() error { // ContestMemberResponse represents a contest member with user information type ContestMemberResponse struct { - UserID int64 `json:"user_id"` - ContestID int64 `json:"contest_id"` - MemberType domain.MemberType `json:"member_type"` - LeaderType domain.LeaderType `json:"leader_type"` - Point int `json:"point"` - Username string `json:"username"` - Tag string `json:"tag"` - Avatar string `json:"avatar"` - CurrentTier *int `json:"current_tier,omitempty"` - CurrentTierPatched *string `json:"current_tier_patched,omitempty"` - PeakTier *int `json:"peak_tier,omitempty"` - PeakTierPatched *string `json:"peak_tier_patched,omitempty"` + UserID int64 `json:"user_id"` + ContestID int64 `json:"contest_id"` + MemberType domain.MemberType `json:"member_type"` + LeaderType domain.LeaderType `json:"leader_type"` + Point int `json:"point"` + ValorantRoles []domain.ValorantRole `json:"valorant_roles,omitempty"` + Description string `json:"description,omitempty"` + Username string `json:"username"` + Tag string `json:"tag"` + Avatar string `json:"avatar"` + CurrentTier *int `json:"current_tier,omitempty"` + CurrentTierPatched *string `json:"current_tier_patched,omitempty"` + PeakTier *int `json:"peak_tier,omitempty"` + PeakTierPatched *string `json:"peak_tier_patched,omitempty"` } // ToContestMemberResponse converts port.ContestMemberWithUser to ContestMemberResponse @@ -189,6 +191,8 @@ func ToContestMemberResponse(member *port.ContestMemberWithUser) *ContestMemberR MemberType: member.MemberType, LeaderType: member.LeaderType, Point: member.Point, + ValorantRoles: member.ValorantRoles, + Description: member.Description, Username: member.Username, Tag: member.Tag, Avatar: avatar, diff --git a/internal/contest/application/port/contest_application_redis_port.go b/internal/contest/application/port/contest_application_redis_port.go index db2cabb..340684d 100644 --- a/internal/contest/application/port/contest_application_redis_port.go +++ b/internal/contest/application/port/contest_application_redis_port.go @@ -3,6 +3,8 @@ package port import ( "context" "time" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/domain" ) type ApplicationStatus string @@ -15,10 +17,12 @@ const ( // SenderSnapshot stores user information at the time of application type SenderSnapshot struct { - UserID int64 `json:"user_id"` - Username string `json:"username"` - Tag string `json:"tag"` - Avatar string `json:"avatar,omitempty"` + UserID int64 `json:"user_id"` + Username string `json:"username"` + Tag string `json:"tag"` + Avatar string `json:"avatar,omitempty"` + ValorantRoles domain.ValorantRoles `json:"valorant_roles,omitempty"` + Description string `json:"description,omitempty"` } type ContestApplication struct { diff --git a/internal/contest/application/port/contest_member_database_port.go b/internal/contest/application/port/contest_member_database_port.go index 8b7ef4a..03af53f 100644 --- a/internal/contest/application/port/contest_member_database_port.go +++ b/internal/contest/application/port/contest_member_database_port.go @@ -7,20 +7,22 @@ import ( // ContestMemberWithUser represents a contest member with user information type ContestMemberWithUser struct { - UserID int64 `json:"user_id"` - ContestID int64 `json:"contest_id"` - MemberType domain.MemberType `json:"member_type"` - LeaderType domain.LeaderType `json:"leader_type"` - Point int `json:"point"` - Username string `json:"username"` - Tag string `json:"tag"` - Avatar string `json:"avatar"` - DiscordId *string `json:"discord_id"` - DiscordAvatar *string `json:"discord_avatar"` - CurrentTier *int `json:"current_tier"` - CurrentTierPatched *string `json:"current_tier_patched"` - PeakTier *int `json:"peak_tier"` - PeakTierPatched *string `json:"peak_tier_patched"` + UserID int64 `json:"user_id"` + ContestID int64 `json:"contest_id"` + MemberType domain.MemberType `json:"member_type"` + LeaderType domain.LeaderType `json:"leader_type"` + Point int `json:"point"` + ValorantRoles domain.ValorantRoles `json:"valorant_roles"` + Description string `json:"description"` + Username string `json:"username"` + Tag string `json:"tag"` + Avatar string `json:"avatar"` + DiscordId *string `json:"discord_id"` + DiscordAvatar *string `json:"discord_avatar"` + CurrentTier *int `json:"current_tier"` + CurrentTierPatched *string `json:"current_tier_patched"` + PeakTier *int `json:"peak_tier"` + PeakTierPatched *string `json:"peak_tier_patched"` } // ContestWithMembership represents a contest with the user's membership info diff --git a/internal/contest/domain/contest_member.go b/internal/contest/domain/contest_member.go index d2d55e2..1c7150a 100644 --- a/internal/contest/domain/contest_member.go +++ b/internal/contest/domain/contest_member.go @@ -1,9 +1,83 @@ package domain import ( + "database/sql/driver" + "encoding/json" + "fmt" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" ) +// ValorantRole represents a Valorant agent role type +type ValorantRole string + +const ( + ValorantRoleDuelist ValorantRole = "DUELIST" + ValorantRoleInitiator ValorantRole = "INITIATOR" + ValorantRoleController ValorantRole = "CONTROLLER" + ValorantRoleSentinel ValorantRole = "SENTINEL" +) + +// IsValid checks if the ValorantRole is valid +func (vr ValorantRole) IsValid() bool { + switch vr { + case ValorantRoleDuelist, ValorantRoleInitiator, ValorantRoleController, ValorantRoleSentinel: + return true + default: + return false + } +} + +// ValorantRoles is a custom type for storing []ValorantRole as JSON in the database +type ValorantRoles []ValorantRole + +// Scan implements the sql.Scanner interface for reading from DB +func (vr *ValorantRoles) Scan(value interface{}) error { + if value == nil { + *vr = nil + return nil + } + + var bytes []byte + switch v := value.(type) { + case []byte: + bytes = v + case string: + bytes = []byte(v) + default: + return fmt.Errorf("unsupported type for ValorantRoles: %T", value) + } + + return json.Unmarshal(bytes, vr) +} + +// Value implements the driver.Valuer interface for writing to DB +func (vr ValorantRoles) Value() (driver.Value, error) { + if vr == nil { + return nil, nil + } + bytes, err := json.Marshal(vr) + if err != nil { + return nil, err + } + return string(bytes), nil +} + +// AreValid checks if all roles in the slice are valid and there are no duplicates +func (vr ValorantRoles) AreValid() bool { + seen := make(map[ValorantRole]bool) + for _, role := range vr { + if !role.IsValid() { + return false + } + if seen[role] { + return false + } + seen[role] = true + } + return true +} + type MemberType string const ( @@ -29,11 +103,13 @@ const ( ) type ContestMember struct { - UserID int64 `gorm:"column:user_id;primaryKey" json:"user_id"` - ContestID int64 `gorm:"column:contest_id;primaryKey" json:"contest_id"` - MemberType MemberType `gorm:"column:member_type;type:varchar(16);not null" json:"member_type"` - LeaderType LeaderType `gorm:"column:leader_type;type:varchar(8);not null" json:"leader_type"` - Point int `gorm:"column:point;type:int;default:0" json:"point"` + UserID int64 `gorm:"column:user_id;primaryKey" json:"user_id"` + ContestID int64 `gorm:"column:contest_id;primaryKey" json:"contest_id"` + MemberType MemberType `gorm:"column:member_type;type:varchar(16);not null" json:"member_type"` + LeaderType LeaderType `gorm:"column:leader_type;type:varchar(8);not null" json:"leader_type"` + Point int `gorm:"column:point;type:int;default:0" json:"point"` + ValorantRoles ValorantRoles `gorm:"column:valorant_roles;type:json" json:"valorant_roles,omitempty"` + Description string `gorm:"column:description;type:varchar(64)" json:"description,omitempty"` } func NewContestMemberAsLeader(userID, contestID int64) *ContestMember { diff --git a/internal/contest/infra/persistence/adapter/contest_member_database_adapter.go b/internal/contest/infra/persistence/adapter/contest_member_database_adapter.go index ed16c15..4df4092 100644 --- a/internal/contest/infra/persistence/adapter/contest_member_database_adapter.go +++ b/internal/contest/infra/persistence/adapter/contest_member_database_adapter.go @@ -149,7 +149,7 @@ func (c ContestMemberDatabaseAdapter) GetMembersWithUserByContest( // Query with JOIN (including discord_accounts for avatar URL) var results []*port.ContestMemberWithUser query := c.db.Table("contests_members cm"). - Select("cm.user_id, cm.contest_id, cm.member_type, cm.leader_type, cm.point, u.username, u.tag, u.avatar, da.discord_id, da.discord_avatar, u.current_tier, u.current_tier_patched, u.peak_tier, u.peak_tier_patched"). + Select("cm.user_id, cm.contest_id, cm.member_type, cm.leader_type, cm.point, cm.valorant_roles, cm.description, u.username, u.tag, u.avatar, da.discord_id, da.discord_avatar, u.current_tier, u.current_tier_patched, u.peak_tier, u.peak_tier_patched"). Joins("JOIN users u ON cm.user_id = u.id"). Joins("LEFT JOIN discord_accounts da ON u.id = da.user_id"). Where("cm.contest_id = ?", contestId). diff --git a/internal/contest/presentation/contest_application_controller.go b/internal/contest/presentation/contest_application_controller.go index cdcb87d..0e17517 100644 --- a/internal/contest/presentation/contest_application_controller.go +++ b/internal/contest/presentation/contest_application_controller.go @@ -55,12 +55,13 @@ func (c *ContestApplicationController) RegisterRoute() { // RequestParticipate godoc // @Summary Request to participate in a contest -// @Description Apply to participate in a contest +// @Description Apply to participate in a contest with optional valorant roles and description // @Tags contest-applications // @Accept json // @Produce json // @Security BearerAuth // @Param contestId path int true "Contest ID" +// @Param request body dto.RequestParticipateRequest false "Participation request with optional valorant roles and description" // @Success 201 {object} response.Response // @Failure 400 {object} response.Response // @Failure 401 {object} response.Response @@ -80,7 +81,12 @@ func (c *ContestApplicationController) RequestParticipate(ctx *gin.Context) { return } - discordLinkRequired, err := c.service.RequestParticipate(ctx.Request.Context(), contestId, userId) + // Parse optional request body + var req dto.RequestParticipateRequest + // ShouldBindJSON returns error if body exists but is invalid; nil body is OK + _ = ctx.ShouldBindJSON(&req) + + discordLinkRequired, err := c.service.RequestParticipate(ctx.Request.Context(), contestId, userId, &req) // Handle Discord link required error if errors.Is(err, exception.ErrDiscordLinkRequired) { diff --git a/internal/global/exception/contest_error_status.go b/internal/global/exception/contest_error_status.go index aa6428f..f06a852 100644 --- a/internal/global/exception/contest_error_status.go +++ b/internal/global/exception/contest_error_status.go @@ -38,4 +38,7 @@ var ( ErrContestNotActive = NewBadRequestError("contest is not in active status", "CT032") ErrCannotChangeLeaderRole = NewBusinessError(http.StatusForbidden, "cannot change leader's role", "CT033") ErrAlreadySameMemberType = NewBadRequestError("member already has the same role", "CT034") + ErrInvalidValorantRole = NewBadRequestError("invalid valorant role", "CT035") + ErrDescriptionTooLong = NewBadRequestError("description must be at most 64 characters", "CT036") + ErrDuplicateValorantRole = NewBadRequestError("duplicate valorant role", "CT037") ) From dccda6b495bd2c0bbc4d7fa305bb6b263c38f4b6 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:11:00 +0900 Subject: [PATCH 13/24] =?UTF-8?q?feat:=20Grafana=20=EB=AA=A8=EB=8B=88?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EC=8A=A4=ED=83=9D=20=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?(#75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prometheus + Grafana 기반 모니터링 인프라를 추가한다. - PrometheusMetrics() Gin 미들웨어 추가 (http_requests_total, http_request_duration_seconds, http_requests_in_flight) - GET /metrics 엔드포인트 등록 (Prometheus scrape 용) - docker-compose에 Prometheus(9090), Grafana(3001) 서비스 추가 - Prometheus scrape 설정 (15s interval, app:8080/metrics) - Grafana datasource / dashboard 프로비저닝 자동화 - HTTP RPS, 에러율, p50/p95/p99 응답 시간, In-Flight, Goroutine, Heap, GC 패널 포함 Co-Authored-By: Claude Sonnet 4.6 --- cmd/server.go | 3 + docker/docker-compose.yaml | 38 ++++++ docker/grafana/dashboards/gamers-api.json | 111 ++++++++++++++++++ .../provisioning/dashboards/dashboards.yml | 10 ++ .../provisioning/datasources/prometheus.yml | 9 ++ docker/prometheus/prometheus.yml | 9 ++ go.mod | 10 +- go.sum | 16 +++ internal/global/common/router/router.go | 5 + .../global/middleware/metrics_middleware.go | 63 ++++++++++ 10 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 docker/grafana/dashboards/gamers-api.json create mode 100644 docker/grafana/provisioning/dashboards/dashboards.yml create mode 100644 docker/grafana/provisioning/datasources/prometheus.yml create mode 100644 docker/prometheus/prometheus.yml create mode 100644 internal/global/middleware/metrics_middleware.go diff --git a/cmd/server.go b/cmd/server.go index 67f69f0..7425be2 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -184,6 +184,7 @@ func startServer(engine interface{}) { log.Println("===========================================") log.Println("Server: http://localhost:8080") log.Println("Health Check: http://localhost:8080/health") + log.Println("Metrics: http://localhost:8080/metrics") log.Println("Swagger UI: http://localhost:8080/swagger/index.html") log.Println("===========================================") @@ -215,9 +216,11 @@ func setupRouter( notificationDeps *notification.Dependencies, ) *router.Router { + appRouter.Engine().Use(middleware.PrometheusMetrics()) appRouter.Engine().Use(middleware.GlobalErrorHandler()) appRouter.RegisterHealthCheck() + appRouter.RegisterMetrics() appRouter.RegisterSwagger(ginSwagger.WrapHandler(swaggerFiles.Handler)) authDeps.Controller.RegisterRoutes() diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index ac33c6a..baf3a16 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -13,6 +13,44 @@ services: networks: - gamers-network + prometheus: + image: prom/prometheus:v3.4.0 + container_name: gamers-prometheus + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus-data:/prometheus + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.retention.time=15d" + ports: + - "9090:9090" + networks: + - gamers-network + restart: unless-stopped + + grafana: + image: grafana/grafana:12.0.1 + container_name: gamers-grafana + volumes: + - ./grafana/provisioning:/etc/grafana/provisioning:ro + - ./grafana/dashboards:/etc/grafana/dashboards:ro + - grafana-data:/var/lib/grafana + environment: + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: admin + GF_USERS_ALLOW_SIGN_UP: "false" + ports: + - "3001:3000" + networks: + - gamers-network + depends_on: + - prometheus + restart: unless-stopped + +volumes: + prometheus-data: + grafana-data: + networks: gamers-network: external: true diff --git a/docker/grafana/dashboards/gamers-api.json b/docker/grafana/dashboards/gamers-api.json new file mode 100644 index 0000000..20a1dff --- /dev/null +++ b/docker/grafana/dashboards/gamers-api.json @@ -0,0 +1,111 @@ +{ + "title": "GAMERS API", + "uid": "gamers-api", + "schemaVersion": 39, + "version": 1, + "refresh": "10s", + "time": { "from": "now-1h", "to": "now" }, + "panels": [ + { + "id": 1, + "title": "RPS (Requests Per Second)", + "type": "timeseries", + "gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 }, + "targets": [ + { + "datasource": { "type": "prometheus" }, + "expr": "sum(rate(http_requests_total[1m])) by (method, path)", + "legendFormat": "{{method}} {{path}}" + } + ] + }, + { + "id": 2, + "title": "Error Rate (5xx / total)", + "type": "timeseries", + "gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 }, + "targets": [ + { + "datasource": { "type": "prometheus" }, + "expr": "sum(rate(http_requests_total{status=~\"5..\"}[1m])) / sum(rate(http_requests_total[1m]))", + "legendFormat": "error rate" + } + ] + }, + { + "id": 3, + "title": "Request Duration p50 / p95 / p99", + "type": "timeseries", + "gridPos": { "x": 0, "y": 8, "w": 12, "h": 8 }, + "targets": [ + { + "datasource": { "type": "prometheus" }, + "expr": "histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket[1m])) by (le))", + "legendFormat": "p50" + }, + { + "datasource": { "type": "prometheus" }, + "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[1m])) by (le))", + "legendFormat": "p95" + }, + { + "datasource": { "type": "prometheus" }, + "expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[1m])) by (le))", + "legendFormat": "p99" + } + ] + }, + { + "id": 4, + "title": "In-Flight Requests", + "type": "timeseries", + "gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 }, + "targets": [ + { + "datasource": { "type": "prometheus" }, + "expr": "http_requests_in_flight", + "legendFormat": "in-flight" + } + ] + }, + { + "id": 5, + "title": "Goroutines", + "type": "timeseries", + "gridPos": { "x": 0, "y": 16, "w": 8, "h": 7 }, + "targets": [ + { + "datasource": { "type": "prometheus" }, + "expr": "go_goroutines", + "legendFormat": "goroutines" + } + ] + }, + { + "id": 6, + "title": "Heap Alloc (MB)", + "type": "timeseries", + "gridPos": { "x": 8, "y": 16, "w": 8, "h": 7 }, + "targets": [ + { + "datasource": { "type": "prometheus" }, + "expr": "go_memstats_heap_alloc_bytes / 1024 / 1024", + "legendFormat": "heap alloc MB" + } + ] + }, + { + "id": 7, + "title": "GC Pause Duration (p99, ms)", + "type": "timeseries", + "gridPos": { "x": 16, "y": 16, "w": 8, "h": 7 }, + "targets": [ + { + "datasource": { "type": "prometheus" }, + "expr": "histogram_quantile(0.99, sum(rate(go_gc_duration_seconds_bucket[1m])) by (le)) * 1000", + "legendFormat": "gc p99 ms" + } + ] + } + ] +} diff --git a/docker/grafana/provisioning/dashboards/dashboards.yml b/docker/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 0000000..52bfa4a --- /dev/null +++ b/docker/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,10 @@ +apiVersion: 1 + +providers: + - name: GAMERS + folder: GAMERS + type: file + disableDeletion: false + editable: true + options: + path: /etc/grafana/dashboards diff --git a/docker/grafana/provisioning/datasources/prometheus.yml b/docker/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 0000000..bb009bb --- /dev/null +++ b/docker/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false diff --git a/docker/prometheus/prometheus.yml b/docker/prometheus/prometheus.yml new file mode 100644 index 0000000..f73642c --- /dev/null +++ b/docker/prometheus/prometheus.yml @@ -0,0 +1,9 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "gamers-api" + static_configs: + - targets: ["app:8080"] + metrics_path: /metrics diff --git a/go.mod b/go.mod index 6b8f4de..2343c8d 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/golang-migrate/migrate/v4 v4.19.1 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 + github.com/prometheus/client_golang v1.23.2 github.com/rabbitmq/amqp091-go v1.9.0 github.com/redis/go-redis/v9 v9.17.2 github.com/stretchr/testify v1.11.1 @@ -25,6 +26,7 @@ require ( github.com/yldshv/go-valorant-api v1.0.7 golang.org/x/crypto v0.46.0 golang.org/x/oauth2 v0.34.0 + golang.org/x/sync v0.19.0 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.1 ) @@ -50,6 +52,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect github.com/aws/smithy-go v1.24.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect @@ -109,12 +112,16 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.58.0 // indirect github.com/shirou/gopsutil/v4 v4.25.6 // indirect @@ -132,12 +139,11 @@ require ( go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.uber.org/goleak v1.3.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.23.0 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect - golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/tools v0.40.0 // indirect diff --git a/go.sum b/go.sum index eb918c4..ca14949 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/ github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -185,6 +187,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -218,6 +222,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -231,6 +237,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= @@ -306,6 +320,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= diff --git a/internal/global/common/router/router.go b/internal/global/common/router/router.go index f781892..c23a8d1 100644 --- a/internal/global/common/router/router.go +++ b/internal/global/common/router/router.go @@ -6,6 +6,7 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus/promhttp" ) type Router struct { @@ -77,6 +78,10 @@ func (r *Router) RegisterHealthCheck() { }) } +func (r *Router) RegisterMetrics() { + r.engine.GET("/metrics", gin.WrapH(promhttp.Handler())) +} + func (r *Router) RegisterSwagger(handler gin.HandlerFunc) { r.engine.GET("/swagger/*any", handler) } diff --git a/internal/global/middleware/metrics_middleware.go b/internal/global/middleware/metrics_middleware.go new file mode 100644 index 0000000..8bfb520 --- /dev/null +++ b/internal/global/middleware/metrics_middleware.go @@ -0,0 +1,63 @@ +package middleware + +import ( + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + httpRequestsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"method", "path", "status"}, + ) + + httpRequestDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "HTTP request duration in seconds", + Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5}, + }, + []string{"method", "path"}, + ) + + httpRequestsInFlight = promauto.NewGauge( + prometheus.GaugeOpts{ + Name: "http_requests_in_flight", + Help: "Current number of HTTP requests being processed", + }, + ) +) + +func PrometheusMetrics() gin.HandlerFunc { + return func(c *gin.Context) { + path := c.FullPath() + if path == "" { + path = "unknown" + } + + // /metrics, /health 는 측정 제외 + if path == "/metrics" || path == "/health" { + c.Next() + return + } + + httpRequestsInFlight.Inc() + start := time.Now() + + c.Next() + + duration := time.Since(start).Seconds() + status := strconv.Itoa(c.Writer.Status()) + + httpRequestsTotal.WithLabelValues(c.Request.Method, path, status).Inc() + httpRequestDuration.WithLabelValues(c.Request.Method, path).Observe(duration) + httpRequestsInFlight.Dec() + } +} From ae9929ef8cd7d537b27d24873c579ea4f45a2dac Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:25:46 +0900 Subject: [PATCH 14/24] fix: Apply PR #76 review feedback - Replace hardcoded Grafana admin credentials with env vars (${GRAFANA_ADMIN_USER}, ${GRAFANA_ADMIN_PASSWORD}) - Add docker/.env.example as reference for required env vars - Downgrade Prometheus image from v3.4.0 to stable v2.53.1 - Use defer for httpRequestsInFlight.Dec() to guarantee decrement on panic Co-Authored-By: Claude Sonnet 4.6 --- docker/.env.example | 2 ++ docker/docker-compose.yaml | 6 +++--- internal/global/middleware/metrics_middleware.go | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 docker/.env.example diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..25d6fcd --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,2 @@ +GRAFANA_ADMIN_USER=admin +GRAFANA_ADMIN_PASSWORD=your_secure_password diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index baf3a16..8a1bbd7 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -14,7 +14,7 @@ services: - gamers-network prometheus: - image: prom/prometheus:v3.4.0 + image: prom/prometheus:v2.53.1 container_name: gamers-prometheus volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro @@ -36,8 +36,8 @@ services: - ./grafana/dashboards:/etc/grafana/dashboards:ro - grafana-data:/var/lib/grafana environment: - GF_SECURITY_ADMIN_USER: admin - GF_SECURITY_ADMIN_PASSWORD: admin + GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER} + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD} GF_USERS_ALLOW_SIGN_UP: "false" ports: - "3001:3000" diff --git a/internal/global/middleware/metrics_middleware.go b/internal/global/middleware/metrics_middleware.go index 8bfb520..1d5359f 100644 --- a/internal/global/middleware/metrics_middleware.go +++ b/internal/global/middleware/metrics_middleware.go @@ -49,6 +49,7 @@ func PrometheusMetrics() gin.HandlerFunc { } httpRequestsInFlight.Inc() + defer httpRequestsInFlight.Dec() start := time.Now() c.Next() @@ -58,6 +59,5 @@ func PrometheusMetrics() gin.HandlerFunc { httpRequestsTotal.WithLabelValues(c.Request.Method, path, status).Inc() httpRequestDuration.WithLabelValues(c.Request.Method, path).Observe(duration) - httpRequestsInFlight.Dec() } } From 1bb228965285e346ab41b3d6f0e1fb35affe4e78 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:12:15 +0900 Subject: [PATCH 15/24] =?UTF-8?q?feat:=20Cloudflare=20R2=20=E2=86=92=20AWS?= =?UTF-8?q?=20S3=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EC=A7=80=20=EC=96=B4?= =?UTF-8?q?=EB=8C=91=ED=84=B0=20=EA=B5=90=EC=B2=B4=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - r2_storage_adapter.go 제거 (R2 커스텀 엔드포인트 방식 삭제) - s3_storage_adapter.go 추가 (표준 AWS 리전 엔드포인트 사용) - StoragePort 인터페이스 동일하게 구현 (Upload/Delete/GetPublicURL) - BucketName, Region 누락 시 초기화 실패 처리 - provider.go: R2 → S3 어댑터 초기화로 교체 - env/.env.example: R2 변수 제거, AWS S3 변수 추가 Co-Authored-By: Claude Sonnet 4.6 --- env/.env.example | 12 ++--- ...orage_adapter.go => s3_storage_adapter.go} | 52 +++++++++---------- internal/storage/provider.go | 6 +-- 3 files changed, 34 insertions(+), 36 deletions(-) rename internal/storage/infra/{r2_storage_adapter.go => s3_storage_adapter.go} (55%) diff --git a/env/.env.example b/env/.env.example index 19bd207..aacbaec 100644 --- a/env/.env.example +++ b/env/.env.example @@ -44,12 +44,12 @@ DISCORD_CLIENT_SECRET= DISCORD_REDIRECT_URL= DISCORD_BOT_TOKEN= -# Cloudflare R2 Storage Configuration -R2_ACCOUNT_ID= -R2_ACCESS_KEY_ID= -R2_SECRET_ACCESS_KEY= -R2_BUCKET_NAME= -R2_PUBLIC_URL= +# AWS S3 Storage Configuration +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION= +AWS_S3_BUCKET_NAME= +AWS_S3_PUBLIC_URL= # Riot OAuth Configuration RIOT_CLIENT_ID= diff --git a/internal/storage/infra/r2_storage_adapter.go b/internal/storage/infra/s3_storage_adapter.go similarity index 55% rename from internal/storage/infra/r2_storage_adapter.go rename to internal/storage/infra/s3_storage_adapter.go index 940c43b..dea0a82 100644 --- a/internal/storage/infra/r2_storage_adapter.go +++ b/internal/storage/infra/s3_storage_adapter.go @@ -1,59 +1,57 @@ package infra import ( - "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/application/port" "context" "fmt" "io" "os" + "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/application/port" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" ) -type R2StorageAdapter struct { +type S3StorageAdapter struct { client *s3.Client bucketName string publicURL string } -type R2Config struct { - AccountID string +type S3Config struct { AccessKeyID string SecretAccessKey string + Region string BucketName string PublicURL string } -func NewR2ConfigFromEnv() *R2Config { - return &R2Config{ - AccountID: os.Getenv("R2_ACCOUNT_ID"), - AccessKeyID: os.Getenv("R2_ACCESS_KEY_ID"), - SecretAccessKey: os.Getenv("R2_SECRET_ACCESS_KEY"), - BucketName: os.Getenv("R2_BUCKET_NAME"), - PublicURL: os.Getenv("R2_PUBLIC_URL"), +func NewS3ConfigFromEnv() *S3Config { + return &S3Config{ + AccessKeyID: os.Getenv("AWS_ACCESS_KEY_ID"), + SecretAccessKey: os.Getenv("AWS_SECRET_ACCESS_KEY"), + Region: os.Getenv("AWS_REGION"), + BucketName: os.Getenv("AWS_S3_BUCKET_NAME"), + PublicURL: os.Getenv("AWS_S3_PUBLIC_URL"), } } -func NewR2StorageAdapter(cfg *R2Config) (port.StoragePort, error) { - r2Endpoint := fmt.Sprintf("https://%s.r2.cloudflarestorage.com", cfg.AccountID) - - r2Resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { - return aws.Endpoint{ - URL: r2Endpoint, - }, nil - }) +func NewS3StorageAdapter(cfg *S3Config) (port.StoragePort, error) { + if cfg.BucketName == "" { + return nil, fmt.Errorf("AWS_S3_BUCKET_NAME is required") + } + if cfg.Region == "" { + return nil, fmt.Errorf("AWS_REGION is required") + } awsCfg, err := config.LoadDefaultConfig(context.Background(), - config.WithEndpointResolverWithOptions(r2Resolver), + config.WithRegion(cfg.Region), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( cfg.AccessKeyID, cfg.SecretAccessKey, "", )), - config.WithRegion("auto"), ) if err != nil { return nil, fmt.Errorf("failed to load AWS config: %w", err) @@ -61,14 +59,14 @@ func NewR2StorageAdapter(cfg *R2Config) (port.StoragePort, error) { client := s3.NewFromConfig(awsCfg) - return &R2StorageAdapter{ + return &S3StorageAdapter{ client: client, bucketName: cfg.BucketName, publicURL: cfg.PublicURL, }, nil } -func (a *R2StorageAdapter) Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) error { +func (a *S3StorageAdapter) Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) error { _, err := a.client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(a.bucketName), Key: aws.String(key), @@ -77,22 +75,22 @@ func (a *R2StorageAdapter) Upload(ctx context.Context, key string, body io.Reade ContentType: aws.String(contentType), }) if err != nil { - return fmt.Errorf("failed to upload to R2: %w", err) + return fmt.Errorf("failed to upload to S3: %w", err) } return nil } -func (a *R2StorageAdapter) Delete(ctx context.Context, key string) error { +func (a *S3StorageAdapter) Delete(ctx context.Context, key string) error { _, err := a.client.DeleteObject(ctx, &s3.DeleteObjectInput{ Bucket: aws.String(a.bucketName), Key: aws.String(key), }) if err != nil { - return fmt.Errorf("failed to delete from R2: %w", err) + return fmt.Errorf("failed to delete from S3: %w", err) } return nil } -func (a *R2StorageAdapter) GetPublicURL(key string) string { +func (a *S3StorageAdapter) GetPublicURL(key string) string { return fmt.Sprintf("%s/%s", a.publicURL, key) } diff --git a/internal/storage/provider.go b/internal/storage/provider.go index d9743c4..663f093 100644 --- a/internal/storage/provider.go +++ b/internal/storage/provider.go @@ -19,10 +19,10 @@ type Dependencies struct { func ProvideStorageDependencies(router *router.Router) *Dependencies { controllerHelper := handler.NewControllerHelper() - r2Config := infra.NewR2ConfigFromEnv() - storageAdapter, err := infra.NewR2StorageAdapter(r2Config) + s3Config := infra.NewS3ConfigFromEnv() + storageAdapter, err := infra.NewS3StorageAdapter(s3Config) if err != nil { - log.Printf("Warning: Failed to initialize R2 storage adapter: %v", err) + log.Printf("Warning: Failed to initialize S3 storage adapter: %v", err) log.Println("Storage endpoints will not be available") return nil } From 36b2d8921ac0cdbe939146db6417df8ecc525295 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:44:03 +0900 Subject: [PATCH 16/24] =?UTF-8?q?feat:=20AWS=20S3=20=E2=86=92=20Google=20C?= =?UTF-8?q?loud=20Storage(GCS)=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EC=A7=80=20?= =?UTF-8?q?=EC=96=B4=EB=8C=91=ED=84=B0=20=EA=B5=90=EC=B2=B4=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - s3_storage_adapter.go 제거 - gcs_storage_adapter.go 추가 (cloud.google.com/go/storage v1.61.3) - GCS_CREDENTIALS_JSON 설정 시 서비스 계정 JSON 인증 - 미설정 시 ADC(Application Default Credentials) 자동 폴백 - GCS_BUCKET_NAME 누락 시 초기화 실패 처리 - provider.go: S3 → GCS 어댑터 초기화로 교체 - env/.env.example: AWS S3 변수 제거, GCS 변수 추가 - go.mod/go.sum: cloud.google.com/go/storage 의존성 추가 Co-Authored-By: Claude Sonnet 4.6 --- env/.env.example | 10 +- go.mod | 55 ++++++++--- go.sum | 84 ++++++++++++++++ internal/storage/infra/gcs_storage_adapter.go | 81 ++++++++++++++++ internal/storage/infra/s3_storage_adapter.go | 96 ------------------- internal/storage/provider.go | 10 +- 6 files changed, 216 insertions(+), 120 deletions(-) create mode 100644 internal/storage/infra/gcs_storage_adapter.go delete mode 100644 internal/storage/infra/s3_storage_adapter.go diff --git a/env/.env.example b/env/.env.example index aacbaec..6d5d18a 100644 --- a/env/.env.example +++ b/env/.env.example @@ -44,12 +44,10 @@ DISCORD_CLIENT_SECRET= DISCORD_REDIRECT_URL= DISCORD_BOT_TOKEN= -# AWS S3 Storage Configuration -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_REGION= -AWS_S3_BUCKET_NAME= -AWS_S3_PUBLIC_URL= +# Google Cloud Storage Configuration +GCS_BUCKET_NAME= +GCS_PUBLIC_URL= +GCS_CREDENTIALS_JSON= # Riot OAuth Configuration RIOT_CLIENT_ID= diff --git a/go.mod b/go.mod index 2343c8d..b76cfb8 100644 --- a/go.mod +++ b/go.mod @@ -24,17 +24,28 @@ require ( github.com/testcontainers/testcontainers-go v0.40.0 github.com/testcontainers/testcontainers-go/modules/mysql v0.40.0 github.com/yldshv/go-valorant-api v1.0.7 - golang.org/x/crypto v0.46.0 - golang.org/x/oauth2 v0.34.0 - golang.org/x/sync v0.19.0 + golang.org/x/crypto v0.48.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.20.0 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.1 ) require ( + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect + cloud.google.com/go/storage v1.61.3 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect @@ -59,6 +70,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -71,9 +83,12 @@ require ( github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect @@ -92,6 +107,9 @@ require ( github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.1 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -117,6 +135,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_model v0.6.2 // indirect @@ -126,6 +145,7 @@ require ( github.com/quic-go/quic-go v0.58.0 // indirect github.com/shirou/gopsutil/v4 v4.25.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect @@ -133,21 +153,28 @@ require ( github.com/ugorji/go/codec v1.3.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/sdk v1.38.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.23.0 // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect - golang.org/x/tools v0.40.0 // indirect - google.golang.org/grpc v1.78.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.41.0 // indirect + google.golang.org/api v0.271.0 // indirect + google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ca14949..2432767 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,19 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg= +cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -6,6 +22,12 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -66,6 +88,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -96,6 +120,11 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= @@ -108,6 +137,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -164,8 +195,14 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -232,6 +269,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -259,6 +298,8 @@ github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dI github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -297,22 +338,36 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= @@ -330,21 +385,31 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -360,33 +425,52 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= +google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= +google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0= +google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/storage/infra/gcs_storage_adapter.go b/internal/storage/infra/gcs_storage_adapter.go new file mode 100644 index 0000000..326f73b --- /dev/null +++ b/internal/storage/infra/gcs_storage_adapter.go @@ -0,0 +1,81 @@ +package infra + +import ( + "context" + "fmt" + "io" + "os" + + "cloud.google.com/go/storage" + "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/application/port" + "google.golang.org/api/option" +) + +type GCSStorageAdapter struct { + client *storage.Client + bucketName string + publicURL string +} + +type GCSConfig struct { + BucketName string + PublicURL string + CredentialsJSON string // Service Account JSON 전체 내용. 미설정 시 ADC 사용 +} + +func NewGCSConfigFromEnv() *GCSConfig { + return &GCSConfig{ + BucketName: os.Getenv("GCS_BUCKET_NAME"), + PublicURL: os.Getenv("GCS_PUBLIC_URL"), + CredentialsJSON: os.Getenv("GCS_CREDENTIALS_JSON"), + } +} + +func NewGCSStorageAdapter(ctx context.Context, cfg *GCSConfig) (port.StoragePort, error) { + if cfg.BucketName == "" { + return nil, fmt.Errorf("GCS_BUCKET_NAME is required") + } + + var opts []option.ClientOption + if cfg.CredentialsJSON != "" { + opts = append(opts, option.WithCredentialsJSON([]byte(cfg.CredentialsJSON))) + } + // GCS_CREDENTIALS_JSON 미설정 시 Application Default Credentials(ADC) 자동 사용 + + client, err := storage.NewClient(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("failed to create GCS client: %w", err) + } + + return &GCSStorageAdapter{ + client: client, + bucketName: cfg.BucketName, + publicURL: cfg.PublicURL, + }, nil +} + +func (a *GCSStorageAdapter) Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) error { + wc := a.client.Bucket(a.bucketName).Object(key).NewWriter(ctx) + wc.ContentType = contentType + + if _, err := io.Copy(wc, body); err != nil { + _ = wc.Close() + return fmt.Errorf("failed to upload to GCS: %w", err) + } + + if err := wc.Close(); err != nil { + return fmt.Errorf("failed to finalize GCS upload: %w", err) + } + return nil +} + +func (a *GCSStorageAdapter) Delete(ctx context.Context, key string) error { + if err := a.client.Bucket(a.bucketName).Object(key).Delete(ctx); err != nil { + return fmt.Errorf("failed to delete from GCS: %w", err) + } + return nil +} + +func (a *GCSStorageAdapter) GetPublicURL(key string) string { + return fmt.Sprintf("%s/%s", a.publicURL, key) +} diff --git a/internal/storage/infra/s3_storage_adapter.go b/internal/storage/infra/s3_storage_adapter.go deleted file mode 100644 index dea0a82..0000000 --- a/internal/storage/infra/s3_storage_adapter.go +++ /dev/null @@ -1,96 +0,0 @@ -package infra - -import ( - "context" - "fmt" - "io" - "os" - - "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/application/port" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/service/s3" -) - -type S3StorageAdapter struct { - client *s3.Client - bucketName string - publicURL string -} - -type S3Config struct { - AccessKeyID string - SecretAccessKey string - Region string - BucketName string - PublicURL string -} - -func NewS3ConfigFromEnv() *S3Config { - return &S3Config{ - AccessKeyID: os.Getenv("AWS_ACCESS_KEY_ID"), - SecretAccessKey: os.Getenv("AWS_SECRET_ACCESS_KEY"), - Region: os.Getenv("AWS_REGION"), - BucketName: os.Getenv("AWS_S3_BUCKET_NAME"), - PublicURL: os.Getenv("AWS_S3_PUBLIC_URL"), - } -} - -func NewS3StorageAdapter(cfg *S3Config) (port.StoragePort, error) { - if cfg.BucketName == "" { - return nil, fmt.Errorf("AWS_S3_BUCKET_NAME is required") - } - if cfg.Region == "" { - return nil, fmt.Errorf("AWS_REGION is required") - } - - awsCfg, err := config.LoadDefaultConfig(context.Background(), - config.WithRegion(cfg.Region), - config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( - cfg.AccessKeyID, - cfg.SecretAccessKey, - "", - )), - ) - if err != nil { - return nil, fmt.Errorf("failed to load AWS config: %w", err) - } - - client := s3.NewFromConfig(awsCfg) - - return &S3StorageAdapter{ - client: client, - bucketName: cfg.BucketName, - publicURL: cfg.PublicURL, - }, nil -} - -func (a *S3StorageAdapter) Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) error { - _, err := a.client.PutObject(ctx, &s3.PutObjectInput{ - Bucket: aws.String(a.bucketName), - Key: aws.String(key), - Body: body, - ContentLength: aws.Int64(size), - ContentType: aws.String(contentType), - }) - if err != nil { - return fmt.Errorf("failed to upload to S3: %w", err) - } - return nil -} - -func (a *S3StorageAdapter) Delete(ctx context.Context, key string) error { - _, err := a.client.DeleteObject(ctx, &s3.DeleteObjectInput{ - Bucket: aws.String(a.bucketName), - Key: aws.String(key), - }) - if err != nil { - return fmt.Errorf("failed to delete from S3: %w", err) - } - return nil -} - -func (a *S3StorageAdapter) GetPublicURL(key string) string { - return fmt.Sprintf("%s/%s", a.publicURL, key) -} diff --git a/internal/storage/provider.go b/internal/storage/provider.go index 663f093..5e76ad5 100644 --- a/internal/storage/provider.go +++ b/internal/storage/provider.go @@ -1,13 +1,15 @@ package storage import ( + "context" + "log" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/common/handler" "github.com/FOR-GAMERS/GAMERS-BE/internal/global/common/router" "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/application" "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/application/port" "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/infra" "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/presentation" - "log" ) type Dependencies struct { @@ -19,10 +21,10 @@ type Dependencies struct { func ProvideStorageDependencies(router *router.Router) *Dependencies { controllerHelper := handler.NewControllerHelper() - s3Config := infra.NewS3ConfigFromEnv() - storageAdapter, err := infra.NewS3StorageAdapter(s3Config) + gcsConfig := infra.NewGCSConfigFromEnv() + storageAdapter, err := infra.NewGCSStorageAdapter(context.Background(), gcsConfig) if err != nil { - log.Printf("Warning: Failed to initialize S3 storage adapter: %v", err) + log.Printf("Warning: Failed to initialize GCS storage adapter: %v", err) log.Println("Storage endpoints will not be available") return nil } From 14c322ca796a3a03aef2fd5a2fbe502f6d348a0e Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:39:48 +0900 Subject: [PATCH 17/24] =?UTF-8?q?feat:=20Add=20pkg/lcu=20=E2=80=94=20Go=20?= =?UTF-8?q?client=20library=20for=20LCU=20(League=20Client=20Update)=20API?= =?UTF-8?q?=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a type-safe Go package that communicates with the locally running League of Legends client via the LCU HTTP/WebSocket API. - lockfile.go: auto-discover lockfile on Windows/macOS/Linux and extract port + password - client.go: http.Client with InsecureSkipVerify (self-signed cert) and automatic Basic auth header injection - session.go: GetGameflowPhase, GetGameflowSession, GetCurrentSummoner - match.go: GetLastGameID, GetMatchDetail, GetMatchHistory with full struct mappings for Game, ParticipantStats, Timeline, etc. - event.go: WebSocket subscription via gorilla/websocket using WAMP protocol; delivers typed Event{Name,Type,URI,Data} to handler func - process.go: LeagueClient.exe process detection and WaitForClientAndConnect helper Adds github.com/gorilla/websocket v1.5.3 dependency. Co-Authored-By: Claude Sonnet 4.6 --- go.mod | 1 + go.sum | 2 + pkg/lcu/client.go | 128 ++++++++++++++++++++++++++++++ pkg/lcu/event.go | 171 ++++++++++++++++++++++++++++++++++++++++ pkg/lcu/example_test.go | 107 +++++++++++++++++++++++++ pkg/lcu/lockfile.go | 100 +++++++++++++++++++++++ pkg/lcu/match.go | 161 +++++++++++++++++++++++++++++++++++++ pkg/lcu/process.go | 95 ++++++++++++++++++++++ pkg/lcu/session.go | 125 +++++++++++++++++++++++++++++ 9 files changed, 890 insertions(+) create mode 100644 pkg/lcu/client.go create mode 100644 pkg/lcu/event.go create mode 100644 pkg/lcu/example_test.go create mode 100644 pkg/lcu/lockfile.go create mode 100644 pkg/lcu/match.go create mode 100644 pkg/lcu/process.go create mode 100644 pkg/lcu/session.go diff --git a/go.mod b/go.mod index b76cfb8..c8b80d0 100644 --- a/go.mod +++ b/go.mod @@ -110,6 +110,7 @@ require ( github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect diff --git a/go.sum b/go.sum index 2432767..e6a916c 100644 --- a/go.sum +++ b/go.sum @@ -203,6 +203,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= diff --git a/pkg/lcu/client.go b/pkg/lcu/client.go new file mode 100644 index 0000000..c07a0aa --- /dev/null +++ b/pkg/lcu/client.go @@ -0,0 +1,128 @@ +package lcu + +import ( + "context" + "crypto/tls" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +const ( + lcuUsername = "riot" + defaultTimeout = 10 * time.Second +) + +// Client is the LCU HTTP client. +// LCU는 로컬 자체 서명 인증서를 사용하므로 InsecureSkipVerify가 필수입니다. +type Client struct { + http *http.Client + baseURL string + authHeader string +} + +// NewClient creates a Client from a pre-parsed LockfileData. +func NewClient(lf *LockfileData) *Client { + return newClient(lf.Port, lf.Password) +} + +// NewClientFromLockfile reads the lockfile automatically and builds a Client. +func NewClientFromLockfile() (*Client, error) { + lf, err := ReadLockfile() + if err != nil { + return nil, err + } + return NewClient(lf), nil +} + +// NewClientFromPath reads the lockfile from the given path and builds a Client. +func NewClientFromPath(lockfilePath string) (*Client, error) { + lf, err := ReadLockfileFromPath(lockfilePath) + if err != nil { + return nil, err + } + return NewClient(lf), nil +} + +func newClient(port int, password string) *Client { + // LCU는 자체 서명 인증서를 사용하므로 SSL 검증을 무시합니다. + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec + } + + // Authorization: Basic base64("riot:{password}") + cred := base64.StdEncoding.EncodeToString([]byte(lcuUsername + ":" + password)) + + return &Client{ + http: &http.Client{ + Transport: transport, + Timeout: defaultTimeout, + }, + baseURL: fmt.Sprintf("https://127.0.0.1:%d", port), + authHeader: "Basic " + cred, + } +} + +// Get performs a GET request to the given LCU endpoint path and +// decodes the JSON response body into dest. +// +// 예) client.Get(ctx, "/lol-gameflow/v1/session", &session) +func (c *Client) Get(ctx context.Context, path string, dest any) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil) + if err != nil { + return fmt.Errorf("lcu: build request: %w", err) + } + c.setCommonHeaders(req) + + resp, err := c.http.Do(req) + if err != nil { + return fmt.Errorf("lcu: do request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("lcu: %s %s returned %d: %s", req.Method, path, resp.StatusCode, body) + } + + if dest == nil { + return nil + } + if err := json.NewDecoder(resp.Body).Decode(dest); err != nil { + return fmt.Errorf("lcu: decode response: %w", err) + } + return nil +} + +// RawGet performs a GET and returns the raw response body bytes. +func (c *Client) RawGet(ctx context.Context, path string) ([]byte, int, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil) + if err != nil { + return nil, 0, fmt.Errorf("lcu: build request: %w", err) + } + c.setCommonHeaders(req) + + resp, err := c.http.Do(req) + if err != nil { + return nil, 0, fmt.Errorf("lcu: do request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + return body, resp.StatusCode, err +} + +func (c *Client) setCommonHeaders(req *http.Request) { + req.Header.Set("Authorization", c.authHeader) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") +} + +// BaseURL returns the base URL for constructing WebSocket connections. +func (c *Client) BaseURL() string { return c.baseURL } + +// AuthHeader returns the precomputed Authorization header value. +func (c *Client) AuthHeader() string { return c.authHeader } diff --git a/pkg/lcu/event.go b/pkg/lcu/event.go new file mode 100644 index 0000000..8b2526f --- /dev/null +++ b/pkg/lcu/event.go @@ -0,0 +1,171 @@ +package lcu + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/gorilla/websocket" +) + +// LCU WebSocket WAMP opcodes +const ( + OpcodeWelcome = 0 + OpcodeSubscribe = 5 + OpcodeUnsubscribe = 6 + OpcodeEvent = 8 +) + +// Event is a decoded LCU WebSocket event. +// LCU 전송 형식: [opcode, eventName, {"eventType":"...", "uri":"...", "data":{...}}] +type Event struct { + // Name is the full event name, e.g. "OnJsonApiEvent_lol-gameflow_v1_session" + Name string + // Type is "Create", "Update", or "Delete" + Type string + // URI is the REST path that changed, e.g. "/lol-gameflow/v1/session" + URI string + // Data is the raw JSON payload. Decode into the appropriate struct as needed. + Data json.RawMessage +} + +// EventHandler is called for every received event. +type EventHandler func(event Event) + +// EventSubscription represents an active WebSocket subscription. +type EventSubscription struct { + conn *websocket.Conn + cancel context.CancelFunc +} + +// Close terminates the WebSocket connection and stops the listener goroutine. +func (s *EventSubscription) Close() { + s.cancel() + _ = s.conn.Close() +} + +// Subscribe connects to the LCU WebSocket, subscribes to the given endpoint events, +// and calls handler for every received event. +// +// uriPatterns은 구독할 URI 목록입니다. 빈 슬라이스를 전달하면 모든 이벤트를 수신합니다. +// +// 예) client.Subscribe(ctx, handler, "/lol-gameflow/v1/session", "/lol-champ-select/v1/session") +// +// 반환된 EventSubscription.Close()를 호출하면 구독이 해제됩니다. +func (c *Client) Subscribe( + ctx context.Context, + handler EventHandler, + uriPatterns ...string, +) (*EventSubscription, error) { + wsURL := "wss://127.0.0.1" + strings.TrimPrefix(c.baseURL, "https://127.0.0.1") + // baseURL already contains the port, e.g. https://127.0.0.1:62000 + // Replace scheme only + wsURL = strings.Replace(c.baseURL, "https://", "wss://", 1) + "/" + + dialer := websocket.Dialer{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec + } + + headers := http.Header{} + headers.Set("Authorization", c.authHeader) + + conn, _, err := dialer.DialContext(ctx, wsURL, headers) + if err != nil { + return nil, fmt.Errorf("lcu: websocket dial: %w", err) + } + + // URI 패턴을 WAMP 이벤트 이름으로 변환: + // /lol-gameflow/v1/session → OnJsonApiEvent_lol-gameflow_v1_session + eventNames := make([]string, 0, len(uriPatterns)) + if len(uriPatterns) == 0 { + // 패턴이 없으면 전체 이벤트 구독 + eventNames = append(eventNames, "OnJsonApiEvent") + } else { + for _, pattern := range uriPatterns { + eventNames = append(eventNames, uriToEventName(pattern)) + } + } + + for _, name := range eventNames { + msg, _ := json.Marshal([]any{OpcodeSubscribe, name}) + if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil { + _ = conn.Close() + return nil, fmt.Errorf("lcu: subscribe to %q: %w", name, err) + } + } + + subCtx, cancel := context.WithCancel(ctx) + sub := &EventSubscription{conn: conn, cancel: cancel} + + go func() { + defer sub.Close() + for { + select { + case <-subCtx.Done(): + return + default: + } + + _, raw, err := conn.ReadMessage() + if err != nil { + // 연결이 닫혔거나 컨텍스트가 취소된 경우 정상 종료 + return + } + + event, err := parseEvent(raw) + if err != nil { + // 파싱 불가 메시지(예: 연결 초기화 응답)는 무시 + continue + } + handler(*event) + } + }() + + return sub, nil +} + +// uriToEventName converts a REST path to an LCU WebSocket event name. +// /lol-gameflow/v1/session → OnJsonApiEvent_lol-gameflow_v1_session +func uriToEventName(uri string) string { + name := strings.TrimPrefix(uri, "/") + name = strings.ReplaceAll(name, "/", "_") + return "OnJsonApiEvent_" + name +} + +// parseEvent parses a raw WAMP message into an Event. +// LCU format: [8, "OnJsonApiEvent_...", {"eventType":"Update","uri":"/...","data":{...}}] +func parseEvent(raw []byte) (*Event, error) { + var envelope []json.RawMessage + if err := json.Unmarshal(raw, &envelope); err != nil || len(envelope) != 3 { + return nil, fmt.Errorf("not a 3-element WAMP array") + } + + var opcode int + if err := json.Unmarshal(envelope[0], &opcode); err != nil || opcode != OpcodeEvent { + return nil, fmt.Errorf("not an event message (opcode=%d)", opcode) + } + + var eventName string + if err := json.Unmarshal(envelope[1], &eventName); err != nil { + return nil, fmt.Errorf("cannot parse event name: %w", err) + } + + var payload struct { + EventType string `json:"eventType"` + URI string `json:"uri"` + Data json.RawMessage `json:"data"` + } + if err := json.Unmarshal(envelope[2], &payload); err != nil { + return nil, fmt.Errorf("cannot parse event payload: %w", err) + } + + return &Event{ + Name: eventName, + Type: payload.EventType, + URI: payload.URI, + Data: payload.Data, + }, nil +} diff --git a/pkg/lcu/example_test.go b/pkg/lcu/example_test.go new file mode 100644 index 0000000..de469f5 --- /dev/null +++ b/pkg/lcu/example_test.go @@ -0,0 +1,107 @@ +package lcu_test + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/FOR-GAMERS/GAMERS-BE/pkg/lcu" +) + +// ExampleNewClientFromLockfile shows the standard usage pattern. +func ExampleNewClientFromLockfile() { + // 1. 클라이언트가 실행될 때까지 대기 후 자동 연결 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + client, err := lcu.WaitForClientAndConnect(ctx, 2*time.Second) + if err != nil { + log.Fatal(err) + } + + // 2. 현재 소환사 정보 조회 + summoner, err := client.GetCurrentSummoner(ctx) + if err != nil { + log.Fatal(err) + } + fmt.Printf("소환사: %s (레벨 %d)\n", summoner.DisplayName, summoner.SummonerLevel) + + // 3. 게임 흐름 단계 확인 + phase, err := client.GetGameflowPhase(ctx) + if err != nil { + log.Fatal(err) + } + fmt.Printf("현재 단계: %s\n", phase) + + // 4. 가장 최근 매치 상세 조회 + game, err := client.GetLastMatchWithDetail(ctx) + if err != nil { + log.Fatal(err) + } + fmt.Printf("마지막 게임 ID: %d, 모드: %s, 시간: %d초\n", + game.GameID, game.GameMode, game.GameDuration) +} + +// ExampleClient_Subscribe shows how to listen for real-time events. +func ExampleClient_Subscribe() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + defer cancel() + + client, err := lcu.NewClientFromLockfile() + if err != nil { + log.Fatal(err) + } + + // 게임 흐름 변경 이벤트 구독 + sub, err := client.Subscribe(ctx, + func(event lcu.Event) { + switch event.URI { + case "/lol-gameflow/v1/gameflow-phase": + var phase lcu.GameflowPhase + if err := json.Unmarshal(event.Data, &phase); err == nil { + fmt.Printf("[이벤트] 게임 단계 변경: %s\n", phase) + } + + case "/lol-gameflow/v1/session": + var session lcu.GameflowSession + if err := json.Unmarshal(event.Data, &session); err == nil { + fmt.Printf("[이벤트] 세션 업데이트: phase=%s, gameId=%d\n", + session.Phase, session.GameData.GameID) + } + } + }, + "/lol-gameflow/v1/gameflow-phase", + "/lol-gameflow/v1/session", + ) + if err != nil { + log.Fatal(err) + } + defer sub.Close() + + fmt.Println("이벤트 수신 대기 중... (Ctrl+C로 종료)") + <-ctx.Done() +} + +// ExampleClient_GetMatchHistory shows paginated match history retrieval. +func ExampleClient_GetMatchHistory() { + ctx := context.Background() + + client, err := lcu.NewClientFromLockfile() + if err != nil { + log.Fatal(err) + } + + // 최근 20경기 조회 (0~19) + history, err := client.GetMatchHistory(ctx, 0, 19) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("총 %d경기\n", history.Games.GameCount) + for _, game := range history.Games.Games { + fmt.Printf(" - gameId=%d mode=%-8s duration=%ds\n", + game.GameID, game.GameMode, game.GameDuration) + } +} diff --git a/pkg/lcu/lockfile.go b/pkg/lcu/lockfile.go new file mode 100644 index 0000000..34294b8 --- /dev/null +++ b/pkg/lcu/lockfile.go @@ -0,0 +1,100 @@ +package lcu + +import ( + "fmt" + "os" + "runtime" + "strconv" + "strings" +) + +// LockfileData holds the parsed lockfile fields. +type LockfileData struct { + ProcessName string + PID int + Port int + Password string + Protocol string +} + +// lockfilePaths returns candidate paths for the LoL lockfile based on OS. +func lockfilePaths() []string { + switch runtime.GOOS { + case "windows": + return []string{ + `C:\Riot Games\League of Legends\lockfile`, + `C:\Program Files\Riot Games\League of Legends\lockfile`, + `C:\Program Files (x86)\Riot Games\League of Legends\lockfile`, + } + case "darwin": + return []string{ + "/Applications/League of Legends.app/Contents/LoL/lockfile", + "/Applications/Riot Games/League of Legends.app/Contents/LoL/lockfile", + } + default: // linux (PBE/wine 환경) + return []string{ + os.ExpandEnv("$HOME/.wine/drive_c/Riot Games/League of Legends/lockfile"), + } + } +} + +// ReadLockfile reads the LoL lockfile and returns parsed credentials. +// lockfile 형식: ProcessName:PID:Port:Password:Protocol +// +// 예) LeagueClient:12345:62000:some-token:https +func ReadLockfile() (*LockfileData, error) { + return ReadLockfileFromPath("") +} + +// ReadLockfileFromPath reads from a specific path. +// path가 비어 있으면 OS별 기본 경로를 순서대로 시도합니다. +func ReadLockfileFromPath(path string) (*LockfileData, error) { + var candidates []string + if path != "" { + candidates = []string{path} + } else { + candidates = lockfilePaths() + } + + var lastErr error + for _, candidate := range candidates { + data, err := parseLockfile(candidate) + if err == nil { + return data, nil + } + lastErr = err + } + + return nil, fmt.Errorf("lockfile not found in any candidate path: %w", lastErr) +} + +func parseLockfile(path string) (*LockfileData, error) { + raw, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("cannot read lockfile at %q: %w", path, err) + } + + // 형식: LeagueClient:12345:62000:some-token:https + parts := strings.Split(strings.TrimSpace(string(raw)), ":") + if len(parts) != 5 { + return nil, fmt.Errorf("unexpected lockfile format: expected 5 fields, got %d", len(parts)) + } + + pid, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, fmt.Errorf("invalid PID %q in lockfile: %w", parts[1], err) + } + + port, err := strconv.Atoi(parts[2]) + if err != nil { + return nil, fmt.Errorf("invalid port %q in lockfile: %w", parts[2], err) + } + + return &LockfileData{ + ProcessName: parts[0], + PID: pid, + Port: port, + Password: parts[3], + Protocol: parts[4], + }, nil +} diff --git a/pkg/lcu/match.go b/pkg/lcu/match.go new file mode 100644 index 0000000..b99877f --- /dev/null +++ b/pkg/lcu/match.go @@ -0,0 +1,161 @@ +package lcu + +import ( + "context" + "fmt" + "strconv" +) + +// ---- Match History Structs ------------------------------------------------- + +// MatchHistoryList is the paginated match history response. +type MatchHistoryList struct { + AccountID int64 `json:"accountId"` + Games Games `json:"games"` +} + +type Games struct { + GameCount int `json:"gameCount"` + Games []Game `json:"games"` +} + +// Game is a single match entry from the match history list. +type Game struct { + GameID int64 `json:"gameId"` + GameCreation int64 `json:"gameCreation"` // unix ms + GameDuration int `json:"gameDuration"` // 초 + GameMode string `json:"gameMode"` // CLASSIC, ARAM + GameType string `json:"gameType"` + MapID int `json:"mapId"` + QueueID int `json:"queueId"` + SeasonID int `json:"seasonId"` + Participants []Participant `json:"participants"` + ParticipantIdentities []ParticipantIdentity `json:"participantIdentities"` + Teams []Team `json:"teams"` +} + +// ParticipantIdentity maps participantId to player info. +type ParticipantIdentity struct { + ParticipantID int `json:"participantId"` + Player Player `json:"player"` +} + +type Player struct { + AccountID int64 `json:"accountId"` + SummonerID string `json:"summonerId"` + SummonerName string `json:"summonerName"` + ProfileIcon int `json:"profileIcon"` + PUUID string `json:"puuid"` +} + +type Team struct { + TeamID int `json:"teamId"` // 100 = blue, 200 = red + Win string `json:"win"` // "Win" or "Fail" + FirstBlood bool `json:"firstBlood"` + FirstTower bool `json:"firstTower"` + FirstDragon bool `json:"firstDragon"` + FirstBaron bool `json:"firstBaron"` + TowerKills int `json:"towerKills"` + DragonKills int `json:"dragonKills"` + BaronKills int `json:"baronKills"` + RiftHeraldKills int `json:"riftHeraldKills"` +} + +// GameParticipant holds one player's in-game stats. +// 주의: 위 session.go의 Participant와 다른 구조체입니다. +type GameParticipant struct { + ParticipantID int `json:"participantId"` + TeamID int `json:"teamId"` + ChampionID int `json:"championId"` + Spell1ID int `json:"spell1Id"` + Spell2ID int `json:"spell2Id"` + Stats ParticipantStats `json:"stats"` + Timeline Timeline `json:"timeline"` +} + +// ParticipantStats contains all end-of-game stats for a player. +type ParticipantStats struct { + Win bool `json:"win"` + Kills int `json:"kills"` + Deaths int `json:"deaths"` + Assists int `json:"assists"` + TotalDamageDealtToChampions int `json:"totalDamageDealtToChampions"` + GoldEarned int `json:"goldEarned"` + TotalMinionsKilled int `json:"totalMinionsKilled"` + NeutralMinionsKilled int `json:"neutralMinionsKilled"` + VisionScore int `json:"visionScore"` + WardsPlaced int `json:"wardsPlaced"` + WardsKilled int `json:"wardsKilled"` + VisionWardsBoughtInGame int `json:"visionWardsBoughtInGame"` + ChampLevel int `json:"champLevel"` + Item0 int `json:"item0"` + Item1 int `json:"item1"` + Item2 int `json:"item2"` + Item3 int `json:"item3"` + Item4 int `json:"item4"` + Item5 int `json:"item5"` + Item6 int `json:"item6"` + PentaKills int `json:"pentaKills"` + QuadraKills int `json:"quadraKills"` + TripleKills int `json:"tripleKills"` + DoubleKills int `json:"doubleKills"` + FirstBloodKill bool `json:"firstBloodKill"` +} + +// Timeline has per-minute stats (lane, role, cs deltas, etc.) +type Timeline struct { + Lane string `json:"lane"` // TOP, JUNGLE, MID, BOTTOM, SUPPORT + Role string `json:"role"` + CreepsPerMinDeltas map[string]float64 `json:"creepsPerMinDeltas"` + GoldPerMinDeltas map[string]float64 `json:"goldPerMinDeltas"` +} + +// LastGameID is the response from /lol-match-history/v1/products/lol/last-game-id +type LastGameID int64 + +// ---- API Methods ----------------------------------------------------------- + +// GetLastGameID returns the game ID of the most recently completed match. +// /lol-match-history/v1/products/lol/last-game-id +func (c *Client) GetLastGameID(ctx context.Context) (int64, error) { + var id int64 + if err := c.Get(ctx, "/lol-match-history/v1/products/lol/last-game-id", &id); err != nil { + return 0, fmt.Errorf("GetLastGameID: %w", err) + } + return id, nil +} + +// GetMatchDetail returns full match data for the given game ID. +// /lol-match-history/v1/games/{gameId} +func (c *Client) GetMatchDetail(ctx context.Context, gameID int64) (*Game, error) { + var game Game + path := "/lol-match-history/v1/games/" + strconv.FormatInt(gameID, 10) + if err := c.Get(ctx, path, &game); err != nil { + return nil, fmt.Errorf("GetMatchDetail(gameID=%d): %w", gameID, err) + } + return &game, nil +} + +// GetMatchHistory returns paginated match history for the current summoner. +// /lol-match-history/v1/products/lol/current-summoner/matches?begIndex=0&endIndex=20 +func (c *Client) GetMatchHistory(ctx context.Context, begIndex, endIndex int) (*MatchHistoryList, error) { + path := fmt.Sprintf( + "/lol-match-history/v1/products/lol/current-summoner/matches?begIndex=%d&endIndex=%d", + begIndex, endIndex, + ) + var result MatchHistoryList + if err := c.Get(ctx, path, &result); err != nil { + return nil, fmt.Errorf("GetMatchHistory: %w", err) + } + return &result, nil +} + +// GetLastMatchWithDetail is a convenience method that fetches the last game ID +// and immediately retrieves its full details in one call. +func (c *Client) GetLastMatchWithDetail(ctx context.Context) (*Game, error) { + id, err := c.GetLastGameID(ctx) + if err != nil { + return nil, err + } + return c.GetMatchDetail(ctx, id) +} diff --git a/pkg/lcu/process.go b/pkg/lcu/process.go new file mode 100644 index 0000000..7b50e8f --- /dev/null +++ b/pkg/lcu/process.go @@ -0,0 +1,95 @@ +package lcu + +import ( + "context" + "fmt" + "os/exec" + "runtime" + "strings" + "time" +) + +const ( + leagueClientProcessName = "LeagueClient.exe" + leagueClientMacProcess = "LeagueClient" +) + +// IsClientRunning returns true if the League of Legends client process is active. +func IsClientRunning() (bool, error) { + switch runtime.GOOS { + case "windows": + return isProcessRunningWindows(leagueClientProcessName) + case "darwin": + return isProcessRunningUnix(leagueClientMacProcess) + default: + return isProcessRunningUnix(leagueClientMacProcess) + } +} + +func isProcessRunningWindows(processName string) (bool, error) { + out, err := exec.Command("tasklist", "/FI", "IMAGENAME eq "+processName, "/NH").Output() + if err != nil { + return false, fmt.Errorf("tasklist error: %w", err) + } + return strings.Contains(string(out), processName), nil +} + +func isProcessRunningUnix(processName string) (bool, error) { + out, err := exec.Command("pgrep", "-x", processName).Output() + if err != nil { + // pgrep exits with code 1 when no match — not a hard error + return false, nil + } + return len(strings.TrimSpace(string(out))) > 0, nil +} + +// WaitForClient blocks until the LoL client process is detected or the context +// is cancelled. It polls every pollInterval. +// +// 예) WaitForClient(ctx, 2*time.Second) → 클라이언트가 뜰 때까지 2초마다 확인 +func WaitForClient(ctx context.Context, pollInterval time.Duration) error { + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + running, err := IsClientRunning() + if err != nil { + return fmt.Errorf("WaitForClient: process check failed: %w", err) + } + if running { + return nil + } + } + } +} + +// WaitForClientAndConnect polls until the LoL client is running, then reads +// the lockfile and returns a ready-to-use Client. +// +// 클라이언트 실행부터 lockfile 생성까지 약간의 딜레이가 있으므로 +// lockfile 읽기를 재시도합니다. +func WaitForClientAndConnect(ctx context.Context, pollInterval time.Duration) (*Client, error) { + if err := WaitForClient(ctx, pollInterval); err != nil { + return nil, err + } + + // lockfile이 생성될 때까지 잠시 대기 후 재시도 + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ticker.C: + client, err := NewClientFromLockfile() + if err == nil { + return client, nil + } + } + } +} diff --git a/pkg/lcu/session.go b/pkg/lcu/session.go new file mode 100644 index 0000000..2f8fa56 --- /dev/null +++ b/pkg/lcu/session.go @@ -0,0 +1,125 @@ +package lcu + +import ( + "context" + "fmt" +) + +// ---- 게임 세션 (Gameflow) -------------------------------------------------- + +// GameflowPhase represents the current phase of the LoL client. +type GameflowPhase string + +const ( + PhaseNone GameflowPhase = "None" + PhaseWaitingForStats GameflowPhase = "WaitingForStats" + PhasePreEndOfGame GameflowPhase = "PreEndOfGame" + PhaseEndOfGame GameflowPhase = "EndOfGame" + PhaseChampionSelect GameflowPhase = "ChampSelect" + PhaseGameStart GameflowPhase = "GameStart" + PhaseInProgress GameflowPhase = "InProgress" + PhaseMatchmaking GameflowPhase = "Matchmaking" + PhaseReadyCheck GameflowPhase = "ReadyCheck" + PhaseLobby GameflowPhase = "Lobby" + PhaseReconnect GameflowPhase = "Reconnect" + PhaseTerminatedInError GameflowPhase = "TerminatedInError" +) + +// GameflowSession is the full game session object from /lol-gameflow/v1/session. +type GameflowSession struct { + Phase GameflowPhase `json:"phase"` + GameClient GameClient `json:"gameClient"` + GameData GameData `json:"gameData"` + Map GameMap `json:"map"` +} + +type GameClient struct { + Running bool `json:"running"` + ServerIP string `json:"serverIp"` + ServerPort int `json:"serverPort"` + Visible bool `json:"visible"` +} + +type GameData struct { + GameID int64 `json:"gameId"` + GameMode string `json:"gameMode"` // CLASSIC, ARAM 등 + GameType string `json:"gameType"` + MapID int `json:"mapId"` + Queue Queue `json:"queue"` + PlayerChampionSelections []ChampionSelection `json:"playerChampionSelections"` + TeamOne []Participant `json:"teamOne"` + TeamTwo []Participant `json:"teamTwo"` +} + +type Queue struct { + ID int `json:"id"` + Name string `json:"name"` + IsRanked bool `json:"isRanked"` + Type string `json:"type"` + GameMode string `json:"gameMode"` +} + +type ChampionSelection struct { + SummonerInternalName string `json:"summonerInternalName"` + ChampionID int `json:"championId"` + SelectedSkinIndex int `json:"selectedSkinIndex"` + Ward int `json:"ward"` + Spell1ID int `json:"spell1Id"` + Spell2ID int `json:"spell2Id"` +} + +type Participant struct { + PUUID string `json:"puuid"` + SummonerName string `json:"summonerName"` + SummonerID string `json:"summonerId"` + TeamID int `json:"teamId"` +} + +type GameMap struct { + ID int `json:"id"` + Name string `json:"name"` +} + +// ---- 현재 소환사 ----------------------------------------------------------- + +// CurrentSummoner holds basic info about the logged-in summoner. +type CurrentSummoner struct { + AccountID int64 `json:"accountId"` + DisplayName string `json:"displayName"` + InternalName string `json:"internalName"` + PUUID string `json:"puuid"` + SummonerID int64 `json:"summonerId"` + SummonerLevel int `json:"summonerLevel"` +} + +// ---- API Methods ----------------------------------------------------------- + +// GetGameflowPhase returns only the phase string. +// /lol-gameflow/v1/gameflow-phase +func (c *Client) GetGameflowPhase(ctx context.Context) (GameflowPhase, error) { + var phase GameflowPhase + if err := c.Get(ctx, "/lol-gameflow/v1/gameflow-phase", &phase); err != nil { + return "", fmt.Errorf("GetGameflowPhase: %w", err) + } + return phase, nil +} + +// GetGameflowSession returns the full game session. +// /lol-gameflow/v1/session +func (c *Client) GetGameflowSession(ctx context.Context) (*GameflowSession, error) { + var session GameflowSession + if err := c.Get(ctx, "/lol-gameflow/v1/session", &session); err != nil { + return nil, fmt.Errorf("GetGameflowSession: %w", err) + } + return &session, nil +} + +// GetCurrentSummoner returns info about the currently logged-in summoner. +// /lol-summoner/v1/current-summoner +func (c *Client) GetCurrentSummoner(ctx context.Context) (*CurrentSummoner, error) { + var s CurrentSummoner + if err := c.Get(ctx, "/lol-summoner/v1/current-summoner", &s); err != nil { + return nil, fmt.Errorf("GetCurrentSummoner: %w", err) + } + return &s, nil +} From 49811c38d50fdc9c6d7a6b32538e3bf6354cce57 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:12:29 +0900 Subject: [PATCH 18/24] =?UTF-8?q?fix:=20Address=20PR=20#80=20code=20review?= =?UTF-8?q?=20=E2=80=94=20pkg/lcu=20robustness=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - client.go: Add functional options pattern (ClientOption/WithTimeout/WithTLSRootCA) for TLS and timeout customisation; store tlsConfig on Client for WebSocket reuse - client.go: Handle io.ReadAll errors explicitly in Get() and RawGet() - event.go: Log WebSocket Close() error instead of silently discarding it - event.go: Share Client.tlsConfig with WebSocket dialer (remove duplicate InsecureSkipVerify) - event.go: Handle json.Marshal error for WAMP subscribe message Co-Authored-By: Claude Sonnet 4.6 --- pkg/lcu/client.go | 99 ++++++++++++++++++++++++++++++++++++++--------- pkg/lcu/event.go | 21 ++++++---- 2 files changed, 94 insertions(+), 26 deletions(-) diff --git a/pkg/lcu/client.go b/pkg/lcu/client.go index c07a0aa..a2ffc9d 100644 --- a/pkg/lcu/client.go +++ b/pkg/lcu/client.go @@ -3,6 +3,7 @@ package lcu import ( "context" "crypto/tls" + "crypto/x509" "encoding/base64" "encoding/json" "fmt" @@ -16,53 +17,108 @@ const ( defaultTimeout = 10 * time.Second ) +// clientConfig holds options applied when constructing a Client. +type clientConfig struct { + timeout time.Duration + tlsConfig *tls.Config +} + +func defaultClientConfig() *clientConfig { + // LCU uses a self-signed certificate that is unique per installation and + // cannot be embedded at compile time. All LCU traffic is bound to + // 127.0.0.1 (localhost), which means a man-in-the-middle attack would + // already require code execution on the host machine — at which point TLS + // provides no additional protection. InsecureSkipVerify is the accepted + // practice in the LCU developer community for this reason. + // + // Callers who can supply the PEM-encoded root CA (e.g. read from + // system.yaml shipped with the LoL client) should use WithTLSRootCA to + // avoid skipping verification entirely. + return &clientConfig{ + timeout: defaultTimeout, + tlsConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // see comment above + } +} + +// ClientOption configures a Client at construction time. +type ClientOption func(*clientConfig) + +// WithTimeout overrides the default HTTP request timeout (10 s). +func WithTimeout(d time.Duration) ClientOption { + return func(cfg *clientConfig) { cfg.timeout = d } +} + +// WithTLSRootCA provides a PEM-encoded root CA certificate so the LCU server's +// TLS certificate can be verified without disabling InsecureSkipVerify. +// +// Usage: +// +// pemData, _ := os.ReadFile("/path/to/riotgames.pem") +// opt, err := lcu.WithTLSRootCA(pemData) +// client := lcu.NewClient(lf, opt) +func WithTLSRootCA(pemData []byte) (ClientOption, error) { + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(pemData) { + return nil, fmt.Errorf("lcu: WithTLSRootCA: no valid PEM certificate found in provided data") + } + return func(cfg *clientConfig) { + cfg.tlsConfig = &tls.Config{ + RootCAs: pool, + MinVersion: tls.VersionTLS12, + } + }, nil +} + +// ---- Client ---------------------------------------------------------------- + // Client is the LCU HTTP client. -// LCU는 로컬 자체 서명 인증서를 사용하므로 InsecureSkipVerify가 필수입니다. type Client struct { - http *http.Client - baseURL string + http *http.Client + baseURL string authHeader string + tlsConfig *tls.Config // shared with Subscribe's WebSocket dialer } // NewClient creates a Client from a pre-parsed LockfileData. -func NewClient(lf *LockfileData) *Client { - return newClient(lf.Port, lf.Password) +func NewClient(lf *LockfileData, opts ...ClientOption) *Client { + return newClient(lf.Port, lf.Password, opts...) } // NewClientFromLockfile reads the lockfile automatically and builds a Client. -func NewClientFromLockfile() (*Client, error) { +func NewClientFromLockfile(opts ...ClientOption) (*Client, error) { lf, err := ReadLockfile() if err != nil { return nil, err } - return NewClient(lf), nil + return NewClient(lf, opts...), nil } // NewClientFromPath reads the lockfile from the given path and builds a Client. -func NewClientFromPath(lockfilePath string) (*Client, error) { +func NewClientFromPath(lockfilePath string, opts ...ClientOption) (*Client, error) { lf, err := ReadLockfileFromPath(lockfilePath) if err != nil { return nil, err } - return NewClient(lf), nil + return NewClient(lf, opts...), nil } -func newClient(port int, password string) *Client { - // LCU는 자체 서명 인증서를 사용하므로 SSL 검증을 무시합니다. - transport := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec +func newClient(port int, password string, opts ...ClientOption) *Client { + cfg := defaultClientConfig() + for _, o := range opts { + o(cfg) } - // Authorization: Basic base64("riot:{password}") + transport := &http.Transport{TLSClientConfig: cfg.tlsConfig} cred := base64.StdEncoding.EncodeToString([]byte(lcuUsername + ":" + password)) return &Client{ http: &http.Client{ Transport: transport, - Timeout: defaultTimeout, + Timeout: cfg.timeout, }, baseURL: fmt.Sprintf("https://127.0.0.1:%d", port), authHeader: "Basic " + cred, + tlsConfig: cfg.tlsConfig, } } @@ -84,7 +140,11 @@ func (c *Client) Get(ctx context.Context, path string, dest any) error { defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { - body, _ := io.ReadAll(resp.Body) + body, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return fmt.Errorf("lcu: %s %s returned %d (could not read body: %w)", + req.Method, path, resp.StatusCode, readErr) + } return fmt.Errorf("lcu: %s %s returned %d: %s", req.Method, path, resp.StatusCode, body) } @@ -111,8 +171,11 @@ func (c *Client) RawGet(ctx context.Context, path string) ([]byte, int, error) { } defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - return body, resp.StatusCode, err + body, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return nil, resp.StatusCode, fmt.Errorf("lcu: read response body: %w", readErr) + } + return body, resp.StatusCode, nil } func (c *Client) setCommonHeaders(req *http.Request) { diff --git a/pkg/lcu/event.go b/pkg/lcu/event.go index 8b2526f..420b011 100644 --- a/pkg/lcu/event.go +++ b/pkg/lcu/event.go @@ -2,9 +2,9 @@ package lcu import ( "context" - "crypto/tls" "encoding/json" "fmt" + "log" "net/http" "strings" @@ -44,7 +44,9 @@ type EventSubscription struct { // Close terminates the WebSocket connection and stops the listener goroutine. func (s *EventSubscription) Close() { s.cancel() - _ = s.conn.Close() + if err := s.conn.Close(); err != nil { + log.Printf("lcu: EventSubscription.Close: %v", err) + } } // Subscribe connects to the LCU WebSocket, subscribes to the given endpoint events, @@ -60,13 +62,12 @@ func (c *Client) Subscribe( handler EventHandler, uriPatterns ...string, ) (*EventSubscription, error) { - wsURL := "wss://127.0.0.1" + strings.TrimPrefix(c.baseURL, "https://127.0.0.1") - // baseURL already contains the port, e.g. https://127.0.0.1:62000 - // Replace scheme only - wsURL = strings.Replace(c.baseURL, "https://", "wss://", 1) + "/" + // baseURL already contains the port, e.g. https://127.0.0.1:62000 — replace scheme only. + wsURL := strings.Replace(c.baseURL, "https://", "wss://", 1) + "/" + // Reuse the same TLS configuration as the HTTP client (set via ClientOption). dialer := websocket.Dialer{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec + TLSClientConfig: c.tlsConfig, } headers := http.Header{} @@ -90,7 +91,11 @@ func (c *Client) Subscribe( } for _, name := range eventNames { - msg, _ := json.Marshal([]any{OpcodeSubscribe, name}) + msg, err := json.Marshal([]any{OpcodeSubscribe, name}) + if err != nil { + _ = conn.Close() + return nil, fmt.Errorf("lcu: marshal subscribe message for %q: %w", name, err) + } if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil { _ = conn.Close() return nil, fmt.Errorf("lcu: subscribe to %q: %w", name, err) From 980bd24b28f6b45df1ad28530cb1e6d9947b1f50 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:21:36 +0900 Subject: [PATCH 19/24] fix: Address PR #80 second code review batch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - match.go: Fix Participants field type []Participant → []GameParticipant to match the actual LCU match history API response structure - process.go: Replace shell-out (tasklist/pgrep) with gopsutil/v4/process for cross-platform process detection without external command dependency - gcs_storage_adapter.go: Fix potential double-slash in GetPublicURL by trimming trailing slash from publicURL before concatenation Co-Authored-By: Claude Sonnet 4.6 --- internal/storage/infra/gcs_storage_adapter.go | 3 +- pkg/lcu/match.go | 2 +- pkg/lcu/process.go | 38 ++++++++++--------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/internal/storage/infra/gcs_storage_adapter.go b/internal/storage/infra/gcs_storage_adapter.go index 326f73b..55aaa32 100644 --- a/internal/storage/infra/gcs_storage_adapter.go +++ b/internal/storage/infra/gcs_storage_adapter.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "strings" "cloud.google.com/go/storage" "github.com/FOR-GAMERS/GAMERS-BE/internal/storage/application/port" @@ -77,5 +78,5 @@ func (a *GCSStorageAdapter) Delete(ctx context.Context, key string) error { } func (a *GCSStorageAdapter) GetPublicURL(key string) string { - return fmt.Sprintf("%s/%s", a.publicURL, key) + return strings.TrimSuffix(a.publicURL, "/") + "/" + key } diff --git a/pkg/lcu/match.go b/pkg/lcu/match.go index b99877f..142e8a4 100644 --- a/pkg/lcu/match.go +++ b/pkg/lcu/match.go @@ -29,7 +29,7 @@ type Game struct { MapID int `json:"mapId"` QueueID int `json:"queueId"` SeasonID int `json:"seasonId"` - Participants []Participant `json:"participants"` + Participants []GameParticipant `json:"participants"` ParticipantIdentities []ParticipantIdentity `json:"participantIdentities"` Teams []Team `json:"teams"` } diff --git a/pkg/lcu/process.go b/pkg/lcu/process.go index 7b50e8f..3b00348 100644 --- a/pkg/lcu/process.go +++ b/pkg/lcu/process.go @@ -3,10 +3,10 @@ package lcu import ( "context" "fmt" - "os/exec" "runtime" - "strings" "time" + + "github.com/shirou/gopsutil/v4/process" ) const ( @@ -15,32 +15,34 @@ const ( ) // IsClientRunning returns true if the League of Legends client process is active. +// It uses gopsutil for cross-platform process enumeration without shelling out. func IsClientRunning() (bool, error) { + var target string switch runtime.GOOS { case "windows": - return isProcessRunningWindows(leagueClientProcessName) - case "darwin": - return isProcessRunningUnix(leagueClientMacProcess) + target = leagueClientProcessName + case "darwin", "linux": + target = leagueClientMacProcess default: - return isProcessRunningUnix(leagueClientMacProcess) + return false, fmt.Errorf("lcu: unsupported OS for process check: %s", runtime.GOOS) } -} -func isProcessRunningWindows(processName string) (bool, error) { - out, err := exec.Command("tasklist", "/FI", "IMAGENAME eq "+processName, "/NH").Output() + procs, err := process.Processes() if err != nil { - return false, fmt.Errorf("tasklist error: %w", err) + return false, fmt.Errorf("lcu: failed to list processes: %w", err) } - return strings.Contains(string(out), processName), nil -} -func isProcessRunningUnix(processName string) (bool, error) { - out, err := exec.Command("pgrep", "-x", processName).Output() - if err != nil { - // pgrep exits with code 1 when no match — not a hard error - return false, nil + for _, p := range procs { + name, err := p.Name() + if err != nil { + // Some processes may be inaccessible due to permissions — skip them. + continue + } + if name == target { + return true, nil + } } - return len(strings.TrimSpace(string(out))) > 0, nil + return false, nil } // WaitForClient blocks until the LoL client process is detected or the context From f6c86e67d01cbd99df930485a53430742bf5c1d7 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:59:40 +0900 Subject: [PATCH 20/24] fix: Resolve build errors in contest application service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - exception: Add ErrContestStartTimePassed (CT038) - port: Add Point, CurrentTier, PeakTier fields to SenderSnapshot to capture tier info at the time of application - service: Fix NewContestMember call — pass point from sender snapshot, defaulting to 0 if no application snapshot is available Co-Authored-By: Claude Sonnet 4.6 --- .../contest/application/contest_application_service.go | 9 +++++++-- .../application/port/contest_application_redis_port.go | 3 +++ internal/global/exception/contest_error_status.go | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/contest/application/contest_application_service.go b/internal/contest/application/contest_application_service.go index 9c6f3e0..dc2018d 100644 --- a/internal/contest/application/contest_application_service.go +++ b/internal/contest/application/contest_application_service.go @@ -188,9 +188,14 @@ func (s *ContestApplicationService) AcceptApplication(ctx context.Context, conte return err } - member := domain.NewContestMember(userId, contestId, domain.MemberTypeNormal, domain.LeaderTypeMember) + // Restore point, valorant roles and description from the application sender snapshot + point := 0 + if appErr == nil && application != nil && application.Sender != nil { + point = application.Sender.Point + } + + member := domain.NewContestMember(userId, contestId, domain.MemberTypeNormal, domain.LeaderTypeMember, point) - // Copy valorant roles and description from application sender snapshot if appErr == nil && application != nil && application.Sender != nil { member.ValorantRoles = application.Sender.ValorantRoles member.Description = application.Sender.Description diff --git a/internal/contest/application/port/contest_application_redis_port.go b/internal/contest/application/port/contest_application_redis_port.go index 340684d..7f24f06 100644 --- a/internal/contest/application/port/contest_application_redis_port.go +++ b/internal/contest/application/port/contest_application_redis_port.go @@ -21,6 +21,9 @@ type SenderSnapshot struct { Username string `json:"username"` Tag string `json:"tag"` Avatar string `json:"avatar,omitempty"` + Point int `json:"point,omitempty"` + CurrentTier string `json:"current_tier,omitempty"` + PeakTier string `json:"peak_tier,omitempty"` ValorantRoles domain.ValorantRoles `json:"valorant_roles,omitempty"` Description string `json:"description,omitempty"` } diff --git a/internal/global/exception/contest_error_status.go b/internal/global/exception/contest_error_status.go index 13f70fe..51f58a7 100644 --- a/internal/global/exception/contest_error_status.go +++ b/internal/global/exception/contest_error_status.go @@ -41,4 +41,5 @@ var ( ErrInvalidValorantRole = NewBadRequestError("invalid valorant role", "CT035") ErrDescriptionTooLong = NewBadRequestError("description must be at most 64 characters", "CT036") ErrDuplicateValorantRole = NewBadRequestError("duplicate valorant role", "CT037") + ErrContestStartTimePassed = NewBadRequestError("contest start time has already passed", "CT038") ) From 63ec28099c72fb8a2b600224e2c890448c747aa6 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:03:01 +0900 Subject: [PATCH 21/24] =?UTF-8?q?feat:=20LoL=20Temporal=20Contest=20?= =?UTF-8?q?=ED=8C=80=20=EB=B0=B8=EB=9F=B0=EC=8B=B1=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /api/contests/lol/temporal 엔드포인트 추가. 5개 라인(TOP/JG/MID/ADC/SUP)에 각 2명 플레이어 랭크 기반으로 2^5=32 완전 탐색으로 팀 점수 차를 최소화하는 5v5 팀 편성 반환. DB 저장 없는 순수 연산(Temporal) 처리. Co-Authored-By: Claude Sonnet 4.6 --- .../contest/application/contest_service.go | 63 +++++++++++++ .../application/dto/lol_temporal_dto.go | 91 +++++++++++++++++++ .../presentation/contest_controller.go | 24 +++++ .../global/exception/contest_error_status.go | 4 + 4 files changed, 182 insertions(+) create mode 100644 internal/contest/application/dto/lol_temporal_dto.go diff --git a/internal/contest/application/contest_service.go b/internal/contest/application/contest_service.go index 45f0f90..d3bd513 100644 --- a/internal/contest/application/contest_service.go +++ b/internal/contest/application/contest_service.go @@ -529,3 +529,66 @@ func (c *ContestService) UploadThumbnail(ctx context.Context, contestId, userId UploadedAt: time.Now(), }, nil } + +// BalanceLolTemporalContest accepts 10 players (2 per lane) and returns a balanced +// 5v5 team assignment where the total rank score difference is minimised. +// No DB persistence — this is a stateless computation. +func (c *ContestService) BalanceLolTemporalContest(req *dto.LolTemporalContestRequest) (*dto.LolTemporalContestResponse, error) { + if err := req.Validate(); err != nil { + return nil, err + } + + lanes := []string{"TOP", "JG", "MID", "ADC", "SUP"} + + // Build score matrix: scores[i][0] and scores[i][1] for each lane. + scores := make([][2]int, len(lanes)) + for i, lane := range lanes { + players := req.Members[lane] + s0, err := dto.ParseLolRankScore(players[0].Rank) + if err != nil { + return nil, err + } + s1, err := dto.ParseLolRankScore(players[1].Rank) + if err != nil { + return nil, err + } + scores[i] = [2]int{s0, s1} + } + + // Exhaustive search over 2^5=32 combinations. + // Bit i=0: players[0] of lane i → Team1; bit i=1: players[1] → Team1. + bestMask, bestDiff := 0, int(^uint(0)>>1) + for mask := 0; mask < 32; mask++ { + t1, t2 := 0, 0 + for i := range lanes { + if (mask>>i)&1 == 0 { + t1 += scores[i][0] + t2 += scores[i][1] + } else { + t1 += scores[i][1] + t2 += scores[i][0] + } + } + diff := t1 - t2 + if diff < 0 { + diff = -diff + } + if diff < bestDiff { + bestDiff = diff + bestMask = mask + } + } + + // Reorder each lane's slice so index 0 = Team1, index 1 = Team2. + result := make(map[string][]*dto.LolTemporalMember, len(lanes)) + for i, lane := range lanes { + players := req.Members[lane] + if (bestMask>>i)&1 == 0 { + result[lane] = []*dto.LolTemporalMember{players[0], players[1]} + } else { + result[lane] = []*dto.LolTemporalMember{players[1], players[0]} + } + } + + return &dto.LolTemporalContestResponse{Members: result}, nil +} diff --git a/internal/contest/application/dto/lol_temporal_dto.go b/internal/contest/application/dto/lol_temporal_dto.go new file mode 100644 index 0000000..21357f1 --- /dev/null +++ b/internal/contest/application/dto/lol_temporal_dto.go @@ -0,0 +1,91 @@ +package dto + +import ( + "strings" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" +) + +var lolTemporalLanes = []string{"TOP", "JG", "MID", "ADC", "SUP"} + +// LolTemporalMember represents a single player with their lane assignment info. +type LolTemporalMember struct { + Username string `json:"username" binding:"required"` + Tag string `json:"tag" binding:"required"` + Rank string `json:"rank" binding:"required"` +} + +// LolTemporalContestRequest is the request body for temporal LoL team balancing. +// Each lane must have exactly 2 players. +type LolTemporalContestRequest struct { + Members map[string][]*LolTemporalMember `json:"members" binding:"required"` +} + +func (r *LolTemporalContestRequest) Validate() error { + for _, lane := range lolTemporalLanes { + players, ok := r.Members[lane] + if !ok || len(players) != 2 { + return exception.ErrLolTemporalInvalidLane + } + for _, p := range players { + if p.Username == "" || p.Tag == "" || p.Rank == "" { + return exception.ErrLolTemporalInvalidLane + } + } + } + return nil +} + +// LolTemporalContestResponse contains the balanced team assignment. +// For each lane, index 0 is Team1's player and index 1 is Team2's player. +type LolTemporalContestResponse struct { + Members map[string][]*LolTemporalMember `json:"members"` +} + +// ParseLolRankScore converts a LoL rank string (e.g. "GOLD II", "MASTER") to a numeric score. +// Scores: IRON IV=1 … IRON I=4, BRONZE IV=5 … DIAMOND I=28, MASTER=29, GM=30, CHALLENGER=31. +func ParseLolRankScore(rank string) (int, error) { + rank = strings.ToUpper(strings.TrimSpace(rank)) + + tierBase := map[string]int{ + "IRON": 0, + "BRONZE": 4, + "SILVER": 8, + "GOLD": 12, + "PLATINUM": 16, + "EMERALD": 20, + "DIAMOND": 24, + "MASTER": 29, + "GRANDMASTER": 30, + "CHALLENGER": 31, + } + divisionScore := map[string]int{ + "IV": 1, "III": 2, "II": 3, "I": 4, + } + + parts := strings.Fields(rank) + if len(parts) == 0 { + return 0, exception.ErrLolTemporalInvalidRank + } + + base, ok := tierBase[parts[0]] + if !ok { + return 0, exception.ErrLolTemporalInvalidRank + } + + // Master+ tiers have no division + if parts[0] == "MASTER" || parts[0] == "GRANDMASTER" || parts[0] == "CHALLENGER" { + return base, nil + } + + if len(parts) < 2 { + return 0, exception.ErrLolTemporalInvalidRank + } + + div, ok := divisionScore[parts[1]] + if !ok { + return 0, exception.ErrLolTemporalInvalidRank + } + + return base + div, nil +} diff --git a/internal/contest/presentation/contest_controller.go b/internal/contest/presentation/contest_controller.go index f886af3..e455644 100644 --- a/internal/contest/presentation/contest_controller.go +++ b/internal/contest/presentation/contest_controller.go @@ -40,6 +40,7 @@ func (c *ContestController) RegisterRoute() { privateGroup.POST("/:id/thumbnail", c.UploadThumbnail) privateGroup.POST("/:id/start", c.StartContest) privateGroup.POST("/:id/stop", c.StopContest) + privateGroup.POST("/lol/temporal", c.LolTemporalContest) publicGroup := c.router.PublicGroup("/api/contests") publicGroup.GET("", c.GetAllContests) @@ -358,6 +359,29 @@ func (c *ContestController) GetMyContests(ctx *gin.Context) { c.helper.RespondOK(ctx, paginationResp, nil, "my contests retrieved successfully") } +// LolTemporalContest godoc +// @Summary Balance teams for a temporary LoL custom game +// @Description Accepts 10 players (2 per lane: TOP/JG/MID/ADC/SUP) with their ranks and returns a balanced 5v5 team assignment. No data is persisted. +// @Tags contests +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body dto.LolTemporalContestRequest true "Players by lane" +// @Success 201 {object} response.Response{data=dto.LolTemporalContestResponse} +// @Failure 400 {object} response.Response +// @Failure 401 {object} response.Response +// @Router /api/contests/lol/temporal [post] +func (c *ContestController) LolTemporalContest(ctx *gin.Context) { + var req dto.LolTemporalContestRequest + + if !c.helper.BindJSON(ctx, &req) { + return + } + + result, err := c.service.BalanceLolTemporalContest(&req) + c.helper.RespondCreated(ctx, result, err, "teams balanced successfully") +} + // ==================== Testable Handler Functions ==================== // These functions are exposed for unit testing without router dependency diff --git a/internal/global/exception/contest_error_status.go b/internal/global/exception/contest_error_status.go index 51f58a7..1d739b2 100644 --- a/internal/global/exception/contest_error_status.go +++ b/internal/global/exception/contest_error_status.go @@ -42,4 +42,8 @@ var ( ErrDescriptionTooLong = NewBadRequestError("description must be at most 64 characters", "CT036") ErrDuplicateValorantRole = NewBadRequestError("duplicate valorant role", "CT037") ErrContestStartTimePassed = NewBadRequestError("contest start time has already passed", "CT038") + + // LoL Temporal Contest errors + ErrLolTemporalInvalidLane = NewBadRequestError("all lanes (TOP, JG, MID, ADC, SUP) must be provided with exactly 2 players each", "CT046") + ErrLolTemporalInvalidRank = NewBadRequestError("invalid lol rank format, expected e.g. GOLD II or MASTER", "CT047") ) From 62d9f97a5c6e5a54d4efeb8fd2f350886345b9f1 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:11:16 +0900 Subject: [PATCH 22/24] =?UTF-8?q?test:=20LoL=20Temporal=20Contest=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ParseLolRankScore: 전 티어/디비전 + 에러 케이스 (5개) - BalanceLolTemporalContest: 정상/밸런싱 검증/에러 케이스 (7개) - HandleLolTemporalContest: HTTP 201/400 케이스 (5개) - 기존 RequestParticipate 시그니처 변경에 따른 테스트 수정 Co-Authored-By: Claude Sonnet 4.6 --- .../presentation/contest_controller.go | 11 + .../contest_application_service_test.go | 17 +- .../application/lol_temporal_service_test.go | 212 ++++++++++++++++++ .../lol_temporal_controller_test.go | 190 ++++++++++++++++ 4 files changed, 422 insertions(+), 8 deletions(-) create mode 100644 test/contest/application/lol_temporal_service_test.go create mode 100644 test/contest/presentation/lol_temporal_controller_test.go diff --git a/internal/contest/presentation/contest_controller.go b/internal/contest/presentation/contest_controller.go index e455644..bc23ea3 100644 --- a/internal/contest/presentation/contest_controller.go +++ b/internal/contest/presentation/contest_controller.go @@ -506,3 +506,14 @@ func HandleUploadThumbnail(ctx *gin.Context, service *application.ContestService result, err := service.UploadThumbnail(ctx.Request.Context(), id, userId, file) helper.RespondCreated(ctx, result, err, "thumbnail uploaded successfully") } + +func HandleLolTemporalContest(ctx *gin.Context, service *application.ContestService, helper *handler.ControllerHelper) { + var req dto.LolTemporalContestRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + response.JSON(ctx, response.BadRequest(err.Error())) + return + } + + result, err := service.BalanceLolTemporalContest(&req) + helper.RespondCreated(ctx, result, err, "teams balanced successfully") +} diff --git a/test/contest/application/contest_application_service_test.go b/test/contest/application/contest_application_service_test.go index 43e3f50..e75568e 100644 --- a/test/contest/application/contest_application_service_test.go +++ b/test/contest/application/contest_application_service_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application/dto" "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application/port" "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/domain" "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" @@ -240,7 +241,7 @@ func TestRequestParticipate_WithScoreTable_CalculatesPoint(t *testing.T) { mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) // When - _, err := service.RequestParticipate(ctx, contestId, userId) + _, err := service.RequestParticipate(ctx, contestId, userId, &dto.RequestParticipateRequest{}) // Then assert.NoError(t, err) @@ -283,7 +284,7 @@ func TestRequestParticipate_WithScoreTable_SameTier_CalculatesExactPoint(t *test mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) // When - _, err := service.RequestParticipate(ctx, contestId, userId) + _, err := service.RequestParticipate(ctx, contestId, userId, &dto.RequestParticipateRequest{}) // Then assert.NoError(t, err) @@ -322,7 +323,7 @@ func TestRequestParticipate_WithoutScoreTable_PointIsZero(t *testing.T) { mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) // When - _, err := service.RequestParticipate(ctx, contestId, userId) + _, err := service.RequestParticipate(ctx, contestId, userId, &dto.RequestParticipateRequest{}) // Then assert.NoError(t, err) @@ -365,7 +366,7 @@ func TestRequestParticipate_ValorantNotLinked_PointIsZero(t *testing.T) { mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) // When - _, err := service.RequestParticipate(ctx, contestId, userId) + _, err := service.RequestParticipate(ctx, contestId, userId, &dto.RequestParticipateRequest{}) // Then assert.NoError(t, err) @@ -401,7 +402,7 @@ func TestRequestParticipate_ScoreTableNotFound_ReturnsError(t *testing.T) { mockScoreTable.On("GetByID", scoreTableId).Return(nil, exception.ErrScoreTableNotFound) // When - _, err := service.RequestParticipate(ctx, contestId, userId) + _, err := service.RequestParticipate(ctx, contestId, userId, &dto.RequestParticipateRequest{}) // Then: error is propagated — no silent failure assert.Error(t, err) @@ -441,7 +442,7 @@ func TestRequestParticipate_NoScoreTablePortSet_PointIsZero(t *testing.T) { mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) // When - _, err := service.RequestParticipate(ctx, contestId, userId) + _, err := service.RequestParticipate(ctx, contestId, userId, &dto.RequestParticipateRequest{}) // Then assert.NoError(t, err) @@ -713,7 +714,7 @@ func TestRequestParticipate_RadiantUser_MaxPoint(t *testing.T) { }), mock.AnythingOfType("time.Duration")).Return(nil) mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) - _, err := service.RequestParticipate(ctx, contestId, userId) + _, err := service.RequestParticipate(ctx, contestId, userId, &dto.RequestParticipateRequest{}) assert.NoError(t, err) mockRedis.AssertExpectations(t) @@ -752,7 +753,7 @@ func TestRequestParticipate_Iron1User_MinPoint(t *testing.T) { }), mock.AnythingOfType("time.Duration")).Return(nil) mockEventPub.On("PublishContestApplicationEvent", mock.Anything, mock.Anything).Return(nil) - _, err := service.RequestParticipate(ctx, contestId, userId) + _, err := service.RequestParticipate(ctx, contestId, userId, &dto.RequestParticipateRequest{}) assert.NoError(t, err) mockRedis.AssertExpectations(t) diff --git a/test/contest/application/lol_temporal_service_test.go b/test/contest/application/lol_temporal_service_test.go new file mode 100644 index 0000000..8d0c841 --- /dev/null +++ b/test/contest/application/lol_temporal_service_test.go @@ -0,0 +1,212 @@ +package application_test + +import ( + "testing" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application/dto" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" + "github.com/stretchr/testify/assert" +) + +// ==================== Helpers ==================== + +func newService() *application.ContestService { + return application.NewContestService(nil, nil, nil, nil, nil) +} + +func player(username, tag, rank string) *dto.LolTemporalMember { + return &dto.LolTemporalMember{Username: username, Tag: tag, Rank: rank} +} + +func validRequest() *dto.LolTemporalContestRequest { + return &dto.LolTemporalContestRequest{ + Members: map[string][]*dto.LolTemporalMember{ + "TOP": {player("TopA", "KR1", "GOLD I"), player("TopB", "KR1", "GOLD III")}, + "JG": {player("JgA", "KR1", "PLATINUM IV"), player("JgB", "KR1", "SILVER I")}, + "MID": {player("MidA", "KR1", "DIAMOND IV"), player("MidB", "KR1", "EMERALD II")}, + "ADC": {player("AdcA", "KR1", "GOLD II"), player("AdcB", "KR1", "SILVER IV")}, + "SUP": {player("SupA", "KR1", "BRONZE I"), player("SupB", "KR1", "IRON I")}, + }, + } +} + +// ==================== ParseLolRankScore Tests ==================== + +func TestParseRankScore_AllDivisions(t *testing.T) { + cases := []struct { + rank string + expected int + }{ + {"IRON IV", 1}, {"IRON III", 2}, {"IRON II", 3}, {"IRON I", 4}, + {"BRONZE IV", 5}, {"BRONZE III", 6}, {"BRONZE II", 7}, {"BRONZE I", 8}, + {"SILVER IV", 9}, {"SILVER III", 10}, {"SILVER II", 11}, {"SILVER I", 12}, + {"GOLD IV", 13}, {"GOLD III", 14}, {"GOLD II", 15}, {"GOLD I", 16}, + {"PLATINUM IV", 17}, {"PLATINUM III", 18}, {"PLATINUM II", 19}, {"PLATINUM I", 20}, + {"EMERALD IV", 21}, {"EMERALD III", 22}, {"EMERALD II", 23}, {"EMERALD I", 24}, + {"DIAMOND IV", 25}, {"DIAMOND III", 26}, {"DIAMOND II", 27}, {"DIAMOND I", 28}, + {"MASTER", 29}, {"GRANDMASTER", 30}, {"CHALLENGER", 31}, + } + + for _, tc := range cases { + score, err := dto.ParseLolRankScore(tc.rank) + assert.NoError(t, err, "rank=%s", tc.rank) + assert.Equal(t, tc.expected, score, "rank=%s", tc.rank) + } +} + +func TestParseRankScore_LowercaseInput(t *testing.T) { + score, err := dto.ParseLolRankScore("gold ii") + assert.NoError(t, err) + assert.Equal(t, 15, score) +} + +func TestParseRankScore_InvalidTier(t *testing.T) { + _, err := dto.ParseLolRankScore("UNKNOWN I") + assert.ErrorIs(t, err, exception.ErrLolTemporalInvalidRank) +} + +func TestParseRankScore_MissingDivision(t *testing.T) { + _, err := dto.ParseLolRankScore("GOLD") + assert.ErrorIs(t, err, exception.ErrLolTemporalInvalidRank) +} + +func TestParseRankScore_EmptyString(t *testing.T) { + _, err := dto.ParseLolRankScore("") + assert.ErrorIs(t, err, exception.ErrLolTemporalInvalidRank) +} + +// ==================== BalanceLolTemporalContest Tests ==================== + +func TestBalanceLolTemporalContest_Success(t *testing.T) { + svc := newService() + req := validRequest() + + resp, err := svc.BalanceLolTemporalContest(req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + + for _, lane := range []string{"TOP", "JG", "MID", "ADC", "SUP"} { + players, ok := resp.Members[lane] + assert.True(t, ok, "lane %s must exist in response", lane) + assert.Len(t, players, 2, "lane %s must have 2 players", lane) + assert.NotNil(t, players[0]) + assert.NotNil(t, players[1]) + } +} + +func TestBalanceLolTemporalContest_MinimizerTeamScoreDiff(t *testing.T) { + // All players same rank except one lane where one player is clearly better. + // GOLD II (15) vs IRON I (4) in TOP → optimal: put the better one in each team. + svc := newService() + req := &dto.LolTemporalContestRequest{ + Members: map[string][]*dto.LolTemporalMember{ + // Same score per lane except TOP where diff=11 + "TOP": {player("TopA", "KR1", "GOLD II"), player("TopB", "KR1", "IRON I")}, + "JG": {player("JgA", "KR1", "SILVER I"), player("JgB", "KR1", "SILVER I")}, + "MID": {player("MidA", "KR1", "GOLD IV"), player("MidB", "KR1", "GOLD IV")}, + "ADC": {player("AdcA", "KR1", "PLATINUM IV"), player("AdcB", "KR1", "PLATINUM IV")}, + "SUP": {player("SupA", "KR1", "BRONZE II"), player("SupB", "KR1", "BRONZE II")}, + }, + } + + resp, err := svc.BalanceLolTemporalContest(req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + + // Team1 TOP and Team2 TOP should be different players (best possible split) + topTeam1 := resp.Members["TOP"][0] + topTeam2 := resp.Members["TOP"][1] + assert.NotEqual(t, topTeam1.Username, topTeam2.Username) +} + +func TestBalanceLolTemporalContest_SymmetricScores_ReturnsBothPlayers(t *testing.T) { + // All players have equal rank → any combination has diff=0, just ensure all 10 players appear. + svc := newService() + req := &dto.LolTemporalContestRequest{ + Members: map[string][]*dto.LolTemporalMember{ + "TOP": {player("T1", "KR1", "GOLD II"), player("T2", "KR1", "GOLD II")}, + "JG": {player("J1", "KR1", "GOLD II"), player("J2", "KR1", "GOLD II")}, + "MID": {player("M1", "KR1", "GOLD II"), player("M2", "KR1", "GOLD II")}, + "ADC": {player("A1", "KR1", "GOLD II"), player("A2", "KR1", "GOLD II")}, + "SUP": {player("S1", "KR1", "GOLD II"), player("S2", "KR1", "GOLD II")}, + }, + } + + resp, err := svc.BalanceLolTemporalContest(req) + + assert.NoError(t, err) + for _, lane := range []string{"TOP", "JG", "MID", "ADC", "SUP"} { + assert.Len(t, resp.Members[lane], 2) + assert.NotEqual(t, resp.Members[lane][0].Username, resp.Members[lane][1].Username) + } +} + +func TestBalanceLolTemporalContest_MissingLane(t *testing.T) { + svc := newService() + req := &dto.LolTemporalContestRequest{ + Members: map[string][]*dto.LolTemporalMember{ + "TOP": {player("T1", "KR1", "GOLD II"), player("T2", "KR1", "GOLD II")}, + // JG, MID, ADC, SUP missing + }, + } + + _, err := svc.BalanceLolTemporalContest(req) + + assert.ErrorIs(t, err, exception.ErrLolTemporalInvalidLane) +} + +func TestBalanceLolTemporalContest_LaneWithOnePlayer(t *testing.T) { + svc := newService() + req := &dto.LolTemporalContestRequest{ + Members: map[string][]*dto.LolTemporalMember{ + "TOP": {player("T1", "KR1", "GOLD II")}, // only 1 player instead of 2 + "JG": {player("J1", "KR1", "SILVER I"), player("J2", "KR1", "SILVER I")}, + "MID": {player("M1", "KR1", "GOLD IV"), player("M2", "KR1", "GOLD IV")}, + "ADC": {player("A1", "KR1", "GOLD IV"), player("A2", "KR1", "GOLD IV")}, + "SUP": {player("S1", "KR1", "BRONZE II"), player("S2", "KR1", "BRONZE II")}, + }, + } + + _, err := svc.BalanceLolTemporalContest(req) + + assert.ErrorIs(t, err, exception.ErrLolTemporalInvalidLane) +} + +func TestBalanceLolTemporalContest_InvalidRank(t *testing.T) { + svc := newService() + req := &dto.LolTemporalContestRequest{ + Members: map[string][]*dto.LolTemporalMember{ + "TOP": {player("T1", "KR1", "GRANDMASTER"), player("T2", "KR1", "INVALID_RANK")}, + "JG": {player("J1", "KR1", "SILVER I"), player("J2", "KR1", "SILVER I")}, + "MID": {player("M1", "KR1", "GOLD IV"), player("M2", "KR1", "GOLD IV")}, + "ADC": {player("A1", "KR1", "GOLD IV"), player("A2", "KR1", "GOLD IV")}, + "SUP": {player("S1", "KR1", "BRONZE II"), player("S2", "KR1", "BRONZE II")}, + }, + } + + _, err := svc.BalanceLolTemporalContest(req) + + assert.ErrorIs(t, err, exception.ErrLolTemporalInvalidRank) +} + +func TestBalanceLolTemporalContest_MasterAndChallengerTiers(t *testing.T) { + svc := newService() + req := &dto.LolTemporalContestRequest{ + Members: map[string][]*dto.LolTemporalMember{ + "TOP": {player("T1", "KR1", "CHALLENGER"), player("T2", "KR1", "GRANDMASTER")}, + "JG": {player("J1", "KR1", "MASTER"), player("J2", "KR1", "DIAMOND I")}, + "MID": {player("M1", "KR1", "CHALLENGER"), player("M2", "KR1", "MASTER")}, + "ADC": {player("A1", "KR1", "GRANDMASTER"), player("A2", "KR1", "DIAMOND II")}, + "SUP": {player("S1", "KR1", "MASTER"), player("S2", "KR1", "DIAMOND IV")}, + }, + } + + resp, err := svc.BalanceLolTemporalContest(req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Len(t, resp.Members["TOP"], 2) +} diff --git a/test/contest/presentation/lol_temporal_controller_test.go b/test/contest/presentation/lol_temporal_controller_test.go new file mode 100644 index 0000000..a998c15 --- /dev/null +++ b/test/contest/presentation/lol_temporal_controller_test.go @@ -0,0 +1,190 @@ +package presentation_test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/presentation" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/common/handler" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +// ==================== Helpers ==================== + +func newTemporalService() *application.ContestService { + return application.NewContestService(nil, nil, nil, nil, nil) +} + +func temporalBody(t *testing.T, body map[string]interface{}) *bytes.Buffer { + t.Helper() + b, err := json.Marshal(body) + assert.NoError(t, err) + return bytes.NewBuffer(b) +} + +func validTemporalBody() map[string]interface{} { + lane := func(r1, r2 string) []map[string]string { + return []map[string]string{ + {"username": "PlayerA", "tag": "KR1", "rank": r1}, + {"username": "PlayerB", "tag": "KR1", "rank": r2}, + } + } + return map[string]interface{}{ + "members": map[string]interface{}{ + "TOP": lane("GOLD I", "SILVER I"), + "JG": lane("PLATINUM IV", "GOLD IV"), + "MID": lane("DIAMOND IV", "EMERALD II"), + "ADC": lane("GOLD II", "SILVER II"), + "SUP": lane("BRONZE I", "IRON I"), + }, + } +} + +// ==================== Controller Tests ==================== + +func TestLolTemporalController_Success(t *testing.T) { + // Given + router := setupTestRouter() + svc := newTemporalService() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", temporalBody(t, validTemporalBody())) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusCreated, w.Code) + + var resp map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &resp) + assert.NoError(t, err) + assert.Equal(t, float64(201), resp["status"]) + + data, ok := resp["data"].(map[string]interface{}) + assert.True(t, ok) + members, ok := data["members"].(map[string]interface{}) + assert.True(t, ok) + + for _, lane := range []string{"TOP", "JG", "MID", "ADC", "SUP"} { + players, ok := members[lane].([]interface{}) + assert.True(t, ok, "lane %s should exist", lane) + assert.Len(t, players, 2, "lane %s should have 2 players", lane) + } +} + +func TestLolTemporalController_MissingLane(t *testing.T) { + // Given + router := setupTestRouter() + svc := newTemporalService() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + body := map[string]interface{}{ + "members": map[string]interface{}{ + "TOP": []map[string]string{ + {"username": "A", "tag": "KR1", "rank": "GOLD II"}, + {"username": "B", "tag": "KR1", "rank": "GOLD II"}, + }, + // JG, MID, ADC, SUP 누락 + }, + } + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", temporalBody(t, body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestLolTemporalController_InvalidRank(t *testing.T) { + // Given + router := setupTestRouter() + svc := newTemporalService() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + lane := func(r1, r2 string) []map[string]string { + return []map[string]string{ + {"username": "A", "tag": "KR1", "rank": r1}, + {"username": "B", "tag": "KR1", "rank": r2}, + } + } + body := map[string]interface{}{ + "members": map[string]interface{}{ + "TOP": lane("GOLD I", "NOT_A_RANK"), + "JG": lane("SILVER I", "SILVER I"), + "MID": lane("GOLD IV", "GOLD IV"), + "ADC": lane("GOLD IV", "GOLD IV"), + "SUP": lane("BRONZE II", "BRONZE II"), + }, + } + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", temporalBody(t, body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestLolTemporalController_InvalidJSON(t *testing.T) { + // Given + router := setupTestRouter() + svc := newTemporalService() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", bytes.NewBufferString("not json")) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestLolTemporalController_EmptyBody(t *testing.T) { + // Given + router := setupTestRouter() + svc := newTemporalService() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + // When + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", bytes.NewBufferString("{}")) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + // Then + assert.Equal(t, http.StatusBadRequest, w.Code) +} From 3f007b0867ef69aa80c746b034a510015335391c Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:21:30 +0900 Subject: [PATCH 23/24] =?UTF-8?q?feat:=20LoL=20Temporal=20=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=EC=85=98=20=EC=84=A0=ED=98=B8=EB=8F=84=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=205:5=20=ED=8C=80=20=EB=A7=A4=EC=B9=AD=20=EC=95=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=EC=A6=98=20=EA=B5=AC=ED=98=84=20(#84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 라인별 2명 고정 방식 → 10명 자유 입력 + 포지션 선호도 기반으로 교체 - C(10,5)=252 × 5!×5!=14,400 조합 완전 탐색 (약 360만 케이스) - 3단계 필터링: 팀 MMR 밸런스 → 라인별 맞대결 편차 → 포지션 만족도 - 포지션 보정(Model A): 1순위 100% ~ 5순위 75% (6.25%씩 선형 감소) - 새 에러코드: ErrLolTemporalInvalidPlayerCount(CT048), ErrLolTemporalInvalidPositions(CT049) - 단위 테스트 13개 추가 (서비스 9개, 컨트롤러 8개) - Swagger 문서 갱신 Co-Authored-By: Claude Sonnet 4.6 --- docs/docs.go | 180 ++++++++++++- docs/swagger.json | 180 ++++++++++++- docs/swagger.yaml | 121 ++++++++- .../contest/application/contest_service.go | 46 ++++ .../application/dto/lol_temporal_dto.go | 64 +++++ .../application/lol_temporal_balance.go | 191 ++++++++++++++ .../presentation/contest_controller.go | 14 +- .../global/exception/contest_error_status.go | 6 +- .../lol_temporal_v2_service_test.go | 247 ++++++++++++++++++ .../lol_temporal_controller_test.go | 28 +- .../lol_temporal_v2_controller_test.go | 228 ++++++++++++++++ 11 files changed, 1271 insertions(+), 34 deletions(-) create mode 100644 internal/contest/application/lol_temporal_balance.go create mode 100644 test/contest/application/lol_temporal_v2_service_test.go create mode 100644 test/contest/presentation/lol_temporal_v2_controller_test.go diff --git a/docs/docs.go b/docs/docs.go index 988ded6..14d3a05 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -587,6 +587,69 @@ const docTemplate = `{ } } }, + "/api/contests/lol/temporal": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Accepts exactly 10 players each with their rank and position preferences (all 5 positions in priority order). Returns a balanced 5v5 team assignment using 3-step filtering: team MMR balance → lane matchup balance → position satisfaction. No data is persisted.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "contests" + ], + "summary": "Balance teams for a temporary LoL custom game", + "parameters": [ + { + "description": "10 players with position preferences", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalContestRequestV2" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalContestResponseV2" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + } + } + } + } + }, "/api/contests/me": { "get": { "security": [ @@ -725,7 +788,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Apply to participate in a contest", + "description": "Apply to participate in a contest with optional valorant roles and description", "consumes": [ "application/json" ], @@ -743,6 +806,14 @@ const docTemplate = `{ "name": "contestId", "in": "path", "required": true + }, + { + "description": "Participation request with optional valorant roles and description", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.RequestParticipateRequest" + } } ], "responses": { @@ -5838,6 +5909,98 @@ const docTemplate = `{ } } }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalAssignedPlayer": { + "type": "object", + "properties": { + "position": { + "type": "string" + }, + "position_preference": { + "description": "1 (most preferred) to 5 (least)", + "type": "integer" + }, + "rank": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalContestRequestV2": { + "type": "object", + "required": [ + "members" + ], + "properties": { + "members": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalPlayerV2" + } + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalContestResponseV2": { + "type": "object", + "properties": { + "team_a": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalAssignedPlayer" + } + }, + "team_b": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalAssignedPlayer" + } + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalPlayerV2": { + "type": "object", + "required": [ + "positions", + "rank", + "tag", + "username" + ], + "properties": { + "positions": { + "type": "array", + "items": { + "type": "string" + } + }, + "rank": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.RequestParticipateRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "valorant_roles": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ValorantRole" + } + } + } + }, "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ThumbnailUploadResponse": { "type": "object", "properties": { @@ -5991,6 +6154,21 @@ const docTemplate = `{ "MemberTypeNormal" ] }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ValorantRole": { + "type": "string", + "enum": [ + "DUELIST", + "INITIATOR", + "CONTROLLER", + "SENTINEL" + ], + "x-enum-varnames": [ + "ValorantRoleDuelist", + "ValorantRoleInitiator", + "ValorantRoleController", + "ValorantRoleSentinel" + ] + }, "github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordChannel": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 66148eb..69807a9 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -581,6 +581,69 @@ } } }, + "/api/contests/lol/temporal": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Accepts exactly 10 players each with their rank and position preferences (all 5 positions in priority order). Returns a balanced 5v5 team assignment using 3-step filtering: team MMR balance → lane matchup balance → position satisfaction. No data is persisted.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "contests" + ], + "summary": "Balance teams for a temporary LoL custom game", + "parameters": [ + { + "description": "10 players with position preferences", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalContestRequestV2" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalContestResponseV2" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response" + } + } + } + } + }, "/api/contests/me": { "get": { "security": [ @@ -719,7 +782,7 @@ "BearerAuth": [] } ], - "description": "Apply to participate in a contest", + "description": "Apply to participate in a contest with optional valorant roles and description", "consumes": [ "application/json" ], @@ -737,6 +800,14 @@ "name": "contestId", "in": "path", "required": true + }, + { + "description": "Participation request with optional valorant roles and description", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.RequestParticipateRequest" + } } ], "responses": { @@ -5832,6 +5903,98 @@ } } }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalAssignedPlayer": { + "type": "object", + "properties": { + "position": { + "type": "string" + }, + "position_preference": { + "description": "1 (most preferred) to 5 (least)", + "type": "integer" + }, + "rank": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalContestRequestV2": { + "type": "object", + "required": [ + "members" + ], + "properties": { + "members": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalPlayerV2" + } + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalContestResponseV2": { + "type": "object", + "properties": { + "team_a": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalAssignedPlayer" + } + }, + "team_b": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalAssignedPlayer" + } + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalPlayerV2": { + "type": "object", + "required": [ + "positions", + "rank", + "tag", + "username" + ], + "properties": { + "positions": { + "type": "array", + "items": { + "type": "string" + } + }, + "rank": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.RequestParticipateRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "valorant_roles": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ValorantRole" + } + } + } + }, "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ThumbnailUploadResponse": { "type": "object", "properties": { @@ -5985,6 +6148,21 @@ "MemberTypeNormal" ] }, + "github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ValorantRole": { + "type": "string", + "enum": [ + "DUELIST", + "INITIATOR", + "CONTROLLER", + "SENTINEL" + ], + "x-enum-varnames": [ + "ValorantRoleDuelist", + "ValorantRoleInitiator", + "ValorantRoleController", + "ValorantRoleSentinel" + ] + }, "github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordChannel": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1968772..511bfe0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -222,6 +222,67 @@ definitions: oauth_url: type: string type: object + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalAssignedPlayer: + properties: + position: + type: string + position_preference: + description: 1 (most preferred) to 5 (least) + type: integer + rank: + type: string + tag: + type: string + username: + type: string + type: object + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalContestRequestV2: + properties: + members: + items: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalPlayerV2' + type: array + required: + - members + type: object + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalContestResponseV2: + properties: + team_a: + items: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalAssignedPlayer' + type: array + team_b: + items: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalAssignedPlayer' + type: array + type: object + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalPlayerV2: + properties: + positions: + items: + type: string + type: array + rank: + type: string + tag: + type: string + username: + type: string + required: + - positions + - rank + - tag + - username + type: object + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.RequestParticipateRequest: + properties: + description: + type: string + valorant_roles: + items: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ValorantRole' + type: array + type: object github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.ThumbnailUploadResponse: properties: key: @@ -329,6 +390,18 @@ definitions: x-enum-varnames: - MemberTypeStaff - MemberTypeNormal + github_com_FOR-GAMERS_GAMERS-BE_internal_contest_domain.ValorantRole: + enum: + - DUELIST + - INITIATOR + - CONTROLLER + - SENTINEL + type: string + x-enum-varnames: + - ValorantRoleDuelist + - ValorantRoleInitiator + - ValorantRoleController + - ValorantRoleSentinel github_com_FOR-GAMERS_GAMERS-BE_internal_discord_application_dto.DiscordChannel: properties: guild_id: @@ -1515,13 +1588,19 @@ paths: post: consumes: - application/json - description: Apply to participate in a contest + description: Apply to participate in a contest with optional valorant roles + and description parameters: - description: Contest ID in: path name: contestId required: true type: integer + - description: Participation request with optional valorant roles and description + in: body + name: request + schema: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.RequestParticipateRequest' produces: - application/json responses: @@ -3249,6 +3328,46 @@ paths: summary: Get contest point tags: - valorant + /api/contests/lol/temporal: + post: + consumes: + - application/json + description: 'Accepts exactly 10 players each with their rank and position preferences + (all 5 positions in priority order). Returns a balanced 5v5 team assignment + using 3-step filtering: team MMR balance → lane matchup balance → position + satisfaction. No data is persisted.' + parameters: + - description: 10 players with position preferences + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalContestRequestV2' + produces: + - application/json + responses: + "201": + description: Created + schema: + allOf: + - $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' + - properties: + data: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_contest_application_dto.LolTemporalContestResponseV2' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/github_com_FOR-GAMERS_GAMERS-BE_internal_global_response.Response' + security: + - BearerAuth: [] + summary: Balance teams for a temporary LoL custom game + tags: + - contests /api/contests/me: get: consumes: diff --git a/internal/contest/application/contest_service.go b/internal/contest/application/contest_service.go index d3bd513..48bbd4b 100644 --- a/internal/contest/application/contest_service.go +++ b/internal/contest/application/contest_service.go @@ -530,6 +530,52 @@ func (c *ContestService) UploadThumbnail(ctx context.Context, contestId, userId }, nil } +// BalanceLolTemporalContestV2 accepts 10 players each with ranked position preferences +// and returns a balanced 5v5 team assignment using 3-step filtering: +// 1. Minimise |team_A_adj_mmr - team_B_adj_mmr| (adjusted for position preference) +// 2. Minimise total per-position matchup deviation +// 3. Minimise total position preference rank sum (highest satisfaction) +// +// No DB persistence — stateless computation. +func (c *ContestService) BalanceLolTemporalContestV2(req *dto.LolTemporalContestRequestV2) (*dto.LolTemporalContestResponseV2, error) { + if err := req.Validate(); err != nil { + return nil, err + } + + players := req.Members + var baseMMRs [10]int + for i, p := range players { + score, err := dto.ParseLolRankScore(p.Rank) + if err != nil { + return nil, err + } + baseMMRs[i] = score + } + + best := balanceLolTeamsV2(players, baseMMRs) + + buildPlayer := func(playerIdx, permIdx int) *dto.LolTemporalAssignedPlayer { + pos := lolPositions[permIdx] + p := players[playerIdx] + return &dto.LolTemporalAssignedPlayer{ + Username: p.Username, + Tag: p.Tag, + Rank: p.Rank, + Position: pos, + PositionPreference: preferenceRankFor(p.Positions, pos), + } + } + + teamA := make([]*dto.LolTemporalAssignedPlayer, 5) + teamB := make([]*dto.LolTemporalAssignedPlayer, 5) + for i := 0; i < 5; i++ { + teamA[i] = buildPlayer(best.teamAIndices[i], best.permA[i]) + teamB[i] = buildPlayer(best.teamBIndices[i], best.permB[i]) + } + + return &dto.LolTemporalContestResponseV2{TeamA: teamA, TeamB: teamB}, nil +} + // BalanceLolTemporalContest accepts 10 players (2 per lane) and returns a balanced // 5v5 team assignment where the total rank score difference is minimised. // No DB persistence — this is a stateless computation. diff --git a/internal/contest/application/dto/lol_temporal_dto.go b/internal/contest/application/dto/lol_temporal_dto.go index 21357f1..15e3605 100644 --- a/internal/contest/application/dto/lol_temporal_dto.go +++ b/internal/contest/application/dto/lol_temporal_dto.go @@ -6,6 +6,70 @@ import ( "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" ) +// ==================== V2: Position-preference based 5v5 balancing ==================== + +var lolAllPositions = []string{"TOP", "JG", "MID", "ADC", "SUP"} + +// LolTemporalPlayerV2 represents a player with their ranked position preferences. +// Positions must be all 5 lanes in preference order (1st = most preferred). +type LolTemporalPlayerV2 struct { + Username string `json:"username" binding:"required"` + Tag string `json:"tag" binding:"required"` + Rank string `json:"rank" binding:"required"` + Positions []string `json:"positions" binding:"required"` +} + +// LolTemporalContestRequestV2 is the request body for the V2 team balancing. +// Exactly 10 players must be provided; each player lists all 5 positions in preference order. +type LolTemporalContestRequestV2 struct { + Members []*LolTemporalPlayerV2 `json:"members" binding:"required"` +} + +func (r *LolTemporalContestRequestV2) Validate() error { + if len(r.Members) != 10 { + return exception.ErrLolTemporalInvalidPlayerCount + } + + validPos := map[string]bool{"TOP": true, "JG": true, "MID": true, "ADC": true, "SUP": true} + + for _, p := range r.Members { + if p.Username == "" || p.Tag == "" || p.Rank == "" { + return exception.ErrLolTemporalInvalidPlayerCount + } + if len(p.Positions) != 5 { + return exception.ErrLolTemporalInvalidPositions + } + seen := make(map[string]bool, 5) + for i, pos := range p.Positions { + pos = strings.ToUpper(strings.TrimSpace(pos)) + p.Positions[i] = pos + if !validPos[pos] { + return exception.ErrLolTemporalInvalidPositions + } + if seen[pos] { + return exception.ErrLolTemporalInvalidPositions + } + seen[pos] = true + } + } + return nil +} + +// LolTemporalAssignedPlayer is a player with their assigned position and how preferred it was. +type LolTemporalAssignedPlayer struct { + Username string `json:"username"` + Tag string `json:"tag"` + Rank string `json:"rank"` + Position string `json:"position"` + PositionPreference int `json:"position_preference"` // 1 (most preferred) to 5 (least) +} + +// LolTemporalContestResponseV2 contains the balanced team assignment. +type LolTemporalContestResponseV2 struct { + TeamA []*LolTemporalAssignedPlayer `json:"team_a"` + TeamB []*LolTemporalAssignedPlayer `json:"team_b"` +} + var lolTemporalLanes = []string{"TOP", "JG", "MID", "ADC", "SUP"} // LolTemporalMember represents a single player with their lane assignment info. diff --git a/internal/contest/application/lol_temporal_balance.go b/internal/contest/application/lol_temporal_balance.go new file mode 100644 index 0000000..df266ba --- /dev/null +++ b/internal/contest/application/lol_temporal_balance.go @@ -0,0 +1,191 @@ +package application + +import ( + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application/dto" +) + +// lolPositions is the canonical order of LoL positions used as indices 0-4. +var lolPositions = []string{"TOP", "JG", "MID", "ADC", "SUP"} + +// positionIndex maps position name to its index in lolPositions. +var positionIndex = map[string]int{"TOP": 0, "JG": 1, "MID": 2, "ADC": 3, "SUP": 4} + +// preferenceRankFor returns the 1-based preference rank (1=most preferred, 5=least) of pos +// in the given preference list. Returns 5 if not found (validation should prevent this). +func preferenceRankFor(positions []string, pos string) int { + for i, p := range positions { + if p == pos { + return i + 1 + } + } + return 5 +} + +// adjustedMMRScore returns the position-adjusted MMR score scaled by 16. +// Formula: baseMMR × (17 - preferenceRank) +// +// rank 1 → 16/16 = 100% +// rank 2 → 15/16 = 93.75% +// rank 5 → 12/16 = 75% +func adjustedMMRScore(baseMMR, preferenceRank int) int { + return baseMMR * (17 - preferenceRank) +} + +// lolTeamCandidate holds a particular (team split + position assignment) and its metric scores. +type lolTeamCandidate struct { + teamAIndices [5]int // indices into the 10-player slice + teamBIndices [5]int + permA [5]int // permA[i] = position index for teamA[i] + permB [5]int + teamBalance int // |adj_mmr_sum_A - adj_mmr_sum_B| (lower is better) + laneDeviation int // sum of per-position |adj_mmr_A - adj_mmr_B| (lower is better) + posSatisfaction int // sum of all 10 preference ranks (lower is better) +} + +func isBetterLolCandidate(a, b lolTeamCandidate) bool { + if a.teamBalance != b.teamBalance { + return a.teamBalance < b.teamBalance + } + if a.laneDeviation != b.laneDeviation { + return a.laneDeviation < b.laneDeviation + } + return a.posSatisfaction < b.posSatisfaction +} + +// generateCombinations returns all C(n,k) index combinations of [0..n-1]. +func generateCombinations(n, k int) [][]int { + result := make([][]int, 0) + combo := make([]int, 0, k) + var rec func(start int) + rec = func(start int) { + if len(combo) == k { + c := make([]int, k) + copy(c, combo) + result = append(result, c) + return + } + remaining := k - len(combo) + for i := start; i <= n-remaining; i++ { + combo = append(combo, i) + rec(i + 1) + combo = combo[:len(combo)-1] + } + } + rec(0) + return result +} + +// generatePermutations returns all n! permutations of [0..n-1]. +func generatePermutations(n int) [][]int { + result := make([][]int, 0) + perm := make([]int, n) + for i := range perm { + perm[i] = i + } + var rec func(start int) + rec = func(start int) { + if start == n { + p := make([]int, n) + copy(p, perm) + result = append(result, p) + return + } + for i := start; i < n; i++ { + perm[start], perm[i] = perm[i], perm[start] + rec(start + 1) + perm[start], perm[i] = perm[i], perm[start] + } + } + rec(0) + return result +} + +// balanceLolTeamsV2 runs the 3-step filtering algorithm over all C(10,5) × 5! × 5! +// combinations and returns the best team assignment. +// +// Step 1 – minimize |team_A_adj_mmr_sum - team_B_adj_mmr_sum| +// Step 2 – minimize sum of per-position |adj_mmr_A - adj_mmr_B| (lane matchup balance) +// Step 3 – minimize total preference rank sum across all 10 players (position satisfaction) +func balanceLolTeamsV2(players []*dto.LolTemporalPlayerV2, baseMMRs [10]int) lolTeamCandidate { + allCombos := generateCombinations(10, 5) // C(10,5) = 252 + allPerms := generatePermutations(5) // 5! = 120 + + var best lolTeamCandidate + bestSet := false + + for _, comboA := range allCombos { + // Build team B as the complement of team A + inA := [10]bool{} + for _, idx := range comboA { + inA[idx] = true + } + comboB := make([]int, 0, 5) + for i := 0; i < 10; i++ { + if !inA[i] { + comboB = append(comboB, i) + } + } + + for _, permA := range allPerms { + // Precompute team A metrics once (reused across all permB iterations). + adjMMR_A := 0 + posSat_A := 0 + var posAdjA [5]int // posAdjA[posIdx] = adjusted MMR of the team A player in that position + + for i, playerIdx := range comboA { + posIdx := permA[i] + pos := lolPositions[posIdx] + prefRank := preferenceRankFor(players[playerIdx].Positions, pos) + adj := adjustedMMRScore(baseMMRs[playerIdx], prefRank) + adjMMR_A += adj + posSat_A += prefRank + posAdjA[posIdx] = adj + } + + for _, permB := range allPerms { + adjMMR_B := 0 + posSat_B := 0 + laneDeviation := 0 + + for i, playerIdx := range comboB { + posIdx := permB[i] + pos := lolPositions[posIdx] + prefRank := preferenceRankFor(players[playerIdx].Positions, pos) + adj := adjustedMMRScore(baseMMRs[playerIdx], prefRank) + adjMMR_B += adj + posSat_B += prefRank + + diff := posAdjA[posIdx] - adj + if diff < 0 { + diff = -diff + } + laneDeviation += diff + } + + teamBalance := adjMMR_A - adjMMR_B + if teamBalance < 0 { + teamBalance = -teamBalance + } + + cand := lolTeamCandidate{ + teamBalance: teamBalance, + laneDeviation: laneDeviation, + posSatisfaction: posSat_A + posSat_B, + } + copy(cand.teamAIndices[:], comboA) + copy(cand.teamBIndices[:], comboB) + for j := 0; j < 5; j++ { + cand.permA[j] = permA[j] + cand.permB[j] = permB[j] + } + + if !bestSet || isBetterLolCandidate(cand, best) { + best = cand + bestSet = true + } + } + } + } + + return best +} diff --git a/internal/contest/presentation/contest_controller.go b/internal/contest/presentation/contest_controller.go index bc23ea3..c2dde81 100644 --- a/internal/contest/presentation/contest_controller.go +++ b/internal/contest/presentation/contest_controller.go @@ -361,24 +361,24 @@ func (c *ContestController) GetMyContests(ctx *gin.Context) { // LolTemporalContest godoc // @Summary Balance teams for a temporary LoL custom game -// @Description Accepts 10 players (2 per lane: TOP/JG/MID/ADC/SUP) with their ranks and returns a balanced 5v5 team assignment. No data is persisted. +// @Description Accepts exactly 10 players each with their rank and position preferences (all 5 positions in priority order). Returns a balanced 5v5 team assignment using 3-step filtering: team MMR balance → lane matchup balance → position satisfaction. No data is persisted. // @Tags contests // @Accept json // @Produce json // @Security BearerAuth -// @Param request body dto.LolTemporalContestRequest true "Players by lane" -// @Success 201 {object} response.Response{data=dto.LolTemporalContestResponse} +// @Param request body dto.LolTemporalContestRequestV2 true "10 players with position preferences" +// @Success 201 {object} response.Response{data=dto.LolTemporalContestResponseV2} // @Failure 400 {object} response.Response // @Failure 401 {object} response.Response // @Router /api/contests/lol/temporal [post] func (c *ContestController) LolTemporalContest(ctx *gin.Context) { - var req dto.LolTemporalContestRequest + var req dto.LolTemporalContestRequestV2 if !c.helper.BindJSON(ctx, &req) { return } - result, err := c.service.BalanceLolTemporalContest(&req) + result, err := c.service.BalanceLolTemporalContestV2(&req) c.helper.RespondCreated(ctx, result, err, "teams balanced successfully") } @@ -508,12 +508,12 @@ func HandleUploadThumbnail(ctx *gin.Context, service *application.ContestService } func HandleLolTemporalContest(ctx *gin.Context, service *application.ContestService, helper *handler.ControllerHelper) { - var req dto.LolTemporalContestRequest + var req dto.LolTemporalContestRequestV2 if err := ctx.ShouldBindJSON(&req); err != nil { response.JSON(ctx, response.BadRequest(err.Error())) return } - result, err := service.BalanceLolTemporalContest(&req) + result, err := service.BalanceLolTemporalContestV2(&req) helper.RespondCreated(ctx, result, err, "teams balanced successfully") } diff --git a/internal/global/exception/contest_error_status.go b/internal/global/exception/contest_error_status.go index 1d739b2..1bf41c8 100644 --- a/internal/global/exception/contest_error_status.go +++ b/internal/global/exception/contest_error_status.go @@ -44,6 +44,8 @@ var ( ErrContestStartTimePassed = NewBadRequestError("contest start time has already passed", "CT038") // LoL Temporal Contest errors - ErrLolTemporalInvalidLane = NewBadRequestError("all lanes (TOP, JG, MID, ADC, SUP) must be provided with exactly 2 players each", "CT046") - ErrLolTemporalInvalidRank = NewBadRequestError("invalid lol rank format, expected e.g. GOLD II or MASTER", "CT047") + ErrLolTemporalInvalidLane = NewBadRequestError("all lanes (TOP, JG, MID, ADC, SUP) must be provided with exactly 2 players each", "CT046") + ErrLolTemporalInvalidRank = NewBadRequestError("invalid lol rank format, expected e.g. GOLD II or MASTER", "CT047") + ErrLolTemporalInvalidPlayerCount = NewBadRequestError("exactly 10 players are required", "CT048") + ErrLolTemporalInvalidPositions = NewBadRequestError("each player must provide all 5 positions (TOP, JG, MID, ADC, SUP) in preference order without duplicates", "CT049") ) diff --git a/test/contest/application/lol_temporal_v2_service_test.go b/test/contest/application/lol_temporal_v2_service_test.go new file mode 100644 index 0000000..72d8613 --- /dev/null +++ b/test/contest/application/lol_temporal_v2_service_test.go @@ -0,0 +1,247 @@ +package application_test + +import ( + "testing" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application/dto" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// allPositions is the full preference list in default order. +var allPositions = []string{"TOP", "JG", "MID", "ADC", "SUP"} + +// playerV2 builds a LolTemporalPlayerV2 with the given preferences. +func playerV2(username, rank string, positions []string) *dto.LolTemporalPlayerV2 { + return &dto.LolTemporalPlayerV2{ + Username: username, + Tag: "KR1", + Rank: rank, + Positions: positions, + } +} + +// validRequestV2 builds a request with 10 distinct players. +func validRequestV2() *dto.LolTemporalContestRequestV2 { + return &dto.LolTemporalContestRequestV2{ + Members: []*dto.LolTemporalPlayerV2{ + playerV2("P1", "GOLD I", []string{"TOP", "JG", "MID", "ADC", "SUP"}), + playerV2("P2", "GOLD III", []string{"JG", "TOP", "MID", "ADC", "SUP"}), + playerV2("P3", "PLATINUM IV", []string{"MID", "ADC", "SUP", "TOP", "JG"}), + playerV2("P4", "SILVER I", []string{"ADC", "SUP", "MID", "JG", "TOP"}), + playerV2("P5", "DIAMOND IV", []string{"SUP", "MID", "ADC", "JG", "TOP"}), + playerV2("P6", "EMERALD II", []string{"TOP", "MID", "JG", "SUP", "ADC"}), + playerV2("P7", "GOLD II", []string{"JG", "ADC", "TOP", "MID", "SUP"}), + playerV2("P8", "SILVER IV", []string{"MID", "TOP", "ADC", "SUP", "JG"}), + playerV2("P9", "BRONZE I", []string{"ADC", "JG", "MID", "TOP", "SUP"}), + playerV2("P10", "IRON I", []string{"SUP", "ADC", "JG", "MID", "TOP"}), + }, + } +} + +// ==================== BalanceLolTemporalContestV2 Tests ==================== + +func TestBalanceLolTemporalContestV2_Success(t *testing.T) { + svc := newService() + req := validRequestV2() + + resp, err := svc.BalanceLolTemporalContestV2(req) + + require.NoError(t, err) + require.NotNil(t, resp) + assert.Len(t, resp.TeamA, 5) + assert.Len(t, resp.TeamB, 5) + + // All 10 players must appear exactly once across both teams + seen := make(map[string]int) + for _, p := range resp.TeamA { + seen[p.Username]++ + } + for _, p := range resp.TeamB { + seen[p.Username]++ + } + assert.Len(t, seen, 10, "all 10 players must appear") + for _, count := range seen { + assert.Equal(t, 1, count, "each player appears exactly once") + } +} + +func TestBalanceLolTemporalContestV2_EachPlayerHasPosition(t *testing.T) { + svc := newService() + req := validRequestV2() + + resp, err := svc.BalanceLolTemporalContestV2(req) + + require.NoError(t, err) + validPos := map[string]bool{"TOP": true, "JG": true, "MID": true, "ADC": true, "SUP": true} + + for _, p := range append(resp.TeamA, resp.TeamB...) { + assert.True(t, validPos[p.Position], "position %q must be valid", p.Position) + assert.GreaterOrEqual(t, p.PositionPreference, 1) + assert.LessOrEqual(t, p.PositionPreference, 5) + } +} + +func TestBalanceLolTemporalContestV2_EachPositionFilledOnce(t *testing.T) { + svc := newService() + req := validRequestV2() + + resp, err := svc.BalanceLolTemporalContestV2(req) + + require.NoError(t, err) + + checkTeam := func(team []*dto.LolTemporalAssignedPlayer, name string) { + posCounts := make(map[string]int) + for _, p := range team { + posCounts[p.Position]++ + } + for _, pos := range allPositions { + assert.Equal(t, 1, posCounts[pos], "%s: position %s must appear exactly once", name, pos) + } + } + checkTeam(resp.TeamA, "TeamA") + checkTeam(resp.TeamB, "TeamB") +} + +func TestBalanceLolTemporalContestV2_PreferredPositionReflected(t *testing.T) { + // All players prefer the same position priority; verify preference rank is set correctly. + svc := newService() + req := &dto.LolTemporalContestRequestV2{ + Members: func() []*dto.LolTemporalPlayerV2 { + players := make([]*dto.LolTemporalPlayerV2, 10) + for i := 0; i < 10; i++ { + players[i] = playerV2("P"+string(rune('0'+i+1)), "GOLD II", []string{"TOP", "JG", "MID", "ADC", "SUP"}) + } + return players + }(), + } + + resp, err := svc.BalanceLolTemporalContestV2(req) + + require.NoError(t, err) + // PositionPreference must equal the index of the assigned position in the preference list + 1 + for _, p := range append(resp.TeamA, resp.TeamB...) { + expectedRank := 0 + for i, pos := range allPositions { + if pos == p.Position { + expectedRank = i + 1 + break + } + } + assert.Equal(t, expectedRank, p.PositionPreference, "player %s: preference rank mismatch", p.Username) + } +} + +func TestBalanceLolTemporalContestV2_MinimisesTeamMMRDiff(t *testing.T) { + // Two groups of 5 equal players: one group all CHALLENGER (31), one all IRON IV (1). + // Best split must put some from each group in each team to balance. + svc := newService() + members := make([]*dto.LolTemporalPlayerV2, 10) + for i := 0; i < 5; i++ { + members[i] = playerV2("High"+string(rune('A'+i)), "CHALLENGER", allPositions) + } + for i := 0; i < 5; i++ { + members[5+i] = playerV2("Low"+string(rune('A'+i)), "IRON IV", allPositions) + } + req := &dto.LolTemporalContestRequestV2{Members: members} + + resp, err := svc.BalanceLolTemporalContestV2(req) + + require.NoError(t, err) + // Count high-MMR players in each team — should be split across teams + countHigh := func(team []*dto.LolTemporalAssignedPlayer) int { + c := 0 + for _, p := range team { + if p.Rank == "CHALLENGER" { + c++ + } + } + return c + } + aHigh := countHigh(resp.TeamA) + bHigh := countHigh(resp.TeamB) + // With 5 CHALLENGER and 5 IRON IV, perfect balance means neither team takes all 5 high players. + assert.NotEqual(t, 5, aHigh, "team A must not take all high-MMR players") + assert.NotEqual(t, 5, bHigh, "team B must not take all high-MMR players") +} + +// ==================== Validation Tests ==================== + +func TestBalanceLolTemporalContestV2_NotEnoughPlayers(t *testing.T) { + svc := newService() + req := &dto.LolTemporalContestRequestV2{ + Members: []*dto.LolTemporalPlayerV2{ + playerV2("P1", "GOLD II", allPositions), + }, + } + + _, err := svc.BalanceLolTemporalContestV2(req) + + assert.ErrorIs(t, err, exception.ErrLolTemporalInvalidPlayerCount) +} + +func TestBalanceLolTemporalContestV2_TooManyPlayers(t *testing.T) { + svc := newService() + members := make([]*dto.LolTemporalPlayerV2, 11) + for i := range members { + members[i] = playerV2("P", "GOLD II", allPositions) + } + req := &dto.LolTemporalContestRequestV2{Members: members} + + _, err := svc.BalanceLolTemporalContestV2(req) + + assert.ErrorIs(t, err, exception.ErrLolTemporalInvalidPlayerCount) +} + +func TestBalanceLolTemporalContestV2_MissingPosition(t *testing.T) { + svc := newService() + req := validRequestV2() + req.Members[0].Positions = []string{"TOP", "JG", "MID", "ADC"} // only 4 + + _, err := svc.BalanceLolTemporalContestV2(req) + + assert.ErrorIs(t, err, exception.ErrLolTemporalInvalidPositions) +} + +func TestBalanceLolTemporalContestV2_DuplicatePosition(t *testing.T) { + svc := newService() + req := validRequestV2() + req.Members[0].Positions = []string{"TOP", "TOP", "MID", "ADC", "SUP"} // duplicate TOP + + _, err := svc.BalanceLolTemporalContestV2(req) + + assert.ErrorIs(t, err, exception.ErrLolTemporalInvalidPositions) +} + +func TestBalanceLolTemporalContestV2_InvalidPositionName(t *testing.T) { + svc := newService() + req := validRequestV2() + req.Members[0].Positions = []string{"TOP", "JG", "MID", "ADC", "SUPPORT"} // invalid name + + _, err := svc.BalanceLolTemporalContestV2(req) + + assert.ErrorIs(t, err, exception.ErrLolTemporalInvalidPositions) +} + +func TestBalanceLolTemporalContestV2_InvalidRank(t *testing.T) { + svc := newService() + req := validRequestV2() + req.Members[0].Rank = "HYPERCARRY" // invalid rank + + _, err := svc.BalanceLolTemporalContestV2(req) + + assert.ErrorIs(t, err, exception.ErrLolTemporalInvalidRank) +} + +func TestBalanceLolTemporalContestV2_PositionsNormalizedToUpper(t *testing.T) { + svc := newService() + req := validRequestV2() + req.Members[0].Positions = []string{"top", "jg", "mid", "adc", "sup"} // lowercase + + resp, err := svc.BalanceLolTemporalContestV2(req) + + // Should succeed since validation normalizes + assert.NoError(t, err) + assert.NotNil(t, resp) +} diff --git a/test/contest/presentation/lol_temporal_controller_test.go b/test/contest/presentation/lol_temporal_controller_test.go index a998c15..3ba4791 100644 --- a/test/contest/presentation/lol_temporal_controller_test.go +++ b/test/contest/presentation/lol_temporal_controller_test.go @@ -28,21 +28,7 @@ func temporalBody(t *testing.T, body map[string]interface{}) *bytes.Buffer { } func validTemporalBody() map[string]interface{} { - lane := func(r1, r2 string) []map[string]string { - return []map[string]string{ - {"username": "PlayerA", "tag": "KR1", "rank": r1}, - {"username": "PlayerB", "tag": "KR1", "rank": r2}, - } - } - return map[string]interface{}{ - "members": map[string]interface{}{ - "TOP": lane("GOLD I", "SILVER I"), - "JG": lane("PLATINUM IV", "GOLD IV"), - "MID": lane("DIAMOND IV", "EMERALD II"), - "ADC": lane("GOLD II", "SILVER II"), - "SUP": lane("BRONZE I", "IRON I"), - }, - } + return validTemporalBodyV2() } // ==================== Controller Tests ==================== @@ -73,14 +59,12 @@ func TestLolTemporalController_Success(t *testing.T) { data, ok := resp["data"].(map[string]interface{}) assert.True(t, ok) - members, ok := data["members"].(map[string]interface{}) + teamA, ok := data["team_a"].([]interface{}) assert.True(t, ok) - - for _, lane := range []string{"TOP", "JG", "MID", "ADC", "SUP"} { - players, ok := members[lane].([]interface{}) - assert.True(t, ok, "lane %s should exist", lane) - assert.Len(t, players, 2, "lane %s should have 2 players", lane) - } + assert.Len(t, teamA, 5) + teamB, ok := data["team_b"].([]interface{}) + assert.True(t, ok) + assert.Len(t, teamB, 5) } func TestLolTemporalController_MissingLane(t *testing.T) { diff --git a/test/contest/presentation/lol_temporal_v2_controller_test.go b/test/contest/presentation/lol_temporal_v2_controller_test.go new file mode 100644 index 0000000..5fc4744 --- /dev/null +++ b/test/contest/presentation/lol_temporal_v2_controller_test.go @@ -0,0 +1,228 @@ +package presentation_test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/application" + "github.com/FOR-GAMERS/GAMERS-BE/internal/contest/presentation" + "github.com/FOR-GAMERS/GAMERS-BE/internal/global/common/handler" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +// validTemporalBodyV2 builds a valid V2 request body with 10 players. +func validTemporalBodyV2() map[string]interface{} { + member := func(username, rank string, positions []string) map[string]interface{} { + return map[string]interface{}{ + "username": username, + "tag": "KR1", + "rank": rank, + "positions": positions, + } + } + allPos := []string{"TOP", "JG", "MID", "ADC", "SUP"} + return map[string]interface{}{ + "members": []interface{}{ + member("P1", "GOLD I", allPos), + member("P2", "GOLD III", []string{"JG", "TOP", "MID", "ADC", "SUP"}), + member("P3", "PLATINUM IV", []string{"MID", "ADC", "SUP", "TOP", "JG"}), + member("P4", "SILVER I", []string{"ADC", "SUP", "MID", "JG", "TOP"}), + member("P5", "DIAMOND IV", []string{"SUP", "MID", "ADC", "JG", "TOP"}), + member("P6", "EMERALD II", allPos), + member("P7", "GOLD II", []string{"JG", "ADC", "TOP", "MID", "SUP"}), + member("P8", "SILVER IV", []string{"MID", "TOP", "ADC", "SUP", "JG"}), + member("P9", "BRONZE I", []string{"ADC", "JG", "MID", "TOP", "SUP"}), + member("P10", "IRON I", []string{"SUP", "ADC", "JG", "MID", "TOP"}), + }, + } +} + +func newTemporalServiceV2() *application.ContestService { + return application.NewContestService(nil, nil, nil, nil, nil) +} + +// ==================== V2 Controller Tests ==================== + +func TestLolTemporalV2Controller_Success(t *testing.T) { + router := setupTestRouter() + svc := newTemporalServiceV2() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", temporalBody(t, validTemporalBodyV2())) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusCreated, w.Code) +} + +func TestLolTemporalV2Controller_ResponseHasTeamAAndTeamB(t *testing.T) { + router := setupTestRouter() + svc := newTemporalServiceV2() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", temporalBody(t, validTemporalBodyV2())) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusCreated, w.Code) + + var resp map[string]interface{} + assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + + data, ok := resp["data"].(map[string]interface{}) + assert.True(t, ok, "data field must be present") + + teamA, ok := data["team_a"].([]interface{}) + assert.True(t, ok, "team_a must be present") + assert.Len(t, teamA, 5, "team_a must have 5 players") + + teamB, ok := data["team_b"].([]interface{}) + assert.True(t, ok, "team_b must be present") + assert.Len(t, teamB, 5, "team_b must have 5 players") +} + +func TestLolTemporalV2Controller_PlayerHasPositionAndPreference(t *testing.T) { + router := setupTestRouter() + svc := newTemporalServiceV2() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", temporalBody(t, validTemporalBodyV2())) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusCreated, w.Code) + + var resp map[string]interface{} + assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + + data := resp["data"].(map[string]interface{}) + teamA := data["team_a"].([]interface{}) + for _, raw := range teamA { + p := raw.(map[string]interface{}) + assert.NotEmpty(t, p["position"]) + assert.NotNil(t, p["position_preference"]) + } +} + +func TestLolTemporalV2Controller_NotEnoughPlayers(t *testing.T) { + router := setupTestRouter() + svc := newTemporalServiceV2() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + body := map[string]interface{}{ + "members": []interface{}{ + map[string]interface{}{ + "username": "P1", + "tag": "KR1", + "rank": "GOLD II", + "positions": []string{"TOP", "JG", "MID", "ADC", "SUP"}, + }, + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", temporalBody(t, body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestLolTemporalV2Controller_InvalidPositions(t *testing.T) { + router := setupTestRouter() + svc := newTemporalServiceV2() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + body := validTemporalBodyV2() + members := body["members"].([]interface{}) + members[0].(map[string]interface{})["positions"] = []string{"TOP", "TOP", "MID", "ADC", "SUP"} + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", temporalBody(t, body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestLolTemporalV2Controller_InvalidRank(t *testing.T) { + router := setupTestRouter() + svc := newTemporalServiceV2() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + body := validTemporalBodyV2() + members := body["members"].([]interface{}) + members[0].(map[string]interface{})["rank"] = "INVALID_RANK" + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", temporalBody(t, body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestLolTemporalV2Controller_InvalidJSON(t *testing.T) { + router := setupTestRouter() + svc := newTemporalServiceV2() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", bytes.NewBufferString("not json")) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestLolTemporalV2Controller_EmptyBody(t *testing.T) { + router := setupTestRouter() + svc := newTemporalServiceV2() + helper := handler.NewControllerHelper() + + router.POST("/api/contests/lol/temporal", func(c *gin.Context) { + presentation.HandleLolTemporalContest(c, svc, helper) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/contests/lol/temporal", temporalBody(t, map[string]interface{}{})) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} From c561a1df2877e7f073c8d50179c695ba836aed17 Mon Sep 17 00:00:00 2001 From: Sunwoo-An <110546006+Sunja-An@users.noreply.github.com> Date: Sat, 4 Apr 2026 12:43:13 +0900 Subject: [PATCH 24/24] =?UTF-8?q?refactor:=20contest=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EC=95=88=EC=A0=95=EC=84=B1=20=EB=B0=8F=20LoL=20?= =?UTF-8?q?=ED=8C=80=20=EB=B0=B8=EB=9F=B0=EC=8B=B1=20=EC=84=B1=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AcceptApplication에 application 조회 에러/nil/상태 검증 추가 - UploadThumbnail goroutine에서 context race condition 수정 (oldKey 캡처) - balanceLolTeamsV2 조합/순열 테이블 패키지 레벨 precompute로 성능 최적화 Co-Authored-By: Claude Sonnet 4.6 --- .../contest_application_service.go | 19 ++++-- .../contest/application/contest_service.go | 7 +- .../application/lol_temporal_balance.go | 64 +++++++++++-------- 3 files changed, 54 insertions(+), 36 deletions(-) diff --git a/internal/contest/application/contest_application_service.go b/internal/contest/application/contest_application_service.go index dc2018d..1897c3a 100644 --- a/internal/contest/application/contest_application_service.go +++ b/internal/contest/application/contest_application_service.go @@ -181,7 +181,16 @@ func (s *ContestApplicationService) AcceptApplication(ctx context.Context, conte } // Get application data before accepting (to preserve valorant roles and description) - application, appErr := s.applicationRepo.GetApplication(ctx, contestId, userId) + application, err := s.applicationRepo.GetApplication(ctx, contestId, userId) + if err != nil { + return fmt.Errorf("[AcceptApplication] failed to get application (contestId=%d, userId=%d): %w", contestId, userId, err) + } + if application == nil { + return exception.ErrApplicationNotFound + } + if application.Status != port.ApplicationStatusPending { + return exception.ErrApplicationNotPending + } err = s.applicationRepo.AcceptRequest(ctx, contestId, userId, leaderUserId) if err != nil { @@ -190,13 +199,9 @@ func (s *ContestApplicationService) AcceptApplication(ctx context.Context, conte // Restore point, valorant roles and description from the application sender snapshot point := 0 - if appErr == nil && application != nil && application.Sender != nil { - point = application.Sender.Point - } - member := domain.NewContestMember(userId, contestId, domain.MemberTypeNormal, domain.LeaderTypeMember, point) - - if appErr == nil && application != nil && application.Sender != nil { + if application.Sender != nil { + member.Point = application.Sender.Point member.ValorantRoles = application.Sender.ValorantRoles member.Description = application.Sender.Description } diff --git a/internal/contest/application/contest_service.go b/internal/contest/application/contest_service.go index 48bbd4b..00d16bf 100644 --- a/internal/contest/application/contest_service.go +++ b/internal/contest/application/contest_service.go @@ -507,9 +507,12 @@ func (c *ContestService) UploadThumbnail(ctx context.Context, contestId, userId // Delete old thumbnail from storage if exists if contest.BannerKey != nil && *contest.BannerKey != "" { + oldKey := *contest.BannerKey go func() { - if delErr := c.storagePort.Delete(context.Background(), *contest.BannerKey); delErr != nil { - log.Printf("[UploadThumbnail] Failed to delete old thumbnail %s: %v", *contest.BannerKey, delErr) + delCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if delErr := c.storagePort.Delete(delCtx, oldKey); delErr != nil { + log.Printf("[UploadThumbnail] Failed to delete old thumbnail %s: %v", oldKey, delErr) } }() } diff --git a/internal/contest/application/lol_temporal_balance.go b/internal/contest/application/lol_temporal_balance.go index df266ba..abe2a32 100644 --- a/internal/contest/application/lol_temporal_balance.go +++ b/internal/contest/application/lol_temporal_balance.go @@ -10,6 +10,12 @@ var lolPositions = []string{"TOP", "JG", "MID", "ADC", "SUP"} // positionIndex maps position name to its index in lolPositions. var positionIndex = map[string]int{"TOP": 0, "JG": 1, "MID": 2, "ADC": 3, "SUP": 4} +// precomputed combination/permutation tables — computed once at package init. +var ( + allLolCombos = generateCombinations(10, 5) // C(10,5) = 252 + allLolPerms = generatePermutations(5) // 5! = 120 +) + // preferenceRankFor returns the 1-based preference rank (1=most preferred, 5=least) of pos // in the given preference list. Returns 5 if not found (validation should prevent this). func preferenceRankFor(positions []string, pos string) int { @@ -107,13 +113,22 @@ func generatePermutations(n int) [][]int { // Step 2 – minimize sum of per-position |adj_mmr_A - adj_mmr_B| (lane matchup balance) // Step 3 – minimize total preference rank sum across all 10 players (position satisfaction) func balanceLolTeamsV2(players []*dto.LolTemporalPlayerV2, baseMMRs [10]int) lolTeamCandidate { - allCombos := generateCombinations(10, 5) // C(10,5) = 252 - allPerms := generatePermutations(5) // 5! = 120 + // Precompute per-player per-position adjusted MMR and preference rank. + // adjMMRTable[playerIdx][posIdx], prefRankTable[playerIdx][posIdx] + var adjMMRTable [10][5]int + var prefRankTable [10][5]int + for p := 0; p < 10; p++ { + for posIdx, pos := range lolPositions { + rank := preferenceRankFor(players[p].Positions, pos) + prefRankTable[p][posIdx] = rank + adjMMRTable[p][posIdx] = adjustedMMRScore(baseMMRs[p], rank) + } + } var best lolTeamCandidate bestSet := false - for _, comboA := range allCombos { + for _, comboA := range allLolCombos { // Build team B as the complement of team A inA := [10]bool{} for _, idx := range comboA { @@ -126,7 +141,7 @@ func balanceLolTeamsV2(players []*dto.LolTemporalPlayerV2, baseMMRs [10]int) lol } } - for _, permA := range allPerms { + for _, permA := range allLolPerms { // Precompute team A metrics once (reused across all permB iterations). adjMMR_A := 0 posSat_A := 0 @@ -134,26 +149,22 @@ func balanceLolTeamsV2(players []*dto.LolTemporalPlayerV2, baseMMRs [10]int) lol for i, playerIdx := range comboA { posIdx := permA[i] - pos := lolPositions[posIdx] - prefRank := preferenceRankFor(players[playerIdx].Positions, pos) - adj := adjustedMMRScore(baseMMRs[playerIdx], prefRank) + adj := adjMMRTable[playerIdx][posIdx] adjMMR_A += adj - posSat_A += prefRank + posSat_A += prefRankTable[playerIdx][posIdx] posAdjA[posIdx] = adj } - for _, permB := range allPerms { + for _, permB := range allLolPerms { adjMMR_B := 0 posSat_B := 0 laneDeviation := 0 for i, playerIdx := range comboB { posIdx := permB[i] - pos := lolPositions[posIdx] - prefRank := preferenceRankFor(players[playerIdx].Positions, pos) - adj := adjustedMMRScore(baseMMRs[playerIdx], prefRank) + adj := adjMMRTable[playerIdx][posIdx] adjMMR_B += adj - posSat_B += prefRank + posSat_B += prefRankTable[playerIdx][posIdx] diff := posAdjA[posIdx] - adj if diff < 0 { @@ -167,20 +178,19 @@ func balanceLolTeamsV2(players []*dto.LolTemporalPlayerV2, baseMMRs [10]int) lol teamBalance = -teamBalance } - cand := lolTeamCandidate{ - teamBalance: teamBalance, - laneDeviation: laneDeviation, - posSatisfaction: posSat_A + posSat_B, - } - copy(cand.teamAIndices[:], comboA) - copy(cand.teamBIndices[:], comboB) - for j := 0; j < 5; j++ { - cand.permA[j] = permA[j] - cand.permB[j] = permB[j] - } - - if !bestSet || isBetterLolCandidate(cand, best) { - best = cand + // Only copy struct data when this candidate is actually better. + if !bestSet || teamBalance < best.teamBalance || + (teamBalance == best.teamBalance && laneDeviation < best.laneDeviation) || + (teamBalance == best.teamBalance && laneDeviation == best.laneDeviation && posSat_A+posSat_B < best.posSatisfaction) { + best.teamBalance = teamBalance + best.laneDeviation = laneDeviation + best.posSatisfaction = posSat_A + posSat_B + copy(best.teamAIndices[:], comboA) + copy(best.teamBIndices[:], comboB) + for j := 0; j < 5; j++ { + best.permA[j] = permA[j] + best.permB[j] = permB[j] + } bestSet = true } }