Skip to content

Commit

Permalink
Merge pull request #1023 from traPtitech/feat/patch_game_genre
Browse files Browse the repository at this point in the history
`PATCH` /genres/:genreID`の実装
  • Loading branch information
ikura-hamu authored Oct 27, 2024
2 parents 5cc251f + c376176 commit 77614ff
Show file tree
Hide file tree
Showing 9 changed files with 906 additions and 13 deletions.
63 changes: 50 additions & 13 deletions src/handler/v2/game_genre.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ import (
)

type GameGenre struct {
gameGenreService service.GameGenre
game service.GameV2
session *Session
gameGenreUnimplemented //実装し終わったら消す
gameGenreService service.GameGenre
game service.GameV2
session *Session
}

func NewGameGenre(gameGenreService service.GameGenre, game service.GameV2, session *Session) *GameGenre {
Expand All @@ -27,15 +26,6 @@ func NewGameGenre(gameGenreService service.GameGenre, game service.GameV2, sessi
}
}

// gameGenreUnimplemented
// メソッドとして実装予定だが、未実装のもの
// TODO: 実装
type gameGenreUnimplemented interface {
// ジャンル情報の変更
// (PATCH /genres/{gameGenreID})
PatchGameGenre(ctx echo.Context, gameGenreID openapi.GameGenreIDInPath) error
}

// ジャンルの削除
// (DELETE /genres/{gameGenreID})
func (gameGenre *GameGenre) DeleteGameGenre(c echo.Context, gameGenreID openapi.GameGenreIDInPath) error {
Expand Down Expand Up @@ -178,3 +168,50 @@ func (gameGenre *GameGenre) PutGameGenres(c echo.Context, gameID openapi.GameIDI

return c.JSON(http.StatusOK, res)
}

// ジャンル情報の変更
// (PATCH /genres/{gameGenreID})
func (gameGenre *GameGenre) PatchGameGenre(c echo.Context, gameGenreID openapi.GameGenreIDInPath) error {
var reqBody openapi.PatchGameGenreJSONRequestBody
if err := c.Bind(&reqBody); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
}

gameGenreName := values.NewGameGenreName(reqBody.Genre)
if err := gameGenreName.Validate(); err != nil {
if errors.Is(err, values.ErrGameGenreNameEmpty) {
return echo.NewHTTPError(http.StatusBadRequest, "genre name must not be empty")
}
if errors.Is(err, values.ErrGameGenreNameTooLong) {
return echo.NewHTTPError(http.StatusBadRequest, "genre name is too long")
}
log.Printf("failed to validate genre name: %v\n", err)
return echo.NewHTTPError(http.StatusInternalServerError, "failed to validate genre name")
}

ctx := c.Request().Context()

gameGenreInfo, err := gameGenre.gameGenreService.UpdateGameGenre(ctx, values.GameGenreIDFromUUID(gameGenreID), gameGenreName)
if errors.Is(err, service.ErrNoGameGenre) {
return echo.NewHTTPError(http.StatusNotFound, "game genre not found")
}
if errors.Is(err, service.ErrDuplicateGameGenreName) {
return echo.NewHTTPError(http.StatusBadRequest, "duplicate genre name")
}
if errors.Is(err, service.ErrNoGameGenreUpdated) {
return echo.NewHTTPError(http.StatusBadRequest, "no game genre updated")
}
if err != nil {
log.Printf("error: failed to update game genre: %v\n", err)
return echo.NewHTTPError(http.StatusInternalServerError, "failed to update game genre")
}

res := openapi.GameGenre{
Id: uuid.UUID(gameGenreInfo.GetID()),
Genre: string(gameGenreInfo.GetName()),
Num: gameGenreInfo.Num,
CreatedAt: gameGenreInfo.GetCreatedAt(),
}

return c.JSON(http.StatusOK, res)
}
207 changes: 207 additions & 0 deletions src/handler/v2/game_genre_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
mockConfig "github.com/traPtitech/trap-collection-server/src/config/mock"
"github.com/traPtitech/trap-collection-server/src/domain"
"github.com/traPtitech/trap-collection-server/src/domain/values"
Expand Down Expand Up @@ -601,3 +602,209 @@ func TestPutGameGenres(t *testing.T) {
})
}
}

func TestPatchGameGenre(t *testing.T) {
t.Parallel()

ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockGameGenreService := mock.NewMockGameGenre(ctrl)
mockGameService := mock.NewMockGameV2(ctrl)

mockConf := mockConfig.NewMockHandler(ctrl)
mockConf.
EXPECT().
SessionKey().
Return("key", nil)
mockConf.
EXPECT().
SessionSecret().
Return("secret", nil)
sess, err := common.NewSession(mockConf)
if err != nil {
t.Fatalf("failed to create session: %v", err)
return
}
session, err := NewSession(sess)
if err != nil {
t.Fatalf("failed to create session: %v", err)
return
}

gameGenreHandler := NewGameGenre(mockGameGenreService, mockGameService, session)

gameGenreID := uuid.New()

testCases := map[string]struct {
gameGenreID openapi.GameGenreIDInPath
req openapi.PatchGameGenreJSONRequestBody
invalidRequestBody bool
executeUpdateGame bool
UpdateGameGenreErr error
gameGenre *service.GameGenreInfo
resBody openapi.GameGenre
statusCode int
isErr bool
expectedErr error
}{
"特に問題ないのでエラー無し": {
gameGenreID: gameGenreID,
req: openapi.PatchGameGenreJSONRequestBody{
Genre: "new genre",
},
executeUpdateGame: true,
gameGenre: &service.GameGenreInfo{
GameGenre: *domain.NewGameGenre(values.GameGenreID(gameGenreID), values.NewGameGenreName("new genre"), time.Now()),
Num: 1,
},
resBody: openapi.GameGenre{
Id: uuid.UUID(gameGenreID),
Genre: "new genre",
CreatedAt: time.Now(),
Num: 1,
},
statusCode: http.StatusOK,
},
"リクエストボディがおかしいので400": {
gameGenreID: gameGenreID,
invalidRequestBody: true,
isErr: true,
statusCode: http.StatusBadRequest,
},
"ジャンル名が空なので400": {
gameGenreID: gameGenreID,
req: openapi.PatchGameGenreJSONRequestBody{
Genre: "",
},
isErr: true,
statusCode: http.StatusBadRequest,
},
"ジャンル名が長すぎるので400": {
gameGenreID: gameGenreID,
req: openapi.PatchGameGenreJSONRequestBody{
Genre: strings.Repeat("a", 100),
},
isErr: true,
statusCode: http.StatusBadRequest,
},
"UpdateGameGenreがErrNoGameGenreなので404": {
gameGenreID: gameGenreID,
req: openapi.PatchGameGenreJSONRequestBody{Genre: "new genre"},
executeUpdateGame: true,
UpdateGameGenreErr: service.ErrNoGameGenre,
isErr: true,
statusCode: http.StatusNotFound,
},
"UpdateGameGenreがErrDuplicateGameGenreNameなので400": {
gameGenreID: gameGenreID,
req: openapi.PatchGameGenreJSONRequestBody{Genre: "new genre"},
executeUpdateGame: true,
UpdateGameGenreErr: service.ErrDuplicateGameGenreName,
isErr: true,
statusCode: http.StatusBadRequest,
},
"UpdateGameGenreがErrNoGameGenreUpdatedなので400": {
gameGenreID: gameGenreID,
req: openapi.PatchGameGenreJSONRequestBody{Genre: "new genre"},
executeUpdateGame: true,
UpdateGameGenreErr: service.ErrNoGameGenreUpdated,
isErr: true,
statusCode: http.StatusBadRequest,
},
"UpdateGameGenreがエラーなので500": {
gameGenreID: gameGenreID,
req: openapi.PatchGameGenreJSONRequestBody{Genre: "new genre"},
executeUpdateGame: true,
UpdateGameGenreErr: errors.New("test error"),
isErr: true,
statusCode: http.StatusInternalServerError,
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
reqBody := new(bytes.Buffer)
if !testCase.invalidRequestBody {
err := json.NewEncoder(reqBody).Encode(testCase.req)
if err != nil {
t.Fatalf("failed to encode request body: %v", err)
}
} else {
_, err := strings.NewReader("invalid request body").WriteTo(reqBody)
if err != nil {
t.Fatalf("failed to write to request body: %v", err)
}
}

e := echo.New()
req := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/api/v2/genres/%s", testCase.gameGenreID), reqBody)
req.Header.Set(echo.HeaderContentType, "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

sess, err := session.New(req)
if err != nil {
t.Fatal(err)
}

authSession := domain.NewOIDCSession(values.NewOIDCAccessToken("token"), time.Now().Add(time.Hour))

sess.Values[accessTokenSessionKey] = string(authSession.GetAccessToken())
sess.Values[expiresAtSessionKey] = authSession.GetExpiresAt()

err = sess.Save(req, rec)
if err != nil {
t.Fatalf("failed to save session: %v", err)
}

setCookieHeader(c)

sess, err = session.Get(req)
if err != nil {
t.Fatal(err)
}

c.Set("session", sess)

if testCase.executeUpdateGame {
mockGameGenreService.
EXPECT().
UpdateGameGenre(gomock.Any(), values.GameGenreIDFromUUID(testCase.gameGenreID), values.NewGameGenreName(testCase.req.Genre)).
Return(testCase.gameGenre, testCase.UpdateGameGenreErr)
}

err = gameGenreHandler.PatchGameGenre(c, testCase.gameGenreID)

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("err must be http error, but not http error: %v", err)
}
} else {
if testCase.expectedErr != nil {
assert.ErrorIs(t, err, testCase.expectedErr)
} else {
assert.Error(t, err)
}
}
}

if testCase.isErr {
return
}

var res openapi.GameGenre
err = json.NewDecoder(rec.Body).Decode(&res)
require.NoError(t, err)

assert.Equal(t, testCase.resBody.Id, res.Id)
assert.Equal(t, testCase.resBody.Genre, res.Genre)
assert.Equal(t, testCase.resBody.Num, res.Num)
assert.WithinDuration(t, testCase.resBody.CreatedAt, res.CreatedAt, time.Second)
})
}
}
12 changes: 12 additions & 0 deletions src/repository/game_genre.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ type GameGenre interface {
// ゲームジャンルが存在しない場合は、ErrIncludeInvalidArgsを返す。
// ゲームジャンルを追加するのではなく、置き換える。
RegisterGenresToGame(ctx context.Context, gameID values.GameID, gameGenres []values.GameGenreID) error
// UpdateGameGenre
// ゲームジャンルの情報を更新する。
// ゲームジャンルが存在しない場合、または変更が起きなかった場合は、ErrNoRecordUpdatedを返す。
// ゲームジャンルの名前が重複する場合は、ErrDuplicatedUniqueKeyを返す。
UpdateGameGenre(ctx context.Context, gameGenre *domain.GameGenre) error
// GetGameGenre
// ゲームジャンルのIDからゲームジャンルを取得する。
// ゲームジャンルが存在しない場合は、ErrRecordNotFoundを返す。
GetGameGenre(ctx context.Context, gameGenreID values.GameGenreID) (*domain.GameGenre, error)
// GetGamesByGenreID
// ゲームジャンルのIDからそのジャンルに含まれるゲームを取得する。
GetGamesByGenreID(ctx context.Context, gameGenreID values.GameGenreID) ([]*domain.Game, error)
}

type GameGenreInfo struct {
Expand Down
Loading

0 comments on commit 77614ff

Please sign in to comment.