128 lines
5.3 KiB
Python
128 lines
5.3 KiB
Python
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)
|