Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 11 additions & 2 deletions CricketGame/backend/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Header, status
from sqlalchemy.orm import Session
from pydantic import BaseModel
import secrets
import json

from ..data.database import get_db
from ..core.config import ADMIN_SECRET
from ..core.auth import register_player, login_player, get_player_stats, decode_token, create_token
from ..data.models import Player, MatchHistory, TournamentHistory, FormatStats

Expand Down Expand Up @@ -62,7 +64,14 @@ def _merge_format_stats(keeper: FormatStats, src: FormatStats) -> None:
keeper.best_bowling_runs = src.best_bowling_runs

@router.get("/migrate-formats")
def migrate_formats(db: Session = Depends(get_db)):
def migrate_formats(
x_admin_secret: str = Header(..., alias="X-Admin-Secret"),
db: Session = Depends(get_db),
):
if not secrets.compare_digest(x_admin_secret, ADMIN_SECRET):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Invalid admin secret"
)
stats_merged_2v2 = 0
stats_deduped = 0
legacy_rows = db.query(FormatStats).filter(FormatStats.format == "2v2").all()
Expand Down
6 changes: 6 additions & 0 deletions CricketGame/backend/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
raise ValueError("SECRET_KEY is required in production")
SECRET_KEY = "cricket-dev-key-2026"

ADMIN_SECRET = os.getenv("ADMIN_SECRET")
if not ADMIN_SECRET:
if APP_ENV in ("production", "prod"):
raise ValueError("ADMIN_SECRET is required in production")
ADMIN_SECRET = "super-secret-admin-key"
Comment on lines +14 to +18
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

ADMIN_SECRET falls back to a hardcoded value when unset. That makes the /auth/migrate-formats admin protection effectively guessable in any non-production environment that’s accidentally exposed. Prefer requiring ADMIN_SECRET in all environments, or generate a random per-process value (and log it only for local dev) so there isn’t a shared default secret across deployments.

Copilot uses AI. Check for mistakes.

ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "1440"))

Expand Down
12 changes: 8 additions & 4 deletions CricketGame/backend/realtime/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,16 @@ async def maybe_cpu_move(manager, room, innings) -> None:
# CPU batter: submit immediately if its slot is empty
if striker_is_cpu and "bat" not in pending:
await asyncio.sleep(0.25)
pending["bat"] = cpu_pick_move(manager, room, "bat", innings.striker)
# Run in thread to prevent blocking event loop with DB queries
pending["bat"] = await asyncio.to_thread(cpu_pick_move, manager, room, "bat", innings.striker)
placed = True

# CPU bowler: submit immediately if its slot is empty
if bowler_is_cpu and "bowl" not in pending:
if not placed:
await asyncio.sleep(0.25)
pending["bowl"] = cpu_pick_move(manager, room, "bowl", innings.current_bowler)
# Run in thread to prevent blocking event loop with DB queries
pending["bowl"] = await asyncio.to_thread(cpu_pick_move, manager, room, "bowl", innings.current_bowler)
placed = True

# Broadcast state so the frontend immediately sees the CPU's ready indicator
Expand Down Expand Up @@ -238,9 +240,11 @@ async def auto_play_cpu_match(manager, room) -> None:
return
pending = room.pending_moves
if "bat" not in pending:
pending["bat"] = cpu_pick_move(manager, room, "bat", innings.striker)
# Run in thread to prevent blocking event loop with DB queries
pending["bat"] = await asyncio.to_thread(cpu_pick_move, manager, room, "bat", innings.striker)
if "bowl" not in pending:
pending["bowl"] = cpu_pick_move(manager, room, "bowl", innings.current_bowler)
# Run in thread to prevent blocking event loop with DB queries
pending["bowl"] = await asyncio.to_thread(cpu_pick_move, manager, room, "bowl", innings.current_bowler)
await asyncio.sleep(0.25)
resolved = await manager._resolve_pending_ball(room, innings)
if not resolved:
Expand Down
29 changes: 29 additions & 0 deletions CricketGame/backend/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from fastapi.testclient import TestClient
from backend.main import app
from backend.core.config import ADMIN_SECRET
from backend.data.database import init_db

# Initialize DB for tests
init_db()

client = TestClient(app)

def test_migrate_formats_no_header():
# Expect 422 because the header is required (Header(...))
response = client.get("/auth/migrate-formats")
assert response.status_code == 422

def test_migrate_formats_wrong_header():
# Expect 403 because the secret is invalid
response = client.get("/auth/migrate-formats", headers={"X-Admin-Secret": "wrong-secret"})
assert response.status_code == 403
assert response.json()["detail"] == "Invalid admin secret"

def test_migrate_formats_correct_header():
# Expect 200 because the secret matches
response = client.get("/auth/migrate-formats", headers={"X-Admin-Secret": ADMIN_SECRET})
assert response.status_code == 200
data = response.json()
assert "format_stats_2v2_merged" in data
assert "duplicate_rows_removed" in data
assert "match_history_fixed" in data
Comment on lines +9 to +29
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

This test module creates a global TestClient(app) at import time and never closes it. Since the app has a lifespan that starts the learning processor, this can leak background threads/resources and may cause the test process to hang or interfere with other tests. Use a context-managed client (with TestClient(app) as client:) or explicitly call client.close()/provide a per-test fixture so lifespan shutdown runs reliably.

Suggested change
client = TestClient(app)
def test_migrate_formats_no_header():
# Expect 422 because the header is required (Header(...))
response = client.get("/auth/migrate-formats")
assert response.status_code == 422
def test_migrate_formats_wrong_header():
# Expect 403 because the secret is invalid
response = client.get("/auth/migrate-formats", headers={"X-Admin-Secret": "wrong-secret"})
assert response.status_code == 403
assert response.json()["detail"] == "Invalid admin secret"
def test_migrate_formats_correct_header():
# Expect 200 because the secret matches
response = client.get("/auth/migrate-formats", headers={"X-Admin-Secret": ADMIN_SECRET})
assert response.status_code == 200
data = response.json()
assert "format_stats_2v2_merged" in data
assert "duplicate_rows_removed" in data
assert "match_history_fixed" in data
def test_migrate_formats_no_header():
# Expect 422 because the header is required (Header(...))
with TestClient(app) as client:
response = client.get("/auth/migrate-formats")
assert response.status_code == 422
def test_migrate_formats_wrong_header():
# Expect 403 because the secret is invalid
with TestClient(app) as client:
response = client.get("/auth/migrate-formats", headers={"X-Admin-Secret": "wrong-secret"})
assert response.status_code == 403
assert response.json()["detail"] == "Invalid admin secret"
def test_migrate_formats_correct_header():
# Expect 200 because the secret matches
with TestClient(app) as client:
response = client.get("/auth/migrate-formats", headers={"X-Admin-Secret": ADMIN_SECRET})
assert response.status_code == 200
data = response.json()
assert "format_stats_2v2_merged" in data
assert "duplicate_rows_removed" in data
assert "match_history_fixed" in data

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

This new test file uses pytest-style discovery, but the existing backend tests in this repo are standalone scripts with run_all_tests() + a __main__ block (and pytest isn’t listed in backend/requirements.txt). As-is, this file likely won’t be executed by the current test workflow. Either adapt it to the repo’s script-style convention or add pytest + CI wiring so these tests actually run.

Suggested change
assert "match_history_fixed" in data
assert "match_history_fixed" in data
def run_all_tests() -> None:
"""
Run all tests in this module.
This mirrors the standalone script-style convention used by other backend tests,
so the existing test workflow can execute these tests without pytest.
"""
test_migrate_formats_no_header()
test_migrate_formats_wrong_header()
test_migrate_formats_correct_header()
if __name__ == "__main__":
run_all_tests()

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion CricketGame/backend/test_game_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def test_cpu_toss_timeout_fallback_triggers():
room.captains["A"] = "CPU Bot"
match = Match("M3", "team", ["CPU Bot"], ["Host"], total_overs=1, total_wickets=1)

def fixed_do_toss():
def fixed_do_toss(caller=None):
match.toss_caller = "CPU Bot"
return {"caller": "CPU Bot"}

Expand Down
16 changes: 0 additions & 16 deletions CricketGame/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions CricketGame/frontend/src/components/Lobby.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ export default function Lobby({ lobby, username, sendMsg }: Props) {
</button>
<button
onClick={() => sendMsg({ action: 'REMOVE_CPU' })}
aria-label="Remove the last added CPU player"
disabled={cpuCount === 0}
className="px-4 py-2 bg-white border border-slate-200 text-slate-400 text-xs font-bold uppercase tracking-wider rounded disabled:opacity-50 disabled:cursor-not-allowed"
>
Expand Down Expand Up @@ -579,12 +580,16 @@ function DraggablePlayer({ player, sendMsg }: {
</div>
<div className="flex gap-1 flex-shrink-0 ml-2">
<button
aria-label={`Assign ${player.username} to Team A`}
title="Assign to Team A"
onClick={(e) => { e.stopPropagation(); sendMsg({ action: 'ASSIGN_TEAM', player: player.username, team: 'A' }) }}
className="h-5 px-1.5 text-[9px] font-bold rounded bg-blue-50 hover:bg-blue-100 text-blue-600 border border-blue-200 transition-colors"
className="h-5 px-1.5 text-[9px] font-bold rounded bg-blue-50 hover:bg-blue-100 text-blue-600 border border-blue-200 transition-colors focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:outline-none"
>A</button>
<button
aria-label={`Assign ${player.username} to Team B`}
title="Assign to Team B"
onClick={(e) => { e.stopPropagation(); sendMsg({ action: 'ASSIGN_TEAM', player: player.username, team: 'B' }) }}
className="h-5 px-1.5 text-[9px] font-bold rounded bg-purple-50 hover:bg-purple-100 text-purple-600 border border-purple-200 transition-colors"
className="h-5 px-1.5 text-[9px] font-bold rounded bg-purple-50 hover:bg-purple-100 text-purple-600 border border-purple-200 transition-colors focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:outline-none"
>B</button>
</div>
</div>
Expand Down Expand Up @@ -634,8 +639,10 @@ function TeamDropZone({ team, lobby, sendMsg, isHost }: {
</div>
{isHost && captain !== playerName && (
<button
aria-label={`Make ${playerName} captain`}
title="Make Captain"
onClick={() => sendMsg({ action: 'SET_CAPTAIN', team, captain: playerName })}
className={`h-5 px-1.5 text-[9px] font-bold rounded border transition-colors ${captainBtnColor}`}
className={`h-5 px-1.5 text-[9px] font-bold rounded border transition-colors ${captainBtnColor} focus-visible:ring-2 focus-visible:ring-yellow-500 focus-visible:outline-none`}
>
👑
</button>
Expand Down
Loading