| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- """Unit tests for TailscaleService — presence detection only.
- Cert provisioning was removed: BambuStudio's printer-MQTT trust path validates
- against its bundled BBL CA, not the system trust store, so a Tailscale-issued
- LE cert was rejected regardless of hostname/IP. The Tailscale toggle is now
- informational (surfacing the host's Tailscale IP/FQDN to guide the user).
- """
- import json
- from unittest.mock import AsyncMock, patch
- import pytest
- class TestTailscaleService:
- """Tests for TailscaleService CLI wrapper — get_status only."""
- @pytest.mark.asyncio
- async def test_get_status_binary_not_found(self):
- """Returns available=False when the tailscale binary is absent from PATH."""
- from backend.app.services.virtual_printer.tailscale import TailscaleService
- svc = TailscaleService()
- with patch("shutil.which", return_value=None):
- status = await svc.get_status()
- assert status.available is False
- assert status.error is not None
- assert "not found" in status.error
- @pytest.mark.asyncio
- async def test_get_status_command_fails(self):
- """Returns available=False when `tailscale status` exits non-zero."""
- from backend.app.services.virtual_printer.tailscale import TailscaleService
- svc = TailscaleService()
- with (
- patch("shutil.which", return_value="/usr/bin/tailscale"),
- patch.object(svc, "_run_tailscale", new_callable=AsyncMock, return_value=(1, b"", b"permission denied")),
- ):
- status = await svc.get_status()
- assert status.available is False
- assert "permission denied" in (status.error or "")
- @pytest.mark.asyncio
- async def test_get_status_success(self):
- """Parses FQDN, hostname, tailnet_name, and IP list from JSON output."""
- from backend.app.services.virtual_printer.tailscale import TailscaleService
- payload = {
- "Self": {
- "DNSName": "myhost.example.ts.net.",
- "TailscaleIPs": ["100.1.2.3", "fd7a::1"],
- }
- }
- svc = TailscaleService()
- with (
- patch("shutil.which", return_value="/usr/bin/tailscale"),
- patch.object(
- svc, "_run_tailscale", new_callable=AsyncMock, return_value=(0, json.dumps(payload).encode(), b"")
- ),
- ):
- status = await svc.get_status()
- assert status.available is True
- assert status.fqdn == "myhost.example.ts.net"
- assert status.hostname == "myhost"
- assert status.tailnet_name == "example.ts.net"
- assert "100.1.2.3" in status.tailscale_ips
- @pytest.mark.asyncio
- async def test_get_status_empty_dnsname(self):
- """Returns available=False when Tailscale daemon reports no DNSName (not connected)."""
- from backend.app.services.virtual_printer.tailscale import TailscaleService
- payload = {"Self": {"DNSName": "", "TailscaleIPs": []}}
- svc = TailscaleService()
- with (
- patch("shutil.which", return_value="/usr/bin/tailscale"),
- patch.object(
- svc, "_run_tailscale", new_callable=AsyncMock, return_value=(0, json.dumps(payload).encode(), b"")
- ),
- ):
- status = await svc.get_status()
- assert status.available is False
- assert "no DNSName" in (status.error or "")
- @pytest.mark.asyncio
- async def test_get_status_malformed_json(self):
- """Returns available=False with a parse-error reason when stdout is not JSON."""
- from backend.app.services.virtual_printer.tailscale import TailscaleService
- svc = TailscaleService()
- with (
- patch("shutil.which", return_value="/usr/bin/tailscale"),
- patch.object(svc, "_run_tailscale", new_callable=AsyncMock, return_value=(0, b"not-json{", b"")),
- ):
- status = await svc.get_status()
- assert status.available is False
- assert "JSON parse error" in (status.error or "")
|