Compare commits
1 Commits
workspace-
...
tests/e2e/
Author | SHA1 | Date | |
---|---|---|---|
4b0d641a51 |
@ -11,7 +11,7 @@ from django.test.runner import DiscoverRunner
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.sentry import sentry_init
|
||||
from authentik.root.signals import post_startup, pre_startup, startup
|
||||
from tests.e2e.utils import get_docker_tag
|
||||
from tests.docker import get_docker_tag
|
||||
|
||||
# globally set maxDiff to none to show full assert error
|
||||
TestCase.maxDiff = None
|
||||
|
@ -0,0 +1,12 @@
|
||||
import socket
|
||||
from os import environ
|
||||
|
||||
IS_CI = "CI" in environ
|
||||
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
|
||||
|
||||
|
||||
def get_local_ip() -> str:
|
||||
"""Get the local machine's IP"""
|
||||
hostname = socket.gethostname()
|
||||
ip_addr = socket.gethostbyname(hostname)
|
||||
return ip_addr
|
||||
|
@ -1,29 +1,18 @@
|
||||
"""authentik e2e testing utilities"""
|
||||
|
||||
# This file cannot import anything django or anything that will load django
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache, wraps
|
||||
from os import environ
|
||||
from sys import stderr
|
||||
from time import sleep
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.case import TestCase
|
||||
from urllib.parse import urlencode
|
||||
|
||||
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
|
||||
from django.urls import reverse
|
||||
from docker import DockerClient, from_env
|
||||
from docker.errors import DockerException
|
||||
from docker.models.containers import Container
|
||||
from docker.models.networks import Network
|
||||
from selenium import webdriver
|
||||
from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException
|
||||
from selenium.common.exceptions import WebDriverException
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.remote.command import Command
|
||||
@ -33,137 +22,27 @@ from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
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 tests import IS_CI, RETRIES, get_local_ip
|
||||
from tests.websocket import BaseWebsocketTestCase
|
||||
|
||||
IS_CI = "CI" in environ
|
||||
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
|
||||
if TYPE_CHECKING:
|
||||
from authentik.core.models import User
|
||||
|
||||
|
||||
def get_docker_tag() -> str:
|
||||
"""Get docker-tag based off of CI variables"""
|
||||
env_pr_branch = "GITHUB_HEAD_REF"
|
||||
default_branch = "GITHUB_REF"
|
||||
branch_name = os.environ.get(default_branch, "main")
|
||||
if os.environ.get(env_pr_branch, "") != "":
|
||||
branch_name = os.environ[env_pr_branch]
|
||||
branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
|
||||
return f"gh-{branch_name}"
|
||||
|
||||
|
||||
def get_local_ip() -> str:
|
||||
"""Get the local machine's IP"""
|
||||
hostname = socket.gethostname()
|
||||
ip_addr = socket.gethostbyname(hostname)
|
||||
return ip_addr
|
||||
|
||||
|
||||
class DockerTestCase(TestCase):
|
||||
"""Mixin for dealing with containers"""
|
||||
|
||||
max_healthcheck_attempts = 30
|
||||
|
||||
__client: DockerClient
|
||||
__network: Network
|
||||
|
||||
__label_id = generate_id()
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.__client = from_env()
|
||||
self.__network = self.docker_client.networks.create(name=f"authentik-test-{generate_id()}")
|
||||
|
||||
@property
|
||||
def docker_client(self) -> DockerClient:
|
||||
return self.__client
|
||||
|
||||
@property
|
||||
def docker_network(self) -> Network:
|
||||
return self.__network
|
||||
|
||||
@property
|
||||
def docker_labels(self) -> dict:
|
||||
return {"io.goauthentik.test": self.__label_id}
|
||||
|
||||
def wait_for_container(self, container: Container):
|
||||
"""Check that container is health"""
|
||||
attempt = 0
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
attempt += 1
|
||||
if attempt >= self.max_healthcheck_attempts:
|
||||
self.failureException("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."""
|
||||
image = f"{base}:gh-main"
|
||||
try:
|
||||
branch_image = f"{base}:{get_docker_tag()}"
|
||||
self.docker_client.images.pull(branch_image)
|
||||
return branch_image
|
||||
except DockerException:
|
||||
self.docker_client.images.pull(image)
|
||||
return image
|
||||
|
||||
def run_container(self, **specs: dict[str, Any]) -> Container:
|
||||
if "network_mode" not in specs:
|
||||
specs["network"] = self.__network.name
|
||||
specs["labels"] = self.docker_labels
|
||||
specs["detach"] = True
|
||||
if hasattr(self, "live_server_url"):
|
||||
specs.setdefault("environment", {})
|
||||
specs["environment"]["AUTHENTIK_HOST"] = self.live_server_url
|
||||
container = self.docker_client.containers.run(**specs)
|
||||
container.reload()
|
||||
state = container.attrs.get("State", {})
|
||||
if "Health" not in state:
|
||||
return container
|
||||
self.wait_for_container(container)
|
||||
return container
|
||||
|
||||
def output_container_logs(self, container: Container | None = None):
|
||||
"""Output the container logs to our STDOUT"""
|
||||
if IS_CI:
|
||||
image = container.image
|
||||
tags = image.tags[0] if len(image.tags) > 0 else str(image)
|
||||
print(f"::group::Container logs - {tags}")
|
||||
for log in container.logs().decode().split("\n"):
|
||||
print(log)
|
||||
if IS_CI:
|
||||
print("::endgroup::")
|
||||
|
||||
def tearDown(self):
|
||||
containers: list[Container] = self.docker_client.containers.list(
|
||||
filters={"label": ",".join(f"{x}={y}" for x, y in self.docker_labels.items())}
|
||||
)
|
||||
for container in containers:
|
||||
self.output_container_logs(container)
|
||||
try:
|
||||
container.kill()
|
||||
except DockerException:
|
||||
pass
|
||||
try:
|
||||
container.remove(force=True)
|
||||
except DockerException:
|
||||
pass
|
||||
self.__network.remove()
|
||||
|
||||
|
||||
class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
|
||||
class BaseSeleniumTestCase(TestCase):
|
||||
"""Mixin which adds helpers for spinning up Selenium"""
|
||||
|
||||
host = get_local_ip()
|
||||
wait_timeout: int
|
||||
user: User
|
||||
user: "User"
|
||||
|
||||
def setUp(self):
|
||||
if IS_CI:
|
||||
print("::group::authentik Logs", file=stderr)
|
||||
from django.apps import apps
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
apps.get_app_config("authentik_tenants").ready()
|
||||
self.wait_timeout = 60
|
||||
self.driver = self._get_driver()
|
||||
@ -290,8 +169,10 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys(Keys.ENTER)
|
||||
sleep(1)
|
||||
|
||||
def assert_user(self, expected_user: User):
|
||||
def assert_user(self, expected_user: "User"):
|
||||
"""Check users/me API and assert it matches expected_user"""
|
||||
from authentik.core.api.users import UserSerializer
|
||||
|
||||
self.driver.get(self.url("authentik_api:user-me") + "?format=json")
|
||||
user_json = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
user = UserSerializer(data=json.loads(user_json)["user"])
|
||||
@ -301,46 +182,9 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
self.assertEqual(user["email"].value, expected_user.email)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_loader():
|
||||
"""Thin wrapper to lazily get a Migration Loader, only when it's needed
|
||||
and only once"""
|
||||
return MigrationLoader(connection)
|
||||
class SeleniumTestCase(BaseSeleniumTestCase, StaticLiveServerTestCase):
|
||||
"""Test case which spins up a selenium instance and a HTTP-only test server"""
|
||||
|
||||
|
||||
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
|
||||
class WebsocketSeleniumTestCase(BaseSeleniumTestCase, BaseWebsocketTestCase):
|
||||
"""Test case which spins up a selenium instance and a Websocket/HTTP test server"""
|
48
tests/decorators.py
Normal file
48
tests/decorators.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""authentik e2e testing utilities"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
|
||||
from django.test.testcases import TransactionTestCase
|
||||
from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from tests import RETRIES
|
||||
|
||||
|
||||
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
|
139
tests/docker.py
Normal file
139
tests/docker.py
Normal file
@ -0,0 +1,139 @@
|
||||
"""Docker testing helpers"""
|
||||
|
||||
import os
|
||||
from time import sleep
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from unittest.case import TestCase
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.errors import DockerException
|
||||
from docker.models.containers import Container
|
||||
from docker.models.networks import Network
|
||||
|
||||
from authentik.lib.generators import generate_id
|
||||
from tests import IS_CI
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.outposts.models import Outpost
|
||||
|
||||
|
||||
def get_docker_tag() -> str:
|
||||
"""Get docker-tag based off of CI variables"""
|
||||
env_pr_branch = "GITHUB_HEAD_REF"
|
||||
default_branch = "GITHUB_REF"
|
||||
branch_name = os.environ.get(default_branch, "main")
|
||||
if os.environ.get(env_pr_branch, "") != "":
|
||||
branch_name = os.environ[env_pr_branch]
|
||||
branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
|
||||
return f"gh-{branch_name}"
|
||||
|
||||
|
||||
class DockerTestCase(TestCase):
|
||||
"""Mixin for dealing with containers"""
|
||||
|
||||
max_healthcheck_attempts = 30
|
||||
|
||||
__client: DockerClient
|
||||
__network: Network
|
||||
|
||||
__label_id = generate_id()
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.__client = from_env()
|
||||
self.__network = self.docker_client.networks.create(
|
||||
name=f"authentik-test-{self.__label_id}"
|
||||
)
|
||||
super().setUp()
|
||||
|
||||
@property
|
||||
def docker_client(self) -> DockerClient:
|
||||
return self.__client
|
||||
|
||||
@property
|
||||
def docker_network(self) -> Network:
|
||||
return self.__network
|
||||
|
||||
@property
|
||||
def docker_labels(self) -> dict:
|
||||
return {"io.goauthentik.test": self.__label_id}
|
||||
|
||||
def get_container_image(self, base: str) -> str:
|
||||
"""Try to pull docker image based on git branch, fallback to main if not found."""
|
||||
image = f"{base}:gh-main"
|
||||
if not IS_CI:
|
||||
return image
|
||||
try:
|
||||
branch_image = f"{base}:{get_docker_tag()}"
|
||||
self.docker_client.images.pull(branch_image)
|
||||
return branch_image
|
||||
except DockerException:
|
||||
self.docker_client.images.pull(image)
|
||||
return image
|
||||
|
||||
def run_container(self, **specs: dict[str, Any]) -> Container:
|
||||
if "network_mode" not in specs:
|
||||
specs["network"] = self.__network.name
|
||||
specs["labels"] = self.docker_labels
|
||||
specs["detach"] = True
|
||||
if hasattr(self, "live_server_url"):
|
||||
specs.setdefault("environment", {})
|
||||
specs["environment"]["AUTHENTIK_HOST"] = self.live_server_url
|
||||
container = self.docker_client.containers.run(**specs)
|
||||
container.reload()
|
||||
state = container.attrs.get("State", {})
|
||||
if "Health" not in state:
|
||||
return container
|
||||
self.wait_for_container(container)
|
||||
return container
|
||||
|
||||
def output_container_logs(self, container: Container | None = None):
|
||||
"""Output the container logs to our STDOUT"""
|
||||
if IS_CI:
|
||||
image = container.image
|
||||
tags = image.tags[0] if len(image.tags) > 0 else str(image)
|
||||
print(f"::group::Container logs - {tags}")
|
||||
for log in container.logs().decode().split("\n"):
|
||||
print(log)
|
||||
if IS_CI:
|
||||
print("::endgroup::")
|
||||
|
||||
def tearDown(self):
|
||||
containers: list[Container] = self.docker_client.containers.list(
|
||||
filters={"label": ",".join(f"{x}={y}" for x, y in self.docker_labels.items())}
|
||||
)
|
||||
for container in containers:
|
||||
self.output_container_logs(container)
|
||||
try:
|
||||
container.stop()
|
||||
except DockerException:
|
||||
pass
|
||||
try:
|
||||
container.remove(force=True)
|
||||
except DockerException:
|
||||
pass
|
||||
self.__network.remove()
|
||||
super().tearDown()
|
||||
|
||||
def wait_for_container(self, container: Container):
|
||||
"""Check that container is health"""
|
||||
attempt = 0
|
||||
while attempt < self.max_healthcheck_attempts:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
attempt += 1
|
||||
sleep(0.5)
|
||||
self.failureException("Container failed to start")
|
||||
|
||||
def wait_for_outpost(self, outpost: "Outpost"):
|
||||
# Wait until outpost healthcheck succeeds
|
||||
attempt = 0
|
||||
while attempt < self.max_healthcheck_attempts:
|
||||
if len(outpost.state) > 0:
|
||||
state = outpost.state[0]
|
||||
if state.last_seen:
|
||||
return
|
||||
attempt += 1
|
||||
sleep(0.5)
|
||||
self.failureException("Outpost failed to become healthy")
|
@ -18,10 +18,12 @@ from authentik.stages.authenticator_static.models import (
|
||||
StaticToken,
|
||||
)
|
||||
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage, TOTPDevice
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestFlowsAuthenticator(SeleniumTestCase):
|
||||
class TestFlowsAuthenticator(DockerTestCase, SeleniumTestCase):
|
||||
"""test flow with otp stages"""
|
||||
|
||||
@retry()
|
||||
|
@ -11,10 +11,12 @@ from authentik.core.models import User
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestFlowsEnroll(SeleniumTestCase):
|
||||
class TestFlowsEnroll(DockerTestCase, SeleniumTestCase):
|
||||
"""Test Enroll flow"""
|
||||
|
||||
@retry()
|
||||
|
@ -2,10 +2,12 @@
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.flows.models import Flow
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestFlowsLogin(SeleniumTestCase):
|
||||
class TestFlowsLogin(DockerTestCase, SeleniumTestCase):
|
||||
"""test default login flow"""
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -6,10 +6,12 @@ from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestFlowsLoginSFE(SeleniumTestCase):
|
||||
class TestFlowsLoginSFE(DockerTestCase, SeleniumTestCase):
|
||||
"""test default login flow"""
|
||||
|
||||
def login(self):
|
||||
|
@ -13,10 +13,12 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestFlowsRecovery(SeleniumTestCase):
|
||||
class TestFlowsRecovery(DockerTestCase, SeleniumTestCase):
|
||||
"""Test Recovery flow"""
|
||||
|
||||
def initial_stages(self, user: User):
|
||||
|
@ -8,10 +8,12 @@ from authentik.core.models import User
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.stages.password.models import PasswordStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestFlowsStageSetup(SeleniumTestCase):
|
||||
class TestFlowsStageSetup(DockerTestCase, SeleniumTestCase):
|
||||
"""test stage setup flows"""
|
||||
|
||||
@retry()
|
||||
|
@ -16,10 +16,12 @@ from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
|
||||
from authentik.providers.ldap.models import APIAccessMode, LDAPProvider
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
from tests.websocket import WebsocketTestCase
|
||||
|
||||
|
||||
class TestProviderLDAP(SeleniumTestCase):
|
||||
class TestProviderLDAP(DockerTestCase, WebsocketTestCase):
|
||||
"""LDAP and Outpost e2e tests"""
|
||||
|
||||
def start_ldap(self, outpost: Outpost):
|
||||
|
@ -18,10 +18,12 @@ from authentik.providers.oauth2.models import (
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
)
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
class TestProviderOAuth2Github(DockerTestCase, SeleniumTestCase):
|
||||
"""test OAuth Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -26,10 +26,12 @@ from authentik.providers.oauth2.models import (
|
||||
RedirectURIMatchingMode,
|
||||
ScopeMapping,
|
||||
)
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
class TestProviderOAuth2OAuth(DockerTestCase, SeleniumTestCase):
|
||||
"""test OAuth with OAuth Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -26,10 +26,12 @@ from authentik.providers.oauth2.models import (
|
||||
RedirectURIMatchingMode,
|
||||
ScopeMapping,
|
||||
)
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
class TestProviderOAuth2OIDC(DockerTestCase, SeleniumTestCase):
|
||||
"""test OAuth with OpenID Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -26,10 +26,12 @@ from authentik.providers.oauth2.models import (
|
||||
RedirectURIMatchingMode,
|
||||
ScopeMapping,
|
||||
)
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
class TestProviderOAuth2OIDCImplicit(DockerTestCase, SeleniumTestCase):
|
||||
"""test OAuth with OpenID Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -3,11 +3,8 @@
|
||||
from base64 import b64encode
|
||||
from dataclasses import asdict
|
||||
from json import loads
|
||||
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
|
||||
|
||||
@ -18,10 +15,13 @@ from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostConfig, OutpostType
|
||||
from authentik.outposts.tasks import outpost_connection_discovery
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
from tests.websocket import WebsocketTestCase
|
||||
|
||||
|
||||
class TestProviderProxy(SeleniumTestCase):
|
||||
class TestProviderProxy(DockerTestCase, SeleniumTestCase):
|
||||
"""Proxy and Outpost e2e tests"""
|
||||
|
||||
def setUp(self):
|
||||
@ -37,13 +37,41 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
"""Start proxy container based on outpost created"""
|
||||
self.run_container(
|
||||
image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"),
|
||||
ports={
|
||||
"9000": "9000",
|
||||
},
|
||||
environment={
|
||||
"AUTHENTIK_TOKEN": outpost.token.key,
|
||||
},
|
||||
ports={"9000": "9000"},
|
||||
environment={"AUTHENTIK_TOKEN": outpost.token.key},
|
||||
)
|
||||
self.wait_for_outpost(outpost)
|
||||
|
||||
def _prepare(self):
|
||||
# set additionalHeaders to test later
|
||||
self.user.attributes["additionalHeaders"] = {"X-Foo": "bar"}
|
||||
self.user.save()
|
||||
|
||||
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"),
|
||||
internal_host=f"http://{self.host}",
|
||||
external_host="http://localhost:9000",
|
||||
basic_auth_enabled=True,
|
||||
basic_auth_user_attribute="basic-username",
|
||||
basic_auth_password_attribute="basic-password", # nosec
|
||||
)
|
||||
# Ensure OAuth2 Params are set
|
||||
proxy.set_oauth_defaults()
|
||||
proxy.save()
|
||||
# we need to create an application to actually access the proxy
|
||||
Application.objects.create(name=generate_id(), slug=generate_id(), provider=proxy)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name=generate_id(),
|
||||
type=OutpostType.PROXY,
|
||||
)
|
||||
outpost.providers.add(proxy)
|
||||
outpost.build_user_permissions(outpost.user)
|
||||
|
||||
self.start_proxy(outpost)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
@ -61,44 +89,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_proxy_simple(self):
|
||||
"""Test simple outpost setup with single provider"""
|
||||
# set additionalHeaders to test later
|
||||
self.user.attributes["additionalHeaders"] = {"X-Foo": "bar"}
|
||||
self.user.save()
|
||||
|
||||
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"),
|
||||
internal_host=f"http://{self.host}",
|
||||
external_host="http://localhost:9000",
|
||||
)
|
||||
# Ensure OAuth2 Params are set
|
||||
proxy.set_oauth_defaults()
|
||||
proxy.save()
|
||||
# we need to create an application to actually access the proxy
|
||||
Application.objects.create(name=generate_id(), slug=generate_id(), provider=proxy)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name=generate_id(),
|
||||
type=OutpostType.PROXY,
|
||||
)
|
||||
outpost.providers.add(proxy)
|
||||
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._prepare()
|
||||
self.driver.get("http://localhost:9000/api")
|
||||
self.login()
|
||||
sleep(1)
|
||||
@ -137,49 +128,13 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
@reconcile_app("authentik_crypto")
|
||||
def test_proxy_basic_auth(self):
|
||||
"""Test simple outpost setup with single provider"""
|
||||
self._prepare()
|
||||
# Setup basic auth
|
||||
cred = generate_id()
|
||||
attr = "basic-password" # nosec
|
||||
self.user.attributes["basic-username"] = cred
|
||||
self.user.attributes[attr] = cred
|
||||
self.user.attributes["basic-password"] = cred
|
||||
self.user.save()
|
||||
|
||||
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
invalidation_flow=Flow.objects.get(slug="default-provider-invalidation-flow"),
|
||||
internal_host=f"http://{self.host}",
|
||||
external_host="http://localhost:9000",
|
||||
basic_auth_enabled=True,
|
||||
basic_auth_user_attribute="basic-username",
|
||||
basic_auth_password_attribute=attr,
|
||||
)
|
||||
# Ensure OAuth2 Params are set
|
||||
proxy.set_oauth_defaults()
|
||||
proxy.save()
|
||||
# we need to create an application to actually access the proxy
|
||||
Application.objects.create(name=generate_id(), slug=generate_id(), provider=proxy)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name=generate_id(),
|
||||
type=OutpostType.PROXY,
|
||||
)
|
||||
outpost.providers.add(proxy)
|
||||
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.driver.get("http://localhost:9000/api")
|
||||
self.login()
|
||||
sleep(1)
|
||||
@ -187,9 +142,9 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
body = loads(full_body_text)
|
||||
|
||||
self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username])
|
||||
self.assertEqual(body.get("headers").get("X-Authentik-Username"), [self.user.username])
|
||||
auth_header = b64encode(f"{cred}:{cred}".encode()).decode()
|
||||
self.assertEqual(body["headers"]["Authorization"], [f"Basic {auth_header}"])
|
||||
self.assertEqual(body.get("headers").get("Authorization"), [f"Basic {auth_header}"])
|
||||
|
||||
self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out")
|
||||
sleep(2)
|
||||
@ -199,10 +154,7 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
self.assertIn("You've logged out of", title)
|
||||
|
||||
|
||||
# TODO: Fix flaky test
|
||||
@skip("Flaky test")
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
||||
class TestProviderProxyConnect(DockerTestCase, WebsocketTestCase):
|
||||
"""Test Proxy connectivity over websockets"""
|
||||
|
||||
@retry(exceptions=[AssertionError])
|
||||
@ -241,14 +193,7 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
||||
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)
|
||||
|
@ -13,10 +13,12 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.models import Outpost, OutpostType
|
||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestProviderProxyForward(SeleniumTestCase):
|
||||
class TestProviderProxyForward(DockerTestCase, SeleniumTestCase):
|
||||
"""Proxy and Outpost e2e tests"""
|
||||
|
||||
def setUp(self):
|
||||
@ -30,14 +32,11 @@ class TestProviderProxyForward(SeleniumTestCase):
|
||||
"""Start proxy container based on outpost created"""
|
||||
self.run_container(
|
||||
image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"),
|
||||
ports={
|
||||
"9000": "9000",
|
||||
},
|
||||
environment={
|
||||
"AUTHENTIK_TOKEN": outpost.token.key,
|
||||
},
|
||||
ports={"9000": "9000"},
|
||||
environment={"AUTHENTIK_TOKEN": outpost.token.key},
|
||||
name="ak-test-outpost",
|
||||
)
|
||||
self.wait_for_outpost(outpost)
|
||||
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
@ -77,17 +76,6 @@ class TestProviderProxyForward(SeleniumTestCase):
|
||||
|
||||
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)
|
||||
|
||||
@retry()
|
||||
def test_traefik(self):
|
||||
"""Test traefik"""
|
||||
|
@ -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
|
||||
@ -9,14 +8,17 @@ from pyrad.packet import AccessAccept, AccessReject, AccessRequest
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.outposts.models import Outpost, OutpostConfig, OutpostType
|
||||
from authentik.providers.radius.models import RadiusProvider
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
from tests.websocket import WebsocketTestCase
|
||||
|
||||
|
||||
class TestProviderRadius(SeleniumTestCase):
|
||||
class TestProviderRadius(DockerTestCase, WebsocketTestCase):
|
||||
"""Radius Outpost e2e tests"""
|
||||
|
||||
def setUp(self):
|
||||
@ -28,13 +30,13 @@ class TestProviderRadius(SeleniumTestCase):
|
||||
self.run_container(
|
||||
image=self.get_container_image("ghcr.io/goauthentik/dev-radius"),
|
||||
ports={"1812/udp": "1812/udp"},
|
||||
environment={
|
||||
"AUTHENTIK_TOKEN": outpost.token.key,
|
||||
},
|
||||
environment={"AUTHENTIK_TOKEN": outpost.token.key},
|
||||
)
|
||||
self.wait_for_outpost(outpost)
|
||||
|
||||
def _prepare(self) -> User:
|
||||
"""prepare user, provider, app and container"""
|
||||
self.user = create_test_user()
|
||||
radius: RadiusProvider = RadiusProvider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=Flow.objects.get(slug="default-authentication-flow"),
|
||||
@ -50,17 +52,6 @@ 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)
|
||||
return outpost
|
||||
|
||||
@retry()
|
||||
|
@ -14,10 +14,12 @@ from authentik.policies.expression.models import ExpressionPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.sources.saml.processors.constants import SAML_BINDING_POST
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestProviderSAML(SeleniumTestCase):
|
||||
class TestProviderSAML(DockerTestCase, SeleniumTestCase):
|
||||
"""test SAML Provider flow"""
|
||||
|
||||
def setup_client(self, provider: SAMLProvider, force_post: bool = False):
|
||||
|
@ -11,10 +11,12 @@ from authentik.sources.ldap.models import LDAPSource, LDAPSourcePropertyMapping
|
||||
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
|
||||
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
|
||||
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestSourceLDAPSamba(SeleniumTestCase):
|
||||
class TestSourceLDAPSamba(DockerTestCase, SeleniumTestCase):
|
||||
"""test LDAP Source"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -16,7 +16,9 @@ from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class OAuth1Callback(OAuthCallback):
|
||||
@ -48,7 +50,7 @@ class OAUth1Type(SourceType):
|
||||
}
|
||||
|
||||
|
||||
class TestSourceOAuth1(SeleniumTestCase):
|
||||
class TestSourceOAuth1(DockerTestCase, SeleniumTestCase):
|
||||
"""Test OAuth1 Source"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
|
@ -16,10 +16,12 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
|
||||
class TestSourceOAuth2(SeleniumTestCase):
|
||||
class TestSourceOAuth2(DockerTestCase, SeleniumTestCase):
|
||||
"""test OAuth Source flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -16,7 +16,9 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
IDP_CERT = """-----BEGIN CERTIFICATE-----
|
||||
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||
@ -70,7 +72,7 @@ Sm75WXsflOxuTn08LbgGc4s=
|
||||
-----END PRIVATE KEY-----"""
|
||||
|
||||
|
||||
class TestSourceSAML(SeleniumTestCase):
|
||||
class TestSourceSAML(DockerTestCase, SeleniumTestCase):
|
||||
"""test SAML Source flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -8,12 +8,14 @@ from docker.types import Healthcheck
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.browser import SeleniumTestCase
|
||||
from tests.decorators import retry
|
||||
from tests.docker import DockerTestCase
|
||||
|
||||
TEST_POLL_MAX = 25
|
||||
|
||||
|
||||
class TestSourceSCIM(SeleniumTestCase):
|
||||
class TestSourceSCIM(DockerTestCase, SeleniumTestCase):
|
||||
"""test SCIM Source flow"""
|
||||
|
||||
def setUp(self):
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@ -19,7 +19,7 @@ from authentik.outposts.models import (
|
||||
)
|
||||
from authentik.outposts.tasks import outpost_connection_discovery
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
from tests.e2e.utils import DockerTestCase, get_docker_tag
|
||||
from tests.docker import DockerTestCase, get_docker_tag
|
||||
|
||||
|
||||
class OutpostDockerTests(DockerTestCase, ChannelsLiveServerTestCase):
|
||||
|
@ -19,7 +19,7 @@ from authentik.outposts.models import (
|
||||
from authentik.outposts.tasks import outpost_connection_discovery
|
||||
from authentik.providers.proxy.controllers.docker import DockerController
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
from tests.e2e.utils import DockerTestCase, get_docker_tag
|
||||
from tests.docker import DockerTestCase, get_docker_tag
|
||||
|
||||
|
||||
class TestProxyDocker(DockerTestCase, ChannelsLiveServerTestCase):
|
||||
|
52
tests/websocket.py
Normal file
52
tests/websocket.py
Normal file
@ -0,0 +1,52 @@
|
||||
# This file cannot import anything django or anything that will load django
|
||||
from sys import stderr
|
||||
|
||||
from channels.testing import ChannelsLiveServerTestCase
|
||||
from daphne.testing import DaphneProcess
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from tests import IS_CI, get_local_ip
|
||||
|
||||
|
||||
def set_database_connection():
|
||||
from django.conf import settings
|
||||
|
||||
settings.DATABASES["default"]["NAME"] = settings.DATABASES["default"]["TEST"]["NAME"]
|
||||
settings.TEST = True
|
||||
|
||||
|
||||
class DatabasePatchDaphneProcess(DaphneProcess):
|
||||
# See https://github.com/django/channels/issues/2048
|
||||
# See https://github.com/django/channels/pull/2033
|
||||
|
||||
def __init__(self, host, get_application, kwargs=None, setup=None, teardown=None):
|
||||
super().__init__(host, get_application, kwargs, setup, teardown)
|
||||
self.setup = set_database_connection
|
||||
|
||||
|
||||
class BaseWebsocketTestCase(ChannelsLiveServerTestCase):
|
||||
"""Base channels test case"""
|
||||
|
||||
host = get_local_ip()
|
||||
ProtocolServerProcess = DatabasePatchDaphneProcess
|
||||
|
||||
|
||||
class WebsocketTestCase(BaseWebsocketTestCase):
|
||||
"""Test case to allow testing against a running Websocket/HTTP server"""
|
||||
|
||||
def setUp(self):
|
||||
if IS_CI:
|
||||
print("::group::authentik Logs", file=stderr)
|
||||
from django.apps import apps
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
apps.get_app_config("authentik_tenants").ready()
|
||||
self.logger = get_logger()
|
||||
self.user = create_test_admin_user()
|
||||
super().setUp()
|
||||
|
||||
def tearDown(self):
|
||||
if IS_CI:
|
||||
print("::endgroup::", file=stderr)
|
||||
super().tearDown()
|
@ -3,11 +3,13 @@
|
||||
* @import { StorybookConfig } from "@storybook/web-components-vite";
|
||||
* @import { InlineConfig, Plugin } from "vite";
|
||||
*/
|
||||
import { createBundleDefinitions } from "@goauthentik/web/scripts/esbuild/environment";
|
||||
import { cwd } from "process";
|
||||
import postcssLit from "rollup-plugin-postcss-lit";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
const CSSImportPattern = /import [\w$]+ from .+\.(css)/g;
|
||||
const NODE_ENV = process.env.NODE_ENV || "development";
|
||||
|
||||
const CSSImportPattern = /import [\w\$]+ from .+\.(css)/g;
|
||||
const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/;
|
||||
|
||||
/**
|
||||
@ -52,7 +54,11 @@ const config = {
|
||||
*/
|
||||
const mergedConfig = {
|
||||
...config,
|
||||
define: createBundleDefinitions(),
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(NODE_ENV),
|
||||
"process.env.CWD": JSON.stringify(cwd()),
|
||||
"process.env.AK_API_BASE_PATH": JSON.stringify(process.env.AK_API_BASE_PATH || ""),
|
||||
},
|
||||
plugins: [inlineCSSPlugin, ...plugins, postcssLit(), tsconfigPaths()],
|
||||
};
|
||||
|
||||
|
@ -21,7 +21,7 @@ const log = console.debug.bind(console, logPrefix);
|
||||
* ESBuild may tree-shake it out of production builds.
|
||||
*
|
||||
* ```ts
|
||||
* if (import.meta.env.NODE_ENV=== "development") {
|
||||
* if (process.env.NODE_ENV === "development") {
|
||||
* await import("@goauthentik/esbuild-plugin-live-reload/client")
|
||||
* .catch(() => console.warn("Failed to import watcher"))
|
||||
* }
|
||||
|
@ -4,20 +4,15 @@
|
||||
|
||||
export {};
|
||||
declare global {
|
||||
/**
|
||||
* Environment variables injected by ESBuild.
|
||||
*/
|
||||
interface ImportMetaEnv {
|
||||
/**
|
||||
* The injected watcher URL for ESBuild.
|
||||
* This is used for live reloading in development mode.
|
||||
*
|
||||
* @format url
|
||||
*/
|
||||
readonly ESBUILD_WATCHER_URL?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
readonly env: {
|
||||
/**
|
||||
* The injected watcher URL for ESBuild.
|
||||
* This is used for live reloading in development mode.
|
||||
*
|
||||
* @format url
|
||||
*/
|
||||
ESBUILD_WATCHER_URL: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,6 @@
|
||||
/**
|
||||
* @file Utility functions for working with environment variables.
|
||||
* @file Utility functions for building and copying files.
|
||||
*/
|
||||
/// <reference types="./types/global.js" />
|
||||
|
||||
//#region Constants
|
||||
|
||||
/**
|
||||
* The current Node.js environment, defaulting to "development" when not set.
|
||||
*
|
||||
* Note, this should only be used during the build process.
|
||||
*
|
||||
* If you need to check the environment at runtime, use `process.env.NODE_ENV` to
|
||||
* ensure that module tree-shaking works correctly.
|
||||
*
|
||||
*/
|
||||
export const NodeEnvironment = process.env.NODE_ENV || "development";
|
||||
|
||||
/**
|
||||
* A source environment variable, which can be a string, number, boolean, null, or undefined.
|
||||
@ -28,26 +14,19 @@ export const NodeEnvironment = process.env.NODE_ENV || "development";
|
||||
* @typedef {T extends string ? `"${T}"` : T} JSONify
|
||||
*/
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Utilities
|
||||
|
||||
/**
|
||||
* Given an object of environment variables, returns a new object with the same keys and values, but
|
||||
* with the values serialized as strings.
|
||||
*
|
||||
* @template {Record<string, EnvironmentVariable>} EnvRecord
|
||||
* @template {string} [Prefix='import.meta.env.']
|
||||
* @template {string} [Prefix='process.env.']
|
||||
*
|
||||
* @param {EnvRecord} input
|
||||
* @param {Prefix} [prefix='import.meta.env.']
|
||||
* @param {Prefix} [prefix='process.env.']
|
||||
*
|
||||
* @returns {{[K in keyof EnvRecord as `${Prefix}${K}`]: JSONify<EnvRecord[K]>}}
|
||||
*/
|
||||
export function serializeEnvironmentVars(
|
||||
input,
|
||||
prefix = /** @type {Prefix} */ ("import.meta.env."),
|
||||
) {
|
||||
export function serializeEnvironmentVars(input, prefix = /** @type {Prefix} */ ("process.env.")) {
|
||||
/**
|
||||
* @type {Record<string, string>}
|
||||
*/
|
||||
@ -61,5 +40,3 @@ export function serializeEnvironmentVars(
|
||||
|
||||
return /** @type {any} */ (env);
|
||||
}
|
||||
|
||||
//#endregion
|
16
web/packages/monorepo/constants.js
Normal file
16
web/packages/monorepo/constants.js
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @file Constants for JavaScript and TypeScript files.
|
||||
*/
|
||||
|
||||
/// <reference types="../../types/global.js" />
|
||||
|
||||
/**
|
||||
* The current Node.js environment, defaulting to "development" when not set.
|
||||
*
|
||||
* Note, this should only be used during the build process.
|
||||
*
|
||||
* If you need to check the environment at runtime, use `process.env.NODE_ENV` to
|
||||
* ensure that module tree-shaking works correctly.
|
||||
*
|
||||
*/
|
||||
export const NodeEnvironment = process.env.NODE_ENV || "development";
|
@ -1,6 +1,7 @@
|
||||
/// <reference types="./types/global.js" />
|
||||
|
||||
export * from "./paths.js";
|
||||
export * from "./environment.js";
|
||||
export * from "./constants.js";
|
||||
export * from "./build.js";
|
||||
export * from "./version.js";
|
||||
export * from "./scripting.js";
|
||||
|
@ -1,32 +1,17 @@
|
||||
import { spawnSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import process from "process";
|
||||
|
||||
/**
|
||||
* @file Lit Localize build script.
|
||||
*
|
||||
* @remarks
|
||||
* Determines if all the Xliff translation source files are present and if the Typescript source files generated from those sources are up-to-date.
|
||||
*
|
||||
* If they are not, it runs the locale building script,
|
||||
* intercepting the long spew of "this string is not translated" and replacing it with a
|
||||
* Determines if all the Xliff translation source files are present and if the Typescript source
|
||||
* files generated from those sources are up-to-date. If they are not, it runs the locale building
|
||||
* script, intercepting the long spew of "this string is not translated" and replacing it with a
|
||||
* summary of how many strings are missing with respect to the source locale.
|
||||
*
|
||||
* @import { ConfigFile } from "@lit/localize-tools/lib/types/config"
|
||||
*/
|
||||
import { PackageRoot } from "@goauthentik/web/paths";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* @type {ConfigFile}
|
||||
*/
|
||||
const localizeRules = JSON.parse(
|
||||
readFileSync(path.join(PackageRoot, "lit-localize.json"), "utf-8"),
|
||||
);
|
||||
const localizeRules = JSON.parse(fs.readFileSync("./lit-localize.json", "utf-8"));
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} loc
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function generatedFileIsUpToDateWithXliffSource(loc) {
|
||||
const xliff = path.join("./xliff", `${loc}.xlf`);
|
||||
const gened = path.join("./src/locales", `${loc}.ts`);
|
||||
@ -37,7 +22,7 @@ function generatedFileIsUpToDateWithXliffSource(loc) {
|
||||
// generates a unique error message and halts the build.
|
||||
|
||||
try {
|
||||
var xlfStat = statSync(xliff);
|
||||
var xlfStat = fs.statSync(xliff);
|
||||
} catch (_error) {
|
||||
console.error(`lit-localize expected '${loc}.xlf', but XLF file is not present`);
|
||||
process.exit(1);
|
||||
@ -45,7 +30,7 @@ function generatedFileIsUpToDateWithXliffSource(loc) {
|
||||
|
||||
// If the generated file doesn't exist, of course it's not up to date.
|
||||
try {
|
||||
var genedStat = statSync(gened);
|
||||
var genedStat = fs.statSync(gened);
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
/// <reference types="../types/esbuild.js" />
|
||||
/**
|
||||
* @file ESBuild script for building the authentik web UI.
|
||||
*
|
||||
@ -10,6 +9,7 @@ import {
|
||||
NodeEnvironment,
|
||||
readBuildIdentifier,
|
||||
resolvePackage,
|
||||
serializeEnvironmentVars,
|
||||
} from "@goauthentik/monorepo";
|
||||
import { DistDirectory, DistDirectoryName, EntryPoint, PackageRoot } from "@goauthentik/web/paths";
|
||||
import { deepmerge } from "deepmerge-ts";
|
||||
@ -20,10 +20,15 @@ import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
|
||||
import { createBundleDefinitions } from "./esbuild/environment.mjs";
|
||||
|
||||
const logPrefix = "[Build]";
|
||||
|
||||
const definitions = serializeEnvironmentVars({
|
||||
NODE_ENV: NodeEnvironment,
|
||||
CWD: process.cwd(),
|
||||
AK_API_BASE_PATH: process.env.AK_API_BASE_PATH,
|
||||
});
|
||||
|
||||
const patternflyPath = resolvePackage("@patternfly/patternfly");
|
||||
|
||||
/**
|
||||
@ -81,7 +86,7 @@ const BASE_ESBUILD_OPTIONS = {
|
||||
root: MonoRepoRoot,
|
||||
}),
|
||||
],
|
||||
define: createBundleDefinitions(),
|
||||
define: definitions,
|
||||
format: "esm",
|
||||
logOverride: {
|
||||
/**
|
||||
|
@ -1,29 +0,0 @@
|
||||
/**
|
||||
* @file ESBuild environment utilities.
|
||||
*/
|
||||
import { AuthentikVersion, NodeEnvironment, serializeEnvironmentVars } from "@goauthentik/monorepo";
|
||||
|
||||
/**
|
||||
* Creates a mapping of environment variables to their respective runtime constants.
|
||||
*/
|
||||
export function createBundleDefinitions() {
|
||||
const SerializedNodeEnvironment = /** @type {`"development"` | `"production"`} */ (
|
||||
JSON.stringify(NodeEnvironment)
|
||||
);
|
||||
|
||||
/**
|
||||
* @satisfies {Record<ESBuildImportEnvKey, string>}
|
||||
*/
|
||||
const envRecord = {
|
||||
AK_VERSION: AuthentikVersion,
|
||||
AK_API_BASE_PATH: process.env.AK_API_BASE_PATH ?? "",
|
||||
};
|
||||
|
||||
return {
|
||||
...serializeEnvironmentVars(envRecord),
|
||||
// We need to explicitly set this for NPM packages that use `process`
|
||||
// to determine their environment.
|
||||
"process.env.NODE_ENV": SerializedNodeEnvironment,
|
||||
"import.meta.env.NODE_ENV": SerializedNodeEnvironment,
|
||||
};
|
||||
}
|
@ -35,11 +35,6 @@ const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
const projectRoot = path.join(__dirname, "..");
|
||||
process.chdir(projectRoot);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string[]} flags
|
||||
* @returns
|
||||
*/
|
||||
const hasFlag = (flags) => process.argv.length > 1 && flags.includes(process.argv[2]);
|
||||
|
||||
const [configFile, files] = hasFlag(["-n", "--nightmare"])
|
||||
|
@ -1,36 +1,22 @@
|
||||
/**
|
||||
* @file Pseudo-localization script.
|
||||
*
|
||||
* @import { ConfigFile } from "@lit/localize-tools/lib/types/config.js"
|
||||
* @import { Config } from '@lit/localize-tools/lib/types/config.js';
|
||||
* @import { ProgramMessage } from "@lit/localize-tools/src/messages.js"
|
||||
* @import { Locale } from "@lit/localize-tools/src/types/locale.js"
|
||||
*/
|
||||
import { PackageRoot } from "@goauthentik/web/paths";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
import pseudolocale from "pseudolocale";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import { makeFormatter } from "@lit/localize-tools/lib/formatters/index.js";
|
||||
import { sortProgramMessages } from "@lit/localize-tools/lib/messages.js";
|
||||
import { TransformLitLocalizer } from "@lit/localize-tools/lib/modes/transform.js";
|
||||
|
||||
const pseudoLocale = /** @type {Locale} */ ("pseudo-LOCALE");
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
const pseudoLocale = "pseudo-LOCALE";
|
||||
const targetLocales = [pseudoLocale];
|
||||
|
||||
/**
|
||||
* @type {ConfigFile}
|
||||
*/
|
||||
const baseConfig = JSON.parse(readFileSync(path.join(PackageRoot, "lit-localize.json"), "utf-8"));
|
||||
const baseConfig = JSON.parse(readFileSync(path.join(__dirname, "../lit-localize.json"), "utf-8"));
|
||||
|
||||
// Need to make some internal specifications to satisfy the transformer. It doesn't actually matter
|
||||
// which Localizer we use (transformer or runtime), because all of the functionality we care about
|
||||
// is in their common parent class, but I had to pick one. Everything else here is just pure
|
||||
// exploitation of the lit/localize-tools internals.
|
||||
|
||||
/**
|
||||
* @satisfies {Config}
|
||||
*/
|
||||
const config = {
|
||||
...baseConfig,
|
||||
baseDir: path.join(__dirname, ".."),
|
||||
@ -42,11 +28,6 @@ const config = {
|
||||
resolve: (path) => path,
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ProgramMessage} message
|
||||
* @returns
|
||||
*/
|
||||
const pseudoMessagify = (message) => ({
|
||||
name: message.name,
|
||||
contents: message.contents.map((content) =>
|
||||
@ -55,7 +36,7 @@ const pseudoMessagify = (message) => ({
|
||||
});
|
||||
|
||||
const localizer = new TransformLitLocalizer(config);
|
||||
const { messages } = localizer.extractSourceMessages();
|
||||
const messages = localizer.extractSourceMessages().messages;
|
||||
const translations = messages.map(pseudoMessagify);
|
||||
const sorted = sortProgramMessages([...messages]);
|
||||
const formatter = makeFormatter(config);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { VERSION } from "@goauthentik/common/constants";
|
||||
import { globalAK } from "@goauthentik/common/global";
|
||||
import { DefaultBrand } from "@goauthentik/common/ui/config";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
@ -44,7 +45,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
|
||||
}
|
||||
return [
|
||||
[msg("Version"), version.versionCurrent],
|
||||
[msg("UI Version"), import.meta.env.AK_VERSION],
|
||||
[msg("UI Version"), VERSION],
|
||||
[msg("Build"), build],
|
||||
[msg("Python version"), status.runtime.pythonVersion],
|
||||
[msg("Platform"), status.runtime.platform],
|
||||
|
@ -43,7 +43,7 @@ import {
|
||||
renderSidebarItems,
|
||||
} from "./AdminSidebar.js";
|
||||
|
||||
if (import.meta.env.NODE_ENV === "development") {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
await import("@goauthentik/esbuild-plugin-live-reload/client");
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import "@goauthentik/admin/admin-overview/cards/WorkerStatusCard";
|
||||
import "@goauthentik/admin/admin-overview/charts/AdminLoginAuthorizeChart";
|
||||
import "@goauthentik/admin/admin-overview/charts/OutpostStatusChart";
|
||||
import "@goauthentik/admin/admin-overview/charts/SyncStatusChart";
|
||||
import { VERSION } from "@goauthentik/common/constants";
|
||||
import { me } from "@goauthentik/common/users";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
|
||||
@ -21,6 +22,8 @@ import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { map } from "lit/directives/map.js";
|
||||
import { when } from "lit/directives/when.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
|
||||
@ -30,17 +33,21 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { SessionUser } from "@goauthentik/api";
|
||||
|
||||
function createReleaseNotesURL(semver: string): URL {
|
||||
const segments = semver.split(".");
|
||||
const versionFamily = segments.slice(0, -1).join(".");
|
||||
|
||||
const release = `${versionFamily}#fixed-in-${segments.join("")}`;
|
||||
|
||||
return new URL(`/docs/releases/${release}`, "https://goauthentik.io");
|
||||
export function versionFamily(): string {
|
||||
const parts = VERSION.split(".");
|
||||
parts.pop();
|
||||
return parts.join(".");
|
||||
}
|
||||
|
||||
const RELEASE = `${VERSION.split(".").slice(0, -1).join(".")}#fixed-in-${VERSION.replaceAll(
|
||||
".",
|
||||
"",
|
||||
)}`;
|
||||
|
||||
const AdminOverviewBase = WithLicenseSummary(AKElement);
|
||||
|
||||
type Renderer = () => TemplateResult | typeof nothing;
|
||||
|
||||
@customElement("ak-admin-overview")
|
||||
export class AdminOverviewPage extends AdminOverviewBase {
|
||||
static get styles(): CSSResult[] {
|
||||
@ -76,11 +83,7 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
[msg("Check the logs"), paramURL("/events/log")],
|
||||
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
|
||||
[msg("Manage users"), paramURL("/identity/users")],
|
||||
[
|
||||
msg("Check the release notes"),
|
||||
createReleaseNotesURL(import.meta.env.AK_VERSION).href,
|
||||
true,
|
||||
],
|
||||
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${RELEASE}`, true],
|
||||
];
|
||||
|
||||
@state()
|
||||
@ -190,6 +193,45 @@ export class AdminOverviewPage extends AdminOverviewBase {
|
||||
</div>`
|
||||
: nothing} `;
|
||||
}
|
||||
|
||||
renderActions() {
|
||||
const release = `${versionFamily()}#fixed-in-${VERSION.replaceAll(".", "")}`;
|
||||
|
||||
const quickActions: [string, string][] = [
|
||||
[msg("Create a new application"), paramURL("/core/applications", { createForm: true })],
|
||||
[msg("Check the logs"), paramURL("/events/log")],
|
||||
[msg("Explore integrations"), "https://goauthentik.io/integrations/"],
|
||||
[msg("Manage users"), paramURL("/identity/users")],
|
||||
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${release}`],
|
||||
];
|
||||
|
||||
const action = ([label, url]: [string, string]) => {
|
||||
const isExternal = url.startsWith("https://");
|
||||
const ex = (truecase: Renderer, falsecase: Renderer) =>
|
||||
when(isExternal, truecase, falsecase);
|
||||
|
||||
const content = html`${label}${ex(
|
||||
() => html`<i class="fas fa-external-link-alt ak-external-link"></i>`,
|
||||
() => nothing,
|
||||
)}`;
|
||||
|
||||
return html`<li>
|
||||
${ex(
|
||||
() =>
|
||||
html`<a
|
||||
href="${url}"
|
||||
class="pf-u-mb-xl"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>${content}</a
|
||||
>`,
|
||||
() => html`<a href="${url}" class="pf-u-mb-xl" )>${content}</a>`,
|
||||
)}
|
||||
</li>`;
|
||||
};
|
||||
|
||||
return html`${map(quickActions, action)}`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
EventMiddleware,
|
||||
LoggingMiddleware,
|
||||
} from "@goauthentik/common/api/middleware.js";
|
||||
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants.js";
|
||||
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants.js";
|
||||
import { globalAK } from "@goauthentik/common/global.js";
|
||||
import { SentryMiddleware } from "@goauthentik/common/sentry";
|
||||
|
||||
@ -79,6 +79,4 @@ export function AndNext(url: string): string {
|
||||
return `?next=${encodeURIComponent(url)}`;
|
||||
}
|
||||
|
||||
console.debug(
|
||||
`authentik(early): version ${import.meta.env.AK_VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`,
|
||||
);
|
||||
console.debug(`authentik(early): version ${VERSION}, apiBase ${DEFAULT_CONFIG.basePath}`);
|
||||
|
@ -1,35 +1,12 @@
|
||||
/**
|
||||
* @file Global constants used throughout the application.
|
||||
*
|
||||
* @todo Much of this content can be moved to a specific file, element, or component.
|
||||
*/
|
||||
|
||||
/// <reference types="../../types/esbuild.js" />
|
||||
|
||||
//#region Patternfly
|
||||
|
||||
export const SECONDARY_CLASS = "pf-m-secondary";
|
||||
export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Application
|
||||
|
||||
export const VERSION = "2025.4.1";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
/**
|
||||
* The delimiter used to parse the URL for the current route.
|
||||
*
|
||||
* @todo Move this to the ak-router.
|
||||
*/
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Events
|
||||
|
||||
export const EVENT_REFRESH = "ak-refresh";
|
||||
export const EVENT_NOTIFICATION_DRAWER_TOGGLE = "ak-notification-toggle";
|
||||
export const EVENT_API_DRAWER_TOGGLE = "ak-api-drawer-toggle";
|
||||
@ -43,17 +20,7 @@ export const EVENT_MESSAGE = "ak-message";
|
||||
export const EVENT_THEME_CHANGE = "ak-theme-change";
|
||||
export const EVENT_REFRESH_ENTERPRISE = "ak-refresh-enterprise";
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region WebSocket
|
||||
|
||||
export const WS_MSG_TYPE_MESSAGE = "message";
|
||||
export const WS_MSG_TYPE_REFRESH = "refresh";
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region LocalStorage
|
||||
|
||||
export const LOCALSTORAGE_AUTHENTIK_KEY = "authentik-local-settings";
|
||||
|
||||
//#endregion
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { VERSION } from "@goauthentik/common/constants";
|
||||
import { SentryIgnoredError } from "@goauthentik/common/sentry";
|
||||
|
||||
export interface PlexPinResponse {
|
||||
@ -18,7 +19,7 @@ export const DEFAULT_HEADERS = {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Plex-Product": "authentik",
|
||||
"X-Plex-Version": import.meta.env.AK_VERSION,
|
||||
"X-Plex-Version": VERSION,
|
||||
"X-Plex-Device-Vendor": "goauthentik.io",
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { VERSION } from "@goauthentik/common/constants";
|
||||
import { globalAK } from "@goauthentik/common/global";
|
||||
import { me } from "@goauthentik/common/users";
|
||||
import { readInterfaceRouteParam } from "@goauthentik/elements/router/utils";
|
||||
@ -49,7 +50,7 @@ export function configureSentry(canDoPpi = false) {
|
||||
/MutationObserver.observe/gi,
|
||||
/NS_ERROR_FAILURE/gi,
|
||||
],
|
||||
release: `authentik@${import.meta.env.AK_VERSION}`,
|
||||
release: `authentik@${VERSION}`,
|
||||
integrations: [
|
||||
browserTracingIntegration({
|
||||
// https://docs.sentry.io/platforms/javascript/tracing/instrumentation/automatic-instrumentation/#custom-routing
|
||||
|
@ -223,7 +223,7 @@ export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleS
|
||||
};
|
||||
}
|
||||
|
||||
if (import.meta.env.NODE_ENV === "development") {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
Object.assign(window, {
|
||||
inspectStyleSheetTree,
|
||||
serializeStyleSheet,
|
||||
|
@ -5,11 +5,11 @@ import {
|
||||
type StyleRoot,
|
||||
createStyleSheetUnsafe,
|
||||
setAdoptedStyleSheets,
|
||||
} from "@goauthentik/web/common/stylesheets.js";
|
||||
import { UIConfig } from "@goauthentik/web/common/ui/config.js";
|
||||
} from "@goauthentik/common/stylesheets.js";
|
||||
import { UIConfig } from "@goauthentik/common/ui/config.js";
|
||||
|
||||
import AKBase from "@goauthentik/web/common/styles/authentik.css";
|
||||
import AKBaseDark from "@goauthentik/web/common/styles/theme-dark.css";
|
||||
import AKBase from "@goauthentik/common/styles/authentik.css";
|
||||
import AKBaseDark from "@goauthentik/common/styles/theme-dark.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api";
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { VERSION } from "@goauthentik/common/constants";
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import {
|
||||
EventContext,
|
||||
@ -75,7 +76,7 @@ ${context.message as string}
|
||||
|
||||
|
||||
**Version and Deployment (please complete the following information):**
|
||||
- authentik version: ${import.meta.env.AK_VERSION}
|
||||
- authentik version: ${VERSION}
|
||||
- Deployment: [e.g. docker-compose, helm]
|
||||
|
||||
**Additional context**
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { VERSION } from "@goauthentik/common/constants";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { WithVersion } from "@goauthentik/elements/Interface/versionProvider";
|
||||
|
||||
@ -9,19 +10,20 @@ import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
|
||||
@customElement("ak-version-banner")
|
||||
export class VersionBanner extends WithVersion(AKElement) {
|
||||
static styles = [PFBanner];
|
||||
static get styles() {
|
||||
return [PFBanner];
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.version?.versionCurrent) return nothing;
|
||||
if (this.version.versionCurrent === import.meta.env.AK_VERSION) return nothing;
|
||||
|
||||
return html`
|
||||
<div class="pf-c-banner pf-m-sticky pf-m-gold">
|
||||
${msg(
|
||||
str`A newer version (${this.version.versionCurrent}) of the UI is available.`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
return this.version && this.version.versionCurrent !== VERSION
|
||||
? html`
|
||||
<div class="pf-c-banner pf-m-sticky pf-m-gold">
|
||||
${msg(
|
||||
str`A newer version (${this.version.versionCurrent}) of the UI is available.`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,6 @@ import "@goauthentik/flow/stages/password/PasswordStage";
|
||||
|
||||
// end of stage import
|
||||
|
||||
if (import.meta.env.NODE_ENV === "development") {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
await import("@goauthentik/esbuild-plugin-live-reload/client");
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
|
||||
|
||||
import { CurrentBrand, EventsApi, SessionUser } from "@goauthentik/api";
|
||||
|
||||
if (import.meta.env.NODE_ENV === "development") {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
await import("@goauthentik/esbuild-plugin-live-reload/client");
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,6 @@
|
||||
{
|
||||
"extends": "@goauthentik/tsconfig",
|
||||
"compilerOptions": {
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"experimentalDecorators": true,
|
||||
|
39
web/types/esbuild.d.ts
vendored
39
web/types/esbuild.d.ts
vendored
@ -1,39 +0,0 @@
|
||||
/**
|
||||
* @file Import meta environment variables available via ESBuild.
|
||||
*/
|
||||
|
||||
export {};
|
||||
declare global {
|
||||
interface ESBuildImportEnv {
|
||||
/**
|
||||
* The authentik version injected by ESBuild during build time.
|
||||
*
|
||||
* @format semver
|
||||
*/
|
||||
readonly AK_VERSION: string;
|
||||
|
||||
/**
|
||||
* @todo Determine where this is used and if it is needed,
|
||||
* give it a better name.
|
||||
* @deprecated
|
||||
*/
|
||||
readonly AK_API_BASE_PATH: string;
|
||||
}
|
||||
|
||||
type ESBuildImportEnvKey = keyof ESBuildImportEnv;
|
||||
|
||||
/**
|
||||
* Environment variables injected by ESBuild.
|
||||
*/
|
||||
interface ImportMetaEnv extends ESBuildImportEnv {
|
||||
/**
|
||||
* An environment variable used to determine
|
||||
* whether Node.js is running in production mode.
|
||||
*/
|
||||
readonly NODE_ENV: "development" | "production";
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
}
|
13
web/types/node.d.ts → web/types/global.d.ts
vendored
13
web/types/node.d.ts → web/types/global.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @file Global variables provided by Node.js
|
||||
* @file Environment variables available via ESBuild.
|
||||
*/
|
||||
|
||||
declare module "module" {
|
||||
@ -14,8 +14,8 @@ declare module "module" {
|
||||
* const relativeDirname = dirname(fileURLToPath(import.meta.url));
|
||||
* ```
|
||||
*/
|
||||
|
||||
const __dirname: string;
|
||||
// eslint-disable-next-line no-var
|
||||
var __dirname: string;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,16 +23,13 @@ declare module "process" {
|
||||
global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
/**
|
||||
* Node environment, if any.
|
||||
*/
|
||||
readonly NODE_ENV?: "development" | "production";
|
||||
CWD: string;
|
||||
/**
|
||||
* @todo Determine where this is used and if it is needed,
|
||||
* give it a better name.
|
||||
* @deprecated
|
||||
*/
|
||||
readonly AK_API_BASE_PATH?: string;
|
||||
AK_API_BASE_PATH: string;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,17 @@
|
||||
/// <reference types="@wdio/browser-runner" />
|
||||
import replace from "@rollup/plugin-replace";
|
||||
import { browser } from "@wdio/globals";
|
||||
import type { Options } from "@wdio/types";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createBundleDefinitions } from "scripts/esbuild/environment.mjs";
|
||||
import type { InlineConfig } from "vite";
|
||||
import litCSS from "vite-plugin-lit-css";
|
||||
import path from "path";
|
||||
import { cwd } from "process";
|
||||
import { fileURLToPath } from "url";
|
||||
import type { UserConfig } from "vite";
|
||||
import litCss from "vite-plugin-lit-css";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
const isProdBuild = process.env.NODE_ENV === "production";
|
||||
const apiBasePath = process.env.AK_API_BASE_PATH || "";
|
||||
const runHeadless = process.env.CI !== undefined;
|
||||
|
||||
const testSafari = process.env.WDIO_TEST_SAFARI !== undefined;
|
||||
@ -70,9 +72,21 @@ export const config: Options.Testrunner = {
|
||||
runner: [
|
||||
"browser",
|
||||
{
|
||||
viteConfig: {
|
||||
define: createBundleDefinitions(),
|
||||
plugins: [litCSS(), tsconfigPaths()],
|
||||
viteConfig: (userConfig: UserConfig = { plugins: [] }) => ({
|
||||
...userConfig,
|
||||
plugins: [
|
||||
litCss(),
|
||||
replace({
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
isProdBuild ? "production" : "development",
|
||||
),
|
||||
"process.env.CWD": JSON.stringify(cwd()),
|
||||
"process.env.AK_API_BASE_PATH": JSON.stringify(apiBasePath),
|
||||
"preventAssignment": true,
|
||||
}),
|
||||
...(userConfig?.plugins ?? []),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@goauthentik/admin": path.resolve(__dirname, "src/admin"),
|
||||
@ -87,7 +101,7 @@ export const config: Options.Testrunner = {
|
||||
"@goauthentik/user": path.resolve(__dirname, "src/user"),
|
||||
},
|
||||
},
|
||||
} satisfies InlineConfig,
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
||||
|
Reference in New Issue
Block a user