e2e: add @retry decorator to make e2e tests more reliable
This commit is contained in:
		| @ -8,7 +8,7 @@ from docker.types import Healthcheck | ||||
| from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.support import expected_conditions as ec | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from e2e.utils import USER, SeleniumTestCase, retry | ||||
| from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding | ||||
| from passbook.stages.email.models import EmailStage, EmailTemplates | ||||
| from passbook.stages.identification.models import IdentificationStage | ||||
| @ -34,6 +34,7 @@ class TestFlowsEnroll(SeleniumTestCase): | ||||
|             ), | ||||
|         } | ||||
|  | ||||
|     @retry() | ||||
|     def test_enroll_2_step(self): | ||||
|         """Test 2-step enroll flow""" | ||||
|         # First stage fields | ||||
| @ -119,6 +120,7 @@ class TestFlowsEnroll(SeleniumTestCase): | ||||
|             "foo@bar.baz", | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     @override_settings(EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend") | ||||
|     def test_enroll_email(self): | ||||
|         """Test enroll with Email verification""" | ||||
|  | ||||
| @ -5,13 +5,14 @@ from unittest.case import skipUnless | ||||
| from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.common.keys import Keys | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from e2e.utils import USER, SeleniumTestCase, retry | ||||
|  | ||||
|  | ||||
| @skipUnless(platform.startswith("linux"), "requires local docker") | ||||
| class TestFlowsLogin(SeleniumTestCase): | ||||
|     """test default login flow""" | ||||
|  | ||||
|     @retry() | ||||
|     def test_login(self): | ||||
|         """test default login flow""" | ||||
|         self.driver.get(f"{self.live_server_url}/flows/default-authentication-flow/") | ||||
|  | ||||
| @ -12,7 +12,7 @@ from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.common.keys import Keys | ||||
| from selenium.webdriver.support import expected_conditions as ec | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from e2e.utils import USER, SeleniumTestCase, retry | ||||
| from passbook.flows.models import Flow, FlowStageBinding | ||||
| from passbook.stages.otp_validate.models import OTPValidateStage | ||||
|  | ||||
| @ -21,6 +21,7 @@ from passbook.stages.otp_validate.models import OTPValidateStage | ||||
| class TestFlowsOTP(SeleniumTestCase): | ||||
|     """test flow with otp stages""" | ||||
|  | ||||
|     @retry() | ||||
|     def test_otp_validate(self): | ||||
|         """test flow with otp stages""" | ||||
|         sleep(1) | ||||
| @ -52,6 +53,7 @@ class TestFlowsOTP(SeleniumTestCase): | ||||
|             USER().username, | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     def test_otp_totp_setup(self): | ||||
|         """test TOTP Setup stage""" | ||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||
| @ -98,6 +100,7 @@ class TestFlowsOTP(SeleniumTestCase): | ||||
|  | ||||
|         self.assertTrue(TOTPDevice.objects.filter(user=USER(), confirmed=True).exists()) | ||||
|  | ||||
|     @retry() | ||||
|     def test_otp_static_setup(self): | ||||
|         """test Static OTP Setup stage""" | ||||
|         flow: Flow = Flow.objects.get(slug="default-authentication-flow") | ||||
|  | ||||
| @ -5,7 +5,7 @@ from unittest.case import skipUnless | ||||
| from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.common.keys import Keys | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from e2e.utils import USER, SeleniumTestCase, retry | ||||
| from passbook.core.models import User | ||||
| from passbook.flows.models import Flow, FlowDesignation | ||||
| from passbook.providers.oauth2.generators import generate_client_secret | ||||
| @ -16,6 +16,7 @@ from passbook.stages.password.models import PasswordStage | ||||
| class TestFlowsStageSetup(SeleniumTestCase): | ||||
|     """test stage setup flows""" | ||||
|  | ||||
|     @retry() | ||||
|     def test_password_change(self): | ||||
|         """test password change flow""" | ||||
|         # Ensure that password stage has change_flow set | ||||
|  | ||||
| @ -9,7 +9,7 @@ from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.common.keys import Keys | ||||
| from selenium.webdriver.support import expected_conditions as ec | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from e2e.utils import USER, SeleniumTestCase, retry | ||||
| from passbook.core.models import Application | ||||
| from passbook.flows.models import Flow | ||||
| from passbook.policies.expression.models import ExpressionPolicy | ||||
| @ -61,6 +61,7 @@ class TestProviderOAuth2Github(SeleniumTestCase): | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|     @retry() | ||||
|     def test_authorization_consent_implied(self): | ||||
|         """test OAuth Provider flow (default authorization flow with implied consent)""" | ||||
|         # Bootstrap all needed objects | ||||
| @ -115,6 +116,7 @@ class TestProviderOAuth2Github(SeleniumTestCase): | ||||
|             USER().username, | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     def test_authorization_consent_explicit(self): | ||||
|         """test OAuth Provider flow (default authorization flow with explicit consent)""" | ||||
|         # Bootstrap all needed objects | ||||
| @ -184,6 +186,7 @@ class TestProviderOAuth2Github(SeleniumTestCase): | ||||
|             USER().username, | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     def test_denied(self): | ||||
|         """test OAuth Provider flow (default authorization flow, denied)""" | ||||
|         # Bootstrap all needed objects | ||||
|  | ||||
| @ -10,7 +10,7 @@ from selenium.webdriver.common.keys import Keys | ||||
| from selenium.webdriver.support import expected_conditions as ec | ||||
| from structlog import get_logger | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from e2e.utils import USER, SeleniumTestCase, retry | ||||
| from passbook.core.models import Application | ||||
| from passbook.crypto.models import CertificateKeyPair | ||||
| from passbook.flows.models import Flow | ||||
| @ -80,6 +80,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|     @retry() | ||||
|     def test_redirect_uri_error(self): | ||||
|         """test OpenID Provider flow (invalid redirect URI, check error message)""" | ||||
|         sleep(1) | ||||
| @ -122,6 +123,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | ||||
|             "Redirect URI Error", | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     def test_authorization_consent_implied(self): | ||||
|         """test OpenID Provider flow (default authorization flow with implied consent)""" | ||||
|         sleep(1) | ||||
| @ -183,6 +185,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | ||||
|             USER().email, | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     def test_authorization_logout(self): | ||||
|         """test OpenID Provider flow with logout""" | ||||
|         sleep(1) | ||||
| @ -252,6 +255,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | ||||
|         ) | ||||
|         self.driver.find_element(By.ID, "logout").click() | ||||
|  | ||||
|     @retry() | ||||
|     def test_authorization_consent_explicit(self): | ||||
|         """test OpenID Provider flow (default authorization flow with explicit consent)""" | ||||
|         sleep(1) | ||||
| @ -325,6 +329,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase): | ||||
|             USER().email, | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     def test_authorization_denied(self): | ||||
|         """test OpenID Provider flow (default authorization with access deny)""" | ||||
|         sleep(1) | ||||
|  | ||||
| @ -12,7 +12,7 @@ from selenium.webdriver.common.keys import Keys | ||||
| from selenium.webdriver.support import expected_conditions as ec | ||||
| from structlog import get_logger | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from e2e.utils import USER, SeleniumTestCase, retry | ||||
| from passbook.core.models import Application | ||||
| from passbook.crypto.models import CertificateKeyPair | ||||
| from passbook.flows.models import Flow | ||||
| @ -76,6 +76,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | ||||
|             LOGGER.info("Container failed healthcheck") | ||||
|             sleep(1) | ||||
|  | ||||
|     @retry() | ||||
|     def test_redirect_uri_error(self): | ||||
|         """test OpenID Provider flow (invalid redirect URI, check error message)""" | ||||
|         sleep(1) | ||||
| @ -119,6 +120,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | ||||
|             "Redirect URI Error", | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     def test_authorization_consent_implied(self): | ||||
|         """test OpenID Provider flow (default authorization flow with implied consent)""" | ||||
|         sleep(1) | ||||
| @ -169,6 +171,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | ||||
|         self.assertEqual(body["IDTokenClaims"]["email"], USER().email) | ||||
|         self.assertEqual(body["UserInfo"]["email"], USER().email) | ||||
|  | ||||
|     @retry() | ||||
|     def test_authorization_consent_explicit(self): | ||||
|         """test OpenID Provider flow (default authorization flow with explicit consent)""" | ||||
|         sleep(1) | ||||
| @ -229,6 +232,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase): | ||||
|         self.assertEqual(body["IDTokenClaims"]["email"], USER().email) | ||||
|         self.assertEqual(body["UserInfo"]["email"], USER().email) | ||||
|  | ||||
|     @retry() | ||||
|     def test_authorization_denied(self): | ||||
|         """test OpenID Provider flow (default authorization with access deny)""" | ||||
|         sleep(1) | ||||
|  | ||||
| @ -11,7 +11,7 @@ from docker.models.containers import Container | ||||
| from selenium.webdriver.common.by import By | ||||
| from selenium.webdriver.common.keys import Keys | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from e2e.utils import USER, SeleniumTestCase, retry | ||||
| from passbook import __version__ | ||||
| from passbook.core.models import Application | ||||
| from passbook.flows.models import Flow | ||||
| @ -57,6 +57,7 @@ class TestProviderProxy(SeleniumTestCase): | ||||
|         ) | ||||
|         return container | ||||
|  | ||||
|     @retry() | ||||
|     def test_proxy_simple(self): | ||||
|         """Test simple outpost setup with single provider""" | ||||
|         proxy: ProxyProvider = ProxyProvider.objects.create( | ||||
| @ -110,6 +111,7 @@ class TestProviderProxy(SeleniumTestCase): | ||||
| class TestProviderProxyConnect(ChannelsLiveServerTestCase): | ||||
|     """Test Proxy connectivity over websockets""" | ||||
|  | ||||
|     @retry() | ||||
|     def test_proxy_connectivity(self): | ||||
|         """Test proxy connectivity over websocket""" | ||||
|         SeleniumTestCase().apply_default_data() | ||||
|  | ||||
| @ -12,7 +12,7 @@ from selenium.webdriver.common.keys import Keys | ||||
| from selenium.webdriver.support import expected_conditions as ec | ||||
| from structlog import get_logger | ||||
|  | ||||
| from e2e.utils import USER, SeleniumTestCase | ||||
| from e2e.utils import USER, SeleniumTestCase, retry | ||||
| from passbook.core.models import Application | ||||
| from passbook.crypto.models import CertificateKeyPair | ||||
| from passbook.flows.models import Flow | ||||
| @ -66,6 +66,7 @@ class TestProviderSAML(SeleniumTestCase): | ||||
|             LOGGER.info("Container failed healthcheck") | ||||
|             sleep(1) | ||||
|  | ||||
|     @retry() | ||||
|     def test_sp_initiated_implicit(self): | ||||
|         """test SAML Provider flow SP-initiated flow (implicit consent)""" | ||||
|         # Bootstrap all needed objects | ||||
| @ -105,6 +106,7 @@ class TestProviderSAML(SeleniumTestCase): | ||||
|         self.assertEqual(body["attr"]["mail"], [USER().email]) | ||||
|         self.assertEqual(body["attr"]["uid"], [str(USER().pk)]) | ||||
|  | ||||
|     @retry() | ||||
|     def test_sp_initiated_explicit(self): | ||||
|         """test SAML Provider flow SP-initiated flow (explicit consent)""" | ||||
|         # Bootstrap all needed objects | ||||
| @ -150,6 +152,7 @@ class TestProviderSAML(SeleniumTestCase): | ||||
|         self.assertEqual(body["attr"]["mail"], [USER().email]) | ||||
|         self.assertEqual(body["attr"]["uid"], [str(USER().pk)]) | ||||
|  | ||||
|     @retry() | ||||
|     def test_idp_initiated_implicit(self): | ||||
|         """test SAML Provider flow IdP-initiated flow (implicit consent)""" | ||||
|         # Bootstrap all needed objects | ||||
| @ -195,6 +198,7 @@ class TestProviderSAML(SeleniumTestCase): | ||||
|         self.assertEqual(body["attr"]["mail"], [USER().email]) | ||||
|         self.assertEqual(body["attr"]["uid"], [str(USER().pk)]) | ||||
|  | ||||
|     @retry() | ||||
|     def test_sp_initiated_denied(self): | ||||
|         """test SAML Provider flow SP-initiated flow (Policy denies access)""" | ||||
|         # Bootstrap all needed objects | ||||
|  | ||||
| @ -14,7 +14,7 @@ from selenium.webdriver.support import expected_conditions as ec | ||||
| from structlog import get_logger | ||||
| from yaml import safe_dump | ||||
|  | ||||
| from e2e.utils import SeleniumTestCase | ||||
| from e2e.utils import SeleniumTestCase, retry | ||||
| from passbook.flows.models import Flow | ||||
| from passbook.providers.oauth2.generators import ( | ||||
|     generate_client_id, | ||||
| @ -106,6 +106,7 @@ class TestSourceOAuth2(SeleniumTestCase): | ||||
|             consumer_secret=self.client_secret, | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     def test_oauth_enroll(self): | ||||
|         """test OAuth Source With With OIDC""" | ||||
|         self.create_objects() | ||||
| @ -159,6 +160,7 @@ class TestSourceOAuth2(SeleniumTestCase): | ||||
|             "admin@example.com", | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     @override_settings(SESSION_COOKIE_SAMESITE="strict") | ||||
|     def test_oauth_samesite_strict(self): | ||||
|         """test OAuth Source With SameSite set to strict | ||||
| @ -195,6 +197,7 @@ class TestSourceOAuth2(SeleniumTestCase): | ||||
|             "Authentication Failed.", | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     def test_oauth_enroll_auth(self): | ||||
|         """test OAuth Source With With OIDC (enroll and authenticate again)""" | ||||
|         self.test_oauth_enroll() | ||||
| @ -291,6 +294,7 @@ class TestSourceOAuth1(SeleniumTestCase): | ||||
|             consumer_secret=self.client_secret, | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     def test_oauth_enroll(self): | ||||
|         """test OAuth Source With With OIDC""" | ||||
|         self.create_objects() | ||||
|  | ||||
| @ -10,7 +10,7 @@ from selenium.webdriver.common.keys import Keys | ||||
| from selenium.webdriver.support import expected_conditions as ec | ||||
| from structlog import get_logger | ||||
|  | ||||
| from e2e.utils import SeleniumTestCase | ||||
| from e2e.utils import SeleniumTestCase, retry | ||||
| from passbook.crypto.models import CertificateKeyPair | ||||
| from passbook.flows.models import Flow | ||||
| from passbook.sources.saml.models import SAMLBindingTypes, SAMLSource | ||||
| @ -92,6 +92,7 @@ class TestSourceSAML(SeleniumTestCase): | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|     @retry() | ||||
|     def test_idp_redirect(self): | ||||
|         """test SAML Source With redirect binding""" | ||||
|         # Bootstrap all needed objects | ||||
| @ -141,6 +142,7 @@ class TestSourceSAML(SeleniumTestCase): | ||||
|             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "" | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     def test_idp_post(self): | ||||
|         """test SAML Source With post binding""" | ||||
|         # Bootstrap all needed objects | ||||
| @ -192,6 +194,7 @@ class TestSourceSAML(SeleniumTestCase): | ||||
|             self.driver.find_element(By.ID, "id_username").get_attribute("value"), "" | ||||
|         ) | ||||
|  | ||||
|     @retry() | ||||
|     def test_idp_post_auto(self): | ||||
|         """test SAML Source With post binding (auto redirect)""" | ||||
|         # Bootstrap all needed objects | ||||
|  | ||||
							
								
								
									
										37
									
								
								e2e/utils.py
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								e2e/utils.py
									
									
									
									
									
								
							| @ -1,19 +1,22 @@ | ||||
| """passbook e2e testing utilities""" | ||||
| from functools import wraps | ||||
| from glob import glob | ||||
| from importlib.util import module_from_spec, spec_from_file_location | ||||
| from inspect import getmembers, isfunction | ||||
| from os import environ, makedirs | ||||
| from time import sleep, time | ||||
| from typing import Any, Dict, Optional | ||||
| from typing import Any, Callable, Dict, Optional | ||||
|  | ||||
| from django.apps import apps | ||||
| from django.contrib.staticfiles.testing import StaticLiveServerTestCase | ||||
| from django.db import connection, transaction | ||||
| from django.db.utils import IntegrityError | ||||
| from django.shortcuts import reverse | ||||
| from django.test.testcases import TestCase | ||||
| from docker import DockerClient, from_env | ||||
| from docker.models.containers import Container | ||||
| from selenium import webdriver | ||||
| from selenium.common.exceptions import TimeoutException | ||||
| from selenium.webdriver.common.desired_capabilities import DesiredCapabilities | ||||
| from selenium.webdriver.remote.webdriver import WebDriver | ||||
| from selenium.webdriver.support.ui import WebDriverWait | ||||
| @ -123,3 +126,35 @@ class SeleniumTestCase(StaticLiveServerTestCase): | ||||
|                             func(apps, schema_editor) | ||||
|                         except IntegrityError: | ||||
|                             pass | ||||
|  | ||||
|  | ||||
| def retry(max_retires=3, exceptions=None): | ||||
|     """Retry test multiple times. Default to catching Selenium Timeout Exception""" | ||||
|  | ||||
|     if not exceptions: | ||||
|         exceptions = [TimeoutException] | ||||
|  | ||||
|     def retry_actual(func: Callable): | ||||
|         """Retry test multiple times""" | ||||
|         count = 1 | ||||
|  | ||||
|         @wraps(func) | ||||
|         def wrapper(self: TestCase, *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) | ||||
|             # pylint: disable=catching-non-exception | ||||
|             except tuple(exceptions) as exc: | ||||
|                 count += 1 | ||||
|                 if count > max_retires: | ||||
|                     # pylint: disable=raising-non-exception | ||||
|                     raise exc | ||||
|                 self.tearDown() | ||||
|                 self.setUp() | ||||
|                 return wrapper(self, *args, **kwargs) | ||||
|  | ||||
|         return wrapper | ||||
|  | ||||
|     return retry_actual | ||||
|  | ||||
| @ -6348,7 +6348,7 @@ definitions: | ||||
|           for input-based policies. | ||||
|         type: boolean | ||||
|       re_evaluate_policies: | ||||
|         title: Evaluate on call | ||||
|         title: Re evaluate policies | ||||
|         description: Evaluate policies when the Stage is present to the user. | ||||
|         type: boolean | ||||
|       order: | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer