07.03 — Scénarios Dashboard login#
Trois familles : happy paths, unhappy paths, data-driven multi-login. Tous exercent le
useAuthà 10% de raté et la coordination OTP.
dashboard_login#
# src/tests/campaigns/dashboard_login.py
def create_igoristan_login_campaign(*, drivers_pool: SeleniumWebDriversPool) -> TestCampaign:
return TestCampaign(
name="Dashboard login",
suites=[
create_igoristan_login_happy_paths_test_suite(drivers_pool=drivers_pool),
create_igoristan_login_unhappy_paths_test_suite(drivers_pool=drivers_pool),
create_igoristan_login_data_driven_test_suite(drivers_pool=drivers_pool),
],
)Happy paths#
# src/tests/suites/dashboard/access/happy_paths.py
def create_igoristan_login_happy_paths_test_suite(*, drivers_pool) -> TestSuite:
return TestSuite(
name="Login happy paths",
tests=[
test_dashboard_login_without_otp_happy_path,
test_dashboard_login_with_otp_happy_path,
test_dashboard_login_page_back_to_igoristan_button,
],
drivers_pool=drivers_pool,
)Test 1 : login sans OTP#
def dashboard_login_without_otp_happy_path(driver, logger):
dashboard_creds = create_env_getters().get_credentials("dashboard")
on_dashboard_login_page = DashboardLoginPage(driver=driver)
on_dashboard_welcome_page = DashboardWelcomePage(driver=driver)
just_log_error = create_just_log_error(logger=logger)
log_error_with_current_url = create_log_error_with_current_url(logger=logger, driver=driver)
just_log_success = create_just_log_success(logger=logger)
log_success_with_current_url_and_take_screenshot = create_log_success_with_current_url_and_take_screenshot(...)
retries_amount = max(get_max_workers(), 10)
return [
drive_page(
act(on_dashboard_login_page, open_dashboard_login_page)
.failure(just_log_error("Failed to open the dashboard login page..."))
.success(just_log_success("Opened the dashboard login page!")),
act(on_dashboard_login_page, verify_dashboard_login_page)
.failure(log_error_with_current_url("Failed to verify..."))
.success(log_success_with_current_url_and_take_screenshot("Verified!")),
act(on_dashboard_login_page,
login_without_otp_and_with_retries(dashboard_creds, retries_amount, logger=logger))
.failure(just_log_error("Failed to connect to the dashboard without OTP..."))
.success(just_log_success("Connected to the dashboard!")),
),
drive_page(
act(on_dashboard_welcome_page, verify_dashboard_welcome_page)
.failure(log_error_with_current_url("Failed to verify..."))
.success(log_success_with_current_url_and_take_screenshot("Verified!")),
),
]retries_amount = max(get_max_workers(), 10): on rejoue le login interne (au POM) au moins 10 fois, à cause des 10% d’échec aléatoire.login_without_otp_and_with_retries(creds, retries_amount, logger=logger): closure qui capture creds + nb retries + logger. Le connector retourneCallable[[DashboardLoginPage], DashboardLoginPage].- 2
drive_page: login + welcome page. Chacun ouvre, vérifie, screenshot. - Log factories partout : aucune lambda inline.
SeleniumTitleMixincôté POM :get_current_title()est utilisé par le hookon_failuredeact.
Test 2 : login avec OTP#
def dashboard_login_with_otp_happy_path(driver, logger):
# ... setup pareil ...
cache = in_memory_cache_with_30m_ttl
fresh_cache_key_for_username = reserve_free_cache_key(cache)
fresh_cache_key_for_otp_send_button_click_date = reserve_free_cache_key(cache)
return [
drive_page(
act(on_dashboard_login_page, open_dashboard_login_page)...,
act(on_dashboard_login_page, verify_dashboard_login_page)...,
act(on_dashboard_login_page,
start_to_login_with_otp_and_with_retries(
dashboard_creds, retries_amount,
cache=cache, logger=logger,
username_cache_key=fresh_cache_key_for_username,
otp_send_button_click_date_cache_key=fresh_cache_key_for_otp_send_button_click_date,
))...,
act(on_dashboard_login_page, verify_otp_screen)...,
act(on_dashboard_login_page,
type_otp_with_retries(
retries_amount,
cache=cache, logger=logger,
username_cache_key=fresh_cache_key_for_username,
otp_send_button_click_date_cache_key=fresh_cache_key_for_otp_send_button_click_date,
))...,
),
drive_page(
act(on_dashboard_welcome_page, verify_dashboard_welcome_page)...,
act(on_dashboard_welcome_page, click_on_go_to_nested_page_btn)...,
),
drive_page(
act(on_dashboard_protected_page, verify_dashboard_protected_page)...,
),
]Trois drive_page. Cinq act dans le premier. Cache L1 + clés réservées pour partager des valeurs entre acts (cf. 08-caches-locks.md).
Test 3 : back to Igoristan button#
Test simple : sur la dashboard login page, cliquer sur « Back to Igoristan » et vérifier qu’on est revenu à la homepage. Utilise post_test_scenarios_fragments=[verify_homepage].
Unhappy paths#
tests/scenarios/dashboard/access/unhappy_paths.py
- Login avec mauvais mot de passe → erreur affichée.
- Accès à
/dashboard/nestedsans login → redirection. - Accès à
/dashboard/nestedavec login sans OTP → message « Access requires OTP ».
Le 3e cas utilise pre_test_scenarios_fragments=[login_without_otp_happy_path] :
test_cant_access_the_protected_page_without_otp_using_the_ui = create_selenium_test(
name="Can't access the protected page without OTP (using the UI)",
test_scenario=lambda driver, logger: Scenario(
test_chain=dashboard_access_to_protected_page_without_otp_using_the_ui(driver, logger)
),
pre_test_scenarios_fragments=[login_without_otp_happy_path],
)→ Le fragment login_without_otp_happy_path est exécuté avant le test. Le test commence donc en étant déjà loggé sans OTP.
Data-driven multi-login#
# tests/scenarios/dashboard/data_driven/datasets/multi_login.py
multi_login_dataset: Sequence[MappingProxyType[ImmutableCredentialsKeys, str]] = [
MappingProxyType({"login": "any", "password": "figatellu"}),
MappingProxyType({"login": "Napoleon", "password": "figatellu"}),
MappingProxyType({"login": "NoSicilianAllowed", "password": "figatellu"}),
MappingProxyType({"login": "anonymous", "password": "figatellu"}),
MappingProxyType({"login": "TheEmpire", "password": "figatellu"}),
]Cinq logins différents (tous valides : figatellu est un passe-partout).
Chaque combinaison génère un test :
# tests/scenarios/dashboard/data_driven/multi_login.py
def _create_login_scenario(credentials: ImmutableCredentials) -> SeleniumTestScenario:
def _scenario(driver, logger):
dashboard_creds = credentials # ← closure
on_dashboard_login_page = DashboardLoginPage(driver=driver)
on_dashboard_welcome_page = DashboardWelcomePage(driver=driver)
...
return Scenario(test_chain=[
drive_page(
...,
act(on_dashboard_login_page,
login_without_otp_and_with_retries(dashboard_creds, retries_amount, logger=logger))
.failure(just_log_error("Failed to connect..."))
.success(just_log_success(f"Connected to the dashboard as {dashboard_creds['login']}!")),
),
drive_page(
act(on_dashboard_welcome_page, ...)...,
),
])
return _scenario
multi_login_tests = [
create_selenium_test(
name=f"Login - {creds['login']}",
test_scenario=_create_login_scenario(creds),
)
for creds in multi_login_dataset
]- Factory
_create_login_scenario(creds): retourne un_scenarioqui capturecredsvia une closure. - Liste de tests : un test par entrée du dataset, nommé
"Login - <login>". Scenarioau lieu d’une simpletest_chain: retourné parce qu’on construit dans la factory, on encapsule.
Suite data-driven#
def create_suite(*, drivers_pool: SeleniumWebDriversPool) -> TestSuite:
return TestSuite(
name="Login (data-driven PoC)",
tests=[*multi_login_tests], # ← unpack
drivers_pool=drivers_pool,
)[*multi_login_tests] unpacke la liste dans le tests=.
Si on veut ajouter des tests en plus : tests=[*multi_login_tests, test_extra].
Pattern généralisable#
Le Holy Book le formalise (chapitre datasets-smoke-tests-...):
Une closure suffit. On notera que
Scenarioest déclaré depuis l’intérieur ici : logique, puisqu’il s’agit de l’encapsuler.
Pattern réutilisable pour tout test où :
- La forme du scénario est identique.
- Seules les données d’entrée varient.