from __future__ import annotations import os import tempfile import unittest from pathlib import Path from fastapi.testclient import TestClient os.environ["TOPOBENCH_SKIP_AUTOAPP"] = "1" from space_app.config import Settings from space_app.dataset import DatasetStore from space_app.db import SessionStore from space_app.main import create_app from space_app.models import DatasetRow class FakeVerifier: def verify(self, *, puzzle_type: str, problem_ascii: str, board_ascii: str, args: str): return { "board_exists": True, "board_valid": True, "board_modified": False, "correct": board_ascii == problem_ascii, "puzzle_type": puzzle_type, "args": args, } class ApiTests(unittest.TestCase): def setUp(self) -> None: self.tempdir = tempfile.TemporaryDirectory() data_dir = Path(self.tempdir.name) self.settings = Settings( data_dir=data_dir, database_path=data_dir / "test.sqlite", hf_cache_dir=data_dir / "hf-cache", admin_api_token="secret-token", frontend_dist_dir=data_dir / "frontend-dist", host="127.0.0.1", port=7860, ) self.settings.prepare() rows = [ DatasetRow( filename="bridges_1.txt", puzzlename="bridges", args="5x5de", problem="1.1\n...\n1.1", solution="1-1\n|.|\n1-1", difficulty="easy", ) ] self.app = create_app( settings=self.settings, dataset_store=DatasetStore(rows), session_store=SessionStore(self.settings.database_path), verifier=FakeVerifier(), ) self.client = TestClient(self.app) def tearDown(self) -> None: self.tempdir.cleanup() def test_session_lifecycle(self) -> None: list_response = self.client.get( "/api/puzzles", params={"puzzle_type": "bridges", "difficulty": "easy"}, ) self.assertEqual(list_response.status_code, 200) options = list_response.json() self.assertEqual(len(options), 1) self.assertEqual(options[0]["puzzle_id"], "bridges_1.txt") create_response = self.client.post( "/api/sessions", json={ "player_name": "Ada Lovelace", "puzzle_type": "bridges", "difficulty": "easy", "puzzle_id": "bridges_1.txt", }, ) self.assertEqual(create_response.status_code, 200) session = create_response.json() self.assertEqual(session["puzzle_type"], "bridges") self.assertEqual(session["payload"]["problem_ascii"], "1.1\n...\n1.1") def test_session_requires_matching_selected_puzzle(self) -> None: create_response = self.client.post( "/api/sessions", json={ "player_name": "Ada Lovelace", "puzzle_type": "flow_free", "difficulty": "easy", "puzzle_id": "bridges_1.txt", }, ) self.assertEqual(create_response.status_code, 400) def test_ready_and_submit_lifecycle(self) -> None: create_response = self.client.post( "/api/sessions", json={ "player_name": "Ada Lovelace", "puzzle_type": "bridges", "difficulty": "easy", "puzzle_id": "bridges_1.txt", }, ) session = create_response.json() ready_response = self.client.post(f"/api/sessions/{session['session_id']}/ready") self.assertEqual(ready_response.status_code, 200) self.assertIsNotNone(ready_response.json()["started_at"]) submit_response = self.client.post( f"/api/sessions/{session['session_id']}/submit", json={"board_ascii": "1.1\n...\n1.1"}, ) self.assertEqual(submit_response.status_code, 200) payload = submit_response.json() self.assertTrue(payload["solved"]) self.assertEqual(payload["status"], "solved") def test_admin_endpoints_require_token(self) -> None: unauthorized = self.client.get("/api/admin/solves") self.assertEqual(unauthorized.status_code, 401) authorized = self.client.get( "/api/admin/aggregates", headers={"Authorization": "Bearer secret-token"}, ) self.assertEqual(authorized.status_code, 200)