feat: publish CodexPi contest snapshot
Some checks are pending
Qualité / tests (push) Waiting to run
Some checks are pending
Qualité / tests (push) Waiting to run
This commit is contained in:
127
tests/test_docker_project_scope.py
Normal file
127
tests/test_docker_project_scope.py
Normal file
@@ -0,0 +1,127 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user