Skip to content

Commit 7029c4e

Browse files
committed
First draft for user permissions test
Updates to test scope based on Jenkins job. isort fixes. Broadening test scope. Adding settings, monitoring, policy_registry and partners. Replacing page.is_displayed with more general check. Removing APIDocsView based on Jenkins job Add tests for the bot protection of the admin portal
1 parent e20fdbe commit 7029c4e

6 files changed

Lines changed: 207 additions & 56 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Test for login into admin portal with bot protection (recaptcha) enabled"""
2+
3+
import pytest
4+
from packaging.version import Version
5+
6+
from testsuite import TESTED_VERSION, settings
7+
from testsuite.ui.views.admin.login import LoginView, RequestAdminPasswordView
8+
from testsuite.ui.views.admin.settings.bot_protection import AdminBotProtection
9+
10+
pytestmark = [
11+
pytest.mark.usefixtures("login"),
12+
pytest.mark.usefixtures("bot_protection_setup"),
13+
pytest.mark.issue("https://redhat.atlassian.net/browse/THREESCALE-765"),
14+
pytest.mark.skipif(TESTED_VERSION < Version("2.16"), reason="TESTED_VERSION < Version('2.16')"),
15+
]
16+
17+
18+
@pytest.fixture(scope="module")
19+
def bot_protection_setup(navigator, browser):
20+
"""
21+
Enables admin portal bot protection via UI,
22+
then clears session so subsequent tests see the unauthenticated login page.
23+
Requires recaptcha keys to be configured in 3scale settings.
24+
Session cookies are saved before deletion and restored in teardown to avoid
25+
circular dependency (recaptcha blocks selenium login during teardown).
26+
"""
27+
bot_page = navigator.navigate(AdminBotProtection)
28+
bot_page.enable_protection()
29+
saved_cookies = browser.selenium.get_cookies()
30+
browser.selenium.delete_all_cookies()
31+
32+
yield
33+
34+
browser.selenium.get(settings["threescale"]["admin"]["url"])
35+
for cookie in saved_cookies:
36+
browser.selenium.add_cookie(cookie)
37+
browser.selenium.refresh()
38+
navigator.navigate(AdminBotProtection).disable_protection()
39+
40+
41+
def test_admin_login_blocked_by_recaptcha(navigator):
42+
"""
43+
Test
44+
- Navigates to the admin portal login page
45+
- Waits for reCAPTCHA to load and generate a token
46+
- Attempts to log in with valid credentials
47+
- Asserts that login is rejected due to low reCAPTCHA score from automated browser
48+
"""
49+
login_page = navigator.open(LoginView, url=settings["threescale"]["admin"]["url"], wait_displayed=False)
50+
assert login_page.recaptcha.is_displayed, "Recaptcha was not found on the admin portal login page"
51+
login_page.browser.execute_script( # hack to force reCAPTCHA V3 get bad score
52+
"window.grecaptcha.execute = () => Promise.resolve('');"
53+
)
54+
login_page.login_widget.do_login(
55+
settings["threescale"]["admin"]["username"], settings["threescale"]["admin"]["password"]
56+
)
57+
assert login_page.error_message.is_displayed, "Expected reCAPTCHA error message to be displayed"
58+
59+
60+
@pytest.mark.xfail # not implemented yet
61+
def test_admin_forgot_password_blocked_by_recaptcha(navigator):
62+
"""
63+
Test
64+
- Navigates to the admin portal forgot password page
65+
- Overrides reCAPTCHA token to force rejection
66+
- Submits a password reset request
67+
- Asserts that the request is rejected due to invalid reCAPTCHA token
68+
"""
69+
forgot_pass = navigator.open(RequestAdminPasswordView, url=settings["threescale"]["admin"]["url"])
70+
assert forgot_pass.recaptcha.is_displayed, "Recaptcha was not found on the admin portal forgot password page"
71+
forgot_pass.browser.execute_script( # hack to force reCAPTCHA V3 get bad score
72+
"window.grecaptcha.execute = () => Promise.resolve('');"
73+
)
74+
forgot_pass.reset_password(settings["threescale"]["admin"]["username"])
75+
assert forgot_pass.error_message.is_displayed, "Expected reCAPTCHA error message to be displayed"

testsuite/tests/ui/test_login_page.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,4 @@ def test_log_with_random_username_password(refreshed_browser):
6262
login_view.login_widget.fill_passwd("password")
6363
assert login_view.login_widget.submit.is_enabled
6464
login_view.login_widget.submit.click()
65-
assert "Incorrect email or password. Please try again." in login_view.error_message.text
65+
assert "Incorrect email or password. Please try again." in login_view.error_message.title
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Conftest for user permissions tests"""
2+
3+
import pytest
4+
from selenium.common.exceptions import NoSuchElementException, WebDriverException
5+
from widgetastic.widget import GenericLocatorWidget
6+
7+
8+
@pytest.fixture
9+
def is_page_accessible():
10+
"""
11+
Helper function for permission tests to check if a page is accessible.
12+
13+
This checks for successful page access by verifying:
14+
1. Correct URL path is loaded
15+
2. The masthead header is present (exists on all allowed pages, not on access denied)
16+
"""
17+
18+
def _check(page):
19+
if page.path not in page.browser.url:
20+
return False
21+
22+
try:
23+
masthead = GenericLocatorWidget(
24+
page, locator="//header[contains(@class, 'pf-c-masthead') and contains(@class, 'pf-m-display-inline')]"
25+
)
26+
if not masthead.is_displayed:
27+
return False
28+
except (NoSuchElementException, WebDriverException):
29+
return False
30+
31+
return True
32+
33+
return _check

testsuite/tests/ui/users_and_roles/test_permissions.py

Lines changed: 45 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,76 +2,68 @@
22

33
import pytest
44

5-
from testsuite.ui.views.admin.audience.billing import BillingSettingsView, BillingView
5+
from testsuite.ui.views.admin.audience.account import AccountsView
6+
from testsuite.ui.views.admin.audience.billing import (
7+
BillingSettingsView,
8+
BillingView,
9+
)
610
from testsuite.ui.views.admin.audience.developer_portal import (
711
ActiveDocsView,
812
CMSNewPageView,
913
CMSNewSectionView,
1014
DeveloperPortalContentView,
1115
)
16+
from testsuite.ui.views.admin.backend.analytics import BackendTrafficView
1217
from testsuite.ui.views.admin.foundation import AccessDeniedView
1318

14-
PERMISSION_DICT = [
15-
pytest.param("portal", DeveloperPortalContentView),
16-
pytest.param("portal", DeveloperPortalContentView),
17-
pytest.param("portal", CMSNewPageView),
18-
pytest.param("portal", CMSNewSectionView),
19-
pytest.param("finance", BillingView),
20-
pytest.param(
21-
"finance",
22-
BillingSettingsView,
23-
marks=[pytest.mark.xfail, pytest.mark.issue("https://issues.redhat.com/browse/THREESCALE-10995")],
24-
),
25-
pytest.param("plans", ActiveDocsView),
26-
]
27-
28-
29-
@pytest.fixture()
30-
def all_page_objects():
31-
"""Returns all page objects from permissions tuple filtered of views with same permission"""
32-
33-
def _all_page_objects(except_permission, current_view):
19+
PERMISSIONS = ["portal", "finance", "settings", "partners", "monitoring", "plans", "policy_registry"]
3420

35-
all_views = [
36-
view
37-
for perm, view in [param.values for param in PERMISSION_DICT]
38-
if perm != except_permission or view == current_view
39-
]
40-
41-
return all_views
42-
43-
return _all_page_objects
21+
VIEWS = [
22+
("portal", DeveloperPortalContentView),
23+
("portal", CMSNewPageView),
24+
("portal", CMSNewSectionView),
25+
("finance", BillingView),
26+
("finance", BillingSettingsView),
27+
("plans", ActiveDocsView),
28+
("monitoring", BackendTrafficView),
29+
("partners", AccountsView),
30+
]
4431

4532

4633
# pylint: disable=too-many-arguments
47-
@pytest.mark.parametrize("permission, page_view", PERMISSION_DICT)
34+
@pytest.mark.parametrize("user_permission", PERMISSIONS)
35+
@pytest.mark.parametrize("required_permission, page_view", VIEWS)
4836
def test_member_user_permissions_per_section(
37+
account_password,
4938
custom_admin_login,
5039
navigator,
5140
provider_member_user,
52-
all_page_objects,
53-
permission,
41+
backend_default,
42+
user_permission,
43+
required_permission,
5444
page_view,
55-
allowed_services=False,
45+
is_page_accessible,
5646
):
57-
"""Tests user permissions permission per permission section"""
58-
member_user = provider_member_user(allowed_sections=permission, allowed_services=allowed_services)
59-
custom_admin_login(member_user.entity_name, "123456")
60-
61-
page_objects = all_page_objects(permission, page_view)
47+
"""
48+
Tests user permissions permission per permission section
49+
- Creates a member user with a specific permission
50+
- Logs in as that member user
51+
- Attempts to access a specific UI page
52+
- If users permission matches page's required permission -> allowed
53+
- Else, access denied
54+
"""
55+
member_user = provider_member_user(allowed_sections=[user_permission], allowed_services=None)
56+
custom_admin_login(member_user.entity_name, account_password)
6257

63-
for pg_obj in page_objects:
64-
# Dynamically import the view class
65-
view_module = __import__(pg_obj.__module__, fromlist=[pg_obj.__name__])
66-
page_class = getattr(view_module, pg_obj.__name__)
67-
page = navigator.open(page_class, wait_displayed=False)
58+
if page_view == BackendTrafficView:
59+
page = navigator.open(page_view, backend=backend_default, wait_displayed=False)
60+
else:
61+
page = navigator.open(page_view, wait_displayed=False)
6862

69-
if pg_obj == page_view:
70-
assert (
71-
page.is_displayed
72-
), f"{pg_obj.__name__} should be displayed for permissions {permission} and services {allowed_services}"
73-
else:
74-
assert AccessDeniedView(navigator.browser.root_browser).is_displayed, (
75-
f"{pg_obj.__name__}"
76-
f" should not be displayed for permissions {permission} and services {allowed_services}"
77-
)
63+
if user_permission == required_permission:
64+
assert is_page_accessible(page), f"A user with {user_permission} should be able to access {page_view}"
65+
else:
66+
access_denied_view = AccessDeniedView(navigator.browser.root_browser)
67+
assert (
68+
access_denied_view.is_displayed
69+
), f"A user with {user_permission} should not be able to access {page_view}"

testsuite/ui/views/admin/login.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Representation of Login specific views"""
22

33
from widgetastic.widget import GenericLocatorWidget, Text, TextInput, View
4-
from widgetastic_patternfly4.ouia import Button
4+
from widgetastic_patternfly4.ouia import Alert, Button
55

66
from testsuite.ui.exception import UIException
77
from testsuite.ui.navigation import Navigable, step
@@ -10,6 +10,17 @@
1010
from testsuite.ui.views.common.login import LoginForm
1111

1212

13+
class AdminReCaptcha(View):
14+
"""ReCaptcha badge for admin portal — searches from browser root to escape view ROOT scoping."""
15+
16+
@property
17+
def is_displayed(self):
18+
element = self.browser.wait_for_element(
19+
"//div[contains(@class,'grecaptcha-logo')]", timeout=10, exception=False
20+
)
21+
return element is not None
22+
23+
1324
class LoginView(View, Navigable):
1425
"""
1526
Basic login view page object that can be found on path
@@ -18,9 +29,10 @@ class LoginView(View, Navigable):
1829
path = "/p/login"
1930
ROOT = "/html//div[@id='pf-login-page-container']"
2031
header = Text("//main/header/h2")
21-
error_message = Text("//h4[@class='pf-c-alert__title']")
32+
error_message = Alert(component_id="OUIA-Generated-Alert-danger-1")
2233
login_widget = View.nested(LoginForm)
2334
password_reset_link = Text("//a[@href='/p/password/reset']")
35+
recaptcha = View.nested(AdminReCaptcha)
2436
auth0_link = Text("//*[@class='login-provider-link' and contains(@href,'auth0')]")
2537
rhsso_link = Text("//*[@class='login-provider-link' and contains(@href,'keycloak')]")
2638

@@ -91,6 +103,8 @@ class RequestAdminPasswordView(View, Navigable):
91103
path = "/p/password/reset"
92104
password_reset_field = TextInput(id="email")
93105
passwd_reset_btn = Button(component_id="OUIA-Generated-Button-primary-1")
106+
recaptcha = View.nested(AdminReCaptcha)
107+
error_message = Alert(component_id="OUIA-Generated-Alert-danger-1")
94108

95109
def reset_password(self, email):
96110
"""Reset password of email address user"""
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""View representation of Admin Portal Bot Protection settings page"""
2+
3+
from widgetastic.widget import Text
4+
5+
from testsuite.ui.views.admin.settings import BaseSettingsView
6+
from testsuite.ui.widgets.buttons import ThreescaleSubmitButton
7+
8+
9+
class AdminBotProtection(BaseSettingsView):
10+
"""View representation of Admin Portal Bot Protection settings"""
11+
12+
path_pattern = "/p/admin/bot_protection/edit"
13+
no_protection = Text('//*[@id="settings_admin_bot_protection_level_none"]')
14+
recaptcha_protection = Text('//*[@id="settings_admin_bot_protection_level_captcha"]')
15+
submit_button = ThreescaleSubmitButton()
16+
17+
def prerequisite(self):
18+
return BaseSettingsView
19+
20+
def disable_protection(self):
21+
"""Disables admin portal bot protection by selecting None and submitting."""
22+
self.no_protection.click()
23+
self.submit_button.click()
24+
25+
def enable_protection(self):
26+
"""Enables admin portal bot protection by selecting reCAPTCHA v3 and submitting."""
27+
self.recaptcha_protection.click()
28+
self.submit_button.click()
29+
30+
@property
31+
def is_displayed(self):
32+
return (
33+
BaseSettingsView.is_displayed.fget(self)
34+
and self.path in self.browser.url
35+
and self.no_protection.is_displayed
36+
and self.recaptcha_protection.is_displayed
37+
)

0 commit comments

Comments
 (0)