Skip to content

Commit ca1287c

Browse files
authored
Merge branch 'master' into dependabot/pip/images/openwisp_base/boto3-gte-1.43.39-and-lt-1.44.0
2 parents 65e8cb6 + 3d6f69c commit ca1287c

2 files changed

Lines changed: 136 additions & 108 deletions

File tree

tests/runtests.py

Lines changed: 39 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
from selenium.webdriver.common.by import By
1313
from selenium.webdriver.support import expected_conditions as EC
1414
from selenium.webdriver.support.ui import WebDriverWait
15-
from utils import TestUtilities
15+
from utils import BaseTestUtils, FunctionalTestUtils
1616

1717

18-
class Pretest(TestUtilities, unittest.TestCase):
18+
# 0 in the name is on purpose for alphabetical discovery
19+
class Test0Preconditions(BaseTestUtils, unittest.TestCase):
1920
"""Checks to perform before tests"""
2021

2122
def test_wait_for_services(self):
@@ -60,49 +61,14 @@ def test_wait_for_services(self):
6061
self.fail(f"All celery workers are not online: {online_workers}")
6162

6263

63-
class TestServices(TestUtilities, unittest.TestCase):
64+
class TestServices(FunctionalTestUtils, unittest.TestCase):
6465
custom_static_token = None
6566

6667
@property
6768
def failureException(self):
6869
TestServices.failed_test = True
6970
return super().failureException
7071

71-
@classmethod
72-
def _execute_docker_compose_command(cls, cmd_args, use_text_mode=False):
73-
"""Execute a docker compose command and log output.
74-
75-
Args:
76-
cmd_args: List of command arguments for subprocess.Popen
77-
use_text_mode: If True, use text mode for subprocess output
78-
79-
Returns:
80-
Tuple of (output, error) from command execution
81-
"""
82-
kwargs = {
83-
"stdout": subprocess.PIPE,
84-
"stderr": subprocess.PIPE,
85-
"cwd": cls.root_location,
86-
}
87-
if use_text_mode:
88-
kwargs["text"] = True
89-
cmd = subprocess.run(cmd_args, check=False, **kwargs)
90-
if use_text_mode:
91-
output, error = cmd.stdout, cmd.stderr
92-
else:
93-
output = cmd.stdout.decode("utf-8", errors="replace") if cmd.stdout else ""
94-
error = cmd.stderr.decode("utf-8", errors="replace") if cmd.stderr else ""
95-
output, error = map(str, (cmd.stdout, cmd.stderr))
96-
with open(cls.config["logs_file"], "a") as logs_file:
97-
logs_file.write(output)
98-
logs_file.write(error)
99-
if cmd.returncode != 0:
100-
raise RuntimeError(
101-
f"docker compose command failed "
102-
f"({cmd.returncode}): {' '.join(cmd_args)}"
103-
)
104-
return output, error
105-
10672
@classmethod
10773
def _setup_admin_theme_links(cls):
10874
"""Configure admin theme links during tests.
@@ -125,8 +91,8 @@ def _setup_admin_theme_links(cls):
12591
f"body{{--openwisp-test: {cls.custom_static_token};}}"
12692
)
12793
script = rf"""
128-
grep -q OPENWISP_ADMIN_THEME_LINKS /opt/openwisp/openwisp/settings.py || \
129-
printf "\nOPENWISP_ADMIN_THEME_LINKS=[{{\"type\":\"text/css\",\"href\":\"/static/admin/css/openwisp.css\",\"rel\":\"stylesheet\",\"media\":\"all\"}},{{\"type\":\"text/css\",\"href\":\"/static/{cls.config["custom_css_filename"]}\",\"rel\":\"stylesheet\",\"media\":\"all\"}},{{\"type\":\"image/x-icon\",\"href\":\"ui/openwisp/images/favicon.png\",\"rel\":\"icon\"}}]\n" >> /opt/openwisp/openwisp/settings.py &&
94+
sed -i '/^OPENWISP_ADMIN_THEME_LINKS[[:space:]]*=/d' /opt/openwisp/openwisp/settings.py &&
95+
printf "\nOPENWISP_ADMIN_THEME_LINKS=[{{\"type\":\"text/css\",\"href\":\"/static/admin/css/openwisp.css\",\"rel\":\"stylesheet\",\"media\":\"all\"}},{{\"type\":\"text/css\",\"href\":\"/static/{cls.config["custom_css_filename"]}\",\"rel\":\"stylesheet\",\"media\":\"all\"}},{{\"type\":\"image/svg+xml\",\"href\":\"/static/ui/openwisp/images/favicon.svg\",\"rel\":\"icon\"}}]\n" >> /opt/openwisp/openwisp/settings.py &&
13096
python collectstatic.py &&
13197
uwsgi --reload uwsgi.pid
13298
""" # noqa: E501
@@ -153,27 +119,7 @@ def _cleanup_stale_test_data(cls):
153119
avoid any Selenium dependency during setup.
154120
"""
155121
try:
156-
cls._execute_docker_compose_command(
157-
[
158-
"docker",
159-
"compose",
160-
"exec",
161-
"-T",
162-
"dashboard",
163-
"python",
164-
"manage.py",
165-
"shell",
166-
"-c",
167-
(
168-
"from openwisp_users.models import User; "
169-
"User.objects.filter("
170-
"username__in=['signup-user', 'test_superuser', "
171-
"'test_superuser2']"
172-
").delete()"
173-
),
174-
],
175-
use_text_mode=True,
176-
)
122+
cls.delete_test_users("signup-user", "test_superuser", "test_superuser2")
177123
except Exception as e:
178124
exc_type = type(e).__name__
179125
print(
@@ -223,6 +169,12 @@ def setUpClass(cls):
223169

224170
@classmethod
225171
def tearDownClass(cls):
172+
try:
173+
cls.delete_test_users(*cls.test_usernames_to_delete)
174+
cls.test_usernames_to_delete.clear()
175+
except Exception as e:
176+
exc_type = type(e).__name__
177+
print(f"Unable to delete test users: {exc_type}: {e}")
226178
for resource_link in cls.objects_to_delete:
227179
try:
228180
cls._delete_object(resource_link)
@@ -277,6 +229,13 @@ def test_admin_login(self):
277229
def test_custom_static_files_loaded(self):
278230
self.login()
279231
self.open("/admin/")
232+
favicon_href = self.web_driver.find_element(
233+
By.CSS_SELECTOR, 'link[rel="icon"]'
234+
).get_attribute("href")
235+
self.assertRegex(
236+
favicon_href,
237+
r"/static/ui/openwisp/images/favicon(\.[0-9a-f]+)?\.svg$",
238+
)
280239
# Check if the custom CSS variable is applied
281240
value = self.web_driver.execute_script(
282241
"return getComputedStyle(document.body)"
@@ -362,6 +321,7 @@ def test_add_superuser(self):
362321
def test_forgot_password(self):
363322
"""Test forgot password to ensure that postfix is working properly."""
364323

324+
self.login()
365325
self.logout()
366326
try:
367327
WebDriverWait(self.base_driver, 3).until(
@@ -463,23 +423,22 @@ def _test_celery_task_registered(container_name):
463423
def test_radius_user_registration(self):
464424
"""Ensure users can register using the RADIUS API."""
465425
url = f"{self.config['api_url']}/api/v1/radius/organization/default/account/"
466-
response = requests.post(
467-
url,
468-
json={
469-
"username": "signup-user",
470-
"email": "user@signup.com",
471-
"password1": "rLx6OH%[",
472-
"password2": "rLx6OH%[",
473-
},
474-
verify=False,
475-
)
476-
self.assertEqual(response.status_code, 201)
477-
# Delete the created user
478-
self.login()
479-
self.get_resource(
480-
"signup-user", "/admin/openwisp_users/user/", "field-username"
481-
)
482-
self.objects_to_delete.append(self.base_driver.current_url)
426+
username = "signup-user"
427+
try:
428+
response = requests.post(
429+
url,
430+
json={
431+
"username": username,
432+
"email": "user@signup.com",
433+
"password1": "rLx6OH%[",
434+
"password2": "rLx6OH%[",
435+
},
436+
verify=False,
437+
timeout=10,
438+
)
439+
self.assertEqual(response.status_code, 201, response.text)
440+
finally:
441+
self.delete_test_users(username)
483442

484443
def test_freeradius(self):
485444
"""Ensure freeradius service is working correctly."""
@@ -532,8 +491,8 @@ def test_containers_down(self):
532491
)
533492

534493

535-
class TestUtils(TestUtilities, unittest.TestCase):
536-
"""Other tests"""
494+
class TestLocalUtils(BaseTestUtils, unittest.TestCase):
495+
"""Tests for local utilities"""
537496

538497
def test_update_version_updates_only_version_file(self):
539498
repository_root = Path(__file__).resolve().parents[1]

tests/utils.py

Lines changed: 97 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,10 @@
1111
from selenium.webdriver.common.by import By
1212

1313

14-
class TestConfig:
15-
"""Configuration class for setting up test parameters and utilities."""
16-
17-
def shortDescription(self):
18-
"""Return a short description for the test."""
19-
return None
14+
class BaseTestUtils:
15+
"""Base class for setting up test parameters and utilities."""
2016

17+
docker_compose_timeout = 120
2118
docker_client = docker.from_env()
2219
ctx = ssl.create_default_context()
2320
ctx.check_hostname = False
@@ -28,13 +25,104 @@ def shortDescription(self):
2825
with open(config_file) as json_file:
2926
config = json.load(json_file)
3027

28+
def shortDescription(self):
29+
"""Keep verbose unittest output focused on test names, not docstrings."""
30+
return None
31+
32+
@classmethod
33+
def _execute_docker_compose_command(cls, cmd_args, use_text_mode=False):
34+
"""Execute a docker compose command and log output."""
35+
kwargs = {
36+
"stdout": subprocess.PIPE,
37+
"stderr": subprocess.PIPE,
38+
"cwd": cls.root_location,
39+
}
40+
if use_text_mode:
41+
kwargs["text"] = True
42+
try:
43+
cmd = subprocess.run(
44+
cmd_args,
45+
check=False,
46+
timeout=cls.docker_compose_timeout,
47+
**kwargs,
48+
)
49+
except subprocess.TimeoutExpired as e:
50+
output = e.stdout or ""
51+
error = e.stderr or ""
52+
if isinstance(output, bytes):
53+
output = output.decode("utf-8", errors="replace") if output else ""
54+
if isinstance(error, bytes):
55+
error = error.decode("utf-8", errors="replace") if error else ""
56+
with open(cls.config["logs_file"], "a") as logs_file:
57+
logs_file.write(output)
58+
logs_file.write(error)
59+
raise RuntimeError(
60+
"docker compose command timed out "
61+
f"after {cls.docker_compose_timeout}s: {' '.join(cmd_args)}"
62+
)
63+
if use_text_mode:
64+
output, error = cmd.stdout, cmd.stderr
65+
else:
66+
output = cmd.stdout.decode("utf-8", errors="replace") if cmd.stdout else ""
67+
error = cmd.stderr.decode("utf-8", errors="replace") if cmd.stderr else ""
68+
with open(cls.config["logs_file"], "a") as logs_file:
69+
logs_file.write(output)
70+
logs_file.write(error)
71+
if cmd.returncode != 0:
72+
raise RuntimeError(
73+
f"docker compose command failed "
74+
f"({cmd.returncode}): {' '.join(cmd_args)}"
75+
)
76+
return output, error
77+
78+
def docker_compose_get_container_id(self, container_name):
79+
"""Get the Docker container ID for a specific container.
80+
81+
Parameters:
82+
83+
- container_name (str): The name of the Docker container.
3184
32-
class TestUtilities(SeleniumTestMixin, TestConfig):
33-
"""Utility functions for testing."""
85+
Returns:
86+
str: The ID of the Docker container.
87+
"""
88+
output, _ = self._execute_docker_compose_command(
89+
["docker", "compose", "ps", "--quiet", container_name]
90+
)
91+
return output.rstrip()
92+
93+
94+
class FunctionalTestUtils(SeleniumTestMixin, BaseTestUtils):
95+
"""Utilities for functional testing."""
3496

3597
objects_to_delete = []
98+
test_usernames_to_delete = set()
3699
browser = "chrome"
37100

101+
@classmethod
102+
def delete_test_users(cls, *usernames):
103+
"""Delete test-created users without relying on browser state."""
104+
usernames = sorted(set(usernames))
105+
if not usernames:
106+
return
107+
cls._execute_docker_compose_command(
108+
[
109+
"docker",
110+
"compose",
111+
"exec",
112+
"-T",
113+
"dashboard",
114+
"python",
115+
"manage.py",
116+
"shell",
117+
"-c",
118+
(
119+
"from openwisp_users.models import User; "
120+
f"User.objects.filter(username__in={usernames!r}).delete()"
121+
),
122+
],
123+
use_text_mode=True,
124+
)
125+
38126
def setUp(self):
39127
# Override TestSeleniumMixin setUp which uses
40128
# Django methods to create superuser
@@ -111,7 +199,7 @@ def create_superuser(
111199
self.find_element(By.NAME, "password2", driver=driver).send_keys(password)
112200
self.find_element(By.NAME, "is_superuser", driver=driver).click()
113201
self._click_save_btn(driver)
114-
self.objects_to_delete.append(driver.current_url)
202+
self.test_usernames_to_delete.add(username)
115203
self._click_save_btn(driver)
116204
self._wait_until_page_ready()
117205
self.wait_for_visibility(By.ID, "content", driver=driver, timeout=10)
@@ -254,25 +342,6 @@ def add_mobile_location_point(self, location_name, driver=None):
254342
self._click_save_btn(driver)
255343
self.get_resource(location_name, "/admin/geo/location/", driver=driver)
256344

257-
def docker_compose_get_container_id(self, container_name):
258-
"""Get the Docker container ID for a specific container.
259-
260-
Parameters:
261-
262-
- container_name (str): The name of the Docker container.
263-
264-
Returns:
265-
str: The ID of the Docker container.
266-
"""
267-
services_output = subprocess.Popen(
268-
["docker", "compose", "ps", "--quiet", container_name],
269-
stdout=subprocess.PIPE,
270-
stderr=subprocess.PIPE,
271-
cwd=self.root_location,
272-
)
273-
output, _ = services_output.communicate()
274-
return output.rstrip().decode("utf-8")
275-
276345
def create_network_topology(
277346
self,
278347
label="automated-selenium-test-01",

0 commit comments

Comments
 (0)