257 lines
9.8 KiB
Python
257 lines
9.8 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from progression_backend.app import create_app
|
|
from progression_backend.app import latest_windows_release_url
|
|
from progression_backend.config import Settings
|
|
from progression_backend.database import Database
|
|
from progression_backend.modlist import (
|
|
ModListItem,
|
|
core_items,
|
|
generate_rml,
|
|
parse_active_mods,
|
|
workshop_items,
|
|
)
|
|
from progression_backend.refresh import RefreshService
|
|
from progression_backend.steam_api import WorkshopDetails
|
|
from progression_backend.workshop import SteamCmdBatchResult, parse_about_xml, utc_now
|
|
|
|
|
|
class FakeSteam:
|
|
def get_collections(self, collection_ids: list[str]) -> dict[str, list[str]]:
|
|
return {collection_id: ["123456"] for collection_id in collection_ids}
|
|
|
|
def get_item_details(self, workshop_ids: list[str]) -> dict[str, WorkshopDetails]:
|
|
return {
|
|
workshop_id: WorkshopDetails(
|
|
workshop_id=workshop_id,
|
|
title="Test & Mod",
|
|
time_updated=123,
|
|
file_size=456,
|
|
consumer_app_id=294100,
|
|
result=1,
|
|
)
|
|
for workshop_id in workshop_ids
|
|
}
|
|
|
|
|
|
class FakeRunner:
|
|
def __init__(self, content_dir: Path):
|
|
self.content_dir = content_dir
|
|
|
|
def available(self) -> bool:
|
|
return True
|
|
|
|
def run_batch(self, workshop_ids: list[str]) -> SteamCmdBatchResult:
|
|
for workshop_id in workshop_ids:
|
|
about = self.content_dir / workshop_id / "About"
|
|
about.mkdir(parents=True)
|
|
(about / "About.xml").write_text(
|
|
"<ModMetaData><name>Test & Mod</name>"
|
|
"<packageId>Author.Test</packageId>"
|
|
"<steamAppId>294100</steamAppId></ModMetaData>",
|
|
encoding="utf-8",
|
|
)
|
|
timestamp = utc_now()
|
|
return SteamCmdBatchResult(
|
|
workshop_ids=workshop_ids,
|
|
started_at=timestamp,
|
|
finished_at=timestamp,
|
|
elapsed_seconds=0.1,
|
|
exit_code=0,
|
|
status="completed",
|
|
failed_ids=set(),
|
|
output_tail="Success",
|
|
)
|
|
|
|
|
|
class BackendTests(unittest.TestCase):
|
|
@patch("progression_backend.app.requests.get")
|
|
def test_latest_windows_release_includes_prereleases(self, get: Mock) -> None:
|
|
response = Mock()
|
|
response.json.return_value = [
|
|
{
|
|
"tag_name": "0.2.2",
|
|
"published_at": "2026-06-13T02:37:15Z",
|
|
"draft": False,
|
|
"prerelease": True,
|
|
"assets": [
|
|
{
|
|
"name": "ProgressionLoader.exe",
|
|
"browser_download_url": "https://git.hudsonriggs.systems/HRiggs/ProgressionMods/releases/download/0.2.2/ProgressionLoader.exe",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
get.return_value = response
|
|
|
|
self.assertEqual(
|
|
latest_windows_release_url(),
|
|
"https://git.hudsonriggs.systems/HRiggs/ProgressionMods/releases/download/0.2.2/ProgressionLoader.exe",
|
|
)
|
|
response.raise_for_status.assert_called_once_with()
|
|
|
|
@patch("progression_backend.app.requests.get")
|
|
def test_latest_windows_release_prefers_stable_build(self, get: Mock) -> None:
|
|
response = Mock()
|
|
response.json.return_value = [
|
|
{
|
|
"published_at": "2026-07-01T00:00:00Z",
|
|
"draft": False,
|
|
"prerelease": True,
|
|
"assets": [
|
|
{
|
|
"name": "ProgressionLoader.exe",
|
|
"browser_download_url": "https://git.hudsonriggs.systems/HRiggs/ProgressionMods/releases/download/0.3.0-rc1/ProgressionLoader.exe",
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"published_at": "2026-06-30T00:00:00Z",
|
|
"draft": False,
|
|
"prerelease": False,
|
|
"assets": [
|
|
{
|
|
"name": "ProgressionLoader.exe",
|
|
"browser_download_url": "https://git.hudsonriggs.systems/HRiggs/ProgressionMods/releases/download/0.2.3/ProgressionLoader.exe",
|
|
}
|
|
],
|
|
},
|
|
]
|
|
get.return_value = response
|
|
|
|
self.assertEqual(
|
|
latest_windows_release_url(),
|
|
"https://git.hudsonriggs.systems/HRiggs/ProgressionMods/releases/download/0.2.3/ProgressionLoader.exe",
|
|
)
|
|
|
|
def test_about_parser_normalizes_package_id(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
about = Path(directory) / "About"
|
|
about.mkdir()
|
|
(about / "About.xml").write_text(
|
|
"<ModMetaData><name>A & B</name><packageId>Author.Mod</packageId></ModMetaData>",
|
|
encoding="utf-8",
|
|
)
|
|
parsed = parse_about_xml(Path(directory))
|
|
self.assertEqual(parsed.package_id, "author.mod")
|
|
self.assertEqual(parsed.display_name, "A & B")
|
|
|
|
def test_rml_uses_workshop_id_and_escapes_names(self) -> None:
|
|
payload = generate_rml(
|
|
[
|
|
*core_items(["royalty"]),
|
|
ModListItem("author.mod", "123456", "A & B"),
|
|
],
|
|
"1.6",
|
|
)
|
|
root = ET.fromstring(payload)
|
|
self.assertEqual(root.findtext("meta/gameVersion"), "1.6")
|
|
self.assertEqual(
|
|
[node.text for node in root.findall("meta/modSteamIds/li")],
|
|
["0", "0", "123456"],
|
|
)
|
|
self.assertIn(b"A & B", payload)
|
|
|
|
def test_homebrew_parser_reads_active_mods(self) -> None:
|
|
items = parse_active_mods(
|
|
b"<ModsConfigData><activeMods><li>ludeon.rimworld</li>"
|
|
b"<li>author.mod</li></activeMods></ModsConfigData>"
|
|
)
|
|
self.assertEqual(
|
|
[item.package_id for item in items], ["ludeon.rimworld", "author.mod"]
|
|
)
|
|
|
|
def test_refresh_persists_download_analytics(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
settings = Settings(
|
|
host="127.0.0.1",
|
|
port=8000,
|
|
database_path=root / "progression.db",
|
|
steamcmd_path="steamcmd",
|
|
workshop_content_dir=root / "workshop" / "294100",
|
|
steamcmd_batch_size=25,
|
|
steamcmd_timeout_seconds=60,
|
|
refresh_interval_seconds=3600,
|
|
refresh_on_startup=False,
|
|
game_version="1.6",
|
|
admin_token=None,
|
|
collections={"Core": "1", "Content": "2", "Cosmetics": "3"},
|
|
static_dir=root,
|
|
)
|
|
database = Database(settings.database_path)
|
|
database.initialize()
|
|
service = RefreshService(
|
|
settings,
|
|
database,
|
|
steam=FakeSteam(),
|
|
runner=FakeRunner(settings.workshop_content_dir),
|
|
)
|
|
service.refresh("test-run", "test")
|
|
|
|
run = database.fetch_one(
|
|
"SELECT * FROM refresh_runs WHERE id = ?", ("test-run",)
|
|
)
|
|
self.assertIsNotNone(run)
|
|
self.assertEqual(run["status"], "completed", run)
|
|
self.assertEqual(run["unique_items"], 1)
|
|
self.assertEqual(run["downloads_succeeded"], 1)
|
|
mod = database.fetch_one("SELECT * FROM mods WHERE workshop_id = '123456'")
|
|
self.assertEqual(mod["package_id"], "author.test")
|
|
self.assertEqual(mod["steam_app_id"], "294100")
|
|
generated = workshop_items([mod])
|
|
self.assertEqual(generated[0].workshop_id, "123456")
|
|
|
|
def test_api_serves_dashboard_and_reports_empty_modlist(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
project_root = Path(__file__).resolve().parents[1]
|
|
settings = Settings(
|
|
host="127.0.0.1",
|
|
port=8000,
|
|
database_path=root / "progression.db",
|
|
steamcmd_path="missing-steamcmd",
|
|
workshop_content_dir=root / "workshop" / "294100",
|
|
steamcmd_batch_size=25,
|
|
steamcmd_timeout_seconds=60,
|
|
refresh_interval_seconds=3600,
|
|
refresh_on_startup=False,
|
|
game_version="1.6",
|
|
admin_token=None,
|
|
collections={"Core": "1", "Content": "2", "Cosmetics": "3"},
|
|
static_dir=project_root / "progression_backend" / "static",
|
|
)
|
|
with TestClient(create_app(settings)) as client:
|
|
self.assertEqual(client.get("/").status_code, 200)
|
|
self.assertEqual(client.get("/art/GameTitle.png").status_code, 200)
|
|
self.assertEqual(client.get("/api/health").json()["status"], "ok")
|
|
self.assertIn("rml_updated_at", client.get("/api/status").json())
|
|
response = client.get("/api/modlists/vanilla")
|
|
self.assertEqual(response.status_code, 409)
|
|
|
|
release_url = (
|
|
"https://git.hudsonriggs.systems/HRiggs/ProgressionMods/"
|
|
"releases/download/0.2.2/ProgressionLoader.exe"
|
|
)
|
|
with patch(
|
|
"progression_backend.app.latest_windows_release_url",
|
|
return_value=release_url,
|
|
):
|
|
response = client.get(
|
|
"/api/downloads/windows", follow_redirects=False
|
|
)
|
|
self.assertEqual(response.status_code, 307)
|
|
self.assertEqual(response.headers["location"], release_url)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|