diff --git a/packages/mosaic/src/lease-broker/promotion_binding_unittest.py b/packages/mosaic/src/lease-broker/promotion_binding_unittest.py index e55c517a..e19e7e8d 100644 --- a/packages/mosaic/src/lease-broker/promotion_binding_unittest.py +++ b/packages/mosaic/src/lease-broker/promotion_binding_unittest.py @@ -26,6 +26,8 @@ collapsing the two is what let a permission change quietly shrink the law. from __future__ import annotations +import contextlib +import io import os import sys import tempfile @@ -45,6 +47,12 @@ REQUIRED = frozenset(lease_promote.REQUIRED_SOURCES) | {RUNTIME_CONTRACT} # exists to catch. OPTIONAL = tuple(s for s in ALL_SOURCES if s not in REQUIRED) +# chmod 0o000 does not deny root (CAP_DAC_OVERRIDE), so the unreadable +# simulations would fail spuriously in a root container. +runs_unprivileged = unittest.skipIf( + os.geteuid() == 0, "chmod 0o000 cannot make a file unreadable to root" +) + class PromotionBindingTest(unittest.TestCase): def setUp(self) -> None: @@ -106,13 +114,19 @@ class PromotionBindingTest(unittest.TestCase): with self.subTest(source=source_id): self.reset_home() (self.root / source_id).unlink() - bound = self.source_ids() + notice = io.StringIO() + with contextlib.redirect_stderr(notice): + bound = self.source_ids() self.assertNotIn(source_id, bound) for required in lease_promote.REQUIRED_SOURCES: self.assertIn(required, bound) + # A silent omission is the original defect in miniature: the + # narrower binding must announce itself. + self.assertIn(source_id, notice.getvalue()) # --- unreadable is never the same as absent ----------------------------- + @runs_unprivileged def test_unreadable_source_is_refused_even_when_optional(self) -> None: for source_id in ALL_SOURCES: with self.subTest(source=source_id): @@ -124,6 +138,7 @@ class PromotionBindingTest(unittest.TestCase): # --- the exact measured regression -------------------------------------- + @runs_unprivileged def test_single_readable_source_cannot_promote(self) -> None: """The observed failure: only USER.md readable produced a valid binding.""" for source_id in ALL_SOURCES: @@ -132,6 +147,7 @@ class PromotionBindingTest(unittest.TestCase): with self.assertRaises(lease_promote.IncompleteBinding): self.build() + @runs_unprivileged def test_no_source_readable_cannot_promote(self) -> None: for source_id in ALL_SOURCES: (self.root / source_id).chmod(0o000)