atagen

macltok

public
Star0
Watch0
66db1b3cd8955da257726f2f3028b931ba34c3c0
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/state.rsRust371 lines12.4 KB
1
mod support;
2
3
use std::time::Duration;
4
5
use macltok::{
6
    ConnectionState, Engine, EngineConfig, EngineError, Event, PackUsage, Retain, RoomMembership,
7
    Termination, ThreadUnread,
8
};
9
use matrix_sdk::{
10
    ruma::{
11
        RoomId,
12
        api::client::sync::sync_events::v5,
13
        event_id,
14
        events::{
15
            AnySyncStateEvent, Mentions,
16
            receipt::{ReceiptThread, ReceiptType},
17
        },
18
        room_id,
19
        serde::Raw,
20
    },
21
    test_utils::mocks::MatrixMockServer,
22
};
23
use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
24
use serde_json::json;
25
use tokio::sync::watch;
26
27
#[tokio::test]
28
async fn rooms_changed_invalidates_room_summaries() {
29
    let server = MatrixMockServer::new().await;
30
    server
31
        .mock_sliding_sync()
32
        .ok(v5::Response::new("1".to_owned()))
33
        .mount()
34
        .await;
35
    let client = server.client_builder().build().await;
36
    let room_id = room_id!("!state:localhost");
37
    server.mock_room_state_encryption().plain().mount().await;
38
    server.sync_joined_room(&client, room_id).await;
39
    let config = EngineConfig::default().with_initial_history_limit(0);
40
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
41
    let running = tokio::spawn(engine.run());
42
43
    support::receive_until(&mut events, |event| matches!(event, Event::RoomsChanged)).await;
44
    let rooms = handle.rooms().await;
45
    let summary = rooms
46
        .iter()
47
        .find(|summary| summary.room_id == room_id)
48
        .expect("synced room was absent from the room summaries");
49
    assert_eq!(summary.membership, RoomMembership::Joined);
50
    assert!(!summary.name.is_empty());
51
52
    support::shut_down(&handle, &mut events, running).await;
53
}
54
55
#[tokio::test]
56
async fn connection_watch_tracks_the_engine_lifecycle() {
57
    let server = MatrixMockServer::new().await;
58
    server
59
        .mock_sliding_sync()
60
        .ok(v5::Response::new("1".to_owned()))
61
        .mount()
62
        .await;
63
    let client = server.client_builder().build().await;
64
    let config = EngineConfig::default().with_initial_history_limit(0);
65
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
66
    let mut connection = handle.connection_state();
67
    let connection_peer = connection.clone();
68
    assert_eq!(*connection.borrow(), ConnectionState::Idle);
69
    assert_eq!(*connection_peer.borrow(), ConnectionState::Idle);
70
71
    let running = tokio::spawn(engine.run());
72
    support::receive_until(&mut events, |event| {
73
        matches!(event, Event::ConnectionChanged(ConnectionState::Connecting))
74
    })
75
    .await;
76
    assert_ne!(*connection.borrow(), ConnectionState::Idle);
77
    support::receive_until(&mut events, |event| {
78
        matches!(event, Event::ConnectionChanged(ConnectionState::Online))
79
    })
80
    .await;
81
    wait_for_connection(&mut connection, ConnectionState::Online).await;
82
    assert_eq!(*connection_peer.borrow(), ConnectionState::Online);
83
84
    support::shut_down(&handle, &mut events, running).await;
85
    wait_for_connection(&mut connection, ConnectionState::Stopped).await;
86
    assert_eq!(*connection_peer.borrow(), ConnectionState::Stopped);
87
}
88
89
#[tokio::test]
90
async fn soft_logout_says_so_and_stops_the_engine() {
91
    let server = MatrixMockServer::new().await;
92
    server
93
        .mock_sliding_sync()
94
        .error_unknown_token(true)
95
        .mount()
96
        .await;
97
    let client = server.client_builder().build().await;
98
    let (engine, _handle, mut events) = Engine::from_client(
99
        client,
100
        EngineConfig::default().with_initial_history_limit(0),
101
    )
102
    .await
103
    .unwrap();
104
    let running = tokio::spawn(engine.run());
105
    support::receive_until(&mut events, |event| {
106
        matches!(
107
            event,
108
            Event::SessionAccessRejected {
109
                soft_logout: Some(_)
110
            }
111
        )
112
    })
113
    .await;
114
    support::receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
115
    let termination = running.await.unwrap().unwrap();
116
    assert!(matches!(termination, Termination::SoftLoggedOut { .. }));
117
    assert_eq!(termination.retain(), Retain::Store);
118
}
119
120
#[tokio::test]
121
async fn closing_the_event_stream_stops_an_idle_engine() {
122
    let server = MatrixMockServer::new().await;
123
    server
124
        .mock_sliding_sync()
125
        .ok(v5::Response::new("1".to_owned()))
126
        .mount()
127
        .await;
128
    let client = server.client_builder().build().await;
129
    let (engine, handle, mut events) = Engine::from_client(
130
        client,
131
        EngineConfig::default().with_initial_history_limit(0),
132
    )
133
    .await
134
    .unwrap();
135
    let running = tokio::spawn(engine.run());
136
    support::receive_until(&mut events, |event| {
137
        matches!(event, Event::ConnectionChanged(ConnectionState::Online))
138
    })
139
    .await;
140
    drop(events);
141
    let result = tokio::time::timeout(Duration::from_secs(2), running)
142
        .await
143
        .expect("engine did not stop after its event receiver closed")
144
        .unwrap();
145
    assert!(matches!(result, Err(EngineError::EventReceiverClosed)));
146
    drop(handle);
147
}
148
149
async fn wait_for_connection(
150
    connection: &mut watch::Receiver<ConnectionState>,
151
    expected: ConnectionState,
152
) {
153
    tokio::time::timeout(Duration::from_secs(2), async {
154
        loop {
155
            if *connection.borrow_and_update() == expected {
156
                return;
157
            }
158
            connection
159
                .changed()
160
                .await
161
                .expect("connection watch closed before the expected state");
162
        }
163
    })
164
    .await
165
    .expect("expected connection state did not arrive");
166
}
167
168
#[tokio::test]
169
async fn image_packs_follow_room_state_and_account_data() {
170
    let server = MatrixMockServer::new().await;
171
    server
172
        .mock_sliding_sync()
173
        .ok(v5::Response::new("1".to_owned()))
174
        .mount()
175
        .await;
176
    let client = server.client_builder().build().await;
177
    let room_id = room_id!("!packs:localhost");
178
    server.mock_room_state_encryption().plain().mount().await;
179
    server.sync_joined_room(&client, room_id).await;
180
    let config = EngineConfig::default().with_initial_history_limit(0);
181
    let (engine, handle, mut events) = Engine::from_client(client.clone(), config).await.unwrap();
182
    let running = tokio::spawn(engine.run());
183
184
    let pack: Raw<AnySyncStateEvent> = serde_json::from_value(json!({
185
        "type": "im.ponies.room_emotes",
186
        "state_key": "cats",
187
        "event_id": "$pack",
188
        "sender": "@example:localhost",
189
        "origin_server_ts": 1,
190
        "content": {
191
            "pack": { "display_name": "cats", "usage": ["emoticon"] },
192
            "images": {
193
                "blep": { "url": "mxc://localhost/blep" },
194
                "broken": { "url": "not an mxc" },
195
                "sticker": { "url": "mxc://localhost/sticker", "usage": ["sticker"] },
196
            },
197
        },
198
    }))
199
    .unwrap();
200
    server
201
        .mock_sync()
202
        .ok_and_run(&client, |builder| {
203
            builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event(pack));
204
            builder.add_custom_global_account_data(json!({
205
                "type": "im.ponies.emote_rooms",
206
                "content": { "rooms": { "!packs:localhost": { "cats": {} } } },
207
            }));
208
        })
209
        .await;
210
    support::receive_until(&mut events, |event| {
211
        matches!(event, Event::ImagePacksChanged)
212
    })
213
    .await;
214
215
    let packs = handle.image_packs().await.unwrap();
216
    let offered = packs.for_room(Some(room_id));
217
    assert_eq!(offered.len(), 1, "an enabled room's pack was offered twice");
218
    let pack = offered[0];
219
    assert_eq!(pack.id, "room:!packs:localhost:cats");
220
    assert_eq!(packs.global, vec![pack.clone()]);
221
    let images: Vec<_> = pack
222
        .images
223
        .iter()
224
        .map(|image| {
225
            (
226
                image.shortcode.as_str(),
227
                image.reaction_key.as_str(),
228
                image.usage.clone(),
229
            )
230
        })
231
        .collect();
232
    assert_eq!(
233
        images,
234
        [
235
            ("blep", "mxc://localhost/blep", vec![PackUsage::Emoticon]),
236
            (
237
                "sticker",
238
                "mxc://localhost/sticker",
239
                vec![PackUsage::Sticker]
240
            ),
241
        ]
242
    );
243
244
    support::shut_down(&handle, &mut events, running).await;
245
}
246
247
/// the sdk counts none of this once threads are on, and a threaded receipt never reaches
248
/// room info, so only our own recount notices it
249
#[tokio::test]
250
async fn thread_unreads_follow_threaded_receipts() {
251
    let server = MatrixMockServer::new().await;
252
    server
253
        .mock_sliding_sync()
254
        .ok(v5::Response::new("1".to_owned()))
255
        .mount()
256
        .await;
257
    let client = server.client_builder().build().await;
258
    let room_id = room_id!("!threads:localhost");
259
    server.mock_room_state_encryption().plain().mount().await;
260
    server.sync_joined_room(&client, room_id).await;
261
    client.event_cache().subscribe().unwrap();
262
    let config = EngineConfig::default().with_initial_history_limit(0);
263
    let (engine, handle, mut events) = Engine::from_client(client.clone(), config).await.unwrap();
264
    let running = tokio::spawn(engine.run());
265
    let own = client.user_id().unwrap().to_owned();
266
267
    let (root, first, second) = (
268
        event_id!("$root"),
269
        event_id!("$first"),
270
        event_id!("$second"),
271
    );
272
    let factory = EventFactory::new().room(room_id).sender(&ALICE);
273
    server
274
        .sync_room(
275
            &client,
276
            JoinedRoomBuilder::new(room_id)
277
                .add_timeline_event(factory.text_msg("root").event_id(root))
278
                .add_timeline_event(
279
                    factory
280
                        .text_msg("you there?")
281
                        .in_thread(root, root)
282
                        .mentions(Mentions::with_user_ids([own.clone()]))
283
                        .event_id(first),
284
                )
285
                .add_timeline_event(
286
                    factory
287
                        .text_msg("hello?")
288
                        .in_thread(root, first)
289
                        .event_id(second),
290
                ),
291
        )
292
        .await;
293
    let unread = thread_unread_once(&handle, &mut events, room_id, |unread| {
294
        unread.is_some_and(|unread| unread.messages == 2)
295
    })
296
    .await;
297
    assert_eq!(unread.mentions, 1);
298
    let summary = room(&handle, room_id).await;
299
    assert!(
300
        summary.unread_mentions >= 1,
301
        "the thread mention left the room's count"
302
    );
303
304
    server
305
        .sync_room(
306
            &client,
307
            JoinedRoomBuilder::new(room_id).add_receipt(
308
                factory
309
                    .read_receipts()
310
                    .add(
311
                        first,
312
                        &own,
313
                        ReceiptType::Read,
314
                        ReceiptThread::Thread(root.to_owned()),
315
                    )
316
                    .into_event(),
317
            ),
318
        )
319
        .await;
320
    let unread = thread_unread_once(&handle, &mut events, room_id, |unread| {
321
        unread.is_some_and(|unread| unread.messages == 1)
322
    })
323
    .await;
324
    assert_eq!(unread.mentions, 0);
325
326
    server
327
        .sync_room(
328
            &client,
329
            JoinedRoomBuilder::new(room_id).add_receipt(
330
                factory
331
                    .read_receipts()
332
                    .add(second, &own, ReceiptType::Read, ReceiptThread::Unthreaded)
333
                    .into_event(),
334
            ),
335
        )
336
        .await;
337
    thread_unread_once(&handle, &mut events, room_id, |unread| unread.is_none()).await;
338
339
    support::shut_down(&handle, &mut events, running).await;
340
}
341
342
async fn room(handle: &macltok::ClientHandle, room_id: &RoomId) -> macltok::RoomSummary {
343
    handle
344
        .rooms()
345
        .await
346
        .into_iter()
347
        .find(|summary| summary.room_id == room_id)
348
        .expect("synced room was absent from the room summaries")
349
}
350
351
/// waits out `RoomsChanged` until the root's counts satisfy `done`
352
async fn thread_unread_once(
353
    handle: &macltok::ClientHandle,
354
    events: &mut macltok::EventStream,
355
    room_id: &RoomId,
356
    done: impl Fn(Option<&ThreadUnread>) -> bool,
357
) -> ThreadUnread {
358
    let root = event_id!("$root");
359
    tokio::time::timeout(Duration::from_secs(10), async {
360
        loop {
361
            let summary = room(handle, room_id).await;
362
            let unread = summary.thread_unreads.get(root);
363
            if done(unread) {
364
                return unread.copied().unwrap_or_default();
365
            }
366
            support::receive_until(events, |event| matches!(event, Event::RoomsChanged)).await;
367
        }
368
    })
369
    .await
370
    .expect("thread unreads never settled")
371
}