e2e: add tests for proxy provider and outposts
This commit is contained in:
parent
6187436518
commit
f1ccef7f6a
93
e2e/test_provider_proxy.py
Normal file
93
e2e/test_provider_proxy.py
Normal 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)
|
|
@ -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}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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__)
|
||||||
|
|
Reference in a new issue