policy-nexus/tests/test_publication_transition.py
tegwick 65862b1e0c
All checks were successful
Build and publish policy-nexus image / build-and-push (push) Successful in 1m25s
Reject history overwrite and unreviewed publication routing changes
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
2026-10-11 02:31:41 +02:00

68 lines
3.8 KiB
Python

import hashlib
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
spec = importlib.util.spec_from_file_location('transition', Path(__file__).parents[1] / 'tools' / 'verify_publication_transition.py')
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
class TransitionTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
self.doc = {'id':'a','revision_path':'a/revisions/1/index.html','revision':'1','source_digest':'old','source_revision':'old-head'}
self.old = self.root / 'baseline.json'
self.old.write_text(json.dumps({'documents':[self.doc]}))
self.audit = self.root / 'audit.json'
self.audit.write_text(json.dumps({'immutable_results':[{'id':'a','revision_path':self.doc['revision_path'],'http_status':200,'served_html_sha256':hashlib.sha256(b'original').hexdigest()}]}))
self.page = self.root / self.doc['revision_path']
self.page.parent.mkdir(parents=True)
self.page.write_bytes(b'original')
self.write_candidate(self.doc | {'source_revision':'new-head'})
def write_candidate(self, doc):
(self.root / 'publication-manifest.json').write_text(json.dumps({'documents':[doc]}))
def verify(self, allow=()):
return m.verify(self.root, self.old, self.audit, allow)
def test_new_head_same_markdown_preserves_original_html(self):
self.assertEqual(self.verify()['history_preserved'], 1)
self.page.write_bytes(b'regenerated new-head')
with self.assertRaisesRegex(ValueError,'immutable bytes changed'): self.verify()
def test_reviewed_new_revision_still_requires_previous_history(self):
self.write_candidate(self.doc | {'revision':'2','source_digest':'new'})
with self.assertRaisesRegex(ValueError,'unreviewed'): self.verify()
self.verify(['a'])
self.page.unlink()
with self.assertRaisesRegex(ValueError,'absent'): self.verify(['a'])
def test_allowlist_cannot_approve_history_rewrite(self):
self.page.write_bytes(b'rewrite')
with self.assertRaisesRegex(ValueError,'immutable bytes changed'): self.verify(['a'])
def test_failed_capture_refuses(self):
self.audit.write_text(json.dumps({'immutable_results':[{'id':'a','revision_path':self.doc['revision_path'],'http_status':404}]}))
with self.assertRaisesRegex(ValueError,'not successfully captured'): self.verify()
def test_path_traversal_refuses(self):
self.doc['revision_path']='../outside'
self.old.write_text(json.dumps({'documents':[self.doc]}))
self.audit.write_text(json.dumps({'immutable_results':[{'id':'a','revision_path':'../outside','http_status':200,'served_html_sha256':'a'*64}]}))
with self.assertRaisesRegex(ValueError,'unsafe'): self.verify()
def test_address_and_owner_routing_changes_require_review(self):
for field in ('canonical_path','revision_path','source_repo','source_path','owner'):
with self.subTest(field=field):
self.write_candidate(self.doc | {field:'changed'})
with self.assertRaisesRegex(ValueError,'unreviewed'):
self.verify()
def test_symlink_parent_cannot_supply_history_from_outside_candidate(self):
import shutil
outside = self.root.parent / (self.root.name + '-outside')
outside.mkdir()
self.addCleanup(lambda: shutil.rmtree(outside))
(outside/'revisions/1').mkdir(parents=True)
(outside/'revisions/1/index.html').write_bytes(b'original')
shutil.rmtree(self.root/'a')
(self.root/'a').symlink_to(outside, target_is_directory=True)
with self.assertRaisesRegex(ValueError,'absent or unsafe'):
self.verify()