Files
codexpi/tests/test_docker_project_scope.py
CodexPi 995b17a6e2
Some checks are pending
Qualité / tests (push) Waiting to run
feat: publish CodexPi contest snapshot
2026-11-14 20:23:59 +01:00

128 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)