From 7857331a1f4bed8b7918a6e4a62bca200c8f3111 Mon Sep 17 00:00:00 2001 From: oabrivard Date: Mon, 9 Feb 2026 00:16:43 +0100 Subject: [PATCH] Finished '2.4 Leaderboard Service (Port 8083)' --- .../application/leaderboard/service_test.go | 6 +- .../infra/persistence/ent/leaderboard_repo.go | 12 +++- .../tests/integration_http_test.go | 55 ++++++++++++++----- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/backend/services/leaderboard-service/internal/application/leaderboard/service_test.go b/backend/services/leaderboard-service/internal/application/leaderboard/service_test.go index 3fb9c88..325ef08 100644 --- a/backend/services/leaderboard-service/internal/application/leaderboard/service_test.go +++ b/backend/services/leaderboard-service/internal/application/leaderboard/service_test.go @@ -77,7 +77,11 @@ func (r *fakeRepo) IngestEntry( 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 { limit = len(r.entries) } diff --git a/backend/services/leaderboard-service/internal/infra/persistence/ent/leaderboard_repo.go b/backend/services/leaderboard-service/internal/infra/persistence/ent/leaderboard_repo.go index 2fb8ccd..849289b 100644 --- a/backend/services/leaderboard-service/internal/infra/persistence/ent/leaderboard_repo.go +++ b/backend/services/leaderboard-service/internal/infra/persistence/ent/leaderboard_repo.go @@ -225,7 +225,10 @@ LIMIT $2 OFFSET $3` } // 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) q := ` SELECT @@ -340,7 +343,10 @@ DO UPDATE SET ) ELSE leaderboard_player_stats.best_duration_seconds 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()` _, err := tx.Exec(ctx, q, @@ -365,6 +371,8 @@ func buildFilterWhere(filter domain.TopFilter) (string, []any) { parts = append(parts, "completion_type=$"+strconvI(len(args))) } switch filter.Window { + case domain.WindowAll: + // no time filter case domain.Window24h: args = append(args, time.Now().UTC().Add(-24*time.Hour)) parts = append(parts, "completed_at>=$"+strconvI(len(args))) diff --git a/backend/services/leaderboard-service/tests/integration_http_test.go b/backend/services/leaderboard-service/tests/integration_http_test.go index a090f16..2bfe02d 100644 --- a/backend/services/leaderboard-service/tests/integration_http_test.go +++ b/backend/services/leaderboard-service/tests/integration_http_test.go @@ -68,7 +68,11 @@ func (r *inMemoryRepo) IngestEntry( } 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 { limit = len(r.entries) } @@ -170,12 +174,18 @@ func TestUpdateAndTop10(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/leaderboard/update", bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") 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) - 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) { @@ -183,22 +193,41 @@ func TestPlayerAuthAndStats(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/leaderboard/players/user-1", nil) 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.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) - 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) { app := setupApp(t) 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) + } }