Files
pulse/scripts/tests/test_configuration_auth_docs.py
T
pulse-triage[bot] 200ce831a8 Keep installation quick starts on bootstrap authentication
Remove the shared Compose password from the main installation entry point, preserve image and data settings, and explain existing-deployment credential overrides. Extend the auth guide controls and rendered browser navigation checks to this page.

Contract-Neutral: Documentation and its controls now reflect the existing bootstrap and environment-override behaviour; no runtime or API contract changes.
Change-source: pulse-maintainer
2026-10-03 08:43:25 +01:00

208 lines
10 KiB
Python

#!/usr/bin/env python3
"""Exercise private-file auth recipes with synthetic credentials, not Docker.
The recorders verify file permissions, argument boundaries and failure stops.
They do not establish native container startup or a password-change outcome.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import re
import stat
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
GUIDES = ("CONFIGURATION.md", "INSTALL.md", "DOCKER.md", "i18n/de/README.md", "i18n/es/README.md")
HASH = "$2a$12$" + "a" * 53 # Synthetic file/argv sentinel, not a real password.
CONTENT = f"PULSE_AUTH_USER=admin\nPULSE_AUTH_PASS={HASH}\n"
def recipe(needle: str) -> str:
text = (ROOT / "docs/CONFIGURATION.md").read_text(encoding="utf-8")
matches = [block for block in re.findall(r"```bash\n(.*?)```", text, re.S)
if needle in block]
if len(matches) != 1:
raise AssertionError(f"expected one configuration recipe containing {needle!r}")
return matches[0]
EDITOR = '''#!/usr/bin/env python3
import json, os, stat, sys
from pathlib import Path
p = Path(sys.argv[1])
assert p == Path.home() / '.config/pulse/docker-auth.env'
assert stat.S_IMODE(p.stat().st_mode) == 0o600
assert stat.S_IMODE(p.parent.stat().st_mode) == 0o700
(Path.home() / 'editor.json').write_text(json.dumps(sys.argv[1:]))
sys.exit(int(os.environ.get('EDITOR_EXIT', '0')))
'''
DOCKER = '''#!/usr/bin/env python3
import json, os, sys
from pathlib import Path
args = sys.argv[1:]
p = Path(args[args.index('--env-file') + 1])
assert p == Path.home() / '.config/pulse/docker-auth.env'
assert p.read_text() == os.environ['EXPECTED_FILE']
assert os.environ['FIXTURE_HASH'] not in ' '.join(args)
assert not any(a.startswith('PULSE_AUTH_PASS=') for a in args)
(Path.home() / 'docker.json').write_text(json.dumps(args))
sys.exit(int(os.environ.get('DOCKER_EXIT', '0')))
'''
def environment(home: Path, **overrides: str) -> dict[str, str]:
tools = home / "tools"
tools.mkdir(exist_ok=True)
for name, body in (("vi", EDITOR), ("docker", DOCKER)):
script = tools / name
script.write_text(body, encoding="utf-8")
script.chmod(0o700)
return {**os.environ, "HOME": str(home), "PATH": f"{tools}:{os.environ['PATH']}",
"FIXTURE_HASH": HASH, "EXPECTED_FILE": CONTENT, **overrides}
def private_file(home: Path, content: str = CONTENT) -> Path:
path = home / ".config/pulse/docker-auth.env"
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
path.write_text(content, encoding="utf-8")
path.chmod(0o600)
return path
class ConfigurationAuthDocsTest(unittest.TestCase):
def test_no_shared_password_or_secret_in_command_arguments(self):
for name in GUIDES:
with self.subTest(guide=name):
text = (ROOT / "docs" / name).read_text(encoding="utf-8")
self.assertNotIn("secret123", text)
shell = "\n".join(re.findall(r"```bash\n(.*?)```", text, re.S))
self.assertNotRegex(shell, r"(?:-e|--env)\s+PULSE_AUTH_(?:PASS|USER)=")
self.assertNotRegex(shell, r"\b(?:export\s+)?PULSE_AUTH_PASS=")
if name != "CONFIGURATION.md":
self.assertNotRegex(text, r"PULSE_AUTH_PASS\s*[:=]")
def test_mirrors_and_sensitive_runtime_limit(self):
for name in GUIDES:
self.assertEqual((ROOT / "docs" / name).read_bytes(),
(ROOT / "frontend-modern/public/docs" / name).read_bytes())
text = (ROOT / "docs/CONFIGURATION.md").read_text(encoding="utf-8")
for expected in ("not out of Docker's", "does not scrub its original value",
"Do not source the file", "Deployment-supplied environment values take precedence",
"complete bcrypt hash", "unquoted", "no `$$` substitution",
"this new-container example is not an upgrade procedure"):
self.assertIn(expected, text)
def test_install_quick_start_preserves_data_and_bootstrap_route(self):
text = (ROOT / "docs/INSTALL.md").read_text(encoding="utf-8")
compose = next(block for block in re.findall(r"```yaml\n(.*?)```", text, re.S)
if "services:" in block)
for expected in ("image: ${PULSE_IMAGE:-rcourtman/pulse:vX.Y.Z}",
"pulse_data:/data", "PULSE_DEPLOYMENT_METHOD=docker_compose"):
self.assertIn(expected, compose)
self.assertNotRegex(compose, r"PULSE_AUTH_(?:USER|PASS)")
for expected in ("[bootstrap-token setup](#step-1-get-the-token)",
"[authentication guide](CONFIGURATION.md#private-docker-authentication-file)",
"Leave authentication overrides unset for a new install",
"not a Compose interpolation recipe", "do not reset authentication",
"deployment-supplied password takes precedence"):
self.assertIn(expected, text)
def test_preparation_preserves_existing_file_and_protects_both_paths(self):
for existing in (False, True):
with self.subTest(existing=existing), tempfile.TemporaryDirectory(prefix="auth docs ") as temp:
home = Path(temp)
path = home / ".config/pulse/docker-auth.env"
if existing:
private_file(home)
path.chmod(0o644)
path.parent.chmod(0o755)
result = subprocess.run(["bash", "-c", recipe('vi "$auth_file"')],
env=environment(home), capture_output=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr.decode())
self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o600)
self.assertEqual(stat.S_IMODE(path.parent.stat().st_mode), 0o700)
self.assertEqual(path.read_text(), CONTENT if existing else "")
self.assertEqual(json.loads((home / "editor.json").read_text()), [str(path)])
self.assertNotIn(HASH.encode(), result.stdout + result.stderr)
def test_preparation_refuses_symlink_without_touching_target(self):
for linked in ("file", "directory"):
with self.subTest(linked=linked), tempfile.TemporaryDirectory(prefix="auth docs ") as temp:
home = Path(temp)
target = home / "retained"
target.mkdir()
credential = target / "docker-auth.env"
credential.write_text(CONTENT)
credential.chmod(0o644)
directory = home / ".config/pulse"
directory.parent.mkdir()
if linked == "directory":
directory.symlink_to(target, target_is_directory=True)
else:
directory.mkdir()
(directory / "docker-auth.env").symlink_to(credential)
result = subprocess.run(["bash", "-c", recipe('vi "$auth_file"')],
env=environment(home), capture_output=True, timeout=10)
self.assertNotEqual(result.returncode, 0)
self.assertIn(b"Refusing a symlinked credential path", result.stderr)
self.assertEqual(credential.read_text(), CONTENT)
self.assertEqual(stat.S_IMODE(credential.stat().st_mode), 0o644)
self.assertFalse((home / "editor.json").exists())
def test_editor_failure_is_not_reported_as_preparation_success(self):
with tempfile.TemporaryDirectory(prefix="auth docs ") as temp:
home = Path(temp)
path = private_file(home)
result = subprocess.run(["bash", "-c", recipe('vi "$auth_file"')],
env=environment(home, EDITOR_EXIT="23"), capture_output=True, timeout=10)
self.assertEqual(result.returncode, 23)
self.assertEqual(path.read_text(), CONTENT)
def test_launch_passes_only_private_path_and_preserves_hash_bytes(self):
with tempfile.TemporaryDirectory(prefix="auth docs ") as temp:
home = Path(temp)
path = private_file(home)
result = subprocess.run(["bash", "-c", recipe("docker run -d")],
env=environment(home), capture_output=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr.decode())
argv = json.loads((home / "docker.json").read_text())
self.assertEqual(argv, ["run", "-d", "--name", "pulse", "-p", "7655:7655",
"-v", "pulse_data:/data", "--env-file", str(path), "-e",
"PULSE_DEPLOYMENT_METHOD=docker_run", "--restart", "unless-stopped",
"rcourtman/pulse:vX.Y.Z"])
self.assertNotIn(HASH, json.dumps(argv))
self.assertNotIn(HASH.encode(), result.stdout + result.stderr)
self.assertEqual(path.read_text(), CONTENT)
def test_absent_or_empty_file_prevents_docker_start(self):
for empty in (False, True):
with self.subTest(empty=empty), tempfile.TemporaryDirectory(prefix="auth docs ") as temp:
home = Path(temp)
if empty:
private_file(home, "")
result = subprocess.run(["bash", "-c", recipe("docker run -d")],
env=environment(home), capture_output=True, timeout=10)
self.assertNotEqual(result.returncode, 0)
self.assertIn(b"Prepare the private authentication file first", result.stderr)
self.assertFalse((home / "docker.json").exists())
def test_docker_failure_remains_failed(self):
with tempfile.TemporaryDirectory(prefix="auth docs ") as temp:
home = Path(temp)
path = private_file(home)
result = subprocess.run(["bash", "-c", recipe("docker run -d")],
env=environment(home, DOCKER_EXIT="31"), capture_output=True, timeout=10)
self.assertEqual(result.returncode, 31)
self.assertEqual(path.read_text(), CONTENT)
if __name__ == "__main__":
unittest.main()