"""Unit tests for docker build service.""" import subprocess import tempfile from pathlib import Path from unittest.mock import MagicMock, patch import pytest from src.services.docker_build import build_image class TestBuildImage: """Tests for build_image function.""" @patch("subprocess.run") def test_builds_image_successfully(self, mock_run) -> None: mock_run.return_value = MagicMock( returncode=0, stdout="Successfully built abc123", stderr="", ) with tempfile.TemporaryDirectory() as tmpdir: result = build_image(tmpdir, "FROM python:3.11", "test-image:latest") assert result[0] == 0 assert "Successfully built" in result[1] mock_run.assert_called_once() call_args = mock_run.call_args assert "test-image:latest" in call_args[0][0] assert "build" in call_args[0][0] @patch("subprocess.run") def test_build_fails(self, mock_run) -> None: mock_run.return_value = MagicMock( returncode=1, stdout="", stderr="Error: failed to build", ) with tempfile.TemporaryDirectory() as tmpdir: result = build_image(tmpdir, "FROM invalid:image", "test-image:latest") assert result[0] == 1 assert "failed to build" in result[2] @patch("subprocess.run") def test_build_with_tag(self, mock_run) -> None: mock_run.return_value = MagicMock( returncode=0, stdout="", stderr="", ) with tempfile.TemporaryDirectory() as tmpdir: build_image(tmpdir, "FROM python:3.11", "my-registry/tool:v1.0") call_args = mock_run.call_args[0][0] assert "my-registry/tool:v1.0" in call_args @patch("subprocess.run") def test_build_command_structure(self, mock_run) -> None: mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") with tempfile.TemporaryDirectory() as tmpdir: build_image(tmpdir, "FROM python:3.11", "test:latest") cmd = mock_run.call_args[0][0] assert cmd[0] == "docker" assert cmd[1] == "build" assert "-t" in cmd assert "test:latest" in cmd assert tmpdir in cmd @patch("subprocess.run") def test_build_writes_dockerfile(self, mock_run) -> None: mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") with tempfile.TemporaryDirectory() as tmpdir: dockerfile_content = "FROM python:3.11\\nRUN pip install flask" build_image(tmpdir, dockerfile_content, "test:latest") dockerfile_path = Path(tmpdir) / "Dockerfile" assert dockerfile_path.exists() assert dockerfile_path.read_text() == dockerfile_content @patch("subprocess.run") def test_build_writes_context_files(self, mock_run) -> None: mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") with tempfile.TemporaryDirectory() as tmpdir: build_context = { "requirements.txt": "flask==2.0\\nnumpy==1.21", "app.py": "from flask import Flask\\napp = Flask(__name__)", } build_image(tmpdir, "FROM python:3.11", "test:latest", build_context) req_path = Path(tmpdir) / "requirements.txt" app_path = Path(tmpdir) / "app.py" assert req_path.exists() assert req_path.read_text() == "flask==2.0\\nnumpy==1.21" assert app_path.exists() assert app_path.read_text() == "from flask import Flask\\napp = Flask(__name__)" @patch("subprocess.run") def test_build_creates_nested_directories(self, mock_run) -> None: mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") with tempfile.TemporaryDirectory() as tmpdir: build_context = { "src/app.py": "print('hello')", } build_image(tmpdir, "FROM python:3.11", "test:latest", build_context) app_path = Path(tmpdir) / "src" / "app.py" assert app_path.exists() @patch("subprocess.run") def test_build_prevents_path_traversal(self, mock_run) -> None: mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") with tempfile.TemporaryDirectory() as tmpdir: build_context = { "../../../etc/passwd": "root:x:0:0", } with pytest.raises(ValueError, match="escapes instance directory"): build_image(tmpdir, "FROM python:3.11", "test:latest", build_context) mock_run.assert_not_called() @patch("subprocess.run") def test_build_timeout(self, mock_run) -> None: mock_run.side_effect = subprocess.TimeoutExpired(cmd=["docker", "build"], timeout=300) with tempfile.TemporaryDirectory() as tmpdir: result = build_image(tmpdir, "FROM python:3.11", "test:latest") assert result[0] == 1 assert "timed out" in result[2].lower() @patch("subprocess.run") def test_build_exception(self, mock_run) -> None: mock_run.side_effect = OSError("Docker not available") with tempfile.TemporaryDirectory() as tmpdir: result = build_image(tmpdir, "FROM python:3.11", "test:latest") assert result[0] == 1 assert "Docker not available" in result[2]