From 4f4e37f2b0370764424270effc90d42f2b2ecee3 Mon Sep 17 00:00:00 2001 From: "Jens L." Date: Sun, 28 Dec 2025 12:53:00 +0100 Subject: [PATCH] tests/e2e: add endpoint tests (#19072) * tests/e2e: add endpoint tests Signed-off-by: Jens Langhammer * dont rely on hostname Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- .github/workflows/ci-main.yml | 2 + tests/e2e/compose.yml | 4 +- tests/e2e/test_endpoints_flow.py | 64 ++++++++++++++++++++++++++++++++ tests/e2e/utils.py | 32 +++++++++++++++- 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/test_endpoints_flow.py diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 0242e1ce83..d6d39066e1 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -193,6 +193,8 @@ jobs: glob: tests/e2e/test_source_scim* - name: flows glob: tests/e2e/test_flows* + - name: endpoints + glob: tests/e2e/test_endpoints_* steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5 - name: Setup authentik env diff --git a/tests/e2e/compose.yml b/tests/e2e/compose.yml index 2ce4b4516e..1e5e88bc92 100644 --- a/tests/e2e/compose.yml +++ b/tests/e2e/compose.yml @@ -1,11 +1,13 @@ services: chromium: - image: docker.io/selenium/standalone-chromium:143.0 + image: ghcr.io/goauthentik/selenium:143.0-ak-0.35.3 shm_size: 2g network_mode: host restart: always extra_hosts: - "host.docker.internal:host-gateway" + labels: + - io.goauthentik.tests=selenium mailpit: image: docker.io/axllent/mailpit:v1.28.0 ports: diff --git a/tests/e2e/test_endpoints_flow.py b/tests/e2e/test_endpoints_flow.py new file mode 100644 index 0000000000..b20da75ec0 --- /dev/null +++ b/tests/e2e/test_endpoints_flow.py @@ -0,0 +1,64 @@ +"""test default login flow""" + +from authentik.blueprints.tests import apply_blueprint, reconcile_app +from authentik.crypto.apps import MANAGED_KEY +from authentik.crypto.models import CertificateKeyPair +from authentik.endpoints.connectors.agent.models import AgentConnector, EnrollmentToken +from authentik.endpoints.models import Device, EndpointStage, StageMode +from authentik.events.models import Event, EventAction +from authentik.flows.models import Flow, FlowStageBinding +from authentik.lib.generators import generate_id +from tests.e2e.utils import SeleniumTestCase, retry + + +class TestEndpointsFlow(SeleniumTestCase): + """test default login flow""" + + @reconcile_app("authentik_crypto") + def setUp(self): + super().setUp() + self.connector = AgentConnector.objects.create( + name=generate_id(), + challenge_key=CertificateKeyPair.objects.filter(managed=MANAGED_KEY).first(), + ) + self.enrollment_token = EnrollmentToken.objects.create( + name=generate_id(), connector=self.connector + ) + + @retry() + @apply_blueprint( + "default/flow-default-authentication-flow.yaml", + "default/flow-default-invalidation-flow.yaml", + ) + def test_login(self): + """test default login flow""" + rc, output = self.driver_container.exec_run( + ["ak-sysd", "domains", "join", "ak", "-a", self.live_server_url], + user="root", + environment={"AK_SYS_INSECURE_ENV_TOKEN": self.enrollment_token.key}, + ) + self.assertEqual(rc, 0, str(output)) + + dev = Device.objects.first() + self.assertIsNotNone(dev) + + stage = EndpointStage.objects.create( + name=generate_id(), connector=self.connector, mode=StageMode.REQUIRED + ) + FlowStageBinding.objects.create( + target=Flow.objects.get(slug="default-authentication-flow"), stage=stage, order=0 + ) + + self.driver.get( + self.url( + "authentik_core:if-flow", + flow_slug="default-authentication-flow", + ) + ) + self.login() + self.wait_for_url(self.if_user_url("/library")) + self.assert_user(self.user) + + login_evt = Event.objects.filter(action=EventAction.LOGIN).first() + self.assertIsNotNone(login_evt) + self.assertEqual(login_evt.context["device"]["pk"], dev.pk.hex) diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index cf298665ea..6e655e54c2 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -2,10 +2,12 @@ import socket from collections.abc import Callable -from functools import lru_cache, wraps +from functools import cached_property, lru_cache, wraps from json import JSONDecodeError, dumps, loads from os import environ, getenv +from pathlib import Path from sys import stderr +from tempfile import gettempdir from time import sleep from typing import Any from unittest.case import TestCase @@ -21,6 +23,7 @@ from docker import DockerClient, from_env from docker.errors import DockerException from docker.models.containers import Container from docker.models.networks import Network +from requests import RequestException from selenium import webdriver from selenium.common.exceptions import ( DetachedShadowRootException, @@ -43,6 +46,7 @@ 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.lib.utils.http import get_http_session from authentik.root.test_runner import get_docker_tag IS_CI = "CI" in environ @@ -179,6 +183,7 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase): opts = webdriver.ChromeOptions() opts.accept_insecure_certs = True opts.add_argument("--disable-search-engine-choice-screen") + opts.add_extension(self._get_chrome_extension()) # This breaks selenium when running remotely...? # opts.set_capability("goog:loggingPrefs", {"browser": "ALL"}) opts.add_experimental_option( @@ -200,6 +205,31 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase): count += 1 raise ValueError(f"Webdriver failed after {RETRIES}.") + def _get_chrome_extension(self): + path = Path(gettempdir()) / "ak-chrome.crx" + try: + self.logger.info("Downloading chrome extension...", path=path) + res = get_http_session().get( + "https://pkg.goauthentik.io/packages/authentik_browser-ext/browser-ext/authentik_chrome.zip", + stream=True, + ) + with open(path, "w+b") as _ext: + for chunk in res.iter_content(chunk_size=1024): + if chunk: + _ext.write(chunk) + except RequestException as exc: + if path.exists() and not IS_CI: + self.logger.info( + "Failed to download chrome extension, using cached copy", path=path + ) + return path + raise exc + return path + + @cached_property + def driver_container(self) -> Container: + return self.docker_client.containers.list(filters={"label": "io.goauthentik.tests"})[0] + def tearDown(self): if IS_CI: print("::endgroup::", file=stderr)