"""What survives a failure mid-request — ARCH-006. The audit counted 58 `db.session.commit()` in the routes against a single `db.session.rollback()` in the whole repository, and read that ratio as a problem. Measured rather than assumed, it mostly is not one: - Flask-SQLAlchemy tears the session down at the end of every request, which rolls back anything uncommitted; - the one rollback is in the 500 handler, which is where it belongs; - after the wave-D work, no route module contains `except Exception` at all, so nothing swallows a failure and carries on. What the count could not see is the real defect: a route that commits *twice*, so a failure after the first one leaves half an operation persisted. There were two such functions. `edit_user` was the serious one and was fixed with ARCH-008; `register` is fixed here. These tests state the guarantee, so that removing the rollback or reintroducing a mid-operation commit fails loudly. """ import time import pytest from app.models import User, UserGamertag class TestNothingPartialSurvives: def test_a_failure_after_add_leaves_no_row(self, app, client, as_role, monkeypatch): """The session is torn down per request; an uncommitted add is gone.""" as_role('admin') from app.routes.users import accounts def _explode(*args, **kwargs): raise RuntimeError('storage unavailable') monkeypatch.setattr(accounts.db.session, 'commit', _explode) with pytest.raises(RuntimeError): client.post( '/users/create', data={ 'username': 'ghost', 'email': 'ghost@example.test', 'password': 'Password123', 'full_name': 'Ghost Account', 'role': 'coach', }, ) with app.app_context(): assert User.query.filter_by(username='ghost').first() is None def test_the_error_handler_still_rolls_back(self, app): """The single rollback in the repository is the one in the 500 handler. Removing it should break something visible.""" import inspect from app import app as app_module source = inspect.getsource(app_module.create_app) assert 'db.session.rollback()' in source class TestRegistrationIsOneOperation: """register() committed the account, then committed the gamertags. A failure in between left an account whose declared games were absent, with the sign-up reported as successful.""" FORM = { 'username': 'newplayer', 'email': 'newplayer@example.test', 'password': 'Password123', 'confirm_password': 'Password123', 'full_name': 'New Player', 'games': 'Valorant', 'gamertag_Valorant': 'newplayer#1234', } def _submit(self, client, app, **overrides): # Registration is screened for robots (SEC-AUTH-008): the form has to # have been issued, and long enough ago. Backdated here rather than # slept through, so the suite does not pay three seconds per call. from app.routes.auth import MIN_REGISTRATION_SECONDS, REGISTRATION_ISSUED_KEY with client.session_transaction() as session: session[REGISTRATION_ISSUED_KEY] = time.time() - MIN_REGISTRATION_SECONDS - 1 payload = dict(self.FORM) payload.update(overrides) return client.post('/auth/register', data=payload, follow_redirects=True) def test_a_successful_sign_up_stores_both(self, app, client): self._submit(client, app) with app.app_context(): user = User.query.filter_by(username='newplayer').one() tags = UserGamertag.query.filter_by(user_id=user.id).all() assert [tag.gamertag for tag in tags] == ['newplayer#1234'] def test_a_failure_leaves_no_half_account(self, app, client, monkeypatch): from app.routes import auth as auth_module real_add = auth_module.db.session.add def _explode_on_gamertag(instance, *args, **kwargs): if isinstance(instance, UserGamertag): raise RuntimeError('storage unavailable') return real_add(instance, *args, **kwargs) monkeypatch.setattr(auth_module.db.session, 'add', _explode_on_gamertag) with pytest.raises(RuntimeError): self._submit(client, app) with app.app_context(): assert User.query.filter_by(username='newplayer').first() is None