Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
273 lines
11 KiB
Python
273 lines
11 KiB
Python
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)
|