test_comments.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. """Exercise both production comment entry points and transactional flag updates."""
  2. import json
  3. import re
  4. import unittest
  5. from unittest.mock import patch
  6. from tests import test_wiedervorlage as fixture
  7. class CommentFlagsTests(unittest.TestCase):
  8. def setUp(self):
  9. self.fixture = fixture.WiedervorlageTests()
  10. self.fixture.setUp()
  11. self.addCleanup(self.fixture.doCleanups)
  12. self.db = self.fixture.db
  13. self.db.connection.execute(
  14. 'UPDATE dbo.Forderungen_Kommentar SET Aktuell_Fibu = 1, Aktuell_Abteilung = 1'
  15. )
  16. self.db.connection.execute("""
  17. INSERT INTO dbo.Forderungen_Kommentar
  18. (Client_DB, Beleg_Nr, Timestamp, Benutzer_ID, Kommentar, Aktuell_Fibu, Aktuell_Abteilung)
  19. VALUES ('1', 'R1', '2026-01-03T12:00:00', 'other', 'Second old comment', 1, 1)
  20. """)
  21. self.db.connection.commit()
  22. def snapshot(self):
  23. return self.db.connection.execute(
  24. 'SELECT * FROM dbo.Forderungen_Kommentar ORDER BY rowid'
  25. ).fetchall()
  26. def post(self, channel, day=6):
  27. if channel == 'wv':
  28. response = self.fixture.post(f'2026-11-{day:02}', begruendung='Neue Zusage')
  29. return response.status_code == 200
  30. response = self.fixture.client.post(
  31. '/app/forderungen/details/1_R1',
  32. data={'source': 'comments', '_csrf': 'csrf', 'Kommentar': 'Chat comment',
  33. 'Client_DB': '2', 'Beleg_Nr': 'R2', 'Benutzer_ID': 'intruder', 'Rolle_ID': 'FIBU'},
  34. )
  35. self.assertEqual(response.status_code, 200)
  36. result = json.loads(re.search(r'data-gc-send-result>(.*?)</script>', response.text, re.S)[1])
  37. return result['comment_saved']
  38. def assert_flags(self, before, fibu):
  39. rows = self.snapshot()
  40. self.assertEqual(len(rows), len(before) + 1)
  41. for previous, current in zip(before, rows):
  42. expected = list(previous)
  43. if previous[:2] == ('1', 'R1'):
  44. expected[5 if fibu else 6] = 0
  45. self.assertEqual(current, tuple(expected))
  46. self.assertEqual(rows[-1][:2], ('1', 'R1'))
  47. self.assertEqual(rows[-1][3], 'tester')
  48. self.assertEqual(rows[-1][5:], (int(fibu), int(not fibu)))
  49. def test_both_channels_reset_only_author_group_and_invoice(self):
  50. # Alternate roles and entry points on the same invoice, including repeated writes.
  51. for day, (channel, fibu) in enumerate(
  52. [('chat', False), ('wv', False), ('chat', True), ('wv', True), ('chat', True), ('wv', False)], 6
  53. ):
  54. with self.subTest(channel=channel, fibu=fibu):
  55. self.db.connection.execute("DELETE FROM dbo.Benutzer_Rolle WHERE Rolle_ID = 'FIBU'")
  56. if fibu:
  57. # Existing MA role remains: any FIBU assignment must qualify.
  58. self.db.connection.execute(
  59. "INSERT INTO dbo.Benutzer_Rolle VALUES ('tester', 'Tester', 'Buchhaltung', 'FIBU')"
  60. )
  61. self.db.connection.commit()
  62. before = self.snapshot()
  63. self.assertTrue(self.post(channel, day))
  64. self.assert_flags(before, fibu)
  65. def test_insert_update_role_lookup_and_commit_failures_roll_back_everything(self):
  66. for channel in ['chat', 'wv']:
  67. for failure in ['INSERT INTO dbo.Forderungen_Kommentar', 'UPDATE dbo.Forderungen_Kommentar',
  68. 'FROM dbo.Benutzer_Rolle', 'commit']:
  69. with self.subTest(channel=channel, failure=failure):
  70. before = self.snapshot()
  71. execute = self.db.execute
  72. def failing(query, values=None):
  73. if failure in str(query):
  74. raise RuntimeError('database unavailable')
  75. return execute(query, values)
  76. target = 'app.wiedervorlage' if channel == 'wv' else 'app.comment_submission'
  77. if failure == 'commit':
  78. mocked = patch.object(self.db, 'commit', side_effect=RuntimeError('commit failed'))
  79. else:
  80. mocked = patch.object(self.db, 'execute', side_effect=failing)
  81. with mocked, self.assertLogs(target, level='ERROR'):
  82. self.assertFalse(self.post(channel))
  83. self.assertEqual(self.snapshot(), before)
  84. self.assertIsNone(self.fixture.row())
  85. def test_lock_is_acquired_before_any_write_in_both_channels(self):
  86. for channel in ['chat', 'wv']:
  87. queries = []
  88. execute = self.db.execute
  89. def recording(query, values=None):
  90. queries.append(str(query))
  91. return execute(query, values)
  92. with patch.object(self.db, 'execute', side_effect=recording):
  93. self.assertTrue(self.post(channel))
  94. lock = next(i for i, sql in enumerate(queries)
  95. if 'Forderungen_Kopf WITH (UPDLOCK, HOLDLOCK)' in sql)
  96. writes = [i for i, sql in enumerate(queries) if re.match(r'\s*(INSERT|UPDATE)\b', sql)]
  97. self.assertTrue(writes)
  98. self.assertLess(lock, min(writes))
  99. def test_unchanged_date_does_not_reset_flags(self):
  100. before = self.snapshot()
  101. self.assertEqual(self.fixture.post('2026-10-15', begruendung='Text').status_code, 200)
  102. self.assertEqual(self.snapshot(), before)
  103. if __name__ == '__main__':
  104. unittest.main()