Compare commits

...

1 Commits

Author SHA1 Message Date
4b0d641a51 tests: better ws support
cherry-picked from https://github.com/goauthentik/authentik/pull/14539

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-05-18 00:45:55 +02:00
30 changed files with 406 additions and 353 deletions

View File

@ -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

View File

@ -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

View File

@ -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
View 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
View 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")

View File

@ -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()

View File

@ -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()

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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()

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -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"""

View File

@ -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()

View File

@ -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):

View File

@ -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):

View File

@ -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:

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -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):

View File

@ -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
View 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()