From 34c8cd1b747efa4708905117b8f43a7fcb4b1a9b Mon Sep 17 00:00:00 2001
From: Alex Vandiver
Date: Thu, 9 Mar 2023 01:27:46 +0000
Subject: [PATCH] tests: Split out test_invite from test_signup.
There is no good reason for this single test file to be more than 6k
lines.
---
zerver/tests/test_invite.py | 2184 +++++++++++++++++++++++++++++++++++
zerver/tests/test_signup.py | 2159 +---------------------------------
2 files changed, 2196 insertions(+), 2147 deletions(-)
create mode 100644 zerver/tests/test_invite.py
diff --git a/zerver/tests/test_invite.py b/zerver/tests/test_invite.py
new file mode 100644
index 0000000000..b6b10a0d96
--- /dev/null
+++ b/zerver/tests/test_invite.py
@@ -0,0 +1,2184 @@
+import datetime
+import re
+import urllib
+from typing import TYPE_CHECKING, List, Optional, Sequence, Union
+from unittest.mock import patch
+from urllib.parse import urlencode
+
+import orjson
+from django.conf import settings
+from django.core.mail.message import EmailMultiAlternatives
+from django.http import HttpRequest
+from django.urls import reverse
+from django.utils.timezone import now as timezone_now
+
+from confirmation import settings as confirmation_settings
+from confirmation.models import (
+ Confirmation,
+ ConfirmationKeyError,
+ create_confirmation_link,
+ get_object_from_key,
+)
+from corporate.lib.stripe import get_latest_seat_count
+from zerver.actions.create_user import do_create_user, process_new_human_user
+from zerver.actions.invites import (
+ do_create_multiuse_invite_link,
+ do_get_invites_controlled_by_user,
+ do_invite_users,
+ do_revoke_multi_use_invite,
+)
+from zerver.actions.realm_settings import (
+ do_set_realm_property,
+)
+from zerver.actions.user_settings import do_change_full_name
+from zerver.actions.users import change_user_is_active
+from zerver.context_processors import common_context
+from zerver.lib.send_email import (
+ FromAddress,
+ deliver_scheduled_emails,
+ send_future_email,
+)
+from zerver.lib.test_classes import ZulipTestCase
+from zerver.lib.test_helpers import (
+ cache_tries_captured,
+ find_key_by_email,
+ queries_captured,
+)
+from zerver.models import (
+ Message,
+ MultiuseInvite,
+ PreregistrationUser,
+ Realm,
+ ScheduledEmail,
+ Stream,
+ UserMessage,
+ UserProfile,
+ get_realm,
+ get_stream,
+ get_user_by_delivery_email,
+)
+from zerver.views.invite import INVITATION_LINK_VALIDITY_MINUTES, get_invitee_emails_set
+from zerver.views.registration import accounts_home
+
+if TYPE_CHECKING:
+ from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
+
+
+class InviteUserBase(ZulipTestCase):
+ def check_sent_emails(self, correct_recipients: List[str]) -> None:
+ from django.core.mail import outbox
+
+ self.assert_length(outbox, len(correct_recipients))
+ email_recipients = [email.recipients()[0] for email in outbox]
+ self.assertEqual(sorted(email_recipients), sorted(correct_recipients))
+ if len(outbox) == 0:
+ return
+
+ self.assertIn("Zulip", self.email_display_from(outbox[0]))
+
+ self.assertEqual(self.email_envelope_from(outbox[0]), settings.NOREPLY_EMAIL_ADDRESS)
+ self.assertRegex(
+ self.email_display_from(outbox[0]), rf" <{self.TOKENIZED_NOREPLY_REGEX}>\Z"
+ )
+
+ self.assertEqual(outbox[0].extra_headers["List-Id"], "Zulip Dev ")
+
+ def invite(
+ self,
+ invitee_emails: str,
+ stream_names: Sequence[str],
+ invite_expires_in_minutes: Optional[int] = INVITATION_LINK_VALIDITY_MINUTES,
+ body: str = "",
+ invite_as: int = PreregistrationUser.INVITE_AS["MEMBER"],
+ ) -> "TestHttpResponse":
+ """
+ Invites the specified users to Zulip with the specified streams.
+
+ users should be a string containing the users to invite, comma or
+ newline separated.
+
+ streams should be a list of strings.
+ """
+ stream_ids = []
+ for stream_name in stream_names:
+ stream_ids.append(self.get_stream_id(stream_name))
+
+ invite_expires_in: Union[str, Optional[int]] = invite_expires_in_minutes
+ if invite_expires_in is None:
+ invite_expires_in = orjson.dumps(None).decode()
+
+ return self.client_post(
+ "/json/invites",
+ {
+ "invitee_emails": invitee_emails,
+ "invite_expires_in_minutes": invite_expires_in,
+ "stream_ids": orjson.dumps(stream_ids).decode(),
+ "invite_as": invite_as,
+ },
+ )
+
+
+class InviteUserTest(InviteUserBase):
+ def test_successful_invite_user(self) -> None:
+ """
+ A call to /json/invites with valid parameters causes an invitation
+ email to be sent.
+ """
+ self.login("hamlet")
+ invitee = "alice-test@zulip.com"
+ self.assert_json_success(self.invite(invitee, ["Denmark"]))
+ self.assertTrue(find_key_by_email(invitee))
+ self.check_sent_emails([invitee])
+
+ def test_newbie_restrictions(self) -> None:
+ user_profile = self.example_user("hamlet")
+ invitee = "alice-test@zulip.com"
+ stream_name = "Denmark"
+
+ self.login_user(user_profile)
+
+ result = self.invite(invitee, [stream_name])
+ self.assert_json_success(result)
+
+ user_profile.date_joined = timezone_now() - datetime.timedelta(days=10)
+ user_profile.save()
+
+ with self.settings(INVITES_MIN_USER_AGE_DAYS=5):
+ result = self.invite(invitee, [stream_name])
+ self.assert_json_success(result)
+
+ with self.settings(INVITES_MIN_USER_AGE_DAYS=15):
+ result = self.invite(invitee, [stream_name])
+ self.assert_json_error_contains(result, "Your account is too new")
+
+ def test_invite_limits(self) -> None:
+ user_profile = self.example_user("hamlet")
+ realm = user_profile.realm
+ stream_name = "Denmark"
+
+ # These constants only need to be in descending order
+ # for this test to trigger an InvitationError based
+ # on max daily counts.
+ site_max = 50
+ realm_max = 40
+ num_invitees = 30
+ max_daily_count = 20
+
+ daily_counts = [(1, max_daily_count)]
+
+ invite_emails = [f"foo-{i:02}@zulip.com" for i in range(num_invitees)]
+ invitees = ",".join(invite_emails)
+
+ self.login_user(user_profile)
+
+ realm.max_invites = realm_max
+ realm.date_created = timezone_now()
+ realm.save()
+
+ def try_invite() -> "TestHttpResponse":
+ with self.settings(
+ OPEN_REALM_CREATION=True,
+ INVITES_DEFAULT_REALM_DAILY_MAX=site_max,
+ INVITES_NEW_REALM_LIMIT_DAYS=daily_counts,
+ ):
+ result = self.invite(invitees, [stream_name])
+ return result
+
+ result = try_invite()
+ self.assert_json_error_contains(result, "reached the limit")
+
+ # Next show that aggregate limits expire once the realm is old
+ # enough.
+
+ realm.date_created = timezone_now() - datetime.timedelta(days=8)
+ realm.save()
+
+ with queries_captured() as queries:
+ with cache_tries_captured() as cache_tries:
+ result = try_invite()
+
+ self.assert_json_success(result)
+
+ # TODO: Fix large query count here.
+ #
+ # TODO: There is some test OTHER than this one
+ # that is leaking some kind of state change
+ # that throws off the query count here. It
+ # is hard to investigate currently (due to
+ # the large number of queries), so I just
+ # use an approximate equality check.
+ actual_count = len(queries)
+ expected_count = 251
+ if abs(actual_count - expected_count) > 1:
+ raise AssertionError(
+ f"""
+ Unexpected number of queries:
+
+ expected query count: {expected_count}
+ actual: {actual_count}
+ """
+ )
+
+ # Almost all of these cache hits are to re-fetch each one of the
+ # invitees. These happen inside our queue processor for sending
+ # confirmation emails, so they are somewhat difficult to avoid.
+ #
+ # TODO: Mock the call to queue_json_publish, so we can measure the
+ # queue impact separately from the user-perceived impact.
+ self.assert_length(cache_tries, 32)
+
+ # Next get line coverage on bumping a realm's max_invites.
+ realm.date_created = timezone_now()
+ realm.max_invites = site_max + 10
+ realm.save()
+
+ result = try_invite()
+ self.assert_json_success(result)
+
+ # Finally get coverage on the case that OPEN_REALM_CREATION is False.
+
+ with self.settings(OPEN_REALM_CREATION=False):
+ result = self.invite(invitees, [stream_name])
+
+ self.assert_json_success(result)
+
+ def test_invite_user_to_realm_on_manual_license_plan(self) -> None:
+ user = self.example_user("hamlet")
+ self.login_user(user)
+ _, ledger = self.subscribe_realm_to_monthly_plan_on_manual_license_management(
+ user.realm, 50, 50
+ )
+
+ with self.settings(BILLING_ENABLED=True):
+ result = self.invite(self.nonreg_email("alice"), ["Denmark"])
+ self.assert_json_success(result)
+
+ ledger.licenses_at_next_renewal = 5
+ ledger.save(update_fields=["licenses_at_next_renewal"])
+ with self.settings(BILLING_ENABLED=True):
+ result = self.invite(self.nonreg_email("bob"), ["Denmark"])
+ self.assert_json_success(result)
+
+ ledger.licenses = get_latest_seat_count(user.realm) + 1
+ ledger.save(update_fields=["licenses"])
+ with self.settings(BILLING_ENABLED=True):
+ invitee_emails = self.nonreg_email("bob") + "," + self.nonreg_email("alice")
+ result = self.invite(invitee_emails, ["Denmark"])
+ self.assert_json_error_contains(
+ result, "Your organization does not have enough unused Zulip licenses to invite 2 users"
+ )
+
+ ledger.licenses = get_latest_seat_count(user.realm)
+ ledger.save(update_fields=["licenses"])
+ with self.settings(BILLING_ENABLED=True):
+ result = self.invite(self.nonreg_email("bob"), ["Denmark"])
+ self.assert_json_error_contains(
+ result, "All Zulip licenses for this organization are currently in use"
+ )
+
+ with self.settings(BILLING_ENABLED=True):
+ result = self.invite(
+ self.nonreg_email("bob"),
+ ["Denmark"],
+ invite_as=PreregistrationUser.INVITE_AS["GUEST_USER"],
+ )
+ self.assert_json_success(result)
+
+ def test_cross_realm_bot(self) -> None:
+ inviter = self.example_user("hamlet")
+ self.login_user(inviter)
+
+ cross_realm_bot_email = "emailgateway@zulip.com"
+ legit_new_email = "fred@zulip.com"
+ invitee_emails = ",".join([cross_realm_bot_email, legit_new_email])
+
+ result = self.invite(invitee_emails, ["Denmark"])
+ self.assert_json_error(
+ result,
+ (
+ "Some of those addresses are already using Zulip, so we didn't send them an"
+ " invitation. We did send invitations to everyone else!"
+ ),
+ )
+
+ def test_invite_mirror_dummy_user(self) -> None:
+ """
+ A mirror dummy account is a temporary account
+ that we keep in our system if we are mirroring
+ data from something like Zephyr or IRC.
+
+ We want users to eventually just sign up or
+ register for Zulip, in which case we will just
+ fully "activate" the account.
+
+ Here we test that you can invite a person who
+ has a mirror dummy account.
+ """
+ inviter = self.example_user("hamlet")
+ self.login_user(inviter)
+
+ mirror_user = self.example_user("cordelia")
+ mirror_user.is_mirror_dummy = True
+ mirror_user.save()
+ change_user_is_active(mirror_user, False)
+
+ self.assertEqual(
+ PreregistrationUser.objects.filter(email=mirror_user.email).count(),
+ 0,
+ )
+
+ result = self.invite(mirror_user.email, ["Denmark"])
+ self.assert_json_success(result)
+
+ prereg_user = PreregistrationUser.objects.get(email=mirror_user.email)
+ assert prereg_user.referred_by is not None and inviter is not None
+ self.assertEqual(
+ prereg_user.referred_by.email,
+ inviter.email,
+ )
+
+ def test_invite_from_now_deactivated_user(self) -> None:
+ """
+ While accepting an invitation from a user,
+ processing for a new user account will only
+ be completed if the inviter is not deactivated
+ after sending the invite.
+ """
+ inviter = self.example_user("hamlet")
+ self.login_user(inviter)
+ invitee = self.nonreg_email("alice")
+
+ result = self.invite(invitee, ["Denmark"])
+ self.assert_json_success(result)
+
+ prereg_user = PreregistrationUser.objects.get(email=invitee)
+ change_user_is_active(inviter, False)
+ do_create_user(
+ invitee,
+ "password",
+ inviter.realm,
+ "full name",
+ prereg_user=prereg_user,
+ acting_user=None,
+ )
+
+ def test_successful_invite_user_as_owner_from_owner_account(self) -> None:
+ self.login("desdemona")
+ invitee = self.nonreg_email("alice")
+ result = self.invite(
+ invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["REALM_OWNER"]
+ )
+ self.assert_json_success(result)
+ self.assertTrue(find_key_by_email(invitee))
+
+ self.submit_reg_form_for_user(invitee, "password")
+ invitee_profile = self.nonreg_user("alice")
+ self.assertTrue(invitee_profile.is_realm_owner)
+ self.assertFalse(invitee_profile.is_guest)
+ self.check_user_added_in_system_group(invitee_profile)
+
+ def test_invite_user_as_owner_from_admin_account(self) -> None:
+ self.login("iago")
+ invitee = self.nonreg_email("alice")
+ response = self.invite(
+ invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["REALM_OWNER"]
+ )
+ self.assert_json_error(response, "Must be an organization owner")
+
+ def test_successful_invite_user_as_admin_from_admin_account(self) -> None:
+ self.login("iago")
+ invitee = self.nonreg_email("alice")
+ result = self.invite(
+ invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["REALM_ADMIN"]
+ )
+ self.assert_json_success(result)
+ self.assertTrue(find_key_by_email(invitee))
+
+ self.submit_reg_form_for_user(invitee, "password")
+ invitee_profile = self.nonreg_user("alice")
+ self.assertTrue(invitee_profile.is_realm_admin)
+ self.assertFalse(invitee_profile.is_realm_owner)
+ self.assertFalse(invitee_profile.is_guest)
+ self.check_user_added_in_system_group(invitee_profile)
+
+ def test_invite_user_as_admin_from_normal_account(self) -> None:
+ self.login("hamlet")
+ invitee = self.nonreg_email("alice")
+ response = self.invite(
+ invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["REALM_ADMIN"]
+ )
+ self.assert_json_error(response, "Must be an organization administrator")
+
+ def test_successful_invite_user_as_moderator_from_admin_account(self) -> None:
+ self.login("iago")
+ invitee = self.nonreg_email("alice")
+ result = self.invite(
+ invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["MODERATOR"]
+ )
+ self.assert_json_success(result)
+ self.assertTrue(find_key_by_email(invitee))
+
+ self.submit_reg_form_for_user(invitee, "password")
+ invitee_profile = self.nonreg_user("alice")
+ self.assertFalse(invitee_profile.is_realm_admin)
+ self.assertTrue(invitee_profile.is_moderator)
+ self.assertFalse(invitee_profile.is_guest)
+ self.check_user_added_in_system_group(invitee_profile)
+
+ def test_invite_user_as_moderator_from_normal_account(self) -> None:
+ self.login("hamlet")
+ invitee = self.nonreg_email("alice")
+ response = self.invite(
+ invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["MODERATOR"]
+ )
+ self.assert_json_error(response, "Must be an organization administrator")
+
+ def test_invite_user_as_moderator_from_moderator_account(self) -> None:
+ self.login("shiva")
+ invitee = self.nonreg_email("alice")
+ response = self.invite(
+ invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["MODERATOR"]
+ )
+ self.assert_json_error(response, "Must be an organization administrator")
+
+ def test_invite_user_as_invalid_type(self) -> None:
+ """
+ Test inviting a user as invalid type of user i.e. type of invite_as
+ is not in PreregistrationUser.INVITE_AS
+ """
+ self.login("iago")
+ invitee = self.nonreg_email("alice")
+ response = self.invite(invitee, ["Denmark"], invite_as=10)
+ self.assert_json_error(response, "Invalid invite_as")
+
+ def test_successful_invite_user_as_guest_from_normal_account(self) -> None:
+ self.login("hamlet")
+ invitee = self.nonreg_email("alice")
+ self.assert_json_success(
+ self.invite(invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["GUEST_USER"])
+ )
+ self.assertTrue(find_key_by_email(invitee))
+
+ self.submit_reg_form_for_user(invitee, "password")
+ invitee_profile = self.nonreg_user("alice")
+ self.assertFalse(invitee_profile.is_realm_admin)
+ self.assertTrue(invitee_profile.is_guest)
+ self.check_user_added_in_system_group(invitee_profile)
+
+ def test_successful_invite_user_as_guest_from_admin_account(self) -> None:
+ self.login("iago")
+ invitee = self.nonreg_email("alice")
+ self.assert_json_success(
+ self.invite(invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["GUEST_USER"])
+ )
+ self.assertTrue(find_key_by_email(invitee))
+
+ self.submit_reg_form_for_user(invitee, "password")
+ invitee_profile = self.nonreg_user("alice")
+ self.assertFalse(invitee_profile.is_realm_admin)
+ self.assertTrue(invitee_profile.is_guest)
+ self.check_user_added_in_system_group(invitee_profile)
+
+ def test_successful_invite_user_with_name(self) -> None:
+ """
+ A call to /json/invites with valid parameters causes an invitation
+ email to be sent.
+ """
+ self.login("hamlet")
+ email = "alice-test@zulip.com"
+ invitee = f"Alice Test <{email}>"
+ self.assert_json_success(self.invite(invitee, ["Denmark"]))
+ self.assertTrue(find_key_by_email(email))
+ self.check_sent_emails([email])
+
+ def test_successful_invite_user_with_name_and_normal_one(self) -> None:
+ """
+ A call to /json/invites with valid parameters causes an invitation
+ email to be sent.
+ """
+ self.login("hamlet")
+ email = "alice-test@zulip.com"
+ email2 = "bob-test@zulip.com"
+ invitee = f"Alice Test <{email}>, {email2}"
+ self.assert_json_success(self.invite(invitee, ["Denmark"]))
+ self.assertTrue(find_key_by_email(email))
+ self.assertTrue(find_key_by_email(email2))
+ self.check_sent_emails([email, email2])
+
+ def test_can_invite_others_to_realm(self) -> None:
+ def validation_func(user_profile: UserProfile) -> bool:
+ user_profile.refresh_from_db()
+ return user_profile.can_invite_others_to_realm()
+
+ realm = get_realm("zulip")
+ do_set_realm_property(
+ realm, "invite_to_realm_policy", Realm.POLICY_NOBODY, acting_user=None
+ )
+ desdemona = self.example_user("desdemona")
+ self.assertFalse(validation_func(desdemona))
+
+ self.check_has_permission_policies("invite_to_realm_policy", validation_func)
+
+ def test_invite_others_to_realm_setting(self) -> None:
+ """
+ The invite_to_realm_policy realm setting works properly.
+ """
+ realm = get_realm("zulip")
+ do_set_realm_property(
+ realm, "invite_to_realm_policy", Realm.POLICY_NOBODY, acting_user=None
+ )
+ self.login("desdemona")
+ email = "alice-test@zulip.com"
+ email2 = "bob-test@zulip.com"
+ invitee = f"Alice Test <{email}>, {email2}"
+ self.assert_json_error(
+ self.invite(invitee, ["Denmark"]),
+ "Insufficient permission",
+ )
+
+ do_set_realm_property(
+ realm, "invite_to_realm_policy", Realm.POLICY_ADMINS_ONLY, acting_user=None
+ )
+
+ self.login("shiva")
+ self.assert_json_error(
+ self.invite(invitee, ["Denmark"]),
+ "Insufficient permission",
+ )
+
+ # Now verify an administrator can do it
+ self.login("iago")
+ self.assert_json_success(self.invite(invitee, ["Denmark"]))
+ self.assertTrue(find_key_by_email(email))
+ self.assertTrue(find_key_by_email(email2))
+
+ self.check_sent_emails([email, email2])
+
+ from django.core import mail
+
+ mail.outbox = []
+
+ do_set_realm_property(
+ realm, "invite_to_realm_policy", Realm.POLICY_MODERATORS_ONLY, acting_user=None
+ )
+ self.login("hamlet")
+ email = "carol-test@zulip.com"
+ email2 = "earl-test@zulip.com"
+ invitee = f"Carol Test <{email}>, {email2}"
+ self.assert_json_error(
+ self.invite(invitee, ["Denmark"]),
+ "Insufficient permission",
+ )
+
+ self.login("shiva")
+ self.assert_json_success(self.invite(invitee, ["Denmark"]))
+ self.assertTrue(find_key_by_email(email))
+ self.assertTrue(find_key_by_email(email2))
+ self.check_sent_emails([email, email2])
+
+ mail.outbox = []
+
+ do_set_realm_property(
+ realm, "invite_to_realm_policy", Realm.POLICY_MEMBERS_ONLY, acting_user=None
+ )
+
+ self.login("polonius")
+ email = "dave-test@zulip.com"
+ email2 = "mark-test@zulip.com"
+ invitee = f"Dave Test <{email}>, {email2}"
+ self.assert_json_error(self.invite(invitee, ["Denmark"]), "Not allowed for guest users")
+
+ self.login("hamlet")
+ self.assert_json_success(self.invite(invitee, ["Denmark"]))
+ self.assertTrue(find_key_by_email(email))
+ self.assertTrue(find_key_by_email(email2))
+ self.check_sent_emails([email, email2])
+
+ mail.outbox = []
+
+ do_set_realm_property(
+ realm, "invite_to_realm_policy", Realm.POLICY_FULL_MEMBERS_ONLY, acting_user=None
+ )
+ do_set_realm_property(realm, "waiting_period_threshold", 1000, acting_user=None)
+
+ hamlet = self.example_user("hamlet")
+ hamlet.date_joined = timezone_now() - datetime.timedelta(
+ days=(realm.waiting_period_threshold - 1)
+ )
+
+ email = "issac-test@zulip.com"
+ email2 = "steven-test@zulip.com"
+ invitee = f"Issac Test <{email}>, {email2}"
+ self.assert_json_error(
+ self.invite(invitee, ["Denmark"]),
+ "Insufficient permission",
+ )
+
+ do_set_realm_property(realm, "waiting_period_threshold", 0, acting_user=None)
+
+ self.assert_json_success(self.invite(invitee, ["Denmark"]))
+ self.assertTrue(find_key_by_email(email))
+ self.assertTrue(find_key_by_email(email2))
+ self.check_sent_emails([email, email2])
+
+ def test_invite_user_signup_initial_history(self) -> None:
+ """
+ Test that a new user invited to a stream receives some initial
+ history but only from public streams.
+ """
+ self.login("hamlet")
+ user_profile = self.example_user("hamlet")
+ private_stream_name = "Secret"
+ self.make_stream(private_stream_name, invite_only=True)
+ self.subscribe(user_profile, private_stream_name)
+ public_msg_id = self.send_stream_message(
+ self.example_user("hamlet"),
+ "Denmark",
+ topic_name="Public topic",
+ content="Public message",
+ )
+ secret_msg_id = self.send_stream_message(
+ self.example_user("hamlet"),
+ private_stream_name,
+ topic_name="Secret topic",
+ content="Secret message",
+ )
+ invitee = self.nonreg_email("alice")
+ self.assert_json_success(self.invite(invitee, [private_stream_name, "Denmark"]))
+ self.assertTrue(find_key_by_email(invitee))
+
+ self.submit_reg_form_for_user(invitee, "password")
+ invitee_profile = self.nonreg_user("alice")
+ invitee_msg_ids = [
+ um.message_id for um in UserMessage.objects.filter(user_profile=invitee_profile)
+ ]
+ self.assertTrue(public_msg_id in invitee_msg_ids)
+ self.assertFalse(secret_msg_id in invitee_msg_ids)
+ self.assertFalse(invitee_profile.is_realm_admin)
+
+ invitee_msg, signups_stream_msg, inviter_msg, secret_msg = Message.objects.all().order_by(
+ "-id"
+ )[0:4]
+
+ self.assertEqual(secret_msg.id, secret_msg_id)
+
+ self.assertEqual(inviter_msg.sender.email, "notification-bot@zulip.com")
+ self.assertTrue(
+ inviter_msg.content.startswith(
+ f"alice_zulip.com <`{invitee_profile.email}`> accepted your",
+ )
+ )
+
+ self.assertEqual(signups_stream_msg.sender.email, "notification-bot@zulip.com")
+ self.assertTrue(
+ signups_stream_msg.content.startswith(
+ f"@_**alice_zulip.com|{invitee_profile.id}** just signed up",
+ )
+ )
+
+ self.assertEqual(invitee_msg.sender.email, "welcome-bot@zulip.com")
+ self.assertTrue(invitee_msg.content.startswith("Hello, and welcome to Zulip!"))
+ self.assertNotIn("demo organization", invitee_msg.content)
+
+ def test_multi_user_invite(self) -> None:
+ """
+ Invites multiple users with a variety of delimiters.
+ """
+ self.login("hamlet")
+ # Intentionally use a weird string.
+ self.assert_json_success(
+ self.invite(
+ """bob-test@zulip.com, carol-test@zulip.com,
+ dave-test@zulip.com
+
+
+earl-test@zulip.com""",
+ ["Denmark"],
+ )
+ )
+ for user in ("bob", "carol", "dave", "earl"):
+ self.assertTrue(find_key_by_email(f"{user}-test@zulip.com"))
+ self.check_sent_emails(
+ [
+ "bob-test@zulip.com",
+ "carol-test@zulip.com",
+ "dave-test@zulip.com",
+ "earl-test@zulip.com",
+ ]
+ )
+
+ def test_max_invites_model(self) -> None:
+ realm = get_realm("zulip")
+ self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
+ realm.max_invites = 3
+ realm.save()
+ self.assertEqual(get_realm("zulip").max_invites, 3)
+ realm.max_invites = settings.INVITES_DEFAULT_REALM_DAILY_MAX
+ realm.save()
+
+ def test_invite_too_many_users(self) -> None:
+ # Only a light test of this pathway; e.g. doesn't test that
+ # the limit gets reset after 24 hours
+ self.login("iago")
+ invitee_emails = "1@zulip.com, 2@zulip.com"
+ self.invite(invitee_emails, ["Denmark"])
+ invitee_emails = ", ".join(str(i) for i in range(get_realm("zulip").max_invites - 1))
+ self.assert_json_error(
+ self.invite(invitee_emails, ["Denmark"]),
+ "To protect users, Zulip limits the number of invitations you can send in one day. Because you have reached the limit, no invitations were sent.",
+ )
+
+ def test_missing_or_invalid_params(self) -> None:
+ """
+ Tests inviting with various missing or invalid parameters.
+ """
+ realm = get_realm("zulip")
+ do_set_realm_property(realm, "emails_restricted_to_domains", True, acting_user=None)
+
+ self.login("hamlet")
+ invitee_emails = "foo@zulip.com"
+ self.assert_json_error(
+ self.invite(invitee_emails, []),
+ "You must specify at least one stream for invitees to join.",
+ )
+
+ for address in ("noatsign.com", "outsideyourdomain@example.net"):
+ self.assert_json_error(
+ self.invite(address, ["Denmark"]),
+ "Some emails did not validate, so we didn't send any invitations.",
+ )
+ self.check_sent_emails([])
+
+ self.assert_json_error(
+ self.invite("", ["Denmark"]), "You must specify at least one email address."
+ )
+ self.check_sent_emails([])
+
+ def test_guest_user_invitation(self) -> None:
+ """
+ Guest user can't invite new users
+ """
+ self.login("polonius")
+ invitee = "alice-test@zulip.com"
+ self.assert_json_error(self.invite(invitee, ["Denmark"]), "Not allowed for guest users")
+ self.assertEqual(find_key_by_email(invitee), None)
+ self.check_sent_emails([])
+
+ def test_invalid_stream(self) -> None:
+ """
+ Tests inviting to a non-existent stream.
+ """
+ self.login("hamlet")
+ self.assert_json_error(
+ self.invite("iago-test@zulip.com", ["NotARealStream"]),
+ f"Stream does not exist with id: {self.INVALID_STREAM_ID}. No invites were sent.",
+ )
+ self.check_sent_emails([])
+
+ def test_invite_existing_user(self) -> None:
+ """
+ If you invite an address already using Zulip, no invitation is sent.
+ """
+ self.login("hamlet")
+
+ hamlet_email = "hAmLeT@zUlIp.com"
+ result = self.invite(hamlet_email, ["Denmark"])
+ self.assert_json_error(result, "We weren't able to invite anyone.")
+
+ self.assertFalse(
+ PreregistrationUser.objects.filter(email__iexact=hamlet_email).exists(),
+ )
+ self.check_sent_emails([])
+
+ def normalize_string(self, s: str) -> str:
+ s = s.strip()
+ return re.sub(r"\s+", " ", s)
+
+ def test_invite_links_in_name(self) -> None:
+ """
+ If you invite an address already using Zulip, no invitation is sent.
+ """
+ hamlet = self.example_user("hamlet")
+ self.login_user(hamlet)
+ # Test we properly handle links in user full names
+ do_change_full_name(hamlet, " https://www.google.com", hamlet)
+
+ result = self.invite("newuser@zulip.com", ["Denmark"])
+ self.assert_json_success(result)
+ self.check_sent_emails(["newuser@zulip.com"])
+ from django.core.mail import outbox
+
+ assert isinstance(outbox[0], EmailMultiAlternatives)
+ assert isinstance(outbox[0].alternatives[0][0], str)
+ body = self.normalize_string(outbox[0].alternatives[0][0])
+
+ # Verify that one can't get Zulip to send invitation emails
+ # that third-party products will linkify using the full_name
+ # field, because we've included that field inside the mailto:
+ # link for the sender.
+ self.assertIn(
+ '</a> https://www.google.com (hamlet@zulip.com) wants',
+ body,
+ )
+
+ # TODO: Ideally, this test would also test the Invitation
+ # Reminder email generated, but the test setup for that is
+ # annoying.
+
+ def test_invite_some_existing_some_new(self) -> None:
+ """
+ If you invite a mix of already existing and new users, invitations are
+ only sent to the new users.
+ """
+ self.login("hamlet")
+ existing = [self.example_email("hamlet"), "othello@zulip.com"]
+ new = ["foo-test@zulip.com", "bar-test@zulip.com"]
+ invitee_emails = "\n".join(existing + new)
+ self.assert_json_error(
+ self.invite(invitee_emails, ["Denmark"]),
+ "Some of those addresses are already using Zulip, \
+so we didn't send them an invitation. We did send invitations to everyone else!",
+ )
+
+ # We only created accounts for the new users.
+ for email in existing:
+ self.assertRaises(
+ PreregistrationUser.DoesNotExist,
+ lambda: PreregistrationUser.objects.get(email=email),
+ )
+ for email in new:
+ self.assertTrue(PreregistrationUser.objects.get(email=email))
+
+ # We only sent emails to the new users.
+ self.check_sent_emails(new)
+
+ prereg_user = PreregistrationUser.objects.get(email="foo-test@zulip.com")
+ self.assertEqual(prereg_user.email, "foo-test@zulip.com")
+
+ def test_invite_outside_domain_in_closed_realm(self) -> None:
+ """
+ In a realm with `emails_restricted_to_domains = True`, you can't invite people
+ with a different domain from that of the realm or your e-mail address.
+ """
+ zulip_realm = get_realm("zulip")
+ zulip_realm.emails_restricted_to_domains = True
+ zulip_realm.save()
+
+ self.login("hamlet")
+ external_address = "foo@example.com"
+
+ self.assert_json_error(
+ self.invite(external_address, ["Denmark"]),
+ "Some emails did not validate, so we didn't send any invitations.",
+ )
+
+ def test_invite_using_disposable_email(self) -> None:
+ """
+ In a realm with `disallow_disposable_email_addresses = True`, you can't invite
+ people with a disposable domain.
+ """
+ zulip_realm = get_realm("zulip")
+ zulip_realm.emails_restricted_to_domains = False
+ zulip_realm.disallow_disposable_email_addresses = True
+ zulip_realm.save()
+
+ self.login("hamlet")
+ external_address = "foo@mailnator.com"
+
+ self.assert_json_error(
+ self.invite(external_address, ["Denmark"]),
+ "Some emails did not validate, so we didn't send any invitations.",
+ )
+
+ def test_invite_outside_domain_in_open_realm(self) -> None:
+ """
+ In a realm with `emails_restricted_to_domains = False`, you can invite people
+ with a different domain from that of the realm or your e-mail address.
+ """
+ zulip_realm = get_realm("zulip")
+ zulip_realm.emails_restricted_to_domains = False
+ zulip_realm.save()
+
+ self.login("hamlet")
+ external_address = "foo@example.com"
+
+ self.assert_json_success(self.invite(external_address, ["Denmark"]))
+ self.check_sent_emails([external_address])
+
+ def test_invite_outside_domain_before_closing(self) -> None:
+ """
+ If you invite someone with a different domain from that of the realm
+ when `emails_restricted_to_domains = False`, but `emails_restricted_to_domains` later
+ changes to true, the invitation should succeed but the invitee's signup
+ attempt should fail.
+ """
+ zulip_realm = get_realm("zulip")
+ zulip_realm.emails_restricted_to_domains = False
+ zulip_realm.save()
+
+ self.login("hamlet")
+ external_address = "foo@example.com"
+
+ self.assert_json_success(self.invite(external_address, ["Denmark"]))
+ self.check_sent_emails([external_address])
+
+ zulip_realm.emails_restricted_to_domains = True
+ zulip_realm.save()
+
+ result = self.submit_reg_form_for_user("foo@example.com", "password")
+ self.assertEqual(result.status_code, 200)
+ self.assert_in_response("only allows users with email addresses", result)
+
+ def test_disposable_emails_before_closing(self) -> None:
+ """
+ If you invite someone with a disposable email when
+ `disallow_disposable_email_addresses = False`, but
+ later changes to true, the invitation should succeed
+ but the invitee's signup attempt should fail.
+ """
+ zulip_realm = get_realm("zulip")
+ zulip_realm.emails_restricted_to_domains = False
+ zulip_realm.disallow_disposable_email_addresses = False
+ zulip_realm.save()
+
+ self.login("hamlet")
+ external_address = "foo@mailnator.com"
+
+ self.assert_json_success(self.invite(external_address, ["Denmark"]))
+ self.check_sent_emails([external_address])
+
+ zulip_realm.disallow_disposable_email_addresses = True
+ zulip_realm.save()
+
+ result = self.submit_reg_form_for_user("foo@mailnator.com", "password")
+ self.assertEqual(result.status_code, 200)
+ self.assert_in_response("Please sign up using a real email address.", result)
+
+ def test_invite_with_email_containing_plus_before_closing(self) -> None:
+ """
+ If you invite someone with an email containing plus when
+ `emails_restricted_to_domains = False`, but later change
+ `emails_restricted_to_domains = True`, the invitation should
+ succeed but the invitee's signup attempt should fail as
+ users are not allowed to sign up using email containing +
+ when the realm is restricted to domain.
+ """
+ zulip_realm = get_realm("zulip")
+ zulip_realm.emails_restricted_to_domains = False
+ zulip_realm.save()
+
+ self.login("hamlet")
+ external_address = "foo+label@zulip.com"
+
+ self.assert_json_success(self.invite(external_address, ["Denmark"]))
+ self.check_sent_emails([external_address])
+
+ zulip_realm.emails_restricted_to_domains = True
+ zulip_realm.save()
+
+ result = self.submit_reg_form_for_user(external_address, "password")
+ self.assertEqual(result.status_code, 200)
+ self.assert_in_response(
+ "Zulip Dev, does not allow signups using emails\n that contains +", result
+ )
+
+ def test_invalid_email_check_after_confirming_email(self) -> None:
+ self.login("hamlet")
+ email = "test@zulip.com"
+
+ self.assert_json_success(self.invite(email, ["Denmark"]))
+
+ obj = Confirmation.objects.get(confirmation_key=find_key_by_email(email))
+ prereg_user = obj.content_object
+ assert prereg_user is not None
+ prereg_user.email = "invalid.email"
+ prereg_user.save()
+
+ result = self.submit_reg_form_for_user(email, "password")
+ self.assertEqual(result.status_code, 200)
+ self.assert_in_response(
+ "The email address you are trying to sign up with is not valid", result
+ )
+
+ def test_invite_with_non_ascii_streams(self) -> None:
+ """
+ Inviting someone to streams with non-ASCII characters succeeds.
+ """
+ self.login("hamlet")
+ invitee = "alice-test@zulip.com"
+
+ stream_name = "hümbüǵ"
+
+ # Make sure we're subscribed before inviting someone.
+ self.subscribe(self.example_user("hamlet"), stream_name)
+
+ self.assert_json_success(self.invite(invitee, [stream_name]))
+
+ def test_invitation_reminder_email(self) -> None:
+ from django.core.mail import outbox
+
+ # All users belong to zulip realm
+ referrer_name = "hamlet"
+ current_user = self.example_user(referrer_name)
+ self.login_user(current_user)
+ invitee_email = self.nonreg_email("alice")
+ self.assert_json_success(self.invite(invitee_email, ["Denmark"]))
+ self.assertTrue(find_key_by_email(invitee_email))
+ self.check_sent_emails([invitee_email])
+
+ data = {"email": invitee_email, "referrer_email": current_user.email}
+ invitee = PreregistrationUser.objects.get(email=data["email"])
+ referrer = self.example_user(referrer_name)
+ validity_in_minutes = 2 * 24 * 60
+ link = create_confirmation_link(
+ invitee, Confirmation.INVITATION, validity_in_minutes=validity_in_minutes
+ )
+ context = common_context(referrer)
+ context.update(
+ activate_url=link,
+ referrer_name=referrer.full_name,
+ referrer_email=referrer.email,
+ referrer_realm_name=referrer.realm.name,
+ )
+ with self.settings(EMAIL_BACKEND="django.core.mail.backends.console.EmailBackend"):
+ email = data["email"]
+ send_future_email(
+ "zerver/emails/invitation_reminder",
+ referrer.realm,
+ to_emails=[email],
+ from_address=FromAddress.no_reply_placeholder,
+ context=context,
+ )
+ email_jobs_to_deliver = ScheduledEmail.objects.filter(
+ scheduled_timestamp__lte=timezone_now()
+ )
+ self.assert_length(email_jobs_to_deliver, 1)
+ email_count = len(outbox)
+ for job in email_jobs_to_deliver:
+ deliver_scheduled_emails(job)
+ self.assert_length(outbox, email_count + 1)
+ self.assertEqual(self.email_envelope_from(outbox[-1]), settings.NOREPLY_EMAIL_ADDRESS)
+ self.assertIn(FromAddress.NOREPLY, self.email_display_from(outbox[-1]))
+
+ # Now verify that signing up clears invite_reminder emails
+ with self.settings(EMAIL_BACKEND="django.core.mail.backends.console.EmailBackend"):
+ email = data["email"]
+ send_future_email(
+ "zerver/emails/invitation_reminder",
+ referrer.realm,
+ to_emails=[email],
+ from_address=FromAddress.no_reply_placeholder,
+ context=context,
+ )
+
+ email_jobs_to_deliver = ScheduledEmail.objects.filter(
+ scheduled_timestamp__lte=timezone_now(), type=ScheduledEmail.INVITATION_REMINDER
+ )
+ self.assert_length(email_jobs_to_deliver, 1)
+
+ self.register(invitee_email, "test")
+ email_jobs_to_deliver = ScheduledEmail.objects.filter(
+ scheduled_timestamp__lte=timezone_now(), type=ScheduledEmail.INVITATION_REMINDER
+ )
+ self.assert_length(email_jobs_to_deliver, 0)
+
+ def test_no_invitation_reminder_when_link_expires_quickly(self) -> None:
+ self.login("hamlet")
+ # Check invitation reminder email is scheduled with 4 day link expiry
+ self.invite("alice@zulip.com", ["Denmark"], invite_expires_in_minutes=4 * 24 * 60)
+ self.assertEqual(
+ ScheduledEmail.objects.filter(type=ScheduledEmail.INVITATION_REMINDER).count(), 1
+ )
+ # Check invitation reminder email is not scheduled with 3 day link expiry
+ self.invite("bob@zulip.com", ["Denmark"], invite_expires_in_minutes=3 * 24 * 60)
+ self.assertEqual(
+ ScheduledEmail.objects.filter(type=ScheduledEmail.INVITATION_REMINDER).count(), 1
+ )
+
+ # make sure users can't take a valid confirmation key from another
+ # pathway and use it with the invitation URL route
+ def test_confirmation_key_of_wrong_type(self) -> None:
+ email = self.nonreg_email("alice")
+ realm = get_realm("zulip")
+ inviter = self.example_user("iago")
+ prereg_user = PreregistrationUser.objects.create(
+ email=email, referred_by=inviter, realm=realm
+ )
+ url = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
+ registration_key = url.split("/")[-1]
+
+ # Mainly a test of get_object_from_key, rather than of the invitation pathway
+ with self.assertRaises(ConfirmationKeyError) as cm:
+ get_object_from_key(registration_key, [Confirmation.INVITATION], mark_object_used=True)
+ self.assertEqual(cm.exception.error_type, ConfirmationKeyError.DOES_NOT_EXIST)
+
+ # Verify that using the wrong type doesn't work in the main confirm code path
+ email_change_url = create_confirmation_link(prereg_user, Confirmation.EMAIL_CHANGE)
+ email_change_key = email_change_url.split("/")[-1]
+ result = self.client_post("/accounts/register/", {"key": email_change_key})
+ self.assertEqual(result.status_code, 404)
+ self.assert_in_response(
+ "Whoops. We couldn't find your confirmation link in the system.", result
+ )
+
+ def test_confirmation_expired(self) -> None:
+ email = self.nonreg_email("alice")
+ realm = get_realm("zulip")
+ inviter = self.example_user("iago")
+ prereg_user = PreregistrationUser.objects.create(
+ email=email, referred_by=inviter, realm=realm
+ )
+ date_sent = timezone_now() - datetime.timedelta(weeks=3)
+ with patch("confirmation.models.timezone_now", return_value=date_sent):
+ url = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
+
+ key = url.split("/")[-1]
+ confirmation_link_path = "/" + url.split("/", 3)[3]
+ # Both the confirmation link and submitting the key to the registration endpoint
+ # directly will return the appropriate error.
+ result = self.client_get(confirmation_link_path)
+ self.assertEqual(result.status_code, 404)
+ self.assert_in_response(
+ "Whoops. The confirmation link has expired or been deactivated.", result
+ )
+
+ result = self.client_post("/accounts/register/", {"key": key})
+ self.assertEqual(result.status_code, 404)
+ self.assert_in_response(
+ "Whoops. The confirmation link has expired or been deactivated.", result
+ )
+
+ def test_never_expire_confirmation_object(self) -> None:
+ email = self.nonreg_email("alice")
+ realm = get_realm("zulip")
+ inviter = self.example_user("iago")
+ prereg_user = PreregistrationUser.objects.create(
+ email=email, referred_by=inviter, realm=realm
+ )
+ activation_url = create_confirmation_link(
+ prereg_user, Confirmation.INVITATION, validity_in_minutes=None
+ )
+ confirmation = Confirmation.objects.last()
+ assert confirmation is not None
+ self.assertEqual(confirmation.expiry_date, None)
+ activation_key = activation_url.split("/")[-1]
+ response = self.client_post(
+ "/accounts/register/",
+ {"key": activation_key, "from_confirmation": 1, "full_nme": "alice"},
+ )
+ self.assertEqual(response.status_code, 200)
+
+ def test_send_more_than_one_invite_to_same_user(self) -> None:
+ self.user_profile = self.example_user("iago")
+ streams = []
+ for stream_name in ["Denmark", "Scotland"]:
+ streams.append(get_stream(stream_name, self.user_profile.realm))
+
+ invite_expires_in_minutes = 2 * 24 * 60
+ do_invite_users(
+ self.user_profile,
+ ["foo@zulip.com"],
+ streams,
+ invite_expires_in_minutes=invite_expires_in_minutes,
+ )
+ prereg_user = PreregistrationUser.objects.get(email="foo@zulip.com")
+ do_invite_users(
+ self.user_profile,
+ ["foo@zulip.com"],
+ streams,
+ invite_expires_in_minutes=invite_expires_in_minutes,
+ )
+ do_invite_users(
+ self.user_profile,
+ ["foo@zulip.com"],
+ streams,
+ invite_expires_in_minutes=invite_expires_in_minutes,
+ )
+
+ # Also send an invite from a different realm.
+ lear = get_realm("lear")
+ lear_user = self.lear_user("cordelia")
+ do_invite_users(
+ lear_user, ["foo@zulip.com"], [], invite_expires_in_minutes=invite_expires_in_minutes
+ )
+
+ invites = PreregistrationUser.objects.filter(email__iexact="foo@zulip.com")
+ self.assert_length(invites, 4)
+
+ created_user = do_create_user(
+ "foo@zulip.com",
+ "password",
+ self.user_profile.realm,
+ "full name",
+ prereg_user=prereg_user,
+ acting_user=None,
+ )
+
+ accepted_invite = PreregistrationUser.objects.filter(
+ email__iexact="foo@zulip.com", status=confirmation_settings.STATUS_USED
+ )
+ revoked_invites = PreregistrationUser.objects.filter(
+ email__iexact="foo@zulip.com", status=confirmation_settings.STATUS_REVOKED
+ )
+ # If a user was invited more than once, when it accepts one invite and register
+ # the others must be canceled.
+ self.assert_length(accepted_invite, 1)
+ self.assertEqual(accepted_invite[0].id, prereg_user.id)
+ self.assertEqual(accepted_invite[0].created_user, created_user)
+
+ expected_revoked_invites = set(invites.exclude(id=prereg_user.id).exclude(realm=lear))
+ self.assertEqual(set(revoked_invites), expected_revoked_invites)
+
+ self.assertEqual(
+ PreregistrationUser.objects.get(email__iexact="foo@zulip.com", realm=lear).status, 0
+ )
+
+ with self.assertRaises(AssertionError):
+ process_new_human_user(created_user, prereg_user)
+
+ def test_confirmation_obj_not_exist_error(self) -> None:
+ """Since the key is a param input by the user to the registration endpoint,
+ if it inserts an invalid value, the confirmation object won't be found. This
+ tests if, in that scenario, we handle the exception by redirecting the user to
+ the link_expired page.
+ """
+ email = self.nonreg_email("alice")
+ password = "password"
+ realm = get_realm("zulip")
+ inviter = self.example_user("iago")
+ prereg_user = PreregistrationUser.objects.create(
+ email=email, referred_by=inviter, realm=realm
+ )
+ confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
+
+ registration_key = "invalid_confirmation_key"
+ url = "/accounts/register/"
+ response = self.client_post(
+ url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
+ )
+ self.assertEqual(response.status_code, 404)
+ self.assert_in_response(
+ "Whoops. We couldn't find your confirmation link in the system.", response
+ )
+
+ registration_key = confirmation_link.split("/")[-1]
+ response = self.client_post(
+ url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
+ )
+ self.assert_in_success_response(["We just need you to do one last thing."], response)
+ response = self.submit_reg_form_for_user(email, password, key=registration_key)
+ self.assertEqual(response.status_code, 302)
+
+ def test_validate_email_not_already_in_realm(self) -> None:
+ email = self.nonreg_email("alice")
+ password = "password"
+ realm = get_realm("zulip")
+ inviter = self.example_user("iago")
+ prereg_user = PreregistrationUser.objects.create(
+ email=email, referred_by=inviter, realm=realm
+ )
+
+ confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
+ registration_key = confirmation_link.split("/")[-1]
+
+ url = "/accounts/register/"
+ self.client_post(
+ url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
+ )
+ self.submit_reg_form_for_user(email, password, key=registration_key)
+
+ new_prereg_user = PreregistrationUser.objects.create(
+ email=email, referred_by=inviter, realm=realm
+ )
+ new_confirmation_link = create_confirmation_link(
+ new_prereg_user, Confirmation.USER_REGISTRATION
+ )
+ new_registration_key = new_confirmation_link.split("/")[-1]
+ url = "/accounts/register/"
+ response = self.client_post(
+ url, {"key": new_registration_key, "from_confirmation": 1, "full_name": "alice"}
+ )
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(
+ response["Location"],
+ reverse("login") + "?" + urlencode({"email": email, "already_registered": 1}),
+ )
+
+ def test_confirmation_key_cant_be_reused(self) -> None:
+ email = self.nonreg_email("alice")
+ password = "password"
+ realm = get_realm("zulip")
+ inviter = self.example_user("iago")
+ prereg_user = PreregistrationUser.objects.create(
+ email=email, referred_by=inviter, realm=realm
+ )
+
+ confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
+ registration_key = confirmation_link.split("/")[-1]
+
+ url = "/accounts/register/"
+ self.client_post(
+ url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
+ )
+ self.submit_reg_form_for_user(email, password, key=registration_key)
+
+ prereg_user.refresh_from_db()
+ self.assertIsNotNone(prereg_user.created_user)
+
+ # Now attempt to re-use the same key.
+ result = self.client_post("/accounts/register/", {"key": registration_key})
+ self.assertEqual(result.status_code, 404)
+ self.assert_in_response(
+ "Whoops. The confirmation link has expired or been deactivated.", result
+ )
+
+ def test_confirmation_link_in_manual_license_plan(self) -> None:
+ inviter = self.example_user("iago")
+ realm = get_realm("zulip")
+
+ email = self.nonreg_email("alice")
+ realm = get_realm("zulip")
+ prereg_user = PreregistrationUser.objects.create(
+ email=email, referred_by=inviter, realm=realm
+ )
+ confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
+ registration_key = confirmation_link.split("/")[-1]
+ url = "/accounts/register/"
+ self.client_post(
+ url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
+ )
+ response = self.submit_reg_form_for_user(email, "password", key=registration_key)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response["Location"], "http://zulip.testserver/")
+
+ # We want to simulate the organization having exactly all their licenses
+ # used, to verify that joining as a regular user is not allowed,
+ # but as a guest still works (guests are free up to a certain number).
+ current_seat_count = get_latest_seat_count(realm)
+ self.subscribe_realm_to_monthly_plan_on_manual_license_management(
+ realm, current_seat_count, current_seat_count
+ )
+
+ email = self.nonreg_email("bob")
+ prereg_user = PreregistrationUser.objects.create(
+ email=email, referred_by=inviter, realm=realm
+ )
+ confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
+ registration_key = confirmation_link.split("/")[-1]
+ url = "/accounts/register/"
+ self.client_post(url, {"key": registration_key, "from_confirmation": 1, "full_name": "bob"})
+ response = self.submit_reg_form_for_user(email, "password", key=registration_key)
+ self.assert_in_success_response(
+ ["New members cannot join this organization because all Zulip licenses are"], response
+ )
+
+ guest_prereg_user = PreregistrationUser.objects.create(
+ email=email,
+ referred_by=inviter,
+ realm=realm,
+ invited_as=PreregistrationUser.INVITE_AS["GUEST_USER"],
+ )
+ confirmation_link = create_confirmation_link(
+ guest_prereg_user, Confirmation.USER_REGISTRATION
+ )
+ registration_key = confirmation_link.split("/")[-1]
+ url = "/accounts/register/"
+
+ self.client_post(
+ url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
+ )
+ response = self.submit_reg_form_for_user(email, "password", key=registration_key)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response["Location"], "http://zulip.testserver/")
+
+
+class InvitationsTestCase(InviteUserBase):
+ def test_do_get_invites_controlled_by_user(self) -> None:
+ user_profile = self.example_user("iago")
+ hamlet = self.example_user("hamlet")
+ othello = self.example_user("othello")
+
+ streams = []
+ for stream_name in ["Denmark", "Scotland"]:
+ streams.append(get_stream(stream_name, user_profile.realm))
+
+ invite_expires_in_minutes = 2 * 24 * 60
+ do_invite_users(
+ user_profile,
+ ["TestOne@zulip.com"],
+ streams,
+ invite_expires_in_minutes=invite_expires_in_minutes,
+ )
+ do_invite_users(
+ user_profile,
+ ["TestTwo@zulip.com"],
+ streams,
+ invite_expires_in_minutes=invite_expires_in_minutes,
+ )
+ do_invite_users(
+ hamlet,
+ ["TestThree@zulip.com"],
+ streams,
+ invite_expires_in_minutes=invite_expires_in_minutes,
+ )
+ do_invite_users(
+ othello,
+ ["TestFour@zulip.com"],
+ streams,
+ invite_expires_in_minutes=invite_expires_in_minutes,
+ )
+ do_invite_users(
+ self.mit_user("sipbtest"),
+ ["TestOne@mit.edu"],
+ [],
+ invite_expires_in_minutes=invite_expires_in_minutes,
+ )
+ do_create_multiuse_invite_link(
+ user_profile, PreregistrationUser.INVITE_AS["MEMBER"], invite_expires_in_minutes
+ )
+ self.assert_length(do_get_invites_controlled_by_user(user_profile), 5)
+ self.assert_length(do_get_invites_controlled_by_user(hamlet), 1)
+ self.assert_length(do_get_invites_controlled_by_user(othello), 1)
+
+ def test_successful_get_open_invitations(self) -> None:
+ """
+ A GET call to /json/invites returns all unexpired invitations.
+ """
+ active_value = getattr(confirmation_settings, "STATUS_USED", "Wrong")
+ self.assertNotEqual(active_value, "Wrong")
+
+ self.login("iago")
+ user_profile = self.example_user("iago")
+ self.login_user(user_profile)
+
+ hamlet = self.example_user("hamlet")
+ othello = self.example_user("othello")
+
+ streams = []
+ for stream_name in ["Denmark", "Scotland"]:
+ streams.append(get_stream(stream_name, user_profile.realm))
+
+ invite_expires_in_minutes = 2 * 24 * 60
+ do_invite_users(
+ user_profile,
+ ["TestOne@zulip.com"],
+ streams,
+ invite_expires_in_minutes=invite_expires_in_minutes,
+ )
+
+ with patch(
+ "confirmation.models.timezone_now",
+ return_value=timezone_now() - datetime.timedelta(days=3),
+ ):
+ do_invite_users(
+ user_profile,
+ ["TestTwo@zulip.com"],
+ streams,
+ invite_expires_in_minutes=invite_expires_in_minutes,
+ )
+ do_create_multiuse_invite_link(
+ othello, PreregistrationUser.INVITE_AS["MEMBER"], invite_expires_in_minutes
+ )
+
+ prereg_user_three = PreregistrationUser(
+ email="TestThree@zulip.com", referred_by=user_profile, status=active_value
+ )
+ prereg_user_three.save()
+ create_confirmation_link(
+ prereg_user_three,
+ Confirmation.INVITATION,
+ validity_in_minutes=invite_expires_in_minutes,
+ )
+
+ do_create_multiuse_invite_link(
+ hamlet, PreregistrationUser.INVITE_AS["MEMBER"], invite_expires_in_minutes
+ )
+
+ result = self.client_get("/json/invites")
+ self.assertEqual(result.status_code, 200)
+ invites = orjson.loads(result.content)["invites"]
+ self.assert_length(invites, 2)
+
+ self.assertFalse(invites[0]["is_multiuse"])
+ self.assertEqual(invites[0]["email"], "TestOne@zulip.com")
+ self.assertTrue(invites[1]["is_multiuse"])
+ self.assertEqual(invites[1]["invited_by_user_id"], hamlet.id)
+
+ def test_get_never_expiring_invitations(self) -> None:
+ self.login("iago")
+ user_profile = self.example_user("iago")
+
+ streams = []
+ for stream_name in ["Denmark", "Scotland"]:
+ streams.append(get_stream(stream_name, user_profile.realm))
+
+ with patch(
+ "confirmation.models.timezone_now",
+ return_value=timezone_now() - datetime.timedelta(days=1000),
+ ):
+ # Testing the invitation with expiry date set to "None" exists
+ # after a large amount of days.
+ do_invite_users(
+ user_profile,
+ ["TestOne@zulip.com"],
+ streams,
+ invite_expires_in_minutes=None,
+ )
+ do_invite_users(
+ user_profile,
+ ["TestTwo@zulip.com"],
+ streams,
+ invite_expires_in_minutes=100 * 24 * 60,
+ )
+ do_create_multiuse_invite_link(
+ user_profile, PreregistrationUser.INVITE_AS["MEMBER"], None
+ )
+ do_create_multiuse_invite_link(
+ user_profile, PreregistrationUser.INVITE_AS["MEMBER"], 100
+ )
+
+ result = self.client_get("/json/invites")
+ self.assertEqual(result.status_code, 200)
+ invites = orjson.loads(result.content)["invites"]
+ # We only get invitations that will never expire because we have mocked time such
+ # that the other invitations are created in the deep past.
+ self.assert_length(invites, 2)
+
+ self.assertFalse(invites[0]["is_multiuse"])
+ self.assertEqual(invites[0]["email"], "TestOne@zulip.com")
+ self.assertEqual(invites[0]["expiry_date"], None)
+ self.assertTrue(invites[1]["is_multiuse"])
+ self.assertEqual(invites[1]["invited_by_user_id"], user_profile.id)
+ self.assertEqual(invites[1]["expiry_date"], None)
+
+ def test_successful_delete_invitation(self) -> None:
+ """
+ A DELETE call to /json/invites/ should delete the invite and
+ any scheduled invitation reminder emails.
+ """
+ self.login("iago")
+
+ invitee = "DeleteMe@zulip.com"
+ self.assert_json_success(self.invite(invitee, ["Denmark"]))
+ prereg_user = PreregistrationUser.objects.get(email=invitee)
+
+ # Verify that the scheduled email exists.
+ ScheduledEmail.objects.get(address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER)
+
+ result = self.client_delete("/json/invites/" + str(prereg_user.id))
+ self.assertEqual(result.status_code, 200)
+ error_result = self.client_delete("/json/invites/" + str(prereg_user.id))
+ self.assert_json_error(error_result, "No such invitation")
+
+ self.assertRaises(
+ ScheduledEmail.DoesNotExist,
+ lambda: ScheduledEmail.objects.get(
+ address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
+ ),
+ )
+
+ def test_successful_member_delete_invitation(self) -> None:
+ """
+ A DELETE call from member account to /json/invites/ should delete the invite and
+ any scheduled invitation reminder emails.
+ """
+ user_profile = self.example_user("hamlet")
+ self.login_user(user_profile)
+ invitee = "DeleteMe@zulip.com"
+ self.assert_json_success(self.invite(invitee, ["Denmark"]))
+
+ # Verify that the scheduled email exists.
+ prereg_user = PreregistrationUser.objects.get(email=invitee, referred_by=user_profile)
+ ScheduledEmail.objects.get(address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER)
+
+ # Verify another non-admin can't delete
+ result = self.api_delete(
+ self.example_user("othello"), "/api/v1/invites/" + str(prereg_user.id)
+ )
+ self.assert_json_error(result, "Must be an organization administrator")
+
+ # Verify that the scheduled email still exists.
+ prereg_user = PreregistrationUser.objects.get(email=invitee, referred_by=user_profile)
+ ScheduledEmail.objects.get(address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER)
+
+ # Verify deletion works.
+ result = self.api_delete(user_profile, "/api/v1/invites/" + str(prereg_user.id))
+ self.assertEqual(result.status_code, 200)
+
+ result = self.api_delete(user_profile, "/api/v1/invites/" + str(prereg_user.id))
+ self.assert_json_error(result, "No such invitation")
+
+ self.assertRaises(
+ ScheduledEmail.DoesNotExist,
+ lambda: ScheduledEmail.objects.get(
+ address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
+ ),
+ )
+
+ def test_delete_owner_invitation(self) -> None:
+ self.login("desdemona")
+ owner = self.example_user("desdemona")
+
+ invitee = "DeleteMe@zulip.com"
+ self.assert_json_success(
+ self.invite(
+ invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["REALM_OWNER"]
+ )
+ )
+ prereg_user = PreregistrationUser.objects.get(email=invitee)
+ result = self.api_delete(
+ self.example_user("iago"), "/api/v1/invites/" + str(prereg_user.id)
+ )
+ self.assert_json_error(result, "Must be an organization owner")
+
+ result = self.api_delete(owner, "/api/v1/invites/" + str(prereg_user.id))
+ self.assert_json_success(result)
+ result = self.api_delete(owner, "/api/v1/invites/" + str(prereg_user.id))
+ self.assert_json_error(result, "No such invitation")
+ self.assertRaises(
+ ScheduledEmail.DoesNotExist,
+ lambda: ScheduledEmail.objects.get(
+ address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
+ ),
+ )
+
+ def test_delete_multiuse_invite(self) -> None:
+ """
+ A DELETE call to /json/invites/multiuse should delete the
+ multiuse_invite.
+ """
+ self.login("iago")
+
+ zulip_realm = get_realm("zulip")
+ multiuse_invite = MultiuseInvite.objects.create(
+ referred_by=self.example_user("hamlet"), realm=zulip_realm
+ )
+ validity_in_minutes = 2 * 24 * 60
+ create_confirmation_link(
+ multiuse_invite, Confirmation.MULTIUSE_INVITE, validity_in_minutes=validity_in_minutes
+ )
+ result = self.client_delete("/json/invites/multiuse/" + str(multiuse_invite.id))
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(
+ MultiuseInvite.objects.get(id=multiuse_invite.id).status,
+ confirmation_settings.STATUS_REVOKED,
+ )
+ # Test that trying to double-delete fails
+ error_result = self.client_delete("/json/invites/multiuse/" + str(multiuse_invite.id))
+ self.assert_json_error(error_result, "Invitation has already been revoked")
+
+ # Test deleting owner mutiuse_invite.
+ multiuse_invite = MultiuseInvite.objects.create(
+ referred_by=self.example_user("desdemona"),
+ realm=zulip_realm,
+ invited_as=PreregistrationUser.INVITE_AS["REALM_OWNER"],
+ )
+ validity_in_minutes = 2
+ create_confirmation_link(
+ multiuse_invite, Confirmation.MULTIUSE_INVITE, validity_in_minutes=validity_in_minutes
+ )
+ error_result = self.client_delete("/json/invites/multiuse/" + str(multiuse_invite.id))
+ self.assert_json_error(error_result, "Must be an organization owner")
+
+ self.login("desdemona")
+ result = self.client_delete("/json/invites/multiuse/" + str(multiuse_invite.id))
+ self.assert_json_success(result)
+ self.assertEqual(
+ MultiuseInvite.objects.get(id=multiuse_invite.id).status,
+ confirmation_settings.STATUS_REVOKED,
+ )
+
+ # Test deleting multiuse invite from another realm
+ mit_realm = get_realm("zephyr")
+ multiuse_invite_in_mit = MultiuseInvite.objects.create(
+ referred_by=self.mit_user("sipbtest"), realm=mit_realm
+ )
+ validity_in_minutes = 2 * 24 * 60
+ create_confirmation_link(
+ multiuse_invite_in_mit,
+ Confirmation.MULTIUSE_INVITE,
+ validity_in_minutes=validity_in_minutes,
+ )
+ error_result = self.client_delete(
+ "/json/invites/multiuse/" + str(multiuse_invite_in_mit.id)
+ )
+ self.assert_json_error(error_result, "No such invitation")
+
+ non_existent_id = MultiuseInvite.objects.count() + 9999
+ error_result = self.client_delete(f"/json/invites/multiuse/{non_existent_id}")
+ self.assert_json_error(error_result, "No such invitation")
+
+ def test_successful_resend_invitation(self) -> None:
+ """
+ A POST call to /json/invites//resend should send an invitation reminder email
+ and delete any scheduled invitation reminder email.
+ """
+ self.login("iago")
+ invitee = "resend_me@zulip.com"
+
+ self.assert_json_success(self.invite(invitee, ["Denmark"]))
+ prereg_user = PreregistrationUser.objects.get(email=invitee)
+
+ # Verify and then clear from the outbox the original invite email
+ self.check_sent_emails([invitee])
+ from django.core.mail import outbox
+
+ outbox.pop()
+
+ # Verify that the scheduled email exists.
+ scheduledemail_filter = ScheduledEmail.objects.filter(
+ address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
+ )
+ self.assertEqual(scheduledemail_filter.count(), 1)
+ original_timestamp = scheduledemail_filter.values_list("scheduled_timestamp", flat=True)
+
+ # Resend invite
+ result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
+ self.assertEqual(
+ ScheduledEmail.objects.filter(
+ address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
+ ).count(),
+ 1,
+ )
+
+ # Check that we have exactly one scheduled email, and that it is different
+ self.assertEqual(scheduledemail_filter.count(), 1)
+ self.assertNotEqual(
+ original_timestamp, scheduledemail_filter.values_list("scheduled_timestamp", flat=True)
+ )
+
+ self.assertEqual(result.status_code, 200)
+ error_result = self.client_post("/json/invites/" + str(9999) + "/resend")
+ self.assert_json_error(error_result, "No such invitation")
+
+ self.check_sent_emails([invitee])
+
+ def test_successful_member_resend_invitation(self) -> None:
+ """A POST call from member a account to /json/invites//resend
+ should send an invitation reminder email and delete any
+ scheduled invitation reminder email if they send the invite.
+ """
+ self.login("hamlet")
+ user_profile = self.example_user("hamlet")
+ invitee = "resend_me@zulip.com"
+ self.assert_json_success(self.invite(invitee, ["Denmark"]))
+ # Verify hamlet has only one invitation (Member can resend invitations only sent by him).
+ invitation = PreregistrationUser.objects.filter(referred_by=user_profile)
+ self.assert_length(invitation, 1)
+ prereg_user = PreregistrationUser.objects.get(email=invitee)
+
+ # Verify and then clear from the outbox the original invite email
+ self.check_sent_emails([invitee])
+ from django.core.mail import outbox
+
+ outbox.pop()
+
+ # Verify that the scheduled email exists.
+ scheduledemail_filter = ScheduledEmail.objects.filter(
+ address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
+ )
+ self.assertEqual(scheduledemail_filter.count(), 1)
+ original_timestamp = scheduledemail_filter.values_list("scheduled_timestamp", flat=True)
+
+ # Resend invite
+ result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
+ self.assertEqual(
+ ScheduledEmail.objects.filter(
+ address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
+ ).count(),
+ 1,
+ )
+
+ # Check that we have exactly one scheduled email, and that it is different
+ self.assertEqual(scheduledemail_filter.count(), 1)
+ self.assertNotEqual(
+ original_timestamp, scheduledemail_filter.values_list("scheduled_timestamp", flat=True)
+ )
+
+ self.assertEqual(result.status_code, 200)
+ error_result = self.client_post("/json/invites/" + str(9999) + "/resend")
+ self.assert_json_error(error_result, "No such invitation")
+
+ self.check_sent_emails([invitee])
+
+ self.logout()
+ self.login("othello")
+ invitee = "TestOne@zulip.com"
+ prereg_user_one = PreregistrationUser(email=invitee, referred_by=user_profile)
+ prereg_user_one.save()
+ prereg_user = PreregistrationUser.objects.get(email=invitee)
+ error_result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
+ self.assert_json_error(error_result, "Must be an organization administrator")
+
+ def test_resend_owner_invitation(self) -> None:
+ self.login("desdemona")
+
+ invitee = "resend_owner@zulip.com"
+ self.assert_json_success(
+ self.invite(
+ invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["REALM_OWNER"]
+ )
+ )
+ self.check_sent_emails([invitee])
+ scheduledemail_filter = ScheduledEmail.objects.filter(
+ address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
+ )
+ self.assertEqual(scheduledemail_filter.count(), 1)
+ original_timestamp = scheduledemail_filter.values_list("scheduled_timestamp", flat=True)
+
+ # Test only organization owners can resend owner invitation.
+ self.login("iago")
+ prereg_user = PreregistrationUser.objects.get(email=invitee)
+ error_result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
+ self.assert_json_error(error_result, "Must be an organization owner")
+
+ self.login("desdemona")
+ result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
+ self.assert_json_success(result)
+
+ self.assertEqual(
+ ScheduledEmail.objects.filter(
+ address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
+ ).count(),
+ 1,
+ )
+
+ # Check that we have exactly one scheduled email, and that it is different
+ self.assertEqual(scheduledemail_filter.count(), 1)
+ self.assertNotEqual(
+ original_timestamp, scheduledemail_filter.values_list("scheduled_timestamp", flat=True)
+ )
+
+ def test_resend_never_expiring_invitation(self) -> None:
+ self.login("iago")
+ invitee = "resend@zulip.com"
+
+ self.assert_json_success(self.invite(invitee, ["Denmark"], None))
+ prereg_user = PreregistrationUser.objects.get(email=invitee)
+
+ # Verify and then clear from the outbox the original invite email
+ self.check_sent_emails([invitee])
+ from django.core.mail import outbox
+
+ outbox.pop()
+
+ result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
+ self.assert_json_success(result)
+ self.check_sent_emails([invitee])
+
+ def test_accessing_invites_in_another_realm(self) -> None:
+ inviter = UserProfile.objects.exclude(realm=get_realm("zulip")).first()
+ assert inviter is not None
+ prereg_user = PreregistrationUser.objects.create(
+ email="email", referred_by=inviter, realm=inviter.realm
+ )
+ self.login("iago")
+ error_result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
+ self.assert_json_error(error_result, "No such invitation")
+ error_result = self.client_delete("/json/invites/" + str(prereg_user.id))
+ self.assert_json_error(error_result, "No such invitation")
+
+ def test_prereg_user_status(self) -> None:
+ email = self.nonreg_email("alice")
+ password = "password"
+ realm = get_realm("zulip")
+
+ inviter = UserProfile.objects.filter(realm=realm).first()
+ prereg_user = PreregistrationUser.objects.create(
+ email=email, referred_by=inviter, realm=realm
+ )
+
+ confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
+ registration_key = confirmation_link.split("/")[-1]
+
+ result = self.client_post(
+ "/accounts/register/",
+ {"key": registration_key, "from_confirmation": "1", "full_name": "alice"},
+ )
+ self.assertEqual(result.status_code, 200)
+ confirmation = Confirmation.objects.get(confirmation_key=registration_key)
+ assert confirmation.content_object is not None
+ prereg_user = confirmation.content_object
+ self.assertEqual(prereg_user.status, 0)
+
+ result = self.submit_reg_form_for_user(email, password, key=registration_key)
+ self.assertEqual(result.status_code, 302)
+ prereg_user = PreregistrationUser.objects.get(email=email, referred_by=inviter, realm=realm)
+ self.assertEqual(prereg_user.status, confirmation_settings.STATUS_USED)
+ user = get_user_by_delivery_email(email, realm)
+ self.assertIsNotNone(user)
+ self.assertEqual(user.delivery_email, email)
+
+
+class InviteeEmailsParserTests(ZulipTestCase):
+ def setUp(self) -> None:
+ super().setUp()
+ self.email1 = "email1@zulip.com"
+ self.email2 = "email2@zulip.com"
+ self.email3 = "email3@zulip.com"
+
+ def test_if_emails_separated_by_commas_are_parsed_and_striped_correctly(self) -> None:
+ emails_raw = f"{self.email1} ,{self.email2}, {self.email3}"
+ expected_set = {self.email1, self.email2, self.email3}
+ self.assertEqual(get_invitee_emails_set(emails_raw), expected_set)
+
+ def test_if_emails_separated_by_newlines_are_parsed_and_striped_correctly(self) -> None:
+ emails_raw = f"{self.email1}\n {self.email2}\n {self.email3} "
+ expected_set = {self.email1, self.email2, self.email3}
+ self.assertEqual(get_invitee_emails_set(emails_raw), expected_set)
+
+ def test_if_emails_from_email_client_separated_by_newlines_are_parsed_correctly(self) -> None:
+ emails_raw = (
+ f"Email One <{self.email1}>\nEmailTwo<{self.email2}>\nEmail Three<{self.email3}>"
+ )
+ expected_set = {self.email1, self.email2, self.email3}
+ self.assertEqual(get_invitee_emails_set(emails_raw), expected_set)
+
+ def test_if_emails_in_mixed_style_are_parsed_correctly(self) -> None:
+ emails_raw = f"Email One <{self.email1}>,EmailTwo<{self.email2}>\n{self.email3}"
+ expected_set = {self.email1, self.email2, self.email3}
+ self.assertEqual(get_invitee_emails_set(emails_raw), expected_set)
+
+
+class MultiuseInviteTest(ZulipTestCase):
+ def setUp(self) -> None:
+ super().setUp()
+ self.realm = get_realm("zulip")
+ self.realm.invite_required = True
+ self.realm.save()
+
+ def generate_multiuse_invite_link(
+ self, streams: Optional[List[Stream]] = None, date_sent: Optional[datetime.datetime] = None
+ ) -> str:
+ invite = MultiuseInvite(realm=self.realm, referred_by=self.example_user("iago"))
+ invite.save()
+
+ if streams is not None:
+ invite.streams.set(streams)
+
+ if date_sent is None:
+ date_sent = timezone_now()
+ validity_in_minutes = 2 * 24 * 60
+ with patch("confirmation.models.timezone_now", return_value=date_sent):
+ return create_confirmation_link(
+ invite, Confirmation.MULTIUSE_INVITE, validity_in_minutes=validity_in_minutes
+ )
+
+ def check_user_able_to_register(self, email: str, invite_link: str) -> None:
+ password = "password"
+
+ result = self.client_post(invite_link, {"email": email})
+ self.assertEqual(result.status_code, 302)
+ self.assertTrue(
+ result["Location"].endswith(
+ f"/accounts/send_confirm/?email={urllib.parse.quote(email)}"
+ )
+ )
+ result = self.client_get(result["Location"])
+ self.assert_in_response("Check your email", result)
+
+ confirmation_url = self.get_confirmation_url_from_outbox(email)
+ result = self.client_get(confirmation_url)
+ self.assertEqual(result.status_code, 200)
+
+ result = self.submit_reg_form_for_user(email, password)
+ self.assertEqual(result.status_code, 302)
+
+ # Verify the PreregistrationUser object was set up as expected.
+ prereg_user = PreregistrationUser.objects.last()
+ multiuse_invite = MultiuseInvite.objects.last()
+
+ assert prereg_user is not None
+ self.assertEqual(prereg_user.email, email)
+ self.assertEqual(prereg_user.multiuse_invite, multiuse_invite)
+
+ from django.core.mail import outbox
+
+ outbox.pop()
+
+ def test_valid_multiuse_link(self) -> None:
+ email1 = self.nonreg_email("test")
+ email2 = self.nonreg_email("test1")
+ email3 = self.nonreg_email("alice")
+
+ date_sent = timezone_now() - datetime.timedelta(days=1)
+ invite_link = self.generate_multiuse_invite_link(date_sent=date_sent)
+
+ self.check_user_able_to_register(email1, invite_link)
+ self.check_user_able_to_register(email2, invite_link)
+ self.check_user_able_to_register(email3, invite_link)
+
+ def test_expired_multiuse_link(self) -> None:
+ email = self.nonreg_email("newuser")
+ date_sent = timezone_now() - datetime.timedelta(
+ days=settings.INVITATION_LINK_VALIDITY_DAYS + 1
+ )
+ invite_link = self.generate_multiuse_invite_link(date_sent=date_sent)
+ result = self.client_post(invite_link, {"email": email})
+
+ self.assertEqual(result.status_code, 404)
+ self.assert_in_response("The confirmation link has expired or been deactivated.", result)
+
+ def test_revoked_multiuse_link(self) -> None:
+ email = self.nonreg_email("newuser")
+ invite_link = self.generate_multiuse_invite_link()
+ multiuse_invite = MultiuseInvite.objects.last()
+ assert multiuse_invite is not None
+ do_revoke_multi_use_invite(multiuse_invite)
+
+ result = self.client_post(invite_link, {"email": email})
+
+ self.assertEqual(result.status_code, 404)
+ self.assert_in_response("We couldn't find your confirmation link in the system.", result)
+
+ def test_invalid_multiuse_link(self) -> None:
+ email = self.nonreg_email("newuser")
+ invite_link = "/join/invalid_key/"
+ result = self.client_post(invite_link, {"email": email})
+
+ self.assertEqual(result.status_code, 404)
+ self.assert_in_response("Whoops. The confirmation link is malformed.", result)
+
+ def test_invalid_multiuse_link_in_open_realm(self) -> None:
+ self.realm.invite_required = False
+ self.realm.save()
+
+ email = self.nonreg_email("newuser")
+ invite_link = "/join/invalid_key/"
+
+ with patch("zerver.views.registration.get_realm_from_request", return_value=self.realm):
+ with patch("zerver.views.registration.get_realm", return_value=self.realm):
+ self.check_user_able_to_register(email, invite_link)
+
+ def test_multiuse_link_with_specified_streams(self) -> None:
+ name1 = "newuser"
+ name2 = "bob"
+ email1 = self.nonreg_email(name1)
+ email2 = self.nonreg_email(name2)
+
+ stream_names = ["Rome", "Scotland", "Venice"]
+ streams = [get_stream(stream_name, self.realm) for stream_name in stream_names]
+ invite_link = self.generate_multiuse_invite_link(streams=streams)
+ self.check_user_able_to_register(email1, invite_link)
+ self.check_user_subscribed_only_to_streams(name1, streams)
+
+ stream_names = ["Rome", "Verona"]
+ streams = [get_stream(stream_name, self.realm) for stream_name in stream_names]
+ invite_link = self.generate_multiuse_invite_link(streams=streams)
+ self.check_user_able_to_register(email2, invite_link)
+ self.check_user_subscribed_only_to_streams(name2, streams)
+
+ def test_multiuse_link_different_realms(self) -> None:
+ """
+ Verify that an invitation generated for one realm can't be used
+ to join another.
+ """
+ lear_realm = get_realm("lear")
+ self.realm = lear_realm
+ invite_link = self.generate_multiuse_invite_link(streams=[])
+ key = invite_link.split("/")[-2]
+
+ result = self.client_get(f"/join/{key}/", subdomain="zulip")
+ self.assertEqual(result.status_code, 404)
+ self.assert_in_response(
+ "Whoops. We couldn't find your confirmation link in the system.", result
+ )
+
+ # Now we want to test the accounts_home function, which can't be used
+ # for the multiuse invite case via an HTTP request, but is still supposed
+ # to do its own verification that the realms match as a hardening measure
+ # against a caller that fails to do that.
+ request = HttpRequest()
+ confirmation = Confirmation.objects.get(confirmation_key=key)
+ multiuse_object = confirmation.content_object
+ with patch(
+ "zerver.views.registration.get_subdomain", return_value="zulip"
+ ), self.assertRaises(AssertionError):
+ accounts_home(request, multiuse_object=multiuse_object)
+
+ def test_create_multiuse_link_api_call(self) -> None:
+ self.login("iago")
+
+ result = self.client_post(
+ "/json/invites/multiuse", {"invite_expires_in_minutes": 2 * 24 * 60}
+ )
+ invite_link = self.assert_json_success(result)["invite_link"]
+ self.check_user_able_to_register(self.nonreg_email("test"), invite_link)
+
+ def test_create_multiuse_link_with_specified_streams_api_call(self) -> None:
+ self.login("iago")
+ stream_names = ["Rome", "Scotland", "Venice"]
+ streams = [get_stream(stream_name, self.realm) for stream_name in stream_names]
+ stream_ids = [stream.id for stream in streams]
+
+ result = self.client_post(
+ "/json/invites/multiuse",
+ {
+ "stream_ids": orjson.dumps(stream_ids).decode(),
+ "invite_expires_in_minutes": 2 * 24 * 60,
+ },
+ )
+ invite_link = self.assert_json_success(result)["invite_link"]
+ self.check_user_able_to_register(self.nonreg_email("test"), invite_link)
+ self.check_user_subscribed_only_to_streams("test", streams)
+
+ def test_only_admin_can_create_multiuse_link_api_call(self) -> None:
+ self.login("iago")
+ # Only admins should be able to create multiuse invites even if
+ # invite_to_realm_policy is set to Realm.POLICY_MEMBERS_ONLY.
+ self.realm.invite_to_realm_policy = Realm.POLICY_MEMBERS_ONLY
+ self.realm.save()
+
+ result = self.client_post(
+ "/json/invites/multiuse", {"invite_expires_in_minutes": 2 * 24 * 60}
+ )
+ invite_link = self.assert_json_success(result)["invite_link"]
+ self.check_user_able_to_register(self.nonreg_email("test"), invite_link)
+
+ self.login("hamlet")
+ result = self.client_post("/json/invites/multiuse")
+ self.assert_json_error(result, "Must be an organization administrator")
+
+ def test_multiuse_link_for_inviting_as_owner(self) -> None:
+ self.login("iago")
+ result = self.client_post(
+ "/json/invites/multiuse",
+ {
+ "invite_as": orjson.dumps(PreregistrationUser.INVITE_AS["REALM_OWNER"]).decode(),
+ "invite_expires_in_minutes": 2 * 24 * 60,
+ },
+ )
+ self.assert_json_error(result, "Must be an organization owner")
+
+ self.login("desdemona")
+ result = self.client_post(
+ "/json/invites/multiuse",
+ {
+ "invite_as": orjson.dumps(PreregistrationUser.INVITE_AS["REALM_OWNER"]).decode(),
+ "invite_expires_in_minutes": 2 * 24 * 60,
+ },
+ )
+ invite_link = self.assert_json_success(result)["invite_link"]
+ self.check_user_able_to_register(self.nonreg_email("test"), invite_link)
+
+ def test_create_multiuse_link_invalid_stream_api_call(self) -> None:
+ self.login("iago")
+ result = self.client_post(
+ "/json/invites/multiuse",
+ {
+ "stream_ids": orjson.dumps([54321]).decode(),
+ "invite_expires_in_minutes": 2 * 24 * 60,
+ },
+ )
+ self.assert_json_error(result, "Invalid stream ID 54321. No invites were sent.")
+
+ def test_create_multiuse_link_invalid_invite_as_api_call(self) -> None:
+ self.login("iago")
+ result = self.client_post(
+ "/json/invites/multiuse",
+ {
+ "invite_as": orjson.dumps(PreregistrationUser.INVITE_AS["GUEST_USER"] + 1).decode(),
+ "invite_expires_in_minutes": 2 * 24 * 60,
+ },
+ )
+ self.assert_json_error(result, "Invalid invite_as")
diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py
index 2e46d4abca..3ff63cf1c7 100644
--- a/zerver/tests/test_signup.py
+++ b/zerver/tests/test_signup.py
@@ -2,7 +2,7 @@ import datetime
import re
import time
import urllib
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union
+from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union
from unittest.mock import MagicMock, patch
from urllib.parse import urlencode
@@ -11,46 +11,30 @@ from django.conf import settings
from django.contrib.auth.views import PasswordResetConfirmView
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
-from django.core.mail.message import EmailMultiAlternatives
-from django.http import HttpRequest, HttpResponse, HttpResponseBase
+from django.http import HttpResponse, HttpResponseBase
from django.template.response import TemplateResponse
from django.test import Client, override_settings
-from django.urls import reverse
from django.utils import translation
-from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _
-from confirmation import settings as confirmation_settings
from confirmation.models import (
Confirmation,
- ConfirmationKeyError,
- create_confirmation_link,
- get_object_from_key,
one_click_unsubscribe_link,
)
-from corporate.lib.stripe import get_latest_seat_count
from zerver.actions.create_realm import do_change_realm_subdomain, do_create_realm
-from zerver.actions.create_user import add_new_user_history, do_create_user, process_new_human_user
+from zerver.actions.create_user import add_new_user_history
from zerver.actions.default_streams import (
do_add_default_stream,
do_create_default_stream_group,
get_default_streams_for_realm,
)
-from zerver.actions.invites import (
- do_create_multiuse_invite_link,
- do_get_invites_controlled_by_user,
- do_invite_users,
- do_revoke_multi_use_invite,
-)
from zerver.actions.realm_settings import (
do_deactivate_realm,
do_set_realm_authentication_methods,
do_set_realm_property,
do_set_realm_user_default_setting,
)
-from zerver.actions.user_settings import do_change_full_name
from zerver.actions.users import change_user_is_active, do_change_user_role, do_deactivate_user
-from zerver.context_processors import common_context
from zerver.decorator import do_two_factor_login
from zerver.forms import HomepageForm, check_subdomain_available
from zerver.lib.email_notifications import enqueue_welcome_emails, followup_day2_email_delay
@@ -69,7 +53,6 @@ from zerver.lib.rate_limiter import add_ratelimit_rule, remove_ratelimit_rule
from zerver.lib.send_email import (
EmailNotDeliveredError,
FromAddress,
- deliver_scheduled_emails,
send_future_email,
)
from zerver.lib.stream_subscription import get_stream_subscriptions_for_user
@@ -86,7 +69,6 @@ from zerver.lib.test_helpers import (
message_stream_count,
most_recent_message,
most_recent_usermessage,
- queries_captured,
reset_email_visibility_to_everyone_in_zulip_realm,
)
from zerver.models import (
@@ -94,7 +76,6 @@ from zerver.models import (
CustomProfileFieldValue,
DefaultStream,
Message,
- MultiuseInvite,
PreregistrationUser,
Realm,
RealmAuditLog,
@@ -114,8 +95,6 @@ from zerver.models import (
)
from zerver.views.auth import redirect_and_log_into_subdomain, start_two_factor_auth
from zerver.views.development.registration import confirmation_key
-from zerver.views.invite import INVITATION_LINK_VALIDITY_MINUTES, get_invitee_emails_set
-from zerver.views.registration import accounts_home
from zproject.backends import ExternalAuthDataDict, ExternalAuthResult, email_auth_enabled
if TYPE_CHECKING:
@@ -1117,2126 +1096,6 @@ https://www.google.com/images/srpr/logo4w.png
"""
self.assertEqual(response.status_code, 200)
-class InviteUserBase(ZulipTestCase):
- def check_sent_emails(self, correct_recipients: List[str]) -> None:
- from django.core.mail import outbox
-
- self.assert_length(outbox, len(correct_recipients))
- email_recipients = [email.recipients()[0] for email in outbox]
- self.assertEqual(sorted(email_recipients), sorted(correct_recipients))
- if len(outbox) == 0:
- return
-
- self.assertIn("Zulip", self.email_display_from(outbox[0]))
-
- self.assertEqual(self.email_envelope_from(outbox[0]), settings.NOREPLY_EMAIL_ADDRESS)
- self.assertRegex(
- self.email_display_from(outbox[0]), rf" <{self.TOKENIZED_NOREPLY_REGEX}>\Z"
- )
-
- self.assertEqual(outbox[0].extra_headers["List-Id"], "Zulip Dev ")
-
- def invite(
- self,
- invitee_emails: str,
- stream_names: Sequence[str],
- invite_expires_in_minutes: Optional[int] = INVITATION_LINK_VALIDITY_MINUTES,
- body: str = "",
- invite_as: int = PreregistrationUser.INVITE_AS["MEMBER"],
- ) -> "TestHttpResponse":
- """
- Invites the specified users to Zulip with the specified streams.
-
- users should be a string containing the users to invite, comma or
- newline separated.
-
- streams should be a list of strings.
- """
- stream_ids = []
- for stream_name in stream_names:
- stream_ids.append(self.get_stream_id(stream_name))
-
- invite_expires_in: Union[str, Optional[int]] = invite_expires_in_minutes
- if invite_expires_in is None:
- invite_expires_in = orjson.dumps(None).decode()
-
- return self.client_post(
- "/json/invites",
- {
- "invitee_emails": invitee_emails,
- "invite_expires_in_minutes": invite_expires_in,
- "stream_ids": orjson.dumps(stream_ids).decode(),
- "invite_as": invite_as,
- },
- )
-
-
-class InviteUserTest(InviteUserBase):
- def test_successful_invite_user(self) -> None:
- """
- A call to /json/invites with valid parameters causes an invitation
- email to be sent.
- """
- self.login("hamlet")
- invitee = "alice-test@zulip.com"
- self.assert_json_success(self.invite(invitee, ["Denmark"]))
- self.assertTrue(find_key_by_email(invitee))
- self.check_sent_emails([invitee])
-
- def test_newbie_restrictions(self) -> None:
- user_profile = self.example_user("hamlet")
- invitee = "alice-test@zulip.com"
- stream_name = "Denmark"
-
- self.login_user(user_profile)
-
- result = self.invite(invitee, [stream_name])
- self.assert_json_success(result)
-
- user_profile.date_joined = timezone_now() - datetime.timedelta(days=10)
- user_profile.save()
-
- with self.settings(INVITES_MIN_USER_AGE_DAYS=5):
- result = self.invite(invitee, [stream_name])
- self.assert_json_success(result)
-
- with self.settings(INVITES_MIN_USER_AGE_DAYS=15):
- result = self.invite(invitee, [stream_name])
- self.assert_json_error_contains(result, "Your account is too new")
-
- def test_invite_limits(self) -> None:
- user_profile = self.example_user("hamlet")
- realm = user_profile.realm
- stream_name = "Denmark"
-
- # These constants only need to be in descending order
- # for this test to trigger an InvitationError based
- # on max daily counts.
- site_max = 50
- realm_max = 40
- num_invitees = 30
- max_daily_count = 20
-
- daily_counts = [(1, max_daily_count)]
-
- invite_emails = [f"foo-{i:02}@zulip.com" for i in range(num_invitees)]
- invitees = ",".join(invite_emails)
-
- self.login_user(user_profile)
-
- realm.max_invites = realm_max
- realm.date_created = timezone_now()
- realm.save()
-
- def try_invite() -> "TestHttpResponse":
- with self.settings(
- OPEN_REALM_CREATION=True,
- INVITES_DEFAULT_REALM_DAILY_MAX=site_max,
- INVITES_NEW_REALM_LIMIT_DAYS=daily_counts,
- ):
- result = self.invite(invitees, [stream_name])
- return result
-
- result = try_invite()
- self.assert_json_error_contains(result, "reached the limit")
-
- # Next show that aggregate limits expire once the realm is old
- # enough.
-
- realm.date_created = timezone_now() - datetime.timedelta(days=8)
- realm.save()
-
- with queries_captured() as queries:
- with cache_tries_captured() as cache_tries:
- result = try_invite()
-
- self.assert_json_success(result)
-
- # TODO: Fix large query count here.
- #
- # TODO: There is some test OTHER than this one
- # that is leaking some kind of state change
- # that throws off the query count here. It
- # is hard to investigate currently (due to
- # the large number of queries), so I just
- # use an approximate equality check.
- actual_count = len(queries)
- expected_count = 251
- if abs(actual_count - expected_count) > 1:
- raise AssertionError(
- f"""
- Unexpected number of queries:
-
- expected query count: {expected_count}
- actual: {actual_count}
- """
- )
-
- # Almost all of these cache hits are to re-fetch each one of the
- # invitees. These happen inside our queue processor for sending
- # confirmation emails, so they are somewhat difficult to avoid.
- #
- # TODO: Mock the call to queue_json_publish, so we can measure the
- # queue impact separately from the user-perceived impact.
- self.assert_length(cache_tries, 32)
-
- # Next get line coverage on bumping a realm's max_invites.
- realm.date_created = timezone_now()
- realm.max_invites = site_max + 10
- realm.save()
-
- result = try_invite()
- self.assert_json_success(result)
-
- # Finally get coverage on the case that OPEN_REALM_CREATION is False.
-
- with self.settings(OPEN_REALM_CREATION=False):
- result = self.invite(invitees, [stream_name])
-
- self.assert_json_success(result)
-
- def test_invite_user_to_realm_on_manual_license_plan(self) -> None:
- user = self.example_user("hamlet")
- self.login_user(user)
- _, ledger = self.subscribe_realm_to_monthly_plan_on_manual_license_management(
- user.realm, 50, 50
- )
-
- with self.settings(BILLING_ENABLED=True):
- result = self.invite(self.nonreg_email("alice"), ["Denmark"])
- self.assert_json_success(result)
-
- ledger.licenses_at_next_renewal = 5
- ledger.save(update_fields=["licenses_at_next_renewal"])
- with self.settings(BILLING_ENABLED=True):
- result = self.invite(self.nonreg_email("bob"), ["Denmark"])
- self.assert_json_success(result)
-
- ledger.licenses = get_latest_seat_count(user.realm) + 1
- ledger.save(update_fields=["licenses"])
- with self.settings(BILLING_ENABLED=True):
- invitee_emails = self.nonreg_email("bob") + "," + self.nonreg_email("alice")
- result = self.invite(invitee_emails, ["Denmark"])
- self.assert_json_error_contains(
- result, "Your organization does not have enough unused Zulip licenses to invite 2 users"
- )
-
- ledger.licenses = get_latest_seat_count(user.realm)
- ledger.save(update_fields=["licenses"])
- with self.settings(BILLING_ENABLED=True):
- result = self.invite(self.nonreg_email("bob"), ["Denmark"])
- self.assert_json_error_contains(
- result, "All Zulip licenses for this organization are currently in use"
- )
-
- with self.settings(BILLING_ENABLED=True):
- result = self.invite(
- self.nonreg_email("bob"),
- ["Denmark"],
- invite_as=PreregistrationUser.INVITE_AS["GUEST_USER"],
- )
- self.assert_json_success(result)
-
- def test_cross_realm_bot(self) -> None:
- inviter = self.example_user("hamlet")
- self.login_user(inviter)
-
- cross_realm_bot_email = "emailgateway@zulip.com"
- legit_new_email = "fred@zulip.com"
- invitee_emails = ",".join([cross_realm_bot_email, legit_new_email])
-
- result = self.invite(invitee_emails, ["Denmark"])
- self.assert_json_error(
- result,
- (
- "Some of those addresses are already using Zulip, so we didn't send them an"
- " invitation. We did send invitations to everyone else!"
- ),
- )
-
- def test_invite_mirror_dummy_user(self) -> None:
- """
- A mirror dummy account is a temporary account
- that we keep in our system if we are mirroring
- data from something like Zephyr or IRC.
-
- We want users to eventually just sign up or
- register for Zulip, in which case we will just
- fully "activate" the account.
-
- Here we test that you can invite a person who
- has a mirror dummy account.
- """
- inviter = self.example_user("hamlet")
- self.login_user(inviter)
-
- mirror_user = self.example_user("cordelia")
- mirror_user.is_mirror_dummy = True
- mirror_user.save()
- change_user_is_active(mirror_user, False)
-
- self.assertEqual(
- PreregistrationUser.objects.filter(email=mirror_user.email).count(),
- 0,
- )
-
- result = self.invite(mirror_user.email, ["Denmark"])
- self.assert_json_success(result)
-
- prereg_user = PreregistrationUser.objects.get(email=mirror_user.email)
- assert prereg_user.referred_by is not None and inviter is not None
- self.assertEqual(
- prereg_user.referred_by.email,
- inviter.email,
- )
-
- def test_invite_from_now_deactivated_user(self) -> None:
- """
- While accepting an invitation from a user,
- processing for a new user account will only
- be completed if the inviter is not deactivated
- after sending the invite.
- """
- inviter = self.example_user("hamlet")
- self.login_user(inviter)
- invitee = self.nonreg_email("alice")
-
- result = self.invite(invitee, ["Denmark"])
- self.assert_json_success(result)
-
- prereg_user = PreregistrationUser.objects.get(email=invitee)
- change_user_is_active(inviter, False)
- do_create_user(
- invitee,
- "password",
- inviter.realm,
- "full name",
- prereg_user=prereg_user,
- acting_user=None,
- )
-
- def test_successful_invite_user_as_owner_from_owner_account(self) -> None:
- self.login("desdemona")
- invitee = self.nonreg_email("alice")
- result = self.invite(
- invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["REALM_OWNER"]
- )
- self.assert_json_success(result)
- self.assertTrue(find_key_by_email(invitee))
-
- self.submit_reg_form_for_user(invitee, "password")
- invitee_profile = self.nonreg_user("alice")
- self.assertTrue(invitee_profile.is_realm_owner)
- self.assertFalse(invitee_profile.is_guest)
- self.check_user_added_in_system_group(invitee_profile)
-
- def test_invite_user_as_owner_from_admin_account(self) -> None:
- self.login("iago")
- invitee = self.nonreg_email("alice")
- response = self.invite(
- invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["REALM_OWNER"]
- )
- self.assert_json_error(response, "Must be an organization owner")
-
- def test_successful_invite_user_as_admin_from_admin_account(self) -> None:
- self.login("iago")
- invitee = self.nonreg_email("alice")
- result = self.invite(
- invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["REALM_ADMIN"]
- )
- self.assert_json_success(result)
- self.assertTrue(find_key_by_email(invitee))
-
- self.submit_reg_form_for_user(invitee, "password")
- invitee_profile = self.nonreg_user("alice")
- self.assertTrue(invitee_profile.is_realm_admin)
- self.assertFalse(invitee_profile.is_realm_owner)
- self.assertFalse(invitee_profile.is_guest)
- self.check_user_added_in_system_group(invitee_profile)
-
- def test_invite_user_as_admin_from_normal_account(self) -> None:
- self.login("hamlet")
- invitee = self.nonreg_email("alice")
- response = self.invite(
- invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["REALM_ADMIN"]
- )
- self.assert_json_error(response, "Must be an organization administrator")
-
- def test_successful_invite_user_as_moderator_from_admin_account(self) -> None:
- self.login("iago")
- invitee = self.nonreg_email("alice")
- result = self.invite(
- invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["MODERATOR"]
- )
- self.assert_json_success(result)
- self.assertTrue(find_key_by_email(invitee))
-
- self.submit_reg_form_for_user(invitee, "password")
- invitee_profile = self.nonreg_user("alice")
- self.assertFalse(invitee_profile.is_realm_admin)
- self.assertTrue(invitee_profile.is_moderator)
- self.assertFalse(invitee_profile.is_guest)
- self.check_user_added_in_system_group(invitee_profile)
-
- def test_invite_user_as_moderator_from_normal_account(self) -> None:
- self.login("hamlet")
- invitee = self.nonreg_email("alice")
- response = self.invite(
- invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["MODERATOR"]
- )
- self.assert_json_error(response, "Must be an organization administrator")
-
- def test_invite_user_as_moderator_from_moderator_account(self) -> None:
- self.login("shiva")
- invitee = self.nonreg_email("alice")
- response = self.invite(
- invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["MODERATOR"]
- )
- self.assert_json_error(response, "Must be an organization administrator")
-
- def test_invite_user_as_invalid_type(self) -> None:
- """
- Test inviting a user as invalid type of user i.e. type of invite_as
- is not in PreregistrationUser.INVITE_AS
- """
- self.login("iago")
- invitee = self.nonreg_email("alice")
- response = self.invite(invitee, ["Denmark"], invite_as=10)
- self.assert_json_error(response, "Invalid invite_as")
-
- def test_successful_invite_user_as_guest_from_normal_account(self) -> None:
- self.login("hamlet")
- invitee = self.nonreg_email("alice")
- self.assert_json_success(
- self.invite(invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["GUEST_USER"])
- )
- self.assertTrue(find_key_by_email(invitee))
-
- self.submit_reg_form_for_user(invitee, "password")
- invitee_profile = self.nonreg_user("alice")
- self.assertFalse(invitee_profile.is_realm_admin)
- self.assertTrue(invitee_profile.is_guest)
- self.check_user_added_in_system_group(invitee_profile)
-
- def test_successful_invite_user_as_guest_from_admin_account(self) -> None:
- self.login("iago")
- invitee = self.nonreg_email("alice")
- self.assert_json_success(
- self.invite(invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["GUEST_USER"])
- )
- self.assertTrue(find_key_by_email(invitee))
-
- self.submit_reg_form_for_user(invitee, "password")
- invitee_profile = self.nonreg_user("alice")
- self.assertFalse(invitee_profile.is_realm_admin)
- self.assertTrue(invitee_profile.is_guest)
- self.check_user_added_in_system_group(invitee_profile)
-
- def test_successful_invite_user_with_name(self) -> None:
- """
- A call to /json/invites with valid parameters causes an invitation
- email to be sent.
- """
- self.login("hamlet")
- email = "alice-test@zulip.com"
- invitee = f"Alice Test <{email}>"
- self.assert_json_success(self.invite(invitee, ["Denmark"]))
- self.assertTrue(find_key_by_email(email))
- self.check_sent_emails([email])
-
- def test_successful_invite_user_with_name_and_normal_one(self) -> None:
- """
- A call to /json/invites with valid parameters causes an invitation
- email to be sent.
- """
- self.login("hamlet")
- email = "alice-test@zulip.com"
- email2 = "bob-test@zulip.com"
- invitee = f"Alice Test <{email}>, {email2}"
- self.assert_json_success(self.invite(invitee, ["Denmark"]))
- self.assertTrue(find_key_by_email(email))
- self.assertTrue(find_key_by_email(email2))
- self.check_sent_emails([email, email2])
-
- def test_can_invite_others_to_realm(self) -> None:
- def validation_func(user_profile: UserProfile) -> bool:
- user_profile.refresh_from_db()
- return user_profile.can_invite_others_to_realm()
-
- realm = get_realm("zulip")
- do_set_realm_property(
- realm, "invite_to_realm_policy", Realm.POLICY_NOBODY, acting_user=None
- )
- desdemona = self.example_user("desdemona")
- self.assertFalse(validation_func(desdemona))
-
- self.check_has_permission_policies("invite_to_realm_policy", validation_func)
-
- def test_invite_others_to_realm_setting(self) -> None:
- """
- The invite_to_realm_policy realm setting works properly.
- """
- realm = get_realm("zulip")
- do_set_realm_property(
- realm, "invite_to_realm_policy", Realm.POLICY_NOBODY, acting_user=None
- )
- self.login("desdemona")
- email = "alice-test@zulip.com"
- email2 = "bob-test@zulip.com"
- invitee = f"Alice Test <{email}>, {email2}"
- self.assert_json_error(
- self.invite(invitee, ["Denmark"]),
- "Insufficient permission",
- )
-
- do_set_realm_property(
- realm, "invite_to_realm_policy", Realm.POLICY_ADMINS_ONLY, acting_user=None
- )
-
- self.login("shiva")
- self.assert_json_error(
- self.invite(invitee, ["Denmark"]),
- "Insufficient permission",
- )
-
- # Now verify an administrator can do it
- self.login("iago")
- self.assert_json_success(self.invite(invitee, ["Denmark"]))
- self.assertTrue(find_key_by_email(email))
- self.assertTrue(find_key_by_email(email2))
-
- self.check_sent_emails([email, email2])
-
- from django.core import mail
-
- mail.outbox = []
-
- do_set_realm_property(
- realm, "invite_to_realm_policy", Realm.POLICY_MODERATORS_ONLY, acting_user=None
- )
- self.login("hamlet")
- email = "carol-test@zulip.com"
- email2 = "earl-test@zulip.com"
- invitee = f"Carol Test <{email}>, {email2}"
- self.assert_json_error(
- self.invite(invitee, ["Denmark"]),
- "Insufficient permission",
- )
-
- self.login("shiva")
- self.assert_json_success(self.invite(invitee, ["Denmark"]))
- self.assertTrue(find_key_by_email(email))
- self.assertTrue(find_key_by_email(email2))
- self.check_sent_emails([email, email2])
-
- mail.outbox = []
-
- do_set_realm_property(
- realm, "invite_to_realm_policy", Realm.POLICY_MEMBERS_ONLY, acting_user=None
- )
-
- self.login("polonius")
- email = "dave-test@zulip.com"
- email2 = "mark-test@zulip.com"
- invitee = f"Dave Test <{email}>, {email2}"
- self.assert_json_error(self.invite(invitee, ["Denmark"]), "Not allowed for guest users")
-
- self.login("hamlet")
- self.assert_json_success(self.invite(invitee, ["Denmark"]))
- self.assertTrue(find_key_by_email(email))
- self.assertTrue(find_key_by_email(email2))
- self.check_sent_emails([email, email2])
-
- mail.outbox = []
-
- do_set_realm_property(
- realm, "invite_to_realm_policy", Realm.POLICY_FULL_MEMBERS_ONLY, acting_user=None
- )
- do_set_realm_property(realm, "waiting_period_threshold", 1000, acting_user=None)
-
- hamlet = self.example_user("hamlet")
- hamlet.date_joined = timezone_now() - datetime.timedelta(
- days=(realm.waiting_period_threshold - 1)
- )
-
- email = "issac-test@zulip.com"
- email2 = "steven-test@zulip.com"
- invitee = f"Issac Test <{email}>, {email2}"
- self.assert_json_error(
- self.invite(invitee, ["Denmark"]),
- "Insufficient permission",
- )
-
- do_set_realm_property(realm, "waiting_period_threshold", 0, acting_user=None)
-
- self.assert_json_success(self.invite(invitee, ["Denmark"]))
- self.assertTrue(find_key_by_email(email))
- self.assertTrue(find_key_by_email(email2))
- self.check_sent_emails([email, email2])
-
- def test_invite_user_signup_initial_history(self) -> None:
- """
- Test that a new user invited to a stream receives some initial
- history but only from public streams.
- """
- self.login("hamlet")
- user_profile = self.example_user("hamlet")
- private_stream_name = "Secret"
- self.make_stream(private_stream_name, invite_only=True)
- self.subscribe(user_profile, private_stream_name)
- public_msg_id = self.send_stream_message(
- self.example_user("hamlet"),
- "Denmark",
- topic_name="Public topic",
- content="Public message",
- )
- secret_msg_id = self.send_stream_message(
- self.example_user("hamlet"),
- private_stream_name,
- topic_name="Secret topic",
- content="Secret message",
- )
- invitee = self.nonreg_email("alice")
- self.assert_json_success(self.invite(invitee, [private_stream_name, "Denmark"]))
- self.assertTrue(find_key_by_email(invitee))
-
- self.submit_reg_form_for_user(invitee, "password")
- invitee_profile = self.nonreg_user("alice")
- invitee_msg_ids = [
- um.message_id for um in UserMessage.objects.filter(user_profile=invitee_profile)
- ]
- self.assertTrue(public_msg_id in invitee_msg_ids)
- self.assertFalse(secret_msg_id in invitee_msg_ids)
- self.assertFalse(invitee_profile.is_realm_admin)
-
- invitee_msg, signups_stream_msg, inviter_msg, secret_msg = Message.objects.all().order_by(
- "-id"
- )[0:4]
-
- self.assertEqual(secret_msg.id, secret_msg_id)
-
- self.assertEqual(inviter_msg.sender.email, "notification-bot@zulip.com")
- self.assertTrue(
- inviter_msg.content.startswith(
- f"alice_zulip.com <`{invitee_profile.email}`> accepted your",
- )
- )
-
- self.assertEqual(signups_stream_msg.sender.email, "notification-bot@zulip.com")
- self.assertTrue(
- signups_stream_msg.content.startswith(
- f"@_**alice_zulip.com|{invitee_profile.id}** just signed up",
- )
- )
-
- self.assertEqual(invitee_msg.sender.email, "welcome-bot@zulip.com")
- self.assertTrue(invitee_msg.content.startswith("Hello, and welcome to Zulip!"))
- self.assertNotIn("demo organization", invitee_msg.content)
-
- def test_multi_user_invite(self) -> None:
- """
- Invites multiple users with a variety of delimiters.
- """
- self.login("hamlet")
- # Intentionally use a weird string.
- self.assert_json_success(
- self.invite(
- """bob-test@zulip.com, carol-test@zulip.com,
- dave-test@zulip.com
-
-
-earl-test@zulip.com""",
- ["Denmark"],
- )
- )
- for user in ("bob", "carol", "dave", "earl"):
- self.assertTrue(find_key_by_email(f"{user}-test@zulip.com"))
- self.check_sent_emails(
- [
- "bob-test@zulip.com",
- "carol-test@zulip.com",
- "dave-test@zulip.com",
- "earl-test@zulip.com",
- ]
- )
-
- def test_max_invites_model(self) -> None:
- realm = get_realm("zulip")
- self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
- realm.max_invites = 3
- realm.save()
- self.assertEqual(get_realm("zulip").max_invites, 3)
- realm.max_invites = settings.INVITES_DEFAULT_REALM_DAILY_MAX
- realm.save()
-
- def test_invite_too_many_users(self) -> None:
- # Only a light test of this pathway; e.g. doesn't test that
- # the limit gets reset after 24 hours
- self.login("iago")
- invitee_emails = "1@zulip.com, 2@zulip.com"
- self.invite(invitee_emails, ["Denmark"])
- invitee_emails = ", ".join(str(i) for i in range(get_realm("zulip").max_invites - 1))
- self.assert_json_error(
- self.invite(invitee_emails, ["Denmark"]),
- "To protect users, Zulip limits the number of invitations you can send in one day. Because you have reached the limit, no invitations were sent.",
- )
-
- def test_missing_or_invalid_params(self) -> None:
- """
- Tests inviting with various missing or invalid parameters.
- """
- realm = get_realm("zulip")
- do_set_realm_property(realm, "emails_restricted_to_domains", True, acting_user=None)
-
- self.login("hamlet")
- invitee_emails = "foo@zulip.com"
- self.assert_json_error(
- self.invite(invitee_emails, []),
- "You must specify at least one stream for invitees to join.",
- )
-
- for address in ("noatsign.com", "outsideyourdomain@example.net"):
- self.assert_json_error(
- self.invite(address, ["Denmark"]),
- "Some emails did not validate, so we didn't send any invitations.",
- )
- self.check_sent_emails([])
-
- self.assert_json_error(
- self.invite("", ["Denmark"]), "You must specify at least one email address."
- )
- self.check_sent_emails([])
-
- def test_guest_user_invitation(self) -> None:
- """
- Guest user can't invite new users
- """
- self.login("polonius")
- invitee = "alice-test@zulip.com"
- self.assert_json_error(self.invite(invitee, ["Denmark"]), "Not allowed for guest users")
- self.assertEqual(find_key_by_email(invitee), None)
- self.check_sent_emails([])
-
- def test_invalid_stream(self) -> None:
- """
- Tests inviting to a non-existent stream.
- """
- self.login("hamlet")
- self.assert_json_error(
- self.invite("iago-test@zulip.com", ["NotARealStream"]),
- f"Stream does not exist with id: {self.INVALID_STREAM_ID}. No invites were sent.",
- )
- self.check_sent_emails([])
-
- def test_invite_existing_user(self) -> None:
- """
- If you invite an address already using Zulip, no invitation is sent.
- """
- self.login("hamlet")
-
- hamlet_email = "hAmLeT@zUlIp.com"
- result = self.invite(hamlet_email, ["Denmark"])
- self.assert_json_error(result, "We weren't able to invite anyone.")
-
- self.assertFalse(
- PreregistrationUser.objects.filter(email__iexact=hamlet_email).exists(),
- )
- self.check_sent_emails([])
-
- def normalize_string(self, s: str) -> str:
- s = s.strip()
- return re.sub(r"\s+", " ", s)
-
- def test_invite_links_in_name(self) -> None:
- """
- If you invite an address already using Zulip, no invitation is sent.
- """
- hamlet = self.example_user("hamlet")
- self.login_user(hamlet)
- # Test we properly handle links in user full names
- do_change_full_name(hamlet, " https://www.google.com", hamlet)
-
- result = self.invite("newuser@zulip.com", ["Denmark"])
- self.assert_json_success(result)
- self.check_sent_emails(["newuser@zulip.com"])
- from django.core.mail import outbox
-
- assert isinstance(outbox[0], EmailMultiAlternatives)
- assert isinstance(outbox[0].alternatives[0][0], str)
- body = self.normalize_string(outbox[0].alternatives[0][0])
-
- # Verify that one can't get Zulip to send invitation emails
- # that third-party products will linkify using the full_name
- # field, because we've included that field inside the mailto:
- # link for the sender.
- self.assertIn(
- '</a> https://www.google.com (hamlet@zulip.com) wants',
- body,
- )
-
- # TODO: Ideally, this test would also test the Invitation
- # Reminder email generated, but the test setup for that is
- # annoying.
-
- def test_invite_some_existing_some_new(self) -> None:
- """
- If you invite a mix of already existing and new users, invitations are
- only sent to the new users.
- """
- self.login("hamlet")
- existing = [self.example_email("hamlet"), "othello@zulip.com"]
- new = ["foo-test@zulip.com", "bar-test@zulip.com"]
- invitee_emails = "\n".join(existing + new)
- self.assert_json_error(
- self.invite(invitee_emails, ["Denmark"]),
- "Some of those addresses are already using Zulip, \
-so we didn't send them an invitation. We did send invitations to everyone else!",
- )
-
- # We only created accounts for the new users.
- for email in existing:
- self.assertRaises(
- PreregistrationUser.DoesNotExist,
- lambda: PreregistrationUser.objects.get(email=email),
- )
- for email in new:
- self.assertTrue(PreregistrationUser.objects.get(email=email))
-
- # We only sent emails to the new users.
- self.check_sent_emails(new)
-
- prereg_user = PreregistrationUser.objects.get(email="foo-test@zulip.com")
- self.assertEqual(prereg_user.email, "foo-test@zulip.com")
-
- def test_invite_outside_domain_in_closed_realm(self) -> None:
- """
- In a realm with `emails_restricted_to_domains = True`, you can't invite people
- with a different domain from that of the realm or your e-mail address.
- """
- zulip_realm = get_realm("zulip")
- zulip_realm.emails_restricted_to_domains = True
- zulip_realm.save()
-
- self.login("hamlet")
- external_address = "foo@example.com"
-
- self.assert_json_error(
- self.invite(external_address, ["Denmark"]),
- "Some emails did not validate, so we didn't send any invitations.",
- )
-
- def test_invite_using_disposable_email(self) -> None:
- """
- In a realm with `disallow_disposable_email_addresses = True`, you can't invite
- people with a disposable domain.
- """
- zulip_realm = get_realm("zulip")
- zulip_realm.emails_restricted_to_domains = False
- zulip_realm.disallow_disposable_email_addresses = True
- zulip_realm.save()
-
- self.login("hamlet")
- external_address = "foo@mailnator.com"
-
- self.assert_json_error(
- self.invite(external_address, ["Denmark"]),
- "Some emails did not validate, so we didn't send any invitations.",
- )
-
- def test_invite_outside_domain_in_open_realm(self) -> None:
- """
- In a realm with `emails_restricted_to_domains = False`, you can invite people
- with a different domain from that of the realm or your e-mail address.
- """
- zulip_realm = get_realm("zulip")
- zulip_realm.emails_restricted_to_domains = False
- zulip_realm.save()
-
- self.login("hamlet")
- external_address = "foo@example.com"
-
- self.assert_json_success(self.invite(external_address, ["Denmark"]))
- self.check_sent_emails([external_address])
-
- def test_invite_outside_domain_before_closing(self) -> None:
- """
- If you invite someone with a different domain from that of the realm
- when `emails_restricted_to_domains = False`, but `emails_restricted_to_domains` later
- changes to true, the invitation should succeed but the invitee's signup
- attempt should fail.
- """
- zulip_realm = get_realm("zulip")
- zulip_realm.emails_restricted_to_domains = False
- zulip_realm.save()
-
- self.login("hamlet")
- external_address = "foo@example.com"
-
- self.assert_json_success(self.invite(external_address, ["Denmark"]))
- self.check_sent_emails([external_address])
-
- zulip_realm.emails_restricted_to_domains = True
- zulip_realm.save()
-
- result = self.submit_reg_form_for_user("foo@example.com", "password")
- self.assertEqual(result.status_code, 200)
- self.assert_in_response("only allows users with email addresses", result)
-
- def test_disposable_emails_before_closing(self) -> None:
- """
- If you invite someone with a disposable email when
- `disallow_disposable_email_addresses = False`, but
- later changes to true, the invitation should succeed
- but the invitee's signup attempt should fail.
- """
- zulip_realm = get_realm("zulip")
- zulip_realm.emails_restricted_to_domains = False
- zulip_realm.disallow_disposable_email_addresses = False
- zulip_realm.save()
-
- self.login("hamlet")
- external_address = "foo@mailnator.com"
-
- self.assert_json_success(self.invite(external_address, ["Denmark"]))
- self.check_sent_emails([external_address])
-
- zulip_realm.disallow_disposable_email_addresses = True
- zulip_realm.save()
-
- result = self.submit_reg_form_for_user("foo@mailnator.com", "password")
- self.assertEqual(result.status_code, 200)
- self.assert_in_response("Please sign up using a real email address.", result)
-
- def test_invite_with_email_containing_plus_before_closing(self) -> None:
- """
- If you invite someone with an email containing plus when
- `emails_restricted_to_domains = False`, but later change
- `emails_restricted_to_domains = True`, the invitation should
- succeed but the invitee's signup attempt should fail as
- users are not allowed to sign up using email containing +
- when the realm is restricted to domain.
- """
- zulip_realm = get_realm("zulip")
- zulip_realm.emails_restricted_to_domains = False
- zulip_realm.save()
-
- self.login("hamlet")
- external_address = "foo+label@zulip.com"
-
- self.assert_json_success(self.invite(external_address, ["Denmark"]))
- self.check_sent_emails([external_address])
-
- zulip_realm.emails_restricted_to_domains = True
- zulip_realm.save()
-
- result = self.submit_reg_form_for_user(external_address, "password")
- self.assertEqual(result.status_code, 200)
- self.assert_in_response(
- "Zulip Dev, does not allow signups using emails\n that contains +", result
- )
-
- def test_invalid_email_check_after_confirming_email(self) -> None:
- self.login("hamlet")
- email = "test@zulip.com"
-
- self.assert_json_success(self.invite(email, ["Denmark"]))
-
- obj = Confirmation.objects.get(confirmation_key=find_key_by_email(email))
- prereg_user = obj.content_object
- assert prereg_user is not None
- prereg_user.email = "invalid.email"
- prereg_user.save()
-
- result = self.submit_reg_form_for_user(email, "password")
- self.assertEqual(result.status_code, 200)
- self.assert_in_response(
- "The email address you are trying to sign up with is not valid", result
- )
-
- def test_invite_with_non_ascii_streams(self) -> None:
- """
- Inviting someone to streams with non-ASCII characters succeeds.
- """
- self.login("hamlet")
- invitee = "alice-test@zulip.com"
-
- stream_name = "hümbüǵ"
-
- # Make sure we're subscribed before inviting someone.
- self.subscribe(self.example_user("hamlet"), stream_name)
-
- self.assert_json_success(self.invite(invitee, [stream_name]))
-
- def test_invitation_reminder_email(self) -> None:
- from django.core.mail import outbox
-
- # All users belong to zulip realm
- referrer_name = "hamlet"
- current_user = self.example_user(referrer_name)
- self.login_user(current_user)
- invitee_email = self.nonreg_email("alice")
- self.assert_json_success(self.invite(invitee_email, ["Denmark"]))
- self.assertTrue(find_key_by_email(invitee_email))
- self.check_sent_emails([invitee_email])
-
- data = {"email": invitee_email, "referrer_email": current_user.email}
- invitee = PreregistrationUser.objects.get(email=data["email"])
- referrer = self.example_user(referrer_name)
- validity_in_minutes = 2 * 24 * 60
- link = create_confirmation_link(
- invitee, Confirmation.INVITATION, validity_in_minutes=validity_in_minutes
- )
- context = common_context(referrer)
- context.update(
- activate_url=link,
- referrer_name=referrer.full_name,
- referrer_email=referrer.email,
- referrer_realm_name=referrer.realm.name,
- )
- with self.settings(EMAIL_BACKEND="django.core.mail.backends.console.EmailBackend"):
- email = data["email"]
- send_future_email(
- "zerver/emails/invitation_reminder",
- referrer.realm,
- to_emails=[email],
- from_address=FromAddress.no_reply_placeholder,
- context=context,
- )
- email_jobs_to_deliver = ScheduledEmail.objects.filter(
- scheduled_timestamp__lte=timezone_now()
- )
- self.assert_length(email_jobs_to_deliver, 1)
- email_count = len(outbox)
- for job in email_jobs_to_deliver:
- deliver_scheduled_emails(job)
- self.assert_length(outbox, email_count + 1)
- self.assertEqual(self.email_envelope_from(outbox[-1]), settings.NOREPLY_EMAIL_ADDRESS)
- self.assertIn(FromAddress.NOREPLY, self.email_display_from(outbox[-1]))
-
- # Now verify that signing up clears invite_reminder emails
- with self.settings(EMAIL_BACKEND="django.core.mail.backends.console.EmailBackend"):
- email = data["email"]
- send_future_email(
- "zerver/emails/invitation_reminder",
- referrer.realm,
- to_emails=[email],
- from_address=FromAddress.no_reply_placeholder,
- context=context,
- )
-
- email_jobs_to_deliver = ScheduledEmail.objects.filter(
- scheduled_timestamp__lte=timezone_now(), type=ScheduledEmail.INVITATION_REMINDER
- )
- self.assert_length(email_jobs_to_deliver, 1)
-
- self.register(invitee_email, "test")
- email_jobs_to_deliver = ScheduledEmail.objects.filter(
- scheduled_timestamp__lte=timezone_now(), type=ScheduledEmail.INVITATION_REMINDER
- )
- self.assert_length(email_jobs_to_deliver, 0)
-
- def test_no_invitation_reminder_when_link_expires_quickly(self) -> None:
- self.login("hamlet")
- # Check invitation reminder email is scheduled with 4 day link expiry
- self.invite("alice@zulip.com", ["Denmark"], invite_expires_in_minutes=4 * 24 * 60)
- self.assertEqual(
- ScheduledEmail.objects.filter(type=ScheduledEmail.INVITATION_REMINDER).count(), 1
- )
- # Check invitation reminder email is not scheduled with 3 day link expiry
- self.invite("bob@zulip.com", ["Denmark"], invite_expires_in_minutes=3 * 24 * 60)
- self.assertEqual(
- ScheduledEmail.objects.filter(type=ScheduledEmail.INVITATION_REMINDER).count(), 1
- )
-
- # make sure users can't take a valid confirmation key from another
- # pathway and use it with the invitation URL route
- def test_confirmation_key_of_wrong_type(self) -> None:
- email = self.nonreg_email("alice")
- realm = get_realm("zulip")
- inviter = self.example_user("iago")
- prereg_user = PreregistrationUser.objects.create(
- email=email, referred_by=inviter, realm=realm
- )
- url = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
- registration_key = url.split("/")[-1]
-
- # Mainly a test of get_object_from_key, rather than of the invitation pathway
- with self.assertRaises(ConfirmationKeyError) as cm:
- get_object_from_key(registration_key, [Confirmation.INVITATION], mark_object_used=True)
- self.assertEqual(cm.exception.error_type, ConfirmationKeyError.DOES_NOT_EXIST)
-
- # Verify that using the wrong type doesn't work in the main confirm code path
- email_change_url = create_confirmation_link(prereg_user, Confirmation.EMAIL_CHANGE)
- email_change_key = email_change_url.split("/")[-1]
- result = self.client_post("/accounts/register/", {"key": email_change_key})
- self.assertEqual(result.status_code, 404)
- self.assert_in_response(
- "Whoops. We couldn't find your confirmation link in the system.", result
- )
-
- def test_confirmation_expired(self) -> None:
- email = self.nonreg_email("alice")
- realm = get_realm("zulip")
- inviter = self.example_user("iago")
- prereg_user = PreregistrationUser.objects.create(
- email=email, referred_by=inviter, realm=realm
- )
- date_sent = timezone_now() - datetime.timedelta(weeks=3)
- with patch("confirmation.models.timezone_now", return_value=date_sent):
- url = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
-
- key = url.split("/")[-1]
- confirmation_link_path = "/" + url.split("/", 3)[3]
- # Both the confirmation link and submitting the key to the registration endpoint
- # directly will return the appropriate error.
- result = self.client_get(confirmation_link_path)
- self.assertEqual(result.status_code, 404)
- self.assert_in_response(
- "Whoops. The confirmation link has expired or been deactivated.", result
- )
-
- result = self.client_post("/accounts/register/", {"key": key})
- self.assertEqual(result.status_code, 404)
- self.assert_in_response(
- "Whoops. The confirmation link has expired or been deactivated.", result
- )
-
- def test_never_expire_confirmation_object(self) -> None:
- email = self.nonreg_email("alice")
- realm = get_realm("zulip")
- inviter = self.example_user("iago")
- prereg_user = PreregistrationUser.objects.create(
- email=email, referred_by=inviter, realm=realm
- )
- activation_url = create_confirmation_link(
- prereg_user, Confirmation.INVITATION, validity_in_minutes=None
- )
- confirmation = Confirmation.objects.last()
- assert confirmation is not None
- self.assertEqual(confirmation.expiry_date, None)
- activation_key = activation_url.split("/")[-1]
- response = self.client_post(
- "/accounts/register/",
- {"key": activation_key, "from_confirmation": 1, "full_nme": "alice"},
- )
- self.assertEqual(response.status_code, 200)
-
- def test_send_more_than_one_invite_to_same_user(self) -> None:
- self.user_profile = self.example_user("iago")
- streams = []
- for stream_name in ["Denmark", "Scotland"]:
- streams.append(get_stream(stream_name, self.user_profile.realm))
-
- invite_expires_in_minutes = 2 * 24 * 60
- do_invite_users(
- self.user_profile,
- ["foo@zulip.com"],
- streams,
- invite_expires_in_minutes=invite_expires_in_minutes,
- )
- prereg_user = PreregistrationUser.objects.get(email="foo@zulip.com")
- do_invite_users(
- self.user_profile,
- ["foo@zulip.com"],
- streams,
- invite_expires_in_minutes=invite_expires_in_minutes,
- )
- do_invite_users(
- self.user_profile,
- ["foo@zulip.com"],
- streams,
- invite_expires_in_minutes=invite_expires_in_minutes,
- )
-
- # Also send an invite from a different realm.
- lear = get_realm("lear")
- lear_user = self.lear_user("cordelia")
- do_invite_users(
- lear_user, ["foo@zulip.com"], [], invite_expires_in_minutes=invite_expires_in_minutes
- )
-
- invites = PreregistrationUser.objects.filter(email__iexact="foo@zulip.com")
- self.assert_length(invites, 4)
-
- created_user = do_create_user(
- "foo@zulip.com",
- "password",
- self.user_profile.realm,
- "full name",
- prereg_user=prereg_user,
- acting_user=None,
- )
-
- accepted_invite = PreregistrationUser.objects.filter(
- email__iexact="foo@zulip.com", status=confirmation_settings.STATUS_USED
- )
- revoked_invites = PreregistrationUser.objects.filter(
- email__iexact="foo@zulip.com", status=confirmation_settings.STATUS_REVOKED
- )
- # If a user was invited more than once, when it accepts one invite and register
- # the others must be canceled.
- self.assert_length(accepted_invite, 1)
- self.assertEqual(accepted_invite[0].id, prereg_user.id)
- self.assertEqual(accepted_invite[0].created_user, created_user)
-
- expected_revoked_invites = set(invites.exclude(id=prereg_user.id).exclude(realm=lear))
- self.assertEqual(set(revoked_invites), expected_revoked_invites)
-
- self.assertEqual(
- PreregistrationUser.objects.get(email__iexact="foo@zulip.com", realm=lear).status, 0
- )
-
- with self.assertRaises(AssertionError):
- process_new_human_user(created_user, prereg_user)
-
- def test_confirmation_obj_not_exist_error(self) -> None:
- """Since the key is a param input by the user to the registration endpoint,
- if it inserts an invalid value, the confirmation object won't be found. This
- tests if, in that scenario, we handle the exception by redirecting the user to
- the link_expired page.
- """
- email = self.nonreg_email("alice")
- password = "password"
- realm = get_realm("zulip")
- inviter = self.example_user("iago")
- prereg_user = PreregistrationUser.objects.create(
- email=email, referred_by=inviter, realm=realm
- )
- confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
-
- registration_key = "invalid_confirmation_key"
- url = "/accounts/register/"
- response = self.client_post(
- url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
- )
- self.assertEqual(response.status_code, 404)
- self.assert_in_response(
- "Whoops. We couldn't find your confirmation link in the system.", response
- )
-
- registration_key = confirmation_link.split("/")[-1]
- response = self.client_post(
- url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
- )
- self.assert_in_success_response(["We just need you to do one last thing."], response)
- response = self.submit_reg_form_for_user(email, password, key=registration_key)
- self.assertEqual(response.status_code, 302)
-
- def test_validate_email_not_already_in_realm(self) -> None:
- email = self.nonreg_email("alice")
- password = "password"
- realm = get_realm("zulip")
- inviter = self.example_user("iago")
- prereg_user = PreregistrationUser.objects.create(
- email=email, referred_by=inviter, realm=realm
- )
-
- confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
- registration_key = confirmation_link.split("/")[-1]
-
- url = "/accounts/register/"
- self.client_post(
- url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
- )
- self.submit_reg_form_for_user(email, password, key=registration_key)
-
- new_prereg_user = PreregistrationUser.objects.create(
- email=email, referred_by=inviter, realm=realm
- )
- new_confirmation_link = create_confirmation_link(
- new_prereg_user, Confirmation.USER_REGISTRATION
- )
- new_registration_key = new_confirmation_link.split("/")[-1]
- url = "/accounts/register/"
- response = self.client_post(
- url, {"key": new_registration_key, "from_confirmation": 1, "full_name": "alice"}
- )
- self.assertEqual(response.status_code, 302)
- self.assertEqual(
- response["Location"],
- reverse("login") + "?" + urlencode({"email": email, "already_registered": 1}),
- )
-
- def test_confirmation_key_cant_be_reused(self) -> None:
- email = self.nonreg_email("alice")
- password = "password"
- realm = get_realm("zulip")
- inviter = self.example_user("iago")
- prereg_user = PreregistrationUser.objects.create(
- email=email, referred_by=inviter, realm=realm
- )
-
- confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
- registration_key = confirmation_link.split("/")[-1]
-
- url = "/accounts/register/"
- self.client_post(
- url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
- )
- self.submit_reg_form_for_user(email, password, key=registration_key)
-
- prereg_user.refresh_from_db()
- self.assertIsNotNone(prereg_user.created_user)
-
- # Now attempt to re-use the same key.
- result = self.client_post("/accounts/register/", {"key": registration_key})
- self.assertEqual(result.status_code, 404)
- self.assert_in_response(
- "Whoops. The confirmation link has expired or been deactivated.", result
- )
-
- def test_confirmation_link_in_manual_license_plan(self) -> None:
- inviter = self.example_user("iago")
- realm = get_realm("zulip")
-
- email = self.nonreg_email("alice")
- realm = get_realm("zulip")
- prereg_user = PreregistrationUser.objects.create(
- email=email, referred_by=inviter, realm=realm
- )
- confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
- registration_key = confirmation_link.split("/")[-1]
- url = "/accounts/register/"
- self.client_post(
- url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
- )
- response = self.submit_reg_form_for_user(email, "password", key=registration_key)
- self.assertEqual(response.status_code, 302)
- self.assertEqual(response["Location"], "http://zulip.testserver/")
-
- # We want to simulate the organization having exactly all their licenses
- # used, to verify that joining as a regular user is not allowed,
- # but as a guest still works (guests are free up to a certain number).
- current_seat_count = get_latest_seat_count(realm)
- self.subscribe_realm_to_monthly_plan_on_manual_license_management(
- realm, current_seat_count, current_seat_count
- )
-
- email = self.nonreg_email("bob")
- prereg_user = PreregistrationUser.objects.create(
- email=email, referred_by=inviter, realm=realm
- )
- confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
- registration_key = confirmation_link.split("/")[-1]
- url = "/accounts/register/"
- self.client_post(url, {"key": registration_key, "from_confirmation": 1, "full_name": "bob"})
- response = self.submit_reg_form_for_user(email, "password", key=registration_key)
- self.assert_in_success_response(
- ["New members cannot join this organization because all Zulip licenses are"], response
- )
-
- guest_prereg_user = PreregistrationUser.objects.create(
- email=email,
- referred_by=inviter,
- realm=realm,
- invited_as=PreregistrationUser.INVITE_AS["GUEST_USER"],
- )
- confirmation_link = create_confirmation_link(
- guest_prereg_user, Confirmation.USER_REGISTRATION
- )
- registration_key = confirmation_link.split("/")[-1]
- url = "/accounts/register/"
-
- self.client_post(
- url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
- )
- response = self.submit_reg_form_for_user(email, "password", key=registration_key)
- self.assertEqual(response.status_code, 302)
- self.assertEqual(response["Location"], "http://zulip.testserver/")
-
-
-class InvitationsTestCase(InviteUserBase):
- def test_do_get_invites_controlled_by_user(self) -> None:
- user_profile = self.example_user("iago")
- hamlet = self.example_user("hamlet")
- othello = self.example_user("othello")
-
- streams = []
- for stream_name in ["Denmark", "Scotland"]:
- streams.append(get_stream(stream_name, user_profile.realm))
-
- invite_expires_in_minutes = 2 * 24 * 60
- do_invite_users(
- user_profile,
- ["TestOne@zulip.com"],
- streams,
- invite_expires_in_minutes=invite_expires_in_minutes,
- )
- do_invite_users(
- user_profile,
- ["TestTwo@zulip.com"],
- streams,
- invite_expires_in_minutes=invite_expires_in_minutes,
- )
- do_invite_users(
- hamlet,
- ["TestThree@zulip.com"],
- streams,
- invite_expires_in_minutes=invite_expires_in_minutes,
- )
- do_invite_users(
- othello,
- ["TestFour@zulip.com"],
- streams,
- invite_expires_in_minutes=invite_expires_in_minutes,
- )
- do_invite_users(
- self.mit_user("sipbtest"),
- ["TestOne@mit.edu"],
- [],
- invite_expires_in_minutes=invite_expires_in_minutes,
- )
- do_create_multiuse_invite_link(
- user_profile, PreregistrationUser.INVITE_AS["MEMBER"], invite_expires_in_minutes
- )
- self.assert_length(do_get_invites_controlled_by_user(user_profile), 5)
- self.assert_length(do_get_invites_controlled_by_user(hamlet), 1)
- self.assert_length(do_get_invites_controlled_by_user(othello), 1)
-
- def test_successful_get_open_invitations(self) -> None:
- """
- A GET call to /json/invites returns all unexpired invitations.
- """
- active_value = getattr(confirmation_settings, "STATUS_USED", "Wrong")
- self.assertNotEqual(active_value, "Wrong")
-
- self.login("iago")
- user_profile = self.example_user("iago")
- self.login_user(user_profile)
-
- hamlet = self.example_user("hamlet")
- othello = self.example_user("othello")
-
- streams = []
- for stream_name in ["Denmark", "Scotland"]:
- streams.append(get_stream(stream_name, user_profile.realm))
-
- invite_expires_in_minutes = 2 * 24 * 60
- do_invite_users(
- user_profile,
- ["TestOne@zulip.com"],
- streams,
- invite_expires_in_minutes=invite_expires_in_minutes,
- )
-
- with patch(
- "confirmation.models.timezone_now",
- return_value=timezone_now() - datetime.timedelta(days=3),
- ):
- do_invite_users(
- user_profile,
- ["TestTwo@zulip.com"],
- streams,
- invite_expires_in_minutes=invite_expires_in_minutes,
- )
- do_create_multiuse_invite_link(
- othello, PreregistrationUser.INVITE_AS["MEMBER"], invite_expires_in_minutes
- )
-
- prereg_user_three = PreregistrationUser(
- email="TestThree@zulip.com", referred_by=user_profile, status=active_value
- )
- prereg_user_three.save()
- create_confirmation_link(
- prereg_user_three,
- Confirmation.INVITATION,
- validity_in_minutes=invite_expires_in_minutes,
- )
-
- do_create_multiuse_invite_link(
- hamlet, PreregistrationUser.INVITE_AS["MEMBER"], invite_expires_in_minutes
- )
-
- result = self.client_get("/json/invites")
- self.assertEqual(result.status_code, 200)
- invites = orjson.loads(result.content)["invites"]
- self.assert_length(invites, 2)
-
- self.assertFalse(invites[0]["is_multiuse"])
- self.assertEqual(invites[0]["email"], "TestOne@zulip.com")
- self.assertTrue(invites[1]["is_multiuse"])
- self.assertEqual(invites[1]["invited_by_user_id"], hamlet.id)
-
- def test_get_never_expiring_invitations(self) -> None:
- self.login("iago")
- user_profile = self.example_user("iago")
-
- streams = []
- for stream_name in ["Denmark", "Scotland"]:
- streams.append(get_stream(stream_name, user_profile.realm))
-
- with patch(
- "confirmation.models.timezone_now",
- return_value=timezone_now() - datetime.timedelta(days=1000),
- ):
- # Testing the invitation with expiry date set to "None" exists
- # after a large amount of days.
- do_invite_users(
- user_profile,
- ["TestOne@zulip.com"],
- streams,
- invite_expires_in_minutes=None,
- )
- do_invite_users(
- user_profile,
- ["TestTwo@zulip.com"],
- streams,
- invite_expires_in_minutes=100 * 24 * 60,
- )
- do_create_multiuse_invite_link(
- user_profile, PreregistrationUser.INVITE_AS["MEMBER"], None
- )
- do_create_multiuse_invite_link(
- user_profile, PreregistrationUser.INVITE_AS["MEMBER"], 100
- )
-
- result = self.client_get("/json/invites")
- self.assertEqual(result.status_code, 200)
- invites = orjson.loads(result.content)["invites"]
- # We only get invitations that will never expire because we have mocked time such
- # that the other invitations are created in the deep past.
- self.assert_length(invites, 2)
-
- self.assertFalse(invites[0]["is_multiuse"])
- self.assertEqual(invites[0]["email"], "TestOne@zulip.com")
- self.assertEqual(invites[0]["expiry_date"], None)
- self.assertTrue(invites[1]["is_multiuse"])
- self.assertEqual(invites[1]["invited_by_user_id"], user_profile.id)
- self.assertEqual(invites[1]["expiry_date"], None)
-
- def test_successful_delete_invitation(self) -> None:
- """
- A DELETE call to /json/invites/ should delete the invite and
- any scheduled invitation reminder emails.
- """
- self.login("iago")
-
- invitee = "DeleteMe@zulip.com"
- self.assert_json_success(self.invite(invitee, ["Denmark"]))
- prereg_user = PreregistrationUser.objects.get(email=invitee)
-
- # Verify that the scheduled email exists.
- ScheduledEmail.objects.get(address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER)
-
- result = self.client_delete("/json/invites/" + str(prereg_user.id))
- self.assertEqual(result.status_code, 200)
- error_result = self.client_delete("/json/invites/" + str(prereg_user.id))
- self.assert_json_error(error_result, "No such invitation")
-
- self.assertRaises(
- ScheduledEmail.DoesNotExist,
- lambda: ScheduledEmail.objects.get(
- address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
- ),
- )
-
- def test_successful_member_delete_invitation(self) -> None:
- """
- A DELETE call from member account to /json/invites/ should delete the invite and
- any scheduled invitation reminder emails.
- """
- user_profile = self.example_user("hamlet")
- self.login_user(user_profile)
- invitee = "DeleteMe@zulip.com"
- self.assert_json_success(self.invite(invitee, ["Denmark"]))
-
- # Verify that the scheduled email exists.
- prereg_user = PreregistrationUser.objects.get(email=invitee, referred_by=user_profile)
- ScheduledEmail.objects.get(address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER)
-
- # Verify another non-admin can't delete
- result = self.api_delete(
- self.example_user("othello"), "/api/v1/invites/" + str(prereg_user.id)
- )
- self.assert_json_error(result, "Must be an organization administrator")
-
- # Verify that the scheduled email still exists.
- prereg_user = PreregistrationUser.objects.get(email=invitee, referred_by=user_profile)
- ScheduledEmail.objects.get(address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER)
-
- # Verify deletion works.
- result = self.api_delete(user_profile, "/api/v1/invites/" + str(prereg_user.id))
- self.assertEqual(result.status_code, 200)
-
- result = self.api_delete(user_profile, "/api/v1/invites/" + str(prereg_user.id))
- self.assert_json_error(result, "No such invitation")
-
- self.assertRaises(
- ScheduledEmail.DoesNotExist,
- lambda: ScheduledEmail.objects.get(
- address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
- ),
- )
-
- def test_delete_owner_invitation(self) -> None:
- self.login("desdemona")
- owner = self.example_user("desdemona")
-
- invitee = "DeleteMe@zulip.com"
- self.assert_json_success(
- self.invite(
- invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["REALM_OWNER"]
- )
- )
- prereg_user = PreregistrationUser.objects.get(email=invitee)
- result = self.api_delete(
- self.example_user("iago"), "/api/v1/invites/" + str(prereg_user.id)
- )
- self.assert_json_error(result, "Must be an organization owner")
-
- result = self.api_delete(owner, "/api/v1/invites/" + str(prereg_user.id))
- self.assert_json_success(result)
- result = self.api_delete(owner, "/api/v1/invites/" + str(prereg_user.id))
- self.assert_json_error(result, "No such invitation")
- self.assertRaises(
- ScheduledEmail.DoesNotExist,
- lambda: ScheduledEmail.objects.get(
- address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
- ),
- )
-
- def test_delete_multiuse_invite(self) -> None:
- """
- A DELETE call to /json/invites/multiuse should delete the
- multiuse_invite.
- """
- self.login("iago")
-
- zulip_realm = get_realm("zulip")
- multiuse_invite = MultiuseInvite.objects.create(
- referred_by=self.example_user("hamlet"), realm=zulip_realm
- )
- validity_in_minutes = 2 * 24 * 60
- create_confirmation_link(
- multiuse_invite, Confirmation.MULTIUSE_INVITE, validity_in_minutes=validity_in_minutes
- )
- result = self.client_delete("/json/invites/multiuse/" + str(multiuse_invite.id))
- self.assertEqual(result.status_code, 200)
- self.assertEqual(
- MultiuseInvite.objects.get(id=multiuse_invite.id).status,
- confirmation_settings.STATUS_REVOKED,
- )
- # Test that trying to double-delete fails
- error_result = self.client_delete("/json/invites/multiuse/" + str(multiuse_invite.id))
- self.assert_json_error(error_result, "Invitation has already been revoked")
-
- # Test deleting owner mutiuse_invite.
- multiuse_invite = MultiuseInvite.objects.create(
- referred_by=self.example_user("desdemona"),
- realm=zulip_realm,
- invited_as=PreregistrationUser.INVITE_AS["REALM_OWNER"],
- )
- validity_in_minutes = 2
- create_confirmation_link(
- multiuse_invite, Confirmation.MULTIUSE_INVITE, validity_in_minutes=validity_in_minutes
- )
- error_result = self.client_delete("/json/invites/multiuse/" + str(multiuse_invite.id))
- self.assert_json_error(error_result, "Must be an organization owner")
-
- self.login("desdemona")
- result = self.client_delete("/json/invites/multiuse/" + str(multiuse_invite.id))
- self.assert_json_success(result)
- self.assertEqual(
- MultiuseInvite.objects.get(id=multiuse_invite.id).status,
- confirmation_settings.STATUS_REVOKED,
- )
-
- # Test deleting multiuse invite from another realm
- mit_realm = get_realm("zephyr")
- multiuse_invite_in_mit = MultiuseInvite.objects.create(
- referred_by=self.mit_user("sipbtest"), realm=mit_realm
- )
- validity_in_minutes = 2 * 24 * 60
- create_confirmation_link(
- multiuse_invite_in_mit,
- Confirmation.MULTIUSE_INVITE,
- validity_in_minutes=validity_in_minutes,
- )
- error_result = self.client_delete(
- "/json/invites/multiuse/" + str(multiuse_invite_in_mit.id)
- )
- self.assert_json_error(error_result, "No such invitation")
-
- non_existent_id = MultiuseInvite.objects.count() + 9999
- error_result = self.client_delete(f"/json/invites/multiuse/{non_existent_id}")
- self.assert_json_error(error_result, "No such invitation")
-
- def test_successful_resend_invitation(self) -> None:
- """
- A POST call to /json/invites//resend should send an invitation reminder email
- and delete any scheduled invitation reminder email.
- """
- self.login("iago")
- invitee = "resend_me@zulip.com"
-
- self.assert_json_success(self.invite(invitee, ["Denmark"]))
- prereg_user = PreregistrationUser.objects.get(email=invitee)
-
- # Verify and then clear from the outbox the original invite email
- self.check_sent_emails([invitee])
- from django.core.mail import outbox
-
- outbox.pop()
-
- # Verify that the scheduled email exists.
- scheduledemail_filter = ScheduledEmail.objects.filter(
- address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
- )
- self.assertEqual(scheduledemail_filter.count(), 1)
- original_timestamp = scheduledemail_filter.values_list("scheduled_timestamp", flat=True)
-
- # Resend invite
- result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
- self.assertEqual(
- ScheduledEmail.objects.filter(
- address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
- ).count(),
- 1,
- )
-
- # Check that we have exactly one scheduled email, and that it is different
- self.assertEqual(scheduledemail_filter.count(), 1)
- self.assertNotEqual(
- original_timestamp, scheduledemail_filter.values_list("scheduled_timestamp", flat=True)
- )
-
- self.assertEqual(result.status_code, 200)
- error_result = self.client_post("/json/invites/" + str(9999) + "/resend")
- self.assert_json_error(error_result, "No such invitation")
-
- self.check_sent_emails([invitee])
-
- def test_successful_member_resend_invitation(self) -> None:
- """A POST call from member a account to /json/invites//resend
- should send an invitation reminder email and delete any
- scheduled invitation reminder email if they send the invite.
- """
- self.login("hamlet")
- user_profile = self.example_user("hamlet")
- invitee = "resend_me@zulip.com"
- self.assert_json_success(self.invite(invitee, ["Denmark"]))
- # Verify hamlet has only one invitation (Member can resend invitations only sent by him).
- invitation = PreregistrationUser.objects.filter(referred_by=user_profile)
- self.assert_length(invitation, 1)
- prereg_user = PreregistrationUser.objects.get(email=invitee)
-
- # Verify and then clear from the outbox the original invite email
- self.check_sent_emails([invitee])
- from django.core.mail import outbox
-
- outbox.pop()
-
- # Verify that the scheduled email exists.
- scheduledemail_filter = ScheduledEmail.objects.filter(
- address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
- )
- self.assertEqual(scheduledemail_filter.count(), 1)
- original_timestamp = scheduledemail_filter.values_list("scheduled_timestamp", flat=True)
-
- # Resend invite
- result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
- self.assertEqual(
- ScheduledEmail.objects.filter(
- address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
- ).count(),
- 1,
- )
-
- # Check that we have exactly one scheduled email, and that it is different
- self.assertEqual(scheduledemail_filter.count(), 1)
- self.assertNotEqual(
- original_timestamp, scheduledemail_filter.values_list("scheduled_timestamp", flat=True)
- )
-
- self.assertEqual(result.status_code, 200)
- error_result = self.client_post("/json/invites/" + str(9999) + "/resend")
- self.assert_json_error(error_result, "No such invitation")
-
- self.check_sent_emails([invitee])
-
- self.logout()
- self.login("othello")
- invitee = "TestOne@zulip.com"
- prereg_user_one = PreregistrationUser(email=invitee, referred_by=user_profile)
- prereg_user_one.save()
- prereg_user = PreregistrationUser.objects.get(email=invitee)
- error_result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
- self.assert_json_error(error_result, "Must be an organization administrator")
-
- def test_resend_owner_invitation(self) -> None:
- self.login("desdemona")
-
- invitee = "resend_owner@zulip.com"
- self.assert_json_success(
- self.invite(
- invitee, ["Denmark"], invite_as=PreregistrationUser.INVITE_AS["REALM_OWNER"]
- )
- )
- self.check_sent_emails([invitee])
- scheduledemail_filter = ScheduledEmail.objects.filter(
- address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
- )
- self.assertEqual(scheduledemail_filter.count(), 1)
- original_timestamp = scheduledemail_filter.values_list("scheduled_timestamp", flat=True)
-
- # Test only organization owners can resend owner invitation.
- self.login("iago")
- prereg_user = PreregistrationUser.objects.get(email=invitee)
- error_result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
- self.assert_json_error(error_result, "Must be an organization owner")
-
- self.login("desdemona")
- result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
- self.assert_json_success(result)
-
- self.assertEqual(
- ScheduledEmail.objects.filter(
- address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER
- ).count(),
- 1,
- )
-
- # Check that we have exactly one scheduled email, and that it is different
- self.assertEqual(scheduledemail_filter.count(), 1)
- self.assertNotEqual(
- original_timestamp, scheduledemail_filter.values_list("scheduled_timestamp", flat=True)
- )
-
- def test_resend_never_expiring_invitation(self) -> None:
- self.login("iago")
- invitee = "resend@zulip.com"
-
- self.assert_json_success(self.invite(invitee, ["Denmark"], None))
- prereg_user = PreregistrationUser.objects.get(email=invitee)
-
- # Verify and then clear from the outbox the original invite email
- self.check_sent_emails([invitee])
- from django.core.mail import outbox
-
- outbox.pop()
-
- result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
- self.assert_json_success(result)
- self.check_sent_emails([invitee])
-
- def test_accessing_invites_in_another_realm(self) -> None:
- inviter = UserProfile.objects.exclude(realm=get_realm("zulip")).first()
- assert inviter is not None
- prereg_user = PreregistrationUser.objects.create(
- email="email", referred_by=inviter, realm=inviter.realm
- )
- self.login("iago")
- error_result = self.client_post("/json/invites/" + str(prereg_user.id) + "/resend")
- self.assert_json_error(error_result, "No such invitation")
- error_result = self.client_delete("/json/invites/" + str(prereg_user.id))
- self.assert_json_error(error_result, "No such invitation")
-
- def test_prereg_user_status(self) -> None:
- email = self.nonreg_email("alice")
- password = "password"
- realm = get_realm("zulip")
-
- inviter = UserProfile.objects.filter(realm=realm).first()
- prereg_user = PreregistrationUser.objects.create(
- email=email, referred_by=inviter, realm=realm
- )
-
- confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
- registration_key = confirmation_link.split("/")[-1]
-
- result = self.client_post(
- "/accounts/register/",
- {"key": registration_key, "from_confirmation": "1", "full_name": "alice"},
- )
- self.assertEqual(result.status_code, 200)
- confirmation = Confirmation.objects.get(confirmation_key=registration_key)
- assert confirmation.content_object is not None
- prereg_user = confirmation.content_object
- self.assertEqual(prereg_user.status, 0)
-
- result = self.submit_reg_form_for_user(email, password, key=registration_key)
- self.assertEqual(result.status_code, 302)
- prereg_user = PreregistrationUser.objects.get(email=email, referred_by=inviter, realm=realm)
- self.assertEqual(prereg_user.status, confirmation_settings.STATUS_USED)
- user = get_user_by_delivery_email(email, realm)
- self.assertIsNotNone(user)
- self.assertEqual(user.delivery_email, email)
-
-
-class InviteeEmailsParserTests(ZulipTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.email1 = "email1@zulip.com"
- self.email2 = "email2@zulip.com"
- self.email3 = "email3@zulip.com"
-
- def test_if_emails_separated_by_commas_are_parsed_and_striped_correctly(self) -> None:
- emails_raw = f"{self.email1} ,{self.email2}, {self.email3}"
- expected_set = {self.email1, self.email2, self.email3}
- self.assertEqual(get_invitee_emails_set(emails_raw), expected_set)
-
- def test_if_emails_separated_by_newlines_are_parsed_and_striped_correctly(self) -> None:
- emails_raw = f"{self.email1}\n {self.email2}\n {self.email3} "
- expected_set = {self.email1, self.email2, self.email3}
- self.assertEqual(get_invitee_emails_set(emails_raw), expected_set)
-
- def test_if_emails_from_email_client_separated_by_newlines_are_parsed_correctly(self) -> None:
- emails_raw = (
- f"Email One <{self.email1}>\nEmailTwo<{self.email2}>\nEmail Three<{self.email3}>"
- )
- expected_set = {self.email1, self.email2, self.email3}
- self.assertEqual(get_invitee_emails_set(emails_raw), expected_set)
-
- def test_if_emails_in_mixed_style_are_parsed_correctly(self) -> None:
- emails_raw = f"Email One <{self.email1}>,EmailTwo<{self.email2}>\n{self.email3}"
- expected_set = {self.email1, self.email2, self.email3}
- self.assertEqual(get_invitee_emails_set(emails_raw), expected_set)
-
-
-class MultiuseInviteTest(ZulipTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.realm = get_realm("zulip")
- self.realm.invite_required = True
- self.realm.save()
-
- def generate_multiuse_invite_link(
- self, streams: Optional[List[Stream]] = None, date_sent: Optional[datetime.datetime] = None
- ) -> str:
- invite = MultiuseInvite(realm=self.realm, referred_by=self.example_user("iago"))
- invite.save()
-
- if streams is not None:
- invite.streams.set(streams)
-
- if date_sent is None:
- date_sent = timezone_now()
- validity_in_minutes = 2 * 24 * 60
- with patch("confirmation.models.timezone_now", return_value=date_sent):
- return create_confirmation_link(
- invite, Confirmation.MULTIUSE_INVITE, validity_in_minutes=validity_in_minutes
- )
-
- def check_user_able_to_register(self, email: str, invite_link: str) -> None:
- password = "password"
-
- result = self.client_post(invite_link, {"email": email})
- self.assertEqual(result.status_code, 302)
- self.assertTrue(
- result["Location"].endswith(
- f"/accounts/send_confirm/?email={urllib.parse.quote(email)}"
- )
- )
- result = self.client_get(result["Location"])
- self.assert_in_response("Check your email", result)
-
- confirmation_url = self.get_confirmation_url_from_outbox(email)
- result = self.client_get(confirmation_url)
- self.assertEqual(result.status_code, 200)
-
- result = self.submit_reg_form_for_user(email, password)
- self.assertEqual(result.status_code, 302)
-
- # Verify the PreregistrationUser object was set up as expected.
- prereg_user = PreregistrationUser.objects.last()
- multiuse_invite = MultiuseInvite.objects.last()
-
- assert prereg_user is not None
- self.assertEqual(prereg_user.email, email)
- self.assertEqual(prereg_user.multiuse_invite, multiuse_invite)
-
- from django.core.mail import outbox
-
- outbox.pop()
-
- def test_valid_multiuse_link(self) -> None:
- email1 = self.nonreg_email("test")
- email2 = self.nonreg_email("test1")
- email3 = self.nonreg_email("alice")
-
- date_sent = timezone_now() - datetime.timedelta(days=1)
- invite_link = self.generate_multiuse_invite_link(date_sent=date_sent)
-
- self.check_user_able_to_register(email1, invite_link)
- self.check_user_able_to_register(email2, invite_link)
- self.check_user_able_to_register(email3, invite_link)
-
- def test_expired_multiuse_link(self) -> None:
- email = self.nonreg_email("newuser")
- date_sent = timezone_now() - datetime.timedelta(
- days=settings.INVITATION_LINK_VALIDITY_DAYS + 1
- )
- invite_link = self.generate_multiuse_invite_link(date_sent=date_sent)
- result = self.client_post(invite_link, {"email": email})
-
- self.assertEqual(result.status_code, 404)
- self.assert_in_response("The confirmation link has expired or been deactivated.", result)
-
- def test_revoked_multiuse_link(self) -> None:
- email = self.nonreg_email("newuser")
- invite_link = self.generate_multiuse_invite_link()
- multiuse_invite = MultiuseInvite.objects.last()
- assert multiuse_invite is not None
- do_revoke_multi_use_invite(multiuse_invite)
-
- result = self.client_post(invite_link, {"email": email})
-
- self.assertEqual(result.status_code, 404)
- self.assert_in_response("We couldn't find your confirmation link in the system.", result)
-
- def test_invalid_multiuse_link(self) -> None:
- email = self.nonreg_email("newuser")
- invite_link = "/join/invalid_key/"
- result = self.client_post(invite_link, {"email": email})
-
- self.assertEqual(result.status_code, 404)
- self.assert_in_response("Whoops. The confirmation link is malformed.", result)
-
- def test_invalid_multiuse_link_in_open_realm(self) -> None:
- self.realm.invite_required = False
- self.realm.save()
-
- email = self.nonreg_email("newuser")
- invite_link = "/join/invalid_key/"
-
- with patch("zerver.views.registration.get_realm_from_request", return_value=self.realm):
- with patch("zerver.views.registration.get_realm", return_value=self.realm):
- self.check_user_able_to_register(email, invite_link)
-
- def test_multiuse_link_with_specified_streams(self) -> None:
- name1 = "newuser"
- name2 = "bob"
- email1 = self.nonreg_email(name1)
- email2 = self.nonreg_email(name2)
-
- stream_names = ["Rome", "Scotland", "Venice"]
- streams = [get_stream(stream_name, self.realm) for stream_name in stream_names]
- invite_link = self.generate_multiuse_invite_link(streams=streams)
- self.check_user_able_to_register(email1, invite_link)
- self.check_user_subscribed_only_to_streams(name1, streams)
-
- stream_names = ["Rome", "Verona"]
- streams = [get_stream(stream_name, self.realm) for stream_name in stream_names]
- invite_link = self.generate_multiuse_invite_link(streams=streams)
- self.check_user_able_to_register(email2, invite_link)
- self.check_user_subscribed_only_to_streams(name2, streams)
-
- def test_multiuse_link_different_realms(self) -> None:
- """
- Verify that an invitation generated for one realm can't be used
- to join another.
- """
- lear_realm = get_realm("lear")
- self.realm = lear_realm
- invite_link = self.generate_multiuse_invite_link(streams=[])
- key = invite_link.split("/")[-2]
-
- result = self.client_get(f"/join/{key}/", subdomain="zulip")
- self.assertEqual(result.status_code, 404)
- self.assert_in_response(
- "Whoops. We couldn't find your confirmation link in the system.", result
- )
-
- # Now we want to test the accounts_home function, which can't be used
- # for the multiuse invite case via an HTTP request, but is still supposed
- # to do its own verification that the realms match as a hardening measure
- # against a caller that fails to do that.
- request = HttpRequest()
- confirmation = Confirmation.objects.get(confirmation_key=key)
- multiuse_object = confirmation.content_object
- with patch(
- "zerver.views.registration.get_subdomain", return_value="zulip"
- ), self.assertRaises(AssertionError):
- accounts_home(request, multiuse_object=multiuse_object)
-
- def test_create_multiuse_link_api_call(self) -> None:
- self.login("iago")
-
- result = self.client_post(
- "/json/invites/multiuse", {"invite_expires_in_minutes": 2 * 24 * 60}
- )
- invite_link = self.assert_json_success(result)["invite_link"]
- self.check_user_able_to_register(self.nonreg_email("test"), invite_link)
-
- def test_create_multiuse_link_with_specified_streams_api_call(self) -> None:
- self.login("iago")
- stream_names = ["Rome", "Scotland", "Venice"]
- streams = [get_stream(stream_name, self.realm) for stream_name in stream_names]
- stream_ids = [stream.id for stream in streams]
-
- result = self.client_post(
- "/json/invites/multiuse",
- {
- "stream_ids": orjson.dumps(stream_ids).decode(),
- "invite_expires_in_minutes": 2 * 24 * 60,
- },
- )
- invite_link = self.assert_json_success(result)["invite_link"]
- self.check_user_able_to_register(self.nonreg_email("test"), invite_link)
- self.check_user_subscribed_only_to_streams("test", streams)
-
- def test_only_admin_can_create_multiuse_link_api_call(self) -> None:
- self.login("iago")
- # Only admins should be able to create multiuse invites even if
- # invite_to_realm_policy is set to Realm.POLICY_MEMBERS_ONLY.
- self.realm.invite_to_realm_policy = Realm.POLICY_MEMBERS_ONLY
- self.realm.save()
-
- result = self.client_post(
- "/json/invites/multiuse", {"invite_expires_in_minutes": 2 * 24 * 60}
- )
- invite_link = self.assert_json_success(result)["invite_link"]
- self.check_user_able_to_register(self.nonreg_email("test"), invite_link)
-
- self.login("hamlet")
- result = self.client_post("/json/invites/multiuse")
- self.assert_json_error(result, "Must be an organization administrator")
-
- def test_multiuse_link_for_inviting_as_owner(self) -> None:
- self.login("iago")
- result = self.client_post(
- "/json/invites/multiuse",
- {
- "invite_as": orjson.dumps(PreregistrationUser.INVITE_AS["REALM_OWNER"]).decode(),
- "invite_expires_in_minutes": 2 * 24 * 60,
- },
- )
- self.assert_json_error(result, "Must be an organization owner")
-
- self.login("desdemona")
- result = self.client_post(
- "/json/invites/multiuse",
- {
- "invite_as": orjson.dumps(PreregistrationUser.INVITE_AS["REALM_OWNER"]).decode(),
- "invite_expires_in_minutes": 2 * 24 * 60,
- },
- )
- invite_link = self.assert_json_success(result)["invite_link"]
- self.check_user_able_to_register(self.nonreg_email("test"), invite_link)
-
- def test_create_multiuse_link_invalid_stream_api_call(self) -> None:
- self.login("iago")
- result = self.client_post(
- "/json/invites/multiuse",
- {
- "stream_ids": orjson.dumps([54321]).decode(),
- "invite_expires_in_minutes": 2 * 24 * 60,
- },
- )
- self.assert_json_error(result, "Invalid stream ID 54321. No invites were sent.")
-
- def test_create_multiuse_link_invalid_invite_as_api_call(self) -> None:
- self.login("iago")
- result = self.client_post(
- "/json/invites/multiuse",
- {
- "invite_as": orjson.dumps(PreregistrationUser.INVITE_AS["GUEST_USER"] + 1).decode(),
- "invite_expires_in_minutes": 2 * 24 * 60,
- },
- )
- self.assert_json_error(result, "Invalid invite_as")
-
-
class EmailUnsubscribeTests(ZulipTestCase):
def test_error_unsubscribe(self) -> None:
# An invalid unsubscribe token "test123" produces an error.
@@ -4087,7 +1946,7 @@ class RealmCreationTest(ZulipTestCase):
check_subdomain_available("stream", allow_reserved_subdomain=True)
-class UserSignUpTest(InviteUserBase):
+class UserSignUpTest(ZulipTestCase):
def _assert_redirected_to(self, result: "TestHttpResponse", url: str) -> None:
self.assertEqual(result.status_code, 302)
self.assertEqual(result["LOCATION"], url)
@@ -5621,8 +3480,14 @@ class UserSignUpTest(InviteUserBase):
"DEBUG:zulip.ldap:ZulipLDAPAuthBackend: No LDAP user matching django_to_ldap_username result: iago. Input username: iago@zulip.com"
],
)
- response = self.invite(
- invitee_emails="newuser@zulip.com", stream_names=streams, invite_as=invite_as
+ stream_ids = [self.get_stream_id(stream_name) for stream_name in streams]
+ response = self.client_post(
+ "/json/invites",
+ {
+ "invitee_emails": email,
+ "stream_ids": orjson.dumps(stream_ids).decode(),
+ "invite_as": invite_as,
+ },
)
self.assert_json_success(response)
self.logout()