mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
141 lines
5.0 KiB
Python
141 lines
5.0 KiB
Python
import json
|
|||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
import types
|
||
|
|
import unittest
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest import mock
|
||
|
|
|
||
|
|
|
||
|
|
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
|
||
|
|
if str(SCRIPTS_DIR) not in sys.path:
|
||
|
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
||
|
|
|
||
|
|
import lxc_apps
|
||
|
|
import notification_templates
|
||
|
|
|
||
|
|
|
||
|
|
def _app(app_id, name, installed, latest, **extra):
|
||
|
|
return {
|
||
|
|
"id": app_id,
|
||
|
|
"name": name,
|
||
|
|
"state": {
|
||
|
|
"installed_version": installed,
|
||
|
|
"latest_version": latest,
|
||
|
|
"update_available": True,
|
||
|
|
},
|
||
|
|
**extra,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class _FakeNotificationManager:
|
||
|
|
def __init__(self):
|
||
|
|
self.calls = []
|
||
|
|
|
||
|
|
def emit_event(self, **kwargs):
|
||
|
|
self.calls.append(kwargs)
|
||
|
|
return {"success": True}
|
||
|
|
|
||
|
|
|
||
|
|
class AppUpdateNotificationBatchTests(unittest.TestCase):
|
||
|
|
def _write_sidecar(self, directory, vmid, apps):
|
||
|
|
Path(directory, f"{vmid}.json").write_text(
|
||
|
|
json.dumps({"vmid": vmid, "apps": apps}),
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
|
||
|
|
def _emit(self, sidecars):
|
||
|
|
fake = _FakeNotificationManager()
|
||
|
|
module = types.SimpleNamespace(notification_manager=fake)
|
||
|
|
with tempfile.TemporaryDirectory() as directory:
|
||
|
|
for vmid, apps in sidecars.items():
|
||
|
|
self._write_sidecar(directory, vmid, apps)
|
||
|
|
with (
|
||
|
|
mock.patch.object(lxc_apps, "_APPS_DIR", directory),
|
||
|
|
mock.patch.dict(sys.modules, {"notification_manager": module}),
|
||
|
|
):
|
||
|
|
count = lxc_apps.emit_all_pending_updates()
|
||
|
|
return count, fake.calls
|
||
|
|
|
||
|
|
def test_multiple_updates_are_sent_as_one_sorted_batch(self):
|
||
|
|
count, calls = self._emit({
|
||
|
|
115: [
|
||
|
|
_app("redis", "Redis", "7.0.15-1", "8.10.1"),
|
||
|
|
_app("docmost", "Docmost", "0.23.2", "0.95.0"),
|
||
|
|
],
|
||
|
|
100: [_app("adguard", "AdGuard Home", "0.107.78", "0.107.79")],
|
||
|
|
})
|
||
|
|
|
||
|
|
self.assertEqual(count, 3)
|
||
|
|
self.assertEqual(len(calls), 1)
|
||
|
|
event = calls[0]
|
||
|
|
self.assertEqual(event["event_type"], "app_update_available")
|
||
|
|
self.assertEqual(event["entity"], "node")
|
||
|
|
self.assertTrue(event["entity_id"].startswith("batch:"))
|
||
|
|
self.assertEqual(event["data"]["count"], 3)
|
||
|
|
self.assertEqual(event["data"]["container_count"], 2)
|
||
|
|
self.assertEqual(
|
||
|
|
[(item["vmid"], item["app_name"]) for item in event["data"]["updates"]],
|
||
|
|
[(100, "AdGuard Home"), (115, "Docmost"), (115, "Redis")],
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_single_update_keeps_the_individual_event_shape(self):
|
||
|
|
count, calls = self._emit({
|
||
|
|
101: [_app("npm", "Nginx Proxy Manager", "2.9.19", "2.15.1")],
|
||
|
|
})
|
||
|
|
|
||
|
|
self.assertEqual(count, 1)
|
||
|
|
self.assertEqual(len(calls), 1)
|
||
|
|
event = calls[0]
|
||
|
|
self.assertEqual(event["entity"], "ct")
|
||
|
|
self.assertNotIn("updates", event["data"])
|
||
|
|
self.assertEqual(event["data"]["vmid"], 101)
|
||
|
|
self.assertEqual(event["data"]["latest"], "2.15.1")
|
||
|
|
|
||
|
|
def test_batch_respects_opt_outs_and_docker_delegation(self):
|
||
|
|
count, calls = self._emit({
|
||
|
|
110: [
|
||
|
|
_app("silent", "Silent", "1.0", "2.0", notifications_enabled=False),
|
||
|
|
_app("docker", "Docker", "1.0", "2.0", helper_slug="docker"),
|
||
|
|
_app("portainer", "Portainer", "2.0", "2.1", update_via="docker"),
|
||
|
|
],
|
||
|
|
})
|
||
|
|
|
||
|
|
self.assertEqual(count, 0)
|
||
|
|
self.assertEqual(calls, [])
|
||
|
|
|
||
|
|
def test_check_all_can_refresh_without_emitting_individual_events(self):
|
||
|
|
sidecar = {"vmid": 120, "apps": [{"id": "one"}, {"id": "two"}]}
|
||
|
|
with (
|
||
|
|
mock.patch.object(lxc_apps, "_read_sidecar", return_value=sidecar),
|
||
|
|
mock.patch.object(lxc_apps, "check_app") as check,
|
||
|
|
):
|
||
|
|
lxc_apps.check_all(120, force=False, notify=False)
|
||
|
|
|
||
|
|
self.assertEqual(check.call_count, 2)
|
||
|
|
check.assert_any_call(120, "one", force=False, notify=False)
|
||
|
|
check.assert_any_call(120, "two", force=False, notify=False)
|
||
|
|
|
||
|
|
def test_batch_formatter_groups_versions_by_container(self):
|
||
|
|
rendered = notification_templates.render_template(
|
||
|
|
"app_update_available",
|
||
|
|
{
|
||
|
|
"hostname": "pve01",
|
||
|
|
"updates": [
|
||
|
|
{"vmid": 115, "app_name": "Redis", "installed": "7.0", "latest": "8.1"},
|
||
|
|
{"vmid": 100, "app_name": "AdGuard Home", "installed": "1.0", "latest": "1.1"},
|
||
|
|
{"vmid": 115, "app_name": "Docmost", "installed": "0.2", "latest": "0.9"},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
self.assertEqual(rendered["title"], "pve01: 3 application updates available")
|
||
|
|
self.assertIn("3 applications in 2 LXC containers", rendered["body"])
|
||
|
|
self.assertLess(rendered["body"].index("CT 100"), rendered["body"].index("CT 115"))
|
||
|
|
self.assertIn("• Docmost: 0.2 → 0.9", rendered["body"])
|
||
|
|
self.assertIn("• Redis: 7.0 → 8.1", rendered["body"])
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|