2020-06-11 00:54:34 +02:00
|
|
|
import base64
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import shutil
|
2020-10-09 03:32:00 +02:00
|
|
|
import subprocess
|
2020-06-11 00:54:34 +02:00
|
|
|
import tempfile
|
|
|
|
import urllib
|
2016-11-10 19:30:09 +01:00
|
|
|
from contextlib import contextmanager
|
2021-04-06 18:07:33 +02:00
|
|
|
from datetime import timedelta
|
|
|
|
from typing import (
|
|
|
|
Any,
|
|
|
|
Callable,
|
2021-05-17 05:39:37 +02:00
|
|
|
Collection,
|
2021-04-06 18:07:33 +02:00
|
|
|
Dict,
|
|
|
|
Iterable,
|
|
|
|
Iterator,
|
|
|
|
List,
|
2021-05-08 08:25:06 +02:00
|
|
|
Mapping,
|
2021-04-06 18:07:33 +02:00
|
|
|
Optional,
|
|
|
|
Sequence,
|
|
|
|
Set,
|
|
|
|
Tuple,
|
|
|
|
Union,
|
|
|
|
)
|
2020-08-19 12:40:10 +02:00
|
|
|
from unittest import TestResult, mock
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2020-10-02 00:14:25 +02:00
|
|
|
import lxml.html
|
2020-08-07 01:09:47 +02:00
|
|
|
import orjson
|
2018-04-09 18:19:55 +02:00
|
|
|
from django.apps import apps
|
2016-11-10 19:30:09 +01:00
|
|
|
from django.conf import settings
|
2021-01-26 04:20:36 +01:00
|
|
|
from django.core.mail import EmailMessage
|
2018-04-09 18:19:55 +02:00
|
|
|
from django.db import connection
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.db.migrations.executor import MigrationExecutor
|
|
|
|
from django.db.migrations.state import StateApps
|
2016-11-10 19:30:09 +01:00
|
|
|
from django.db.utils import IntegrityError
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.http import HttpRequest, HttpResponse
|
|
|
|
from django.test import TestCase
|
|
|
|
from django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart
|
|
|
|
from django.test.testcases import SerializeMixin
|
|
|
|
from django.urls import resolve
|
2019-05-06 00:59:02 +02:00
|
|
|
from django.utils import translation
|
2021-06-26 10:07:54 +02:00
|
|
|
from django.utils.module_loading import import_string
|
2021-04-06 18:07:33 +02:00
|
|
|
from django.utils.timezone import now as timezone_now
|
2020-06-11 00:54:34 +02:00
|
|
|
from fakeldap import MockLDAP
|
2017-07-13 13:42:57 +02:00
|
|
|
from two_factor.models import PhoneDevice
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2021-06-03 12:20:31 +02:00
|
|
|
from corporate.models import Customer, CustomerPlan, LicenseLedger
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.decorator import do_two_factor_login
|
2016-11-10 19:30:09 +01:00
|
|
|
from zerver.lib.actions import (
|
2020-06-11 00:54:34 +02:00
|
|
|
bulk_add_subscriptions,
|
2019-02-02 23:53:55 +01:00
|
|
|
bulk_remove_subscriptions,
|
2020-06-11 00:54:34 +02:00
|
|
|
check_send_message,
|
|
|
|
check_send_stream_message,
|
2021-04-06 18:07:33 +02:00
|
|
|
do_set_realm_property,
|
2020-06-11 00:54:34 +02:00
|
|
|
gather_subscriptions,
|
2016-11-10 19:30:09 +01:00
|
|
|
)
|
2020-07-01 09:47:09 +02:00
|
|
|
from zerver.lib.cache import bounce_key_prefix_for_testing
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.initial_password import initial_password
|
2021-06-15 14:30:51 +02:00
|
|
|
from zerver.lib.notification_data import UserMessageNotificationsData
|
2020-07-01 09:47:09 +02:00
|
|
|
from zerver.lib.rate_limiter import bounce_redis_key_prefix_for_testing
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.sessions import get_session_dict_user
|
|
|
|
from zerver.lib.stream_subscription import get_stream_subscriptions_for_user
|
2020-03-24 14:47:41 +01:00
|
|
|
from zerver.lib.streams import (
|
|
|
|
create_stream_if_needed,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
get_default_value_for_history_public_to_subscribers,
|
2020-03-24 14:47:41 +01:00
|
|
|
)
|
2020-08-19 12:40:10 +02:00
|
|
|
from zerver.lib.test_console_output import (
|
|
|
|
ExtraConsoleOutputFinder,
|
|
|
|
ExtraConsoleOutputInTestException,
|
|
|
|
TeeStderrAndFindExtraConsoleOutput,
|
|
|
|
TeeStdoutAndFindExtraConsoleOutput,
|
|
|
|
)
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.test_helpers import find_key_by_email, instrument_url
|
|
|
|
from zerver.lib.users import get_api_key
|
2020-06-23 00:54:02 +02:00
|
|
|
from zerver.lib.validator import check_string
|
2021-06-26 10:07:54 +02:00
|
|
|
from zerver.lib.webhooks.common import (
|
|
|
|
check_send_webhook_message,
|
|
|
|
get_fixture_http_headers,
|
|
|
|
standardize_headers,
|
|
|
|
)
|
2016-11-10 19:30:09 +01:00
|
|
|
from zerver.models import (
|
|
|
|
Client,
|
|
|
|
Message,
|
|
|
|
Realm,
|
|
|
|
Recipient,
|
|
|
|
Stream,
|
|
|
|
Subscription,
|
|
|
|
UserProfile,
|
2020-06-11 00:54:34 +02:00
|
|
|
clear_supported_auth_backends_cache,
|
|
|
|
flush_per_request_caches,
|
|
|
|
get_client,
|
|
|
|
get_display_recipient,
|
|
|
|
get_realm,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
get_realm_stream,
|
2020-06-11 00:54:34 +02:00
|
|
|
get_stream,
|
|
|
|
get_system_bot,
|
|
|
|
get_user,
|
|
|
|
get_user_by_delivery_email,
|
2016-11-10 19:30:09 +01:00
|
|
|
)
|
2020-07-09 21:09:05 +02:00
|
|
|
from zerver.openapi.openapi import validate_against_openapi_schema, validate_request
|
2018-08-10 22:43:58 +02:00
|
|
|
from zerver.tornado.event_queue import clear_client_event_queues_for_testing
|
2020-08-27 22:46:39 +02:00
|
|
|
|
|
|
|
if settings.ZILENCER_ENABLED:
|
|
|
|
from zilencer.models import get_remote_server_by_uuid
|
2016-11-10 19:30:09 +01:00
|
|
|
|
|
|
|
|
2021-06-30 09:46:14 +02:00
|
|
|
class EmptyResponseError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2017-02-16 10:10:37 +01:00
|
|
|
class UploadSerializeMixin(SerializeMixin):
|
|
|
|
"""
|
|
|
|
We cannot use override_settings to change upload directory because
|
2020-10-23 02:43:28 +02:00
|
|
|
because settings.LOCAL_UPLOADS_DIR is used in URL pattern and URLs
|
2017-02-16 10:10:37 +01:00
|
|
|
are compiled only once. Otherwise using a different upload directory
|
|
|
|
for conflicting test cases would have provided better performance
|
|
|
|
while providing the required isolation.
|
|
|
|
"""
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
lockfile = "var/upload_lock"
|
2017-02-16 10:10:37 +01:00
|
|
|
|
|
|
|
@classmethod
|
2017-11-27 05:27:04 +01:00
|
|
|
def setUpClass(cls: Any, *args: Any, **kwargs: Any) -> None:
|
2017-02-16 10:10:37 +01:00
|
|
|
if not os.path.exists(cls.lockfile):
|
2021-02-12 08:20:45 +01:00
|
|
|
with open(cls.lockfile, "w"): # nocoverage - rare locking case
|
2017-02-16 10:10:37 +01:00
|
|
|
pass
|
|
|
|
|
2020-04-09 21:51:58 +02:00
|
|
|
super().setUpClass(*args, **kwargs)
|
2017-02-16 10:10:37 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2016-11-10 19:30:09 +01:00
|
|
|
class ZulipTestCase(TestCase):
|
2017-03-21 15:34:16 +01:00
|
|
|
# Ensure that the test system just shows us diffs
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
maxDiff: Optional[int] = None
|
2017-03-21 15:34:16 +01:00
|
|
|
|
2020-01-16 22:02:06 +01:00
|
|
|
def setUp(self) -> None:
|
|
|
|
super().setUp()
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
self.API_KEYS: Dict[str, str] = {}
|
2020-01-16 22:02:06 +01:00
|
|
|
|
2020-07-01 09:47:09 +02:00
|
|
|
test_name = self.id()
|
|
|
|
bounce_key_prefix_for_testing(test_name)
|
|
|
|
bounce_redis_key_prefix_for_testing(test_name)
|
|
|
|
|
2018-08-10 22:43:58 +02:00
|
|
|
def tearDown(self) -> None:
|
|
|
|
super().tearDown()
|
|
|
|
# Important: we need to clear event queues to avoid leaking data to future tests.
|
|
|
|
clear_client_event_queues_for_testing()
|
2019-03-17 22:19:53 +01:00
|
|
|
clear_supported_auth_backends_cache()
|
2019-05-03 22:52:56 +02:00
|
|
|
flush_per_request_caches()
|
2019-05-06 00:59:02 +02:00
|
|
|
translation.activate(settings.LANGUAGE_CODE)
|
2018-08-10 22:43:58 +02:00
|
|
|
|
2020-10-23 02:43:28 +02:00
|
|
|
# Clean up after using fakeldap in LDAP tests:
|
2021-02-12 08:20:45 +01:00
|
|
|
if hasattr(self, "mock_ldap") and hasattr(self, "mock_initialize"):
|
2019-10-16 18:01:38 +02:00
|
|
|
if self.mock_ldap is not None:
|
|
|
|
self.mock_ldap.reset()
|
|
|
|
self.mock_initialize.stop()
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def run(self, result: Optional[TestResult] = None) -> Optional[TestResult]: # nocoverage
|
2020-08-19 12:40:10 +02:00
|
|
|
if not settings.BAN_CONSOLE_OUTPUT:
|
2020-09-02 20:26:49 +02:00
|
|
|
return super().run(result)
|
2020-08-19 12:40:10 +02:00
|
|
|
extra_output_finder = ExtraConsoleOutputFinder()
|
2021-02-12 08:19:30 +01:00
|
|
|
with TeeStderrAndFindExtraConsoleOutput(
|
|
|
|
extra_output_finder
|
|
|
|
), TeeStdoutAndFindExtraConsoleOutput(extra_output_finder):
|
2020-09-02 20:26:49 +02:00
|
|
|
test_result = super().run(result)
|
2020-08-19 12:40:10 +02:00
|
|
|
if extra_output_finder.full_extra_output:
|
|
|
|
exception_message = f"""
|
|
|
|
---- UNEXPECTED CONSOLE OUTPUT DETECTED ----
|
|
|
|
|
|
|
|
To ensure that we never miss important error output/warnings,
|
|
|
|
we require test-backend to have clean console output.
|
|
|
|
|
|
|
|
This message usually is triggered by forgotten debugging print()
|
|
|
|
statements or new logging statements. For the latter, you can
|
|
|
|
use `with self.assertLogs()` to capture and verify the log output;
|
|
|
|
use `git grep assertLogs` to see dozens of correct examples.
|
|
|
|
|
|
|
|
You should be able to quickly reproduce this failure with:
|
|
|
|
|
|
|
|
test-backend --ban-console-output {self.id()}
|
|
|
|
|
|
|
|
Output:
|
|
|
|
{extra_output_finder.full_extra_output}
|
|
|
|
--------------------------------------------
|
|
|
|
"""
|
|
|
|
raise ExtraConsoleOutputInTestException(exception_message)
|
|
|
|
return test_result
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
"""
|
2016-11-10 19:30:09 +01:00
|
|
|
WRAPPER_COMMENT:
|
|
|
|
|
|
|
|
We wrap calls to self.client.{patch,put,get,post,delete} for various
|
|
|
|
reasons. Some of this has to do with fixing encodings before calling
|
|
|
|
into the Django code. Some of this has to do with providing a future
|
|
|
|
path for instrumentation. Some of it's just consistency.
|
|
|
|
|
|
|
|
The linter will prevent direct calls to self.client.foo, so the wrapper
|
|
|
|
functions have to fake out the linter by using a local variable called
|
|
|
|
django_client to fool the regext.
|
2021-02-12 08:20:45 +01:00
|
|
|
"""
|
2017-08-26 01:33:53 +02:00
|
|
|
DEFAULT_SUBDOMAIN = "zulip"
|
2018-06-08 11:06:18 +02:00
|
|
|
TOKENIZED_NOREPLY_REGEX = settings.TOKENIZED_NOREPLY_EMAIL_ADDRESS.format(token="[a-z0-9_]{24}")
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2020-02-25 02:40:46 +01:00
|
|
|
def set_http_headers(self, kwargs: Dict[str, Any]) -> None:
|
2021-02-12 08:20:45 +01:00
|
|
|
if "subdomain" in kwargs:
|
|
|
|
kwargs["HTTP_HOST"] = Realm.host_for_subdomain(kwargs["subdomain"])
|
|
|
|
del kwargs["subdomain"]
|
|
|
|
elif "HTTP_HOST" not in kwargs:
|
|
|
|
kwargs["HTTP_HOST"] = Realm.host_for_subdomain(self.DEFAULT_SUBDOMAIN)
|
2017-08-26 00:02:02 +02:00
|
|
|
|
2020-02-25 02:53:12 +01:00
|
|
|
# set User-Agent
|
2021-02-12 08:20:45 +01:00
|
|
|
if "HTTP_AUTHORIZATION" in kwargs:
|
2020-02-25 02:53:12 +01:00
|
|
|
# An API request; use mobile as the default user agent
|
|
|
|
default_user_agent = "ZulipMobile/26.22.145 (iOS 10.3.1)"
|
|
|
|
else:
|
2021-05-14 00:16:30 +02:00
|
|
|
# A web app request; use a browser User-Agent string.
|
2021-02-12 08:19:30 +01:00
|
|
|
default_user_agent = (
|
|
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
|
|
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
|
|
+ "Chrome/79.0.3945.130 Safari/537.36"
|
|
|
|
)
|
2021-02-12 08:20:45 +01:00
|
|
|
if kwargs.get("skip_user_agent"):
|
2020-02-25 02:53:12 +01:00
|
|
|
# Provide a way to disable setting User-Agent if desired.
|
2021-02-12 08:20:45 +01:00
|
|
|
assert "HTTP_USER_AGENT" not in kwargs
|
|
|
|
del kwargs["skip_user_agent"]
|
|
|
|
elif "HTTP_USER_AGENT" not in kwargs:
|
|
|
|
kwargs["HTTP_USER_AGENT"] = default_user_agent
|
2020-02-25 02:53:12 +01:00
|
|
|
|
2020-07-09 21:09:05 +02:00
|
|
|
def extract_api_suffix_url(self, url: str) -> Tuple[str, Dict[str, Any]]:
|
|
|
|
"""
|
2020-10-23 02:43:28 +02:00
|
|
|
Function that extracts the URL after `/api/v1` or `/json` and also
|
|
|
|
returns the query data in the URL, if there is any.
|
2020-07-09 21:09:05 +02:00
|
|
|
"""
|
2021-02-12 08:20:45 +01:00
|
|
|
url_split = url.split("?")
|
2020-07-09 21:09:05 +02:00
|
|
|
data: Dict[str, Any] = {}
|
|
|
|
if len(url_split) == 2:
|
|
|
|
data = urllib.parse.parse_qs(url_split[1])
|
|
|
|
url = url_split[0]
|
|
|
|
url = url.replace("/json/", "/").replace("/api/v1/", "/")
|
|
|
|
return (url, data)
|
|
|
|
|
2020-08-07 04:45:55 +02:00
|
|
|
def validate_api_response_openapi(
|
|
|
|
self,
|
|
|
|
url: str,
|
|
|
|
method: str,
|
|
|
|
result: HttpResponse,
|
|
|
|
data: Union[str, bytes, Dict[str, Any]],
|
|
|
|
http_headers: Dict[str, Any],
|
|
|
|
intentionally_undocumented: bool = False,
|
|
|
|
) -> None:
|
2020-06-13 17:59:46 +02:00
|
|
|
"""
|
|
|
|
Validates all API responses received by this test against Zulip's API documentation,
|
|
|
|
declared in zerver/openapi/zulip.yaml. This powerful test lets us use Zulip's
|
|
|
|
extensive test coverage of corner cases in the API to ensure that we've properly
|
|
|
|
documented those corner cases.
|
|
|
|
"""
|
|
|
|
if not (url.startswith("/json") or url.startswith("/api/v1")):
|
|
|
|
return
|
|
|
|
try:
|
2020-08-07 01:09:47 +02:00
|
|
|
content = orjson.loads(result.content)
|
2020-08-12 20:23:23 +02:00
|
|
|
except orjson.JSONDecodeError:
|
2020-06-13 17:59:46 +02:00
|
|
|
return
|
2020-07-09 21:09:05 +02:00
|
|
|
json_url = False
|
2021-02-12 08:20:45 +01:00
|
|
|
if url.startswith("/json"):
|
2020-07-09 21:09:05 +02:00
|
|
|
json_url = True
|
|
|
|
url, query_data = self.extract_api_suffix_url(url)
|
|
|
|
if len(query_data) != 0:
|
2020-10-23 02:43:28 +02:00
|
|
|
# In some cases the query parameters are defined in the URL itself. In such cases
|
2020-08-11 01:47:44 +02:00
|
|
|
# The `data` argument of our function is not used. Hence get `data` argument
|
2020-07-09 21:09:05 +02:00
|
|
|
# from url.
|
|
|
|
data = query_data
|
2021-02-12 08:19:30 +01:00
|
|
|
response_validated = validate_against_openapi_schema(
|
|
|
|
content, url, method, str(result.status_code)
|
|
|
|
)
|
2020-07-09 21:09:05 +02:00
|
|
|
if response_validated:
|
2021-02-12 08:19:30 +01:00
|
|
|
validate_request(
|
|
|
|
url,
|
|
|
|
method,
|
|
|
|
data,
|
|
|
|
http_headers,
|
|
|
|
json_url,
|
|
|
|
str(result.status_code),
|
|
|
|
intentionally_undocumented=intentionally_undocumented,
|
|
|
|
)
|
2020-06-13 17:59:46 +02:00
|
|
|
|
2016-11-10 19:30:09 +01:00
|
|
|
@instrument_url
|
2021-02-12 08:19:30 +01:00
|
|
|
def client_patch(
|
|
|
|
self,
|
|
|
|
url: str,
|
|
|
|
info: Dict[str, Any] = {},
|
|
|
|
intentionally_undocumented: bool = False,
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> HttpResponse:
|
2016-11-10 19:30:09 +01:00
|
|
|
"""
|
|
|
|
We need to urlencode, since Django's function won't do it for us.
|
|
|
|
"""
|
|
|
|
encoded = urllib.parse.urlencode(info)
|
2017-05-17 21:13:40 +02:00
|
|
|
django_client = self.client # see WRAPPER_COMMENT
|
2020-02-25 02:40:46 +01:00
|
|
|
self.set_http_headers(kwargs)
|
2020-06-13 17:59:46 +02:00
|
|
|
result = django_client.patch(url, encoded, **kwargs)
|
2021-02-12 08:19:30 +01:00
|
|
|
self.validate_api_response_openapi(
|
|
|
|
url,
|
|
|
|
"patch",
|
|
|
|
result,
|
|
|
|
info,
|
|
|
|
kwargs,
|
|
|
|
intentionally_undocumented=intentionally_undocumented,
|
|
|
|
)
|
2020-06-13 17:59:46 +02:00
|
|
|
return result
|
2016-11-10 19:30:09 +01:00
|
|
|
|
|
|
|
@instrument_url
|
2021-02-12 08:19:30 +01:00
|
|
|
def client_patch_multipart(
|
|
|
|
self, url: str, info: Dict[str, Any] = {}, **kwargs: Any
|
|
|
|
) -> HttpResponse:
|
2016-11-10 19:30:09 +01:00
|
|
|
"""
|
|
|
|
Use this for patch requests that have file uploads or
|
|
|
|
that need some sort of multi-part content. In the future
|
|
|
|
Django's test client may become a bit more flexible,
|
|
|
|
so we can hopefully eliminate this. (When you post
|
|
|
|
with the Django test client, it deals with MULTIPART_CONTENT
|
|
|
|
automatically, but not patch.)
|
|
|
|
"""
|
|
|
|
encoded = encode_multipart(BOUNDARY, info)
|
2017-05-17 21:13:40 +02:00
|
|
|
django_client = self.client # see WRAPPER_COMMENT
|
2020-02-25 02:40:46 +01:00
|
|
|
self.set_http_headers(kwargs)
|
2021-02-12 08:19:30 +01:00
|
|
|
result = django_client.patch(url, encoded, content_type=MULTIPART_CONTENT, **kwargs)
|
2020-07-09 21:09:05 +02:00
|
|
|
self.validate_api_response_openapi(url, "patch", result, info, kwargs)
|
2020-06-13 17:59:46 +02:00
|
|
|
return result
|
2016-11-10 19:30:09 +01:00
|
|
|
|
|
|
|
@instrument_url
|
2021-02-12 08:19:30 +01:00
|
|
|
def client_put(self, url: str, info: Dict[str, Any] = {}, **kwargs: Any) -> HttpResponse:
|
2016-11-10 19:30:09 +01:00
|
|
|
encoded = urllib.parse.urlencode(info)
|
2017-05-17 21:13:40 +02:00
|
|
|
django_client = self.client # see WRAPPER_COMMENT
|
2020-02-25 02:40:46 +01:00
|
|
|
self.set_http_headers(kwargs)
|
2016-11-10 19:30:09 +01:00
|
|
|
return django_client.put(url, encoded, **kwargs)
|
|
|
|
|
2016-12-21 21:29:29 +01:00
|
|
|
@instrument_url
|
2021-02-12 08:19:30 +01:00
|
|
|
def client_delete(self, url: str, info: Dict[str, Any] = {}, **kwargs: Any) -> HttpResponse:
|
2016-11-10 19:30:09 +01:00
|
|
|
encoded = urllib.parse.urlencode(info)
|
2017-05-17 21:13:40 +02:00
|
|
|
django_client = self.client # see WRAPPER_COMMENT
|
2020-02-25 02:40:46 +01:00
|
|
|
self.set_http_headers(kwargs)
|
2020-06-13 17:59:46 +02:00
|
|
|
result = django_client.delete(url, encoded, **kwargs)
|
2020-07-09 21:09:05 +02:00
|
|
|
self.validate_api_response_openapi(url, "delete", result, info, kwargs)
|
2020-06-13 17:59:46 +02:00
|
|
|
return result
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2017-03-05 09:31:17 +01:00
|
|
|
@instrument_url
|
2021-02-12 08:19:30 +01:00
|
|
|
def client_options(self, url: str, info: Dict[str, Any] = {}, **kwargs: Any) -> HttpResponse:
|
2017-03-05 09:31:17 +01:00
|
|
|
encoded = urllib.parse.urlencode(info)
|
2017-05-17 21:13:40 +02:00
|
|
|
django_client = self.client # see WRAPPER_COMMENT
|
2020-02-25 02:40:46 +01:00
|
|
|
self.set_http_headers(kwargs)
|
2017-03-05 09:31:17 +01:00
|
|
|
return django_client.options(url, encoded, **kwargs)
|
|
|
|
|
2017-08-26 01:24:50 +02:00
|
|
|
@instrument_url
|
2021-02-12 08:19:30 +01:00
|
|
|
def client_head(self, url: str, info: Dict[str, Any] = {}, **kwargs: Any) -> HttpResponse:
|
2017-08-26 01:24:50 +02:00
|
|
|
encoded = urllib.parse.urlencode(info)
|
|
|
|
django_client = self.client # see WRAPPER_COMMENT
|
2020-02-25 02:40:46 +01:00
|
|
|
self.set_http_headers(kwargs)
|
2017-08-26 01:24:50 +02:00
|
|
|
return django_client.head(url, encoded, **kwargs)
|
|
|
|
|
2016-11-10 19:30:09 +01:00
|
|
|
@instrument_url
|
2020-08-07 04:45:55 +02:00
|
|
|
def client_post(
|
|
|
|
self,
|
|
|
|
url: str,
|
|
|
|
info: Union[str, bytes, Dict[str, Any]] = {},
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> HttpResponse:
|
|
|
|
intentionally_undocumented: bool = kwargs.pop("intentionally_undocumented", False)
|
2017-05-17 21:13:40 +02:00
|
|
|
django_client = self.client # see WRAPPER_COMMENT
|
2020-02-25 02:40:46 +01:00
|
|
|
self.set_http_headers(kwargs)
|
2020-06-13 17:59:46 +02:00
|
|
|
result = django_client.post(url, info, **kwargs)
|
2021-02-12 08:19:30 +01:00
|
|
|
self.validate_api_response_openapi(
|
|
|
|
url, "post", result, info, kwargs, intentionally_undocumented=intentionally_undocumented
|
|
|
|
)
|
2020-06-13 17:59:46 +02:00
|
|
|
return result
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2016-11-17 16:52:28 +01:00
|
|
|
@instrument_url
|
2018-05-11 01:40:45 +02:00
|
|
|
def client_post_request(self, url: str, req: Any) -> HttpResponse:
|
2016-11-17 16:52:28 +01:00
|
|
|
"""
|
|
|
|
We simulate hitting an endpoint here, although we
|
|
|
|
actually resolve the URL manually and hit the view
|
|
|
|
directly. We have this helper method to allow our
|
|
|
|
instrumentation to work for /notify_tornado and
|
|
|
|
future similar methods that require doing funny
|
|
|
|
things to a request object.
|
|
|
|
"""
|
|
|
|
|
|
|
|
match = resolve(url)
|
|
|
|
return match.func(req)
|
|
|
|
|
2016-11-10 19:30:09 +01:00
|
|
|
@instrument_url
|
2020-08-07 04:45:55 +02:00
|
|
|
def client_get(self, url: str, info: Dict[str, Any] = {}, **kwargs: Any) -> HttpResponse:
|
|
|
|
intentionally_undocumented: bool = kwargs.pop("intentionally_undocumented", False)
|
2017-05-17 21:13:40 +02:00
|
|
|
django_client = self.client # see WRAPPER_COMMENT
|
2020-02-25 02:40:46 +01:00
|
|
|
self.set_http_headers(kwargs)
|
2020-06-13 17:59:46 +02:00
|
|
|
result = django_client.get(url, info, **kwargs)
|
2021-02-12 08:19:30 +01:00
|
|
|
self.validate_api_response_openapi(
|
|
|
|
url, "get", result, info, kwargs, intentionally_undocumented=intentionally_undocumented
|
|
|
|
)
|
2020-06-13 17:59:46 +02:00
|
|
|
return result
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2017-05-07 17:21:26 +02:00
|
|
|
example_user_map = dict(
|
2021-02-12 08:20:45 +01:00
|
|
|
hamlet="hamlet@zulip.com",
|
|
|
|
cordelia="cordelia@zulip.com",
|
|
|
|
iago="iago@zulip.com",
|
|
|
|
prospero="prospero@zulip.com",
|
|
|
|
othello="othello@zulip.com",
|
|
|
|
AARON="AARON@zulip.com",
|
|
|
|
aaron="aaron@zulip.com",
|
|
|
|
ZOE="ZOE@zulip.com",
|
|
|
|
polonius="polonius@zulip.com",
|
|
|
|
desdemona="desdemona@zulip.com",
|
2020-12-22 15:46:00 +01:00
|
|
|
shiva="shiva@zulip.com",
|
2021-02-12 08:20:45 +01:00
|
|
|
webhook_bot="webhook-bot@zulip.com",
|
|
|
|
welcome_bot="welcome-bot@zulip.com",
|
|
|
|
outgoing_webhook_bot="outgoing-webhook@zulip.com",
|
|
|
|
default_bot="default-bot@zulip.com",
|
2017-05-07 17:21:26 +02:00
|
|
|
)
|
|
|
|
|
2017-05-23 01:26:38 +02:00
|
|
|
mit_user_map = dict(
|
2017-11-03 03:12:25 +01:00
|
|
|
sipbtest="sipbtest@mit.edu",
|
|
|
|
starnine="starnine@mit.edu",
|
|
|
|
espuser="espuser@mit.edu",
|
2017-05-23 01:26:38 +02:00
|
|
|
)
|
|
|
|
|
2018-09-14 12:49:42 +02:00
|
|
|
lear_user_map = dict(
|
|
|
|
cordelia="cordelia@zulip.com",
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
king="king@lear.org",
|
2018-09-14 12:49:42 +02:00
|
|
|
)
|
|
|
|
|
2017-05-24 02:42:31 +02:00
|
|
|
# Non-registered test users
|
|
|
|
nonreg_user_map = dict(
|
2021-02-12 08:20:45 +01:00
|
|
|
test="test@zulip.com",
|
|
|
|
test1="test1@zulip.com",
|
|
|
|
alice="alice@zulip.com",
|
|
|
|
newuser="newuser@zulip.com",
|
|
|
|
bob="bob@zulip.com",
|
|
|
|
cordelia="cordelia@zulip.com",
|
|
|
|
newguy="newguy@zulip.com",
|
|
|
|
me="me@zulip.com",
|
2017-05-24 02:42:31 +02:00
|
|
|
)
|
|
|
|
|
2019-10-18 18:25:51 +02:00
|
|
|
example_user_ldap_username_map = dict(
|
2021-02-12 08:20:45 +01:00
|
|
|
hamlet="hamlet",
|
|
|
|
cordelia="cordelia",
|
2019-10-18 18:25:51 +02:00
|
|
|
# aaron's uid in our test directory is "letham".
|
2021-02-12 08:20:45 +01:00
|
|
|
aaron="letham",
|
2019-10-18 18:25:51 +02:00
|
|
|
)
|
|
|
|
|
2017-11-27 05:27:04 +01:00
|
|
|
def nonreg_user(self, name: str) -> UserProfile:
|
2017-05-24 02:42:31 +02:00
|
|
|
email = self.nonreg_user_map[name]
|
2020-03-12 14:17:25 +01:00
|
|
|
return get_user_by_delivery_email(email, get_realm("zulip"))
|
2017-05-24 02:42:31 +02:00
|
|
|
|
2017-11-27 05:27:04 +01:00
|
|
|
def example_user(self, name: str) -> UserProfile:
|
2017-05-07 17:21:26 +02:00
|
|
|
email = self.example_user_map[name]
|
2021-02-12 08:20:45 +01:00
|
|
|
return get_user_by_delivery_email(email, get_realm("zulip"))
|
2017-05-23 01:26:38 +02:00
|
|
|
|
2017-11-27 05:27:04 +01:00
|
|
|
def mit_user(self, name: str) -> UserProfile:
|
2017-05-23 01:26:38 +02:00
|
|
|
email = self.mit_user_map[name]
|
2021-02-12 08:20:45 +01:00
|
|
|
return get_user(email, get_realm("zephyr"))
|
2017-05-07 17:21:26 +02:00
|
|
|
|
2018-09-14 12:49:42 +02:00
|
|
|
def lear_user(self, name: str) -> UserProfile:
|
|
|
|
email = self.lear_user_map[name]
|
2021-02-12 08:20:45 +01:00
|
|
|
return get_user(email, get_realm("lear"))
|
2018-09-14 12:49:42 +02:00
|
|
|
|
2018-05-11 01:40:45 +02:00
|
|
|
def nonreg_email(self, name: str) -> str:
|
2017-05-24 02:42:31 +02:00
|
|
|
return self.nonreg_user_map[name]
|
|
|
|
|
2018-05-11 01:40:45 +02:00
|
|
|
def example_email(self, name: str) -> str:
|
2017-05-23 23:35:03 +02:00
|
|
|
return self.example_user_map[name]
|
|
|
|
|
2018-05-11 01:40:45 +02:00
|
|
|
def mit_email(self, name: str) -> str:
|
2017-05-23 23:35:03 +02:00
|
|
|
return self.mit_user_map[name]
|
|
|
|
|
2021-03-08 11:39:48 +01:00
|
|
|
def notification_bot(self, realm: Realm) -> UserProfile:
|
|
|
|
return get_system_bot(settings.NOTIFICATION_BOT, realm.id)
|
2017-05-08 17:42:50 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def create_test_bot(
|
2021-02-12 08:20:45 +01:00
|
|
|
self, short_name: str, user_profile: UserProfile, full_name: str = "Foo Bot", **extras: Any
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> UserProfile:
|
2020-03-06 18:40:46 +01:00
|
|
|
self.login_user(user_profile)
|
2018-01-30 17:05:14 +01:00
|
|
|
bot_info = {
|
2021-02-12 08:20:45 +01:00
|
|
|
"short_name": short_name,
|
|
|
|
"full_name": full_name,
|
2018-01-30 17:05:14 +01:00
|
|
|
}
|
|
|
|
bot_info.update(extras)
|
|
|
|
result = self.client_post("/json/bots", bot_info)
|
2020-07-05 00:50:18 +02:00
|
|
|
self.assert_json_success(result)
|
2021-02-12 08:20:45 +01:00
|
|
|
bot_email = f"{short_name}-bot@zulip.testserver"
|
2020-07-05 00:50:18 +02:00
|
|
|
bot_profile = get_user(bot_email, user_profile.realm)
|
|
|
|
return bot_profile
|
|
|
|
|
|
|
|
def fail_to_create_test_bot(
|
2021-02-12 08:19:30 +01:00
|
|
|
self,
|
|
|
|
short_name: str,
|
2020-07-05 00:50:18 +02:00
|
|
|
user_profile: UserProfile,
|
2021-02-12 08:20:45 +01:00
|
|
|
full_name: str = "Foo Bot",
|
2020-07-05 00:50:18 +02:00
|
|
|
*,
|
|
|
|
assert_json_error_msg: str,
|
|
|
|
**extras: Any,
|
|
|
|
) -> None:
|
|
|
|
self.login_user(user_profile)
|
|
|
|
bot_info = {
|
2021-02-12 08:20:45 +01:00
|
|
|
"short_name": short_name,
|
|
|
|
"full_name": full_name,
|
2020-07-05 00:50:18 +02:00
|
|
|
}
|
|
|
|
bot_info.update(extras)
|
|
|
|
result = self.client_post("/json/bots", bot_info)
|
|
|
|
self.assert_json_error(result, assert_json_error_msg)
|
2017-10-25 17:17:17 +02:00
|
|
|
|
2020-10-02 00:14:25 +02:00
|
|
|
def _get_page_params(self, result: HttpResponse) -> Dict[str, Any]:
|
2021-05-14 00:16:30 +02:00
|
|
|
"""Helper for parsing page_params after fetching the web app's home view."""
|
2020-10-02 00:14:25 +02:00
|
|
|
doc = lxml.html.document_fromstring(result.content)
|
|
|
|
[div] = doc.xpath("//div[@id='page-params']")
|
|
|
|
page_params_json = div.get("data-params")
|
|
|
|
page_params = orjson.loads(page_params_json)
|
|
|
|
return page_params
|
|
|
|
|
|
|
|
def check_rendered_logged_in_app(self, result: HttpResponse) -> None:
|
|
|
|
"""Verifies that a visit of / was a 200 that rendered page_params
|
2021-06-15 18:03:32 +02:00
|
|
|
and not for a (logged-out) spectator."""
|
2020-10-02 00:14:25 +02:00
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
page_params = self._get_page_params(result)
|
2021-06-15 18:03:32 +02:00
|
|
|
# It is important to check `is_spectator` to verify
|
2020-10-02 00:14:25 +02:00
|
|
|
# that we treated this request as a normal logged-in session,
|
2021-06-15 18:03:32 +02:00
|
|
|
# not as a spectator.
|
|
|
|
self.assertEqual(page_params["is_spectator"], False)
|
2020-10-02 00:14:25 +02:00
|
|
|
|
2021-06-15 18:03:32 +02:00
|
|
|
def check_rendered_spectator(self, result: HttpResponse) -> None:
|
2020-09-27 06:49:16 +02:00
|
|
|
"""Verifies that a visit of / was a 200 that rendered page_params
|
2021-06-15 18:03:32 +02:00
|
|
|
for a (logged-out) spectator."""
|
2020-09-27 06:49:16 +02:00
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
page_params = self._get_page_params(result)
|
2021-06-15 18:03:32 +02:00
|
|
|
# It is important to check `is_spectator` to verify
|
|
|
|
# that we treated this request to render for a `spectator`
|
|
|
|
self.assertEqual(page_params["is_spectator"], True)
|
2020-09-27 06:49:16 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def login_with_return(
|
|
|
|
self, email: str, password: Optional[str] = None, **kwargs: Any
|
|
|
|
) -> HttpResponse:
|
2016-11-10 19:30:09 +01:00
|
|
|
if password is None:
|
|
|
|
password = initial_password(email)
|
2021-02-12 08:19:30 +01:00
|
|
|
result = self.client_post(
|
2021-02-12 08:20:45 +01:00
|
|
|
"/accounts/login/", {"username": email, "password": password}, **kwargs
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2019-01-29 20:32:49 +01:00
|
|
|
self.assertNotEqual(result.status_code, 500)
|
|
|
|
return result
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2020-03-06 18:40:46 +01:00
|
|
|
def login(self, name: str) -> None:
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2020-03-06 18:40:46 +01:00
|
|
|
Use this for really simple tests where you just need
|
|
|
|
to be logged in as some user, but don't need the actual
|
|
|
|
user object for anything else. Try to use 'hamlet' for
|
|
|
|
non-admins and 'iago' for admins:
|
|
|
|
|
|
|
|
self.login('hamlet')
|
|
|
|
|
|
|
|
Try to use 'cordelia' or 'othello' as "other" users.
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2021-02-12 08:20:45 +01:00
|
|
|
assert "@" not in name, "use login_by_email for email logins"
|
2020-03-06 18:40:46 +01:00
|
|
|
user = self.example_user(name)
|
|
|
|
self.login_user(user)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def login_by_email(self, email: str, password: str) -> None:
|
2020-03-06 18:40:46 +01:00
|
|
|
realm = get_realm("zulip")
|
2020-06-26 19:29:37 +02:00
|
|
|
request = HttpRequest()
|
|
|
|
request.session = self.client.session
|
2020-03-06 18:40:46 +01:00
|
|
|
self.assertTrue(
|
|
|
|
self.client.login(
|
2020-06-26 19:29:37 +02:00
|
|
|
request=request,
|
2020-03-06 18:40:46 +01:00
|
|
|
username=email,
|
|
|
|
password=password,
|
|
|
|
realm=realm,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
),
|
2020-03-06 18:40:46 +01:00
|
|
|
)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def assert_login_failure(self, email: str, password: str) -> None:
|
2020-03-06 18:40:46 +01:00
|
|
|
realm = get_realm("zulip")
|
|
|
|
self.assertFalse(
|
|
|
|
self.client.login(
|
|
|
|
username=email,
|
|
|
|
password=password,
|
|
|
|
realm=realm,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
),
|
2020-03-06 18:40:46 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
def login_user(self, user_profile: UserProfile) -> None:
|
|
|
|
email = user_profile.delivery_email
|
|
|
|
realm = user_profile.realm
|
|
|
|
password = initial_password(email)
|
2020-06-26 19:29:37 +02:00
|
|
|
request = HttpRequest()
|
|
|
|
request.session = self.client.session
|
2021-02-12 08:19:30 +01:00
|
|
|
self.assertTrue(
|
|
|
|
self.client.login(request=request, username=email, password=password, realm=realm)
|
|
|
|
)
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2017-07-13 13:42:57 +02:00
|
|
|
def login_2fa(self, user_profile: UserProfile) -> None:
|
|
|
|
"""
|
|
|
|
We need this function to call request.session.save().
|
|
|
|
do_two_factor_login doesn't save session; in normal request-response
|
|
|
|
cycle this doesn't matter because middleware will save the session
|
|
|
|
when it finds it dirty; however,in tests we will have to do that
|
|
|
|
explicitly.
|
|
|
|
"""
|
|
|
|
request = HttpRequest()
|
|
|
|
request.session = self.client.session
|
|
|
|
request.user = user_profile
|
|
|
|
do_two_factor_login(request, user_profile)
|
|
|
|
request.session.save()
|
|
|
|
|
2017-11-27 05:27:04 +01:00
|
|
|
def logout(self) -> None:
|
2017-04-18 03:23:32 +02:00
|
|
|
self.client.logout()
|
|
|
|
|
2018-05-11 01:40:45 +02:00
|
|
|
def register(self, email: str, password: str, **kwargs: Any) -> HttpResponse:
|
2021-02-12 08:20:45 +01:00
|
|
|
self.client_post("/accounts/home/", {"email": email}, **kwargs)
|
2017-08-26 01:08:14 +02:00
|
|
|
return self.submit_reg_form_for_user(email, password, **kwargs)
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2017-11-27 05:27:04 +01:00
|
|
|
def submit_reg_form_for_user(
|
2021-02-12 08:19:30 +01:00
|
|
|
self,
|
|
|
|
email: str,
|
2021-06-24 23:54:50 +02:00
|
|
|
password: Optional[str],
|
2021-02-12 08:19:30 +01:00
|
|
|
realm_name: str = "Zulip Test",
|
|
|
|
realm_subdomain: str = "zuliptest",
|
2021-02-12 08:20:45 +01:00
|
|
|
from_confirmation: str = "",
|
2021-02-12 08:19:30 +01:00
|
|
|
full_name: Optional[str] = None,
|
2021-02-12 08:20:45 +01:00
|
|
|
timezone: str = "",
|
2021-02-12 08:19:30 +01:00
|
|
|
realm_in_root_domain: Optional[str] = None,
|
|
|
|
default_stream_groups: Sequence[str] = [],
|
2020-12-04 19:45:58 +01:00
|
|
|
source_realm_id: str = "",
|
2021-02-12 08:19:30 +01:00
|
|
|
key: Optional[str] = None,
|
2021-06-24 20:05:06 +02:00
|
|
|
realm_type: Optional[int] = Realm.ORG_TYPES["business"]["id"],
|
2021-02-12 08:19:30 +01:00
|
|
|
**kwargs: Any,
|
|
|
|
) -> HttpResponse:
|
2016-11-10 19:30:09 +01:00
|
|
|
"""
|
|
|
|
Stage two of the two-step registration process.
|
|
|
|
|
|
|
|
If things are working correctly the account should be fully
|
|
|
|
registered after this call.
|
|
|
|
|
|
|
|
You can pass the HTTP_HOST variable for subdomains via kwargs.
|
|
|
|
"""
|
2017-02-08 05:04:14 +01:00
|
|
|
if full_name is None:
|
|
|
|
full_name = email.replace("@", "_")
|
2017-10-19 08:23:27 +02:00
|
|
|
payload = {
|
2021-02-12 08:20:45 +01:00
|
|
|
"full_name": full_name,
|
|
|
|
"realm_name": realm_name,
|
|
|
|
"realm_subdomain": realm_subdomain,
|
2021-06-24 20:05:06 +02:00
|
|
|
"realm_type": realm_type,
|
2021-02-12 08:20:45 +01:00
|
|
|
"key": key if key is not None else find_key_by_email(email),
|
|
|
|
"timezone": timezone,
|
|
|
|
"terms": True,
|
|
|
|
"from_confirmation": from_confirmation,
|
|
|
|
"default_stream_group": default_stream_groups,
|
2020-12-04 19:45:58 +01:00
|
|
|
"source_realm_id": source_realm_id,
|
2017-10-19 08:23:27 +02:00
|
|
|
}
|
2021-06-24 23:54:50 +02:00
|
|
|
if password is not None:
|
|
|
|
payload["password"] = password
|
2017-10-19 08:30:40 +02:00
|
|
|
if realm_in_root_domain is not None:
|
2021-02-12 08:20:45 +01:00
|
|
|
payload["realm_in_root_domain"] = realm_in_root_domain
|
|
|
|
return self.client_post("/accounts/register/", payload, **kwargs)
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2020-07-05 02:14:06 +02:00
|
|
|
def get_confirmation_url_from_outbox(
|
|
|
|
self,
|
|
|
|
email_address: str,
|
|
|
|
*,
|
2021-02-12 08:19:30 +01:00
|
|
|
url_pattern: Optional[str] = None,
|
2021-05-26 19:40:12 +02:00
|
|
|
email_subject_contains: Optional[str] = None,
|
|
|
|
email_body_contains: Optional[str] = None,
|
2020-07-05 02:14:06 +02:00
|
|
|
) -> str:
|
2016-11-10 19:30:09 +01:00
|
|
|
from django.core.mail import outbox
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2017-10-30 22:56:14 +01:00
|
|
|
if url_pattern is None:
|
|
|
|
# This is a bit of a crude heuristic, but good enough for most tests.
|
2018-07-02 00:05:24 +02:00
|
|
|
url_pattern = settings.EXTERNAL_HOST + r"(\S+)>"
|
2016-11-10 19:30:09 +01:00
|
|
|
for message in reversed(outbox):
|
2020-06-05 23:26:35 +02:00
|
|
|
if any(
|
2021-02-12 08:19:30 +01:00
|
|
|
addr == email_address or addr.endswith(f" <{email_address}>") for addr in message.to
|
2020-06-05 23:26:35 +02:00
|
|
|
):
|
2020-07-05 02:14:06 +02:00
|
|
|
match = re.search(url_pattern, message.body)
|
|
|
|
assert match is not None
|
2021-05-26 19:40:12 +02:00
|
|
|
|
|
|
|
if email_subject_contains:
|
|
|
|
self.assertIn(email_subject_contains, message.subject)
|
|
|
|
|
|
|
|
if email_body_contains:
|
|
|
|
self.assertIn(email_body_contains, message.body)
|
|
|
|
|
2020-07-05 02:14:06 +02:00
|
|
|
[confirmation_url] = match.groups()
|
|
|
|
return confirmation_url
|
2016-11-10 19:30:09 +01:00
|
|
|
else:
|
2017-03-05 09:01:49 +01:00
|
|
|
raise AssertionError("Couldn't find a confirmation email.")
|
2016-11-10 19:30:09 +01:00
|
|
|
|
tests: Add uuid_get and uuid_post.
We want a clean codepath for the vast majority
of cases of using api_get/api_post, which now
uses email and which we'll soon convert to
accepting `user` as a parameter.
These apis that take two different types of
values for the same parameter make sweeps
like this kinda painful, and they're pretty
easy to avoid by extracting helpers to do
the actual common tasks. So, for example,
here I still keep a common method to
actually encode the credentials (since
the whole encode/decode business is an
annoying detail that you don't want to fix
in two places):
def encode_credentials(self, identifier: str, api_key: str) -> str:
"""
identifier: Can be an email or a remote server uuid.
"""
credentials = "%s:%s" % (identifier, api_key)
return 'Basic ' + base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
But then the rest of the code has two separate
codepaths.
And for the uuid functions, we no longer have
crufty references to realm. (In fairness, realm
will also go away when we introduce users.)
For the `is_remote_server` helper, I just inlined
it, since it's now only needed in one place, and the
name didn't make total sense anyway, plus it wasn't
a super robust check. In context, it's easier
just to use a comment now to say what we're doing:
# If `role` doesn't look like an email, it might be a uuid.
if settings.ZILENCER_ENABLED and role is not None and '@' not in role:
# do stuff
2020-03-10 12:34:25 +01:00
|
|
|
def encode_uuid(self, uuid: str) -> str:
|
2017-04-27 11:41:27 +02:00
|
|
|
"""
|
|
|
|
identifier: Can be an email or a remote server uuid.
|
|
|
|
"""
|
tests: Add uuid_get and uuid_post.
We want a clean codepath for the vast majority
of cases of using api_get/api_post, which now
uses email and which we'll soon convert to
accepting `user` as a parameter.
These apis that take two different types of
values for the same parameter make sweeps
like this kinda painful, and they're pretty
easy to avoid by extracting helpers to do
the actual common tasks. So, for example,
here I still keep a common method to
actually encode the credentials (since
the whole encode/decode business is an
annoying detail that you don't want to fix
in two places):
def encode_credentials(self, identifier: str, api_key: str) -> str:
"""
identifier: Can be an email or a remote server uuid.
"""
credentials = "%s:%s" % (identifier, api_key)
return 'Basic ' + base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
But then the rest of the code has two separate
codepaths.
And for the uuid functions, we no longer have
crufty references to realm. (In fairness, realm
will also go away when we introduce users.)
For the `is_remote_server` helper, I just inlined
it, since it's now only needed in one place, and the
name didn't make total sense anyway, plus it wasn't
a super robust check. In context, it's easier
just to use a comment now to say what we're doing:
# If `role` doesn't look like an email, it might be a uuid.
if settings.ZILENCER_ENABLED and role is not None and '@' not in role:
# do stuff
2020-03-10 12:34:25 +01:00
|
|
|
if uuid in self.API_KEYS:
|
|
|
|
api_key = self.API_KEYS[uuid]
|
2016-10-27 23:55:31 +02:00
|
|
|
else:
|
tests: Add uuid_get and uuid_post.
We want a clean codepath for the vast majority
of cases of using api_get/api_post, which now
uses email and which we'll soon convert to
accepting `user` as a parameter.
These apis that take two different types of
values for the same parameter make sweeps
like this kinda painful, and they're pretty
easy to avoid by extracting helpers to do
the actual common tasks. So, for example,
here I still keep a common method to
actually encode the credentials (since
the whole encode/decode business is an
annoying detail that you don't want to fix
in two places):
def encode_credentials(self, identifier: str, api_key: str) -> str:
"""
identifier: Can be an email or a remote server uuid.
"""
credentials = "%s:%s" % (identifier, api_key)
return 'Basic ' + base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
But then the rest of the code has two separate
codepaths.
And for the uuid functions, we no longer have
crufty references to realm. (In fairness, realm
will also go away when we introduce users.)
For the `is_remote_server` helper, I just inlined
it, since it's now only needed in one place, and the
name didn't make total sense anyway, plus it wasn't
a super robust check. In context, it's easier
just to use a comment now to say what we're doing:
# If `role` doesn't look like an email, it might be a uuid.
if settings.ZILENCER_ENABLED and role is not None and '@' not in role:
# do stuff
2020-03-10 12:34:25 +01:00
|
|
|
api_key = get_remote_server_by_uuid(uuid).api_key
|
|
|
|
self.API_KEYS[uuid] = api_key
|
2016-10-27 23:55:31 +02:00
|
|
|
|
tests: Add uuid_get and uuid_post.
We want a clean codepath for the vast majority
of cases of using api_get/api_post, which now
uses email and which we'll soon convert to
accepting `user` as a parameter.
These apis that take two different types of
values for the same parameter make sweeps
like this kinda painful, and they're pretty
easy to avoid by extracting helpers to do
the actual common tasks. So, for example,
here I still keep a common method to
actually encode the credentials (since
the whole encode/decode business is an
annoying detail that you don't want to fix
in two places):
def encode_credentials(self, identifier: str, api_key: str) -> str:
"""
identifier: Can be an email or a remote server uuid.
"""
credentials = "%s:%s" % (identifier, api_key)
return 'Basic ' + base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
But then the rest of the code has two separate
codepaths.
And for the uuid functions, we no longer have
crufty references to realm. (In fairness, realm
will also go away when we introduce users.)
For the `is_remote_server` helper, I just inlined
it, since it's now only needed in one place, and the
name didn't make total sense anyway, plus it wasn't
a super robust check. In context, it's easier
just to use a comment now to say what we're doing:
# If `role` doesn't look like an email, it might be a uuid.
if settings.ZILENCER_ENABLED and role is not None and '@' not in role:
# do stuff
2020-03-10 12:34:25 +01:00
|
|
|
return self.encode_credentials(uuid, api_key)
|
|
|
|
|
2020-03-10 11:48:26 +01:00
|
|
|
def encode_user(self, user: UserProfile) -> str:
|
|
|
|
email = user.delivery_email
|
|
|
|
api_key = user.api_key
|
|
|
|
return self.encode_credentials(email, api_key)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def encode_email(self, email: str, realm: str = "zulip") -> str:
|
2020-03-10 11:48:26 +01:00
|
|
|
# TODO: use encode_user where possible
|
2021-02-12 08:20:45 +01:00
|
|
|
assert "@" in email
|
2020-03-10 11:48:26 +01:00
|
|
|
user = get_user_by_delivery_email(email, get_realm(realm))
|
|
|
|
api_key = get_api_key(user)
|
tests: Add uuid_get and uuid_post.
We want a clean codepath for the vast majority
of cases of using api_get/api_post, which now
uses email and which we'll soon convert to
accepting `user` as a parameter.
These apis that take two different types of
values for the same parameter make sweeps
like this kinda painful, and they're pretty
easy to avoid by extracting helpers to do
the actual common tasks. So, for example,
here I still keep a common method to
actually encode the credentials (since
the whole encode/decode business is an
annoying detail that you don't want to fix
in two places):
def encode_credentials(self, identifier: str, api_key: str) -> str:
"""
identifier: Can be an email or a remote server uuid.
"""
credentials = "%s:%s" % (identifier, api_key)
return 'Basic ' + base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
But then the rest of the code has two separate
codepaths.
And for the uuid functions, we no longer have
crufty references to realm. (In fairness, realm
will also go away when we introduce users.)
For the `is_remote_server` helper, I just inlined
it, since it's now only needed in one place, and the
name didn't make total sense anyway, plus it wasn't
a super robust check. In context, it's easier
just to use a comment now to say what we're doing:
# If `role` doesn't look like an email, it might be a uuid.
if settings.ZILENCER_ENABLED and role is not None and '@' not in role:
# do stuff
2020-03-10 12:34:25 +01:00
|
|
|
|
|
|
|
return self.encode_credentials(email, api_key)
|
|
|
|
|
|
|
|
def encode_credentials(self, identifier: str, api_key: str) -> str:
|
|
|
|
"""
|
|
|
|
identifier: Can be an email or a remote server uuid.
|
|
|
|
"""
|
2020-06-10 06:41:04 +02:00
|
|
|
credentials = f"{identifier}:{api_key}"
|
2021-08-02 23:20:39 +02:00
|
|
|
return "Basic " + base64.b64encode(credentials.encode()).decode()
|
2016-11-10 19:30:09 +01:00
|
|
|
|
tests: Add uuid_get and uuid_post.
We want a clean codepath for the vast majority
of cases of using api_get/api_post, which now
uses email and which we'll soon convert to
accepting `user` as a parameter.
These apis that take two different types of
values for the same parameter make sweeps
like this kinda painful, and they're pretty
easy to avoid by extracting helpers to do
the actual common tasks. So, for example,
here I still keep a common method to
actually encode the credentials (since
the whole encode/decode business is an
annoying detail that you don't want to fix
in two places):
def encode_credentials(self, identifier: str, api_key: str) -> str:
"""
identifier: Can be an email or a remote server uuid.
"""
credentials = "%s:%s" % (identifier, api_key)
return 'Basic ' + base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
But then the rest of the code has two separate
codepaths.
And for the uuid functions, we no longer have
crufty references to realm. (In fairness, realm
will also go away when we introduce users.)
For the `is_remote_server` helper, I just inlined
it, since it's now only needed in one place, and the
name didn't make total sense anyway, plus it wasn't
a super robust check. In context, it's easier
just to use a comment now to say what we're doing:
# If `role` doesn't look like an email, it might be a uuid.
if settings.ZILENCER_ENABLED and role is not None and '@' not in role:
# do stuff
2020-03-10 12:34:25 +01:00
|
|
|
def uuid_get(self, identifier: str, *args: Any, **kwargs: Any) -> HttpResponse:
|
2021-02-12 08:20:45 +01:00
|
|
|
kwargs["HTTP_AUTHORIZATION"] = self.encode_uuid(identifier)
|
tests: Add uuid_get and uuid_post.
We want a clean codepath for the vast majority
of cases of using api_get/api_post, which now
uses email and which we'll soon convert to
accepting `user` as a parameter.
These apis that take two different types of
values for the same parameter make sweeps
like this kinda painful, and they're pretty
easy to avoid by extracting helpers to do
the actual common tasks. So, for example,
here I still keep a common method to
actually encode the credentials (since
the whole encode/decode business is an
annoying detail that you don't want to fix
in two places):
def encode_credentials(self, identifier: str, api_key: str) -> str:
"""
identifier: Can be an email or a remote server uuid.
"""
credentials = "%s:%s" % (identifier, api_key)
return 'Basic ' + base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
But then the rest of the code has two separate
codepaths.
And for the uuid functions, we no longer have
crufty references to realm. (In fairness, realm
will also go away when we introduce users.)
For the `is_remote_server` helper, I just inlined
it, since it's now only needed in one place, and the
name didn't make total sense anyway, plus it wasn't
a super robust check. In context, it's easier
just to use a comment now to say what we're doing:
# If `role` doesn't look like an email, it might be a uuid.
if settings.ZILENCER_ENABLED and role is not None and '@' not in role:
# do stuff
2020-03-10 12:34:25 +01:00
|
|
|
return self.client_get(*args, **kwargs)
|
|
|
|
|
|
|
|
def uuid_post(self, identifier: str, *args: Any, **kwargs: Any) -> HttpResponse:
|
2021-02-12 08:20:45 +01:00
|
|
|
kwargs["HTTP_AUTHORIZATION"] = self.encode_uuid(identifier)
|
tests: Add uuid_get and uuid_post.
We want a clean codepath for the vast majority
of cases of using api_get/api_post, which now
uses email and which we'll soon convert to
accepting `user` as a parameter.
These apis that take two different types of
values for the same parameter make sweeps
like this kinda painful, and they're pretty
easy to avoid by extracting helpers to do
the actual common tasks. So, for example,
here I still keep a common method to
actually encode the credentials (since
the whole encode/decode business is an
annoying detail that you don't want to fix
in two places):
def encode_credentials(self, identifier: str, api_key: str) -> str:
"""
identifier: Can be an email or a remote server uuid.
"""
credentials = "%s:%s" % (identifier, api_key)
return 'Basic ' + base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
But then the rest of the code has two separate
codepaths.
And for the uuid functions, we no longer have
crufty references to realm. (In fairness, realm
will also go away when we introduce users.)
For the `is_remote_server` helper, I just inlined
it, since it's now only needed in one place, and the
name didn't make total sense anyway, plus it wasn't
a super robust check. In context, it's easier
just to use a comment now to say what we're doing:
# If `role` doesn't look like an email, it might be a uuid.
if settings.ZILENCER_ENABLED and role is not None and '@' not in role:
# do stuff
2020-03-10 12:34:25 +01:00
|
|
|
return self.client_post(*args, **kwargs)
|
|
|
|
|
2020-03-10 11:48:26 +01:00
|
|
|
def api_get(self, user: UserProfile, *args: Any, **kwargs: Any) -> HttpResponse:
|
2021-02-12 08:20:45 +01:00
|
|
|
kwargs["HTTP_AUTHORIZATION"] = self.encode_user(user)
|
2017-12-14 19:02:02 +01:00
|
|
|
return self.client_get(*args, **kwargs)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def api_post(
|
|
|
|
self, user: UserProfile, *args: Any, intentionally_undocumented: bool = False, **kwargs: Any
|
|
|
|
) -> HttpResponse:
|
2021-02-12 08:20:45 +01:00
|
|
|
kwargs["HTTP_AUTHORIZATION"] = self.encode_user(user)
|
2021-02-12 08:19:30 +01:00
|
|
|
return self.client_post(
|
|
|
|
*args, intentionally_undocumented=intentionally_undocumented, **kwargs
|
|
|
|
)
|
2017-12-14 19:02:02 +01:00
|
|
|
|
2020-03-10 11:48:26 +01:00
|
|
|
def api_patch(self, user: UserProfile, *args: Any, **kwargs: Any) -> HttpResponse:
|
2021-02-12 08:20:45 +01:00
|
|
|
kwargs["HTTP_AUTHORIZATION"] = self.encode_user(user)
|
2017-12-14 19:02:02 +01:00
|
|
|
return self.client_patch(*args, **kwargs)
|
|
|
|
|
2020-03-10 11:48:26 +01:00
|
|
|
def api_delete(self, user: UserProfile, *args: Any, **kwargs: Any) -> HttpResponse:
|
2021-02-12 08:20:45 +01:00
|
|
|
kwargs["HTTP_AUTHORIZATION"] = self.encode_user(user)
|
2017-12-14 19:02:02 +01:00
|
|
|
return self.client_delete(*args, **kwargs)
|
|
|
|
|
2020-03-09 21:41:26 +01:00
|
|
|
def get_streams(self, user_profile: UserProfile) -> List[str]:
|
2016-11-10 19:30:09 +01:00
|
|
|
"""
|
|
|
|
Helper function to get the stream names for a user
|
|
|
|
"""
|
2017-10-29 17:11:11 +01:00
|
|
|
subs = get_stream_subscriptions_for_user(user_profile).filter(
|
2016-11-10 19:30:09 +01:00
|
|
|
active=True,
|
2017-10-29 17:11:11 +01:00
|
|
|
)
|
2021-02-12 08:19:30 +01:00
|
|
|
return [check_string("recipient", get_display_recipient(sub.recipient)) for sub in subs]
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def send_personal_message(
|
|
|
|
self,
|
|
|
|
from_user: UserProfile,
|
|
|
|
to_user: UserProfile,
|
|
|
|
content: str = "test content",
|
|
|
|
sending_client_name: str = "test suite",
|
|
|
|
) -> int:
|
2020-03-08 03:30:07 +01:00
|
|
|
recipient_list = [to_user.id]
|
2019-11-07 02:51:45 +01:00
|
|
|
(sending_client, _) = Client.objects.get_or_create(name=sending_client_name)
|
2017-10-27 19:28:02 +02:00
|
|
|
|
|
|
|
return check_send_message(
|
2021-02-12 08:19:30 +01:00
|
|
|
from_user,
|
|
|
|
sending_client,
|
2021-02-12 08:20:45 +01:00
|
|
|
"private",
|
2021-02-12 08:19:30 +01:00
|
|
|
recipient_list,
|
|
|
|
None,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
content,
|
2017-10-27 19:28:02 +02:00
|
|
|
)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def send_huddle_message(
|
|
|
|
self,
|
|
|
|
from_user: UserProfile,
|
|
|
|
to_users: List[UserProfile],
|
|
|
|
content: str = "test content",
|
|
|
|
sending_client_name: str = "test suite",
|
|
|
|
) -> int:
|
2020-03-08 03:30:07 +01:00
|
|
|
to_user_ids = [u.id for u in to_users]
|
2021-02-12 08:19:30 +01:00
|
|
|
assert len(to_user_ids) >= 2
|
2017-10-27 19:53:08 +02:00
|
|
|
|
2019-11-07 02:51:45 +01:00
|
|
|
(sending_client, _) = Client.objects.get_or_create(name=sending_client_name)
|
2017-10-27 19:53:08 +02:00
|
|
|
|
|
|
|
return check_send_message(
|
2021-02-12 08:19:30 +01:00
|
|
|
from_user,
|
|
|
|
sending_client,
|
2021-02-12 08:20:45 +01:00
|
|
|
"private",
|
2021-02-12 08:19:30 +01:00
|
|
|
to_user_ids,
|
|
|
|
None,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
content,
|
2017-10-27 19:53:08 +02:00
|
|
|
)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def send_stream_message(
|
|
|
|
self,
|
|
|
|
sender: UserProfile,
|
|
|
|
stream_name: str,
|
|
|
|
content: str = "test content",
|
|
|
|
topic_name: str = "test",
|
|
|
|
recipient_realm: Optional[Realm] = None,
|
|
|
|
sending_client_name: str = "test suite",
|
|
|
|
) -> int:
|
2019-11-07 02:51:45 +01:00
|
|
|
(sending_client, _) = Client.objects.get_or_create(name=sending_client_name)
|
2017-10-27 17:57:23 +02:00
|
|
|
|
|
|
|
return check_send_stream_message(
|
|
|
|
sender=sender,
|
|
|
|
client=sending_client,
|
|
|
|
stream_name=stream_name,
|
|
|
|
topic=topic_name,
|
|
|
|
body=content,
|
2019-10-15 22:53:28 +02:00
|
|
|
realm=recipient_realm,
|
2017-10-27 17:57:23 +02:00
|
|
|
)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def get_messages_response(
|
|
|
|
self,
|
|
|
|
anchor: Union[int, str] = 1,
|
|
|
|
num_before: int = 100,
|
|
|
|
num_after: int = 100,
|
|
|
|
use_first_unread_anchor: bool = False,
|
|
|
|
) -> Dict[str, List[Dict[str, Any]]]:
|
|
|
|
post_params = {
|
|
|
|
"anchor": anchor,
|
|
|
|
"num_before": num_before,
|
|
|
|
"num_after": num_after,
|
|
|
|
"use_first_unread_anchor": orjson.dumps(use_first_unread_anchor).decode(),
|
|
|
|
}
|
2016-11-10 19:30:09 +01:00
|
|
|
result = self.client_get("/json/messages", dict(post_params))
|
2017-08-17 08:46:39 +02:00
|
|
|
data = result.json()
|
2018-02-14 04:44:41 +01:00
|
|
|
return data
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def get_messages(
|
|
|
|
self,
|
|
|
|
anchor: Union[str, int] = 1,
|
|
|
|
num_before: int = 100,
|
|
|
|
num_after: int = 100,
|
|
|
|
use_first_unread_anchor: bool = False,
|
|
|
|
) -> List[Dict[str, Any]]:
|
2018-02-14 04:44:41 +01:00
|
|
|
data = self.get_messages_response(anchor, num_before, num_after, use_first_unread_anchor)
|
2021-02-12 08:20:45 +01:00
|
|
|
return data["messages"]
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2018-05-11 01:40:45 +02:00
|
|
|
def users_subscribed_to_stream(self, stream_name: str, realm: Realm) -> List[UserProfile]:
|
2016-11-10 19:30:09 +01:00
|
|
|
stream = Stream.objects.get(name=stream_name, realm=realm)
|
|
|
|
recipient = Recipient.objects.get(type_id=stream.id, type=Recipient.STREAM)
|
|
|
|
subscriptions = Subscription.objects.filter(recipient=recipient, active=True)
|
|
|
|
|
|
|
|
return [subscription.user_profile for subscription in subscriptions]
|
|
|
|
|
2017-11-27 05:27:04 +01:00
|
|
|
def assert_url_serves_contents_of_file(self, url: str, result: bytes) -> None:
|
2016-12-19 16:17:19 +01:00
|
|
|
response = self.client_get(url)
|
|
|
|
data = b"".join(response.streaming_content)
|
2017-02-09 01:32:42 +01:00
|
|
|
self.assertEqual(result, data)
|
2016-12-19 16:17:19 +01:00
|
|
|
|
2017-11-27 05:27:04 +01:00
|
|
|
def assert_json_success(self, result: HttpResponse) -> Dict[str, Any]:
|
2016-11-10 19:30:09 +01:00
|
|
|
"""
|
|
|
|
Successful POSTs return a 200 and JSON of the form {"result": "success",
|
|
|
|
"msg": ""}.
|
|
|
|
"""
|
2017-08-29 06:33:10 +02:00
|
|
|
try:
|
2020-08-07 01:09:47 +02:00
|
|
|
json = orjson.loads(result.content)
|
2020-10-09 02:17:33 +02:00
|
|
|
except orjson.JSONDecodeError: # nocoverage
|
2021-02-12 08:20:45 +01:00
|
|
|
json = {"msg": "Error parsing JSON in response!"}
|
|
|
|
self.assertEqual(result.status_code, 200, json["msg"])
|
2016-11-10 19:30:09 +01:00
|
|
|
self.assertEqual(json.get("result"), "success")
|
|
|
|
# We have a msg key for consistency with errors, but it typically has an
|
|
|
|
# empty value.
|
|
|
|
self.assertIn("msg", json)
|
2017-08-29 06:33:10 +02:00
|
|
|
self.assertNotEqual(json["msg"], "Error parsing JSON in response!")
|
2016-11-10 19:30:09 +01:00
|
|
|
return json
|
|
|
|
|
2021-04-12 23:12:04 +02:00
|
|
|
def get_json_error(self, result: HttpResponse, status_code: int = 400) -> str:
|
2017-08-29 06:33:10 +02:00
|
|
|
try:
|
2020-08-07 01:09:47 +02:00
|
|
|
json = orjson.loads(result.content)
|
2020-10-09 02:17:33 +02:00
|
|
|
except orjson.JSONDecodeError: # nocoverage
|
2021-02-12 08:20:45 +01:00
|
|
|
json = {"msg": "Error parsing JSON in response!"}
|
|
|
|
self.assertEqual(result.status_code, status_code, msg=json.get("msg"))
|
2016-11-10 19:30:09 +01:00
|
|
|
self.assertEqual(json.get("result"), "error")
|
2021-02-12 08:20:45 +01:00
|
|
|
return json["msg"]
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def assert_json_error(self, result: HttpResponse, msg: str, status_code: int = 400) -> None:
|
2016-11-10 19:30:09 +01:00
|
|
|
"""
|
|
|
|
Invalid POSTs return an error status code and JSON of the form
|
|
|
|
{"result": "error", "msg": "reason"}.
|
|
|
|
"""
|
|
|
|
self.assertEqual(self.get_json_error(result, status_code=status_code), msg)
|
|
|
|
|
2021-05-17 05:39:37 +02:00
|
|
|
def assert_length(self, items: Collection[Any], count: int) -> None:
|
2017-10-06 23:28:22 +02:00
|
|
|
actual_count = len(items)
|
|
|
|
if actual_count != count: # nocoverage
|
2021-05-17 05:39:37 +02:00
|
|
|
print("\nITEMS:\n")
|
2017-10-06 23:28:22 +02:00
|
|
|
for item in items:
|
|
|
|
print(item)
|
2020-06-10 06:41:04 +02:00
|
|
|
print(f"\nexpected length: {count}\nactual length: {actual_count}")
|
2021-05-17 05:39:37 +02:00
|
|
|
raise AssertionError(f"{str(type(items))} is of unexpected size!")
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def assert_json_error_contains(
|
|
|
|
self, result: HttpResponse, msg_substring: str, status_code: int = 400
|
|
|
|
) -> None:
|
2016-11-10 19:30:09 +01:00
|
|
|
self.assertIn(msg_substring, self.get_json_error(result, status_code=status_code))
|
|
|
|
|
2018-05-11 01:40:45 +02:00
|
|
|
def assert_in_response(self, substring: str, response: HttpResponse) -> None:
|
2021-08-02 23:20:39 +02:00
|
|
|
self.assertIn(substring, response.content.decode())
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def assert_in_success_response(self, substrings: List[str], response: HttpResponse) -> None:
|
2016-11-19 21:54:00 +01:00
|
|
|
self.assertEqual(response.status_code, 200)
|
2021-08-02 23:20:39 +02:00
|
|
|
decoded = response.content.decode()
|
2016-11-19 21:54:00 +01:00
|
|
|
for substring in substrings:
|
|
|
|
self.assertIn(substring, decoded)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def assert_not_in_success_response(self, substrings: List[str], response: HttpResponse) -> None:
|
2017-04-10 12:35:56 +02:00
|
|
|
self.assertEqual(response.status_code, 200)
|
2021-08-02 23:20:39 +02:00
|
|
|
decoded = response.content.decode()
|
2017-04-10 12:35:56 +02:00
|
|
|
for substring in substrings:
|
|
|
|
self.assertNotIn(substring, decoded)
|
|
|
|
|
2019-05-26 22:12:46 +02:00
|
|
|
def assert_logged_in_user_id(self, user_id: Optional[int]) -> None:
|
|
|
|
"""
|
|
|
|
Verifies the user currently logged in for the test client has the provided user_id.
|
|
|
|
Pass None to verify no user is logged in.
|
|
|
|
"""
|
|
|
|
self.assertEqual(get_session_dict_user(self.client.session), user_id)
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
def webhook_fixture_data(self, type: str, action: str, file_type: str = "json") -> str:
|
2017-11-04 18:03:59 +01:00
|
|
|
fn = os.path.join(
|
|
|
|
os.path.dirname(__file__),
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
f"../webhooks/{type}/fixtures/{action}.{file_type}",
|
2017-11-04 18:03:59 +01:00
|
|
|
)
|
2020-10-24 09:33:54 +02:00
|
|
|
with open(fn) as f:
|
|
|
|
return f.read()
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
def fixture_file_name(self, file_name: str, type: str = "") -> str:
|
2019-03-26 12:46:54 +01:00
|
|
|
return os.path.join(
|
2018-04-20 03:57:21 +02:00
|
|
|
os.path.dirname(__file__),
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
f"../tests/fixtures/{type}/{file_name}",
|
2018-04-20 03:57:21 +02:00
|
|
|
)
|
2019-03-26 12:46:54 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
def fixture_data(self, file_name: str, type: str = "") -> str:
|
2019-03-26 12:46:54 +01:00
|
|
|
fn = self.fixture_file_name(file_name, type)
|
2020-10-24 09:33:54 +02:00
|
|
|
with open(fn) as f:
|
|
|
|
return f.read()
|
2018-04-20 03:57:21 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def make_stream(
|
|
|
|
self,
|
|
|
|
stream_name: str,
|
|
|
|
realm: Optional[Realm] = None,
|
|
|
|
invite_only: bool = False,
|
|
|
|
is_web_public: bool = False,
|
|
|
|
history_public_to_subscribers: Optional[bool] = None,
|
|
|
|
) -> Stream:
|
2016-11-10 19:30:09 +01:00
|
|
|
if realm is None:
|
2021-02-12 08:20:45 +01:00
|
|
|
realm = get_realm("zulip")
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2018-05-03 00:07:08 +02:00
|
|
|
history_public_to_subscribers = get_default_value_for_history_public_to_subscribers(
|
2021-02-12 08:19:30 +01:00
|
|
|
realm, invite_only, history_public_to_subscribers
|
|
|
|
)
|
2018-05-02 17:36:26 +02:00
|
|
|
|
2016-11-10 19:30:09 +01:00
|
|
|
try:
|
|
|
|
stream = Stream.objects.create(
|
|
|
|
realm=realm,
|
|
|
|
name=stream_name,
|
|
|
|
invite_only=invite_only,
|
2020-07-23 20:34:38 +02:00
|
|
|
is_web_public=is_web_public,
|
2018-04-27 01:00:26 +02:00
|
|
|
history_public_to_subscribers=history_public_to_subscribers,
|
2016-11-10 19:30:09 +01:00
|
|
|
)
|
2017-03-05 09:01:49 +01:00
|
|
|
except IntegrityError: # nocoverage -- this is for bugs in the tests
|
2021-02-12 08:19:30 +01:00
|
|
|
raise Exception(
|
2021-02-12 08:20:45 +01:00
|
|
|
f"""
|
2020-06-13 08:57:35 +02:00
|
|
|
{stream_name} already exists
|
2016-11-10 19:30:09 +01:00
|
|
|
|
|
|
|
Please call make_stream with a stream name
|
2021-02-12 08:20:45 +01:00
|
|
|
that is not already in use."""
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2019-11-28 16:56:04 +01:00
|
|
|
recipient = Recipient.objects.create(type_id=stream.id, type=Recipient.STREAM)
|
|
|
|
stream.recipient = recipient
|
|
|
|
stream.save(update_fields=["recipient"])
|
2016-11-10 19:30:09 +01:00
|
|
|
return stream
|
|
|
|
|
2019-09-18 15:04:17 +02:00
|
|
|
INVALID_STREAM_ID = 999999
|
2019-10-22 07:14:46 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def get_stream_id(self, name: str, realm: Optional[Realm] = None) -> int:
|
2019-09-18 15:04:17 +02:00
|
|
|
if not realm:
|
2021-02-12 08:20:45 +01:00
|
|
|
realm = get_realm("zulip")
|
2019-09-18 15:04:17 +02:00
|
|
|
try:
|
|
|
|
stream = get_realm_stream(name, realm.id)
|
|
|
|
except Stream.DoesNotExist:
|
|
|
|
return self.INVALID_STREAM_ID
|
|
|
|
return stream.id
|
|
|
|
|
2017-08-25 06:01:29 +02:00
|
|
|
# Subscribe to a stream directly
|
2018-05-11 01:40:45 +02:00
|
|
|
def subscribe(self, user_profile: UserProfile, stream_name: str) -> Stream:
|
2020-10-13 15:16:27 +02:00
|
|
|
realm = user_profile.realm
|
2017-08-25 06:01:29 +02:00
|
|
|
try:
|
|
|
|
stream = get_stream(stream_name, user_profile.realm)
|
|
|
|
except Stream.DoesNotExist:
|
2020-10-13 15:16:27 +02:00
|
|
|
stream, from_stream_creation = create_stream_if_needed(realm, stream_name)
|
2021-04-02 18:33:28 +02:00
|
|
|
bulk_add_subscriptions(realm, [stream], [user_profile], acting_user=None)
|
2017-08-25 06:01:29 +02:00
|
|
|
return stream
|
|
|
|
|
2018-05-11 01:40:45 +02:00
|
|
|
def unsubscribe(self, user_profile: UserProfile, stream_name: str) -> None:
|
2018-03-14 00:13:21 +01:00
|
|
|
client = get_client("website")
|
2017-08-25 06:23:11 +02:00
|
|
|
stream = get_stream(stream_name, user_profile.realm)
|
2021-04-02 18:48:08 +02:00
|
|
|
bulk_remove_subscriptions([user_profile], [stream], client, acting_user=None)
|
2016-11-10 19:30:09 +01:00
|
|
|
|
|
|
|
# Subscribe to a stream by making an API request
|
2021-02-12 08:19:30 +01:00
|
|
|
def common_subscribe_to_streams(
|
|
|
|
self,
|
|
|
|
user: UserProfile,
|
|
|
|
streams: Iterable[str],
|
|
|
|
extra_post_data: Dict[str, Any] = {},
|
|
|
|
invite_only: bool = False,
|
|
|
|
is_web_public: bool = False,
|
|
|
|
allow_fail: bool = False,
|
|
|
|
**kwargs: Any,
|
|
|
|
) -> HttpResponse:
|
|
|
|
post_data = {
|
2021-02-12 08:20:45 +01:00
|
|
|
"subscriptions": orjson.dumps([{"name": stream} for stream in streams]).decode(),
|
|
|
|
"is_web_public": orjson.dumps(is_web_public).decode(),
|
|
|
|
"invite_only": orjson.dumps(invite_only).decode(),
|
2021-02-12 08:19:30 +01:00
|
|
|
}
|
2016-11-10 19:30:09 +01:00
|
|
|
post_data.update(extra_post_data)
|
2020-03-10 11:48:26 +01:00
|
|
|
result = self.api_post(user, "/api/v1/users/me/subscriptions", post_data, **kwargs)
|
2020-06-17 23:49:33 +02:00
|
|
|
if not allow_fail:
|
|
|
|
self.assert_json_success(result)
|
2016-11-10 19:30:09 +01:00
|
|
|
return result
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def check_user_subscribed_only_to_streams(self, user_name: str, streams: List[Stream]) -> None:
|
2017-11-16 22:12:31 +01:00
|
|
|
streams = sorted(streams, key=lambda x: x.name)
|
|
|
|
subscribed_streams = gather_subscriptions(self.nonreg_user(user_name))[0]
|
|
|
|
|
2021-05-17 05:41:32 +02:00
|
|
|
self.assert_length(subscribed_streams, len(streams))
|
2017-11-16 22:12:31 +01:00
|
|
|
|
|
|
|
for x, y in zip(subscribed_streams, streams):
|
|
|
|
self.assertEqual(x["name"], y.name)
|
|
|
|
|
2020-08-23 19:09:27 +02:00
|
|
|
def send_webhook_payload(
|
|
|
|
self,
|
|
|
|
user_profile: UserProfile,
|
|
|
|
url: str,
|
|
|
|
payload: Union[str, Dict[str, Any]],
|
|
|
|
**post_params: Any,
|
|
|
|
) -> Message:
|
|
|
|
"""
|
|
|
|
Send a webhook payload to the server, and verify that the
|
|
|
|
post is successful.
|
|
|
|
|
|
|
|
This is a pretty low-level function. For most use cases
|
|
|
|
see the helpers that call this function, which do additional
|
|
|
|
checks.
|
|
|
|
|
|
|
|
Occasionally tests will call this directly, for unique
|
|
|
|
situations like having multiple messages go to a stream,
|
|
|
|
where the other helper functions are a bit too rigid,
|
|
|
|
and you'll want the test itself do various assertions.
|
|
|
|
Even in those cases, you're often better to simply
|
|
|
|
call client_post and assert_json_success.
|
2020-08-23 19:30:12 +02:00
|
|
|
|
|
|
|
If the caller expects a message to be sent to a stream,
|
|
|
|
the caller should make sure the user is subscribed.
|
2020-08-23 19:09:27 +02:00
|
|
|
"""
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2019-01-28 20:57:54 +01:00
|
|
|
prior_msg = self.get_last_message()
|
|
|
|
|
2016-11-10 19:30:09 +01:00
|
|
|
result = self.client_post(url, payload, **post_params)
|
|
|
|
self.assert_json_success(result)
|
|
|
|
|
|
|
|
# Check the correct message was sent
|
|
|
|
msg = self.get_last_message()
|
2019-01-28 20:57:54 +01:00
|
|
|
|
|
|
|
if msg.id == prior_msg.id:
|
2021-06-30 09:46:14 +02:00
|
|
|
raise EmptyResponseError(
|
2021-02-12 08:20:45 +01:00
|
|
|
"""
|
2019-01-28 20:57:54 +01:00
|
|
|
Your test code called an endpoint that did
|
|
|
|
not write any new messages. It is probably
|
|
|
|
broken (but still returns 200 due to exception
|
|
|
|
handling).
|
2020-08-23 19:30:12 +02:00
|
|
|
|
|
|
|
One possible gotcha is that you forgot to
|
|
|
|
subscribe the test user to the stream that
|
|
|
|
the webhook sends to.
|
2021-02-12 08:20:45 +01:00
|
|
|
"""
|
2021-02-12 08:19:30 +01:00
|
|
|
) # nocoverage
|
2019-01-28 20:57:54 +01:00
|
|
|
|
2017-08-25 06:37:47 +02:00
|
|
|
self.assertEqual(msg.sender.email, user_profile.email)
|
2016-11-10 19:30:09 +01:00
|
|
|
|
|
|
|
return msg
|
|
|
|
|
2017-11-27 05:27:04 +01:00
|
|
|
def get_last_message(self) -> Message:
|
2021-02-12 08:20:45 +01:00
|
|
|
return Message.objects.latest("id")
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2017-11-27 05:27:04 +01:00
|
|
|
def get_second_to_last_message(self) -> Message:
|
2021-02-12 08:20:45 +01:00
|
|
|
return Message.objects.all().order_by("-id")[1]
|
2016-11-10 19:30:09 +01:00
|
|
|
|
|
|
|
@contextmanager
|
2017-11-27 05:27:04 +01:00
|
|
|
def simulated_markdown_failure(self) -> Iterator[None]:
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
2016-11-10 19:30:09 +01:00
|
|
|
This raises a failure inside of the try/except block of
|
2020-06-28 16:40:18 +02:00
|
|
|
markdown.__init__.do_convert.
|
2021-02-12 08:19:30 +01:00
|
|
|
"""
|
|
|
|
with self.settings(ERROR_BOT=None), mock.patch(
|
2021-02-12 08:20:45 +01:00
|
|
|
"zerver.lib.markdown.timeout", side_effect=subprocess.CalledProcessError(1, [])
|
2021-07-26 18:35:51 +02:00
|
|
|
), self.assertLogs(
|
|
|
|
level="ERROR"
|
|
|
|
): # For markdown_logger.exception
|
2016-11-10 19:30:09 +01:00
|
|
|
yield
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def create_default_device(
|
|
|
|
self, user_profile: UserProfile, number: str = "+12125550100"
|
|
|
|
) -> None:
|
|
|
|
phone_device = PhoneDevice(
|
|
|
|
user=user_profile,
|
2021-02-12 08:20:45 +01:00
|
|
|
name="default",
|
2021-02-12 08:19:30 +01:00
|
|
|
confirmed=True,
|
|
|
|
number=number,
|
2021-02-12 08:20:45 +01:00
|
|
|
key="abcd",
|
|
|
|
method="sms",
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2017-07-13 13:42:57 +02:00
|
|
|
phone_device.save()
|
|
|
|
|
2019-04-04 12:03:54 +02:00
|
|
|
def rm_tree(self, path: str) -> None:
|
|
|
|
if os.path.exists(path):
|
|
|
|
shutil.rmtree(path)
|
|
|
|
|
2019-04-04 12:05:54 +02:00
|
|
|
def make_import_output_dir(self, exported_from: str) -> str:
|
2021-02-12 08:19:30 +01:00
|
|
|
output_dir = tempfile.mkdtemp(
|
|
|
|
dir=settings.TEST_WORKER_DIR, prefix="test-" + exported_from + "-import-"
|
|
|
|
)
|
2019-04-04 12:05:54 +02:00
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
return output_dir
|
|
|
|
|
2019-05-21 12:21:32 +02:00
|
|
|
def get_set(self, data: List[Dict[str, Any]], field: str) -> Set[str]:
|
2020-04-09 21:51:58 +02:00
|
|
|
values = {r[field] for r in data}
|
2019-05-21 12:21:32 +02:00
|
|
|
return values
|
|
|
|
|
2019-05-21 12:29:09 +02:00
|
|
|
def find_by_id(self, data: List[Dict[str, Any]], db_id: int) -> Dict[str, Any]:
|
2021-02-12 08:20:45 +01:00
|
|
|
return [r for r in data if r["id"] == db_id][0]
|
2019-05-21 12:29:09 +02:00
|
|
|
|
2019-10-16 18:01:38 +02:00
|
|
|
def init_default_ldap_database(self) -> None:
|
|
|
|
"""
|
|
|
|
Takes care of the mock_ldap setup, loads
|
|
|
|
a directory from zerver/tests/fixtures/ldap/directory.json with various entries
|
|
|
|
to be used by tests.
|
|
|
|
If a test wants to specify its own directory, it can just replace
|
|
|
|
self.mock_ldap.directory with its own content, but in most cases it should be
|
|
|
|
enough to use change_user_attr to make simple modifications to the pre-loaded
|
|
|
|
directory. If new user entries are needed to test for some additional unusual
|
|
|
|
scenario, it's most likely best to add that to directory.json.
|
|
|
|
"""
|
2020-08-07 01:09:47 +02:00
|
|
|
directory = orjson.loads(self.fixture_data("directory.json", type="ldap"))
|
2019-10-16 18:01:38 +02:00
|
|
|
|
|
|
|
for dn, attrs in directory.items():
|
2021-02-12 08:20:45 +01:00
|
|
|
if "uid" in attrs:
|
2020-10-23 02:43:28 +02:00
|
|
|
# Generate a password for the LDAP account:
|
2021-02-12 08:20:45 +01:00
|
|
|
attrs["userPassword"] = [self.ldap_password(attrs["uid"][0])]
|
2020-02-19 19:40:49 +01:00
|
|
|
|
|
|
|
# Load binary attributes. If in "directory", an attribute as its value
|
|
|
|
# has a string starting with "file:", the rest of the string is assumed
|
|
|
|
# to be a path to the file from which binary data should be loaded,
|
2020-10-23 02:43:28 +02:00
|
|
|
# as the actual value of the attribute in LDAP.
|
2019-10-16 18:01:38 +02:00
|
|
|
for attr, value in attrs.items():
|
|
|
|
if isinstance(value, str) and value.startswith("file:"):
|
2021-02-12 08:20:45 +01:00
|
|
|
with open(value[5:], "rb") as f:
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
attrs[attr] = [f.read()]
|
2019-10-16 18:01:38 +02:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
ldap_patcher = mock.patch("django_auth_ldap.config.ldap.initialize")
|
2019-10-16 18:01:38 +02:00
|
|
|
self.mock_initialize = ldap_patcher.start()
|
|
|
|
self.mock_ldap = MockLDAP(directory)
|
|
|
|
self.mock_initialize.return_value = self.mock_ldap
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def change_ldap_user_attr(
|
|
|
|
self, username: str, attr_name: str, attr_value: Union[str, bytes], binary: bool = False
|
|
|
|
) -> None:
|
2019-10-16 18:01:38 +02:00
|
|
|
"""
|
|
|
|
Method for changing the value of an attribute of a user entry in the mock
|
|
|
|
directory. Use option binary=True if you want binary data to be loaded
|
|
|
|
into the attribute from a file specified at attr_value. This changes
|
|
|
|
the attribute only for the specific test function that calls this method,
|
|
|
|
and is isolated from other tests.
|
|
|
|
"""
|
2020-06-09 00:25:09 +02:00
|
|
|
dn = f"uid={username},ou=users,dc=zulip,dc=com"
|
2019-10-16 18:01:38 +02:00
|
|
|
if binary:
|
|
|
|
with open(attr_value, "rb") as f:
|
|
|
|
# attr_value should be a path to the file with the binary data
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
data: Union[str, bytes] = f.read()
|
2019-10-16 18:01:38 +02:00
|
|
|
else:
|
|
|
|
data = attr_value
|
|
|
|
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
self.mock_ldap.directory[dn][attr_name] = [data]
|
2019-10-16 18:01:38 +02:00
|
|
|
|
2020-09-22 18:18:45 +02:00
|
|
|
def remove_ldap_user_attr(self, username: str, attr_name: str) -> None:
|
|
|
|
"""
|
|
|
|
Method for removing the value of an attribute of a user entry in the mock
|
|
|
|
directory. This changes the attribute only for the specific test function
|
|
|
|
that calls this method, and is isolated from other tests.
|
|
|
|
"""
|
|
|
|
dn = f"uid={username},ou=users,dc=zulip,dc=com"
|
|
|
|
self.mock_ldap.directory[dn].pop(attr_name, None)
|
|
|
|
|
2019-10-18 18:25:51 +02:00
|
|
|
def ldap_username(self, username: str) -> str:
|
|
|
|
"""
|
2020-10-23 02:43:28 +02:00
|
|
|
Maps Zulip username to the name of the corresponding LDAP user
|
2019-10-18 18:25:51 +02:00
|
|
|
in our test directory at zerver/tests/fixtures/ldap/directory.json,
|
2020-10-23 02:43:28 +02:00
|
|
|
if the LDAP user exists.
|
2019-10-18 18:25:51 +02:00
|
|
|
"""
|
|
|
|
return self.example_user_ldap_username_map[username]
|
|
|
|
|
2020-02-19 19:40:49 +01:00
|
|
|
def ldap_password(self, uid: str) -> str:
|
2020-06-09 00:25:09 +02:00
|
|
|
return f"{uid}_ldap_password"
|
2019-10-18 18:25:51 +02:00
|
|
|
|
2021-01-26 04:20:36 +01:00
|
|
|
def email_display_from(self, email_message: EmailMessage) -> str:
|
|
|
|
"""
|
|
|
|
Returns the email address that will show in email clients as the
|
|
|
|
"From" field.
|
|
|
|
"""
|
|
|
|
# The extra_headers field may contain a "From" which is used
|
|
|
|
# for display in email clients, and appears in the RFC822
|
|
|
|
# header as `From`. The `.from_email` accessor is the
|
|
|
|
# "envelope from" address, used by mail transfer agents if
|
|
|
|
# the email bounces.
|
|
|
|
return email_message.extra_headers.get("From", email_message.from_email)
|
|
|
|
|
|
|
|
def email_envelope_from(self, email_message: EmailMessage) -> str:
|
|
|
|
"""
|
|
|
|
Returns the email address that will be used if the email bounces.
|
|
|
|
"""
|
|
|
|
# See email_display_from, above.
|
|
|
|
return email_message.from_email
|
|
|
|
|
2021-04-06 18:07:33 +02:00
|
|
|
def check_has_permission_policies(
|
2021-04-13 16:01:40 +02:00
|
|
|
self, policy: str, validation_func: Callable[[UserProfile], bool]
|
2021-04-06 18:07:33 +02:00
|
|
|
) -> None:
|
|
|
|
|
2021-04-13 16:01:40 +02:00
|
|
|
realm = get_realm("zulip")
|
|
|
|
admin_user = self.example_user("iago")
|
|
|
|
moderator_user = self.example_user("shiva")
|
|
|
|
member_user = self.example_user("hamlet")
|
|
|
|
new_member_user = self.example_user("othello")
|
|
|
|
guest_user = self.example_user("polonius")
|
|
|
|
|
|
|
|
do_set_realm_property(realm, "waiting_period_threshold", 1000, acting_user=None)
|
|
|
|
new_member_user.date_joined = timezone_now() - timedelta(
|
|
|
|
days=(realm.waiting_period_threshold - 1)
|
2021-04-06 18:07:33 +02:00
|
|
|
)
|
2021-04-13 16:01:40 +02:00
|
|
|
new_member_user.save()
|
2021-04-06 18:07:33 +02:00
|
|
|
|
2021-04-13 16:01:40 +02:00
|
|
|
member_user.date_joined = timezone_now() - timedelta(
|
|
|
|
days=(realm.waiting_period_threshold + 1)
|
2021-04-06 18:07:33 +02:00
|
|
|
)
|
2021-04-13 16:01:40 +02:00
|
|
|
member_user.save()
|
|
|
|
|
|
|
|
do_set_realm_property(realm, policy, Realm.POLICY_ADMINS_ONLY, acting_user=None)
|
|
|
|
self.assertTrue(validation_func(admin_user))
|
|
|
|
self.assertFalse(validation_func(moderator_user))
|
|
|
|
self.assertFalse(validation_func(member_user))
|
|
|
|
self.assertFalse(validation_func(new_member_user))
|
|
|
|
self.assertFalse(validation_func(guest_user))
|
|
|
|
|
|
|
|
do_set_realm_property(realm, policy, Realm.POLICY_MODERATORS_ONLY, acting_user=None)
|
|
|
|
self.assertTrue(validation_func(admin_user))
|
|
|
|
self.assertTrue(validation_func(moderator_user))
|
|
|
|
self.assertFalse(validation_func(member_user))
|
|
|
|
self.assertFalse(validation_func(new_member_user))
|
|
|
|
self.assertFalse(validation_func(guest_user))
|
|
|
|
|
|
|
|
do_set_realm_property(realm, policy, Realm.POLICY_FULL_MEMBERS_ONLY, acting_user=None)
|
|
|
|
self.assertTrue(validation_func(admin_user))
|
|
|
|
self.assertTrue(validation_func(moderator_user))
|
|
|
|
self.assertTrue(validation_func(member_user))
|
|
|
|
self.assertFalse(validation_func(new_member_user))
|
|
|
|
self.assertFalse(validation_func(guest_user))
|
|
|
|
|
|
|
|
do_set_realm_property(realm, policy, Realm.POLICY_MEMBERS_ONLY, acting_user=None)
|
|
|
|
self.assertTrue(validation_func(admin_user))
|
|
|
|
self.assertTrue(validation_func(moderator_user))
|
|
|
|
self.assertTrue(validation_func(member_user))
|
|
|
|
self.assertTrue(validation_func(new_member_user))
|
|
|
|
self.assertFalse(validation_func(guest_user))
|
2021-04-06 18:07:33 +02:00
|
|
|
|
2021-06-03 12:20:31 +02:00
|
|
|
def subscribe_realm_to_manual_license_management_plan(
|
|
|
|
self, realm: Realm, licenses: int, licenses_at_next_renewal: int, billing_schedule: int
|
|
|
|
) -> Tuple[CustomerPlan, LicenseLedger]:
|
|
|
|
customer, _ = Customer.objects.get_or_create(realm=realm)
|
|
|
|
plan = CustomerPlan.objects.create(
|
|
|
|
customer=customer,
|
|
|
|
automanage_licenses=False,
|
|
|
|
billing_cycle_anchor=timezone_now(),
|
|
|
|
billing_schedule=billing_schedule,
|
|
|
|
tier=CustomerPlan.STANDARD,
|
|
|
|
)
|
|
|
|
ledger = LicenseLedger.objects.create(
|
|
|
|
plan=plan,
|
|
|
|
is_renewal=True,
|
|
|
|
event_time=timezone_now(),
|
|
|
|
licenses=licenses,
|
|
|
|
licenses_at_next_renewal=licenses_at_next_renewal,
|
|
|
|
)
|
|
|
|
realm.plan_type = Realm.STANDARD
|
|
|
|
realm.save(update_fields=["plan_type"])
|
|
|
|
return plan, ledger
|
|
|
|
|
|
|
|
def subscribe_realm_to_monthly_plan_on_manual_license_management(
|
|
|
|
self, realm: Realm, licenses: int, licenses_at_next_renewal: int
|
|
|
|
) -> Tuple[CustomerPlan, LicenseLedger]:
|
|
|
|
return self.subscribe_realm_to_manual_license_management_plan(
|
|
|
|
realm, licenses, licenses_at_next_renewal, CustomerPlan.MONTHLY
|
|
|
|
)
|
|
|
|
|
2021-05-08 08:25:06 +02:00
|
|
|
@contextmanager
|
2021-05-27 15:53:22 +02:00
|
|
|
def tornado_redirected_to_list(
|
2021-05-28 07:27:50 +02:00
|
|
|
self, lst: List[Mapping[str, Any]], expected_num_events: int
|
2021-05-27 15:53:22 +02:00
|
|
|
) -> Iterator[None]:
|
2021-05-28 07:59:38 +02:00
|
|
|
lst.clear()
|
2021-06-03 07:44:56 +02:00
|
|
|
|
2021-05-08 08:25:06 +02:00
|
|
|
# process_notification takes a single parameter called 'notice'.
|
|
|
|
# lst.append takes a single argument called 'object'.
|
|
|
|
# Some code might call process_notification using keyword arguments,
|
|
|
|
# so mypy doesn't allow assigning lst.append to process_notification
|
|
|
|
# So explicitly change parameter name to 'notice' to work around this problem
|
2021-07-16 22:10:57 +02:00
|
|
|
with mock.patch(
|
|
|
|
"zerver.tornado.django_api.process_notification", lambda notice: lst.append(notice)
|
|
|
|
):
|
|
|
|
# Some `send_event` calls need to be executed only after the current transaction
|
|
|
|
# commits (using `on_commit` hooks). Because the transaction in Django tests never
|
|
|
|
# commits (rather, gets rolled back after the test completes), such events would
|
|
|
|
# never be sent in tests, and we would be unable to verify them. Hence, we use
|
|
|
|
# this helper to make sure the `send_event` calls actually run.
|
|
|
|
with self.captureOnCommitCallbacks(execute=True):
|
|
|
|
yield
|
2021-05-08 08:25:06 +02:00
|
|
|
|
2021-05-27 15:53:22 +02:00
|
|
|
self.assert_length(lst, expected_num_events)
|
|
|
|
|
2021-06-23 10:44:34 +02:00
|
|
|
def create_user_notifications_data_object(
|
|
|
|
self, *, user_id: int, **kwargs: Any
|
|
|
|
) -> UserMessageNotificationsData:
|
2021-06-15 14:30:51 +02:00
|
|
|
return UserMessageNotificationsData(
|
2021-06-23 10:44:34 +02:00
|
|
|
user_id=user_id,
|
2021-06-15 14:30:51 +02:00
|
|
|
online_push_enabled=kwargs.get("online_push_enabled", False),
|
notifications: Calculate PMs/mentions settings like other settings.
Previously, we checked for the `enable_offline_email_notifications` and
`enable_offline_push_notifications` settings (which determine whether the
user will receive notifications for PMs and mentions) just before sending
notifications. This has a few problem:
1. We do not have access to all the user settings in the notification
handlers (`handle_missedmessage_emails` and `handle_push_notifications`),
and therefore, we cannot correctly determine whether the notification should
be sent. Checks like the following which existed previously, will, for
example, incorrectly not send notifications even when stream email
notifications are enabled-
```
if not receives_offline_email_notifications(user_profile):
return
```
With this commit, we simply do not enqueue notifications if the "offline"
settings are disabled, which fixes that bug.
Additionally, this also fixes a bug with the "online push notifications"
feature, which was, if someone were to:
* turn off notifications for PMs and mentions (`enable_offline_push_notifications`)
* turn on stream push notifications (`enable_stream_push_notifications`)
* turn on "online push" (`enable_online_push_notifications`)
then, they would still receive notifications for PMs when online.
This isn't how the "online push enabled" feature is supposed to work;
it should only act as a wrapper around the other notification settings.
The buggy code was this in `handle_push_notifications`:
```
if not (
receives_offline_push_notifications(user_profile)
or receives_online_push_notifications(user_profile)
):
return
// send notifications
```
This commit removes that code, and extends our `notification_data.py` logic
to cover this case, along with tests.
2. The name for these settings is slightly misleading. They essentially
talk about "what to send notifications for" (PMs and mentions), and not
"when to send notifications" (offline). This commit improves this condition
by restricting the use of this term only to the database field, and using
clearer names everywhere else. This distinction will be important to have
non-confusing code when we implement multiple options for notifications
in the future as dropdown (never/when offline/when offline or online, etc).
3. We should ideally re-check all notification settings just before the
notifications are sent. This is especially important for email notifications,
which may be sent after a long time after the message was sent. We will
in the future add code to thoroughly re-check settings before sending
notifications in a clean manner, but temporarily not re-checking isn't
a terrible scenario either.
2021-07-14 15:34:01 +02:00
|
|
|
pm_email_notify=kwargs.get("pm_email_notify", False),
|
|
|
|
pm_push_notify=kwargs.get("pm_push_notify", False),
|
|
|
|
mention_email_notify=kwargs.get("mention_email_notify", False),
|
|
|
|
mention_push_notify=kwargs.get("mention_push_notify", False),
|
2021-08-10 15:41:41 +02:00
|
|
|
wildcard_mention_email_notify=kwargs.get("wildcard_mention_email_notify", False),
|
|
|
|
wildcard_mention_push_notify=kwargs.get("wildcard_mention_push_notify", False),
|
2021-06-15 14:30:51 +02:00
|
|
|
stream_email_notify=kwargs.get("stream_email_notify", False),
|
|
|
|
stream_push_notify=kwargs.get("stream_push_notify", False),
|
|
|
|
sender_is_muted=kwargs.get("sender_is_muted", False),
|
|
|
|
)
|
|
|
|
|
maybe_enqueue_notifications: Take in notification_data dataclass.
* Modify `maybe_enqueue_notifications` to take in an instance of the
dataclass introduced in 951b49c048ba3464e74ad7965da3453fe36d0a96.
* The `check_notify` tests tested the "when to notify" logic in a way
which involved `maybe_enqueue_notifications`. To simplify things, we've
earlier extracted this logic in 8182632d7e9f8490b9b9295e01b5912dcf173fd5.
So, we just kill off the `check_notify` test, and keep only those parts
which verify the queueing and return value behavior of that funtion.
* We retain the the missedmessage_hook and message
message_edit_notifications since they are more integration-style.
* There's a slightly subtle change with the missedmessage_hook tests.
Before this commit, we short-circuited the hook if the sender was muted
(5a642cea115be159175d1189f83ba25d2c5c7632).
With this commit, we delegate the check to our dataclass methods.
So, `maybe_enqueue_notifications` will be called even if the sender was
muted, and the test needs to be updated.
* In our test helper `get_maybe_enqueue_notifications_parameters` which
generates default values for testing `maybe_enqueue_notifications` calls,
we keep `message_id`, `sender_id`, and `user_id` as required arguments,
so that the tests are super-clear and avoid accidental false positives.
* Because `do_update_embedded_data` also sends `update_message` events,
we deal with that case with some hacky code for now. See the comment
there.
This mostly completes the extraction of the "when to notify" logic into
our new `notification_data` module.
2021-06-23 14:12:32 +02:00
|
|
|
def get_maybe_enqueue_notifications_parameters(
|
2021-06-25 13:58:53 +02:00
|
|
|
self, *, message_id: int, user_id: int, acting_user_id: int, **kwargs: Any
|
maybe_enqueue_notifications: Take in notification_data dataclass.
* Modify `maybe_enqueue_notifications` to take in an instance of the
dataclass introduced in 951b49c048ba3464e74ad7965da3453fe36d0a96.
* The `check_notify` tests tested the "when to notify" logic in a way
which involved `maybe_enqueue_notifications`. To simplify things, we've
earlier extracted this logic in 8182632d7e9f8490b9b9295e01b5912dcf173fd5.
So, we just kill off the `check_notify` test, and keep only those parts
which verify the queueing and return value behavior of that funtion.
* We retain the the missedmessage_hook and message
message_edit_notifications since they are more integration-style.
* There's a slightly subtle change with the missedmessage_hook tests.
Before this commit, we short-circuited the hook if the sender was muted
(5a642cea115be159175d1189f83ba25d2c5c7632).
With this commit, we delegate the check to our dataclass methods.
So, `maybe_enqueue_notifications` will be called even if the sender was
muted, and the test needs to be updated.
* In our test helper `get_maybe_enqueue_notifications_parameters` which
generates default values for testing `maybe_enqueue_notifications` calls,
we keep `message_id`, `sender_id`, and `user_id` as required arguments,
so that the tests are super-clear and avoid accidental false positives.
* Because `do_update_embedded_data` also sends `update_message` events,
we deal with that case with some hacky code for now. See the comment
there.
This mostly completes the extraction of the "when to notify" logic into
our new `notification_data` module.
2021-06-23 14:12:32 +02:00
|
|
|
) -> Dict[str, Any]:
|
2021-06-08 15:00:11 +02:00
|
|
|
"""
|
|
|
|
Returns a dictionary with the passed parameters, after filling up the
|
|
|
|
missing data with default values, for testing what was passed to the
|
|
|
|
`maybe_enqueue_notifications` method.
|
|
|
|
"""
|
maybe_enqueue_notifications: Take in notification_data dataclass.
* Modify `maybe_enqueue_notifications` to take in an instance of the
dataclass introduced in 951b49c048ba3464e74ad7965da3453fe36d0a96.
* The `check_notify` tests tested the "when to notify" logic in a way
which involved `maybe_enqueue_notifications`. To simplify things, we've
earlier extracted this logic in 8182632d7e9f8490b9b9295e01b5912dcf173fd5.
So, we just kill off the `check_notify` test, and keep only those parts
which verify the queueing and return value behavior of that funtion.
* We retain the the missedmessage_hook and message
message_edit_notifications since they are more integration-style.
* There's a slightly subtle change with the missedmessage_hook tests.
Before this commit, we short-circuited the hook if the sender was muted
(5a642cea115be159175d1189f83ba25d2c5c7632).
With this commit, we delegate the check to our dataclass methods.
So, `maybe_enqueue_notifications` will be called even if the sender was
muted, and the test needs to be updated.
* In our test helper `get_maybe_enqueue_notifications_parameters` which
generates default values for testing `maybe_enqueue_notifications` calls,
we keep `message_id`, `sender_id`, and `user_id` as required arguments,
so that the tests are super-clear and avoid accidental false positives.
* Because `do_update_embedded_data` also sends `update_message` events,
we deal with that case with some hacky code for now. See the comment
there.
This mostly completes the extraction of the "when to notify" logic into
our new `notification_data` module.
2021-06-23 14:12:32 +02:00
|
|
|
user_notifications_data = self.create_user_notifications_data_object(
|
|
|
|
user_id=user_id, **kwargs
|
|
|
|
)
|
|
|
|
return dict(
|
2021-06-25 14:08:41 +02:00
|
|
|
user_notifications_data=user_notifications_data,
|
maybe_enqueue_notifications: Take in notification_data dataclass.
* Modify `maybe_enqueue_notifications` to take in an instance of the
dataclass introduced in 951b49c048ba3464e74ad7965da3453fe36d0a96.
* The `check_notify` tests tested the "when to notify" logic in a way
which involved `maybe_enqueue_notifications`. To simplify things, we've
earlier extracted this logic in 8182632d7e9f8490b9b9295e01b5912dcf173fd5.
So, we just kill off the `check_notify` test, and keep only those parts
which verify the queueing and return value behavior of that funtion.
* We retain the the missedmessage_hook and message
message_edit_notifications since they are more integration-style.
* There's a slightly subtle change with the missedmessage_hook tests.
Before this commit, we short-circuited the hook if the sender was muted
(5a642cea115be159175d1189f83ba25d2c5c7632).
With this commit, we delegate the check to our dataclass methods.
So, `maybe_enqueue_notifications` will be called even if the sender was
muted, and the test needs to be updated.
* In our test helper `get_maybe_enqueue_notifications_parameters` which
generates default values for testing `maybe_enqueue_notifications` calls,
we keep `message_id`, `sender_id`, and `user_id` as required arguments,
so that the tests are super-clear and avoid accidental false positives.
* Because `do_update_embedded_data` also sends `update_message` events,
we deal with that case with some hacky code for now. See the comment
there.
This mostly completes the extraction of the "when to notify" logic into
our new `notification_data` module.
2021-06-23 14:12:32 +02:00
|
|
|
message_id=message_id,
|
2021-06-25 13:58:53 +02:00
|
|
|
acting_user_id=acting_user_id,
|
2021-07-01 17:40:16 +02:00
|
|
|
mentioned_user_group_id=kwargs.get("mentioned_user_group_id", None),
|
maybe_enqueue_notifications: Take in notification_data dataclass.
* Modify `maybe_enqueue_notifications` to take in an instance of the
dataclass introduced in 951b49c048ba3464e74ad7965da3453fe36d0a96.
* The `check_notify` tests tested the "when to notify" logic in a way
which involved `maybe_enqueue_notifications`. To simplify things, we've
earlier extracted this logic in 8182632d7e9f8490b9b9295e01b5912dcf173fd5.
So, we just kill off the `check_notify` test, and keep only those parts
which verify the queueing and return value behavior of that funtion.
* We retain the the missedmessage_hook and message
message_edit_notifications since they are more integration-style.
* There's a slightly subtle change with the missedmessage_hook tests.
Before this commit, we short-circuited the hook if the sender was muted
(5a642cea115be159175d1189f83ba25d2c5c7632).
With this commit, we delegate the check to our dataclass methods.
So, `maybe_enqueue_notifications` will be called even if the sender was
muted, and the test needs to be updated.
* In our test helper `get_maybe_enqueue_notifications_parameters` which
generates default values for testing `maybe_enqueue_notifications` calls,
we keep `message_id`, `sender_id`, and `user_id` as required arguments,
so that the tests are super-clear and avoid accidental false positives.
* Because `do_update_embedded_data` also sends `update_message` events,
we deal with that case with some hacky code for now. See the comment
there.
This mostly completes the extraction of the "when to notify" logic into
our new `notification_data` module.
2021-06-23 14:12:32 +02:00
|
|
|
idle=kwargs.get("idle", True),
|
|
|
|
already_notified=kwargs.get(
|
|
|
|
"already_notified", {"email_notified": False, "push_notified": False}
|
|
|
|
),
|
2021-06-08 15:00:11 +02:00
|
|
|
)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2016-11-10 19:30:09 +01:00
|
|
|
class WebhookTestCase(ZulipTestCase):
|
2021-06-26 10:07:54 +02:00
|
|
|
"""Shared test class for all incoming webhooks tests.
|
|
|
|
|
|
|
|
Used by configuring the below class attributes, and calling
|
|
|
|
send_and_test_message in individual tests.
|
|
|
|
|
|
|
|
* Tests can override build_webhook_url if the webhook requires a
|
|
|
|
different URL format.
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2021-06-26 10:07:54 +02:00
|
|
|
* Tests can override get_body for cases where there is no
|
|
|
|
available fixture file.
|
|
|
|
|
|
|
|
* Tests should specify WEBHOOK_DIR_NAME to enforce that all event
|
|
|
|
types are declared in the @webhook_view decorator. This is
|
|
|
|
important for ensuring we document all fully supported event types.
|
2016-11-10 19:30:09 +01:00
|
|
|
"""
|
2021-02-12 08:19:30 +01:00
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
STREAM_NAME: Optional[str] = None
|
2021-02-12 08:20:45 +01:00
|
|
|
TEST_USER_EMAIL = "webhook-bot@zulip.com"
|
2020-07-05 02:14:06 +02:00
|
|
|
URL_TEMPLATE: str
|
2021-06-26 09:18:33 +02:00
|
|
|
WEBHOOK_DIR_NAME: Optional[str] = None
|
2021-06-26 10:07:54 +02:00
|
|
|
# This last parameter is a workaround to handle webhooks that do not
|
|
|
|
# name the main function api_{WEBHOOK_DIR_NAME}_webhook.
|
|
|
|
VIEW_FUNCTION_NAME: Optional[str] = None
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2017-08-25 06:37:47 +02:00
|
|
|
@property
|
2017-11-27 05:27:04 +01:00
|
|
|
def test_user(self) -> UserProfile:
|
2017-08-25 08:23:13 +02:00
|
|
|
return get_user(self.TEST_USER_EMAIL, get_realm("zulip"))
|
2017-08-25 06:37:47 +02:00
|
|
|
|
2017-11-27 05:27:04 +01:00
|
|
|
def setUp(self) -> None:
|
2019-10-19 20:47:00 +02:00
|
|
|
super().setUp()
|
2016-11-10 19:30:09 +01:00
|
|
|
self.url = self.build_webhook_url()
|
|
|
|
|
2021-07-17 18:05:15 +02:00
|
|
|
if self.WEBHOOK_DIR_NAME is not None:
|
2021-06-26 10:07:54 +02:00
|
|
|
# If VIEW_FUNCTION_NAME is explicitly specified and
|
|
|
|
# WEBHOOK_DIR_NAME is not None, an exception will be
|
|
|
|
# raised when a test triggers events that are not
|
|
|
|
# explicitly specified via the event_types parameter to
|
|
|
|
# the @webhook_view decorator.
|
2021-07-17 18:05:15 +02:00
|
|
|
if self.VIEW_FUNCTION_NAME is None:
|
|
|
|
function = import_string(
|
|
|
|
f"zerver.webhooks.{self.WEBHOOK_DIR_NAME}.view.api_{self.WEBHOOK_DIR_NAME}_webhook"
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
function = import_string(
|
|
|
|
f"zerver.webhooks.{self.WEBHOOK_DIR_NAME}.view.{self.VIEW_FUNCTION_NAME}"
|
|
|
|
)
|
2021-06-26 10:07:54 +02:00
|
|
|
all_event_types = None
|
|
|
|
|
|
|
|
if hasattr(function, "_all_event_types"):
|
|
|
|
all_event_types = function._all_event_types
|
|
|
|
|
|
|
|
if all_event_types is None:
|
|
|
|
return # nocoverage
|
|
|
|
|
|
|
|
def side_effect(*args: Any, **kwargs: Any) -> None:
|
|
|
|
complete_event_type = (
|
|
|
|
kwargs.get("complete_event_type")
|
|
|
|
if len(args) < 5
|
|
|
|
else args[4] # complete_event_type is the argument at index 4
|
|
|
|
)
|
|
|
|
if (
|
|
|
|
complete_event_type is not None
|
|
|
|
and all_event_types is not None
|
|
|
|
and complete_event_type not in all_event_types
|
|
|
|
):
|
|
|
|
raise Exception(
|
|
|
|
f"""
|
|
|
|
Error: This test triggered a message using the event "{complete_event_type}", which was not properly
|
|
|
|
registered via the @webhook_view(..., event_types=[...]). These registrations are important for Zulip
|
|
|
|
self-documenting the supported event types for this integration.
|
|
|
|
|
|
|
|
You can fix this by adding "{complete_event_type}" to ALL_EVENT_TYPES for this webhook.
|
|
|
|
""".strip()
|
|
|
|
)
|
|
|
|
check_send_webhook_message(*args, **kwargs)
|
|
|
|
|
|
|
|
self.patch = mock.patch(
|
|
|
|
f"zerver.webhooks.{self.WEBHOOK_DIR_NAME}.view.check_send_webhook_message",
|
|
|
|
side_effect=side_effect,
|
|
|
|
)
|
|
|
|
self.patch.start()
|
|
|
|
self.addCleanup(self.patch.stop)
|
|
|
|
|
2020-03-10 11:48:26 +01:00
|
|
|
def api_stream_message(self, user: UserProfile, *args: Any, **kwargs: Any) -> HttpResponse:
|
2021-02-12 08:20:45 +01:00
|
|
|
kwargs["HTTP_AUTHORIZATION"] = self.encode_user(user)
|
2020-08-23 15:49:24 +02:00
|
|
|
return self.check_webhook(*args, **kwargs)
|
2017-12-14 19:02:02 +01:00
|
|
|
|
2020-08-23 15:49:24 +02:00
|
|
|
def check_webhook(
|
|
|
|
self,
|
|
|
|
fixture_name: str,
|
2021-06-30 09:46:14 +02:00
|
|
|
expected_topic: Optional[str] = None,
|
|
|
|
expected_message: Optional[str] = None,
|
2021-02-12 08:19:30 +01:00
|
|
|
content_type: Optional[str] = "application/json",
|
2021-06-30 09:46:14 +02:00
|
|
|
expect_noop: Optional[bool] = False,
|
2020-08-23 15:49:24 +02:00
|
|
|
**kwargs: Any,
|
2020-08-24 17:47:30 +02:00
|
|
|
) -> None:
|
2020-08-23 15:49:24 +02:00
|
|
|
"""
|
|
|
|
check_webhook is the main way to test "normal" webhooks that
|
|
|
|
work by receiving a payload from a third party and then writing
|
|
|
|
some message to a Zulip stream.
|
|
|
|
|
|
|
|
We use `fixture_name` to find the payload data in of our test
|
|
|
|
fixtures. Then we verify that a message gets sent to a stream:
|
|
|
|
|
|
|
|
self.STREAM_NAME: stream name
|
|
|
|
expected_topic: topic
|
|
|
|
expected_message: content
|
|
|
|
|
|
|
|
We simulate the delivery of the payload with `content_type`,
|
|
|
|
and you can pass other headers via `kwargs`.
|
|
|
|
|
|
|
|
For the rare cases of webhooks actually sending private messages,
|
|
|
|
see send_and_test_private_message.
|
2021-06-30 09:46:14 +02:00
|
|
|
|
|
|
|
When no message is expected to be sent, set `expect_noop` to True.
|
2020-08-23 15:49:24 +02:00
|
|
|
"""
|
2020-08-23 19:30:12 +02:00
|
|
|
assert self.STREAM_NAME is not None
|
|
|
|
self.subscribe(self.test_user, self.STREAM_NAME)
|
|
|
|
|
2020-08-20 17:03:43 +02:00
|
|
|
payload = self.get_payload(fixture_name)
|
2016-11-10 19:30:09 +01:00
|
|
|
if content_type is not None:
|
2021-02-12 08:20:45 +01:00
|
|
|
kwargs["content_type"] = content_type
|
2021-06-26 09:18:33 +02:00
|
|
|
if self.WEBHOOK_DIR_NAME is not None:
|
|
|
|
headers = get_fixture_http_headers(self.WEBHOOK_DIR_NAME, fixture_name)
|
2020-07-05 02:14:06 +02:00
|
|
|
headers = standardize_headers(headers)
|
|
|
|
kwargs.update(headers)
|
2021-06-30 09:46:14 +02:00
|
|
|
try:
|
|
|
|
msg = self.send_webhook_payload(
|
|
|
|
self.test_user,
|
|
|
|
self.url,
|
|
|
|
payload,
|
|
|
|
**kwargs,
|
|
|
|
)
|
|
|
|
except EmptyResponseError:
|
|
|
|
if expect_noop:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
raise AssertionError(
|
|
|
|
"No message was sent. Pass expect_noop=True if this is intentional."
|
|
|
|
)
|
2020-08-23 19:30:12 +02:00
|
|
|
|
2021-06-30 09:46:14 +02:00
|
|
|
if expect_noop:
|
|
|
|
raise Exception(
|
|
|
|
"""
|
|
|
|
While no message is expected given expect_noop=True,
|
|
|
|
your test code triggered an endpoint that did write
|
|
|
|
one or more new messages.
|
|
|
|
""".strip()
|
|
|
|
)
|
|
|
|
assert expected_message is not None and expected_topic is not None
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2020-08-24 17:47:30 +02:00
|
|
|
self.assert_stream_message(
|
|
|
|
message=msg,
|
|
|
|
stream_name=self.STREAM_NAME,
|
|
|
|
topic_name=expected_topic,
|
|
|
|
content=expected_message,
|
|
|
|
)
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2020-08-24 14:21:58 +02:00
|
|
|
def assert_stream_message(
|
|
|
|
self,
|
|
|
|
message: Message,
|
|
|
|
stream_name: str,
|
|
|
|
topic_name: str,
|
|
|
|
content: str,
|
|
|
|
) -> None:
|
|
|
|
self.assertEqual(get_display_recipient(message.recipient), stream_name)
|
|
|
|
self.assertEqual(message.topic_name(), topic_name)
|
|
|
|
self.assertEqual(message.content, content)
|
|
|
|
|
2020-07-05 02:14:06 +02:00
|
|
|
def send_and_test_private_message(
|
|
|
|
self,
|
|
|
|
fixture_name: str,
|
2020-08-23 16:45:07 +02:00
|
|
|
expected_message: str,
|
|
|
|
content_type: str = "application/json",
|
2020-07-05 02:14:06 +02:00
|
|
|
**kwargs: Any,
|
|
|
|
) -> Message:
|
2020-08-23 15:49:24 +02:00
|
|
|
"""
|
|
|
|
For the rare cases that you are testing a webhook that sends
|
|
|
|
private messages, use this function.
|
|
|
|
|
|
|
|
Most webhooks send to streams, and you will want to look at
|
|
|
|
check_webhook.
|
|
|
|
"""
|
2020-08-20 17:03:43 +02:00
|
|
|
payload = self.get_payload(fixture_name)
|
2021-02-12 08:20:45 +01:00
|
|
|
kwargs["content_type"] = content_type
|
2020-08-23 16:45:07 +02:00
|
|
|
|
2021-06-26 09:18:33 +02:00
|
|
|
if self.WEBHOOK_DIR_NAME is not None:
|
|
|
|
headers = get_fixture_http_headers(self.WEBHOOK_DIR_NAME, fixture_name)
|
2020-07-05 02:14:06 +02:00
|
|
|
headers = standardize_headers(headers)
|
|
|
|
kwargs.update(headers)
|
2020-01-30 17:25:01 +01:00
|
|
|
# The sender profile shouldn't be passed any further in kwargs, so we pop it.
|
2021-02-12 08:20:45 +01:00
|
|
|
sender = kwargs.pop("sender", self.test_user)
|
2020-08-23 19:30:12 +02:00
|
|
|
|
2020-08-23 19:09:27 +02:00
|
|
|
msg = self.send_webhook_payload(
|
|
|
|
sender,
|
|
|
|
self.url,
|
|
|
|
payload,
|
|
|
|
**kwargs,
|
|
|
|
)
|
2020-08-24 17:32:54 +02:00
|
|
|
self.assertEqual(msg.content, expected_message)
|
2016-11-10 19:30:09 +01:00
|
|
|
|
|
|
|
return msg
|
|
|
|
|
2018-05-11 01:40:45 +02:00
|
|
|
def build_webhook_url(self, *args: Any, **kwargs: Any) -> str:
|
2017-04-21 23:35:40 +02:00
|
|
|
url = self.URL_TEMPLATE
|
|
|
|
if url.find("api_key") >= 0:
|
2018-08-01 10:53:40 +02:00
|
|
|
api_key = get_api_key(self.test_user)
|
2021-02-12 08:19:30 +01:00
|
|
|
url = self.URL_TEMPLATE.format(api_key=api_key, stream=self.STREAM_NAME)
|
2017-04-21 23:35:40 +02:00
|
|
|
else:
|
|
|
|
url = self.URL_TEMPLATE.format(stream=self.STREAM_NAME)
|
|
|
|
|
|
|
|
has_arguments = kwargs or args
|
2021-02-12 08:20:45 +01:00
|
|
|
if has_arguments and url.find("?") == -1:
|
2020-06-09 00:25:09 +02:00
|
|
|
url = f"{url}?" # nocoverage
|
2017-04-06 23:26:29 +02:00
|
|
|
else:
|
2020-06-09 00:25:09 +02:00
|
|
|
url = f"{url}&"
|
2017-04-06 23:26:29 +02:00
|
|
|
|
|
|
|
for key, value in kwargs.items():
|
2020-06-09 00:25:09 +02:00
|
|
|
url = f"{url}{key}={value}&"
|
2017-04-06 23:26:29 +02:00
|
|
|
|
2017-04-21 23:35:40 +02:00
|
|
|
for arg in args:
|
2020-06-09 00:25:09 +02:00
|
|
|
url = f"{url}{arg}&"
|
2017-04-21 23:35:40 +02:00
|
|
|
|
|
|
|
return url[:-1] if has_arguments else url
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2020-08-20 17:03:43 +02:00
|
|
|
def get_payload(self, fixture_name: str) -> Union[str, Dict[str, str]]:
|
|
|
|
"""
|
|
|
|
Generally webhooks that override this should return dicts."""
|
|
|
|
return self.get_body(fixture_name)
|
|
|
|
|
|
|
|
def get_body(self, fixture_name: str) -> str:
|
2021-06-26 09:18:33 +02:00
|
|
|
assert self.WEBHOOK_DIR_NAME is not None
|
|
|
|
body = self.webhook_fixture_data(self.WEBHOOK_DIR_NAME, fixture_name)
|
2020-08-20 17:40:09 +02:00
|
|
|
# fail fast if we don't have valid json
|
|
|
|
orjson.loads(body)
|
|
|
|
return body
|
2016-11-10 19:30:09 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2019-05-13 07:04:31 +02:00
|
|
|
class MigrationsTestCase(ZulipTestCase): # nocoverage
|
2018-05-21 18:56:45 +02:00
|
|
|
"""
|
|
|
|
Test class for database migrations inspired by this blog post:
|
|
|
|
https://www.caktusgroup.com/blog/2016/02/02/writing-unit-tests-django-migrations/
|
|
|
|
Documented at https://zulip.readthedocs.io/en/latest/subsystems/schema-migrations.html
|
|
|
|
"""
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2018-04-09 18:19:55 +02:00
|
|
|
@property
|
|
|
|
def app(self) -> str:
|
2021-07-24 16:56:39 +02:00
|
|
|
app_config = apps.get_containing_app_config(type(self).__module__)
|
|
|
|
assert app_config is not None
|
|
|
|
return app_config.name
|
2018-04-09 18:19:55 +02:00
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
migrate_from: Optional[str] = None
|
|
|
|
migrate_to: Optional[str] = None
|
2018-04-09 18:19:55 +02:00
|
|
|
|
|
|
|
def setUp(self) -> None:
|
2021-02-12 08:19:30 +01:00
|
|
|
assert (
|
|
|
|
self.migrate_from and self.migrate_to
|
|
|
|
), f"TestCase '{type(self).__name__}' must define migrate_from and migrate_to properties"
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
migrate_from: List[Tuple[str, str]] = [(self.app, self.migrate_from)]
|
|
|
|
migrate_to: List[Tuple[str, str]] = [(self.app, self.migrate_to)]
|
2018-04-09 18:19:55 +02:00
|
|
|
executor = MigrationExecutor(connection)
|
|
|
|
old_apps = executor.loader.project_state(migrate_from).apps
|
|
|
|
|
|
|
|
# Reverse to the original migration
|
|
|
|
executor.migrate(migrate_from)
|
|
|
|
|
|
|
|
self.setUpBeforeMigration(old_apps)
|
|
|
|
|
|
|
|
# Run the migration to test
|
|
|
|
executor = MigrationExecutor(connection)
|
|
|
|
executor.loader.build_graph() # reload.
|
|
|
|
executor.migrate(migrate_to)
|
|
|
|
|
|
|
|
self.apps = executor.loader.project_state(migrate_to).apps
|
|
|
|
|
|
|
|
def setUpBeforeMigration(self, apps: StateApps) -> None:
|
|
|
|
pass # nocoverage
|