Skip to content

lifecycle/ak: make sure /data has the correct permissions (cherry-pick #19935 to version-2025.12)#19940

Merged
rissson merged 1 commit intoversion-2025.12from
cherry-pick/19935-to-version-2025.12
Feb 2, 2026
Merged

lifecycle/ak: make sure /data has the correct permissions (cherry-pick #19935 to version-2025.12)#19940
rissson merged 1 commit intoversion-2025.12from
cherry-pick/19935-to-version-2025.12

Conversation

@authentik-automation
Copy link
Contributor

Cherry-pick of #19935 to version-2025.12 branch.

Original PR: #19935
Original Author: @rissson
Cherry-picked commit: 7d473d7

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
@netlify
Copy link

netlify bot commented Feb 2, 2026

Deploy Preview for authentik-integrations ready!

Name Link
🔨 Latest commit 7484c94
🔍 Latest deploy log https://app.netlify.com/projects/authentik-integrations/deploys/6980b4291401120008b55a5b
😎 Deploy Preview https://deploy-preview-19940--authentik-integrations.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov
Copy link

codecov bot commented Feb 2, 2026

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
2884 2 2882 4
View the top 2 failed test(s) by shortest run time
tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC::test_authorization_consent_explicit
Stack Traces | 53.8s run time
self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>
args = (), kwargs = {}

    @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)

tests/e2e/utils.py:405: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>,)
kwargs = {}, file = 'default/flow-default-invalidation-flow.yaml'
content = 'version: 1\nmetadata:\n  name: Default - Invalidation flow\nentries:\n- attrs:\n    designation: invalidation\n    na...0\n    stage: !KeyOf default-invalidation-logout\n    target: !KeyOf flow\n  model: authentik_flows.flowstagebinding\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>,)
kwargs = {}
file = 'default/flow-default-provider-authorization-explicit-consent.yaml'
content = 'version: 1\nmetadata:\n  name: Default - Provider authorization flow (explicit consent)\nentries:\n- attrs:\n    desi...e: !KeyOf default-provider-authorization-consent\n    target: !KeyOf flow\n  model: authentik_flows.flowstagebinding\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>,)
kwargs = {}, file = 'system/providers-oauth2.yaml'
content = 'version: 1\nmetadata:\n  labels:\n    blueprints.goauthentik.io/system: "true"\n  name: System - OAuth2 Provider - Sc... application the ability to access the authentik API\n        # on behalf of the authorizing user\n        return {}\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>,)
kwargs = {}, config = <AuthentikCryptoConfig: authentik_crypto>

    @wraps(func)
    def wrapper(*args, **kwargs):
        config = apps.get_app_config(app_name)
        if isinstance(config, ManagedAppConfig):
            config._on_startup_callback(None)
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:43: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>

    @retry()
    @apply_blueprint(
        "default/flow-default-authentication-flow.yaml",
        "default/flow-default-invalidation-flow.yaml",
    )
    @apply_blueprint("default/flow-default-provider-authorization-explicit-consent.yaml")
    @apply_blueprint("system/providers-oauth2.yaml")
    @reconcile_app("authentik_crypto")
    def test_authorization_consent_explicit(self):
        """test OpenID Provider flow (default authorization flow with explicit consent)"""
        sleep(1)
        # Bootstrap all needed objects
        authorization_flow = Flow.objects.get(
            slug="default-provider-authorization-explicit-consent"
        )
        provider = OAuth2Provider.objects.create(
            name=self.application_slug,
            authorization_flow=authorization_flow,
            client_type=ClientTypes.CONFIDENTIAL,
            client_id=self.client_id,
            client_secret=self.client_secret,
            signing_key=create_test_cert(),
            redirect_uris=[
                RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback")
            ],
        )
        provider.property_mappings.set(
            ScopeMapping.objects.filter(
                scope_name__in=[
                    SCOPE_OPENID,
                    SCOPE_OPENID_EMAIL,
                    SCOPE_OPENID_PROFILE,
                    SCOPE_OFFLINE_ACCESS,
                ]
            )
        )
        app = Application.objects.create(
            name=self.application_slug,
            slug=self.application_slug,
            provider=provider,
        )
        self.setup_client()
    
        self.driver.get("http://localhost:9009")
        self.login()
    
        self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-flow-executor")))
    
        flow_executor = self.get_shadow_root("ak-flow-executor")
>       consent_stage = self.get_shadow_root("ak-stage-consent", flow_executor)

tests/e2e/test_provider_oidc.py:264: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>
selector = 'ak-stage-consent'
container = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="69446318b9ef313726118b987fbe26d9", element="f.D7850BA8CA2A594B8EEC15526A748191.d.69E08CA334F91BCA4F90E0ECA4B07733.e.10")>
timeout = 10

    def get_shadow_root(
        self, selector: str, container: WebElement | WebDriver | None = None, timeout: float = 10
    ) -> WebElement:
        """Get the shadow root of a web component specified by `selector`."""
        if not container:
            container = self.driver
        wait = WebDriverWait(container, timeout)
        host: WebElement | None = None
    
        try:
>           host = wait.until(lambda c: c.find_element(By.CSS_SELECTOR, selector))

tests/e2e/utils.py:260: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <[AttributeError("'ShadowRoot' object has no attribute 'session_id'") raised in repr()] WebDriverWait object at 0x7fa81c0e5630>
method = <function SeleniumTestCase.get_shadow_root.<locals>.<lambda> at 0x7fa81d8afd80>
message = ''

    def until(self, method: Callable[[D], Union[Literal[False], T]], message: str = "") -> T:
        """Wait until the method returns a value that is not False.
    
        Calls the method provided with the driver as an argument until the
        return value does not evaluate to ``False``.
    
        Parameters:
        -----------
        method: callable(WebDriver)
            - A callable object that takes a WebDriver instance as an argument.
    
        message: str
            - Optional message for :exc:`TimeoutException`
    
        Return:
        -------
        object: T
            - The result of the last call to `method`
    
        Raises:
        -------
        TimeoutException
            - If 'method' does not return a truthy value within the WebDriverWait
            object's timeout
    
        Example:
        --------
        >>> from selenium.webdriver.common.by import By
        >>> from selenium.webdriver.support.ui import WebDriverWait
        >>> from selenium.webdriver.support import expected_conditions as EC
    
        # Wait until an element is visible on the page
        >>> wait = WebDriverWait(driver, 10)
        >>> element = wait.until(EC.visibility_of_element_located((By.ID, "exampleId")))
        >>> print(element.text)
        """
        screen = None
        stacktrace = None
    
        end_time = time.monotonic() + self._timeout
        while True:
            try:
>               value = method(self._driver)

.venv/lib/python3.13.../webdriver/support/wait.py:137: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

c = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="69446318b9ef313726118b987fbe26d9", element="f.D7850BA8CA2A594B8EEC15526A748191.d.69E08CA334F91BCA4F90E0ECA4B07733.e.10")>

>   host = wait.until(lambda c: c.find_element(By.CSS_SELECTOR, selector))

tests/e2e/utils.py:260: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="69446318b9ef313726118b987fbe26d9", element="f.D7850BA8CA2A594B8EEC15526A748191.d.69E08CA334F91BCA4F90E0ECA4B07733.e.10")>
by = 'css selector', value = 'ak-stage-consent'

    def find_element(self, by: str = By.ID, value: str = None):
        """Find an element inside a shadow root given a By strategy and
        locator.
    
        Parameters:
        -----------
        by : selenium.webdriver.common.by.By
            The locating strategy to use. Default is `By.ID`. Supported values include:
            - By.ID: Locate by element ID.
            - By.NAME: Locate by the `name` attribute.
            - By.XPATH: Locate by an XPath expression.
            - By.CSS_SELECTOR: Locate by a CSS selector.
            - By.CLASS_NAME: Locate by the `class` attribute.
            - By.TAG_NAME: Locate by the tag name (e.g., "input", "button").
            - By.LINK_TEXT: Locate a link element by its exact text.
            - By.PARTIAL_LINK_TEXT: Locate a link element by partial text match.
            - RelativeBy: Locate elements relative to a specified root element.
    
        Example:
        --------
        element = driver.find_element(By.ID, 'foo')
    
        Returns:
        -------
        WebElement
            The first matching `WebElement` found on the page.
        """
        if by == By.ID:
            by = By.CSS_SELECTOR
            value = f'[id="{value}"]'
        elif by == By.CLASS_NAME:
            by = By.CSS_SELECTOR
            value = f".{value}"
        elif by == By.NAME:
            by = By.CSS_SELECTOR
            value = f'[name="{value}"]'
    
>       return self._execute(Command.FIND_ELEMENT_FROM_SHADOW_ROOT, {"using": by, "value": value})["value"]

.venv/lib/python3.13.../webdriver/remote/shadowroot.py:79: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="69446318b9ef313726118b987fbe26d9", element="f.D7850BA8CA2A594B8EEC15526A748191.d.69E08CA334F91BCA4F90E0ECA4B07733.e.10")>
command = 'findElementFromShadowRoot'
params = {'shadowId': 'f.D7850BA8CA2A594B8EEC15526A748191.d.69E08CA334F91BCA4F90E0ECA4B07733.e.10', 'using': 'css selector', 'value': 'ak-stage-consent'}

    def _execute(self, command, params=None):
        """Executes a command against the underlying HTML element.
    
        Args:
          command: The name of the command to _execute as a string.
          params: A dictionary of named parameters to send with the command.
    
        Returns:
          The command's JSON response loaded into a dictionary object.
        """
        if not params:
            params = {}
        params["shadowId"] = self._id
>       return self.session.execute(command, params)

.venv/lib/python3.13.../webdriver/remote/shadowroot.py:133: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.webdriver.WebDriver (session="69446318b9ef313726118b987fbe26d9")>
driver_command = 'findElementFromShadowRoot'
params = {'using': 'css selector', 'value': 'ak-stage-consent'}

    def execute(self, driver_command: str, params: dict = None) -> dict:
        """Sends a command to be executed by a command.CommandExecutor.
    
        Parameters:
        -----------
        driver_command : str
            - The name of the command to execute as a string.
    
        params : dict
            - A dictionary of named Parameters to send with the command.
    
        Returns:
        --------
          dict - The command's JSON response loaded into a dictionary object.
        """
        params = self._wrap_value(params)
    
        if self.session_id:
            if not params:
                params = {"sessionId": self.session_id}
            elif "sessionId" not in params:
                params["sessionId"] = self.session_id
    
        response = self.command_executor.execute(driver_command, params)
        if response:
>           self.error_handler.check_response(response)

.venv/lib/python3.13.../webdriver/remote/webdriver.py:448: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7fa81c741480>
response = {'status': 404, 'value': '{"value":{"error":"detached shadow root","message":"detached shadow root: detached shadow ro...\\n#27 0x558da489acd5 \\u003Cunknown>\\n#28 0x558da48ac709 \\u003Cunknown>\\n#29 0x7f77bff9d428 \\u003Cunknown>\\n"}}'}

    def check_response(self, response: Dict[str, Any]) -> None:
        """Checks that a JSON response from the WebDriver does not have an
        error.
    
        :Args:
         - response - The JSON response from the WebDriver server as a dictionary
           object.
    
        :Raises: If the response contains an error message.
        """
        status = response.get("status", None)
        if not status or status == ErrorCode.SUCCESS:
            return
        value = None
        message = response.get("message", "")
        screen: str = response.get("screen", "")
        stacktrace = None
        if isinstance(status, int):
            value_json = response.get("value", None)
            if value_json and isinstance(value_json, str):
                import json
    
                try:
                    value = json.loads(value_json)
                    if len(value) == 1:
                        value = value["value"]
                    status = value.get("error", None)
                    if not status:
                        status = value.get("status", ErrorCode.UNKNOWN_ERROR)
                        message = value.get("value") or value.get("message")
                        if not isinstance(message, str):
                            value = message
                            message = message.get("message")
                    else:
                        message = value.get("message", None)
                except ValueError:
                    pass
    
        exception_class: Type[WebDriverException]
        e = ErrorCode()
        error_codes = [item for item in dir(e) if not item.startswith("__")]
        for error_code in error_codes:
            error_info = getattr(ErrorCode, error_code)
            if isinstance(error_info, list) and status in error_info:
                exception_class = getattr(ExceptionMapping, error_code, WebDriverException)
                break
        else:
            exception_class = WebDriverException
    
        if not value:
            value = response["value"]
        if isinstance(value, str):
            raise exception_class(value)
        if message == "" and "message" in value:
            message = value["message"]
    
        screen = None  # type: ignore[assignment]
        if "screen" in value:
            screen = value["screen"]
    
        stacktrace = None
        st_value = value.get("stackTrace") or value.get("stacktrace")
        if st_value:
            if isinstance(st_value, str):
                stacktrace = st_value.split("\n")
            else:
                stacktrace = []
                try:
                    for frame in st_value:
                        line = frame.get("lineNumber", "")
                        file = frame.get("fileName", "<anonymous>")
                        if line:
                            file = f"{file}:{line}"
                        meth = frame.get("methodName", "<anonymous>")
                        if "className" in frame:
                            meth = f"{frame['className']}.{meth}"
                        msg = "    at %s (%s)"
                        msg = msg % (meth, file)
                        stacktrace.append(msg)
                except TypeError:
                    pass
        if exception_class == UnexpectedAlertPresentException:
            alert_text = None
            if "data" in value:
                alert_text = value["data"].get("text")
            elif "alert" in value:
                alert_text = value["alert"].get("text")
            raise exception_class(message, screen, stacktrace, alert_text)  # type: ignore[call-arg]  # mypy is not smart enough here
>       raise exception_class(message, screen, stacktrace)
E       selenium.common.exceptions.DetachedShadowRootException: Message: detached shadow root: detached shadow root not found
E         (Session info: chrome=143.0.7499.169)
E       Stacktrace:
E       #0 0x558da48ad432 <unknown>
E       #1 0x558da42f271b <unknown>
E       #2 0x558da4306207 <unknown>
E       #3 0x558da4304e10 <unknown>
E       #4 0x558da4306719 <unknown>
E       #5 0x558da42f9c64 <unknown>
E       #6 0x558da42f8873 <unknown>
E       #7 0x558da42fbd6f <unknown>
E       #8 0x558da42fbe55 <unknown>
E       #9 0x558da4341da4 <unknown>
E       #10 0x558da43427d5 <unknown>
E       #11 0x558da4336caa <unknown>
E       #12 0x558da4366ee1 <unknown>
E       #13 0x558da4336b81 <unknown>
E       #14 0x558da43670a2 <unknown>
E       #15 0x558da4388b62 <unknown>
E       #16 0x558da4366c67 <unknown>
E       #17 0x558da4335047 <unknown>
E       #18 0x558da4335e15 <unknown>
E       #19 0x558da4878064 <unknown>
E       #20 0x558da487b354 <unknown>
E       #21 0x558da487ae0e <unknown>
E       #22 0x558da487b7e9 <unknown>
E       #23 0x558da4861a7a <unknown>
E       #24 0x558da487bb5a <unknown>
E       #25 0x558da484a629 <unknown>
E       #26 0x558da489aae9 <unknown>
E       #27 0x558da489acd5 <unknown>
E       #28 0x558da48ac709 <unknown>
E       #29 0x7f77bff9d428 <unknown>

.venv/lib/python3.13.../webdriver/remote/errorhandler.py:232: DetachedShadowRootException

During handling of the above exception, another exception occurred:

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>
args = (), kwargs = {}

    @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)

tests/e2e/utils.py:405: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>,)
kwargs = {}, file = 'default/flow-default-invalidation-flow.yaml'
content = 'version: 1\nmetadata:\n  name: Default - Invalidation flow\nentries:\n- attrs:\n    designation: invalidation\n    na...0\n    stage: !KeyOf default-invalidation-logout\n    target: !KeyOf flow\n  model: authentik_flows.flowstagebinding\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>,)
kwargs = {}
file = 'default/flow-default-provider-authorization-explicit-consent.yaml'
content = 'version: 1\nmetadata:\n  name: Default - Provider authorization flow (explicit consent)\nentries:\n- attrs:\n    desi...e: !KeyOf default-provider-authorization-consent\n    target: !KeyOf flow\n  model: authentik_flows.flowstagebinding\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>,)
kwargs = {}, file = 'system/providers-oauth2.yaml'
content = 'version: 1\nmetadata:\n  labels:\n    blueprints.goauthentik.io/system: "true"\n  name: System - OAuth2 Provider - Sc... application the ability to access the authentik API\n        # on behalf of the authorizing user\n        return {}\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>,)
kwargs = {}, config = <AuthentikCryptoConfig: authentik_crypto>

    @wraps(func)
    def wrapper(*args, **kwargs):
        config = apps.get_app_config(app_name)
        if isinstance(config, ManagedAppConfig):
            config._on_startup_callback(None)
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:43: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>

    @retry()
    @apply_blueprint(
        "default/flow-default-authentication-flow.yaml",
        "default/flow-default-invalidation-flow.yaml",
    )
    @apply_blueprint("default/flow-default-provider-authorization-explicit-consent.yaml")
    @apply_blueprint("system/providers-oauth2.yaml")
    @reconcile_app("authentik_crypto")
    def test_authorization_consent_explicit(self):
        """test OpenID Provider flow (default authorization flow with explicit consent)"""
        sleep(1)
        # Bootstrap all needed objects
        authorization_flow = Flow.objects.get(
            slug="default-provider-authorization-explicit-consent"
        )
        provider = OAuth2Provider.objects.create(
            name=self.application_slug,
            authorization_flow=authorization_flow,
            client_type=ClientTypes.CONFIDENTIAL,
            client_id=self.client_id,
            client_secret=self.client_secret,
            signing_key=create_test_cert(),
            redirect_uris=[
                RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback")
            ],
        )
        provider.property_mappings.set(
            ScopeMapping.objects.filter(
                scope_name__in=[
                    SCOPE_OPENID,
                    SCOPE_OPENID_EMAIL,
                    SCOPE_OPENID_PROFILE,
                    SCOPE_OFFLINE_ACCESS,
                ]
            )
        )
        app = Application.objects.create(
            name=self.application_slug,
            slug=self.application_slug,
            provider=provider,
        )
        self.setup_client()
    
        self.driver.get("http://localhost:9009")
        self.login()
    
        self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-flow-executor")))
    
        flow_executor = self.get_shadow_root("ak-flow-executor")
>       consent_stage = self.get_shadow_root("ak-stage-consent", flow_executor)

tests/e2e/test_provider_oidc.py:264: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>
selector = 'ak-stage-consent'
container = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="c3449369f5f1634b2d67f5bed8e3f1a4", element="f.AD3C9A793C10334D0CE0AF29C247BEFB.d.AEE9E0D5DCCED15ACA7C74D15C227559.e.10")>
timeout = 10

    def get_shadow_root(
        self, selector: str, container: WebElement | WebDriver | None = None, timeout: float = 10
    ) -> WebElement:
        """Get the shadow root of a web component specified by `selector`."""
        if not container:
            container = self.driver
        wait = WebDriverWait(container, timeout)
        host: WebElement | None = None
    
        try:
>           host = wait.until(lambda c: c.find_element(By.CSS_SELECTOR, selector))

tests/e2e/utils.py:260: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <[AttributeError("'ShadowRoot' object has no attribute 'session_id'") raised in repr()] WebDriverWait object at 0x7fa81daa95c0>
method = <function SeleniumTestCase.get_shadow_root.<locals>.<lambda> at 0x7fa81d9f6ca0>
message = ''

    def until(self, method: Callable[[D], Union[Literal[False], T]], message: str = "") -> T:
        """Wait until the method returns a value that is not False.
    
        Calls the method provided with the driver as an argument until the
        return value does not evaluate to ``False``.
    
        Parameters:
        -----------
        method: callable(WebDriver)
            - A callable object that takes a WebDriver instance as an argument.
    
        message: str
            - Optional message for :exc:`TimeoutException`
    
        Return:
        -------
        object: T
            - The result of the last call to `method`
    
        Raises:
        -------
        TimeoutException
            - If 'method' does not return a truthy value within the WebDriverWait
            object's timeout
    
        Example:
        --------
        >>> from selenium.webdriver.common.by import By
        >>> from selenium.webdriver.support.ui import WebDriverWait
        >>> from selenium.webdriver.support import expected_conditions as EC
    
        # Wait until an element is visible on the page
        >>> wait = WebDriverWait(driver, 10)
        >>> element = wait.until(EC.visibility_of_element_located((By.ID, "exampleId")))
        >>> print(element.text)
        """
        screen = None
        stacktrace = None
    
        end_time = time.monotonic() + self._timeout
        while True:
            try:
>               value = method(self._driver)

.venv/lib/python3.13.../webdriver/support/wait.py:137: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

c = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="c3449369f5f1634b2d67f5bed8e3f1a4", element="f.AD3C9A793C10334D0CE0AF29C247BEFB.d.AEE9E0D5DCCED15ACA7C74D15C227559.e.10")>

>   host = wait.until(lambda c: c.find_element(By.CSS_SELECTOR, selector))

tests/e2e/utils.py:260: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="c3449369f5f1634b2d67f5bed8e3f1a4", element="f.AD3C9A793C10334D0CE0AF29C247BEFB.d.AEE9E0D5DCCED15ACA7C74D15C227559.e.10")>
by = 'css selector', value = 'ak-stage-consent'

    def find_element(self, by: str = By.ID, value: str = None):
        """Find an element inside a shadow root given a By strategy and
        locator.
    
        Parameters:
        -----------
        by : selenium.webdriver.common.by.By
            The locating strategy to use. Default is `By.ID`. Supported values include:
            - By.ID: Locate by element ID.
            - By.NAME: Locate by the `name` attribute.
            - By.XPATH: Locate by an XPath expression.
            - By.CSS_SELECTOR: Locate by a CSS selector.
            - By.CLASS_NAME: Locate by the `class` attribute.
            - By.TAG_NAME: Locate by the tag name (e.g., "input", "button").
            - By.LINK_TEXT: Locate a link element by its exact text.
            - By.PARTIAL_LINK_TEXT: Locate a link element by partial text match.
            - RelativeBy: Locate elements relative to a specified root element.
    
        Example:
        --------
        element = driver.find_element(By.ID, 'foo')
    
        Returns:
        -------
        WebElement
            The first matching `WebElement` found on the page.
        """
        if by == By.ID:
            by = By.CSS_SELECTOR
            value = f'[id="{value}"]'
        elif by == By.CLASS_NAME:
            by = By.CSS_SELECTOR
            value = f".{value}"
        elif by == By.NAME:
            by = By.CSS_SELECTOR
            value = f'[name="{value}"]'
    
>       return self._execute(Command.FIND_ELEMENT_FROM_SHADOW_ROOT, {"using": by, "value": value})["value"]

.venv/lib/python3.13.../webdriver/remote/shadowroot.py:79: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="c3449369f5f1634b2d67f5bed8e3f1a4", element="f.AD3C9A793C10334D0CE0AF29C247BEFB.d.AEE9E0D5DCCED15ACA7C74D15C227559.e.10")>
command = 'findElementFromShadowRoot'
params = {'shadowId': 'f.AD3C9A793C10334D0CE0AF29C247BEFB.d.AEE9E0D5DCCED15ACA7C74D15C227559.e.10', 'using': 'css selector', 'value': 'ak-stage-consent'}

    def _execute(self, command, params=None):
        """Executes a command against the underlying HTML element.
    
        Args:
          command: The name of the command to _execute as a string.
          params: A dictionary of named parameters to send with the command.
    
        Returns:
          The command's JSON response loaded into a dictionary object.
        """
        if not params:
            params = {}
        params["shadowId"] = self._id
>       return self.session.execute(command, params)

.venv/lib/python3.13.../webdriver/remote/shadowroot.py:133: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.webdriver.WebDriver (session="c3449369f5f1634b2d67f5bed8e3f1a4")>
driver_command = 'findElementFromShadowRoot'
params = {'using': 'css selector', 'value': 'ak-stage-consent'}

    def execute(self, driver_command: str, params: dict = None) -> dict:
        """Sends a command to be executed by a command.CommandExecutor.
    
        Parameters:
        -----------
        driver_command : str
            - The name of the command to execute as a string.
    
        params : dict
            - A dictionary of named Parameters to send with the command.
    
        Returns:
        --------
          dict - The command's JSON response loaded into a dictionary object.
        """
        params = self._wrap_value(params)
    
        if self.session_id:
            if not params:
                params = {"sessionId": self.session_id}
            elif "sessionId" not in params:
                params["sessionId"] = self.session_id
    
        response = self.command_executor.execute(driver_command, params)
        if response:
>           self.error_handler.check_response(response)

.venv/lib/python3.13.../webdriver/remote/webdriver.py:448: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7fa81d9ff350>
response = {'status': 404, 'value': '{"value":{"error":"detached shadow root","message":"detached shadow root: detached shadow ro...\\n#27 0x55f8450abcd5 \\u003Cunknown>\\n#28 0x55f8450bd709 \\u003Cunknown>\\n#29 0x7f7dd263e428 \\u003Cunknown>\\n"}}'}

    def check_response(self, response: Dict[str, Any]) -> None:
        """Checks that a JSON response from the WebDriver does not have an
        error.
    
        :Args:
         - response - The JSON response from the WebDriver server as a dictionary
           object.
    
        :Raises: If the response contains an error message.
        """
        status = response.get("status", None)
        if not status or status == ErrorCode.SUCCESS:
            return
        value = None
        message = response.get("message", "")
        screen: str = response.get("screen", "")
        stacktrace = None
        if isinstance(status, int):
            value_json = response.get("value", None)
            if value_json and isinstance(value_json, str):
                import json
    
                try:
                    value = json.loads(value_json)
                    if len(value) == 1:
                        value = value["value"]
                    status = value.get("error", None)
                    if not status:
                        status = value.get("status", ErrorCode.UNKNOWN_ERROR)
                        message = value.get("value") or value.get("message")
                        if not isinstance(message, str):
                            value = message
                            message = message.get("message")
                    else:
                        message = value.get("message", None)
                except ValueError:
                    pass
    
        exception_class: Type[WebDriverException]
        e = ErrorCode()
        error_codes = [item for item in dir(e) if not item.startswith("__")]
        for error_code in error_codes:
            error_info = getattr(ErrorCode, error_code)
            if isinstance(error_info, list) and status in error_info:
                exception_class = getattr(ExceptionMapping, error_code, WebDriverException)
                break
        else:
            exception_class = WebDriverException
    
        if not value:
            value = response["value"]
        if isinstance(value, str):
            raise exception_class(value)
        if message == "" and "message" in value:
            message = value["message"]
    
        screen = None  # type: ignore[assignment]
        if "screen" in value:
            screen = value["screen"]
    
        stacktrace = None
        st_value = value.get("stackTrace") or value.get("stacktrace")
        if st_value:
            if isinstance(st_value, str):
                stacktrace = st_value.split("\n")
            else:
                stacktrace = []
                try:
                    for frame in st_value:
                        line = frame.get("lineNumber", "")
                        file = frame.get("fileName", "<anonymous>")
                        if line:
                            file = f"{file}:{line}"
                        meth = frame.get("methodName", "<anonymous>")
                        if "className" in frame:
                            meth = f"{frame['className']}.{meth}"
                        msg = "    at %s (%s)"
                        msg = msg % (meth, file)
                        stacktrace.append(msg)
                except TypeError:
                    pass
        if exception_class == UnexpectedAlertPresentException:
            alert_text = None
            if "data" in value:
                alert_text = value["data"].get("text")
            elif "alert" in value:
                alert_text = value["alert"].get("text")
            raise exception_class(message, screen, stacktrace, alert_text)  # type: ignore[call-arg]  # mypy is not smart enough here
>       raise exception_class(message, screen, stacktrace)
E       selenium.common.exceptions.DetachedShadowRootException: Message: detached shadow root: detached shadow root not found
E         (Session info: chrome=143.0.7499.169)
E       Stacktrace:
E       #0 0x55f8450be432 <unknown>
E       #1 0x55f844b0371b <unknown>
E       #2 0x55f844b17207 <unknown>
E       #3 0x55f844b15e10 <unknown>
E       #4 0x55f844b17719 <unknown>
E       #5 0x55f844b0ac64 <unknown>
E       #6 0x55f844b09873 <unknown>
E       #7 0x55f844b0cd6f <unknown>
E       #8 0x55f844b0ce55 <unknown>
E       #9 0x55f844b52da4 <unknown>
E       #10 0x55f844b537d5 <unknown>
E       #11 0x55f844b47caa <unknown>
E       #12 0x55f844b77ee1 <unknown>
E       #13 0x55f844b47b81 <unknown>
E       #14 0x55f844b780a2 <unknown>
E       #15 0x55f844b99b62 <unknown>
E       #16 0x55f844b77c67 <unknown>
E       #17 0x55f844b46047 <unknown>
E       #18 0x55f844b46e15 <unknown>
E       #19 0x55f845089064 <unknown>
E       #20 0x55f84508c354 <unknown>
E       #21 0x55f84508be0e <unknown>
E       #22 0x55f84508c7e9 <unknown>
E       #23 0x55f845072a7a <unknown>
E       #24 0x55f84508cb5a <unknown>
E       #25 0x55f84505b629 <unknown>
E       #26 0x55f8450abae9 <unknown>
E       #27 0x55f8450abcd5 <unknown>
E       #28 0x55f8450bd709 <unknown>
E       #29 0x7f7dd263e428 <unknown>

.venv/lib/python3.13.../webdriver/remote/errorhandler.py:232: DetachedShadowRootException

During handling of the above exception, another exception occurred:

self = <unittest.case._Outcome object at 0x7fa81bd11ae0>
test_case = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>
subTest = False

    @contextlib.contextmanager
    def testPartExecutor(self, test_case, subTest=False):
        old_success = self.success
        self.success = True
        try:
>           yield

.../hostedtoolcache/Python/3.13.11........./x64/lib/python3.13/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>
result = <TestCaseFunction test_authorization_consent_explicit>

    def run(self, result=None):
        if result is None:
            result = self.defaultTestResult()
            startTestRun = getattr(result, 'startTestRun', None)
            stopTestRun = getattr(result, 'stopTestRun', None)
            if startTestRun is not None:
                startTestRun()
        else:
            stopTestRun = None
    
        result.startTest(self)
        try:
            testMethod = getattr(self, self._testMethodName)
            if (getattr(self.__class__, "__unittest_skip__", False) or
                getattr(testMethod, "__unittest_skip__", False)):
                # If the class or method was skipped.
                skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                            or getattr(testMethod, '__unittest_skip_why__', ''))
                _addSkip(result, self, skip_why)
                return result
    
            expecting_failure = (
                getattr(self, "__unittest_expecting_failure__", False) or
                getattr(testMethod, "__unittest_expecting_failure__", False)
            )
            outcome = _Outcome(result)
            start_time = time.perf_counter()
            try:
                self._outcome = outcome
    
                with outcome.testPartExecutor(self):
                    self._callSetUp()
                if outcome.success:
                    outcome.expecting_failure = expecting_failure
                    with outcome.testPartExecutor(self):
>                       self._callTestMethod(testMethod)

.../hostedtoolcache/Python/3.13.11........./x64/lib/python3.13/unittest/case.py:651: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>
method = <bound method TestProviderOAuth2OIDC.test_authorization_consent_explicit of <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>>

    def _callTestMethod(self, method):
>       if method() is not None:

.../hostedtoolcache/Python/3.13.11........./x64/lib/python3.13/unittest/case.py:606: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>
args = (), kwargs = {}

    @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)

tests/e2e/utils.py:418: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>
args = (), kwargs = {}

    @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)

tests/e2e/utils.py:418: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>
args = (), kwargs = {}

    @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

tests/e2e/utils.py:412: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>
args = (), kwargs = {}

    @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)

tests/e2e/utils.py:405: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>,)
kwargs = {}, file = 'default/flow-default-invalidation-flow.yaml'
content = 'version: 1\nmetadata:\n  name: Default - Invalidation flow\nentries:\n- attrs:\n    designation: invalidation\n    na...0\n    stage: !KeyOf default-invalidation-logout\n    target: !KeyOf flow\n  model: authentik_flows.flowstagebinding\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>,)
kwargs = {}
file = 'default/flow-default-provider-authorization-explicit-consent.yaml'
content = 'version: 1\nmetadata:\n  name: Default - Provider authorization flow (explicit consent)\nentries:\n- attrs:\n    desi...e: !KeyOf default-provider-authorization-consent\n    target: !KeyOf flow\n  model: authentik_flows.flowstagebinding\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>,)
kwargs = {}, file = 'system/providers-oauth2.yaml'
content = 'version: 1\nmetadata:\n  labels:\n    blueprints.goauthentik.io/system: "true"\n  name: System - OAuth2 Provider - Sc... application the ability to access the authentik API\n        # on behalf of the authorizing user\n        return {}\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>,)
kwargs = {}, config = <AuthentikCryptoConfig: authentik_crypto>

    @wraps(func)
    def wrapper(*args, **kwargs):
        config = apps.get_app_config(app_name)
        if isinstance(config, ManagedAppConfig):
            config._on_startup_callback(None)
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:43: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>

    @retry()
    @apply_blueprint(
        "default/flow-default-authentication-flow.yaml",
        "default/flow-default-invalidation-flow.yaml",
    )
    @apply_blueprint("default/flow-default-provider-authorization-explicit-consent.yaml")
    @apply_blueprint("system/providers-oauth2.yaml")
    @reconcile_app("authentik_crypto")
    def test_authorization_consent_explicit(self):
        """test OpenID Provider flow (default authorization flow with explicit consent)"""
        sleep(1)
        # Bootstrap all needed objects
        authorization_flow = Flow.objects.get(
            slug="default-provider-authorization-explicit-consent"
        )
        provider = OAuth2Provider.objects.create(
            name=self.application_slug,
            authorization_flow=authorization_flow,
            client_type=ClientTypes.CONFIDENTIAL,
            client_id=self.client_id,
            client_secret=self.client_secret,
            signing_key=create_test_cert(),
            redirect_uris=[
                RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/auth/callback")
            ],
        )
        provider.property_mappings.set(
            ScopeMapping.objects.filter(
                scope_name__in=[
                    SCOPE_OPENID,
                    SCOPE_OPENID_EMAIL,
                    SCOPE_OPENID_PROFILE,
                    SCOPE_OFFLINE_ACCESS,
                ]
            )
        )
        app = Application.objects.create(
            name=self.application_slug,
            slug=self.application_slug,
            provider=provider,
        )
        self.setup_client()
    
        self.driver.get("http://localhost:9009")
        self.login()
    
        self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-flow-executor")))
    
        flow_executor = self.get_shadow_root("ak-flow-executor")
>       consent_stage = self.get_shadow_root("ak-stage-consent", flow_executor)

tests/e2e/test_provider_oidc.py:264: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc.TestProviderOAuth2OIDC testMethod=test_authorization_consent_explicit>
selector = 'ak-stage-consent'
container = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="cc4a9ad4f26ee948ac065875ac8fc4ed", element="f.B44F3DF20F7A0156FD5CF10AA0A9A383.d.757483041EF3891F8170D16E26B1AFD8.e.10")>
timeout = 10

    def get_shadow_root(
        self, selector: str, container: WebElement | WebDriver | None = None, timeout: float = 10
    ) -> WebElement:
        """Get the shadow root of a web component specified by `selector`."""
        if not container:
            container = self.driver
        wait = WebDriverWait(container, timeout)
        host: WebElement | None = None
    
        try:
>           host = wait.until(lambda c: c.find_element(By.CSS_SELECTOR, selector))

tests/e2e/utils.py:260: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <[AttributeError("'ShadowRoot' object has no attribute 'session_id'") raised in repr()] WebDriverWait object at 0x7fa81c7b7af0>
method = <function SeleniumTestCase.get_shadow_root.<locals>.<lambda> at 0x7fa81d9c3060>
message = ''

    def until(self, method: Callable[[D], Union[Literal[False], T]], message: str = "") -> T:
        """Wait until the method returns a value that is not False.
    
        Calls the method provided with the driver as an argument until the
        return value does not evaluate to ``False``.
    
        Parameters:
        -----------
        method: callable(WebDriver)
            - A callable object that takes a WebDriver instance as an argument.
    
        message: str
            - Optional message for :exc:`TimeoutException`
    
        Return:
        -------
        object: T
            - The result of the last call to `method`
    
        Raises:
        -------
        TimeoutException
            - If 'method' does not return a truthy value within the WebDriverWait
            object's timeout
    
        Example:
        --------
        >>> from selenium.webdriver.common.by import By
        >>> from selenium.webdriver.support.ui import WebDriverWait
        >>> from selenium.webdriver.support import expected_conditions as EC
    
        # Wait until an element is visible on the page
        >>> wait = WebDriverWait(driver, 10)
        >>> element = wait.until(EC.visibility_of_element_located((By.ID, "exampleId")))
        >>> print(element.text)
        """
        screen = None
        stacktrace = None
    
        end_time = time.monotonic() + self._timeout
        while True:
            try:
>               value = method(self._driver)

.venv/lib/python3.13.../webdriver/support/wait.py:137: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

c = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="cc4a9ad4f26ee948ac065875ac8fc4ed", element="f.B44F3DF20F7A0156FD5CF10AA0A9A383.d.757483041EF3891F8170D16E26B1AFD8.e.10")>

>   host = wait.until(lambda c: c.find_element(By.CSS_SELECTOR, selector))

tests/e2e/utils.py:260: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="cc4a9ad4f26ee948ac065875ac8fc4ed", element="f.B44F3DF20F7A0156FD5CF10AA0A9A383.d.757483041EF3891F8170D16E26B1AFD8.e.10")>
by = 'css selector', value = 'ak-stage-consent'

    def find_element(self, by: str = By.ID, value: str = None):
        """Find an element inside a shadow root given a By strategy and
        locator.
    
        Parameters:
        -----------
        by : selenium.webdriver.common.by.By
            The locating strategy to use. Default is `By.ID`. Supported values include:
            - By.ID: Locate by element ID.
            - By.NAME: Locate by the `name` attribute.
            - By.XPATH: Locate by an XPath expression.
            - By.CSS_SELECTOR: Locate by a CSS selector.
            - By.CLASS_NAME: Locate by the `class` attribute.
            - By.TAG_NAME: Locate by the tag name (e.g., "input", "button").
            - By.LINK_TEXT: Locate a link element by its exact text.
            - By.PARTIAL_LINK_TEXT: Locate a link element by partial text match.
            - RelativeBy: Locate elements relative to a specified root element.
    
        Example:
        --------
        element = driver.find_element(By.ID, 'foo')
    
        Returns:
        -------
        WebElement
            The first matching `WebElement` found on the page.
        """
        if by == By.ID:
            by = By.CSS_SELECTOR
            value = f'[id="{value}"]'
        elif by == By.CLASS_NAME:
            by = By.CSS_SELECTOR
            value = f".{value}"
        elif by == By.NAME:
            by = By.CSS_SELECTOR
            value = f'[name="{value}"]'
    
>       return self._execute(Command.FIND_ELEMENT_FROM_SHADOW_ROOT, {"using": by, "value": value})["value"]

.venv/lib/python3.13.../webdriver/remote/shadowroot.py:79: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.shadowroot.ShadowRoot (session="cc4a9ad4f26ee948ac065875ac8fc4ed", element="f.B44F3DF20F7A0156FD5CF10AA0A9A383.d.757483041EF3891F8170D16E26B1AFD8.e.10")>
command = 'findElementFromShadowRoot'
params = {'shadowId': 'f.B44F3DF20F7A0156FD5CF10AA0A9A383.d.757483041EF3891F8170D16E26B1AFD8.e.10', 'using': 'css selector', 'value': 'ak-stage-consent'}

    def _execute(self, command, params=None):
        """Executes a command against the underlying HTML element.
    
        Args:
          command: The name of the command to _execute as a string.
          params: A dictionary of named parameters to send with the command.
    
        Returns:
          The command's JSON response loaded into a dictionary object.
        """
        if not params:
            params = {}
        params["shadowId"] = self._id
>       return self.session.execute(command, params)

.venv/lib/python3.13.../webdriver/remote/shadowroot.py:133: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.webdriver.WebDriver (session="cc4a9ad4f26ee948ac065875ac8fc4ed")>
driver_command = 'findElementFromShadowRoot'
params = {'using': 'css selector', 'value': 'ak-stage-consent'}

    def execute(self, driver_command: str, params: dict = None) -> dict:
        """Sends a command to be executed by a command.CommandExecutor.
    
        Parameters:
        -----------
        driver_command : str
            - The name of the command to execute as a string.
    
        params : dict
            - A dictionary of named Parameters to send with the command.
    
        Returns:
        --------
          dict - The command's JSON response loaded into a dictionary object.
        """
        params = self._wrap_value(params)
    
        if self.session_id:
            if not params:
                params = {"sessionId": self.session_id}
            elif "sessionId" not in params:
                params["sessionId"] = self.session_id
    
        response = self.command_executor.execute(driver_command, params)
        if response:
>           self.error_handler.check_response(response)

.venv/lib/python3.13.../webdriver/remote/webdriver.py:448: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <selenium.webdriver.remote.errorhandler.ErrorHandler object at 0x7fa81d9ff250>
response = {'status': 404, 'value': '{"value":{"error":"detached shadow root","message":"detached shadow root: detached shadow ro...\\n#27 0x564a9f715cd5 \\u003Cunknown>\\n#28 0x564a9f727709 \\u003Cunknown>\\n#29 0x7fa080169428 \\u003Cunknown>\\n"}}'}

    def check_response(self, response: Dict[str, Any]) -> None:
        """Checks that a JSON response from the WebDriver does not have an
        error.
    
        :Args:
         - response - The JSON response from the WebDriver server as a dictionary
           object.
    
        :Raises: If the response contains an error message.
        """
        status = response.get("status", None)
        if not status or status == ErrorCode.SUCCESS:
            return
        value = None
        message = response.get("message", "")
        screen: str = response.get("screen", "")
        stacktrace = None
        if isinstance(status, int):
            value_json = response.get("value", None)
            if value_json and isinstance(value_json, str):
                import json
    
                try:
                    value = json.loads(value_json)
                    if len(value) == 1:
                        value = value["value"]
                    status = value.get("error", None)
                    if not status:
                        status = value.get("status", ErrorCode.UNKNOWN_ERROR)
                        message = value.get("value") or value.get("message")
                        if not isinstance(message, str):
                            value = message
                            message = message.get("message")
                    else:
                        message = value.get("message", None)
                except ValueError:
                    pass
    
        exception_class: Type[WebDriverException]
        e = ErrorCode()
        error_codes = [item for item in dir(e) if not item.startswith("__")]
        for error_code in error_codes:
            error_info = getattr(ErrorCode, error_code)
            if isinstance(error_info, list) and status in error_info:
                exception_class = getattr(ExceptionMapping, error_code, WebDriverException)
                break
        else:
            exception_class = WebDriverException
    
        if not value:
            value = response["value"]
        if isinstance(value, str):
            raise exception_class(value)
        if message == "" and "message" in value:
            message = value["message"]
    
        screen = None  # type: ignore[assignment]
        if "screen" in value:
            screen = value["screen"]
    
        stacktrace = None
        st_value = value.get("stackTrace") or value.get("stacktrace")
        if st_value:
            if isinstance(st_value, str):
                stacktrace = st_value.split("\n")
            else:
                stacktrace = []
                try:
                    for frame in st_value:
                        line = frame.get("lineNumber", "")
                        file = frame.get("fileName", "<anonymous>")
                        if line:
                            file = f"{file}:{line}"
                        meth = frame.get("methodName", "<anonymous>")
                        if "className" in frame:
                            meth = f"{frame['className']}.{meth}"
                        msg = "    at %s (%s)"
                        msg = msg % (meth, file)
                        stacktrace.append(msg)
                except TypeError:
                    pass
        if exception_class == UnexpectedAlertPresentException:
            alert_text = None
            if "data" in value:
                alert_text = value["data"].get("text")
            elif "alert" in value:
                alert_text = value["alert"].get("text")
            raise exception_class(message, screen, stacktrace, alert_text)  # type: ignore[call-arg]  # mypy is not smart enough here
>       raise exception_class(message, screen, stacktrace)
E       selenium.common.exceptions.DetachedShadowRootException: Message: detached shadow root: detached shadow root not found
E         (Session info: chrome=143.0.7499.169)
E       Stacktrace:
E       #0 0x564a9f728432 <unknown>
E       #1 0x564a9f16d71b <unknown>
E       #2 0x564a9f181207 <unknown>
E       #3 0x564a9f17fe10 <unknown>
E       #4 0x564a9f181719 <unknown>
E       #5 0x564a9f174c64 <unknown>
E       #6 0x564a9f173873 <unknown>
E       #7 0x564a9f176d6f <unknown>
E       #8 0x564a9f176e55 <unknown>
E       #9 0x564a9f1bcda4 <unknown>
E       #10 0x564a9f1bd7d5 <unknown>
E       #11 0x564a9f1b1caa <unknown>
E       #12 0x564a9f1e1ee1 <unknown>
E       #13 0x564a9f1b1b81 <unknown>
E       #14 0x564a9f1e20a2 <unknown>
E       #15 0x564a9f203b62 <unknown>
E       #16 0x564a9f1e1c67 <unknown>
E       #17 0x564a9f1b0047 <unknown>
E       #18 0x564a9f1b0e15 <unknown>
E       #19 0x564a9f6f3064 <unknown>
E       #20 0x564a9f6f6354 <unknown>
E       #21 0x564a9f6f5e0e <unknown>
E       #22 0x564a9f6f67e9 <unknown>
E       #23 0x564a9f6dca7a <unknown>
E       #24 0x564a9f6f6b5a <unknown>
E       #25 0x564a9f6c5629 <unknown>
E       #26 0x564a9f715ae9 <unknown>
E       #27 0x564a9f715cd5 <unknown>
E       #28 0x564a9f727709 <unknown>
E       #29 0x7fa080169428 <unknown>

.venv/lib/python3.13.../webdriver/remote/errorhandler.py:232: DetachedShadowRootException
tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit::test_authorization_consent_implied
Stack Traces | 124s run time
self = <unittest.case._Outcome object at 0x7fa81dea41a0>
test_case = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>
subTest = False

    @contextlib.contextmanager
    def testPartExecutor(self, test_case, subTest=False):
        old_success = self.success
        self.success = True
        try:
>           yield

.../hostedtoolcache/Python/3.13.11............/x64/lib/python3.13/unittest/case.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>
result = <TestCaseFunction test_authorization_consent_implied>

    def run(self, result=None):
        if result is None:
            result = self.defaultTestResult()
            startTestRun = getattr(result, 'startTestRun', None)
            stopTestRun = getattr(result, 'stopTestRun', None)
            if startTestRun is not None:
                startTestRun()
        else:
            stopTestRun = None
    
        result.startTest(self)
        try:
            testMethod = getattr(self, self._testMethodName)
            if (getattr(self.__class__, "__unittest_skip__", False) or
                getattr(testMethod, "__unittest_skip__", False)):
                # If the class or method was skipped.
                skip_why = (getattr(self.__class__, '__unittest_skip_why__', '')
                            or getattr(testMethod, '__unittest_skip_why__', ''))
                _addSkip(result, self, skip_why)
                return result
    
            expecting_failure = (
                getattr(self, "__unittest_expecting_failure__", False) or
                getattr(testMethod, "__unittest_expecting_failure__", False)
            )
            outcome = _Outcome(result)
            start_time = time.perf_counter()
            try:
                self._outcome = outcome
    
                with outcome.testPartExecutor(self):
                    self._callSetUp()
                if outcome.success:
                    outcome.expecting_failure = expecting_failure
                    with outcome.testPartExecutor(self):
>                       self._callTestMethod(testMethod)

.../hostedtoolcache/Python/3.13.11............/x64/lib/python3.13/unittest/case.py:651: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>
method = <bound method TestProviderOAuth2OIDCImplicit.test_authorization_consent_implied of <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>>

    def _callTestMethod(self, method):
>       if method() is not None:

.../hostedtoolcache/Python/3.13.11............/x64/lib/python3.13/unittest/case.py:606: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>
args = (), kwargs = {}

    @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)

tests/e2e/utils.py:405: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>,)
kwargs = {}, file = 'default/flow-default-invalidation-flow.yaml'
content = 'version: 1\nmetadata:\n  name: Default - Invalidation flow\nentries:\n- attrs:\n    designation: invalidation\n    na...0\n    stage: !KeyOf default-invalidation-logout\n    target: !KeyOf flow\n  model: authentik_flows.flowstagebinding\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>,)
kwargs = {}
file = 'default/flow-default-provider-authorization-implicit-consent.yaml'
content = 'version: 1\nmetadata:\n  name: Default - Provider authorization flow (implicit consent)\nentries:\n- attrs:\n    desi...henticated\n  identifiers:\n    slug: default-provider-authorization-implicit-consent\n  model: authentik_flows.flow\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>,)
kwargs = {}, file = 'system/providers-oauth2.yaml'
content = 'version: 1\nmetadata:\n  labels:\n    blueprints.goauthentik.io/system: "true"\n  name: System - OAuth2 Provider - Sc... application the ability to access the authentik API\n        # on behalf of the authorizing user\n        return {}\n'

    @wraps(func)
    def wrapper(*args, **kwargs):
        for file in files:
            content = BlueprintInstance(path=file).retrieve()
            Importer.from_string(content).apply()
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

args = (<tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>,)
kwargs = {}, config = <AuthentikCryptoConfig: authentik_crypto>

    @wraps(func)
    def wrapper(*args, **kwargs):
        config = apps.get_app_config(app_name)
        if isinstance(config, ManagedAppConfig):
            config._on_startup_callback(None)
>       return func(*args, **kwargs)

.../blueprints/tests/__init__.py:43: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>

    @retry()
    @apply_blueprint(
        "default/flow-default-authentication-flow.yaml",
        "default/flow-default-invalidation-flow.yaml",
    )
    @apply_blueprint("default/flow-default-provider-authorization-implicit-consent.yaml")
    @apply_blueprint("system/providers-oauth2.yaml")
    @reconcile_app("authentik_crypto")
    def test_authorization_consent_implied(self):
        """test OpenID Provider flow (default authorization flow with implied consent)"""
        sleep(1)
        # Bootstrap all needed objects
        authorization_flow = Flow.objects.get(
            slug="default-provider-authorization-implicit-consent"
        )
        provider = OAuth2Provider.objects.create(
            name=self.application_slug,
            client_type=ClientTypes.CONFIDENTIAL,
            client_id=self.client_id,
            client_secret=self.client_secret,
            signing_key=create_test_cert(),
            redirect_uris=[
                RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost:9009/implicit/")
            ],
            authorization_flow=authorization_flow,
        )
        provider.property_mappings.set(
            ScopeMapping.objects.filter(
                scope_name__in=[
                    SCOPE_OPENID,
                    SCOPE_OPENID_EMAIL,
                    SCOPE_OPENID_PROFILE,
                    SCOPE_OFFLINE_ACCESS,
                ]
            )
        )
        provider.save()
        Application.objects.create(
            name=self.application_slug,
            slug=self.application_slug,
            provider=provider,
        )
        self.setup_client()
    
        self.driver.get("http://localhost:9009/implicit/")
        self.wait.until(ec.title_contains("authentik"))
        self.login()
    
>       body = self.parse_json_content()

tests/e2e/test_provider_oidc_implicit.py:153: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>
context = None, timeout = 10

    def parse_json_content(
        self, context: WebElement | None = None, timeout: float | None = 10
    ) -> JSONType:
        """
        Parse JSON from a Selenium element's text content.
    
        If `context` is not provided, defaults to the <body> element.
        Raises a clear test failure if the element isn't found, the text doesn't appear
        within `timeout` seconds, or the text is not valid JSON.
        """
        use_body = context is None
        wait_timeout = timeout or self.wait_timeout
    
        def get_context() -> WebElement:
            """Get or refresh the context element."""
            if use_body:
                return self.driver.find_element(By.TAG_NAME, "body")
            return context
    
        def get_text_safely() -> str:
            """Get element text, re-finding element if stale."""
            for _ in range(5):
                try:
                    return get_context().text.strip()
                except StaleElementReferenceException:
                    sleep(0.5)
            return get_context().text.strip()
    
        def get_inner_html_safely() -> str:
            """Get innerHTML, re-finding element if stale."""
            for _ in range(5):
                try:
                    return get_context().get_attribute("innerHTML") or ""
                except StaleElementReferenceException:
                    sleep(0.5)
            return get_context().get_attribute("innerHTML") or ""
    
        try:
            get_context()
        except NoSuchElementException:
            self.fail(
                f"No element found (defaulted to <body>). Current URL: {self.driver.current_url}"
            )
    
        wait = WebDriverWait(self.driver, wait_timeout)
    
        try:
            wait.until(lambda d: len(get_text_safely()) != 0)
        except TimeoutException:
            snippet = get_text_safely()[:500].replace("\n", " ")
            self.fail(
                f"Timed out waiting for element text to appear at {self.driver.current_url}. "
                f"Current content: {snippet or '<empty>'}"
            )
    
        body_text = get_text_safely()
        inner_html = get_inner_html_safely()
    
        if "redirecting" in inner_html.lower():
            try:
                wait.until(lambda d: "redirecting" not in get_inner_html_safely().lower())
            except TimeoutException:
                snippet = get_text_safely()[:500].replace("\n", " ")
                inner_html = get_inner_html_safely()
    
                self.fail(
                    f"Timed out waiting for redirect to finish at {self.driver.current_url}. "
                    f"Current content: {snippet or '<empty>'}"
                    f"{inner_html or '<empty>'}"
                )
    
            inner_html = get_inner_html_safely()
            body_text = get_text_safely()
    
        snippet = body_text[:500].replace("\n", " ")
    
        if not body_text.startswith("{") and not body_text.startswith("["):
>           self.fail(
                f"Expected JSON content but got non-JSON text at {self.driver.current_url}: "
                f"{snippet or '<empty>'}"
                f"{inner_html or '<empty>'}"
            )

tests/e2e/utils.py:232: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.e2e.test_provider_oidc_implicit.TestProviderOAuth2OIDCImplicit testMethod=test_authorization_consent_implied>
msg = 'Expected JSON content but got non-JSON text at http://localhost:9009/implicit/#access_token=eyJhbGciOiJSUzI1NiIsImtpZ...en_type=Bearer&expires_in=3600&state=b6c42d677f4b4be4ac45314d457375f6: <empty>\n    <pre id="loginResult"></pre>\n\n\n'

    def fail(self, msg=None):
        """Fail immediately, with the given message."""
>       raise self.failureException(msg)
E       AssertionError: Expected JSON content but got non-JSON text at http://localhost:9009/implicit/#access_token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImUwMmJkMjA1NGI3OWRmNmVjM2NkZDQ3OTUzMWRhZWE0IiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwOi8vMTAuMS4wLjk0OjU4ODI5L2FwcGxpY2F0aW9uL28vdGVzdC8iLCJzdWIiOiJiYzFlM2RmYTAwMGI0NTM0YmZmZDE1YjAxNjQwMjQwMDNkMWQ5NGY0YzEyYjI0NjVhNDYyMWEyMWNhZjVkZGYzIiwiYXVkIjoiYzZ3WDRDSFd6QWRLWGdFUzdGb2pPUGtSZk1vYWNXNXlMTE5zd2ZKRSIsImV4cCI6MTc3MDA0NjQ1OCwiaWF0IjoxNzcwMDQyODU4LCJhdXRoX3RpbWUiOjE3NzAwNDI4NTgsImFjciI6ImdvYXV0aGVudGlrLmlvL3Byb3ZpZGVycy9vYXV0aDIvZGVmYXVsdCIsImFtciI6WyJwd2QiXSwibm9uY2UiOiIwMjEzNDZiMmFkZTg0MWZjYTBlN2M5NjkxNTUyNGY0MyIsInNpZCI6ImUxZjU2MDQzMTU4MzkwNWMxYjM1NmVjYjVlNGZmMjJmOGQyYmI2NmQyNzdhNTg1MTMxNWZlMDE1ZWVlNDZjZmQiLCJlbWFpbCI6IndTY1BOdnBxODZRWWR1dmRoY2dpQGdvYXV0aGVudGlrLmlvIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoid1NjUE52cHE4NlFZZHV2ZGhjZ2kiLCJnaXZlbl9uYW1lIjoid1NjUE52cHE4NlFZZHV2ZGhjZ2kiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ3U2NQTnZwcTg2UVlkdXZkaGNnaSIsIm5pY2tuYW1lIjoid1NjUE52cHE4NlFZZHV2ZGhjZ2kiLCJncm91cHMiOlsid1NjUE52cHE4NlFZZHV2ZGhjZ2kiXSwiYXpwIjoiYzZ3WDRDSFd6QWRLWGdFUzdGb2pPUGtSZk1vYWNXNXlMTE5zd2ZKRSIsInVpZCI6IlNtUWFsaVA4ZXJvdkE2WjJmeGR5SGg3VG8zY3pHdlVnOHh6Uk14M2siLCJzY29wZSI6InByb2ZpbGUgZW1haWwgb3BlbmlkIn0.ME5Pbe8hlvVrU3BOk2OCiLtMSnn3waThTp8neWOrLnRHbuF8PbwAC4Ek45IXYEAZSC6ECqYjfbw2MkNiz3w2UCv7xHlusEv0S58t8cWrhQ0qwAphoQJbCYBt5-SjOuLcxCLR7ukVC1HYMdW-NlQiIkuEkms7_qhPa-olRq3WVPNP8RGUDgGLAW38b1k231TmdMNTNGVIQ0Su7s21Se1aS9JIo9CED942lQiYxZMXbxFcOjTKg8rDiFQVhTMr0qevoNLvvLDIQaqx135wcmVWt0wAyDri3E4yXrHO95qpa_9YZVJSwzlEC9RXpdXzds1cfoyPWV9-JmcPdLKv-J6yCDaHdOkGHhN3TZdoeudxv_Yk3_wEycODVr0GiMwYkjsPsifykr1tkyRIkcVWWZRQtrTnrm75hXG_05XhDqjYVdkZzMMTLRzGJKI6JQT4Novlws8Y7V0v3fYjIrGqrkfUMKd2rU4caZ7Y88Ef03f8RHYdqOxD466cyGXOmrooUGKTcu73nZ0dPZzmWf4ywoVj1zzh2IdnHDGyjz_Tgaj3irS2WwUlj2uXCxwFzDNhV-MHsG9ObJi5k-yEDfW6hu59QfSppwKqa3rMscxNuzNP7ovUhC0WOv7fjP3bQ5add3y1ML7ONQ9RuwV4vrsuwrlnxGe5arTxrnihMC0P7Mxkkcg&id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImUwMmJkMjA1NGI3OWRmNmVjM2NkZDQ3OTUzMWRhZWE0IiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwOi8vMTAuMS4wLjk0OjU4ODI5L2FwcGxpY2F0aW9uL28vdGVzdC8iLCJzdWIiOiJiYzFlM2RmYTAwMGI0NTM0YmZmZDE1YjAxNjQwMjQwMDNkMWQ5NGY0YzEyYjI0NjVhNDYyMWEyMWNhZjVkZGYzIiwiYXVkIjoiYzZ3WDRDSFd6QWRLWGdFUzdGb2pPUGtSZk1vYWNXNXlMTE5zd2ZKRSIsImV4cCI6MTc3MDA0NjQ1OCwiaWF0IjoxNzcwMDQyODU4LCJhdXRoX3RpbWUiOjE3NzAwNDI4NTgsImFjciI6ImdvYXV0aGVudGlrLmlvL3Byb3ZpZGVycy9vYXV0aDIvZGVmYXVsdCIsImFtciI6WyJwd2QiXSwibm9uY2UiOiIwMjEzNDZiMmFkZTg0MWZjYTBlN2M5NjkxNTUyNGY0MyIsImF0X2hhc2giOiJVYmNQNlBoTVdpUHFwM2d6OVVmaTNBIiwic2lkIjoiZTFmNTYwNDMxNTgzOTA1YzFiMzU2ZWNiNWU0ZmYyMmY4ZDJiYjY2ZDI3N2E1ODUxMzE1ZmUwMTVlZWU0NmNmZCIsImVtYWlsIjoid1NjUE52cHE4NlFZZHV2ZGhjZ2lAZ29hdXRoZW50aWsuaW8iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJ3U2NQTnZwcTg2UVlkdXZkaGNnaSIsImdpdmVuX25hbWUiOiJ3U2NQTnZwcTg2UVlkdXZkaGNnaSIsInByZWZlcnJlZF91c2VybmFtZSI6IndTY1BOdnBxODZRWWR1dmRoY2dpIiwibmlja25hbWUiOiJ3U2NQTnZwcTg2UVlkdXZkaGNnaSIsImdyb3VwcyI6WyJ3U2NQTnZwcTg2UVlkdXZkaGNnaSJdfQ.sQaORceG-fdJ-xeD1p7VmM4p6z5uNi3M_dPUhRc5fkpVp2b-RViVdOxoJu50Fd1wri3wka0huC8605tWick2IuK1GL6JYfRaOuCFGfYululeRDh6okW9KIifXQCZ8vJ7_7Q2g1OmFRW8Qv2K-ZP0ndwq88KP5AnhepruKQYJbOV5GSB_LcmPnHzCz9I_W-D6Wa2s-iMWlgE0QnW3PeZDKHAopG9JU7b2oWqAIQO9lAvVBqJ1t5NqlEdAYMkPF_uoXuroSMfZXvIq4_trT0USjolShH_94cyKwiMAbLurLVwW6EijUnIgSnSVos1Qt4rqTnuBDgVX1gPEhLuI-DeLeu3tKi83Y6qxia_8nv5yKAeQme5Qelu1qfsC4aIIRxMedv0y9TsIvTiKYuqi3FCMYPHNUip9f4yJ3TzwGAHmqi3EQOESEtRSh6r8lYdNEfZcSSYBm-BGlXFDk5GjO4G6g3rakoqPxs5JMxQKVxwWN0n_c5pvOcF9N0lZKtARCA_ytyLDV9MzchLAxdWFN-5XrUU58TFKbmlAihx3vk9rRYoNxFY-a-DOA3osclxH_--PH7yeDwv9bWqcaaUrUPQOXYxV3N0Xq30b6y03d8jneda5GXv9PjjFq3SNO98D5eFa6170sUWjnZ8H24mU_lGyDiaNzzHROsCkL2w8qqiKGXM&token_type=Bearer&expires_in=3600&state=b6c42d677f4b4be4ac45314d457375f6: <empty>
E           <pre id="loginResult"></pre>

.../hostedtoolcache/Python/3.13.11............/x64/lib/python3.13/unittest/case.py:732: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@rissson rissson merged commit a109c99 into version-2025.12 Feb 2, 2026
81 of 83 checks passed
@rissson rissson deleted the cherry-pick/19935-to-version-2025.12 branch February 2, 2026 14:35
enchantednatures pushed a commit to enchantednatures/HomeCluster that referenced this pull request Feb 9, 2026
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[authentik](https://registry.terraform.io/providers/goauthentik/authentik)
([source](https://redirect.github.com/goauthentik/terraform-provider-authentik))
| required_provider | patch | `2025.12.0` → `2025.12.1` |
| [authentik](https://goauthentik.io)
([source](https://redirect.github.com/goauthentik/helm)) | | patch |
`2025.12.2` → `2025.12.3` |

---

> [!WARNING]
> Some dependencies could not be looked up. Check the Dependency
Dashboard for more information.

---

### Release Notes

<details>
<summary>goauthentik/terraform-provider-authentik (authentik)</summary>

###
[`v2025.12.1`](https://redirect.github.com/goauthentik/terraform-provider-authentik/releases/tag/v2025.12.1)

[Compare
Source](https://redirect.github.com/goauthentik/terraform-provider-authentik/compare/v2025.12.0...v2025.12.1)

##### Changelog

-
[`a19acb4`](https://redirect.github.com/goauthentik/terraform-provider-authentik/commit/a19acb417fb54a542a6476bda37775684ae8574a):
fix int conversion for outpost provider attachment
([#&#8203;832](https://redirect.github.com/goauthentik/terraform-provider-authentik/issues/832))
([@&#8203;BeryJu](https://redirect.github.com/BeryJu))
-
[`a923cda`](https://redirect.github.com/goauthentik/terraform-provider-authentik/commit/a923cda4bf2f0ec295be296dcbbebe8f7ada11d1):
fix: Support path-based Authentik deployments in URL configuration
([#&#8203;829](https://redirect.github.com/goauthentik/terraform-provider-authentik/issues/829))
([@&#8203;StefanMarkmann](https://redirect.github.com/StefanMarkmann))
-
[`8edeec9`](https://redirect.github.com/goauthentik/terraform-provider-authentik/commit/8edeec9805f126d1deb1ad5aa1a50740c5aaadfd):
Bump github.com/getsentry/sentry-go from 0.40.0 to 0.41.0
([#&#8203;834](https://redirect.github.com/goauthentik/terraform-provider-authentik/issues/834))
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])
-
[`e10b039`](https://redirect.github.com/goauthentik/terraform-provider-authentik/commit/e10b039884fb2fbe8ff34a6557dc38d4e714ae96):
Bump actions/checkout from 6.0.1 to 6.0.2
([#&#8203;836](https://redirect.github.com/goauthentik/terraform-provider-authentik/issues/836))
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])
-
[`aa73b84`](https://redirect.github.com/goauthentik/terraform-provider-authentik/commit/aa73b84a5653eeb68633f9334cf7836d9112bbbc):
Bump github.com/getsentry/sentry-go from 0.41.0 to 0.42.0
([#&#8203;842](https://redirect.github.com/goauthentik/terraform-provider-authentik/issues/842))
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])
-
[`4553f1d`](https://redirect.github.com/goauthentik/terraform-provider-authentik/commit/4553f1dba852ed64819277f3426aaf1cf449c25e):
Bump github.com/hashicorp/terraform-plugin-sdk/v2 from 2.38.1 to 2.38.2
([#&#8203;844](https://redirect.github.com/goauthentik/terraform-provider-authentik/issues/844))
([@&#8203;dependabot](https://redirect.github.com/dependabot)\[bot])
-
[`749d807`](https://redirect.github.com/goauthentik/terraform-provider-authentik/commit/749d807758f8d4f5a0dd2f5b4a52ad54827ac28b):
fix: paginate RbacPermissionsList to find all role permissions
([#&#8203;841](https://redirect.github.com/goauthentik/terraform-provider-authentik/issues/841))
([@&#8203;dominic-r](https://redirect.github.com/dominic-r))

</details>

<details>
<summary>goauthentik/helm (authentik)</summary>

###
[`v2025.12.3`](https://redirect.github.com/goauthentik/helm/releases/tag/authentik-2025.12.3)

[Compare
Source](https://redirect.github.com/goauthentik/helm/compare/authentik-2025.12.2...authentik-2025.12.3)

authentik is an open-source Identity Provider focused on flexibility and
versatility

#### What's Changed

- charts/authentik: bump to 2025.12.3 by
[@&#8203;authentik-automation](https://redirect.github.com/authentik-automation)\[bot]
in [#&#8203;444](https://redirect.github.com/goauthentik/helm/pull/444)
- 2025.12: Revert bulk revoke added by accident in release branch by
[@&#8203;dominic-r](https://redirect.github.com/dominic-r) in
[goauthentik/authentik#19870](https://redirect.github.com/goauthentik/authentik/pull/19870)
- web/admin: fix toggle-group for bindings now showing up (cherry-pick
[#&#8203;19820](https://redirect.github.com/goauthentik/helm/issues/19820)
to version-2025.12) by
[@&#8203;authentik-automation](https://redirect.github.com/authentik-automation)\[bot]
in
[goauthentik/authentik#19895](https://redirect.github.com/goauthentik/authentik/pull/19895)
- web: fix Brand CSS not applied to nested Shadow DOM components
(cherry-pick
[#&#8203;19892](https://redirect.github.com/goauthentik/helm/issues/19892)
to version-2025.12) by
[@&#8203;authentik-automation](https://redirect.github.com/authentik-automation)\[bot]
in
[goauthentik/authentik#19900](https://redirect.github.com/goauthentik/authentik/pull/19900)
- ci: always generate API clients
([#&#8203;19906](https://redirect.github.com/goauthentik/helm/issues/19906))
by [@&#8203;BeryJu](https://redirect.github.com/BeryJu) in
[goauthentik/authentik#19932](https://redirect.github.com/goauthentik/authentik/pull/19932)
- lifecycle/ak: make sure /data has the correct permissions (cherry-pick
[#&#8203;19935](https://redirect.github.com/goauthentik/helm/issues/19935)
to version-2025.12) by
[@&#8203;authentik-automation](https://redirect.github.com/authentik-automation)\[bot]
in
[goauthentik/authentik#19940](https://redirect.github.com/goauthentik/authentik/pull/19940)
- lifecycle/aws: add /data volume (cherry-pick
[#&#8203;19936](https://redirect.github.com/goauthentik/helm/issues/19936)
to version-2025.12) by
[@&#8203;authentik-automation](https://redirect.github.com/authentik-automation)\[bot]
in
[goauthentik/authentik#19938](https://redirect.github.com/goauthentik/authentik/pull/19938)
- core: fix non-expiring service accounts and app passwords (cherry-pick
[#&#8203;19913](https://redirect.github.com/goauthentik/helm/issues/19913)
to version-2025.12) by
[@&#8203;authentik-automation](https://redirect.github.com/authentik-automation)\[bot]
in
[goauthentik/authentik#19941](https://redirect.github.com/goauthentik/authentik/pull/19941)

**Full Changelog**:
<goauthentik/authentik@version/2025.12.2...version/2025.12.3>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "every weekend" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/enchantednatures/HomeCluster).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi45NS4yIiwidXBkYXRlZEluVmVyIjoiNDIuOTUuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsicmVub3ZhdGUvaGVsbSIsInR5cGUvcGF0Y2giXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant