import os
import re
from typing import Any, Dict, Sequence
from unittest import mock, skipUnless
from urllib.parse import urlsplit
import orjson
from django.conf import settings
from django.http import HttpResponse
from django.test import override_settings
from django.utils.timezone import now as timezone_now
from corporate.models import Customer, CustomerPlan
from zerver.context_processors import get_apps_page_url
from zerver.lib.integrations import INTEGRATIONS
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import HostRequestMock
from zerver.models import Realm, get_realm
from zerver.views.documentation import add_api_uri_context
class DocPageTest(ZulipTestCase):
def get_doc(self, url: str, subdomain: str) -> HttpResponse:
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: HttpResponse) -> 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 _test(
self,
url: str,
expected_content: str,
extra_strings: Sequence[str] = [],
landing_missing_strings: Sequence[str] = [],
landing_page: bool = True,
doc_html_str: bool = False,
) -> None:
# Test the URL on the "zephyr" subdomain
result = self.get_doc(url, subdomain="zephyr")
self.print_msg_if_error(url, result)
self.assertEqual(result.status_code, 200)
self.assertIn(expected_content, str(result.content))
for s in extra_strings:
self.assertIn(s, str(result.content))
if not doc_html_str:
self.assert_in_success_response(
[''], result
)
# Test the URL on the root subdomain
result = self.get_doc(url, subdomain="")
self.print_msg_if_error(url, result)
self.assertEqual(result.status_code, 200)
self.assertIn(expected_content, str(result.content))
if not doc_html_str:
self.assert_in_success_response(
[''], result
)
for s in extra_strings:
self.assertIn(s, str(result.content))
if not landing_page:
return
with self.settings(ROOT_DOMAIN_LANDING_PAGE=True):
# Test the URL on the root subdomain with the landing page setting
result = self.get_doc(url, subdomain="")
self.print_msg_if_error(url, result)
self.assertEqual(result.status_code, 200)
self.assertIn(expected_content, str(result.content))
for s in extra_strings:
self.assertIn(s, str(result.content))
for s in landing_missing_strings:
self.assertNotIn(s, str(result.content))
if not doc_html_str:
# Every page has a meta-description
self.assert_in_success_response([''], result
)
# Test the URL on the "zephyr" subdomain with the landing page setting
result = self.get_doc(url, subdomain="zephyr")
self.print_msg_if_error(url, result)
self.assertEqual(result.status_code, 200)
self.assertIn(expected_content, str(result.content))
for s in extra_strings:
self.assertIn(s, str(result.content))
if not doc_html_str:
self.assert_in_success_response(
[''], result
)
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 = [f"/api/{endpoint}" for endpoint in endpoint_list_set]
# Validate that the parsing logic isn't broken, since if it
# broke, the below would become a noop.
self.assertGreater(len(endpoint_list), 70)
for endpoint in endpoint_list:
self._test(endpoint, "", doc_html_str=True)
result = self.client_get(
"/api/nonexistent-page",
follow=True,
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(result.status_code, 404)
def test_doc_endpoints(self) -> None:
self._test("/api/", "The Zulip API")
self._test("/api/api-keys", "be careful with it")
self._test("/api/installation-instructions", "No download required!")
self._test("/api/send-message", "steal away your hearts")
self._test("/api/render-message", "**foo**")
self._test("/api/get-streams", "include_public")
self._test("/api/get-stream-id", "The name of the stream to access.")
self._test("/api/get-subscriptions", "Get all streams that the user is subscribed to.")
self._test("/api/get-users", "client_gravatar")
self._test("/api/register-queue", "apply_markdown")
self._test("/api/get-events", "dont_block")
self._test("/api/delete-queue", "Delete a previously registered queue")
self._test("/api/update-message", "propagate_mode")
self._test("/api/get-own-user", "does not accept any parameters.")
self._test("/api/subscribe", "authorization_errors_fatal")
self._test("/api/create-user", "zuliprc-admin")
self._test("/api/unsubscribe", "not_removed")
if settings.ZILENCER_ENABLED:
self._test("/team/", "industry veterans")
self._test("/history/", "Cambridge, Massachusetts")
# Test the i18n version of one of these pages.
self._test("/en/history/", "Cambridge, Massachusetts")
if settings.ZILENCER_ENABLED:
self._test("/apps/", "Apps for every platform.")
self._test("/features/", "Beautiful messaging")
self._test("/use-cases/", "Use cases and customer stories")
self._test("/hello/", "Chat for distributed teams", landing_missing_strings=["Log in"])
self._test("/development-community/", "Zulip development community")
self._test("/why-zulip/", "Why Zulip?")
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("/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/lean/", "Lean theorem prover")
self._test("/case-studies/idrift/", "Case study: iDrift AS")
self._test("/for/research/", "for research")
self._test("/for/business/", "Communication efficiency represents")
self._test("/for/communities/", "Zulip for communities")
self._test("/self-hosting/", "Self-host Zulip")
self._test("/security/", "TLS encryption")
self._test("/attribution/", "Attributions")
self._test("/devlogin/", "Normal users", landing_page=False)
self._test("/devtools/", "Useful development URLs")
self._test("/errors/404/", "Page not found")
self._test("/errors/5xx/", "Internal server error")
self._test("/emails/", "manually generate most of the emails by clicking")
result = self.client_get(
"/integrations/doc-html/nonexistent_integration",
follow=True,
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(result.status_code, 404)
result = self.client_get("/new-user/")
self.assertEqual(result.status_code, 301)
self.assertIn("hello", result["Location"])
result = self.client_get("/developer-community/")
self.assertEqual(result.status_code, 301)
self.assertIn("development-community", result["Location"])
result = self.client_get("/for/companies/", follow=True)
self.assert_in_success_response(["Communication efficiency represents"], result)
def test_portico_pages_open_graph_metadata(self) -> None:
# Why Zulip
url = "/why-zulip/"
title = ''
description = ' None:
self._test(
"/integrations/",
"native integrations.",
extra_strings=[
"And hundreds more through",
"Zapier",
"IFTTT",
],
)
for integration in INTEGRATIONS.keys():
url = f"/integrations/doc-html/{integration}"
self._test(url, "", doc_html_str=True)
def test_integration_pages_open_graph_metadata(self) -> None:
url = "/integrations/doc/github"
title = ''
description = ' 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 HelpTest(ZulipTestCase):
def test_help_settings_links(self) -> None:
result = self.client_get("/help/change-the-time-format")
self.assertEqual(result.status_code, 200)
self.assertIn(
'Go to Display settings', str(result.content)
)
# Check that the sidebar was rendered properly.
self.assertIn("Getting started with Zulip", str(result.content))
with self.settings(ROOT_DOMAIN_LANDING_PAGE=True):
result = self.client_get("/help/change-the-time-format", subdomain="")
self.assertEqual(result.status_code, 200)
self.assertIn("Display settings", str(result.content))
self.assertNotIn("/#settings", str(result.content))
def test_help_relative_links_for_gear(self) -> None:
result = self.client_get("/help/analytics")
self.assertIn('Usage statistics', str(result.content))
self.assertEqual(result.status_code, 200)
with self.settings(ROOT_DOMAIN_LANDING_PAGE=True):
result = self.client_get("/help/analytics", subdomain="")
self.assertEqual(result.status_code, 200)
self.assertIn("Usage statistics", str(result.content))
self.assertNotIn("/stats", str(result.content))
def test_help_relative_links_for_stream(self) -> None:
result = self.client_get("/help/message-a-stream-by-email")
self.assertIn('Your streams', str(result.content))
self.assertEqual(result.status_code, 200)
with self.settings(ROOT_DOMAIN_LANDING_PAGE=True):
result = self.client_get("/help/message-a-stream-by-email", subdomain="")
self.assertEqual(result.status_code, 200)
self.assertIn("Manage streams", str(result.content))
self.assertNotIn("/#streams", str(result.content))
class IntegrationTest(ZulipTestCase):
def test_check_if_every_integration_has_logo_that_exists(self) -> None:
for integration in INTEGRATIONS.values():
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_uri_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_uri_context(context, HostRequestMock())
self.assertEqual(context["api_url_scheme_relative"], "yourZulipDomain.testserver/api")
self.assertEqual(context["api_url"], "http://yourZulipDomain.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_uri_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"])
def test_html_settings_links(self) -> None:
context: Dict[str, Any] = {}
with self.settings(ROOT_DOMAIN_LANDING_PAGE=True):
add_api_uri_context(context, HostRequestMock())
self.assertEqual(context["settings_html"], "Zulip settings page")
self.assertEqual(context["subscriptions_html"], "streams page")
context = {}
with self.settings(ROOT_DOMAIN_LANDING_PAGE=True):
add_api_uri_context(context, HostRequestMock(host="mysubdomain.testserver"))
self.assertEqual(context["settings_html"], 'Zulip settings page')
self.assertEqual(
context["subscriptions_html"], 'streams page'
)
context = {}
add_api_uri_context(context, HostRequestMock())
self.assertEqual(context["settings_html"], 'Zulip settings page')
self.assertEqual(
context["subscriptions_html"], 'streams page'
)
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("zerver.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:
result = self.client_get("/config-error/smtp")
self.assertEqual(result.status_code, 200)
self.assert_in_success_response(["email configuration"], result)
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(["/upgrade#sponsorship"], result)
self.assert_in_success_response(["/accounts/go/?next=/upgrade%23sponsorship"], result)
non_existent_domain = "moo"
result = self.client_get("/plans/", subdomain=non_existent_domain)
self.assertEqual(result.status_code, 404)
self.assert_in_response("does not exist", 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.assert_in_success_response(["Current plan"], result)
self.assert_in_success_response(["/upgrade#sponsorship"], result)
self.assert_not_in_success_response(["/accounts/go/?next=/upgrade%23sponsorship"], result)
# 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 pending"
# 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")
# But in the development environment, it renders a page
result = self.client_get("/plans/", subdomain="zulip")
self.assert_in_success_response([sign_up_now, upgrade_to_standard], result)
self.assert_not_in_success_response([current_plan, sponsorship_pending], result)
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(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
)
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
)
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
)
customer = Customer.objects.create(realm=get_realm("zulip"), stripe_customer_id="cus_id")
plan = CustomerPlan.objects.create(
customer=customer,
tier=CustomerPlan.STANDARD,
status=CustomerPlan.FREE_TRIAL,
billing_cycle_anchor=timezone_now(),
billing_schedule=CustomerPlan.MONTHLY,
)
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(ZILENCER_ENABLED=False):
apps_page_url = get_apps_page_url()
self.assertEqual(apps_page_url, "https://zulip.com/apps/")
with self.settings(ZILENCER_ENABLED=True):
apps_page_url = get_apps_page_url()
self.assertEqual(apps_page_url, "/apps/")
def test_apps_view(self) -> None:
result = self.client_get("/apps")
self.assertEqual(result.status_code, 301)
self.assertTrue(result["Location"].endswith("/apps/"))
with self.settings(ZILENCER_ENABLED=False):
result = self.client_get("/apps/")
self.assertEqual(result.status_code, 301)
self.assertTrue(result["Location"] == "https://zulip.com/apps/")
with self.settings(ZILENCER_ENABLED=False):
result = self.client_get("/apps/linux")
self.assertEqual(result.status_code, 301)
self.assertTrue(result["Location"] == "https://zulip.com/apps/")
with self.settings(ZILENCER_ENABLED=True):
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(
"zerver.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)
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="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="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 /privacy and /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)