diff --git a/docs/openapi/v2.yaml b/docs/openapi/v2.yaml index 6fee7136..715ccd6d 100644 --- a/docs/openapi/v2.yaml +++ b/docs/openapi/v2.yaml @@ -1565,7 +1565,7 @@ paths: - edition security: - AdminAuth: [] - operationId: postEditionGame + operationId: patchEditionGame requestBody: content: application/json: diff --git a/go.mod b/go.mod index 2c16ffed..eb3de6e9 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,19 @@ module github.com/traPtitech/trap-collection-server -go 1.24 +go 1.24.0 require ( github.com/aws/aws-sdk-go-v2 v1.36.3 + github.com/aws/aws-sdk-go-v2/config v1.29.9 github.com/aws/aws-sdk-go-v2/credentials v1.17.62 - github.com/cosmtrek/air v1.52.1 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.66 + github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 + github.com/cosmtrek/air v1.61.2 github.com/deepmap/oapi-codegen v1.16.3 github.com/dgraph-io/ristretto v0.2.0 github.com/getkin/kin-openapi v0.131.0 + github.com/go-gormigrate/gormigrate/v2 v2.1.3 + github.com/go-sql-driver/mysql v1.9.1 github.com/go-task/task/v3 v3.42.1 github.com/google/uuid v1.6.0 github.com/google/wire v0.6.0 @@ -16,18 +21,21 @@ require ( github.com/h2non/filetype v1.1.3 github.com/labstack/echo-contrib v0.17.2 github.com/labstack/echo/v4 v4.13.3 + github.com/labstack/gommon v0.4.2 github.com/mazrean/formstream v1.1.2 github.com/ncw/swift/v2 v2.0.3 github.com/oapi-codegen/echo-middleware v1.0.2 github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 github.com/oapi-codegen/runtime v1.1.1 github.com/ory/dockertest/v3 v3.11.0 + github.com/prometheus/client_golang v1.21.1 github.com/stretchr/testify v1.10.0 go.uber.org/mock v0.5.0 golang.org/x/mod v0.24.0 golang.org/x/sync v0.12.0 gorm.io/driver/mysql v1.5.7 gorm.io/gorm v1.25.12 + gorm.io/plugin/prometheus v0.1.0 ) require ( @@ -55,15 +63,18 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect github.com/aws/smithy-go v1.22.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/bep/godartsass/v2 v2.3.2 // indirect github.com/bep/golibsass v1.2.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chainguard-dev/git-urls v1.0.2 // indirect github.com/cli/safeexec v1.0.1 // indirect github.com/cloudflare/circl v1.6.0 // indirect github.com/containerd/continuity v0.4.3 // indirect github.com/creack/pty v1.1.24 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/docker/cli v26.1.4+incompatible // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -71,6 +82,7 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dominikbraun/graph v0.23.0 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.18.0 // indirect @@ -87,14 +99,21 @@ require ( github.com/gohugoio/hugo v0.139.4 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/subcommands v1.2.0 // indirect + github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-zglob v0.0.6 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect @@ -106,11 +125,16 @@ require ( github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runc v1.1.14 // indirect + github.com/opencontainers/runc v1.1.13 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/radovskyb/watcher v1.0.7 // indirect github.com/sajari/fuzzy v1.0.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect @@ -121,6 +145,8 @@ require ( github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/tdewolff/parse/v2 v2.7.15 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect @@ -134,31 +160,6 @@ require ( ) require ( - github.com/aws/aws-sdk-go-v2/config v1.29.9 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.66 - github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/go-gormigrate/gormigrate/v2 v2.1.3 - github.com/go-sql-driver/mysql v1.9.1 - github.com/google/subcommands v1.2.0 // indirect - github.com/gorilla/context v1.1.2 // indirect - github.com/gorilla/securecookie v1.1.2 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/labstack/gommon v0.4.2 - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.21.1 - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect golang.org/x/crypto v0.35.0 // indirect golang.org/x/net v0.36.0 // indirect golang.org/x/sys v0.31.0 // indirect @@ -167,7 +168,6 @@ require ( golang.org/x/tools v0.27.0 // indirect google.golang.org/protobuf v1.36.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/plugin/prometheus v0.1.0 ) tool go.uber.org/mock/mockgen diff --git a/go.sum b/go.sum index 368d1a96..b4c292d7 100644 --- a/go.sum +++ b/go.sum @@ -166,8 +166,8 @@ github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= -github.com/cosmtrek/air v1.52.1 h1:R+BiGX9tWJ36ENM0ZroaRkagKgIlCmy+8IGTvMyoDV4= -github.com/cosmtrek/air v1.52.1/go.mod h1:xILtq8JGIYwe++r/ib4PdhubiuKBmE1vutC49E+5A78= +github.com/cosmtrek/air v1.61.2 h1:F0IkTrmb5Uf0LVfroPKE4xVV9NeoEkuvks3X83B5Hqo= +github.com/cosmtrek/air v1.61.2/go.mod h1:yOz9vy7edZ75KRN9+Ofqwm3OU0wuv4Csc+ikMeZxxS8= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -514,8 +514,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= -github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= +github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= +github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= @@ -624,9 +624,8 @@ github.com/tdewolff/minify/v2 v2.20.37 h1:Q97cx4STXCh1dlWDlNHZniE8BJ2EBL0+2b0n92 github.com/tdewolff/minify/v2 v2.20.37/go.mod h1:L1VYef/jwKw6Wwyk5A+T0mBjjn3mMPgmjjA688RNsxU= github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGpCTw= github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= +github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52 h1:gAQliwn+zJrkjAHVcBEYW/RFvd2St4yYimisvozAYlA= github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= -github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= -github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= 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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= diff --git a/src/handler/v2/edition.go b/src/handler/v2/edition.go index 76f245be..188273e5 100644 --- a/src/handler/v2/edition.go +++ b/src/handler/v2/edition.go @@ -299,7 +299,7 @@ func (edition *Edition) GetEditionGames(ctx echo.Context, editionID openapi.Edit // エディションのゲームの変更 // (PATCH /editions/{editionID}/games) -func (edition *Edition) PostEditionGame(c echo.Context, editionID openapi.EditionIDInPath) error { +func (edition *Edition) PatchEditionGame(c echo.Context, editionID openapi.EditionIDInPath) error { var req openapi.PatchEditionGameRequest err := c.Bind(&req) if err != nil { diff --git a/src/handler/v2/edition_test.go b/src/handler/v2/edition_test.go new file mode 100644 index 00000000..4ef49c90 --- /dev/null +++ b/src/handler/v2/edition_test.go @@ -0,0 +1,1820 @@ +package v2 + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/traPtitech/trap-collection-server/pkg/types" + "go.uber.org/mock/gomock" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/traPtitech/trap-collection-server/src/domain" + "github.com/traPtitech/trap-collection-server/src/domain/values" + "github.com/traPtitech/trap-collection-server/src/handler/v2/openapi" + "github.com/traPtitech/trap-collection-server/src/service" + "github.com/traPtitech/trap-collection-server/src/service/mock" +) + +func TestGetEditions(t *testing.T) { + t.Parallel() + + type test struct { + description string + editions []*domain.LauncherVersion + getEditionsErr error + expectEditions []openapi.Edition + isErr bool + statusCode int + } + + now := time.Now() + editionID1 := values.NewLauncherVersionIDFromUUID(uuid.New()) + editionID2 := values.NewLauncherVersionIDFromUUID(uuid.New()) + editionName1 := values.NewLauncherVersionName("テストエディション") + editionName2 := values.NewLauncherVersionName("テストエディション2") + strURL := "https://example.com/questionnaire" + questionnaireURL, err := url.Parse(strURL) + if err != nil { + t.Fatalf("failed to parse url: %v", err) + } + + testCases := []test{ + { + description: "特に問題ないのでエラーなし", + editions: []*domain.LauncherVersion{ + domain.NewLauncherVersionWithQuestionnaire( + editionID1, + editionName1, + values.NewLauncherVersionQuestionnaireURL(questionnaireURL), + now, + ), + }, + expectEditions: []openapi.Edition{ + { + Id: uuid.UUID(editionID1), + Name: string(editionName1), + Questionnaire: &strURL, + CreatedAt: now, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "アンケートURLが無くてもエラーなし", + editions: []*domain.LauncherVersion{ + domain.NewLauncherVersionWithoutQuestionnaire( + editionID1, + editionName1, + now, + ), + }, + expectEditions: []openapi.Edition{ + { + Id: uuid.UUID(editionID1), + Name: string(editionName1), + Questionnaire: nil, + CreatedAt: now, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "GetEditionsがエラーなので500", + getEditionsErr: errors.New("error"), + isErr: true, + statusCode: http.StatusInternalServerError, + }, + { + description: "複数エディションでもエラーなし", + editions: []*domain.LauncherVersion{ + domain.NewLauncherVersionWithQuestionnaire( + editionID1, + editionName1, + values.NewLauncherVersionQuestionnaireURL(questionnaireURL), + now, + ), + domain.NewLauncherVersionWithoutQuestionnaire( + editionID2, + editionName2, + now, + ), + }, + expectEditions: []openapi.Edition{ + { + Id: uuid.UUID(editionID1), + Name: string(editionName1), + Questionnaire: &strURL, + CreatedAt: now, + }, + { + Id: uuid.UUID(editionID2), + Name: string(editionName2), + Questionnaire: nil, + CreatedAt: now, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "エディションが存在しなくてもでもエラーなし", + editions: []*domain.LauncherVersion{}, + statusCode: http.StatusOK, + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.description, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockEditionService := mock.NewMockEdition(ctrl) + edition := NewEdition(mockEditionService) + + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/api/v2/editions", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + mockEditionService. + EXPECT(). + GetEditions(gomock.Any()). + Return(testCase.editions, testCase.getEditionsErr) + + err := edition.GetEditions(c) + + if testCase.isErr { + if testCase.statusCode != 0 { + var httpErr *echo.HTTPError + if assert.ErrorAs(t, err, &httpErr, "error should be *echo.HTTPError") { + assert.Equal(t, testCase.statusCode, httpErr.Code) + } + } else { + assert.Error(t, err) + } + return + } + assert.NoError(t, err) + if err != nil || testCase.isErr { + return + } + + assert.Equal(t, testCase.statusCode, rec.Code) + + var res []openapi.Edition + err = json.NewDecoder(rec.Body).Decode(&res) + if err != nil { + t.Fatalf("failed to decode response body: %v", err) + } + + assert.Len(t, res, len(testCase.expectEditions)) + for i, ed := range res { + assert.Equal(t, testCase.expectEditions[i].Id, ed.Id) + assert.Equal(t, testCase.expectEditions[i].Name, ed.Name) + assert.Equal(t, testCase.expectEditions[i].Questionnaire, ed.Questionnaire) + assert.WithinDuration(t, testCase.expectEditions[i].CreatedAt, ed.CreatedAt, time.Second) + } + }) + } +} + +func TestPostEdition(t *testing.T) { + t.Parallel() + + type test struct { + description string + reqBody *openapi.NewEdition + invalidBody bool + executeCreateEdition bool + name values.LauncherVersionName + questionnaireURL types.Option[values.LauncherVersionQuestionnaireURL] + gameVersionIDs []values.GameVersionID + createEditionErr error + resultEdition *domain.LauncherVersion + isErr bool + statusCode int + expectEdition *openapi.Edition + } + + now := time.Now() + editionUUID := uuid.New() + editionID := values.NewLauncherVersionIDFromUUID(editionUUID) + editionName := "テストエディション" + strURL := "https://example.com/questionnaire" + invalidURL := " https://example.com/questionnaire with spaces" + longName := strings.Repeat("あ", 33) + questionnaireURL, err := url.Parse(strURL) + if err != nil { + t.Fatalf("failed to parse url: %v", err) + } + gameVersionUUID1 := uuid.New() + gameVersionUUID2 := uuid.New() + + testCases := []test{ + { + description: "特に問題ないのでエラーなし", + reqBody: &openapi.NewEdition{ + Name: editionName, + Questionnaire: &strURL, + GameVersions: []uuid.UUID{gameVersionUUID1, gameVersionUUID2}, + }, + executeCreateEdition: true, + name: values.NewLauncherVersionName(editionName), + questionnaireURL: types.NewOption(values.NewLauncherVersionQuestionnaireURL(questionnaireURL)), + gameVersionIDs: []values.GameVersionID{ + values.NewGameVersionIDFromUUID(gameVersionUUID1), + values.NewGameVersionIDFromUUID(gameVersionUUID2), + }, + resultEdition: domain.NewLauncherVersionWithQuestionnaire( + editionID, + values.NewLauncherVersionName(editionName), + values.NewLauncherVersionQuestionnaireURL(questionnaireURL), + now, + ), + expectEdition: &openapi.Edition{ + Id: editionUUID, + Name: editionName, + Questionnaire: &strURL, + CreatedAt: now, + }, + statusCode: http.StatusCreated, + }, + { + description: "アンケートURLがなくてもエラーなし", + reqBody: &openapi.NewEdition{ + Name: editionName, + GameVersions: []uuid.UUID{gameVersionUUID1}, + }, + executeCreateEdition: true, + name: values.NewLauncherVersionName(editionName), + questionnaireURL: types.Option[values.LauncherVersionQuestionnaireURL]{}, + gameVersionIDs: []values.GameVersionID{values.NewGameVersionIDFromUUID(gameVersionUUID1)}, + resultEdition: domain.NewLauncherVersionWithoutQuestionnaire( + editionID, + values.NewLauncherVersionName(editionName), + now, + ), + expectEdition: &openapi.Edition{ + Id: editionUUID, + Name: editionName, + Questionnaire: nil, + CreatedAt: now, + }, + statusCode: http.StatusCreated, + }, + { + description: "Edition名が空文字なので400", + reqBody: &openapi.NewEdition{ + Name: "", + GameVersions: []uuid.UUID{gameVersionUUID1}, + }, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "Edition名が長すぎるので400", + reqBody: &openapi.NewEdition{ + Name: longName, + GameVersions: []uuid.UUID{gameVersionUUID1}, + }, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "URLが正しくないので400", + reqBody: &openapi.NewEdition{ + Name: editionName, + Questionnaire: &invalidURL, + GameVersions: []uuid.UUID{gameVersionUUID1}, + }, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "ゲームバージョンが重複しているので400", + reqBody: &openapi.NewEdition{ + Name: editionName, + GameVersions: []uuid.UUID{gameVersionUUID1, gameVersionUUID1}, + }, + executeCreateEdition: true, + name: values.NewLauncherVersionName(editionName), + questionnaireURL: types.Option[values.LauncherVersionQuestionnaireURL]{}, + gameVersionIDs: []values.GameVersionID{ + values.NewGameVersionIDFromUUID(gameVersionUUID1), + values.NewGameVersionIDFromUUID(gameVersionUUID1), + }, + createEditionErr: service.ErrDuplicateGameVersion, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "ゲームが重複しているので400", + reqBody: &openapi.NewEdition{ + Name: editionName, + GameVersions: []uuid.UUID{gameVersionUUID1, gameVersionUUID2}, + }, + executeCreateEdition: true, + name: values.NewLauncherVersionName(editionName), + questionnaireURL: types.Option[values.LauncherVersionQuestionnaireURL]{}, + gameVersionIDs: []values.GameVersionID{ + values.NewGameVersionIDFromUUID(gameVersionUUID1), + values.NewGameVersionIDFromUUID(gameVersionUUID2), + }, + createEditionErr: service.ErrDuplicateGame, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "無効なゲームバージョンIDが含まれているので400", + reqBody: &openapi.NewEdition{ + Name: editionName, + GameVersions: []uuid.UUID{gameVersionUUID1}, + }, + executeCreateEdition: true, + name: values.NewLauncherVersionName(editionName), + questionnaireURL: types.Option[values.LauncherVersionQuestionnaireURL]{}, + gameVersionIDs: []values.GameVersionID{values.NewGameVersionIDFromUUID(gameVersionUUID1)}, + createEditionErr: service.ErrInvalidGameVersionID, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "サービス層でエラーが発生したので500", + reqBody: &openapi.NewEdition{ + Name: editionName, + GameVersions: []uuid.UUID{gameVersionUUID1}, + }, + executeCreateEdition: true, + name: values.NewLauncherVersionName(editionName), + questionnaireURL: types.Option[values.LauncherVersionQuestionnaireURL]{}, + gameVersionIDs: []values.GameVersionID{values.NewGameVersionIDFromUUID(gameVersionUUID1)}, + createEditionErr: errors.New("internal error"), + isErr: true, + statusCode: http.StatusInternalServerError, + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.description, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockEditionService := mock.NewMockEdition(ctrl) + edition := NewEdition(mockEditionService) + + e := echo.New() + var req *http.Request + if testCase.invalidBody { + reqBody := bytes.NewBuffer([]byte("invalid")) + req = httptest.NewRequest(http.MethodPost, "/api/v2/editions", reqBody) + req.Header.Set("Content-Type", echo.MIMETextPlain) + } else { + reqBody := bytes.NewBuffer(nil) + if err := json.NewEncoder(reqBody).Encode(testCase.reqBody); err != nil { + t.Fatalf("failed to encode request body: %v", err) + } + req = httptest.NewRequest(http.MethodPost, "/api/v2/editions", reqBody) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + } + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + if testCase.executeCreateEdition { + mockEditionService. + EXPECT(). + CreateEdition( + gomock.Any(), + testCase.name, + testCase.questionnaireURL, + testCase.gameVersionIDs, + ). + Return(testCase.resultEdition, testCase.createEditionErr) + } + + err := edition.PostEdition(c) + + if testCase.isErr { + if testCase.statusCode != 0 { + var httpErr *echo.HTTPError + if errors.As(err, &httpErr) { + assert.Equal(t, testCase.statusCode, httpErr.Code) + } else { + t.Errorf("error is not *echo.HTTPError") + } + } else { + assert.Error(t, err) + } + return + } + + assert.NoError(t, err) + assert.Equal(t, testCase.statusCode, rec.Code) + t.Logf("テストケース: %s", testCase.description) + t.Logf("レスポンスコード: %d", rec.Code) + + if testCase.expectEdition != nil { + var res openapi.Edition + if err := json.NewDecoder(rec.Body).Decode(&res); err != nil { + t.Fatalf("failed to decode response body: %v", err) + } + + assert.Equal(t, testCase.expectEdition.Id, res.Id) + assert.Equal(t, testCase.expectEdition.Name, res.Name) + assert.Equal(t, testCase.expectEdition.Questionnaire, res.Questionnaire) + assert.WithinDuration(t, testCase.expectEdition.CreatedAt, res.CreatedAt, time.Second) + } + }) + } +} + +func TestDeleteEdition(t *testing.T) { + t.Parallel() + + editionID := uuid.New() + + type test struct { + description string + editionID openapi.EditionIDInPath + executeDeleteMock bool + launcherVersionID values.LauncherVersionID + deleteEditionErr error + isErr bool + statusCode int + } + + testCases := []test{ + { + description: "特に問題ないのでエラー無し", + editionID: editionID, + executeDeleteMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionID), + statusCode: http.StatusOK, + }, + { + description: "存在しないエディションIDなので400", + editionID: editionID, + executeDeleteMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionID), + deleteEditionErr: service.ErrInvalidEditionID, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "DeleteEditionがエラーなので500", + editionID: editionID, + executeDeleteMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionID), + deleteEditionErr: errors.New("internal error"), + isErr: true, + statusCode: http.StatusInternalServerError, + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.description, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockEditionService := mock.NewMockEdition(ctrl) + edition := NewEdition(mockEditionService) + + if testCase.executeDeleteMock { + mockEditionService. + EXPECT(). + DeleteEdition(gomock.Any(), testCase.launcherVersionID). + Return(testCase.deleteEditionErr) + } + + e := echo.New() + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/v2/editions/%s", testCase.editionID), nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := edition.DeleteEdition(c, testCase.editionID) + + if testCase.isErr { + if testCase.statusCode != 0 { + var httpErr *echo.HTTPError + if assert.ErrorAs(t, err, &httpErr, "error should be *echo.HTTPError") { + assert.Equal(t, testCase.statusCode, httpErr.Code) + } + } else { + assert.Error(t, err) + } + return + } + + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + }) + } +} + +func TestGetEdition(t *testing.T) { + t.Parallel() + + type test struct { + description string + editionID openapi.EditionIDInPath + resultEdition *domain.LauncherVersion + GetEditionErr error + expectEdition *openapi.Edition + isErr bool + statusCode int + } + + now := time.Now() + editionUUID := uuid.New() + editionID := values.NewLauncherVersionIDFromUUID(editionUUID) + editionName := values.NewLauncherVersionName("テストエディション") + strURL := "https://example.com/questionnaire" + questionnaireURL, err := url.Parse(strURL) + if err != nil { + t.Fatalf("failed to parse url: %v", err) + } + + testCases := []test{ + { + description: "アンケートURLありのエディションが取得できる", + editionID: editionUUID, + resultEdition: domain.NewLauncherVersionWithQuestionnaire( + editionID, + editionName, + values.NewLauncherVersionQuestionnaireURL(questionnaireURL), + now, + ), + expectEdition: &openapi.Edition{ + Id: editionUUID, + Name: string(editionName), + Questionnaire: &strURL, + CreatedAt: now, + }, + statusCode: http.StatusOK, + }, + { + description: "アンケートURLなしのエディションが取得できる", + editionID: editionUUID, + resultEdition: domain.NewLauncherVersionWithoutQuestionnaire( + editionID, + editionName, + now, + ), + expectEdition: &openapi.Edition{ + Id: editionUUID, + Name: string(editionName), + Questionnaire: nil, + CreatedAt: now, + }, + statusCode: http.StatusOK, + }, + { + description: "存在しないエディションIDなので400", + editionID: editionUUID, + GetEditionErr: service.ErrInvalidEditionID, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "GetEditionがエラーなので500", + editionID: editionUUID, + GetEditionErr: errors.New("internal error"), + isErr: true, + statusCode: http.StatusInternalServerError, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockEditionService := mock.NewMockEdition(ctrl) + edition := NewEdition(mockEditionService) + + mockEditionService. + EXPECT(). + GetEdition(gomock.Any(), values.NewLauncherVersionIDFromUUID(testCase.editionID)). + Return(testCase.resultEdition, testCase.GetEditionErr) + + e := echo.New() + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v2/editions/%s", testCase.editionID), nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := edition.GetEdition(c, testCase.editionID) + + if testCase.isErr { + if testCase.statusCode != 0 { + var httpErr *echo.HTTPError + if assert.ErrorAs(t, err, &httpErr, "error should be *echo.HTTPError") { + assert.Equal(t, testCase.statusCode, httpErr.Code) + } + } else { + assert.Error(t, err) + } + return + } + + assert.NoError(t, err) + assert.Equal(t, testCase.statusCode, rec.Code) + + var res openapi.Edition + err = json.NewDecoder(rec.Body).Decode(&res) + assert.NoError(t, err) + + assert.Equal(t, testCase.expectEdition.Id, res.Id) + assert.Equal(t, testCase.expectEdition.Name, res.Name) + assert.Equal(t, testCase.expectEdition.Questionnaire, res.Questionnaire) + assert.WithinDuration(t, testCase.expectEdition.CreatedAt, res.CreatedAt, time.Second) + }) + } +} + +func TestPatchEdition(t *testing.T) { + t.Parallel() + + type test struct { + description string + editionID openapi.EditionIDInPath + reqBody *openapi.PatchEdition + invalidBody bool + executeUpdateMock bool + launcherVersionID values.LauncherVersionID + name values.LauncherVersionName + questionnaireURL types.Option[values.LauncherVersionQuestionnaireURL] + updateEditionErr error + resultEdition *domain.LauncherVersion + isErr bool + statusCode int + expectedRes *openapi.Edition + } + + now := time.Now() + editionUUID := uuid.New() + editionID := values.NewLauncherVersionIDFromUUID(editionUUID) + editionName := "テストエディション" + strURL := "https://example.com/questionnaire" + invalidURL := " https://example.com/questionnaire with spaces" + longName := strings.Repeat("あ", 33) + questionnaireURL, err := url.Parse(strURL) + if err != nil { + t.Fatalf("failed to parse url: %v", err) + } + + testCases := []test{ + { + description: "特に問題ないのでエラーなし", + editionID: editionUUID, + reqBody: &openapi.PatchEdition{ + Name: editionName, + Questionnaire: &strURL, + }, + executeUpdateMock: true, + launcherVersionID: editionID, + name: values.NewLauncherVersionName(editionName), + questionnaireURL: types.NewOption(values.NewLauncherVersionQuestionnaireURL(questionnaireURL)), + resultEdition: domain.NewLauncherVersionWithQuestionnaire( + editionID, + values.NewLauncherVersionName(editionName), + values.NewLauncherVersionQuestionnaireURL(questionnaireURL), + now, + ), + expectedRes: &openapi.Edition{ + Id: editionUUID, + Name: editionName, + Questionnaire: &strURL, + CreatedAt: now, + }, + statusCode: http.StatusOK, + }, + { + description: "URLがなくてもエラーなし", + editionID: editionUUID, + reqBody: &openapi.PatchEdition{ + Name: editionName, + }, + executeUpdateMock: true, + launcherVersionID: editionID, + name: values.NewLauncherVersionName(editionName), + questionnaireURL: types.Option[values.LauncherVersionQuestionnaireURL]{}, + resultEdition: domain.NewLauncherVersionWithoutQuestionnaire( + editionID, + values.NewLauncherVersionName(editionName), + now, + ), + expectedRes: &openapi.Edition{ + Id: editionUUID, + Name: editionName, + Questionnaire: nil, + CreatedAt: now, + }, + statusCode: http.StatusOK, + }, + { + description: "リクエストボディが不正なので400", + editionID: editionUUID, + invalidBody: true, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "名前が空文字なので400", + editionID: editionUUID, + reqBody: &openapi.PatchEdition{ + Name: "", + }, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "名前が長すぎるので400", + editionID: editionUUID, + reqBody: &openapi.PatchEdition{ + Name: longName, + }, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "URLが正しくないので400", + editionID: editionUUID, + reqBody: &openapi.PatchEdition{ + Name: editionName, + Questionnaire: &invalidURL, + }, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "ErrInvalidEditionIDなので400", + editionID: editionUUID, + reqBody: &openapi.PatchEdition{ + Name: editionName, + }, + executeUpdateMock: true, + launcherVersionID: editionID, + name: values.NewLauncherVersionName(editionName), + questionnaireURL: types.Option[values.LauncherVersionQuestionnaireURL]{}, + updateEditionErr: service.ErrInvalidEditionID, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "ErrDuplicateGameVersionなので500", + editionID: editionUUID, + reqBody: &openapi.PatchEdition{ + Name: editionName, + }, + executeUpdateMock: true, + launcherVersionID: editionID, + name: values.NewLauncherVersionName(editionName), + questionnaireURL: types.Option[values.LauncherVersionQuestionnaireURL]{}, + updateEditionErr: service.ErrDuplicateGameVersion, + isErr: true, + statusCode: http.StatusInternalServerError, + }, + { + description: "ErrDuplicateGameなので500", + editionID: editionUUID, + reqBody: &openapi.PatchEdition{ + Name: editionName, + }, + executeUpdateMock: true, + launcherVersionID: editionID, + name: values.NewLauncherVersionName(editionName), + questionnaireURL: types.Option[values.LauncherVersionQuestionnaireURL]{}, + updateEditionErr: service.ErrDuplicateGame, + isErr: true, + statusCode: http.StatusInternalServerError, + }, + { + description: "サービス層でエラーなので500", + editionID: editionUUID, + reqBody: &openapi.PatchEdition{ + Name: editionName, + }, + executeUpdateMock: true, + launcherVersionID: editionID, + name: values.NewLauncherVersionName(editionName), + questionnaireURL: types.Option[values.LauncherVersionQuestionnaireURL]{}, + updateEditionErr: errors.New("internal error"), + isErr: true, + statusCode: http.StatusInternalServerError, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockEditionService := mock.NewMockEdition(ctrl) + edition := NewEdition(mockEditionService) + + if !testCase.invalidBody && testCase.executeUpdateMock { + mockEditionService. + EXPECT(). + UpdateEdition( + gomock.Any(), + testCase.launcherVersionID, + testCase.name, + testCase.questionnaireURL, + ). + Return(testCase.resultEdition, testCase.updateEditionErr) + } + + var reqBody []byte + var err error + if !testCase.invalidBody { + reqBody, err = json.Marshal(testCase.reqBody) + assert.NoError(t, err) + } else { + reqBody = []byte("invalid json") + } + + e := echo.New() + req := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/api/v2/editions/%s", testCase.editionID), bytes.NewReader(reqBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err = edition.PatchEdition(c, testCase.editionID) + + if testCase.isErr { + if testCase.statusCode != 0 { + var httpErr *echo.HTTPError + if assert.ErrorAs(t, err, &httpErr, "error should be *echo.HTTPError") { + assert.Equal(t, testCase.statusCode, httpErr.Code) + } + } else { + assert.Error(t, err) + } + return + } + + assert.NoError(t, err) + assert.Equal(t, testCase.statusCode, rec.Code) + + if testCase.expectedRes != nil { + var res openapi.Edition + err = json.NewDecoder(rec.Body).Decode(&res) + assert.NoError(t, err) + + assert.Equal(t, testCase.expectedRes.Id, res.Id) + assert.Equal(t, testCase.expectedRes.Name, res.Name) + assert.Equal(t, testCase.expectedRes.Questionnaire, res.Questionnaire) + assert.WithinDuration(t, testCase.expectedRes.CreatedAt, res.CreatedAt, time.Second) + } + }) + } +} + +func TestGetEditionGames(t *testing.T) { + t.Parallel() + + type test struct { + description string + editionID openapi.EditionIDInPath + gameVersions []*service.GameVersionWithGame + getEditionGamesErr error + expectGames []openapi.EditionGameResponse + isErr bool + statusCode int + } + + now := time.Now() + editionUUID := uuid.New() + gameID := values.NewGameID() + gameID2 := values.NewGameID() + gameVersionID := values.NewGameVersionID() + gameVersionID2 := values.NewGameVersionID() + imageID := values.NewGameImageID() + videoID := values.NewGameVideoID() + fileID1 := values.NewGameFileID() + fileID2 := values.NewGameFileID() + fileID1UUID := uuid.UUID(fileID1) + fileID2UUID := uuid.UUID(fileID2) + game1 := domain.NewGame( + gameID, + values.NewGameName("テストゲーム1"), + values.NewGameDescription("テスト説明1"), + values.GameVisibilityTypePublic, + now, + ) + game2 := domain.NewGame( + gameID2, + values.NewGameName("テストゲーム2"), + values.NewGameDescription("テスト説明2"), + values.GameVisibilityTypePrivate, + now, + ) + gameLimited := domain.NewGame( + gameID, + values.NewGameName("テストゲーム"), + values.NewGameDescription("テスト説明"), + values.GameVisibilityTypeLimited, + now, + ) + gameVersion := domain.NewGameVersion( + gameVersionID, + values.NewGameVersionName("v1.0.0"), + values.NewGameVersionDescription("リリース"), + now, + ) + gameVersion2 := domain.NewGameVersion( + gameVersionID2, + values.NewGameVersionName("v1.0.0"), + values.NewGameVersionDescription("リリース"), + now, + ) + + strURL := "https://example.com" + questionnaireURL, err := url.Parse(strURL) + if err != nil { + t.Fatalf("failed to parse url: %v", err) + } + urlValue := values.NewGameURLLink(questionnaireURL) + + testCases := []test{ + { + description: "特に問題ないのでエラーなし", + editionID: editionUUID, + gameVersions: []*service.GameVersionWithGame{ + { + Game: game1, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{ + URL: types.NewOption(urlValue), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム1", + Description: "テスト説明1", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Url: &strURL, + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "ゲームURLとゲームファイルがnullでもエラーなし", + editionID: editionUUID, + gameVersions: []*service.GameVersionWithGame{ + { + Game: gameLimited, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{}, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム", + Description: "テスト説明", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "windowsでもエラーなし", + editionID: editionUUID, + gameVersions: []*service.GameVersionWithGame{ + { + Game: game1, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{ + Windows: types.NewOption(fileID1), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム1", + Description: "テスト説明1", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Files: &openapi.GameVersionFiles{ + Win32: &fileID1UUID, + }, + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "macでもエラーなし", + editionID: editionUUID, + gameVersions: []*service.GameVersionWithGame{ + { + Game: game1, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{ + Mac: types.NewOption(fileID1), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム1", + Description: "テスト説明1", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Files: &openapi.GameVersionFiles{ + Darwin: &fileID1UUID, + }, + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "jarでもエラーなし", + editionID: editionUUID, + gameVersions: []*service.GameVersionWithGame{ + { + Game: game1, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{ + Jar: types.NewOption(fileID1), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム1", + Description: "テスト説明1", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Files: &openapi.GameVersionFiles{ + Jar: &fileID1UUID, + }, + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "ファイルが複数あってももエラーなし", + editionID: editionUUID, + gameVersions: []*service.GameVersionWithGame{ + { + Game: game1, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{ + Jar: types.NewOption(fileID1), + Windows: types.NewOption(fileID2), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム1", + Description: "テスト説明1", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Files: &openapi.GameVersionFiles{ + Jar: &fileID1UUID, + Win32: &fileID2UUID, + }, + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "ファイルとurlが両方あってもエラーなし", + editionID: editionUUID, + gameVersions: []*service.GameVersionWithGame{ + { + Game: game1, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{ + Windows: types.NewOption(fileID1), + URL: types.NewOption(urlValue), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム1", + Description: "テスト説明1", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Files: &openapi.GameVersionFiles{ + Win32: &fileID1UUID, + }, + Url: &strURL, + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "2つ以上のゲームがリストに含まれていてもエラーなし", + editionID: editionUUID, + gameVersions: []*service.GameVersionWithGame{ + { + Game: game1, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{ + URL: types.NewOption(urlValue), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + { + Game: game2, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion2, + Assets: &service.Assets{ + URL: types.NewOption(urlValue), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム1", + Description: "テスト説明1", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Url: &strURL, + }, + }, + { + Id: uuid.UUID(gameID2), + Name: "テストゲーム2", + Description: "テスト説明2", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID2), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Url: &strURL, + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "不正なeditionIDなので400", + editionID: editionUUID, + getEditionGamesErr: service.ErrInvalidEditionID, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "サービス層でエラーが発生したので500", + editionID: editionUUID, + getEditionGamesErr: errors.New("internal error"), + isErr: true, + statusCode: http.StatusInternalServerError, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockEditionService := mock.NewMockEdition(ctrl) + edition := NewEdition(mockEditionService) + + mockEditionService. + EXPECT(). + GetEditionGameVersions( + gomock.Any(), + values.NewLauncherVersionIDFromUUID(testCase.editionID), + ). + Return(testCase.gameVersions, testCase.getEditionGamesErr) + + e := echo.New() + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v2/editions/%s/games", testCase.editionID), nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := edition.GetEditionGames(c, testCase.editionID) + + if testCase.isErr { + if testCase.statusCode != 0 { + var httpErr *echo.HTTPError + if assert.ErrorAs(t, err, &httpErr, "error should be *echo.HTTPError") { + assert.Equal(t, testCase.statusCode, httpErr.Code) + } + } else { + assert.Error(t, err) + } + return + } + + assert.NoError(t, err) + assert.Equal(t, testCase.statusCode, rec.Code) + + var res []openapi.EditionGameResponse + err = json.NewDecoder(rec.Body).Decode(&res) + if err != nil { + t.Fatalf("failed to decode response body: %v", err) + } + + assert.Len(t, res, len(testCase.expectGames)) + for i, game := range res { + assert.Equal(t, testCase.expectGames[i].Id, game.Id) + assert.Equal(t, testCase.expectGames[i].Name, game.Name) + assert.Equal(t, testCase.expectGames[i].Description, game.Description) + assert.WithinDuration(t, testCase.expectGames[i].CreatedAt, game.CreatedAt, 2*time.Second) + + assert.Equal(t, testCase.expectGames[i].Version.Id, game.Version.Id) + assert.Equal(t, testCase.expectGames[i].Version.Name, game.Version.Name) + assert.Equal(t, testCase.expectGames[i].Version.Description, game.Version.Description) + assert.WithinDuration(t, testCase.expectGames[i].Version.CreatedAt, game.Version.CreatedAt, 2*time.Second) + assert.Equal(t, testCase.expectGames[i].Version.Url, game.Version.Url) + assert.Equal(t, testCase.expectGames[i].Version.Files, game.Version.Files) + assert.Equal(t, testCase.expectGames[i].Version.ImageID, game.Version.ImageID) + assert.Equal(t, testCase.expectGames[i].Version.VideoID, game.Version.VideoID) + } + }) + } +} + +func TestPatchEditionGame(t *testing.T) { + t.Parallel() + + type test struct { + description string + editionID openapi.EditionIDInPath + reqBody *openapi.PatchEditionGameRequest + invalidBody bool + executeUpdateMock bool + launcherVersionID values.LauncherVersionID + gameVersionIDs []values.GameVersionID + updateEditionGamesErr error + resultGameVersions []*service.GameVersionWithGame + expectGames []openapi.EditionGameResponse + isErr bool + statusCode int + } + + now := time.Now() + editionUUID := uuid.New() + gameID := values.NewGameID() + gameVersionID1 := values.NewGameVersionID() + gameVersionID2 := values.NewGameVersionID() + fileID1 := values.NewGameFileID() + fileID2 := values.NewGameFileID() + fileID1UUID := uuid.UUID(fileID1) + fileID2UUID := uuid.UUID(fileID2) + imageID := values.NewGameImageID() + videoID := values.NewGameVideoID() + game := domain.NewGame( + gameID, + values.NewGameName("テストゲーム"), + values.NewGameDescription("テスト説明"), + values.GameVisibilityTypePublic, + now, + ) + gameVersion := domain.NewGameVersion( + gameVersionID1, + values.NewGameVersionName("v1.0.0"), + values.NewGameVersionDescription("リリース"), + now, + ) + + strURL := "https://example.com" + questionnaireURL, err := url.Parse(strURL) + if err != nil { + t.Fatalf("failed to parse url: %v", err) + } + urlValue := values.NewGameURLLink(questionnaireURL) + + testCases := []test{ + { + description: "特に問題ないのでエラーなし", + editionID: editionUUID, + reqBody: &openapi.PatchEditionGameRequest{ + GameVersionIDs: []uuid.UUID{ + uuid.UUID(gameVersionID1), + uuid.UUID(gameVersionID2), + }, + }, + executeUpdateMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionUUID), + gameVersionIDs: []values.GameVersionID{ + gameVersionID1, + gameVersionID2, + }, + resultGameVersions: []*service.GameVersionWithGame{ + { + Game: game, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{}, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム", + Description: "テスト説明", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID1), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "空のゲームバージョン一覧でもエラーなし", + editionID: editionUUID, + reqBody: &openapi.PatchEditionGameRequest{ + GameVersionIDs: []uuid.UUID{}, + }, + executeUpdateMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionUUID), + gameVersionIDs: []values.GameVersionID{}, + resultGameVersions: []*service.GameVersionWithGame{}, + expectGames: []openapi.EditionGameResponse{}, + statusCode: http.StatusOK, + }, + { + description: "不正なリクエストボディなので400", + editionID: editionUUID, + invalidBody: true, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "不正なゲームバージョンIDなので400", + editionID: editionUUID, + reqBody: &openapi.PatchEditionGameRequest{ + GameVersionIDs: []uuid.UUID{uuid.UUID(gameVersionID1)}, + }, + executeUpdateMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionUUID), + gameVersionIDs: []values.GameVersionID{gameVersionID1}, + updateEditionGamesErr: service.ErrInvalidEditionID, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "ErrDuplicateGameVersionなので400", + editionID: editionUUID, + reqBody: &openapi.PatchEditionGameRequest{ + GameVersionIDs: []uuid.UUID{ + uuid.UUID(gameVersionID1), + uuid.UUID(gameVersionID1), + }, + }, + executeUpdateMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionUUID), + gameVersionIDs: []values.GameVersionID{gameVersionID1, gameVersionID1}, + updateEditionGamesErr: service.ErrDuplicateGameVersion, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "ErrDuplicateGameなので400", + editionID: editionUUID, + reqBody: &openapi.PatchEditionGameRequest{ + GameVersionIDs: []uuid.UUID{uuid.UUID(gameVersionID1)}, + }, + executeUpdateMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionUUID), + gameVersionIDs: []values.GameVersionID{gameVersionID1}, + updateEditionGamesErr: service.ErrDuplicateGame, + isErr: true, + statusCode: http.StatusBadRequest, + }, + { + description: "サービス層でエラーが発生したので500", + editionID: editionUUID, + reqBody: &openapi.PatchEditionGameRequest{ + GameVersionIDs: []uuid.UUID{uuid.UUID(gameVersionID1)}, + }, + executeUpdateMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionUUID), + gameVersionIDs: []values.GameVersionID{gameVersionID1}, + updateEditionGamesErr: errors.New("internal error"), + isErr: true, + statusCode: http.StatusInternalServerError, + }, + { + description: "windowsでもエラーなし", + editionID: editionUUID, + reqBody: &openapi.PatchEditionGameRequest{ + GameVersionIDs: []uuid.UUID{ + uuid.UUID(gameVersionID1), + uuid.UUID(gameVersionID2), + }, + }, + executeUpdateMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionUUID), + gameVersionIDs: []values.GameVersionID{ + gameVersionID1, + gameVersionID2, + }, + resultGameVersions: []*service.GameVersionWithGame{ + { + Game: game, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{ + Windows: types.NewOption(fileID1), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム", + Description: "テスト説明", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID1), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Files: &openapi.GameVersionFiles{ + Win32: &fileID1UUID, + }, + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "macファイルでもエラーなし", + editionID: editionUUID, + reqBody: &openapi.PatchEditionGameRequest{ + GameVersionIDs: []uuid.UUID{uuid.UUID(gameVersionID1)}, + }, + executeUpdateMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionUUID), + gameVersionIDs: []values.GameVersionID{gameVersionID1}, + resultGameVersions: []*service.GameVersionWithGame{ + { + Game: game, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{ + Mac: types.NewOption(fileID2), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム", + Description: "テスト説明", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID1), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Files: &openapi.GameVersionFiles{ + Darwin: &fileID2UUID, + }, + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "jarファイルでもエラーなし", + editionID: editionUUID, + reqBody: &openapi.PatchEditionGameRequest{ + GameVersionIDs: []uuid.UUID{uuid.UUID(gameVersionID1)}, + }, + executeUpdateMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionUUID), + gameVersionIDs: []values.GameVersionID{gameVersionID1}, + resultGameVersions: []*service.GameVersionWithGame{ + { + Game: game, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{ + Jar: types.NewOption(fileID1), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム", + Description: "テスト説明", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID1), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Files: &openapi.GameVersionFiles{ + Jar: &fileID1UUID, + }, + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "ファイルが複数でもエラーなし", + editionID: editionUUID, + reqBody: &openapi.PatchEditionGameRequest{ + GameVersionIDs: []uuid.UUID{uuid.UUID(gameVersionID1)}, + }, + executeUpdateMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionUUID), + gameVersionIDs: []values.GameVersionID{gameVersionID1}, + resultGameVersions: []*service.GameVersionWithGame{ + { + Game: game, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{ + Windows: types.NewOption(fileID1), + Mac: types.NewOption(fileID2), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム", + Description: "テスト説明", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID1), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Files: &openapi.GameVersionFiles{ + Win32: &fileID1UUID, + Darwin: &fileID2UUID, + }, + }, + }, + }, + statusCode: http.StatusOK, + }, + { + description: "urlとファイルでもエラーなし", + editionID: editionUUID, + reqBody: &openapi.PatchEditionGameRequest{ + GameVersionIDs: []uuid.UUID{uuid.UUID(gameVersionID1)}, + }, + executeUpdateMock: true, + launcherVersionID: values.NewLauncherVersionIDFromUUID(editionUUID), + gameVersionIDs: []values.GameVersionID{gameVersionID1}, + resultGameVersions: []*service.GameVersionWithGame{ + { + Game: game, + GameVersion: service.GameVersionInfo{ + GameVersion: gameVersion, + Assets: &service.Assets{ + URL: types.NewOption(urlValue), + Windows: types.NewOption(fileID1), + }, + ImageID: imageID, + VideoID: videoID, + }, + }, + }, + expectGames: []openapi.EditionGameResponse{ + { + Id: uuid.UUID(gameID), + Name: "テストゲーム", + Description: "テスト説明", + CreatedAt: now, + Version: openapi.GameVersion{ + Id: uuid.UUID(gameVersionID1), + Name: "v1.0.0", + Description: "リリース", + CreatedAt: now, + ImageID: uuid.UUID(imageID), + VideoID: uuid.UUID(videoID), + Url: &strURL, + Files: &openapi.GameVersionFiles{ + Win32: &fileID1UUID, + }, + }, + }, + }, + statusCode: http.StatusOK, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.description, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mockEditionService := mock.NewMockEdition(ctrl) + edition := NewEdition(mockEditionService) + + var reqBody []byte + var err error + if !testCase.invalidBody { + reqBody, err = json.Marshal(testCase.reqBody) + assert.NoError(t, err) + } else { + reqBody = []byte("invalid json") + } + + e := echo.New() + req := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/api/v2/editions/%s/games", testCase.editionID), bytes.NewReader(reqBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + if testCase.executeUpdateMock { + mockEditionService. + EXPECT(). + UpdateEditionGameVersions( + gomock.Any(), + testCase.launcherVersionID, + testCase.gameVersionIDs, + ). + Return(testCase.resultGameVersions, testCase.updateEditionGamesErr) + } + + err = edition.PatchEditionGame(c, testCase.editionID) + + if testCase.isErr { + if testCase.statusCode != 0 { + var httpErr *echo.HTTPError + if assert.ErrorAs(t, err, &httpErr, "error should be *echo.HTTPError") { + assert.Equal(t, testCase.statusCode, httpErr.Code) + } + } else { + assert.Error(t, err) + } + return + } + + assert.NoError(t, err) + assert.Equal(t, testCase.statusCode, rec.Code) + + if testCase.expectGames != nil { + var res []openapi.EditionGameResponse + err = json.NewDecoder(rec.Body).Decode(&res) + assert.NoError(t, err) + + assert.Len(t, res, len(testCase.expectGames)) + for i, game := range res { + assert.Equal(t, testCase.expectGames[i].Id, game.Id) + assert.Equal(t, testCase.expectGames[i].Name, game.Name) + assert.Equal(t, testCase.expectGames[i].Description, game.Description) + assert.WithinDuration(t, testCase.expectGames[i].CreatedAt, game.CreatedAt, 2*time.Second) + + assert.Equal(t, testCase.expectGames[i].Version.Id, game.Version.Id) + assert.Equal(t, testCase.expectGames[i].Version.Name, game.Version.Name) + assert.Equal(t, testCase.expectGames[i].Version.Description, game.Version.Description) + assert.WithinDuration(t, testCase.expectGames[i].Version.CreatedAt, game.Version.CreatedAt, 2*time.Second) + assert.Equal(t, testCase.expectGames[i].Version.Url, game.Version.Url) + assert.Equal(t, testCase.expectGames[i].Version.Files, game.Version.Files) + assert.Equal(t, testCase.expectGames[i].Version.ImageID, game.Version.ImageID) + assert.Equal(t, testCase.expectGames[i].Version.VideoID, game.Version.VideoID) + } + } + }) + } +} diff --git a/src/handler/v2/game_version_test.go b/src/handler/v2/game_version_test.go index 33fd6918..cb124c6d 100644 --- a/src/handler/v2/game_version_test.go +++ b/src/handler/v2/game_version_test.go @@ -552,7 +552,7 @@ func TestPostGameVersion(t *testing.T) { fileID2 := values.NewGameFileID() fileID1UUID := uuid.UUID(fileID1) fileID2UUID := uuid.UUID(fileID2) - invalidURL := " https://example.com" + invalidURL := " https://example.com with spaces" strURL := "https://example.com" urlLink, err := url.Parse(strURL) if err != nil { diff --git a/src/handler/v2/openapi/openapi.gen.go b/src/handler/v2/openapi/openapi.gen.go index 58785335..764c5678 100644 --- a/src/handler/v2/openapi/openapi.gen.go +++ b/src/handler/v2/openapi/openapi.gen.go @@ -823,8 +823,8 @@ type PostEditionAuthorizeJSONRequestBody = EditionAuthorizeRequest // PatchEditionJSONRequestBody defines body for PatchEdition for application/json ContentType. type PatchEditionJSONRequestBody = PatchEdition -// PostEditionGameJSONRequestBody defines body for PostEditionGame for application/json ContentType. -type PostEditionGameJSONRequestBody = PatchEditionGameRequest +// PatchEditionGameJSONRequestBody defines body for PatchEditionGame for application/json ContentType. +type PatchEditionGameJSONRequestBody = PatchEditionGameRequest // PostGameJSONRequestBody defines body for PostGame for application/json ContentType. type PostGameJSONRequestBody = NewGame @@ -896,7 +896,7 @@ type ServerInterface interface { GetEditionGames(ctx echo.Context, editionID EditionIDInPath) error // エディションのゲームの変更 // (PATCH /editions/{editionID}/games) - PostEditionGame(ctx echo.Context, editionID EditionIDInPath) error + PatchEditionGame(ctx echo.Context, editionID EditionIDInPath) error // プロダクトキーの一覧の取得 // (GET /editions/{editionID}/keys) GetProductKeys(ctx echo.Context, editionID EditionIDInPath, params GetProductKeysParams) error @@ -1174,8 +1174,8 @@ func (w *ServerInterfaceWrapper) GetEditionGames(ctx echo.Context) error { return err } -// PostEditionGame converts echo context to params. -func (w *ServerInterfaceWrapper) PostEditionGame(ctx echo.Context) error { +// PatchEditionGame converts echo context to params. +func (w *ServerInterfaceWrapper) PatchEditionGame(ctx echo.Context) error { var err error // ------------- Path parameter "editionID" ------------- var editionID EditionIDInPath @@ -1188,7 +1188,7 @@ func (w *ServerInterfaceWrapper) PostEditionGame(ctx echo.Context) error { ctx.Set(AdminAuthScopes, []string{}) // Invoke the callback with all the unmarshaled arguments - err = w.Handler.PostEditionGame(ctx, editionID) + err = w.Handler.PatchEditionGame(ctx, editionID) return err } @@ -2010,7 +2010,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/editions/:editionID", wrapper.GetEdition) router.PATCH(baseURL+"/editions/:editionID", wrapper.PatchEdition) router.GET(baseURL+"/editions/:editionID/games", wrapper.GetEditionGames) - router.PATCH(baseURL+"/editions/:editionID/games", wrapper.PostEditionGame) + router.PATCH(baseURL+"/editions/:editionID/games", wrapper.PatchEditionGame) router.GET(baseURL+"/editions/:editionID/keys", wrapper.GetProductKeys) router.POST(baseURL+"/editions/:editionID/keys", wrapper.PostProductKey) router.POST(baseURL+"/editions/:editionID/keys/:productKeyID/activate", wrapper.PostActivateProductKey) @@ -2156,82 +2156,82 @@ var swaggerSpec = []string{ "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", } // GetSwagger returns the content of the embedded swagger specification file