e2e: add tests for proxy provider and outposts

This commit is contained in:
Jens Langhammer 2020-09-16 21:54:35 +02:00
parent 6187436518
commit f1ccef7f6a
7 changed files with 121 additions and 33 deletions

View file

@ -0,0 +1,93 @@
"""Proxy and Outpost e2e tests"""
from time import sleep
from typing import Any, Dict, Optional
from docker.client import DockerClient, from_env
from docker.models.containers import Container
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from e2e.utils import USER, SeleniumTestCase
from passbook.core.models import Application
from passbook.flows.models import Flow
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
from passbook.providers.proxy.models import ProxyProvider
class TestProviderProxy(SeleniumTestCase):
"""Proxy and Outpost e2e tests"""
proxy_container: Container
def tearDown(self) -> None:
super().tearDown()
self.proxy_container.kill()
def get_container_specs(self) -> Optional[Dict[str, Any]]:
return {
"image": "traefik/whoami:latest",
"detach": True,
"network_mode": "host",
"auto_remove": True,
}
def start_proxy(self, outpost: Outpost) -> Container:
"""Start proxy container based on outpost created"""
client: DockerClient = from_env()
container = client.containers.run(
image="beryju/passbook-proxy:latest",
detach=True,
network_mode="host",
auto_remove=True,
environment={
"PASSBOOK_HOST": self.live_server_url,
"PASSBOOK_TOKEN": outpost.token.token_uuid.hex,
},
)
return container
def test_proxy_simple(self):
"""Test simple outpost setup with single provider"""
proxy: ProxyProvider = ProxyProvider.objects.create(
name="proxy_provider",
authorization_flow=Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
),
internal_host="http://localhost:80",
external_host="http://localhost:4180",
)
# Ensure OAuth2 Params are set
proxy.set_oauth_defaults()
proxy.save()
# we need to create an application to actually access the proxy
Application.objects.create(name="proxy", slug="proxy", provider=proxy)
outpost: Outpost = Outpost.objects.create(
name="proxy_outpost",
type=OutpostType.PROXY,
deployment_type=OutpostDeploymentType.CUSTOM,
)
outpost.providers.add(proxy)
outpost.save()
self.proxy_container = self.start_proxy(outpost)
# Wait until outpost healthcheck succeeds
healthcheck_retries = 0
while healthcheck_retries < 50:
if outpost.health:
break
healthcheck_retries += 1
sleep(0.5)
self.driver.get("http://localhost:4180")
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
sleep(1)
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
self.assertIn("X-Forwarded-Preferred-Username: pbadmin", full_body_text)

View file

@ -17,6 +17,7 @@ from docker.models.containers import Container
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from structlog import get_logger from structlog import get_logger
@ -50,6 +51,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
def _start_container(self, specs: Dict[str, Any]) -> Container: def _start_container(self, specs: Dict[str, Any]) -> Container:
client: DockerClient = from_env() client: DockerClient = from_env()
container = client.containers.run(**specs) container = client.containers.run(**specs)
if "healthcheck" not in specs:
return container
while True: while True:
container.reload() container.reload()
status = container.attrs.get("State", {}).get("Health", {}).get("Status") status = container.attrs.get("State", {}).get("Health", {}).get("Status")
@ -88,7 +91,7 @@ class SeleniumTestCase(StaticLiveServerTestCase):
def wait_for_url(self, desired_url): def wait_for_url(self, desired_url):
"""Wait until URL is `desired_url`.""" """Wait until URL is `desired_url`."""
self.wait.until( self.wait.until(
lambda driver: driver.current_url == desired_url, ec.url_to_be(desired_url),
f"URL {self.driver.current_url} doesn't match expected URL {desired_url}", f"URL {self.driver.current_url} doesn't match expected URL {desired_url}",
) )

View file

@ -15,11 +15,8 @@ class CodeMirrorWidget(forms.Textarea):
self.mode = mode self.mode = mode
def render(self, *args, **kwargs): def render(self, *args, **kwargs):
if "attrs" not in kwargs: attrs = kwargs.setdefault("attrs", {})
kwargs["attrs"] = {} attrs.setdefault("class", "")
attrs = kwargs["attrs"]
if "class" not in attrs:
attrs["class"] = ""
attrs["class"] += " codemirror" attrs["class"] += " codemirror"
attrs["data-cm-mode"] = self.mode attrs["data-cm-mode"] = self.mode
return super().render(*args, **kwargs) return super().render(*args, **kwargs)

View file

@ -4,7 +4,7 @@ from django.core.management.base import BaseCommand, no_translations
from passbook.flows.transfer.importer import FlowImporter from passbook.flows.transfer.importer import FlowImporter
class Command(BaseCommand): class Command(BaseCommand): # pragma: no cover
"""Apply flow from commandline""" """Apply flow from commandline"""
@no_translations @no_translations

View file

@ -1,11 +1,5 @@
"""passbook lib template utilities""" """passbook lib template utilities"""
from django.template import Context, Template, loader from django.template import Context, loader
def render_from_string(tmpl: str, ctx: Context) -> str:
"""Render template from string to string"""
template = Template(tmpl)
return template.render(ctx)
def render_to_string(template_path: str, ctx: Context) -> str: def render_to_string(template_path: str, ctx: Context) -> str:

View file

@ -25,6 +25,19 @@ from passbook.lib.config import CONFIG
from passbook.lib.logging import add_process_id from passbook.lib.logging import add_process_id
from passbook.lib.sentry import before_send from passbook.lib.sentry import before_send
def j_print(event: str, log_level: str = "info", **kwargs):
"""Print event in the same format as structlog with JSON.
Used before structlog is configured."""
data = {
"event": event,
"level": log_level,
"logger": __name__,
}
data.update(**kwargs)
print(dumps(data))
LOGGER = structlog.get_logger() LOGGER = structlog.get_logger()
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@ -276,16 +289,7 @@ if CONFIG.y("postgresql.backup"):
AWS_STORAGE_BUCKET_NAME = CONFIG.y("postgresql.backup.bucket") AWS_STORAGE_BUCKET_NAME = CONFIG.y("postgresql.backup.bucket")
AWS_S3_ENDPOINT_URL = CONFIG.y("postgresql.backup.host") AWS_S3_ENDPOINT_URL = CONFIG.y("postgresql.backup.host")
AWS_DEFAULT_ACL = None AWS_DEFAULT_ACL = None
print( j_print("Database backup is configured.", host=CONFIG.y("postgresql.backup.host"))
dumps(
{
"event": "Database backup is configured.",
"level": "info",
"logger": __name__,
"host": CONFIG.y("postgresql.backup.host"),
}
)
)
# Add automatic task to backup # Add automatic task to backup
CELERY_BEAT_SCHEDULE["db_backup"] = { CELERY_BEAT_SCHEDULE["db_backup"] = {
"task": "passbook.lib.tasks.backup_database", "task": "passbook.lib.tasks.backup_database",
@ -295,15 +299,6 @@ if CONFIG.y("postgresql.backup"):
# Sentry integration # Sentry integration
_ERROR_REPORTING = CONFIG.y_bool("error_reporting.enabled", False) _ERROR_REPORTING = CONFIG.y_bool("error_reporting.enabled", False)
if not DEBUG and _ERROR_REPORTING: if not DEBUG and _ERROR_REPORTING:
print(
dumps(
{
"event": "Error reporting is enabled.",
"level": "info",
"logger": __name__,
}
)
)
sentry_init( sentry_init(
dsn="https://33cdbcb23f8b436dbe0ee06847410b67@sentry.beryju.org/3", dsn="https://33cdbcb23f8b436dbe0ee06847410b67@sentry.beryju.org/3",
integrations=[ integrations=[
@ -316,6 +311,10 @@ if not DEBUG and _ERROR_REPORTING:
environment=CONFIG.y("error_reporting.environment", "customer"), environment=CONFIG.y("error_reporting.environment", "customer"),
send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False), send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False),
) )
j_print(
"Error reporting is enabled.",
env=CONFIG.y("error_reporting.environment", "customer"),
)
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
@ -434,3 +433,5 @@ if DEBUG:
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
INSTALLED_APPS.append("passbook.core.apps.PassbookCoreConfig") INSTALLED_APPS.append("passbook.core.apps.PassbookCoreConfig")
j_print("Booting passbook", version=__version__)