conftest.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. """Test fixtures for FTP service tests.
  2. Provides a real implicit FTPS server (via mock_ftp_server) and client factory
  3. for integration-style testing of BambuFTPClient against a live server.
  4. The server fixture is class-scoped to avoid the overhead of starting a new
  5. TLS server for every test (~67 TLS handshakes → ~9 per class).
  6. """
  7. import os
  8. import shutil
  9. import socket
  10. from unittest.mock import patch
  11. import pytest
  12. from backend.app.services.bambu_ftp import BambuFTPClient
  13. from backend.app.services.virtual_printer.certificate import CertificateService
  14. from backend.tests.unit.services.mock_ftp_server import MockBambuFTPServer
  15. BAMBU_DIRS = ("cache", "timelapse", "model", "data", "data/Metadata")
  16. @pytest.fixture(scope="session")
  17. def ftp_certs(tmp_path_factory):
  18. """Generate self-signed TLS certificates once per test session."""
  19. cert_dir = tmp_path_factory.mktemp("ftp_certs")
  20. svc = CertificateService(cert_dir, serial="TEST_FTP_SERVER")
  21. cert_path, key_path = svc.generate_certificates()
  22. return str(cert_path), str(key_path)
  23. def _find_free_port() -> int:
  24. """Find a free TCP port on localhost."""
  25. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  26. s.bind(("127.0.0.1", 0))
  27. return s.getsockname()[1]
  28. @pytest.fixture(scope="class")
  29. def ftp_root(tmp_path_factory):
  30. """Create temp directory with standard Bambu printer directory structure."""
  31. root = tmp_path_factory.mktemp("ftp_root")
  32. for d in BAMBU_DIRS:
  33. (root / d).mkdir(parents=True, exist_ok=True)
  34. return root
  35. @pytest.fixture(scope="class")
  36. def ftp_server(ftp_certs, ftp_root):
  37. """Start a mock implicit FTPS server, yield it, stop on cleanup."""
  38. cert_path, key_path = ftp_certs
  39. port = _find_free_port()
  40. server = MockBambuFTPServer(
  41. host="127.0.0.1",
  42. port=port,
  43. root_dir=str(ftp_root),
  44. cert_path=cert_path,
  45. key_path=key_path,
  46. access_code="12345678",
  47. )
  48. server.start()
  49. yield server
  50. server.stop()
  51. @pytest.fixture(autouse=True)
  52. def _ftp_test_cleanup(request):
  53. """Reset server state between tests within a class.
  54. Clears injected failures and restores the Bambu directory structure
  55. so each test starts with a clean filesystem. Skips cleanup for test
  56. classes that don't use the class-scoped ftp_server (e.g.
  57. TestDisconnectServerGone).
  58. """
  59. yield
  60. # Only clean up if this test class uses the class-scoped fixtures
  61. ftp_root = request.node.funcargs.get("ftp_root")
  62. if ftp_root is None:
  63. return
  64. server = request.node.funcargs.get("ftp_server")
  65. if server is not None:
  66. server.clear_failures()
  67. # Restore clean directory structure
  68. root = str(ftp_root)
  69. for entry in os.listdir(root):
  70. path = os.path.join(root, entry)
  71. if os.path.isdir(path):
  72. shutil.rmtree(path)
  73. else:
  74. os.remove(path)
  75. for d in BAMBU_DIRS:
  76. os.makedirs(os.path.join(root, d), exist_ok=True)
  77. @pytest.fixture()
  78. def ftp_client_factory(ftp_server):
  79. """Factory that creates BambuFTPClient instances pointed at the mock server."""
  80. def _make_client(
  81. printer_model: str = "X1C",
  82. force_prot_c: bool = False,
  83. access_code: str = "12345678",
  84. timeout: float = 10.0,
  85. ) -> BambuFTPClient:
  86. client = BambuFTPClient(
  87. ip_address="127.0.0.1",
  88. access_code=access_code,
  89. timeout=timeout,
  90. printer_model=printer_model,
  91. force_prot_c=force_prot_c,
  92. )
  93. # Override port to point at mock server
  94. client.FTP_PORT = ftp_server.port
  95. return client
  96. return _make_client
  97. @pytest.fixture(autouse=True)
  98. def clear_ftp_mode_cache():
  99. """Clear BambuFTPClient mode cache before and after each test."""
  100. BambuFTPClient._mode_cache.clear()
  101. yield
  102. BambuFTPClient._mode_cache.clear()
  103. @pytest.fixture()
  104. def patch_ftp_port(ftp_server):
  105. """Patch FTP_PORT at class level for async wrapper tests.
  106. Async wrappers create their own BambuFTPClient instances internally,
  107. so we need to patch the class-level default port.
  108. """
  109. with patch.object(BambuFTPClient, "FTP_PORT", ftp_server.port):
  110. yield ftp_server