Compare commits

...

10 Commits

Author SHA1 Message Date
60000812fd patch email port always
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-17 16:46:42 +01:00
929e70669a bump healthcheck tries
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-16 02:19:57 +01:00
36114284bf retry on container failure
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-16 01:47:30 +01:00
a65ea0de94 fix rac missing in CI
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-16 01:23:42 +01:00
2056b0cbee add rac SSH tests
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-16 01:18:45 +01:00
af85ddf60b simplify waiting for outpost
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-16 01:04:25 +01:00
e8e42261e3 simplify and patch enterprise
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-16 01:02:14 +01:00
9fda4e91ad maybe use channels server for testing?
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-16 00:47:17 +01:00
a11f1258e1 fix exception for container failing to start not being raised
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-16 00:45:45 +01:00
a97578ac62 add test_runner option to not capture stdout
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-16 00:45:27 +01:00
10 changed files with 217 additions and 76 deletions

View File

@ -168,6 +168,8 @@ jobs:
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
- name: ldap
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
- name: rac
glob: tests/e2e/test_provider_rac*
- name: radius
glob: tests/e2e/test_provider_radius*
- name: scim

View File

@ -31,6 +31,8 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
if kwargs.get("randomly_seed", None):
self.args.append(f"--randomly-seed={kwargs['randomly_seed']}")
if kwargs.get("no_capture"):
self.args.append("-s")
settings.TEST = True
settings.CELERY["task_always_eager"] = True
@ -56,6 +58,10 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
def add_arguments(cls, parser: ArgumentParser):
"""Add more pytest-specific arguments"""
DiscoverRunner.add_arguments(parser)
parser.add_argument(
"--no-capture",
action="store_true",
)
parser.add_argument(
"--randomly-seed",
type=int,

44
tests/e2e/_process.py Normal file
View File

@ -0,0 +1,44 @@
"""authentik e2e testing utilities"""
from datetime import timedelta
from time import mktime
from unittest.mock import MagicMock, patch
from daphne.testing import DaphneProcess
from django import setup as django_setup
from django.conf import settings
from django.utils.timezone import now
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_id
class TestDatabaseProcess(DaphneProcess):
"""Channels does not correctly switch to the test database by default.
https://github.com/django/channels/issues/2048"""
def run(self):
if not settings.configured: # Fix For raise AppRegistryNotReady("Apps aren't loaded yet.")
django_setup() # Ensure Django is fully set up before using settings
if not settings.DATABASES[list(settings.DATABASES.keys())[0]]["NAME"].startswith("test_"):
for _, db_settings in settings.DATABASES.items():
db_settings["NAME"] = f"test_{db_settings['NAME']}"
settings.TEST = True
from authentik.enterprise.license import LicenseKey
with (
patch(
"authentik.enterprise.license.LicenseKey.validate",
MagicMock(
return_value=LicenseKey(
aud="",
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
name=generate_id(),
internal_users=100,
external_users=100,
)
),
),
CONFIG.patch("email.port", 1025),
):
return super().run()

View File

@ -1,7 +1,6 @@
"""LDAP and Outpost e2e tests"""
from dataclasses import asdict
from time import sleep
from guardian.shortcuts import assign_perm
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
@ -56,17 +55,7 @@ class TestProviderLDAP(SeleniumTestCase):
outpost.providers.add(ldap)
self.start_ldap(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.wait_for_outpost(outpost)
return outpost
@retry()

View File

@ -7,7 +7,6 @@ from sys import platform
from time import sleep
from unittest.case import skip, skipUnless
from channels.testing import ChannelsLiveServerTestCase
from jwt import decode
from selenium.webdriver.common.by import By
@ -87,17 +86,7 @@ class TestProviderProxy(SeleniumTestCase):
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.wait_for_outpost(outpost)
self.driver.get("http://localhost:9000/api")
self.login()
@ -168,17 +157,7 @@ class TestProviderProxy(SeleniumTestCase):
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.wait_for_outpost(outpost)
self.driver.get("http://localhost:9000/api")
self.login()
@ -202,7 +181,7 @@ class TestProviderProxy(SeleniumTestCase):
# TODO: Fix flaky test
@skip("Flaky test")
@skipUnless(platform.startswith("linux"), "requires local docker")
class TestProviderProxyConnect(ChannelsLiveServerTestCase):
class TestProviderProxyConnect(SeleniumTestCase):
"""Test Proxy connectivity over websockets"""
@retry(exceptions=[AssertionError])
@ -239,16 +218,7 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
)
outpost.providers.add(proxy)
outpost.build_user_permissions(outpost.user)
# 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 and state.version:
break
healthcheck_retries += 1
sleep(0.5)
self.wait_for_outpost(outpost)
state = outpost.state
self.assertGreaterEqual(len(state), 1)

View File

@ -76,17 +76,7 @@ class TestProviderProxyForward(SeleniumTestCase):
outpost.build_user_permissions(outpost.user)
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)
self.wait_for_outpost(outpost)
@retry()
def test_traefik(self):

View File

@ -0,0 +1,126 @@
"""RAC e2e tests"""
from datetime import timedelta
from time import mktime, sleep
from unittest.mock import MagicMock, patch
from django.utils.timezone import now
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from authentik.blueprints.tests import apply_blueprint, reconcile_app
from authentik.core.models import Application
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import License
from authentik.enterprise.providers.rac.models import Endpoint, Protocols, RACProvider
from authentik.flows.models import Flow
from authentik.lib.generators import generate_id
from authentik.outposts.models import Outpost, OutpostType
from tests.e2e.utils import SeleniumTestCase, retry
class TestProviderRAC(SeleniumTestCase):
"""RAC e2e tests"""
def setUp(self):
super().setUp()
self.password = generate_id()
def start_rac(self, outpost: Outpost):
"""Start rac container based on outpost created"""
self.run_container(
image=self.get_container_image("ghcr.io/goauthentik/dev-rac"),
environment={
"AUTHENTIK_TOKEN": outpost.token.key,
},
)
@patch(
"authentik.enterprise.license.LicenseKey.validate",
MagicMock(
return_value=LicenseKey(
aud="",
exp=int(mktime((now() + timedelta(days=3000)).timetuple())),
name=generate_id(),
internal_users=100,
external_users=100,
)
),
)
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"default/flow-default-provider-authorization-implicit-consent.yaml",
"default/flow-default-provider-invalidation.yaml",
)
@apply_blueprint(
"system/providers-rac.yaml",
)
@reconcile_app("authentik_crypto")
def test_rac_ssh(self):
"""Test SSH RAC"""
License.objects.create(key=generate_id())
test_ssh = self.run_container(
image="lscr.io/linuxserver/openssh-server:latest",
ports={
"2222": "2222",
},
environment={
"USER_NAME": "authentik",
"USER_PASSWORD": self.password,
"PASSWORD_ACCESS": "true",
"SUDO_ACCESS": "true",
},
)
rac: RACProvider = RACProvider.objects.create(
name=generate_id(),
authorization_flow=Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
),
)
endpoint = Endpoint.objects.create(
name=generate_id(),
protocol=Protocols.SSH,
host=f"{self.host}:2222",
settings={
"username": "authentik",
"password": self.password,
},
provider=rac,
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=rac)
outpost: Outpost = Outpost.objects.create(
name=generate_id(),
type=OutpostType.RAC,
)
outpost.providers.add(rac)
outpost.build_user_permissions(outpost.user)
self.start_rac(outpost)
self.wait_for_outpost(outpost)
self.driver.get(
self.url("authentik_providers_rac:start", app=app.slug, endpoint=endpoint.pk)
)
self.login()
sleep(1)
iface = self.driver.find_element(By.CSS_SELECTOR, "ak-rac")
sleep(5)
state = self.driver.execute_script("return arguments[0].clientState", iface)
self.assertEqual(state, 3)
uid = generate_id()
self.driver.find_element(By.CSS_SELECTOR, "body").send_keys(
f'echo "{uid}" > /tmp/test' + Keys.ENTER
)
sleep(2)
_, output = test_ssh.exec_run("cat /tmp/test")
self.assertEqual(output, f"{uid}\n".encode())

View File

@ -1,7 +1,6 @@
"""Radius e2e tests"""
from dataclasses import asdict
from time import sleep
from pyrad.client import Client
from pyrad.dictionary import Dictionary
@ -50,17 +49,7 @@ class TestProviderRadius(SeleniumTestCase):
outpost.providers.add(radius)
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)
self.wait_for_outpost(outpost)
return outpost
@retry()

View File

@ -419,7 +419,6 @@ class TestSourceSAML(SeleniumTestCase):
# Wait until we're logged in
self.wait_for_url(self.if_user_url())
# sleep(999999)
self.assert_user(
User.objects.exclude(username="akadmin")
.exclude(username__startswith="ak-outpost")

View File

@ -12,8 +12,8 @@ from typing import Any
from unittest.case import TestCase
from urllib.parse import urlencode
from channels.testing import ChannelsLiveServerTestCase
from django.apps import apps
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
@ -35,6 +35,8 @@ from authentik.core.api.users import UserSerializer
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
from authentik.outposts.models import Outpost
from tests.e2e._process import TestDatabaseProcess
RETRIES = int(environ.get("RETRIES", "3"))
IS_CI = "CI" in environ
@ -58,10 +60,13 @@ def get_local_ip() -> str:
return ip_addr
class ContainerException(Exception): ...
class DockerTestCase(TestCase):
"""Mixin for dealing with containers"""
max_healthcheck_attempts = 30
max_healthcheck_attempts = 50
__client: DockerClient
__network: Network
@ -95,7 +100,7 @@ class DockerTestCase(TestCase):
sleep(1)
attempt += 1
if attempt >= self.max_healthcheck_attempts:
self.failureException("Container failed to start")
raise ContainerException("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."""
@ -152,13 +157,17 @@ class DockerTestCase(TestCase):
self.__network.remove()
class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
class SeleniumTestCase(DockerTestCase, ChannelsLiveServerTestCase):
"""ChannelsLiveServerTestCase which automatically creates a Webdriver instance"""
ProtocolServerProcess = TestDatabaseProcess
host = get_local_ip()
wait_timeout: int
user: User
serve_static = True
def setUp(self):
if IS_CI:
print("::group::authentik Logs", file=stderr)
@ -265,6 +274,18 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
self.assertEqual(user["name"].value, expected_user.name)
self.assertEqual(user["email"].value, expected_user.email)
def wait_for_outpost(self, outpost: Outpost, tries=50):
"""Wait until outpost healthcheck succeeds"""
healthcheck_retries = 0
while healthcheck_retries < tries:
if len(outpost.state) > 0:
state = outpost.state[0]
if state.last_seen:
return
healthcheck_retries += 1
sleep(0.5)
raise ContainerException("Outpost failed to become healthy")
@lru_cache
def get_loader():
@ -277,7 +298,12 @@ def retry(max_retires=RETRIES, exceptions=None):
"""Retry test multiple times. Default to catching Selenium Timeout Exception"""
if not exceptions:
exceptions = [WebDriverException, TimeoutException, NoSuchElementException]
exceptions = [
WebDriverException,
TimeoutException,
NoSuchElementException,
ContainerException,
]
logger = get_logger()