2021-07-15 15:15:19 -04:00
|
|
|
import asyncio
|
2018-06-03 02:14:16 -04:00
|
|
|
import os
|
2018-06-03 12:37:17 -04:00
|
|
|
import shutil
|
2021-08-24 19:50:57 -04:00
|
|
|
import sys
|
2018-06-03 12:37:17 -04:00
|
|
|
from contextlib import contextmanager
|
2021-05-28 05:00:04 -04:00
|
|
|
from subprocess import PIPE, Popen
|
2018-06-03 12:37:17 -04:00
|
|
|
from tempfile import mkdtemp
|
2021-07-15 15:15:19 -04:00
|
|
|
from typing import Any, Awaitable, Callable, Iterator, List
|
2021-05-28 05:00:04 -04:00
|
|
|
from unittest import TestCase, mock
|
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
import nio
|
|
|
|
|
|
|
|
from .matrix_bridge import MatrixToZulip, ZulipToMatrix, read_configuration
|
2022-08-23 00:12:14 -04:00
|
|
|
|
2018-06-03 02:14:16 -04:00
|
|
|
script_file = "matrix_bridge.py"
|
|
|
|
script_dir = os.path.dirname(__file__)
|
|
|
|
script = os.path.join(script_dir, script_file)
|
|
|
|
|
2018-06-03 12:37:17 -04:00
|
|
|
sample_config_path = "matrix_test.conf"
|
|
|
|
|
|
|
|
sample_config_text = """[matrix]
|
|
|
|
host = https://matrix.org
|
2021-11-02 07:11:33 -04:00
|
|
|
mxid = @username:matrix.org
|
2018-06-03 12:37:17 -04:00
|
|
|
password = password
|
|
|
|
room_id = #zulip:matrix.org
|
|
|
|
|
|
|
|
[zulip]
|
|
|
|
email = glitch-bot@chat.zulip.org
|
|
|
|
api_key = aPiKeY
|
|
|
|
site = https://chat.zulip.org
|
|
|
|
stream = test here
|
|
|
|
topic = matrix
|
2018-06-03 02:18:07 -04:00
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
[additional_bridge1]
|
|
|
|
room_id = #example:matrix.org
|
|
|
|
stream = new test
|
|
|
|
topic = matrix
|
|
|
|
|
2018-06-03 12:37:17 -04:00
|
|
|
"""
|
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
ZULIP_MESSAGE_TEMPLATE: str = "**{username}** [{uid}]: {message}"
|
|
|
|
|
|
|
|
|
2023-01-24 14:07:21 -05:00
|
|
|
# For Python 3.7 compatibility.
|
2021-07-15 15:15:19 -04:00
|
|
|
# (Since 3.8, there is unittest.IsolatedAsyncioTestCase!)
|
|
|
|
# source: https://stackoverflow.com/a/46324983
|
|
|
|
def async_test(coro: Callable[..., Awaitable[Any]]) -> Callable[..., Any]:
|
|
|
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
try:
|
|
|
|
return loop.run_until_complete(coro(*args, **kwargs))
|
|
|
|
finally:
|
|
|
|
loop.close()
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
2021-05-28 05:03:46 -04:00
|
|
|
|
2018-06-03 12:37:17 -04:00
|
|
|
@contextmanager
|
2020-04-18 18:59:12 -04:00
|
|
|
def new_temp_dir() -> Iterator[str]:
|
2018-06-03 12:37:17 -04:00
|
|
|
path = mkdtemp()
|
|
|
|
yield path
|
|
|
|
shutil.rmtree(path)
|
2018-06-03 02:14:16 -04:00
|
|
|
|
2021-05-28 05:03:46 -04:00
|
|
|
|
2018-06-17 14:51:47 -04:00
|
|
|
class MatrixBridgeScriptTests(TestCase):
|
2020-04-18 18:59:12 -04:00
|
|
|
def output_from_script(self, options: List[str]) -> List[str]:
|
2021-05-28 05:03:46 -04:00
|
|
|
popen = Popen(
|
2021-08-24 19:50:57 -04:00
|
|
|
[sys.executable, script] + options, stdin=PIPE, stdout=PIPE, universal_newlines=True
|
2021-05-28 05:03:46 -04:00
|
|
|
)
|
2018-06-03 02:18:07 -04:00
|
|
|
return popen.communicate()[0].strip().split("\n")
|
|
|
|
|
2020-04-18 18:59:12 -04:00
|
|
|
def test_no_args(self) -> None:
|
2018-06-03 02:18:07 -04:00
|
|
|
output_lines = self.output_from_script([])
|
2018-06-03 02:14:16 -04:00
|
|
|
expected_lines = [
|
|
|
|
"Options required: -c or --config to run, OR --write-sample-config.",
|
2021-05-28 07:19:40 -04:00
|
|
|
f"usage: {script_file} [-h]",
|
2018-06-03 02:14:16 -04:00
|
|
|
]
|
|
|
|
for expected, output in zip(expected_lines, output_lines):
|
|
|
|
self.assertIn(expected, output)
|
|
|
|
|
2020-04-18 18:59:12 -04:00
|
|
|
def test_help_usage_and_description(self) -> None:
|
2018-06-03 02:18:07 -04:00
|
|
|
output_lines = self.output_from_script(["-h"])
|
2021-05-28 07:19:40 -04:00
|
|
|
usage = f"usage: {script_file} [-h]"
|
2021-07-15 15:15:19 -04:00
|
|
|
description = "Bridge between Zulip topics and Matrix channels."
|
2018-06-03 02:14:16 -04:00
|
|
|
self.assertIn(usage, output_lines[0])
|
2021-05-28 05:05:11 -04:00
|
|
|
blank_lines = [num for num, line in enumerate(output_lines) if line == ""]
|
2018-06-03 02:14:16 -04:00
|
|
|
# There should be blank lines in the output
|
|
|
|
self.assertTrue(blank_lines)
|
|
|
|
# There should be finite output
|
|
|
|
self.assertTrue(len(output_lines) > blank_lines[0])
|
|
|
|
# Minimal description should be in the first line of the 2nd "paragraph"
|
|
|
|
self.assertIn(description, output_lines[blank_lines[0] + 1])
|
2018-06-03 12:37:17 -04:00
|
|
|
|
2020-04-18 18:59:12 -04:00
|
|
|
def test_write_sample_config(self) -> None:
|
2018-06-03 12:37:17 -04:00
|
|
|
with new_temp_dir() as tempdir:
|
|
|
|
path = os.path.join(tempdir, sample_config_path)
|
|
|
|
output_lines = self.output_from_script(["--write-sample-config", path])
|
2021-05-28 07:19:40 -04:00
|
|
|
self.assertEqual(output_lines, [f"Wrote sample configuration to '{path}'"])
|
2018-06-03 12:37:17 -04:00
|
|
|
|
|
|
|
with open(path) as sample_file:
|
|
|
|
self.assertEqual(sample_file.read(), sample_config_text)
|
2018-06-17 14:51:47 -04:00
|
|
|
|
2020-04-18 18:59:12 -04:00
|
|
|
def test_write_sample_config_from_zuliprc(self) -> None:
|
2018-06-03 16:47:28 -04:00
|
|
|
zuliprc_template = ["[api]", "email={email}", "key={key}", "site={site}"]
|
2021-05-28 05:03:46 -04:00
|
|
|
zulip_params = {
|
2021-05-28 05:05:11 -04:00
|
|
|
"email": "foo@bar",
|
|
|
|
"key": "some_api_key",
|
|
|
|
"site": "https://some.chat.serverplace",
|
2021-05-28 05:03:46 -04:00
|
|
|
}
|
2018-06-03 16:47:28 -04:00
|
|
|
with new_temp_dir() as tempdir:
|
|
|
|
path = os.path.join(tempdir, sample_config_path)
|
|
|
|
zuliprc_path = os.path.join(tempdir, "zuliprc")
|
|
|
|
with open(zuliprc_path, "w") as zuliprc_file:
|
|
|
|
zuliprc_file.write("\n".join(zuliprc_template).format(**zulip_params))
|
2021-05-28 05:03:46 -04:00
|
|
|
output_lines = self.output_from_script(
|
|
|
|
["--write-sample-config", path, "--from-zuliprc", zuliprc_path]
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
output_lines,
|
|
|
|
[
|
|
|
|
"Wrote sample configuration to '{}' using zuliprc file '{}'".format(
|
|
|
|
path, zuliprc_path
|
|
|
|
)
|
|
|
|
],
|
|
|
|
)
|
2018-06-03 16:47:28 -04:00
|
|
|
|
|
|
|
with open(path) as sample_file:
|
|
|
|
sample_lines = [line.strip() for line in sample_file.readlines()]
|
|
|
|
expected_lines = sample_config_text.split("\n")
|
2021-05-28 05:05:11 -04:00
|
|
|
expected_lines[7] = "email = {}".format(zulip_params["email"])
|
|
|
|
expected_lines[8] = "api_key = {}".format(zulip_params["key"])
|
|
|
|
expected_lines[9] = "site = {}".format(zulip_params["site"])
|
2018-06-03 16:47:28 -04:00
|
|
|
self.assertEqual(sample_lines, expected_lines[:-1])
|
2018-06-17 14:51:47 -04:00
|
|
|
|
2020-04-18 18:59:12 -04:00
|
|
|
def test_detect_zuliprc_does_not_exist(self) -> None:
|
2018-06-06 00:59:17 -04:00
|
|
|
with new_temp_dir() as tempdir:
|
|
|
|
path = os.path.join(tempdir, sample_config_path)
|
|
|
|
zuliprc_path = os.path.join(tempdir, "zuliprc")
|
|
|
|
# No writing of zuliprc file here -> triggers check for zuliprc absence
|
2021-05-28 05:03:46 -04:00
|
|
|
output_lines = self.output_from_script(
|
|
|
|
["--write-sample-config", path, "--from-zuliprc", zuliprc_path]
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
output_lines,
|
|
|
|
[
|
|
|
|
"Could not write sample config: Zuliprc file '{}' does not exist.".format(
|
|
|
|
zuliprc_path
|
|
|
|
)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
def test_parse_multiple_bridges(self) -> None:
|
|
|
|
with new_temp_dir() as tempdir:
|
|
|
|
path = os.path.join(tempdir, sample_config_path)
|
|
|
|
output_lines = self.output_from_script(["--write-sample-config", path])
|
|
|
|
self.assertEqual(output_lines, [f"Wrote sample configuration to '{path}'"])
|
2018-06-06 00:59:17 -04:00
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
config = read_configuration(path)
|
2018-06-17 15:11:00 -04:00
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
self.assertIn("zulip", config)
|
|
|
|
self.assertIn("matrix", config)
|
|
|
|
self.assertIn("bridges", config["zulip"])
|
|
|
|
self.assertIn("bridges", config["matrix"])
|
|
|
|
self.assertEqual(
|
|
|
|
{
|
|
|
|
("test here", "matrix"): "#zulip:matrix.org",
|
|
|
|
("new test", "matrix"): "#example:matrix.org",
|
|
|
|
},
|
|
|
|
config["zulip"]["bridges"],
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
{
|
|
|
|
"#zulip:matrix.org": ("test here", "matrix"),
|
|
|
|
"#example:matrix.org": ("new test", "matrix"),
|
|
|
|
},
|
|
|
|
config["matrix"]["bridges"],
|
|
|
|
)
|
2018-06-17 16:04:07 -04:00
|
|
|
|
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
class MatrixBridgeMatrixToZulipTests(TestCase):
|
|
|
|
user_name = "John Smith"
|
|
|
|
user_uid = "@johnsmith:matrix.org"
|
|
|
|
room = mock.MagicMock()
|
|
|
|
room.user_name = lambda _: "John Smith"
|
2022-08-23 00:12:14 -04:00
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
def setUp(self) -> None:
|
|
|
|
self.matrix_to_zulip = mock.MagicMock()
|
|
|
|
self.matrix_to_zulip.get_message_content_from_event = (
|
|
|
|
lambda event: MatrixToZulip.get_message_content_from_event(
|
|
|
|
self.matrix_to_zulip, event, self.room
|
|
|
|
)
|
|
|
|
)
|
2022-08-23 00:12:14 -04:00
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
@async_test
|
|
|
|
async def test_get_message_content_from_event(self) -> None:
|
|
|
|
class RoomMemberEvent(nio.RoomMemberEvent):
|
|
|
|
def __init__(self, sender: str = self.user_uid) -> None:
|
|
|
|
self.sender = sender
|
2022-08-23 00:12:14 -04:00
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
class RoomMessageFormatted(nio.RoomMessageFormatted):
|
|
|
|
def __init__(self, sender: str = self.user_uid) -> None:
|
|
|
|
self.sender = sender
|
|
|
|
self.body = "this is a message"
|
2022-08-23 00:12:14 -04:00
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
self.assertIsNone(
|
|
|
|
await self.matrix_to_zulip.get_message_content_from_event(RoomMemberEvent())
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
await self.matrix_to_zulip.get_message_content_from_event(RoomMessageFormatted()),
|
|
|
|
ZULIP_MESSAGE_TEMPLATE.format(
|
|
|
|
username=self.user_name, uid=self.user_uid, message="this is a message"
|
|
|
|
),
|
|
|
|
)
|
2022-08-23 00:12:14 -04:00
|
|
|
|
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
class MatrixBridgeZulipToMatrixTests(TestCase):
|
|
|
|
room = mock.MagicMock()
|
|
|
|
valid_zulip_config = dict(
|
|
|
|
stream="some stream",
|
|
|
|
topic="some topic",
|
|
|
|
email="some@email",
|
|
|
|
bridges={("some stream", "some topic"): room},
|
|
|
|
)
|
|
|
|
valid_msg = dict(
|
|
|
|
sender_email="John@Smith.smith", # must not be equal to config:email
|
|
|
|
sender_id=42,
|
|
|
|
type="stream", # Can only mirror Zulip streams
|
|
|
|
display_recipient=valid_zulip_config["stream"],
|
|
|
|
subject=valid_zulip_config["topic"],
|
|
|
|
)
|
2022-08-23 00:12:14 -04:00
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
def setUp(self) -> None:
|
|
|
|
self.zulip_to_matrix = mock.MagicMock()
|
|
|
|
self.zulip_to_matrix.zulip_config = self.valid_zulip_config
|
|
|
|
self.zulip_to_matrix.get_matrix_room_for_zulip_message = (
|
|
|
|
lambda msg: ZulipToMatrix.get_matrix_room_for_zulip_message(self.zulip_to_matrix, msg)
|
|
|
|
)
|
2022-08-23 00:12:14 -04:00
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
def test_get_matrix_room_for_zulip_message_success(self) -> None:
|
|
|
|
self.assertEqual(
|
|
|
|
self.zulip_to_matrix.get_matrix_room_for_zulip_message(self.valid_msg), self.room
|
|
|
|
)
|
2022-08-23 00:12:14 -04:00
|
|
|
|
2021-07-15 15:15:19 -04:00
|
|
|
def test_get_matrix_room_for_zulip_message_failure(self) -> None:
|
|
|
|
self.assertIsNone(
|
|
|
|
self.zulip_to_matrix.get_matrix_room_for_zulip_message(
|
|
|
|
dict(self.valid_msg, type="private")
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.assertIsNone(
|
|
|
|
self.zulip_to_matrix.get_matrix_room_for_zulip_message(
|
|
|
|
dict(self.valid_msg, sender_email="some@email")
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.assertIsNone(
|
|
|
|
self.zulip_to_matrix.get_matrix_room_for_zulip_message(
|
|
|
|
dict(self.valid_msg, display_recipient="other stream")
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.assertIsNone(
|
|
|
|
self.zulip_to_matrix.get_matrix_room_for_zulip_message(
|
|
|
|
dict(self.valid_msg, subject="other topic")
|
|
|
|
)
|
|
|
|
)
|