eventsourcing 9.2.22__py3-none-any.whl → 9.3.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of eventsourcing might be problematic. Click here for more details.

Files changed (144) hide show
  1. eventsourcing/__init__.py +1 -1
  2. eventsourcing/application.py +106 -135
  3. eventsourcing/cipher.py +15 -12
  4. eventsourcing/dispatch.py +31 -91
  5. eventsourcing/domain.py +138 -143
  6. eventsourcing/examples/__init__.py +0 -0
  7. eventsourcing/examples/aggregate1/__init__.py +0 -0
  8. eventsourcing/examples/aggregate1/application.py +27 -0
  9. eventsourcing/examples/aggregate1/domainmodel.py +16 -0
  10. eventsourcing/examples/aggregate1/test_application.py +37 -0
  11. eventsourcing/examples/aggregate2/__init__.py +0 -0
  12. eventsourcing/examples/aggregate2/application.py +27 -0
  13. eventsourcing/examples/aggregate2/domainmodel.py +22 -0
  14. eventsourcing/examples/aggregate2/test_application.py +37 -0
  15. eventsourcing/examples/aggregate3/__init__.py +0 -0
  16. eventsourcing/examples/aggregate3/application.py +27 -0
  17. eventsourcing/examples/aggregate3/domainmodel.py +38 -0
  18. eventsourcing/examples/aggregate3/test_application.py +37 -0
  19. eventsourcing/examples/aggregate4/__init__.py +0 -0
  20. eventsourcing/examples/aggregate4/application.py +27 -0
  21. eventsourcing/examples/aggregate4/domainmodel.py +128 -0
  22. eventsourcing/examples/aggregate4/test_application.py +38 -0
  23. eventsourcing/examples/aggregate5/__init__.py +0 -0
  24. eventsourcing/examples/aggregate5/application.py +27 -0
  25. eventsourcing/examples/aggregate5/domainmodel.py +131 -0
  26. eventsourcing/examples/aggregate5/test_application.py +38 -0
  27. eventsourcing/examples/aggregate6/__init__.py +0 -0
  28. eventsourcing/examples/aggregate6/application.py +30 -0
  29. eventsourcing/examples/aggregate6/domainmodel.py +123 -0
  30. eventsourcing/examples/aggregate6/test_application.py +38 -0
  31. eventsourcing/examples/aggregate6a/__init__.py +0 -0
  32. eventsourcing/examples/aggregate6a/application.py +40 -0
  33. eventsourcing/examples/aggregate6a/domainmodel.py +149 -0
  34. eventsourcing/examples/aggregate6a/test_application.py +45 -0
  35. eventsourcing/examples/aggregate7/__init__.py +0 -0
  36. eventsourcing/examples/aggregate7/application.py +48 -0
  37. eventsourcing/examples/aggregate7/domainmodel.py +144 -0
  38. eventsourcing/examples/aggregate7/persistence.py +57 -0
  39. eventsourcing/examples/aggregate7/test_application.py +38 -0
  40. eventsourcing/examples/aggregate7/test_compression_and_encryption.py +45 -0
  41. eventsourcing/examples/aggregate7/test_snapshotting_intervals.py +67 -0
  42. eventsourcing/examples/aggregate7a/__init__.py +0 -0
  43. eventsourcing/examples/aggregate7a/application.py +56 -0
  44. eventsourcing/examples/aggregate7a/domainmodel.py +170 -0
  45. eventsourcing/examples/aggregate7a/test_application.py +46 -0
  46. eventsourcing/examples/aggregate7a/test_compression_and_encryption.py +45 -0
  47. eventsourcing/examples/aggregate8/__init__.py +0 -0
  48. eventsourcing/examples/aggregate8/application.py +47 -0
  49. eventsourcing/examples/aggregate8/domainmodel.py +65 -0
  50. eventsourcing/examples/aggregate8/persistence.py +57 -0
  51. eventsourcing/examples/aggregate8/test_application.py +37 -0
  52. eventsourcing/examples/aggregate8/test_compression_and_encryption.py +44 -0
  53. eventsourcing/examples/aggregate8/test_snapshotting_intervals.py +38 -0
  54. eventsourcing/examples/bankaccounts/__init__.py +0 -0
  55. eventsourcing/examples/bankaccounts/application.py +70 -0
  56. eventsourcing/examples/bankaccounts/domainmodel.py +56 -0
  57. eventsourcing/examples/bankaccounts/test.py +173 -0
  58. eventsourcing/examples/cargoshipping/__init__.py +0 -0
  59. eventsourcing/examples/cargoshipping/application.py +126 -0
  60. eventsourcing/examples/cargoshipping/domainmodel.py +330 -0
  61. eventsourcing/examples/cargoshipping/interface.py +143 -0
  62. eventsourcing/examples/cargoshipping/test.py +231 -0
  63. eventsourcing/examples/contentmanagement/__init__.py +0 -0
  64. eventsourcing/examples/contentmanagement/application.py +118 -0
  65. eventsourcing/examples/contentmanagement/domainmodel.py +69 -0
  66. eventsourcing/examples/contentmanagement/test.py +180 -0
  67. eventsourcing/examples/contentmanagement/utils.py +26 -0
  68. eventsourcing/examples/contentmanagementsystem/__init__.py +0 -0
  69. eventsourcing/examples/contentmanagementsystem/application.py +54 -0
  70. eventsourcing/examples/contentmanagementsystem/postgres.py +17 -0
  71. eventsourcing/examples/contentmanagementsystem/sqlite.py +17 -0
  72. eventsourcing/examples/contentmanagementsystem/system.py +14 -0
  73. eventsourcing/examples/contentmanagementsystem/test_system.py +174 -0
  74. eventsourcing/examples/searchablecontent/__init__.py +0 -0
  75. eventsourcing/examples/searchablecontent/application.py +45 -0
  76. eventsourcing/examples/searchablecontent/persistence.py +23 -0
  77. eventsourcing/examples/searchablecontent/postgres.py +118 -0
  78. eventsourcing/examples/searchablecontent/sqlite.py +136 -0
  79. eventsourcing/examples/searchablecontent/test_application.py +111 -0
  80. eventsourcing/examples/searchablecontent/test_recorder.py +69 -0
  81. eventsourcing/examples/searchabletimestamps/__init__.py +0 -0
  82. eventsourcing/examples/searchabletimestamps/application.py +32 -0
  83. eventsourcing/examples/searchabletimestamps/persistence.py +20 -0
  84. eventsourcing/examples/searchabletimestamps/postgres.py +110 -0
  85. eventsourcing/examples/searchabletimestamps/sqlite.py +99 -0
  86. eventsourcing/examples/searchabletimestamps/test_searchabletimestamps.py +91 -0
  87. eventsourcing/examples/test_invoice.py +176 -0
  88. eventsourcing/examples/test_parking_lot.py +206 -0
  89. eventsourcing/interface.py +2 -2
  90. eventsourcing/persistence.py +85 -81
  91. eventsourcing/popo.py +30 -31
  92. eventsourcing/postgres.py +361 -578
  93. eventsourcing/sqlite.py +91 -99
  94. eventsourcing/system.py +42 -57
  95. eventsourcing/tests/application.py +20 -32
  96. eventsourcing/tests/application_tests/__init__.py +0 -0
  97. eventsourcing/tests/application_tests/test_application_with_automatic_snapshotting.py +55 -0
  98. eventsourcing/tests/application_tests/test_application_with_popo.py +22 -0
  99. eventsourcing/tests/application_tests/test_application_with_postgres.py +75 -0
  100. eventsourcing/tests/application_tests/test_application_with_sqlite.py +72 -0
  101. eventsourcing/tests/application_tests/test_cache.py +134 -0
  102. eventsourcing/tests/application_tests/test_event_sourced_log.py +162 -0
  103. eventsourcing/tests/application_tests/test_notificationlog.py +232 -0
  104. eventsourcing/tests/application_tests/test_notificationlogreader.py +126 -0
  105. eventsourcing/tests/application_tests/test_processapplication.py +110 -0
  106. eventsourcing/tests/application_tests/test_processingpolicy.py +109 -0
  107. eventsourcing/tests/application_tests/test_repository.py +504 -0
  108. eventsourcing/tests/application_tests/test_snapshotting.py +68 -0
  109. eventsourcing/tests/application_tests/test_upcasting.py +459 -0
  110. eventsourcing/tests/docs_tests/__init__.py +0 -0
  111. eventsourcing/tests/docs_tests/test_docs.py +293 -0
  112. eventsourcing/tests/domain.py +1 -1
  113. eventsourcing/tests/domain_tests/__init__.py +0 -0
  114. eventsourcing/tests/domain_tests/test_aggregate.py +1159 -0
  115. eventsourcing/tests/domain_tests/test_aggregate_decorators.py +1604 -0
  116. eventsourcing/tests/domain_tests/test_domainevent.py +80 -0
  117. eventsourcing/tests/interface_tests/__init__.py +0 -0
  118. eventsourcing/tests/interface_tests/test_remotenotificationlog.py +258 -0
  119. eventsourcing/tests/persistence.py +49 -50
  120. eventsourcing/tests/persistence_tests/__init__.py +0 -0
  121. eventsourcing/tests/persistence_tests/test_aes.py +93 -0
  122. eventsourcing/tests/persistence_tests/test_connection_pool.py +722 -0
  123. eventsourcing/tests/persistence_tests/test_eventstore.py +72 -0
  124. eventsourcing/tests/persistence_tests/test_infrastructure_factory.py +21 -0
  125. eventsourcing/tests/persistence_tests/test_mapper.py +113 -0
  126. eventsourcing/tests/persistence_tests/test_noninterleaving_notification_ids.py +69 -0
  127. eventsourcing/tests/persistence_tests/test_popo.py +124 -0
  128. eventsourcing/tests/persistence_tests/test_postgres.py +1121 -0
  129. eventsourcing/tests/persistence_tests/test_sqlite.py +348 -0
  130. eventsourcing/tests/persistence_tests/test_transcoder.py +44 -0
  131. eventsourcing/tests/postgres_utils.py +7 -7
  132. eventsourcing/tests/system_tests/__init__.py +0 -0
  133. eventsourcing/tests/system_tests/test_runner.py +935 -0
  134. eventsourcing/tests/system_tests/test_system.py +287 -0
  135. eventsourcing/tests/utils_tests/__init__.py +0 -0
  136. eventsourcing/tests/utils_tests/test_utils.py +226 -0
  137. eventsourcing/utils.py +47 -50
  138. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0a1.dist-info}/METADATA +28 -80
  139. eventsourcing-9.3.0a1.dist-info/RECORD +144 -0
  140. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0a1.dist-info}/WHEEL +1 -2
  141. eventsourcing-9.2.22.dist-info/AUTHORS +0 -10
  142. eventsourcing-9.2.22.dist-info/RECORD +0 -25
  143. eventsourcing-9.2.22.dist-info/top_level.txt +0 -1
  144. {eventsourcing-9.2.22.dist-info → eventsourcing-9.3.0a1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,1121 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from threading import Event, Thread
5
+ from time import sleep
6
+ from typing import List
7
+ from unittest import TestCase, skipIf
8
+ from uuid import uuid4
9
+
10
+ import psycopg
11
+ from psycopg import Connection
12
+ from psycopg_pool import ConnectionPool
13
+
14
+ from eventsourcing.persistence import (
15
+ DatabaseError,
16
+ DataError,
17
+ InfrastructureFactory,
18
+ IntegrityError,
19
+ InterfaceError,
20
+ InternalError,
21
+ NotSupportedError,
22
+ OperationalError,
23
+ PersistenceError,
24
+ ProgrammingError,
25
+ StoredEvent,
26
+ Tracking,
27
+ )
28
+ from eventsourcing.postgres import (
29
+ Factory,
30
+ PostgresAggregateRecorder,
31
+ PostgresApplicationRecorder,
32
+ PostgresDatastore,
33
+ PostgresProcessRecorder,
34
+ )
35
+ from eventsourcing.tests.persistence import (
36
+ AggregateRecorderTestCase,
37
+ ApplicationRecorderTestCase,
38
+ InfrastructureFactoryTestCase,
39
+ ProcessRecorderTestCase,
40
+ )
41
+ from eventsourcing.tests.persistence_tests.test_connection_pool import (
42
+ TestConnectionPool,
43
+ )
44
+ from eventsourcing.tests.postgres_utils import (
45
+ drop_postgres_table,
46
+ pg_close_all_connections,
47
+ )
48
+ from eventsourcing.utils import Environment, get_topic
49
+
50
+
51
+ class TestPostgresDatastore(TestCase):
52
+ def test_is_pipeline_supported(self):
53
+ self.assertTrue(psycopg.Pipeline.is_supported())
54
+
55
+ def test_has_connection_pool(self):
56
+ datastore = PostgresDatastore(
57
+ dbname="eventsourcing",
58
+ host="127.0.0.1",
59
+ port="5432",
60
+ user="eventsourcing",
61
+ password="eventsourcing", # noqa: S106
62
+ )
63
+ self.assertIsInstance(datastore.pool, ConnectionPool)
64
+
65
+ def test_get_connection(self):
66
+ datastore = PostgresDatastore(
67
+ dbname="eventsourcing",
68
+ host="127.0.0.1",
69
+ port="5432",
70
+ user="eventsourcing",
71
+ password="eventsourcing", # noqa: S106
72
+ )
73
+ conn: Connection
74
+ with datastore.get_connection() as conn:
75
+ self.assertIsInstance(conn, Connection)
76
+
77
+ def test_context_manager_converts_exceptions_and_conditionally_calls_close(self):
78
+ cases = [
79
+ (InterfaceError, psycopg.InterfaceError(), True),
80
+ (DataError, psycopg.DataError(), False),
81
+ (OperationalError, psycopg.OperationalError(), True),
82
+ (IntegrityError, psycopg.IntegrityError(), False),
83
+ (InternalError, psycopg.InternalError(), False),
84
+ (ProgrammingError, psycopg.ProgrammingError(), False),
85
+ (NotSupportedError, psycopg.NotSupportedError(), False),
86
+ (DatabaseError, psycopg.DatabaseError(), False),
87
+ (PersistenceError, psycopg.Error(), True),
88
+ (TypeError, TypeError(), True),
89
+ (TypeError, TypeError, True),
90
+ ]
91
+ datastore = PostgresDatastore(
92
+ dbname="eventsourcing",
93
+ host="127.0.0.1",
94
+ port="5432",
95
+ user="eventsourcing",
96
+ password="eventsourcing", # noqa: S106
97
+ )
98
+ for expected_exc_type, raised_exc, expect_conn_closed in cases:
99
+ with self.assertRaises(expected_exc_type):
100
+ conn: Connection
101
+ with datastore.get_connection() as conn:
102
+ self.assertFalse(conn.closed)
103
+ raise raised_exc
104
+ self.assertTrue(conn.closed is expect_conn_closed, raised_exc)
105
+
106
+ def test_transaction_from_datastore(self):
107
+ datastore = PostgresDatastore(
108
+ dbname="eventsourcing",
109
+ host="127.0.0.1",
110
+ port="5432",
111
+ user="eventsourcing",
112
+ password="eventsourcing", # noqa: S106
113
+ )
114
+ # As a convenience, we can use the transaction() method.
115
+ with datastore.transaction(commit=False) as curs:
116
+ curs.execute("SELECT 1")
117
+ self.assertEqual(curs.fetchall(), [{"?column?": 1}])
118
+
119
+ def test_connect_failure_raises_operational_error(self):
120
+ datastore = PostgresDatastore(
121
+ dbname="eventsourcing",
122
+ host="127.0.0.1",
123
+ port="4321", # wrong port
124
+ user="eventsourcing",
125
+ password="eventsourcing", # noqa: S106
126
+ pool_open_timeout=2,
127
+ )
128
+ with self.assertRaises(OperationalError), datastore.get_connection():
129
+ pass
130
+
131
+ datastore = PostgresDatastore(
132
+ dbname="eventsourcing",
133
+ host="127.0.0.1",
134
+ port="987654321", # bad value
135
+ user="eventsourcing",
136
+ password="eventsourcing", # noqa: S106
137
+ pool_open_timeout=2,
138
+ )
139
+ with self.assertRaises(OperationalError), datastore.get_connection():
140
+ pass
141
+
142
+ @skipIf(
143
+ sys.version_info[:2] < (3, 8),
144
+ "The 'check' argument and the check_connection() method aren't supported.",
145
+ )
146
+ def test_pre_ping(self):
147
+ # Define method to open and close a connection, and then execute a statement.
148
+ def open_close_execute(*, pre_ping: bool):
149
+ datastore = PostgresDatastore(
150
+ dbname="eventsourcing",
151
+ host="127.0.0.1",
152
+ port="5432",
153
+ user="eventsourcing",
154
+ password="eventsourcing", # noqa: S106
155
+ pool_size=1,
156
+ pre_ping=pre_ping,
157
+ )
158
+
159
+ # Create a connection.
160
+ conn: Connection
161
+ with datastore.get_connection() as conn, conn.cursor() as curs:
162
+ curs.execute("SELECT 1")
163
+ self.assertEqual(curs.fetchall(), [{"?column?": 1}])
164
+
165
+ # Close all connections via separate connection.
166
+ pg_close_all_connections()
167
+
168
+ # Check the connection doesn't think it's closed.
169
+ self.assertTrue(datastore.pool._pool)
170
+ self.assertFalse(datastore.pool._pool[0].closed)
171
+
172
+ # Get a closed connection.
173
+ conn: Connection
174
+ with datastore.get_connection() as conn:
175
+ self.assertFalse(conn.closed)
176
+
177
+ with conn.cursor() as curs:
178
+ curs.execute("SELECT 1")
179
+
180
+ # Check using the closed connection gives an error.
181
+ with self.assertRaises(OperationalError):
182
+ open_close_execute(pre_ping=False)
183
+
184
+ # Now try that again with pre-ping enabled.
185
+ open_close_execute(pre_ping=True)
186
+
187
+ def test_idle_in_transaction_session_timeout(self):
188
+ datastore = PostgresDatastore(
189
+ dbname="eventsourcing",
190
+ host="127.0.0.1",
191
+ port="5432",
192
+ user="eventsourcing",
193
+ password="eventsourcing", # noqa: S106
194
+ idle_in_transaction_session_timeout=1,
195
+ )
196
+
197
+ # Error on commit is raised.
198
+ with self.assertRaises(OperationalError), datastore.get_connection() as curs:
199
+ curs.execute("BEGIN")
200
+ curs.execute("SELECT 1")
201
+ self.assertFalse(curs.closed)
202
+ sleep(2)
203
+
204
+ # Error on commit is raised.
205
+ with self.assertRaises(OperationalError), datastore.transaction(
206
+ commit=True
207
+ ) as curs:
208
+ # curs.execute("BEGIN")
209
+ curs.execute("SELECT 1")
210
+ self.assertFalse(curs.closed)
211
+ sleep(2)
212
+
213
+ # Force rollback. Error is ignored.
214
+ with datastore.transaction(commit=False) as curs:
215
+ # curs.execute("BEGIN")
216
+ curs.execute("SELECT 1")
217
+ self.assertFalse(curs.closed)
218
+ sleep(2)
219
+
220
+ # Autocommit mode - transaction is commited in time.
221
+ with datastore.get_connection() as curs:
222
+ curs.execute("SELECT 1")
223
+ self.assertFalse(curs.closed)
224
+ sleep(2)
225
+
226
+ def test_get_password_func(self):
227
+ # Check correct password is required, wrong password causes operational error.
228
+ datastore = PostgresDatastore(
229
+ dbname="eventsourcing",
230
+ host="127.0.0.1",
231
+ port="5432",
232
+ user="eventsourcing",
233
+ password="wrong", # noqa: S106
234
+ pool_size=1,
235
+ )
236
+
237
+ conn: Connection
238
+ with self.assertRaises(
239
+ OperationalError
240
+ ), datastore.get_connection() as conn, conn.cursor() as curs:
241
+ curs.execute("SELECT 1")
242
+
243
+ # Define a "get password" function, with a generator that returns
244
+ # wrong password a few times first.
245
+ def password_token_generator():
246
+ yield "wrong"
247
+ yield "wrong"
248
+ yield "eventsourcing"
249
+
250
+ password_generator = password_token_generator()
251
+
252
+ def get_password_func():
253
+ return next(password_generator)
254
+
255
+ # Construct datastore with "get password" function.
256
+ datastore = PostgresDatastore(
257
+ dbname="eventsourcing",
258
+ host="127.0.0.1",
259
+ port="5432",
260
+ user="eventsourcing",
261
+ password="",
262
+ pool_size=1,
263
+ get_password_func=get_password_func,
264
+ connect_timeout=10,
265
+ )
266
+
267
+ # Create a connection, and check it works (this test depends on psycopg
268
+ # retrying attempt to connect, should call "get password" twice).
269
+ with datastore.get_connection() as conn, conn.cursor() as curs:
270
+ curs.execute("SELECT 1")
271
+ self.assertEqual(curs.fetchall(), [{"?column?": 1}])
272
+
273
+
274
+ # Use maximally long identifier for table name.
275
+ EVENTS_TABLE_NAME = "s" * 50 + "stored_events"
276
+
277
+ MAX_IDENTIFIER_LEN = 63
278
+
279
+
280
+ def _check_identifier_is_max_len(identifier):
281
+ if len(identifier) != MAX_IDENTIFIER_LEN:
282
+ msg = "Expected length of name string to be max identifier length"
283
+ raise ValueError(msg)
284
+
285
+
286
+ _check_identifier_is_max_len(EVENTS_TABLE_NAME)
287
+
288
+
289
+ class SetupPostgresDatastore(TestCase):
290
+ schema = ""
291
+
292
+ def setUp(self) -> None:
293
+ super().setUp()
294
+ self.datastore = PostgresDatastore(
295
+ "eventsourcing",
296
+ "127.0.0.1",
297
+ "5432",
298
+ "eventsourcing",
299
+ "eventsourcing",
300
+ schema=self.schema,
301
+ )
302
+ self.drop_tables()
303
+
304
+ def tearDown(self) -> None:
305
+ super().tearDown()
306
+ self.drop_tables()
307
+ self.datastore.close()
308
+
309
+ def drop_tables(self):
310
+ events_table_name = EVENTS_TABLE_NAME
311
+ if self.datastore.schema:
312
+ events_table_name = f"{self.datastore.schema}.{events_table_name}"
313
+ drop_postgres_table(self.datastore, events_table_name)
314
+
315
+
316
+ class WithSchema(SetupPostgresDatastore):
317
+ schema = "myschema"
318
+
319
+ def test_datastore_has_schema(self):
320
+ self.assertEqual(self.datastore.schema, self.schema)
321
+
322
+
323
+ class TestPostgresAggregateRecorder(SetupPostgresDatastore, AggregateRecorderTestCase):
324
+ def create_recorder(
325
+ self, table_name=EVENTS_TABLE_NAME
326
+ ) -> PostgresAggregateRecorder:
327
+ if self.datastore.schema:
328
+ table_name = f"{self.datastore.schema}.{table_name}"
329
+ recorder = PostgresAggregateRecorder(
330
+ datastore=self.datastore, events_table_name=table_name
331
+ )
332
+ recorder.create_table()
333
+ return recorder
334
+
335
+ def drop_tables(self):
336
+ super().drop_tables()
337
+ drop_postgres_table(self.datastore, "stored_events")
338
+
339
+ def test_create_table(self):
340
+ recorder = PostgresAggregateRecorder(
341
+ datastore=self.datastore, events_table_name="stored_events"
342
+ )
343
+ recorder.create_table()
344
+
345
+ def test_insert_and_select(self):
346
+ super().test_insert_and_select()
347
+
348
+ def test_performance(self):
349
+ super().test_performance()
350
+
351
+ def test_retry_insert_events_after_closing_connection(self):
352
+ # This checks connection is recreated after connections are closed.
353
+ self.datastore.pool.pool_size = 1
354
+
355
+ # Construct the recorder.
356
+ recorder = self.create_recorder()
357
+
358
+ # Check we have open connections.
359
+ self.assertTrue(self.datastore.pool._pool)
360
+
361
+ # Close connections.
362
+ pg_close_all_connections()
363
+ self.assertFalse(self.datastore.pool._pool[0].closed)
364
+
365
+ # Write a stored event.
366
+ stored_event1 = StoredEvent(
367
+ originator_id=uuid4(),
368
+ originator_version=0,
369
+ topic="topic1",
370
+ state=b"state1",
371
+ )
372
+ recorder.insert_events([stored_event1])
373
+
374
+
375
+ class TestPostgresAggregateRecorderWithSchema(
376
+ WithSchema, TestPostgresAggregateRecorder
377
+ ):
378
+ pass
379
+
380
+
381
+ class TestPostgresAggregateRecorderErrors(SetupPostgresDatastore, TestCase):
382
+ def create_recorder(self, table_name=EVENTS_TABLE_NAME):
383
+ return PostgresAggregateRecorder(
384
+ datastore=self.datastore, events_table_name=table_name
385
+ )
386
+
387
+ def test_excessively_long_table_name_raises_error(self):
388
+ # Add one more character to the table name.
389
+ long_table_name = "s" + EVENTS_TABLE_NAME
390
+ self.assertEqual(len(long_table_name), 64)
391
+ with self.assertRaises(ProgrammingError):
392
+ self.create_recorder(long_table_name)
393
+
394
+ def test_create_table_raises_programming_error_when_sql_is_broken(self):
395
+ recorder = self.create_recorder()
396
+
397
+ # Mess up the statement.
398
+ recorder.create_table_statements = ["BLAH"]
399
+ with self.assertRaises(ProgrammingError):
400
+ recorder.create_table()
401
+
402
+ def test_insert_events_raises_programming_error_when_table_not_created(self):
403
+ # Construct the recorder.
404
+ recorder = self.create_recorder()
405
+
406
+ # Write a stored event without creating the table.
407
+ stored_event1 = StoredEvent(
408
+ originator_id=uuid4(),
409
+ originator_version=0,
410
+ topic="topic1",
411
+ state=b"state1",
412
+ )
413
+ with self.assertRaises(ProgrammingError):
414
+ recorder.insert_events([stored_event1])
415
+
416
+ def test_insert_events_raises_programming_error_when_sql_is_broken(self):
417
+ # Construct the recorder.
418
+ recorder = self.create_recorder()
419
+
420
+ # Create the table.
421
+ recorder.create_table()
422
+
423
+ # Write a stored event with broken statement.
424
+ recorder.insert_events_statement = "BLAH"
425
+ stored_event1 = StoredEvent(
426
+ originator_id=uuid4(),
427
+ originator_version=0,
428
+ topic="topic1",
429
+ state=b"state1",
430
+ )
431
+ with self.assertRaises(ProgrammingError):
432
+ recorder.insert_events([stored_event1])
433
+
434
+ def test_select_events_raises_programming_error_when_table_not_created(self):
435
+ # Construct the recorder.
436
+ recorder = self.create_recorder()
437
+
438
+ # Select events without creating the table.
439
+ originator_id = uuid4()
440
+ with self.assertRaises(ProgrammingError):
441
+ recorder.select_events(originator_id=originator_id)
442
+
443
+ def test_select_events_raises_programming_error_when_sql_is_broken(self):
444
+ # Construct the recorder.
445
+ recorder = self.create_recorder()
446
+
447
+ # Create the table.
448
+ recorder.create_table()
449
+
450
+ # Select events with broken statement.
451
+ recorder.select_events_statement = "BLAH"
452
+ originator_id = uuid4()
453
+ with self.assertRaises(ProgrammingError):
454
+ recorder.select_events(originator_id=originator_id)
455
+
456
+
457
+ class TestPostgresApplicationRecorder(
458
+ SetupPostgresDatastore, ApplicationRecorderTestCase
459
+ ):
460
+ def create_recorder(
461
+ self, table_name=EVENTS_TABLE_NAME
462
+ ) -> PostgresApplicationRecorder:
463
+ if self.datastore.schema:
464
+ table_name = f"{self.datastore.schema}.{table_name}"
465
+ recorder = PostgresApplicationRecorder(
466
+ self.datastore, events_table_name=table_name
467
+ )
468
+ recorder.create_table()
469
+ return recorder
470
+
471
+ def test_insert_select(self) -> None:
472
+ super().test_insert_select()
473
+
474
+ def test_concurrent_no_conflicts(self):
475
+ super().test_concurrent_no_conflicts()
476
+
477
+ def test_concurrent_throughput(self):
478
+ self.datastore.pool.pool_size = 4
479
+ super().test_concurrent_throughput()
480
+
481
+ def test_retry_select_notifications_after_closing_connection(self):
482
+ # This checks connection is recreated after InterfaceError.
483
+
484
+ # Construct the recorder.
485
+ recorder = self.create_recorder()
486
+ self.datastore.pool.pool_size = 1
487
+
488
+ # Write a stored event.
489
+ originator_id = uuid4()
490
+ stored_event1 = StoredEvent(
491
+ originator_id=originator_id,
492
+ originator_version=0,
493
+ topic="topic1",
494
+ state=b"state1",
495
+ )
496
+ recorder.insert_events([stored_event1])
497
+
498
+ # Close connections.
499
+ pg_close_all_connections()
500
+ self.assertFalse(self.datastore.pool._pool[0].closed)
501
+
502
+ # Select events.
503
+ recorder.select_notifications(start=1, limit=1)
504
+
505
+ def test_retry_max_notification_id_after_closing_connection(self):
506
+ # This checks connection is recreated after InterfaceError.
507
+
508
+ # Construct the recorder.
509
+ recorder = self.create_recorder()
510
+ self.datastore.pool.pool_size = 1
511
+
512
+ # Write a stored event.
513
+ originator_id = uuid4()
514
+ stored_event1 = StoredEvent(
515
+ originator_id=originator_id,
516
+ originator_version=0,
517
+ topic="topic1",
518
+ state=b"state1",
519
+ )
520
+ recorder.insert_events([stored_event1])
521
+
522
+ # Close connections.
523
+ pg_close_all_connections()
524
+ self.assertFalse(self.datastore.pool._pool[0].closed)
525
+
526
+ # Get max notification ID.
527
+ recorder.max_notification_id()
528
+
529
+ def test_insert_lock_timeout_actually_works(self):
530
+ self.datastore.lock_timeout = 1
531
+ recorder: PostgresApplicationRecorder = self.create_recorder()
532
+
533
+ stored_event1 = StoredEvent(
534
+ originator_id=uuid4(),
535
+ originator_version=1,
536
+ topic="topic1",
537
+ state=b"state1",
538
+ )
539
+ stored_event2 = StoredEvent(
540
+ originator_id=uuid4(),
541
+ originator_version=1,
542
+ topic="topic1",
543
+ state=b"state1",
544
+ )
545
+
546
+ table_lock_acquired = Event()
547
+ test_ended = Event()
548
+ table_lock_timed_out = Event()
549
+
550
+ def insert1():
551
+ conn = self.datastore.get_connection()
552
+ with conn as conn, conn.transaction(), conn.cursor() as curs:
553
+ # Lock table.
554
+ recorder._insert_stored_events(curs, [stored_event1])
555
+ table_lock_acquired.set()
556
+ # Wait for other thread to timeout.
557
+ test_ended.wait(timeout=5) # keep the lock
558
+
559
+ def insert2():
560
+ try:
561
+ conn: Connection
562
+ with self.datastore.get_connection() as conn:
563
+ # Wait for other thread to lock table.
564
+ table_lock_acquired.wait(timeout=5)
565
+ # Expect to timeout.
566
+ with conn.transaction(), conn.cursor() as curs:
567
+ recorder._insert_stored_events(curs, [stored_event2])
568
+ except OperationalError as e:
569
+ if "lock timeout" in e.args[0]:
570
+ table_lock_timed_out.set()
571
+
572
+ thread1 = Thread(target=insert1, daemon=True)
573
+ thread1.start()
574
+ thread2 = Thread(target=insert2, daemon=True)
575
+ thread2.start()
576
+
577
+ table_lock_timed_out.wait(timeout=4)
578
+ test_ended.set()
579
+
580
+ thread1.join(timeout=10)
581
+ thread2.join(timeout=10)
582
+
583
+ self.assertTrue(table_lock_timed_out.is_set())
584
+
585
+
586
+ class TestPostgresApplicationRecorderWithSchema(
587
+ WithSchema, TestPostgresApplicationRecorder
588
+ ):
589
+ pass
590
+
591
+
592
+ class TestPostgresApplicationRecorderErrors(SetupPostgresDatastore, TestCase):
593
+ def create_recorder(self, table_name=EVENTS_TABLE_NAME):
594
+ return PostgresApplicationRecorder(self.datastore, events_table_name=table_name)
595
+
596
+ def test_excessively_long_table_name_raises_error(self):
597
+ # Add one more character to the table name.
598
+ long_table_name = "s" + EVENTS_TABLE_NAME
599
+ self.assertEqual(len(long_table_name), 64)
600
+ with self.assertRaises(ProgrammingError):
601
+ self.create_recorder(long_table_name)
602
+
603
+ def test_select_notification_raises_programming_error_when_table_not_created(self):
604
+ # Construct the recorder.
605
+ recorder = self.create_recorder()
606
+
607
+ # Select notifications without creating table.
608
+ with self.assertRaises(ProgrammingError):
609
+ recorder.select_notifications(start=1, limit=1)
610
+
611
+ def test_max_notification_id_raises_programming_error_when_table_not_created(self):
612
+ # Construct the recorder.
613
+ recorder = PostgresApplicationRecorder(
614
+ datastore=self.datastore, events_table_name=EVENTS_TABLE_NAME
615
+ )
616
+
617
+ # Select notifications without creating table.
618
+ with self.assertRaises(ProgrammingError):
619
+ recorder.max_notification_id()
620
+
621
+ def test_fetch_ids_after_insert_events(self):
622
+ def make_events() -> List[StoredEvent]:
623
+ return [
624
+ StoredEvent(
625
+ originator_id=uuid4(),
626
+ originator_version=1,
627
+ state=b"",
628
+ topic="",
629
+ )
630
+ ]
631
+
632
+ #
633
+ # Check it actually works.
634
+ recorder = PostgresApplicationRecorder(
635
+ datastore=self.datastore, events_table_name=EVENTS_TABLE_NAME
636
+ )
637
+ recorder.create_table()
638
+ max_notification_id = recorder.max_notification_id()
639
+ notification_ids = recorder.insert_events(make_events())
640
+ self.assertEqual(len(notification_ids), 1)
641
+ self.assertEqual(max_notification_id + 1, notification_ids[0])
642
+
643
+ # Events but no lock table statements.
644
+ with self.assertRaises(ProgrammingError):
645
+ recorder = PostgresApplicationRecorder(
646
+ datastore=self.datastore, events_table_name=EVENTS_TABLE_NAME
647
+ )
648
+ recorder.create_table()
649
+ recorder.lock_table_statements = []
650
+ recorder.insert_events(make_events())
651
+
652
+
653
+ TRACKING_TABLE_NAME = "n" * 42 + "notification_tracking"
654
+ _check_identifier_is_max_len(TRACKING_TABLE_NAME)
655
+
656
+
657
+ class TestPostgresProcessRecorder(SetupPostgresDatastore, ProcessRecorderTestCase):
658
+ def drop_tables(self):
659
+ super().drop_tables()
660
+ tracking_table_name = TRACKING_TABLE_NAME
661
+ if self.datastore.schema:
662
+ tracking_table_name = f"{self.datastore.schema}.{tracking_table_name}"
663
+ drop_postgres_table(self.datastore, tracking_table_name)
664
+
665
+ def create_recorder(self):
666
+ events_table_name = EVENTS_TABLE_NAME
667
+ tracking_table_name = TRACKING_TABLE_NAME
668
+ if self.datastore.schema:
669
+ events_table_name = f"{self.datastore.schema}.{events_table_name}"
670
+ if self.datastore.schema:
671
+ tracking_table_name = f"{self.datastore.schema}.{tracking_table_name}"
672
+ recorder = PostgresProcessRecorder(
673
+ datastore=self.datastore,
674
+ events_table_name=events_table_name,
675
+ tracking_table_name=tracking_table_name,
676
+ )
677
+ recorder.create_table()
678
+ return recorder
679
+
680
+ def test_insert_select(self):
681
+ super().test_insert_select()
682
+
683
+ def test_performance(self):
684
+ super().test_performance()
685
+
686
+ def test_excessively_long_table_names_raise_error(self):
687
+ with self.assertRaises(ProgrammingError):
688
+ PostgresProcessRecorder(
689
+ datastore=self.datastore,
690
+ events_table_name="e" + EVENTS_TABLE_NAME,
691
+ tracking_table_name=TRACKING_TABLE_NAME,
692
+ )
693
+
694
+ with self.assertRaises(ProgrammingError):
695
+ PostgresProcessRecorder(
696
+ datastore=self.datastore,
697
+ events_table_name=EVENTS_TABLE_NAME,
698
+ tracking_table_name="n" + TRACKING_TABLE_NAME,
699
+ )
700
+
701
+ def test_retry_max_tracking_id_after_closing_connection(self):
702
+ # This checks connection is recreated after InterfaceError.
703
+
704
+ # Construct the recorder.
705
+ recorder = self.create_recorder()
706
+ self.datastore.pool.pool_size = 1
707
+
708
+ # Write a tracking record.
709
+ originator_id = uuid4()
710
+ stored_event1 = StoredEvent(
711
+ originator_id=originator_id,
712
+ originator_version=0,
713
+ topic="topic1",
714
+ state=b"state1",
715
+ )
716
+ recorder.insert_events([stored_event1], tracking=Tracking("upstream", 1))
717
+
718
+ # Close connections.
719
+ pg_close_all_connections()
720
+ self.assertFalse(self.datastore.pool._pool[0].closed)
721
+
722
+ # Get max tracking ID.
723
+ notification_id = recorder.max_tracking_id("upstream")
724
+ self.assertEqual(notification_id, 1)
725
+
726
+
727
+ class TestPostgresProcessRecorderWithSchema(WithSchema, TestPostgresProcessRecorder):
728
+ pass
729
+
730
+
731
+ class TestPostgresProcessRecorderErrors(SetupPostgresDatastore, TestCase):
732
+ def drop_tables(self):
733
+ super().drop_tables()
734
+ drop_postgres_table(self.datastore, TRACKING_TABLE_NAME)
735
+
736
+ def create_recorder(self):
737
+ return PostgresProcessRecorder(
738
+ datastore=self.datastore,
739
+ events_table_name=EVENTS_TABLE_NAME,
740
+ tracking_table_name=TRACKING_TABLE_NAME,
741
+ )
742
+
743
+ def test_max_tracking_id_raises_programming_error_when_table_not_created(self):
744
+ # Construct the recorder.
745
+ recorder = self.create_recorder()
746
+
747
+ # Get max tracking ID without creating table.
748
+ with self.assertRaises(ProgrammingError):
749
+ recorder.max_tracking_id("upstream")
750
+
751
+
752
+ class TestPostgresInfrastructureFactory(InfrastructureFactoryTestCase):
753
+ def test_create_application_recorder(self):
754
+ super().test_create_application_recorder()
755
+
756
+ def expected_factory_class(self):
757
+ return Factory
758
+
759
+ def expected_aggregate_recorder_class(self):
760
+ return PostgresAggregateRecorder
761
+
762
+ def expected_application_recorder_class(self):
763
+ return PostgresApplicationRecorder
764
+
765
+ def expected_process_recorder_class(self):
766
+ return PostgresProcessRecorder
767
+
768
+ def setUp(self) -> None:
769
+ self.env = Environment("TestCase")
770
+ self.env[InfrastructureFactory.PERSISTENCE_MODULE] = Factory.__module__
771
+ self.env[Factory.POSTGRES_DBNAME] = "eventsourcing"
772
+ self.env[Factory.POSTGRES_HOST] = "127.0.0.1"
773
+ self.env[Factory.POSTGRES_PORT] = "5432"
774
+ self.env[Factory.POSTGRES_USER] = "eventsourcing"
775
+ self.env[Factory.POSTGRES_PASSWORD] = "eventsourcing"
776
+ self.drop_tables()
777
+ super().setUp()
778
+
779
+ def tearDown(self) -> None:
780
+ self.drop_tables()
781
+ super().tearDown()
782
+
783
+ def drop_tables(self):
784
+ datastore = PostgresDatastore(
785
+ "eventsourcing",
786
+ "127.0.0.1",
787
+ "5432",
788
+ "eventsourcing",
789
+ "eventsourcing",
790
+ )
791
+ drop_postgres_table(datastore, "testcase_events")
792
+ drop_postgres_table(datastore, "testcase_tracking")
793
+
794
+ def test_close(self):
795
+ factory = Factory(self.env)
796
+ conn: Connection
797
+ with factory.datastore.get_connection() as conn:
798
+ conn.execute("SELECT 1")
799
+ self.assertFalse(factory.datastore.pool.closed)
800
+ factory.close()
801
+ self.assertTrue(factory.datastore.pool.closed)
802
+
803
+ def test_conn_max_age_is_set_to_float(self):
804
+ self.env[Factory.POSTGRES_CONN_MAX_AGE] = ""
805
+ self.factory = Factory(self.env)
806
+ self.assertEqual(self.factory.datastore.pool.max_lifetime, 60 * 60.0)
807
+
808
+ def test_conn_max_age_is_set_to_number(self):
809
+ self.env[Factory.POSTGRES_CONN_MAX_AGE] = "0"
810
+ self.factory = Factory(self.env)
811
+ self.assertEqual(self.factory.datastore.pool.max_lifetime, 0)
812
+
813
+ def test_pool_size_is_five_by_default(self):
814
+ self.assertTrue(Factory.POSTGRES_POOL_SIZE not in self.env)
815
+ self.factory = Factory(self.env)
816
+ self.assertEqual(self.factory.datastore.pool.min_size, 5)
817
+
818
+ self.env[Factory.POSTGRES_POOL_SIZE] = ""
819
+ self.factory = Factory(self.env)
820
+ self.assertEqual(self.factory.datastore.pool.min_size, 5)
821
+
822
+ def test_max_overflow_is_ten_by_default(self):
823
+ self.assertTrue(Factory.POSTGRES_POOL_MAX_OVERFLOW not in self.env)
824
+ self.factory = Factory(self.env)
825
+ self.assertEqual(self.factory.datastore.pool.max_size, 15)
826
+
827
+ self.env[Factory.POSTGRES_POOL_MAX_OVERFLOW] = ""
828
+ self.factory = Factory(self.env)
829
+ self.assertEqual(self.factory.datastore.pool.max_size, 15)
830
+
831
+ def test_max_overflow_is_set(self):
832
+ self.env[Factory.POSTGRES_POOL_MAX_OVERFLOW] = "7"
833
+ self.factory = Factory(self.env)
834
+ self.assertEqual(self.factory.datastore.pool.max_size, 12)
835
+
836
+ def test_pool_size_is_set(self):
837
+ self.env[Factory.POSTGRES_POOL_SIZE] = "6"
838
+ self.factory = Factory(self.env)
839
+ self.assertEqual(self.factory.datastore.pool.min_size, 6)
840
+
841
+ def test_connect_timeout_is_five_by_default(self):
842
+ self.assertTrue(Factory.POSTGRES_CONNECT_TIMEOUT not in self.env)
843
+ self.factory = Factory(self.env)
844
+ self.assertEqual(self.factory.datastore.pool.timeout, 5)
845
+
846
+ self.env[Factory.POSTGRES_CONNECT_TIMEOUT] = ""
847
+ self.factory = Factory(self.env)
848
+ self.assertEqual(self.factory.datastore.pool.timeout, 5)
849
+
850
+ def test_connect_timeout_is_set(self):
851
+ self.env[Factory.POSTGRES_CONNECT_TIMEOUT] = "8"
852
+ self.factory = Factory(self.env)
853
+ self.assertEqual(self.factory.datastore.pool.timeout, 8)
854
+
855
+ def test_pool_timeout_is_30_by_default(self):
856
+ self.assertTrue(Factory.POSTGRES_POOL_TIMEOUT not in self.env)
857
+ self.factory = Factory(self.env)
858
+ self.assertEqual(self.factory.datastore.pool.max_waiting, 30)
859
+
860
+ self.env[Factory.POSTGRES_POOL_TIMEOUT] = ""
861
+ self.factory = Factory(self.env)
862
+ self.assertEqual(self.factory.datastore.pool.max_waiting, 30)
863
+
864
+ def test_pool_timeout_is_set(self):
865
+ self.env[Factory.POSTGRES_POOL_TIMEOUT] = "8"
866
+ self.factory = Factory(self.env)
867
+ self.assertEqual(self.factory.datastore.pool.max_waiting, 8)
868
+
869
+ def test_lock_timeout_is_zero_by_default(self):
870
+ self.assertTrue(Factory.POSTGRES_LOCK_TIMEOUT not in self.env)
871
+ self.factory = Factory(self.env)
872
+ self.assertEqual(self.factory.datastore.lock_timeout, 0)
873
+
874
+ self.env[Factory.POSTGRES_LOCK_TIMEOUT] = ""
875
+ self.factory = Factory(self.env)
876
+ self.assertEqual(self.factory.datastore.lock_timeout, 0)
877
+
878
+ def test_lock_timeout_is_set(self):
879
+ self.env[Factory.POSTGRES_LOCK_TIMEOUT] = "1"
880
+ self.factory = Factory(self.env)
881
+ self.assertEqual(self.factory.datastore.lock_timeout, 1)
882
+
883
+ def test_idle_in_transaction_session_timeout_is_5_by_default(self):
884
+ self.assertTrue(
885
+ Factory.POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT not in self.env
886
+ )
887
+ self.factory = Factory(self.env)
888
+ self.assertEqual(self.factory.datastore.idle_in_transaction_session_timeout, 5)
889
+ self.factory.close()
890
+
891
+ self.env[Factory.POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT] = ""
892
+ self.factory = Factory(self.env)
893
+ self.assertEqual(self.factory.datastore.idle_in_transaction_session_timeout, 5)
894
+
895
+ def test_idle_in_transaction_session_timeout_is_set(self):
896
+ self.env[Factory.POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT] = "10"
897
+ self.factory = Factory(self.env)
898
+ self.assertEqual(self.factory.datastore.idle_in_transaction_session_timeout, 10)
899
+
900
+ def test_pre_ping_off_by_default(self):
901
+ self.factory = Factory(self.env)
902
+ self.assertEqual(self.factory.datastore.pre_ping, False)
903
+
904
+ def test_pre_ping_off(self):
905
+ self.env[Factory.POSTGRES_PRE_PING] = "off"
906
+ self.factory = Factory(self.env)
907
+ self.assertEqual(self.factory.datastore.pre_ping, False)
908
+
909
+ def test_pre_ping_on(self):
910
+ self.env[Factory.POSTGRES_PRE_PING] = "on"
911
+ self.factory = Factory(self.env)
912
+ self.assertEqual(self.factory.datastore.pre_ping, True)
913
+
914
+ def test_get_password_topic_not_set(self):
915
+ self.factory = Factory(self.env)
916
+ self.assertIsNone(self.factory.datastore.pool.get_password_func, None)
917
+
918
+ def test_get_password_topic_set(self):
919
+ def get_password_func():
920
+ return "eventsourcing"
921
+
922
+ self.env[Factory.POSTGRES_GET_PASSWORD_TOPIC] = get_topic(get_password_func)
923
+ self.factory = Factory(self.env)
924
+ self.assertEqual(
925
+ self.factory.datastore.pool.get_password_func, get_password_func
926
+ )
927
+
928
+ def test_environment_error_raised_when_conn_max_age_not_a_float(self):
929
+ self.env[Factory.POSTGRES_CONN_MAX_AGE] = "abc"
930
+ with self.assertRaises(EnvironmentError) as cm:
931
+ Factory(self.env)
932
+ self.assertEqual(
933
+ cm.exception.args[0],
934
+ "Postgres environment value for key 'POSTGRES_CONN_MAX_AGE' "
935
+ "is invalid. If set, a float or empty string is expected: 'abc'",
936
+ )
937
+
938
+ def test_environment_error_raised_when_connect_timeout_not_an_integer(self):
939
+ self.env[Factory.POSTGRES_CONNECT_TIMEOUT] = "abc"
940
+ with self.assertRaises(EnvironmentError) as cm:
941
+ Factory(self.env)
942
+ self.assertEqual(
943
+ cm.exception.args[0],
944
+ "Postgres environment value for key 'POSTGRES_CONNECT_TIMEOUT' "
945
+ "is invalid. If set, an integer or empty string is expected: 'abc'",
946
+ )
947
+
948
+ def test_environment_error_raised_when_pool_timeout_not_an_integer(self):
949
+ self.env[Factory.POSTGRES_POOL_TIMEOUT] = "abc"
950
+ with self.assertRaises(EnvironmentError) as cm:
951
+ Factory(self.env)
952
+ self.assertEqual(
953
+ cm.exception.args[0],
954
+ "Postgres environment value for key 'POSTGRES_POOL_TIMEOUT' "
955
+ "is invalid. If set, a float or empty string is expected: 'abc'",
956
+ )
957
+
958
+ def test_environment_error_raised_when_lock_timeout_not_an_integer(self):
959
+ self.env[Factory.POSTGRES_LOCK_TIMEOUT] = "abc"
960
+ with self.assertRaises(EnvironmentError) as cm:
961
+ Factory(self.env)
962
+ self.assertEqual(
963
+ cm.exception.args[0],
964
+ "Postgres environment value for key 'POSTGRES_LOCK_TIMEOUT' "
965
+ "is invalid. If set, an integer or empty string is expected: 'abc'",
966
+ )
967
+
968
+ def test_environment_error_raised_when_min_conn_not_an_integer(self):
969
+ self.env[Factory.POSTGRES_POOL_SIZE] = "abc"
970
+ with self.assertRaises(EnvironmentError) as cm:
971
+ Factory(self.env)
972
+ self.assertEqual(
973
+ cm.exception.args[0],
974
+ "Postgres environment value for key 'POSTGRES_POOL_SIZE' "
975
+ "is invalid. If set, an integer or empty string is expected: 'abc'",
976
+ )
977
+
978
+ def test_environment_error_raised_when_max_conn_not_an_integer(self):
979
+ self.env[Factory.POSTGRES_POOL_MAX_OVERFLOW] = "abc"
980
+ with self.assertRaises(EnvironmentError) as cm:
981
+ Factory(self.env)
982
+ self.assertEqual(
983
+ cm.exception.args[0],
984
+ "Postgres environment value for key 'POSTGRES_POOL_MAX_OVERFLOW' "
985
+ "is invalid. If set, an integer or empty string is expected: 'abc'",
986
+ )
987
+
988
+ def test_environment_error_raised_when_idle_in_transaction_session_timeout_not_int(
989
+ self,
990
+ ):
991
+ self.env[Factory.POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT] = "abc"
992
+ with self.assertRaises(EnvironmentError) as cm:
993
+ Factory(self.env)
994
+ self.assertEqual(
995
+ cm.exception.args[0],
996
+ "Postgres environment value for key "
997
+ "'POSTGRES_IDLE_IN_TRANSACTION_SESSION_TIMEOUT' "
998
+ "is invalid. If set, an integer or empty string is expected: 'abc'",
999
+ )
1000
+
1001
+ def test_environment_error_raised_when_dbname_missing(self):
1002
+ del self.env[Factory.POSTGRES_DBNAME]
1003
+ with self.assertRaises(EnvironmentError) as cm:
1004
+ InfrastructureFactory.construct(self.env)
1005
+ self.assertEqual(
1006
+ cm.exception.args[0],
1007
+ "Postgres database name not found in environment "
1008
+ "with key 'POSTGRES_DBNAME'",
1009
+ )
1010
+
1011
+ def test_environment_error_raised_when_dbhost_missing(self):
1012
+ del self.env[Factory.POSTGRES_HOST]
1013
+ with self.assertRaises(EnvironmentError) as cm:
1014
+ InfrastructureFactory.construct(self.env)
1015
+ self.assertEqual(
1016
+ cm.exception.args[0],
1017
+ "Postgres host not found in environment with key 'POSTGRES_HOST'",
1018
+ )
1019
+
1020
+ def test_environment_error_raised_when_user_missing(self):
1021
+ del self.env[Factory.POSTGRES_USER]
1022
+ with self.assertRaises(EnvironmentError) as cm:
1023
+ InfrastructureFactory.construct(self.env)
1024
+ self.assertEqual(
1025
+ cm.exception.args[0],
1026
+ "Postgres user not found in environment with key 'POSTGRES_USER'",
1027
+ )
1028
+
1029
+ def test_environment_error_raised_when_password_missing(self):
1030
+ del self.env[Factory.POSTGRES_PASSWORD]
1031
+ with self.assertRaises(EnvironmentError) as cm:
1032
+ InfrastructureFactory.construct(self.env)
1033
+ self.assertEqual(
1034
+ cm.exception.args[0],
1035
+ "Postgres password not found in environment with key 'POSTGRES_PASSWORD'",
1036
+ )
1037
+
1038
+ def test_schema_set_to_empty_string(self):
1039
+ self.env[Factory.POSTGRES_SCHEMA] = ""
1040
+ self.factory = Factory(self.env)
1041
+ self.assertEqual(self.factory.datastore.schema, "")
1042
+
1043
+ def test_schema_set_to_whitespace(self):
1044
+ self.env[Factory.POSTGRES_SCHEMA] = " "
1045
+ self.factory = Factory(self.env)
1046
+ self.assertEqual(self.factory.datastore.schema, "")
1047
+
1048
+ def test_scheme_adjusts_table_names_on_aggregate_recorder(self):
1049
+ self.factory = Factory(self.env)
1050
+
1051
+ # Check by default the table name is not qualified.
1052
+ recorder = self.factory.aggregate_recorder("events")
1053
+ assert isinstance(recorder, PostgresAggregateRecorder)
1054
+ self.assertEqual(recorder.events_table_name, "testcase_events")
1055
+
1056
+ # Check by default the table name is not qualified.
1057
+ recorder = self.factory.aggregate_recorder("snapshots")
1058
+ assert isinstance(recorder, PostgresAggregateRecorder)
1059
+ self.assertEqual(recorder.events_table_name, "testcase_snapshots")
1060
+
1061
+ # Set schema in environment.
1062
+ self.env[Factory.POSTGRES_SCHEMA] = "public"
1063
+ self.factory = Factory(self.env)
1064
+ self.assertEqual(self.factory.datastore.schema, "public")
1065
+
1066
+ # Check by default the table name is qualified.
1067
+ recorder = self.factory.aggregate_recorder("events")
1068
+ assert isinstance(recorder, PostgresAggregateRecorder)
1069
+ self.assertEqual(recorder.events_table_name, "public.testcase_events")
1070
+
1071
+ # Check by default the table name is qualified.
1072
+ recorder = self.factory.aggregate_recorder("snapshots")
1073
+ assert isinstance(recorder, PostgresAggregateRecorder)
1074
+ self.assertEqual(recorder.events_table_name, "public.testcase_snapshots")
1075
+
1076
+ def test_scheme_adjusts_table_name_on_application_recorder(self):
1077
+ self.factory = Factory(self.env)
1078
+
1079
+ # Check by default the table name is not qualified.
1080
+ recorder = self.factory.application_recorder()
1081
+ assert isinstance(recorder, PostgresApplicationRecorder)
1082
+ self.assertEqual(recorder.events_table_name, "testcase_events")
1083
+
1084
+ # Set schema in environment.
1085
+ self.env[Factory.POSTGRES_SCHEMA] = "public"
1086
+ self.factory = Factory(self.env)
1087
+ self.assertEqual(self.factory.datastore.schema, "public")
1088
+
1089
+ # Check by default the table name is qualified.
1090
+ recorder = self.factory.application_recorder()
1091
+ assert isinstance(recorder, PostgresApplicationRecorder)
1092
+ self.assertEqual(recorder.events_table_name, "public.testcase_events")
1093
+
1094
+ def test_scheme_adjusts_table_names_on_process_recorder(self):
1095
+ self.factory = Factory(self.env)
1096
+
1097
+ # Check by default the table name is not qualified.
1098
+ recorder = self.factory.process_recorder()
1099
+ assert isinstance(recorder, PostgresProcessRecorder)
1100
+ self.assertEqual(recorder.events_table_name, "testcase_events")
1101
+ self.assertEqual(recorder.tracking_table_name, "testcase_tracking")
1102
+
1103
+ # Set schema in environment.
1104
+ self.env[Factory.POSTGRES_SCHEMA] = "public"
1105
+ self.factory = Factory(self.env)
1106
+ self.assertEqual(self.factory.datastore.schema, "public")
1107
+
1108
+ # Check by default the table name is qualified.
1109
+ recorder = self.factory.process_recorder()
1110
+ assert isinstance(recorder, PostgresProcessRecorder)
1111
+ self.assertEqual(recorder.events_table_name, "public.testcase_events")
1112
+ self.assertEqual(recorder.tracking_table_name, "public.testcase_tracking")
1113
+
1114
+
1115
+ del AggregateRecorderTestCase
1116
+ del ApplicationRecorderTestCase
1117
+ del ProcessRecorderTestCase
1118
+ del InfrastructureFactoryTestCase
1119
+ del SetupPostgresDatastore
1120
+ del WithSchema
1121
+ del TestConnectionPool