Compare commits
	
		
			1 Commits
		
	
	
		
			docusaurus
			...
			tests/e2e/
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4b0d641a51 | 
| @ -11,7 +11,7 @@ from django.test.runner import DiscoverRunner | |||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.sentry import sentry_init | from authentik.lib.sentry import sentry_init | ||||||
| from authentik.root.signals import post_startup, pre_startup, startup | from authentik.root.signals import post_startup, pre_startup, startup | ||||||
| from tests.e2e.utils import get_docker_tag | from tests.docker import get_docker_tag | ||||||
|  |  | ||||||
| # globally set maxDiff to none to show full assert error | # globally set maxDiff to none to show full assert error | ||||||
| TestCase.maxDiff = None | TestCase.maxDiff = None | ||||||
|  | |||||||
| @ -0,0 +1,12 @@ | |||||||
|  | import socket | ||||||
|  | from os import environ | ||||||
|  |  | ||||||
|  | IS_CI = "CI" in environ | ||||||
|  | RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_local_ip() -> str: | ||||||
|  |     """Get the local machine's IP""" | ||||||
|  |     hostname = socket.gethostname() | ||||||
|  |     ip_addr = socket.gethostbyname(hostname) | ||||||
|  |     return ip_addr | ||||||
|  | |||||||
| @ -1,29 +1,18 @@ | |||||||
| """authentik e2e testing utilities""" | """authentik e2e testing utilities""" | ||||||
| 
 | 
 | ||||||
|  | # This file cannot import anything django or anything that will load django | ||||||
|  | 
 | ||||||
| import json | import json | ||||||
| import os |  | ||||||
| import socket |  | ||||||
| from collections.abc import Callable |  | ||||||
| from functools import lru_cache, wraps |  | ||||||
| from os import environ |  | ||||||
| from sys import stderr | from sys import stderr | ||||||
| from time import sleep | from time import sleep | ||||||
| from typing import Any | from typing import TYPE_CHECKING | ||||||
| from unittest.case import TestCase | from unittest.case import TestCase | ||||||
| from urllib.parse import urlencode | from urllib.parse import urlencode | ||||||
| 
 | 
 | ||||||
| from django.apps import apps |  | ||||||
| from django.contrib.staticfiles.testing import StaticLiveServerTestCase | from django.contrib.staticfiles.testing import StaticLiveServerTestCase | ||||||
| from django.db import connection |  | ||||||
| from django.db.migrations.loader import MigrationLoader |  | ||||||
| from django.test.testcases import TransactionTestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from docker import DockerClient, from_env |  | ||||||
| from docker.errors import DockerException |  | ||||||
| from docker.models.containers import Container |  | ||||||
| from docker.models.networks import Network |  | ||||||
| from selenium import webdriver | from selenium import webdriver | ||||||
| from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException | from selenium.common.exceptions import WebDriverException | ||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
| from selenium.webdriver.common.keys import Keys | from selenium.webdriver.common.keys import Keys | ||||||
| from selenium.webdriver.remote.command import Command | from selenium.webdriver.remote.command import Command | ||||||
| @ -33,137 +22,27 @@ from selenium.webdriver.support import expected_conditions as ec | |||||||
| from selenium.webdriver.support.wait import WebDriverWait | from selenium.webdriver.support.wait import WebDriverWait | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
| 
 | 
 | ||||||
| from authentik.core.api.users import UserSerializer | from tests import IS_CI, RETRIES, get_local_ip | ||||||
| from authentik.core.models import User | from tests.websocket import BaseWebsocketTestCase | ||||||
| from authentik.core.tests.utils import create_test_admin_user |  | ||||||
| from authentik.lib.generators import generate_id |  | ||||||
| 
 | 
 | ||||||
| IS_CI = "CI" in environ | if TYPE_CHECKING: | ||||||
| RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1 |     from authentik.core.models import User | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_docker_tag() -> str: | class BaseSeleniumTestCase(TestCase): | ||||||
|     """Get docker-tag based off of CI variables""" |     """Mixin which adds helpers for spinning up Selenium""" | ||||||
|     env_pr_branch = "GITHUB_HEAD_REF" |  | ||||||
|     default_branch = "GITHUB_REF" |  | ||||||
|     branch_name = os.environ.get(default_branch, "main") |  | ||||||
|     if os.environ.get(env_pr_branch, "") != "": |  | ||||||
|         branch_name = os.environ[env_pr_branch] |  | ||||||
|     branch_name = branch_name.replace("refs/heads/", "").replace("/", "-") |  | ||||||
|     return f"gh-{branch_name}" |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def get_local_ip() -> str: |  | ||||||
|     """Get the local machine's IP""" |  | ||||||
|     hostname = socket.gethostname() |  | ||||||
|     ip_addr = socket.gethostbyname(hostname) |  | ||||||
|     return ip_addr |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class DockerTestCase(TestCase): |  | ||||||
|     """Mixin for dealing with containers""" |  | ||||||
| 
 |  | ||||||
|     max_healthcheck_attempts = 30 |  | ||||||
| 
 |  | ||||||
|     __client: DockerClient |  | ||||||
|     __network: Network |  | ||||||
| 
 |  | ||||||
|     __label_id = generate_id() |  | ||||||
| 
 |  | ||||||
|     def setUp(self) -> None: |  | ||||||
|         self.__client = from_env() |  | ||||||
|         self.__network = self.docker_client.networks.create(name=f"authentik-test-{generate_id()}") |  | ||||||
| 
 |  | ||||||
|     @property |  | ||||||
|     def docker_client(self) -> DockerClient: |  | ||||||
|         return self.__client |  | ||||||
| 
 |  | ||||||
|     @property |  | ||||||
|     def docker_network(self) -> Network: |  | ||||||
|         return self.__network |  | ||||||
| 
 |  | ||||||
|     @property |  | ||||||
|     def docker_labels(self) -> dict: |  | ||||||
|         return {"io.goauthentik.test": self.__label_id} |  | ||||||
| 
 |  | ||||||
|     def wait_for_container(self, container: Container): |  | ||||||
|         """Check that container is health""" |  | ||||||
|         attempt = 0 |  | ||||||
|         while True: |  | ||||||
|             container.reload() |  | ||||||
|             status = container.attrs.get("State", {}).get("Health", {}).get("Status") |  | ||||||
|             if status == "healthy": |  | ||||||
|                 return container |  | ||||||
|             sleep(1) |  | ||||||
|             attempt += 1 |  | ||||||
|             if attempt >= self.max_healthcheck_attempts: |  | ||||||
|                 self.failureException("Container failed to start") |  | ||||||
| 
 |  | ||||||
|     def get_container_image(self, base: str) -> str: |  | ||||||
|         """Try to pull docker image based on git branch, fallback to main if not found.""" |  | ||||||
|         image = f"{base}:gh-main" |  | ||||||
|         try: |  | ||||||
|             branch_image = f"{base}:{get_docker_tag()}" |  | ||||||
|             self.docker_client.images.pull(branch_image) |  | ||||||
|             return branch_image |  | ||||||
|         except DockerException: |  | ||||||
|             self.docker_client.images.pull(image) |  | ||||||
|         return image |  | ||||||
| 
 |  | ||||||
|     def run_container(self, **specs: dict[str, Any]) -> Container: |  | ||||||
|         if "network_mode" not in specs: |  | ||||||
|             specs["network"] = self.__network.name |  | ||||||
|         specs["labels"] = self.docker_labels |  | ||||||
|         specs["detach"] = True |  | ||||||
|         if hasattr(self, "live_server_url"): |  | ||||||
|             specs.setdefault("environment", {}) |  | ||||||
|             specs["environment"]["AUTHENTIK_HOST"] = self.live_server_url |  | ||||||
|         container = self.docker_client.containers.run(**specs) |  | ||||||
|         container.reload() |  | ||||||
|         state = container.attrs.get("State", {}) |  | ||||||
|         if "Health" not in state: |  | ||||||
|             return container |  | ||||||
|         self.wait_for_container(container) |  | ||||||
|         return container |  | ||||||
| 
 |  | ||||||
|     def output_container_logs(self, container: Container | None = None): |  | ||||||
|         """Output the container logs to our STDOUT""" |  | ||||||
|         if IS_CI: |  | ||||||
|             image = container.image |  | ||||||
|             tags = image.tags[0] if len(image.tags) > 0 else str(image) |  | ||||||
|             print(f"::group::Container logs - {tags}") |  | ||||||
|         for log in container.logs().decode().split("\n"): |  | ||||||
|             print(log) |  | ||||||
|         if IS_CI: |  | ||||||
|             print("::endgroup::") |  | ||||||
| 
 |  | ||||||
|     def tearDown(self): |  | ||||||
|         containers: list[Container] = self.docker_client.containers.list( |  | ||||||
|             filters={"label": ",".join(f"{x}={y}" for x, y in self.docker_labels.items())} |  | ||||||
|         ) |  | ||||||
|         for container in containers: |  | ||||||
|             self.output_container_logs(container) |  | ||||||
|             try: |  | ||||||
|                 container.kill() |  | ||||||
|             except DockerException: |  | ||||||
|                 pass |  | ||||||
|             try: |  | ||||||
|                 container.remove(force=True) |  | ||||||
|             except DockerException: |  | ||||||
|                 pass |  | ||||||
|         self.__network.remove() |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase): |  | ||||||
|     """StaticLiveServerTestCase which automatically creates a Webdriver instance""" |  | ||||||
| 
 | 
 | ||||||
|     host = get_local_ip() |     host = get_local_ip() | ||||||
|     wait_timeout: int |     wait_timeout: int | ||||||
|     user: User |     user: "User" | ||||||
| 
 | 
 | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         if IS_CI: |         if IS_CI: | ||||||
|             print("::group::authentik Logs", file=stderr) |             print("::group::authentik Logs", file=stderr) | ||||||
|  |         from django.apps import apps | ||||||
|  | 
 | ||||||
|  |         from authentik.core.tests.utils import create_test_admin_user | ||||||
|  | 
 | ||||||
|         apps.get_app_config("authentik_tenants").ready() |         apps.get_app_config("authentik_tenants").ready() | ||||||
|         self.wait_timeout = 60 |         self.wait_timeout = 60 | ||||||
|         self.driver = self._get_driver() |         self.driver = self._get_driver() | ||||||
| @ -290,8 +169,10 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase): | |||||||
|         password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER) |         password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER) | ||||||
|         sleep(1) |         sleep(1) | ||||||
| 
 | 
 | ||||||
|     def assert_user(self, expected_user: User): |     def assert_user(self, expected_user: "User"): | ||||||
|         """Check users/me API and assert it matches expected_user""" |         """Check users/me API and assert it matches expected_user""" | ||||||
|  |         from authentik.core.api.users import UserSerializer | ||||||
|  | 
 | ||||||
|         self.driver.get(self.url("authentik_api:user-me") + "?format=json") |         self.driver.get(self.url("authentik_api:user-me") + "?format=json") | ||||||
|         user_json = self.driver.find_element(By.CSS_SELECTOR, "pre").text |         user_json = self.driver.find_element(By.CSS_SELECTOR, "pre").text | ||||||
|         user = UserSerializer(data=json.loads(user_json)["user"]) |         user = UserSerializer(data=json.loads(user_json)["user"]) | ||||||
| @ -301,46 +182,9 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase): | |||||||
|         self.assertEqual(user["email"].value, expected_user.email) |         self.assertEqual(user["email"].value, expected_user.email) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @lru_cache | class SeleniumTestCase(BaseSeleniumTestCase, StaticLiveServerTestCase): | ||||||
| def get_loader(): |     """Test case which spins up a selenium instance and a HTTP-only test server""" | ||||||
|     """Thin wrapper to lazily get a Migration Loader, only when it's needed |  | ||||||
|     and only once""" |  | ||||||
|     return MigrationLoader(connection) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def retry(max_retires=RETRIES, exceptions=None): | class WebsocketSeleniumTestCase(BaseSeleniumTestCase, BaseWebsocketTestCase): | ||||||
|     """Retry test multiple times. Default to catching Selenium Timeout Exception""" |     """Test case which spins up a selenium instance and a Websocket/HTTP test server""" | ||||||
| 
 |  | ||||||
|     if not exceptions: |  | ||||||
|         exceptions = [WebDriverException, TimeoutException, NoSuchElementException] |  | ||||||
| 
 |  | ||||||
|     logger = get_logger() |  | ||||||
| 
 |  | ||||||
|     def retry_actual(func: Callable): |  | ||||||
|         """Retry test multiple times""" |  | ||||||
|         count = 1 |  | ||||||
| 
 |  | ||||||
|         @wraps(func) |  | ||||||
|         def wrapper(self: TransactionTestCase, *args, **kwargs): |  | ||||||
|             """Run test again if we're below max_retries, including tearDown and |  | ||||||
|             setUp. Otherwise raise the error""" |  | ||||||
|             nonlocal count |  | ||||||
|             try: |  | ||||||
|                 return func(self, *args, **kwargs) |  | ||||||
| 
 |  | ||||||
|             except tuple(exceptions) as exc: |  | ||||||
|                 count += 1 |  | ||||||
|                 if count > max_retires: |  | ||||||
|                     logger.debug("Exceeded retry count", exc=exc, test=self) |  | ||||||
| 
 |  | ||||||
|                     raise exc |  | ||||||
|                 logger.debug("Retrying on error", exc=exc, test=self) |  | ||||||
|                 self.tearDown() |  | ||||||
|                 self._post_teardown() |  | ||||||
|                 self._pre_setup() |  | ||||||
|                 self.setUp() |  | ||||||
|                 return wrapper(self, *args, **kwargs) |  | ||||||
| 
 |  | ||||||
|         return wrapper |  | ||||||
| 
 |  | ||||||
|     return retry_actual |  | ||||||
							
								
								
									
										48
									
								
								tests/decorators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								tests/decorators.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | |||||||
|  | """authentik e2e testing utilities""" | ||||||
|  |  | ||||||
|  | from collections.abc import Callable | ||||||
|  | from functools import wraps | ||||||
|  |  | ||||||
|  | from django.test.testcases import TransactionTestCase | ||||||
|  | from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from tests import RETRIES | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def retry(max_retires=RETRIES, exceptions=None): | ||||||
|  |     """Retry test multiple times. Default to catching Selenium Timeout Exception""" | ||||||
|  |  | ||||||
|  |     if not exceptions: | ||||||
|  |         exceptions = [WebDriverException, TimeoutException, NoSuchElementException] | ||||||
|  |  | ||||||
|  |     logger = get_logger() | ||||||
|  |  | ||||||
|  |     def retry_actual(func: Callable): | ||||||
|  |         """Retry test multiple times""" | ||||||
|  |         count = 1 | ||||||
|  |  | ||||||
|  |         @wraps(func) | ||||||
|  |         def wrapper(self: TransactionTestCase, *args, **kwargs): | ||||||
|  |             """Run test again if we're below max_retries, including tearDown and | ||||||
|  |             setUp. Otherwise raise the error""" | ||||||
|  |             nonlocal count | ||||||
|  |             try: | ||||||
|  |                 return func(self, *args, **kwargs) | ||||||
|  |  | ||||||
|  |             except tuple(exceptions) as exc: | ||||||
|  |                 count += 1 | ||||||
|  |                 if count > max_retires: | ||||||
|  |                     logger.debug("Exceeded retry count", exc=exc, test=self) | ||||||
|  |  | ||||||
|  |                     raise exc | ||||||
|  |                 logger.debug("Retrying on error", exc=exc, test=self) | ||||||
|  |                 self.tearDown() | ||||||
|  |                 self._post_teardown() | ||||||
|  |                 self._pre_setup() | ||||||
|  |                 self.setUp() | ||||||
|  |                 return wrapper(self, *args, **kwargs) | ||||||
|  |  | ||||||
|  |         return wrapper | ||||||
|  |  | ||||||
|  |     return retry_actual | ||||||
							
								
								
									
										139
									
								
								tests/docker.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								tests/docker.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,139 @@ | |||||||
|  | """Docker testing helpers""" | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | from time import sleep | ||||||
|  | from typing import TYPE_CHECKING, Any | ||||||
|  | from unittest.case import TestCase | ||||||
|  |  | ||||||
|  | from docker import DockerClient, from_env | ||||||
|  | from docker.errors import DockerException | ||||||
|  | from docker.models.containers import Container | ||||||
|  | from docker.models.networks import Network | ||||||
|  |  | ||||||
|  | from authentik.lib.generators import generate_id | ||||||
|  | from tests import IS_CI | ||||||
|  |  | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from authentik.outposts.models import Outpost | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_docker_tag() -> str: | ||||||
|  |     """Get docker-tag based off of CI variables""" | ||||||
|  |     env_pr_branch = "GITHUB_HEAD_REF" | ||||||
|  |     default_branch = "GITHUB_REF" | ||||||
|  |     branch_name = os.environ.get(default_branch, "main") | ||||||
|  |     if os.environ.get(env_pr_branch, "") != "": | ||||||
|  |         branch_name = os.environ[env_pr_branch] | ||||||
|  |     branch_name = branch_name.replace("refs/heads/", "").replace("/", "-") | ||||||
|  |     return f"gh-{branch_name}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DockerTestCase(TestCase): | ||||||
|  |     """Mixin for dealing with containers""" | ||||||
|  |  | ||||||
|  |     max_healthcheck_attempts = 30 | ||||||
|  |  | ||||||
|  |     __client: DockerClient | ||||||
|  |     __network: Network | ||||||
|  |  | ||||||
|  |     __label_id = generate_id() | ||||||
|  |  | ||||||
|  |     def setUp(self) -> None: | ||||||
|  |         self.__client = from_env() | ||||||
|  |         self.__network = self.docker_client.networks.create( | ||||||
|  |             name=f"authentik-test-{self.__label_id}" | ||||||
|  |         ) | ||||||
|  |         super().setUp() | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def docker_client(self) -> DockerClient: | ||||||
|  |         return self.__client | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def docker_network(self) -> Network: | ||||||
|  |         return self.__network | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def docker_labels(self) -> dict: | ||||||
|  |         return {"io.goauthentik.test": self.__label_id} | ||||||
|  |  | ||||||
|  |     def get_container_image(self, base: str) -> str: | ||||||
|  |         """Try to pull docker image based on git branch, fallback to main if not found.""" | ||||||
|  |         image = f"{base}:gh-main" | ||||||
|  |         if not IS_CI: | ||||||
|  |             return image | ||||||
|  |         try: | ||||||
|  |             branch_image = f"{base}:{get_docker_tag()}" | ||||||
|  |             self.docker_client.images.pull(branch_image) | ||||||
|  |             return branch_image | ||||||
|  |         except DockerException: | ||||||
|  |             self.docker_client.images.pull(image) | ||||||
|  |         return image | ||||||
|  |  | ||||||
|  |     def run_container(self, **specs: dict[str, Any]) -> Container: | ||||||
|  |         if "network_mode" not in specs: | ||||||
|  |             specs["network"] = self.__network.name | ||||||
|  |         specs["labels"] = self.docker_labels | ||||||
|  |         specs["detach"] = True | ||||||
|  |         if hasattr(self, "live_server_url"): | ||||||
|  |             specs.setdefault("environment", {}) | ||||||
|  |             specs["environment"]["AUTHENTIK_HOST"] = self.live_server_url | ||||||
|  |         container = self.docker_client.containers.run(**specs) | ||||||
|  |         container.reload() | ||||||
|  |         state = container.attrs.get("State", {}) | ||||||
|  |         if "Health" not in state: | ||||||
|  |             return container | ||||||
|  |         self.wait_for_container(container) | ||||||
|  |         return container | ||||||
|  |  | ||||||
|  |     def output_container_logs(self, container: Container | None = None): | ||||||
|  |         """Output the container logs to our STDOUT""" | ||||||
|  |         if IS_CI: | ||||||
|  |             image = container.image | ||||||
|  |             tags = image.tags[0] if len(image.tags) > 0 else str(image) | ||||||
|  |             print(f"::group::Container logs - {tags}") | ||||||
|  |         for log in container.logs().decode().split("\n"): | ||||||
|  |             print(log) | ||||||
|  |         if IS_CI: | ||||||
|  |             print("::endgroup::") | ||||||
|  |  | ||||||
|  |     def tearDown(self): | ||||||
|  |         containers: list[Container] = self.docker_client.containers.list( | ||||||
|  |             filters={"label": ",".join(f"{x}={y}" for x, y in self.docker_labels.items())} | ||||||
|  |         ) | ||||||
|  |         for container in containers: | ||||||
|  |             self.output_container_logs(container) | ||||||
|  |             try: | ||||||
|  |                 container.stop() | ||||||
|  |             except DockerException: | ||||||
|  |                 pass | ||||||
|  |             try: | ||||||
|  |                 container.remove(force=True) | ||||||
|  |             except DockerException: | ||||||
|  |                 pass | ||||||
|  |         self.__network.remove() | ||||||
|  |         super().tearDown() | ||||||
|  |  | ||||||
|  |     def wait_for_container(self, container: Container): | ||||||
|  |         """Check that container is health""" | ||||||
|  |         attempt = 0 | ||||||
|  |         while attempt < self.max_healthcheck_attempts: | ||||||
|  |             container.reload() | ||||||
|  |             status = container.attrs.get("State", {}).get("Health", {}).get("Status") | ||||||
|  |             if status == "healthy": | ||||||
|  |                 return container | ||||||
|  |             attempt += 1 | ||||||
|  |             sleep(0.5) | ||||||
|  |         self.failureException("Container failed to start") | ||||||
|  |  | ||||||
|  |     def wait_for_outpost(self, outpost: "Outpost"): | ||||||
|  |         # Wait until outpost healthcheck succeeds | ||||||
|  |         attempt = 0 | ||||||
|  |         while attempt < self.max_healthcheck_attempts: | ||||||
|  |             if len(outpost.state) > 0: | ||||||
|  |                 state = outpost.state[0] | ||||||
|  |                 if state.last_seen: | ||||||
|  |                     return | ||||||
|  |             attempt += 1 | ||||||
|  |             sleep(0.5) | ||||||
|  |         self.failureException("Outpost failed to become healthy") | ||||||
| @ -18,10 +18,12 @@ from authentik.stages.authenticator_static.models import ( | |||||||
|     StaticToken, |     StaticToken, | ||||||
| ) | ) | ||||||
| from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage, TOTPDevice | from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage, TOTPDevice | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestFlowsAuthenticator(SeleniumTestCase): | class TestFlowsAuthenticator(DockerTestCase, SeleniumTestCase): | ||||||
|     """test flow with otp stages""" |     """test flow with otp stages""" | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|  | |||||||
| @ -11,10 +11,12 @@ from authentik.core.models import User | |||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.stages.identification.models import IdentificationStage | from authentik.stages.identification.models import IdentificationStage | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestFlowsEnroll(SeleniumTestCase): | class TestFlowsEnroll(DockerTestCase, SeleniumTestCase): | ||||||
|     """Test Enroll flow""" |     """Test Enroll flow""" | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|  | |||||||
| @ -2,10 +2,12 @@ | |||||||
|  |  | ||||||
| from authentik.blueprints.tests import apply_blueprint | from authentik.blueprints.tests import apply_blueprint | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestFlowsLogin(SeleniumTestCase): | class TestFlowsLogin(DockerTestCase, SeleniumTestCase): | ||||||
|     """test default login flow""" |     """test default login flow""" | ||||||
|  |  | ||||||
|     def tearDown(self): |     def tearDown(self): | ||||||
|  | |||||||
| @ -6,10 +6,12 @@ from selenium.webdriver.common.by import By | |||||||
| from selenium.webdriver.common.keys import Keys | from selenium.webdriver.common.keys import Keys | ||||||
|  |  | ||||||
| from authentik.blueprints.tests import apply_blueprint | from authentik.blueprints.tests import apply_blueprint | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestFlowsLoginSFE(SeleniumTestCase): | class TestFlowsLoginSFE(DockerTestCase, SeleniumTestCase): | ||||||
|     """test default login flow""" |     """test default login flow""" | ||||||
|  |  | ||||||
|     def login(self): |     def login(self): | ||||||
|  | |||||||
| @ -13,10 +13,12 @@ from authentik.flows.models import Flow | |||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.stages.identification.models import IdentificationStage | from authentik.stages.identification.models import IdentificationStage | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestFlowsRecovery(SeleniumTestCase): | class TestFlowsRecovery(DockerTestCase, SeleniumTestCase): | ||||||
|     """Test Recovery flow""" |     """Test Recovery flow""" | ||||||
|  |  | ||||||
|     def initial_stages(self, user: User): |     def initial_stages(self, user: User): | ||||||
|  | |||||||
| @ -8,10 +8,12 @@ from authentik.core.models import User | |||||||
| from authentik.flows.models import Flow, FlowDesignation | from authentik.flows.models import Flow, FlowDesignation | ||||||
| from authentik.lib.generators import generate_key | from authentik.lib.generators import generate_key | ||||||
| from authentik.stages.password.models import PasswordStage | from authentik.stages.password.models import PasswordStage | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestFlowsStageSetup(SeleniumTestCase): | class TestFlowsStageSetup(DockerTestCase, SeleniumTestCase): | ||||||
|     """test stage setup flows""" |     """test stage setup flows""" | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|  | |||||||
| @ -16,10 +16,12 @@ from authentik.lib.generators import generate_id | |||||||
| from authentik.outposts.apps import MANAGED_OUTPOST | from authentik.outposts.apps import MANAGED_OUTPOST | ||||||
| from authentik.outposts.models import Outpost, OutpostConfig, OutpostType | from authentik.outposts.models import Outpost, OutpostConfig, OutpostType | ||||||
| from authentik.providers.ldap.models import APIAccessMode, LDAPProvider | from authentik.providers.ldap.models import APIAccessMode, LDAPProvider | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  | from tests.websocket import WebsocketTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestProviderLDAP(SeleniumTestCase): | class TestProviderLDAP(DockerTestCase, WebsocketTestCase): | ||||||
|     """LDAP and Outpost e2e tests""" |     """LDAP and Outpost e2e tests""" | ||||||
|  |  | ||||||
|     def start_ldap(self, outpost: Outpost): |     def start_ldap(self, outpost: Outpost): | ||||||
|  | |||||||
| @ -18,10 +18,12 @@ from authentik.providers.oauth2.models import ( | |||||||
|     RedirectURI, |     RedirectURI, | ||||||
|     RedirectURIMatchingMode, |     RedirectURIMatchingMode, | ||||||
| ) | ) | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestProviderOAuth2Github(SeleniumTestCase): | class TestProviderOAuth2Github(DockerTestCase, SeleniumTestCase): | ||||||
|     """test OAuth Provider flow""" |     """test OAuth Provider flow""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|  | |||||||
| @ -26,10 +26,12 @@ from authentik.providers.oauth2.models import ( | |||||||
|     RedirectURIMatchingMode, |     RedirectURIMatchingMode, | ||||||
|     ScopeMapping, |     ScopeMapping, | ||||||
| ) | ) | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestProviderOAuth2OAuth(SeleniumTestCase): | class TestProviderOAuth2OAuth(DockerTestCase, SeleniumTestCase): | ||||||
|     """test OAuth with OAuth Provider flow""" |     """test OAuth with OAuth Provider flow""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|  | |||||||
| @ -26,10 +26,12 @@ from authentik.providers.oauth2.models import ( | |||||||
|     RedirectURIMatchingMode, |     RedirectURIMatchingMode, | ||||||
|     ScopeMapping, |     ScopeMapping, | ||||||
| ) | ) | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestProviderOAuth2OIDC(SeleniumTestCase): | class TestProviderOAuth2OIDC(DockerTestCase, SeleniumTestCase): | ||||||
|     """test OAuth with OpenID Provider flow""" |     """test OAuth with OpenID Provider flow""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|  | |||||||
| @ -26,10 +26,12 @@ from authentik.providers.oauth2.models import ( | |||||||
|     RedirectURIMatchingMode, |     RedirectURIMatchingMode, | ||||||
|     ScopeMapping, |     ScopeMapping, | ||||||
| ) | ) | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): | class TestProviderOAuth2OIDCImplicit(DockerTestCase, SeleniumTestCase): | ||||||
|     """test OAuth with OpenID Provider flow""" |     """test OAuth with OpenID Provider flow""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|  | |||||||
| @ -3,11 +3,8 @@ | |||||||
| from base64 import b64encode | from base64 import b64encode | ||||||
| from dataclasses import asdict | from dataclasses import asdict | ||||||
| from json import loads | from json import loads | ||||||
| from sys import platform |  | ||||||
| from time import sleep | from time import sleep | ||||||
| from unittest.case import skip, skipUnless |  | ||||||
|  |  | ||||||
| from channels.testing import ChannelsLiveServerTestCase |  | ||||||
| from jwt import decode | from jwt import decode | ||||||
| from selenium.webdriver.common.by import By | from selenium.webdriver.common.by import By | ||||||
|  |  | ||||||
| @ -18,10 +15,13 @@ from authentik.lib.generators import generate_id | |||||||
| from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType | from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType | ||||||
| from authentik.outposts.tasks import outpost_connection_discovery | from authentik.outposts.tasks import outpost_connection_discovery | ||||||
| from authentik.providers.proxy.models import ProxyProvider | from authentik.providers.proxy.models import ProxyProvider | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  | from tests.websocket import WebsocketTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestProviderProxy(SeleniumTestCase): | class TestProviderProxy(DockerTestCase, SeleniumTestCase): | ||||||
|     """Proxy and Outpost e2e tests""" |     """Proxy and Outpost e2e tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
| @ -37,13 +37,41 @@ class TestProviderProxy(SeleniumTestCase): | |||||||
|         """Start proxy container based on outpost created""" |         """Start proxy container based on outpost created""" | ||||||
|         self.run_container( |         self.run_container( | ||||||
|             image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"), |             image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"), | ||||||
|             ports={ |             ports={"9000": "9000"}, | ||||||
|                 "9000": "9000", |             environment={"AUTHENTIK_TOKEN": outpost.token.key}, | ||||||
|             }, |  | ||||||
|             environment={ |  | ||||||
|                 "AUTHENTIK_TOKEN": outpost.token.key, |  | ||||||
|             }, |  | ||||||
|         ) |         ) | ||||||
|  |         self.wait_for_outpost(outpost) | ||||||
|  |  | ||||||
|  |     def _prepare(self): | ||||||
|  |         # set additionalHeaders to test later | ||||||
|  |         self.user.attributes["additionalHeaders"] = {"X-Foo": "bar"} | ||||||
|  |         self.user.save() | ||||||
|  |  | ||||||
|  |         proxy: ProxyProvider = ProxyProvider.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |             authorization_flow=Flow.objects.get( | ||||||
|  |                 slug="default-provider-authorization-implicit-consent" | ||||||
|  |             ), | ||||||
|  |             invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"), | ||||||
|  |             internal_host=f"http://{self.host}", | ||||||
|  |             external_host="http://localhost:9000", | ||||||
|  |             basic_auth_enabled=True, | ||||||
|  |             basic_auth_user_attribute="basic-username", | ||||||
|  |             basic_auth_password_attribute="basic-password",  # nosec | ||||||
|  |         ) | ||||||
|  |         # 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=generate_id(), slug=generate_id(), provider=proxy) | ||||||
|  |         outpost: Outpost = Outpost.objects.create( | ||||||
|  |             name=generate_id(), | ||||||
|  |             type=OutpostType.PROXY, | ||||||
|  |         ) | ||||||
|  |         outpost.providers.add(proxy) | ||||||
|  |         outpost.build_user_permissions(outpost.user) | ||||||
|  |  | ||||||
|  |         self.start_proxy(outpost) | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     @apply_blueprint( |     @apply_blueprint( | ||||||
| @ -61,44 +89,7 @@ class TestProviderProxy(SeleniumTestCase): | |||||||
|     @reconcile_app("authentik_crypto") |     @reconcile_app("authentik_crypto") | ||||||
|     def test_proxy_simple(self): |     def test_proxy_simple(self): | ||||||
|         """Test simple outpost setup with single provider""" |         """Test simple outpost setup with single provider""" | ||||||
|         # set additionalHeaders to test later |         self._prepare() | ||||||
|         self.user.attributes["additionalHeaders"] = {"X-Foo": "bar"} |  | ||||||
|         self.user.save() |  | ||||||
|  |  | ||||||
|         proxy: ProxyProvider = ProxyProvider.objects.create( |  | ||||||
|             name=generate_id(), |  | ||||||
|             authorization_flow=Flow.objects.get( |  | ||||||
|                 slug="default-provider-authorization-implicit-consent" |  | ||||||
|             ), |  | ||||||
|             invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"), |  | ||||||
|             internal_host=f"http://{self.host}", |  | ||||||
|             external_host="http://localhost:9000", |  | ||||||
|         ) |  | ||||||
|         # 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=generate_id(), slug=generate_id(), provider=proxy) |  | ||||||
|         outpost: Outpost = Outpost.objects.create( |  | ||||||
|             name=generate_id(), |  | ||||||
|             type=OutpostType.PROXY, |  | ||||||
|         ) |  | ||||||
|         outpost.providers.add(proxy) |  | ||||||
|         outpost.build_user_permissions(outpost.user) |  | ||||||
|  |  | ||||||
|         self.start_proxy(outpost) |  | ||||||
|  |  | ||||||
|         # Wait until outpost healthcheck succeeds |  | ||||||
|         healthcheck_retries = 0 |  | ||||||
|         while healthcheck_retries < 50:  # noqa: PLR2004 |  | ||||||
|             if len(outpost.state) > 0: |  | ||||||
|                 state = outpost.state[0] |  | ||||||
|                 if state.last_seen: |  | ||||||
|                     break |  | ||||||
|             healthcheck_retries += 1 |  | ||||||
|             sleep(0.5) |  | ||||||
|         sleep(5) |  | ||||||
|  |  | ||||||
|         self.driver.get("http://localhost:9000/api") |         self.driver.get("http://localhost:9000/api") | ||||||
|         self.login() |         self.login() | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -137,49 +128,13 @@ class TestProviderProxy(SeleniumTestCase): | |||||||
|     @reconcile_app("authentik_crypto") |     @reconcile_app("authentik_crypto") | ||||||
|     def test_proxy_basic_auth(self): |     def test_proxy_basic_auth(self): | ||||||
|         """Test simple outpost setup with single provider""" |         """Test simple outpost setup with single provider""" | ||||||
|  |         self._prepare() | ||||||
|  |         # Setup basic auth | ||||||
|         cred = generate_id() |         cred = generate_id() | ||||||
|         attr = "basic-password"  # nosec |  | ||||||
|         self.user.attributes["basic-username"] = cred |         self.user.attributes["basic-username"] = cred | ||||||
|         self.user.attributes[attr] = cred |         self.user.attributes["basic-password"] = cred | ||||||
|         self.user.save() |         self.user.save() | ||||||
|  |  | ||||||
|         proxy: ProxyProvider = ProxyProvider.objects.create( |  | ||||||
|             name=generate_id(), |  | ||||||
|             authorization_flow=Flow.objects.get( |  | ||||||
|                 slug="default-provider-authorization-implicit-consent" |  | ||||||
|             ), |  | ||||||
|             invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"), |  | ||||||
|             internal_host=f"http://{self.host}", |  | ||||||
|             external_host="http://localhost:9000", |  | ||||||
|             basic_auth_enabled=True, |  | ||||||
|             basic_auth_user_attribute="basic-username", |  | ||||||
|             basic_auth_password_attribute=attr, |  | ||||||
|         ) |  | ||||||
|         # 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=generate_id(), slug=generate_id(), provider=proxy) |  | ||||||
|         outpost: Outpost = Outpost.objects.create( |  | ||||||
|             name=generate_id(), |  | ||||||
|             type=OutpostType.PROXY, |  | ||||||
|         ) |  | ||||||
|         outpost.providers.add(proxy) |  | ||||||
|         outpost.build_user_permissions(outpost.user) |  | ||||||
|  |  | ||||||
|         self.start_proxy(outpost) |  | ||||||
|  |  | ||||||
|         # Wait until outpost healthcheck succeeds |  | ||||||
|         healthcheck_retries = 0 |  | ||||||
|         while healthcheck_retries < 50:  # noqa: PLR2004 |  | ||||||
|             if len(outpost.state) > 0: |  | ||||||
|                 state = outpost.state[0] |  | ||||||
|                 if state.last_seen: |  | ||||||
|                     break |  | ||||||
|             healthcheck_retries += 1 |  | ||||||
|             sleep(0.5) |  | ||||||
|         sleep(5) |  | ||||||
|  |  | ||||||
|         self.driver.get("http://localhost:9000/api") |         self.driver.get("http://localhost:9000/api") | ||||||
|         self.login() |         self.login() | ||||||
|         sleep(1) |         sleep(1) | ||||||
| @ -187,9 +142,9 @@ class TestProviderProxy(SeleniumTestCase): | |||||||
|         full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text |         full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text | ||||||
|         body = loads(full_body_text) |         body = loads(full_body_text) | ||||||
|  |  | ||||||
|         self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username]) |         self.assertEqual(body.get("headers").get("X-Authentik-Username"), [self.user.username]) | ||||||
|         auth_header = b64encode(f"{cred}:{cred}".encode()).decode() |         auth_header = b64encode(f"{cred}:{cred}".encode()).decode() | ||||||
|         self.assertEqual(body["headers"]["Authorization"], [f"Basic {auth_header}"]) |         self.assertEqual(body.get("headers").get("Authorization"), [f"Basic {auth_header}"]) | ||||||
|  |  | ||||||
|         self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out") |         self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out") | ||||||
|         sleep(2) |         sleep(2) | ||||||
| @ -199,10 +154,7 @@ class TestProviderProxy(SeleniumTestCase): | |||||||
|         self.assertIn("You've logged out of", title) |         self.assertIn("You've logged out of", title) | ||||||
|  |  | ||||||
|  |  | ||||||
| # TODO: Fix flaky test | class TestProviderProxyConnect(DockerTestCase, WebsocketTestCase): | ||||||
| @skip("Flaky test") |  | ||||||
| @skipUnless(platform.startswith("linux"), "requires local docker") |  | ||||||
| class TestProviderProxyConnect(ChannelsLiveServerTestCase): |  | ||||||
|     """Test Proxy connectivity over websockets""" |     """Test Proxy connectivity over websockets""" | ||||||
|  |  | ||||||
|     @retry(exceptions=[AssertionError]) |     @retry(exceptions=[AssertionError]) | ||||||
| @ -241,14 +193,7 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase): | |||||||
|         outpost.build_user_permissions(outpost.user) |         outpost.build_user_permissions(outpost.user) | ||||||
|  |  | ||||||
|         # Wait until outpost healthcheck succeeds |         # Wait until outpost healthcheck succeeds | ||||||
|         healthcheck_retries = 0 |         self.wait_for_outpost(outpost) | ||||||
|         while healthcheck_retries < 50:  # noqa: PLR2004 |  | ||||||
|             if len(outpost.state) > 0: |  | ||||||
|                 state = outpost.state[0] |  | ||||||
|                 if state.last_seen and state.version: |  | ||||||
|                     break |  | ||||||
|             healthcheck_retries += 1 |  | ||||||
|             sleep(0.5) |  | ||||||
|  |  | ||||||
|         state = outpost.state |         state = outpost.state | ||||||
|         self.assertGreaterEqual(len(state), 1) |         self.assertGreaterEqual(len(state), 1) | ||||||
|  | |||||||
| @ -13,10 +13,12 @@ from authentik.flows.models import Flow | |||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.outposts.models import Outpost, OutpostType | from authentik.outposts.models import Outpost, OutpostType | ||||||
| from authentik.providers.proxy.models import ProxyMode, ProxyProvider | from authentik.providers.proxy.models import ProxyMode, ProxyProvider | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestProviderProxyForward(SeleniumTestCase): | class TestProviderProxyForward(DockerTestCase, SeleniumTestCase): | ||||||
|     """Proxy and Outpost e2e tests""" |     """Proxy and Outpost e2e tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
| @ -30,14 +32,11 @@ class TestProviderProxyForward(SeleniumTestCase): | |||||||
|         """Start proxy container based on outpost created""" |         """Start proxy container based on outpost created""" | ||||||
|         self.run_container( |         self.run_container( | ||||||
|             image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"), |             image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"), | ||||||
|             ports={ |             ports={"9000": "9000"}, | ||||||
|                 "9000": "9000", |             environment={"AUTHENTIK_TOKEN": outpost.token.key}, | ||||||
|             }, |  | ||||||
|             environment={ |  | ||||||
|                 "AUTHENTIK_TOKEN": outpost.token.key, |  | ||||||
|             }, |  | ||||||
|             name="ak-test-outpost", |             name="ak-test-outpost", | ||||||
|         ) |         ) | ||||||
|  |         self.wait_for_outpost(outpost) | ||||||
|  |  | ||||||
|     @apply_blueprint( |     @apply_blueprint( | ||||||
|         "default/flow-default-authentication-flow.yaml", |         "default/flow-default-authentication-flow.yaml", | ||||||
| @ -77,17 +76,6 @@ class TestProviderProxyForward(SeleniumTestCase): | |||||||
|  |  | ||||||
|         self.start_outpost(outpost) |         self.start_outpost(outpost) | ||||||
|  |  | ||||||
|         # Wait until outpost healthcheck succeeds |  | ||||||
|         healthcheck_retries = 0 |  | ||||||
|         while healthcheck_retries < 50:  # noqa: PLR2004 |  | ||||||
|             if len(outpost.state) > 0: |  | ||||||
|                 state = outpost.state[0] |  | ||||||
|                 if state.last_seen: |  | ||||||
|                     break |  | ||||||
|             healthcheck_retries += 1 |  | ||||||
|             sleep(0.5) |  | ||||||
|         sleep(5) |  | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|     def test_traefik(self): |     def test_traefik(self): | ||||||
|         """Test traefik""" |         """Test traefik""" | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| """Radius e2e tests""" | """Radius e2e tests""" | ||||||
|  |  | ||||||
| from dataclasses import asdict | from dataclasses import asdict | ||||||
| from time import sleep |  | ||||||
|  |  | ||||||
| from pyrad.client import Client | from pyrad.client import Client | ||||||
| from pyrad.dictionary import Dictionary | from pyrad.dictionary import Dictionary | ||||||
| @ -9,14 +8,17 @@ from pyrad.packet import AccessAccept, AccessReject, AccessRequest | |||||||
|  |  | ||||||
| from authentik.blueprints.tests import apply_blueprint | from authentik.blueprints.tests import apply_blueprint | ||||||
| from authentik.core.models import Application, User | from authentik.core.models import Application, User | ||||||
|  | from authentik.core.tests.utils import create_test_user | ||||||
| from authentik.flows.models import Flow | from authentik.flows.models import Flow | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id, generate_key | ||||||
| from authentik.outposts.models import Outpost, OutpostConfig, OutpostType | from authentik.outposts.models import Outpost, OutpostConfig, OutpostType | ||||||
| from authentik.providers.radius.models import RadiusProvider | from authentik.providers.radius.models import RadiusProvider | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  | from tests.websocket import WebsocketTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestProviderRadius(SeleniumTestCase): | class TestProviderRadius(DockerTestCase, WebsocketTestCase): | ||||||
|     """Radius Outpost e2e tests""" |     """Radius Outpost e2e tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
| @ -28,13 +30,13 @@ class TestProviderRadius(SeleniumTestCase): | |||||||
|         self.run_container( |         self.run_container( | ||||||
|             image=self.get_container_image("ghcr.io/goauthentik/dev-radius"), |             image=self.get_container_image("ghcr.io/goauthentik/dev-radius"), | ||||||
|             ports={"1812/udp": "1812/udp"}, |             ports={"1812/udp": "1812/udp"}, | ||||||
|             environment={ |             environment={"AUTHENTIK_TOKEN": outpost.token.key}, | ||||||
|                 "AUTHENTIK_TOKEN": outpost.token.key, |  | ||||||
|             }, |  | ||||||
|         ) |         ) | ||||||
|  |         self.wait_for_outpost(outpost) | ||||||
|  |  | ||||||
|     def _prepare(self) -> User: |     def _prepare(self) -> User: | ||||||
|         """prepare user, provider, app and container""" |         """prepare user, provider, app and container""" | ||||||
|  |         self.user = create_test_user() | ||||||
|         radius: RadiusProvider = RadiusProvider.objects.create( |         radius: RadiusProvider = RadiusProvider.objects.create( | ||||||
|             name=generate_id(), |             name=generate_id(), | ||||||
|             authorization_flow=Flow.objects.get(slug="default-authentication-flow"), |             authorization_flow=Flow.objects.get(slug="default-authentication-flow"), | ||||||
| @ -50,17 +52,6 @@ class TestProviderRadius(SeleniumTestCase): | |||||||
|         outpost.providers.add(radius) |         outpost.providers.add(radius) | ||||||
|  |  | ||||||
|         self.start_radius(outpost) |         self.start_radius(outpost) | ||||||
|  |  | ||||||
|         # Wait until outpost healthcheck succeeds |  | ||||||
|         healthcheck_retries = 0 |  | ||||||
|         while healthcheck_retries < 50:  # noqa: PLR2004 |  | ||||||
|             if len(outpost.state) > 0: |  | ||||||
|                 state = outpost.state[0] |  | ||||||
|                 if state.last_seen: |  | ||||||
|                     break |  | ||||||
|             healthcheck_retries += 1 |  | ||||||
|             sleep(0.5) |  | ||||||
|         sleep(5) |  | ||||||
|         return outpost |         return outpost | ||||||
|  |  | ||||||
|     @retry() |     @retry() | ||||||
|  | |||||||
| @ -14,10 +14,12 @@ from authentik.policies.expression.models import ExpressionPolicy | |||||||
| from authentik.policies.models import PolicyBinding | from authentik.policies.models import PolicyBinding | ||||||
| from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider | from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider | ||||||
| from authentik.sources.saml.processors.constants import SAML_BINDING_POST | from authentik.sources.saml.processors.constants import SAML_BINDING_POST | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestProviderSAML(SeleniumTestCase): | class TestProviderSAML(DockerTestCase, SeleniumTestCase): | ||||||
|     """test SAML Provider flow""" |     """test SAML Provider flow""" | ||||||
|  |  | ||||||
|     def setup_client(self, provider: SAMLProvider, force_post: bool = False): |     def setup_client(self, provider: SAMLProvider, force_post: bool = False): | ||||||
|  | |||||||
| @ -11,10 +11,12 @@ from authentik.sources.ldap.models import LDAPSource, LDAPSourcePropertyMapping | |||||||
| from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer | from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer | ||||||
| from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer | from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer | ||||||
| from authentik.sources.ldap.sync.users import UserLDAPSynchronizer | from authentik.sources.ldap.sync.users import UserLDAPSynchronizer | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestSourceLDAPSamba(SeleniumTestCase): | class TestSourceLDAPSamba(DockerTestCase, SeleniumTestCase): | ||||||
|     """test LDAP Source""" |     """test LDAP Source""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|  | |||||||
| @ -16,7 +16,9 @@ from authentik.sources.oauth.models import OAuthSource | |||||||
| from authentik.sources.oauth.types.registry import SourceType, registry | from authentik.sources.oauth.types.registry import SourceType, registry | ||||||
| from authentik.sources.oauth.views.callback import OAuthCallback | from authentik.sources.oauth.views.callback import OAuthCallback | ||||||
| from authentik.stages.identification.models import IdentificationStage | from authentik.stages.identification.models import IdentificationStage | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class OAuth1Callback(OAuthCallback): | class OAuth1Callback(OAuthCallback): | ||||||
| @ -48,7 +50,7 @@ class OAUth1Type(SourceType): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestSourceOAuth1(SeleniumTestCase): | class TestSourceOAuth1(DockerTestCase, SeleniumTestCase): | ||||||
|     """Test OAuth1 Source""" |     """Test OAuth1 Source""" | ||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|  | |||||||
| @ -16,10 +16,12 @@ from authentik.flows.models import Flow | |||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.sources.oauth.models import OAuthSource | from authentik.sources.oauth.models import OAuthSource | ||||||
| from authentik.stages.identification.models import IdentificationStage | from authentik.stages.identification.models import IdentificationStage | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestSourceOAuth2(SeleniumTestCase): | class TestSourceOAuth2(DockerTestCase, SeleniumTestCase): | ||||||
|     """test OAuth Source flow""" |     """test OAuth Source flow""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|  | |||||||
| @ -16,7 +16,9 @@ from authentik.flows.models import Flow | |||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource | from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource | ||||||
| from authentik.stages.identification.models import IdentificationStage | from authentik.stages.identification.models import IdentificationStage | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
| IDP_CERT = """-----BEGIN CERTIFICATE----- | IDP_CERT = """-----BEGIN CERTIFICATE----- | ||||||
| MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV | MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV | ||||||
| @ -70,7 +72,7 @@ Sm75WXsflOxuTn08LbgGc4s= | |||||||
| -----END PRIVATE KEY-----""" | -----END PRIVATE KEY-----""" | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestSourceSAML(SeleniumTestCase): | class TestSourceSAML(DockerTestCase, SeleniumTestCase): | ||||||
|     """test SAML Source flow""" |     """test SAML Source flow""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|  | |||||||
| @ -8,12 +8,14 @@ from docker.types import Healthcheck | |||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.lib.utils.http import get_http_session | from authentik.lib.utils.http import get_http_session | ||||||
| from authentik.sources.scim.models import SCIMSource | from authentik.sources.scim.models import SCIMSource | ||||||
| from tests.e2e.utils import SeleniumTestCase, retry | from tests.browser import SeleniumTestCase | ||||||
|  | from tests.decorators import retry | ||||||
|  | from tests.docker import DockerTestCase | ||||||
|  |  | ||||||
| TEST_POLL_MAX = 25 | TEST_POLL_MAX = 25 | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestSourceSCIM(SeleniumTestCase): | class TestSourceSCIM(DockerTestCase, SeleniumTestCase): | ||||||
|     """test SCIM Source flow""" |     """test SCIM Source flow""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|  | |||||||
| Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB | 
| Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB | 
| @ -19,7 +19,7 @@ from authentik.outposts.models import ( | |||||||
| ) | ) | ||||||
| from authentik.outposts.tasks import outpost_connection_discovery | from authentik.outposts.tasks import outpost_connection_discovery | ||||||
| from authentik.providers.proxy.models import ProxyProvider | from authentik.providers.proxy.models import ProxyProvider | ||||||
| from tests.e2e.utils import DockerTestCase, get_docker_tag | from tests.docker import DockerTestCase, get_docker_tag | ||||||
|  |  | ||||||
|  |  | ||||||
| class OutpostDockerTests(DockerTestCase, ChannelsLiveServerTestCase): | class OutpostDockerTests(DockerTestCase, ChannelsLiveServerTestCase): | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ from authentik.outposts.models import ( | |||||||
| from authentik.outposts.tasks import outpost_connection_discovery | from authentik.outposts.tasks import outpost_connection_discovery | ||||||
| from authentik.providers.proxy.controllers.docker import DockerController | from authentik.providers.proxy.controllers.docker import DockerController | ||||||
| from authentik.providers.proxy.models import ProxyProvider | from authentik.providers.proxy.models import ProxyProvider | ||||||
| from tests.e2e.utils import DockerTestCase, get_docker_tag | from tests.docker import DockerTestCase, get_docker_tag | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestProxyDocker(DockerTestCase, ChannelsLiveServerTestCase): | class TestProxyDocker(DockerTestCase, ChannelsLiveServerTestCase): | ||||||
|  | |||||||
							
								
								
									
										52
									
								
								tests/websocket.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								tests/websocket.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | |||||||
|  | # This file cannot import anything django or anything that will load django | ||||||
|  | from sys import stderr | ||||||
|  |  | ||||||
|  | from channels.testing import ChannelsLiveServerTestCase | ||||||
|  | from daphne.testing import DaphneProcess | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from tests import IS_CI, get_local_ip | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def set_database_connection(): | ||||||
|  |     from django.conf import settings | ||||||
|  |  | ||||||
|  |     settings.DATABASES["default"]["NAME"] = settings.DATABASES["default"]["TEST"]["NAME"] | ||||||
|  |     settings.TEST = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DatabasePatchDaphneProcess(DaphneProcess): | ||||||
|  |     # See https://github.com/django/channels/issues/2048 | ||||||
|  |     # See https://github.com/django/channels/pull/2033 | ||||||
|  |  | ||||||
|  |     def __init__(self, host, get_application, kwargs=None, setup=None, teardown=None): | ||||||
|  |         super().__init__(host, get_application, kwargs, setup, teardown) | ||||||
|  |         self.setup = set_database_connection | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BaseWebsocketTestCase(ChannelsLiveServerTestCase): | ||||||
|  |     """Base channels test case""" | ||||||
|  |  | ||||||
|  |     host = get_local_ip() | ||||||
|  |     ProtocolServerProcess = DatabasePatchDaphneProcess | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class WebsocketTestCase(BaseWebsocketTestCase): | ||||||
|  |     """Test case to allow testing against a running Websocket/HTTP server""" | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         if IS_CI: | ||||||
|  |             print("::group::authentik Logs", file=stderr) | ||||||
|  |         from django.apps import apps | ||||||
|  |  | ||||||
|  |         from authentik.core.tests.utils import create_test_admin_user | ||||||
|  |  | ||||||
|  |         apps.get_app_config("authentik_tenants").ready() | ||||||
|  |         self.logger = get_logger() | ||||||
|  |         self.user = create_test_admin_user() | ||||||
|  |         super().setUp() | ||||||
|  |  | ||||||
|  |     def tearDown(self): | ||||||
|  |         if IS_CI: | ||||||
|  |             print("::endgroup::", file=stderr) | ||||||
|  |         super().tearDown() | ||||||
		Reference in New Issue
	
	Block a user
	