Replaces LICENSE (GPLv3 -> AGPLv3) and prepends `SPDX-License-Identifier: AGPL-3.0-or-later` to every source file across decnet/, decnet_web/, tests/, scripts/, and tools/. Rationale: closes the GPLv3 ASP loophole so any party operating a modified DECNET as a network service must offer their modified source. Personal copyright (Samuel Paschuan) + inbound=outbound contributions make a future unilateral relicense infeasible. - LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt) - COPYRIGHT: project copyright notice - tools/add_spdx_headers.py: idempotent header injector (shebang- and PEP 263-aware) Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh). No behavior change; comments only.
181 lines
5.7 KiB
Python
181 lines
5.7 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""Tests for ``POST /api/v1/workers/{name}/start`` + ``start-all``.
|
|
|
|
Uses the shared ``client`` / ``auth_token`` / ``viewer_token`` fixtures
|
|
from ``tests/api/conftest.py``. Stubs out ``systemd_control`` so tests
|
|
never touch real systemctl.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Set
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from decnet.web.router.workers import api_list_workers as _list
|
|
from decnet.web.router.workers import api_start_all_workers as _start_all
|
|
from decnet.web.router.workers import api_start_worker as _start
|
|
from decnet.web.services import systemd_control as _sc
|
|
|
|
|
|
def _patch_installed(monkeypatch: Any, names: Set[str]) -> None:
|
|
async def _stub() -> Set[str]:
|
|
return set(names)
|
|
|
|
# Each module imported `systemd_control` directly; patch on the
|
|
# module-level attribute so all three endpoints see the stub.
|
|
for mod in (_start, _start_all, _list):
|
|
monkeypatch.setattr(mod.systemd_control, "list_installed", _stub)
|
|
|
|
|
|
def _patch_start(monkeypatch: Any, *, raises: _sc.SystemctlError | None = None) -> list[str]:
|
|
calls: list[str] = []
|
|
|
|
async def _stub(name: str) -> None:
|
|
calls.append(name)
|
|
if raises is not None:
|
|
raise raises
|
|
|
|
monkeypatch.setattr(_sc, "start", _stub)
|
|
return calls
|
|
|
|
|
|
def _patch_is_active(monkeypatch: Any, active: Set[str]) -> None:
|
|
async def _stub(name: str) -> bool:
|
|
return name in active
|
|
|
|
monkeypatch.setattr(_sc, "is_active", _stub)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_worker_admin_happy_path(
|
|
client: httpx.AsyncClient, auth_token: str, monkeypatch,
|
|
) -> None:
|
|
_patch_installed(monkeypatch, {"mutator", "bus"})
|
|
calls = _patch_start(monkeypatch)
|
|
resp = await client.post(
|
|
"/api/v1/workers/mutator/start",
|
|
headers={"Authorization": f"Bearer {auth_token}"},
|
|
)
|
|
assert resp.status_code == 202
|
|
body = resp.json()
|
|
assert body == {"accepted": True, "worker": "mutator", "action": "start"}
|
|
assert calls == ["mutator"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_worker_viewer_forbidden(
|
|
client: httpx.AsyncClient, viewer_token: str, monkeypatch,
|
|
) -> None:
|
|
_patch_installed(monkeypatch, {"mutator"})
|
|
_patch_start(monkeypatch)
|
|
resp = await client.post(
|
|
"/api/v1/workers/mutator/start",
|
|
headers={"Authorization": f"Bearer {viewer_token}"},
|
|
)
|
|
assert resp.status_code == 403
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_worker_unknown_name_404(
|
|
client: httpx.AsyncClient, auth_token: str, monkeypatch,
|
|
) -> None:
|
|
_patch_installed(monkeypatch, {"mutator"})
|
|
_patch_start(monkeypatch)
|
|
resp = await client.post(
|
|
"/api/v1/workers/nosuch/start",
|
|
headers={"Authorization": f"Bearer {auth_token}"},
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_worker_not_installed_503(
|
|
client: httpx.AsyncClient, auth_token: str, monkeypatch,
|
|
) -> None:
|
|
_patch_installed(monkeypatch, set()) # nothing installed
|
|
_patch_start(monkeypatch)
|
|
resp = await client.post(
|
|
"/api/v1/workers/mutator/start",
|
|
headers={"Authorization": f"Bearer {auth_token}"},
|
|
)
|
|
assert resp.status_code == 503
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_worker_systemctl_failure_502(
|
|
client: httpx.AsyncClient, auth_token: str, monkeypatch,
|
|
) -> None:
|
|
_patch_installed(monkeypatch, {"mutator"})
|
|
err = _sc.SystemctlError(
|
|
unit="decnet-mutator.service",
|
|
returncode=1,
|
|
stderr="Failed to start decnet-mutator.service: unit not found",
|
|
)
|
|
_patch_start(monkeypatch, raises=err)
|
|
resp = await client.post(
|
|
"/api/v1/workers/mutator/start",
|
|
headers={"Authorization": f"Bearer {auth_token}"},
|
|
)
|
|
assert resp.status_code == 502
|
|
body = resp.json()
|
|
assert "not found" in body["detail"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_all_aggregates_success_running_and_failure(
|
|
client: httpx.AsyncClient, auth_token: str, monkeypatch,
|
|
) -> None:
|
|
_patch_installed(monkeypatch, {"bus", "api", "mutator"})
|
|
_patch_is_active(monkeypatch, {"bus"}) # bus is already running
|
|
|
|
async def _stub_start(name: str) -> None:
|
|
if name == "mutator":
|
|
raise _sc.SystemctlError(
|
|
unit="decnet-mutator.service",
|
|
returncode=1,
|
|
stderr="Unit decnet-mutator.service is masked.",
|
|
)
|
|
# api starts cleanly
|
|
|
|
monkeypatch.setattr(_sc, "start", _stub_start)
|
|
|
|
resp = await client.post(
|
|
"/api/v1/workers/start-all",
|
|
headers={"Authorization": f"Bearer {auth_token}"},
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["already_running"] == ["bus"]
|
|
assert body["started"] == ["api"]
|
|
assert len(body["failed"]) == 1
|
|
assert body["failed"][0]["name"] == "mutator"
|
|
assert "masked" in body["failed"][0]["reason"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_all_viewer_forbidden(
|
|
client: httpx.AsyncClient, viewer_token: str, monkeypatch,
|
|
) -> None:
|
|
_patch_installed(monkeypatch, {"bus"})
|
|
resp = await client.post(
|
|
"/api/v1/workers/start-all",
|
|
headers={"Authorization": f"Bearer {viewer_token}"},
|
|
)
|
|
assert resp.status_code == 403
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_all_skips_uninstalled(
|
|
client: httpx.AsyncClient, auth_token: str, monkeypatch,
|
|
) -> None:
|
|
_patch_installed(monkeypatch, set()) # no units installed
|
|
_patch_is_active(monkeypatch, set())
|
|
_patch_start(monkeypatch)
|
|
resp = await client.post(
|
|
"/api/v1/workers/start-all",
|
|
headers={"Authorization": f"Bearer {auth_token}"},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json() == {"started": [], "already_running": [], "failed": []}
|