Files
Claude (The Golden Chronicler #50) 04e9b407d5 feat: Migrate Arbiter and Modpack Version Checker to monorepo
WHAT WAS DONE:
- Migrated Arbiter (discord-oauth-arbiter) code to services/arbiter/
- Migrated Modpack Version Checker code to services/modpack-version-checker/
- Created .env.example for Arbiter with all required environment variables
- Moved systemd service file to services/arbiter/deploy/
- Organized directory structure per Gemini monorepo recommendations

WHY:
- Consolidate all service code in one repository
- Prepare for Gemini code review (Panel v1.12 compatibility check)
- Enable service-prefixed Git tagging (arbiter-v2.1.0, modpack-v1.0.0)
- Support npm workspaces for shared dependencies

SERVICES MIGRATED:
1. Arbiter (Discord OAuth bot) - Originally written by Gemini + Claude
   - Full source code from ops-manual docs/implementation/
   - Created comprehensive .env.example
   - Ready for Panel v1.12 compatibility verification

2. Modpack Version Checker (Python CLI tool)
   - Full source code from ops-manual docs/tasks/
   - Written for Panel v1.11, needs Gemini review for v1.12
   - Never had code review before

STILL TODO:
- Whitelist Manager - Pull from Billing VPS (38.68.14.188)
  - Currently deployed and running
  - Needs Panel v1.12 API compatibility fix (Task #86)
  - Requires SSH access to pull code

NEXT STEPS:
- Gemini code review for Panel v1.12 API compatibility
- Create package.json for each service
- Test npm workspaces integration
- Deploy after verification

FILES:
- services/arbiter/ (25 new files, full application)
- services/modpack-version-checker/ (21 new files, full application)

Signed-off-by: The Golden Chronicler <claude@firefrostgaming.com>
2026-03-31 21:52:42 +00:00

340 lines
11 KiB
Python

"""Tests for cli.py using Click's test runner."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
import responses as responses_lib
from click.testing import CliRunner
from modpack_checker.cli import cli
from modpack_checker.curseforge import CurseForgeNotFoundError
@pytest.fixture
def runner():
return CliRunner()
@pytest.fixture
def mock_cfg(tmp_path):
"""Return a Config-like mock backed by a real temp database."""
cfg = MagicMock()
cfg.curseforge_api_key = "test-api-key"
cfg.database_path = str(tmp_path / "test.db")
cfg.discord_webhook_url = None
cfg.notification_on_update = True
cfg.check_interval_hours = 6
return cfg
# ---------------------------------------------------------------------------
# Root / help
# ---------------------------------------------------------------------------
def test_help(runner):
result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
assert "Modpack Version Checker" in result.output
def test_version(runner):
result = runner.invoke(cli, ["--version"])
assert result.exit_code == 0
assert "1.0.0" in result.output
# ---------------------------------------------------------------------------
# config show
# ---------------------------------------------------------------------------
def test_config_show(runner, mock_cfg):
mock_cfg.curseforge_api_key = "abcd1234efgh5678"
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["config", "show"])
assert result.exit_code == 0
assert "abcd" in result.output # first 4 chars
assert "5678" in result.output # last 4 chars
def test_config_show_no_key(runner, mock_cfg):
mock_cfg.curseforge_api_key = ""
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["config", "show"])
assert result.exit_code == 0
assert "Not configured" in result.output
# ---------------------------------------------------------------------------
# list
# ---------------------------------------------------------------------------
def test_list_empty(runner, mock_cfg):
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["list"])
assert result.exit_code == 0
assert "empty" in result.output.lower()
def test_list_with_modpacks(runner, mock_cfg, tmp_path):
from modpack_checker.database import Database
db = Database(str(tmp_path / "test.db"))
db.add_modpack(111, "Pack Alpha")
db.add_modpack(222, "Pack Beta")
mock_cfg.database_path = str(tmp_path / "test.db")
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["list"])
assert result.exit_code == 0
assert "Pack Alpha" in result.output
assert "Pack Beta" in result.output
# ---------------------------------------------------------------------------
# add
# ---------------------------------------------------------------------------
def test_add_requires_api_key(runner, mock_cfg):
mock_cfg.curseforge_api_key = ""
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["add", "12345"])
assert result.exit_code == 1
assert "API key" in result.output
@responses_lib.activate
def test_add_success(runner, mock_cfg):
responses_lib.add(
responses_lib.GET,
"https://api.curseforge.com/v1/mods/12345",
json={"data": {"id": 12345, "name": "Test Modpack", "latestFiles": []}},
status=200,
)
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["add", "12345"])
assert result.exit_code == 0
assert "Test Modpack" in result.output
assert "tracking" in result.output.lower()
@responses_lib.activate
def test_add_not_found(runner, mock_cfg):
responses_lib.add(
responses_lib.GET,
"https://api.curseforge.com/v1/mods/99999",
status=404,
)
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["add", "99999"])
assert result.exit_code == 1
assert "99999" in result.output
@responses_lib.activate
def test_add_duplicate_shows_warning(runner, mock_cfg, tmp_path):
responses_lib.add(
responses_lib.GET,
"https://api.curseforge.com/v1/mods/12345",
json={"data": {"id": 12345, "name": "Test Modpack", "latestFiles": []}},
status=200,
)
from modpack_checker.database import Database
db = Database(str(tmp_path / "test.db"))
db.add_modpack(12345, "Test Modpack")
mock_cfg.database_path = str(tmp_path / "test.db")
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["add", "12345"])
assert "already tracked" in result.output.lower()
# ---------------------------------------------------------------------------
# remove
# ---------------------------------------------------------------------------
def test_remove_not_in_list(runner, mock_cfg):
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["remove", "99999"], input="y\n")
assert result.exit_code == 1
assert "not in your watch list" in result.output
def test_remove_success(runner, mock_cfg, tmp_path):
from modpack_checker.database import Database
db = Database(str(tmp_path / "test.db"))
db.add_modpack(12345, "Test Pack")
mock_cfg.database_path = str(tmp_path / "test.db")
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["remove", "12345"], input="y\n")
assert result.exit_code == 0
assert "Removed" in result.output
def test_remove_aborted(runner, mock_cfg, tmp_path):
from modpack_checker.database import Database
db = Database(str(tmp_path / "test.db"))
db.add_modpack(12345, "Test Pack")
mock_cfg.database_path = str(tmp_path / "test.db")
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["remove", "12345"], input="n\n")
# Aborted — pack should still exist
assert db.get_modpack(12345) is not None
# ---------------------------------------------------------------------------
# check
# ---------------------------------------------------------------------------
def test_check_empty_list(runner, mock_cfg):
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["check"])
assert result.exit_code == 0
assert "No modpacks" in result.output
@responses_lib.activate
def test_check_up_to_date(runner, mock_cfg, tmp_path):
from modpack_checker.database import Database
db = Database(str(tmp_path / "test.db"))
db.add_modpack(12345, "Test Pack")
db.update_version(12345, "1.0.0")
responses_lib.add(
responses_lib.GET,
"https://api.curseforge.com/v1/mods/12345",
json={
"data": {
"id": 12345,
"name": "Test Pack",
"latestFiles": [
{
"id": 1,
"displayName": "1.0.0",
"fileName": "pack-1.0.0.zip",
"fileDate": "2026-01-01T00:00:00Z",
}
],
}
},
status=200,
)
mock_cfg.database_path = str(tmp_path / "test.db")
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["check", "--no-notify"])
assert result.exit_code == 0
assert "up to date" in result.output.lower()
@responses_lib.activate
def test_check_update_available(runner, mock_cfg, tmp_path):
from modpack_checker.database import Database
db = Database(str(tmp_path / "test.db"))
db.add_modpack(12345, "Test Pack")
db.update_version(12345, "1.0.0")
responses_lib.add(
responses_lib.GET,
"https://api.curseforge.com/v1/mods/12345",
json={
"data": {
"id": 12345,
"name": "Test Pack",
"latestFiles": [
{
"id": 2,
"displayName": "1.1.0",
"fileName": "pack-1.1.0.zip",
"fileDate": "2026-02-01T00:00:00Z",
}
],
}
},
status=200,
)
mock_cfg.database_path = str(tmp_path / "test.db")
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["check", "--no-notify"])
assert result.exit_code == 0
assert "1.1.0" in result.output
# ---------------------------------------------------------------------------
# status
# ---------------------------------------------------------------------------
def test_status_not_found(runner, mock_cfg):
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["status", "99999"])
assert result.exit_code == 1
assert "not in your watch list" in result.output
def test_status_shows_info(runner, mock_cfg, tmp_path):
from modpack_checker.database import Database
db = Database(str(tmp_path / "test.db"))
db.add_modpack(12345, "Test Pack")
db.update_version(12345, "2.0.0")
mock_cfg.database_path = str(tmp_path / "test.db")
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["status", "12345"])
assert result.exit_code == 0
assert "Test Pack" in result.output
assert "2.0.0" in result.output
# ---------------------------------------------------------------------------
# notifications command
# ---------------------------------------------------------------------------
def test_notifications_disable(runner, mock_cfg, tmp_path):
from modpack_checker.database import Database
db = Database(str(tmp_path / "test.db"))
db.add_modpack(12345, "Test Pack")
mock_cfg.database_path = str(tmp_path / "test.db")
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["notifications", "12345", "--disable"])
assert result.exit_code == 0
assert "disabled" in result.output
assert db.get_modpack(12345).notification_enabled is False
def test_notifications_enable(runner, mock_cfg, tmp_path):
from modpack_checker.database import Database
db = Database(str(tmp_path / "test.db"))
db.add_modpack(12345, "Test Pack")
db.toggle_notifications(12345, False)
mock_cfg.database_path = str(tmp_path / "test.db")
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["notifications", "12345", "--enable"])
assert result.exit_code == 0
assert "enabled" in result.output
assert db.get_modpack(12345).notification_enabled is True
def test_notifications_missing_modpack(runner, mock_cfg):
with patch("modpack_checker.cli.Config.load", return_value=mock_cfg):
result = runner.invoke(cli, ["notifications", "99999", "--enable"])
assert result.exit_code == 1