markdown: Fix use of pure_markdown for non-pure markdown rendering.
`render_markdown_path` renders Markdown, and also (since baff121115a1)
runs Jinja2 on the resulting HTML.
The `pure_markdown` flag was added in 0a99fa2fd669, and did two
things: retried the path directly in the filesystem if it wasn't found
by the Jinja2 resolver, and also skipped the subsequent Jinja2
templating step (regardless of where the content was found). In this
context, the name `pure_markdown` made some sense. The only two
callsites were the TOS and privacy policy renders, which might have
had user-supplied arbitrary paths, and we wished to handle absolute
paths in addition to ones inside `templates/`.
Unfortunately, the follow-up of 01bd55bbcbf7 did not refactor the
logic -- it changed it, by making `pure_markdown` only do the former
of the two behaviors. Passing `pure_markdown=True` after that commit
still caused it to always run Jinja2, but allowed it to look elsewhere
in the filesystem.
This set the stage for calls, such as the one introduced in
dedea237456b, which passed both a context for Jinja2, as well as
`pure_markdown=True` implying that Jinja2 was not to be used.
Split the two previous behaviors of the `pure_markdown` flag, and use
pre-existing data to control them, rather than an explicit flag. For
handling policy information which is stored at an absolute path
outside of the template root, we switch to using the template search
path if and only if the path is relative. This also closes the
potential inconsistency based on CWD when `pure_markdown=True` was
passed and the path was relative, not absolute.
Decide whether to run Jinja2 based on if a context is passed in at
all. This restores the behavior in the initial 0a99fa2fd669 where a
call to `rendar_markdown_path` could be made to just render markdown,
and not some other unmentioned and unrelated templating language as
well.
2023-03-10 02:47:44 +01:00
|
|
|
import tempfile
|
2022-04-17 01:47:25 +02:00
|
|
|
from datetime import datetime, timedelta, timezone
|
2020-06-11 00:54:34 +02:00
|
|
|
from unittest.mock import patch
|
2017-03-08 12:28:24 +01:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
import ldap
|
2020-08-07 01:09:47 +02:00
|
|
|
import orjson
|
2017-03-08 12:28:24 +01:00
|
|
|
from django.core import mail
|
2022-07-01 03:05:12 +02:00
|
|
|
from django.core.mail.message import EmailMultiAlternatives
|
2017-03-08 12:28:24 +01:00
|
|
|
from django.test import override_settings
|
2021-09-09 21:51:55 +02:00
|
|
|
from django.utils.timezone import now as timezone_now
|
2019-10-05 03:54:48 +02:00
|
|
|
from django_auth_ldap.config import LDAPSearch
|
2017-03-08 12:28:24 +01:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.email_notifications import (
|
|
|
|
enqueue_welcome_emails,
|
2023-03-15 18:09:26 +01:00
|
|
|
get_onboarding_email_schedule,
|
2023-06-30 13:27:25 +02:00
|
|
|
send_account_registered_email,
|
2017-05-08 17:54:11 +02:00
|
|
|
)
|
2023-12-13 23:05:21 +01:00
|
|
|
from zerver.lib.send_email import (
|
|
|
|
deliver_scheduled_emails,
|
|
|
|
send_custom_email,
|
|
|
|
send_custom_server_email,
|
|
|
|
)
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.test_classes import ZulipTestCase
|
2023-12-15 02:14:24 +01:00
|
|
|
from zerver.models import Realm, ScheduledEmail, UserProfile
|
|
|
|
from zerver.models.realms import get_realm
|
2023-12-13 23:05:21 +01:00
|
|
|
from zilencer.models import RemoteZulipServer
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2017-03-08 12:28:24 +01:00
|
|
|
|
2020-04-09 13:39:16 +02:00
|
|
|
class TestCustomEmails(ZulipTestCase):
|
|
|
|
def test_send_custom_email_argument(self) -> None:
|
2021-02-12 08:20:45 +01:00
|
|
|
hamlet = self.example_user("hamlet")
|
|
|
|
email_subject = "subject_test"
|
|
|
|
reply_to = "reply_to_test"
|
2020-04-09 13:39:16 +02:00
|
|
|
from_name = "from_name_test"
|
markdown: Fix use of pure_markdown for non-pure markdown rendering.
`render_markdown_path` renders Markdown, and also (since baff121115a1)
runs Jinja2 on the resulting HTML.
The `pure_markdown` flag was added in 0a99fa2fd669, and did two
things: retried the path directly in the filesystem if it wasn't found
by the Jinja2 resolver, and also skipped the subsequent Jinja2
templating step (regardless of where the content was found). In this
context, the name `pure_markdown` made some sense. The only two
callsites were the TOS and privacy policy renders, which might have
had user-supplied arbitrary paths, and we wished to handle absolute
paths in addition to ones inside `templates/`.
Unfortunately, the follow-up of 01bd55bbcbf7 did not refactor the
logic -- it changed it, by making `pure_markdown` only do the former
of the two behaviors. Passing `pure_markdown=True` after that commit
still caused it to always run Jinja2, but allowed it to look elsewhere
in the filesystem.
This set the stage for calls, such as the one introduced in
dedea237456b, which passed both a context for Jinja2, as well as
`pure_markdown=True` implying that Jinja2 was not to be used.
Split the two previous behaviors of the `pure_markdown` flag, and use
pre-existing data to control them, rather than an explicit flag. For
handling policy information which is stored at an absolute path
outside of the template root, we switch to using the template search
path if and only if the path is relative. This also closes the
potential inconsistency based on CWD when `pure_markdown=True` was
passed and the path was relative, not absolute.
Decide whether to run Jinja2 based on if a context is passed in at
all. This restores the behavior in the initial 0a99fa2fd669 where a
call to `rendar_markdown_path` could be made to just render markdown,
and not some other unmentioned and unrelated templating language as
well.
2023-03-10 02:47:44 +01:00
|
|
|
|
|
|
|
with tempfile.NamedTemporaryFile() as markdown_template:
|
|
|
|
markdown_template.write(b"# Some heading\n\nSome content\n{{ realm_name }}")
|
|
|
|
markdown_template.flush()
|
|
|
|
send_custom_email(
|
2023-08-03 22:20:37 +02:00
|
|
|
UserProfile.objects.filter(id=hamlet.id),
|
2023-12-13 22:53:47 +01:00
|
|
|
dry_run=False,
|
markdown: Fix use of pure_markdown for non-pure markdown rendering.
`render_markdown_path` renders Markdown, and also (since baff121115a1)
runs Jinja2 on the resulting HTML.
The `pure_markdown` flag was added in 0a99fa2fd669, and did two
things: retried the path directly in the filesystem if it wasn't found
by the Jinja2 resolver, and also skipped the subsequent Jinja2
templating step (regardless of where the content was found). In this
context, the name `pure_markdown` made some sense. The only two
callsites were the TOS and privacy policy renders, which might have
had user-supplied arbitrary paths, and we wished to handle absolute
paths in addition to ones inside `templates/`.
Unfortunately, the follow-up of 01bd55bbcbf7 did not refactor the
logic -- it changed it, by making `pure_markdown` only do the former
of the two behaviors. Passing `pure_markdown=True` after that commit
still caused it to always run Jinja2, but allowed it to look elsewhere
in the filesystem.
This set the stage for calls, such as the one introduced in
dedea237456b, which passed both a context for Jinja2, as well as
`pure_markdown=True` implying that Jinja2 was not to be used.
Split the two previous behaviors of the `pure_markdown` flag, and use
pre-existing data to control them, rather than an explicit flag. For
handling policy information which is stored at an absolute path
outside of the template root, we switch to using the template search
path if and only if the path is relative. This also closes the
potential inconsistency based on CWD when `pure_markdown=True` was
passed and the path was relative, not absolute.
Decide whether to run Jinja2 based on if a context is passed in at
all. This restores the behavior in the initial 0a99fa2fd669 where a
call to `rendar_markdown_path` could be made to just render markdown,
and not some other unmentioned and unrelated templating language as
well.
2023-03-10 02:47:44 +01:00
|
|
|
options={
|
|
|
|
"markdown_template_path": markdown_template.name,
|
|
|
|
"reply_to": reply_to,
|
|
|
|
"subject": email_subject,
|
|
|
|
"from_name": from_name,
|
|
|
|
},
|
|
|
|
)
|
2021-05-17 05:41:32 +02:00
|
|
|
self.assert_length(mail.outbox, 1)
|
2020-04-09 13:39:16 +02:00
|
|
|
msg = mail.outbox[0]
|
|
|
|
self.assertEqual(msg.subject, email_subject)
|
2021-05-17 05:41:32 +02:00
|
|
|
self.assert_length(msg.reply_to, 1)
|
2020-04-09 13:39:16 +02:00
|
|
|
self.assertEqual(msg.reply_to[0], reply_to)
|
|
|
|
self.assertNotIn("{% block content %}", msg.body)
|
markdown: Fix use of pure_markdown for non-pure markdown rendering.
`render_markdown_path` renders Markdown, and also (since baff121115a1)
runs Jinja2 on the resulting HTML.
The `pure_markdown` flag was added in 0a99fa2fd669, and did two
things: retried the path directly in the filesystem if it wasn't found
by the Jinja2 resolver, and also skipped the subsequent Jinja2
templating step (regardless of where the content was found). In this
context, the name `pure_markdown` made some sense. The only two
callsites were the TOS and privacy policy renders, which might have
had user-supplied arbitrary paths, and we wished to handle absolute
paths in addition to ones inside `templates/`.
Unfortunately, the follow-up of 01bd55bbcbf7 did not refactor the
logic -- it changed it, by making `pure_markdown` only do the former
of the two behaviors. Passing `pure_markdown=True` after that commit
still caused it to always run Jinja2, but allowed it to look elsewhere
in the filesystem.
This set the stage for calls, such as the one introduced in
dedea237456b, which passed both a context for Jinja2, as well as
`pure_markdown=True` implying that Jinja2 was not to be used.
Split the two previous behaviors of the `pure_markdown` flag, and use
pre-existing data to control them, rather than an explicit flag. For
handling policy information which is stored at an absolute path
outside of the template root, we switch to using the template search
path if and only if the path is relative. This also closes the
potential inconsistency based on CWD when `pure_markdown=True` was
passed and the path was relative, not absolute.
Decide whether to run Jinja2 based on if a context is passed in at
all. This restores the behavior in the initial 0a99fa2fd669 where a
call to `rendar_markdown_path` could be made to just render markdown,
and not some other unmentioned and unrelated templating language as
well.
2023-03-10 02:47:44 +01:00
|
|
|
self.assertIn("# Some heading", msg.body)
|
|
|
|
self.assertIn("Zulip Dev", msg.body)
|
2023-08-03 19:57:21 +02:00
|
|
|
self.assertNotIn("{{ realm_name }}", msg.body)
|
|
|
|
self.assertNotIn("</div>", msg.body)
|
markdown: Fix use of pure_markdown for non-pure markdown rendering.
`render_markdown_path` renders Markdown, and also (since baff121115a1)
runs Jinja2 on the resulting HTML.
The `pure_markdown` flag was added in 0a99fa2fd669, and did two
things: retried the path directly in the filesystem if it wasn't found
by the Jinja2 resolver, and also skipped the subsequent Jinja2
templating step (regardless of where the content was found). In this
context, the name `pure_markdown` made some sense. The only two
callsites were the TOS and privacy policy renders, which might have
had user-supplied arbitrary paths, and we wished to handle absolute
paths in addition to ones inside `templates/`.
Unfortunately, the follow-up of 01bd55bbcbf7 did not refactor the
logic -- it changed it, by making `pure_markdown` only do the former
of the two behaviors. Passing `pure_markdown=True` after that commit
still caused it to always run Jinja2, but allowed it to look elsewhere
in the filesystem.
This set the stage for calls, such as the one introduced in
dedea237456b, which passed both a context for Jinja2, as well as
`pure_markdown=True` implying that Jinja2 was not to be used.
Split the two previous behaviors of the `pure_markdown` flag, and use
pre-existing data to control them, rather than an explicit flag. For
handling policy information which is stored at an absolute path
outside of the template root, we switch to using the template search
path if and only if the path is relative. This also closes the
potential inconsistency based on CWD when `pure_markdown=True` was
passed and the path was relative, not absolute.
Decide whether to run Jinja2 based on if a context is passed in at
all. This restores the behavior in the initial 0a99fa2fd669 where a
call to `rendar_markdown_path` could be made to just render markdown,
and not some other unmentioned and unrelated templating language as
well.
2023-03-10 02:47:44 +01:00
|
|
|
|
|
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
|
|
|
self.assertIn("Some heading</h1>", str(msg.alternatives[0][0]))
|
2023-08-03 19:57:21 +02:00
|
|
|
self.assertNotIn("{{ realm_name }}", str(msg.alternatives[0][0]))
|
2020-04-09 13:39:16 +02:00
|
|
|
|
2021-12-15 02:09:12 +01:00
|
|
|
def test_send_custom_email_remote_server(self) -> None:
|
|
|
|
email_subject = "subject_test"
|
|
|
|
reply_to = "reply_to_test"
|
|
|
|
from_name = "from_name_test"
|
|
|
|
markdown_template_path = "templates/corporate/policies/index.md"
|
2023-12-13 23:05:21 +01:00
|
|
|
send_custom_server_email(
|
|
|
|
remote_servers=RemoteZulipServer.objects.all(),
|
2023-12-13 22:53:47 +01:00
|
|
|
dry_run=False,
|
2021-12-15 02:09:12 +01:00
|
|
|
options={
|
|
|
|
"markdown_template_path": markdown_template_path,
|
|
|
|
"reply_to": reply_to,
|
|
|
|
"subject": email_subject,
|
|
|
|
"from_name": from_name,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
self.assert_length(mail.outbox, 1)
|
|
|
|
msg = mail.outbox[0]
|
|
|
|
self.assertEqual(msg.subject, email_subject)
|
2023-12-13 23:05:21 +01:00
|
|
|
self.assertEqual(msg.to, ["remotezulipserver@zulip.com"])
|
2021-12-15 02:09:12 +01:00
|
|
|
self.assert_length(msg.reply_to, 1)
|
|
|
|
self.assertEqual(msg.reply_to[0], reply_to)
|
|
|
|
self.assertNotIn("{% block content %}", msg.body)
|
|
|
|
# Verify that the HTML version contains the footer.
|
markdown: Fix use of pure_markdown for non-pure markdown rendering.
`render_markdown_path` renders Markdown, and also (since baff121115a1)
runs Jinja2 on the resulting HTML.
The `pure_markdown` flag was added in 0a99fa2fd669, and did two
things: retried the path directly in the filesystem if it wasn't found
by the Jinja2 resolver, and also skipped the subsequent Jinja2
templating step (regardless of where the content was found). In this
context, the name `pure_markdown` made some sense. The only two
callsites were the TOS and privacy policy renders, which might have
had user-supplied arbitrary paths, and we wished to handle absolute
paths in addition to ones inside `templates/`.
Unfortunately, the follow-up of 01bd55bbcbf7 did not refactor the
logic -- it changed it, by making `pure_markdown` only do the former
of the two behaviors. Passing `pure_markdown=True` after that commit
still caused it to always run Jinja2, but allowed it to look elsewhere
in the filesystem.
This set the stage for calls, such as the one introduced in
dedea237456b, which passed both a context for Jinja2, as well as
`pure_markdown=True` implying that Jinja2 was not to be used.
Split the two previous behaviors of the `pure_markdown` flag, and use
pre-existing data to control them, rather than an explicit flag. For
handling policy information which is stored at an absolute path
outside of the template root, we switch to using the template search
path if and only if the path is relative. This also closes the
potential inconsistency based on CWD when `pure_markdown=True` was
passed and the path was relative, not absolute.
Decide whether to run Jinja2 based on if a context is passed in at
all. This restores the behavior in the initial 0a99fa2fd669 where a
call to `rendar_markdown_path` could be made to just render markdown,
and not some other unmentioned and unrelated templating language as
well.
2023-03-10 02:47:44 +01:00
|
|
|
assert isinstance(msg, EmailMultiAlternatives)
|
2021-12-15 02:09:12 +01:00
|
|
|
self.assertIn(
|
|
|
|
"You are receiving this email to update you about important changes to Zulip",
|
markdown: Fix use of pure_markdown for non-pure markdown rendering.
`render_markdown_path` renders Markdown, and also (since baff121115a1)
runs Jinja2 on the resulting HTML.
The `pure_markdown` flag was added in 0a99fa2fd669, and did two
things: retried the path directly in the filesystem if it wasn't found
by the Jinja2 resolver, and also skipped the subsequent Jinja2
templating step (regardless of where the content was found). In this
context, the name `pure_markdown` made some sense. The only two
callsites were the TOS and privacy policy renders, which might have
had user-supplied arbitrary paths, and we wished to handle absolute
paths in addition to ones inside `templates/`.
Unfortunately, the follow-up of 01bd55bbcbf7 did not refactor the
logic -- it changed it, by making `pure_markdown` only do the former
of the two behaviors. Passing `pure_markdown=True` after that commit
still caused it to always run Jinja2, but allowed it to look elsewhere
in the filesystem.
This set the stage for calls, such as the one introduced in
dedea237456b, which passed both a context for Jinja2, as well as
`pure_markdown=True` implying that Jinja2 was not to be used.
Split the two previous behaviors of the `pure_markdown` flag, and use
pre-existing data to control them, rather than an explicit flag. For
handling policy information which is stored at an absolute path
outside of the template root, we switch to using the template search
path if and only if the path is relative. This also closes the
potential inconsistency based on CWD when `pure_markdown=True` was
passed and the path was relative, not absolute.
Decide whether to run Jinja2 based on if a context is passed in at
all. This restores the behavior in the initial 0a99fa2fd669 where a
call to `rendar_markdown_path` could be made to just render markdown,
and not some other unmentioned and unrelated templating language as
well.
2023-03-10 02:47:44 +01:00
|
|
|
str(msg.alternatives[0][0]),
|
2021-12-15 02:09:12 +01:00
|
|
|
)
|
2024-09-05 09:02:56 +02:00
|
|
|
self.assertIn("Unsubscribe", str(msg.alternatives[0][0]))
|
2024-09-05 11:29:39 +02:00
|
|
|
# Verify that the Text version contains the footer.
|
|
|
|
self.assertIn(
|
|
|
|
"You are receiving this email to update you about important changes to Zulip", msg.body
|
|
|
|
)
|
|
|
|
self.assertIn("Unsubscribe", msg.body)
|
2021-12-15 02:09:12 +01:00
|
|
|
|
2020-04-09 13:39:16 +02:00
|
|
|
def test_send_custom_email_headers(self) -> None:
|
2021-02-12 08:20:45 +01:00
|
|
|
hamlet = self.example_user("hamlet")
|
2021-02-12 08:19:30 +01:00
|
|
|
markdown_template_path = (
|
2023-08-03 23:07:36 +02:00
|
|
|
"zerver/tests/fixtures/email/custom_emails/email_base_headers_test.md"
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
|
|
|
send_custom_email(
|
2023-08-03 22:20:37 +02:00
|
|
|
UserProfile.objects.filter(id=hamlet.id),
|
2023-12-13 22:53:47 +01:00
|
|
|
dry_run=False,
|
2021-12-15 01:45:35 +01:00
|
|
|
options={
|
2021-02-12 08:19:30 +01:00
|
|
|
"markdown_template_path": markdown_template_path,
|
|
|
|
},
|
|
|
|
)
|
2021-05-17 05:41:32 +02:00
|
|
|
self.assert_length(mail.outbox, 1)
|
2020-04-09 13:39:16 +02:00
|
|
|
msg = mail.outbox[0]
|
2021-05-10 07:02:14 +02:00
|
|
|
self.assertEqual(msg.subject, "Test subject")
|
2020-04-09 13:39:16 +02:00
|
|
|
self.assertFalse(msg.reply_to)
|
2024-09-05 11:29:39 +02:00
|
|
|
self.assertIn("Test body", msg.body)
|
2020-04-09 13:39:16 +02:00
|
|
|
|
2023-08-03 23:22:21 +02:00
|
|
|
def test_send_custom_email_context(self) -> None:
|
|
|
|
hamlet = self.example_user("hamlet")
|
|
|
|
markdown_template_path = (
|
|
|
|
"zerver/tests/fixtures/email/custom_emails/email_base_headers_test.md"
|
|
|
|
)
|
|
|
|
send_custom_email(
|
|
|
|
UserProfile.objects.filter(id=hamlet.id),
|
2023-12-13 22:53:47 +01:00
|
|
|
dry_run=False,
|
2023-08-03 23:22:21 +02:00
|
|
|
options={
|
|
|
|
"markdown_template_path": markdown_template_path,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
self.assert_length(mail.outbox, 1)
|
|
|
|
msg = mail.outbox[0]
|
|
|
|
|
|
|
|
# We default to not including an unsubscribe link in the headers
|
|
|
|
self.assertEqual(msg.extra_headers.get("X-Auto-Response-Suppress"), "All")
|
|
|
|
self.assertIsNone(msg.extra_headers.get("List-Unsubscribe"))
|
|
|
|
|
|
|
|
mail.outbox = []
|
|
|
|
markdown_template_path = (
|
|
|
|
"zerver/tests/fixtures/email/custom_emails/email_base_headers_custom_test.md"
|
|
|
|
)
|
|
|
|
|
2024-07-12 02:30:17 +02:00
|
|
|
def add_context(context: dict[str, object], user: UserProfile) -> None:
|
2023-08-03 23:22:21 +02:00
|
|
|
context["unsubscribe_link"] = "some@email"
|
|
|
|
context["custom"] = str(user.id)
|
|
|
|
|
|
|
|
send_custom_email(
|
|
|
|
UserProfile.objects.filter(id=hamlet.id),
|
2023-12-13 22:53:47 +01:00
|
|
|
dry_run=False,
|
2023-08-03 23:22:21 +02:00
|
|
|
options={
|
|
|
|
"markdown_template_path": markdown_template_path,
|
|
|
|
},
|
|
|
|
add_context=add_context,
|
|
|
|
)
|
|
|
|
self.assert_length(mail.outbox, 1)
|
|
|
|
msg = mail.outbox[0]
|
|
|
|
self.assertEqual(msg.extra_headers.get("X-Auto-Response-Suppress"), "All")
|
|
|
|
self.assertEqual(msg.extra_headers.get("List-Unsubscribe"), "<some@email>")
|
|
|
|
self.assertIn(f"Test body with {hamlet.id} value", msg.body)
|
|
|
|
|
2020-04-09 13:39:16 +02:00
|
|
|
def test_send_custom_email_no_argument(self) -> None:
|
2021-02-12 08:20:45 +01:00
|
|
|
hamlet = self.example_user("hamlet")
|
2020-04-09 13:39:16 +02:00
|
|
|
from_name = "from_name_test"
|
2021-02-12 08:20:45 +01:00
|
|
|
email_subject = "subject_test"
|
2023-04-05 11:19:58 +02:00
|
|
|
markdown_template_path = (
|
2023-08-03 23:07:36 +02:00
|
|
|
"zerver/tests/fixtures/email/custom_emails/email_base_headers_no_headers_test.md"
|
2023-04-05 11:19:58 +02:00
|
|
|
)
|
2020-04-09 13:39:16 +02:00
|
|
|
|
2022-11-17 09:30:48 +01:00
|
|
|
from zerver.lib.send_email import NoEmailArgumentError
|
2020-04-09 13:39:16 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
self.assertRaises(
|
2022-11-17 09:30:48 +01:00
|
|
|
NoEmailArgumentError,
|
2021-02-12 08:19:30 +01:00
|
|
|
send_custom_email,
|
2023-08-03 22:20:37 +02:00
|
|
|
UserProfile.objects.filter(id=hamlet.id),
|
2023-12-13 22:53:47 +01:00
|
|
|
dry_run=False,
|
2021-12-15 01:45:35 +01:00
|
|
|
options={
|
2021-02-12 08:19:30 +01:00
|
|
|
"markdown_template_path": markdown_template_path,
|
|
|
|
"from_name": from_name,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assertRaises(
|
2022-11-17 09:30:48 +01:00
|
|
|
NoEmailArgumentError,
|
2021-02-12 08:19:30 +01:00
|
|
|
send_custom_email,
|
2023-08-03 22:20:37 +02:00
|
|
|
UserProfile.objects.filter(id=hamlet.id),
|
2023-12-13 22:53:47 +01:00
|
|
|
dry_run=False,
|
2021-12-15 01:45:35 +01:00
|
|
|
options={
|
2021-02-12 08:19:30 +01:00
|
|
|
"markdown_template_path": markdown_template_path,
|
|
|
|
"subject": email_subject,
|
|
|
|
},
|
|
|
|
)
|
2020-04-09 13:39:16 +02:00
|
|
|
|
|
|
|
def test_send_custom_email_doubled_arguments(self) -> None:
|
2021-02-12 08:20:45 +01:00
|
|
|
hamlet = self.example_user("hamlet")
|
2020-04-09 13:39:16 +02:00
|
|
|
from_name = "from_name_test"
|
2021-02-12 08:20:45 +01:00
|
|
|
email_subject = "subject_test"
|
2021-02-12 08:19:30 +01:00
|
|
|
markdown_template_path = (
|
2023-08-03 23:07:36 +02:00
|
|
|
"zerver/tests/fixtures/email/custom_emails/email_base_headers_test.md"
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2020-04-09 13:39:16 +02:00
|
|
|
|
2022-11-17 09:30:48 +01:00
|
|
|
from zerver.lib.send_email import DoubledEmailArgumentError
|
2020-04-09 13:39:16 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
self.assertRaises(
|
2022-11-17 09:30:48 +01:00
|
|
|
DoubledEmailArgumentError,
|
2021-02-12 08:19:30 +01:00
|
|
|
send_custom_email,
|
2023-08-03 22:20:37 +02:00
|
|
|
UserProfile.objects.filter(id=hamlet.id),
|
2023-12-13 22:53:47 +01:00
|
|
|
dry_run=False,
|
2021-12-15 01:45:35 +01:00
|
|
|
options={
|
2021-02-12 08:19:30 +01:00
|
|
|
"markdown_template_path": markdown_template_path,
|
|
|
|
"subject": email_subject,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assertRaises(
|
2022-11-17 09:30:48 +01:00
|
|
|
DoubledEmailArgumentError,
|
2021-02-12 08:19:30 +01:00
|
|
|
send_custom_email,
|
2023-08-03 22:20:37 +02:00
|
|
|
UserProfile.objects.filter(id=hamlet.id),
|
2023-12-13 22:53:47 +01:00
|
|
|
dry_run=False,
|
2021-12-15 01:45:35 +01:00
|
|
|
options={
|
2021-02-12 08:19:30 +01:00
|
|
|
"markdown_template_path": markdown_template_path,
|
|
|
|
"from_name": from_name,
|
|
|
|
},
|
|
|
|
)
|
2020-04-09 13:39:16 +02:00
|
|
|
|
2021-04-07 01:27:02 +02:00
|
|
|
def test_send_custom_email_dry_run(self) -> None:
|
|
|
|
hamlet = self.example_user("hamlet")
|
|
|
|
email_subject = "subject_test"
|
|
|
|
reply_to = "reply_to_test"
|
|
|
|
from_name = "from_name_test"
|
|
|
|
markdown_template_path = "templates/zerver/tests/markdown/test_nested_code_blocks.md"
|
|
|
|
with patch("builtins.print") as _:
|
|
|
|
send_custom_email(
|
2023-08-03 22:20:37 +02:00
|
|
|
UserProfile.objects.filter(id=hamlet.id),
|
2023-12-13 22:53:47 +01:00
|
|
|
dry_run=True,
|
2021-12-15 01:45:35 +01:00
|
|
|
options={
|
2021-04-07 01:27:02 +02:00
|
|
|
"markdown_template_path": markdown_template_path,
|
|
|
|
"reply_to": reply_to,
|
|
|
|
"subject": email_subject,
|
|
|
|
"from_name": from_name,
|
|
|
|
},
|
|
|
|
)
|
2021-05-17 05:41:32 +02:00
|
|
|
self.assert_length(mail.outbox, 0)
|
2021-04-07 01:27:02 +02:00
|
|
|
|
2020-04-09 13:39:16 +02:00
|
|
|
|
2018-11-14 12:46:56 +01:00
|
|
|
class TestFollowupEmails(ZulipTestCase):
|
2023-09-10 17:12:59 +02:00
|
|
|
def test_account_registered_email_context(self) -> None:
|
2018-11-14 12:46:56 +01:00
|
|
|
hamlet = self.example_user("hamlet")
|
2023-06-30 13:27:25 +02:00
|
|
|
send_account_registered_email(hamlet)
|
2023-03-15 20:18:09 +01:00
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=hamlet).order_by(
|
|
|
|
"scheduled_timestamp"
|
|
|
|
)
|
2020-08-07 01:09:47 +02:00
|
|
|
email_data = orjson.loads(scheduled_emails[0].data)
|
2018-11-14 12:46:56 +01:00
|
|
|
self.assertEqual(email_data["context"]["email"], self.example_email("hamlet"))
|
|
|
|
self.assertEqual(email_data["context"]["is_realm_admin"], False)
|
2023-02-03 02:16:43 +01:00
|
|
|
self.assertEqual(
|
|
|
|
email_data["context"]["getting_user_started_link"],
|
|
|
|
"http://zulip.testserver/help/getting-started-with-zulip",
|
|
|
|
)
|
2018-11-14 12:46:56 +01:00
|
|
|
self.assertNotIn("ldap_username", email_data["context"])
|
|
|
|
|
|
|
|
ScheduledEmail.objects.all().delete()
|
|
|
|
|
|
|
|
iago = self.example_user("iago")
|
2023-06-30 13:27:25 +02:00
|
|
|
send_account_registered_email(iago)
|
2023-03-15 20:18:09 +01:00
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=iago).order_by("scheduled_timestamp")
|
2020-08-07 01:09:47 +02:00
|
|
|
email_data = orjson.loads(scheduled_emails[0].data)
|
2018-11-14 12:46:56 +01:00
|
|
|
self.assertEqual(email_data["context"]["email"], self.example_email("iago"))
|
|
|
|
self.assertEqual(email_data["context"]["is_realm_admin"], True)
|
2021-02-12 08:19:30 +01:00
|
|
|
self.assertEqual(
|
2023-02-03 02:16:43 +01:00
|
|
|
email_data["context"]["getting_organization_started_link"],
|
2021-02-12 08:19:30 +01:00
|
|
|
"http://zulip.testserver/help/getting-your-organization-started-with-zulip",
|
|
|
|
)
|
2023-02-03 02:16:43 +01:00
|
|
|
self.assertEqual(
|
|
|
|
email_data["context"]["getting_user_started_link"],
|
|
|
|
"http://zulip.testserver/help/getting-started-with-zulip",
|
|
|
|
)
|
2018-11-14 12:46:56 +01:00
|
|
|
self.assertNotIn("ldap_username", email_data["context"])
|
|
|
|
|
2018-11-29 16:32:17 +01:00
|
|
|
# See https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#ldap-including-active-directory
|
|
|
|
# for case details.
|
2021-02-12 08:19:30 +01:00
|
|
|
@override_settings(
|
|
|
|
AUTHENTICATION_BACKENDS=(
|
2021-02-12 08:20:45 +01:00
|
|
|
"zproject.backends.ZulipLDAPAuthBackend",
|
|
|
|
"zproject.backends.ZulipDummyBackend",
|
2021-02-12 08:19:30 +01:00
|
|
|
),
|
|
|
|
# configure email search for email address in the uid attribute:
|
|
|
|
AUTH_LDAP_REVERSE_EMAIL_SEARCH=LDAPSearch(
|
|
|
|
"ou=users,dc=zulip,dc=com", ldap.SCOPE_ONELEVEL, "(uid=%(email)s)"
|
|
|
|
),
|
|
|
|
)
|
2023-09-10 17:12:59 +02:00
|
|
|
def test_account_registered_email_ldap_case_a_login_credentials(self) -> None:
|
2019-10-16 18:27:53 +02:00
|
|
|
self.init_default_ldap_database()
|
2021-02-12 08:20:45 +01:00
|
|
|
ldap_user_attr_map = {"full_name": "cn"}
|
2019-10-16 18:27:53 +02:00
|
|
|
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map):
|
2021-02-12 08:19:30 +01:00
|
|
|
self.login_with_return(
|
|
|
|
"newuser_email_as_uid@zulip.com",
|
|
|
|
self.ldap_password("newuser_email_as_uid@zulip.com"),
|
|
|
|
)
|
2020-03-12 14:17:25 +01:00
|
|
|
user = UserProfile.objects.get(delivery_email="newuser_email_as_uid@zulip.com")
|
2023-03-15 20:18:09 +01:00
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=user).order_by(
|
|
|
|
"scheduled_timestamp"
|
|
|
|
)
|
2018-11-29 16:32:17 +01:00
|
|
|
|
2023-03-15 20:18:09 +01:00
|
|
|
self.assert_length(scheduled_emails, 3)
|
2020-08-07 01:09:47 +02:00
|
|
|
email_data = orjson.loads(scheduled_emails[0].data)
|
2018-11-29 16:32:17 +01:00
|
|
|
self.assertEqual(email_data["context"]["ldap"], True)
|
2021-02-12 08:19:30 +01:00
|
|
|
self.assertEqual(
|
|
|
|
email_data["context"]["ldap_username"], "newuser_email_as_uid@zulip.com"
|
|
|
|
)
|
|
|
|
|
|
|
|
@override_settings(
|
|
|
|
AUTHENTICATION_BACKENDS=(
|
2021-02-12 08:20:45 +01:00
|
|
|
"zproject.backends.ZulipLDAPAuthBackend",
|
|
|
|
"zproject.backends.ZulipDummyBackend",
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
|
|
|
)
|
2023-09-10 17:12:59 +02:00
|
|
|
def test_account_registered_email_ldap_case_b_login_credentials(self) -> None:
|
2019-10-16 18:27:53 +02:00
|
|
|
self.init_default_ldap_database()
|
2021-02-12 08:20:45 +01:00
|
|
|
ldap_user_attr_map = {"full_name": "cn"}
|
2018-11-14 12:46:56 +01:00
|
|
|
|
|
|
|
with self.settings(
|
2021-02-12 08:20:45 +01:00
|
|
|
LDAP_APPEND_DOMAIN="zulip.com",
|
2021-02-12 08:19:30 +01:00
|
|
|
AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map,
|
2019-10-16 18:27:53 +02:00
|
|
|
):
|
2020-02-19 19:40:49 +01:00
|
|
|
self.login_with_return("newuser@zulip.com", self.ldap_password("newuser"))
|
2018-11-29 16:32:17 +01:00
|
|
|
|
2020-03-12 14:17:25 +01:00
|
|
|
user = UserProfile.objects.get(delivery_email="newuser@zulip.com")
|
2023-03-15 20:18:09 +01:00
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=user).order_by(
|
|
|
|
"scheduled_timestamp"
|
|
|
|
)
|
|
|
|
self.assert_length(scheduled_emails, 3)
|
2020-08-07 01:09:47 +02:00
|
|
|
email_data = orjson.loads(scheduled_emails[0].data)
|
2018-11-29 16:32:17 +01:00
|
|
|
self.assertEqual(email_data["context"]["ldap"], True)
|
|
|
|
self.assertEqual(email_data["context"]["ldap_username"], "newuser")
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
@override_settings(
|
|
|
|
AUTHENTICATION_BACKENDS=(
|
2021-02-12 08:20:45 +01:00
|
|
|
"zproject.backends.ZulipLDAPAuthBackend",
|
|
|
|
"zproject.backends.ZulipDummyBackend",
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
|
|
|
)
|
2023-09-10 17:12:59 +02:00
|
|
|
def test_account_registered_email_ldap_case_c_login_credentials(self) -> None:
|
2019-10-16 18:27:53 +02:00
|
|
|
self.init_default_ldap_database()
|
2021-02-12 08:20:45 +01:00
|
|
|
ldap_user_attr_map = {"full_name": "cn"}
|
2018-11-29 16:32:17 +01:00
|
|
|
|
|
|
|
with self.settings(
|
2021-02-12 08:20:45 +01:00
|
|
|
LDAP_EMAIL_ATTR="mail",
|
2021-02-12 08:19:30 +01:00
|
|
|
AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map,
|
2019-10-16 18:27:53 +02:00
|
|
|
):
|
2020-02-19 19:40:49 +01:00
|
|
|
self.login_with_return("newuser_with_email", self.ldap_password("newuser_with_email"))
|
2020-03-12 14:17:25 +01:00
|
|
|
user = UserProfile.objects.get(delivery_email="newuser_email@zulip.com")
|
2023-03-15 20:18:09 +01:00
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=user).order_by(
|
|
|
|
"scheduled_timestamp"
|
|
|
|
)
|
|
|
|
self.assert_length(scheduled_emails, 3)
|
2020-08-07 01:09:47 +02:00
|
|
|
email_data = orjson.loads(scheduled_emails[0].data)
|
2018-11-29 16:32:17 +01:00
|
|
|
self.assertEqual(email_data["context"]["ldap"], True)
|
2019-11-05 02:29:03 +01:00
|
|
|
self.assertEqual(email_data["context"]["ldap_username"], "newuser_with_email")
|
2018-11-14 12:46:56 +01:00
|
|
|
|
2018-11-14 12:58:35 +01:00
|
|
|
def test_followup_emails_count(self) -> None:
|
|
|
|
hamlet = self.example_user("hamlet")
|
2023-03-15 20:18:09 +01:00
|
|
|
iago = self.example_user("iago")
|
2018-11-14 12:58:35 +01:00
|
|
|
cordelia = self.example_user("cordelia")
|
2023-03-15 20:18:09 +01:00
|
|
|
realm = get_realm("zulip")
|
2018-11-14 12:58:35 +01:00
|
|
|
|
2023-09-10 17:12:59 +02:00
|
|
|
# Hamlet has account only in Zulip realm so all onboarding emails should be sent
|
2023-06-30 13:27:25 +02:00
|
|
|
send_account_registered_email(self.example_user("hamlet"))
|
2023-03-15 20:18:09 +01:00
|
|
|
enqueue_welcome_emails(self.example_user("hamlet"))
|
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=hamlet).order_by(
|
|
|
|
"scheduled_timestamp"
|
|
|
|
)
|
|
|
|
self.assert_length(scheduled_emails, 3)
|
|
|
|
self.assertEqual(
|
2023-07-18 11:44:27 +02:00
|
|
|
orjson.loads(scheduled_emails[0].data)["template_prefix"],
|
|
|
|
"zerver/emails/account_registered",
|
2023-03-15 20:18:09 +01:00
|
|
|
)
|
|
|
|
self.assertEqual(
|
2023-07-18 11:50:12 +02:00
|
|
|
orjson.loads(scheduled_emails[1].data)["template_prefix"],
|
|
|
|
"zerver/emails/onboarding_zulip_topics",
|
2023-03-15 20:18:09 +01:00
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
orjson.loads(scheduled_emails[2].data)["template_prefix"],
|
|
|
|
"zerver/emails/onboarding_zulip_guide",
|
|
|
|
)
|
|
|
|
|
|
|
|
ScheduledEmail.objects.all().delete()
|
|
|
|
|
|
|
|
# The onboarding_zulip_guide email should not be sent to non-admin users in organizations
|
|
|
|
# that are sent the `/for/communities/` guide; see note in enqueue_welcome_emails.
|
|
|
|
realm.org_type = Realm.ORG_TYPES["community"]["id"]
|
|
|
|
realm.save()
|
|
|
|
|
|
|
|
# Hamlet is not an admin so the `/for/communities/` zulip_guide should not be sent
|
2023-06-30 13:27:25 +02:00
|
|
|
send_account_registered_email(self.example_user("hamlet"))
|
2018-11-14 12:58:35 +01:00
|
|
|
enqueue_welcome_emails(self.example_user("hamlet"))
|
2019-04-29 07:00:03 +02:00
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=hamlet).order_by(
|
2021-02-12 08:19:30 +01:00
|
|
|
"scheduled_timestamp"
|
|
|
|
)
|
2021-07-13 19:39:37 +02:00
|
|
|
self.assert_length(scheduled_emails, 2)
|
2023-03-15 20:18:09 +01:00
|
|
|
self.assertEqual(
|
2023-07-18 11:44:27 +02:00
|
|
|
orjson.loads(scheduled_emails[0].data)["template_prefix"],
|
|
|
|
"zerver/emails/account_registered",
|
2023-03-15 20:18:09 +01:00
|
|
|
)
|
|
|
|
self.assertEqual(
|
2023-07-18 11:50:12 +02:00
|
|
|
orjson.loads(scheduled_emails[1].data)["template_prefix"],
|
|
|
|
"zerver/emails/onboarding_zulip_topics",
|
2023-03-15 20:18:09 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
ScheduledEmail.objects.all().delete()
|
|
|
|
|
|
|
|
# Iago is an admin so the `/for/communities/` zulip_guide should be sent
|
2023-06-30 13:27:25 +02:00
|
|
|
send_account_registered_email(self.example_user("iago"))
|
2023-03-15 20:18:09 +01:00
|
|
|
enqueue_welcome_emails(self.example_user("iago"))
|
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=iago).order_by("scheduled_timestamp")
|
|
|
|
self.assert_length(scheduled_emails, 3)
|
|
|
|
self.assertEqual(
|
2023-07-18 11:44:27 +02:00
|
|
|
orjson.loads(scheduled_emails[0].data)["template_prefix"],
|
|
|
|
"zerver/emails/account_registered",
|
2023-03-15 20:18:09 +01:00
|
|
|
)
|
2021-02-12 08:19:30 +01:00
|
|
|
self.assertEqual(
|
2023-07-18 11:50:12 +02:00
|
|
|
orjson.loads(scheduled_emails[1].data)["template_prefix"],
|
|
|
|
"zerver/emails/onboarding_zulip_topics",
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2023-03-15 20:18:09 +01:00
|
|
|
self.assertEqual(
|
|
|
|
orjson.loads(scheduled_emails[2].data)["template_prefix"],
|
|
|
|
"zerver/emails/onboarding_zulip_guide",
|
|
|
|
)
|
|
|
|
|
|
|
|
ScheduledEmail.objects.all().delete()
|
|
|
|
|
|
|
|
# The organization_type context for "education_nonprofit" orgs is simplified to be "education"
|
|
|
|
realm.org_type = Realm.ORG_TYPES["education_nonprofit"]["id"]
|
|
|
|
realm.save()
|
|
|
|
|
2023-09-10 17:12:59 +02:00
|
|
|
# Cordelia has account in more than 1 realm so onboarding_zulip_topics email should not be sent
|
2023-06-30 13:27:25 +02:00
|
|
|
send_account_registered_email(self.example_user("cordelia"))
|
2023-03-15 20:18:09 +01:00
|
|
|
enqueue_welcome_emails(self.example_user("cordelia"))
|
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=cordelia).order_by(
|
|
|
|
"scheduled_timestamp"
|
|
|
|
)
|
|
|
|
self.assert_length(scheduled_emails, 2)
|
2021-02-12 08:19:30 +01:00
|
|
|
self.assertEqual(
|
2023-07-18 11:44:27 +02:00
|
|
|
orjson.loads(scheduled_emails[0].data)["template_prefix"],
|
|
|
|
"zerver/emails/account_registered",
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2023-03-15 20:18:09 +01:00
|
|
|
self.assertEqual(
|
|
|
|
orjson.loads(scheduled_emails[1].data)["template_prefix"],
|
|
|
|
"zerver/emails/onboarding_zulip_guide",
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
orjson.loads(scheduled_emails[1].data)["context"]["organization_type"],
|
|
|
|
"education",
|
|
|
|
)
|
2018-11-14 12:58:35 +01:00
|
|
|
|
|
|
|
ScheduledEmail.objects.all().delete()
|
|
|
|
|
2023-03-15 20:18:09 +01:00
|
|
|
# Only a subset of Realm.ORG_TYPES are sent the zulip_guide_followup email
|
|
|
|
realm.org_type = Realm.ORG_TYPES["other"]["id"]
|
|
|
|
realm.save()
|
|
|
|
|
2023-09-10 17:12:59 +02:00
|
|
|
# In this case, Cordelia should only be sent the account_registered email
|
2023-06-30 13:27:25 +02:00
|
|
|
send_account_registered_email(self.example_user("cordelia"))
|
2023-03-15 20:18:09 +01:00
|
|
|
enqueue_welcome_emails(self.example_user("cordelia"))
|
2019-01-04 01:50:21 +01:00
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=cordelia)
|
2021-05-17 05:41:32 +02:00
|
|
|
self.assert_length(scheduled_emails, 1)
|
2023-03-15 20:18:09 +01:00
|
|
|
self.assertEqual(
|
2023-07-18 11:44:27 +02:00
|
|
|
orjson.loads(scheduled_emails[0].data)["template_prefix"],
|
|
|
|
"zerver/emails/account_registered",
|
2023-03-15 20:18:09 +01:00
|
|
|
)
|
2018-11-14 12:58:35 +01:00
|
|
|
|
2021-09-09 21:51:55 +02:00
|
|
|
def test_followup_emails_for_regular_realms(self) -> None:
|
|
|
|
cordelia = self.example_user("cordelia")
|
2023-06-30 13:27:25 +02:00
|
|
|
send_account_registered_email(self.example_user("cordelia"), realm_creation=True)
|
2023-04-24 17:46:49 +02:00
|
|
|
enqueue_welcome_emails(self.example_user("cordelia"), realm_creation=True)
|
2023-03-15 20:18:09 +01:00
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=cordelia).order_by(
|
|
|
|
"scheduled_timestamp"
|
|
|
|
)
|
|
|
|
assert scheduled_emails is not None
|
2023-04-24 17:46:49 +02:00
|
|
|
self.assert_length(scheduled_emails, 3)
|
2023-03-15 20:18:09 +01:00
|
|
|
self.assertEqual(
|
2023-07-18 11:44:27 +02:00
|
|
|
orjson.loads(scheduled_emails[0].data)["template_prefix"],
|
|
|
|
"zerver/emails/account_registered",
|
2023-03-15 20:18:09 +01:00
|
|
|
)
|
2021-09-09 21:51:55 +02:00
|
|
|
self.assertEqual(
|
2023-03-15 20:18:09 +01:00
|
|
|
orjson.loads(scheduled_emails[1].data)["template_prefix"],
|
|
|
|
"zerver/emails/onboarding_zulip_guide",
|
2021-09-09 21:51:55 +02:00
|
|
|
)
|
2023-04-24 17:46:49 +02:00
|
|
|
self.assertEqual(
|
|
|
|
orjson.loads(scheduled_emails[2].data)["template_prefix"],
|
|
|
|
"zerver/emails/onboarding_team_to_zulip",
|
|
|
|
)
|
2021-09-09 21:51:55 +02:00
|
|
|
|
2023-03-15 20:18:09 +01:00
|
|
|
deliver_scheduled_emails(scheduled_emails[0])
|
2021-09-09 21:51:55 +02:00
|
|
|
from django.core.mail import outbox
|
|
|
|
|
|
|
|
self.assert_length(outbox, 1)
|
|
|
|
|
|
|
|
message = outbox[0]
|
2023-02-03 02:16:43 +01:00
|
|
|
self.assertIn("you have created a new Zulip organization", message.body)
|
2021-09-09 21:51:55 +02:00
|
|
|
self.assertNotIn("demo org", message.body)
|
|
|
|
|
|
|
|
def test_followup_emails_for_demo_realms(self) -> None:
|
|
|
|
cordelia = self.example_user("cordelia")
|
|
|
|
cordelia.realm.demo_organization_scheduled_deletion_date = timezone_now() + timedelta(
|
|
|
|
days=30
|
|
|
|
)
|
|
|
|
cordelia.realm.save()
|
2023-06-30 13:27:25 +02:00
|
|
|
send_account_registered_email(self.example_user("cordelia"), realm_creation=True)
|
2023-04-24 17:46:49 +02:00
|
|
|
enqueue_welcome_emails(self.example_user("cordelia"), realm_creation=True)
|
2023-03-15 20:18:09 +01:00
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=cordelia).order_by(
|
|
|
|
"scheduled_timestamp"
|
|
|
|
)
|
|
|
|
assert scheduled_emails is not None
|
2023-04-24 17:46:49 +02:00
|
|
|
self.assert_length(scheduled_emails, 3)
|
2023-03-15 20:18:09 +01:00
|
|
|
self.assertEqual(
|
2023-07-18 11:44:27 +02:00
|
|
|
orjson.loads(scheduled_emails[0].data)["template_prefix"],
|
|
|
|
"zerver/emails/account_registered",
|
2023-03-15 20:18:09 +01:00
|
|
|
)
|
2021-09-09 21:51:55 +02:00
|
|
|
self.assertEqual(
|
2023-03-15 20:18:09 +01:00
|
|
|
orjson.loads(scheduled_emails[1].data)["template_prefix"],
|
|
|
|
"zerver/emails/onboarding_zulip_guide",
|
2021-09-09 21:51:55 +02:00
|
|
|
)
|
2023-04-24 17:46:49 +02:00
|
|
|
self.assertEqual(
|
|
|
|
orjson.loads(scheduled_emails[2].data)["template_prefix"],
|
|
|
|
"zerver/emails/onboarding_team_to_zulip",
|
|
|
|
)
|
2021-09-09 21:51:55 +02:00
|
|
|
|
2023-03-15 20:18:09 +01:00
|
|
|
deliver_scheduled_emails(scheduled_emails[0])
|
2021-09-09 21:51:55 +02:00
|
|
|
from django.core.mail import outbox
|
|
|
|
|
|
|
|
self.assert_length(outbox, 1)
|
|
|
|
|
|
|
|
message = outbox[0]
|
2023-02-03 02:16:43 +01:00
|
|
|
self.assertIn("you have created a new demo Zulip organization", message.body)
|
2021-09-09 21:51:55 +02:00
|
|
|
|
2023-03-15 20:18:09 +01:00
|
|
|
def test_onboarding_zulip_guide_with_invalid_org_type(self) -> None:
|
|
|
|
cordelia = self.example_user("cordelia")
|
|
|
|
realm = get_realm("zulip")
|
|
|
|
|
|
|
|
invalid_org_type_id = 999
|
|
|
|
realm.org_type = invalid_org_type_id
|
|
|
|
realm.save()
|
|
|
|
|
|
|
|
with self.assertLogs(level="ERROR") as m:
|
|
|
|
enqueue_welcome_emails(self.example_user("cordelia"))
|
|
|
|
|
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=cordelia)
|
2023-06-30 13:27:25 +02:00
|
|
|
self.assert_length(scheduled_emails, 0)
|
2023-03-15 20:18:09 +01:00
|
|
|
self.assertEqual(
|
|
|
|
m.output,
|
|
|
|
[f"ERROR:root:Unknown organization type '{invalid_org_type_id}'"],
|
|
|
|
)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2023-04-24 17:43:29 +02:00
|
|
|
class TestOnboardingEmailDelay(ZulipTestCase):
|
|
|
|
def verify_onboarding_email_schedule(
|
|
|
|
self,
|
|
|
|
user: UserProfile,
|
|
|
|
date_joined: str,
|
|
|
|
onboarding_zulip_topics: int,
|
|
|
|
onboarding_zulip_guide: int,
|
2023-04-24 17:46:49 +02:00
|
|
|
onboarding_team_to_zulip: int,
|
2023-04-24 17:43:29 +02:00
|
|
|
) -> None:
|
|
|
|
DAY_OF_WEEK = {
|
2023-03-15 18:09:26 +01:00
|
|
|
"Monday": datetime(2018, 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
|
|
|
"Tuesday": datetime(2018, 1, 2, 1, 0, 0, 0, tzinfo=timezone.utc),
|
|
|
|
"Wednesday": datetime(2018, 1, 3, 1, 0, 0, 0, tzinfo=timezone.utc),
|
|
|
|
"Thursday": datetime(2018, 1, 4, 1, 0, 0, 0, tzinfo=timezone.utc),
|
|
|
|
"Friday": datetime(2018, 1, 5, 1, 0, 0, 0, tzinfo=timezone.utc),
|
|
|
|
"Saturday": datetime(2018, 1, 6, 1, 0, 0, 0, tzinfo=timezone.utc),
|
|
|
|
"Sunday": datetime(2018, 1, 7, 1, 0, 0, 0, tzinfo=timezone.utc),
|
|
|
|
}
|
2023-04-24 17:43:29 +02:00
|
|
|
WEEKEND = [6, 7]
|
2023-03-15 20:18:09 +01:00
|
|
|
|
2023-04-24 17:43:29 +02:00
|
|
|
user.date_joined = DAY_OF_WEEK[date_joined]
|
|
|
|
onboarding_email_schedule = get_onboarding_email_schedule(user)
|
2023-03-15 20:18:09 +01:00
|
|
|
|
2023-04-24 17:43:29 +02:00
|
|
|
# onboarding_zulip_topics
|
|
|
|
day_sent = (
|
|
|
|
DAY_OF_WEEK[date_joined] + onboarding_email_schedule["onboarding_zulip_topics"]
|
|
|
|
).isoweekday()
|
|
|
|
self.assertEqual(day_sent, onboarding_zulip_topics)
|
|
|
|
self.assertNotIn(day_sent, WEEKEND)
|
2023-03-15 20:18:09 +01:00
|
|
|
|
2023-04-24 17:43:29 +02:00
|
|
|
# onboarding_zulip_guide
|
|
|
|
day_sent = (
|
|
|
|
DAY_OF_WEEK[date_joined] + onboarding_email_schedule["onboarding_zulip_guide"]
|
|
|
|
).isoweekday()
|
|
|
|
self.assertEqual(day_sent, onboarding_zulip_guide)
|
|
|
|
self.assertNotIn(day_sent, WEEKEND)
|
2023-03-15 18:09:26 +01:00
|
|
|
|
2023-04-24 17:46:49 +02:00
|
|
|
# onboarding_team_to_zulip
|
|
|
|
day_sent = (
|
|
|
|
DAY_OF_WEEK[date_joined] + onboarding_email_schedule["onboarding_team_to_zulip"]
|
|
|
|
).isoweekday()
|
|
|
|
self.assertEqual(day_sent, onboarding_team_to_zulip)
|
|
|
|
self.assertNotIn(day_sent, WEEKEND)
|
|
|
|
|
2023-04-24 17:43:29 +02:00
|
|
|
def test_get_onboarding_email_schedule(self) -> None:
|
|
|
|
user_profile = self.example_user("hamlet")
|
2023-03-15 20:18:09 +01:00
|
|
|
|
2023-04-24 17:46:49 +02:00
|
|
|
# joined Monday: schedule = Wednesday:3, Friday:5, Tuesday:2
|
|
|
|
self.verify_onboarding_email_schedule(user_profile, "Monday", 3, 5, 2)
|
2023-03-15 18:09:26 +01:00
|
|
|
|
2023-04-24 17:46:49 +02:00
|
|
|
# joined Tuesday: schedule = Thursday:4, Monday:1, Wednesday:3
|
|
|
|
self.verify_onboarding_email_schedule(user_profile, "Tuesday", 4, 1, 3)
|
2023-03-15 20:18:09 +01:00
|
|
|
|
2023-04-24 17:46:49 +02:00
|
|
|
# joined Wednesday: schedule = Friday:5, Tuesday:2, Thursday:4
|
|
|
|
self.verify_onboarding_email_schedule(user_profile, "Wednesday", 5, 2, 4)
|
2023-03-15 20:18:09 +01:00
|
|
|
|
2023-04-24 17:46:49 +02:00
|
|
|
# joined Thursday: schedule = Monday:1, Wednesday:3, Friday:5
|
|
|
|
self.verify_onboarding_email_schedule(user_profile, "Thursday", 1, 3, 5)
|
2023-03-15 18:09:26 +01:00
|
|
|
|
2023-04-24 17:46:49 +02:00
|
|
|
# joined Friday: schedule = Tuesday:2, Thursday:4, Monday:1
|
|
|
|
self.verify_onboarding_email_schedule(user_profile, "Friday", 2, 4, 1)
|
2023-03-15 20:18:09 +01:00
|
|
|
|
2023-04-24 17:46:49 +02:00
|
|
|
# joined Saturday: schedule = Monday:1, Wednesday:3, Friday:5
|
|
|
|
self.verify_onboarding_email_schedule(user_profile, "Saturday", 1, 3, 5)
|
2023-03-15 20:18:09 +01:00
|
|
|
|
2023-04-24 17:46:49 +02:00
|
|
|
# joined Sunday: schedule = Tuesday:2, Thursday:4, Monday:1
|
|
|
|
self.verify_onboarding_email_schedule(user_profile, "Sunday", 2, 4, 1)
|
2023-03-15 20:18:09 +01:00
|
|
|
|
2023-04-24 17:43:29 +02:00
|
|
|
def test_time_offset_for_onboarding_email_schedule(self) -> None:
|
|
|
|
user_profile = self.example_user("hamlet")
|
|
|
|
days_delayed = {
|
|
|
|
"4": timedelta(days=4, hours=-1),
|
|
|
|
"6": timedelta(days=6, hours=-1),
|
2023-04-24 17:46:49 +02:00
|
|
|
"8": timedelta(days=8, hours=-1),
|
2023-04-24 17:43:29 +02:00
|
|
|
}
|
2023-03-15 18:09:26 +01:00
|
|
|
|
|
|
|
# Time offset of America/Phoenix is -07:00
|
|
|
|
user_profile.timezone = "America/Phoenix"
|
2023-03-15 20:18:09 +01:00
|
|
|
|
2023-03-15 18:09:26 +01:00
|
|
|
# Test date_joined == Friday in UTC, but Thursday in the user's time zone
|
2022-04-17 01:47:25 +02:00
|
|
|
user_profile.date_joined = datetime(2018, 1, 5, 1, 0, 0, 0, tzinfo=timezone.utc)
|
2023-03-15 18:09:26 +01:00
|
|
|
onboarding_email_schedule = get_onboarding_email_schedule(user_profile)
|
2023-03-15 20:18:09 +01:00
|
|
|
|
2023-07-18 11:50:12 +02:00
|
|
|
# onboarding_zulip_topics email sent on Monday
|
2023-03-15 18:09:26 +01:00
|
|
|
self.assertEqual(
|
2023-07-18 11:50:12 +02:00
|
|
|
onboarding_email_schedule["onboarding_zulip_topics"],
|
2023-03-15 20:18:09 +01:00
|
|
|
days_delayed["4"],
|
|
|
|
)
|
|
|
|
|
|
|
|
# onboarding_zulip_guide sent on Wednesday
|
|
|
|
self.assertEqual(
|
|
|
|
onboarding_email_schedule["onboarding_zulip_guide"],
|
|
|
|
days_delayed["6"],
|
2023-03-15 18:09:26 +01:00
|
|
|
)
|
2022-08-02 07:16:49 +02:00
|
|
|
|
2023-04-24 17:46:49 +02:00
|
|
|
# onboarding_team_to_zulip sent on Friday
|
|
|
|
self.assertEqual(
|
|
|
|
onboarding_email_schedule["onboarding_team_to_zulip"],
|
|
|
|
days_delayed["8"],
|
|
|
|
)
|
|
|
|
|
2022-08-02 07:16:49 +02:00
|
|
|
|
2023-06-30 13:27:25 +02:00
|
|
|
class TestCustomWelcomeEmailSender(ZulipTestCase):
|
|
|
|
def test_custom_welcome_email_sender(self) -> None:
|
2022-08-02 07:16:49 +02:00
|
|
|
name = "Nonreg Email"
|
|
|
|
email = self.nonreg_email("test")
|
|
|
|
with override_settings(
|
|
|
|
WELCOME_EMAIL_SENDER={
|
|
|
|
"name": name,
|
|
|
|
"email": email,
|
|
|
|
}
|
|
|
|
):
|
|
|
|
hamlet = self.example_user("hamlet")
|
|
|
|
enqueue_welcome_emails(hamlet)
|
2023-03-15 20:18:09 +01:00
|
|
|
scheduled_emails = ScheduledEmail.objects.filter(users=hamlet).order_by(
|
|
|
|
"scheduled_timestamp"
|
|
|
|
)
|
2022-08-02 07:16:49 +02:00
|
|
|
email_data = orjson.loads(scheduled_emails[0].data)
|
|
|
|
self.assertEqual(email_data["from_name"], name)
|
|
|
|
self.assertEqual(email_data["from_address"], email)
|