1
use oo7::dbus;
2

            
3
use super::*;
4
use crate::tests::{TestServiceSetup, gnome_prompter_test, plasma_prompter_test};
5

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

            
10
    assert!(
11
        setup.aes_key.is_none(),
12
        "Plain session should not have AES key"
13
    );
14

            
15
    // Should have 2 collections: default + session
16
    assert_eq!(
17
        setup.collections.len(),
18
        2,
19
        "Expected default and session collections"
20
    );
21
    Ok(())
22
}
23

            
24
#[tokio::test]
25
async fn open_session_encrypted() -> Result<(), Box<dyn std::error::Error>> {
26
    let setup = TestServiceSetup::encrypted_session(false).await?;
27
    assert!(
28
        setup.server_public_key.is_some(),
29
        "Encrypted session should have server public key"
30
    );
31
    let key = setup.aes_key.unwrap().clone();
32
    assert_eq!((*key).as_ref().len(), 16, "AES key should be 16 bytes");
33
    Ok(())
34
}
35

            
36
#[tokio::test]
37
async fn session_collection_only() -> Result<(), Box<dyn std::error::Error>> {
38
    let setup = TestServiceSetup::plain_session(false).await?;
39

            
40
    // Should have only session collection (no default)
41
    assert_eq!(
42
        setup.collections.len(),
43
        1,
44
        "Should have exactly one collection"
45
    );
46
    Ok(())
47
}
48

            
49
#[tokio::test]
50
async fn search_items() -> Result<(), Box<dyn std::error::Error>> {
51
    let setup = TestServiceSetup::plain_session(true).await?;
52

            
53
    // Search for items (should return empty initially)
54
    let (unlocked, locked) = setup
55
        .service_api
56
        .search_items(&[("application", "test-app")])
57
        .await?;
58

            
59
    assert!(
60
        unlocked.is_empty(),
61
        "Should have no unlocked items initially"
62
    );
63
    assert!(locked.is_empty(), "Should have no locked items initially");
64

            
65
    // Search with empty attributes - edge case
66
    let attributes: HashMap<&str, &str> = HashMap::default();
67
    let (unlocked, locked) = setup.service_api.search_items(&attributes).await?;
68

            
69
    assert!(
70
        locked.is_empty(),
71
        "Should have no locked items with empty search"
72
    );
73
    assert!(
74
        unlocked.is_empty(),
75
        "Should have no unlocked items with empty search"
76
    );
77

            
78
    // Test with both locked and unlocked items
79
    // Create items in default collection (unlocked)
80
    let secret1 = Secret::text("password1");
81
    let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1);
82

            
83
    setup.collections[0]
84
        .create_item(
85
            "Unlocked Item",
86
            &[("app", "testapp")],
87
            &dbus_secret1,
88
            false,
89
            None,
90
        )
91
        .await?;
92

            
93
    // Create item in default collection and lock it
94
    let secret2 = Secret::text("password2");
95
    let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2);
96

            
97
    let locked_item = setup.collections[0]
98
        .create_item(
99
            "Locked Item",
100
            &[("app", "testapp")],
101
            &dbus_secret2,
102
            false,
103
            None,
104
        )
105
        .await?;
106

            
107
    // Lock just this item (not the whole collection)
108
    let collection = setup
109
        .server
110
        .collection_from_path(setup.collections[0].inner().path())
111
        .await
112
        .expect("Collection should exist");
113

            
114
    let keyring = collection.keyring.read().await;
115
    let unlocked_keyring = keyring.as_ref().unwrap().as_unlocked();
116

            
117
    let locked_item = collection
118
        .item_from_path(locked_item.inner().path())
119
        .await
120
        .unwrap();
121
    locked_item.set_locked(true, unlocked_keyring).await?;
122

            
123
    // Search for items with the shared attribute
124
    let (unlocked, locked) = setup
125
        .service_api
126
        .search_items(&[("app", "testapp")])
127
        .await?;
128

            
129
    assert_eq!(unlocked.len(), 1, "Should find 1 unlocked item");
130
    assert_eq!(locked.len(), 1, "Should find 1 locked item");
131

            
132
    Ok(())
133
}
134

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

            
139
    // Test with empty items list - edge case
140
    let secrets = setup.service_api.secrets(&vec![], &setup.session).await?;
141
    assert!(
142
        secrets.is_empty(),
143
        "Should return empty secrets for empty items list"
144
    );
145

            
146
    // Create two items with different secrets
147
    let secret1 = Secret::text("password1");
148
    let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1.clone());
149

            
150
    let item1 = setup.collections[0]
151
        .create_item("Item 1", &[("app", "test1")], &dbus_secret1, false, None)
152
        .await?;
153

            
154
    let secret2 = Secret::text("password2");
155
    let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2.clone());
156

            
157
    let item2 = setup.collections[0]
158
        .create_item("Item 2", &[("app", "test2")], &dbus_secret2, false, None)
159
        .await?;
160

            
161
    // Get secrets for both items
162
    let item_paths = vec![item1.clone(), item2.clone()];
163
    let secrets = setup
164
        .service_api
165
        .secrets(&item_paths, &setup.session)
166
        .await?;
167

            
168
    // Should have both secrets
169
    assert_eq!(secrets.len(), 2, "Should retrieve both secrets");
170

            
171
    // Verify first secret
172
    let retrieved_secret1 = secrets.get(&item1).unwrap();
173
    assert_eq!(retrieved_secret1.value(), secret1.as_bytes());
174

            
175
    // Verify second secret
176
    let retrieved_secret2 = secrets.get(&item2).unwrap();
177
    assert_eq!(retrieved_secret2.value(), secret2.as_bytes());
178

            
179
    Ok(())
180
}
181

            
182
#[tokio::test]
183
async fn get_secrets_multiple_collections() -> Result<(), Box<dyn std::error::Error>> {
184
    let setup = TestServiceSetup::plain_session(true).await?;
185

            
186
    // Should have 2 collections: default (Login) and session
187
    assert_eq!(setup.collections.len(), 2);
188

            
189
    // Create item in default collection (index 0)
190
    let secret1 = Secret::text("default-password");
191
    let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1.clone());
192

            
193
    let item1 = setup.collections[0]
194
        .create_item(
195
            "Default Item",
196
            &[("app", "default-app")],
197
            &dbus_secret1,
198
            false,
199
            None,
200
        )
201
        .await?;
202

            
203
    // Create item in session collection (index 1)
204
    let secret2 = Secret::text("session-password");
205
    let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2.clone());
206

            
207
    let item2 = setup.collections[1]
208
        .create_item(
209
            "Session Item",
210
            &[("app", "session-app")],
211
            &dbus_secret2,
212
            false,
213
            None,
214
        )
215
        .await?;
216

            
217
    // Get secrets for both items from different collections
218
    let item_paths = vec![item1.clone(), item2.clone()];
219
    let secrets = setup
220
        .service_api
221
        .secrets(&item_paths, &setup.session)
222
        .await?;
223

            
224
    // Should have both secrets
225
    assert_eq!(
226
        secrets.len(),
227
        2,
228
        "Should retrieve secrets from both collections"
229
    );
230

            
231
    // Verify default collection secret
232
    let retrieved_secret1 = secrets.get(&item1).unwrap();
233
    assert_eq!(retrieved_secret1.value(), secret1.as_bytes());
234

            
235
    // Verify session collection secret
236
    let retrieved_secret2 = secrets.get(&item2).unwrap();
237
    assert_eq!(retrieved_secret2.value(), secret2.as_bytes());
238

            
239
    Ok(())
240
}
241

            
242
#[tokio::test]
243
async fn read_alias() -> Result<(), Box<dyn std::error::Error>> {
244
    let setup = TestServiceSetup::plain_session(true).await?;
245

            
246
    // Default collection should have "default" alias
247
    let default_collection = setup.service_api.read_alias("default").await?;
248
    assert!(
249
        default_collection.is_some(),
250
        "Default alias should return a collection"
251
    );
252

            
253
    // Verify it's the Login collection by checking its label
254
    let label = default_collection.as_ref().unwrap().label().await?;
255
    assert_eq!(
256
        label, "Login",
257
        "Default alias should point to Login collection"
258
    );
259

            
260
    // Non-existent alias should return None
261
    let nonexistent = setup.service_api.read_alias("nonexistent").await?;
262
    assert!(
263
        nonexistent.is_none(),
264
        "Non-existent alias should return None"
265
    );
266

            
267
    Ok(())
268
}
269

            
270
#[tokio::test]
271
async fn set_alias() -> Result<(), Box<dyn std::error::Error>> {
272
    let setup = TestServiceSetup::plain_session(true).await?;
273

            
274
    // Set alias for session collection
275
    setup
276
        .service_api
277
        .set_alias("my-alias", &setup.collections[1])
278
        .await?;
279

            
280
    // Read the alias back
281
    let alias_collection = setup.service_api.read_alias("my-alias").await?;
282
    assert!(
283
        alias_collection.is_some(),
284
        "Alias should return a collection"
285
    );
286
    assert_eq!(
287
        alias_collection.unwrap().inner().path(),
288
        setup.collections[1].inner().path(),
289
        "Alias should point to session collection"
290
    );
291

            
292
    Ok(())
293
}
294

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

            
299
    // Create items in default collection
300
    let secret1 = Secret::text("password1");
301
    let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1);
302

            
303
    setup.collections[0]
304
        .create_item(
305
            "Firefox Login",
306
            &[("application", "firefox"), ("type", "login")],
307
            &dbus_secret1,
308
            false,
309
            None,
310
        )
311
        .await?;
312

            
313
    let secret2 = Secret::text("password2");
314
    let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2);
315

            
316
    setup.collections[0]
317
        .create_item(
318
            "Chrome Login",
319
            &[("application", "chrome"), ("type", "login")],
320
            &dbus_secret2,
321
            false,
322
            None,
323
        )
324
        .await?;
325

            
326
    // Create item in session collection
327
    let secret3 = Secret::text("password3");
328
    let dbus_secret3 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret3);
329

            
330
    setup.collections[1]
331
        .create_item(
332
            "Session Item",
333
            &[("application", "firefox"), ("type", "session")],
334
            &dbus_secret3,
335
            false,
336
            None,
337
        )
338
        .await?;
339

            
340
    // Search for all firefox items
341
    let (unlocked, locked) = setup
342
        .service_api
343
        .search_items(&[("application", "firefox")])
344
        .await?;
345

            
346
    assert_eq!(unlocked.len(), 2, "Should find 2 firefox items");
347
    assert!(locked.is_empty(), "Should have no locked items");
348

            
349
    // Search for login type items
350
    let (unlocked, locked) = setup.service_api.search_items(&[("type", "login")]).await?;
351

            
352
    assert_eq!(unlocked.len(), 2, "Should find 2 login items");
353
    assert!(locked.is_empty(), "Should have no locked items");
354

            
355
    // Search for chrome items
356
    let (unlocked, locked) = setup
357
        .service_api
358
        .search_items(&[("application", "chrome")])
359
        .await?;
360

            
361
    assert_eq!(unlocked.len(), 1, "Should find 1 chrome item");
362
    assert!(locked.is_empty(), "Should have no locked items");
363

            
364
    // Search for non-existent
365
    let (unlocked, locked) = setup
366
        .service_api
367
        .search_items(&[("application", "nonexistent")])
368
        .await?;
369

            
370
    assert!(unlocked.is_empty(), "Should find no items");
371
    assert!(locked.is_empty(), "Should have no locked items");
372

            
373
    Ok(())
374
}
375

            
376
#[tokio::test]
377
async fn get_secrets_invalid_session() -> Result<(), Box<dyn std::error::Error>> {
378
    let setup = TestServiceSetup::plain_session(true).await?;
379

            
380
    // Create an item
381
    let secret = Secret::text("test-password");
382
    let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret);
383

            
384
    let item = setup.collections[0]
385
        .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
386
        .await?;
387

            
388
    // Try to get secrets with invalid session path
389
    let invalid_session =
390
        dbus::api::Session::new(&setup.client_conn, "/invalid/session/path").await?;
391
    let result = setup.service_api.secrets(&[item], &invalid_session).await;
392

            
393
    assert!(
394
        matches!(
395
            result,
396
            Err(oo7::dbus::Error::Service(
397
                oo7::dbus::ServiceError::NoSession(_)
398
            ))
399
        ),
400
        "Should be NoSession error"
401
    );
402

            
403
    Ok(())
404
}
405

            
406
#[tokio::test]
407
async fn set_alias_invalid_collection() -> Result<(), Box<dyn std::error::Error>> {
408
    let setup = TestServiceSetup::plain_session(true).await?;
409

            
410
    // Try to set alias for non-existent collection
411
    let invalid_collection = dbus::api::Collection::new(
412
        &setup.client_conn,
413
        "/org/freedesktop/secrets/collection/nonexistent",
414
    )
415
    .await?;
416
    let result = setup
417
        .service_api
418
        .set_alias("test-alias", &invalid_collection)
419
        .await;
420

            
421
    assert!(
422
        matches!(
423
            result,
424
            Err(oo7::dbus::Error::Service(
425
                oo7::dbus::ServiceError::NoSuchObject(_)
426
            ))
427
        ),
428
        "Should be NoSuchObject error"
429
    );
430

            
431
    Ok(())
432
}
433

            
434
#[tokio::test]
435
async fn get_secrets_with_non_existent_items() -> Result<(), Box<dyn std::error::Error>> {
436
    let setup = TestServiceSetup::plain_session(true).await?;
437

            
438
    // Create one real item
439
    let secret = Secret::text("password1");
440
    let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret.clone());
441

            
442
    let item1 = setup.collections[0]
443
        .create_item("Item 1", &[("app", "test")], &dbus_secret, false, None)
444
        .await?;
445

            
446
    // Create a fake item path that doesn't exist
447
    let fake_item = dbus::api::Item::new(
448
        &setup.client_conn,
449
        "/org/freedesktop/secrets/collection/Login/999",
450
    )
451
    .await?;
452

            
453
    // Request secrets for both real and fake items
454
    let item_paths = vec![item1.clone(), fake_item];
455
    let secrets = setup
456
        .service_api
457
        .secrets(&item_paths, &setup.session)
458
        .await?;
459

            
460
    // Should only get the secret for the real item
461
    assert_eq!(
462
        secrets.len(),
463
        1,
464
        "Should only retrieve secret for existing item"
465
    );
466
    assert!(secrets.contains_key(&item1), "Should have item1 secret");
467

            
468
    Ok(())
469
}
470

            
471
#[tokio::test]
472
async fn search_items_across_collections() -> Result<(), Box<dyn std::error::Error>> {
473
    let setup = TestServiceSetup::plain_session(true).await?;
474

            
475
    let collections = setup.service_api.collections().await?;
476
    assert_eq!(collections.len(), 2, "Should have 2 collections");
477

            
478
    // Create item in first collection
479
    let secret1 = Secret::text("password1");
480
    let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1);
481

            
482
    collections[0]
483
        .create_item(
484
            "Default Item",
485
            &[("shared", "attr")],
486
            &dbus_secret1,
487
            false,
488
            None,
489
        )
490
        .await?;
491

            
492
    // Create item in second collection with same attributes
493
    let secret2 = Secret::text("password2");
494
    let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2);
495

            
496
    collections[1]
497
        .create_item(
498
            "Session Item",
499
            &[("shared", "attr")],
500
            &dbus_secret2,
501
            false,
502
            None,
503
        )
504
        .await?;
505

            
506
    // Search should find items from both collections
507
    let (unlocked, locked) = setup
508
        .service_api
509
        .search_items(&[("shared", "attr")])
510
        .await?;
511

            
512
    assert_eq!(unlocked.len(), 2, "Should find items from both collections");
513
    assert!(locked.is_empty(), "Should have no locked items");
514

            
515
    Ok(())
516
}
517

            
518
#[tokio::test]
519
async fn unlock_edge_cases() -> Result<(), Box<dyn std::error::Error>> {
520
    let setup = TestServiceSetup::plain_session(true).await?;
521

            
522
    // Test 1: Empty object list
523
    let items: Vec<ObjectPath<'_>> = vec![];
524
    let unlocked = setup.service_api.unlock(&items, None).await?;
525
    assert!(unlocked.is_empty(), "Should return empty for empty input");
526

            
527
    // Test 2: Non-existent objects
528
    let fake_collection = dbus::api::Collection::new(
529
        &setup.client_conn,
530
        "/org/freedesktop/secrets/collection/NonExistent",
531
    )
532
    .await?;
533

            
534
    let fake_item = dbus::api::Item::new(
535
        &setup.client_conn,
536
        "/org/freedesktop/secrets/collection/Login/999",
537
    )
538
    .await?;
539

            
540
    let unlocked = setup
541
        .service_api
542
        .unlock(
543
            &[fake_collection.inner().path(), fake_item.inner().path()],
544
            None,
545
        )
546
        .await?;
547

            
548
    assert!(
549
        unlocked.is_empty(),
550
        "Should have no unlocked objects for non-existent paths"
551
    );
552

            
553
    // Test 3: Already unlocked objects
554
    let secret = Secret::text("test-password");
555
    let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret);
556

            
557
    let item = setup.collections[0]
558
        .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
559
        .await?;
560

            
561
    // Verify item is unlocked
562
    assert!(!item.is_locked().await?, "Item should be unlocked");
563

            
564
    // Try to unlock already unlocked item
565
    let unlocked = setup
566
        .service_api
567
        .unlock(&[item.inner().path()], None)
568
        .await?;
569

            
570
    assert_eq!(unlocked.len(), 1, "Should return the already-unlocked item");
571
    assert_eq!(
572
        unlocked[0].as_str(),
573
        item.inner().path().as_str(),
574
        "Should return the same item path"
575
    );
576

            
577
    // Also test with collection (starts unlocked by default)
578
    assert!(
579
        !setup.collections[0].is_locked().await?,
580
        "Collection should be unlocked"
581
    );
582

            
583
    let unlocked = setup
584
        .service_api
585
        .unlock(&[setup.collections[0].inner().path()], None)
586
        .await?;
587

            
588
    assert_eq!(
589
        unlocked.len(),
590
        1,
591
        "Should return the already-unlocked collection"
592
    );
593

            
594
    Ok(())
595
}
596

            
597
#[tokio::test]
598
async fn lock_non_existent_objects() -> Result<(), Box<dyn std::error::Error>> {
599
    let setup = TestServiceSetup::encrypted_session(true).await?;
600

            
601
    // Test with empty object list
602
    let items: Vec<ObjectPath<'_>> = vec![];
603
    let locked = setup.service_api.lock(&items, None).await?;
604
    assert!(locked.is_empty(), "Should return empty for empty input");
605

            
606
    // Test locking non-existent objects
607
    let fake_collection = dbus::api::Collection::new(
608
        &setup.client_conn,
609
        "/org/freedesktop/secrets/collection/NonExistent",
610
    )
611
    .await?;
612

            
613
    let fake_item = dbus::api::Item::new(
614
        &setup.client_conn,
615
        "/org/freedesktop/secrets/collection/Login/999",
616
    )
617
    .await?;
618

            
619
    let locked = setup
620
        .service_api
621
        .lock(
622
            &[fake_collection.inner().path(), fake_item.inner().path()],
623
            None,
624
        )
625
        .await?;
626

            
627
    assert!(
628
        locked.is_empty(),
629
        "Should have no locked objects for non-existent paths"
630
    );
631

            
632
    Ok(())
633
}
634

            
635
gnome_prompter_test!(unlock_collection_prompt_gnome, unlock_collection_prompt);
636
plasma_prompter_test!(unlock_collection_prompt_plasma, unlock_collection_prompt);
637

            
638
20
async fn unlock_collection_prompt() -> Result<(), Box<dyn std::error::Error>> {
639
6
    let setup = TestServiceSetup::plain_session(true).await?;
640

            
641
    // Lock the collection using server-side API
642
8
    let collection = setup
643
        .server
644
4
        .collection_from_path(setup.collections[0].inner().path())
645
6
        .await
646
        .expect("Collection should exist");
647
8
    collection
648
4
        .set_locked(true, setup.keyring_secret.clone())
649
6
        .await?;
650

            
651
    assert!(
652
        setup.collections[0].is_locked().await?,
653
        "Collection should be locked"
654
    );
655

            
656
    // Test 1: Unlock with accept
657
8
    let unlocked = setup
658
        .service_api
659
4
        .unlock(&[setup.collections[0].inner().path()], None)
660
8
        .await?;
661

            
662
    assert_eq!(unlocked.len(), 1, "Should have unlocked 1 collection");
663
    assert_eq!(
664
        unlocked[0].as_str(),
665
        setup.collections[0].inner().path().as_str(),
666
        "Should return the collection path"
667
    );
668
    assert!(
669
        !setup.collections[0].is_locked().await?,
670
        "Collection should be unlocked after accepting prompt"
671
    );
672

            
673
    // Lock the collection again for dismiss test
674
8
    collection
675
4
        .set_locked(true, setup.keyring_secret.clone())
676
6
        .await?;
677
    assert!(
678
        setup.collections[0].is_locked().await?,
679
        "Collection should be locked again"
680
    );
681

            
682
    // Test 2: Unlock with dismiss
683
4
    setup.set_password_accept(false).await;
684
6
    let result = setup
685
        .service_api
686
2
        .unlock(&[setup.collections[0].inner().path()], None)
687
8
        .await;
688

            
689
    assert!(
690
        matches!(result, Err(oo7::dbus::Error::Dismissed)),
691
        "Should return Dismissed error when prompt dismissed"
692
    );
693
    assert!(
694
        setup.collections[0].is_locked().await?,
695
        "Collection should still be locked after dismissing prompt"
696
    );
697

            
698
2
    Ok(())
699
}
700

            
701
gnome_prompter_test!(unlock_item_prompt_gnome, unlock_item_prompt);
702
plasma_prompter_test!(unlock_item_prompt_plasma, unlock_item_prompt);
703

            
704
24
async fn unlock_item_prompt() -> Result<(), Box<dyn std::error::Error>> {
705
6
    let setup = TestServiceSetup::plain_session(true).await?;
706

            
707
    // Create an item
708
2
    let secret = Secret::text("test-password");
709
4
    let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret);
710
6
    let default_collection = setup.service_api.read_alias("default").await?.unwrap();
711
8
    let item = default_collection
712
2
        .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
713
8
        .await?;
714

            
715
    // Lock the collection (which locks the item)
716
8
    let collection = setup
717
        .server
718
4
        .collection_from_path(default_collection.inner().path())
719
6
        .await
720
        .expect("Collection should exist");
721
8
    collection
722
4
        .set_locked(true, setup.keyring_secret.clone())
723
6
        .await?;
724

            
725
    assert!(
726
        item.is_locked().await?,
727
        "Item should be locked when collection is locked"
728
    );
729

            
730
    // Test 1: Unlock with accept
731
8
    let unlocked = setup
732
        .service_api
733
4
        .unlock(&[item.inner().path()], None)
734
8
        .await?;
735

            
736
    assert_eq!(unlocked.len(), 1, "Should have unlocked 1 item");
737
    assert_eq!(
738
        unlocked[0].as_str(),
739
        item.inner().path().as_str(),
740
        "Should return the item path"
741
    );
742
    assert!(
743
        !item.is_locked().await?,
744
        "Item should be unlocked after accepting prompt"
745
    );
746

            
747
    // Lock the item again for dismiss test
748
8
    collection
749
4
        .set_locked(true, setup.keyring_secret.clone())
750
6
        .await?;
751
    assert!(item.is_locked().await?, "Item should be locked again");
752

            
753
    // Test 2: Unlock with dismiss
754
4
    setup.set_password_accept(false).await;
755
4
    let result = setup.service_api.unlock(&[item.inner().path()], None).await;
756

            
757
    assert!(
758
        matches!(result, Err(oo7::dbus::Error::Dismissed)),
759
        "Should return Dismissed error when prompt dismissed"
760
    );
761
    assert!(
762
        item.is_locked().await?,
763
        "Item should still be locked after dismissing prompt"
764
    );
765

            
766
2
    Ok(())
767
}
768

            
769
#[tokio::test]
770
async fn lock_item_in_unlocked_collection() -> Result<(), Box<dyn std::error::Error>> {
771
    let setup = TestServiceSetup::plain_session(true).await?;
772

            
773
    // Create an item (starts unlocked)
774
    let secret = Secret::text("test-password");
775
    let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret);
776
    let item = setup.collections[0]
777
        .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
778
        .await?;
779

            
780
    assert!(!item.is_locked().await?, "Item should start unlocked");
781
    assert!(
782
        !setup.collections[0].is_locked().await?,
783
        "Collection should be unlocked"
784
    );
785

            
786
    // When collection is unlocked, locking an item should happen directly without a
787
    // prompt
788
    let locked = setup.service_api.lock(&[item.inner().path()], None).await?;
789

            
790
    assert_eq!(locked.len(), 1, "Should have locked 1 item");
791
    assert_eq!(
792
        locked[0].as_str(),
793
        item.inner().path().as_str(),
794
        "Should return the item path"
795
    );
796
    assert!(item.is_locked().await?, "Item should be locked directly");
797

            
798
    // Unlock the item again (using service API to unlock just the item)
799
    let unlocked = setup
800
        .service_api
801
        .unlock(&[item.inner().path()], None)
802
        .await?;
803
    assert_eq!(unlocked.len(), 1, "Should have unlocked 1 item");
804
    assert!(!item.is_locked().await?, "Item should be unlocked again");
805

            
806
    // Locking again should work the same way (no prompt)
807
    let locked = setup.service_api.lock(&[item.inner().path()], None).await?;
808
    assert_eq!(locked.len(), 1, "Should have locked 1 item again");
809
    assert!(item.is_locked().await?, "Item should be locked again");
810

            
811
    Ok(())
812
}
813

            
814
#[tokio::test]
815
async fn lock_collection_no_prompt() -> Result<(), Box<dyn std::error::Error>> {
816
    let setup = TestServiceSetup::plain_session(true).await?;
817

            
818
    // Collection starts unlocked
819
    assert!(
820
        !setup.collections[0].is_locked().await?,
821
        "Collection should start unlocked"
822
    );
823

            
824
    // Lock the collection
825
    let locked = setup
826
        .service_api
827
        .lock(&[setup.collections[0].inner().path()], None)
828
        .await?;
829

            
830
    assert_eq!(locked.len(), 1, "Should have locked 1 collection");
831
    assert_eq!(
832
        locked[0].as_str(),
833
        setup.collections[0].inner().path().as_str(),
834
        "Should return the collection path"
835
    );
836
    assert!(
837
        setup.collections[0].is_locked().await?,
838
        "Collection should be locked instantly"
839
    );
840

            
841
    // Unlock the collection
842
    let collection = setup
843
        .server
844
        .collection_from_path(setup.collections[0].inner().path())
845
        .await
846
        .expect("Collection should exist");
847
    collection
848
        .set_locked(false, setup.keyring_secret.clone())
849
        .await?;
850
    assert!(
851
        !setup.collections[0].is_locked().await?,
852
        "Collection should be unlocked"
853
    );
854

            
855
    // Lock again to verify it works multiple times
856
    let locked = setup
857
        .service_api
858
        .lock(&[setup.collections[0].inner().path()], None)
859
        .await?;
860

            
861
    assert_eq!(locked.len(), 1, "Should have locked 1 collection again");
862
    assert!(
863
        setup.collections[0].is_locked().await?,
864
        "Collection should be locked again"
865
    );
866

            
867
    Ok(())
868
}
869

            
870
gnome_prompter_test!(
871
    create_collection_basic_gnome,
872
    create_collection_basic,
873
    serial_test::serial(xdg_env)
874
);
875
plasma_prompter_test!(
876
    create_collection_basic_plasma,
877
    create_collection_basic,
878
    serial_test::serial(xdg_env)
879
);
880

            
881
16
async fn create_collection_basic() -> Result<(), Box<dyn std::error::Error>> {
882
6
    let setup = TestServiceSetup::plain_session(true).await?;
883

            
884
    // Get initial collection count
885
4
    let initial_collections = setup.service_api.collections().await?;
886
4
    let initial_count = initial_collections.len();
887

            
888
    // Create a new collection
889
8
    let collection = setup
890
        .service_api
891
2
        .create_collection("MyNewKeyring", Some("my-custom-alias"), None)
892
8
        .await?;
893

            
894
    // Verify collection appears in collections list
895
4
    let collections = setup.service_api.collections().await?;
896
    assert_eq!(
897
        collections.len(),
898
        initial_count + 1,
899
        "Should have one more collection"
900
    );
901

            
902
    // Verify the collection label
903
6
    let label = collection.label().await?;
904
    assert_eq!(
905
        label, "MyNewKeyring",
906
        "Collection should have correct label"
907
    );
908

            
909
    // Verify the keyring file exists on disk
910
8
    let server_collection = setup
911
        .server
912
4
        .collection_from_path(collection.inner().path())
913
6
        .await
914
        .expect("Collection should exist on server");
915
4
    let keyring_guard = server_collection.keyring.read().await;
916
4
    let keyring_path = keyring_guard.as_ref().unwrap().path().unwrap();
917

            
918
    assert!(
919
        keyring_path.exists(),
920
        "Keyring file should exist on disk at {:?}",
921
        keyring_path
922
    );
923

            
924
    // Verify the alias was set
925
6
    let alias_collection = setup.service_api.read_alias("my-custom-alias").await?;
926
    assert!(
927
        alias_collection.is_some(),
928
        "Should be able to read collection by alias"
929
    );
930
    assert_eq!(
931
        alias_collection.unwrap().inner().path(),
932
        collection.inner().path(),
933
        "Alias should point to the new collection"
934
    );
935

            
936
4
    tokio::fs::remove_file(keyring_path).await?;
937

            
938
2
    Ok(())
939
}
940

            
941
gnome_prompter_test!(
942
    create_collection_signal_gnome,
943
    create_collection_signal,
944
    serial_test::serial(xdg_env)
945
);
946
plasma_prompter_test!(
947
    create_collection_signal_plasma,
948
    create_collection_signal,
949
    serial_test::serial(xdg_env)
950
);
951

            
952
13
async fn create_collection_signal() -> Result<(), Box<dyn std::error::Error>> {
953
6
    let setup = TestServiceSetup::plain_session(true).await?;
954

            
955
    // Subscribe to CollectionCreated signal
956
6
    let signal_stream = setup.service_api.receive_collection_created().await?;
957
4
    tokio::pin!(signal_stream);
958

            
959
    // Create a new collection
960
8
    let collection = setup
961
        .service_api
962
2
        .create_collection("TestKeyring", None, None)
963
8
        .await?;
964

            
965
    // Wait for signal with timeout
966
4
    let signal_result =
967
        tokio::time::timeout(tokio::time::Duration::from_secs(1), signal_stream.next()).await;
968

            
969
    assert!(
970
        signal_result.is_ok(),
971
        "Should receive CollectionCreated signal"
972
    );
973
4
    let signal = signal_result.unwrap();
974
    assert!(signal.is_some(), "Signal should not be None");
975

            
976
4
    let signal_collection = signal.unwrap();
977
    assert_eq!(
978
        signal_collection.inner().path().as_str(),
979
        collection.inner().path().as_str(),
980
        "Signal should contain the created collection path"
981
    );
982

            
983
8
    let server_collection = setup
984
        .server
985
4
        .collection_from_path(collection.inner().path())
986
6
        .await
987
        .expect("Collection should exist on server");
988
4
    let keyring_guard = server_collection.keyring.read().await;
989
4
    let keyring_path = keyring_guard.as_ref().unwrap().path().unwrap();
990
3
    tokio::fs::remove_file(keyring_path).await?;
991
2
    Ok(())
992
}
993

            
994
gnome_prompter_test!(
995
    create_collection_and_add_items_gnome,
996
    create_collection_and_add_items,
997
    serial_test::serial(xdg_env)
998
);
999
plasma_prompter_test!(
    create_collection_and_add_items_plasma,
    create_collection_and_add_items,
    serial_test::serial(xdg_env)
);
20
async fn create_collection_and_add_items() -> Result<(), Box<dyn std::error::Error>> {
6
    let setup = TestServiceSetup::plain_session(true).await?;
    // Create a new collection
8
    let collection = setup
        .service_api
2
        .create_collection("ItemTestKeyring", None, None)
8
        .await?;
    // Verify collection is unlocked and ready for items
    assert!(
        !collection.is_locked().await?,
        "New collection should be unlocked"
    );
    // Create an item in the new collection
2
    let secret = oo7::Secret::text("hello-world-test");
2
    let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret.clone());
8
    let item = collection
        .create_item(
            "Test Item",
            &[("app", "test-app")],
2
            &dbus_secret,
            false,
2
            None,
        )
8
        .await?;
    // Verify item was created
6
    let items = collection.items().await?;
    assert_eq!(items.len(), 1, "Should have one item in new collection");
    assert_eq!(
        items[0].inner().path(),
        item.inner().path(),
        "Item path should match"
    );
    // Verify we can retrieve the secret
6
    let retrieved_secret = item.secret(&setup.session).await?;
    assert_eq!(
        retrieved_secret.value(),
        secret.as_bytes(),
        "Should be able to retrieve secret from item in new collection"
    );
8
    let server_collection = setup
        .server
4
        .collection_from_path(collection.inner().path())
6
        .await
        .expect("Collection should exist on server");
4
    let keyring_guard = server_collection.keyring.read().await;
4
    let keyring_path = keyring_guard.as_ref().unwrap().path().unwrap();
4
    tokio::fs::remove_file(&keyring_path).await?;
2
    Ok(())
}
gnome_prompter_test!(
    create_collection_dismissed_gnome,
    create_collection_dismissed,
    serial_test::serial(xdg_env)
);
plasma_prompter_test!(
    create_collection_dismissed_plasma,
    create_collection_dismissed,
    serial_test::serial(xdg_env)
);
10
async fn create_collection_dismissed() -> Result<(), Box<dyn std::error::Error>> {
6
    let setup = TestServiceSetup::plain_session(true).await?;
    // Get initial collection count
4
    let initial_collections = setup.service_api.collections().await?;
4
    let initial_count = initial_collections.len();
    // Set mock prompter to dismiss
2
    setup.set_password_accept(false).await;
    // Try to create a collection
6
    let result = setup
        .service_api
2
        .create_collection("DismissedKeyring", None, None)
8
        .await;
    // Should get Dismissed error
    assert!(
        matches!(result, Err(oo7::dbus::Error::Dismissed)),
        "Should return Dismissed error when prompt dismissed"
    );
    // Verify collection was NOT created
4
    let collections = setup.service_api.collections().await?;
    assert_eq!(
        collections.len(),
        initial_count,
        "Should not have created a new collection after dismissal"
    );
2
    Ok(())
}
#[tokio::test]
async fn complete_collection_creation_no_pending() -> Result<(), Box<dyn std::error::Error>> {
    let setup = TestServiceSetup::plain_session(true).await?;
    // Try to complete collection creation with a prompt path that has no pending
    // collection
    let fake_prompt_path = ObjectPath::try_from("/org/freedesktop/secrets/prompt/p999").unwrap();
    let secret = Secret::from("test-password-long-enough");
    let result = setup
        .server
        .complete_collection_creation(&fake_prompt_path, secret)
        .await;
    // Should get NoSuchObject error
    assert!(
        matches!(result, Err(ServiceError::NoSuchObject(_))),
        "Should return NoSuchObject error when no pending collection exists"
    );
    Ok(())
}
#[tokio::test]
#[serial_test::serial(xdg_env)]
async fn discover_v1_keyrings() -> Result<(), Box<dyn std::error::Error>> {
    let service = Service::default();
    // Set up a temporary data directory
    let temp_dir = tempfile::tempdir()?;
    unsafe { std::env::set_var("XDG_DATA_HOME", temp_dir.path()) };
    // Create v1 keyrings directory
    let v1_dir = temp_dir.path().join("keyrings/v1");
    tokio::fs::create_dir_all(&v1_dir).await?;
    // Test 1: Empty directory
    let discovered = service.discover_keyrings(None).await?;
    assert!(
        discovered.is_empty(),
        "Should discover no keyrings in empty directory"
    );
    // Create multiple keyrings with different passwords
    // Add items to each so password validation works
    let secret1 = Secret::from("password-for-work");
    let keyring1 = UnlockedKeyring::open("work", secret1.clone()).await?;
    keyring1
        .create_item(
            "Work Item",
            &[("type", "work")],
            Secret::text("work-secret"),
            false,
        )
        .await?;
    keyring1.write().await?;
    let secret2 = Secret::from("password-for-personal");
    let keyring2 = UnlockedKeyring::open("personal", secret2.clone()).await?;
    keyring2
        .create_item(
            "Personal Item",
            &[("type", "personal")],
            Secret::text("personal-secret"),
            false,
        )
        .await?;
    keyring2.write().await?;
    // Create a "login" keyring which should get the default alias
    let secret3 = Secret::from("password-for-login");
    let keyring3 = UnlockedKeyring::open("login", secret3.clone()).await?;
    keyring3
        .create_item(
            "Login Item",
            &[("type", "login")],
            Secret::text("login-secret"),
            false,
        )
        .await?;
    keyring3.write().await?;
    // Create some non-keyring files that should be skipped
    tokio::fs::write(v1_dir.join("README.txt"), b"This is a readme").await?;
    tokio::fs::write(v1_dir.join("config.json"), b"{}").await?;
    tokio::fs::create_dir(v1_dir.join("subdir")).await?;
    // Test 2: Discover without any password, all should be locked
    let discovered = service.discover_keyrings(None).await?;
    assert_eq!(discovered.len(), 3, "Should discover 3 keyrings");
    for (_, _, keyring) in &discovered {
        assert!(
            keyring.is_locked(),
            "All keyrings should be locked without secret"
        );
    }
    // Test 3: Discover with one password, only that keyring should be unlocked
    let discovered = service.discover_keyrings(Some(secret1.clone())).await?;
    assert_eq!(discovered.len(), 3, "Should discover 3 keyrings");
    let work_keyring = discovered
        .iter()
        .find(|(label, _, _)| label == "Work")
        .unwrap();
    assert!(
        !work_keyring.2.is_locked(),
        "Work keyring should be unlocked with correct password"
    );
    let personal_keyring = discovered
        .iter()
        .find(|(label, _, _)| label == "Personal")
        .unwrap();
    assert!(
        personal_keyring.2.is_locked(),
        "Personal keyring should be locked with wrong password"
    );
    // Test 4: Verify login keyring gets default alias
    let login_keyring = discovered
        .iter()
        .find(|(label, _, _)| label == "Login")
        .unwrap();
    assert_eq!(
        login_keyring.1,
        oo7::dbus::Service::DEFAULT_COLLECTION,
        "Login keyring should have default alias"
    );
    assert!(
        login_keyring.2.is_locked(),
        "Login keyring should be locked with wrong password"
    );
    // Test 5: Verify labels are properly capitalized
    let labels: Vec<_> = discovered
        .iter()
        .map(|(label, _, _)| label.as_str())
        .collect();
    assert!(labels.contains(&"Work"), "Should have Work with capital W");
    assert!(
        labels.contains(&"Personal"),
        "Should have Personal with capital P"
    );
    assert!(
        labels.contains(&"Login"),
        "Should have Login with capital L"
    );
    // Clean up
    unsafe { std::env::remove_var("XDG_DATA_HOME") };
    Ok(())
}
#[tokio::test]
#[serial_test::serial(xdg_env)]
async fn discover_v0_keyrings() -> Result<(), Box<dyn std::error::Error>> {
    let service = Service::default();
    let temp_dir = tempfile::tempdir()?;
    unsafe { std::env::set_var("XDG_DATA_HOME", temp_dir.path()) };
    let keyrings_dir = temp_dir.path().join("keyrings");
    let v1_dir = keyrings_dir.join("v1");
    tokio::fs::create_dir_all(&keyrings_dir).await?;
    tokio::fs::create_dir_all(&v1_dir).await?;
    // Copy the existing v0 keyring fixture
    let v0_secret = Secret::from("test");
    let fixture_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .unwrap()
        .join("client/fixtures/legacy.keyring");
    let v0_path = keyrings_dir.join("legacy.keyring");
    tokio::fs::copy(&fixture_path, &v0_path).await?;
    // Create a v1 keyring for mixed scenario
    let v1_secret = Secret::from("v1-password");
    let v1_keyring = UnlockedKeyring::open("modern", v1_secret.clone()).await?;
    v1_keyring
        .create_item(
            "V1 Item",
            &[("type", "v1")],
            Secret::text("v1-secret"),
            false,
        )
        .await?;
    v1_keyring.write().await?;
    // Test 1: Discover without secret, v0 marked for migration, v1 locked
    let discovered = service.discover_keyrings(None).await?;
    assert_eq!(discovered.len(), 1, "Should discover v1 keyring only");
    assert!(discovered[0].2.is_locked(), "V1 should be locked");
    let pending = service.pending_migrations.lock().await;
    assert_eq!(pending.len(), 1, "V0 should be pending migration");
    assert!(pending.contains_key("legacy"));
    drop(pending);
    // Test 2: Discover with v0 secret, v0 migrated, v1 locked
    service.pending_migrations.lock().await.clear();
    let discovered = service.discover_keyrings(Some(v0_secret.clone())).await?;
    assert_eq!(discovered.len(), 2, "Should discover both keyrings");
    let legacy = discovered.iter().find(|(l, _, _)| l == "Legacy").unwrap();
    assert!(!legacy.2.is_locked(), "V0 should be migrated and unlocked");
    assert_eq!(
        service.pending_migrations.lock().await.len(),
        0,
        "No pending after successful migration"
    );
    // Verify v1 file was created
    let v1_migrated = temp_dir.path().join("keyrings/v1/legacy.keyring");
    assert!(v1_migrated.exists(), "V1 file should exist after migration");
    // Test 3: Discover with wrong v0 secret,  marked for pending migration
    tokio::fs::remove_file(&v1_migrated).await?;
    service.pending_migrations.lock().await.clear();
    // Restore the v0 file for this test
    tokio::fs::copy(&fixture_path, &v0_path).await?;
    let wrong_secret = Secret::from("wrong-password");
    let discovered = service.discover_keyrings(Some(wrong_secret)).await?;
    assert_eq!(
        discovered.len(),
        1,
        "Only v1 should be discovered with wrong v0 password"
    );
    assert_eq!(
        service.pending_migrations.lock().await.len(),
        1,
        "V0 should be pending with wrong password"
    );
    unsafe { std::env::remove_var("XDG_DATA_HOME") };
    Ok(())
}