import os import re from contextlib import ExitStack from typing import Any from unittest import mock from django.conf import settings from django.core.management import call_command from django.core.management.base import SystemCheckError from django.test import override_settings from typing_extensions import override from zerver.lib.test_classes import ZulipTestCase class TestChecks(ZulipTestCase): @override def setUp(self) -> None: super().setUp() # The base tearDown removes LOCAL_UPLOADS_DIR after each test. # Since call_command("check") runs check_uploads_settings which # verifies the directory exists, we must re-create it. assert settings.LOCAL_UPLOADS_DIR is not None os.makedirs(settings.LOCAL_UPLOADS_DIR, exist_ok=True) def assert_check_with_error(self, test: re.Pattern[str] | str | None, **kwargs: Any) -> None: with open(os.devnull, "w") as DEVNULL, override_settings(**kwargs), ExitStack() as stack: if isinstance(test, str): stack.enter_context(self.assertRaisesMessage(SystemCheckError, test)) elif isinstance(test, re.Pattern): stack.enter_context(self.assertRaisesRegex(SystemCheckError, test)) call_command("check", stdout=DEVNULL) @override_settings(RUNNING_IN_DOCKER=False) def test_checks_required_setting(self) -> None: self.assert_check_with_error( "(zulip.E001) ZULIP_ADMINISTRATOR is still set to the example value " "'zulip-admin@example.com'; change it in /etc/zulip/settings.py", ZULIP_ADMINISTRATOR="zulip-admin@example.com", ) self.assert_check_with_error( "(zulip.E001) You must set ZULIP_ADMINISTRATOR in /etc/zulip/settings.py", ZULIP_ADMINISTRATOR="", ) self.assert_check_with_error( "(zulip.E001) You must set ZULIP_ADMINISTRATOR in /etc/zulip/settings.py", ZULIP_ADMINISTRATOR=None, ) @override_settings(RUNNING_IN_DOCKER=True) def test_checks_required_setting_docker(self) -> None: self.assert_check_with_error( "(zulip.E001) SETTING_ZULIP_ADMINISTRATOR is still set to the example value " "'zulip-admin@example.com'; change it in your Docker environment configuration", ZULIP_ADMINISTRATOR="zulip-admin@example.com", ) self.assert_check_with_error( "(zulip.E001) You must set SETTING_ZULIP_ADMINISTRATOR in your Docker environment configuration", ZULIP_ADMINISTRATOR="", ) self.assert_check_with_error( "(zulip.E001) You must set SETTING_ZULIP_ADMINISTRATOR in your Docker environment configuration", ZULIP_ADMINISTRATOR=None, ) @override_settings(RUNNING_IN_DOCKER=True, RUNNING_IN_HELM=True) def test_checks_required_setting_helm(self) -> None: self.assert_check_with_error( "(zulip.E001) zulip.environment.SETTING_ZULIP_ADMINISTRATOR is still set to " "the example value 'zulip-admin@example.com'; change it in your Helm values", ZULIP_ADMINISTRATOR="zulip-admin@example.com", ) self.assert_check_with_error( "(zulip.E001) You must set zulip.environment.SETTING_ZULIP_ADMINISTRATOR in your Helm values", ZULIP_ADMINISTRATOR="", ) self.assert_check_with_error( "(zulip.E001) You must set zulip.environment.SETTING_ZULIP_ADMINISTRATOR in your Helm values", ZULIP_ADMINISTRATOR=None, ) @override_settings(RUNNING_IN_DOCKER=True) def test_checks_required_setting_docker_manual_configuration(self) -> None: # With MANUAL_CONFIGURATION, the admin manages # /etc/zulip/settings.py themselves, so the bare-metal # message is the accurate one. with mock.patch.dict(os.environ, {"MANUAL_CONFIGURATION": "True"}): self.assert_check_with_error( "(zulip.E001) ZULIP_ADMINISTRATOR is still set to the example value " "'zulip-admin@example.com'; change it in /etc/zulip/settings.py", ZULIP_ADMINISTRATOR="zulip-admin@example.com", ) @override_settings(DEVELOPMENT=False, PRODUCTION=True, EXTERNAL_URI_SCHEME="https://") def test_checks_external_host_domain(self) -> None: message_re = r"\(zulip\.E002\) EXTERNAL_HOST \(\S+\) does not contain a domain part" try: # We default to skippping this check in CI, because # "testserver" is part of so many tests. We temporarily # strip out the environment variable we use to detect # that, so we can trigger the check. del os.environ["ZULIP_TEST_SUITE"] self.assert_check_with_error(None, EXTERNAL_HOST="server-1.local") self.assert_check_with_error( re.compile(rf"{message_re}\s*HINT: Add .local to the end"), EXTERNAL_HOST="server-1" ) self.assert_check_with_error( re.compile(rf"{message_re}\s*HINT: Add .localdomain to the end"), EXTERNAL_HOST="localhost", ) finally: os.environ["ZULIP_TEST_SUITE"] = "true" def test_checks_external_host_value(self) -> None: self.assert_check_with_error(None, EXTERNAL_HOST="testserver.local") self.assert_check_with_error(None, EXTERNAL_HOST="testserver.local:443") self.assert_check_with_error(None, EXTERNAL_HOST="testserver.local:https") self.assert_check_with_error( re.compile(r"EXTERNAL_HOST \(\S+\) is too long"), EXTERNAL_HOST=("a234567890." * 25 + "local"), ) self.assert_check_with_error( re.compile( r"\(zulip\.E002\) EXTERNAL_HOST \(\S+\) contains non-ASCII characters\n.*xn--wgv71a119e\.example\.com" ), EXTERNAL_HOST="日本語.example.com", ) self.assert_check_with_error( "EXTERNAL_HOST (-bogus-.example.com) does not validate as a hostname", EXTERNAL_HOST="-bogus-.example.com:443", ) def test_checks_external_scheme(self) -> None: self.assert_check_with_error( None, EXTERNAL_URI_SCHEME="https://", DEVELOPMENT=False, PRODUCTION=True ) self.assert_check_with_error( None, EXTERNAL_URI_SCHEME="http://", DEVELOPMENT=True, PRODUCTION=False ) self.assert_check_with_error( "Zulip does not support a non-HTTPS external scheme in production", EXTERNAL_URI_SCHEME="http://", DEVELOPMENT=False, PRODUCTION=True, ) self.assert_check_with_error( "Zulip does not support a non-HTTPS external scheme in production", EXTERNAL_URI_SCHEME="https", DEVELOPMENT=False, PRODUCTION=True, ) def test_checks_auth(self) -> None: self.assert_check_with_error( ( 'SOCIAL_AUTH_SAML_ENABLED_IDPS["idp_name"]["extra_attrs"]: ' "(zulip.E003) zulip_groups can't be listed in extra_attrs" ), SOCIAL_AUTH_SAML_ENABLED_IDPS={ "idp_name": { "entity_id": "https://idp.testshib.org/idp/shibboleth", "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", "attr_user_permanent_id": "email", "attr_first_name": "first_name", "attr_last_name": "last_name", "attr_username": "email", "attr_email": "email", "extra_attrs": ["title", "mobilePhone", "zulip_role", "zulip_groups"], } }, ) self.assert_check_with_error( ( 'settings.SOCIAL_AUTH_SYNC_ATTRS_DICT["example_org"]["saml"]["custom__groups"]: ' "(zulip.E004) zulip_groups can't be listed as a SAML attribute" ), SOCIAL_AUTH_SYNC_ATTRS_DICT={ "example_org": { "saml": { "role": "zulip_role", "custom__groups": "zulip_groups", "custom__title": "title", "groups": ["group1", "group2", ("samlgroup3", "zulipgroup3"), "group4"], } } }, ) def test_checks_uploads_s3_missing_buckets(self) -> None: self.assert_check_with_error( "Neither settings.LOCAL_UPLOADS_DIR nor settings.S3_AUTH_UPLOADS_BUCKET is set", LOCAL_UPLOADS_DIR=None, S3_AUTH_UPLOADS_BUCKET="", S3_AVATAR_BUCKET="some-avatar-bucket", ) self.assert_check_with_error( "Neither settings.LOCAL_UPLOADS_DIR nor settings.S3_AVATAR_BUCKET is set", LOCAL_UPLOADS_DIR=None, S3_AUTH_UPLOADS_BUCKET="some-uploads-bucket", S3_AVATAR_BUCKET="", ) self.assert_check_with_error( "Neither settings.LOCAL_UPLOADS_DIR nor settings.S3_AUTH_UPLOADS_BUCKET is set", LOCAL_UPLOADS_DIR=None, S3_AUTH_UPLOADS_BUCKET="", S3_AVATAR_BUCKET="", ) def test_checks_uploads_s3_configured(self) -> None: self.assert_check_with_error( None, LOCAL_UPLOADS_DIR=None, S3_AUTH_UPLOADS_BUCKET="some-uploads-bucket", S3_AVATAR_BUCKET="some-avatar-bucket", ) def test_checks_uploads_local_dir_missing(self) -> None: self.assert_check_with_error( "(zulip.E006) settings.LOCAL_UPLOADS_DIR (/nonexistent/path) does not exist", LOCAL_UPLOADS_DIR="/nonexistent/path", ) def test_checks_uploads_local_dir_not_writable(self) -> None: with mock.patch("os.access", return_value=False): self.assert_check_with_error( f"(zulip.E006) settings.LOCAL_UPLOADS_DIR ({settings.LOCAL_UPLOADS_DIR}) is not writable", ) def test_checks_uploads_local_dir_valid(self) -> None: self.assert_check_with_error(None) @override_settings(RUNNING_IN_DOCKER=False) def test_check_jitsi_server_url(self) -> None: self.assert_check_with_error( "(zulip.E007) JITSI_SERVER_URL in /etc/zulip/settings.py is not a URL", JITSI_SERVER_URL="example.jitsi.com", ) @override_settings(RUNNING_IN_DOCKER=True) def test_check_jitsi_server_url_in_docker(self) -> None: self.assert_check_with_error( "(zulip.E007) SETTING_JITSI_SERVER_URL in your Docker environment configuration is not a URL", JITSI_SERVER_URL="jitsi[etc]", ) @override_settings(RUNNING_IN_DOCKER=True, RUNNING_IN_HELM=True) def test_check_jitsi_server_url_in_helm(self) -> None: self.assert_check_with_error( "(zulip.E007) zulip.environment.SETTING_JITSI_SERVER_URL in your Helm values is not a URL", JITSI_SERVER_URL="example.com/jitsi", ) @override_settings(JITSI_SERVER_URL="https://example.com/jitsi") def test_check_jitsi_server_url_valid(self) -> None: self.assert_check_with_error(None)