25
25
import pyramid .testing
26
26
import pytest
27
27
import stripe
28
+ import transaction
28
29
import webtest as _webtest
29
30
30
31
from jinja2 import Environment , FileSystemLoader
@@ -298,8 +299,7 @@ def mock_manifest_cache_buster():
298
299
return MockManifestCacheBuster
299
300
300
301
301
- @pytest .fixture (scope = "session" )
302
- def app_config (database ):
302
+ def get_app_config (database , nondefaults = None ):
303
303
settings = {
304
304
"warehouse.prevent_esi" : True ,
305
305
"warehouse.token" : "insecure token" ,
@@ -329,20 +329,42 @@ def app_config(database):
329
329
"statuspage.url" : "https://2p66nmmycsj3.statuspage.io" ,
330
330
"warehouse.xmlrpc.cache.url" : "redis://localhost:0/" ,
331
331
}
332
+
333
+ if nondefaults :
334
+ settings .update (nondefaults )
335
+
332
336
with mock .patch .object (config , "ManifestCacheBuster" , MockManifestCacheBuster ):
333
337
with mock .patch ("warehouse.admin.ManifestCacheBuster" , MockManifestCacheBuster ):
334
338
with mock .patch .object (static , "whitenoise_add_manifest" ):
335
339
cfg = config .configure (settings = settings )
336
340
337
- # Ensure our migrations have been ran.
341
+ # Run migrations:
342
+ # This might harmlessly run multiple times if there are several app config fixtures
343
+ # in the test session, using the same database.
338
344
alembic .command .upgrade (cfg .alembic_config (), "head" )
339
345
340
346
return cfg
341
347
342
348
343
- @pytest .fixture
344
- def db_session (app_config ):
345
- engine = app_config .registry ["sqlalchemy.engine" ]
349
+ @contextmanager
350
+ def get_db_session_for_app_config (app_config ):
351
+ """
352
+ Refactor: This helper function is designed to help fixtures yield a database
353
+ session for a particular app_config.
354
+
355
+ It needs the app_config in order to fetch the database engine that's owned
356
+ by the config.
357
+ """
358
+
359
+ # TODO: We possibly accept 2 instances of the sqlalchemy engine.
360
+ # There's a bit of circular dependencies in place:
361
+ # 1) To create a database session, we need to create an app config registry
362
+ # and read config.registry["sqlalchemy.engine"]
363
+ # 2) To create an app config registry, we need to be able to dictate the
364
+ # database session through the initial config.
365
+ #
366
+ # 1) and 2) clash.
367
+ engine = app_config .registry ["sqlalchemy.engine" ] # get_sqlalchemy_engine(database)
346
368
conn = engine .connect ()
347
369
trans = conn .begin ()
348
370
session = Session (bind = conn , join_transaction_mode = "create_savepoint" )
@@ -357,6 +379,35 @@ def db_session(app_config):
357
379
engine .dispose ()
358
380
359
381
382
+ @pytest .fixture (scope = "session" )
383
+ def app_config (database ):
384
+
385
+ return get_app_config (database )
386
+
387
+
388
+ @pytest .fixture (scope = "session" )
389
+ def app_config_dbsession_from_env (database ):
390
+
391
+ nondefaults = {
392
+ "warehouse.db_create_session" : lambda r : r .environ .get ("warehouse.db_session" )
393
+ }
394
+
395
+ return get_app_config (database , nondefaults )
396
+
397
+
398
+ @pytest .fixture
399
+ def db_session (app_config ):
400
+ """
401
+ Refactor:
402
+
403
+ This fixture actually manages a specific app_config paired with a database
404
+ connection. For this reason, it's suggested to change the name to
405
+ db_and_app, and yield both app_config and db_session.
406
+ """
407
+ with get_db_session_for_app_config (app_config ) as _db_session :
408
+ yield _db_session
409
+
410
+
360
411
@pytest .fixture
361
412
def user_service (db_session , metrics , remote_addr ):
362
413
return account_services .DatabaseUserService (
@@ -622,18 +673,42 @@ def xmlrpc(self, path, method, *args):
622
673
623
674
624
675
@pytest .fixture
625
- def webtest (app_config ):
626
- # TODO: Ensure that we have per test isolation of the database level
627
- # changes. This probably involves flushing the database or something
628
- # between test cases to wipe any committed changes.
676
+ def webtest (app_config_dbsession_from_env ):
677
+ """
678
+ This fixture yields a test app with an alternative Pyramid configuration,
679
+ injecting the database session and transaction manager into the app.
680
+
681
+ This is because the Warehouse app normally manages its own database session.
682
+
683
+ After the fixture has yielded the app, the transaction is rolled back and
684
+ the database is left in its previous state.
685
+ """
629
686
630
687
# We want to disable anything that relies on TLS here.
631
- app_config .add_settings (enforce_https = False )
688
+ app_config_dbsession_from_env .add_settings (enforce_https = False )
689
+
690
+ app = app_config_dbsession_from_env .make_wsgi_app ()
691
+
692
+ # Create a new transaction manager for dependant test cases
693
+ tm = transaction .TransactionManager (explicit = True )
694
+ tm .begin ()
695
+ tm .doom ()
696
+
697
+ with get_db_session_for_app_config (app_config_dbsession_from_env ) as _db_session :
698
+ # Register the app with the external test environment, telling
699
+ # request.db to use this db_session and use the Transaction manager.
700
+ testapp = _TestApp (
701
+ app ,
702
+ extra_environ = {
703
+ "warehouse.db_session" : _db_session ,
704
+ "tm.active" : True , # disable pyramid_tm
705
+ "tm.manager" : tm , # pass in our own tm for the app to use
706
+ },
707
+ )
708
+ yield testapp
632
709
633
- try :
634
- yield _TestApp (app_config .make_wsgi_app ())
635
- finally :
636
- app_config .registry ["sqlalchemy.engine" ].dispose ()
710
+ # Abort the transaction, leaving database in previous state
711
+ tm .abort ()
637
712
638
713
639
714
class _MockRedis :
0 commit comments