Finished '2.4 Leaderboard Service (Port 8083)'

master
oabrivard 1 month ago
parent 80003d19ca
commit 7857331a1f

@ -77,7 +77,11 @@ func (r *fakeRepo) IngestEntry(
return &cp, false, nil return &cp, false, nil
} }
func (r *fakeRepo) ListTop(ctx context.Context, filter domain.TopFilter, limit int) ([]*domain.LeaderboardEntry, error) { func (r *fakeRepo) ListTop(
ctx context.Context,
filter domain.TopFilter,
limit int,
) ([]*domain.LeaderboardEntry, error) {
if len(r.entries) < limit { if len(r.entries) < limit {
limit = len(r.entries) limit = len(r.entries)
} }

@ -225,7 +225,10 @@ LIMIT $2 OFFSET $3`
} }
// GetGlobalStats computes global leaderboard statistics. // GetGlobalStats computes global leaderboard statistics.
func (r *LeaderboardRepository) GetGlobalStats(ctx context.Context, filter domain.TopFilter) (*domain.GlobalStats, error) { func (r *LeaderboardRepository) GetGlobalStats(
ctx context.Context,
filter domain.TopFilter,
) (*domain.GlobalStats, error) {
where, args := buildFilterWhere(filter) where, args := buildFilterWhere(filter)
q := ` q := `
SELECT SELECT
@ -340,7 +343,10 @@ DO UPDATE SET
) )
ELSE leaderboard_player_stats.best_duration_seconds ELSE leaderboard_player_stats.best_duration_seconds
END, END,
last_played_at = GREATEST(COALESCE(leaderboard_player_stats.last_played_at, EXCLUDED.last_played_at), EXCLUDED.last_played_at), last_played_at = GREATEST(
COALESCE(leaderboard_player_stats.last_played_at, EXCLUDED.last_played_at),
EXCLUDED.last_played_at
),
updated_at = NOW()` updated_at = NOW()`
_, err := tx.Exec(ctx, q, _, err := tx.Exec(ctx, q,
@ -365,6 +371,8 @@ func buildFilterWhere(filter domain.TopFilter) (string, []any) {
parts = append(parts, "completion_type=$"+strconvI(len(args))) parts = append(parts, "completion_type=$"+strconvI(len(args)))
} }
switch filter.Window { switch filter.Window {
case domain.WindowAll:
// no time filter
case domain.Window24h: case domain.Window24h:
args = append(args, time.Now().UTC().Add(-24*time.Hour)) args = append(args, time.Now().UTC().Add(-24*time.Hour))
parts = append(parts, "completed_at>=$"+strconvI(len(args))) parts = append(parts, "completed_at>=$"+strconvI(len(args)))

@ -68,7 +68,11 @@ func (r *inMemoryRepo) IngestEntry(
} }
return &cp, false, nil return &cp, false, nil
} }
func (r *inMemoryRepo) ListTop(ctx context.Context, filter domain.TopFilter, limit int) ([]*domain.LeaderboardEntry, error) { func (r *inMemoryRepo) ListTop(
ctx context.Context,
filter domain.TopFilter,
limit int,
) ([]*domain.LeaderboardEntry, error) {
if len(r.entries) < limit { if len(r.entries) < limit {
limit = len(r.entries) limit = len(r.entries)
} }
@ -170,12 +174,18 @@ func TestUpdateAndTop10(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/leaderboard/update", bytes.NewReader(payload)) req := httptest.NewRequest(http.MethodPost, "/leaderboard/update", bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer service") req.Header.Set("Authorization", "Bearer service")
resp := sharedhttpx.MustTest(t, app, req) {
sharedhttpx.AssertStatusAndClose(t, resp, http.StatusOK, "update failed") resp := sharedhttpx.MustTest(t, app, req)
defer func() { _ = resp.Body.Close() }()
assertStatus(t, resp, http.StatusOK, "update failed")
}
req = httptest.NewRequest(http.MethodGet, "/leaderboard/top10", nil) req = httptest.NewRequest(http.MethodGet, "/leaderboard/top10", nil)
resp = sharedhttpx.MustTest(t, app, req) {
sharedhttpx.AssertStatusAndClose(t, resp, http.StatusOK, "top10 failed") resp := sharedhttpx.MustTest(t, app, req)
defer func() { _ = resp.Body.Close() }()
assertStatus(t, resp, http.StatusOK, "top10 failed")
}
} }
func TestPlayerAuthAndStats(t *testing.T) { func TestPlayerAuthAndStats(t *testing.T) {
@ -183,22 +193,41 @@ func TestPlayerAuthAndStats(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/leaderboard/players/user-1", nil) req := httptest.NewRequest(http.MethodGet, "/leaderboard/players/user-1", nil)
req.Header.Set("Authorization", "Bearer player") req.Header.Set("Authorization", "Bearer player")
resp := sharedhttpx.MustTest(t, app, req) {
sharedhttpx.AssertStatusAndClose(t, resp, http.StatusNotFound, "expected not found before update") resp := sharedhttpx.MustTest(t, app, req)
defer func() { _ = resp.Body.Close() }()
assertStatus(t, resp, http.StatusNotFound, "expected not found before update")
}
req = httptest.NewRequest(http.MethodGet, "/leaderboard/players/user-2", nil) req = httptest.NewRequest(http.MethodGet, "/leaderboard/players/user-2", nil)
req.Header.Set("Authorization", "Bearer player") req.Header.Set("Authorization", "Bearer player")
resp = sharedhttpx.MustTest(t, app, req) {
sharedhttpx.AssertStatusAndClose(t, resp, http.StatusForbidden, "expected forbidden for other player") resp := sharedhttpx.MustTest(t, app, req)
defer func() { _ = resp.Body.Close() }()
assertStatus(t, resp, http.StatusForbidden, "expected forbidden for other player")
}
req = httptest.NewRequest(http.MethodGet, "/leaderboard/stats", nil) req = httptest.NewRequest(http.MethodGet, "/leaderboard/stats", nil)
resp = sharedhttpx.MustTest(t, app, req) {
sharedhttpx.AssertStatusAndClose(t, resp, http.StatusOK, "stats failed") resp := sharedhttpx.MustTest(t, app, req)
defer func() { _ = resp.Body.Close() }()
assertStatus(t, resp, http.StatusOK, "stats failed")
}
} }
func TestMetricsEndpoint(t *testing.T) { func TestMetricsEndpoint(t *testing.T) {
app := setupApp(t) app := setupApp(t)
req := httptest.NewRequest(http.MethodGet, "/metrics", nil) req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
resp := sharedhttpx.MustTest(t, app, req) {
sharedhttpx.AssertStatusAndClose(t, resp, http.StatusOK, "metrics failed") resp := sharedhttpx.MustTest(t, app, req)
defer func() { _ = resp.Body.Close() }()
assertStatus(t, resp, http.StatusOK, "metrics failed")
}
}
func assertStatus(t *testing.T, resp *http.Response, want int, msg string) {
t.Helper()
if resp.StatusCode != want {
t.Fatalf("%s: status=%d want=%d", msg, resp.StatusCode, want)
}
} }

Loading…
Cancel
Save