Files
codexpi/tests/test_docker_project_scope.py

128 lines
5.3 KiB
Python
Raw Normal View History

2026-11-14 20:23:59 +01:00
from __future__ import annotations
import json
from pathlib import Path
from types import SimpleNamespace
import pytest
from codexpi.docker_service import DockerError, _belongs_to_project, container_action, containers
def inspected(root: Path, *, name: str = "project-web", working_dir: str | None = None, running: bool = False) -> dict:
return {
"Id": "a" * 64,
"Name": f"/{name}",
"Config": {"Labels": {
"com.docker.compose.project": "example",
"com.docker.compose.service": "web",
"com.docker.compose.project.working_dir": working_dir or str(root),
"com.docker.compose.project.config_files": str(root / "docker-compose.yml"),
}},
"Mounts": [],
"State": {"Running": running},
}
def test_project_membership_uses_compose_directory_or_bind_mount(tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
assert _belongs_to_project(inspected(project), project)
assert not _belongs_to_project(inspected(tmp_path / "other"), project)
assert _belongs_to_project({"Config": {"Labels": {}}, "Mounts": [
{"Type": "bind", "Source": str(project / "src")},
]}, project)
@pytest.mark.asyncio
async def test_container_listing_only_returns_current_project(tmp_path: Path, monkeypatch):
project = tmp_path / "project"
project.mkdir()
rows = [
{"ID": "a" * 12, "Names": "project-web"},
{"ID": "b" * 12, "Names": "someone-else"},
]
values = [inspected(project), {**inspected(tmp_path / "other", name="someone-else"), "Id": "b" * 64}]
async def fake(argv, cwd=None, timeout=60):
stdout = "\n".join(json.dumps(row) for row in rows) if argv[1] == "ps" else json.dumps(values)
return SimpleNamespace(returncode=0, stdout=stdout, stderr="")
monkeypatch.setattr("codexpi.docker_service.run_command", fake)
result = await containers(project)
assert [json.loads(line)["Names"] for line in result["stdout"].splitlines()] == ["project-web"]
@pytest.mark.asyncio
async def test_rebuild_targets_only_the_scoped_compose_service(tmp_path: Path, monkeypatch):
project = tmp_path / "project"
project.mkdir()
calls: list[tuple[list[str], Path | None]] = []
async def fake(argv, cwd=None, timeout=60):
calls.append((argv, cwd))
if len(calls) == 1:
return SimpleNamespace(returncode=0, stdout=json.dumps({"ID": "a" * 12, "Names": "project-web"}), stderr="")
if len(calls) == 2:
return SimpleNamespace(returncode=0, stdout=json.dumps([inspected(project)]), stderr="")
return SimpleNamespace(returncode=0, stdout="rebuilt", stderr="")
monkeypatch.setattr("codexpi.docker_service.run_command", fake)
monkeypatch.setattr("codexpi.docker_service._compose_prefix", lambda _root: ["docker", "compose"])
result = await container_action("project-web", "rebuild", project)
assert result["stdout"] == "rebuilt"
assert calls[-1] == ([
"docker", "compose", "-f", str(project / "docker-compose.yml"),
"--project-name", "example", "up", "-d", "--build", "web",
], project)
@pytest.mark.asyncio
async def test_project_action_rejects_unrelated_container(tmp_path: Path, monkeypatch):
project = tmp_path / "project"
project.mkdir()
async def fake(argv, cwd=None, timeout=60):
if argv[1] == "ps":
return SimpleNamespace(returncode=0, stdout=json.dumps({"ID": "b" * 12, "Names": "other"}), stderr="")
return SimpleNamespace(returncode=0, stdout=json.dumps([{**inspected(tmp_path / "other", name="other"), "Id": "b" * 64}]), stderr="")
monkeypatch.setattr("codexpi.docker_service.run_command", fake)
with pytest.raises(DockerError, match="n’appartient pas"):
await container_action("other", "restart", project)
@pytest.mark.asyncio
async def test_delete_only_removes_stopped_scoped_container(tmp_path: Path, monkeypatch):
project = tmp_path / "project"
project.mkdir()
calls: list[list[str]] = []
async def fake(argv, cwd=None, timeout=60):
calls.append(argv)
if argv[1] == "ps":
return SimpleNamespace(returncode=0, stdout=json.dumps({"ID": "a" * 12, "Names": "project-web"}), stderr="")
if argv[1] == "inspect":
return SimpleNamespace(returncode=0, stdout=json.dumps([inspected(project)]), stderr="")
return SimpleNamespace(returncode=0, stdout="project-web", stderr="")
monkeypatch.setattr("codexpi.docker_service.run_command", fake)
result = await container_action("project-web", "delete", project)
assert result["stdout"] == "project-web"
assert calls[-1] == ["docker", "rm", "project-web"]
@pytest.mark.asyncio
async def test_delete_refuses_running_container(tmp_path: Path, monkeypatch):
project = tmp_path / "project"
project.mkdir()
async def fake(argv, cwd=None, timeout=60):
if argv[1] == "ps":
return SimpleNamespace(returncode=0, stdout=json.dumps({"ID": "a" * 12, "Names": "project-web"}), stderr="")
return SimpleNamespace(returncode=0, stdout=json.dumps([inspected(project, running=True)]), stderr="")
monkeypatch.setattr("codexpi.docker_service.run_command", fake)
with pytest.raises(DockerError, match="Arrêtez"):
await container_action("project-web", "delete", project)