Skip to content

Commit 0a395ec

Browse files
Merge pull request #1413 from traPtitech/GetGamePlayStats_in_handler
Get game play stats in handler
2 parents 9907c1a + 9f00a2d commit 0a395ec

File tree

2 files changed

+276
-3
lines changed

2 files changed

+276
-3
lines changed

src/handler/v2/game_play_log.go

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,62 @@ func (gpl *GamePlayLog) PatchGamePlayLogEnd(c echo.Context, editionIDPath openap
9292

9393
// ゲームプレイ統計の取得
9494
// (GET /games/{gameID}/play-stats)
95-
func (gpl *GamePlayLog) GetGamePlayStats(_ echo.Context, _ openapi.GameIDInPath, _ openapi.GetGamePlayStatsParams) error {
96-
// TODO: 実装が必要
97-
return echo.NewHTTPError(http.StatusNotImplemented, "not implemented yet")
95+
func (gpl *GamePlayLog) GetGamePlayStats(c echo.Context, gameIDPath openapi.GameIDInPath, params openapi.GetGamePlayStatsParams) error {
96+
ctx := c.Request().Context()
97+
98+
gameID := values.NewGameIDFromUUID(gameIDPath)
99+
100+
var gameVersionID *values.GameVersionID
101+
if params.GameVersionID != nil {
102+
vID := values.NewGameVersionIDFromUUID(*params.GameVersionID)
103+
gameVersionID = &vID
104+
}
105+
106+
var start, end time.Time
107+
if params.End != nil {
108+
end = *params.End
109+
} else {
110+
end = time.Now()
111+
}
112+
if params.Start != nil {
113+
start = *params.Start
114+
} else {
115+
start = end.Add(-24 * time.Hour)
116+
}
117+
118+
// Serviceの呼び出し
119+
stats, err := gpl.gamePlayLogService.GetGamePlayStats(ctx, gameID, gameVersionID, start, end)
120+
if errors.Is(err, service.ErrInvalidGame) {
121+
return echo.NewHTTPError(http.StatusNotFound, "game not found")
122+
}
123+
if errors.Is(err, service.ErrInvalidTimeRange) {
124+
return echo.NewHTTPError(http.StatusBadRequest, "invalid time range")
125+
}
126+
if errors.Is(err, service.ErrTimePeriodTooLong) {
127+
return echo.NewHTTPError(http.StatusBadRequest, "time period too long")
128+
}
129+
if err != nil {
130+
log.Printf("get game play stats: %v\n", err)
131+
return echo.NewHTTPError(http.StatusInternalServerError, "get game play stats")
132+
}
133+
134+
hourlyStats := make([]openapi.HourlyPlayStats, 0, len(stats.GetHourlyStats()))
135+
for _, hourlyStat := range stats.GetHourlyStats() {
136+
hourlyStats = append(hourlyStats, openapi.HourlyPlayStats{
137+
StartTime: hourlyStat.GetStartTime(),
138+
PlayCount: hourlyStat.GetPlayCount(),
139+
PlayTime: int(hourlyStat.GetPlayTime().Seconds()),
140+
})
141+
}
142+
143+
res := openapi.GamePlayStats{
144+
GameID: uuid.UUID(stats.GetGameID()),
145+
TotalPlayCount: stats.GetTotalPlayCount(),
146+
TotalPlaySeconds: int(stats.GetTotalPlayTime().Seconds()),
147+
HourlyStats: hourlyStats,
148+
}
149+
150+
return c.JSON(http.StatusOK, res)
98151
}
99152

100153
// エディションプレイ統計の取得

src/handler/v2/game_play_log_test.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,3 +553,223 @@ func TestGetEditionPlayStats(t *testing.T) {
553553
})
554554
}
555555
}
556+
557+
func TestGetGamePlayStats(t *testing.T) {
558+
t.Parallel()
559+
ctrl := gomock.NewController(t)
560+
561+
gameID := values.NewGameID()
562+
gameVersionID := values.NewGameVersionID()
563+
now := time.Now()
564+
defaultStart := now.Add(-24 * time.Hour)
565+
customStart := now.Add(-48 * time.Hour)
566+
customEnd := now.Add(-24 * time.Hour)
567+
568+
mockHourlyStats := []*domain.HourlyPlayStats{
569+
domain.NewHourlyPlayStats(now.Truncate(time.Hour), 5, 120*time.Second),
570+
}
571+
mockStats := domain.NewGamePlayStats(
572+
gameID,
573+
10,
574+
300*time.Second,
575+
mockHourlyStats,
576+
)
577+
578+
expectedHourlyStats := []openapi.HourlyPlayStats{
579+
{
580+
StartTime: now.Truncate(time.Hour),
581+
PlayCount: 5,
582+
PlayTime: 120,
583+
},
584+
}
585+
expectedGamePlayStats := openapi.GamePlayStats{
586+
GameID: uuid.UUID(gameID),
587+
TotalPlayCount: 10,
588+
TotalPlaySeconds: 300,
589+
HourlyStats: expectedHourlyStats,
590+
}
591+
592+
testCases := map[string]struct {
593+
gameID values.GameID
594+
queryParams map[string]string
595+
executeGetGamePlayStats bool
596+
expectedGameVersionID *values.GameVersionID
597+
expectedStart time.Time
598+
expectedEnd time.Time
599+
getGamePlayStatsResult *domain.GamePlayStats
600+
getGamePlayStatsErr error
601+
expectedResponse openapi.GamePlayStats
602+
isError bool
603+
statusCode int
604+
}{
605+
"クエリパラメータなし": {
606+
gameID: gameID,
607+
queryParams: map[string]string{},
608+
executeGetGamePlayStats: true,
609+
expectedGameVersionID: nil,
610+
expectedStart: defaultStart,
611+
expectedEnd: now,
612+
getGamePlayStatsResult: mockStats,
613+
expectedResponse: expectedGamePlayStats,
614+
statusCode: http.StatusOK,
615+
},
616+
"正常系: game_version_idあり": {
617+
gameID: gameID,
618+
queryParams: map[string]string{
619+
"game_version_id": uuid.UUID(gameVersionID).String(),
620+
},
621+
executeGetGamePlayStats: true,
622+
expectedGameVersionID: &gameVersionID,
623+
expectedStart: defaultStart,
624+
expectedEnd: now,
625+
getGamePlayStatsResult: mockStats,
626+
expectedResponse: expectedGamePlayStats,
627+
statusCode: http.StatusOK,
628+
},
629+
"正常系: start, end指定": {
630+
gameID: gameID,
631+
queryParams: map[string]string{
632+
"start": customStart.Format(time.RFC3339),
633+
"end": customEnd.Format(time.RFC3339),
634+
},
635+
executeGetGamePlayStats: true,
636+
expectedGameVersionID: nil,
637+
expectedStart: customStart,
638+
expectedEnd: customEnd,
639+
getGamePlayStatsResult: mockStats,
640+
expectedResponse: expectedGamePlayStats,
641+
statusCode: http.StatusOK,
642+
},
643+
"異常系:404 serviceでErrInvalidGame": {
644+
gameID: gameID,
645+
queryParams: map[string]string{},
646+
executeGetGamePlayStats: true,
647+
expectedStart: defaultStart,
648+
expectedEnd: now,
649+
getGamePlayStatsErr: service.ErrInvalidGame,
650+
isError: true,
651+
statusCode: http.StatusNotFound,
652+
},
653+
"異常系: 400 serviceでErrInvalidTimeRange": {
654+
gameID: gameID,
655+
queryParams: map[string]string{},
656+
executeGetGamePlayStats: true,
657+
expectedStart: defaultStart,
658+
expectedEnd: now,
659+
getGamePlayStatsErr: service.ErrInvalidTimeRange,
660+
isError: true,
661+
statusCode: http.StatusBadRequest,
662+
},
663+
"異常系:400 serviceでErrTimePeriodTooLong": {
664+
gameID: gameID,
665+
queryParams: map[string]string{},
666+
executeGetGamePlayStats: true,
667+
expectedStart: defaultStart,
668+
expectedEnd: now,
669+
getGamePlayStatsErr: service.ErrTimePeriodTooLong,
670+
isError: true,
671+
statusCode: http.StatusBadRequest,
672+
},
673+
"異常系:500 serviceでその他のエラー": {
674+
gameID: gameID,
675+
queryParams: map[string]string{},
676+
executeGetGamePlayStats: true,
677+
expectedStart: defaultStart,
678+
expectedEnd: now,
679+
getGamePlayStatsErr: assert.AnError,
680+
isError: true,
681+
statusCode: http.StatusInternalServerError,
682+
},
683+
}
684+
685+
for name, tt := range testCases {
686+
t.Run(name, func(t *testing.T) {
687+
t.Parallel()
688+
689+
serviceMock := mock.NewMockGamePlayLogV2(ctrl)
690+
h := NewGamePlayLog(serviceMock)
691+
692+
if tt.executeGetGamePlayStats {
693+
serviceMock.
694+
EXPECT().
695+
GetGamePlayStats(
696+
gomock.Any(),
697+
tt.gameID,
698+
tt.expectedGameVersionID,
699+
gomock.Cond(func(start time.Time) bool {
700+
return start.Sub(tt.expectedStart).Abs() < time.Second
701+
}),
702+
gomock.Cond(func(end time.Time) bool {
703+
return end.Sub(tt.expectedEnd).Abs() < time.Second
704+
}),
705+
).
706+
Return(tt.getGamePlayStatsResult, tt.getGamePlayStatsErr)
707+
}
708+
709+
url := fmt.Sprintf("/games/%s/play-stats", uuid.UUID(tt.gameID).String())
710+
if len(tt.queryParams) > 0 {
711+
url += "?"
712+
first := true
713+
for key, value := range tt.queryParams {
714+
if !first {
715+
url += "&"
716+
}
717+
url += fmt.Sprintf("%s=%s", key, value)
718+
first = false
719+
}
720+
}
721+
722+
c, _, rec := setupTestRequest(t, http.MethodGet, url, nil)
723+
724+
var params openapi.GetGamePlayStatsParams
725+
if v, ok := tt.queryParams["game_version_id"]; ok {
726+
parsed, err := uuid.Parse(v)
727+
if err == nil {
728+
params.GameVersionID = &parsed
729+
}
730+
}
731+
if v, ok := tt.queryParams["start"]; ok {
732+
parsed, err := time.Parse(time.RFC3339, v)
733+
if err == nil {
734+
params.Start = &parsed
735+
}
736+
}
737+
if v, ok := tt.queryParams["end"]; ok {
738+
parsed, err := time.Parse(time.RFC3339, v)
739+
if err == nil {
740+
params.End = &parsed
741+
}
742+
}
743+
744+
err := h.GetGamePlayStats(c, openapi.GameIDInPath(tt.gameID), params)
745+
746+
if tt.isError {
747+
var httpError *echo.HTTPError
748+
if assert.ErrorAs(t, err, &httpError) {
749+
assert.Equal(t, tt.statusCode, httpError.Code)
750+
}
751+
return
752+
}
753+
754+
assert.NoError(t, err)
755+
assert.Equal(t, tt.statusCode, rec.Code)
756+
757+
var resBody openapi.GamePlayStats
758+
err = json.NewDecoder(rec.Body).Decode(&resBody)
759+
assert.NoError(t, err)
760+
761+
// GameID, TotalPlayCount, TotalPlaySecondsのチェック
762+
assert.Equal(t, expectedGamePlayStats.GameID, resBody.GameID)
763+
assert.Equal(t, expectedGamePlayStats.TotalPlayCount, resBody.TotalPlayCount)
764+
assert.Equal(t, expectedGamePlayStats.TotalPlaySeconds, resBody.TotalPlaySeconds)
765+
766+
assert.Len(t, resBody.HourlyStats, len(expectedGamePlayStats.HourlyStats))
767+
for i, expectedHourly := range expectedGamePlayStats.HourlyStats {
768+
assert.WithinDuration(t, expectedHourly.StartTime, resBody.HourlyStats[i].StartTime, time.Second)
769+
assert.Equal(t, expectedHourly.PlayCount, resBody.HourlyStats[i].PlayCount)
770+
assert.Equal(t, expectedHourly.PlayTime, resBody.HourlyStats[i].PlayTime)
771+
}
772+
773+
})
774+
}
775+
}

0 commit comments

Comments
 (0)