Skip to content

Commit 524bd23

Browse files
benjaomingmikethemanjzohrab
authored
Tell warehouse.db to reuse db_session pytest fixture (#16031)
Co-authored-by: Mike Fiedler <miketheman@gmail.com> Co-authored-by: Jeff Zohrab <jzohrab@gmail.com>
1 parent 6264166 commit 524bd23

File tree

3 files changed

+126
-17
lines changed

3 files changed

+126
-17
lines changed

tests/conftest.py

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import pyramid.testing
2626
import pytest
2727
import stripe
28+
import transaction
2829
import webtest as _webtest
2930

3031
from jinja2 import Environment, FileSystemLoader
@@ -298,8 +299,7 @@ def mock_manifest_cache_buster():
298299
return MockManifestCacheBuster
299300

300301

301-
@pytest.fixture(scope="session")
302-
def app_config(database):
302+
def get_app_config(database, nondefaults=None):
303303
settings = {
304304
"warehouse.prevent_esi": True,
305305
"warehouse.token": "insecure token",
@@ -329,20 +329,42 @@ def app_config(database):
329329
"statuspage.url": "https://2p66nmmycsj3.statuspage.io",
330330
"warehouse.xmlrpc.cache.url": "redis://localhost:0/",
331331
}
332+
333+
if nondefaults:
334+
settings.update(nondefaults)
335+
332336
with mock.patch.object(config, "ManifestCacheBuster", MockManifestCacheBuster):
333337
with mock.patch("warehouse.admin.ManifestCacheBuster", MockManifestCacheBuster):
334338
with mock.patch.object(static, "whitenoise_add_manifest"):
335339
cfg = config.configure(settings=settings)
336340

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.
338344
alembic.command.upgrade(cfg.alembic_config(), "head")
339345

340346
return cfg
341347

342348

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)
346368
conn = engine.connect()
347369
trans = conn.begin()
348370
session = Session(bind=conn, join_transaction_mode="create_savepoint")
@@ -357,6 +379,35 @@ def db_session(app_config):
357379
engine.dispose()
358380

359381

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+
360411
@pytest.fixture
361412
def user_service(db_session, metrics, remote_addr):
362413
return account_services.DatabaseUserService(
@@ -622,18 +673,42 @@ def xmlrpc(self, path, method, *args):
622673

623674

624675
@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+
"""
629686

630687
# 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
632709

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()
637712

638713

639714
class _MockRedis:

tests/functional/test_user_profile.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
from tests.common.db.accounts import UserFactory
14+
15+
16+
def test_user_profile(webtest):
17+
"""
18+
This test is maintained as a POC for future tests that want to add data to
19+
the database and test HTTP endpoints afterwards.
20+
21+
The trick is to use the ``webtest`` fixture which will create a special
22+
instance of the Warehouse WSGI app, sharing the same DB session as is active
23+
in pytest.
24+
"""
25+
# Create a user
26+
user = UserFactory.create()
27+
assert user.username
28+
# ...and verify that the user's profile page exists
29+
resp = webtest.get(f"/user/{user.username}/")
30+
assert resp.status_code == 200

warehouse/db.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,12 @@ def includeme(config):
187187
pool_timeout=20,
188188
)
189189

190-
# Register our request.db property
191-
config.add_request_method(_create_session, name="db", reify=True)
190+
# Possibly override how to fetch new db sessions from config.settings
191+
# Useful in test fixtures
192+
db_session_factory = config.registry.settings.get(
193+
"warehouse.db_create_session", _create_session
194+
)
195+
config.add_request_method(db_session_factory, name="db", reify=True)
192196

193197
# Set a custom JSON serializer for psycopg
194198
renderer = JSON()

0 commit comments

Comments
 (0)