2020-06-07 18:30:56 +01:00
|
|
|
"""passbook e2e testing utilities"""
|
|
|
|
from glob import glob
|
2020-06-19 18:34:27 +01:00
|
|
|
from importlib.util import module_from_spec, spec_from_file_location
|
2020-06-07 18:30:56 +01:00
|
|
|
from inspect import getmembers, isfunction
|
2020-07-12 15:17:04 +01:00
|
|
|
from os import environ, makedirs
|
2020-09-11 22:21:11 +01:00
|
|
|
from time import sleep, time
|
|
|
|
from typing import Any, Dict, Optional
|
2020-06-19 18:34:27 +01:00
|
|
|
|
2020-06-07 18:30:56 +01:00
|
|
|
from django.apps import apps
|
2020-06-20 16:06:00 +01:00
|
|
|
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
2020-06-07 18:30:56 +01:00
|
|
|
from django.db import connection, transaction
|
|
|
|
from django.db.utils import IntegrityError
|
2020-06-20 22:56:35 +01:00
|
|
|
from django.shortcuts import reverse
|
2020-09-11 22:21:11 +01:00
|
|
|
from docker import DockerClient, from_env
|
|
|
|
from docker.models.containers import Container
|
2020-06-20 16:06:00 +01:00
|
|
|
from selenium import webdriver
|
|
|
|
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
|
|
|
from selenium.webdriver.remote.webdriver import WebDriver
|
|
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
2020-06-26 14:06:46 +01:00
|
|
|
from structlog import get_logger
|
2020-06-19 18:34:27 +01:00
|
|
|
|
2020-06-20 22:52:06 +01:00
|
|
|
from passbook.core.models import User
|
|
|
|
|
|
|
|
|
|
|
|
# pylint: disable=invalid-name
|
2020-06-20 23:26:29 +01:00
|
|
|
def USER() -> User: # noqa
|
2020-06-20 22:52:06 +01:00
|
|
|
"""Cached function that always returns pbadmin"""
|
|
|
|
return User.objects.get(username="pbadmin")
|
|
|
|
|
2020-06-19 18:34:27 +01:00
|
|
|
|
2020-06-20 16:06:00 +01:00
|
|
|
class SeleniumTestCase(StaticLiveServerTestCase):
|
2020-06-20 22:52:06 +01:00
|
|
|
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
|
|
|
|
|
2020-09-11 22:21:11 +01:00
|
|
|
container: Optional[Container] = None
|
|
|
|
|
2020-06-20 16:06:00 +01:00
|
|
|
def setUp(self):
|
|
|
|
super().setUp()
|
2020-07-02 20:55:02 +01:00
|
|
|
makedirs("selenium_screenshots/", exist_ok=True)
|
2020-06-20 16:06:00 +01:00
|
|
|
self.driver = self._get_driver()
|
2020-06-20 23:26:29 +01:00
|
|
|
self.driver.maximize_window()
|
2020-09-11 22:21:11 +01:00
|
|
|
self.driver.implicitly_wait(10)
|
|
|
|
self.wait = WebDriverWait(self.driver, 30)
|
2020-06-20 16:06:00 +01:00
|
|
|
self.apply_default_data()
|
2020-06-26 14:06:46 +01:00
|
|
|
self.logger = get_logger()
|
2020-09-11 22:21:11 +01:00
|
|
|
if specs := self.get_container_specs():
|
|
|
|
self.container = self._start_container(specs)
|
|
|
|
|
|
|
|
def _start_container(self, specs: Dict[str, Any]) -> Container:
|
|
|
|
client: DockerClient = from_env()
|
2020-09-29 10:55:10 +01:00
|
|
|
client.images.pull(specs['image'])
|
2020-09-11 22:21:11 +01:00
|
|
|
container = client.containers.run(**specs)
|
2020-09-16 20:54:35 +01:00
|
|
|
if "healthcheck" not in specs:
|
|
|
|
return container
|
2020-09-11 22:21:11 +01:00
|
|
|
while True:
|
|
|
|
container.reload()
|
|
|
|
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
|
|
|
if status == "healthy":
|
|
|
|
return container
|
|
|
|
self.logger.info("Container failed healthcheck")
|
|
|
|
sleep(1)
|
|
|
|
|
|
|
|
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
|
|
|
"""Optionally get container specs which will launched on setup, wait for the container to
|
|
|
|
be healthy, and deleted again on tearDown"""
|
|
|
|
return None
|
2020-06-20 16:06:00 +01:00
|
|
|
|
|
|
|
def _get_driver(self) -> WebDriver:
|
|
|
|
return webdriver.Remote(
|
|
|
|
command_executor="http://localhost:4444/wd/hub",
|
|
|
|
desired_capabilities=DesiredCapabilities.CHROME,
|
|
|
|
)
|
|
|
|
|
|
|
|
def tearDown(self):
|
2020-07-23 19:03:35 +01:00
|
|
|
if "TF_BUILD" in environ:
|
2020-07-12 15:17:04 +01:00
|
|
|
screenshot_file = (
|
|
|
|
f"selenium_screenshots/{self.__class__.__name__}_{time()}.png"
|
|
|
|
)
|
|
|
|
self.driver.save_screenshot(screenshot_file)
|
|
|
|
self.logger.warning("Saved screenshot", file=screenshot_file)
|
2020-06-26 14:06:46 +01:00
|
|
|
for line in self.driver.get_log("browser"):
|
|
|
|
self.logger.warning(
|
|
|
|
line["message"], source=line["source"], level=line["level"]
|
|
|
|
)
|
2020-09-11 22:21:11 +01:00
|
|
|
if self.container:
|
|
|
|
self.container.kill()
|
2020-06-20 16:06:00 +01:00
|
|
|
self.driver.quit()
|
2020-06-20 16:06:15 +01:00
|
|
|
super().tearDown()
|
2020-06-20 16:06:00 +01:00
|
|
|
|
2020-06-26 15:21:59 +01:00
|
|
|
def wait_for_url(self, desired_url):
|
|
|
|
"""Wait until URL is `desired_url`."""
|
2020-06-30 15:36:30 +01:00
|
|
|
self.wait.until(
|
2020-09-16 22:31:16 +01:00
|
|
|
lambda driver: driver.current_url == desired_url,
|
2020-06-30 15:36:30 +01:00
|
|
|
f"URL {self.driver.current_url} doesn't match expected URL {desired_url}",
|
|
|
|
)
|
2020-06-26 15:21:59 +01:00
|
|
|
|
2020-06-20 22:56:35 +01:00
|
|
|
def url(self, view, **kwargs) -> str:
|
|
|
|
"""reverse `view` with `**kwargs` into full URL using live_server_url"""
|
|
|
|
return self.live_server_url + reverse(view, kwargs=kwargs)
|
|
|
|
|
2020-06-20 16:06:00 +01:00
|
|
|
def apply_default_data(self):
|
|
|
|
"""apply objects created by migrations after tables have been truncated"""
|
|
|
|
# Find all migration files
|
|
|
|
# load all functions
|
|
|
|
migration_files = glob("**/migrations/*.py", recursive=True)
|
|
|
|
matches = []
|
|
|
|
for migration in migration_files:
|
|
|
|
with open(migration, "r+") as migration_file:
|
|
|
|
# Check if they have a `RunPython`
|
|
|
|
if "RunPython" in migration_file.read():
|
|
|
|
matches.append(migration)
|
|
|
|
|
|
|
|
with connection.schema_editor() as schema_editor:
|
|
|
|
for match in matches:
|
|
|
|
# Load module from file path
|
|
|
|
spec = spec_from_file_location("", match)
|
|
|
|
migration_module = module_from_spec(spec)
|
|
|
|
# pyright: reportGeneralTypeIssues=false
|
|
|
|
spec.loader.exec_module(migration_module)
|
|
|
|
# Call all functions from module
|
|
|
|
for _, func in getmembers(migration_module, isfunction):
|
|
|
|
with transaction.atomic():
|
|
|
|
try:
|
|
|
|
func(apps, schema_editor)
|
|
|
|
except IntegrityError:
|
|
|
|
pass
|