168 lines
6.3 KiB
Python
168 lines
6.3 KiB
Python
import os
|
|
from pathlib import Path
|
|
import sqlite3
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
APP_DIR = ROOT / 'app'
|
|
sys.path.insert(0, str(APP_DIR))
|
|
|
|
import scanner_db
|
|
from scanner_db import ScannerDB
|
|
|
|
|
|
class OperationsSchemaTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.environment = mock.patch.dict(
|
|
os.environ,
|
|
{'SCANNER_DB_URL': '', 'DATABASE_URL': ''},
|
|
)
|
|
self.environment.start()
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.db = ScannerDB(db_path=os.path.join(self.temp.name, 'scanner.db'))
|
|
|
|
def tearDown(self):
|
|
self.db.close()
|
|
self.temp.cleanup()
|
|
self.environment.stop()
|
|
|
|
def test_fresh_schema_has_operations_authority_and_migration(self):
|
|
self.assertEqual(
|
|
scanner_db.PIPELINE_MIGRATION_VERSIONS[-1],
|
|
scanner_db.REMOTE_ASSIGNMENT_CAPACITY_MIGRATION,
|
|
)
|
|
self.assertEqual(len(scanner_db.PIPELINE_MIGRATION_VERSIONS), 33)
|
|
for table in (
|
|
'runtime_operations',
|
|
'runtime_operations_control',
|
|
'runtime_audit_events',
|
|
):
|
|
with self.subTest(table=table):
|
|
self.assertEqual(
|
|
set(self.db.conn.table_columns(table)),
|
|
set(scanner_db.RUNTIME_TABLE_SPECS[table]),
|
|
)
|
|
control = self.db.conn.execute(
|
|
'SELECT * FROM runtime_operations_control WHERE id = 1'
|
|
).fetchone()
|
|
self.assertEqual(int(control['revision']), 0)
|
|
self.assertEqual(int(control['discovery_paused']), 0)
|
|
self.assertEqual(int(control['dispatch_paused']), 0)
|
|
self.assertEqual(control['drain_state'], 'normal')
|
|
self.assertEqual(control['actor'], 'system:migration')
|
|
marker = self.db.conn.execute(
|
|
'SELECT version FROM runtime_schema_migrations WHERE version = ?',
|
|
(scanner_db.OPERATIONS_CONTROL_MIGRATION,),
|
|
).fetchone()
|
|
self.assertIsNotNone(marker)
|
|
self.assertTrue(self.db.runtime_safety_schema_available(), self.db.last_error)
|
|
self.assertTrue(self.db.pipeline_schema_available(), self.db.last_error)
|
|
|
|
def test_operations_authority_invariants_fail_closed(self):
|
|
mutations = (
|
|
(
|
|
'DELETE FROM runtime_operations_control WHERE id = 1',
|
|
'runtime_operations_control singleton row',
|
|
),
|
|
(
|
|
'DROP INDEX idx_runtime_operations_status_updated',
|
|
'index idx_runtime_operations_status_updated',
|
|
),
|
|
(
|
|
'DROP TRIGGER runtime_audit_events_reject_delete',
|
|
'trigger runtime_audit_events_reject_delete',
|
|
),
|
|
)
|
|
for sql, expected in mutations:
|
|
with self.subTest(sql=sql):
|
|
self.db.conn.execute(sql)
|
|
self.db.conn.commit()
|
|
self.assertFalse(self.db.runtime_safety_schema_available())
|
|
self.assertIn(expected, self.db.last_error)
|
|
with self.assertRaisesRegex(
|
|
scanner_db.RuntimeSafetySchemaError,
|
|
expected,
|
|
):
|
|
self.db.require_runtime_safety_schema()
|
|
self.db.initialize_schema()
|
|
self.assertTrue(self.db.runtime_safety_schema_available(), self.db.last_error)
|
|
|
|
def test_pipeline_schema_requires_operations_authority_indexes(self):
|
|
self.db.conn.execute('DROP INDEX idx_runtime_audit_events_operation')
|
|
self.db.conn.commit()
|
|
|
|
self.assertFalse(self.db.pipeline_schema_available())
|
|
self.assertIn('index idx_runtime_audit_events_operation', self.db.last_error)
|
|
|
|
def test_initialize_schema_repairs_malformed_audit_trigger(self):
|
|
self.db.conn.execute('DROP TRIGGER runtime_audit_events_reject_update')
|
|
self.db.conn.execute('''
|
|
CREATE TRIGGER runtime_audit_events_reject_update
|
|
AFTER INSERT ON runtime_audit_events BEGIN SELECT 1; END
|
|
''')
|
|
self.db.conn.commit()
|
|
|
|
self.assertFalse(self.db.runtime_safety_schema_available())
|
|
self.assertIn('trigger runtime_audit_events_reject_update', self.db.last_error)
|
|
self.db.initialize_schema()
|
|
|
|
self.assertTrue(self.db.runtime_safety_schema_available(), self.db.last_error)
|
|
trigger = self.db.conn.table_triggers('runtime_audit_events')[
|
|
'runtime_audit_events_reject_update'
|
|
]
|
|
self.assertIn('BEFORE UPDATE', trigger['sql'].upper())
|
|
|
|
def test_control_seed_is_idempotent(self):
|
|
self.db.conn.execute(
|
|
'''UPDATE runtime_operations_control
|
|
SET revision = 7, discovery_paused = 1, actor = 'operator', updated_at = ?
|
|
WHERE id = 1''',
|
|
(scanner_db.utc_now_iso(),),
|
|
)
|
|
self.db.conn.commit()
|
|
|
|
self.db.initialize_schema()
|
|
|
|
control = self.db.conn.execute(
|
|
'SELECT revision, discovery_paused, actor FROM runtime_operations_control WHERE id = 1'
|
|
).fetchone()
|
|
self.assertEqual(dict(control), {
|
|
'revision': 7,
|
|
'discovery_paused': 1,
|
|
'actor': 'operator',
|
|
})
|
|
|
|
def test_audit_events_are_append_only(self):
|
|
self.db.conn.execute(
|
|
'''INSERT INTO runtime_audit_events(
|
|
actor, action, target_kind, target_ref, result,
|
|
event_sha256, created_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?)''',
|
|
(
|
|
'operator', 'control-update', 'runtime-control', '', 'accepted',
|
|
'a' * 64, scanner_db.utc_now_iso(),
|
|
),
|
|
)
|
|
self.db.conn.commit()
|
|
|
|
with self.assertRaisesRegex(sqlite3.DatabaseError, 'append-only'):
|
|
self.db.conn.execute(
|
|
"UPDATE runtime_audit_events SET result = 'succeeded' WHERE id = 1"
|
|
)
|
|
self.db.conn.rollback()
|
|
with self.assertRaisesRegex(sqlite3.DatabaseError, 'append-only'):
|
|
self.db.conn.execute('DELETE FROM runtime_audit_events WHERE id = 1')
|
|
self.db.conn.rollback()
|
|
count = self.db.conn.execute(
|
|
'SELECT COUNT(*) AS count FROM runtime_audit_events'
|
|
).fetchone()
|
|
self.assertEqual(int(count['count']), 1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|