mirror of
https://github.com/goauthentik/authentik
synced 2026-04-25 17:15:26 +02:00
tests: refactor test harness to split apart a single file (#21391)
* re-instate previously flaky test Signed-off-by: Jens Langhammer <jens@goauthentik.io> * break up big file Signed-off-by: Jens Langhammer <jens@goauthentik.io> * move geoip data to subdir Signed-off-by: Jens Langhammer <jens@goauthentik.io> * i am but a weak man Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix ldap disconnect in testing Signed-off-by: Jens Langhammer <jens@goauthentik.io> * account for mismatched uid due to test server process Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
10
Makefile
10
Makefile
@@ -144,8 +144,14 @@ dev-create-db:
|
|||||||
dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
|
dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik PostgreSQL instance to a "fresh install" state.
|
||||||
|
|
||||||
update-test-mmdb: ## Update test GeoIP and ASN Databases
|
update-test-mmdb: ## Update test GeoIP and ASN Databases
|
||||||
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb -o ${PWD}/tests/GeoLite2-ASN-Test.mmdb
|
curl \
|
||||||
curl -L https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb -o ${PWD}/tests/GeoLite2-City-Test.mmdb
|
-L \
|
||||||
|
-o ${PWD}/tests/geoip/GeoLite2-ASN-Test.mmdb \
|
||||||
|
https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-ASN-Test.mmdb
|
||||||
|
curl \
|
||||||
|
-L \
|
||||||
|
-o ${PWD}/tests/geoip/GeoLite2-City-Test.mmdb \
|
||||||
|
https://raw.githubusercontent.com/maxmind/MaxMind-DB/refs/heads/main/test-data/GeoLite2-City-Test.mmdb
|
||||||
|
|
||||||
bump: ## Bump authentik version. Usage: make bump version=20xx.xx.xx
|
bump: ## Bump authentik version. Usage: make bump version=20xx.xx.xx
|
||||||
ifndef version
|
ifndef version
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pytest
|
|||||||
from cryptography.hazmat.backends.openssl.backend import backend
|
from cryptography.hazmat.backends.openssl.backend import backend
|
||||||
|
|
||||||
from authentik import authentik_full_version
|
from authentik import authentik_full_version
|
||||||
from tests.e2e.utils import get_local_ip
|
from tests.decorators import get_local_ip
|
||||||
|
|
||||||
IS_CI = "CI" in environ
|
IS_CI = "CI" in environ
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ class PytestTestRunner(DiscoverRunner): # pragma: no cover
|
|||||||
|
|
||||||
# Test-specific configuration
|
# Test-specific configuration
|
||||||
test_config = {
|
test_config = {
|
||||||
"events.context_processors.geoip": "tests/GeoLite2-City-Test.mmdb",
|
"events.context_processors.geoip": "tests/geoip/GeoLite2-City-Test.mmdb",
|
||||||
"events.context_processors.asn": "tests/GeoLite2-ASN-Test.mmdb",
|
"events.context_processors.asn": "tests/geoip/GeoLite2-ASN-Test.mmdb",
|
||||||
"blueprints_dir": "./blueprints",
|
"blueprints_dir": "./blueprints",
|
||||||
"outposts.container_image_base": f"ghcr.io/goauthentik/dev-%(type)s:{get_docker_tag()}",
|
"outposts.container_image_base": f"ghcr.io/goauthentik/dev-%(type)s:{get_docker_tag()}",
|
||||||
"tenants.enabled": False,
|
"tenants.enabled": False,
|
||||||
|
|||||||
@@ -15,5 +15,14 @@ func (fe *FlowExecutor) Session() *jwt.Token {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
t, _, _ := jwt.NewParser().ParseUnverified(sc.Value, &SessionCookieClaims{})
|
t, _, _ := jwt.NewParser().ParseUnverified(sc.Value, &SessionCookieClaims{})
|
||||||
|
// During testing the session cookie value is not a JWT but rather just the session ID
|
||||||
|
// in which case we wrap that in a pseudo-JWT
|
||||||
|
if t == nil {
|
||||||
|
return &jwt.Token{
|
||||||
|
Claims: &SessionCookieClaims{
|
||||||
|
SessionID: sc.Value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ func (ls *LDAPServer) handleWSSessionEnd(ctx context.Context, msg ak.Event) erro
|
|||||||
ls.log.Info("Disconnecting session due to session end event")
|
ls.log.Info("Disconnecting session due to session end event")
|
||||||
conn, ok := ls.connections[mmsg.SessionID]
|
conn, ok := ls.connections[mmsg.SessionID]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
ls.log.Warn("Could not disconnect session, connection not found")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
delete(ls.connections, mmsg.SessionID)
|
delete(ls.connections, mmsg.SessionID)
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ module = [
|
|||||||
"authentik.tenants.*",
|
"authentik.tenants.*",
|
||||||
"guardian.*",
|
"guardian.*",
|
||||||
"lifecycle.*",
|
"lifecycle.*",
|
||||||
|
"tests.*",
|
||||||
"tests.e2e.*",
|
"tests.e2e.*",
|
||||||
"tests.integration.*",
|
"tests.integration.*",
|
||||||
"tests.openid_conformance.*",
|
"tests.openid_conformance.*",
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ def generate_local_config() -> dict[str, Any]:
|
|||||||
"cert_discovery_dir": "./certs",
|
"cert_discovery_dir": "./certs",
|
||||||
"events": {
|
"events": {
|
||||||
"processors": {
|
"processors": {
|
||||||
"geoip": "tests/GeoLite2-City-Test.mmdb",
|
"geoip": "tests/geoip/GeoLite2-City-Test.mmdb",
|
||||||
"asn": "tests/GeoLite2-ASN-Test.mmdb",
|
"asn": "tests/geoip/GeoLite2-ASN-Test.mmdb",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
|
|||||||
77
tests/decorators.py
Normal file
77
tests/decorators.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import socket
|
||||||
|
from collections.abc import Callable
|
||||||
|
from functools import lru_cache, wraps
|
||||||
|
from os import environ, getenv
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
from django.db.migrations.loader import MigrationLoader
|
||||||
|
from django.test.testcases import TransactionTestCase
|
||||||
|
from selenium.common.exceptions import (
|
||||||
|
NoSuchElementException,
|
||||||
|
TimeoutException,
|
||||||
|
WebDriverException,
|
||||||
|
)
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
IS_CI = "CI" in environ
|
||||||
|
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
|
||||||
|
SHADOW_ROOT_RETRIES = 5
|
||||||
|
|
||||||
|
JSONType = dict[str, Any] | list[Any] | str | int | float | bool | None
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_ip(override=True) -> str:
|
||||||
|
"""Get the local machine's IP"""
|
||||||
|
if (local_ip := getenv("LOCAL_IP")) and override:
|
||||||
|
return local_ip
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
try:
|
||||||
|
return socket.gethostbyname(hostname)
|
||||||
|
except socket.gaierror:
|
||||||
|
return "0.0.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_loader():
|
||||||
|
"""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):
|
||||||
|
"""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
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
"""authentik e2e testing utilities"""
|
"""authentik e2e testing utilities"""
|
||||||
|
|
||||||
from os import environ
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.case import TestCase
|
from unittest.case import TestCase
|
||||||
@@ -13,8 +12,6 @@ from docker.models.networks import Network
|
|||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.root.test_runner import get_docker_tag
|
from authentik.root.test_runner import get_docker_tag
|
||||||
|
|
||||||
IS_CI = "CI" in environ
|
|
||||||
|
|
||||||
|
|
||||||
class DockerTestCase(TestCase):
|
class DockerTestCase(TestCase):
|
||||||
"""Mixin for dealing with containers"""
|
"""Mixin for dealing with containers"""
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ from authentik.endpoints.models import Device, EndpointStage, StageMode
|
|||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.models import Flow, FlowStageBinding
|
from authentik.flows.models import Flow, FlowStageBinding
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestEndpointsFlow(SeleniumTestCase):
|
class TestEndpointsFlow(SeleniumTestCase):
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ 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.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestFlowsAuthenticator(SeleniumTestCase):
|
class TestFlowsAuthenticator(SeleniumTestCase):
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ from authentik.stages.authenticator_webauthn.models import (
|
|||||||
WebAuthnDevice,
|
WebAuthnDevice,
|
||||||
)
|
)
|
||||||
from authentik.stages.identification.models import IdentificationStage
|
from authentik.stages.identification.models import IdentificationStage
|
||||||
|
from tests.decorators import retry
|
||||||
from tests.e2e.test_flows_login_sfe import login_sfe
|
from tests.e2e.test_flows_login_sfe import login_sfe
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestFlowsAuthenticatorWebAuthn(SeleniumTestCase):
|
class TestFlowsAuthenticatorWebAuthn(SeleniumTestCase):
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ 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.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestFlowsEnroll(SeleniumTestCase):
|
class TestFlowsEnroll(SeleniumTestCase):
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
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.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestFlowsLogin(SeleniumTestCase):
|
class TestFlowsLogin(SeleniumTestCase):
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ from authentik.blueprints.tests import apply_blueprint
|
|||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.flows.models import NotConfiguredAction
|
from authentik.flows.models import NotConfiguredAction
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
def login_sfe(driver: WebDriver, user: User):
|
def login_sfe(driver: WebDriver, user: User):
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ 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.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestFlowsRecovery(SeleniumTestCase):
|
class TestFlowsRecovery(SeleniumTestCase):
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ 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.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestFlowsStageSetup(SeleniumTestCase):
|
class TestFlowsStageSetup(SeleniumTestCase):
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"""LDAP and Outpost e2e tests"""
|
"""LDAP and Outpost e2e tests"""
|
||||||
|
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
|
from ldap3 import ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE, Connection, Server
|
||||||
from ldap3.core.exceptions import LDAPInvalidCredentialsResult
|
from ldap3.core.exceptions import LDAPInvalidCredentialsResult, LDAPSessionTerminatedByServerError
|
||||||
|
|
||||||
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import Application, AuthenticatedSession, User
|
||||||
from authentik.core.tests.utils import create_test_user
|
from authentik.core.tests.utils import create_test_user
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
@@ -14,12 +15,32 @@ 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 E2ETestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.live import ChannelsE2ETestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProviderLDAP(E2ETestCase):
|
def clean_response(response):
|
||||||
|
# Remove raw_attributes to make checking easier
|
||||||
|
for obj in response:
|
||||||
|
del obj["raw_attributes"]
|
||||||
|
del obj["raw_dn"]
|
||||||
|
obj["attributes"] = dict(obj["attributes"])
|
||||||
|
obj["attributes"].pop("uid", None)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class TestProviderLDAP(ChannelsE2ETestCase):
|
||||||
"""LDAP and Outpost e2e tests"""
|
"""LDAP and Outpost e2e tests"""
|
||||||
|
|
||||||
|
def assert_list_dict_equal(self, expected: list[dict], actual: list[dict], match_key="dn"):
|
||||||
|
"""Assert a list of dictionaries is identical, ignoring the ordering of items"""
|
||||||
|
self.assertEqual(len(expected), len(actual))
|
||||||
|
for res_item in actual:
|
||||||
|
all_matching = [x for x in expected if x[match_key] == res_item[match_key]]
|
||||||
|
self.assertEqual(len(all_matching), 1)
|
||||||
|
matching = all_matching[0]
|
||||||
|
self.assertDictEqual(res_item, matching)
|
||||||
|
|
||||||
def start_ldap(self, outpost: Outpost):
|
def start_ldap(self, outpost: Outpost):
|
||||||
"""Start ldap container based on outpost created"""
|
"""Start ldap container based on outpost created"""
|
||||||
self.run_container(
|
self.run_container(
|
||||||
@@ -209,12 +230,7 @@ class TestProviderLDAP(E2ETestCase):
|
|||||||
search_scope=SUBTREE,
|
search_scope=SUBTREE,
|
||||||
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
|
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
|
||||||
)
|
)
|
||||||
response: list = _connection.response
|
response = clean_response(_connection.response)
|
||||||
# Remove raw_attributes to make checking easier
|
|
||||||
for obj in response:
|
|
||||||
del obj["raw_attributes"]
|
|
||||||
del obj["raw_dn"]
|
|
||||||
obj["attributes"] = dict(obj["attributes"])
|
|
||||||
o_user = outpost.user
|
o_user = outpost.user
|
||||||
expected = [
|
expected = [
|
||||||
{
|
{
|
||||||
@@ -222,7 +238,6 @@ class TestProviderLDAP(E2ETestCase):
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"cn": o_user.username,
|
"cn": o_user.username,
|
||||||
"sAMAccountName": o_user.username,
|
"sAMAccountName": o_user.username,
|
||||||
"uid": o_user.uid,
|
|
||||||
"name": o_user.name,
|
"name": o_user.name,
|
||||||
"displayName": o_user.name,
|
"displayName": o_user.name,
|
||||||
"sn": o_user.name,
|
"sn": o_user.name,
|
||||||
@@ -253,7 +268,6 @@ class TestProviderLDAP(E2ETestCase):
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"cn": embedded_account.username,
|
"cn": embedded_account.username,
|
||||||
"sAMAccountName": embedded_account.username,
|
"sAMAccountName": embedded_account.username,
|
||||||
"uid": embedded_account.uid,
|
|
||||||
"name": embedded_account.name,
|
"name": embedded_account.name,
|
||||||
"displayName": embedded_account.name,
|
"displayName": embedded_account.name,
|
||||||
"sn": embedded_account.name,
|
"sn": embedded_account.name,
|
||||||
@@ -284,7 +298,6 @@ class TestProviderLDAP(E2ETestCase):
|
|||||||
"attributes": {
|
"attributes": {
|
||||||
"cn": self.user.username,
|
"cn": self.user.username,
|
||||||
"sAMAccountName": self.user.username,
|
"sAMAccountName": self.user.username,
|
||||||
"uid": self.user.uid,
|
|
||||||
"name": self.user.name,
|
"name": self.user.name,
|
||||||
"displayName": self.user.name,
|
"displayName": self.user.name,
|
||||||
"sn": self.user.name,
|
"sn": self.user.name,
|
||||||
@@ -353,19 +366,13 @@ class TestProviderLDAP(E2ETestCase):
|
|||||||
search_scope=SUBTREE,
|
search_scope=SUBTREE,
|
||||||
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
|
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
|
||||||
)
|
)
|
||||||
response: list = _connection.response
|
response = clean_response(_connection.response)
|
||||||
# Remove raw_attributes to make checking easier
|
|
||||||
for obj in response:
|
|
||||||
del obj["raw_attributes"]
|
|
||||||
del obj["raw_dn"]
|
|
||||||
obj["attributes"] = dict(obj["attributes"])
|
|
||||||
expected = [
|
expected = [
|
||||||
{
|
{
|
||||||
"dn": f"cn={user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
"dn": f"cn={user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"cn": user.username,
|
"cn": user.username,
|
||||||
"sAMAccountName": user.username,
|
"sAMAccountName": user.username,
|
||||||
"uid": user.uid,
|
|
||||||
"name": user.name,
|
"name": user.name,
|
||||||
"displayName": user.name,
|
"displayName": user.name,
|
||||||
"sn": user.name,
|
"sn": user.name,
|
||||||
@@ -397,15 +404,6 @@ class TestProviderLDAP(E2ETestCase):
|
|||||||
]
|
]
|
||||||
self.assert_list_dict_equal(expected, response)
|
self.assert_list_dict_equal(expected, response)
|
||||||
|
|
||||||
def assert_list_dict_equal(self, expected: list[dict], actual: list[dict], match_key="dn"):
|
|
||||||
"""Assert a list of dictionaries is identical, ignoring the ordering of items"""
|
|
||||||
self.assertEqual(len(expected), len(actual))
|
|
||||||
for res_item in actual:
|
|
||||||
all_matching = [x for x in expected if x[match_key] == res_item[match_key]]
|
|
||||||
self.assertEqual(len(all_matching), 1)
|
|
||||||
matching = all_matching[0]
|
|
||||||
self.assertDictEqual(res_item, matching)
|
|
||||||
|
|
||||||
@retry()
|
@retry()
|
||||||
@apply_blueprint(
|
@apply_blueprint(
|
||||||
"default/flow-default-authentication-flow.yaml",
|
"default/flow-default-authentication-flow.yaml",
|
||||||
@@ -469,11 +467,8 @@ class TestProviderLDAP(E2ETestCase):
|
|||||||
search_scope=SUBTREE,
|
search_scope=SUBTREE,
|
||||||
attributes=["cn"],
|
attributes=["cn"],
|
||||||
)
|
)
|
||||||
response: list = _connection.response
|
response = clean_response(_connection.response)
|
||||||
# Remove raw_attributes to make checking easier
|
|
||||||
for obj in response:
|
|
||||||
del obj["raw_attributes"]
|
|
||||||
del obj["raw_dn"]
|
|
||||||
o_user = outpost.user
|
o_user = outpost.user
|
||||||
self.assert_list_dict_equal(
|
self.assert_list_dict_equal(
|
||||||
[
|
[
|
||||||
@@ -501,3 +496,44 @@ class TestProviderLDAP(E2ETestCase):
|
|||||||
],
|
],
|
||||||
response,
|
response,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@retry()
|
||||||
|
@apply_blueprint(
|
||||||
|
"default/flow-default-authentication-flow.yaml",
|
||||||
|
"default/flow-default-invalidation-flow.yaml",
|
||||||
|
)
|
||||||
|
@reconcile_app("authentik_tenants")
|
||||||
|
@reconcile_app("authentik_outposts")
|
||||||
|
def test_ldap_bind_logout_search(self):
|
||||||
|
"""Test bind + session deletion -> failed search"""
|
||||||
|
self._prepare()
|
||||||
|
server = Server("ldap://localhost:3389", get_info=ALL)
|
||||||
|
_connection = Connection(
|
||||||
|
server,
|
||||||
|
raise_exceptions=True,
|
||||||
|
user=f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||||
|
password=self.user.username,
|
||||||
|
)
|
||||||
|
_connection.bind()
|
||||||
|
self.assertTrue(
|
||||||
|
Event.objects.filter(
|
||||||
|
action=EventAction.LOGIN,
|
||||||
|
user={
|
||||||
|
"pk": self.user.pk,
|
||||||
|
"email": self.user.email,
|
||||||
|
"username": self.user.username,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
c, _ = AuthenticatedSession.objects.filter(user_id=self.user.pk).delete()
|
||||||
|
self.assertGreaterEqual(c, 1)
|
||||||
|
# Give the sign out signal time to propagate
|
||||||
|
sleep(3)
|
||||||
|
|
||||||
|
with self.assertRaises(LDAPSessionTerminatedByServerError):
|
||||||
|
_connection.search(
|
||||||
|
"ou=Users,DC=ldaP,dc=goauthentik,dc=io",
|
||||||
|
"(objectClass=user)",
|
||||||
|
search_scope=SUBTREE,
|
||||||
|
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
|
||||||
|
)
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ from authentik.providers.oauth2.models import (
|
|||||||
RedirectURI,
|
RedirectURI,
|
||||||
RedirectURIMatchingMode,
|
RedirectURIMatchingMode,
|
||||||
)
|
)
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProviderOAuth2Github(SeleniumTestCase):
|
class TestProviderOAuth2Github(SeleniumTestCase):
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ from authentik.providers.oauth2.models import (
|
|||||||
RedirectURIMatchingMode,
|
RedirectURIMatchingMode,
|
||||||
ScopeMapping,
|
ScopeMapping,
|
||||||
)
|
)
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProviderOAuth2OAuth(SeleniumTestCase):
|
class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ from authentik.providers.oauth2.models import (
|
|||||||
RedirectURIMatchingMode,
|
RedirectURIMatchingMode,
|
||||||
ScopeMapping,
|
ScopeMapping,
|
||||||
)
|
)
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProviderOAuth2OIDC(SeleniumTestCase):
|
class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ from authentik.providers.oauth2.models import (
|
|||||||
RedirectURIMatchingMode,
|
RedirectURIMatchingMode,
|
||||||
ScopeMapping,
|
ScopeMapping,
|
||||||
)
|
)
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||||
|
|||||||
@@ -3,11 +3,8 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from json import dumps
|
from json import dumps
|
||||||
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
|
||||||
|
|
||||||
@@ -15,10 +12,11 @@ from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
|||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.flows.models import Flow
|
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 DockerServiceConnection, Outpost, OutpostConfig, OutpostType
|
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
|
||||||
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.decorators import retry
|
||||||
|
from tests.live import ChannelsE2ETestCase
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProviderProxy(SeleniumTestCase):
|
class TestProviderProxy(SeleniumTestCase):
|
||||||
@@ -215,10 +213,7 @@ class TestProviderProxy(SeleniumTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO: Fix flaky test
|
class TestProviderProxyConnect(ChannelsE2ETestCase):
|
||||||
@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])
|
||||||
@@ -232,7 +227,6 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
|||||||
@reconcile_app("authentik_crypto")
|
@reconcile_app("authentik_crypto")
|
||||||
def test_proxy_connectivity(self):
|
def test_proxy_connectivity(self):
|
||||||
"""Test proxy connectivity over websocket"""
|
"""Test proxy connectivity over websocket"""
|
||||||
outpost_connection_discovery()
|
|
||||||
proxy: ProxyProvider = ProxyProvider.objects.create(
|
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
authorization_flow=Flow.objects.get(
|
authorization_flow=Flow.objects.get(
|
||||||
@@ -246,16 +240,25 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
|||||||
proxy.save()
|
proxy.save()
|
||||||
# we need to create an application to actually access the proxy
|
# we need to create an application to actually access the proxy
|
||||||
Application.objects.create(name=generate_id(), slug=generate_id(), provider=proxy)
|
Application.objects.create(name=generate_id(), slug=generate_id(), provider=proxy)
|
||||||
service_connection = DockerServiceConnection.objects.get(local=True)
|
|
||||||
outpost: Outpost = Outpost.objects.create(
|
outpost: Outpost = Outpost.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
type=OutpostType.PROXY,
|
type=OutpostType.PROXY,
|
||||||
service_connection=service_connection,
|
|
||||||
_config=asdict(OutpostConfig(authentik_host=self.live_server_url, log_level="debug")),
|
_config=asdict(OutpostConfig(authentik_host=self.live_server_url, log_level="debug")),
|
||||||
)
|
)
|
||||||
outpost.providers.add(proxy)
|
outpost.providers.add(proxy)
|
||||||
outpost.build_user_permissions(outpost.user)
|
outpost.build_user_permissions(outpost.user)
|
||||||
|
|
||||||
|
self.run_container(
|
||||||
|
image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"),
|
||||||
|
ports={
|
||||||
|
"9000": "9000",
|
||||||
|
},
|
||||||
|
environment={
|
||||||
|
"AUTHENTIK_TOKEN": outpost.token.key,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# Wait until outpost healthcheck succeeds
|
# Wait until outpost healthcheck succeeds
|
||||||
healthcheck_retries = 0
|
healthcheck_retries = 0
|
||||||
while healthcheck_retries < 50: # noqa: PLR2004
|
while healthcheck_retries < 50: # noqa: PLR2004
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ 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.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProviderProxyForward(SeleniumTestCase):
|
class TestProviderProxyForward(SeleniumTestCase):
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ 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.rac.models import Endpoint, Protocols, RACProvider
|
from authentik.providers.rac.models import Endpoint, Protocols, RACProvider
|
||||||
from tests.e2e.utils import ChannelsSeleniumTestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.selenium import ChannelsSeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProviderRAC(ChannelsSeleniumTestCase):
|
class TestProviderRAC(ChannelsSeleniumTestCase):
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ 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 E2ETestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.live import E2ETestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProviderRadius(E2ETestCase):
|
class TestProviderRadius(E2ETestCase):
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ from authentik.lib.generators import generate_id
|
|||||||
from authentik.policies.expression.models import ExpressionPolicy
|
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 tests.e2e.utils import SeleniumTestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProviderSAML(SeleniumTestCase):
|
class TestProviderSAML(SeleniumTestCase):
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ from authentik.enterprise.providers.ws_federation.models import WSFederationProv
|
|||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.saml.models import SAMLPropertyMapping
|
from authentik.providers.saml.models import SAMLPropertyMapping
|
||||||
from tests.e2e.utils import SeleniumTestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProviderWSFed(SeleniumTestCase):
|
class TestProviderWSFed(SeleniumTestCase):
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ 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 authentik.tasks.models import Task
|
from authentik.tasks.models import Task
|
||||||
from tests.e2e.utils import E2ETestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.live import E2ETestCase
|
||||||
|
|
||||||
|
|
||||||
class TestSourceLDAPSamba(E2ETestCase):
|
class TestSourceLDAPSamba(E2ETestCase):
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ 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.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class OAuth1Callback(OAuthCallback):
|
class OAuth1Callback(OAuthCallback):
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ from pathlib import Path
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from docker.types import Healthcheck
|
from docker.types import Healthcheck
|
||||||
|
from selenium.common.exceptions import (
|
||||||
|
NoSuchElementException,
|
||||||
|
TimeoutException,
|
||||||
|
)
|
||||||
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.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
@@ -15,7 +19,8 @@ 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 NoSuchElementException, SeleniumTestCase, TimeoutException, retry
|
from tests.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
MAX_REFRESH_RETRIES = 5
|
MAX_REFRESH_RETRIES = 5
|
||||||
INTERFACE_TIMEOUT = 10
|
INTERFACE_TIMEOUT = 10
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ 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.decorators import retry
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
IDP_CERT = """-----BEGIN CERTIFICATE-----
|
IDP_CERT = """-----BEGIN CERTIFICATE-----
|
||||||
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ 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 E2ETestCase, retry
|
from tests.decorators import retry
|
||||||
|
from tests.live import E2ETestCase
|
||||||
|
|
||||||
TEST_POLL_MAX = 25
|
TEST_POLL_MAX = 25
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
54
tests/live.py
Normal file
54
tests/live.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from sys import stderr
|
||||||
|
|
||||||
|
from channels.testing import ChannelsLiveServerTestCase
|
||||||
|
from django.apps import apps
|
||||||
|
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||||
|
from dramatiq import get_broker
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.models import User
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
from authentik.tasks.test import use_test_broker
|
||||||
|
from tests._process import TestDatabaseProcess
|
||||||
|
from tests.decorators import IS_CI, get_local_ip
|
||||||
|
from tests.docker import DockerTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class E2ETestMixin(DockerTestCase):
|
||||||
|
host = get_local_ip()
|
||||||
|
user: User
|
||||||
|
serve_static = True
|
||||||
|
ProtocolServerProcess = TestDatabaseProcess
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
if IS_CI:
|
||||||
|
print("::group::authentik Logs", file=stderr)
|
||||||
|
apps.get_app_config("authentik_tenants").ready()
|
||||||
|
self.wait_timeout = 60
|
||||||
|
self.logger = get_logger()
|
||||||
|
self.user = create_test_admin_user()
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pre_setup(cls):
|
||||||
|
use_test_broker()
|
||||||
|
return super()._pre_setup()
|
||||||
|
|
||||||
|
def _post_teardown(self):
|
||||||
|
broker = get_broker()
|
||||||
|
broker.flush_all()
|
||||||
|
broker.close()
|
||||||
|
return super()._post_teardown()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if IS_CI:
|
||||||
|
print("::endgroup::", file=stderr)
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
|
||||||
|
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)"""
|
||||||
@@ -7,8 +7,8 @@ from selenium.webdriver.support import expected_conditions as ec
|
|||||||
|
|
||||||
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
from authentik.blueprints.tests import apply_blueprint, reconcile_app
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider
|
from authentik.providers.oauth2.models import OAuth2Provider
|
||||||
from tests.e2e.utils import SeleniumTestCase
|
|
||||||
from tests.openid_conformance.conformance import Conformance
|
from tests.openid_conformance.conformance import Conformance
|
||||||
|
from tests.selenium import SeleniumTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestOpenIDConformance(SeleniumTestCase):
|
class TestOpenIDConformance(SeleniumTestCase):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from tests.e2e.utils import retry
|
from tests.decorators import retry
|
||||||
from tests.openid_conformance.base import TestOpenIDConformance
|
from tests.openid_conformance.base import TestOpenIDConformance
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from tests.e2e.utils import retry
|
from tests.decorators import retry
|
||||||
from tests.openid_conformance.base import TestOpenIDConformance
|
from tests.openid_conformance.base import TestOpenIDConformance
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,15 @@
|
|||||||
"""authentik e2e testing utilities"""
|
"""authentik e2e testing utilities"""
|
||||||
|
|
||||||
import socket
|
from functools import cached_property
|
||||||
from collections.abc import Callable
|
|
||||||
from functools import cached_property, lru_cache, wraps
|
|
||||||
from json import JSONDecodeError, dumps, loads
|
from json import JSONDecodeError, dumps, loads
|
||||||
from os import environ, getenv
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from sys import stderr
|
|
||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Any
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from channels.testing import ChannelsLiveServerTestCase
|
|
||||||
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.models.containers import Container
|
from docker.models.containers import Container
|
||||||
from dramatiq import get_broker
|
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
from selenium.common.exceptions import (
|
from selenium.common.exceptions import (
|
||||||
@@ -38,72 +27,12 @@ from selenium.webdriver.remote.webdriver import WebDriver
|
|||||||
from selenium.webdriver.remote.webelement import WebElement
|
from selenium.webdriver.remote.webelement import WebElement
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
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 authentik.core.api.users import UserSerializer
|
from authentik.core.api.users import UserSerializer
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
|
||||||
from authentik.lib.utils.http import get_http_session
|
from authentik.lib.utils.http import get_http_session
|
||||||
from authentik.tasks.test import use_test_broker
|
from tests.decorators import IS_CI, RETRIES, SHADOW_ROOT_RETRIES, JSONType
|
||||||
from tests.docker import DockerTestCase
|
from tests.live import ChannelsE2ETestCase, E2ETestMixin
|
||||||
from tests.e2e._process import TestDatabaseProcess
|
|
||||||
|
|
||||||
IS_CI = "CI" in environ
|
|
||||||
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
|
|
||||||
SHADOW_ROOT_RETRIES = 5
|
|
||||||
|
|
||||||
JSONType = dict[str, Any] | list[Any] | str | int | float | bool | None
|
|
||||||
|
|
||||||
|
|
||||||
def get_local_ip(override=True) -> str:
|
|
||||||
"""Get the local machine's IP"""
|
|
||||||
if (local_ip := getenv("LOCAL_IP")) and override:
|
|
||||||
return local_ip
|
|
||||||
hostname = socket.gethostname()
|
|
||||||
try:
|
|
||||||
return socket.gethostbyname(hostname)
|
|
||||||
except socket.gaierror:
|
|
||||||
return "0.0.0.0"
|
|
||||||
|
|
||||||
|
|
||||||
class E2ETestMixin(DockerTestCase):
|
|
||||||
host = get_local_ip()
|
|
||||||
user: User
|
|
||||||
serve_static = True
|
|
||||||
ProtocolServerProcess = TestDatabaseProcess
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
if IS_CI:
|
|
||||||
print("::group::authentik Logs", file=stderr)
|
|
||||||
apps.get_app_config("authentik_tenants").ready()
|
|
||||||
self.wait_timeout = 60
|
|
||||||
self.logger = get_logger()
|
|
||||||
self.user = create_test_admin_user()
|
|
||||||
super().setUp()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pre_setup(cls):
|
|
||||||
use_test_broker()
|
|
||||||
return super()._pre_setup()
|
|
||||||
|
|
||||||
def _post_teardown(self):
|
|
||||||
broker = get_broker()
|
|
||||||
broker.flush_all()
|
|
||||||
broker.close()
|
|
||||||
return super()._post_teardown()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
if IS_CI:
|
|
||||||
print("::endgroup::", file=stderr)
|
|
||||||
super().tearDown()
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
class SeleniumTestMixin(E2ETestMixin):
|
||||||
@@ -454,48 +383,3 @@ class SeleniumTestCase(SeleniumTestMixin, StaticLiveServerTestCase):
|
|||||||
|
|
||||||
class ChannelsSeleniumTestCase(SeleniumTestMixin, ChannelsE2ETestCase):
|
class ChannelsSeleniumTestCase(SeleniumTestMixin, ChannelsE2ETestCase):
|
||||||
"""Selenium Test case with channels live server (websocket + static)"""
|
"""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
|
|
||||||
and only once"""
|
|
||||||
return MigrationLoader(connection)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
Reference in New Issue
Block a user