diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 33a96f0055..5836da217f 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -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() }} diff --git a/Makefile b/Makefile index 5196921a98..a84c72fb38 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/pyproject.toml b/pyproject.toml index b05ace97e7..b592ad6292 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -278,6 +278,9 @@ omit = [ "website/", "docs/", ] +concurrency = ["thread","multiprocessing"] +parallel = true +sigterm = true [tool.coverage.report] sort = "Cover" diff --git a/tests/e2e/_process.py b/tests/e2e/_process.py new file mode 100644 index 0000000000..e00d0c9ffd --- /dev/null +++ b/tests/e2e/_process.py @@ -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() diff --git a/tests/e2e/test_provider_rac.py b/tests/e2e/test_provider_rac.py new file mode 100644 index 0000000000..5a332b45cc --- /dev/null +++ b/tests/e2e/test_provider_rac.py @@ -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()) diff --git a/tests/e2e/test_source_saml.py b/tests/e2e/test_source_saml.py index 0294431c3d..ca7dd62bda 100644 --- a/tests/e2e/test_source_saml.py +++ b/tests/e2e/test_source_saml.py @@ -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") diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index fa7e2afc5c..058f2fa4e2 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -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