providers/rac: add e2e tests (#21390)

* add test_runner option to not capture stdout

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix exception for container failing to start not being raised

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* maybe use channels server for testing?

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* simplify and patch enterprise

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* simplify waiting for outpost

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add rac SSH tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix rac missing in CI

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* retry on container failure

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* bump healthcheck tries

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* patch email port always

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fixup?

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix guardian cache

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* only build webui when using selenium

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* only use channels when needed

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix coverage and combine

based on https://github.com/django/channels/issues/2063#issuecomment-2067722400

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* dont even cache

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* test with delete_token_on_disconnect

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2026-04-05 18:07:31 +01:00
committed by GitHub
parent c93e0115d0
commit dc320df3a3
7 changed files with 188 additions and 4 deletions
+8 -1
View File
@@ -196,6 +196,7 @@ jobs:
- name: run integration
run: |
uv run coverage run manage.py test tests/integration
uv run coverage combine
uv run coverage xml
- uses: ./.github/actions/test-results
if: ${{ always() }}
@@ -223,6 +224,9 @@ jobs:
profiles: selenium
- name: ldap
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
- name: rac
glob: tests/e2e/test_provider_rac*
profiles: selenium
- name: ws-fed
glob: tests/e2e/test_provider_ws_fed*
profiles: selenium
@@ -247,11 +251,12 @@ jobs:
docker compose -f tests/e2e/compose.yml up -d --quiet-pull
- id: cache-web
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v4
if: contains(matrix.job.profiles, 'selenium')
with:
path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
- name: prepare web ui
if: steps.cache-web.outputs.cache-hit != 'true'
if: steps.cache-web.outputs.cache-hit != 'true' && contains(matrix.job.profiles, 'selenium')
working-directory: web
run: |
npm ci
@@ -260,6 +265,7 @@ jobs:
- name: run e2e
run: |
uv run coverage run manage.py test ${{ matrix.job.glob }}
uv run coverage combine
uv run coverage xml
- uses: ./.github/actions/test-results
if: ${{ always() }}
@@ -304,6 +310,7 @@ jobs:
- name: run conformance
run: |
uv run coverage run manage.py test ${{ matrix.job.glob }}
uv run coverage combine
uv run coverage xml
- uses: ./.github/actions/test-results
if: ${{ always() }}
+2
View File
@@ -74,6 +74,7 @@ rust-test: ## Run the Rust tests
test: ## Run the server tests and produce a coverage report (locally)
$(UV) run coverage run manage.py test --keepdb $(or $(filter-out $@,$(MAKECMDGOALS)),authentik)
$(UV) run coverage combine
$(UV) run coverage html
$(UV) run coverage report
@@ -344,5 +345,6 @@ ci-lint-clippy: ci--meta-debug
ci-test: ci--meta-debug
$(UV) run coverage run manage.py test --keepdb authentik
$(UV) run coverage combine
$(UV) run coverage report
$(UV) run coverage xml
+3
View File
@@ -278,6 +278,9 @@ omit = [
"website/",
"docs/",
]
concurrency = ["thread","multiprocessing"]
parallel = true
sigterm = true
[tool.coverage.report]
sort = "Cover"
+46
View File
@@ -0,0 +1,46 @@
"""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
from authentik.root.test_runner import patched__get_ct_cached
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),
patch("guardian.shortcuts._get_ct_cached", patched__get_ct_cached),
):
return super().run()
+107
View File
@@ -0,0 +1,107 @@
"""RAC e2e tests"""
from time import sleep
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.flows.models import Flow
from authentik.lib.generators import generate_id
from authentik.outposts.models import Outpost, OutpostType
from authentik.providers.rac.models import Endpoint, Protocols, RACProvider
from tests.e2e.utils import ChannelsSeleniumTestCase, retry
class TestProviderRAC(ChannelsSeleniumTestCase):
"""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,
},
)
@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"""
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"
),
delete_token_on_disconnect=True,
)
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.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())
-1
View File
@@ -393,7 +393,6 @@ class TestSourceSAML(SeleniumTestCase):
self.login_via_saml_source()
# sleep(999999)
self.assert_user(
User.objects.exclude(username="akadmin")
.exclude(username__startswith="ak-outpost")
+22 -2
View File
@@ -12,6 +12,7 @@ from time import sleep
from typing import Any
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
@@ -45,6 +46,7 @@ from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.utils.http import get_http_session
from authentik.tasks.test import use_test_broker
from tests.docker import DockerTestCase
from tests.e2e._process import TestDatabaseProcess
IS_CI = "CI" in environ
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
@@ -64,9 +66,11 @@ def get_local_ip(override=True) -> str:
return "0.0.0.0"
class E2ETestCase(DockerTestCase, StaticLiveServerTestCase):
class E2ETestMixin(DockerTestCase):
host = get_local_ip()
user: User
serve_static = True
ProtocolServerProcess = TestDatabaseProcess
def setUp(self):
if IS_CI:
@@ -94,7 +98,15 @@ class E2ETestCase(DockerTestCase, StaticLiveServerTestCase):
super().tearDown()
class SeleniumTestCase(E2ETestCase):
class E2ETestCase(E2ETestMixin, StaticLiveServerTestCase):
"""E2E Test case with django static live server"""
class ChannelsE2ETestCase(E2ETestMixin, ChannelsLiveServerTestCase):
"""E2E Test case with channels live server (websocket + static)"""
class SeleniumTestMixin(E2ETestMixin):
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
wait_timeout: int
@@ -436,6 +448,14 @@ class SeleniumTestCase(E2ETestCase):
)
class SeleniumTestCase(SeleniumTestMixin, StaticLiveServerTestCase):
"""Selenium Test case with django static live server"""
class ChannelsSeleniumTestCase(SeleniumTestMixin, ChannelsE2ETestCase):
"""Selenium Test case with channels live server (websocket + static)"""
@lru_cache
def get_loader():
"""Thin wrapper to lazily get a Migration Loader, only when it's needed