atagen

macltok

public
Star0
Watch0
fa9114fcf75f7bdf1a7cec3b3367d604cf7de472
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
tests/media.rsRust992 lines29.9 KB
1
use std::{sync::Arc, time::Duration};
2
3
use macltok::{
4
    AttachmentUpload, ClientHandle, Command, CommandErrorKind, Engine, EngineConfig, EngineError,
5
    Event, EventStream, LifecycleCommand, MediaCommand, MediaId, MediaSize, RoomCommand,
6
    TimelineDiff, TimelineItem, UploadDetails, UploadThumbnail,
7
};
8
use matrix_sdk::{
9
    media::{MediaFormat, MediaRequestParameters},
10
    ruma::{
11
        api::client::sync::sync_events::v5,
12
        event_id,
13
        events::room::{
14
            EncryptedFile, MediaSource,
15
            message::{
16
                AudioMessageEventContent, FileInfo, FileMessageEventContent,
17
                ImageMessageEventContent, MessageType, RoomMessageEventContent,
18
            },
19
        },
20
        media::Method,
21
        mxc_uri, room_id,
22
    },
23
    test_utils::mocks::MatrixMockServer,
24
};
25
use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
26
use serde_json::{Value, json};
27
use wiremock::{
28
    Mock, ResponseTemplate,
29
    matchers::{method, path_regex},
30
};
31
32
struct MediaHarness {
33
    server: MatrixMockServer,
34
    client: matrix_sdk::Client,
35
    handle: ClientHandle,
36
    events: EventStream,
37
    running: tokio::task::JoinHandle<Result<(), EngineError>>,
38
    media_id: MediaId,
39
}
40
41
#[tokio::test]
42
async fn full_size_uses_full_source() {
43
    let mut harness = start(file_with_thumbnail()).await;
44
    mount_downloads(&harness.server).await;
45
46
    let bytes = fetch_ready(&mut harness, MediaSize::Full).await;
47
    assert_eq!(&*bytes, b"binaryjpegfullimagedata");
48
    assert!(
49
        media_paths(&harness.server)
50
            .await
51
            .iter()
52
            .any(|path| path.ends_with("/localhost/full"))
53
    );
54
55
    stop(harness).await;
56
}
57
58
#[tokio::test]
59
async fn encrypted_media_is_cached_as_plaintext_like_the_sdk_does() {
60
    use std::io::Read as _;
61
62
    let plaintext = b"very secret image".to_vec();
63
    let mut cursor = std::io::Cursor::new(plaintext.clone());
64
    let mut encryptor = matrix_sdk_crypto::AttachmentEncryptor::new(&mut cursor);
65
    let mut ciphertext = Vec::new();
66
    encryptor.read_to_end(&mut ciphertext).unwrap();
67
    let keys = encryptor.finish();
68
    let file = EncryptedFile::new(
69
        mxc_uri!("mxc://localhost/secret").to_owned(),
70
        keys.encryption_info,
71
        keys.hashes,
72
    );
73
    let source = MediaSource::Encrypted(Box::new(file.clone()));
74
    let mut harness = start(RoomMessageEventContent::new(MessageType::Image(
75
        ImageMessageEventContent::encrypted("secret.png".to_owned(), file),
76
    )))
77
    .await;
78
    Mock::given(method("GET"))
79
        .and(path_regex("/download/localhost/secret$"))
80
        .respond_with(ResponseTemplate::new(200).set_body_bytes(ciphertext))
81
        .mount(harness.server.server())
82
        .await;
83
84
    let from_sdk = harness
85
        .client
86
        .media()
87
        .get_media_content(
88
            &MediaRequestParameters {
89
                source,
90
                format: MediaFormat::File,
91
            },
92
            true,
93
        )
94
        .await
95
        .unwrap();
96
    assert_eq!(from_sdk, plaintext);
97
    assert_eq!(
98
        &*fetch_ready(&mut harness, MediaSize::Full).await,
99
        &plaintext[..]
100
    );
101
    assert_eq!(media_paths(&harness.server).await.len(), 1);
102
103
    stop(harness).await;
104
}
105
106
#[tokio::test]
107
async fn simultaneous_fetches_share_one_download() {
108
    let mut harness = start(plain_image()).await;
109
    Mock::given(method("GET"))
110
        .and(path_regex("/download/localhost/image$"))
111
        .respond_with(
112
            ResponseTemplate::new(200)
113
                .set_body_bytes(b"shared image bytes".to_vec())
114
                .set_delay(Duration::from_millis(300)),
115
        )
116
        .mount(harness.server.server())
117
        .await;
118
119
    let mut pending = Vec::new();
120
    for _ in 0..2 {
121
        pending.push(
122
            harness
123
                .handle
124
                .send(Command::Media(MediaCommand::Fetch {
125
                    id: harness.media_id.clone(),
126
                    size: MediaSize::Full,
127
                }))
128
                .await
129
                .unwrap(),
130
        );
131
    }
132
    let mut ready = false;
133
    tokio::time::timeout(Duration::from_secs(5), async {
134
        loop {
135
            match harness.events.recv().await.expect("event stream closed") {
136
                Event::MediaReady {
137
                    media_id, bytes, ..
138
                } if media_id == harness.media_id => {
139
                    assert_eq!(&*bytes, b"shared image bytes");
140
                    ready = true;
141
                }
142
                Event::CommandSucceeded { id, .. } => pending.retain(|waiting| *waiting != id),
143
                Event::CommandFailed(failure) if pending.contains(&failure.id) => {
144
                    panic!("fetch failed: {failure:?}");
145
                }
146
                _ => {}
147
            }
148
            if ready && pending.is_empty() {
149
                break;
150
            }
151
        }
152
    })
153
    .await
154
    .expect("both fetches did not complete");
155
    let downloads = harness
156
        .server
157
        .received_requests()
158
        .await
159
        .unwrap()
160
        .iter()
161
        .filter(|request| request.url.path().ends_with("/download/localhost/image"))
162
        .count();
163
    assert_eq!(downloads, 1, "duplicate in-flight media download");
164
    stop(harness).await;
165
}
166
167
#[tokio::test]
168
async fn downloaded_media_over_limit_is_rejected() {
169
    let mut harness = start_with_config(
170
        plain_image(),
171
        EngineConfig::default()
172
            .with_initial_history_limit(0)
173
            .with_max_media_bytes(4),
174
    )
175
    .await;
176
    Mock::given(method("GET"))
177
        .and(path_regex("/download/localhost/image$"))
178
        .respond_with(ResponseTemplate::new(200).set_body_bytes(b"too large".to_vec()))
179
        .mount(harness.server.server())
180
        .await;
181
    let id = harness
182
        .handle
183
        .send(Command::Media(MediaCommand::Fetch {
184
            id: harness.media_id.clone(),
185
            size: MediaSize::Full,
186
        }))
187
        .await
188
        .unwrap();
189
    receive_until(&mut harness.events, |event| {
190
        matches!(event, Event::CommandFailed(failure) if failure.id == id && failure.kind == CommandErrorKind::MediaTooLarge)
191
    })
192
    .await;
193
    stop(harness).await;
194
}
195
196
#[tokio::test]
197
async fn thumbnail_size_prefers_explicit_thumbnail() {
198
    let mut harness = start(file_with_thumbnail()).await;
199
    mount_downloads(&harness.server).await;
200
201
    let bytes = fetch_ready(&mut harness, MediaSize::Thumbnail).await;
202
    assert_eq!(&*bytes, b"binaryjpegfullimagedata");
203
    let paths = media_paths(&harness.server).await;
204
    assert!(
205
        paths
206
            .iter()
207
            .any(|path| path.ends_with("/localhost/explicit-thumbnail"))
208
    );
209
    assert!(!paths.iter().any(|path| path.ends_with("/localhost/full")));
210
211
    stop(harness).await;
212
}
213
214
#[tokio::test]
215
async fn plain_image_thumbnail_uses_thumbnail_endpoint() {
216
    let mut harness = start(plain_image()).await;
217
    harness
218
        .server
219
        .mock_media_thumbnail(Method::Scale, 800, 800, false)
220
        .ok()
221
        .mount()
222
        .await;
223
    harness
224
        .server
225
        .mock_authed_media_thumbnail(Method::Scale, 800, 800, false)
226
        .ok()
227
        .mount()
228
        .await;
229
230
    let bytes = fetch_ready(&mut harness, MediaSize::Thumbnail).await;
231
    assert_eq!(&*bytes, b"binaryjpegthumbnaildata");
232
    assert!(
233
        media_paths(&harness.server)
234
            .await
235
            .iter()
236
            .any(|path| path.contains("/thumbnail/localhost/image"))
237
    );
238
239
    stop(harness).await;
240
}
241
242
#[tokio::test]
243
async fn unavailable_thumbnail_reports_returned_command_id() {
244
    let mut harness = start(plain_audio()).await;
245
    let command_id = harness
246
        .handle
247
        .send(Command::Media(MediaCommand::Fetch {
248
            id: harness.media_id.clone(),
249
            size: MediaSize::Thumbnail,
250
        }))
251
        .await
252
        .unwrap();
253
254
    let event = receive_until(&mut harness.events, |event| {
255
        matches!(
256
            event,
257
            Event::CommandFailed(failure)
258
                if failure.id == command_id && failure.kind == CommandErrorKind::MediaUnavailable
259
        )
260
    })
261
    .await;
262
    assert!(matches!(
263
        event,
264
        Event::CommandFailed(failure)
265
            if failure.id == command_id && failure.kind == CommandErrorKind::MediaUnavailable
266
    ));
267
268
    stop(harness).await;
269
}
270
271
#[tokio::test]
272
async fn media_upload_is_queued_and_sent() {
273
    let mut harness = start(plain_image()).await;
274
    harness
275
        .server
276
        .mock_authenticated_media_config()
277
        .ok_default()
278
        .mount()
279
        .await;
280
    harness
281
        .server
282
        .mock_upload()
283
        .expect_mime_type("image/jpeg")
284
        .ok(mxc_uri!("mxc://localhost/uploaded"))
285
        .expect(1)
286
        .mount()
287
        .await;
288
    harness
289
        .server
290
        .mock_room_send()
291
        .ok(event_id!("$uploaded:localhost"))
292
        .expect(1)
293
        .mount()
294
        .await;
295
296
    let id = harness
297
        .handle
298
        .upload_media(
299
            room_id!("!media:localhost").to_owned().into(),
300
            "photo.jpg".to_owned(),
301
            "image/jpeg".to_owned(),
302
            b"photo data".to_vec(),
303
            Some("holiday".to_owned()),
304
        )
305
        .await
306
        .unwrap();
307
    receive_until(
308
        &mut harness.events,
309
        |event| matches!(event, Event::CommandSucceeded { id: completed, .. } if *completed == id),
310
    )
311
    .await;
312
313
    tokio::time::timeout(Duration::from_secs(5), async {
314
        loop {
315
            let requests = harness.server.received_requests().await.unwrap();
316
            if requests.iter().any(|request| {
317
                request.url.path().contains("/send/m.room.message/")
318
                    && String::from_utf8_lossy(&request.body).contains("mxc://localhost/uploaded")
319
            }) {
320
                break;
321
            }
322
            tokio::task::yield_now().await;
323
        }
324
    })
325
    .await
326
    .expect("queued media was not sent");
327
328
    stop(harness).await;
329
}
330
331
#[tokio::test]
332
async fn queued_offline_attachment_exposes_fetchable_local_media() {
333
    let mut harness = start(plain_image()).await;
334
    harness.client.send_queue().set_enabled(false).await;
335
    let original = b"offline image bytes".to_vec();
336
    let mut upload = AttachmentUpload::new(
337
        room_id!("!media:localhost").to_owned(),
338
        "pending.png",
339
        "image/png",
340
        original.clone(),
341
    );
342
    upload.details = Some(UploadDetails::Image {
343
        width: Some(32),
344
        height: Some(16),
345
        blurhash: None,
346
    });
347
    harness.handle.upload_attachment(upload).await.unwrap();
348
    let event = receive_until(&mut harness.events, |event| {
349
        event_messages(event).any(|message| {
350
            message.own
351
                && message.transaction_id.is_some()
352
                && message
353
                    .media
354
                    .as_ref()
355
                    .is_some_and(|media| media.filename == "pending.png")
356
        })
357
    })
358
    .await;
359
    let media = event_messages(&event)
360
        .find(|message| message.own && message.media.is_some())
361
        .and_then(|message| message.media.as_ref())
362
        .unwrap();
363
    assert_eq!((media.width, media.height), (Some(32), Some(16)));
364
    assert_eq!(media.size_bytes, Some(original.len() as u64));
365
    let media_id = media.id.clone();
366
367
    let fetch_id = harness
368
        .handle
369
        .send(Command::Media(MediaCommand::Fetch {
370
            id: media_id.clone(),
371
            size: MediaSize::Full,
372
        }))
373
        .await
374
        .unwrap();
375
    let ready = receive_until(
376
        &mut harness.events,
377
        |event| matches!(event, Event::MediaReady { media_id: id, .. } if id == &media_id),
378
    )
379
    .await;
380
    assert!(matches!(ready, Event::MediaReady { bytes, .. } if *bytes == original));
381
    receive_until(
382
        &mut harness.events,
383
        |event| matches!(event, Event::CommandSucceeded { id, .. } if *id == fetch_id),
384
    )
385
    .await;
386
    stop(harness).await;
387
}
388
389
#[tokio::test]
390
async fn image_upload_uses_caller_supplied_metadata_and_thumbnail() {
391
    let mut harness = start(plain_image()).await;
392
    harness
393
        .server
394
        .mock_authenticated_media_config()
395
        .ok_default()
396
        .mount()
397
        .await;
398
    harness
399
        .server
400
        .mock_upload()
401
        .expect_mime_type("image/png")
402
        .respond_with(|request: &wiremock::Request| {
403
            let uri = if request.body == b"thumbnail bytes" {
404
                "mxc://localhost/image-thumb"
405
            } else {
406
                "mxc://localhost/image-upload"
407
            };
408
            ResponseTemplate::new(200).set_body_json(json!({ "content_uri": uri }))
409
        })
410
        .expect(2)
411
        .mount()
412
        .await;
413
    harness
414
        .server
415
        .mock_room_send()
416
        .ok(event_id!("$image-upload:localhost"))
417
        .expect(1)
418
        .mount()
419
        .await;
420
421
    let original = b"image bytes".to_vec();
422
    let thumbnail_bytes = b"thumbnail bytes".to_vec();
423
    let mut upload = AttachmentUpload::new(
424
        room_id!("!media:localhost").to_owned(),
425
        "large.png",
426
        "image/png",
427
        original.clone(),
428
    );
429
    upload.details = Some(UploadDetails::Image {
430
        width: Some(900),
431
        height: Some(450),
432
        blurhash: None,
433
    });
434
    upload.thumbnail = Some(UploadThumbnail {
435
        bytes: thumbnail_bytes.clone(),
436
        mimetype: "image/png".to_owned(),
437
        width: 800,
438
        height: 400,
439
    });
440
    let id = harness.handle.upload_attachment(upload).await.unwrap();
441
    receive_until(
442
        &mut harness.events,
443
        |event| matches!(event, Event::CommandSucceeded { id: completed, .. } if *completed == id),
444
    )
445
    .await;
446
447
    let content = uploaded_content(&harness.server).await;
448
    assert_eq!(content["msgtype"], "m.image");
449
    assert_eq!(content["info"]["w"], 900);
450
    assert_eq!(content["info"]["h"], 450);
451
    assert_eq!(content["info"]["size"], original.len());
452
    assert_eq!(content["info"]["thumbnail_info"]["w"], 800);
453
    assert_eq!(content["info"]["thumbnail_info"]["h"], 400);
454
    assert_eq!(content["info"]["thumbnail_info"]["mimetype"], "image/png");
455
    assert_eq!(
456
        content["info"]["thumbnail_info"]["size"],
457
        thumbnail_bytes.len()
458
    );
459
    assert_eq!(
460
        content["info"]["thumbnail_url"],
461
        "mxc://localhost/image-thumb"
462
    );
463
    assert_eq!(content["url"], "mxc://localhost/image-upload");
464
465
    let requests = harness.server.received_requests().await.unwrap();
466
    let uploads = requests
467
        .iter()
468
        .filter(|request| request.url.path().ends_with("/_matrix/media/v3/upload"))
469
        .collect::<Vec<_>>();
470
    assert_eq!(uploads.len(), 2);
471
    assert!(uploads.iter().any(|request| request.body == original));
472
    assert!(
473
        uploads
474
            .iter()
475
            .any(|request| request.body == thumbnail_bytes)
476
    );
477
478
    stop(harness).await;
479
}
480
481
#[tokio::test]
482
async fn video_upload_preserves_metadata_and_supplied_thumbnail() {
483
    let mut harness = start(plain_image()).await;
484
    harness
485
        .server
486
        .mock_authenticated_media_config()
487
        .ok_default()
488
        .mount()
489
        .await;
490
    harness
491
        .server
492
        .mock_upload()
493
        .expect_mime_type("image/png")
494
        .ok(mxc_uri!("mxc://localhost/video-thumb"))
495
        .expect(1)
496
        .mount()
497
        .await;
498
    harness
499
        .server
500
        .mock_upload()
501
        .expect_mime_type("video/mp4")
502
        .ok(mxc_uri!("mxc://localhost/video-upload"))
503
        .expect(1)
504
        .mount()
505
        .await;
506
    harness
507
        .server
508
        .mock_room_send()
509
        .ok(event_id!("$video-upload:localhost"))
510
        .expect(1)
511
        .mount()
512
        .await;
513
514
    let mut upload = AttachmentUpload::new(
515
        room_id!("!media:localhost").to_owned(),
516
        "clip.mp4",
517
        "video/mp4",
518
        b"video bytes".to_vec(),
519
    );
520
    upload.details = Some(UploadDetails::Video {
521
        width: Some(1920),
522
        height: Some(1080),
523
        duration_ms: Some(3500),
524
        blurhash: Some("blurhash".to_owned()),
525
    });
526
    upload.thumbnail = Some(UploadThumbnail {
527
        bytes: b"poster bytes".to_vec(),
528
        mimetype: "image/png".to_owned(),
529
        width: 320,
530
        height: 180,
531
    });
532
    let id = harness.handle.upload_attachment(upload).await.unwrap();
533
    receive_until(
534
        &mut harness.events,
535
        |event| matches!(event, Event::CommandSucceeded { id: completed, .. } if *completed == id),
536
    )
537
    .await;
538
539
    let content = uploaded_content(&harness.server).await;
540
    assert_eq!(content["msgtype"], "m.video");
541
    assert_eq!(content["info"]["w"], 1920);
542
    assert_eq!(content["info"]["h"], 1080);
543
    assert_eq!(content["info"]["duration"], 3500);
544
    assert_eq!(content["info"]["size"], 11);
545
    assert_eq!(content["info"]["thumbnail_info"]["w"], 320);
546
    assert_eq!(content["info"]["thumbnail_info"]["h"], 180);
547
    assert_eq!(content["info"]["thumbnail_info"]["size"], 12);
548
    assert_eq!(
549
        content["info"]["thumbnail_url"],
550
        "mxc://localhost/video-thumb"
551
    );
552
    assert_eq!(content["url"], "mxc://localhost/video-upload");
553
554
    let requests = harness.server.received_requests().await.unwrap();
555
    assert!(requests.iter().any(|request| {
556
        request.url.path().ends_with("/_matrix/media/v3/upload") && request.body == b"poster bytes"
557
    }));
558
559
    stop(harness).await;
560
}
561
562
#[tokio::test]
563
async fn voice_upload_emits_voice_details_and_correlated_progress() {
564
    let mut harness = start(plain_image()).await;
565
    harness
566
        .server
567
        .mock_authenticated_media_config()
568
        .ok_default()
569
        .mount()
570
        .await;
571
    harness
572
        .server
573
        .mock_upload()
574
        .expect_mime_type("audio/ogg")
575
        .respond_with(
576
            ResponseTemplate::new(200)
577
                .set_body_json(json!({ "content_uri": "mxc://localhost/voice-upload" }))
578
                .set_delay(Duration::from_millis(500)),
579
        )
580
        .expect(1)
581
        .mount()
582
        .await;
583
    harness
584
        .server
585
        .mock_room_send()
586
        .ok(event_id!("$voice-upload:localhost"))
587
        .expect(1)
588
        .mount()
589
        .await;
590
591
    let data = b"voice bytes".to_vec();
592
    let mut upload = AttachmentUpload::new(
593
        room_id!("!media:localhost").to_owned(),
594
        "voice.ogg",
595
        "audio/ogg",
596
        data.clone(),
597
    );
598
    upload.details = Some(UploadDetails::Voice {
599
        duration_ms: 4200,
600
        waveform: vec![0, 512, 1024],
601
    });
602
    let id = harness.handle.upload_attachment(upload).await.unwrap();
603
    receive_until(
604
        &mut harness.events,
605
        |event| matches!(event, Event::CommandSucceeded { id: completed, .. } if *completed == id),
606
    )
607
    .await;
608
609
    let pending = harness
610
        .handle
611
        .pending_send_ids(room_id!("!media:localhost"))
612
        .await
613
        .unwrap();
614
    assert_eq!(pending.len(), 1);
615
    let transaction_id = pending[0].clone();
616
    let progress = receive_until(&mut harness.events, |event| {
617
        matches!(event, Event::MediaUploadProgress { transaction_id: seen, complete: true, .. } if seen == &transaction_id)
618
    })
619
    .await;
620
    assert!(matches!(progress, Event::MediaUploadProgress {
621
        room_id,
622
        transaction_id: seen,
623
        uploaded_bytes,
624
        total_bytes,
625
        complete: true,
626
    } if room_id == room_id!("!media:localhost") && seen == transaction_id && uploaded_bytes == data.len() as u64 && total_bytes == data.len() as u64));
627
628
    let content = uploaded_content(&harness.server).await;
629
    assert_eq!(content["msgtype"], "m.audio");
630
    assert_eq!(content["info"]["duration"], 4200);
631
    assert_eq!(content["info"]["size"], data.len());
632
    assert_eq!(content["org.matrix.msc1767.audio"]["duration"], 4200);
633
    assert_eq!(
634
        content["org.matrix.msc1767.audio"]["waveform"],
635
        json!([0, 512, 1024])
636
    );
637
    assert_eq!(content["org.matrix.msc3245.voice"], json!({}));
638
639
    stop(harness).await;
640
}
641
642
#[tokio::test]
643
async fn mismatched_upload_metadata_and_invalid_waveform_fail_without_uploading() {
644
    let mut harness = start(plain_image()).await;
645
    let mut mismatched = AttachmentUpload::new(
646
        room_id!("!media:localhost").to_owned(),
647
        "clip.mp4",
648
        "video/mp4",
649
        b"video bytes".to_vec(),
650
    );
651
    mismatched.details = Some(UploadDetails::Image {
652
        width: Some(32),
653
        height: Some(32),
654
        blurhash: None,
655
    });
656
    let id = harness.handle.upload_attachment(mismatched).await.unwrap();
657
    let failure = receive_until(
658
        &mut harness.events,
659
        |event| matches!(event, Event::CommandFailed(failure) if failure.id == id),
660
    )
661
    .await;
662
    assert!(
663
        matches!(failure, Event::CommandFailed(failure) if failure.kind == CommandErrorKind::Other)
664
    );
665
666
    let mut voice = AttachmentUpload::new(
667
        room_id!("!media:localhost").to_owned(),
668
        "voice.ogg",
669
        "audio/ogg",
670
        b"voice bytes".to_vec(),
671
    );
672
    voice.details = Some(UploadDetails::Voice {
673
        duration_ms: 1000,
674
        waveform: vec![1025],
675
    });
676
    let id = harness.handle.upload_attachment(voice).await.unwrap();
677
    let failure = receive_until(
678
        &mut harness.events,
679
        |event| matches!(event, Event::CommandFailed(failure) if failure.id == id),
680
    )
681
    .await;
682
    assert!(
683
        matches!(failure, Event::CommandFailed(failure) if failure.kind == CommandErrorKind::Other)
684
    );
685
686
    assert!(
687
        !harness
688
            .server
689
            .received_requests()
690
            .await
691
            .unwrap()
692
            .iter()
693
            .any(|request| { request.url.path().ends_with("/_matrix/media/v3/upload") })
694
    );
695
    stop(harness).await;
696
}
697
698
#[tokio::test]
699
async fn upload_over_media_limit_fails_without_uploading() {
700
    let mut harness = start_with_config(
701
        plain_image(),
702
        EngineConfig::default()
703
            .with_initial_history_limit(0)
704
            .with_max_media_bytes(4),
705
    )
706
    .await;
707
    let upload = AttachmentUpload::new(
708
        room_id!("!media:localhost").to_owned(),
709
        "large.bin",
710
        "application/octet-stream",
711
        b"too large".to_vec(),
712
    );
713
    let id = harness.handle.upload_attachment(upload).await.unwrap();
714
    let failure = receive_until(
715
        &mut harness.events,
716
        |event| matches!(event, Event::CommandFailed(failure) if failure.id == id),
717
    )
718
    .await;
719
    assert!(matches!(
720
        failure,
721
        Event::CommandFailed(failure) if failure.kind == CommandErrorKind::MediaTooLarge
722
    ));
723
    assert!(
724
        !harness
725
            .server
726
            .received_requests()
727
            .await
728
            .unwrap()
729
            .iter()
730
            .any(|request| { request.url.path().ends_with("/_matrix/media/v3/upload") })
731
    );
732
    stop(harness).await;
733
}
734
735
#[tokio::test]
736
async fn audio_thumbnail_fails_without_uploading() {
737
    let mut harness = start(plain_image()).await;
738
    let mut upload = AttachmentUpload::new(
739
        room_id!("!media:localhost").to_owned(),
740
        "voice.ogg",
741
        "audio/ogg",
742
        b"voice bytes".to_vec(),
743
    );
744
    upload.thumbnail = Some(UploadThumbnail {
745
        bytes: b"thumbnail".to_vec(),
746
        mimetype: "image/png".to_owned(),
747
        width: 80,
748
        height: 80,
749
    });
750
    let id = harness.handle.upload_attachment(upload).await.unwrap();
751
    let failure = receive_until(
752
        &mut harness.events,
753
        |event| matches!(event, Event::CommandFailed(failure) if failure.id == id),
754
    )
755
    .await;
756
    assert!(matches!(
757
        failure,
758
        Event::CommandFailed(failure) if failure.kind == CommandErrorKind::MediaUnavailable
759
    ));
760
    assert!(
761
        !harness
762
            .server
763
            .received_requests()
764
            .await
765
            .unwrap()
766
            .iter()
767
            .any(|request| { request.url.path().ends_with("/_matrix/media/v3/upload") })
768
    );
769
    stop(harness).await;
770
}
771
772
#[tokio::test]
773
async fn invalid_upload_mime_reports_a_command_failure() {
774
    let mut harness = start(plain_image()).await;
775
    let id = harness
776
        .handle
777
        .upload_media(
778
            room_id!("!media:localhost").to_owned().into(),
779
            "photo.jpg".to_owned(),
780
            "not a MIME type".to_owned(),
781
            b"photo data".to_vec(),
782
            None,
783
        )
784
        .await
785
        .unwrap();
786
    let event = receive_until(
787
        &mut harness.events,
788
        |event| matches!(event, Event::CommandFailed(failure) if failure.id == id),
789
    )
790
    .await;
791
    assert!(
792
        matches!(event, Event::CommandFailed(failure) if failure.kind == CommandErrorKind::Other)
793
    );
794
795
    stop(harness).await;
796
}
797
798
async fn start(content: RoomMessageEventContent) -> MediaHarness {
799
    start_with_config(
800
        content,
801
        EngineConfig::default().with_initial_history_limit(0),
802
    )
803
    .await
804
}
805
806
async fn start_with_config(content: RoomMessageEventContent, config: EngineConfig) -> MediaHarness {
807
    let server = MatrixMockServer::new().await;
808
    server
809
        .mock_sliding_sync()
810
        .ok(v5::Response::new("1".to_owned()))
811
        .mount()
812
        .await;
813
    let client = server.client_builder().build().await;
814
    let room_id = room_id!("!media:localhost");
815
    server.mock_room_state_encryption().plain().mount().await;
816
    server.sync_joined_room(&client, room_id).await;
817
    let sync_client = client.clone();
818
    let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
819
    let running = tokio::spawn(engine.run());
820
821
    handle
822
        .send(Command::Room(RoomCommand::Open(room_id.to_owned())))
823
        .await
824
        .unwrap();
825
    receive_until(
826
        &mut events,
827
        |event| matches!(event, Event::TimelineReset { timeline_id: reset, .. } if reset.room_id() == room_id),
828
    )
829
    .await;
830
831
    server
832
        .sync_room(
833
            &sync_client,
834
            JoinedRoomBuilder::new(room_id)
835
                .add_timeline_event(EventFactory::new().event(content).sender(&ALICE)),
836
        )
837
        .await;
838
    let event = receive_until(&mut events, |event| {
839
        event_messages(event).any(|message| message.media.is_some())
840
    })
841
    .await;
842
    let media_id = event_messages(&event)
843
        .find_map(|message| message.media.as_ref().map(|media| media.id.clone()))
844
        .unwrap();
845
846
    MediaHarness {
847
        server,
848
        client: sync_client,
849
        handle,
850
        events,
851
        running,
852
        media_id,
853
    }
854
}
855
856
async fn mount_downloads(server: &MatrixMockServer) {
857
    server.mock_media_download().ok_image().mount().await;
858
    server.mock_authed_media_download().ok_image().mount().await;
859
}
860
861
async fn fetch_ready(harness: &mut MediaHarness, size: MediaSize) -> Arc<[u8]> {
862
    harness
863
        .handle
864
        .send(Command::Media(MediaCommand::Fetch {
865
            id: harness.media_id.clone(),
866
            size,
867
        }))
868
        .await
869
        .unwrap();
870
    match receive_until(&mut harness.events, |event| {
871
        matches!(
872
            event,
873
            Event::MediaReady { media_id, size: received, .. }
874
                if media_id == &harness.media_id && received == &size
875
        )
876
    })
877
    .await
878
    {
879
        Event::MediaReady { bytes, .. } => bytes,
880
        _ => unreachable!(),
881
    }
882
}
883
884
async fn stop(mut harness: MediaHarness) {
885
    harness
886
        .handle
887
        .send(Command::Lifecycle(LifecycleCommand::Shutdown))
888
        .await
889
        .unwrap();
890
    receive_until(&mut harness.events, |event| matches!(event, Event::Stopped)).await;
891
    harness.running.await.unwrap().unwrap();
892
}
893
894
async fn media_paths(server: &MatrixMockServer) -> Vec<String> {
895
    server
896
        .received_requests()
897
        .await
898
        .unwrap()
899
        .into_iter()
900
        .map(|request| request.url.path().to_owned())
901
        .filter(|path| path.contains("/media/"))
902
        .collect()
903
}
904
905
async fn uploaded_content(server: &MatrixMockServer) -> Value {
906
    tokio::time::timeout(Duration::from_secs(5), async {
907
        loop {
908
            if let Some(request) = server
909
                .received_requests()
910
                .await
911
                .unwrap()
912
                .iter()
913
                .find(|request| request.url.path().contains("/send/m.room.message/"))
914
            {
915
                return serde_json::from_slice(&request.body).unwrap();
916
            }
917
            tokio::task::yield_now().await;
918
        }
919
    })
920
    .await
921
    .expect("queued media was not sent")
922
}
923
924
fn file_with_thumbnail() -> RoomMessageEventContent {
925
    let mut info = FileInfo::new();
926
    info.thumbnail_source = Some(MediaSource::Plain(
927
        mxc_uri!("mxc://localhost/explicit-thumbnail").to_owned(),
928
    ));
929
    RoomMessageEventContent::new(MessageType::File(
930
        FileMessageEventContent::plain(
931
            "file.bin".to_owned(),
932
            mxc_uri!("mxc://localhost/full").to_owned(),
933
        )
934
        .info(Box::new(info)),
935
    ))
936
}
937
938
fn plain_image() -> RoomMessageEventContent {
939
    RoomMessageEventContent::new(MessageType::Image(ImageMessageEventContent::plain(
940
        "image.jpg".to_owned(),
941
        mxc_uri!("mxc://localhost/image").to_owned(),
942
    )))
943
}
944
945
fn plain_audio() -> RoomMessageEventContent {
946
    RoomMessageEventContent::new(MessageType::Audio(AudioMessageEventContent::plain(
947
        "audio.ogg".to_owned(),
948
        mxc_uri!("mxc://localhost/audio").to_owned(),
949
    )))
950
}
951
952
fn event_messages(event: &Event) -> std::vec::IntoIter<&macltok::TimelineMessage> {
953
    let items = match event {
954
        Event::TimelineReset { items, .. } => items.iter().collect::<Vec<_>>(),
955
        Event::TimelineChanged { diffs, .. } => {
956
            diffs.iter().flat_map(diff_items).collect::<Vec<_>>()
957
        }
958
        _ => Vec::new(),
959
    };
960
    items
961
        .into_iter()
962
        .filter_map(|item| match item {
963
            TimelineItem::Message(message) => Some(message.as_ref()),
964
            _ => None,
965
        })
966
        .collect::<Vec<_>>()
967
        .into_iter()
968
}
969
970
fn diff_items(diff: &TimelineDiff) -> Vec<&TimelineItem> {
971
    match diff {
972
        TimelineDiff::Append { values } | TimelineDiff::Reset { values } => values.iter().collect(),
973
        TimelineDiff::PushFront { value }
974
        | TimelineDiff::PushBack { value }
975
        | TimelineDiff::Insert { value, .. }
976
        | TimelineDiff::Set { value, .. } => vec![value],
977
        _ => Vec::new(),
978
    }
979
}
980
981
async fn receive_until(events: &mut EventStream, predicate: impl Fn(&Event) -> bool) -> Event {
982
    tokio::time::timeout(Duration::from_secs(5), async {
983
        while let Some(event) = events.recv().await {
984
            if predicate(&event) {
985
                return event;
986
            }
987
        }
988
        panic!("event stream closed before the expected event");
989
    })
990
    .await
991
    .expect("expected event did not arrive")
992
}