Add Snacks app: automated video library encoder with hardware acceleration (#6)
Co-authored-by: Joachim Friberg <joachim.friberg@ip-solutions.se> Reviewed-on: phirna/zima-apps#6
This commit is contained in:
@@ -11,6 +11,7 @@ BACKEND_DIR = ROOT_DIR / "backend"
|
||||
sys.path.insert(0, str(BACKEND_DIR))
|
||||
|
||||
from app.docker_api import DockerApiError, DockerUsageResolver
|
||||
from app.dns_sync import DnsSyncError
|
||||
from app.models import IpEntry
|
||||
from app.service import ConflictError, DependencyError, EntryService
|
||||
from app.storage import EntryStorage
|
||||
@@ -75,12 +76,40 @@ class FakeIpManager:
|
||||
self.present.discard((ip, cidr, device))
|
||||
|
||||
|
||||
class FakeDnsProvider:
|
||||
def __init__(self, fail_upsert=False, fail_delete=False):
|
||||
self.records = {}
|
||||
self.upserts = []
|
||||
self.deletes = []
|
||||
self.fail_upsert = fail_upsert
|
||||
self.fail_delete = fail_delete
|
||||
|
||||
def upsert_a_record(self, fqdn: str, ip: str, ttl: int):
|
||||
if self.fail_upsert:
|
||||
raise DnsSyncError("upsert failed")
|
||||
self.records[fqdn] = ip
|
||||
self.upserts.append((fqdn, ip, ttl))
|
||||
|
||||
def delete_a_record(self, fqdn: str):
|
||||
if self.fail_delete:
|
||||
raise DnsSyncError("delete failed")
|
||||
self.records.pop(fqdn, None)
|
||||
self.deletes.append(fqdn)
|
||||
|
||||
|
||||
def assert_true(condition, message):
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def build_service(tmp_path: Path, entries=None, usage_resolver=None, ip_manager=None):
|
||||
def build_service(
|
||||
tmp_path: Path,
|
||||
entries=None,
|
||||
usage_resolver=None,
|
||||
ip_manager=None,
|
||||
dns_provider=None,
|
||||
dns_base_domain="home.arpa",
|
||||
):
|
||||
storage = EntryStorage(str(tmp_path / "entries.json"))
|
||||
if entries:
|
||||
storage.save_entries(entries)
|
||||
@@ -93,6 +122,9 @@ def build_service(tmp_path: Path, entries=None, usage_resolver=None, ip_manager=
|
||||
usage_resolver=resolver,
|
||||
ip_manager=ipm,
|
||||
interface_provider=lambda: ["eth0", "eth1"],
|
||||
dns_provider=dns_provider,
|
||||
dns_base_domain=dns_base_domain,
|
||||
dns_ttl_seconds=120,
|
||||
)
|
||||
|
||||
|
||||
@@ -163,6 +195,79 @@ def test_reconcile_reapplies_enabled(tmp_path: Path):
|
||||
assert_true(("10.0.4.10", 16, "eth0") in ip_manager.present, "enabled IP must be re-applied on startup reconcile")
|
||||
|
||||
|
||||
def test_dns_upsert_on_enable_when_used(tmp_path: Path):
|
||||
entry = IpEntry(id="dns1", name="Lan App", ip="10.0.4.20", cidr=16, device="eth0", enabled=False)
|
||||
resolver = FakeUsageResolver(mapping={"10.0.4.20": {"nginx"}})
|
||||
ip_manager = FakeIpManager()
|
||||
dns = FakeDnsProvider()
|
||||
service = build_service(tmp_path, entries=[entry], usage_resolver=resolver, ip_manager=ip_manager, dns_provider=dns)
|
||||
|
||||
service.set_enabled("dns1", enabled=True)
|
||||
|
||||
assert_true(dns.records.get("lan-app.home.arpa") == "10.0.4.20", "DNS record should be created on enable+used")
|
||||
|
||||
|
||||
def test_dns_no_upsert_on_enable_when_unused(tmp_path: Path):
|
||||
entry = IpEntry(id="dns2", name="Lan App 2", ip="10.0.4.21", cidr=16, device="eth0", enabled=False)
|
||||
resolver = FakeUsageResolver(mapping={})
|
||||
ip_manager = FakeIpManager()
|
||||
dns = FakeDnsProvider()
|
||||
service = build_service(tmp_path, entries=[entry], usage_resolver=resolver, ip_manager=ip_manager, dns_provider=dns)
|
||||
|
||||
service.set_enabled("dns2", enabled=True)
|
||||
|
||||
assert_true("lan-app-2.home.arpa" not in dns.records, "Unused entries must not create DNS record")
|
||||
|
||||
|
||||
def test_dns_reconcile_deletes_when_no_longer_used(tmp_path: Path):
|
||||
entry = IpEntry(id="dns3", name="Lan App 3", ip="10.0.4.22", cidr=16, device="eth0", enabled=True)
|
||||
resolver = FakeUsageResolver(mapping={"10.0.4.22": {"nginx"}})
|
||||
dns = FakeDnsProvider()
|
||||
service = build_service(tmp_path, entries=[entry], usage_resolver=resolver, dns_provider=dns)
|
||||
|
||||
service.reconcile_dns_records()
|
||||
assert_true(dns.records.get("lan-app-3.home.arpa") == "10.0.4.22", "record should exist after used reconcile")
|
||||
|
||||
resolver._mapping = {}
|
||||
service.reconcile_dns_records()
|
||||
assert_true("lan-app-3.home.arpa" not in dns.records, "record should be removed when usage disappears")
|
||||
|
||||
|
||||
def test_dns_fail_closed_rolls_back_enable(tmp_path: Path):
|
||||
entry = IpEntry(id="dns4", name="Lan App 4", ip="10.0.4.23", cidr=16, device="eth0", enabled=False)
|
||||
resolver = FakeUsageResolver(mapping={"10.0.4.23": {"nginx"}})
|
||||
ip_manager = FakeIpManager()
|
||||
dns = FakeDnsProvider(fail_upsert=True)
|
||||
service = build_service(tmp_path, entries=[entry], usage_resolver=resolver, ip_manager=ip_manager, dns_provider=dns)
|
||||
|
||||
failed = False
|
||||
try:
|
||||
service.set_enabled("dns4", enabled=True)
|
||||
except ConflictError:
|
||||
failed = True
|
||||
|
||||
assert_true(failed, "enable must fail-closed when DNS upsert fails")
|
||||
current = service.list_entries().items[0]
|
||||
assert_true(not current.enabled, "entry must roll back to disabled on DNS failure")
|
||||
assert_true(not ip_manager.is_present("10.0.4.23", 16, "eth0"), "IP presence must roll back on DNS failure")
|
||||
|
||||
|
||||
def test_dns_fail_closed_blocks_delete(tmp_path: Path):
|
||||
entry = IpEntry(id="dns5", name="Lan App 5", ip="10.0.4.24", cidr=16, device="eth0", enabled=False)
|
||||
resolver = FakeUsageResolver(mapping={})
|
||||
dns = FakeDnsProvider(fail_delete=True)
|
||||
service = build_service(tmp_path, entries=[entry], usage_resolver=resolver, dns_provider=dns)
|
||||
|
||||
failed = False
|
||||
try:
|
||||
service.delete_entry("dns5")
|
||||
except ConflictError:
|
||||
failed = True
|
||||
|
||||
assert_true(failed, "delete must fail-closed when DNS cleanup fails")
|
||||
assert_true(len(service.list_entries().items) == 1, "entry must remain when delete fails")
|
||||
|
||||
|
||||
def main():
|
||||
test_exact_hostip_match_only()
|
||||
|
||||
@@ -172,6 +277,11 @@ def main():
|
||||
test_disable_blocked_when_docker_check_fails(tmp_path)
|
||||
test_delete_blocked_when_enabled(tmp_path)
|
||||
test_reconcile_reapplies_enabled(tmp_path)
|
||||
test_dns_upsert_on_enable_when_used(tmp_path)
|
||||
test_dns_no_upsert_on_enable_when_unused(tmp_path)
|
||||
test_dns_reconcile_deletes_when_no_longer_used(tmp_path)
|
||||
test_dns_fail_closed_rolls_back_enable(tmp_path)
|
||||
test_dns_fail_closed_blocks_delete(tmp_path)
|
||||
|
||||
print("Integration tests passed")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user