Some checks failed
CI / skinny-install (aco) (push) Successful in 1m14s
CI / skinny-install (bib) (push) Successful in 5m47s
CI / skinny-install (api) (push) Successful in 37s
CI / skinny-install (bcda) (push) Successful in 37s
CI / skinny-install (bls) (push) Successful in 40s
CI / skinny-install (ccw) (push) Successful in 41s
CI / skinny-install (cli) (push) Successful in 42s
CI / skinny-install (cms) (push) Successful in 39s
CI / skinny-install (conf) (push) Successful in 41s
CI / skinny-install (opps) (push) Successful in 39s
CI / skinny-install (perf) (push) Successful in 40s
CI / skinny-install (pfs) (push) Successful in 50s
CI / skinny-install (rex) (push) Successful in 33s
Deploy / build-scan-report (push) Failing after 12m6s
Infra CI / notebooks (push) Successful in 22s
Infra CI / zotero (push) Successful in 12s
Infra CI / docs (push) Successful in 11s
Infra CI / api (push) Successful in 12s
Infra CI / mc (push) Successful in 22s
CI / lint-test (push) Failing after 41m1s
Clean 17 files across src/, tests/, dev/, stack.toml, deploy.sh: - api/auth/provision.py: remove WoodpeckerClient, provision_woodpecker, _get_woodpecker_token, woodpecker field from ProvisionResult - api/auth/manifest.py: remove woodpecker from CREDENTIALS + Provisioner - api/diag: remove woodpecker log fetching - sem/hooks.py: remove woodpecker sync step - stack.toml: remove [services.woodpecker] config - deploy.sh: remove woodpecker deploy steps - dev/scripts: remove woodpecker from config gen, secrets, readme - tests: remove all woodpecker assertions and test cases Zero woodpecker references remain in the codebase.
166 lines
5.4 KiB
Python
166 lines
5.4 KiB
Python
"""Tests for api.auth.deploy — two-phase credential rotation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from api.auth.deploy import (
|
|
_set_env_var,
|
|
deploy,
|
|
verify_all,
|
|
verify_gitea,
|
|
verify_postgres,
|
|
verify_rustfs,
|
|
)
|
|
from api.auth.provision import derive_all
|
|
|
|
ROOT = bytes.fromhex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
|
|
COMMIT = "abc1234"
|
|
|
|
|
|
class TestVerifyPostgres:
|
|
def test_returns_empty_on_success(self):
|
|
values = derive_all(ROOT, COMMIT)
|
|
with patch("api.auth.deploy.subprocess.run") as mock:
|
|
mock.return_value = subprocess.CompletedProcess([], 0)
|
|
errors = verify_postgres(values, container="pg")
|
|
assert errors == []
|
|
|
|
def test_returns_errors_on_failure(self):
|
|
values = derive_all(ROOT, COMMIT)
|
|
with patch("api.auth.deploy.subprocess.run") as mock:
|
|
mock.return_value = subprocess.CompletedProcess([], 1, stderr="auth failed")
|
|
errors = verify_postgres(values, container="pg")
|
|
assert len(errors) > 0
|
|
assert "auth failed" in errors[0]
|
|
|
|
|
|
class TestVerifyGitea:
|
|
def test_returns_empty_on_success(self):
|
|
values = {"GITEA_TOKEN": "tok"}
|
|
mock_resp = MagicMock(status_code=200)
|
|
with patch("api.auth.deploy.httpx.get", return_value=mock_resp):
|
|
errors = verify_gitea(values)
|
|
assert errors == []
|
|
|
|
def test_returns_error_on_401(self):
|
|
values = {"GITEA_TOKEN": "bad"}
|
|
mock_resp = MagicMock(status_code=401)
|
|
with patch("api.auth.deploy.httpx.get", return_value=mock_resp):
|
|
errors = verify_gitea(values)
|
|
assert len(errors) == 1
|
|
assert "401" in errors[0]
|
|
|
|
def test_returns_error_on_no_token(self):
|
|
errors = verify_gitea({})
|
|
assert len(errors) == 1
|
|
assert "no GITEA_TOKEN" in errors[0]
|
|
|
|
|
|
class TestVerifyRustfs:
|
|
def test_returns_empty_on_success(self):
|
|
mock_resp = MagicMock(status_code=200)
|
|
with patch("api.auth.deploy.httpx.get", return_value=mock_resp):
|
|
errors = verify_rustfs()
|
|
assert errors == []
|
|
|
|
def test_returns_error_on_failure(self):
|
|
with patch("api.auth.deploy.httpx.get", side_effect=Exception("down")):
|
|
errors = verify_rustfs()
|
|
assert len(errors) == 1
|
|
|
|
|
|
class TestVerifyAll:
|
|
def test_aggregates_errors(self):
|
|
values = derive_all(ROOT, COMMIT)
|
|
values["GITEA_TOKEN"] = "tok"
|
|
with (
|
|
patch("api.auth.deploy.verify_postgres", return_value=["pg: fail"]),
|
|
patch("api.auth.deploy.verify_gitea", return_value=[]),
|
|
patch("api.auth.deploy.verify_rustfs", return_value=["rustfs: fail"]),
|
|
):
|
|
errors = verify_all(values)
|
|
assert len(errors) == 2
|
|
|
|
|
|
class TestSetEnvVar:
|
|
def test_updates_existing(self, tmp_path: Path):
|
|
p = tmp_path / ".env"
|
|
p.write_text("A=1\nB=2\n")
|
|
_set_env_var(p, "B", "99")
|
|
assert "B=99" in p.read_text()
|
|
assert "B=2" not in p.read_text()
|
|
|
|
def test_appends_new(self, tmp_path: Path):
|
|
p = tmp_path / ".env"
|
|
p.write_text("A=1\n")
|
|
_set_env_var(p, "B", "2")
|
|
content = p.read_text()
|
|
assert "A=1" in content
|
|
assert "B=2" in content
|
|
|
|
|
|
class TestDeploy:
|
|
def test_two_phase_rotation(self, tmp_path: Path):
|
|
env = tmp_path / ".env"
|
|
env.write_text("OLD_KEY=keep\n")
|
|
|
|
with (
|
|
# provision_postgres no longer called
|
|
patch("api.auth.deploy.provision_gitea", return_value="new-tok"),
|
|
patch("api.auth.deploy.subprocess.run"),
|
|
patch("api.auth.deploy.verify_all", return_value=[]),
|
|
patch("api.auth.deploy.time.sleep"),
|
|
):
|
|
result = deploy(ROOT, COMMIT, env, compose_dir=tmp_path)
|
|
|
|
assert result.ok
|
|
assert result.postgres
|
|
assert result.gitea
|
|
assert result.env_written
|
|
content = env.read_text()
|
|
assert "GITEA_TOKEN=new-tok" in content
|
|
assert "OLD_KEY=keep" in content
|
|
|
|
def test_rollback_on_health_failure(self, tmp_path: Path):
|
|
env = tmp_path / ".env"
|
|
env.write_text("SAFE=original\n")
|
|
|
|
with (
|
|
# provision_postgres no longer called
|
|
patch("api.auth.deploy.provision_gitea", return_value="tok"),
|
|
patch("api.auth.deploy.subprocess.run"),
|
|
patch(
|
|
"api.auth.deploy.verify_all",
|
|
return_value=["pg: auth failed"],
|
|
),
|
|
patch("api.auth.deploy.time.sleep"),
|
|
):
|
|
result = deploy(ROOT, COMMIT, env, compose_dir=tmp_path)
|
|
|
|
assert not result.ok
|
|
# .env should be rolled back to original
|
|
content = env.read_text()
|
|
assert "SAFE=original" in content
|
|
|
|
def test_gitea_failure_still_deploys(self, tmp_path: Path):
|
|
env = tmp_path / ".env"
|
|
env.write_text("GITEA_TOKEN=old\n")
|
|
|
|
with (
|
|
patch(
|
|
"api.auth.deploy.provision_gitea",
|
|
side_effect=Exception("gitea down"),
|
|
),
|
|
patch("api.auth.deploy.subprocess.run"),
|
|
patch("api.auth.deploy.verify_all", return_value=[]),
|
|
patch("api.auth.deploy.time.sleep"),
|
|
):
|
|
result = deploy(ROOT, COMMIT, env, compose_dir=tmp_path)
|
|
|
|
assert not result.gitea
|
|
assert result.postgres # preserved, not rotated
|
|
assert result.env_written
|