e2e: add tests for proxy provider and outposts
This commit is contained in:
		
							
								
								
									
										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.webdriver.common.desired_capabilities import DesiredCapabilities
 | 
			
		||||
from selenium.webdriver.remote.webdriver import WebDriver
 | 
			
		||||
from selenium.webdriver.support import expected_conditions as ec
 | 
			
		||||
from selenium.webdriver.support.ui import WebDriverWait
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
@ -50,6 +51,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
 | 
			
		||||
    def _start_container(self, specs: Dict[str, Any]) -> Container:
 | 
			
		||||
        client: DockerClient = from_env()
 | 
			
		||||
        container = client.containers.run(**specs)
 | 
			
		||||
        if "healthcheck" not in specs:
 | 
			
		||||
            return container
 | 
			
		||||
        while True:
 | 
			
		||||
            container.reload()
 | 
			
		||||
            status = container.attrs.get("State", {}).get("Health", {}).get("Status")
 | 
			
		||||
@ -88,7 +91,7 @@ class SeleniumTestCase(StaticLiveServerTestCase):
 | 
			
		||||
    def wait_for_url(self, desired_url):
 | 
			
		||||
        """Wait until URL is `desired_url`."""
 | 
			
		||||
        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}",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,11 +15,8 @@ class CodeMirrorWidget(forms.Textarea):
 | 
			
		||||
        self.mode = mode
 | 
			
		||||
 | 
			
		||||
    def render(self, *args, **kwargs):
 | 
			
		||||
        if "attrs" not in kwargs:
 | 
			
		||||
            kwargs["attrs"] = {}
 | 
			
		||||
        attrs = kwargs["attrs"]
 | 
			
		||||
        if "class" not in attrs:
 | 
			
		||||
            attrs["class"] = ""
 | 
			
		||||
        attrs = kwargs.setdefault("attrs", {})
 | 
			
		||||
        attrs.setdefault("class", "")
 | 
			
		||||
        attrs["class"] += " codemirror"
 | 
			
		||||
        attrs["data-cm-mode"] = self.mode
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
class Command(BaseCommand):  # pragma: no cover
 | 
			
		||||
    """Apply flow from commandline"""
 | 
			
		||||
 | 
			
		||||
    @no_translations
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,5 @@
 | 
			
		||||
"""passbook lib template utilities"""
 | 
			
		||||
from django.template import Context, Template, loader
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def render_from_string(tmpl: str, ctx: Context) -> str:
 | 
			
		||||
    """Render template from string to string"""
 | 
			
		||||
    template = Template(tmpl)
 | 
			
		||||
    return template.render(ctx)
 | 
			
		||||
from django.template import Context, loader
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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.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()
 | 
			
		||||
 | 
			
		||||
# 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_S3_ENDPOINT_URL = CONFIG.y("postgresql.backup.host")
 | 
			
		||||
    AWS_DEFAULT_ACL = None
 | 
			
		||||
    print(
 | 
			
		||||
        dumps(
 | 
			
		||||
            {
 | 
			
		||||
                "event": "Database backup is configured.",
 | 
			
		||||
                "level": "info",
 | 
			
		||||
                "logger": __name__,
 | 
			
		||||
                "host": CONFIG.y("postgresql.backup.host"),
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    j_print("Database backup is configured.", host=CONFIG.y("postgresql.backup.host"))
 | 
			
		||||
    # Add automatic task to backup
 | 
			
		||||
    CELERY_BEAT_SCHEDULE["db_backup"] = {
 | 
			
		||||
        "task": "passbook.lib.tasks.backup_database",
 | 
			
		||||
@ -295,15 +299,6 @@ if CONFIG.y("postgresql.backup"):
 | 
			
		||||
# Sentry integration
 | 
			
		||||
_ERROR_REPORTING = CONFIG.y_bool("error_reporting.enabled", False)
 | 
			
		||||
if not DEBUG and _ERROR_REPORTING:
 | 
			
		||||
    print(
 | 
			
		||||
        dumps(
 | 
			
		||||
            {
 | 
			
		||||
                "event": "Error reporting is enabled.",
 | 
			
		||||
                "level": "info",
 | 
			
		||||
                "logger": __name__,
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    sentry_init(
 | 
			
		||||
        dsn="https://33cdbcb23f8b436dbe0ee06847410b67@sentry.beryju.org/3",
 | 
			
		||||
        integrations=[
 | 
			
		||||
@ -316,6 +311,10 @@ if not DEBUG and _ERROR_REPORTING:
 | 
			
		||||
        environment=CONFIG.y("error_reporting.environment", "customer"),
 | 
			
		||||
        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)
 | 
			
		||||
@ -434,3 +433,5 @@ if DEBUG:
 | 
			
		||||
    MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
 | 
			
		||||
 | 
			
		||||
INSTALLED_APPS.append("passbook.core.apps.PassbookCoreConfig")
 | 
			
		||||
 | 
			
		||||
j_print("Booting passbook", version=__version__)
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user