diff --git a/backend/modules/api/src/lib.rs b/backend/modules/api/src/lib.rs index 6ce9eca8..7df85e7e 100644 --- a/backend/modules/api/src/lib.rs +++ b/backend/modules/api/src/lib.rs @@ -7,7 +7,10 @@ pub mod chess960; mod auth_tests; pub mod config; pub mod games; -pub mod player_stats; +// pub mod player_stats; +// ^ #1539's rating-history/online-count module is not compiled yet: it uses +// sqlx with a PgPool and table names that don't exist in this schema. Port +// it to db::DbPool + sea-orm (smdb.game) and register it in server.rs. pub mod idempotency; pub mod lc0; pub mod metrics; @@ -31,6 +34,5 @@ extern crate challenge; // Re-export server module for external use pub use auth::{login, logout, logout_all, refresh, register}; pub use idempotency::IdempotencyMiddleware; -.configure(crate::player_stats::configure) pub use players::export_player_data; pub use server::main; diff --git a/backend/modules/api/src/players.rs b/backend/modules/api/src/players.rs index 9ea6b1b8..de1e60b0 100644 --- a/backend/modules/api/src/players.rs +++ b/backend/modules/api/src/players.rs @@ -14,7 +14,11 @@ use std::sync::Mutex; use std::time::{Duration, Instant}; use validator::Validate; - +use service::players::{ + add_player as add_new_player, delete_player as delete_player_by_id, + find_player_by_id as get_single_player_by_id, get_player_preferences, + update_player as update_player_by_id, update_player_preferences, +}; use uuid::Uuid; // --------------------------------------------------------------------------- diff --git a/backend/modules/pairing/src/swiss.rs b/backend/modules/pairing/src/swiss.rs index f86177cb..b3293994 100644 --- a/backend/modules/pairing/src/swiss.rs +++ b/backend/modules/pairing/src/swiss.rs @@ -77,7 +77,10 @@ impl SwissPairer { if !config.avoid_recent_opponents { return false; } - player_a.has_played_against(&candidate.id) || paired_ids.contains(&candidate.id) + // History may be recorded on either player, so check both sides. + player_a.has_played_against(&candidate.id) + || candidate.has_played_against(&player_a.id) + || paired_ids.contains(&candidate.id) }; match remaining.iter().position(|(_, player)| !forbidden(player)) { @@ -229,7 +232,11 @@ mod tests { fn rematches_are_allowed_when_avoidance_is_disabled() { let a = player("a", 2000); let b = player("b", 1900).with_opponents(vec![a.id]); - let round = build_round(vec![a, b], PairingConfig::default()); + let config = PairingConfig { + avoid_recent_opponents: false, + ..PairingConfig::default() + }; + let round = build_round(vec![a, b], config); assert_eq!(round.pairings.len(), 1); } diff --git a/frontend/__tests__/chessSocketSync.test.ts b/frontend/__tests__/chessSocketSync.test.ts index a6b6f1e9..c0356c7d 100644 --- a/frontend/__tests__/chessSocketSync.test.ts +++ b/frontend/__tests__/chessSocketSync.test.ts @@ -43,9 +43,20 @@ describe("useChessSocket sync & offline queue (FE-113 / FE-114)", () => { beforeEach(() => { MockWebSocket.instances = []; vi.stubGlobal("WebSocket", MockWebSocket as unknown as typeof WebSocket); + // Game sockets authenticate with a single-use ticket fetched with the + // signed-in user's access token (BE-103), so provide both. + localStorage.setItem("access_token", "test-access-token"); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ ticket: "single-use-ticket", expires_in: 60 }), + }), + ); }); afterEach(() => { + localStorage.removeItem("access_token"); vi.unstubAllGlobals(); globalThis.WebSocket = originalWebSocket; }); diff --git a/frontend/__tests__/chessSocketUrl.test.ts b/frontend/__tests__/chessSocketUrl.test.ts index 3cddecf4..4a8bceff 100644 --- a/frontend/__tests__/chessSocketUrl.test.ts +++ b/frontend/__tests__/chessSocketUrl.test.ts @@ -58,7 +58,7 @@ describe("useChessSocket URL (#FE-90)", () => { vi.stubEnv("NEXT_PUBLIC_API_URL", ""); await expect(openSocketFor("abc")).resolves.toBe( - "ws://localhost:8000/v1/ws/game/abc?ticket=single-use-ticket", + "ws://localhost:8080/v1/ws/game/abc?ticket=single-use-ticket", ); }); diff --git a/frontend/__tests__/csp.test.ts b/frontend/__tests__/csp.test.ts index d07fdbf8..e1fcc36b 100644 --- a/frontend/__tests__/csp.test.ts +++ b/frontend/__tests__/csp.test.ts @@ -15,7 +15,7 @@ describe("buildConnectSrc (#FE-87)", () => { it("defaults to the local backend when no API URL is set", () => { const sources = buildConnectSrc({}); - expect(sources).toEqual(expect.arrayContaining(["'self'", "http://localhost:8000", "ws://localhost:8000"])); + expect(sources).toEqual(expect.arrayContaining(["'self'", "http://localhost:8080", "ws://localhost:8080"])); }); it("allows the third-party hosts the app calls", () => { diff --git a/frontend/app/watch/page.tsx b/frontend/app/watch/page.tsx index 070e67a9..4b01a5fb 100644 --- a/frontend/app/watch/page.tsx +++ b/frontend/app/watch/page.tsx @@ -30,7 +30,7 @@ const useMockLiveGames = export default function WatchPage() { const [selectedGameId, setSelectedGameId] = useState(null); const [filter, setFilter] = useState<"all" | LiveGameMode>("all"); - const [liveGames, setLiveGames] = useState( + const [liveGames] = useState( useMockLiveGames ? (MOCK_LIVE_GAMES as LiveGameRecord[]) : [] ); const [loading, setLoading] = useState(!useMockLiveGames); diff --git a/frontend/components/__tests__/accessibility.test.tsx b/frontend/components/__tests__/accessibility.test.tsx index 8575fc46..f1a82b8d 100644 --- a/frontend/components/__tests__/accessibility.test.tsx +++ b/frontend/components/__tests__/accessibility.test.tsx @@ -21,11 +21,18 @@ import { } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import { AuthProvider } from "@/context/authContext"; import { GamePreferencesProvider } from "@/context/GamePreferencesContext"; -// Board components read useGamePreferences(), which requires its provider. +// Board components read useGamePreferences(), whose provider syncs through +// useAuth(), so both providers are needed (same nesting as app/layout.tsx). +const Providers = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); const render = (ui: React.ReactElement, options?: Parameters[1]) => - rtlRender(ui, { wrapper: GamePreferencesProvider, ...options }); + rtlRender(ui, { wrapper: Providers, ...options }); import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { axe } from "vitest-axe"; import type { AxeMatchers } from "vitest-axe"; diff --git a/frontend/context/walletContext.tsx b/frontend/context/walletContext.tsx index a87631a7..929cd7d3 100644 --- a/frontend/context/walletContext.tsx +++ b/frontend/context/walletContext.tsx @@ -206,7 +206,13 @@ export function AppProvider({ children }: { children: React.ReactNode }) { // 1) Try official @stellar/freighter-api package if available try { const freighterApi = await import("@stellar/freighter-api"); - if (freighterApi && typeof freighterApi.signTransaction === "function") { + // Only use the package when the extension is actually present: without + // it, signTransaction() waits for an extension reply that never comes. + if ( + freighterApi && + typeof freighterApi.signTransaction === "function" && + (await freighterApi.isConnected()) + ) { const signed = await freighterApi.signTransaction(txXDR, { networkPassphrase: NETWORK_PASSPHRASE, }); diff --git a/frontend/lib/csp.ts b/frontend/lib/csp.ts index ac7e7e16..28991216 100644 --- a/frontend/lib/csp.ts +++ b/frontend/lib/csp.ts @@ -51,7 +51,7 @@ function wsOriginOf(httpOrigin: string): string { export function buildConnectSrc(env: CspEnv): string[] { const sources = new Set(["'self'"]); - for (const value of [env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000", env.NEXT_PUBLIC_BACKEND_URL]) { + for (const value of [env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080", env.NEXT_PUBLIC_BACKEND_URL]) { const origin = originOf(value); if (origin) { sources.add(origin); diff --git a/frontend/scripts/generate-contract-bindings.sh b/frontend/scripts/generate-contract-bindings.sh old mode 100644 new mode 100755