100 lines
4.2 KiB
Python
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()
|