atagen

macltok

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