atagen

macltok

public
Star0
Watch0
cbb715e119632f60c9b3ec1bc8803f1f33e39dce
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/mentions/service_tests.rs

cbb715e11963atagen2026-10-24 23:24 UTC1
use std::{
cbb715e11963atagen2026-10-24 23:24 UTC2
    sync::{
cbb715e11963atagen2026-10-24 23:24 UTC3
        Arc,
cbb715e11963atagen2026-10-24 23:24 UTC4
        atomic::{AtomicUsize, Ordering},
cbb715e11963atagen2026-10-24 23:24 UTC5
    },
cbb715e11963atagen2026-10-24 23:24 UTC6
    time::Duration,
cbb715e11963atagen2026-10-24 23:24 UTC7
};
cbb715e11963atagen2026-10-24 23:24 UTC8
cbb715e11963atagen2026-10-24 23:24 UTC9
use matrix_sdk::ruma::events::room::MediaSource;
cbb715e11963atagen2026-10-24 23:24 UTC10
use matrix_sdk::{
cbb715e11963atagen2026-10-24 23:24 UTC11
    ruma::{event_id, events::Mentions, push::Ruleset, room_id, user_id},
cbb715e11963atagen2026-10-24 23:24 UTC12
    test_utils::mocks::MatrixMockServer,
cbb715e11963atagen2026-10-24 23:24 UTC13
};
cbb715e11963atagen2026-10-24 23:24 UTC14
use matrix_sdk_test::{JoinedRoomBuilder, event_factory::EventFactory};
cbb715e11963atagen2026-10-24 23:24 UTC15
use rusqlite::Connection;
cbb715e11963atagen2026-10-24 23:24 UTC16
use serde_json::json;
cbb715e11963atagen2026-10-24 23:24 UTC17
use wiremock::{
cbb715e11963atagen2026-10-24 23:24 UTC18
    Mock, ResponseTemplate,
cbb715e11963atagen2026-10-24 23:24 UTC19
    matchers::{method, path, query_param, query_param_is_missing},
cbb715e11963atagen2026-10-24 23:24 UTC20
};
cbb715e11963atagen2026-10-24 23:24 UTC21
cbb715e11963atagen2026-10-24 23:24 UTC22
use super::super::store::{Account, MentionsStore};
cbb715e11963atagen2026-10-24 23:24 UTC23
use super::*;
cbb715e11963atagen2026-10-24 23:24 UTC24
use crate::{
cbb715e11963atagen2026-10-24 23:24 UTC25
    MediaId, MediaKind, StoreConfig, TimelineContent, TimelineItem,
cbb715e11963atagen2026-10-24 23:24 UTC26
    normalise::{MediaRegistration, MediaSourceInfo},
cbb715e11963atagen2026-10-24 23:24 UTC27
};
cbb715e11963atagen2026-10-24 23:24 UTC28
cbb715e11963atagen2026-10-24 23:24 UTC29
static STORES: AtomicUsize = AtomicUsize::new(0);
cbb715e11963atagen2026-10-24 23:24 UTC30
const MAX_BYTES: usize = 64 * 1024 * 1024;
cbb715e11963atagen2026-10-24 23:24 UTC31
cbb715e11963atagen2026-10-24 23:24 UTC32
fn config() -> StoreConfig {
cbb715e11963atagen2026-10-24 23:24 UTC33
    let number = STORES.fetch_add(1, Ordering::Relaxed);
cbb715e11963atagen2026-10-24 23:24 UTC34
    StoreConfig::sqlite(
cbb715e11963atagen2026-10-24 23:24 UTC35
        std::env::temp_dir().join(format!(
cbb715e11963atagen2026-10-24 23:24 UTC36
            "macltok-mentions-service-{}-{number}",
cbb715e11963atagen2026-10-24 23:24 UTC37
            std::process::id()
cbb715e11963atagen2026-10-24 23:24 UTC38
        )),
cbb715e11963atagen2026-10-24 23:24 UTC39
        "service commit test passphrase",
cbb715e11963atagen2026-10-24 23:24 UTC40
    )
cbb715e11963atagen2026-10-24 23:24 UTC41
}
cbb715e11963atagen2026-10-24 23:24 UTC42
cbb715e11963atagen2026-10-24 23:24 UTC43
fn account(client: &Client) -> Account {
cbb715e11963atagen2026-10-24 23:24 UTC44
    Account {
cbb715e11963atagen2026-10-24 23:24 UTC45
        homeserver: client.homeserver().to_string(),
cbb715e11963atagen2026-10-24 23:24 UTC46
        user_id: client.user_id().unwrap().to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC47
    }
cbb715e11963atagen2026-10-24 23:24 UTC48
}
cbb715e11963atagen2026-10-24 23:24 UTC49
cbb715e11963atagen2026-10-24 23:24 UTC50
async fn joined_client(
cbb715e11963atagen2026-10-24 23:24 UTC51
    server: &MatrixMockServer,
cbb715e11963atagen2026-10-24 23:24 UTC52
    room_id: &ruma::RoomId,
cbb715e11963atagen2026-10-24 23:24 UTC53
) -> (Client, ruma::OwnedUserId) {
cbb715e11963atagen2026-10-24 23:24 UTC54
    let client = server.client_builder().build().await;
cbb715e11963atagen2026-10-24 23:24 UTC55
    let own_user = client.user_id().unwrap().to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC56
    let room_id = room_id.to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC57
    server
cbb715e11963atagen2026-10-24 23:24 UTC58
        .mock_sync()
cbb715e11963atagen2026-10-24 23:24 UTC59
        .ok_and_run(&client, {
cbb715e11963atagen2026-10-24 23:24 UTC60
            let own_user = own_user.clone();
cbb715e11963atagen2026-10-24 23:24 UTC61
            move |builder| {
cbb715e11963atagen2026-10-24 23:24 UTC62
                let factory = EventFactory::new().room(&room_id);
cbb715e11963atagen2026-10-24 23:24 UTC63
                builder.add_global_account_data(
cbb715e11963atagen2026-10-24 23:24 UTC64
                    factory.push_rules(Ruleset::server_default(&own_user)),
cbb715e11963atagen2026-10-24 23:24 UTC65
                );
cbb715e11963atagen2026-10-24 23:24 UTC66
                builder.add_joined_room(
cbb715e11963atagen2026-10-24 23:24 UTC67
                    JoinedRoomBuilder::new(&room_id).add_state_event(factory.member(&own_user)),
cbb715e11963atagen2026-10-24 23:24 UTC68
                );
cbb715e11963atagen2026-10-24 23:24 UTC69
            }
cbb715e11963atagen2026-10-24 23:24 UTC70
        })
cbb715e11963atagen2026-10-24 23:24 UTC71
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC72
    (client, own_user)
cbb715e11963atagen2026-10-24 23:24 UTC73
}
cbb715e11963atagen2026-10-24 23:24 UTC74
cbb715e11963atagen2026-10-24 23:24 UTC75
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC76
async fn failed_batch_keeps_inbox_and_checkpoint_at_the_same_commit_boundary() {
cbb715e11963atagen2026-10-24 23:24 UTC77
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC78
    let room_id = room_id!("!mentions:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC79
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC80
    let event = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC81
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC82
        .text_msg("commit boundary")
cbb715e11963atagen2026-10-24 23:24 UTC83
        .mentions(Mentions::with_user_ids([own_user.clone()]))
cbb715e11963atagen2026-10-24 23:24 UTC84
        .event_id(event_id!("$commit-boundary:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC85
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC86
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC87
    let raw_room = serde_json::from_value(json!({
cbb715e11963atagen2026-10-24 23:24 UTC88
        "initial": true,
cbb715e11963atagen2026-10-24 23:24 UTC89
        "timeline": [serde_json::to_value(event).unwrap()],
cbb715e11963atagen2026-10-24 23:24 UTC90
    }))
cbb715e11963atagen2026-10-24 23:24 UTC91
    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC92
    let batch = ResponseCommitBatch {
cbb715e11963atagen2026-10-24 23:24 UTC93
        connection_id: "mentions-connection".to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC94
        previous_pos: None,
cbb715e11963atagen2026-10-24 23:24 UTC95
        next_pos: "mentions-position-1".to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC96
        response: Default::default(),
cbb715e11963atagen2026-10-24 23:24 UTC97
        raw_rooms: [(room_id.to_owned(), raw_room)].into(),
cbb715e11963atagen2026-10-24 23:24 UTC98
    };
cbb715e11963atagen2026-10-24 23:24 UTC99
    let config = config();
cbb715e11963atagen2026-10-24 23:24 UTC100
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC101
        .await
cbb715e11963atagen2026-10-24 23:24 UTC102
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC103
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC104
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC105
        EngineConfig::default().max_media_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC106
        EngineConfig::default().max_media_sources,
cbb715e11963atagen2026-10-24 23:24 UTC107
    ));
cbb715e11963atagen2026-10-24 23:24 UTC108
    let (_handle, mut service, updates) =
cbb715e11963atagen2026-10-24 23:24 UTC109
        Service::new(client, media, Some(store), EngineConfig::default())
cbb715e11963atagen2026-10-24 23:24 UTC110
            .await
cbb715e11963atagen2026-10-24 23:24 UTC111
            .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC112
cbb715e11963atagen2026-10-24 23:24 UTC113
    let connection = Connection::open(config.mentions_location()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC114
    connection
cbb715e11963atagen2026-10-24 23:24 UTC115
        .execute_batch(
cbb715e11963atagen2026-10-24 23:24 UTC116
            "CREATE TRIGGER fail_mentions_inbox_insert BEFORE INSERT ON records WHEN NEW.key NOT IN ('!cipher', '!head') BEGIN SELECT RAISE(ABORT, 'injected inbox write failure'); END;
cbb715e11963atagen2026-10-24 23:24 UTC117
             CREATE TRIGGER fail_mentions_inbox_update BEFORE UPDATE ON records WHEN OLD.key NOT IN ('!cipher', '!head') BEGIN SELECT RAISE(ABORT, 'injected inbox write failure'); END;",
cbb715e11963atagen2026-10-24 23:24 UTC118
        )
cbb715e11963atagen2026-10-24 23:24 UTC119
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC120
cbb715e11963atagen2026-10-24 23:24 UTC121
    assert!(service.batch(batch.clone()).await.is_err());
cbb715e11963atagen2026-10-24 23:24 UTC122
    assert_eq!(service.data.revision, 0);
cbb715e11963atagen2026-10-24 23:24 UTC123
    assert!(service.data.entries.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC124
    assert!(service.data.checkpoints.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC125
    assert_eq!(updates.borrow().revision, 0);
cbb715e11963atagen2026-10-24 23:24 UTC126
    assert!(updates.borrow().entries.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC127
cbb715e11963atagen2026-10-24 23:24 UTC128
    let reopened = MentionsStore::open(&config, account(&service.client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC129
        .await
cbb715e11963atagen2026-10-24 23:24 UTC130
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC131
    assert_eq!(reopened.revision(), 0);
cbb715e11963atagen2026-10-24 23:24 UTC132
    assert!(reopened.read::<Data>("inbox").unwrap().is_none());
cbb715e11963atagen2026-10-24 23:24 UTC133
    drop(reopened);
cbb715e11963atagen2026-10-24 23:24 UTC134
cbb715e11963atagen2026-10-24 23:24 UTC135
    connection
cbb715e11963atagen2026-10-24 23:24 UTC136
        .execute_batch(
cbb715e11963atagen2026-10-24 23:24 UTC137
            "DROP TRIGGER fail_mentions_inbox_insert;
cbb715e11963atagen2026-10-24 23:24 UTC138
             DROP TRIGGER fail_mentions_inbox_update;",
cbb715e11963atagen2026-10-24 23:24 UTC139
        )
cbb715e11963atagen2026-10-24 23:24 UTC140
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC141
    drop(connection);
cbb715e11963atagen2026-10-24 23:24 UTC142
cbb715e11963atagen2026-10-24 23:24 UTC143
    service.batch(batch.clone()).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC144
    assert_eq!(service.data.revision, 1);
cbb715e11963atagen2026-10-24 23:24 UTC145
    assert_eq!(service.data.entries.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC146
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC147
        service.data.checkpoints.get("mentions-connection"),
cbb715e11963atagen2026-10-24 23:24 UTC148
        Some(&batch.next_pos)
cbb715e11963atagen2026-10-24 23:24 UTC149
    );
cbb715e11963atagen2026-10-24 23:24 UTC150
    assert_eq!(updates.borrow().revision, 1);
cbb715e11963atagen2026-10-24 23:24 UTC151
    assert_eq!(updates.borrow().entries.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC152
cbb715e11963atagen2026-10-24 23:24 UTC153
    let reopened = MentionsStore::open(&config, account(&service.client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC154
        .await
cbb715e11963atagen2026-10-24 23:24 UTC155
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC156
    let persisted: Data = reopened.read("inbox").unwrap().unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC157
    assert_eq!(reopened.revision(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC158
    assert_eq!(persisted.revision, 1);
cbb715e11963atagen2026-10-24 23:24 UTC159
    assert_eq!(persisted.entries.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC160
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC161
        persisted.checkpoints.get("mentions-connection"),
cbb715e11963atagen2026-10-24 23:24 UTC162
        Some(&batch.next_pos)
cbb715e11963atagen2026-10-24 23:24 UTC163
    );
cbb715e11963atagen2026-10-24 23:24 UTC164
    drop(reopened);
cbb715e11963atagen2026-10-24 23:24 UTC165
cbb715e11963atagen2026-10-24 23:24 UTC166
    service.batch(batch).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC167
    assert_eq!(service.data.revision, 1);
cbb715e11963atagen2026-10-24 23:24 UTC168
    assert_eq!(service.store.as_ref().unwrap().revision(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC169
cbb715e11963atagen2026-10-24 23:24 UTC170
    drop(service);
cbb715e11963atagen2026-10-24 23:24 UTC171
    config.delete().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC172
}
cbb715e11963atagen2026-10-24 23:24 UTC173
cbb715e11963atagen2026-10-24 23:24 UTC174
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC175
async fn service_recovers_notification_history_into_durable_unread_inbox() {
cbb715e11963atagen2026-10-24 23:24 UTC176
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC177
    let room_id = room_id!("!mentions:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC178
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC179
    let first = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC180
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC181
        .text_msg("history mention one")
cbb715e11963atagen2026-10-24 23:24 UTC182
        .mentions(Mentions::with_user_ids([own_user.clone()]))
cbb715e11963atagen2026-10-24 23:24 UTC183
        .event_id(event_id!("$history-one:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC184
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC185
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC186
    let second = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC187
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC188
        .text_msg("history mention two")
cbb715e11963atagen2026-10-24 23:24 UTC189
        .mentions(Mentions::with_user_ids([own_user]))
cbb715e11963atagen2026-10-24 23:24 UTC190
        .event_id(event_id!("$history-two:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC191
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC192
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC193
    let first = serde_json::from_str::<serde_json::Value>(first.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC194
    let second = serde_json::from_str::<serde_json::Value>(second.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC195
    Mock::given(method("GET"))
cbb715e11963atagen2026-10-24 23:24 UTC196
        .and(path("/_matrix/client/v3/notifications"))
cbb715e11963atagen2026-10-24 23:24 UTC197
        .and(query_param_is_missing("from"))
cbb715e11963atagen2026-10-24 23:24 UTC198
        .and(query_param("limit", "50"))
cbb715e11963atagen2026-10-24 23:24 UTC199
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
cbb715e11963atagen2026-10-24 23:24 UTC200
            "notifications": [{
cbb715e11963atagen2026-10-24 23:24 UTC201
                "actions": ["notify"],
cbb715e11963atagen2026-10-24 23:24 UTC202
                "event": first,
cbb715e11963atagen2026-10-24 23:24 UTC203
                "read": true,
cbb715e11963atagen2026-10-24 23:24 UTC204
                "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC205
                "ts": 1,
cbb715e11963atagen2026-10-24 23:24 UTC206
            }],
cbb715e11963atagen2026-10-24 23:24 UTC207
            "next_token": "history-page-two",
cbb715e11963atagen2026-10-24 23:24 UTC208
        })))
cbb715e11963atagen2026-10-24 23:24 UTC209
        .mount(server.server())
cbb715e11963atagen2026-10-24 23:24 UTC210
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC211
    Mock::given(method("GET"))
cbb715e11963atagen2026-10-24 23:24 UTC212
        .and(path("/_matrix/client/v3/notifications"))
cbb715e11963atagen2026-10-24 23:24 UTC213
        .and(query_param("from", "history-page-two"))
cbb715e11963atagen2026-10-24 23:24 UTC214
        .and(query_param("limit", "50"))
cbb715e11963atagen2026-10-24 23:24 UTC215
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
cbb715e11963atagen2026-10-24 23:24 UTC216
            "notifications": [{
cbb715e11963atagen2026-10-24 23:24 UTC217
                "actions": ["notify"],
cbb715e11963atagen2026-10-24 23:24 UTC218
                "event": second,
cbb715e11963atagen2026-10-24 23:24 UTC219
                "read": true,
cbb715e11963atagen2026-10-24 23:24 UTC220
                "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC221
                "ts": 2,
cbb715e11963atagen2026-10-24 23:24 UTC222
            }],
cbb715e11963atagen2026-10-24 23:24 UTC223
        })))
cbb715e11963atagen2026-10-24 23:24 UTC224
        .mount(server.server())
cbb715e11963atagen2026-10-24 23:24 UTC225
        .await;
cbb715e11963atagen2026-10-24 23:24 UTC226
cbb715e11963atagen2026-10-24 23:24 UTC227
    let config = config();
cbb715e11963atagen2026-10-24 23:24 UTC228
    let store = MentionsStore::open(&config, account(&client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC229
        .await
cbb715e11963atagen2026-10-24 23:24 UTC230
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC231
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC232
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC233
        EngineConfig::default().max_media_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC234
        EngineConfig::default().max_media_sources,
cbb715e11963atagen2026-10-24 23:24 UTC235
    ));
cbb715e11963atagen2026-10-24 23:24 UTC236
    let (handle, service, mut updates) =
cbb715e11963atagen2026-10-24 23:24 UTC237
        Service::new(client.clone(), media, Some(store), EngineConfig::default())
cbb715e11963atagen2026-10-24 23:24 UTC238
            .await
cbb715e11963atagen2026-10-24 23:24 UTC239
            .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC240
    let service_task = tokio::spawn(service.run());
cbb715e11963atagen2026-10-24 23:24 UTC241
cbb715e11963atagen2026-10-24 23:24 UTC242
    let discovered = tokio::time::timeout(Duration::from_secs(10), async {
cbb715e11963atagen2026-10-24 23:24 UTC243
        loop {
cbb715e11963atagen2026-10-24 23:24 UTC244
            let page = updates.borrow_and_update().clone();
cbb715e11963atagen2026-10-24 23:24 UTC245
            if page.entries.len() == 2 && !page.coverage.recovering {
cbb715e11963atagen2026-10-24 23:24 UTC246
                break page;
cbb715e11963atagen2026-10-24 23:24 UTC247
            }
cbb715e11963atagen2026-10-24 23:24 UTC248
            updates.changed().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC249
        }
cbb715e11963atagen2026-10-24 23:24 UTC250
    })
cbb715e11963atagen2026-10-24 23:24 UTC251
    .await;
cbb715e11963atagen2026-10-24 23:24 UTC252
    handle.stop().await;
cbb715e11963atagen2026-10-24 23:24 UTC253
    service_task.await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC254
    let discovered = discovered.expect("notification history should be recovered");
cbb715e11963atagen2026-10-24 23:24 UTC255
    assert_eq!(discovered.entries.len(), 2);
cbb715e11963atagen2026-10-24 23:24 UTC256
    assert!(discovered.entries.iter().all(|entry| !entry.read));
cbb715e11963atagen2026-10-24 23:24 UTC257
cbb715e11963atagen2026-10-24 23:24 UTC258
    let reopened = MentionsStore::open(&config, account(&client), MAX_BYTES)
cbb715e11963atagen2026-10-24 23:24 UTC259
        .await
cbb715e11963atagen2026-10-24 23:24 UTC260
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC261
    let persisted: Data = reopened.read("inbox").unwrap().unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC262
    assert_eq!(reopened.revision(), persisted.revision);
cbb715e11963atagen2026-10-24 23:24 UTC263
    assert_eq!(persisted.entries.len(), 2);
cbb715e11963atagen2026-10-24 23:24 UTC264
    assert!(persisted.entries.values().all(|entry| !entry.read));
cbb715e11963atagen2026-10-24 23:24 UTC265
    assert!(persisted.latest_cursor.is_none());
cbb715e11963atagen2026-10-24 23:24 UTC266
    assert!(persisted.history_cursor.is_none());
cbb715e11963atagen2026-10-24 23:24 UTC267
    drop(reopened);
cbb715e11963atagen2026-10-24 23:24 UTC268
cbb715e11963atagen2026-10-24 23:24 UTC269
    config.delete().await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC270
}
cbb715e11963atagen2026-10-24 23:24 UTC271
cbb715e11963atagen2026-10-24 23:24 UTC272
fn message_body(item: &TimelineItem) -> Option<&str> {
cbb715e11963atagen2026-10-24 23:24 UTC273
    match item {
cbb715e11963atagen2026-10-24 23:24 UTC274
        TimelineItem::Event(event) => match &event.content {
cbb715e11963atagen2026-10-24 23:24 UTC275
            TimelineContent::Message(message) => Some(&message.body),
cbb715e11963atagen2026-10-24 23:24 UTC276
            _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC277
        },
cbb715e11963atagen2026-10-24 23:24 UTC278
        _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC279
    }
cbb715e11963atagen2026-10-24 23:24 UTC280
}
cbb715e11963atagen2026-10-24 23:24 UTC281
cbb715e11963atagen2026-10-24 23:24 UTC282
fn media_registration() -> MediaRegistration {
cbb715e11963atagen2026-10-24 23:24 UTC283
    MediaRegistration {
cbb715e11963atagen2026-10-24 23:24 UTC284
        id: MediaId::new("test-scope#image".to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC285
        kind: MediaKind::Image,
cbb715e11963atagen2026-10-24 23:24 UTC286
        full: MediaSourceInfo {
cbb715e11963atagen2026-10-24 23:24 UTC287
            source: MediaSource::Plain(ruma::mxc_uri!("mxc://mentions.example/image").to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC288
            mimetype: Some("image/png".to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC289
            declared_size: Some(1234),
cbb715e11963atagen2026-10-24 23:24 UTC290
        },
cbb715e11963atagen2026-10-24 23:24 UTC291
        thumbnail: None,
cbb715e11963atagen2026-10-24 23:24 UTC292
    }
cbb715e11963atagen2026-10-24 23:24 UTC293
}
cbb715e11963atagen2026-10-24 23:24 UTC294
cbb715e11963atagen2026-10-24 23:24 UTC295
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC296
async fn live_context_survives_capture_then_edit_and_redaction() {
cbb715e11963atagen2026-10-24 23:24 UTC297
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC298
    let room_id = room_id!("!mentions:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC299
    let (client, own_user) = joined_client(&server, room_id).await;
cbb715e11963atagen2026-10-24 23:24 UTC300
    let mention = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC301
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC302
        .text_msg("direct mention")
cbb715e11963atagen2026-10-24 23:24 UTC303
        .mentions(Mentions::with_user_ids([own_user]))
cbb715e11963atagen2026-10-24 23:24 UTC304
        .event_id(event_id!("$live-mention:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC305
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC306
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC307
    let mut mention: serde_json::Value = serde_json::from_str(mention.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC308
    mention["origin_server_ts"] = json!(10);
cbb715e11963atagen2026-10-24 23:24 UTC309
    let following = EventFactory::new()
cbb715e11963atagen2026-10-24 23:24 UTC310
        .room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC311
        .text_msg("following")
cbb715e11963atagen2026-10-24 23:24 UTC312
        .event_id(event_id!("$live-following:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC313
        .sender(user_id!("@bob:localhost"))
cbb715e11963atagen2026-10-24 23:24 UTC314
        .into_raw_sync();
cbb715e11963atagen2026-10-24 23:24 UTC315
    let mut following: serde_json::Value = serde_json::from_str(following.json().get()).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC316
    following["origin_server_ts"] = json!(11);
cbb715e11963atagen2026-10-24 23:24 UTC317
cbb715e11963atagen2026-10-24 23:24 UTC318
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC319
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC320
        EngineConfig::default().max_media_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC321
        EngineConfig::default().max_media_sources,
cbb715e11963atagen2026-10-24 23:24 UTC322
    ));
cbb715e11963atagen2026-10-24 23:24 UTC323
    let (_handle, mut service, _updates) =
cbb715e11963atagen2026-10-24 23:24 UTC324
        Service::new(client, media, None, EngineConfig::default())
cbb715e11963atagen2026-10-24 23:24 UTC325
            .await
cbb715e11963atagen2026-10-24 23:24 UTC326
            .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC327
    let room_id = room_id.to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC328
    let mention_key = MentionKey {
cbb715e11963atagen2026-10-24 23:24 UTC329
        room_id: room_id.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC330
        event_id: event_id!("$live-mention:localhost").to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC331
    };
cbb715e11963atagen2026-10-24 23:24 UTC332
    let following_id = format!("{room_id}\n$live-following:localhost");
cbb715e11963atagen2026-10-24 23:24 UTC333
    let mut data = Data::default();
cbb715e11963atagen2026-10-24 23:24 UTC334
cbb715e11963atagen2026-10-24 23:24 UTC335
    service.observe(&mut data, &room_id, mention).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC336
    service
cbb715e11963atagen2026-10-24 23:24 UTC337
        .observe(&mut data, &room_id, following)
cbb715e11963atagen2026-10-24 23:24 UTC338
        .await
cbb715e11963atagen2026-10-24 23:24 UTC339
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC340
cbb715e11963atagen2026-10-24 23:24 UTC341
    let mention_identity = id(&mention_key);
cbb715e11963atagen2026-10-24 23:24 UTC342
    let entry = data.entries.get(&mention_identity).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC343
    let revision = entry.revision;
cbb715e11963atagen2026-10-24 23:24 UTC344
    let target = entry.event.clone();
cbb715e11963atagen2026-10-24 23:24 UTC345
    service.captured(
cbb715e11963atagen2026-10-24 23:24 UTC346
        &mut data,
cbb715e11963atagen2026-10-24 23:24 UTC347
        mention_identity.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC348
        revision,
cbb715e11963atagen2026-10-24 23:24 UTC349
        Ok((
cbb715e11963atagen2026-10-24 23:24 UTC350
            crate::EventContext {
cbb715e11963atagen2026-10-24 23:24 UTC351
                events_before: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC352
                event: Some(target),
cbb715e11963atagen2026-10-24 23:24 UTC353
                events_after: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC354
                start_token: None,
cbb715e11963atagen2026-10-24 23:24 UTC355
                end_token: None,
cbb715e11963atagen2026-10-24 23:24 UTC356
            },
cbb715e11963atagen2026-10-24 23:24 UTC357
            Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC358
            Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC359
        )),
cbb715e11963atagen2026-10-24 23:24 UTC360
    );
cbb715e11963atagen2026-10-24 23:24 UTC361
cbb715e11963atagen2026-10-24 23:24 UTC362
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC363
        data.entries[&mention_identity].capture,
cbb715e11963atagen2026-10-24 23:24 UTC364
        MentionCaptureState::Partial
cbb715e11963atagen2026-10-24 23:24 UTC365
    );
cbb715e11963atagen2026-10-24 23:24 UTC366
    assert!(data.events.contains_key(&following_id));
cbb715e11963atagen2026-10-24 23:24 UTC367
    let (saved, _) = data.context(&mention_key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC368
    let context = saved.context.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC369
    assert_eq!(context.events_after.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC370
    assert_eq!(message_body(&context.events_after[0]), Some("following"));
cbb715e11963atagen2026-10-24 23:24 UTC371
cbb715e11963atagen2026-10-24 23:24 UTC372
    let edit = json!({
cbb715e11963atagen2026-10-24 23:24 UTC373
        "content": {
cbb715e11963atagen2026-10-24 23:24 UTC374
            "body": "* edited following",
cbb715e11963atagen2026-10-24 23:24 UTC375
            "m.new_content": {"body": "edited following", "msgtype": "m.text"},
cbb715e11963atagen2026-10-24 23:24 UTC376
            "m.relates_to": {"event_id": "$live-following:localhost", "rel_type": "m.replace"},
cbb715e11963atagen2026-10-24 23:24 UTC377
            "msgtype": "m.text"
cbb715e11963atagen2026-10-24 23:24 UTC378
        },
cbb715e11963atagen2026-10-24 23:24 UTC379
        "event_id": "$live-following-edit:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC380
        "origin_server_ts": 12,
cbb715e11963atagen2026-10-24 23:24 UTC381
        "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC382
        "sender": "@bob:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC383
        "type": "m.room.message"
cbb715e11963atagen2026-10-24 23:24 UTC384
    });
cbb715e11963atagen2026-10-24 23:24 UTC385
    service.observe(&mut data, &room_id, edit).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC386
    let (saved, _) = data.context(&mention_key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC387
    let context = saved.context.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC388
    assert_eq!(
cbb715e11963atagen2026-10-24 23:24 UTC389
        message_body(&context.events_after[0]),
cbb715e11963atagen2026-10-24 23:24 UTC390
        Some("edited following")
cbb715e11963atagen2026-10-24 23:24 UTC391
    );
cbb715e11963atagen2026-10-24 23:24 UTC392
cbb715e11963atagen2026-10-24 23:24 UTC393
    let saved_following = data.events.get_mut(&following_id).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC394
    assert!(saved_following.raw.is_some());
cbb715e11963atagen2026-10-24 23:24 UTC395
    saved_following.media.push(media_registration());
cbb715e11963atagen2026-10-24 23:24 UTC396
    assert_eq!(saved_following.media.len(), 1);
cbb715e11963atagen2026-10-24 23:24 UTC397
    let redaction = json!({
cbb715e11963atagen2026-10-24 23:24 UTC398
        "content": {},
cbb715e11963atagen2026-10-24 23:24 UTC399
        "event_id": "$live-following-redaction:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC400
        "origin_server_ts": 13,
cbb715e11963atagen2026-10-24 23:24 UTC401
        "redacts": "$live-following:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC402
        "room_id": room_id.as_str(),
cbb715e11963atagen2026-10-24 23:24 UTC403
        "sender": "@bob:localhost",
cbb715e11963atagen2026-10-24 23:24 UTC404
        "type": "m.room.redaction"
cbb715e11963atagen2026-10-24 23:24 UTC405
    });
cbb715e11963atagen2026-10-24 23:24 UTC406
    service
cbb715e11963atagen2026-10-24 23:24 UTC407
        .observe(&mut data, &room_id, redaction)
cbb715e11963atagen2026-10-24 23:24 UTC408
        .await
cbb715e11963atagen2026-10-24 23:24 UTC409
        .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC410
cbb715e11963atagen2026-10-24 23:24 UTC411
    let saved_following = data.events.get(&following_id).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC412
    assert!(matches!(
cbb715e11963atagen2026-10-24 23:24 UTC413
        &saved_following.item,
cbb715e11963atagen2026-10-24 23:24 UTC414
        TimelineItem::Event(event) if event.content == TimelineContent::Redacted
cbb715e11963atagen2026-10-24 23:24 UTC415
    ));
cbb715e11963atagen2026-10-24 23:24 UTC416
    assert!(saved_following.raw.is_none());
cbb715e11963atagen2026-10-24 23:24 UTC417
    assert!(saved_following.media.is_empty());
cbb715e11963atagen2026-10-24 23:24 UTC418
    let (saved, _) = data.context(&mention_key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC419
    let context = saved.context.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC420
    assert!(matches!(
cbb715e11963atagen2026-10-24 23:24 UTC421
        &context.events_after[0],
cbb715e11963atagen2026-10-24 23:24 UTC422
        TimelineItem::Event(event) if event.content == TimelineContent::Redacted
cbb715e11963atagen2026-10-24 23:24 UTC423
    ));
cbb715e11963atagen2026-10-24 23:24 UTC424
    data.windows.remove(&mention_identity);
cbb715e11963atagen2026-10-24 23:24 UTC425
    service.data = data;
cbb715e11963atagen2026-10-24 23:24 UTC426
    let CommandOutput::MentionContext(context) = service
cbb715e11963atagen2026-10-24 23:24 UTC427
        .command(MentionsCommand::RetryContext(mention_key))
cbb715e11963atagen2026-10-24 23:24 UTC428
        .await
cbb715e11963atagen2026-10-24 23:24 UTC429
        .unwrap()
cbb715e11963atagen2026-10-24 23:24 UTC430
    else {
cbb715e11963atagen2026-10-24 23:24 UTC431
        panic!("retry should return saved context");
cbb715e11963atagen2026-10-24 23:24 UTC432
    };
cbb715e11963atagen2026-10-24 23:24 UTC433
    assert_eq!(context.state, MentionCaptureState::Pending);
cbb715e11963atagen2026-10-24 23:24 UTC434
    assert!(context.context.unwrap().event.is_some());
cbb715e11963atagen2026-10-24 23:24 UTC435
}
cbb715e11963atagen2026-10-24 23:24 UTC436
cbb715e11963atagen2026-10-24 23:24 UTC437
#[tokio::test]
cbb715e11963atagen2026-10-24 23:24 UTC438
async fn explicit_recovery_rechecks_server_support() {
cbb715e11963atagen2026-10-24 23:24 UTC439
    let server = MatrixMockServer::new().await;
cbb715e11963atagen2026-10-24 23:24 UTC440
    let (client, _) = joined_client(&server, room_id!("!mentions:localhost")).await;
cbb715e11963atagen2026-10-24 23:24 UTC441
    let config = EngineConfig::default();
cbb715e11963atagen2026-10-24 23:24 UTC442
    let media = Arc::new(MediaService::new(
cbb715e11963atagen2026-10-24 23:24 UTC443
        client.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC444
        config.max_media_bytes,
cbb715e11963atagen2026-10-24 23:24 UTC445
        config.max_media_sources,
cbb715e11963atagen2026-10-24 23:24 UTC446
    ));
cbb715e11963atagen2026-10-24 23:24 UTC447
    let (_, mut service, _) = Service::new(client, media, None, config).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC448
    service.data.coverage.historical_supported = Some(false);
cbb715e11963atagen2026-10-24 23:24 UTC449
    service.recovery_budget = 0;
cbb715e11963atagen2026-10-24 23:24 UTC450
    service.command(MentionsCommand::Recover).await.unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC451
    assert_eq!(service.data.coverage.historical_supported, None);
cbb715e11963atagen2026-10-24 23:24 UTC452
    assert!(service.data.coverage.recovering);
cbb715e11963atagen2026-10-24 23:24 UTC453
    assert_eq!(service.recovery_budget, 20);
cbb715e11963atagen2026-10-24 23:24 UTC454
}