Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/openapi/v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2086,6 +2086,7 @@ paths:
## 期間の指定について
- startとendを両方指定しない場合:現在時刻から24時間前までの統計データを返します
- startのみ指定した場合:指定された開始時刻から現在時刻までの統計データを返します
- endのみ指定した場合:指定された終了時刻から24時間前までの統計データを返します
- startとendを両方指定した場合:指定された期間の統計データを返します
統計データは時台ごとに区切られており、各時台(例:14時台は14:00:00〜14:59:59)の統計値が含まれます。

Expand Down
56 changes: 53 additions & 3 deletions src/handler/v2/game_play_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"log"
"net/http"
"time"

"github.com/google/uuid"
"github.com/labstack/echo/v4"
Expand Down Expand Up @@ -98,7 +99,56 @@ func (gpl *GamePlayLog) GetGamePlayStats(_ echo.Context, _ openapi.GameIDInPath,

// エディションプレイ統計の取得
// (GET /editions/{editionID}/play-stats)
func (gpl *GamePlayLog) GetEditionPlayStats(_ echo.Context, _ openapi.EditionIDInPath, _ openapi.GetEditionPlayStatsParams) error {
// TODO: 実装が必要
return echo.NewHTTPError(http.StatusNotImplemented, "not implemented yet")
func (gpl *GamePlayLog) GetEditionPlayStats(c echo.Context, editionIDPath openapi.EditionIDInPath, params openapi.GetEditionPlayStatsParams) error {
ctx := c.Request().Context()

editionID := values.NewLauncherVersionIDFromUUID(editionIDPath)

var start, end time.Time

if params.End != nil {
end = *params.End
} else {
end = time.Now()
}
if params.Start != nil {
start = *params.Start
} else {
start = end.Add(-24 * time.Hour)
}

stats, err := gpl.gamePlayLogService.GetEditionPlayStats(ctx, editionID, start, end)
if errors.Is(err, service.ErrInvalidEdition) {
return echo.NewHTTPError(http.StatusNotFound, "edition not found")
}
if err != nil {
log.Printf("error: failed to get edition play stats: %v\n", err)
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get edition play stats")
}

res := openapi.EditionPlayStats{
EditionID: openapi.EditionID(stats.GetEditionID()),
EditionName: string(stats.GetEditionName()),
TotalPlayCount: stats.GetTotalPlayCount(),
TotalPlaySeconds: int(stats.GetTotalPlayTime().Seconds()),
GameStats: make([]openapi.GamePlayStatsInEdition, 0, len(stats.GetGameStats())),
HourlyStats: make([]openapi.HourlyPlayStats, 0, len(stats.GetHourlyStats())),
}

for _, gameStat := range stats.GetGameStats() {
res.GameStats = append(res.GameStats, openapi.GamePlayStatsInEdition{
GameID: openapi.GameID(gameStat.GetGameID()),
PlayCount: gameStat.GetPlayCount(),
PlayTime: int(gameStat.GetPlayTime().Seconds()),
})
}
for _, hourlyStat := range stats.GetHourlyStats() {
res.HourlyStats = append(res.HourlyStats, openapi.HourlyPlayStats{
StartTime: hourlyStat.GetStartTime(),
PlayCount: hourlyStat.GetPlayCount(),
PlayTime: int(hourlyStat.GetPlayTime().Seconds()),
})
}

return c.JSON(http.StatusOK, res)
}
243 changes: 243 additions & 0 deletions src/handler/v2/game_play_log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,246 @@ func TestPatchGamePlayLogEnd(t *testing.T) {
})
}
}

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

ctrl := gomock.NewController(t)

editionID := values.NewLauncherVersionID()
editionName := "Test Edition"
gameID1 := values.NewGameID()
gameID2 := values.NewGameID()

now := time.Now()
defaultStart := now.Add(-24 * time.Hour)
customStart := now.Add(-48 * time.Hour)
customEnd := now.Add(-24 * time.Hour)

gameStats := []*domain.GamePlayStatsInEdition{
domain.NewGamePlayStatsInEdition(gameID1, 10, 3600*time.Second),
domain.NewGamePlayStatsInEdition(gameID2, 5, 1800*time.Second),
}
hourlyStats := []*domain.HourlyPlayStats{
domain.NewHourlyPlayStats(customStart, 3, 900*time.Second),
domain.NewHourlyPlayStats(customStart.Add(time.Hour), 5, 1500*time.Second),
domain.NewHourlyPlayStats(customStart.Add(2*time.Hour), 7, 2000*time.Second),
}

editionStats := domain.NewEditionPlayStats(
editionID,
values.NewLauncherVersionName(editionName),
15,
5400*time.Second,
gameStats,
hourlyStats,
)

expectedGameStats := []openapi.GamePlayStatsInEdition{
{
GameID: openapi.GameID(gameID1),
PlayCount: 10,
PlayTime: 3600,
},
{
GameID: openapi.GameID(gameID2),
PlayCount: 5,
PlayTime: 1800,
},
}

expectedHourlyStats := []openapi.HourlyPlayStats{
{
StartTime: customStart,
PlayCount: 3,
PlayTime: 900,
},
{
StartTime: customStart.Add(time.Hour),
PlayCount: 5,
PlayTime: 1500,
},
{
StartTime: customStart.Add(2 * time.Hour),
PlayCount: 7,
PlayTime: 2000,
},
}

expectedEditionPlayStats := openapi.EditionPlayStats{
EditionID: openapi.EditionID(editionID),
EditionName: editionName,
TotalPlayCount: 15,
TotalPlaySeconds: 5400,
GameStats: expectedGameStats,
HourlyStats: expectedHourlyStats,
}

testCases := map[string]struct {
editionID values.LauncherVersionID
queryParams map[string]string
executeGetEditionStats bool
expectedStart time.Time
expectedEnd time.Time
editionStats *domain.EditionPlayStats
getEditionStatsErr error
expectedResponse openapi.EditionPlayStats
isError bool
statusCode int
}{
"クエリパラメータなしでエラーなし": {
editionID: editionID,
queryParams: map[string]string{},
executeGetEditionStats: true,
expectedStart: defaultStart,
expectedEnd: now,
editionStats: editionStats,
expectedResponse: expectedEditionPlayStats,
statusCode: http.StatusOK,
},
"start/endの両方を指定してもエラーなし": {
editionID: editionID,
queryParams: map[string]string{
"start": customStart.Format(time.RFC3339),
"end": customEnd.Format(time.RFC3339),
},
executeGetEditionStats: true,
expectedStart: customStart,
expectedEnd: customEnd,
editionStats: editionStats,
expectedResponse: expectedEditionPlayStats,
statusCode: http.StatusOK,
},
"startのみ指定でもエラーなし": {
editionID: editionID,
queryParams: map[string]string{
"start": customStart.Format(time.RFC3339),
},
executeGetEditionStats: true,
expectedStart: customStart,
expectedEnd: now,
editionStats: editionStats,
expectedResponse: expectedEditionPlayStats,
statusCode: http.StatusOK,
},
"endのみ指定でもエラーなし": {
editionID: editionID,
queryParams: map[string]string{
"end": customEnd.Format(time.RFC3339),
},
executeGetEditionStats: true,
expectedStart: customEnd.Add(-24 * time.Hour),
expectedEnd: customEnd,
editionStats: editionStats,
expectedResponse: expectedEditionPlayStats,
statusCode: http.StatusOK,
},
"GetEditionPlayStatsがErrInvalidEditionなので404": {
editionID: editionID,
queryParams: map[string]string{},
executeGetEditionStats: true,
expectedStart: defaultStart,
expectedEnd: now,
getEditionStatsErr: service.ErrInvalidEdition,
isError: true,
statusCode: http.StatusNotFound,
},
"GetEditionPlayStatsがその他のエラーなので500": {
editionID: editionID,
queryParams: map[string]string{},
executeGetEditionStats: true,
expectedStart: defaultStart,
expectedEnd: now,
getEditionStatsErr: assert.AnError,
isError: true,
statusCode: http.StatusInternalServerError,
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()

serviceMock := mock.NewMockGamePlayLogV2(ctrl)
h := NewGamePlayLog(serviceMock)

if testCase.executeGetEditionStats {
serviceMock.
EXPECT().
GetEditionPlayStats(
gomock.Any(),
testCase.editionID,
gomock.Cond(func(start time.Time) bool {
return start.Sub(testCase.expectedStart).Abs() < time.Hour
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここHourの幅はいらない気がするんですが、付けた意図は何ですか?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

意図していたのは秒の幅でしたのでその様に修正しました:pray:

}),
gomock.Cond(func(end time.Time) bool {
return end.Sub(testCase.expectedEnd).Abs() < time.Hour
}),
).
Return(testCase.editionStats, testCase.getEditionStatsErr)
}

url := fmt.Sprintf("/editions/%s/play-stats", uuid.UUID(testCase.editionID).String())
if len(testCase.queryParams) > 0 {
url += "?"
first := true
for key, value := range testCase.queryParams {
if !first {
url += "&"
}
url += fmt.Sprintf("%s=%s", key, value)
first = false
}
}

c, _, rec := setupTestRequest(t, http.MethodGet, url, nil)

params := openapi.GetEditionPlayStatsParams{}
if start, ok := testCase.queryParams["start"]; ok {
startTime, _ := time.Parse(time.RFC3339, start)
params.Start = &startTime
}
if end, ok := testCase.queryParams["end"]; ok {
endTime, _ := time.Parse(time.RFC3339, end)
params.End = &endTime
}

err := h.GetEditionPlayStats(c, openapi.EditionIDInPath(testCase.editionID), params)

if testCase.isError {
var httpError *echo.HTTPError
assert.ErrorAs(t, err, &httpError)
assert.Equal(t, testCase.statusCode, httpError.Code)
return
}

assert.NoError(t, err)
assert.Equal(t, testCase.statusCode, rec.Code)

if !testCase.isError {
var resBody openapi.EditionPlayStats
err = json.NewDecoder(rec.Body).Decode(&resBody)
assert.NoError(t, err)

assert.Equal(t, testCase.expectedResponse.EditionID, resBody.EditionID)
assert.Equal(t, testCase.expectedResponse.EditionName, resBody.EditionName)
assert.Equal(t, testCase.expectedResponse.TotalPlayCount, resBody.TotalPlayCount)
assert.Equal(t, testCase.expectedResponse.TotalPlaySeconds, resBody.TotalPlaySeconds)

assert.Len(t, resBody.GameStats, len(testCase.expectedResponse.GameStats))
for i, expectedGame := range testCase.expectedResponse.GameStats {
assert.Equal(t, expectedGame.GameID, resBody.GameStats[i].GameID)
assert.Equal(t, expectedGame.PlayCount, resBody.GameStats[i].PlayCount)
assert.Equal(t, expectedGame.PlayTime, resBody.GameStats[i].PlayTime)
}

assert.Len(t, resBody.HourlyStats, len(testCase.expectedResponse.HourlyStats))
for i, expectedHourly := range testCase.expectedResponse.HourlyStats {
assert.WithinDuration(t, expectedHourly.StartTime, resBody.HourlyStats[i].StartTime, time.Second)
assert.Equal(t, expectedHourly.PlayCount, resBody.HourlyStats[i].PlayCount)
assert.Equal(t, expectedHourly.PlayTime, resBody.HourlyStats[i].PlayTime)
}
}
})
}
}
Loading