Exemple #1
0
async def test_local_echo(e2e_room: Room):
    new = []
    cb = lambda room, event: new.append(event)  # noqa
    e2e_room.client.rooms.callbacks[Text].append(cb)

    initial_events = len(e2e_room.timeline)
    await e2e_room.timeline.send(Text("hi"), transaction_id="abc")
    assert len(e2e_room.timeline) == initial_events + 1

    for event in new:
        assert event.content == Text("hi")
        assert event.sender == e2e_room.client.user_id
        assert not event.decryption

    assert new[0].id == "$echo.abc"
    assert new[0].sending == SendStep.sending
    assert new[0].id not in e2e_room.timeline

    assert new[1].id != new[0].id
    assert new[1].sending == SendStep.sent
    assert new[1].id in e2e_room.timeline

    await e2e_room.client.sync.once()
    assert new[2].sending == SendStep.synced
    assert len(new) == 3
    assert len(e2e_room.timeline) == initial_events + 1
Exemple #2
0
def test_textual_no_reply_fallback():
    assert Text("plain").html_no_reply_fallback is None

    reply = Text.from_html("<b>foo</b>")  # from_html will strip mx-reply
    reply.formatted_body = HTML_REPLY_FALLBACK + reply.formatted_body
    assert reply.html_no_reply_fallback == "<b>foo</b>"

    not_reply = Text.from_html("<b>foo</b>")
    assert not_reply.html_no_reply_fallback == "<b>foo</b>"
Exemple #3
0
async def test_sending_failure(room: Room, bob: Client):
    new = []
    cb = lambda room, event: new.append((event.id, event.sending))  # noqa
    room.client.rooms.callbacks[TimelineEvent].append(cb)

    await room.timeline.send(Text("abc"))
    await bob.rooms.join(room.id)  # prevent room from becoming inaccessible
    await room.leave()

    # Event that has failed sending in timeline

    with raises(MatrixError):
        await room.timeline.send(Text("def"), transaction_id="123")

    unsent_id = EventId("$echo.123")
    assert new[-1] == (unsent_id, SendStep.failed)
    assert unsent_id in room.timeline
    assert list(room.timeline.unsent) == [unsent_id]
    assert room.timeline.unsent[unsent_id].content == Text("def")
    assert room.timeline.unsent[unsent_id].sending == SendStep.failed
    assert not room.timeline.unsent[unsent_id].historic

    # Failed in unsent but not timeline._data after client restart

    await room.client.terminate()
    client2 = Client(room.client.base_dir)
    new = []
    cb = lambda room, event: new.append((event.id, event.sending))  # noqa
    client2.rooms.callbacks[TimelineEvent].append(cb)

    await client2.load()
    assert new == [(unsent_id, SendStep.failed)]

    timeline2 = client2.rooms[room.id].timeline
    assert unsent_id in timeline2
    assert list(timeline2.unsent) == [unsent_id]
    assert timeline2.unsent[unsent_id].content == Text("def")
    assert timeline2.unsent[unsent_id].sending == SendStep.failed
    assert timeline2.unsent[unsent_id].historic

    # Retry sending that failed

    await client2.rooms.join(room.id)
    event_id = await timeline2.resend_failed(timeline2.unsent[unsent_id])
    assert timeline2[event_id].sending == SendStep.sent
    assert not timeline2.unsent
    assert new[-1] == (event_id, SendStep.sent)

    with raises(AssertionError):
        await timeline2.resend_failed(timeline2[event_id])
Exemple #4
0
async def test_timeline_redaction(e2e_room: Room):
    new = []
    cb = lambda room, event: new.append(event)  # noqa
    e2e_room.client.rooms.callbacks[TimelineEvent].append(cb)

    # Send original message
    await e2e_room.timeline.send(Text("hi"))
    await e2e_room.client.sync.once()

    # Redact + test local echo
    await e2e_room.timeline[-1].redact("bye")
    assert isinstance(new[-2].content, Redaction)
    assert isinstance(new[-1].content, Redacted)
    assert new[-1].redacted_by == new[-2]
    assert new[-1].redacted_by.sending == SendStep.sent
    await e2e_room.client.sync.once()

    # Check Redaction in timeline
    redaction = e2e_room.timeline[-1]
    assert isinstance(redaction.content, Redaction)
    assert redaction.content.reason == "bye"

    # Check message now redacted in timeline
    redacted = e2e_room.timeline[-2]
    assert isinstance(redacted.content, Redacted)
    assert redacted.redacted_by == redaction

    # Check callback results for real events
    assert isinstance(new[-2].content, Redaction)
    assert isinstance(new[-1].content, Redacted)
    assert new[2].sending == SendStep.synced
Exemple #5
0
async def test_cancel_session_forward(alice: Client, e2e_room: Room, tmp_path):
    await e2e_room.timeline.send(Text("sent before other devices exist"))

    alice2 = await new_device_from(alice, tmp_path / "alice2")
    alice3 = await new_device_from(alice, tmp_path / "alice3")

    # Make alice3 send a request to both alice and alice2

    for other in (alice2, alice3):
        await other.sync.once()
        await other.rooms[e2e_room.id].timeline.load_history(1)

    sent_to = {alice.user_id, alice2.user_id}
    assert next(iter(alice3.e2e._sent_session_requests.values()))[1] == sent_to

    # Make alice respond to alice3's request before alice2

    await alice.sync.once()
    await alice.devices.own[alice3.device_id].trust()
    await alice3.sync.once()

    # alice3 got alice's response, so it should have cancelled alice2's request

    cancelled = False
    sync_data = await alice2.sync.once()
    assert sync_data

    for event in sync_data["to_device"]["events"]:
        if event["content"]["action"] == "request_cancellation":
            cancelled = True

    assert cancelled
Exemple #6
0
async def test_redact_failure(room: Room):
    await room.timeline.send(Text("hi"))
    await room.client.sync.once()
    await room.leave()

    new = []
    cb = lambda room, event: new.append((  # noqa
        type(event.content),
        event.sending,
        event.redacted_by.sending if event.redacted_by else None,
    ))
    room.client.rooms.callbacks[TimelineEvent].append(cb)

    with raises(MatrixError):
        await room.timeline[-1].redact()

    assert isinstance(room.timeline[-2].content, Redacted)
    assert room.timeline[-2].redacted_by
    assert room.timeline[-2].redacted_by.sending == SendStep.failed

    assert isinstance(room.timeline[-1].content, Redaction)
    assert room.timeline[-1].sending == SendStep.failed
    assert room.timeline[-1].id not in room.timeline.unsent

    assert new == [
        (Redaction, SendStep.sending, None),
        (Redacted, SendStep.synced, SendStep.sending),
        (Redaction, SendStep.failed, None),
        (Redacted, SendStep.synced, SendStep.failed),
    ]
Exemple #7
0
async def test_session_forwarding(alice: Client, e2e_room: Room, tmp_path):
    # https://github.com/matrix-org/synapse/pull/8675 cross-user sharing broken

    await e2e_room.timeline.send(Text("sent before other devices exist"))

    untrusted = await new_device_from(alice, tmp_path / "unstrusted")
    blocked   = await new_device_from(alice, tmp_path / "blocked")

    for other in (untrusted, blocked):
        await other.sync.once()
        await other.rooms[e2e_room.id].timeline.load_history(1)

    await alice.sync.once()
    await alice.devices.own[blocked.device_id].block()
    assert alice.devices.own[untrusted.device_id].trusted is None
    assert alice.devices.own[blocked.device_id].trusted is False

    for other in (untrusted, blocked):
        # Ensure session requests from untrusted or blocked device are pended

        assert alice.devices.own[other.device_id].pending_session_requests
        assert len(other.e2e._sent_session_requests) == 1

        other_event = other.rooms[e2e_room.id].timeline[-1]
        assert isinstance(other_event.content, Megolm)

        # Test replying to pending forward request of previously untrusted dev.

        await alice.devices.own[other.device_id].trust()
        await other.sync.once()
        assert not alice.devices.own[other.device_id].pending_session_requests
        assert not other.e2e._sent_session_requests

        other_event = other.rooms[e2e_room.id].timeline[-1]
        assert isinstance(other_event.content, Text)
Exemple #8
0
async def test_broken_olm_lost_forwarded_session_recovery(
    alice: Client, e2e_room: Room, tmp_path,
):
    await e2e_room.timeline.send(Text("sent before other devices exist"))

    # Receive an undecryptable megolm and send a group session request for it

    alice2 = await new_device_from(alice, tmp_path / "second")
    await alice2.sync.once()
    assert isinstance(alice2.rooms[e2e_room.id].timeline[-1].content, Megolm)

    # Respond to alice2's group session request

    await alice.sync.once()
    await alice.devices.own[alice2.device_id].trust()

    # Fail to decrypt alice's response to our group session request,
    # alice2 will create a new olm session and send a new request

    sync_data = await alice2.sync.once(_handle=False)
    assert sync_data
    sync_data["to_device"]["events"][0]["content"]["ciphertext"].clear()
    await alice2.sync._handle_sync(sync_data)
    assert isinstance(alice2.rooms[e2e_room.id].timeline[-1].content, Megolm)

    # Alice responds to the new requerst, then alice2 should be able to decrypt

    await alice.sync.once()
    await alice2.sync.once()
    assert isinstance(alice2.rooms[e2e_room.id].timeline[-1].content, Text)
Exemple #9
0
async def test_leave(e2e_room: Room):
    await e2e_room.timeline.send(Text("make a session"))
    assert e2e_room.id in e2e_room.client.e2e._out_group_sessions

    await e2e_room.leave(reason="bye")
    await e2e_room.client.sync.once()
    assert e2e_room.left
    assert e2e_room.id not in e2e_room.client.e2e._out_group_sessions
    assert e2e_room.state.me.membership_reason == "bye"
Exemple #10
0
async def test_tracking(alice: Client, e2e_room: Room, bob: Client, tmp_path):
    # Get initial devices of users we share an encrypted room with:

    await bob.rooms.join(e2e_room.id)
    await bob.sync.once()
    await bob.rooms[e2e_room.id].timeline.send(Text("makes alice get my key"))

    bob_dev1 = bob.devices.current
    assert bob.user_id not in alice.devices
    assert bob_dev1.curve25519 not in alice.devices.by_curve

    await alice.sync.once()
    assert alice.devices[bob.user_id] == {bob_dev1.device_id: bob_dev1}
    assert alice.devices.by_curve[bob_dev1.curve25519] == bob_dev1

    # Notice a user's new devices at runtime and share session with it:

    bob2 = await new_device_from(bob, tmp_path)
    bob_dev2 = bob2.devices.current

    await alice.sync.once()
    await bob.sync.once()
    await e2e_room.timeline.send(Text("notice bob's new device"))
    await bob.rooms[e2e_room.id].timeline.send(Text("notice my new device"))

    assert alice.devices[bob.user_id] == bob.devices.own == {
        bob_dev1.device_id: bob_dev1,
        bob_dev2.device_id: bob_dev2,
    }
    assert alice.devices.by_curve[bob_dev1.curve25519] == bob_dev1
    assert alice.devices.by_curve[bob_dev2.curve25519] == bob_dev2

    await bob2.sync.once()
    assert isinstance(bob2.rooms[e2e_room.id].timeline[-1].content, Text)

    # Stop tracking devices of users we no longer share an encrypted room with:

    await bob.rooms[e2e_room.id].leave()
    await alice.sync.once()
    assert bob.user_id not in alice.devices
    assert bob_dev1.curve25519 not in alice.devices.by_curve
    assert bob_dev2.curve25519 not in alice.devices.by_curve
Exemple #11
0
async def test_timeline_event_callback(alice: Client, room: Room):
    got = []
    cb  = lambda r, e: got.extend([r, type(e.content)])  # noqa
    alice.rooms.callbacks[TimelineEvent].append(cb)

    await room.timeline.send(Text("This is a test"), local_echo_to=[])
    await room.timeline.send(Emote("tests"), local_echo_to=[])
    # We parse a corresponding timeline event on sync for new states
    await room.state.send(CanonicalAlias())
    await alice.sync.once()
    assert got == [room, Text, room, Emote, room, CanonicalAlias]
Exemple #12
0
async def test_send_to_lazy_encrypted_room(e2e_room: Room, bob: Client):
    await e2e_room.invite(bob.user_id)
    await bob.rooms.join(e2e_room.id)
    await e2e_room.client.sync.once()

    assert not e2e_room.state.all_users_loaded
    await e2e_room.timeline.send(Text("hi"))
    assert e2e_room.state.all_users_loaded
    assert len(e2e_room.state.users) == 2

    await bob.sync.once()
    assert isinstance(bob.rooms[e2e_room.id].timeline[-1].content, Text)
Exemple #13
0
async def test_textual_replying_to(room: Room):
    await room.timeline.send(Text("plain\nmsg"))
    await room.timeline.send(Text.from_html("<b>html</b><br>msg"))
    await room.state.send(Name("Test room"))
    await room.client.sync.once()
    plain, html, other = list(room.timeline.values())[-3:]

    def check_reply_attrs(first_event, reply, fallback_content, reply_content):
        assert reply.replies_to == first_event.id
        assert reply.format == HTML_FORMAT

        assert reply.formatted_body == HTML_REPLY_FALLBACK.format(
            matrix_to=MATRIX_TO,
            room_id=room.id,
            user_id=room.client.user_id,
            event_id=first_event.id,
            content=fallback_content,
        ) + reply_content

    reply = Text("nice").replying_to(plain)
    assert reply.body == f"> <{plain.sender}> plain\n> msg\n\nnice"
    check_reply_attrs(plain, reply, plain2html(plain.content.body), "nice")

    reply = Text("nice").replying_to(html)
    assert reply.body == f"> <{plain.sender}> **html**  \n> msg\n\nnice"
    check_reply_attrs(html, reply, html.content.formatted_body, "nice")

    reply = Text.from_html("<i>nice</i>").replying_to(html)
    assert reply.body == f"> <{plain.sender}> **html**  \n> msg\n\n*nice*"
    check_reply_attrs(html, reply, html.content.formatted_body, "<i>nice</i>")

    reply = Text("nice").replying_to(other)
    assert reply.body == f"> <{plain.sender}> {Name.type}\n\nnice"
    assert other.type
    check_reply_attrs(other, reply, plain2html(other.type), "nice")
Exemple #14
0
async def test_multiclient_local_echo(e2e_room: Room, bob: Client):
    new1 = []
    cb1 = lambda room, event: new1.append(event)  # noqa
    e2e_room.client.rooms.callbacks[Text].append(cb1)

    new2 = []
    cb2 = lambda room, event: new2.append(event)  # noqa
    bob.rooms.callbacks[Text].append(cb2)

    await bob.rooms.join(e2e_room.id)
    await bob.sync.once()

    await e2e_room.timeline.send(Text("hi"), [e2e_room.client, bob])
    assert new1 == new2
Exemple #15
0
async def test_trust(alice: Client, e2e_room: Room, bob: Client):
    bob_dev = bob.devices.current
    await bob.rooms.join(e2e_room.id)
    await bob.sync.once()
    await bob.rooms[e2e_room.id].timeline.send(Text("makes alice get my key"))
    await alice.sync.once()

    assert alice.devices[bob.user_id][bob_dev.device_id].trusted is None
    await e2e_room.timeline.send(Text("unset"))
    await bob.sync.once()
    assert isinstance(bob.rooms[e2e_room.id].timeline[-1].content, Text)

    await alice.devices[bob.user_id][bob_dev.device_id].trust()
    assert alice.devices[bob.user_id][bob_dev.device_id].trusted is True
    await e2e_room.timeline.send(Text("trusted"))
    await bob.sync.once()
    assert isinstance(bob.rooms[e2e_room.id].timeline[-1].content, Text)

    await alice.devices[bob.user_id][bob_dev.device_id].block()
    assert alice.devices[bob.user_id][bob_dev.device_id].trusted is False
    await e2e_room.timeline.send(Text("blocked"))
    await bob.sync.once()
    assert isinstance(bob.rooms[e2e_room.id].timeline[-1].content, Megolm)
Exemple #16
0
async def test_timeline_event_callback_group(alice: Client, room: Room):
    cb_group = CallbackGroupTest()
    alice.rooms.callback_groups.append(cb_group)

    text  = Text("This is a test")
    emote = Emote("tests")

    await room.timeline.send(text, local_echo_to=[])
    await room.timeline.send(emote, local_echo_to=[])
    # We parse a corresponding timeline event on sync for new states
    await room.state.send(CanonicalAlias())
    await alice.sync.once()

    expected = [room, text, room, emote, room, CanonicalAlias()]
    assert cb_group.timeline_result == expected
Exemple #17
0
async def test_trust_megolm_validation(alice: Client, e2e_room, bob: Client):
    bob_dev = bob.devices.current
    await bob.rooms.join(e2e_room.id)
    await bob.sync.once()
    await bob.rooms[e2e_room.id].timeline.send(Text("makes alice get my key"))
    await alice.sync.once()

    assert alice.devices[bob.user_id][bob_dev.device_id].trusted is None
    await bob.rooms[e2e_room.id].timeline.send(Text("unset"))
    await alice.sync.once()
    error = e2e_room.timeline[-1].decryption.verification_errors[0]
    assert isinstance(error, MegolmFromUntrustedDevice)

    await alice.devices[bob.user_id][bob_dev.device_id].trust()
    await bob.rooms[e2e_room.id].timeline.send(Text("trusted"))
    await alice.sync.once()
    assert not e2e_room.timeline[-1].decryption.verification_errors

    await alice.devices[bob.user_id][bob_dev.device_id].block()
    await bob.rooms[e2e_room.id].timeline.send(Text("blocked"))
    await alice.sync.once()
    error = e2e_room.timeline[-1].decryption.verification_errors[0]
    assert isinstance(error, MegolmFromBlockedDevice)
    assert error.device is alice.devices[bob.user_id][bob_dev.device_id]
Exemple #18
0
async def test_session_forwarding_already_trusted_device(
    alice: Client, e2e_room: Room, tmp_path,
):
    await e2e_room.timeline.send(Text("sent before other devices exist"))

    alice2 = await new_device_from(alice, tmp_path / "unstrusted")
    await alice.sync.once()
    await alice.devices.own[alice2.device_id].trust()

    await alice2.sync.once()
    await alice2.rooms[e2e_room.id].timeline.load_history(1)
    await alice.sync.once()   # get session request
    await alice2.sync.once()  # get forwarded session

    event = alice2.rooms[e2e_room.id].timeline[-1]
    assert isinstance(event.content, Text)
Exemple #19
0
def test_textual_same_html_plaintext():
    text = Text.from_html("abc", plaintext="abc")
    assert text.body == "abc"
    assert text.format is None
    assert text.formatted_body is None
Exemple #20
0
def test_textual_from_html_manual_plaintext():
    text = Text.from_html("<p>abc</p>", plaintext="123")
    assert text.body == "123"
    assert text.format == HTML_FORMAT
    assert text.formatted_body == "<p>abc</p>"
Exemple #21
0
async def test_forwarding_chains(alice: Client, e2e_room: Room, tmp_path):
    await e2e_room.timeline.send(Text("sent before other devices exist"))

    # [no forward chain] alice/trusted → alice2

    alice2 = await new_device_from(alice, tmp_path / "alice2")

    await alice.sync.once()
    await alice.devices.own[alice2.device_id].trust()
    await alice2.devices.own[alice.device_id].trust()

    await alice2.sync.once()
    await alice2.rooms[e2e_room.id].timeline.load_history(1)
    await alice.sync.once()   # get session request
    await alice2.sync.once()  # get forwarded session

    event = alice2.rooms[e2e_room.id].timeline[-1]
    assert isinstance(event.content, Text) and event.decryption
    assert not event.decryption.forward_chain
    assert not event.decryption.verification_errors

    # [forward chain: alice/trusted →] alice2/trusted → alice3

    alice3 = await new_device_from(alice, tmp_path / "alice3")

    await alice2.sync.once()
    await alice2.devices.own[alice3.device_id].trust()
    await alice3.devices.own[alice.device_id].trust()
    await alice3.devices.own[alice2.device_id].trust()

    await alice3.sync.once()
    await alice3.rooms[e2e_room.id].timeline.load_history(1)
    await alice2.sync.once()  # get session request
    await alice3.sync.once()  # get forwarded session

    event = alice3.rooms[e2e_room.id].timeline[-1]
    assert isinstance(event.content, Text) and event.decryption
    assert event.decryption.forward_chain == [alice.devices.current]
    assert not event.decryption.verification_errors

    # [forward chain: alice/blocked →] alice2/trusted → alice3

    await alice3.devices.own[alice.device_id].block()
    event = await event.decryption.original._decrypted()
    assert isinstance(event.content, Text) and event.decryption
    assert event.decryption.forward_chain == [alice.devices.current]

    assert event.decryption.verification_errors == [
        err.MegolmFromBlockedDevice(alice.devices.current),
        err.MegolmBlockedDeviceInForwardChain(alice.devices.current),
    ]

    # [forward chain: alice/untrusted →] alice2/trusted → alice3

    alice3.devices.own[alice.device_id].trusted = None
    event = await event.decryption.original._decrypted()
    assert isinstance(event.content, Text) and event.decryption
    assert event.decryption.forward_chain == [alice.devices.current]

    assert event.decryption.verification_errors == [
        err.MegolmFromUntrustedDevice(alice.devices.current),
        err.MegolmUntrustedDeviceInForwardChain(alice.devices.current),
    ]
Exemple #22
0
async def test_push_rules_triggering(alice: Client, bob: Client, room: Room):
    await room.timeline.send(Text.from_html("<b>abc</b>, def"))
    await room.timeline.send(Text(f"...{alice.profile.name}..."))
    await alice.sync.once()

    assert isinstance(room.timeline[0].content, Creation)
    creation = room.timeline[0]
    abc = room.timeline[-2]
    mention = room.timeline[-1]

    # Individual conditions

    assert PushEventMatch({}, "content.format", "org.*.hTmL").triggered_by(abc)
    assert not PushEventMatch({}, "content.format", "html").triggered_by(abc)
    assert not PushEventMatch({}, "bad field", "blah").triggered_by(abc)

    assert PushEventMatch({}, "content.body", "abc").triggered_by(abc)
    assert PushEventMatch({}, "content.body", "Ab[cd]").triggered_by(abc)
    assert not PushEventMatch({}, "content.body", "ab").triggered_by(abc)

    assert PushContainsDisplayName({}).triggered_by(mention)
    assert not PushContainsDisplayName({}).triggered_by(abc)
    assert not PushContainsDisplayName({}).triggered_by(creation)

    ops = PushRoomMemberCount.Operator
    assert PushRoomMemberCount({}, 1).triggered_by(creation)
    assert PushRoomMemberCount({}, 2, ops.lt).triggered_by(abc)
    assert PushRoomMemberCount({}, 0, ops.gt).triggered_by(abc)
    assert PushRoomMemberCount({}, 1, ops.le).triggered_by(abc)
    assert PushRoomMemberCount({}, 1, ops.ge).triggered_by(abc)
    assert not PushRoomMemberCount({}, 1, ops.gt).triggered_by(abc)
    assert not PushRoomMemberCount({}, 1, ops.lt).triggered_by(abc)
    assert not PushRoomMemberCount({}, 2, ops.eq).triggered_by(abc)

    assert PushSenderNotificationPermission({}, "room").triggered_by(abc)
    assert PushSenderNotificationPermission({}, "unknown").triggered_by(abc)
    users = {alice.user_id: 49}
    await room.state.send(room.state.power_levels.but(users=users))
    await alice.sync.once()
    assert not PushSenderNotificationPermission({}, "room").triggered_by(abc)
    assert not PushSenderNotificationPermission({}, "xyz").triggered_by(abc)

    # PushRule

    kinds = PushRule.Kind
    assert PushRule("test").triggered_by(abc)
    assert not PushRule("test", enabled=False).triggered_by(abc)
    mention_1 = PushRule("test",
                         conditions=[
                             PushContainsDisplayName({}),
                             PushRoomMemberCount({}, 1),
                         ])
    assert mention_1.triggered_by(mention)
    assert not mention_1.triggered_by(abc)

    assert PushRule("c", kind=kinds.content, pattern="abc").triggered_by(abc)
    assert not PushRule("c", kind=kinds.content, pattern="a").triggered_by(abc)

    assert PushRule(room.id, kind=kinds.room).triggered_by(abc)
    assert not PushRule(room.id + "a", kind=kinds.room).triggered_by(abc)

    assert PushRule(alice.user_id, kind=kinds.sender).triggered_by(abc)
    assert not PushRule(bob.user_id, kind=kinds.sender).triggered_by(abc)

    # PushRuleset

    rule = alice.account_data.push_rules.main.triggered(abc)
    assert rule and rule.id == ".m.rule.message"
Exemple #23
0
async def test_session_export(alice: Client, e2e_room: Room, bob: Client):
    alice_ses = alice.e2e._in_group_sessions
    bob_ses   = bob.e2e._in_group_sessions

    # Alice won't auto-share her session to Bob since his device isn't trusted
    await e2e_room.timeline.send(Text("undecryptable to bob"))
    await bob.rooms.join(e2e_room.id)
    await bob.sync.once()
    await e2e_room.timeline.send(Text("make a session"))
    await alice.sync.once()
    assert not bob_ses
    assert len(alice_ses) == 1

    assert isinstance(bob.rooms[e2e_room.id].timeline[-2].content, Megolm)
    exported = await alice.e2e.export_sessions(passphrase="test")

    # 100% successful import and previous message decrypted as a result

    await bob.e2e.import_sessions(exported, "test")
    assert alice_ses.keys() == bob_ses.keys()
    assert isinstance(bob.rooms[e2e_room.id].timeline[-2].content, Text)

    for session1, sender_ed1, _, forward_chain1 in alice_ses.values():
        for session2, sender_ed2, _, forward_chain2 in bob_ses.values():
            export1 = session1.export_session(session1.first_known_index)
            export2 = session2.export_session(session2.first_known_index)

            assert export1 == export2
            assert sender_ed1 == sender_ed2
            assert forward_chain1 == forward_chain2

    # Total import failures

    with raises(err.SessionFileMissingHeader):
        await bob.e2e.import_sessions(exported[1:], "test")

    with raises(err.SessionFileMissingFooter):
        await bob.e2e.import_sessions(exported[:-1], "test")

    with raises(err.SessionFileInvalidBase64):
        bad = SESSION_FILE_HEADER + "abcdE" + SESSION_FILE_FOOTER
        await bob.e2e.import_sessions(bad, "test")

    with raises(err.SessionFileInvalidDataSize):
        bad = SESSION_FILE_HEADER + "abcd" + SESSION_FILE_FOOTER
        await bob.e2e.import_sessions(bad, "test")

    with raises(err.SessionFileUnsupportedVersion):
        base64   = "aaab" * err.SessionFileInvalidDataSize.minimum
        bad      = SESSION_FILE_HEADER + base64 + SESSION_FILE_FOOTER
        await bob.e2e.import_sessions(bad, "test")

    with raises(err.SessionFileInvalidHMAC):
        await bob.e2e.import_sessions(exported, "incorrect passphrase")

    with raises(err.SessionFileInvalidJSON):
        bad = await alice.e2e.export_sessions("test", lambda j: j + "break")
        await bob.e2e.import_sessions(bad, "test")

    with raises(err.SessionFileInvalidJSON):
        bad = await alice.e2e.export_sessions("test", lambda j: "{}")
        await bob.e2e.import_sessions(bad, "test")

    # Skipped session due to older/same version of it already being present

    current = list(bob_ses.values())[0][0]
    await bob.e2e.import_sessions(exported, "test")
    assert list(bob_ses.values())[0][0] == current

    # Skipped session due to unsupported algo

    def corrupt_session0_algo(json_data):
        data                 = json.loads(json_data)
        data[0]["algorithm"] = "123"
        return json.dumps(data)

    bad = await alice.e2e.export_sessions("test", corrupt_session0_algo)
    bob_ses.clear()
    await bob.e2e.import_sessions(bad, "test")
    assert not bob_ses

    # Skipped session due to general error, in this case a missing dict key

    def kill_session0_essential_key(json_data):
        data = json.loads(json_data)
        del data[0]["algorithm"]
        return json.dumps(data)

    bad = await alice.e2e.export_sessions("test", kill_session0_essential_key)
    bob_ses.clear()
    await bob.e2e.import_sessions(bad, "test")
    assert not bob_ses