Files
upstream-zulip/zerver/tests/test_mattermost_importer.py
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

1644 lines
70 KiB
Python

import filecmp
import json
import os
import shutil
import tempfile
from collections import defaultdict
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any
from unittest.mock import patch
import orjson
from django.db.models import Q
from django_stubs_ext import QuerySetAny
from zerver.data_import.import_util import (
AttachmentRecordData,
SubscriberHandler,
UploadRecordData,
ZerverFieldsT,
)
from zerver.data_import.mattermost import (
COMPILED_CHANNEL_ID_FORMAT,
DEFAULT_SINGLE_TEAM_OBJECT,
backfill_user_data_from_posts,
build_reactions,
check_user_in_team,
convert_channel_data,
convert_direct_message_group_data,
convert_user_data,
create_username_to_user_mapping,
do_convert_data,
get_mentioned_user_ids,
make_realm,
mattermost_data_file_to_dict,
process_message_attachments,
process_user,
reset_mirror_dummy_users,
write_emoticon_data,
)
from zerver.data_import.sequencer import IdMapper
from zerver.data_import.user_handler import UserHandler
from zerver.lib.emoji import name_to_codepoint
from zerver.lib.import_realm import do_import_realm
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.thumbnail import THUMBNAIL_ACCEPT_IMAGE_TYPES
from zerver.models import Attachment, Message, Reaction, Recipient, UserProfile
from zerver.models.presence import PresenceSequence
from zerver.models.realms import Realm, get_realm
from zerver.models.streams import Stream, get_stream
from zerver.models.users import get_user
from zerver.tests.test_import_export import make_export_output_dir
from zerver.tests.test_microsoft_teams_importer import get_channel_subscriber_emails
from zproject.computed_settings import CROSS_REALM_BOT_EMAILS
@dataclass
class MattermostUserMaps:
username_to_email_map: dict[str, str]
exported_channel_subscriber_dict: dict[str, set[str]]
@dataclass
class ConversionResult:
mattermost_data: dict[str, Any]
imported_realm: Any
log_output: list[str]
class MattermostImportTestBase(ZulipTestCase):
def team_output_dir(self, output_dir: str, team_name: str) -> str:
return os.path.join(output_dir, team_name)
def read_file(self, team_output_dir: str, output_file: str) -> Any:
full_path = os.path.join(team_output_dir, output_file)
with open(full_path, "rb") as f:
return orjson.loads(f.read())
def assert_imported_messages_match_exported(
self,
importable_messages: Sequence[dict[str, Any]],
imported_messages: QuerySetAny[Message, Message],
username_to_email_map: dict[str, str],
) -> None:
exported_message_datetimes: list[float] = []
exported_sender_messages_map: dict[str, list[float]] = defaultdict(list)
for message in importable_messages:
message_datesent = float(int(message["create_at"] / 1000))
exported_message_datetimes.append(message_datesent)
exported_sender_messages_map[username_to_email_map[message["user"]]].append(
message_datesent
)
imported_message_datetimes: list[float] = []
imported_sender_messages_map: dict[str, list[float]] = defaultdict(list)
last_date_sent: float = float("-inf")
for imported_message in imported_messages:
message_date_sent = imported_message.date_sent.timestamp()
# Imported messages are sorted chronologically.
self.assertLessEqual(last_date_sent, message_date_sent)
last_date_sent = message_date_sent
# We have some unit tests that check the message content is converted
# correctly: test_do_convert_data, test_do_convert_data_with_direct_messages
# and test_do_convert_data_with_prefering_direct_messages_for_1_to_1_messages.
self.assertIsNotNone(imported_message.content)
self.assertIsNotNone(imported_message.rendered_content)
imported_message_datetimes.append(message_date_sent)
imported_sender_messages_map[imported_message.sender.email].append(message_date_sent)
self.assertListEqual(
sorted(imported_message_datetimes),
sorted(exported_message_datetimes),
)
# Message sender is correct.
for sender_email, users_exported_message_datetimes in exported_sender_messages_map.items():
self.assertListEqual(
sorted(users_exported_message_datetimes),
sorted(imported_sender_messages_map[sender_email]),
)
def build_user_maps(
self,
team_name: str,
mattermost_data: dict[str, Any],
) -> MattermostUserMaps:
# Bot user's team and channel data is added here.
backfill_user_data_from_posts(
1,
team_name,
mattermost_data,
create_username_to_user_mapping(mattermost_data["user"]),
)
username_to_email_map: dict[str, str] = {}
exported_channel_subscriber_dict: dict[str, set[str]] = defaultdict(set)
for user in mattermost_data["user"]:
if user["teams"] == []:
# Bots that don't send any messages won't be part of any team/channel
# and won't get converted.
continue
if user.get("is_bot"):
email = f"{user['username']}-bot@zulip.example.com"
else:
email = user["email"]
username_to_email_map[user["username"]] = email
for channel in user["teams"][0]["channels"]:
exported_channel_subscriber_dict[channel["name"]].add(email)
return MattermostUserMaps(
username_to_email_map=username_to_email_map,
exported_channel_subscriber_dict=exported_channel_subscriber_dict,
)
def run_convert_and_import(
self,
export_file_name: str,
fixture_dir: str,
team_name: str,
subdomain: str,
combine_into_one_realm: bool = False,
) -> ConversionResult:
fixture_file = self.fixture_file_name(export_file_name, fixture_dir)
mattermost_data = mattermost_data_file_to_dict(fixture_file, combine_into_one_realm)
mattermost_data_dir = self.fixture_file_name("", fixture_dir)
output_dir = make_export_output_dir()
with self.assertLogs(level="INFO") as mock_log:
do_convert_data(
mattermost_data_dir=mattermost_data_dir,
output_dir=output_dir,
masking_content=False,
combine_into_one_realm=combine_into_one_realm,
)
team_output_dir = self.team_output_dir(output_dir, team_name)
with self.assertLogs(level="INFO"):
do_import_realm(import_dir=team_output_dir, subdomain=subdomain)
imported_realm = get_realm(subdomain)
return ConversionResult(
mattermost_data=mattermost_data,
imported_realm=imported_realm,
log_output=mock_log.output,
)
def assert_user_conversion(
self,
mattermost_data: dict[str, Any],
imported_realm: Any,
expected_owner_emails: set[str],
expected_guest_emails: set[str],
expected_bot_user_emails: set[str],
expected_number_of_imported_users: int,
) -> None:
imported_user_profiles = UserProfile.objects.filter(realm=imported_realm)
self.assert_length(imported_user_profiles, expected_number_of_imported_users)
imported_users = imported_user_profiles.filter(is_bot=False, is_mirror_dummy=False)
self.assert_length(
imported_users, expected_number_of_imported_users - len(expected_bot_user_emails)
)
imported_realm_owners = imported_user_profiles.filter(
is_bot=False, role=UserProfile.ROLE_REALM_OWNER, is_mirror_dummy=False
)
self.assertSetEqual(
{owner.email for owner in imported_realm_owners},
expected_owner_emails,
)
imported_realm_guests = imported_user_profiles.filter(
is_bot=False, role=UserProfile.ROLE_GUEST, is_mirror_dummy=False
)
self.assertSetEqual(
{guest.email for guest in imported_realm_guests},
expected_guest_emails,
)
imported_bot_users = imported_user_profiles.filter(
is_bot=True, bot_type=UserProfile.DEFAULT_BOT
)
self.assertSetEqual(
{bot.email for bot in imported_bot_users},
expected_bot_user_emails,
)
def assert_channel_conversion(
self,
mattermost_data: dict[str, Any],
imported_realm: Realm,
exported_channel_subscriber_dict: dict[str, set[str]],
) -> None:
checked_channels: list[str] = []
for channel_data in mattermost_data["channel"]:
channel_name = channel_data["display_name"]
if channel_name in checked_channels:
# This can't handle realms with colliding channel names for now.
# test_convert_channel_data has some tests on channel name collision.
continue
mattermost_channel_id = channel_data["name"]
imported_channel = Stream.objects.get(realm=imported_realm, name=channel_name)
self.assertEqual(
imported_channel.description, channel_data["purpose"] or channel_data["header"]
)
self.assertEqual(imported_channel.invite_only, channel_data["type"] == "P")
expected_subscribers = set(exported_channel_subscriber_dict[mattermost_channel_id])
self.assertSetEqual(
expected_subscribers,
get_channel_subscriber_emails(imported_realm, imported_channel),
)
checked_channels.append(channel_name)
def assert_channel_messages(
self,
mattermost_data: dict[str, Any],
imported_realm: Any,
mattermost_channel_id: str,
channel_name: str,
username_to_email_map: dict[str, str],
expected_number_of_bot_messages: int,
) -> None:
imported_channel = get_stream(channel_name, imported_realm)
imported_channel_messages = (
Message.objects.filter(
Q(sender__is_bot=False) | Q(sender__bot_type=UserProfile.DEFAULT_BOT),
recipient=imported_channel.recipient,
realm=imported_realm,
)
.exclude(sender__email__in=CROSS_REALM_BOT_EMAILS)
.order_by("id")
)
importable_messages: list[dict[str, Any]] = []
# Get all exported channel messages that in theory we should be able to
# convert and import. We can't import bot users and their messages just yet.
for m in mattermost_data["post"]["channel_post"]:
if m["channel"] != mattermost_channel_id:
continue
importable_messages.append(m)
if m["replies"] is not None:
importable_messages += m["replies"]
self.assert_length(imported_channel_messages, len(importable_messages))
self.assert_imported_messages_match_exported(
importable_messages, imported_channel_messages, username_to_email_map
)
self.assert_length(
imported_channel_messages.filter(sender__is_bot=True), expected_number_of_bot_messages
)
def assert_direct_messages(
self,
mattermost_data: dict[str, Any],
imported_realm: Any,
username_to_email_map: dict[str, str],
expected_number_of_bot_messages: int,
) -> None:
imported_dms = (
Message.objects.filter(
Q(sender__is_bot=False) | Q(sender__bot_type=UserProfile.DEFAULT_BOT),
realm=imported_realm,
)
.exclude(
Q(recipient__type=Recipient.STREAM) | Q(sender__email__in=CROSS_REALM_BOT_EMAILS)
)
.order_by("id")
)
importable_dms: list[dict[str, Any]] = []
for m in mattermost_data["post"]["direct_post"]:
importable_dms.append(m)
if m["replies"] is not None:
importable_dms += m["replies"]
self.assert_length(imported_dms, len(importable_dms))
self.assert_imported_messages_match_exported(
importable_dms, imported_dms, username_to_email_map
)
self.assert_length(
imported_dms.filter(sender__is_bot=True), expected_number_of_bot_messages
)
def assert_attachments(
self,
imported_realm: Any,
expected_count: int,
) -> None:
imported_attachments = Attachment.objects.filter(realm=imported_realm)
self.assertTrue(imported_attachments.exists())
self.assert_length(imported_attachments, expected_count)
for attachment in imported_attachments:
for message in Message.objects.filter(realm=imported_realm, attachment=attachment):
self.assertTrue(message.has_attachment)
self.assertEqual(
message.has_image, attachment.content_type in THUMBNAIL_ACCEPT_IMAGE_TYPES
)
self.assertTrue(message.has_link)
assert isinstance(message.rendered_content, str)
self.assertIn(
f"/user_uploads/{attachment.path_id}",
message.rendered_content,
)
class MatterMostImporter(MattermostImportTestBase):
def test_mattermost_data_file_to_dict(self) -> None:
fixture_file_name = self.fixture_file_name("export.json", "mattermost_fixtures")
mattermost_data = mattermost_data_file_to_dict(fixture_file_name)
self.assert_length(mattermost_data, 8)
self.assertEqual(mattermost_data["version"], [1])
self.assert_length(mattermost_data["team"], 2)
self.assertEqual(mattermost_data["team"][0]["name"], "gryffindor")
self.assert_length(mattermost_data["channel"], 13)
self.assertEqual(mattermost_data["channel"][0]["name"], "gryffindor-common-room")
self.assertEqual(mattermost_data["channel"][0]["team"], "gryffindor")
self.assert_length(mattermost_data["user"], 5)
self.assertEqual(mattermost_data["user"][1]["username"], "harry")
self.assert_length(mattermost_data["user"][1]["teams"], 1)
self.assert_length(mattermost_data["post"]["channel_post"], 21)
self.assertEqual(mattermost_data["post"]["channel_post"][0]["team"], "gryffindor")
self.assertEqual(mattermost_data["post"]["channel_post"][0]["channel"], "dumbledores-army")
self.assertEqual(mattermost_data["post"]["channel_post"][0]["user"], "harry")
self.assert_length(mattermost_data["post"]["channel_post"][0]["replies"], 1)
self.assert_length(mattermost_data["emoji"], 2)
self.assertEqual(mattermost_data["emoji"][0]["name"], "peerdium")
fixture_file_name = self.fixture_file_name(
"export.json", "mattermost_fixtures/direct_channel"
)
mattermost_data = mattermost_data_file_to_dict(fixture_file_name)
self.assert_length(mattermost_data["post"]["channel_post"], 4)
self.assertEqual(mattermost_data["post"]["channel_post"][0]["team"], "gryffindor")
self.assertEqual(
mattermost_data["post"]["channel_post"][0]["channel"], "gryffindor-common-room"
)
self.assertEqual(mattermost_data["post"]["channel_post"][0]["user"], "ron")
self.assertEqual(mattermost_data["post"]["channel_post"][0]["replies"], None)
self.assert_length(mattermost_data["post"]["direct_post"], 7)
self.assertEqual(mattermost_data["post"]["direct_post"][0]["user"], "ron")
self.assertEqual(mattermost_data["post"]["direct_post"][0]["replies"], None)
self.assertEqual(mattermost_data["post"]["direct_post"][0]["message"], "hey harry")
self.assertEqual(
mattermost_data["post"]["direct_post"][0]["channel_members"], ["ron", "harry"]
)
def test_process_user(self) -> None:
user_id_mapper = IdMapper[str]()
fixture_file_name = self.fixture_file_name("export.json", "mattermost_fixtures")
mattermost_data = mattermost_data_file_to_dict(fixture_file_name)
username_to_user = create_username_to_user_mapping(mattermost_data["user"])
reset_mirror_dummy_users(username_to_user)
harry_dict = username_to_user["harry"]
harry_dict["is_mirror_dummy"] = False
realm_id = 3
team_name = "gryffindor"
user = process_user(harry_dict, realm_id, team_name, user_id_mapper)
self.assertEqual(user["avatar_source"], "J")
self.assertEqual(user["delivery_email"], "harry@zulip.com")
self.assertEqual(user["email"], "harry@zulip.com")
self.assertEqual(user["full_name"], "Harry Potter")
self.assertEqual(user["id"], 1)
self.assertEqual(user["is_active"], True)
self.assertEqual(user["role"], UserProfile.ROLE_REALM_OWNER)
self.assertEqual(user["is_mirror_dummy"], False)
self.assertEqual(user["realm"], 3)
self.assertEqual(user["short_name"], "harry")
self.assertEqual(user["timezone"], "UTC")
self.assertEqual(user["is_imported_stub"], True)
# A user with a `null` team value shouldn't be an admin.
harry_dict["teams"] = None
user = process_user(harry_dict, realm_id, team_name, user_id_mapper)
self.assertEqual(user["role"], UserProfile.ROLE_MEMBER)
team_name = "slytherin"
snape_dict = username_to_user["snape"]
snape_dict["is_mirror_dummy"] = True
user = process_user(snape_dict, realm_id, team_name, user_id_mapper)
self.assertEqual(user["avatar_source"], "J")
self.assertEqual(user["delivery_email"], "snape@zulip.com")
self.assertEqual(user["email"], "snape@zulip.com")
self.assertEqual(user["full_name"], "Severus Snape")
self.assertEqual(user["id"], 2)
self.assertEqual(user["is_active"], False)
self.assertEqual(user["role"], UserProfile.ROLE_MEMBER)
self.assertEqual(user["is_mirror_dummy"], True)
self.assertEqual(user["realm"], 3)
self.assertEqual(user["short_name"], "snape")
self.assertEqual(user["timezone"], "UTC")
self.assertEqual(user["is_imported_stub"], True)
def test_process_guest_user(self) -> None:
user_id_mapper = IdMapper[str]()
fixture_file_name = self.fixture_file_name("guestExport.json", "mattermost_fixtures")
mattermost_data = mattermost_data_file_to_dict(fixture_file_name)
username_to_user = create_username_to_user_mapping(mattermost_data["user"])
reset_mirror_dummy_users(username_to_user)
sirius_dict = username_to_user["sirius"]
sirius_dict["is_mirror_dummy"] = False
realm_id = 3
team_name = "slytherin"
user = process_user(sirius_dict, realm_id, team_name, user_id_mapper)
self.assertEqual(user["avatar_source"], "J")
self.assertEqual(user["delivery_email"], "sirius@zulip.com")
self.assertEqual(user["email"], "sirius@zulip.com")
self.assertEqual(user["full_name"], "Sirius Black")
self.assertEqual(user["role"], UserProfile.ROLE_GUEST)
self.assertEqual(user["is_mirror_dummy"], False)
self.assertEqual(user["realm"], 3)
self.assertEqual(user["short_name"], "sirius")
self.assertEqual(user["timezone"], "UTC")
self.assertEqual(user["is_imported_stub"], True)
# A guest user with a `null` team value should be a regular
# user. (It's a bit of a mystery why the Mattermost export
# tool generates such `teams` lists).
sirius_dict["teams"] = None
user = process_user(sirius_dict, realm_id, team_name, user_id_mapper)
self.assertEqual(user["role"], UserProfile.ROLE_MEMBER)
def test_convert_user_data(self) -> None:
user_id_mapper = IdMapper[str]()
realm_id = 3
fixture_file_name = self.fixture_file_name("export.json", "mattermost_fixtures")
mattermost_data = mattermost_data_file_to_dict(fixture_file_name)
username_to_user = create_username_to_user_mapping(mattermost_data["user"])
reset_mirror_dummy_users(username_to_user)
team_name = "gryffindor"
user_handler = UserHandler()
realm = make_realm(realm_id=0, team={"name": team_name})
convert_user_data(
user_handler, user_id_mapper, username_to_user, realm, realm_id, team_name
)
self.assert_length(user_handler.get_all_users(), 2)
self.assertTrue(user_id_mapper.has("harry"))
self.assertTrue(user_id_mapper.has("ron"))
self.assertEqual(
user_handler.get_user(user_id_mapper.get("harry"))["full_name"], "Harry Potter"
)
self.assertEqual(
user_handler.get_user(user_id_mapper.get("ron"))["full_name"], "Ron Weasley"
)
team_name = "slytherin"
user_handler = UserHandler()
realm = make_realm(realm_id=0, team={"name": team_name})
convert_user_data(
user_handler, user_id_mapper, username_to_user, realm, realm_id, team_name
)
self.assert_length(user_handler.get_all_users(), 3)
self.assertTrue(user_id_mapper.has("malfoy"))
self.assertTrue(user_id_mapper.has("pansy"))
self.assertTrue(user_id_mapper.has("snape"))
team_name = "gryffindor"
# Snape is a mirror dummy user in Harry's team.
backfill_user_data_from_posts(2, team_name, mattermost_data, username_to_user)
user_handler = UserHandler()
realm = make_realm(realm_id=0, team={"name": team_name})
convert_user_data(
user_handler, user_id_mapper, username_to_user, realm, realm_id, team_name
)
self.assert_length(user_handler.get_all_users(), 3)
self.assertTrue(user_id_mapper.has("snape"))
team_name = "slytherin"
user_handler = UserHandler()
realm = make_realm(realm_id=0, team={"name": team_name})
convert_user_data(
user_handler, user_id_mapper, username_to_user, realm, realm_id, team_name
)
self.assert_length(user_handler.get_all_users(), 3)
# Warn if the converted realm will have no realm owner.
team_name = "gryffindor"
user_map_with_no_realm_owner = {
k: v
for k, v in username_to_user.items()
if any("team_admin" not in team["roles"] for team in v["teams"])
}
with self.assertLogs(level="INFO") as info_log:
convert_user_data(
user_handler,
user_id_mapper,
user_map_with_no_realm_owner,
realm,
realm_id,
team_name,
)
self.assertEqual(
info_log.output,
["WARNING:root:Converted realm has no owners!"],
)
# Importer should raise error when user emails are malformed
team_name = "gryffindor"
bad_email1 = username_to_user["harry"]["email"] = "harry.ceramicist@zuL1[p.c0m"
bad_email2 = username_to_user["ron"]["email"] = "ron.ferret@zulup...com"
realm = make_realm(realm_id=0, team={"name": team_name})
with self.assertRaises(Exception) as e:
convert_user_data(
user_handler, user_id_mapper, username_to_user, realm, realm_id, team_name
)
error_message = str(e.exception)
expected_error_message = f"['Invalid email format, please fix the following email(s) and try again: {bad_email2}, {bad_email1}']"
self.assertEqual(error_message, expected_error_message)
def test_convert_channel_data(self) -> None:
fixture_file_name = self.fixture_file_name("export.json", "mattermost_fixtures")
mattermost_data = mattermost_data_file_to_dict(fixture_file_name)
username_to_user = create_username_to_user_mapping(mattermost_data["user"])
reset_mirror_dummy_users(username_to_user)
user_handler = UserHandler()
subscriber_handler = SubscriberHandler[frozenset[str]]()
stream_id_mapper = IdMapper[str]()
user_id_mapper = IdMapper[str]()
team_name = "gryffindor"
mock_realm_dict: ZerverFieldsT = make_realm(realm_id=0, team={"name": team_name})
zerver_realm = mock_realm_dict["zerver_realm"]
convert_user_data(
user_handler=user_handler,
user_id_mapper=user_id_mapper,
user_data_map=username_to_user,
realm=mock_realm_dict,
realm_id=3,
team_name=team_name,
)
with patch(
"zerver.data_import.mattermost.MATTERMOST_DEFAULT_ANNOUNCEMENTS_CHANNEL_NAME",
"Gryffindor common room",
):
convert_channel_data(
realm=mock_realm_dict,
channel_data=mattermost_data["channel"],
user_data_map=username_to_user,
subscriber_handler=subscriber_handler,
stream_id_mapper=stream_id_mapper,
user_id_mapper=user_id_mapper,
realm_id=3,
team_name=team_name,
)
zerver_stream = mock_realm_dict["zerver_stream"]
self.assert_length(zerver_stream, 11)
self.assertEqual(zerver_stream[0]["name"], "Gryffindor common room")
self.assertEqual(zerver_stream[0]["invite_only"], False)
self.assertEqual(
zerver_stream[0]["description"], "A place for talking about Gryffindor common room"
)
self.assertEqual(zerver_stream[0]["rendered_description"], "")
self.assertEqual(zerver_stream[0]["realm"], 3)
self.assertEqual(
zerver_realm[0]["zulip_update_announcements_stream"], zerver_stream[0]["id"]
)
self.assertEqual(zerver_realm[0]["new_stream_announcements_stream"], zerver_stream[0]["id"])
self.assertEqual(zerver_stream[1]["name"], "Gryffindor quidditch team")
self.assertEqual(zerver_stream[1]["invite_only"], False)
self.assertEqual(
zerver_stream[1]["description"], "A place for talking about Gryffindor quidditch team"
)
self.assertEqual(zerver_stream[1]["rendered_description"], "")
self.assertEqual(zerver_stream[1]["realm"], 3)
self.assertEqual(zerver_stream[2]["name"], "Dumbledores army")
self.assertEqual(zerver_stream[2]["invite_only"], True)
self.assertEqual(
zerver_stream[2]["description"], "A place for talking about Dumbledores army"
)
self.assertEqual(zerver_stream[2]["rendered_description"], "")
self.assertEqual(zerver_stream[2]["realm"], 3)
# Long channel name is truncated
self.assertEqual(
zerver_stream[3]["name"],
"Super long channel name, it's more than 60 characters, whic…", # codespell:ignore whic
)
self.assertGreaterEqual(Stream.MAX_NAME_LENGTH, len(zerver_stream[3]["name"]))
self.assertEqual(
zerver_stream[4]["name"], "Super long channel name, it's more than 60 characters, … (2)"
)
self.assertGreaterEqual(Stream.MAX_NAME_LENGTH, len(zerver_stream[4]["name"]))
# Identical truncated channel names doesn't collide
self.assertEqual(
zerver_stream[5]["name"], "Super long channel name, it's more than 60 characters, … (3)"
)
self.assertGreaterEqual(Stream.MAX_NAME_LENGTH, len(zerver_stream[5]["name"]))
# Channel names which differ only in capitalization need collision resolution,
# because Zulip treats them case-insensitively.
self.assertEqual(zerver_stream[7]["name"], "Slug club")
self.assertEqual(zerver_stream[8]["name"], "SLUG CLUB (2)")
# "Slug club (3)" is imported as an original channel name from Mattermost.
# Collision resolution for the "slug club" channel then generates the same name,
# which therefore needs another round of collision resolution itself, resulting
# in a doubly suffixed name.
self.assertEqual(zerver_stream[9]["name"], "Slug club (3)")
self.assertEqual(zerver_stream[10]["name"], "slug club (3) (2)")
self.assertTrue(stream_id_mapper.has("gryffindor-common-room"))
self.assertTrue(stream_id_mapper.has("gryffindor-quidditch-team"))
self.assertTrue(stream_id_mapper.has("dumbledores-army"))
# TODO: Add ginny
ron_id = user_id_mapper.get("ron")
harry_id = user_id_mapper.get("harry")
self.assertEqual({ron_id, harry_id}, {1, 2})
self.assertEqual(
subscriber_handler.get_users(stream_id=stream_id_mapper.get("gryffindor-common-room")),
{ron_id, harry_id},
)
self.assertEqual(
subscriber_handler.get_users(
stream_id=stream_id_mapper.get("gryffindor-quidditch-team")
),
{ron_id, harry_id},
)
self.assertEqual(
subscriber_handler.get_users(stream_id=stream_id_mapper.get("dumbledores-army")),
{ron_id, harry_id},
)
# Converting channel data when a user's `teams` value is `null`.
username_to_user["ron"].update(teams=None)
mock_realm_dict = make_realm(realm_id=0, team={"name": "test-realm"})
zerver_stream = convert_channel_data(
realm=mock_realm_dict,
channel_data=mattermost_data["channel"],
user_data_map=username_to_user,
subscriber_handler=subscriber_handler,
stream_id_mapper=stream_id_mapper,
user_id_mapper=user_id_mapper,
realm_id=3,
team_name=team_name,
)
harry_id = user_id_mapper.get("harry")
self.assertIn(harry_id, {1, 2})
self.assertEqual(
subscriber_handler.get_users(stream_id=stream_id_mapper.get("gryffindor-common-room")),
{harry_id},
)
self.assertEqual(
subscriber_handler.get_users(
stream_id=stream_id_mapper.get("gryffindor-quidditch-team")
),
{harry_id},
)
self.assertEqual(
subscriber_handler.get_users(stream_id=stream_id_mapper.get("dumbledores-army")),
{harry_id},
)
team_name = "slytherin"
mock_realm_dict = make_realm(realm_id=0, team={"name": "test-realm"})
zerver_stream = convert_channel_data(
realm=mock_realm_dict,
channel_data=mattermost_data["channel"],
user_data_map=username_to_user,
subscriber_handler=subscriber_handler,
stream_id_mapper=stream_id_mapper,
user_id_mapper=user_id_mapper,
realm_id=4,
team_name=team_name,
)
malfoy_id = user_id_mapper.get("malfoy")
pansy_id = user_id_mapper.get("pansy")
snape_id = user_id_mapper.get("snape")
self.assertEqual({malfoy_id, pansy_id, snape_id}, {3, 4, 5})
self.assertEqual(
subscriber_handler.get_users(stream_id=stream_id_mapper.get("slytherin-common-room")),
{malfoy_id, pansy_id, snape_id},
)
self.assertEqual(
subscriber_handler.get_users(
stream_id=stream_id_mapper.get("slytherin-quidditch-team")
),
{malfoy_id, pansy_id},
)
def test_convert_direct_message_group_data(self) -> None:
fixture_file_name = self.fixture_file_name(
"export.json", "mattermost_fixtures/direct_channel"
)
mattermost_data = mattermost_data_file_to_dict(fixture_file_name)
username_to_user = create_username_to_user_mapping(mattermost_data["user"])
reset_mirror_dummy_users(username_to_user)
user_handler = UserHandler()
subscriber_handler = SubscriberHandler[frozenset[str]]()
direct_message_group_id_mapper = IdMapper[frozenset[str]]()
user_id_mapper = IdMapper[str]()
team_name = "gryffindor"
realm = make_realm(realm_id=0, team={"name": team_name})
convert_user_data(
user_handler=user_handler,
user_id_mapper=user_id_mapper,
user_data_map=username_to_user,
realm=realm,
realm_id=3,
team_name=team_name,
)
with self.assertLogs(level="INFO") as mock_log:
zerver_huddle = convert_direct_message_group_data(
direct_message_group_data=mattermost_data["direct_channel"],
user_data_map=username_to_user,
subscriber_handler=subscriber_handler,
direct_message_group_id_mapper=direct_message_group_id_mapper,
user_id_mapper=user_id_mapper,
realm=realm,
realm_id=3,
team_name=team_name,
)
self.assert_length(zerver_huddle, 3)
expected_dm_groups = [
(0, {2, 3}), # direct_channel[0] should have users 2, 3
(1, {1, 2, 3}), # direct_channel[1] should have users 1, 2, 3
(3, {3}), # direct_channel[3] should have users 3
]
for channel_index, expected_users in expected_dm_groups:
direct_message_group_members = frozenset(
[
username
for username in mattermost_data["direct_channel"][channel_index]["members"]
if user_id_mapper.has(username)
]
)
self.assertTrue(direct_message_group_id_mapper.has(direct_message_group_members))
actual_users = subscriber_handler.get_users(
direct_message_group_id=direct_message_group_id_mapper.get(
direct_message_group_members
)
)
self.assertEqual(actual_users, expected_users)
self.assertEqual(
mock_log.output,
["INFO:root:Duplicate direct message group found in the export data. Skipping."],
)
def test_write_emoticon_data(self) -> None:
fixture_file_name = self.fixture_file_name("export.json", "mattermost_fixtures")
mattermost_data = mattermost_data_file_to_dict(fixture_file_name)
output_dir = self.make_import_output_dir("mattermost")
with self.assertLogs(level="INFO"):
zerver_realm_emoji = write_emoticon_data(
realm_id=3,
custom_emoji_data=mattermost_data["emoji"],
data_dir=self.fixture_file_name("", "mattermost_fixtures"),
output_dir=output_dir,
)
self.assert_length(zerver_realm_emoji, 2)
self.assertEqual(zerver_realm_emoji[0]["file_name"], "peerdium")
self.assertEqual(zerver_realm_emoji[0]["realm"], 3)
self.assertEqual(zerver_realm_emoji[0]["deactivated"], False)
self.assertEqual(zerver_realm_emoji[1]["file_name"], "tick")
self.assertEqual(zerver_realm_emoji[1]["realm"], 3)
self.assertEqual(zerver_realm_emoji[1]["deactivated"], False)
records_file = os.path.join(output_dir, "emoji", "records.json")
with open(records_file, "rb") as f:
records_json = orjson.loads(f.read())
self.assertEqual(records_json[0]["file_name"], "peerdium")
self.assertEqual(records_json[0]["realm_id"], 3)
exported_emoji_path = self.fixture_file_name(
mattermost_data["emoji"][0]["image"], "mattermost_fixtures"
)
emoji_dir = os.path.join(output_dir, "emoji")
self.assertTrue(
filecmp.cmp(os.path.join(emoji_dir, records_json[0]["path"]), exported_emoji_path)
)
self.assertEqual(records_json[1]["file_name"], "tick")
self.assertEqual(records_json[1]["realm_id"], 3)
exported_emoji_path = self.fixture_file_name(
mattermost_data["emoji"][1]["image"], "mattermost_fixtures"
)
self.assertTrue(
filecmp.cmp(os.path.join(emoji_dir, records_json[1]["path"]), exported_emoji_path)
)
def test_process_message_attachments(self) -> None:
mattermost_data_dir = self.fixture_file_name("", "mattermost_fixtures/direct_channel")
output_dir = self.make_import_output_dir("mattermost")
fixture_file_name = self.fixture_file_name(
"export.json", "mattermost_fixtures/direct_channel"
)
mattermost_data = mattermost_data_file_to_dict(fixture_file_name)
username_to_user = create_username_to_user_mapping(mattermost_data["user"])
reset_mirror_dummy_users(username_to_user)
user_handler = UserHandler()
user_id_mapper = IdMapper[str]()
team_name = "gryffindor"
realm = make_realm(realm_id=0, team={"name": team_name})
convert_user_data(
user_handler=user_handler,
user_id_mapper=user_id_mapper,
user_data_map=username_to_user,
realm=realm,
realm_id=3,
team_name=team_name,
)
zerver_attachments: list[AttachmentRecordData] = []
uploads_list: list[UploadRecordData] = []
process_message_attachments(
attachments=mattermost_data["post"]["direct_post"][0]["attachments"],
realm_id=3,
message_id=1,
user_id=2,
zerver_attachment=zerver_attachments,
uploads_list=uploads_list,
mattermost_data_dir=mattermost_data_dir,
output_dir=output_dir,
)
self.assert_length(zerver_attachments, 1)
self.assertEqual(zerver_attachments[0].file_name, "harry-ron.jpg")
self.assertEqual(zerver_attachments[0].owner, 2)
self.assertEqual(
user_handler.get_user(zerver_attachments[0].owner)["email"], "ron@zulip.com"
)
# TODO: Assert this for False after fixing the file permissions in direct messages
self.assertTrue(zerver_attachments[0].is_realm_public)
self.assert_length(uploads_list, 1)
self.assertEqual(uploads_list[0].user_profile_id, 2)
attachment_path = self.fixture_file_name(
mattermost_data["post"]["direct_post"][0]["attachments"][0]["path"],
"mattermost_fixtures/direct_channel/data",
)
attachment_out_path = os.path.join(output_dir, "uploads", zerver_attachments[0].path_id)
self.assertTrue(os.path.exists(attachment_out_path))
self.assertTrue(filecmp.cmp(attachment_path, attachment_out_path))
def test_get_mentioned_user_ids(self) -> None:
user_id_mapper = IdMapper[str]()
harry_id = user_id_mapper.get("harry")
raw_message = {
"content": "Hello @harry",
}
ids = get_mentioned_user_ids(raw_message, user_id_mapper)
self.assertEqual(list(ids), [harry_id])
raw_message = {
"content": "Hello",
}
ids = get_mentioned_user_ids(raw_message, user_id_mapper)
self.assertEqual(list(ids), [])
raw_message = {
"content": "@harry How are you?",
}
ids = get_mentioned_user_ids(raw_message, user_id_mapper)
self.assertEqual(list(ids), [harry_id])
raw_message = {
"content": "@harry @ron Where are you folks?",
}
ron_id = user_id_mapper.get("ron")
ids = get_mentioned_user_ids(raw_message, user_id_mapper)
self.assertEqual(list(ids), [harry_id, ron_id])
raw_message = {
"content": "@harry.com How are you?",
}
ids = get_mentioned_user_ids(raw_message, user_id_mapper)
self.assertEqual(list(ids), [])
raw_message = {
"content": "hello@harry.com How are you?",
}
ids = get_mentioned_user_ids(raw_message, user_id_mapper)
self.assertEqual(list(ids), [])
harry_id = user_id_mapper.get("harry_")
raw_message = {
"content": "Hello @harry_",
}
ids = get_mentioned_user_ids(raw_message, user_id_mapper)
self.assertEqual(list(ids), [harry_id])
harry_id = user_id_mapper.get("harry.")
raw_message = {
"content": "Hello @harry.",
}
ids = get_mentioned_user_ids(raw_message, user_id_mapper)
self.assertEqual(list(ids), [harry_id])
harry_id = user_id_mapper.get("ha_rry.")
raw_message = {
"content": "Hello @ha_rry.",
}
ids = get_mentioned_user_ids(raw_message, user_id_mapper)
self.assertEqual(list(ids), [harry_id])
ron_id = user_id_mapper.get("ron")
raw_message = {
"content": "Hello @ron.",
}
ids = get_mentioned_user_ids(raw_message, user_id_mapper)
self.assertEqual(list(ids), [])
raw_message = {
"content": "Hello @ron_",
}
ids = get_mentioned_user_ids(raw_message, user_id_mapper)
self.assertEqual(list(ids), [])
def test_check_user_in_team(self) -> None:
fixture_file_name = self.fixture_file_name("export.json", "mattermost_fixtures")
mattermost_data = mattermost_data_file_to_dict(fixture_file_name)
username_to_user = create_username_to_user_mapping(mattermost_data["user"])
reset_mirror_dummy_users(username_to_user)
harry = username_to_user["harry"]
self.assertTrue(check_user_in_team(harry, "gryffindor"))
self.assertFalse(check_user_in_team(harry, "slytherin"))
snape = username_to_user["snape"]
self.assertFalse(check_user_in_team(snape, "gryffindor"))
self.assertTrue(check_user_in_team(snape, "slytherin"))
snape.update(teams=None)
self.assertFalse(check_user_in_team(snape, "slytherin"))
def test_backfill_user_data_from_posts(self) -> None:
fixture_file_name = self.fixture_file_name("export.json", "mattermost_fixtures")
mattermost_data = mattermost_data_file_to_dict(fixture_file_name)
username_to_user = create_username_to_user_mapping(mattermost_data["user"])
reset_mirror_dummy_users(username_to_user)
backfill_user_data_from_posts(
num_teams=2,
team_name="gryffindor",
mattermost_data=mattermost_data,
username_to_user=username_to_user,
)
self.assertFalse(username_to_user["harry"]["is_mirror_dummy"])
self.assertFalse(username_to_user["ron"]["is_mirror_dummy"])
self.assertFalse(username_to_user["malfoy"]["is_mirror_dummy"])
# snape is mirror dummy since the user sent a message in gryffindor and
# left the team
self.assertTrue(username_to_user["snape"]["is_mirror_dummy"])
def test_build_reactions(self) -> None:
fixture_file_name = self.fixture_file_name("export.json", "mattermost_fixtures")
mattermost_data = mattermost_data_file_to_dict(fixture_file_name)
total_reactions: list[dict[str, Any]] = []
reactions = [
{"user": "harry", "create_at": 1553165521410, "emoji_name": "tick"},
{"user": "ron", "create_at": 1553166530805, "emoji_name": "smile"},
{"user": "ron", "create_at": 1553166540953, "emoji_name": "world_map"},
{"user": "harry", "create_at": 1553166540957, "emoji_name": "world_map"},
]
with self.assertLogs(level="INFO"):
zerver_realmemoji = write_emoticon_data(
realm_id=3,
custom_emoji_data=mattermost_data["emoji"],
data_dir=self.fixture_file_name("", "mattermost_fixtures"),
output_dir=self.make_import_output_dir("mattermost"),
)
# Make sure tick is present in fixture data
self.assertEqual(zerver_realmemoji[1]["name"], "tick")
tick_emoji_code = zerver_realmemoji[1]["id"]
user_id_mapper = IdMapper[str]()
harry_id = user_id_mapper.get("harry")
ron_id = user_id_mapper.get("ron")
build_reactions(
realm_id=3,
total_reactions=total_reactions,
reactions=reactions,
message_id=5,
user_id_mapper=user_id_mapper,
zerver_realmemoji=zerver_realmemoji,
)
smile_emoji_code = name_to_codepoint["smile"]
world_map_emoji_code = name_to_codepoint["world_map"]
self.assert_length(total_reactions, 4)
self.assertEqual(
self.get_set(total_reactions, "reaction_type"),
{Reaction.REALM_EMOJI, Reaction.UNICODE_EMOJI},
)
self.assertEqual(
self.get_set(total_reactions, "emoji_name"), {"tick", "smile", "world_map"}
)
self.assertEqual(
self.get_set(total_reactions, "emoji_code"),
{tick_emoji_code, smile_emoji_code, world_map_emoji_code},
)
self.assertEqual(self.get_set(total_reactions, "user_profile"), {harry_id, ron_id})
self.assert_length(self.get_set(total_reactions, "id"), 4)
self.assert_length(self.get_set(total_reactions, "message"), 1)
def test_do_convert_data(self) -> None:
mattermost_data_dir = self.fixture_file_name("", "mattermost_fixtures")
output_dir = self.make_import_output_dir("mattermost")
with self.assertLogs(level="WARNING") as warn_log:
do_convert_data(
mattermost_data_dir=mattermost_data_dir,
output_dir=output_dir,
masking_content=False,
)
self.assertEqual(
warn_log.output,
[
"WARNING:root:Skipping importing direct message groups and DMs since there are multiple teams in the export",
"WARNING:root:Skipping importing direct message groups and DMs since there are multiple teams in the export",
],
)
harry_team_output_dir = self.team_output_dir(output_dir, "gryffindor")
self.assertEqual(os.path.exists(os.path.join(harry_team_output_dir, "avatars")), True)
self.assertEqual(os.path.exists(os.path.join(harry_team_output_dir, "emoji")), True)
self.assertEqual(
os.path.exists(os.path.join(harry_team_output_dir, "attachment.json")), True
)
self.assertEqual(
os.path.exists(os.path.join(harry_team_output_dir, "migration_status.json")), True
)
realm = self.read_file(harry_team_output_dir, "realm.json")
self.assertEqual(
"Organization imported from Mattermost!", realm["zerver_realm"][0]["description"]
)
exported_user_ids = self.get_set(realm["zerver_userprofile"], "id")
exported_user_full_names = self.get_set(realm["zerver_userprofile"], "full_name")
self.assertEqual({"Harry Potter", "Ron Weasley", "Severus Snape"}, exported_user_full_names)
exported_user_emails = self.get_set(realm["zerver_userprofile"], "email")
self.assertEqual(
{"harry@zulip.com", "ron@zulip.com", "snape@zulip.com"}, exported_user_emails
)
self.assert_length(realm["zerver_stream"], 11)
exported_stream_names = self.get_set(realm["zerver_stream"], "name")
self.assertSetEqual(
exported_stream_names,
{
"Gryffindor common room",
"Gryffindor quidditch team",
"Dumbledores army",
"Super long channel name, it's more than 60 characters, whic…", # codespell:ignore whic
"Super long channel name, it's more than 60 characters, … (2)",
"Super long channel name, it's more than 60 characters, … (3)",
"Gryffindor quidditch team (2)",
"Slug club",
"SLUG CLUB (2)",
"Slug club (3)",
"slug club (3) (2)",
},
)
self.assertEqual(
self.get_set(realm["zerver_stream"], "realm"), {realm["zerver_realm"][0]["id"]}
)
self.assertEqual(self.get_set(realm["zerver_stream"], "deactivated"), {False})
self.assert_length(realm["zerver_defaultstream"], 0)
exported_recipient_ids = self.get_set(realm["zerver_recipient"], "id")
self.assert_length(exported_recipient_ids, 11)
exported_recipient_types = self.get_set(realm["zerver_recipient"], "type")
self.assertEqual(exported_recipient_types, {Recipient.STREAM})
exported_recipient_type_ids = self.get_set(realm["zerver_recipient"], "type_id")
self.assert_length(exported_recipient_type_ids, 11)
exported_subscription_userprofile = self.get_set(
realm["zerver_subscription"], "user_profile"
)
self.assert_length(exported_subscription_userprofile, 2)
exported_subscription_recipients = self.get_set(realm["zerver_subscription"], "recipient")
self.assert_length(exported_subscription_recipients, 3)
messages = self.read_file(harry_team_output_dir, "messages-000001.json")
exported_messages_id = self.get_set(messages["zerver_message"], "id")
self.assertIn(messages["zerver_message"][0]["sender"], exported_user_ids)
self.assertIn(messages["zerver_message"][0]["recipient"], exported_recipient_ids)
self.assertIn(messages["zerver_message"][0]["content"], "harry joined the channel.\n\n")
exported_usermessage_userprofiles = self.get_set(
messages["zerver_usermessage"], "user_profile"
)
self.assert_length(exported_usermessage_userprofiles, 3)
exported_usermessage_messages = self.get_set(messages["zerver_usermessage"], "message")
self.assertEqual(exported_usermessage_messages, exported_messages_id)
with self.assertLogs(level="INFO"):
do_import_realm(
import_dir=harry_team_output_dir,
subdomain="gryffindor",
)
realm = get_realm("gryffindor")
presence_sequence = PresenceSequence.objects.get(realm=realm)
self.assertEqual(presence_sequence.last_update_id, 0)
self.assertFalse(get_user("harry@zulip.com", realm).is_mirror_dummy)
self.assertFalse(get_user("ron@zulip.com", realm).is_mirror_dummy)
self.assertTrue(get_user("snape@zulip.com", realm).is_mirror_dummy)
messages = Message.objects.filter(realm=realm)
for message in messages:
self.assertIsNotNone(message.rendered_content)
self.verify_emoji_code_foreign_keys()
def test_do_convert_data_with_prefering_direct_messages_for_1_to_1_messages(self) -> None:
mattermost_data_dir = self.fixture_file_name("direct_channel", "mattermost_fixtures")
output_dir = self.make_import_output_dir("mattermost")
with self.assertLogs(level="INFO"):
do_convert_data(
mattermost_data_dir=mattermost_data_dir,
output_dir=output_dir,
masking_content=False,
)
harry_team_output_dir = self.team_output_dir(output_dir, "gryffindor")
realm = self.read_file(harry_team_output_dir, "realm.json")
self.assertEqual(
"Organization imported from Mattermost!", realm["zerver_realm"][0]["description"]
)
exported_user_ids = self.get_set(realm["zerver_userprofile"], "id")
exported_user_full_names = self.get_set(realm["zerver_userprofile"], "full_name")
self.assertEqual(
{"Harry Potter", "Ron Weasley", "Ginny Weasley", "Tom Riddle"}, exported_user_full_names
)
exported_user_emails = self.get_set(realm["zerver_userprofile"], "email")
self.assertEqual(
{"harry@zulip.com", "ron@zulip.com", "ginny@zulip.com", "voldemort@zulip.com"},
exported_user_emails,
)
self.assert_length(realm["zerver_stream"], 3)
exported_stream_names = self.get_set(realm["zerver_stream"], "name")
self.assertEqual(
exported_stream_names,
{"Gryffindor common room", "Gryffindor quidditch team", "Dumbledores army"},
)
self.assertEqual(
self.get_set(realm["zerver_stream"], "realm"), {realm["zerver_realm"][0]["id"]}
)
self.assertEqual(self.get_set(realm["zerver_stream"], "deactivated"), {False})
self.assert_length(realm["zerver_defaultstream"], 0)
exported_recipient_ids = self.get_set(realm["zerver_recipient"], "id")
self.assert_length(exported_recipient_ids, 6)
exported_recipient_types = self.get_set(realm["zerver_recipient"], "type")
self.assertEqual(
exported_recipient_types,
{Recipient.STREAM, Recipient.DIRECT_MESSAGE_GROUP},
)
exported_recipient_type_ids = self.get_set(realm["zerver_recipient"], "type_id")
self.assert_length(exported_recipient_type_ids, 3)
exported_subscription_userprofile = self.get_set(
realm["zerver_subscription"], "user_profile"
)
self.assert_length(exported_subscription_userprofile, 4)
exported_subscription_recipients = self.get_set(realm["zerver_subscription"], "recipient")
self.assert_length(exported_subscription_recipients, 6)
messages = self.read_file(harry_team_output_dir, "messages-000001.json")
exported_messages_id = self.get_set(messages["zerver_message"], "id")
self.assertIn(messages["zerver_message"][0]["sender"], exported_user_ids)
self.assertIn(messages["zerver_message"][0]["recipient"], exported_recipient_ids)
self.assertIn(messages["zerver_message"][0]["content"], "ron joined the channel.\n\n")
exported_usermessage_userprofiles = self.get_set(
messages["zerver_usermessage"], "user_profile"
)
self.assert_length(exported_usermessage_userprofiles, 3)
exported_usermessage_messages = self.get_set(messages["zerver_usermessage"], "message")
self.assertEqual(exported_usermessage_messages, exported_messages_id)
with self.assertLogs(level="INFO"):
do_import_realm(
import_dir=harry_team_output_dir,
subdomain="gryffindor",
)
realm = get_realm("gryffindor")
messages = Message.objects.filter(realm=realm)
for message in messages:
self.assertIsNotNone(message.rendered_content)
self.assert_length(messages, 24)
stream_messages = messages.filter(recipient__type=Recipient.STREAM).order_by("date_sent")
stream_recipients = stream_messages.values_list("recipient", flat=True)
self.assert_length(stream_messages, 13)
self.assert_length(set(stream_recipients), 2)
group_direct_messages = messages.filter(
recipient__type=Recipient.DIRECT_MESSAGE_GROUP
).order_by("date_sent")
self.assert_length(group_direct_messages, 11)
direct_message_group_recipients = group_direct_messages.values_list("recipient", flat=True)
self.assert_length(set(direct_message_group_recipients), 7)
self.assertEqual(group_direct_messages[0].sender.email, "ron@zulip.com")
self.assertRegex(
group_direct_messages[0].content, "hey harry\n\n\\[harry-ron.jpg\\]\\(.*\\)"
)
self.assertTrue(group_direct_messages[0].has_attachment)
self.assertTrue(group_direct_messages[0].has_image)
self.assertTrue(group_direct_messages[0].has_link)
self.assertEqual(group_direct_messages[0].topic_name(), Message.DM_TOPIC)
self.assertEqual(group_direct_messages[1].sender.email, "ginny@zulip.com")
self.assertEqual(
group_direct_messages[1].content, "Who is going to Hogsmeade this weekend?"
)
self.assertEqual(group_direct_messages[1].topic_name(), Message.DM_TOPIC)
def test_do_convert_data_with_masking(self) -> None:
mattermost_data_dir = self.fixture_file_name("", "mattermost_fixtures")
output_dir = self.make_import_output_dir("mattermost")
with self.assertLogs(level="WARNING") as warn_log:
do_convert_data(
mattermost_data_dir=mattermost_data_dir,
output_dir=output_dir,
masking_content=True,
)
self.assertEqual(
warn_log.output,
[
"WARNING:root:Skipping importing direct message groups and DMs since there are multiple teams in the export",
"WARNING:root:Skipping importing direct message groups and DMs since there are multiple teams in the export",
],
)
harry_team_output_dir = self.team_output_dir(output_dir, "gryffindor")
messages = self.read_file(harry_team_output_dir, "messages-000001.json")
self.assertIn(messages["zerver_message"][0]["content"], "xxxxx xxxxxx xxx xxxxxxx.\n\n")
def test_import_data_to_existing_database(self) -> None:
mattermost_data_dir = self.fixture_file_name("", "mattermost_fixtures")
output_dir = self.make_import_output_dir("mattermost")
with self.assertLogs(level="WARNING") as warn_log:
do_convert_data(
mattermost_data_dir=mattermost_data_dir,
output_dir=output_dir,
masking_content=True,
)
self.assertEqual(
warn_log.output,
[
"WARNING:root:Skipping importing direct message groups and DMs since there are multiple teams in the export",
"WARNING:root:Skipping importing direct message groups and DMs since there are multiple teams in the export",
],
)
harry_team_output_dir = self.team_output_dir(output_dir, "gryffindor")
with self.assertLogs(level="INFO"):
do_import_realm(
import_dir=harry_team_output_dir,
subdomain="gryffindor",
)
realm = get_realm("gryffindor")
messages = Message.objects.filter(realm=realm)
for message in messages:
self.assertIsNotNone(message.rendered_content)
self.verify_emoji_code_foreign_keys()
def test_import_unknown_jsonl_file(self) -> None:
export_dir = tempfile.mkdtemp()
output_dir = self.make_import_output_dir("mattermost")
try:
with self.assertRaises(AssertionError) as e:
do_convert_data(
mattermost_data_dir=export_dir,
output_dir=output_dir,
masking_content=True,
)
self.assertEqual(
f"Missing import.jsonl or export.json file in {export_dir}. Files: []",
str(e.exception),
)
finally:
shutil.rmtree(export_dir)
class MattermostV1110ImportTest(MattermostImportTestBase):
FIXTURE_DIR = "mattermost_v11.1.0_fixtures/raw_mmctl_output"
SUBDOMAIN = "test-realm"
TEAM = "ad-1"
OWNER_EMAILS = {"sysadmin@sample.mattermost.com"}
GUEST_EMAILS = {"guest@sample.mattermost.com"}
BOT_EMAILS = {"lori.carter-bot@zulip.example.com"}
def test_mattermost_data_file_to_dict(self) -> None:
fixture_file = self.fixture_file_name("import.jsonl", self.FIXTURE_DIR)
mattermost_data = mattermost_data_file_to_dict(fixture_file)
self.assert_length(mattermost_data, 8)
self.assert_length(mattermost_data["team"], 1)
self.assertEqual(mattermost_data["team"][0]["name"], "ad-1")
self.assert_length(mattermost_data["channel"], 5)
self.assert_length(mattermost_data["user"], 20)
self.assert_length(mattermost_data["emoji"], 1)
self.assert_length(mattermost_data["post"]["channel_post"], 51)
self.assert_length(mattermost_data["post"]["direct_post"], 50)
self.assert_length(mattermost_data["direct_channel"], 79)
self.assert_length(mattermost_data["role"], 23)
exported_bot_users = [user for user in mattermost_data["user"] if user.get("is_bot")]
self.assert_length(exported_bot_users, 3)
def test_e2e_export_data_v11_1_0(self) -> None:
with self.settings(EXTERNAL_HOST="zulip.example.com"):
conversion_result = self.run_convert_and_import(
export_file_name="import.jsonl",
fixture_dir=self.FIXTURE_DIR,
team_name=self.TEAM,
subdomain="test-realm",
)
user_map_data = self.build_user_maps(self.TEAM, conversion_result.mattermost_data)
with self.subTest("test user conversion"):
self.assert_user_conversion(
mattermost_data=conversion_result.mattermost_data,
imported_realm=conversion_result.imported_realm,
expected_owner_emails=self.OWNER_EMAILS,
expected_guest_emails=self.GUEST_EMAILS,
expected_bot_user_emails=self.BOT_EMAILS,
# Out of the three bots, two (Jira bot and system-bot) never participated in
# any channel, so they don't get converted.
expected_number_of_imported_users=len(conversion_result.mattermost_data["user"])
- 2,
)
with self.subTest("test channel conversion"):
self.assert_channel_conversion(
mattermost_data=conversion_result.mattermost_data,
imported_realm=conversion_result.imported_realm,
exported_channel_subscriber_dict=user_map_data.exported_channel_subscriber_dict,
)
with self.subTest("test channel message conversion"):
self.assert_channel_messages(
conversion_result.mattermost_data,
conversion_result.imported_realm,
# "sequi-7" is the Mattermost channel ID of "nesciunt"
mattermost_channel_id="sequi-7",
channel_name="nesciunt",
username_to_email_map=user_map_data.username_to_email_map,
expected_number_of_bot_messages=2,
)
with self.subTest("test direct messages"):
self.assert_direct_messages(
mattermost_data=conversion_result.mattermost_data,
imported_realm=conversion_result.imported_realm,
username_to_email_map=user_map_data.username_to_email_map,
expected_number_of_bot_messages=2,
)
with self.subTest("test attachments"):
self.assert_attachments(conversion_result.imported_realm, expected_count=3)
# There's a post that references a missing attachment file, the importer
# should continue processing the message but skip processing the missing
# attachment file. Missing custom emoji files should also be treated the
# same.
self.assertListEqual(
[
"INFO:root:Generating data for ad-1",
"INFO:root:Starting to process emoticons",
"INFO:root:Emoji file no found: 'data/bogus/emoji.png'; continuing.",
"INFO:root:Done processing emoticons",
"INFO:root:Message attachment file not found: '20260123/bogus/path/to/file.txt'; continuing.",
"INFO:root:Processed messages up to 63 / 63",
"INFO:root:Processed messages up to 60 / 60",
"INFO:root:Exporting migration status",
],
conversion_result.log_output,
)
class MattermostCombinedTeamsImportTest(MattermostImportTestBase):
FIXTURE_DIR = "mattermost_v11.6.0_fixtures/raw_mmctl_output"
SUBDOMAIN = "test-realm"
TEAM = "zulip-1"
OWNER_EMAILS = {
"user-4@sample.mattermost.com",
"user-8@sample.mattermost.com",
"user-7@sample.mattermost.com",
"sysadmin@sample.mattermost.com",
"adminuser123@gmail.com",
"user-10@sample.mattermost.com",
"user-9@sample.mattermost.com",
}
GUEST_EMAILS = {"guest@sample.mattermost.com"}
BOT_EMAILS = {
"system-bot-bot@zulip.example.com",
"keith.ryan-bot@zulip.example.com",
"robert.ward-bot@zulip.example.com",
"lori.carter-bot@zulip.example.com",
}
def test_mattermost_data_file_to_dict(self) -> None:
fixture_file = self.fixture_file_name("import.jsonl", self.FIXTURE_DIR)
mattermost_data = mattermost_data_file_to_dict(fixture_file, combine_into_one_realm=True)
exported_bot_users = [user for user in mattermost_data["user"] if user.get("is_bot")]
self.assert_length(mattermost_data, 8)
self.assert_length(mattermost_data["team"], 1)
self.assertDictEqual(mattermost_data["team"][0], DEFAULT_SINGLE_TEAM_OBJECT)
self.assert_length(mattermost_data["channel"], 26)
self.assert_length(mattermost_data["user"], 19)
self.assert_length(mattermost_data["emoji"], 0)
self.assert_length(mattermost_data["post"]["channel_post"], 105)
self.assertEqual(
mattermost_data["post"]["channel_post"][0]["team"], DEFAULT_SINGLE_TEAM_OBJECT["name"]
)
self.assertEqual(
mattermost_data["post"]["channel_post"][0]["channel"],
COMPILED_CHANNEL_ID_FORMAT.format(id="off-topic", team="ad-1"),
)
self.assert_length(mattermost_data["post"]["direct_post"], 97)
self.assert_length(mattermost_data["direct_channel"], 79)
self.assert_length(mattermost_data["role"], 25)
exported_bot_users = [user for user in mattermost_data["user"] if user.get("is_bot")]
self.assert_length(exported_bot_users, len(self.BOT_EMAILS))
def test_combining_user_not_in_any_team(self) -> None:
# Mattermost exports "teams": null for users who are not a
# member of any team. Verify such users are passed through
# unchanged when combining teams, so that they are imported
# only as mirror dummies backfilled from their posts, just
# like in the non-combined code path.
user_object = {"type": "user", "user": {"username": "teamless.user", "teams": None}}
jsonl_line = json.dumps(user_object)
with tempfile.TemporaryDirectory() as tmp_dir:
import_jsonl_path = os.path.join(tmp_dir, "import.jsonl")
with open(import_jsonl_path, "w") as f:
f.write(jsonl_line + "\n")
mattermost_data = mattermost_data_file_to_dict(
import_jsonl_path, combine_into_one_realm=True
)
self.assert_length(mattermost_data["user"], 1)
self.assertEqual(mattermost_data["user"][0], {"username": "teamless.user", "teams": None})
def test_e2e_export_data_v11_6_0(self) -> None:
# The assert functions here iterate over the exported Mattermost objects and checks
# whether a corresponding Zulip object exists in the imported realm. So, If
# combined_into_one_realm=True and the export fixture contains multiple teams, it
# should check that all rows from different Mattermost teams exist inside a single
# Zulip realm.
with self.settings(EXTERNAL_HOST="zulip.example.com"):
conversion_result = self.run_convert_and_import(
export_file_name="import.jsonl",
fixture_dir=self.FIXTURE_DIR,
team_name=self.TEAM,
subdomain="test-realm",
combine_into_one_realm=True,
)
user_map_data = self.build_user_maps(self.TEAM, conversion_result.mattermost_data)
with self.subTest("test user conversion"):
self.assert_user_conversion(
mattermost_data=conversion_result.mattermost_data,
imported_realm=conversion_result.imported_realm,
expected_owner_emails=self.OWNER_EMAILS,
# The user "bobby.watson" is a team guest in ad-1, they will be converted into
# a normal Zulip user since they're a normal user in at least one other team.
expected_guest_emails=self.GUEST_EMAILS,
expected_bot_user_emails=self.BOT_EMAILS,
expected_number_of_imported_users=len(conversion_result.mattermost_data["user"]),
)
with self.subTest("test channel conversion"):
self.assert_channel_conversion(
mattermost_data=conversion_result.mattermost_data,
imported_realm=conversion_result.imported_realm,
exported_channel_subscriber_dict=user_map_data.exported_channel_subscriber_dict,
)
with self.subTest("test channel message conversion"):
mattermost_channels = [
(
COMPILED_CHANNEL_ID_FORMAT.format(id="voluptas-9", team="reiciendis-0"),
"voluptatem",
3,
),
(COMPILED_CHANNEL_ID_FORMAT.format(id="minima-3", team="ad-1"), "veritatis", 0),
]
for mattermost_channel_id, channel_name, number_of_bot_messages in mattermost_channels:
self.assert_channel_messages(
conversion_result.mattermost_data,
conversion_result.imported_realm,
mattermost_channel_id=mattermost_channel_id,
channel_name=channel_name,
username_to_email_map=user_map_data.username_to_email_map,
expected_number_of_bot_messages=number_of_bot_messages,
)
with self.subTest("test direct messages"):
self.assert_direct_messages(
mattermost_data=conversion_result.mattermost_data,
imported_realm=conversion_result.imported_realm,
username_to_email_map=user_map_data.username_to_email_map,
expected_number_of_bot_messages=28,
)
def test_combining_unknown_object(self) -> None:
unknown_object = {"type": "unknown", "unknown": {"team": "some-team", "foo": "bar"}}
jsonl_line = json.dumps(unknown_object)
with tempfile.TemporaryDirectory() as tmp_dir:
import_jsonl_path = os.path.join(tmp_dir, "import.jsonl")
with open(import_jsonl_path, "w") as f:
f.write(jsonl_line + "\n")
with self.assertRaises(AssertionError) as e:
do_convert_data(
mattermost_data_dir=tmp_dir,
output_dir=make_export_output_dir(),
masking_content=False,
combine_into_one_realm=True,
)
self.assertEqual(
str(e.exception),
f"Found unexpected 'unknown' object while compiling into combined realm. {unknown_object}",
)