FrameBridge 0.3.4 corresponding-source release
Snapshot assembled only from the signed corresponding-source archive. Source archive SHA-256: db91a270cdefc4be79056ed38a0b4a16747e1ab130592645eac40fa3809809df
This commit is contained in:
1 parent
b8c9b713d8
commit
cdd980e1eb
993 files changed
+255481
-1720
No files matched your search
@@ -31,7 +31,8 @@ class CandidateArchiveTests(unittest.TestCase):
|
||||
mock.patch.object(archive_sources, "validate_vendor", return_value=evidence), \
|
||||
contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()) as stderr:
|
||||
archive_sources.main(args)
|
||||
return self.base / "release/candidate-source" / archive_sources.ARCHIVE_NAME, stderr.getvalue()
|
||||
directory = "public-source" if "--public-release" in args else "candidate-source"
|
||||
return self.base / "release" / directory / archive_sources.ARCHIVE_NAME, stderr.getvalue()
|
||||
|
||||
def test_default_archive_declares_missing_alternate_abis(self):
|
||||
output, warning = self.run_archive([])
|
||||
@@ -64,6 +65,25 @@ class CandidateArchiveTests(unittest.TestCase):
|
||||
self.assertEqual(set(data["vendors"]), {"cp312", "cp313", "cp314"})
|
||||
self.assertEqual(data["omitted_vendor_abis"], [])
|
||||
|
||||
def test_public_archive_requires_matching_cp312_and_excludes_experimental_input(self):
|
||||
for args in (["--public-release"], ["--public-release", "--vendor-cp312", str(self.root), "--vendor-cp314", str(self.root)]):
|
||||
with self.assertRaises(SystemExit):
|
||||
self.run_archive(args)
|
||||
|
||||
def test_public_archive_describes_three_profiles_without_claiming_publication(self):
|
||||
vendor = self.root / "cp312"; vendor.mkdir()
|
||||
(vendor / "module.py").write_text("# reviewed cp312 input\n")
|
||||
output, warning = self.run_archive(["--public-release", "--vendor-cp312", str(vendor)])
|
||||
self.assertFalse(warning)
|
||||
manifest = json.loads((output.parent / "SOURCE-ARCHIVE-MANIFEST.json").read_text())
|
||||
self.assertEqual(manifest["status"], "RELEASE_SOURCE_ASSEMBLED")
|
||||
self.assertEqual(set(manifest["vendor_inputs"]["vendors"]), {"cp312", "cp313"})
|
||||
with tarfile.open(output) as archive:
|
||||
readme = archive.extractfile(archive_sources.ARCHIVE_ROOT + "/README-SOURCE-ARCHIVE.md").read().decode()
|
||||
self.assertIn("--public-release --source-manifest", readme)
|
||||
self.assertNotIn("compatibility candidate source", readme)
|
||||
self.assertIn("Signatures, publication", readme)
|
||||
|
||||
def test_alternate_vendor_symlink_is_rejected(self):
|
||||
vendor = self.root / "cp312"; vendor.mkdir()
|
||||
(vendor / "unsafe.py").symlink_to(self.input)
|
||||
|
||||
@@ -9,7 +9,7 @@ import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from framebridge_expanded import autostart, config, runtime, connection_background
|
||||
from framebridge_expanded import autostart, config, runtime, connection_background, credential_helper
|
||||
|
||||
ID = "tunnel_0123456789abcdef0123456789abcdef"
|
||||
MARKER = "FRAMEBRIDGE-SYNTHETIC-NOT-A-REAL-KEY-AUTOSTART"
|
||||
@@ -435,6 +435,161 @@ class AutostartTests(unittest.TestCase):
|
||||
self.assertNotIn("LoadCredentialEncrypted", unit.read_text())
|
||||
self.assertFalse((directory / "credential.cred").exists())
|
||||
|
||||
def test_fresh_node_with_existing_linger_authenticates_before_existing_grant_cleanup(self):
|
||||
for start_now in (False, True):
|
||||
with self.subTest(start_now=start_now):
|
||||
state = self.home / ("node-now" if start_now else "node-boot")
|
||||
config.save_config(state, config.default_config(role="node"))
|
||||
before = (state / "config.json").read_bytes()
|
||||
self.calls.clear()
|
||||
self.system_calls.clear()
|
||||
with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "_helper_present", return_value=True), mock.patch.object(autostart, "_node_helper_cleanup_required", return_value=True), mock.patch.object(autostart, "_credential_backend", side_effect=AssertionError("A node has no connection credential")):
|
||||
autostart._enable(state, start_now=start_now, confirm=False)
|
||||
_, directory, unit = autostart._paths(state)
|
||||
expected_prune = ["sudo", "-n", "--", str(autostart.HELPER), "prune", autostart._profile_id(state), "-"]
|
||||
# Exercise the real prune implementation through the command
|
||||
# boundary: no authentication or global boot change is hidden.
|
||||
self.assertEqual([["sudo", "-v"], expected_prune], [args for args, _ in self.calls])
|
||||
self.assertEqual(before, (state / "config.json").read_bytes())
|
||||
self.assertTrue(autostart._manifest(directory)["linger_before"])
|
||||
self.assertTrue(autostart._manifest(directory)["node"])
|
||||
self.assertFalse((directory / "credential.cred").exists())
|
||||
self.assertIn(("enable", "--now", unit.name) if start_now else ("enable", unit.name), self.system_calls)
|
||||
self.assertEqual(0o600, stat.S_IMODE(unit.stat().st_mode))
|
||||
self.inputs.assert_not_called()
|
||||
self.key_prompt.assert_not_called()
|
||||
|
||||
def test_fresh_node_with_existing_linger_fails_closed_when_prune_is_not_authorized(self):
|
||||
config.save_config(self.state, config.default_config(role="node"))
|
||||
before = (self.state / "config.json").read_bytes()
|
||||
|
||||
def denied_prune(args, **kwargs):
|
||||
if len(args) > 4 and args[:4] == ["sudo", "-n", "--", str(autostart.HELPER)] and args[4] == "prune":
|
||||
self.calls.append((args, kwargs))
|
||||
raise RuntimeError("Synthetic cleanup grant denied")
|
||||
return self.fake_command(args, **kwargs)
|
||||
|
||||
self.command.side_effect = denied_prune
|
||||
with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "_helper_present", return_value=True), mock.patch.object(autostart, "_node_helper_cleanup_required", return_value=True):
|
||||
with self.assertRaisesRegex(RuntimeError, "cleanup grant denied"):
|
||||
autostart._enable(self.state, confirm=False)
|
||||
_, directory, unit = autostart._paths(self.state)
|
||||
self.assertEqual(before, (self.state / "config.json").read_bytes())
|
||||
self.assertFalse(unit.exists())
|
||||
self.assertFalse((directory / "installation.json").exists())
|
||||
self.assertEqual([], self.system_calls)
|
||||
self.assertEqual(["sudo", "-v"], self.calls[0][0])
|
||||
self.assertFalse(any("loginctl" in args for args, _ in self.calls))
|
||||
|
||||
def test_fresh_node_with_existing_linger_and_no_managed_grants_needs_no_privilege(self):
|
||||
policies = self.root / "policies"
|
||||
policies.mkdir()
|
||||
profile = autostart._profile_id(self.state)
|
||||
# Other profiles and owners must not cause an unnecessary request.
|
||||
(policies / ("framebridge-credential-" + str(os.getuid()) + "-" + "a" * 64 + "-" + "b" * 64)).touch()
|
||||
(policies / ("framebridge-credential-987654321-" + profile + "-" + "b" * 64)).touch()
|
||||
config.save_config(self.state, config.default_config(role="node"))
|
||||
before = (self.state / "config.json").read_bytes()
|
||||
with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "_helper_present", return_value=True), mock.patch.object(credential_helper, "SUDOERS", policies), mock.patch.object(credential_helper, "_root_path", side_effect=lambda path, **kw: path):
|
||||
autostart._enable(self.state, confirm=False, start_now=False)
|
||||
_, directory, unit = autostart._paths(self.state)
|
||||
self.assertEqual([], self.calls)
|
||||
self.assertEqual(before, (self.state / "config.json").read_bytes())
|
||||
self.assertIn(("enable", unit.name), self.system_calls)
|
||||
self.assertTrue(autostart._manifest(directory)["node"])
|
||||
self.assertFalse((directory / "credential.cred").exists())
|
||||
self.key_prompt.assert_not_called()
|
||||
|
||||
def test_ambiguous_node_policy_fails_before_authentication_or_managed_writes(self):
|
||||
policies = self.root / "policies"
|
||||
policies.mkdir()
|
||||
name = "framebridge-credential-authorize-" + str(os.getuid()) + "-" + autostart._profile_id(self.state) + "-unknown"
|
||||
(policies / name).touch()
|
||||
config.save_config(self.state, config.default_config(role="node"))
|
||||
before = (self.state / "config.json").read_bytes()
|
||||
with mock.patch.object(autostart, "_linger", return_value=True), mock.patch.object(autostart, "_helper_present", return_value=True), mock.patch.object(credential_helper, "SUDOERS", policies), mock.patch.object(credential_helper, "_root_path", side_effect=lambda path, **kw: path):
|
||||
with self.assertRaisesRegex(RuntimeError, "ambiguous.*administrator review"):
|
||||
autostart._enable(self.state, confirm=False)
|
||||
_, directory, unit = autostart._paths(self.state)
|
||||
self.assertEqual([], self.calls)
|
||||
self.assertEqual([], self.system_calls)
|
||||
self.assertEqual(before, (self.state / "config.json").read_bytes())
|
||||
self.assertFalse(unit.exists())
|
||||
self.assertFalse((directory / "installation.json").exists())
|
||||
self.assertFalse((directory / "credential.cred").exists())
|
||||
self.assertTrue((policies / name).exists())
|
||||
|
||||
def test_fresh_node_without_linger_requires_authentication_before_managed_writes(self):
|
||||
config.save_config(self.state, config.default_config(role="node"))
|
||||
before = (self.state / "config.json").read_bytes()
|
||||
|
||||
def denied_authentication(args, **kwargs):
|
||||
if args == ["sudo", "-v"]:
|
||||
self.calls.append((args, kwargs))
|
||||
raise RuntimeError("Synthetic owner authentication required")
|
||||
return self.fake_command(args, **kwargs)
|
||||
|
||||
self.command.side_effect = denied_authentication
|
||||
with mock.patch.object(autostart, "_helper_present", return_value=True):
|
||||
with self.assertRaisesRegex(RuntimeError, "owner authentication required"):
|
||||
autostart._enable(self.state, confirm=False)
|
||||
_, directory, unit = autostart._paths(self.state)
|
||||
self.assertEqual([["sudo", "-v"]], [args for args, _ in self.calls])
|
||||
self.assertEqual(before, (self.state / "config.json").read_bytes())
|
||||
self.assertFalse(unit.exists())
|
||||
self.assertFalse((directory / "installation.json").exists())
|
||||
self.assertFalse((directory / "credential.cred").exists())
|
||||
self.assertEqual([], self.system_calls)
|
||||
self.key_prompt.assert_not_called()
|
||||
|
||||
def test_fresh_node_without_linger_keeps_administrative_boot_setup(self):
|
||||
config.save_config(self.state, config.default_config(role="node"))
|
||||
with mock.patch.object(autostart, "_helper_present", return_value=True):
|
||||
autostart._enable(self.state, confirm=False)
|
||||
user = autostart.pwd.getpwuid(os.getuid()).pw_name
|
||||
expected_prune = ["sudo", "-n", "--", str(autostart.HELPER), "prune", autostart._profile_id(self.state), "-"]
|
||||
self.assertEqual([["sudo", "-v"], expected_prune,
|
||||
["sudo", "-n", "--", "loginctl", "enable-linger", user]],
|
||||
[args for args, _ in self.calls])
|
||||
self.key_prompt.assert_not_called()
|
||||
|
||||
def test_central_with_existing_linger_still_authenticates_and_protects_its_key(self):
|
||||
settings = config.default_config(role="central")
|
||||
settings.update(tunnel_id=ID, headless="auto")
|
||||
config.save_config(self.state, settings)
|
||||
with mock.patch.object(autostart, "_linger", return_value=True):
|
||||
autostart.enable(self.state)
|
||||
self.assertIn(["sudo", "-v"], [args for args, _ in self.calls])
|
||||
encryption = [(args, options) for args, options in self.calls if "encrypt" in args and "-ll" not in args]
|
||||
self.assertEqual(1, len(encryption))
|
||||
self.assertEqual(MARKER.encode(), encryption[0][1]["data"])
|
||||
self.assertNotIn(MARKER, " ".join(encryption[0][0]))
|
||||
self.key_prompt.assert_called_once_with("Connection key (hidden): ")
|
||||
_, directory, _ = autostart._paths(self.state)
|
||||
self.assertTrue((directory / "credential.cred").exists())
|
||||
|
||||
def test_central_existing_linger_does_not_bypass_failed_owner_authentication(self):
|
||||
settings = config.default_config(role="central")
|
||||
settings.update(tunnel_id=ID, headless="auto")
|
||||
config.save_config(self.state, settings)
|
||||
before = (self.state / "config.json").read_bytes()
|
||||
|
||||
def denied_authentication(args, **kwargs):
|
||||
if args == ["sudo", "-v"]:
|
||||
raise RuntimeError("Synthetic owner authentication required")
|
||||
return self.fake_command(args, **kwargs)
|
||||
|
||||
self.command.side_effect = denied_authentication
|
||||
with mock.patch.object(autostart, "_linger", return_value=True):
|
||||
with self.assertRaisesRegex(RuntimeError, "owner authentication required"):
|
||||
autostart.enable(self.state)
|
||||
_, directory, unit = autostart._paths(self.state)
|
||||
self.assertEqual(before, (self.state / "config.json").read_bytes())
|
||||
self.assertFalse(unit.exists())
|
||||
self.assertFalse((directory / "installation.json").exists())
|
||||
self.assertFalse((directory / "credential.cred").exists())
|
||||
self.key_prompt.assert_not_called()
|
||||
|
||||
def test_private_file_reader_rejects_symlink_hardlink_fifo_and_public_file(self):
|
||||
original = self.root / "original"
|
||||
original.write_bytes(b"safe")
|
||||
@@ -466,6 +621,89 @@ class AutostartTests(unittest.TestCase):
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
def _service_directory_owners(self, owners):
|
||||
actual_lstat = Path.lstat
|
||||
|
||||
def owner_lstat(path, *args, **kwargs):
|
||||
values = list(actual_lstat(path, *args, **kwargs))
|
||||
values[4] = owners.get(path, values[4])
|
||||
return os.stat_result(values)
|
||||
|
||||
return mock.patch.object(Path, "lstat", owner_lstat)
|
||||
|
||||
def test_root_owned_service_ancestors_allow_owned_final_directory_without_changes(self):
|
||||
final = self.home / ".config/systemd/user"
|
||||
final.mkdir(parents=True, mode=0o700)
|
||||
directories = [self.home / ".config", final.parent, final]
|
||||
before = {p: (p.stat().st_uid, p.stat().st_mode) for p in directories}
|
||||
owners = {directories[0]: 0, directories[1]: 0, final: 1000}
|
||||
with mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners(owners):
|
||||
autostart._check_unit_directory(final / "test.service")
|
||||
self.assertEqual(before, {p: (p.stat().st_uid, p.stat().st_mode) for p in directories})
|
||||
self.assertEqual([], list(final.iterdir()))
|
||||
|
||||
def test_root_or_other_account_cannot_own_final_service_directory(self):
|
||||
final = self.home / ".config/systemd/user"
|
||||
final.mkdir(parents=True, mode=0o700)
|
||||
for owner in (0, 2000):
|
||||
with self.subTest(owner=owner), mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners({self.home / ".config": 0, final.parent: 0, final: owner}):
|
||||
with self.assertRaisesRegex(RuntimeError, "final directory must belong"):
|
||||
autostart._check_unit_directory(final / "test.service")
|
||||
self.assertEqual([], list(final.iterdir()))
|
||||
|
||||
def test_other_account_cannot_own_service_ancestors(self):
|
||||
final = self.home / ".config/systemd/user"
|
||||
final.mkdir(parents=True, mode=0o700)
|
||||
for ancestor in (self.home / ".config", final.parent):
|
||||
owners = {self.home / ".config": 0, final.parent: 0, final: 1000, ancestor: 2000}
|
||||
with self.subTest(ancestor=ancestor.name), mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners(owners):
|
||||
with self.assertRaisesRegex(RuntimeError, "safe owner"):
|
||||
autostart._check_unit_directory(final / "test.service")
|
||||
|
||||
def test_root_owned_service_ancestors_still_reject_writable_or_symbolic_directories(self):
|
||||
final = self.home / ".config/systemd/user"
|
||||
final.mkdir(parents=True, mode=0o700)
|
||||
directories = [self.home / ".config", final.parent, final]
|
||||
owners = {directories[0]: 0, directories[1]: 0, final: 1000}
|
||||
for directory in directories:
|
||||
original = directory.stat().st_mode
|
||||
for mode in (0o775, 0o777):
|
||||
with self.subTest(directory=directory.name, mode=mode):
|
||||
directory.chmod(mode)
|
||||
with mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners(owners):
|
||||
with self.assertRaisesRegex(RuntimeError, "not be writable"):
|
||||
autostart._check_unit_directory(final / "test.service")
|
||||
self.assertEqual(mode, stat.S_IMODE(directory.stat().st_mode))
|
||||
directory.chmod(stat.S_IMODE(original))
|
||||
moved = directory.with_name(directory.name + "-target")
|
||||
directory.rename(moved)
|
||||
directory.symlink_to(moved, target_is_directory=True)
|
||||
try:
|
||||
with self.subTest(directory=directory.name, symbolic=True):
|
||||
with self.assertRaisesRegex(RuntimeError, "symbolic links"):
|
||||
autostart._check_unit_directory(final / "test.service")
|
||||
finally:
|
||||
directory.unlink()
|
||||
moved.rename(directory)
|
||||
|
||||
def test_root_owned_parent_creation_failure_preserves_permissions_and_explains_failure(self):
|
||||
parent = self.home / ".config/systemd"
|
||||
parent.mkdir(parents=True, mode=0o755)
|
||||
final = parent / "user"
|
||||
before = {p: (p.stat().st_uid, p.stat().st_mode) for p in (parent.parent, parent)}
|
||||
actual_mkdir = Path.mkdir
|
||||
|
||||
def denied_final(path, *args, **kwargs):
|
||||
if path == final:
|
||||
raise PermissionError("Synthetic administrator-owned parent")
|
||||
return actual_mkdir(path, *args, **kwargs)
|
||||
|
||||
with mock.patch.object(autostart.os, "getuid", return_value=1000), self._service_directory_owners({parent.parent: 0, parent: 0}), mock.patch.object(Path, "mkdir", denied_final):
|
||||
with self.assertRaisesRegex(RuntimeError, "cannot create.*left unchanged"):
|
||||
autostart._check_unit_directory(final / "test.service")
|
||||
self.assertFalse(final.exists())
|
||||
self.assertEqual(before, {p: (p.stat().st_uid, p.stat().st_mode) for p in before})
|
||||
|
||||
def test_unit_quotes_specifiers_and_dollars_without_shell_or_missing_package_loop(self):
|
||||
text = autostart.unit_text(Path('/tmp/a % b ${HOME} "quoted"'), Path('/tmp/key % file'))
|
||||
self.assertIn("%%", text)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""The public vendor name root_model.py is not the build user's home directory."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
PACKAGING = Path(__file__).resolve().parents[1] / "packaging"
|
||||
sys.path.insert(0, str(PACKAGING))
|
||||
spec = importlib.util.spec_from_file_location("privacy_test_builder", PACKAGING / "build_expanded.py")
|
||||
builder = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(builder)
|
||||
|
||||
|
||||
class BuildPrivacyPathTests(unittest.TestCase):
|
||||
def test_public_module_name_does_not_block_a_root_user_build(self):
|
||||
data = b'{"path":"source/vendor/pydantic/root_model.py"}'
|
||||
self.assertFalse(builder.contains_private_path(data, "/root"))
|
||||
|
||||
def test_actual_home_paths_are_still_rejected(self):
|
||||
for data in (b'"/root/private-file"', b'"/root"', b'input: /root\n',
|
||||
b'file:///root/private-file', b'"/root/"'):
|
||||
with self.subTest(data=data):
|
||||
self.assertTrue(builder.contains_private_path(data, "/root"))
|
||||
|
||||
def test_neighboring_names_are_not_this_home_directory(self):
|
||||
for data in (b'"/home/tester-other/file"', b'"/home/tester_backup/file"'):
|
||||
self.assertFalse(builder.contains_private_path(data, "/home/tester"))
|
||||
self.assertTrue(builder.contains_private_path(b'"/home/tester/file"', "/home/tester"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Exercise the public health command without starting a profile or transport."""
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from framebridge_expanded import cli, health, __version__
|
||||
|
||||
|
||||
class HealthCliTests(unittest.TestCase):
|
||||
def invoke(self, arguments):
|
||||
output, errors = io.StringIO(), io.StringIO()
|
||||
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors):
|
||||
code = cli.main(arguments)
|
||||
return code, output.getvalue(), errors.getvalue()
|
||||
|
||||
def test_absent_profile_is_reported_without_creating_it(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
profile = Path(temporary) / "absent-profile"
|
||||
code, text, _ = self.invoke(["--state-dir", str(profile), "--json", "health"])
|
||||
self.assertFalse(profile.exists())
|
||||
value = json.loads(text)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertEqual(value["version"], __version__)
|
||||
self.assertEqual(value["command"], "health")
|
||||
self.assertTrue(value["ok"])
|
||||
self.assertEqual(value["result"]["health"]["state"], "unconfigured")
|
||||
self.assertFalse(value["result"]["health"]["healthy"])
|
||||
self.assertTrue(value["result"]["read_only"])
|
||||
|
||||
def test_query_success_does_not_make_exit_status_healthy(self):
|
||||
for state, healthy, expected in (("healthy", True, 0), ("paused", True, 0),
|
||||
("degraded", False, 1), ("unavailable", None, 1)):
|
||||
with self.subTest(state=state):
|
||||
report = {"ok": True, "health": {"state": state, "healthy": healthy}}
|
||||
module = SimpleNamespace(collect=mock.Mock(return_value=report))
|
||||
with mock.patch.object(cli, "_module", return_value=module):
|
||||
code, text, _ = self.invoke(["health", "--json"])
|
||||
self.assertEqual(code, expected)
|
||||
self.assertEqual(json.loads(text)["result"], report)
|
||||
|
||||
def test_failed_observation_cannot_pass_even_with_a_healthy_flag(self):
|
||||
module = SimpleNamespace(collect=mock.Mock(return_value={"ok": False, "health": {"healthy": True}}))
|
||||
with mock.patch.object(cli, "_module", return_value=module):
|
||||
code, text, _ = self.invoke(["--json", "health"])
|
||||
self.assertEqual(code, 1)
|
||||
self.assertFalse(json.loads(text)["ok"])
|
||||
|
||||
def test_human_summary_names_the_local_machine_and_its_limits(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
code, text, _ = self.invoke(["health", "--state-dir", str(Path(temporary) / "absent")])
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("Target: LOCAL = this computer", text)
|
||||
self.assertIn("unconfigured", text)
|
||||
self.assertIn("not a fleet scan", text)
|
||||
|
||||
def test_json_output_never_prompts_for_authentication(self):
|
||||
with tempfile.TemporaryDirectory() as temporary, mock.patch("builtins.input") as ask:
|
||||
code, text, _ = self.invoke(["health", "--json", "--state-dir", str(Path(temporary) / "absent")])
|
||||
ask.assert_not_called()
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIsNone(json.loads(text)["result"]["software"]["runtime_version"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -141,6 +141,11 @@ class BackgroundTests(unittest.TestCase):
|
||||
record=json.loads(path.read_text());record['verified_at']=time.time()-30
|
||||
record['message']='\x1b[31m'+FAKE
|
||||
background._write(path,record)
|
||||
if record.get('heartbeat_backend') == background._HEARTBEAT_BACKEND:
|
||||
heartbeat_path=background._heartbeat_directory(self.state)/'status.json'
|
||||
heartbeat=background._read(heartbeat_path)
|
||||
heartbeat['verified_at']=time.time()-30
|
||||
background._write(heartbeat_path,heartbeat)
|
||||
current=background.status(self.state)
|
||||
self.assertFalse(current['verified'])
|
||||
self.assertEqual(current['state'],'retrying')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Routing contract tests; original read provider is exercised locally."""
|
||||
import base64
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import socket
|
||||
@@ -10,6 +11,8 @@ from unittest import mock
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "expanded"))
|
||||
from framebridge_expanded import fleet
|
||||
from framebridge_expanded.real_screen.screen_frontdoor import RPC_COMMAND
|
||||
from framebridge_expanded.real_screen.screen_rpc import encode
|
||||
|
||||
LOCAL_ID = "c6d827a4-219f-48fc-82d8-f410176313ca"
|
||||
REMOTE_ID = "6d88c5ae-343e-4413-a202-ccae2ae7694a"
|
||||
@@ -134,6 +137,99 @@ class FleetRoutingTests(unittest.TestCase):
|
||||
self.assertEqual(fleet.execute_node("test-node", "printf ok", True)["error"], "NODE_OFFLINE")
|
||||
self.assertEqual(fleet.node_read("test-node", "system.status", {})["error"], "NODE_OFFLINE")
|
||||
|
||||
@staticmethod
|
||||
def screen_script():
|
||||
return RPC_COMMAND + base64.b64encode(encode({"op": "observe"})).decode("ascii") + "\n"
|
||||
|
||||
def assert_identity(self, value, requested, local=False):
|
||||
self.assertEqual(value["requested_target"], requested)
|
||||
self.assertEqual(value["resolved_target"], "test-central" if local else "test-node")
|
||||
self.assertEqual(value["node"], value["resolved_target"])
|
||||
self.assertEqual(value["target"], "LOCAL" if local else "test-node")
|
||||
self.assertEqual(value["node_id"], LOCAL_ID if local else REMOTE_ID)
|
||||
self.assertEqual(value["hostname"], socket.gethostname() if local else "test-node")
|
||||
self.assertEqual(value["transport"], "local" if local else "broker_outbound")
|
||||
|
||||
@staticmethod
|
||||
def misleading_response(payload=None):
|
||||
value = {"ok": True, "target": "wrong-machine", "node": "wrong-machine",
|
||||
"requested_target": "wrong-machine", "resolved_target": "wrong-machine",
|
||||
"node_id": "wrong-id", "hostname": "wrong-host", "transport": "wrong-route"}
|
||||
if payload is not None:
|
||||
value.update(job_id=payload["job_id"], command_sha256=payload.get("sha256"))
|
||||
return value
|
||||
|
||||
def test_read_identity_comes_from_confirmed_route(self):
|
||||
for requested, local in [("LOCAL", True), ("test-central", True), (LOCAL_ID, True),
|
||||
("test-node", False), (REMOTE_ID, False)]:
|
||||
with self.subTest(requested=requested), \
|
||||
mock.patch.object(fleet, "read_local", return_value=self.misleading_response()), \
|
||||
mock.patch("framebridge_expanded.broker.request", return_value=self.misleading_response()):
|
||||
value = fleet.node_read(requested, "system.status", {})
|
||||
self.assertTrue(value["ok"])
|
||||
self.assert_identity(value, requested, local)
|
||||
self.assertEqual(value["node"], value["resolved_target"])
|
||||
|
||||
def test_execution_routes_overwrite_upstream_identity(self):
|
||||
def remote_response(node_id, capability, payload, timeout_s):
|
||||
return self.misleading_response(payload)
|
||||
calls = [
|
||||
lambda alias: fleet.execute_node(alias, "printf ok", True, job_id="identity-exec"),
|
||||
lambda alias: fleet.execute_screen_node(alias, self.screen_script(), True,
|
||||
job_id="identity-screen", include_stdout=True),
|
||||
lambda alias: fleet.cua_node(alias, {"action": "OBSERVE"}, True, job_id="identity-cua"),
|
||||
]
|
||||
for route in calls:
|
||||
for requested, local in [("LOCAL", True), ("test-central", True), (LOCAL_ID, True),
|
||||
("test-node", False), (REMOTE_ID, False)]:
|
||||
with self.subTest(route=route, requested=requested), \
|
||||
mock.patch("framebridge_expanded.runtime.adapter_request",
|
||||
side_effect=self.misleading_response), \
|
||||
mock.patch("framebridge_expanded.broker.request", side_effect=remote_response):
|
||||
value = route(requested)
|
||||
self.assertTrue(value["ok"], value)
|
||||
self.assert_identity(value, requested, local)
|
||||
|
||||
def test_unknown_target_never_falls_back_to_local(self):
|
||||
with mock.patch("framebridge_expanded.runtime.adapter_request") as local, \
|
||||
mock.patch("framebridge_expanded.broker.request") as remote:
|
||||
values = [
|
||||
fleet.node_read("unknown-node", "system.status", {}),
|
||||
fleet.execute_node("unknown-node", "printf ok", True),
|
||||
fleet.execute_screen_node("unknown-node", self.screen_script(), True, include_stdout=True),
|
||||
fleet.cua_node("unknown-node", {"action": "OBSERVE"}, True),
|
||||
]
|
||||
local.assert_not_called()
|
||||
remote.assert_not_called()
|
||||
for value in values:
|
||||
self.assertFalse(value["ok"])
|
||||
self.assertEqual(value["error"], "NODE_NOT_FOUND")
|
||||
self.assertNotIn("resolved_target", value)
|
||||
self.assertNotIn("node_id", value)
|
||||
|
||||
def test_offline_target_errors_retain_confirmed_identity(self):
|
||||
self.remote["operational_transport_ready"] = False
|
||||
with mock.patch("framebridge_expanded.broker.request") as remote:
|
||||
values = [
|
||||
fleet.node_read(REMOTE_ID, "system.status", {}),
|
||||
fleet.execute_node(REMOTE_ID, "printf ok", True),
|
||||
fleet.execute_screen_node(REMOTE_ID, self.screen_script(), True, include_stdout=True),
|
||||
fleet.cua_node(REMOTE_ID, {"action": "OBSERVE"}, True),
|
||||
]
|
||||
remote.assert_not_called()
|
||||
for value in values:
|
||||
self.assertFalse(value["ok"])
|
||||
self.assertEqual(value["error"], "NODE_OFFLINE")
|
||||
self.assert_identity(value, REMOTE_ID)
|
||||
|
||||
def test_pre_resolution_failure_does_not_claim_identity(self):
|
||||
with mock.patch.object(fleet, "resolve_node") as lookup:
|
||||
value = fleet.execute_node("test-node", "printf ok", False)
|
||||
lookup.assert_not_called()
|
||||
self.assertEqual(value["error"], "AUTHORIZED_TRUE_REQUIRED")
|
||||
for field in ("requested_target", "resolved_target", "node_id", "hostname", "transport"):
|
||||
self.assertNotIn(field, value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,196 @@
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "expanded"))
|
||||
from framebridge_expanded import health
|
||||
|
||||
|
||||
NODE_ID = "51f0acf5-3633-41f6-aea7-4fed5cffcb01"
|
||||
|
||||
|
||||
def snapshots():
|
||||
return ({"ok": True, "configured": True, "role": "central", "name": "gateway",
|
||||
"runtime": {"running": True, "ready": True, "node_id": NODE_ID,
|
||||
"display_context": {"kind": "text"},
|
||||
"capabilities": {"fbexec": True, "fbterm": True, "cua_tty_module": True, "graphical_cua": False}},
|
||||
"connection": {"verified": True, "state": "ready"},
|
||||
"boot": {"configured": True, "enabled": False, "service_state": "active"},
|
||||
"desired_connection": "connected"},
|
||||
{"ok": True, "platform_compatibility": {"ok": True, "profile_id": "debian13-amd64-py313", "validation_status": "validated", "issues": []},
|
||||
"issues": [], "missing_commands": [], "capabilities": {"fbexec": True, "fbterm": True, "cua_tty_module": True, "graphical_cua": False}})
|
||||
|
||||
|
||||
class HealthTests(unittest.TestCase):
|
||||
def test_release_build_identity_is_retained_without_relabelling_it_validated(self):
|
||||
status, doctor = snapshots()
|
||||
doctor["platform_compatibility"]["validation_status"] = "release_build"
|
||||
value = health.build_report(status, doctor)
|
||||
self.assertTrue(value["ok"])
|
||||
self.assertEqual(value["prerequisites"]["validation_status"], "release_build")
|
||||
self.assertNotEqual(value["prerequisites"]["validation_status"], "validated")
|
||||
|
||||
def test_healthy_text_mode_does_not_require_graphical_tools_or_boot_enabled(self):
|
||||
status, doctor = snapshots()
|
||||
value = health.build_report(status, doctor, observed_at="2026-10-28T12:00:00+00:00", host="gateway")
|
||||
self.assertTrue(value["ok"])
|
||||
self.assertEqual(value["health"], {"state": "healthy", "healthy": True, "issues": []})
|
||||
self.assertEqual(value["boot"]["policy"], "disabled")
|
||||
self.assertEqual(value["effective_target"]["node_id"], NODE_ID)
|
||||
self.assertFalse(value["desktop"]["capture_verified_by_this_report"])
|
||||
self.assertIsNone(value["software"]["runtime_version"])
|
||||
|
||||
def test_query_success_is_not_readiness(self):
|
||||
status, doctor = snapshots()
|
||||
status["runtime"].update(running=False, ready=False)
|
||||
status["connection"].update(verified=False, state="stopped")
|
||||
value = health.build_report(status, doctor)
|
||||
self.assertTrue(value["ok"])
|
||||
self.assertEqual(value["health"]["state"], "degraded")
|
||||
self.assertFalse(value["health"]["healthy"])
|
||||
self.assertIn("LOCAL_TOOLS_NOT_READY", value["health"]["issues"])
|
||||
|
||||
def test_paused_connection_is_distinct_from_failure(self):
|
||||
status, doctor = snapshots()
|
||||
status["desired_connection"] = "disconnected"
|
||||
status["connection"].update(verified=False, state="stopped")
|
||||
value = health.build_report(status, doctor)
|
||||
self.assertEqual(value["health"]["state"], "paused")
|
||||
self.assertTrue(value["health"]["healthy"])
|
||||
self.assertEqual(value["health"]["issues"], [])
|
||||
|
||||
def test_node_does_not_claim_central_connection_verified(self):
|
||||
status, doctor = snapshots()
|
||||
status.update(role="node", desired_connection="not_applicable")
|
||||
value = health.build_report(status, doctor)
|
||||
self.assertEqual(value["connection"]["state"], "managed_by_central")
|
||||
self.assertIsNone(value["connection"]["verified"])
|
||||
self.assertEqual(value["health"]["state"], "healthy")
|
||||
|
||||
def test_requested_pause_does_not_hide_a_still_running_tunnel(self):
|
||||
status, doctor = snapshots()
|
||||
status["desired_connection"] = "disconnected"
|
||||
value = health.build_report(status, doctor)
|
||||
self.assertEqual(value["health"]["state"], "degraded")
|
||||
self.assertIn("CONNECTION_INTENT_MISMATCH", value["health"]["issues"])
|
||||
status["connection"].update(verified=False, state="stopped", alive=True)
|
||||
value = health.build_report(status, doctor)
|
||||
self.assertFalse(value["health"]["healthy"])
|
||||
|
||||
def test_failed_connection_query_cannot_certify_a_healthy_profile(self):
|
||||
status, doctor = snapshots()
|
||||
status["connection"]["ok"] = False
|
||||
value = health.build_report(status, doctor)
|
||||
self.assertFalse(value["ok"])
|
||||
self.assertIsNone(value["health"]["healthy"])
|
||||
self.assertIn("CONNECTION_STATUS_UNAVAILABLE", value["health"]["issues"])
|
||||
|
||||
def test_missing_boot_policy_is_unknown_rather_than_healthy(self):
|
||||
status, doctor = snapshots()
|
||||
status["boot"] = {}
|
||||
value = health.build_report(status, doctor)
|
||||
self.assertFalse(value["ok"])
|
||||
self.assertEqual(value["health"]["state"], "unavailable")
|
||||
|
||||
def test_missing_runtime_status_is_not_a_stopped_runtime(self):
|
||||
status, doctor = snapshots()
|
||||
status["runtime"] = {"status_unavailable": True, "running": False, "ready": False}
|
||||
value = health.build_report(status, doctor)
|
||||
self.assertFalse(value["ok"])
|
||||
self.assertEqual(value["runtime"]["state"], "unavailable")
|
||||
self.assertEqual(value["health"]["state"], "unavailable")
|
||||
self.assertIsNone(value["health"]["healthy"])
|
||||
|
||||
def test_failed_runtime_query_cannot_certify_ready(self):
|
||||
status, doctor = snapshots()
|
||||
status["runtime"]["ok"] = False
|
||||
value = health.build_report(status, doctor)
|
||||
self.assertFalse(value["ok"])
|
||||
self.assertEqual(value["health"]["state"], "unavailable")
|
||||
|
||||
def test_failed_profile_query_cannot_claim_unconfigured(self):
|
||||
value = health.build_report({"ok": False, "configured": False}, None)
|
||||
self.assertFalse(value["ok"])
|
||||
self.assertEqual(value["health"]["state"], "unavailable")
|
||||
|
||||
def test_missing_diagnostic_result_is_unavailable_not_passed(self):
|
||||
status, _ = snapshots()
|
||||
value = health.build_report(status, {})
|
||||
self.assertFalse(value["ok"])
|
||||
self.assertEqual(value["health"]["state"], "unavailable")
|
||||
|
||||
def test_unconfigured_profile_is_observed_without_running_doctor_or_creating_files(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
state = Path(directory) / "not-created"
|
||||
with mock.patch.object(health.runtime, "doctor", side_effect=AssertionError("doctor must not run")):
|
||||
value = health.collect(state)
|
||||
self.assertFalse(state.exists())
|
||||
self.assertTrue(value["ok"])
|
||||
self.assertEqual(value["health"]["state"], "unconfigured")
|
||||
self.assertEqual(value["health"]["issues"], [])
|
||||
self.assertEqual(value["prerequisites"]["state"], "not_run")
|
||||
|
||||
def test_diagnostics_failure_retains_status_without_leaking_exception(self):
|
||||
status, _ = snapshots()
|
||||
with mock.patch.object(health.lifecycle, "status", return_value=status), \
|
||||
mock.patch.object(health.runtime, "doctor", side_effect=RuntimeError("secret-access-key")):
|
||||
value = health.collect(Path("/unused"))
|
||||
self.assertFalse(value["ok"])
|
||||
self.assertEqual(value["runtime"]["state"], "ready")
|
||||
self.assertEqual(value["health"]["state"], "unavailable")
|
||||
self.assertNotIn("secret-access-key", json.dumps(value))
|
||||
|
||||
def test_partial_invalid_profile_does_not_guess_it_is_unconfigured(self):
|
||||
with mock.patch.object(health.lifecycle, "status", side_effect=ValueError("private-input")), \
|
||||
mock.patch.object(health.runtime, "doctor") as doctor:
|
||||
value = health.collect(Path("/unused"))
|
||||
doctor.assert_not_called()
|
||||
self.assertFalse(value["ok"])
|
||||
self.assertEqual(value["health"]["state"], "unavailable")
|
||||
self.assertNotIn("private-input", json.dumps(value))
|
||||
|
||||
def test_allowlist_excludes_connection_material_paths_and_untrusted_free_text(self):
|
||||
status, doctor = snapshots()
|
||||
status["tunnel_id"] = "secret-tunnel"
|
||||
status["runtime"].update(state_dir="/private/secret-path", error="secret-error", access_key="secret-key")
|
||||
status["runtime"]["display_context"]["authority"] = "/private/secret-authority"
|
||||
status["connection"].update(message="secret-message", log="/private/secret-log", token="secret-token")
|
||||
doctor.update(network={"token": "secret-network"}, issues=["secret-issue"])
|
||||
doctor["platform_compatibility"]["issues"] = ["secret-compatibility"]
|
||||
result = health.build_report(status, doctor, host="host")
|
||||
self.assertNotIn("secret-", json.dumps(result))
|
||||
self.assertEqual(result["prerequisites"]["issue_count"], 1)
|
||||
self.assertEqual(result["prerequisites"]["compatibility_issue_count"], 1)
|
||||
|
||||
def test_prerequisite_failure_is_degraded_without_claiming_no_runtime(self):
|
||||
status, doctor = snapshots()
|
||||
doctor.update(ok=False, missing_commands=["tmux"])
|
||||
value = health.build_report(status, doctor)
|
||||
self.assertTrue(value["ok"])
|
||||
self.assertEqual(value["health"]["state"], "degraded")
|
||||
self.assertEqual(value["runtime"]["state"], "ready")
|
||||
self.assertEqual(value["prerequisites"]["missing_commands"], ["tmux"])
|
||||
|
||||
def test_reports_do_not_modify_snapshots(self):
|
||||
status, doctor = snapshots()
|
||||
before = copy.deepcopy((status, doctor))
|
||||
health.build_report(status, doctor)
|
||||
self.assertEqual((status, doctor), before)
|
||||
|
||||
def test_terminal_output_is_explicit_about_target_and_observation_limit(self):
|
||||
status, doctor = snapshots()
|
||||
value = health.build_report(status, doctor, host="gateway")
|
||||
text = health.render(value)
|
||||
self.assertIn("Target: LOCAL = this computer (gateway)", text)
|
||||
self.assertIn(NODE_ID, text)
|
||||
self.assertIn("capture/input not tested", text)
|
||||
self.assertIn("not proof of the running version", text)
|
||||
self.assertIn("not a fleet scan", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Liveness stays fresh without periodically syncing the durable profile."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from framebridge_expanded import autostart, connection_background as background
|
||||
|
||||
|
||||
class IdleStateIOTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.root = Path(temporary.name)
|
||||
self.state = self.root / "profile"
|
||||
self.directory = background._directory(self.state)
|
||||
self.runtime_parent = self.root / "runtime"
|
||||
self.runtime_parent.mkdir(mode=0o700)
|
||||
(self.runtime_parent / str(os.getuid())).mkdir(mode=0o700)
|
||||
self.addCleanup(mock.patch.stopall)
|
||||
mock.patch.object(background, "_RUNTIME_PARENT", self.runtime_parent).start()
|
||||
self.identity = dict(pid=12345, uid=os.getuid(), start_ticks=99,
|
||||
boot_id="boot-one", session_id=12345,
|
||||
process_group=12345, state_dir=str(self.state), lock_fd=8)
|
||||
|
||||
def sink(self):
|
||||
runtime = background._heartbeat_directory(self.state, create=True)
|
||||
return background._StatusOutput(self.directory / "status.json", self.identity,
|
||||
runtime / "status.json")
|
||||
|
||||
def status(self):
|
||||
with mock.patch.object(background, "_valid_identity", return_value=True):
|
||||
return background.status(self.state)
|
||||
|
||||
def test_ready_heartbeat_fresh_beyond_timeout_without_durable_writes(self):
|
||||
clock = [100.0]
|
||||
with mock.patch.object(background.time, "time", side_effect=lambda: clock[0]), mock.patch.object(background.time, "monotonic", side_effect=lambda: clock[0]):
|
||||
sink = self.sink()
|
||||
sink.observe("ready")
|
||||
sink.change("ready", "[OK] Connected.")
|
||||
durable = sink.path.stat()
|
||||
original = sink.path.read_bytes()
|
||||
for instant in range(101, 141):
|
||||
clock[0] = float(instant)
|
||||
sink.observe("ready")
|
||||
self.assertTrue(self.status()["verified"])
|
||||
self.assertEqual(sink.path.read_bytes(), original)
|
||||
self.assertEqual(sink.path.stat().st_ino, durable.st_ino)
|
||||
self.assertEqual(sink.path.stat().st_mtime_ns, durable.st_mtime_ns)
|
||||
clock[0] = 156.0
|
||||
self.assertFalse(self.status()["verified"])
|
||||
self.assertEqual(self.status()["state"], "retrying")
|
||||
sink.observe("ready")
|
||||
self.assertTrue(self.status()["verified"])
|
||||
|
||||
def test_failure_and_recovery_publish_transitions_immediately(self):
|
||||
sink = self.sink()
|
||||
sink.observe("ready")
|
||||
sink.change("ready", "[OK] Connected.")
|
||||
self.assertTrue(self.status()["verified"])
|
||||
sink.observe("access")
|
||||
self.assertFalse(self.status()["verified"])
|
||||
sink.change("retrying", "[RETRYING] Lost readiness.")
|
||||
self.assertEqual(json.loads(sink.path.read_text())["state"], "retrying")
|
||||
sink.observe("ready")
|
||||
sink.change("ready", "[OK] Connected.")
|
||||
self.assertTrue(self.status()["verified"])
|
||||
|
||||
def test_missing_or_old_worker_heartbeat_never_uses_durable_timestamp(self):
|
||||
sink = self.sink()
|
||||
sink.observe("ready")
|
||||
sink.change("ready", "[OK] Connected.")
|
||||
heartbeat = background._read(sink.heartbeat_path)
|
||||
for field, value in (("pid", 99999), ("boot_id", "older-boot"), ("start_ticks", 44), ("state_dir", "/another-profile")):
|
||||
wrong = dict(heartbeat, **{field: value})
|
||||
background._write(sink.heartbeat_path, wrong)
|
||||
self.assertFalse(self.status()["verified"], field)
|
||||
sink.heartbeat_path.unlink()
|
||||
self.assertFalse(self.status()["verified"])
|
||||
|
||||
def test_no_runtime_falls_back_to_bounded_fresh_durable_heartbeat(self):
|
||||
(self.runtime_parent / str(os.getuid())).rmdir()
|
||||
self.assertIsNone(background._heartbeat_directory(self.state, create=True))
|
||||
clock = [100.0]
|
||||
with mock.patch.object(background.time, "time", side_effect=lambda: clock[0]), mock.patch.object(background.time, "monotonic", side_effect=lambda: clock[0]), mock.patch.object(background, "_write", wraps=background._write) as write:
|
||||
sink = background._StatusOutput(self.directory / "status.json", self.identity)
|
||||
sink.observe("ready")
|
||||
sink.change("ready", "[OK] Connected.")
|
||||
write.reset_mock()
|
||||
for instant in range(101, 121):
|
||||
clock[0] = float(instant)
|
||||
sink.observe("ready")
|
||||
self.assertTrue(self.status()["verified"])
|
||||
self.assertEqual(write.call_count, 4)
|
||||
sink.observe("access")
|
||||
self.assertFalse(self.status()["verified"])
|
||||
|
||||
def test_legacy_durable_heartbeat_remains_readable_with_runtime_available(self):
|
||||
background._write(self.directory / "status.json", dict(self.identity, state="ready", verified_at=100.0))
|
||||
with mock.patch.object(background.time, "time", return_value=110.0):
|
||||
self.assertTrue(self.status()["verified"])
|
||||
with mock.patch.object(background.time, "time", return_value=116.0):
|
||||
self.assertFalse(self.status()["verified"])
|
||||
|
||||
def test_runtime_permissions_and_symlinks_rejected_without_changes(self):
|
||||
root = self.runtime_parent / str(os.getuid())
|
||||
root.chmod(0o755)
|
||||
with self.assertRaises(RuntimeError):
|
||||
background._heartbeat_directory(self.state, create=True)
|
||||
self.assertEqual(root.stat().st_mode & 0o777, 0o755)
|
||||
root.chmod(0o700)
|
||||
directory = background._heartbeat_directory(self.state, create=True)
|
||||
directory.rmdir()
|
||||
directory.symlink_to(self.directory, target_is_directory=True)
|
||||
with self.assertRaises(ValueError):
|
||||
background._heartbeat_directory(self.state, create=True)
|
||||
with self.assertRaises(RuntimeError):
|
||||
background._heartbeat_directory(self.state)
|
||||
|
||||
def test_runtime_parent_symlink_rejected(self):
|
||||
alias = self.root / "runtime-alias"
|
||||
alias.symlink_to(self.runtime_parent, target_is_directory=True)
|
||||
with mock.patch.object(background, "_RUNTIME_PARENT", alias), self.assertRaises(RuntimeError):
|
||||
background._heartbeat_directory(self.state, create=True)
|
||||
|
||||
def test_heartbeat_file_symlink_and_hardlink_rejected(self):
|
||||
sink = self.sink()
|
||||
sink.heartbeat_path.symlink_to(sink.path)
|
||||
with self.assertRaises(OSError):
|
||||
sink.observe("ready")
|
||||
sink.heartbeat_path.unlink()
|
||||
os.link(sink.path, sink.heartbeat_path)
|
||||
with self.assertRaises(RuntimeError):
|
||||
sink.observe("ready")
|
||||
|
||||
def test_worker_state_only_writes_on_transition(self):
|
||||
with mock.patch.object(autostart, "_json", wraps=autostart._json) as write:
|
||||
autostart._worker_status(self.directory, "connected")
|
||||
first = (self.directory / "worker-status.json").read_bytes()
|
||||
for _ in range(20):
|
||||
autostart._worker_status(self.directory, "connected")
|
||||
self.assertEqual(write.call_count, 1)
|
||||
self.assertEqual((self.directory / "worker-status.json").read_bytes(), first)
|
||||
autostart._worker_status(self.directory, "reconnecting")
|
||||
self.assertEqual(write.call_count, 2)
|
||||
self.assertEqual(json.loads((self.directory / "worker-status.json").read_text())["state"], "reconnecting")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,10 +1,13 @@
|
||||
"""Candidate profile/ABI selection; tests use inert native-file fixtures."""
|
||||
import hashlib
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import stat
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
@@ -64,6 +67,43 @@ class LinuxCompatibilityTests(unittest.TestCase):
|
||||
self.assertFalse(self.check(distro, version, (3, 12))["ok"])
|
||||
self.assertIsNone(compat.profile_for({"ID": "other", "ID_LIKE": "ubuntu", "VERSION_ID": "24.04"}))
|
||||
|
||||
def test_public_builder_profiles_are_accepted_by_the_runtime_without_claiming_validation(self):
|
||||
# Use the real public builder's profile output, rather than duplicating
|
||||
# its validation_status value in a fixture. Archive/vendor verification
|
||||
# has separate tests; --check-vendor does not stage/install a package.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging"))
|
||||
import archive_sources
|
||||
import build_expanded
|
||||
targets = [("debian13-amd64-py313", "debian", "13", (3, 13)),
|
||||
("ubuntu24.04-amd64-py312", "ubuntu", "24.04", (3, 12)),
|
||||
("mint22-amd64-py312", "linuxmint", "22.3", (3, 12))]
|
||||
for profile_id, distro, version, python in targets:
|
||||
with self.subTest(profile=profile_id):
|
||||
self.artifact(profile_id)
|
||||
output = io.StringIO()
|
||||
globals_before = {name: getattr(build_expanded, name) for name in
|
||||
("VERSION", "PUBLIC_RELEASE", "SOURCE_ARCHIVE", "PUBLIC_FILENAME", "RELEASE", "STAGE", "PREFIX")}
|
||||
args = ["--target", profile_id, "--public-release", "--source-manifest", str(self.prefix / "source.json"), "--check-vendor"]
|
||||
if profile_id != "debian13-amd64-py313":
|
||||
args += ["--vendor-dir", str(self.prefix / "reviewed-cp312")]
|
||||
with patch.multiple(build_expanded, **globals_before), \
|
||||
patch.object(build_expanded.subprocess, "run"), \
|
||||
patch.object(build_expanded, "vendor_versions", return_value={}), \
|
||||
patch.object(build_expanded, "validate_vendor", return_value={"vendor_sha256": "a" * 64}), \
|
||||
patch.object(build_expanded, "release_source", return_value={"filename": "FrameBridge-0.3.4-source.tar.gz", "sha256": "b" * 64}), \
|
||||
patch.object(build_expanded, "legal_preflight"), \
|
||||
patch.object(archive_sources, "source_files", return_value={}), \
|
||||
contextlib.redirect_stdout(output):
|
||||
build_expanded.main(args)
|
||||
manifest = json.loads(output.getvalue())
|
||||
self.assertEqual(manifest["validation_status"], "release_build")
|
||||
(self.prefix / "release-profile.json").write_text(json.dumps(manifest))
|
||||
result = self.check(distro, version, python)
|
||||
self.assertTrue(result["ok"], result)
|
||||
self.assertEqual(result["validation_status"], manifest["validation_status"])
|
||||
self.assertIn("Release assembly does not establish a tested installation", result["warnings"][0])
|
||||
self.assertNotEqual(result["validation_status"], "validated")
|
||||
|
||||
def test_interpreter_artifact_and_cpu_must_all_match(self):
|
||||
self.artifact("ubuntu24.04-amd64-py312")
|
||||
self.assertFalse(self.check("ubuntu", "24.04", (3, 13))["ok"])
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Policy-name inspection only: no sudo, registry reads or rule contents."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from framebridge_expanded import autostart, credential_helper
|
||||
|
||||
|
||||
class NodeHelperCleanupInspectionTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.state = self.root / "profile"
|
||||
self.policies = self.root / "policies"
|
||||
self.policies.mkdir()
|
||||
self.profile = autostart._profile_id(self.state)
|
||||
self.uid = str(os.getuid())
|
||||
self.addCleanup(mock.patch.stopall)
|
||||
mock.patch.object(autostart, "_helper_ready").start()
|
||||
mock.patch.object(credential_helper, "SUDOERS", self.policies).start()
|
||||
self.trust = mock.patch.object(credential_helper, "_root_path", side_effect=lambda path, **kw: path)
|
||||
self.trust.start()
|
||||
|
||||
def test_own_canonical_decrypt_and_authorization_names_require_root_cleanup(self):
|
||||
names = [
|
||||
"framebridge-credential-" + self.uid + "-" + self.profile + "-" + "a" * 64,
|
||||
"framebridge-credential-authorize-" + self.uid + "-" + self.profile + "-host",
|
||||
"framebridge-credential-authorize-" + self.uid + "-" + self.profile + "-user",
|
||||
]
|
||||
for name in names:
|
||||
with self.subTest(name=name):
|
||||
path = self.policies / name
|
||||
path.touch()
|
||||
with mock.patch.object(Path, "read_bytes", side_effect=AssertionError("Do not read rule contents")):
|
||||
self.assertTrue(autostart._node_helper_cleanup_required(self.state))
|
||||
path.unlink()
|
||||
|
||||
def test_ambiguous_names_require_review_even_alongside_a_canonical_grant(self):
|
||||
canonical = "framebridge-credential-" + self.uid + "-" + self.profile + "-" + "a" * 64
|
||||
for name in ["framebridge-credential-" + self.uid + "-" + self.profile + "-invalid",
|
||||
"framebridge-credential-authorize-" + self.uid + "-" + self.profile + "-unknown"]:
|
||||
with self.subTest(name=name), mock.patch.object(Path, "iterdir", return_value=iter([self.policies / canonical, self.policies / name])):
|
||||
with self.assertRaisesRegex(RuntimeError, "ambiguous.*administrator review"):
|
||||
autostart._node_helper_cleanup_required(self.state)
|
||||
|
||||
def test_symlink_in_own_namespace_still_requires_root_cleanup(self):
|
||||
path = self.policies / ("framebridge-credential-" + self.uid + "-" + self.profile + "-" + "a" * 64)
|
||||
path.symlink_to(self.root / "nonexistent")
|
||||
self.assertTrue(autostart._node_helper_cleanup_required(self.state))
|
||||
|
||||
def test_permission_denied_listing_is_not_treated_as_empty(self):
|
||||
with mock.patch.object(Path, "iterdir", side_effect=PermissionError("synthetic denied")):
|
||||
self.assertTrue(autostart._node_helper_cleanup_required(self.state))
|
||||
|
||||
def test_empty_verified_policy_directory_omits_privileged_cleanup(self):
|
||||
self.assertFalse(autostart._node_helper_cleanup_required(self.state))
|
||||
|
||||
def test_insecure_parent_or_policy_directory_is_not_trusted(self):
|
||||
self.trust.stop()
|
||||
with mock.patch.object(credential_helper, "SUDOERS", Path("/etc/sudoers.d")):
|
||||
for unsafe, kind, uid, mode in [
|
||||
(Path("/etc"), stat.S_IFDIR, 0, 0o775),
|
||||
(Path("/etc/sudoers.d"), stat.S_IFLNK, 0, 0o777),
|
||||
(Path("/etc/sudoers.d"), stat.S_IFDIR, 12345, 0o755),
|
||||
]:
|
||||
def info(path, *, selected=unsafe, selected_kind=kind, selected_uid=uid, selected_mode=mode):
|
||||
return os.stat_result((selected_kind | selected_mode if path == selected else stat.S_IFDIR | 0o755,
|
||||
1, 1, 1, selected_uid if path == selected else 0, 0, 0, 0, 0, 0))
|
||||
with self.subTest(path=unsafe, kind=kind, uid=uid, mode=mode), mock.patch.object(Path, "lstat", info), mock.patch.object(Path, "iterdir") as listing:
|
||||
self.assertTrue(autostart._node_helper_cleanup_required(self.state))
|
||||
listing.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -9,7 +9,7 @@ import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging"))
|
||||
from build_expanded import maintainer_prerm, PRIVILEGED_HELPER_FLOOR
|
||||
from build_expanded import maintainer_preinst, maintainer_prerm, PRIVILEGED_HELPER_FLOOR
|
||||
from build_profiles import CANDIDATE_VERSION
|
||||
|
||||
|
||||
@@ -91,6 +91,38 @@ class MaintainerScriptTests(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
maintainer_prerm("0.3.2'; /tmp/untrusted-script; '")
|
||||
|
||||
def run_preinst(self, arguments, *, version="0.3.4~dev.20261029.4", fail=0):
|
||||
script = maintainer_preinst(version)
|
||||
script = script.replace("/usr/bin/python3 -I -S - <<'FRAMEBRIDGE_UPGRADE_SNAPSHOT'",
|
||||
shlex.quote(str(self.stub)) + " -I -S - <<'FRAMEBRIDGE_UPGRADE_SNAPSHOT'")
|
||||
result = subprocess.run(["/bin/sh", "-s", "--", *arguments], input=script,
|
||||
text=True, capture_output=True, timeout=10,
|
||||
env=dict(os.environ, TEST_CALLS=str(self.trace), TEST_CLEANUP_RC=str(fail)))
|
||||
calls = [json.loads(row) for row in self.trace.read_text().splitlines()] if self.trace.exists() else []
|
||||
return result, calls
|
||||
|
||||
def test_same_version_reinstall_takes_a_new_current_runtime_snapshot(self):
|
||||
result, calls = self.run_preinst(["upgrade", "0.3.4~dev.20261029.4"])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(calls, [["-I", "-S", "-"]])
|
||||
|
||||
def test_forward_upgrade_still_takes_a_runtime_snapshot(self):
|
||||
result, calls = self.run_preinst(["upgrade", "0.3.4~dev.20261029.3"])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(calls, [["-I", "-S", "-"]])
|
||||
|
||||
def test_fresh_install_and_downgrade_do_not_capture_unrelated_running_profiles(self):
|
||||
for arguments in (["install"], ["upgrade", "0.3.5"]):
|
||||
with self.subTest(arguments=arguments):
|
||||
result, calls = self.run_preinst(arguments)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(calls, [])
|
||||
|
||||
def test_failed_reinstall_snapshot_blocks_package_replacement(self):
|
||||
result, calls = self.run_preinst(["upgrade", "0.3.4~dev.20261029.4"], fail=17)
|
||||
self.assertEqual(result.returncode, 17)
|
||||
self.assertEqual(calls, [["-I", "-S", "-"]])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -202,6 +202,58 @@ class PackageUpgradeTests(unittest.TestCase):
|
||||
self.assertEqual(value['phase'], 'CAPTURED')
|
||||
run.assert_not_called()
|
||||
|
||||
def test_reinstall_after_failure_preserves_receipt_and_keeps_stopped_profiles_stopped(self):
|
||||
failed = {'schema': 1, 'transaction': 'a' * 16, 'phase': 'FAILED',
|
||||
'profiles': [{'state': str(self.state), 'phase': 'FAILED',
|
||||
'error': 'UPGRADE_OWNER_OPERATION_FAILED:user-ready'}]}
|
||||
self.write(self.root / 'pending.json', failed)
|
||||
processes = self.root / 'no-running-managers'
|
||||
processes.mkdir()
|
||||
marker = self.state / 'manual-stop.json'
|
||||
marker.write_bytes(b'{"manual_stop": true}\n')
|
||||
before_marker = marker.read_bytes()
|
||||
with mock.patch.object(upgrade, 'ROOT', self.root), \
|
||||
mock.patch.object(upgrade, '_root'), \
|
||||
mock.patch.object(upgrade, '_read', side_effect=self.root_read), \
|
||||
mock.patch.object(upgrade.subprocess, 'run') as run:
|
||||
value = upgrade.snapshot(processes)
|
||||
self.assertEqual(value['profiles'], [])
|
||||
self.assertEqual(upgrade.schedule()['phase'], 'COMPLETE')
|
||||
self.assertEqual(json.loads((self.root / 'previous.json').read_text()), failed)
|
||||
self.assertEqual(marker.read_bytes(), before_marker)
|
||||
run.assert_not_called()
|
||||
|
||||
def test_reinstall_after_failure_captures_only_current_managers_without_replaying_old_intent(self):
|
||||
failed = {'schema': 1, 'phase': 'FAILED',
|
||||
'profiles': [{'state': '/home/owner/previous-profile', 'phase': 'FAILED'}]}
|
||||
self.write(self.root / 'pending.json', failed)
|
||||
row = {'uid': 1000, 'state': str(self.state), 'pid': 1234, 'phase': 'CAPTURED'}
|
||||
with mock.patch.object(upgrade, 'ROOT', self.root), \
|
||||
mock.patch.object(upgrade, '_root'), \
|
||||
mock.patch.object(upgrade, '_read', side_effect=self.root_read), \
|
||||
mock.patch.object(upgrade, 'identify', return_value=row), \
|
||||
mock.patch.object(upgrade.subprocess, 'run', return_value=SimpleNamespace(returncode=0)) as run:
|
||||
value = upgrade.snapshot(self.root)
|
||||
self.assertEqual(value['profiles'], [row])
|
||||
self.assertEqual(upgrade.schedule()['phase'], 'SCHEDULED')
|
||||
self.assertEqual(json.loads((self.root / 'previous.json').read_text()), failed)
|
||||
self.assertEqual(run.call_count, 1)
|
||||
self.assertEqual(run.call_args.args[0][-2:], [upgrade.MODULE, 'resume'])
|
||||
|
||||
def test_reinstall_does_not_replace_a_snapshot_with_a_live_upgrade_worker(self):
|
||||
for phase in ('SCHEDULED', 'RUNNING'):
|
||||
with self.subTest(phase=phase):
|
||||
self.write(self.root / 'pending.json', {'schema': 1, 'phase': phase})
|
||||
before = (self.root / 'pending.json').read_bytes()
|
||||
with mock.patch.object(upgrade, 'ROOT', self.root), \
|
||||
mock.patch.object(upgrade, '_root'), \
|
||||
mock.patch.object(upgrade, '_read', side_effect=self.root_read), \
|
||||
mock.patch.object(upgrade, 'identify', return_value=None):
|
||||
with self.assertRaisesRegex(RuntimeError, 'PREVIOUS_WORKER_PENDING'):
|
||||
upgrade.snapshot(self.root)
|
||||
self.assertEqual((self.root / 'pending.json').read_bytes(), before)
|
||||
self.assertFalse((self.root / 'previous.json').exists())
|
||||
|
||||
def test_optional_desktop_migration_failure_does_not_stop_runtime_recovery(self):
|
||||
row = {'uid': 1000, 'user': 'owner', 'home': '/home/owner',
|
||||
'state': str(self.state), 'pid': 1234, 'birth': '99',
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Release assembly rejects stale or mismatched corresponding source."""
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "packaging"))
|
||||
import build_expanded
|
||||
from release_metadata import release_source
|
||||
|
||||
|
||||
class PublicReleaseSourceTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
self.file = self.root / "module.py"
|
||||
self.file.write_text("# reviewed release source\n")
|
||||
self.archive = self.root / "FrameBridge-0.3.4-source.tar.gz"
|
||||
self.vendor_paths = {
|
||||
"cp313": "source/sources/original-geranas/FrameBridge-Fleet-Hub/vendor",
|
||||
"cp312": "source/target-vendors/cp312",
|
||||
}
|
||||
vendor_data = b"# reviewed vendor input\n"
|
||||
self.vendor_files = {"module.py": vendor_data,
|
||||
"package/__init__.py": b"# package\n",
|
||||
"package-1.dist-info/METADATA": b"Name: package\nVersion: 1\n"}
|
||||
self.payloads = {"source/module.py": self.file.read_bytes(),
|
||||
**{path + "/" + relative: data for path in self.vendor_paths.values()
|
||||
for relative, data in self.vendor_files.items()}}
|
||||
self.write_archive()
|
||||
# The reviewed vendor format uses Path component ordering. In particular
|
||||
# package/__init__.py comes before package-1.dist-info/METADATA.
|
||||
digest = hashlib.sha256()
|
||||
for relative in ("module.py", "package/__init__.py", "package-1.dist-info/METADATA"):
|
||||
digest.update(relative.encode() + b"\0")
|
||||
digest.update(hashlib.sha256(self.vendor_files[relative]).digest())
|
||||
self.vendor = {"vendor_sha256": digest.hexdigest()}
|
||||
self.data = {
|
||||
"version": "0.3.4", "status": "RELEASE_SOURCE_ASSEMBLED",
|
||||
"archive": self.archive.name, "bytes": self.archive.stat().st_size,
|
||||
"sha256": hashlib.sha256(self.archive.read_bytes()).hexdigest(),
|
||||
"vendor_inputs": {"vendors": {abi: {**self.vendor, "path": path} for abi, path in self.vendor_paths.items()}},
|
||||
"files": [{"path": path, "sha256": hashlib.sha256(data).hexdigest()} for path, data in self.payloads.items()],
|
||||
}
|
||||
self.manifest = self.root / "SOURCE-ARCHIVE-MANIFEST.json"
|
||||
|
||||
def write_archive(self):
|
||||
with tarfile.open(self.archive, "w:gz") as archive:
|
||||
for path, data in self.payloads.items():
|
||||
info = tarfile.TarInfo("FrameBridge-0.3.4/" + path)
|
||||
info.size = len(data)
|
||||
archive.addfile(info, io.BytesIO(data))
|
||||
|
||||
def check(self, profile="debian13-amd64-py313", version="0.3.4"):
|
||||
self.manifest.write_text(json.dumps(self.data))
|
||||
return release_source(self.manifest, version, profile, {"source/module.py": self.file}, self.vendor)
|
||||
|
||||
def test_release_binds_archive_source_and_both_vendor_abis(self):
|
||||
for profile in ("debian13-amd64-py313", "ubuntu24.04-amd64-py312", "mint22-amd64-py312"):
|
||||
self.assertEqual(self.check(profile)["sha256"], self.data["sha256"])
|
||||
|
||||
def test_source_changed_after_archive_is_rejected(self):
|
||||
self.file.write_text("# later edit\n")
|
||||
with self.assertRaisesRegex(RuntimeError, "Package source differs"):
|
||||
self.check()
|
||||
|
||||
def test_archive_tamper_is_rejected(self):
|
||||
with self.archive.open("ab") as stream:
|
||||
stream.write(b"altered")
|
||||
with self.assertRaisesRegex(RuntimeError, "archive hash or size"):
|
||||
self.check()
|
||||
|
||||
def test_changed_manifest_cannot_relabel_old_archive_contents(self):
|
||||
self.file.write_text("# later edit\n")
|
||||
self.data["files"][0]["sha256"] = hashlib.sha256(self.file.read_bytes()).hexdigest()
|
||||
with self.assertRaisesRegex(RuntimeError, "archive content differs"):
|
||||
self.check()
|
||||
|
||||
def test_relabelled_alternate_vendor_bytes_are_rejected_even_by_debian_build(self):
|
||||
self.payloads[self.vendor_paths["cp312"] + "/module.py"] = b"# different vendor bytes\n"
|
||||
self.write_archive()
|
||||
self.data.update(bytes=self.archive.stat().st_size, sha256=hashlib.sha256(self.archive.read_bytes()).hexdigest())
|
||||
self.data["files"] = [{"path": path, "sha256": hashlib.sha256(data).hexdigest()} for path, data in self.payloads.items()]
|
||||
with self.assertRaisesRegex(RuntimeError, "Archived vendor bytes differ"):
|
||||
self.check()
|
||||
|
||||
def test_unsafe_archive_member_is_rejected_without_extracting(self):
|
||||
self.payloads["../escape"] = b"unwanted path"
|
||||
self.write_archive()
|
||||
self.data.update(bytes=self.archive.stat().st_size, sha256=hashlib.sha256(self.archive.read_bytes()).hexdigest())
|
||||
self.data["files"].append({"path": "../escape", "sha256": hashlib.sha256(b"unwanted path").hexdigest()})
|
||||
with self.assertRaisesRegex(RuntimeError, "Unsafe release source archive path"):
|
||||
self.check()
|
||||
|
||||
def test_release_requires_cp312_corresponding_source(self):
|
||||
del self.data["vendor_inputs"]["vendors"]["cp312"]
|
||||
with self.assertRaisesRegex(RuntimeError, "cp312 and cp313"):
|
||||
self.check()
|
||||
|
||||
def test_vendor_input_cannot_differ_from_corresponding_source(self):
|
||||
self.data["vendor_inputs"]["vendors"]["cp312"]["vendor_sha256"] = "b" * 64
|
||||
with self.assertRaisesRegex(RuntimeError, "vendor input differs"):
|
||||
self.check("mint22-amd64-py312")
|
||||
|
||||
def test_candidate_version_and_experimental_profile_are_not_public_release(self):
|
||||
for profile, version in (("lmde7-amd64-py313", "0.3.4"), ("debian13-amd64-py313", "0.3.4~dev.1")):
|
||||
with self.assertRaisesRegex(RuntimeError, "stable version and an official"):
|
||||
self.check(profile, version)
|
||||
|
||||
def test_public_metadata_names_source_without_claiming_signature_or_vm_qa(self):
|
||||
docs = self.root / "docs"
|
||||
docs.mkdir()
|
||||
identity = self.check()
|
||||
with mock.patch.object(build_expanded, "PUBLIC_RELEASE", True), \
|
||||
mock.patch.object(build_expanded, "SOURCE_ARCHIVE", identity), \
|
||||
mock.patch.object(build_expanded, "VERSION", "0.3.4"):
|
||||
build_expanded.install_legal_material(docs)
|
||||
result = json.loads((docs / "COMMUNITY-RELEASE-STATUS.json").read_text())
|
||||
self.assertEqual(result["status"], "PUBLIC_RELEASE_BUILD_ASSEMBLED")
|
||||
self.assertEqual(result["source_archive"], identity)
|
||||
self.assertEqual(result["signature_status"], "EXTERNAL_RELEASE_RECORD")
|
||||
self.assertEqual(result["vm_lifecycle_validation"], "NOT_ASSERTED_BY_BUILDER")
|
||||
self.assertNotIn("compatibility candidate", (docs / "copyright").read_text())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user