mirror of
https://github.com/zulip/zulip.git
synced 2026-06-21 21:32:29 +08:00
tests: Add test_example.py.
The idea here is to quickly learn basic testing idioms with code that's executed (unlike docs which are prone to bitrot).
This commit is contained in:
parent
ac360b3a6b
commit
47da28429b
318
zerver/tests/test_example.py
Normal file
318
zerver/tests/test_example.py
Normal file
@ -0,0 +1,318 @@
|
||||
from typing import Any, List, Mapping
|
||||
|
||||
import orjson
|
||||
|
||||
from zerver.lib.actions import do_change_can_create_users, do_change_user_role
|
||||
from zerver.lib.exceptions import JsonableError
|
||||
from zerver.lib.streams import access_stream_for_send_message
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.test_helpers import most_recent_message, queries_captured
|
||||
from zerver.lib.users import is_administrator_role
|
||||
from zerver.models import (
|
||||
UserProfile,
|
||||
UserStatus,
|
||||
get_display_recipient,
|
||||
get_realm,
|
||||
get_stream,
|
||||
get_user_by_delivery_email,
|
||||
)
|
||||
|
||||
|
||||
# Most Zulip tests use ZulipTestCase, which inherits from django.test.TestCase.
|
||||
# We recommend learning Django basics first, so search the web for "django testing".
|
||||
# A common first result is https://docs.djangoproject.com/en/3.2/topics/testing/
|
||||
class TestBasics(ZulipTestCase):
|
||||
def test_basics(self) -> None:
|
||||
# Django's tests are based on Python's unittest module, so you
|
||||
# will see us use things like assertEqual, assertTrue, and assertRaisesRegex
|
||||
# quite often.
|
||||
# See https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertEqual
|
||||
self.assertEqual(7 * 6, 42)
|
||||
|
||||
|
||||
class TestBasicUserStuff(ZulipTestCase):
|
||||
# Zulip has test fixtures with built-in users. It's good to know
|
||||
# which users are special. For example, Iago is our built-in
|
||||
# realm administrator. You can also modify users as needed.
|
||||
def test_users(self) -> None:
|
||||
# The example_user() helper returns a UserProfile object.
|
||||
hamlet = self.example_user("hamlet")
|
||||
self.assertEqual(hamlet.full_name, "King Hamlet")
|
||||
self.assertEqual(hamlet.role, UserProfile.ROLE_MEMBER)
|
||||
|
||||
iago = self.example_user("iago")
|
||||
self.assertEqual(iago.role, UserProfile.ROLE_REALM_ADMINISTRATOR)
|
||||
|
||||
polonius = self.example_user("polonius")
|
||||
self.assertEqual(polonius.role, UserProfile.ROLE_GUEST)
|
||||
|
||||
self.assertEqual(self.example_email("cordelia"), "cordelia@zulip.com")
|
||||
|
||||
def test_lib_functions(self) -> None:
|
||||
# This test is an example of testing a single library function.
|
||||
# Our tests aren't always at this level of granularity, but it's
|
||||
# often possible to write concise tests for library functions.
|
||||
|
||||
# Get our UserProfile objects first.
|
||||
iago = self.example_user("iago")
|
||||
hamlet = self.example_user("hamlet")
|
||||
|
||||
# It is a good idea for your tests to clearly demonstrate a
|
||||
# **change** to a value. So here we want to make sure that
|
||||
# do_change_user_role will change Hamlet such that
|
||||
# is_administrator_role becomes True, but we first assert it's
|
||||
# False.
|
||||
self.assertFalse(is_administrator_role(hamlet.role))
|
||||
|
||||
do_change_user_role(hamlet, UserProfile.ROLE_REALM_OWNER, acting_user=iago)
|
||||
self.assertTrue(is_administrator_role(hamlet.role))
|
||||
|
||||
# After we promote Hamlet, we also demote him. Testing state
|
||||
# changes like this in a single test can be a good technique,
|
||||
# although we also don't want tests to be too long.
|
||||
do_change_user_role(hamlet, UserProfile.ROLE_MODERATOR, acting_user=iago)
|
||||
self.assertFalse(is_administrator_role(hamlet.role))
|
||||
|
||||
|
||||
class TestFullStack(ZulipTestCase):
|
||||
# A lot of Zulip's unit tests are actually somewhat full-stack in
|
||||
# nature, and some folks might consider them to be more like "integration"
|
||||
# tests. Django makes it pretty easy to test Zulip endpoints, and then
|
||||
# ZulipTestCase has some additional helpers.
|
||||
def test_client_get(self) -> None:
|
||||
hamlet = self.example_user("hamlet")
|
||||
cordelia = self.example_user("cordelia")
|
||||
|
||||
# Most full-stack tests require you to log in the user.
|
||||
# The login_user helper basically wraps Django's client.login().
|
||||
self.login_user(hamlet)
|
||||
|
||||
# Zulip's client_get is a very thin wrapper on Django's client.get.
|
||||
# We always use the Zulip wrappers for client_get and client_post.
|
||||
url = f"/json/users/{cordelia.id}"
|
||||
result = self.client_get(url)
|
||||
|
||||
# Almost every meaningful full-stack test for a "happy path" situation
|
||||
# uses assert_json_success().
|
||||
self.assert_json_success(result)
|
||||
|
||||
# When we unpack the result.content object, we prefer the orjson library.
|
||||
content = orjson.loads(result.content)
|
||||
|
||||
# In this case we will validate the entire payload. It's good to use
|
||||
# concrete values where possible, but some things, like "cordelia.id",
|
||||
# are somewhat unpredictable, so we don't hard code values.
|
||||
self.assertEqual(
|
||||
content["user"],
|
||||
dict(
|
||||
avatar_url=content["user"]["avatar_url"],
|
||||
avatar_version=1,
|
||||
date_joined=content["user"]["date_joined"],
|
||||
email=cordelia.email,
|
||||
full_name="Cordelia, Lear's daughter",
|
||||
is_active=True,
|
||||
is_admin=False,
|
||||
is_billing_admin=False,
|
||||
is_bot=False,
|
||||
is_guest=False,
|
||||
is_owner=False,
|
||||
role=UserProfile.ROLE_MEMBER,
|
||||
timezone="",
|
||||
user_id=cordelia.id,
|
||||
),
|
||||
)
|
||||
|
||||
def test_client_post(self) -> None:
|
||||
# Here we're gonna test a POST call to /json/users, and it's
|
||||
# important that we not only check the payload, but we make
|
||||
# sure that the intended side effects actually happen.
|
||||
iago = self.example_user("iago")
|
||||
self.login_user(iago)
|
||||
|
||||
realm = get_realm("zulip")
|
||||
self.assertEqual(realm.id, iago.realm_id)
|
||||
|
||||
# Get our failing test first.
|
||||
self.assertRaises(
|
||||
UserProfile.DoesNotExist, lambda: get_user_by_delivery_email("romeo@zulip.net", realm)
|
||||
)
|
||||
|
||||
# Before we can successfully post, we need to ensure
|
||||
# that Iago can create users.
|
||||
do_change_can_create_users(iago, True)
|
||||
|
||||
params = dict(
|
||||
email="romeo@zulip.net",
|
||||
password="xxxx",
|
||||
full_name="Romeo Montague",
|
||||
)
|
||||
|
||||
# Use the Zulip wrapper.
|
||||
result = self.client_post("/json/users", params)
|
||||
|
||||
# Once again we check that the HTTP request was successful.
|
||||
self.assert_json_success(result)
|
||||
content = orjson.loads(result.content)
|
||||
|
||||
# Finally we test the side effect of the post.
|
||||
user_id = content["user_id"]
|
||||
romeo = get_user_by_delivery_email("romeo@zulip.net", realm)
|
||||
self.assertEqual(romeo.id, user_id)
|
||||
|
||||
def test_errors(self) -> None:
|
||||
iago = self.example_user("iago")
|
||||
self.login_user(iago)
|
||||
|
||||
do_change_can_create_users(iago, False)
|
||||
params = dict(
|
||||
email="romeo@zulip.net",
|
||||
password="xxxx",
|
||||
full_name="Romeo Montague",
|
||||
)
|
||||
|
||||
# We often use assert_json_error for negative tests.
|
||||
result = self.client_post("/json/users", params)
|
||||
self.assert_json_error(result, "User not authorized for this query", 400)
|
||||
|
||||
do_change_can_create_users(iago, True)
|
||||
params = dict(
|
||||
full_name="Romeo Montague",
|
||||
)
|
||||
result = self.client_post("/json/users", params)
|
||||
self.assert_json_error(result, "Missing 'email' argument", 400)
|
||||
|
||||
def test_tornado_redirects(self) -> None:
|
||||
# Let's poke a bit at Zulip's event system.
|
||||
# See https://zulip.readthedocs.io/en/latest/subsystems/events-system.html
|
||||
# for context on the system itself.
|
||||
cordelia = self.example_user("cordelia")
|
||||
self.login_user(cordelia)
|
||||
|
||||
params = dict(status_text="on vacation")
|
||||
|
||||
events: List[Mapping[str, Any]] = []
|
||||
|
||||
# Use the tornado_redirected_to_list context manager to capture
|
||||
# events.
|
||||
with self.tornado_redirected_to_list(events, expected_num_events=1):
|
||||
result = self.api_post(cordelia, "/api/v1/users/me/status", params)
|
||||
|
||||
self.assert_json_success(result)
|
||||
|
||||
# Check that the POST to Zulip causes the correct events to be sent
|
||||
# to Tornado.
|
||||
self.assertEqual(
|
||||
events[0]["event"],
|
||||
dict(type="user_status", user_id=cordelia.id, status_text="on vacation"),
|
||||
)
|
||||
|
||||
row = UserStatus.objects.last()
|
||||
self.assertEqual(row.user_profile_id, cordelia.id)
|
||||
self.assertEqual(row.status_text, "on vacation")
|
||||
|
||||
|
||||
class TestStreamHelpers(ZulipTestCase):
|
||||
# Streams are an important concept in Zulip, and ZulipTestCase
|
||||
# has helpers such as subscribe, users_subscribed_to_stream,
|
||||
# and make_stream.
|
||||
def test_new_streams(self) -> None:
|
||||
cordelia = self.example_user("cordelia")
|
||||
othello = self.example_user("othello")
|
||||
realm = cordelia.realm
|
||||
|
||||
stream_name = "Some new stream"
|
||||
self.subscribe(cordelia, stream_name)
|
||||
|
||||
self.assertEqual(set(self.users_subscribed_to_stream(stream_name, realm)), {cordelia})
|
||||
|
||||
self.subscribe(othello, stream_name)
|
||||
self.assertEqual(
|
||||
set(self.users_subscribed_to_stream(stream_name, realm)), {cordelia, othello}
|
||||
)
|
||||
|
||||
def test_private_stream(self) -> None:
|
||||
# When we test stream permissions, it's very common to use at least
|
||||
# two users, so that you can see how different users are impacted.
|
||||
# We commonly use Othello to represent the "other" user from the primary user.
|
||||
cordelia = self.example_user("cordelia")
|
||||
othello = self.example_user("othello")
|
||||
|
||||
realm = cordelia.realm
|
||||
stream_name = "Some private stream"
|
||||
|
||||
# Use the invite_only flag in make_stream to make a stream "private".
|
||||
stream = self.make_stream(stream_name=stream_name, invite_only=True)
|
||||
self.subscribe(cordelia, stream_name)
|
||||
|
||||
self.assertEqual(set(self.users_subscribed_to_stream(stream_name, realm)), {cordelia})
|
||||
|
||||
stream = get_stream(stream_name, realm)
|
||||
self.assertEqual(stream.name, stream_name)
|
||||
self.assertTrue(stream.invite_only)
|
||||
|
||||
# We will now observe that Cordelia can access the stream...
|
||||
access_stream_for_send_message(cordelia, stream, forwarder_user_profile=None)
|
||||
|
||||
# ...but Othello can't.
|
||||
msg = "Not authorized to send to stream"
|
||||
with self.assertRaisesRegex(JsonableError, msg):
|
||||
access_stream_for_send_message(othello, stream, forwarder_user_profile=None)
|
||||
|
||||
|
||||
class TestMessageHelpers(ZulipTestCase):
|
||||
# If you are testing behavior related to messages, then it's good
|
||||
# to know about send_stream_message, send_personal_message, and
|
||||
# most_recent_message.
|
||||
def test_stream_message(self) -> None:
|
||||
hamlet = self.example_user("hamlet")
|
||||
iago = self.example_user("iago")
|
||||
self.subscribe(hamlet, "Denmark")
|
||||
self.subscribe(iago, "Denmark")
|
||||
|
||||
self.send_stream_message(
|
||||
sender=hamlet,
|
||||
stream_name="Denmark",
|
||||
topic_name="lunch",
|
||||
content="I want pizza!",
|
||||
)
|
||||
|
||||
iago_message = most_recent_message(iago)
|
||||
|
||||
self.assertEqual(iago_message.sender_id, hamlet.id)
|
||||
self.assertEqual(get_display_recipient(iago_message.recipient), "Denmark")
|
||||
self.assertEqual(iago_message.topic_name(), "lunch")
|
||||
self.assertEqual(iago_message.content, "I want pizza!")
|
||||
|
||||
def test_personal_message(self) -> None:
|
||||
hamlet = self.example_user("hamlet")
|
||||
cordelia = self.example_user("cordelia")
|
||||
|
||||
self.send_personal_message(
|
||||
from_user=hamlet,
|
||||
to_user=cordelia,
|
||||
content="hello there!",
|
||||
)
|
||||
|
||||
cordelia_message = most_recent_message(cordelia)
|
||||
|
||||
self.assertEqual(cordelia_message.sender_id, hamlet.id)
|
||||
self.assertEqual(cordelia_message.content, "hello there!")
|
||||
|
||||
|
||||
class TestQueryCounts(ZulipTestCase):
|
||||
def test_capturing_queries(self) -> None:
|
||||
# It's a common pitfall in Django to have your app perform
|
||||
# too many queries due to lazy evaluation. We use the queries_captured
|
||||
# context manager to ensure our query count is predictable.
|
||||
hamlet = self.example_user("hamlet")
|
||||
cordelia = self.example_user("cordelia")
|
||||
|
||||
with queries_captured() as queries:
|
||||
self.send_personal_message(
|
||||
from_user=hamlet,
|
||||
to_user=cordelia,
|
||||
content="hello there!",
|
||||
)
|
||||
|
||||
# The assert_length helper is another useful extra from ZulipTestCase.
|
||||
self.assert_length(queries, 16)
|
||||
Loading…
Reference in New Issue
Block a user