3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
172 lines
5.3 KiB
Python
172 lines
5.3 KiB
Python
# SPDX-License-Identifier: MIT
|
|
"""Tests for winpodx.core.rotation — moved from tests/test_provisioner.py
|
|
in Track A Sprint 1 Step 2."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture()
|
|
def _rotation_cfg(tmp_path, monkeypatch):
|
|
"""Config set up to trigger _auto_rotate_password work."""
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
|
|
from winpodx.core.config import Config
|
|
|
|
cfg = Config()
|
|
cfg.rdp.user = "User"
|
|
cfg.rdp.password = "old-password"
|
|
cfg.rdp.password_max_age = 1 # day
|
|
cfg.rdp.password_updated = (datetime.now(timezone.utc) - timedelta(days=10)).isoformat()
|
|
cfg.pod.backend = "podman"
|
|
cfg.save()
|
|
return cfg
|
|
|
|
|
|
def test_rotation_rollback_success_reverts_password(_rotation_cfg, monkeypatch):
|
|
# When config.save fails but Windows rollback succeeds, config keeps the old password.
|
|
from winpodx.core import rotation
|
|
from winpodx.core.pod import PodState, PodStatus
|
|
|
|
monkeypatch.setattr(
|
|
"winpodx.core.rotation.pod_status",
|
|
lambda cfg: PodStatus(state=PodState.RUNNING),
|
|
)
|
|
monkeypatch.setattr(rotation, "_change_windows_password", lambda cfg, pw: True)
|
|
|
|
with patch.object(_rotation_cfg, "save", side_effect=OSError("disk full")):
|
|
result = rotation._auto_rotate_password(_rotation_cfg)
|
|
|
|
assert result.rdp.password == "old-password"
|
|
assert not rotation._rotation_marker_path().exists()
|
|
|
|
|
|
def test_rotation_compose_failure_does_not_save_new_password(_rotation_cfg, monkeypatch):
|
|
# Compose failure happens before config save, so disk config must never get
|
|
# the new password. Windows is rolled back to the old password.
|
|
from winpodx.core import rotation
|
|
from winpodx.core.pod import PodState, PodStatus
|
|
|
|
monkeypatch.setattr(
|
|
"winpodx.core.rotation.pod_status",
|
|
lambda cfg: PodStatus(state=PodState.RUNNING),
|
|
)
|
|
|
|
changes: list[str] = []
|
|
|
|
def fake_change(cfg, pw):
|
|
changes.append(pw)
|
|
return True
|
|
|
|
monkeypatch.setattr(rotation, "_change_windows_password", fake_change)
|
|
monkeypatch.setattr(
|
|
rotation,
|
|
"generate_compose",
|
|
lambda cfg: (_ for _ in ()).throw(OSError("compose full")),
|
|
)
|
|
|
|
with patch.object(_rotation_cfg, "save") as mock_save:
|
|
result = rotation._auto_rotate_password(_rotation_cfg)
|
|
|
|
assert result.rdp.password == "old-password"
|
|
assert len(changes) == 2
|
|
assert changes[1] == "old-password"
|
|
mock_save.assert_not_called()
|
|
assert not rotation._rotation_marker_path().exists()
|
|
|
|
|
|
def test_rotation_rollback_failure_writes_marker(_rotation_cfg, monkeypatch):
|
|
# Config save and Windows rollback both fail: must log error and write .rotation_pending marker.
|
|
from winpodx.core import rotation
|
|
from winpodx.core.pod import PodState, PodStatus
|
|
|
|
monkeypatch.setattr(
|
|
"winpodx.core.rotation.pod_status",
|
|
lambda cfg: PodStatus(state=PodState.RUNNING),
|
|
)
|
|
|
|
calls: list[str] = []
|
|
|
|
def fake_change(cfg, pw):
|
|
calls.append(pw)
|
|
return len(calls) == 1
|
|
|
|
monkeypatch.setattr(rotation, "_change_windows_password", fake_change)
|
|
|
|
with patch.object(_rotation_cfg, "save", side_effect=OSError("disk full")):
|
|
rotation._auto_rotate_password(_rotation_cfg)
|
|
|
|
assert len(calls) == 2
|
|
marker = rotation._rotation_marker_path()
|
|
assert marker.exists()
|
|
assert marker.stat().st_mode & 0o777 == 0o600
|
|
|
|
|
|
def test_check_rotation_pending_warns(tmp_path, monkeypatch, caplog):
|
|
import logging
|
|
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
from winpodx.core import rotation
|
|
|
|
marker = rotation._rotation_marker_path()
|
|
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
marker.write_text("pending\n")
|
|
|
|
with caplog.at_level(logging.ERROR, logger="winpodx.core.rotation"):
|
|
rotation._check_rotation_pending()
|
|
|
|
assert any("Pending password rotation" in r.message for r in caplog.records)
|
|
|
|
|
|
def test_rotation_marker_cleared_on_success(_rotation_cfg, monkeypatch):
|
|
# A successful rotation must clear any previously-written marker.
|
|
from winpodx.core import rotation
|
|
from winpodx.core.pod import PodState, PodStatus
|
|
|
|
marker = rotation._rotation_marker_path()
|
|
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
marker.write_text("pending\n")
|
|
|
|
monkeypatch.setattr(
|
|
"winpodx.core.rotation.pod_status",
|
|
lambda cfg: PodStatus(state=PodState.RUNNING),
|
|
)
|
|
monkeypatch.setattr(rotation, "_change_windows_password", lambda cfg, pw: True)
|
|
|
|
rotation._auto_rotate_password(_rotation_cfg)
|
|
|
|
assert not marker.exists()
|
|
|
|
|
|
# --- Public API smoke tests ---
|
|
|
|
|
|
def test_maybe_rotate_returns_cfg_when_no_password(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
from winpodx.core import rotation
|
|
from winpodx.core.config import Config
|
|
|
|
cfg = Config()
|
|
cfg.rdp.password = ""
|
|
|
|
result = rotation.maybe_rotate(cfg)
|
|
|
|
assert result is cfg
|
|
|
|
|
|
def test_check_pending_no_marker_quiet(tmp_path, monkeypatch, caplog):
|
|
import logging
|
|
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
from winpodx.core import rotation
|
|
|
|
with caplog.at_level(logging.ERROR, logger="winpodx.core.rotation"):
|
|
rotation.check_pending()
|
|
|
|
assert not any("Pending password rotation" in r.message for r in caplog.records)
|