test_database_delivery.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import contextlib
  2. import hashlib
  3. import importlib.util
  4. import json
  5. from pathlib import Path
  6. import shutil
  7. import sqlite3
  8. import tempfile
  9. import unittest
  10. from unittest.mock import patch
  11. import test_voc_db as fixture
  12. ROOT = Path(__file__).resolve().parents[2]
  13. spec = importlib.util.spec_from_file_location('database_installer', ROOT / 'scripts/install-voc-database.py')
  14. installer = importlib.util.module_from_spec(spec)
  15. spec.loader.exec_module(installer)
  16. class DatabaseDeliveryTests(unittest.TestCase):
  17. @classmethod
  18. def setUpClass(cls):
  19. fixture.VocLibraryTests.setUpClass()
  20. cls.db = fixture.VocLibraryTests.db
  21. @classmethod
  22. def tearDownClass(cls):
  23. fixture.VocLibraryTests.tearDownClass()
  24. def setUp(self):
  25. self.temp = tempfile.TemporaryDirectory()
  26. self.addCleanup(self.temp.cleanup)
  27. self.root = Path(self.temp.name)
  28. self.package = self.root / 'delivery'
  29. self.target = self.root / 'ksf-voc-library'
  30. source = self.package / 'library/library.sqlite'
  31. source.parent.mkdir(parents=True)
  32. shutil.copyfile(self.db, source)
  33. with fixture.v.readonly(source) as con:
  34. meta = dict(con.execute('SELECT key,value FROM meta'))
  35. counts = fixture.v.table_counts(con)
  36. counts['retrieval_documents'] = con.execute('SELECT count(*) FROM retrieval_documents').fetchone()[0]
  37. expected = dict(sha256=installer.digest(source), bytes=source.stat().st_size, schema=meta['schema'],
  38. generation=meta['generation'], originalArchiveSha256=meta['archive_sha256'], counts=counts)
  39. (self.package / 'DATABASE.json').write_text(json.dumps(dict(database=expected)), encoding='utf-8')
  40. self.manifest()
  41. self.private = self.root / 'skills/private-binding.json'
  42. self.private.parent.mkdir()
  43. self.private.write_text('keep', encoding='utf-8')
  44. def manifest(self):
  45. paths = [p for p in self.package.rglob('*') if p.is_file() and p.name != 'FILES.sha256']
  46. (self.package / 'FILES.sha256').write_text(''.join(installer.digest(p) + ' ' + p.relative_to(self.package).as_posix() + '\n' for p in sorted(paths)), encoding='utf-8')
  47. def snapshot(self):
  48. return {p.relative_to(self.root).as_posix(): (installer.digest(p), p.stat().st_mtime_ns) for p in self.root.rglob('*') if p.is_file()}
  49. def test_dry_run_installs_only_database_and_repeat_is_noop(self):
  50. before = self.snapshot()
  51. plan = installer.install(self.package, self.target)
  52. self.assertTrue(plan['readOnly']); self.assertFalse(self.target.exists())
  53. self.assertEqual(self.snapshot(), before)
  54. receipt = installer.install(self.package, self.target, True)
  55. self.assertTrue(receipt['filesystemVerified']); self.assertTrue(receipt['applied'])
  56. self.assertEqual((self.target / 'library.sqlite').read_bytes(), self.db.read_bytes())
  57. self.assertEqual(self.private.read_text(), 'keep')
  58. landed = self.snapshot()
  59. again = installer.install(self.package, self.target, True)
  60. self.assertEqual(again['operation'], 'unchanged'); self.assertFalse(again['applied'])
  61. self.assertEqual(self.snapshot(), landed)
  62. def test_different_database_and_existing_registry_are_preserved(self):
  63. self.target.mkdir()
  64. (self.target / 'library.sqlite').write_text('existing database', encoding='utf-8')
  65. before = self.snapshot()
  66. with self.assertRaisesRegex(ValueError, 'TARGET_EXISTS_DIFFERENT_DATABASE'):
  67. installer.install(self.package, self.target, True)
  68. self.assertEqual(self.snapshot(), before)
  69. (self.target / 'current.json').write_text('{}', encoding='utf-8')
  70. with self.assertRaisesRegex(ValueError, 'TARGET_EXISTS_DIFFERENT_CONTENT'):
  71. installer.install(self.package, self.target, True)
  72. def test_package_tampering_stops_before_creation(self):
  73. (self.package / 'library/library.sqlite').write_text('tampered', encoding='utf-8')
  74. with self.assertRaisesRegex(ValueError, 'PACKAGE_HASH_MISMATCH'):
  75. installer.install(self.package, self.target, True)
  76. self.assertFalse(self.target.exists())
  77. def test_staging_failure_leaves_no_database_or_temporary_files(self):
  78. copy = installer.shutil.copyfile
  79. def fail_copy(source, target):
  80. copy(source, target)
  81. raise OSError('copy failed')
  82. with patch.object(installer.shutil, 'copyfile', fail_copy):
  83. with self.assertRaisesRegex(OSError, 'copy failed'):
  84. installer.install(self.package, self.target, True)
  85. self.assertFalse(self.target.exists())
  86. self.assertFalse(list(self.root.glob('.ksf-voc-database-*')))
  87. def test_declared_generation_mismatch_is_not_installed(self):
  88. path = self.package / 'DATABASE.json'
  89. manifest = json.loads(path.read_text(encoding='utf-8'))
  90. manifest['database']['generation'] = 'wrong-generation'
  91. path.write_text(json.dumps(manifest), encoding='utf-8'); self.manifest()
  92. with self.assertRaisesRegex(ValueError, 'DATABASE_VERSION_OR_GENERATION_MISMATCH'):
  93. installer.install(self.package, self.target, True)
  94. self.assertFalse(self.target.exists())