fix(cache): refuse a tor deploy whose node onion is empty

get_merged_domains gated the onion injection on node_onion being set AND
svc-net-tor being in group_names. When the node was empty it answered the
clearnet map with no error and no log line, and because group_names is
part of the cache key, a clearnet map and an onion map coexist in one
process under separate keys. One render then serves onion while another
freezes a clearnet asset URL into a stylesheet.

That URL is unrecoverable at request time: body_filter.lua returns a CSS
response before onionize(), so the <link href> in the head is rewritten
while the url(...) inside the stylesheet body is not. The symptom is a
single toHaveCSS mismatch and nothing else.

An empty node under a deployed svc-net-tor now raises. A scope carrying no
group_names at all stays silent, because that is indistinguishable from a
legitimate non-tor deploy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-10-14 00:21:24 +02:00
1 parent 0f6278873f
commit e16d807e62
2 files changed
+48 -1

No files matched your search

+40
View File
@@ -403,5 +403,45 @@ class TestOnionFlagsAreRendered(unittest.TestCase):
)
class TestOnionNodeIsRequiredWhenTorIsDeployed(unittest.TestCase):
"""A deploy carrying `svc-net-tor` without a provisioned node used to
answer the clearnet map and cache it under its own key, because
`group_names` is part of that key. One scope then served onion while
another froze a clearnet asset URL into a stylesheet, which
`body_filter.lua`'s `onionize` returns before rewriting.
"""
ONION = TestOnionDomainInjection.ONION
def setUp(self) -> None:
_reset_cache_for_tests()
def _merge(self, node, group_names):
with tempfile.TemporaryDirectory() as tmp:
roles = _seed_minimal_role(Path(tmp))
with patch(
"utils.cache.applications.get_merged_applications",
return_value={"svc-net-tor": {"services": {"tor": {"node": node}}}},
):
return cache_domains.get_merged_domains(
variables={
"DOMAIN_PRIMARY": "infinito.test",
"group_names": group_names,
},
roles_dir=roles,
templar=None,
)
def test_an_empty_node_is_an_error(self):
with self.assertRaisesRegex(ValueError, "services.tor.node"):
self._merge("", ["svc-net-tor", "web-app-x"])
def test_a_provisioned_node_is_accepted(self):
self.assertIsInstance(self._merge(self.ONION, ["svc-net-tor"]), dict)
def test_an_empty_node_is_fine_without_tor_in_the_groups(self):
self.assertIsInstance(self._merge("", ["web-app-x"]), dict)
if __name__ == "__main__":
unittest.main()