Files

100 lines
4.2 KiB
Python

import json
import sys
import tempfile
import unittest
import zipfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
import evidence
class EvidenceTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
(self.root / 'observation.txt').write_text('measured=1', encoding='utf-8')
def tearDown(self):
self.temp.cleanup()
def run_bundle(self, assertions):
manifest = self.root / 'input.json'
manifest.write_text(json.dumps({'schema_version': 1, 'run_id': 'LAB-001',
'evidence_root': '.', 'assertions': assertions}), encoding='utf-8')
return evidence.bundle(manifest, self.root / 'output')
def assertion(self, verdict='passed', refs=None):
return {'id': 'live', 'expected': '1', 'actual': '1', 'verdict': verdict,
'evidence': refs if refs is not None else ['observation.txt']}
def test_success_has_hash_and_portable_archive(self):
result = self.run_bundle([self.assertion()])
self.assertEqual(result['verdict'], 'passed')
self.assertEqual(len(result['artifacts'][0]['sha256']), 64)
with zipfile.ZipFile(self.root / 'output' / 'bundle.zip') as z:
self.assertIn('report.md', z.namelist())
self.assertIn('evidence/observation.txt', z.namelist())
def test_missing_file_does_not_pass(self):
result = self.run_bundle([self.assertion(refs=['missing.txt'])])
self.assertEqual(result['verdict'], 'inconclusive')
def test_empty_evidence_does_not_pass(self):
self.assertEqual(self.run_bundle([self.assertion(refs=[])])['verdict'], 'inconclusive')
def test_expected_hash_mismatch_does_not_pass(self):
result = self.run_bundle([self.assertion(refs=[{'path': 'observation.txt', 'sha256': '0' * 64}])])
self.assertEqual(result['verdict'], 'inconclusive')
self.assertEqual(result['assertions'][0]['requested_evidence'][0]['sha256'], '0' * 64)
self.assertEqual(result['artifacts'][0]['integrity'], 'mismatch')
self.assertTrue((self.root / 'output' / 'evidence' / 'observation.txt').is_file())
def test_real_failure_preserved_alongside_blocked(self):
blocked = self.assertion('blocked', [])
blocked['id'] = 'later'
self.assertEqual(self.run_bundle([self.assertion('failed'), blocked])['verdict'], 'failed')
def test_no_assertions_not_pass(self):
self.assertEqual(self.run_bundle([])['verdict'], 'inconclusive')
def test_path_escape_rejected(self):
with self.assertRaises(ValueError):
self.run_bundle([self.assertion(refs=['../outside.txt'])])
def test_cannot_overwrite_run(self):
self.run_bundle([self.assertion()])
with self.assertRaises(FileExistsError):
evidence.bundle(self.root / 'input.json', self.root / 'output')
def test_invalid_verdict_rejected(self):
with self.assertRaises(ValueError):
self.run_bundle([self.assertion('completed')])
def test_duplicate_ids_rejected(self):
with self.assertRaises(ValueError):
self.run_bundle([self.assertion(), self.assertion()])
def test_optional_checks_alone_cannot_pass(self):
a = self.assertion()
a['required'] = False
self.assertEqual(self.run_bundle([a])['verdict'], 'inconclusive')
def test_source_named_bundle_zip_is_preserved(self):
(self.root / 'bundle.zip').write_bytes(b'external evidence archive')
self.run_bundle([self.assertion(refs=['bundle.zip'])])
with zipfile.ZipFile(self.root / 'output' / 'bundle.zip') as z:
self.assertIn('evidence/bundle.zip', z.namelist())
def test_simulation_is_visible_in_human_report(self):
manifest = self.root / 'input.json'
manifest.write_text(json.dumps({'schema_version': 1, 'run_id': 'EXAMPLE',
'context': {'execution_mode': 'simulation', 'real_device_test_executed': False},
'assertions': [self.assertion()]}), encoding='utf-8')
evidence.bundle(manifest, self.root / 'output')
self.assertIn('simulation', (self.root / 'output' / 'report.md').read_text(encoding='utf-8'))
if __name__ == '__main__':
unittest.main()