Source code for portal_web.tests.conftest

# For copyright and license terms, see COPYRIGHT.rst (top level of repository)
# Repository: https://github.com/C3S/portal_web

"""
Pytest Fixtures
"""

import os
import subprocess
import glob
import datetime
import inspect
import logging
import pytest

from trytond.transaction import Transaction
from pyramid import testing
from paste.deploy.loadwsgi import appconfig
from webtest import TestApp
from webtest.http import StopableWSGIServer
from selenium.webdriver import Remote, FirefoxOptions

from portal_web import main
from portal_web.models import Tdb
from portal_web.config import get_plugins, replace_environment_vars


# --- pytest ------------------------------------------------------------------

[docs] def pytest_collection_modifyitems(session, config, items): """ Fixes the nodid path string to show the full path. """ # hack for vs code test explorer python adapter if int(os.environ.get("DEBUGGER_DEBUGPY", 0)) == 1: for item in items: relpath = os.path.relpath(item.path, "/shared/src") nodeid = "::".join([relpath] + item.nodeid.split("::")[1:]) item._nodeid = f"{nodeid}" return items # use absolute paths for nodeids for item in items: nodeid = "::".join( [f"{item.path}"] + item.nodeid.split("::")[1:]) item._nodeid = f"{nodeid}" return items
[docs] @pytest.fixture def debug(caplog): caplog.set_level(logging.DEBUG, logger="pyramid") caplog.set_level(logging.DEBUG, logger="portal_web") caplog.set_level(logging.DEBUG, logger="collecting_society_web")
# --- database ----------------------------------------------------------------
[docs] @pytest.fixture(autouse=True, scope='session') def reset_database(): """ Resets the database for each test run. """ if os.environ.get('COMPOSE_PROJECT_NAME'): if int(os.environ.get('DB_KEEP', 0)) == 1: return subprocess.call( [ 'db-setup', 'collecting_society_test_template', '--dataset', 'production', '--no-template', ] ) subprocess.call( [ 'db-copy', '--force', 'collecting_society_test_template', 'collecting_society_test', ] )
# --- tryton ------------------------------------------------------------------
[docs] class TrytonHelper: """ Tryton helper for use in pytests. Attributes: settings (paste.deploy.config): parsed pyramid ini config Tdb (Tdb): Tdb class pool (Pool): Initialized pool Methods: transaction: Tdb transaction decorator delete_records: helper function to delete records in reversed order """ def __init__(self, settings): Tdb._db = settings['tryton.database'] Tdb._company = settings['tryton.company'] Tdb._user = settings['tryton.user'] Tdb._configfile = settings['tryton.configfile'] Tdb.init() self.settings = settings self.Tdb = Tdb self.pool = Tdb.pool() self.transaction = Tdb.transaction
[docs] @staticmethod @Tdb.transaction(readonly=False) def delete_records(records): if not records: return records.reverse() for instance in records: instance.delete([instance]) Transaction().commit()
[docs] @pytest.fixture(scope='session') def tryton(settings): """ Provides the tryton test helper class. Yields: TrytonHelper: Helper with usual tryton testing objects accessible """ return TrytonHelper(settings)
# --- pyramid -----------------------------------------------------------------
[docs] @pytest.fixture(scope='session') def settings(): """ Provides parsed pyramid settings (plugins combined, envvars substituted). """ environment = 'testing' settings = appconfig( 'config:' + os.path.join( os.path.dirname(__file__), '..', '..', f'{environment}.ini' ) ) plugins = get_plugins(settings, environment) for priority in sorted(plugins, reverse=True): settings.update(plugins[priority]['settings']) settings = replace_environment_vars(settings) return settings
[docs] class PyramidHelper: """ Pyramid helper for use in pytests. Attributes: settings (paste.deploy.config): parsed pyramid ini config request (pyramid.testing.DummyRequest): empty dummy request resource (pyramid.testing.DummyResource): empty dummy resource config (pyramid.config.Configurator): pyramid app config registry (pyramid.registry.Registry): pyramid registry """ def __init__(self, settings): self.settings = settings self.request = testing.DummyRequest() self.resource = testing.DummyResource() self.config = testing.setUp(request=self.request, settings=settings) self.registry = self.config.registry
[docs] @pytest.fixture def pyramid(settings): """ Provides the pyramid helper class. Yields: PyramidHelper: Helper with usual pyramid testing objects accessible """ yield PyramidHelper(settings) testing.tearDown()
[docs] @pytest.fixture def dummy_request(): """ Creates an empty dummy request. Returns: pyramid.testing.DummyRequest: dummy request """ return testing.DummyRequest()
[docs] @pytest.fixture def request_with_registry(settings): """ Creates a request with registry set up. Returns: pyramid.testing.DummyRequest: dummy request """ request = testing.DummyRequest() testing.setUp(request=request, settings=settings) return request
[docs] @pytest.fixture def dummy_resource(): """ Creates an empty dummy resource. Returns: pyramid.testing.DummyResource: dummy resource """ return testing.DummyResource()
# --- webtest -----------------------------------------------------------------
[docs] @pytest.fixture(scope='class') def api(settings): """ Sets up an webapi service with TestApp. Returns: webtest.TestApp: for functional tests with webtest """ settings['service'] = "webapi" return TestApp(main({}, **settings))
[docs] @pytest.fixture(scope='class') def gui(settings): """ Sets up a webgui service with TestApp. Returns: webtest.TestApp: for functional tests with webtest """ settings['service'] = "webgui" return TestApp(main({}, **settings))
# --- webdriver ---------------------------------------------------------------
[docs] @pytest.fixture(scope='session') def browser_api(settings): """ Sets up an webapi service with StopableWSGIServer. Yields: webtest.http.StopableWSGIServer: for integration tests with selenium """ settings['service'] = "webapi" app = main({}, **settings) server = StopableWSGIServer.create( app, host='0.0.0.0', port=6545, clear_untrusted_proxy_headers=True, threads=10 ) if not server.wait(): raise Exception('Server could not be fired up. Exiting ...') yield server server.shutdown()
[docs] @pytest.fixture(scope='session') @pytest.mark.usefixtures('browser_api') def browser_gui(settings): """ Sets up a webgui service with StopableWSGIServer. Yields: webtest.http.StopableWSGIServer: for integration tests with selenium """ settings['service'] = "webgui" app = main({}, **settings) server = StopableWSGIServer.create( app, host='0.0.0.0', port=6544, clear_untrusted_proxy_headers=True, threads=10 ) if not server.wait(): raise Exception('Server could not be fired up. Exiting ...') yield server server.shutdown()
[docs] class BrowserHelper: """ Browser helper for use in selenium tests with pytest. Notes: - undefined attributes of this class will be requested from self.browser - urls starting with "/" or "gui/" will send requests to the the webgui service and "api/" to the webapi service. Attributes: browser (webdriver.Remote): remote webdriver options (webdriver.Options): options used for the webdriver host (str): hostname/ip to remote access the app gui (webtest.http.StopableWSGIServer): webgui app api (webtest.http.StopableWSGIServer): webapi app screenshots (bool): create screenshots during a test run screenshot_path (str): directory for screenshots Methods: get: overrides browser.get to autocomplete the url and automate """ def __init__(self, gui, api, screenshots=True, screenshot_path='/shared/tests/screenshots'): # webdriver options = FirefoxOptions() options.add_argument('--headless') options.add_argument('--verbose') # options.set_capability('loggingPrefs', {'browser': 'ALL'}) browser = Remote( command_executor='http://test_browser:4444/wd/hub', options=options ) self.browser = browser self.options = options self.host = 'test_web' self.gui = gui self.api = api # window self.testsizes = { 'xs': (360, 640), 'sm': (768, 1024), 'md': (1024, 768), 'lg': (1920, 1080), } browser.set_window_size(*self.testsizes['lg']) # screenshots self.screenshots = screenshots self.screenshot_path = screenshot_path def __getattr__(self, name): """ Delegate attribute access to browser. """ return getattr(self.browser, name)
[docs] def get(self, url, *args, **kwargs): """ Overrides browser.get to autocomplete the url and automate screenshot handling. urls starting with "/" or "gui/" will send requests to the the webgui service and "api/" to the webapi service. """ # get full_url = url if url.startswith(("/", "gui/")): full_url = f"http://{self.host}:{self.gui.addr[1]}/{url}" elif url.startswith("api/"): full_url = f"http://{self.host}:{self.api.addr[1]}/{url}" self.browser.get(full_url, *args, **kwargs) # screenshot url = f"GET-{url}".replace("/", "⧸").replace("?", "-QUERY-") self.screenshot("%s" % url)
[docs] def screenshot(self, name=""): """ Takes a screenshot of the current browser client viewport. """ if not self.screenshots: return # generate filename testtime = datetime.datetime.utcnow().strftime('%y%m%d.%H%M%S.%f')[:-4] testclass = '' testmethod = '' for frameinfo in inspect.stack(): if frameinfo[3].startswith('test_'): testclass = frameinfo[0].f_locals["self"].__class__.__name__ testmethod = frameinfo[3] filename = f"{testtime}-{testclass}.{testmethod}-{name}".strip("._-") # make a screenshot for each screen resolution for name, size in self.testsizes.items(): # resize window self.set_window_size(*size) required_height = self.execute_script( 'return document.body.parentNode.scrollHeight') self.set_window_size(size[0], required_height) # make screenshot self.get_screenshot_as_file(os.path.join( self.screenshot_path, f'{name}-{filename}.png')) self.set_window_size(*self.testsizes['lg'])
screenshot_path = '/shared/tests/screenshots'
[docs] @pytest.fixture(autouse=True, scope='session') def delete_screenshots(): """ Deletes all screenshots of previous selenium tests. """ if not os.path.isdir(screenshot_path): os.makedirs(screenshot_path) path = os.path.join(screenshot_path, '*.png') for screenshot in glob.glob(path): os.unlink(screenshot)
[docs] @pytest.fixture(scope='session') def browser(browser_gui, browser_api): """ Provides the selenoum test browser. Yields: BrowserHelper: selenium remote connection to selenium hub service with additional helper functions """ try: browser = BrowserHelper( gui=browser_gui, api=browser_api, screenshots=True, screenshot_path=screenshot_path) yield browser finally: browser.quit()
[docs] @pytest.fixture def reset(request): """ Resets the session. """ if 'gui' in request.fixturenames: gui = request.getfixturevalue('gui') if isinstance(gui, TestApp): gui.reset() if 'api' in request.fixturenames: api = request.getfixturevalue('gui') if isinstance(api, TestApp): api.reset() if 'browser' in request.fixturenames: browser = request.getfixturevalue('browser') browser.delete_all_cookies() browser.refresh() browser.set_window_size(*browser.testsizes['lg'])
# --- models ------------------------------------------------------------------
[docs] @pytest.fixture(scope='class') def create_party(tryton): """ Yields a function to create a party. """ records = [] Party = tryton.pool.get('party.party') @staticmethod @tryton.transaction(readonly=False) def create(**kwargs): party = Party(**kwargs) party.save() records.append(party) return party yield create tryton.delete_records(records)
[docs] @pytest.fixture(scope='class') def create_web_user(tryton): """ Yields a function to create a web user. """ records = [] WebUser = tryton.pool.get('web.user') WebUserRole = tryton.pool.get('web.user.role') @staticmethod @tryton.transaction(readonly=False) def create(**kwargs): if 'roles' not in kwargs: kwargs['roles'] = WebUserRole.search([]) web_user = WebUser(**kwargs) web_user.save() records.append(web_user) return web_user yield create tryton.delete_records(records)