import os import re from collections.abc import Sequence from typing import TYPE_CHECKING, Any from unittest import mock, skipUnless from urllib.parse import urlsplit import orjson from django.conf import settings from django.test import override_settings from django.utils.timezone import now as timezone_now from corporate.models.customers import Customer from corporate.models.plans import CustomerPlan from zerver.context_processors import get_apps_page_url from zerver.lib.integrations import CATEGORIES, INTEGRATIONS, META_CATEGORY from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import HostRequestMock from zerver.models import Realm from zerver.models.realms import get_realm from zerver.views.documentation import add_api_url_context def get_canonical_url(path: str) -> str: return f'' if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse class DocPageTest(ZulipTestCase): def get_doc(self, url: str, subdomain: str) -> "TestHttpResponse": if url[0:23] == "/integrations/doc-html/": return self.client_get(url, subdomain=subdomain, HTTP_X_REQUESTED_WITH="XMLHttpRequest") return self.client_get(url, subdomain=subdomain) def print_msg_if_error(self, url: str, response: "TestHttpResponse") -> None: # nocoverage if response.status_code == 200: return print("Error processing URL:", url) if response.get("Content-Type") == "application/json": content = orjson.loads(response.content) print() print("======================================================================") print("ERROR: {}".format(content.get("msg"))) print() def _is_landing_page(self, url: str) -> bool: for prefix in [ "/api/", "/devlogin/", "/devtools/", "/emails/", "/errors/", "/help", "/integrations/", ]: if url.startswith(prefix): return False return True def _check_basic_fetch( self, *, url: str, subdomain: str, expected_strings: Sequence[str], allow_robots: bool, ) -> "TestHttpResponse": # For whatever reason, we have some urls that don't follow # the same policies as the majority of our urls. if url.startswith("/integrations/doc-html"): allow_robots = True if url.startswith("/attribution/"): allow_robots = False # When a raw MDX file is being fetched, the meta tag to # disallow robots will be absent. if url in ["/help/status-and-availability?raw", "/help/?raw", "/help?raw"]: allow_robots = True result = self.get_doc(url, subdomain=subdomain) self.print_msg_if_error(url, result) self.assertEqual(result.status_code, 200) for s in expected_strings: self.assertIn(s, str(result.content)) if allow_robots: self.assert_not_in_success_response( [''], result ) else: self.assert_in_success_response( [''], result ) return result def _test(self, url: str, expected_strings: Sequence[str]) -> "TestHttpResponse": # Test the URL on the root subdomain response = self._check_basic_fetch( url=url, subdomain="", expected_strings=expected_strings, allow_robots=False, ) if not self._is_landing_page(url): return response with self.settings(ROOT_DOMAIN_LANDING_PAGE=True): # Test the URL on the root subdomain with the landing page setting result = self._check_basic_fetch( url=url, subdomain="", expected_strings=expected_strings, allow_robots=True, ) # Confirm page has the following HTML elements: # (I have no idea why we don't support this for /attribution/.) if not url.startswith("/attribution/"): self.assert_in_success_response( [ "", '<meta name="description" content="', '<meta property="og:title" content="', '<meta property="og:description" content="', ], result, ) return response def test_zephyr_disallows_robots(self) -> None: sample_urls = [ "/apps/", "/case-studies/end-point/", "/communities/", "/devlogin/", "/devtools/", "/emails/", "/errors/404/", "/errors/5xx/", "/integrations/", "/integrations/bots", "/integrations/doc-html/asana", "/integrations/doc/github", "/team/", ] for url in sample_urls: self._check_basic_fetch( url=url, subdomain="zephyr", expected_strings=[], allow_robots=False, ) if self._is_landing_page(url): with self.settings(ROOT_DOMAIN_LANDING_PAGE=True): self._check_basic_fetch( url=url, subdomain="zephyr", expected_strings=[], allow_robots=False, ) def test_api_doc_endpoints(self) -> None: # We extract the set of /api/ endpoints to check by parsing # the /api/ page sidebar for links starting with /api/. api_page_raw = str(self.client_get("/api/").content) ENDPOINT_REGEXP = re.compile(r"href=\"/api/\s*(.*?)\"") endpoint_list_set = set(re.findall(ENDPOINT_REGEXP, api_page_raw)) endpoint_list = sorted(endpoint_list_set) # We want to make sure our regex captured the actual main page. assert "" in endpoint_list content = { "/api/": "The Zulip API", "/api/api-keys": "be careful with it", "/api/create-user": "zuliprc-admin", "/api/delete-queue": "Delete a previously registered queue", "/api/get-events": "dont_block", "/api/get-own-user": "does not accept any parameters.", "/api/get-stream-id": "The name of the channel to access.", "/api/get-streams": "include_public", "/api/get-subscriptions": "Get all channels that the user is subscribed to.", "/api/get-users": "client_gravatar", "/api/installation-instructions": "No download required!", "/api/register-queue": "apply_markdown", "/api/render-message": "**foo**", "/api/send-message": "steal away your hearts", "/api/subscribe": "authorization_errors_fatal", "/api/unsubscribe": "not_removed", "/api/update-message": "propagate_mode", } """ We have 110 endpoints as of June 2023. If the way we represent links changes, or the way we put links into the main /api page changes, or if somebody simply introduces a bug into the test, there is a danger of losing coverage, although this is mitigated by other factors such as line coverage checks. For that reason, as well as developer convenience, we don't make the check here super precise. """ self.assertGreater(len(endpoint_list), 100) for endpoint in endpoint_list: url = f"/api/{endpoint}" if url in content: expected_strings = [content[url]] del content[url] else: # TODO: Just fill out dictionary for all ~110 endpoints # with some specific data from the page. expected_strings = ["This is an API doc"] # Mock OpenGraph call purely to speed up these tests. with mock.patch( "zerver.lib.html_to_text.html_to_text", return_value="This is an API doc" ) as m: # Test that canonical URL points to zulip.com expected_strings.append(get_canonical_url(url)) self._test( url=url, expected_strings=expected_strings, ) if url != "/api/": m.assert_called() # Make sure we exercised all content checks. self.assert_length(content, 0) def test_api_doc_404_status_codes(self) -> None: result = self.client_get( "/api/nonexistent-page", follow=True, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(result.status_code, 404) result = self.client_get( # This template shouldn't be accessed directly. "/api/api-doc-template", follow=True, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(result.status_code, 404) def test_dev_environment_endpoints(self) -> None: self._test("/devlogin/", ["Normal users"]) self._test("/devtools/", ["Useful development URLs"]) self._test("/emails/", ["Manually generate most emails"]) def test_dev_help_default_page_endpoints(self) -> None: # View on Zulip.com and View source URLs should be visible. self._test( "/help/status-and-availability", [ 'href="https://zulip.com/help/status-and-availability"', 'href="/help/status-and-availability?raw"', ], ) # Raw MDX file should be shown when `?raw` is present. self._test( "/help/status-and-availability?raw", ["---", "title: Status and availability", "### Set a status"], ) self._test( "/help/nonexistent-page-that-does-not-exist", ["This is not a valid help path and not a valid MDX file"], ) # `?raw` should have no effect when the page does not exist self._test( "/help/nonexistent-page-that-does-not-exist?raw", ["This is not a valid help path and not a valid MDX file"], ) # Root /help and /help/ is a special case without subpath. self._test("/help/?raw", ["---", "title: Zulip help center"]) self._test("/help?raw", ["---", "title: Zulip help center"]) with ( mock.patch("builtins.open", side_effect=OSError("File read error")), self.assertLogs("django.request", level="ERROR") as m, ): result = self.client_get("/help/status-and-availability?raw") self.assertEqual(result.status_code, 500) self.assertIn("Error reading MDX file", result.content.decode()) self.assertEqual( m.output, ["ERROR:django.request:Internal Server Error: /help/status-and-availability"], ) def test_error_endpoints(self) -> None: self._test("/errors/404/", ["Page not found"]) self._test("/errors/5xx/", ["Internal server error"]) def test_corporate_portico_endpoints(self) -> None: self._test("/team/", ["industry veterans"]) self._test("/apps/", ["Apps for every platform."]) self._test("/history/", ["Zulip released as open source!"]) # Test the i18n version of one of these pages. self._test("/en/history/", ["Zulip released as open source!"]) self._test("/values/", ["designed our company"]) self._test("/hello/", ["remote and flexible work"]) self._test("/communities/", ["Open communities directory"]) self._test("/development-community/", ["Zulip development community"]) self._test("/features/", ["Organized team chat solution"]) self._test("/jobs/", ["Work with us"]) self._test("/self-hosting/", ["Self-host Zulip"]) self._test("/security/", ["TLS encryption"]) self._test("/use-cases/", ["Use cases and customer stories"]) self._test("/why-zulip/", ["Why Zulip?"]) # /for/... pages self._test("/for/open-source/", ["for open source projects"]) self._test("/for/events/", ["for conferences and events"]) self._test("/for/education/", ["education pricing"]) self._test("/for/research/", ["for research"]) self._test("/for/business/", ["Communication efficiency represents"]) self._test("/for/communities/", ["Zulip for communities"]) # case-studies self._test("/case-studies/tum/", ["Technical University of Munich"]) self._test("/case-studies/ucsd/", ["UCSD"]) self._test("/case-studies/rust/", ["Rust programming language"]) self._test("/case-studies/recurse-center/", ["Recurse Center"]) self._test("/case-studies/lean/", ["Lean theorem prover"]) self._test("/case-studies/idrift/", ["Case study: iDrift AS"]) self._test("/case-studies/end-point/", ["Case study: End Point"]) self._test("/case-studies/atolio/", ["Case study: Atolio"]) self._test("/case-studies/asciidoctor/", ["Case study: Asciidoctor"]) self._test("/case-studies/rush-stack/", ["Case study: Rush Stack"]) def test_oddball_attributions_page(self) -> None: # Look elsewhere in the code--this page never allows robots nor does # it provide og data. self._test("/attribution/", ["Website attributions"]) def test_open_organizations_endpoint(self) -> None: zulip_dev_info = ["Zulip Dev", "great for testing!"] result = self.client_get("/communities/") self.assert_not_in_success_response(zulip_dev_info, result) realm = get_realm("zulip") realm.want_advertise_in_communities_directory = True realm.save() realm.description = "" realm.save() result = self.client_get("/communities/") # Not shown because the realm has default description set. self.assert_not_in_success_response(["Zulip Dev"], result) realm.description = "Some description" realm.save() self._test("/communities/", ["Open communities directory", "Zulip Dev", "Some description"]) # No org with research type so research category not displayed. result = self.client_get("/communities/") self.assert_not_in_success_response(['data-category="research"'], result) realm.org_type = Realm.ORG_TYPES["research"]["id"] realm.save() self._test( "/communities/", ["Open communities directory", "Zulip Dev", 'data-category="research"'] ) def test_integration_doc_endpoints(self) -> None: images_in_docs = set() response = self._test( "/integrations/", expected_strings=[ "native integrations.", "And hundreds more through", "Zapier", "IFTTT", ], ) page = response.content.decode("utf-8") for image in re.findall(r"/static/images/integrations/(logos/.*)\"", page): images_in_docs.add(image) for integration in INTEGRATIONS: url = f"/integrations/doc-html/{integration}" response = self._test(url, expected_strings=[]) doc = response.content.decode("utf-8") for image in re.findall(r"/static/images/integrations/(.*)\"", doc): images_in_docs.add(image) result = self.client_get( "/integrations/doc-html/nonexistent_integration", follow=True, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(result.status_code, 404) directory = "static/images/integrations" images_in_dir = { image_path for root, _, files in os.walk(directory) for file in files if "bot_avatars" not in (image_path := os.path.relpath(os.path.join(root, file), directory)) } # The integration docs and the screenshot images are in different repos # for the PythonAPIIntegrations, so we cannot avoid going out of sync # when adding/deleting screenshots. # Use this set to temporarily add exclusions to this test. exception_images: set[str] = {"jira-plugin/001.png"} images_in_dir.update(exception_images) images_in_docs.update(exception_images) self.assertEqual( images_in_dir, images_in_docs, ( "\n\nThe following images are not used in documentation and can be removed:\n" + "\n".join(extra_images) if (extra_images := images_in_dir - images_in_docs) else "" ) + ( "\n\nThe following images are used in documentation but do not exist:\n" + "\n".join(missing_images) if (missing_images := images_in_docs - images_in_dir) else "" ), ) def test_integration_pages_open_graph_metadata(self) -> None: og_description = '<meta property="og:description" content="Zulip comes with over' # Test a particular integration page url = "/integrations/doc/github" title = '<meta property="og:title" content="GitHub | Zulip integrations" />' description = '<meta property="og:description" content="Zulip comes with over' self._test(url, [title, description, get_canonical_url(url)]) # Test category pages for category in CATEGORIES: url = f"/integrations/{category}" if category in META_CATEGORY: title = f"<title>{CATEGORIES[category]} | Zulip integrations" og_title = f'' else: title = f"{CATEGORIES[category]} tools | Zulip integrations" og_title = f'' self._test(url, [title, og_title, og_description, get_canonical_url(url)]) # Test integrations index page url = "/integrations/" og_title = '' self._test(url, [og_title, og_description, get_canonical_url(url)]) def test_integration_404s(self) -> None: # We don't need to test all the pages for 404 for integration in list(INTEGRATIONS.keys())[5]: with self.settings(ROOT_DOMAIN_LANDING_PAGE=True): url = f"/en/integrations/doc-html/{integration}" result = self.client_get(url, subdomain="", follow=True) self.assertEqual(result.status_code, 404) result = self.client_get(url, subdomain="zephyr", follow=True) self.assertEqual(result.status_code, 404) url = f"/en/integrations/doc-html/{integration}" result = self.client_get(url, subdomain="", follow=True) self.assertEqual(result.status_code, 404) result = self.client_get(url, subdomain="zephyr", follow=True) self.assertEqual(result.status_code, 404) result = self.client_get("/integrations/doc-html/nonexistent_integration", follow=True) self.assertEqual(result.status_code, 404) def test_electron_detection(self) -> None: result = self.client_get("/accounts/password/reset/") # TODO: Ideally, this Mozilla would be the specific browser. self.assertTrue('data-platform="Mozilla"' in result.content.decode()) result = self.client_get("/accounts/password/reset/", HTTP_USER_AGENT="ZulipElectron/1.0.0") self.assertTrue('data-platform="ZulipElectron"' in result.content.decode()) class CustomShorthandLinksTest(ZulipTestCase): # We do not test relative links here since relative links were only # used in the old help pages. We should probably remove the logic # for relative links after we have done the official cutover to the # starlight help center: https://github.com/zulip/zulip/issues/35654. def test_settings_links(self) -> None: result = self.client_get("/api/api-keys") self.assertEqual(result.status_code, 200) self.assertIn( 'Go to Account & privacy', str(result.content), ) # Check that the sidebar was rendered properly. self.assertIn("API documentation home", str(result.content)) with self.settings(ROOT_DOMAIN_LANDING_PAGE=True): result = self.client_get("/api/api-keys", subdomain="") self.assertEqual(result.status_code, 200) self.assertIn("Account & privacy", str(result.content)) self.assertNotIn("/#settings", str(result.content)) class IntegrationTest(ZulipTestCase): def test_check_if_every_integration_has_logo_that_exists(self) -> None: for integration in INTEGRATIONS.values(): assert integration.logo_url is not None path = urlsplit(integration.logo_url).path self.assertTrue(os.path.isfile(settings.DEPLOY_ROOT + path), integration.name) def test_api_url_view_subdomains_base(self) -> None: context: dict[str, Any] = {} add_api_url_context(context, HostRequestMock()) self.assertEqual(context["api_url_scheme_relative"], "testserver/api") self.assertEqual(context["api_url"], "http://testserver/api") self.assertTrue(context["html_settings_links"]) @override_settings(ROOT_DOMAIN_LANDING_PAGE=True) def test_api_url_view_subdomains_homepage_base(self) -> None: context: dict[str, Any] = {} add_api_url_context(context, HostRequestMock()) self.assertEqual(context["api_url_scheme_relative"], "your-org.testserver/api") self.assertEqual(context["api_url"], "http://your-org.testserver/api") self.assertFalse(context["html_settings_links"]) def test_api_url_view_subdomains_full(self) -> None: context: dict[str, Any] = {} request = HostRequestMock(host="mysubdomain.testserver") add_api_url_context(context, request) self.assertEqual(context["api_url_scheme_relative"], "mysubdomain.testserver/api") self.assertEqual(context["api_url"], "http://mysubdomain.testserver/api") self.assertTrue(context["html_settings_links"]) class AboutPageTest(ZulipTestCase): @skipUnless(settings.ZILENCER_ENABLED, "requires zilencer") def test_endpoint(self) -> None: with self.settings(CONTRIBUTOR_DATA_FILE_PATH="zerver/tests/fixtures/authors.json"): result = self.client_get("/team/") self.assert_in_success_response(["Our amazing community"], result) self.assert_in_success_response(["2017-11-20"], result) self.assert_in_success_response(["timabbott", "showell", "gnprice", "rishig"], result) with mock.patch("corporate.views.portico.open", side_effect=FileNotFoundError) as m: result = self.client_get("/team/") self.assertEqual(result.status_code, 200) self.assert_in_success_response(["Never ran"], result) m.assert_called_once() with self.settings(ZILENCER_ENABLED=False): result = self.client_get("/team/") self.assertEqual(result.status_code, 301) self.assertEqual(result["Location"], "https://zulip.com/team/") class SmtpConfigErrorTest(ZulipTestCase): def test_smtp_error(self) -> None: with self.assertLogs("django.request", level="ERROR") as m: result = self.client_get("/config-error/smtp") self.assertEqual(result.status_code, 500) self.assert_in_response("email configuration", result) self.assertEqual( m.output, ["ERROR:django.request:Internal Server Error: /config-error/smtp"], ) class PlansPageTest(ZulipTestCase): def test_plans_auth(self) -> None: root_domain = "" result = self.client_get("/plans/", subdomain=root_domain) self.assert_in_success_response(["Self-host Zulip"], result) self.assert_not_in_success_response(["/sponsorship/"], result) self.assert_in_success_response(["/accounts/go/?next=%2Fsponsorship%2F"], result) non_existent_domain = "moo" result = self.client_get("/plans/", subdomain=non_existent_domain) self.assertEqual(result.status_code, 404) self.assert_in_response("There is no Zulip organization at", result) realm = get_realm("zulip") realm.plan_type = Realm.PLAN_TYPE_STANDARD_FREE realm.save(update_fields=["plan_type"]) result = self.client_get("/plans/", subdomain="zulip") self.assertEqual(result.status_code, 302) self.assertEqual(result["Location"], "/accounts/login/?next=/plans/") guest_user = "polonius" self.login(guest_user) result = self.client_get("/plans/", subdomain="zulip", follow=True) self.assertEqual(result.status_code, 404) organization_member = "hamlet" self.login(organization_member) result = self.client_get("/plans/", subdomain="zulip") self.assertEqual(result.status_code, 302) self.assertEqual("/billing/", result["Location"]) # Test root domain, with login on different domain result = self.client_get("/plans/", subdomain="") # TODO: works in manual testing, but I suspect something is funny in # the test environment # self.assert_in_success_response(["Sign up now"], result) def test_CTA_text_by_plan_type(self) -> None: sign_up_now = "Create organization" upgrade_to_standard = "Upgrade to Standard" current_plan = "Current plan" sponsorship_pending = "Sponsorship requested" # Root domain result = self.client_get("/plans/", subdomain="") self.assert_in_success_response([sign_up_now, upgrade_to_standard], result) self.assert_not_in_success_response([current_plan, sponsorship_pending], result) realm = get_realm("zulip") realm.plan_type = Realm.PLAN_TYPE_SELF_HOSTED realm.save(update_fields=["plan_type"]) with self.settings(PRODUCTION=True): result = self.client_get("/plans/", subdomain="zulip") self.assertEqual(result.status_code, 302) self.assertEqual(result["Location"], "https://zulip.com/plans/") self.login("iago") # SELF_HOSTED should hide the local plans page, even if logged in result = self.client_get("/plans/", subdomain="zulip") self.assertEqual(result.status_code, 302) self.assertEqual(result["Location"], "https://zulip.com/plans/") realm.plan_type = Realm.PLAN_TYPE_LIMITED realm.save(update_fields=["plan_type"]) result = self.client_get("/plans/", subdomain="zulip") self.assert_in_success_response([current_plan, upgrade_to_standard], result) self.assert_not_in_success_response([sign_up_now, sponsorship_pending], result) with self.settings(CLOUD_FREE_TRIAL_DAYS=60): result = self.client_get("/plans/", subdomain="zulip") self.assert_in_success_response([current_plan, "Start 60-day free trial"], result) self.assert_not_in_success_response( [sign_up_now, sponsorship_pending, upgrade_to_standard], result ) # Sponsored realms always have Customer entry. customer = Customer.objects.create(realm=get_realm("zulip"), stripe_customer_id="cus_id") realm.plan_type = Realm.PLAN_TYPE_STANDARD_FREE realm.save(update_fields=["plan_type"]) result = self.client_get("/plans/", subdomain="zulip") self.assert_in_success_response([current_plan], result) self.assert_not_in_success_response( [sign_up_now, upgrade_to_standard, sponsorship_pending], result ) plan = CustomerPlan.objects.create( customer=customer, tier=CustomerPlan.TIER_CLOUD_STANDARD, status=CustomerPlan.ACTIVE, billing_cycle_anchor=timezone_now(), billing_schedule=CustomerPlan.BILLING_SCHEDULE_MONTHLY, ) realm.plan_type = Realm.PLAN_TYPE_STANDARD realm.save(update_fields=["plan_type"]) result = self.client_get("/plans/", subdomain="zulip") self.assert_in_success_response([current_plan], result) self.assert_not_in_success_response( [sign_up_now, upgrade_to_standard, sponsorship_pending], result ) plan.status = CustomerPlan.FREE_TRIAL plan.save(update_fields=["status"]) result = self.client_get("/plans/", subdomain="zulip") self.assert_in_success_response(["Current plan (free trial)"], result) self.assert_not_in_success_response( [sign_up_now, upgrade_to_standard, sponsorship_pending], result ) realm.plan_type = Realm.PLAN_TYPE_LIMITED realm.save() customer.sponsorship_pending = True customer.save() plan.delete() result = self.client_get("/plans/", subdomain="zulip") self.assert_in_success_response([current_plan], result) self.assert_in_success_response([current_plan, sponsorship_pending], result) self.assert_not_in_success_response([sign_up_now, upgrade_to_standard], result) class AppsPageTest(ZulipTestCase): def test_get_apps_page_url(self) -> None: with self.settings(CORPORATE_ENABLED=False): apps_page_url = get_apps_page_url() self.assertEqual(apps_page_url, "https://zulip.com/apps/") with self.settings(CORPORATE_ENABLED=True): apps_page_url = get_apps_page_url() self.assertEqual(apps_page_url, "/apps/") def test_apps_view(self) -> None: with self.settings(CORPORATE_ENABLED=False): # Note that because this cannot actually uninstall the # "corporate" app and trigger updates to URL resolution, # this does not test the "apps/" path installed in # zproject.urls, but rather the special-case for testing # in corporate.views.portico result = self.client_get("/apps") self.assertEqual(result.status_code, 301) self.assertTrue(result["Location"].endswith("/apps/")) result = self.client_get("/apps/") self.assertEqual(result.status_code, 301) self.assertTrue(result["Location"] == "https://zulip.com/apps/") result = self.client_get("/apps/linux") self.assertEqual(result.status_code, 301) self.assertTrue(result["Location"] == "https://zulip.com/apps/") with self.settings(CORPORATE_ENABLED=True): result = self.client_get("/apps") self.assertEqual(result.status_code, 301) self.assertTrue(result["Location"].endswith("/apps/")) result = self.client_get("/apps/") self.assertEqual(result.status_code, 200) html = result.content.decode() self.assertIn("Apps for every platform.", html) def test_app_download_link_view(self) -> None: return_value = "https://desktop-download.zulip.com/v5.4.3/Zulip-Web-Setup-5.4.3.exe" with mock.patch( "corporate.views.portico.get_latest_github_release_download_link_for_platform", return_value=return_value, ) as m: result = self.client_get("/apps/download/windows") m.assert_called_once_with("windows") self.assertEqual(result.status_code, 302) self.assertTrue(result["Location"] == return_value) result = self.client_get("/apps/download/plan9") self.assertEqual(result.status_code, 404) class PrivacyTermsTest(ZulipTestCase): def test_terms_and_policies_index(self) -> None: with self.settings(POLICIES_DIRECTORY="corporate/policies"): response = self.client_get("/policies/") self.assert_in_success_response(["Terms and policies"], response) # Test that canonical URL points to zulip.com self.assert_in_success_response([get_canonical_url("/policies/")], response) # We don't add a rel-canonical link to self-hosted server policies docs. with self.settings(POLICIES_DIRECTORY="corporate/policies", CORPORATE_ENABLED=False): response = self.client_get("/policies/") self.assert_not_in_success_response([get_canonical_url("/policies/")], response) def test_custom_terms_of_service_template(self) -> None: not_configured_message = "This server is an installation" with self.settings(POLICIES_DIRECTORY="zerver/policies_absent"): response = self.client_get("/policies/terms") self.assert_in_response(not_configured_message, response) with self.settings(POLICIES_DIRECTORY="zerver/policies_minimal"): response = self.client_get("/policies/terms") self.assert_in_success_response(["These are the custom terms and conditions."], response) # Test that canonical URL points to zulip.com self.assert_in_success_response([get_canonical_url("/policies/terms")], response) with self.settings(POLICIES_DIRECTORY="corporate/policies"): response = self.client_get("/policies/terms") self.assert_in_success_response(["Kandra Labs"], response) def test_custom_privacy_policy_template(self) -> None: not_configured_message = "This server is an installation" with self.settings(POLICIES_DIRECTORY="zerver/policies_absent"): response = self.client_get("/policies/privacy") self.assert_in_response(not_configured_message, response) with self.settings(POLICIES_DIRECTORY="zerver/policies_minimal"): response = self.client_get("/policies/privacy") self.assert_in_success_response(["This is the custom privacy policy."], response) with self.settings(POLICIES_DIRECTORY="corporate/policies"): response = self.client_get("/policies/privacy") self.assert_in_success_response(["Kandra Labs"], response) def test_custom_privacy_policy_template_with_absolute_url(self) -> None: """Verify that using our recommended production default of an absolute path like /etc/zulip/policies/ works.""" current_dir = os.path.dirname(os.path.abspath(__file__)) abs_path = os.path.abspath( os.path.join(current_dir, "..", "..", "templates/corporate/policies") ) with self.settings(POLICIES_DIRECTORY=abs_path): response = self.client_get("/policies/privacy") self.assert_in_success_response(["Kandra Labs"], response) with self.settings(POLICIES_DIRECTORY=abs_path): response = self.client_get("/policies/nonexistent") self.assert_in_response("No such page", response) def test_redirects_from_older_urls(self) -> None: with self.settings(POLICIES_DIRECTORY="corporate/policies"): result = self.client_get("/privacy/", follow=True) self.assert_in_success_response(["Kandra Labs"], result) with self.settings(POLICIES_DIRECTORY="corporate/policies"): result = self.client_get("/terms/", follow=True) self.assert_in_success_response(["Kandra Labs"], result) def test_no_nav(self) -> None: # Test that our ?nav=0 feature of /policies/privacy and /policies/terms, # designed to comply with the Apple App Store draconian # policies that ToS/Privacy pages linked from an iOS app have # no links to the rest of the site if there's pricing # information for anything elsewhere on the site. # We don't have this link at all on these pages; this first # line of the test would change if we were to adjust the # design. response = self.client_get("/policies/terms") self.assert_not_in_success_response(["Back to Zulip"], response) response = self.client_get("/policies/terms", {"nav": "no"}) self.assert_not_in_success_response(["Back to Zulip"], response) response = self.client_get("/policies/privacy", {"nav": "no"}) self.assert_not_in_success_response(["Back to Zulip"], response)