import datetime import socket import threading from typing import Optional from wsgiref import simple_server from pyramid.authentication import AuthTktCookieHelper from pyramid.testing import DummyRequest from testutils import load_gpx_asset from fietsboek import models from fietsboek.models.track import Visibility, TrackType from fietsboek.config import Config import pytest @pytest.fixture(scope="session") def server_port(): """Return a (likely) free port. Note that due to OS race conditions between picking the port and opening something on it, it might be taken again, but that is unlikely. """ sock = socket.socket(socket.AF_INET) sock.bind(("", 0)) port = sock.getsockname()[1] sock.close() return port @pytest.fixture(scope="session", autouse=True) def running_server(server_port, app): """Have the app running as an actual server.""" server = simple_server.make_server("127.0.0.1", server_port, app) thread = threading.Thread(target=server.serve_forever) thread.daemon = True thread.start() yield server.shutdown() @pytest.fixture def browser_context_args(server_port) -> dict: return {"base_url": f"http://localhost:{server_port}"} @pytest.fixture def dbaccess(app): """Provide direct access to the database. This is needed for the selenium tests, because the normal "dbsession" fixture has a doomed transaction attached. This is nice for keeping the test database clean, but it does mean that the changes are not propagated through and the running WSGI app cannot read them. """ session_factory = app.registry["dbsession_factory"] return session_factory() class Helper: """Helper to insert objects for testing into the database and similar.""" def __init__(self, dbaccess, app_settings, page, data_manager): self.dbaccess = dbaccess self.app_settings = app_settings self.page = page self.data_manager = data_manager self._johnny = None def john_doe(self) -> models.User: """Provides a test user (John Doe). This fixture either returns the existing John or creates a new one. """ if self._johnny: return self._johnny with self.dbaccess: user = models.User(name="John Doe", email="john@doe.com", is_verified=True) user.set_password("password") self.dbaccess.add(user) self.dbaccess.commit() self.dbaccess.refresh(user, ["id"]) self.dbaccess.expunge(user) self._johnny = user return user def login(self, user: Optional[models.User] = None): """Logs the given user in by setting the auth cookie.""" if user is None: user = self.john_doe() config = Config.construct(session_key=self.app_settings["session_key"]) secret = config.derive_secret("auth-cookie") helper = AuthTktCookieHelper(secret) headers = helper.remember(DummyRequest(), str(user.id)) for _, header_val in headers: cookie = header_val.split(";")[0] name, value = cookie.split("=", 1) self.page.context.add_cookies( [ {"name": name, "value": value, "domain": "localhost", "path": "/"}, ] ) def add_track( self, user: Optional[models.User] = None, track_name: str = "Teasi_1.gpx.gz" ) -> models.Track: """Add a track to the given user. If the user is None, John Doe is used. """ if user is None: user = self.john_doe() with self.dbaccess: user = self.dbaccess.merge(user) track = models.Track( title="Another awesome track", visibility=Visibility.PRIVATE, description="Another description", ) track.date = datetime.datetime.now(datetime.timezone.utc) user.tracks.append(track) self.dbaccess.flush() self.dbaccess.refresh(track, ["id"]) self.dbaccess.expunge(track) self.data_manager.initialize(track.id).compress_gpx(load_gpx_asset(track_name)) self.dbaccess.commit() return track @pytest.fixture def playwright_helper(dbaccess, app_settings, page, data_manager): return Helper(dbaccess, app_settings, page, data_manager)