88
99import pytest
1010import sqlalchemy as sa
11- from asyncpg . exceptions import CheckViolationError , ForeignKeyViolationError
11+ import sqlalchemy . exc
1212from faker import Faker
1313from pytest_simcore .helpers .faker_factories import random_product
1414from simcore_postgres_database .models .products import products
2424
2525@pytest .fixture
2626async def connection (asyncpg_engine : AsyncEngine ) -> AsyncIterator [AsyncConnection ]:
27- async with asyncpg_engine .begin () as conn :
27+ async with asyncpg_engine .connect () as conn :
28+ isolation_level = await conn .get_isolation_level ()
29+ assert isolation_level == "READ COMMITTED"
2830 yield conn
2931
3032
3133@pytest .fixture
3234async def fake_product (connection : AsyncConnection ) -> Row :
3335 result = await connection .execute (
3436 products .insert ()
35- .values (random_product (group_id = None ))
36- .returning (sa .literal_column ("*" ))
37+ .values (random_product (name = "tip" , group_id = None ))
38+ .returning (sa .literal_column ("*" )),
3739 )
40+ await connection .commit ()
41+
42+ async with connection .begin ():
43+ result = await connection .execute (
44+ products .insert ()
45+ .values (random_product (name = "s4l" , group_id = None ))
46+ .returning (sa .literal_column ("*" )),
47+ )
48+
3849 return result .one ()
3950
4051
4152async def test_creating_product_prices (
42- connection : AsyncConnection , fake_product : Row , faker : Faker
53+ asyncpg_engine : AsyncEngine ,
54+ connection : AsyncConnection ,
55+ fake_product : Row ,
56+ faker : Faker ,
4357):
44-
4558 # a price per product
46- result = await connection .execute (
47- products_prices .insert ()
48- .values (
49- product_name = fake_product .name ,
50- usd_per_credit = 100 ,
51- comment = "PO Mr X" ,
52- stripe_price_id = faker .word (),
53- stripe_tax_rate_id = faker .word (),
59+ async with connection .begin ():
60+ result = await connection .execute (
61+ products_prices .insert ()
62+ .values (
63+ product_name = fake_product .name ,
64+ usd_per_credit = 100 ,
65+ comment = "PO Mr X" ,
66+ stripe_price_id = faker .word (),
67+ stripe_tax_rate_id = faker .word (),
68+ )
69+ .returning (sa .literal_column ("*" )),
5470 )
55- .returning (sa .literal_column ("*" ))
56- )
57- product_prices = result .one ()
58- assert product_prices
71+ got = result .one ()
72+ assert got
73+
74+ # insert still NOT commited but can read from this connection
75+ read_query = sa .select (products_prices ).where (
76+ products_prices .c .product_name == fake_product .name
77+ )
78+ result = await connection .execute (read_query )
79+ assert result .one ()._asdict () == got ._asdict ()
80+
81+ assert connection .in_transaction () is True
82+
83+ # cannot read from other connection though
84+ async with asyncpg_engine .connect () as other_connection :
85+ result = await other_connection .execute (read_query )
86+ assert result .one_or_none () is None
87+
88+ # AFTER commit
89+ assert connection .in_transaction () is False
90+ async with asyncpg_engine .connect () as yet_another_connection :
91+ result = await yet_another_connection .execute (read_query )
92+ assert result .one ()._asdict () == got ._asdict ()
5993
6094
6195async def test_non_negative_price_not_allowed (
6296 connection : AsyncConnection , fake_product : Row , faker : Faker
6397):
64- # negative price not allowed
65- with pytest .raises (CheckViolationError ) as exc_info :
98+
99+ assert not connection .in_transaction ()
100+
101+ # WRITE: negative price not allowed
102+ with pytest .raises (sqlalchemy .exc .IntegrityError ) as exc_info :
66103 await connection .execute (
67104 products_prices .insert ().values (
68105 product_name = fake_product .name ,
@@ -74,42 +111,72 @@ async def test_non_negative_price_not_allowed(
74111 )
75112
76113 assert exc_info .value
114+ assert connection .in_transaction ()
115+ await connection .rollback ()
116+ assert not connection .in_transaction ()
77117
78- # zero price is allowed
79- await connection .execute (
80- products_prices .insert ().values (
118+ # WRITE: zero price is allowed
119+ result = await connection .execute (
120+ products_prices .insert ()
121+ .values (
81122 product_name = fake_product .name ,
82123 usd_per_credit = 0 , # <----- ZERO
83124 comment = "PO Mr X" ,
84125 stripe_price_id = faker .word (),
85126 stripe_tax_rate_id = faker .word (),
86127 )
128+ .returning ("*" )
87129 )
88130
131+ assert result .one ()
132+
133+ assert connection .in_transaction ()
134+ await connection .commit ()
135+ assert not connection .in_transaction ()
136+
137+ with pytest .raises (sqlalchemy .exc .ResourceClosedError ):
138+ # can only get result once!
139+ assert result .one ()
140+
141+ # READ
142+ result = await connection .execute (sa .select (products_prices ))
143+ assert connection .in_transaction ()
144+
145+ assert result .one ()
146+ with pytest .raises (sqlalchemy .exc .ResourceClosedError ):
147+ # can only get result once!
148+ assert result .one ()
149+
89150
90151async def test_delete_price_constraints (
91152 connection : AsyncConnection , fake_product : Row , faker : Faker
92153):
93154 # products_prices
94- await connection .execute (
95- products_prices .insert ().values (
96- product_name = fake_product .name ,
97- usd_per_credit = 10 ,
98- comment = "PO Mr X" ,
99- stripe_price_id = faker .word (),
100- stripe_tax_rate_id = faker .word (),
155+ async with connection .begin ():
156+ await connection .execute (
157+ products_prices .insert ().values (
158+ product_name = fake_product .name ,
159+ usd_per_credit = 10 ,
160+ comment = "PO Mr X" ,
161+ stripe_price_id = faker .word (),
162+ stripe_tax_rate_id = faker .word (),
163+ )
101164 )
102- )
103165
166+ # BAD DELETE:
104167 # should not be able to delete a product w/o deleting price first
105- with pytest .raises (ForeignKeyViolationError ) as exc_info :
106- await connection .execute (products .delete ())
168+ async with connection .begin ():
169+ with pytest .raises (sqlalchemy .exc .IntegrityError , match = "delete" ) as exc_info :
170+ await connection .execute (products .delete ())
107171
108- assert exc_info .match ("delete" )
172+ # NOTE: that asyncpg.exceptions are converted to sqlalchemy.exc
173+ # sqlalchemy.exc.IntegrityError: (sqlalchemy.dialects.postgresql.asyncpg.IntegrityError) <class 'asyncpg.exceptions.ForeignKeyViolationError'>:
174+ assert "asyncpg.exceptions.ForeignKeyViolationError" in exc_info .value .args [0 ]
109175
110- # this is the correct way to delete
111- await connection .execute (products_prices .delete ())
112- await connection .execute (products .delete ())
176+ # GOOD DELETE: this is the correct way to delete
177+ async with connection .begin ():
178+ await connection .execute (products_prices .delete ())
179+ await connection .execute (products .delete ())
113180
114181
115182async def test_get_product_latest_price_or_none (
@@ -140,26 +207,28 @@ async def test_price_history_of_a_product(
140207 connection : AsyncConnection , fake_product : Row , faker : Faker
141208):
142209 # initial price
143- await connection .execute (
144- products_prices .insert ().values (
145- product_name = fake_product .name ,
146- usd_per_credit = 1 ,
147- comment = "PO Mr X" ,
148- stripe_price_id = faker .word (),
149- stripe_tax_rate_id = faker .word (),
210+ async with connection .begin ():
211+ await connection .execute (
212+ products_prices .insert ().values (
213+ product_name = fake_product .name ,
214+ usd_per_credit = 1 ,
215+ comment = "PO Mr X" ,
216+ stripe_price_id = faker .word (),
217+ stripe_tax_rate_id = faker .word (),
218+ )
150219 )
151- )
152220
153221 # new price
154- await connection .execute (
155- products_prices .insert ().values (
156- product_name = fake_product .name ,
157- usd_per_credit = 2 ,
158- comment = "Update by Mr X" ,
159- stripe_price_id = faker .word (),
160- stripe_tax_rate_id = faker .word (),
222+ async with connection .begin ():
223+ await connection .execute (
224+ products_prices .insert ().values (
225+ product_name = fake_product .name ,
226+ usd_per_credit = 2 ,
227+ comment = "Update by Mr X" ,
228+ stripe_price_id = faker .word (),
229+ stripe_tax_rate_id = faker .word (),
230+ )
161231 )
162- )
163232
164233 # latest is 2 USD!
165234 assert await get_product_latest_price_info_or_none (
@@ -176,23 +245,24 @@ async def test_get_product_latest_stripe_info(
176245 stripe_tax_rate_id_value = faker .word ()
177246
178247 # products_prices
179- await connection .execute (
180- products_prices .insert ().values (
181- product_name = fake_product .name ,
182- usd_per_credit = 10 ,
183- comment = "PO Mr X" ,
184- stripe_price_id = stripe_price_id_value ,
185- stripe_tax_rate_id = stripe_tax_rate_id_value ,
248+ async with connection .begin ():
249+ await connection .execute (
250+ products_prices .insert ().values (
251+ product_name = fake_product .name ,
252+ usd_per_credit = 10 ,
253+ comment = "PO Mr X" ,
254+ stripe_price_id = stripe_price_id_value ,
255+ stripe_tax_rate_id = stripe_tax_rate_id_value ,
256+ )
186257 )
187- )
258+
259+ # undefined product
260+ with pytest .raises (ValueError , match = "undefined" ):
261+ await get_product_latest_stripe_info (connection , product_name = "undefined" )
188262
189263 # defined product
190264 product_stripe_info = await get_product_latest_stripe_info (
191265 connection , product_name = fake_product .name
192266 )
193267 assert product_stripe_info [0 ] == stripe_price_id_value
194268 assert product_stripe_info [1 ] == stripe_tax_rate_id_value
195-
196- # undefined product
197- with pytest .raises (ValueError , match = "undefined" ):
198- await get_product_latest_stripe_info (connection , product_name = "undefined" )
0 commit comments