From a0c6c39b69a8b570176dad488ab51e8f441e24cb Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 18 Mar 2026 23:30:53 +0530 Subject: [PATCH 01/12] [change] Update theme customization paths and enhance collectstatic logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: Previously, when the OPENWISP_ADMIN_THEME_LINKS setting included any CSS or JavaScript file paths starting with /static/, the OpenWISP utilities logic would strip the /static/ prefix. As a result, Django couldn't recognize these custom static files, preventing them from being collected and served. Additionally, static files were previously mounted directly in the Nginx container, bypassing Django’s static collection workflow entirely. Fix: Mount custom theme assets inside the dashboard container and integrate them into Django’s staticfiles pipeline, ensuring they are processed by collectstatic and correctly served via Nginx. --- docker-compose.yml | 3 +- docs/user/customization.rst | 34 +++++++++++++++---- images/common/collectstatic.py | 52 +++++++++++++++++++++++++++--- images/common/openwisp/settings.py | 1 + 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 67e57f2b..62265c3e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: - openwisp_ssh:/home/openwisp/.ssh - influxdb_data:/var/lib/influxdb - ./customization/configuration/django/:/opt/openwisp/openwisp/configuration:ro + - ./customization/theme:/opt/openwisp/static_custom:ro depends_on: - postgres - redis @@ -128,7 +129,7 @@ services: - openwisp_media:/opt/openwisp/public/media:ro - openwisp_private_storage:/opt/openwisp/public/private:ro - openwisp_certs:/etc/letsencrypt - - ./customization/theme:/opt/openwisp/public/custom:ro + - ./customization/nginx:/opt/openwisp/public/custom:ro networks: default: aliases: diff --git a/docs/user/customization.rst b/docs/user/customization.rst index df0afa06..c9644201 100644 --- a/docs/user/customization.rst +++ b/docs/user/customization.rst @@ -24,6 +24,7 @@ adding customizations. Execute these commands in the same location as the touch customization/configuration/django/__init__.py touch customization/configuration/django/custom_django_settings.py mkdir -p customization/theme + mkdir -p customization/nginx You can also refer to the `directory structure of Docker OpenWISP repository @@ -84,16 +85,24 @@ follow the following guide. 2. Create your custom CSS / Javascript file in ``customization/theme`` directory created in the above section. E.g. - ``customization/theme/static/custom/css/custom-theme.css``. -3. Start the nginx containers. + ``customization/theme/custom/css/custom-theme.css``. +3. Recreate the dashboard container to apply the changes: + +.. code-block:: shell + + docker compose up -d --force-recreate dashboard .. note:: - 1. You can edit the styles / JavaScript files now without restarting - the container, as long as file is in the correct place, it will be - picked. - 2. You can create a ``maintenance.html`` file inside the ``customize`` - directory to have a custom maintenance page for scheduled downtime. + After adding new files to ``customization/theme``, you must recreate + the dashboard container using the command above. + + Alternatively, you can apply changes without recreating the container + by running: + + .. code-block:: shell + + docker compose exec dashboard bash -c "python collectstatic.py && uwsgi --reload uwsgi.pid" Supplying Custom uWSGI configuration ------------------------------------ @@ -175,6 +184,17 @@ Docker PATH/TO/YOUR/DEFAULT:/etc/raddb/sites-enabled/default ... +Enabling Maintenance Mode +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To enable maintenance mode, create a ``maintenance.html`` file in the +``customization/custom/maintenance.html`` created above. + +When this file is present, Nginx will automatically serve it instead of +the application for incoming requests. + +To disable maintenance mode, simply remove the file. + Supplying Custom Python Source Code ----------------------------------- diff --git a/images/common/collectstatic.py b/images/common/collectstatic.py index 373b773f..7f627f37 100644 --- a/images/common/collectstatic.py +++ b/images/common/collectstatic.py @@ -26,6 +26,31 @@ def get_pip_freeze_hash(): sys.exit(1) +def get_dir_shasum(directory_path): + """Return a sha256 hexdigest of all files (names + contents) under directory_path. + + If the directory does not exist, return the hash of empty contents. + """ + if not os.path.exists(directory_path): + return hashlib.sha256(b"").hexdigest() + hasher = hashlib.sha256() + for root, dirs, files in os.walk(directory_path): + dirs.sort() + files.sort() + for fname in files: + fpath = os.path.join(root, fname) + relpath = os.path.relpath(fpath, directory_path) + hasher.update(relpath.encode()) + try: + with open(fpath, "rb") as fh: + for chunk in iter(lambda: fh.read(4096), b""): + hasher.update(chunk) + except OSError: + # If a file can't be read, skip it but continue hashing others + continue + return hasher.hexdigest() + + def run_collectstatic(): try: subprocess.run( @@ -42,13 +67,32 @@ def main(): return redis_connection = redis.Redis.from_url(settings.CACHES["default"]["LOCATION"]) current_pip_hash = get_pip_freeze_hash() + current_static_hash = get_dir_shasum( + os.path.join(settings.BASE_DIR, "static_custom") + ) cached_pip_hash = redis_connection.get("pip_freeze_hash") - if not cached_pip_hash or cached_pip_hash.decode() != current_pip_hash: - print("Changes in Python dependencies detected, running collectstatic...") + cached_static_hash = redis_connection.get("static_custom_hash") + pip_changed = not cached_pip_hash or cached_pip_hash.decode() != current_pip_hash + static_changed = ( + not cached_static_hash or cached_static_hash.decode() != current_static_hash + ) + if pip_changed or static_changed: + print( + "Changes in Python dependencies or static_custom detected," + " running collectstatic..." + ) run_collectstatic() - redis_connection.set("pip_freeze_hash", current_pip_hash) + try: + redis_connection.set("pip_freeze_hash", current_pip_hash) + redis_connection.set("static_custom_hash", current_static_hash) + except Exception: + # If caching fails, don't crash the startup; collectstatic already ran + pass else: - print("No changes in Python dependencies, skipping collectstatic...") + print( + "No changes in Python dependencies or static_custom," + " skipping collectstatic..." + ) if __name__ == "__main__": diff --git a/images/common/openwisp/settings.py b/images/common/openwisp/settings.py index a6f1e512..048de604 100644 --- a/images/common/openwisp/settings.py +++ b/images/common/openwisp/settings.py @@ -284,6 +284,7 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.9/howto/static-files/ +STATICFILES_DIRS = [os.path.join(BASE_DIR, "static_custom")] STATIC_ROOT = os.path.join(BASE_DIR, "static") MEDIA_ROOT = os.path.join(BASE_DIR, "media") # PRIVATE_STORAGE_ROOT path should be similar to ansible-openwisp2 From 0ece44c496fb46b2e3c169069684cd7c4012252f Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 19 Mar 2026 23:01:26 +0530 Subject: [PATCH 02/12] [test] Added regression test --- Makefile | 2 +- docs/user/customization.rst | 6 ++- images/common/collectstatic.py | 13 ++--- tests/config.json | 3 +- tests/runtests.py | 94 +++++++++++++++++++++++++++------- 5 files changed, 91 insertions(+), 27 deletions(-) diff --git a/Makefile b/Makefile index 12f8da46..b5ef7f32 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # the heading "Makefile Options". # The .env file can override ?= variables in the Makefile (e.g. OPENWISP_VERSION, IMAGE_OWNER) -include .env +include .env # RELEASE_VERSION: version string used when tagging a new release. RELEASE_VERSION = 25.10.0 diff --git a/docs/user/customization.rst b/docs/user/customization.rst index c9644201..ee771793 100644 --- a/docs/user/customization.rst +++ b/docs/user/customization.rst @@ -188,7 +188,11 @@ Enabling Maintenance Mode ~~~~~~~~~~~~~~~~~~~~~~~~~ To enable maintenance mode, create a ``maintenance.html`` file in the -``customization/custom/maintenance.html`` created above. +``customization/nginx/`` directory: + +.. code-block:: shell + + customization/nginx/maintenance.html When this file is present, Nginx will automatically serve it instead of the application for incoming requests. diff --git a/images/common/collectstatic.py b/images/common/collectstatic.py index 7f627f37..e2cedcc4 100644 --- a/images/common/collectstatic.py +++ b/images/common/collectstatic.py @@ -51,11 +51,12 @@ def get_dir_shasum(directory_path): return hasher.hexdigest() -def run_collectstatic(): +def run_collectstatic(clear=False): try: - subprocess.run( - [sys.executable, "manage.py", "collectstatic", "--noinput"], check=True - ) + cmd = [sys.executable, "manage.py", "collectstatic", "--noinput"] + if clear: + cmd.append("--clear") + subprocess.run(cmd, check=True) except subprocess.CalledProcessError as e: print(f"Error running 'collectstatic': {e}", file=sys.stderr) sys.exit(1) @@ -63,7 +64,7 @@ def run_collectstatic(): def main(): if os.environ.get("COLLECTSTATIC_WHEN_DEPS_CHANGE", "true").lower() == "false": - run_collectstatic() + run_collectstatic(clear=True) return redis_connection = redis.Redis.from_url(settings.CACHES["default"]["LOCATION"]) current_pip_hash = get_pip_freeze_hash() @@ -81,7 +82,7 @@ def main(): "Changes in Python dependencies or static_custom detected," " running collectstatic..." ) - run_collectstatic() + run_collectstatic(clear=static_changed) try: redis_connection.set("pip_freeze_hash", current_pip_hash) redis_connection.set("static_custom_hash", current_static_hash) diff --git a/tests/config.json b/tests/config.json index a4b9d185..1ff8cb97 100644 --- a/tests/config.json +++ b/tests/config.json @@ -9,5 +9,6 @@ "username": "admin", "password": "admin", "services_max_retries": 25, - "services_delay_retries": 5 + "services_delay_retries": 5, + "custom_css_filename": "customization/theme/custom-2hjsdf734653.css" } diff --git a/tests/runtests.py b/tests/runtests.py index df9c964f..862c4e74 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -64,6 +64,60 @@ def failureException(self): TestServices.failed_test = True return super().failureException + @classmethod + def _execute_docker_compose_command(cls, cmd_args, use_text_mode=False): + """Execute a docker compose command and log output. + + Args: + cmd_args: List of command arguments for subprocess.Popen + use_text_mode: If True, use text mode for subprocess output + + Returns: + Tuple of (output, error) from command execution + """ + kwargs = { + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + "cwd": cls.root_location, + } + if use_text_mode: + kwargs["text"] = True + cmd = subprocess.Popen(cmd_args, **kwargs) + output, error = map(str, cmd.communicate()) + with open(cls.config["logs_file"], "a") as logs_file: + logs_file.write(output) + logs_file.write(error) + return output, error + + @classmethod + def _setup_admin_theme_links(cls): + """Update Django settings to add ADMIN_THEME_LINKS and reload uwsgi.""" + css_path = os.path.join( + cls.root_location, + cls.config["custom_css_filename"], + ) + with open(css_path, "w") as custom_css_file: + custom_css_file.write("body{--openwisp-test: 1;}") + script = rf""" + grep -q OPENWISP_ADMIN_THEME_LINKS /opt/openwisp/openwisp/settings.py || \ + 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 && + python collectstatic.py && + uwsgi --reload uwsgi.pid + """ # noqa: E501 + cls._execute_docker_compose_command( + [ + "docker", + "compose", + "exec", + "-T", + "dashboard", + "bash", + "-c", + script, + ], + use_text_mode=True, + ) + @classmethod def setUpClass(cls): cls.failed_test = False @@ -76,7 +130,7 @@ def setUpClass(cls): os.path.dirname(os.path.realpath(__file__)), "data.py" ) entrypoint = "python manage.py shell --command='import data; data.setup()'" - cmd = subprocess.Popen( + cls._execute_docker_compose_command( [ "docker", "compose", @@ -87,22 +141,12 @@ def setUpClass(cls): "--volume", f"{test_data_file}:/opt/openwisp/data.py", "dashboard", - ], - universal_newlines=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=cls.root_location, + ] ) - output, error = map(str, cmd.communicate()) - with open(cls.config["logs_file"], "w") as logs_file: - logs_file.write(output) - logs_file.write(error) - subprocess.run( + cls._execute_docker_compose_command( ["docker", "compose", "up", "--detach"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - cwd=cls.root_location, ) + cls._setup_admin_theme_links() # Create base drivers (Firefox) if cls.config["driver"] == "firefox": cls.base_driver = cls.get_firefox_webdriver() @@ -122,6 +166,13 @@ def tearDownClass(cls): print(f"Unable to delete resource at: {resource_link}") cls.second_driver.quit() cls.base_driver.quit() + # Remove the temporary custom CSS file created for testing + css_path = os.path.join( + cls.root_location, + cls.config["custom_css_filename"], + ) + if os.path.exists(css_path): + os.remove(css_path) if cls.failed_test and cls.config["logs"]: cmd = subprocess.Popen( ["docker", "compose", "logs"], @@ -185,6 +236,16 @@ def test_admin_login(self): ) self.fail(message) + def test_custom_static_files_loaded(self): + self.login() + self.open("/admin/") + # Check if the custom CSS variable is applied + value = self.web_driver.execute_script( + "return getComputedStyle(document.body)" + ".getPropertyValue('--openwisp-test');" + ) + self.assertEqual(value, "1") + def test_device_monitoring_charts(self): self.login() self.get_resource("test-device", "/admin/config/device/") @@ -490,7 +551,4 @@ def test_containers_down(self): if __name__ == "__main__": - suite = unittest.TestSuite() - suite.addTest(TestServices("test_topology_graph")) - runner = unittest.TextTestRunner(verbosity=2) - runner.run(suite) + unittest.main(verbosity=2) From d1563896936e05aeb6b6f17768ca12c7417a9e99 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 19 Mar 2026 23:25:38 +0530 Subject: [PATCH 03/12] [fix] Requested changes by @coderabbitai --- .github/workflows/ci.yml | 2 +- docs/user/customization.rst | 4 ++-- images/common/collectstatic.py | 8 ++++++-- tests/runtests.py | 18 +++++++++++++++--- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48f3c38f..3095b656 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ jobs: # /opt/openwisp/docker-openwisp. To ensure the test runs correctly # and environment variables remain intact, it is essential to # execute the test from this directory. - command: cd /opt/openwisp/docker-openwisp && make develop-pythontests && make stop + command: cd /opt/openwisp/docker-openwisp && sudo make develop-pythontests && make stop env: SELENIUM_HEADLESS: 1 diff --git a/docs/user/customization.rst b/docs/user/customization.rst index ee771793..a958189f 100644 --- a/docs/user/customization.rst +++ b/docs/user/customization.rst @@ -94,8 +94,8 @@ follow the following guide. .. note:: - After adding new files to ``customization/theme``, you must recreate - the dashboard container using the command above. + After adding, updating, or removing files in ``customization/theme``, + you must recreate the dashboard container using the command above. Alternatively, you can apply changes without recreating the container by running: diff --git a/images/common/collectstatic.py b/images/common/collectstatic.py index e2cedcc4..1741615a 100644 --- a/images/common/collectstatic.py +++ b/images/common/collectstatic.py @@ -40,11 +40,15 @@ def get_dir_shasum(directory_path): for fname in files: fpath = os.path.join(root, fname) relpath = os.path.relpath(fpath, directory_path) - hasher.update(relpath.encode()) try: + file_hasher = hashlib.sha256() with open(fpath, "rb") as fh: for chunk in iter(lambda: fh.read(4096), b""): - hasher.update(chunk) + file_hasher.update(chunk) + relpath_bytes = relpath.encode() + hasher.update(len(relpath_bytes).to_bytes(8, "big")) + hasher.update(relpath_bytes) + hasher.update(file_hasher.digest()) except OSError: # If a file can't be read, skip it but continue hashing others continue diff --git a/tests/runtests.py b/tests/runtests.py index 862c4e74..19191360 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -82,16 +82,28 @@ def _execute_docker_compose_command(cls, cmd_args, use_text_mode=False): } if use_text_mode: kwargs["text"] = True - cmd = subprocess.Popen(cmd_args, **kwargs) - output, error = map(str, cmd.communicate()) + cmd = subprocess.run(cmd_args, check=False, **kwargs) + output, error = map(str, (cmd.stdout, cmd.stderr)) with open(cls.config["logs_file"], "a") as logs_file: logs_file.write(output) logs_file.write(error) + if cmd.returncode != 0: + raise RuntimeError( + f"docker compose command failed " + f"({cmd.returncode}): {' '.join(cmd_args)}" + ) return output, error @classmethod def _setup_admin_theme_links(cls): - """Update Django settings to add ADMIN_THEME_LINKS and reload uwsgi.""" + """Configure admin theme links during tests. + + The default docker-compose setup does not allow injecting + OPENWISP_ADMIN_THEME_LINKS dynamically, so this method updates + Django settings inside the running container and reloads uWSGI. + This enables the Selenium tests to verify that a custom static CSS + file is served by the admin interface. + """ css_path = os.path.join( cls.root_location, cls.config["custom_css_filename"], From fef33a81e057cd17449deec477cfd0c774cdfdcb Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Thu, 19 Mar 2026 23:36:03 +0530 Subject: [PATCH 04/12] [temp] Debug with tmate --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3095b656..8a0857f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,9 @@ jobs: GIT_BRANCH="${GIT_BRANCH}" SKIP_PULL=true sudo -E ./deploy/auto-install.sh --upgrade \ || (cat /opt/openwisp/autoinstall.log && exit 1) + - name: Debugging with tmate + uses: mxschmitt/action-tmate@v3.23 + - name: Test if: ${{ !cancelled() && steps.auto_install_upgrade.conclusion == 'success' }} uses: openwisp/openwisp-utils/.github/actions/retry-command@master From 4d64c1eaea9e2bec4e422dea5d22b86e99066cbd Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Tue, 24 Mar 2026 22:58:27 +0530 Subject: [PATCH 05/12] [fix] Trigger githb action From 0a28ec951284e288ad37256432b98e91f3968f5f Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 25 Mar 2026 16:22:03 +0530 Subject: [PATCH 06/12] [fix] Fixed test action --- .github/workflows/ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a0857f5..b2e7a955 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,12 +64,14 @@ jobs: if: ${{ !cancelled() && steps.set_git_branch.conclusion == 'success' }} # Do not remove the blank lines from the input. run: | - printf "latest\n" | \ + printf "edge\n" | \ GIT_BRANCH="${GIT_BRANCH}" SKIP_PULL=true sudo -E ./deploy/auto-install.sh --upgrade \ || (cat /opt/openwisp/autoinstall.log && exit 1) - - name: Debugging with tmate - uses: mxschmitt/action-tmate@v3.23 + - name: Fix permissions for CI user + if: ${{ !cancelled() && steps.auto_install_upgrade.conclusion == 'success' }} + run: | + sudo chown -R $USER:$USER /opt/openwisp - name: Test if: ${{ !cancelled() && steps.auto_install_upgrade.conclusion == 'success' }} @@ -81,7 +83,7 @@ jobs: # /opt/openwisp/docker-openwisp. To ensure the test runs correctly # and environment variables remain intact, it is essential to # execute the test from this directory. - command: cd /opt/openwisp/docker-openwisp && sudo make develop-pythontests && make stop + command: cd /opt/openwisp/docker-openwisp && make develop-pythontests && make stop env: SELENIUM_HEADLESS: 1 From 6db8ce3d2091466566eb67f64fc40c53258a9cce Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 25 Mar 2026 19:40:45 +0530 Subject: [PATCH 07/12] [ci] Debug with tmate --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2e7a955..68bcd686 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,9 @@ jobs: run: | sudo chown -R $USER:$USER /opt/openwisp + - name: Debugging with tmate + uses: mxschmitt/action-tmate@v3.23 + - name: Test if: ${{ !cancelled() && steps.auto_install_upgrade.conclusion == 'success' }} uses: openwisp/openwisp-utils/.github/actions/retry-command@master From fecc5aa594f5c37a0b2108323de468c6ed580cf9 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 25 Mar 2026 21:01:03 +0530 Subject: [PATCH 08/12] [fix] Fixed tests --- tests/config.json | 2 +- tests/runtests.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/config.json b/tests/config.json index 1ff8cb97..a9fe43bd 100644 --- a/tests/config.json +++ b/tests/config.json @@ -10,5 +10,5 @@ "password": "admin", "services_max_retries": 25, "services_delay_retries": 5, - "custom_css_filename": "customization/theme/custom-2hjsdf734653.css" + "custom_css_filename": "custom-openwisp-test.css" } diff --git a/tests/runtests.py b/tests/runtests.py index 19191360..7747596b 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -59,6 +59,8 @@ def test_wait_for_services(self): class TestServices(TestUtilities, unittest.TestCase): + custom_static_token = None + @property def failureException(self): TestServices.failed_test = True @@ -106,10 +108,14 @@ def _setup_admin_theme_links(cls): """ css_path = os.path.join( cls.root_location, + "customization", + "theme", cls.config["custom_css_filename"], ) with open(css_path, "w") as custom_css_file: - custom_css_file.write("body{--openwisp-test: 1;}") + custom_css_file.write( + f"body{{--openwisp-test: {cls.custom_static_token};}}" + ) script = rf""" grep -q OPENWISP_ADMIN_THEME_LINKS /opt/openwisp/openwisp/settings.py || \ 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 && @@ -181,6 +187,8 @@ def tearDownClass(cls): # Remove the temporary custom CSS file created for testing css_path = os.path.join( cls.root_location, + "customization", + "theme", cls.config["custom_css_filename"], ) if os.path.exists(css_path): @@ -256,7 +264,7 @@ def test_custom_static_files_loaded(self): "return getComputedStyle(document.body)" ".getPropertyValue('--openwisp-test');" ) - self.assertEqual(value, "1") + self.assertEqual(value.strip(), self.custom_static_token) def test_device_monitoring_charts(self): self.login() From 2e2c5e2520262da3eb37e4067b5d3516e3868e66 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Wed, 25 Mar 2026 23:21:06 +0530 Subject: [PATCH 09/12] [ci] Removed tmate --- .github/workflows/ci.yml | 3 --- tests/runtests.py | 6 ++---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68bcd686..b2e7a955 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,9 +73,6 @@ jobs: run: | sudo chown -R $USER:$USER /opt/openwisp - - name: Debugging with tmate - uses: mxschmitt/action-tmate@v3.23 - - name: Test if: ${{ !cancelled() && steps.auto_install_upgrade.conclusion == 'success' }} uses: openwisp/openwisp-utils/.github/actions/retry-command@master diff --git a/tests/runtests.py b/tests/runtests.py index 7747596b..97486949 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -112,6 +112,7 @@ def _setup_admin_theme_links(cls): "theme", cls.config["custom_css_filename"], ) + cls.custom_static_token = str(time.time_ns()) with open(css_path, "w") as custom_css_file: custom_css_file.write( f"body{{--openwisp-test: {cls.custom_static_token};}}" @@ -443,9 +444,7 @@ def test_celery(self): "openwisp_firmware_upgrader.tasks.create_all_device_firmwares", "openwisp_firmware_upgrader.tasks.create_device_firmware", "openwisp_firmware_upgrader.tasks.upgrade_firmware", - "openwisp_monitoring.check.tasks.auto_create_config_check", - "openwisp_monitoring.check.tasks.auto_create_iperf3_check", - "openwisp_monitoring.check.tasks.auto_create_ping", + "openwisp_monitoring.check.tasks.auto_create_check", "openwisp_monitoring.check.tasks.perform_check", "openwisp_monitoring.check.tasks.run_checks", "openwisp_monitoring.device.tasks.delete_wifi_clients_and_sessions", @@ -466,7 +465,6 @@ def test_celery(self): "openwisp_notifications.tasks.ns_organization_user_deleted", "openwisp_notifications.tasks.ns_register_unregister_notification_type", "openwisp_notifications.tasks.update_org_user_notificationsetting", - "openwisp_notifications.tasks.update_superuser_notification_settings", "openwisp_radius.tasks.cleanup_stale_radacct", "openwisp_radius.tasks.convert_called_station_id", "openwisp_radius.tasks.deactivate_expired_users", From b59d7179039737f6c4070200959b21c0fb490e40 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 27 Mar 2026 15:20:03 +0530 Subject: [PATCH 10/12] [fix] Fixed tests --- .github/workflows/ci.yml | 2 +- tests/runtests.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2e7a955..83c336e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ jobs: uses: openwisp/openwisp-utils/.github/actions/retry-command@master with: delay_seconds: 30 - max_attempts: 5 + max_attempts: 1 # The auto-install script installs docker-openwisp by default in # /opt/openwisp/docker-openwisp. To ensure the test runs correctly # and environment variables remain intact, it is essential to diff --git a/tests/runtests.py b/tests/runtests.py index 97486949..7eb5bb5f 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -309,9 +309,11 @@ def test_create_prefix_users(self): prefix_pdf_file_path = self.base_driver.find_element( By.XPATH, '//a[text()="Download User Credentials"]' ).get_property("href") - reqHeader = { - "Cookie": f"sessionid={self.base_driver.get_cookies()[0]['value']}" - } + reqHeader = {} + for cookies in self.base_driver.get_cookies(): + if cookies["name"] == "sessionid": + reqHeader = {"Cookie": f"sessionid={cookies['value']}"} + break curlRequest = request.Request(prefix_pdf_file_path, headers=reqHeader) try: if request.urlopen(curlRequest, context=self.ctx).getcode() != 200: @@ -408,9 +410,10 @@ def test_add_superuser(self): def test_forgot_password(self): """Test forgot password to ensure that postfix is working properly.""" + self.logout() self.open("/accounts/password/reset/") self.find_element(By.NAME, "email").send_keys("admin@example.com") - self.find_element(By.XPATH, '//button[@type="submit"]').click() + self.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click() self._wait_until_page_ready() self.assertIn( "We have sent you an email. If you have not received " From efa70fd7fa5ec5af6f5d2b7df142f8c6c16882d5 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Fri, 27 Mar 2026 22:50:36 +0530 Subject: [PATCH 11/12] [ci] Added tmate for debugging --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83c336e0..27e2b159 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,9 @@ jobs: run: | sudo chown -R $USER:$USER /opt/openwisp + - name: Debug with tmate + uses: mxschmitt/action-tmate@v3 + - name: Test if: ${{ !cancelled() && steps.auto_install_upgrade.conclusion == 'success' }} uses: openwisp/openwisp-utils/.github/actions/retry-command@master From f3f1cb3abdc2a30547535b2c977c7f1afaf63f67 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Sat, 28 Mar 2026 01:36:49 +0530 Subject: [PATCH 12/12] [ci] Removed tmate --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27e2b159..83c336e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,9 +73,6 @@ jobs: run: | sudo chown -R $USER:$USER /opt/openwisp - - name: Debug with tmate - uses: mxschmitt/action-tmate@v3 - - name: Test if: ${{ !cancelled() && steps.auto_install_upgrade.conclusion == 'success' }} uses: openwisp/openwisp-utils/.github/actions/retry-command@master