import importlib.util from pathlib import Path import tempfile import unittest from unittest import mock import contextlib import io import json import sys from types import SimpleNamespace MODULE = Path(__file__).resolve().parents[1] / "deploy/start.py" spec = importlib.util.spec_from_file_location("start_console", MODULE) start = importlib.util.module_from_spec(spec) spec.loader.exec_module(start) class DeploymentPathsTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.base = Path(self.temp.name) self.project = self.base / "project" self.project.mkdir() self.outside = self.base / "other-owner" self.outside.mkdir() def validator(self): self.assertTrue(hasattr(start, "validate_runtime"), "runtime boundary validation is missing") return start.validate_runtime def link(self, target, destination): try: destination.symlink_to(target, target_is_directory=True) except OSError: self.skipTest("OS account cannot create a directory symlink") def test_runtime_parent_link_cannot_escape_project(self): validate = self.validator() self.link(self.outside, self.project / "runtime") before = self.outside.stat().st_mode with self.assertRaises(ValueError): validate(self.project) self.assertEqual(self.outside.stat().st_mode, before) self.assertEqual(list(self.outside.iterdir()), []) def test_runtime_child_link_cannot_escape_project(self): validate = self.validator() (self.project / "runtime").mkdir() self.link(self.outside, self.project / "runtime/data") with self.assertRaises(ValueError): validate(self.project) self.assertEqual(list(self.outside.iterdir()), []) def test_normal_runtime_is_resolved_without_writing(self): validate = self.validator() self.assertEqual(validate(self.project), self.project.resolve() / "runtime") self.assertEqual(list(self.project.iterdir()), []) def test_backend_is_only_on_proxy_loopback_namespace(self): # Regress the reviewed direct-container-IP role-header bypass at the Docker boundary. deploy = self.project / 'deploy' deploy.mkdir() (deploy/'lan-settings.json').write_text(json.dumps({ 'address':'10.88.118.188','allowed_cidrs':['10.88.0.0/17','10.20.0.0/21'],'port':14440})) calls = [] def docker(args, **kwargs): calls.append(args) absent = args[:3] in (['docker','container','inspect'], ['docker','network','inspect']) return SimpleNamespace(returncode=1 if absent else 0, stdout='') with mock.patch.object(sys, 'argv', ['start.py','--project-root',str(self.project)]), \ mock.patch.object(sys, 'path', [str(MODULE.parent)] + sys.path), \ mock.patch.object(start.subprocess, 'run', side_effect=docker), \ mock.patch.object(start.os, 'chown', create=True), \ mock.patch.object(start.socket, 'socket'), contextlib.redirect_stdout(io.StringIO()): start.main() backend = next(c for c in calls if '--name' in c and c[c.index('--name')+1]=='adamwen-test-console') proxy = next(c for c in calls if '--name' in c and c[c.index('--name')+1]=='adamwen-test-console-proxy') self.assertIn('RUNDECK_SERVER_ADDRESS=127.0.0.1', backend) self.assertEqual(backend[backend.index('--network')+1], 'container:adamwen-test-console-proxy') self.assertNotIn('--publish', backend) self.assertLess(calls.index(proxy), calls.index(backend))