Files
EscapistsReintroduced/tests/test_external_addons.py
T
jakub cab998fb64 Combine Workshop addon and fix prison startup
Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
2026-09-20 16:42:52 +02:00

73 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Exercise exact dependency closure, resource ownership and isolated profile inputs."""
import copy
import hashlib
import json
from pathlib import Path
import shutil
import sys
import tempfile
import unittest
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
import external_addons as ext
ROOT=Path(__file__).resolve().parents[1]
class ExternalAddonsTests(unittest.TestCase):
def setUp(self):
self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup)
self.home=Path(self.temp.name);self.source=self.home/'installed';self.source.mkdir()
self.addons=self.home/'profile'/'addons';self.addons.mkdir(parents=True)
self.cache=self.home/'profile'/'cache.json';self.value=ext.policy(ROOT)
for guid,deps in ext.ALLOWED.items():
folder=self.source/guid;folder.mkdir()
manifest='GameProject { GUID "'+guid+'" Dependencies { '+' '.join('"'+d+'"' for d in sorted(deps))+' } }\n'
(folder/'addon.gproj').write_text(manifest)
self.value['addons'][guid]['project_sha256']=hashlib.sha256(manifest.encode()).hexdigest()
data=b'RDB synthetic fixture\0'
for path,entry in self.value['resources'].items():
if entry['addon']==guid:data+=path.encode()+b'\0\x06\0\0\0\0\0'+bytes.fromhex(entry['guid'])[::-1]
(folder/'resourceDatabase.rdb').write_bytes(data);(folder/'data.pak').write_bytes(b'fixture '+guid.encode())
def test_resource_identity_requires_visible_owner(self):
for path,item in self.value['resources'].items():
self.assertTrue(ext.resource_allowed(self.value,{item['addon']},item['guid'],path))
self.assertFalse(ext.resource_allowed(self.value,{'core'},item['guid'],path))
self.assertFalse(ext.resource_allowed(self.value,{item['addon']},'0000000000000000',path))
self.assertEqual(ext.direct_for('core'),set(ext.DIRECT));self.assertEqual(ext.direct_for('chat'),set())
def test_policy_rejects_original_transitive_dependency(self):
root=self.home/'root';(root/'docs').mkdir(parents=True)
value=copy.deepcopy(self.value);value['addons'][ext.DIRECT[0]]['dependencies'].append('5F16D7E4A1CBE075')
(root/'docs/external-addons.json').write_text(json.dumps(value))
with self.assertRaisesRegex(ValueError,'transitive'):ext.policy(root)
def test_only_exact_six_are_exposed_and_hashed(self):
unrelated=self.source/'original';unrelated.mkdir();(unrelated/'addon.gproj').write_text('GameProject { GUID "5F16D7E4A1CBE075" }')
records=ext.prepare_profile(self.addons,self.source,self.value,self.cache)
self.assertEqual(set(records),set(ext.ALLOWED))
self.assertEqual({p.name for p in self.addons.iterdir()},{'ERRI_'+g for g in ext.ALLOWED})
self.assertTrue(all(p.is_symlink() for p in self.addons.iterdir()))
self.assertTrue(ext.verify_profile(records,self.source,self.value,self.cache,self.addons))
self.assertEqual(ext.prepare_profile(self.addons,self.source,self.value,self.cache),records)
(self.source/ext.DIRECT[0]/'data.pak').write_bytes(b'mutated runtime')
self.assertFalse(ext.verify_profile(records,self.source,self.value,self.cache,self.addons))
def test_missing_and_duplicate_identity_fail(self):
guid=ext.DIRECT[0];shutil.copytree(self.source/guid,self.source/'duplicate')
with self.assertRaisesRegex(ValueError,'Duplicate'):ext.installed(self.source,self.value)
shutil.rmtree(self.source/'duplicate');shutil.rmtree(self.source/guid)
with self.assertRaisesRegex(ValueError,'Missing installed'):ext.installed(self.source,self.value)
def test_manifest_and_database_mutations_fail(self):
guid=ext.DIRECT[0];manifest=self.source/guid/'addon.gproj';old=manifest.read_text();manifest.write_text(old+'// changed')
with self.assertRaisesRegex(ValueError,'manifest changed'):ext.installed(self.source,self.value)
manifest.write_text(old)
resource=next(iter(self.value['resources'].values()));(self.source/resource['addon']/'resourceDatabase.rdb').write_bytes(b'wrong GUID')
with self.assertRaisesRegex(ValueError,'resource identity'):ext.installed(self.source,self.value)
def test_unrelated_profile_addon_and_wrong_link_fail(self):
(self.addons/'original').mkdir()
with self.assertRaisesRegex(ValueError,'Unrelated'):ext.prepare_profile(self.addons,self.source,self.value,self.cache)
(self.addons/'original').rmdir();guid=ext.DIRECT[0]
(self.addons/('ERRI_'+guid)).symlink_to(self.source/ext.DIRECT[1],target_is_directory=True)
with self.assertRaisesRegex(ValueError,'Unexpected private'):ext.prepare_profile(self.addons,self.source,self.value,self.cache)
def test_symlinked_runtime_file_fails(self):
pak=self.source/ext.DIRECT[0]/'data.pak';pak.unlink();pak.symlink_to(self.source/ext.DIRECT[1]/'data.pak')
with self.assertRaisesRegex(ValueError,'regular file'):ext.installed(self.source,self.value)
if __name__=='__main__':unittest.main()