mirror of
https://github.com/migatu/conjurer.git
synced 2026-07-14 13:34:40 +00:00
ci: replace broken default workflows with compile/unit/integration CI
The two scaffold workflows (Python application / Python package) failed on
every PR: they installed deps from a non-existent requirements.txt, ran
flake8/pytest over the vendored yt_dlp fork (new syntax under the 3.8/3.9
matrix), and collected ad-hoc root scripts — notably test_ai.py, which is
an invalid pasted object dump (not Python).
- Remove python-app.yml / python-package.yml and the junk root scripts
(test.py, test_ai.py, test_time.py)
- Add .github/workflows/ci.yml with three PR-check jobs:
* compile — py_compile every first-party .py (no deps)
* unit — pytest on pure logic (conanjurer_functions, constants)
* integration — boot the Flask services and assert the X-Conjurer-Api-Key
auth contract (communication_subroutine + conjurer_musician)
- Add tests/ suite, pytest.ini (testpaths=tests) and conftest.py (sys.path)
Fixes surfaced by the compile gate / needed for the integration job:
- conjurer_librarian/search_bot.py + search_bot2.py: f-string reused the
same quote ({item["exists"]}) -> SyntaxError on Python < 3.12
- conjurer_musician/media_search_functions.py: made import-safe
(env-overridable paths, lazy DB load / mkdir) so the service can be
imported and tested off the Pi
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"""Integration: the bot's communication Flask layer enforces the shared key,
|
||||
and the key the bot would *send* (constants.service_headers) is accepted.
|
||||
"""
|
||||
import constants
|
||||
import communication_subroutine as cs
|
||||
|
||||
|
||||
def _client(key="test-secret"):
|
||||
cs.API_KEY = key
|
||||
return cs.app.test_client()
|
||||
|
||||
|
||||
def test_prepped_tracks_rejected_without_key():
|
||||
client = _client()
|
||||
resp = client.post(
|
||||
"/prepped_tracks", data='["all", "x"]', content_type="application/json"
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_prepped_tracks_accepted_with_key():
|
||||
client = _client()
|
||||
resp = client.post(
|
||||
"/prepped_tracks",
|
||||
data='["all", "x"]',
|
||||
headers={"X-Conjurer-Api-Key": "test-secret"},
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
def test_conjurer_get_is_open():
|
||||
client = _client()
|
||||
assert client.get("/conjurer").status_code == 200
|
||||
|
||||
|
||||
def test_open_when_key_unset():
|
||||
client = _client(key=None)
|
||||
resp = client.post(
|
||||
"/prepped_tracks", data='["all", "x"]', content_type="application/json"
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
def test_bot_service_headers_accepted_by_service(monkeypatch):
|
||||
# End-to-end auth contract: the header constants.service_headers() produces
|
||||
# is exactly what communication_subroutine._authorize_request() expects.
|
||||
monkeypatch.setattr(constants, "API_SHARED_KEY", "shared-xyz")
|
||||
cs.API_KEY = "shared-xyz"
|
||||
resp = cs.app.test_client().post(
|
||||
"/prepped_tracks",
|
||||
data='["all", "x"]',
|
||||
headers=constants.service_headers(),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Integration: the musician Flask service enforces the shared key on its
|
||||
authenticated endpoints while leaving the open ones reachable.
|
||||
"""
|
||||
import conjurer_musician as m
|
||||
|
||||
|
||||
def _client(key="test-secret"):
|
||||
m.API_KEY = key
|
||||
return m.app.test_client()
|
||||
|
||||
|
||||
def test_clear_pr_pls_rejected_without_key():
|
||||
client = _client()
|
||||
assert client.get("/clear_pr_pls").status_code == 401
|
||||
|
||||
|
||||
def test_clear_pr_pls_accepted_with_key():
|
||||
client = _client()
|
||||
resp = client.get(
|
||||
"/clear_pr_pls", headers={"X-Conjurer-Api-Key": "test-secret"}
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
def test_mp3_list_is_open():
|
||||
client = _client()
|
||||
resp = client.get("/mp3")
|
||||
assert resp.status_code == 200
|
||||
assert "music_file_list" in resp.get_json()
|
||||
|
||||
|
||||
def test_open_when_key_unset():
|
||||
client = _client(key=None)
|
||||
assert client.get("/clear_pr_pls").status_code == 200
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Unit tests for the Conan Exiles bridge helpers (no Discord/RCON needed)."""
|
||||
import conanjurer_functions as cf
|
||||
|
||||
|
||||
def test_parse_players_basic():
|
||||
out = (
|
||||
"Idx | Char name | Player name | User ID\n"
|
||||
"0 | Conan | SteamGuy | 1\n"
|
||||
"1 | Khasar | OtherGuy | 2\n"
|
||||
"--- | --- | --- | ---"
|
||||
)
|
||||
assert cf.parse_players(out) == {"Conan", "Khasar"}
|
||||
|
||||
|
||||
def test_parse_players_empty_inputs():
|
||||
assert cf.parse_players("No players connected.") == set()
|
||||
assert cf.parse_players("") == set()
|
||||
|
||||
|
||||
def test_parse_line_login():
|
||||
event = cf.parse_line("SomeGuy joined the server")
|
||||
assert event is not None
|
||||
assert event.kind == "login"
|
||||
assert "SomeGuy" in event.text
|
||||
|
||||
|
||||
def test_parse_line_chat():
|
||||
event = cf.parse_line("Chat: Bob: hello there")
|
||||
assert event is not None
|
||||
assert event.kind == "chat"
|
||||
assert "Bob" in event.text and "hello" in event.text
|
||||
|
||||
|
||||
def test_parse_line_unrecognised_is_ignored():
|
||||
assert cf.parse_line("random server noise") is None
|
||||
|
||||
|
||||
def test_conanconfig_disabled_when_unconfigured():
|
||||
cfg = cf.ConanConfig("", 25575, "", "local", "", "", 22, "", "")
|
||||
assert cfg.rcon_enabled is False
|
||||
assert cfg.log_enabled is False
|
||||
|
||||
|
||||
def test_conanconfig_local_log_enabled():
|
||||
cfg = cf.ConanConfig("", 0, "", "local", "/tmp/conan.log", "", 22, "", "")
|
||||
assert cfg.log_enabled is True
|
||||
|
||||
|
||||
def test_conanconfig_rcon_enabled_requires_lib(monkeypatch):
|
||||
# Without aiomcrcon installed, rcon stays disabled even when host+pw are set.
|
||||
monkeypatch.setattr(cf, "_Rcon", None)
|
||||
cfg = cf.ConanConfig("1.2.3.4", 25575, "pw", "local", "", "", 22, "", "")
|
||||
assert cfg.rcon_enabled is False
|
||||
# With the lib present, it enables.
|
||||
monkeypatch.setattr(cf, "_Rcon", object)
|
||||
cfg2 = cf.ConanConfig("1.2.3.4", 25575, "pw", "local", "", "", 22, "", "")
|
||||
assert cfg2.rcon_enabled is True
|
||||
|
||||
|
||||
def test_conanconfig_sftp_log_requires_asyncssh(monkeypatch):
|
||||
monkeypatch.setattr(cf, "asyncssh", None)
|
||||
cfg = cf.ConanConfig("", 0, "", "sftp", "/log", "sftp.host", 22, "u", "p")
|
||||
assert cfg.log_enabled is False
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Unit tests for constants helpers (defensive config / auth headers)."""
|
||||
import constants
|
||||
|
||||
|
||||
def test_service_headers_empty_when_no_key(monkeypatch):
|
||||
monkeypatch.setattr(constants, "API_SHARED_KEY", "")
|
||||
assert constants.service_headers() == {}
|
||||
|
||||
|
||||
def test_service_headers_with_key(monkeypatch):
|
||||
monkeypatch.setattr(constants, "API_SHARED_KEY", "s3cr3t")
|
||||
assert constants.service_headers() == {"X-Conjurer-Api-Key": "s3cr3t"}
|
||||
|
||||
|
||||
def test_load_json_missing_returns_fallback(tmp_path):
|
||||
missing = tmp_path / "nope.json"
|
||||
assert constants._load_json(str(missing), {"fallback": 1}) == {"fallback": 1}
|
||||
|
||||
|
||||
def test_load_json_valid(tmp_path):
|
||||
good = tmp_path / "ok.json"
|
||||
good.write_text('{"a": 2}', encoding="utf-8")
|
||||
assert constants._load_json(str(good), {}) == {"a": 2}
|
||||
|
||||
|
||||
def test_load_json_corrupt_returns_fallback(tmp_path):
|
||||
bad = tmp_path / "bad.json"
|
||||
bad.write_text("{ not valid json", encoding="utf-8")
|
||||
assert constants._load_json(str(bad), []) == []
|
||||
|
||||
|
||||
def test_conan_defaults_present():
|
||||
# Feature toggles default to "off" so the bridge stays dormant.
|
||||
assert constants.CONAN_JOIN_CHANNEL_ID == 0
|
||||
assert constants.CONAN_RCON_HOST == ""
|
||||
Reference in New Issue
Block a user