1
use std::sync::Arc;
2

            
3
use oo7::dbus;
4
use tokio_stream::StreamExt;
5

            
6
use crate::tests::{TestServiceSetup, gnome_prompter_test, plasma_prompter_test};
7

            
8
#[tokio::test]
9
async fn create_item_plain() -> Result<(), Box<dyn std::error::Error>> {
10
    let setup = TestServiceSetup::plain_session(true).await?;
11

            
12
    // Get initial modified timestamp
13
    let initial_modified = setup.collections[0].modified().await?;
14

            
15
    // Wait to ensure timestamp will be different
16
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
17

            
18
    // Create an item using the proper API
19
    let secret = oo7::Secret::text("my-secret-password");
20
    let dbus_secret = dbus::api::DBusSecret::new(setup.session, secret.clone());
21

            
22
    let item = setup.collections[0]
23
        .create_item(
24
            "Test Item",
25
            &[("application", "test-app"), ("type", "password")],
26
            &dbus_secret,
27
            false,
28
            None,
29
        )
30
        .await?;
31

            
32
    // Verify item exists in collection
33
    let items = setup.collections[0].items().await?;
34
    assert_eq!(items.len(), 1, "Collection should have one item");
35
    assert_eq!(items[0].inner().path(), item.inner().path());
36

            
37
    // Verify item label
38
    let label = item.label().await?;
39
    assert_eq!(label, "Test Item");
40

            
41
    // Verify modified timestamp was updated
42
    let new_modified = setup.collections[0].modified().await?;
43
    assert!(
44
        new_modified > initial_modified,
45
        "Modified timestamp should be updated after creating item"
46
    );
47

            
48
    Ok(())
49
}
50

            
51
#[tokio::test]
52
async fn create_item_encrypted() -> Result<(), Box<dyn std::error::Error>> {
53
    let setup = TestServiceSetup::encrypted_session(true).await?;
54
    let aes_key = setup.aes_key.unwrap();
55

            
56
    // Create an encrypted item using the proper API
57
    let secret = oo7::Secret::text("my-encrypted-secret");
58
    let dbus_secret = dbus::api::DBusSecret::new_encrypted(setup.session, secret, &aes_key)?;
59

            
60
    let item = setup.collections[0]
61
        .create_item(
62
            "Test Encrypted Item",
63
            &[("application", "test-app"), ("type", "encrypted-password")],
64
            &dbus_secret,
65
            false,
66
            None,
67
        )
68
        .await?;
69

            
70
    // Verify item exists
71
    let items = setup.collections[0].items().await?;
72
    assert_eq!(items.len(), 1, "Collection should have one item");
73
    assert_eq!(items[0].inner().path(), item.inner().path());
74

            
75
    Ok(())
76
}
77

            
78
#[tokio::test]
79
async fn search_items_after_creation() -> Result<(), Box<dyn std::error::Error>> {
80
    let setup = TestServiceSetup::plain_session(true).await?;
81

            
82
    // Create two items with different attributes
83
    let secret1 = oo7::Secret::text("password1");
84
    let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1);
85

            
86
    setup.collections[0]
87
        .create_item(
88
            "Firefox Password",
89
            &[("application", "firefox"), ("username", "user1")],
90
            &dbus_secret1,
91
            false,
92
            None,
93
        )
94
        .await?;
95

            
96
    let secret2 = oo7::Secret::text("password2");
97
    let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2);
98

            
99
    setup.collections[0]
100
        .create_item(
101
            "Chrome Password",
102
            &[("application", "chrome"), ("username", "user2")],
103
            &dbus_secret2,
104
            false,
105
            None,
106
        )
107
        .await?;
108

            
109
    // Search for firefox item
110
    let firefox_attrs = &[("application", "firefox")];
111
    let firefox_items = setup.collections[0].search_items(firefox_attrs).await?;
112

            
113
    assert_eq!(firefox_items.len(), 1, "Should find one firefox item");
114

            
115
    // Search for chrome item
116
    let chrome_items = setup.collections[0]
117
        .search_items(&[("application", "chrome")])
118
        .await?;
119

            
120
    assert_eq!(chrome_items.len(), 1, "Should find one chrome item");
121

            
122
    // Search for non-existent item
123
    let nonexistent_items = setup.collections[0]
124
        .search_items(&[("application", "nonexistent")])
125
        .await?;
126

            
127
    assert_eq!(
128
        nonexistent_items.len(),
129
        0,
130
        "Should find no nonexistent items"
131
    );
132

            
133
    Ok(())
134
}
135

            
136
#[tokio::test]
137
async fn search_items_subset_matching() -> Result<(), Box<dyn std::error::Error>> {
138
    let setup = TestServiceSetup::plain_session(true).await?;
139

            
140
    // Create an item with multiple attributes (url and username)
141
    let secret = oo7::Secret::text("my-password");
142
    let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret);
143

            
144
    setup.collections[0]
145
        .create_item(
146
            "Zed Login",
147
            &[("url", "https://zed.dev"), ("username", "alice")],
148
            &dbus_secret,
149
            false,
150
            None,
151
        )
152
        .await?;
153

            
154
    // Search with only the url attribute (subset of stored attributes)
155
    let results = setup.collections[0]
156
        .search_items(&[("url", "https://zed.dev")])
157
        .await?;
158

            
159
    assert_eq!(
160
        results.len(),
161
        1,
162
        "Should find item when searching with subset of its attributes"
163
    );
164

            
165
    // Search with only the username attribute (another subset)
166
    let results = setup.collections[0]
167
        .search_items(&[("username", "alice")])
168
        .await?;
169

            
170
    assert_eq!(
171
        results.len(),
172
        1,
173
        "Should find item when searching with different subset of its attributes"
174
    );
175

            
176
    // Search with both attributes (exact match)
177
    let results = setup.collections[0]
178
        .search_items(&[("url", "https://zed.dev"), ("username", "alice")])
179
        .await?;
180

            
181
    assert_eq!(
182
        results.len(),
183
        1,
184
        "Should find item when searching with all its attributes"
185
    );
186

            
187
    // Search with superset of attributes (should not match)
188
    let results = setup.collections[0]
189
        .search_items(&[
190
            ("url", "https://zed.dev"),
191
            ("username", "alice"),
192
            ("extra", "attribute"),
193
        ])
194
        .await?;
195

            
196
    assert_eq!(
197
        results.len(),
198
        0,
199
        "Should not find item when searching with superset of its attributes"
200
    );
201

            
202
    Ok(())
203
}
204

            
205
#[tokio::test]
206
async fn create_item_with_replace() -> Result<(), Box<dyn std::error::Error>> {
207
    let setup = TestServiceSetup::plain_session(true).await?;
208

            
209
    // Create first item
210
    let secret1 = oo7::Secret::text("original-password");
211
    let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1.clone());
212

            
213
    let item1 = setup.collections[0]
214
        .create_item(
215
            "Test Item",
216
            &[("application", "myapp"), ("username", "user")],
217
            &dbus_secret1,
218
            false,
219
            None,
220
        )
221
        .await?;
222

            
223
    // Verify one item exists
224
    let items = setup.collections[0].items().await?;
225
    assert_eq!(items.len(), 1, "Should have one item");
226

            
227
    // Get the secret from first item
228
    let retrieved1 = item1.secret(&setup.session).await?;
229
    assert_eq!(retrieved1.value(), secret1.as_bytes());
230

            
231
    // Create second item with same attributes and replace=true
232
    let secret2 = oo7::Secret::text("replaced-password");
233
    let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2.clone());
234

            
235
    let item2 = setup.collections[0]
236
        .create_item(
237
            "Test Item",
238
            &[("application", "myapp"), ("username", "user")],
239
            &dbus_secret2,
240
            true, // replace=true
241
            None,
242
        )
243
        .await?;
244

            
245
    // Should still have only one item (replaced)
246
    let items = setup.collections[0].items().await?;
247
    assert_eq!(items.len(), 1, "Should still have one item after replace");
248

            
249
    // Verify the new item has the updated secret
250
    let retrieved2 = item2.secret(&setup.session).await?;
251
    assert_eq!(retrieved2.value(), secret2.as_bytes());
252

            
253
    Ok(())
254
}
255

            
256
#[tokio::test]
257
async fn label_property() -> Result<(), Box<dyn std::error::Error>> {
258
    let setup = TestServiceSetup::plain_session(true).await?;
259

            
260
    // Get the Login collection via alias (don't rely on collection ordering)
261
    let login_collection = setup
262
        .service_api
263
        .read_alias("default")
264
        .await?
265
        .expect("Default collection should exist");
266

            
267
    // Get initial label (should be "Login" for default collection)
268
    let label = login_collection.label().await?;
269
    assert_eq!(label, "Login");
270

            
271
    // Get initial modified timestamp
272
    let initial_modified = login_collection.modified().await?;
273

            
274
    // Wait to ensure timestamp will be different
275
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
276

            
277
    // Set new label
278
    login_collection.set_label("My Custom Collection").await?;
279

            
280
    // Verify new label
281
    let label = login_collection.label().await?;
282
    assert_eq!(label, "My Custom Collection");
283

            
284
    // Verify modified timestamp was updated
285
    let new_modified = login_collection.modified().await?;
286
    assert!(
287
        new_modified > initial_modified,
288
        "Modified timestamp should be updated after label change"
289
    );
290

            
291
    Ok(())
292
}
293

            
294
#[tokio::test]
295
async fn timestamps() -> Result<(), Box<dyn std::error::Error>> {
296
    let setup = TestServiceSetup::plain_session(true).await?;
297

            
298
    // Get created timestamp
299
    let created = setup.collections[0].created().await?;
300
    assert!(created.as_secs() > 0, "Created timestamp should be set");
301

            
302
    // Get modified timestamp
303
    let modified = setup.collections[0].modified().await?;
304
    assert!(modified.as_secs() > 0, "Modified timestamp should be set");
305

            
306
    // Created and modified should be close (within a second for new collection)
307
    let diff = if created > modified {
308
        created.as_secs() - modified.as_secs()
309
    } else {
310
        modified.as_secs() - created.as_secs()
311
    };
312
    assert!(diff <= 1, "Created and modified should be within 1 second");
313

            
314
    Ok(())
315
}
316

            
317
#[tokio::test]
318
async fn create_item_invalid_session() -> Result<(), Box<dyn std::error::Error>> {
319
    let setup = TestServiceSetup::plain_session(true).await?;
320

            
321
    // Create an item using the proper API
322
    let secret = oo7::Secret::text("my-secret-password");
323
    let invalid_session =
324
        dbus::api::Session::new(&setup.client_conn, "/invalid/session/path").await?;
325
    let dbus_secret = dbus::api::DBusSecret::new(Arc::new(invalid_session), secret.clone());
326

            
327
    let result = setup.collections[0]
328
        .create_item(
329
            "Test Item",
330
            &[("application", "test-app"), ("type", "password")],
331
            &dbus_secret,
332
            false,
333
            None,
334
        )
335
        .await;
336

            
337
    assert!(
338
        matches!(
339
            result,
340
            Err(oo7::dbus::Error::Service(
341
                oo7::dbus::ServiceError::NoSession(_)
342
            ))
343
        ),
344
        "Should be NoSession error"
345
    );
346

            
347
    Ok(())
348
}
349

            
350
#[tokio::test]
351
async fn item_created_signal() -> Result<(), Box<dyn std::error::Error>> {
352
    let setup = TestServiceSetup::plain_session(true).await?;
353

            
354
    // Subscribe to ItemCreated signal
355
    let signal_stream = setup.collections[0].receive_item_created().await?;
356
    tokio::pin!(signal_stream);
357

            
358
    // Create an item
359
    let secret = oo7::Secret::text("test-secret");
360
    let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret);
361

            
362
    let item = setup.collections[0]
363
        .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
364
        .await?;
365

            
366
    // Wait for signal with timeout
367
    let signal_result =
368
        tokio::time::timeout(tokio::time::Duration::from_secs(1), signal_stream.next()).await;
369

            
370
    assert!(signal_result.is_ok(), "Should receive ItemCreated signal");
371
    let signal = signal_result.unwrap();
372
    assert!(signal.is_some(), "Signal should not be None");
373

            
374
    let signal_item = signal.unwrap();
375
    assert_eq!(
376
        signal_item.inner().path().as_str(),
377
        item.inner().path().as_str(),
378
        "Signal should contain the created item path"
379
    );
380

            
381
    Ok(())
382
}
383

            
384
#[tokio::test]
385
async fn item_deleted_signal() -> Result<(), Box<dyn std::error::Error>> {
386
    let setup = TestServiceSetup::plain_session(true).await?;
387

            
388
    // Create an item
389
    let secret = oo7::Secret::text("test-secret");
390
    let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret);
391

            
392
    let item = setup.collections[0]
393
        .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
394
        .await?;
395

            
396
    let item_path = item.inner().path().to_owned();
397

            
398
    // Subscribe to ItemDeleted signal
399
    let signal_stream = setup.collections[0].receive_item_deleted().await?;
400
    tokio::pin!(signal_stream);
401

            
402
    // Delete the item
403
    item.delete(None).await?;
404

            
405
    // Wait for signal with timeout
406
    let signal_result =
407
        tokio::time::timeout(tokio::time::Duration::from_secs(1), signal_stream.next()).await;
408

            
409
    assert!(signal_result.is_ok(), "Should receive ItemDeleted signal");
410
    let signal = signal_result.unwrap();
411
    assert!(signal.is_some(), "Signal should not be None");
412

            
413
    let signal_item = signal.unwrap();
414
    assert_eq!(
415
        signal_item.as_str(),
416
        item_path.as_str(),
417
        "Signal should contain the deleted item path"
418
    );
419

            
420
    Ok(())
421
}
422

            
423
#[tokio::test]
424
async fn collection_changed_signal() -> Result<(), Box<dyn std::error::Error>> {
425
    let setup = TestServiceSetup::plain_session(true).await?;
426

            
427
    // Subscribe to CollectionChanged signal
428
    let signal_stream = setup.service_api.receive_collection_changed().await?;
429
    tokio::pin!(signal_stream);
430

            
431
    // Change the collection label
432
    setup.collections[0]
433
        .set_label("Updated Collection Label")
434
        .await?;
435

            
436
    // Wait for signal with timeout
437
    let signal_result =
438
        tokio::time::timeout(tokio::time::Duration::from_secs(1), signal_stream.next()).await;
439

            
440
    assert!(
441
        signal_result.is_ok(),
442
        "Should receive CollectionChanged signal after label change"
443
    );
444
    let signal = signal_result.unwrap();
445
    assert!(signal.is_some(), "Signal should not be None");
446

            
447
    let signal_collection = signal.unwrap();
448
    assert_eq!(
449
        signal_collection.inner().path().as_str(),
450
        setup.collections[0].inner().path().as_str(),
451
        "Signal should contain the changed collection path"
452
    );
453

            
454
    Ok(())
455
}
456

            
457
#[tokio::test]
458
async fn delete_collection() -> Result<(), Box<dyn std::error::Error>> {
459
    let setup = TestServiceSetup::plain_session(true).await?;
460

            
461
    // Create some items in the collection
462
    let secret1 = oo7::Secret::text("password1");
463
    let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1);
464

            
465
    setup.collections[0]
466
        .create_item("Item 1", &[("app", "test")], &dbus_secret1, false, None)
467
        .await?;
468

            
469
    let secret2 = oo7::Secret::text("password2");
470
    let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2);
471

            
472
    setup.collections[0]
473
        .create_item("Item 2", &[("app", "test")], &dbus_secret2, false, None)
474
        .await?;
475

            
476
    // Verify items were created
477
    let items = setup.collections[0].items().await?;
478
    assert_eq!(items.len(), 2, "Should have 2 items before deletion");
479

            
480
    // Get collection path for later verification
481
    let collection_path = setup.collections[0].inner().path().to_owned();
482

            
483
    // Verify collection exists in service
484
    let collections_before = setup.service_api.collections().await?;
485
    let initial_count = collections_before.len();
486

            
487
    // Delete the collection
488
    setup.collections[0].delete(None).await?;
489

            
490
    // Give the system a moment to process the deletion
491
    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
492

            
493
    // Verify collection is no longer in service's collection list
494
    let collections_after = setup.service_api.collections().await?;
495
    assert_eq!(
496
        collections_after.len(),
497
        initial_count - 1,
498
        "Service should have one less collection after deletion"
499
    );
500

            
501
    // Verify the specific collection is not in the list
502
    let collection_paths: Vec<_> = collections_after
503
        .iter()
504
        .map(|c| c.inner().path().as_str())
505
        .collect();
506
    assert!(
507
        !collection_paths.contains(&collection_path.as_str()),
508
        "Deleted collection should not be in service collections list"
509
    );
510

            
511
    Ok(())
512
}
513

            
514
#[tokio::test]
515
async fn collection_deleted_signal() -> Result<(), Box<dyn std::error::Error>> {
516
    let setup = TestServiceSetup::plain_session(true).await?;
517

            
518
    // Subscribe to CollectionDeleted signal
519
    let signal_stream = setup.service_api.receive_collection_deleted().await?;
520
    tokio::pin!(signal_stream);
521

            
522
    let collection_path = setup.collections[0].inner().path().to_owned();
523

            
524
    // Delete the collection
525
    setup.collections[0].delete(None).await?;
526

            
527
    // Wait for signal with timeout
528
    let signal_result =
529
        tokio::time::timeout(tokio::time::Duration::from_secs(1), signal_stream.next()).await;
530

            
531
    assert!(
532
        signal_result.is_ok(),
533
        "Should receive CollectionDeleted signal"
534
    );
535
    let signal = signal_result.unwrap();
536
    assert!(signal.is_some(), "Signal should not be None");
537

            
538
    let signal_collection = signal.unwrap();
539
    assert_eq!(
540
        signal_collection.as_str(),
541
        collection_path.as_str(),
542
        "Signal should contain the deleted collection path"
543
    );
544

            
545
    Ok(())
546
}
547

            
548
gnome_prompter_test!(
549
    create_item_in_locked_collection_gnome,
550
    create_item_in_locked_collection
551
);
552
plasma_prompter_test!(
553
    create_item_in_locked_collection_plasma,
554
    create_item_in_locked_collection
555
);
556

            
557
22
async fn create_item_in_locked_collection() -> Result<(), Box<dyn std::error::Error>> {
558
6
    let setup = TestServiceSetup::plain_session(true).await?;
559

            
560
8
    let collection = setup
561
        .server
562
4
        .collection_from_path(setup.collections[0].inner().path())
563
6
        .await
564
        .expect("Collection should exist");
565
8
    collection
566
4
        .set_locked(true, setup.keyring_secret.clone())
567
6
        .await?;
568

            
569
    assert!(
570
        setup.collections[0].is_locked().await?,
571
        "Collection should be locked"
572
    );
573

            
574
2
    let secret = oo7::Secret::text("test-password");
575
2
    let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret.clone());
576

            
577
8
    let item = setup.collections[0]
578
        .create_item(
579
            "Test Item",
580
            &[("app", "test"), ("type", "password")],
581
2
            &dbus_secret,
582
            false,
583
2
            None,
584
        )
585
8
        .await?;
586

            
587
    assert!(
588
        !setup.collections[0].is_locked().await?,
589
        "Collection should be unlocked after prompt"
590
    );
591

            
592
6
    let items = setup.collections[0].items().await?;
593
    assert_eq!(items.len(), 1, "Collection should have one item");
594
    assert_eq!(
595
        items[0].inner().path(),
596
        item.inner().path(),
597
        "Created item should be in the collection"
598
    );
599

            
600
6
    let label = item.label().await?;
601
    assert_eq!(label, "Test Item", "Item should have correct label");
602

            
603
6
    let attributes = item.attributes().await?;
604
    assert_eq!(attributes.get("app"), Some(&"test".to_string()));
605
    assert_eq!(attributes.get("type"), Some(&"password".to_string()));
606

            
607
4
    let retrieved_secret = item.secret(&setup.session).await?;
608
    assert_eq!(retrieved_secret.value(), secret.as_bytes());
609

            
610
2
    Ok(())
611
}
612

            
613
gnome_prompter_test!(
614
    delete_locked_collection_with_prompt_gnome,
615
    delete_locked_collection_with_prompt
616
);
617
plasma_prompter_test!(
618
    delete_locked_collection_with_prompt_plasma,
619
    delete_locked_collection_with_prompt
620
);
621

            
622
16
async fn delete_locked_collection_with_prompt() -> Result<(), Box<dyn std::error::Error>> {
623
6
    let setup = TestServiceSetup::plain_session(true).await?;
624
6
    let default_collection = setup.default_collection().await?;
625

            
626
8
    let collection = setup
627
        .server
628
2
        .collection_from_path(default_collection.inner().path())
629
6
        .await
630
        .expect("Collection should exist");
631
8
    collection
632
4
        .set_locked(true, setup.keyring_secret.clone())
633
6
        .await?;
634

            
635
    assert!(
636
        default_collection.is_locked().await?,
637
        "Collection should be locked"
638
    );
639

            
640
4
    let collection_path = default_collection.inner().path().to_owned();
641

            
642
    // Get initial collection count
643
4
    let collections_before = setup.service_api.collections().await?;
644
4
    let initial_count = collections_before.len();
645

            
646
    // Delete the locked collection
647
4
    default_collection.delete(None).await?;
648

            
649
    // Give the system a moment to process the deletion
650
4
    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
651

            
652
    // Verify collection was deleted
653
2
    let collections_after = setup.service_api.collections().await?;
654
    assert_eq!(
655
        collections_after.len(),
656
        initial_count - 1,
657
        "Collection should be deleted after prompt"
658
    );
659

            
660
    // Verify the specific collection is not in the list
661
2
    let collection_paths: Vec<_> = collections_after
662
        .iter()
663
6
        .map(|c| c.inner().path().as_str())
664
        .collect();
665
    assert!(
666
        !collection_paths.contains(&collection_path.as_str()),
667
        "Deleted collection should not be in service collections list"
668
    );
669

            
670
2
    Ok(())
671
}
672

            
673
gnome_prompter_test!(unlock_retry_gnome, unlock_retry);
674
plasma_prompter_test!(unlock_retry_plasma, unlock_retry);
675

            
676
18
async fn unlock_retry() -> Result<(), Box<dyn std::error::Error>> {
677
6
    let setup = TestServiceSetup::plain_session(true).await?;
678
6
    let default_collection = setup.default_collection().await?;
679

            
680
2
    let secret = oo7::Secret::text("test-secret-data");
681
4
    let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret);
682
8
    default_collection
683
2
        .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
684
10
        .await?;
685

            
686
8
    let collection = setup
687
        .server
688
2
        .collection_from_path(default_collection.inner().path())
689
6
        .await
690
        .expect("Collection should exist");
691
8
    collection
692
4
        .set_locked(true, setup.keyring_secret.clone())
693
6
        .await?;
694

            
695
    assert!(
696
        default_collection.is_locked().await?,
697
        "Collection should be locked"
698
    );
699

            
700
4
    setup
701
6
        .set_password_queue(vec![
702
2
            oo7::Secret::from("wrong-password"),
703
2
            oo7::Secret::from("wrong-password2"),
704
2
            oo7::Secret::from("test-password-long-enough"),
705
        ])
706
4
        .await;
707

            
708
8
    let unlocked = setup
709
        .service_api
710
2
        .unlock(&[default_collection.inner().path()], None)
711
8
        .await?;
712

            
713
    assert_eq!(unlocked.len(), 1, "Should have unlocked 1 collection");
714
    assert_eq!(
715
        unlocked[0].as_str(),
716
        default_collection.inner().path().as_str(),
717
        "Should return the collection path"
718
    );
719
    assert!(
720
        !default_collection.is_locked().await?,
721
        "Collection should be unlocked after retry with correct password"
722
    );
723

            
724
2
    Ok(())
725
}
726

            
727
#[tokio::test]
728
async fn locked_collection_operations() -> Result<(), Box<dyn std::error::Error>> {
729
    let setup = TestServiceSetup::plain_session(true).await?;
730

            
731
    // Verify collection is unlocked initially
732
    assert!(
733
        !setup.collections[0].is_locked().await?,
734
        "Collection should start unlocked"
735
    );
736

            
737
    // Lock the collection
738
    let collection = setup
739
        .server
740
        .collection_from_path(setup.collections[0].inner().path())
741
        .await
742
        .expect("Collection should exist");
743
    collection
744
        .set_locked(true, setup.keyring_secret.clone())
745
        .await?;
746

            
747
    // Verify collection is now locked
748
    assert!(
749
        setup.collections[0].is_locked().await?,
750
        "Collection should be locked"
751
    );
752

            
753
    // Test 1: set_label should fail with IsLocked
754
    let result = setup.collections[0].set_label("New Label").await;
755
    assert!(
756
        matches!(result, Err(oo7::dbus::Error::ZBus(zbus::Error::FDO(_)))),
757
        "set_label should fail with IsLocked error, got: {:?}",
758
        result
759
    );
760

            
761
    // Verify read-only operations still work on locked collections
762
    assert!(
763
        setup.collections[0].label().await.is_ok(),
764
        "Should be able to read label of locked collection"
765
    );
766

            
767
    let items = setup.collections[0].items().await?;
768
    assert!(
769
        items.is_empty(),
770
        "Should be able to read items (empty) from locked collection"
771
    );
772

            
773
    Ok(())
774
}