wecom-tool-result-install.test.py 4.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. """Installer fixtures are isolated: no gateway, profiles or connection settings."""
  2. import importlib.util
  3. import json
  4. import os
  5. from pathlib import Path
  6. import tempfile
  7. import unittest
  8. from unittest.mock import patch
  9. ROOT = Path(__file__).resolve().parent.parent
  10. spec = importlib.util.spec_from_file_location('hotfix_installer', ROOT / 'scripts/install-wecom-tool-result-hotfix.py')
  11. installer = importlib.util.module_from_spec(spec); spec.loader.exec_module(installer)
  12. BASELINE = Path(os.environ.get('KSF_TOOL_HOTFIX_BASELINE') or ROOT / 'work/adapter-v4-review-20261102/adapter-patch-v4')
  13. class InstallTests(unittest.TestCase):
  14. def setUp(self):
  15. temp = tempfile.TemporaryDirectory(prefix='ksf-result-hotfix-'); self.addCleanup(temp.cleanup)
  16. self.base = Path(temp.name).resolve()
  17. self.dest = self.base / 'platform'; self.dest.mkdir()
  18. self.source = self.base / 'package'; (self.source / 'integrations/wecom-identity').mkdir(parents=True)
  19. manifest = ROOT / 'HOTFIX.json'
  20. if not manifest.is_file(): manifest = ROOT / 'work/tool-result-hotfix-20261102/HOTFIX.json'
  21. (self.source / 'HOTFIX.json').write_bytes(manifest.read_bytes())
  22. for name in installer.BEFORE:
  23. (self.source / 'integrations/wecom-identity' / name).write_bytes((ROOT / 'integrations/wecom-identity' / name).read_bytes())
  24. for name in installer.BEFORE:
  25. origin = BASELINE / ('platform' if name == 'workshop_registration.py' else '') / name
  26. data = origin.read_bytes()
  27. self.assertEqual(installer.digest(data), installer.BEFORE[name])
  28. (self.dest / name).write_bytes(data)
  29. self.backup = self.base / 'backup'
  30. def test_readonly_install_repeat_and_no_state_change(self):
  31. before = {p.name: p.read_bytes() for p in self.dest.iterdir()}
  32. result = installer.install(self.source, self.dest)
  33. self.assertTrue(result['readOnly']); self.assertFalse(self.backup.exists())
  34. self.assertEqual(before, {p.name: p.read_bytes() for p in self.dest.iterdir()})
  35. result = installer.install(self.source, self.dest, True, self.backup)
  36. self.assertFalse(result['gatewayRestarted']); self.assertFalse(result['identityStateChanged'])
  37. self.assertEqual(result['businessWrites'], 0)
  38. for name, data in before.items():
  39. self.assertEqual((self.backup / name).read_bytes(), data)
  40. self.assertTrue(all(x['action'] == 'unchanged' for x in installer.install(self.source, self.dest, True)['files']))
  41. def test_unknown_version_and_backup_collision_stop_before_writes(self):
  42. first = self.dest / 'identity_bridge.py'; first.write_text('external-change', encoding='utf-8')
  43. with self.assertRaisesRegex(installer.PatchError, 'UNKNOWN_TARGET_VERSION'):
  44. installer.install(self.source, self.dest, True, self.backup)
  45. self.assertFalse(self.backup.exists())
  46. def test_failure_rolls_back_and_preserves_concurrent_external_change(self):
  47. atomic = installer.atomic; count = 0
  48. def fail_second(path, data, mode, uid, gid):
  49. nonlocal count
  50. count += 1
  51. if count == 2:
  52. raise OSError('fixture')
  53. atomic(path, data, mode, uid, gid)
  54. with patch.object(installer, 'atomic', side_effect=fail_second):
  55. with self.assertRaises(installer.PatchError) as caught:
  56. installer.install(self.source, self.dest, True, self.backup)
  57. self.assertTrue(caught.exception.receipt['rollbackComplete'])
  58. for name in installer.BEFORE:
  59. self.assertEqual(installer.digest((self.dest / name).read_bytes()), installer.BEFORE[name])
  60. count = 0
  61. def fail_with_concurrent_change(path, data, mode, uid, gid):
  62. nonlocal count
  63. count += 1
  64. if count == 2:
  65. (self.dest / 'workshop_registration.py').write_text('external-owner-edit', encoding='utf-8')
  66. raise OSError('fixture')
  67. atomic(path, data, mode, uid, gid)
  68. with patch.object(installer, 'atomic', side_effect=fail_with_concurrent_change):
  69. with self.assertRaises(installer.PatchError) as caught:
  70. installer.install(self.source, self.dest, True, self.base / 'backup2')
  71. self.assertFalse(caught.exception.receipt['rollbackComplete'])
  72. self.assertEqual((self.dest / 'workshop_registration.py').read_text(), 'external-owner-edit')
  73. if __name__ == '__main__':
  74. unittest.main()