1
// org.freedesktop.Secret.Item
2

            
3
use std::{collections::HashMap, sync::Arc};
4

            
5
use oo7::dbus::{ServiceError, api::DBusSecretInner};
6
use tokio::sync::Mutex;
7
use zbus::zvariant::{ObjectPath, OwnedObjectPath};
8

            
9
use crate::{Service, collection::Collection, error::custom_service_error};
10

            
11
#[derive(Debug, Clone)]
12
pub struct Item {
13
    // Properties
14
    pub(super) inner: Arc<Mutex<Option<oo7::file::Item>>>,
15
    // Other attributes
16
    service: Service,
17
    collection_path: OwnedObjectPath,
18
    path: OwnedObjectPath,
19
}
20

            
21
#[zbus::interface(name = "org.freedesktop.Secret.Item")]
22
impl Item {
23
    #[zbus(out_args("Prompt"))]
24
8
    pub async fn delete(&self) -> Result<OwnedObjectPath, ServiceError> {
25
8
        let Some(collection) = self
26
            .service
27
4
            .collection_from_path(&self.collection_path)
28
6
            .await
29
        else {
30
            return Err(ServiceError::NoSuchObject(format!(
31
                "Collection `{}` does not exist.",
32
                &self.collection_path
33
            )));
34
        };
35

            
36
        // Check if item or collection is locked
37
6
        if self.is_locked().await || collection.is_locked().await {
38
            // Create a prompt to unlock and delete the item
39
            let prompt = crate::prompt::Prompt::new(
40
4
                self.service.clone(),
41
2
                crate::prompt::PromptRole::Unlock,
42
4
                collection.label().await,
43
4
                Some(collection.clone()),
44
            )
45
6
            .await;
46
4
            let prompt_path = OwnedObjectPath::from(prompt.path().clone());
47

            
48
4
            let item_self = self.clone();
49
2
            let coll = collection.clone();
50
10
            let action =
51
                crate::prompt::PromptAction::new(move |unlock_secret: oo7::Secret| async move {
52
                    // Unlock the collection
53
4
                    coll.set_locked(false, Some(unlock_secret)).await?;
54

            
55
                    // Now delete the item
56
2
                    item_self.delete_unlocked(&coll).await?;
57

            
58
4
                    Ok(zbus::zvariant::Value::new(OwnedObjectPath::default())
59
2
                        .try_into_owned()
60
2
                        .unwrap())
61
                });
62

            
63
2
            prompt.set_action(action).await;
64

            
65
            // Register the prompt
66
6
            self.service
67
4
                .register_prompt(prompt_path.clone(), prompt.clone())
68
4
                .await;
69

            
70
8
            self.service
71
                .object_server()
72
2
                .at(&prompt_path, prompt)
73
6
                .await?;
74

            
75
4
            tracing::debug!(
76
                "Delete prompt created at `{}` for locked item `{}`",
77
                prompt_path,
78
                self.path
79
            );
80

            
81
2
            return Ok(prompt_path);
82
        }
83

            
84
        // Item and collection are unlocked, proceed directly
85
2
        self.delete_unlocked(&collection).await?;
86
2
        Ok(OwnedObjectPath::default())
87
    }
88

            
89
    #[zbus(out_args("secret"))]
90
2
    pub async fn get_secret(
91
        &self,
92
        session: OwnedObjectPath,
93
    ) -> Result<(DBusSecretInner,), ServiceError> {
94
4
        let Some(session) = self.service.session(&session).await else {
95
5
            tracing::error!("The session `{}` does not exist.", session);
96
4
            return Err(ServiceError::NoSession(format!(
97
                "The session `{session}` does not exist."
98
            )));
99
        };
100

            
101
4
        if self.is_locked().await {
102
5
            tracing::error!("Cannot get secret of a locked object `{}`", self.path);
103
4
            return Err(ServiceError::IsLocked(format!(
104
                "Cannot get secret of a locked object `{}`.",
105
                self.path
106
            )));
107
        }
108

            
109
4
        let inner = self.inner.lock().await;
110
4
        let inner = inner.as_ref().unwrap();
111
4
        let secret = inner.as_unlocked().secret();
112
4
        let content_type = secret.content_type();
113

            
114
4
        tracing::debug!("Secret retrieved from the item: {}.", self.path);
115

            
116
4
        match session.aes_key() {
117
2
            Some(key) => {
118
4
                let iv = oo7::crypto::generate_iv().map_err(|err| {
119
                    custom_service_error(&format!("Failed to generate iv {err}."))
120
                })?;
121
4
                let encrypted = oo7::crypto::encrypt(secret, &key, &iv).map_err(|err| {
122
                    custom_service_error(&format!("Failed to encrypt secret {err}."))
123
                })?;
124

            
125
2
                Ok((DBusSecretInner(
126
4
                    session.path().clone().into(),
127
2
                    iv,
128
2
                    encrypted,
129
                    content_type,
130
                ),))
131
            }
132
2
            None => Ok((DBusSecretInner(
133
2
                session.path().clone().into(),
134
2
                Vec::new(),
135
4
                secret.to_vec(),
136
                content_type,
137
            ),)),
138
        }
139
    }
140

            
141
11
    pub async fn set_secret(&self, secret: DBusSecretInner) -> Result<(), ServiceError> {
142
2
        let DBusSecretInner(session, iv, secret, content_type) = secret;
143

            
144
4
        let Some(session) = self.service.session(&session).await else {
145
5
            tracing::error!("The session `{}` does not exist.", session);
146
4
            return Err(ServiceError::NoSession(format!(
147
                "The session `{session}` does not exist."
148
            )));
149
        };
150

            
151
4
        if self.is_locked().await {
152
5
            tracing::error!("Cannot set secret of a locked object `{}`", self.path);
153
4
            return Err(ServiceError::IsLocked(format!(
154
                "Cannot set secret of a locked object `{}`.",
155
                self.path
156
            )));
157
        }
158

            
159
        {
160
4
            let mut inner = self.inner.lock().await;
161
4
            let inner = inner.as_mut().unwrap();
162

            
163
2
            match session.aes_key() {
164
2
                Some(key) => {
165
4
                    let decrypted = oo7::crypto::decrypt(secret, &key, &iv).map_err(|err| {
166
                        custom_service_error(&format!("Failed to decrypt secret {err}."))
167
                    })?;
168
4
                    inner.as_mut_unlocked().set_secret(decrypted);
169
                }
170
                None => {
171
2
                    inner.as_mut_unlocked().set_secret(secret);
172
                }
173
            }
174

            
175
            // Ensure content-type attribute is stored
176
4
            let mut attributes = inner.as_unlocked().attributes().clone();
177
4
            if !attributes.contains_key(oo7::CONTENT_TYPE_ATTRIBUTE) {
178
                attributes.insert(
179
                    oo7::CONTENT_TYPE_ATTRIBUTE.to_owned(),
180
                    content_type.as_str().into(),
181
                );
182
            } else {
183
                attributes
184
4
                    .entry(oo7::CONTENT_TYPE_ATTRIBUTE.to_string())
185
6
                    .and_modify(|v| *v = content_type.as_str().into());
186
            }
187
4
            inner.as_mut_unlocked().set_attributes(&attributes);
188
        }
189

            
190
2
        let signal_emitter = self.service.signal_emitter(&self.collection_path)?;
191
4
        Collection::item_changed(&signal_emitter, &self.path).await?;
192

            
193
4
        if let Ok(signal_emitter) = self.service.signal_emitter(&self.path) {
194
4
            if let Err(err) = self.modified_changed(&signal_emitter).await {
195
                tracing::error!(
196
                    "Failed to emit PropertiesChanged signal for Modified: {}",
197
                    err
198
                );
199
            }
200
        }
201

            
202
2
        Ok(())
203
    }
204

            
205
    #[zbus(property, name = "Locked")]
206
8
    pub async fn is_locked(&self) -> bool {
207
4
        self.inner.lock().await.as_ref().unwrap().is_locked()
208
    }
209

            
210
    #[zbus(property, name = "Attributes")]
211
8
    pub async fn attributes(&self) -> zbus::fdo::Result<HashMap<String, String>> {
212
4
        if self.is_locked().await {
213
4
            return Err(zbus::fdo::Error::Failed(format!(
214
                "Cannot get attributes of a locked object `{}`.",
215
                self.path
216
            )));
217
        }
218

            
219
10
        Ok(self
220
            .inner
221
2
            .lock()
222
6
            .await
223
2
            .as_ref()
224
2
            .unwrap()
225
2
            .as_unlocked()
226
2
            .attributes()
227
2
            .iter()
228
6
            .map(|(k, v)| (k.to_owned(), v.to_string()))
229
4
            .collect())
230
    }
231

            
232
    #[zbus(property, name = "Attributes")]
233
2
    pub async fn set_attributes(
234
        &self,
235
        attributes: HashMap<String, String>,
236
    ) -> Result<(), zbus::Error> {
237
4
        if self.is_locked().await {
238
5
            tracing::error!("Cannot set attributes of a locked object `{}`", self.path);
239
2
            return Err(zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed(
240
4
                format!("Cannot set attributes of a locked object `{}`.", self.path),
241
            ))));
242
        }
243

            
244
        {
245
4
            let mut inner = self.inner.lock().await;
246
2
            inner
247
                .as_mut()
248
                .unwrap()
249
                .as_mut_unlocked()
250
2
                .set_attributes(&attributes);
251
        }
252

            
253
4
        let signal_emitter = self
254
            .service
255
2
            .signal_emitter(&self.collection_path)
256
2
            .map_err(|err| zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed(err.to_string()))))?;
257
4
        Collection::item_changed(&signal_emitter, &self.path).await?;
258

            
259
4
        let signal_emitter = self
260
            .service
261
2
            .signal_emitter(&self.path)
262
2
            .map_err(|err| zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed(err.to_string()))))?;
263
4
        self.attributes_changed(&signal_emitter).await?;
264
2
        self.modified_changed(&signal_emitter).await?;
265
2
        Ok(())
266
    }
267

            
268
    #[zbus(property, name = "Label")]
269
8
    pub async fn label(&self) -> zbus::fdo::Result<String> {
270
4
        if self.is_locked().await {
271
4
            return Err(zbus::fdo::Error::Failed(format!(
272
                "Cannot get label of a locked object `{}`.",
273
                self.path
274
            )));
275
        }
276

            
277
10
        Ok(self
278
            .inner
279
2
            .lock()
280
6
            .await
281
2
            .as_ref()
282
2
            .unwrap()
283
2
            .as_unlocked()
284
2
            .label()
285
4
            .to_owned())
286
    }
287

            
288
    #[zbus(property, name = "Label")]
289
8
    pub async fn set_label(&self, label: &str) -> Result<(), zbus::Error> {
290
4
        if self.is_locked().await {
291
5
            tracing::error!("Cannot set label of a locked object `{}`", self.path);
292
2
            return Err(zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed(
293
4
                format!("Cannot set label of a locked object `{}`.", self.path),
294
            ))));
295
        }
296
        {
297
4
            let mut inner = self.inner.lock().await;
298
4
            inner.as_mut().unwrap().as_mut_unlocked().set_label(label);
299
        }
300

            
301
4
        let signal_emitter = self
302
            .service
303
2
            .signal_emitter(&self.collection_path)
304
2
            .map_err(|err| zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed(err.to_string()))))?;
305
4
        Collection::item_changed(&signal_emitter, &self.path).await?;
306

            
307
4
        let signal_emitter = self
308
            .service
309
2
            .signal_emitter(&self.path)
310
2
            .map_err(|err| zbus::Error::FDO(Box::new(zbus::fdo::Error::Failed(err.to_string()))))?;
311
4
        self.label_changed(&signal_emitter).await?;
312
2
        self.modified_changed(&signal_emitter).await?;
313

            
314
2
        Ok(())
315
    }
316

            
317
    #[zbus(property, name = "Created")]
318
8
    pub async fn created_at(&self) -> zbus::fdo::Result<u64> {
319
4
        if self.is_locked().await {
320
4
            return Err(zbus::fdo::Error::Failed(format!(
321
                "Cannot get created timestamp of a locked object `{}`.",
322
                self.path
323
            )));
324
        }
325

            
326
12
        Ok(self
327
            .inner
328
2
            .lock()
329
6
            .await
330
2
            .as_ref()
331
2
            .unwrap()
332
2
            .as_unlocked()
333
2
            .created()
334
4
            .as_secs())
335
    }
336

            
337
    #[zbus(property, name = "Modified")]
338
8
    pub async fn modified_at(&self) -> zbus::fdo::Result<u64> {
339
4
        if self.is_locked().await {
340
4
            return Err(zbus::fdo::Error::Failed(format!(
341
                "Cannot get modified timestamp of a locked object `{}`.",
342
                self.path
343
            )));
344
        }
345

            
346
12
        Ok(self
347
            .inner
348
2
            .lock()
349
6
            .await
350
2
            .as_ref()
351
2
            .unwrap()
352
2
            .as_unlocked()
353
2
            .modified()
354
4
            .as_secs())
355
    }
356
}
357

            
358
impl Item {
359
2
    pub fn new(
360
        item: oo7::file::Item,
361
        service: Service,
362
        collection_path: OwnedObjectPath,
363
        path: OwnedObjectPath,
364
    ) -> Self {
365
        Self {
366
4
            inner: Arc::new(Mutex::new(Some(item))),
367
            path,
368
            collection_path,
369
            service,
370
        }
371
    }
372

            
373
2
    pub fn path(&self) -> &ObjectPath<'_> {
374
2
        &self.path
375
    }
376

            
377
2
    pub(crate) async fn set_locked(
378
        &self,
379
        locked: bool,
380
        keyring: &oo7::file::UnlockedKeyring,
381
    ) -> Result<(), ServiceError> {
382
4
        let mut inner_guard = self.inner.lock().await;
383

            
384
6
        if let Some(old_item) = inner_guard.take() {
385
4
            let new_item = match (old_item, locked) {
386
2
                (oo7::file::Item::Unlocked(unlocked), true) => {
387
4
                    let locked_item = keyring.lock_item(unlocked).await.map_err(|err| {
388
                        custom_service_error(&format!("Failed to lock item: {err}"))
389
                    })?;
390
2
                    oo7::file::Item::Locked(locked_item)
391
                }
392
2
                (oo7::file::Item::Locked(locked_item), false) => {
393
4
                    let unlocked = keyring.unlock_item(locked_item).await.map_err(|err| {
394
                        custom_service_error(&format!("Failed to unlock item: {err}"))
395
                    })?;
396
2
                    oo7::file::Item::Unlocked(unlocked)
397
                }
398
                (other, _) => other,
399
            };
400
2
            *inner_guard = Some(new_item);
401
        }
402

            
403
2
        drop(inner_guard);
404

            
405
2
        let signal_emitter = self.service.signal_emitter(&self.path)?;
406
4
        self.locked_changed(&signal_emitter).await?;
407

            
408
2
        let signal_emitter = self.service.signal_emitter(&self.collection_path)?;
409
4
        Collection::item_changed(&signal_emitter, &self.path).await?;
410

            
411
1
        tracing::debug!(
412
            "Item: {} is {}.",
413
            self.path,
414
            if locked { "locked" } else { "unlocked" }
415
        );
416

            
417
2
        Ok(())
418
    }
419

            
420
8
    async fn delete_unlocked(&self, collection: &Collection) -> Result<(), ServiceError> {
421
        // Delete from keyring and collection's items list
422
4
        collection.delete_item(&self.path).await?;
423

            
424
        // Remove from object server
425
8
        self.service
426
            .object_server()
427
2
            .remove::<Item, _>(&self.path)
428
6
            .await?;
429

            
430
        // Emit ItemDeleted signal
431
2
        let signal_emitter = self.service.signal_emitter(&self.collection_path)?;
432
4
        Collection::item_deleted(&signal_emitter, &self.path).await?;
433

            
434
4
        tracing::info!("Item `{}` deleted.", &self.path);
435

            
436
2
        Ok(())
437
    }
438
}
439

            
440
#[cfg(test)]
441
mod tests {
442
    use std::sync::Arc;
443

            
444
    use oo7::dbus;
445
    use tokio_stream::StreamExt;
446

            
447
    use crate::tests::TestServiceSetup;
448

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

            
453
        let secret = oo7::Secret::text("test-secret");
454
        let dbus_secret = dbus::api::DBusSecret::new(setup.session, secret);
455

            
456
        let item = setup.collections[0]
457
            .create_item(
458
                "Original Label",
459
                &[("app", "test")],
460
                &dbus_secret,
461
                false,
462
                None,
463
            )
464
            .await?;
465

            
466
        // Get label
467
        let label = item.label().await?;
468
        assert_eq!(label, "Original Label");
469

            
470
        // Get initial modified timestamp
471
        let initial_modified = item.modified().await?;
472

            
473
        // Wait to ensure timestamp will be different
474
        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
475

            
476
        // Set label
477
        item.set_label("New Label").await?;
478

            
479
        // Verify new label
480
        let label = item.label().await?;
481
        assert_eq!(label, "New Label");
482

            
483
        // Verify modified timestamp was updated
484
        let new_modified = item.modified().await?;
485
        println!("New modified: {:?}", new_modified);
486
        assert!(
487
            new_modified > initial_modified,
488
            "Modified timestamp should be updated after label change (initial: {:?}, new: {:?})",
489
            initial_modified,
490
            new_modified
491
        );
492

            
493
        Ok(())
494
    }
495

            
496
    #[tokio::test]
497
    async fn attributes_property() -> Result<(), Box<dyn std::error::Error>> {
498
        let setup = TestServiceSetup::plain_session(true).await?;
499

            
500
        let secret = oo7::Secret::text("test-secret");
501
        let dbus_secret = dbus::api::DBusSecret::new(setup.session, secret);
502

            
503
        let item = setup.collections[0]
504
            .create_item(
505
                "Test Item",
506
                &[("app", "firefox"), ("username", "user@example.com")],
507
                &dbus_secret,
508
                false,
509
                None,
510
            )
511
            .await?;
512

            
513
        // Get attributes
514
        let attrs = item.attributes().await?;
515
        assert_eq!(attrs.get("app").unwrap(), "firefox");
516
        assert_eq!(attrs.get("username").unwrap(), "user@example.com");
517

            
518
        // Get initial modified timestamp
519
        let initial_modified = item.modified().await?;
520

            
521
        // Wait to ensure timestamp will be different
522
        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
523

            
524
        // Set new attributes
525
        item.set_attributes(&[("app", "chrome"), ("username", "newuser@example.com")])
526
            .await?;
527

            
528
        // Verify new attributes
529
        let attrs = item.attributes().await?;
530
        assert_eq!(attrs.get("app").unwrap(), "chrome");
531
        assert_eq!(attrs.get("username").unwrap(), "newuser@example.com");
532

            
533
        // Verify modified timestamp was updated
534
        let new_modified = item.modified().await?;
535
        assert!(
536
            new_modified > initial_modified,
537
            "Modified timestamp should be updated after attributes change"
538
        );
539

            
540
        Ok(())
541
    }
542

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

            
547
        let collections = setup.service_api.collections().await?;
548
        let secret = oo7::Secret::text("test-secret");
549
        let dbus_secret = dbus::api::DBusSecret::new(setup.session, secret);
550

            
551
        let item = collections[0]
552
            .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
553
            .await?;
554

            
555
        // Get created timestamp
556
        let created = item.created().await?;
557
        assert!(created.as_secs() > 0, "Created timestamp should be set");
558

            
559
        // Get modified timestamp
560
        let modified = item.modified().await?;
561
        assert!(modified.as_secs() > 0, "Modified timestamp should be set");
562

            
563
        // Created and modified should be close (within a second for new item)
564
        let diff = if created > modified {
565
            created.as_secs() - modified.as_secs()
566
        } else {
567
            modified.as_secs() - created.as_secs()
568
        };
569
        assert!(diff <= 1, "Created and modified should be within 1 second");
570
        Ok(())
571
    }
572

            
573
    #[tokio::test]
574
    async fn secret_retrieval_plain() -> Result<(), Box<dyn std::error::Error>> {
575
        let setup = TestServiceSetup::plain_session(true).await?;
576

            
577
        let secret = oo7::Secret::blob(b"my-secret-password");
578
        let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret.clone());
579

            
580
        let item = setup.collections[0]
581
            .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
582
            .await?;
583

            
584
        // Retrieve secret
585
        let retrieved_secret = item.secret(&setup.session).await?;
586
        assert_eq!(retrieved_secret.value(), secret.as_bytes());
587

            
588
        // Verify content-type is preserved
589
        assert_eq!(
590
            retrieved_secret.content_type(),
591
            secret.content_type(),
592
            "Content-type should be preserved"
593
        );
594
        Ok(())
595
    }
596

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

            
601
        let aes_key = setup.aes_key.as_ref().unwrap();
602
        let secret = oo7::Secret::text("my-encrypted-secret");
603
        let dbus_secret = dbus::api::DBusSecret::new_encrypted(
604
            Arc::clone(&setup.session),
605
            secret.clone(),
606
            aes_key,
607
        )?;
608

            
609
        let item = setup.collections[0]
610
            .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
611
            .await?;
612

            
613
        // Retrieve secret
614
        let retrieved_secret = item.secret(&setup.session).await?;
615
        assert_eq!(
616
            retrieved_secret.decrypt(Some(&aes_key.clone()))?.as_bytes(),
617
            secret.as_bytes()
618
        );
619
        // Verify content-type is preserved
620
        assert_eq!(
621
            retrieved_secret
622
                .decrypt(Some(&aes_key.clone()))?
623
                .content_type(),
624
            secret.content_type(),
625
            "Content-type should be preserved"
626
        );
627

            
628
        Ok(())
629
    }
630

            
631
    #[tokio::test]
632
    async fn delete_item() -> Result<(), Box<dyn std::error::Error>> {
633
        let setup = TestServiceSetup::plain_session(true).await?;
634

            
635
        let secret = oo7::Secret::text("test-secret");
636
        let dbus_secret = dbus::api::DBusSecret::new(setup.session, secret);
637

            
638
        let item = setup.collections[0]
639
            .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
640
            .await?;
641

            
642
        // Verify item exists
643
        let items = setup.collections[0].items().await?;
644
        assert_eq!(items.len(), 1);
645

            
646
        // Delete item
647
        item.delete(None).await?;
648

            
649
        // Verify item is deleted
650
        let items = setup.collections[0].items().await?;
651
        assert_eq!(items.len(), 0, "Item should be deleted from collection");
652
        Ok(())
653
    }
654

            
655
    #[tokio::test]
656
    async fn set_secret_plain() -> Result<(), Box<dyn std::error::Error>> {
657
        let setup = TestServiceSetup::plain_session(true).await?;
658

            
659
        let original_secret = oo7::Secret::text("original-password");
660
        let dbus_secret =
661
            dbus::api::DBusSecret::new(Arc::clone(&setup.session), original_secret.clone());
662

            
663
        let item = setup.collections[0]
664
            .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
665
            .await?;
666

            
667
        // Verify original secret
668
        let retrieved = item.secret(&setup.session).await?;
669
        assert_eq!(retrieved.value(), original_secret.as_bytes());
670
        assert_eq!(
671
            retrieved.content_type(),
672
            original_secret.content_type(),
673
            "Content-type should be preserved"
674
        );
675

            
676
        // Get initial modified timestamp
677
        let initial_modified = item.modified().await?;
678

            
679
        // Wait to ensure timestamp will be different
680
        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
681

            
682
        // Update the secret
683
        let new_secret = oo7::Secret::blob(b"new-password");
684
        let new_dbus_secret =
685
            dbus::api::DBusSecret::new(Arc::clone(&setup.session), new_secret.clone());
686
        item.set_secret(&new_dbus_secret).await?;
687

            
688
        // Verify updated secret
689
        let retrieved = item.secret(&setup.session).await?;
690
        assert_eq!(retrieved.value(), new_secret.as_bytes());
691
        assert_eq!(
692
            retrieved.content_type(),
693
            new_secret.content_type(),
694
            "Content-type should be preserved"
695
        );
696

            
697
        // Verify modified timestamp was updated
698
        let new_modified = item.modified().await?;
699
        assert!(
700
            new_modified > initial_modified,
701
            "Modified timestamp should be updated after secret change"
702
        );
703

            
704
        Ok(())
705
    }
706

            
707
    #[tokio::test]
708
    async fn set_secret_encrypted() -> Result<(), Box<dyn std::error::Error>> {
709
        let setup = TestServiceSetup::encrypted_session(true).await?;
710
        let aes_key = setup.aes_key.unwrap();
711

            
712
        let original_secret = oo7::Secret::text("original-encrypted-password");
713
        let dbus_secret = dbus::api::DBusSecret::new_encrypted(
714
            Arc::clone(&setup.session),
715
            original_secret.clone(),
716
            &aes_key,
717
        )?;
718

            
719
        let item = setup.collections[0]
720
            .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
721
            .await?;
722

            
723
        // Verify original secret
724
        let retrieved = item.secret(&setup.session).await?;
725
        assert_eq!(
726
            retrieved.decrypt(Some(&aes_key.clone()))?.as_bytes(),
727
            original_secret.as_bytes()
728
        );
729

            
730
        // Get initial modified timestamp
731
        let initial_modified = item.modified().await?;
732

            
733
        // Wait to ensure timestamp will be different
734
        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
735

            
736
        // Update the secret
737
        let new_secret = oo7::Secret::text("new-encrypted-password");
738
        let new_dbus_secret = dbus::api::DBusSecret::new_encrypted(
739
            Arc::clone(&setup.session),
740
            new_secret.clone(),
741
            &aes_key,
742
        )?;
743
        item.set_secret(&new_dbus_secret).await?;
744

            
745
        // Verify updated secret
746
        let retrieved = item.secret(&setup.session).await?;
747
        assert_eq!(
748
            retrieved.decrypt(Some(&aes_key.clone()))?.as_bytes(),
749
            new_secret.as_bytes()
750
        );
751

            
752
        // Verify modified timestamp was updated
753
        let new_modified = item.modified().await?;
754
        assert!(
755
            new_modified > initial_modified,
756
            "Modified timestamp should be updated after secret change"
757
        );
758

            
759
        Ok(())
760
    }
761

            
762
    #[tokio::test]
763
    async fn get_secret_invalid_session() -> Result<(), Box<dyn std::error::Error>> {
764
        let setup = TestServiceSetup::plain_session(true).await?;
765

            
766
        let secret = oo7::Secret::text("test-secret");
767
        let dbus_secret = dbus::api::DBusSecret::new(setup.session, secret);
768

            
769
        let item = setup.collections[0]
770
            .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
771
            .await?;
772

            
773
        // Try to get secret with invalid session path
774
        let invalid_session =
775
            oo7::dbus::api::Session::new(&setup.client_conn, "/invalid/session").await?;
776
        let result = item.secret(&invalid_session).await;
777

            
778
        assert!(
779
            matches!(
780
                result,
781
                Err(oo7::dbus::Error::Service(
782
                    oo7::dbus::ServiceError::NoSession(_)
783
                ))
784
            ),
785
            "Should be NoSession error"
786
        );
787

            
788
        Ok(())
789
    }
790

            
791
    #[tokio::test]
792
    async fn set_secret_invalid_session() -> Result<(), Box<dyn std::error::Error>> {
793
        let setup = TestServiceSetup::plain_session(true).await?;
794

            
795
        let secret = oo7::Secret::text("test-secret");
796
        let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret);
797

            
798
        let item = setup.collections[0]
799
            .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
800
            .await?;
801

            
802
        let new_secret = oo7::Secret::text("new-secret");
803
        let invalid_dbus_secret = dbus::api::DBusSecret::new(
804
            Arc::new(dbus::api::Session::new(&setup.client_conn, "/invalid/session").await?),
805
            new_secret,
806
        );
807

            
808
        let result = item.set_secret(&invalid_dbus_secret).await;
809

            
810
        // Should return NoSession error
811
        assert!(
812
            matches!(
813
                result,
814
                Err(oo7::dbus::Error::Service(
815
                    oo7::dbus::ServiceError::NoSession(_)
816
                ))
817
            ),
818
            "Should be NoSession error"
819
        );
820

            
821
        Ok(())
822
    }
823

            
824
    #[tokio::test]
825
    async fn item_changed_signal() -> Result<(), Box<dyn std::error::Error>> {
826
        let setup = TestServiceSetup::plain_session(true).await?;
827

            
828
        let secret = oo7::Secret::text("test-secret");
829
        let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret);
830

            
831
        let item = setup.collections[0]
832
            .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
833
            .await?;
834

            
835
        // Subscribe to ItemChanged signal
836
        let signal_stream = setup.collections[0].receive_item_changed().await?;
837
        tokio::pin!(signal_stream);
838

            
839
        // Change the label
840
        item.set_label("Updated Label").await?;
841

            
842
        // Wait for signal
843
        let signal_result =
844
            tokio::time::timeout(tokio::time::Duration::from_secs(1), signal_stream.next()).await;
845

            
846
        assert!(
847
            signal_result.is_ok(),
848
            "Should receive ItemChanged signal after label change"
849
        );
850
        let signal = signal_result.unwrap();
851
        assert!(signal.is_some(), "Signal should not be None");
852

            
853
        let signal_item = signal.unwrap();
854
        assert_eq!(
855
            signal_item.inner().path().as_str(),
856
            item.inner().path().as_str(),
857
            "Signal should contain the changed item path"
858
        );
859

            
860
        // Change attributes and verify signal again
861
        item.set_attributes(&[("app", "updated-app")]).await?;
862

            
863
        let signal_result =
864
            tokio::time::timeout(tokio::time::Duration::from_secs(1), signal_stream.next()).await;
865

            
866
        assert!(
867
            signal_result.is_ok(),
868
            "Should receive ItemChanged signal after attributes change"
869
        );
870

            
871
        // Change secret and verify signal again
872
        let new_secret = oo7::Secret::text("new-secret");
873
        let new_dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), new_secret);
874
        item.set_secret(&new_dbus_secret).await?;
875

            
876
        let signal_result =
877
            tokio::time::timeout(tokio::time::Duration::from_secs(1), signal_stream.next()).await;
878

            
879
        assert!(
880
            signal_result.is_ok(),
881
            "Should receive ItemChanged signal after secret change"
882
        );
883

            
884
        Ok(())
885
    }
886

            
887
    #[tokio::test]
888
    async fn delete_locked_item_with_prompt() -> Result<(), Box<dyn std::error::Error>> {
889
        let setup = TestServiceSetup::plain_session(true).await?;
890
        let default_collection = setup.default_collection().await?;
891

            
892
        let secret = oo7::Secret::text("test-password");
893
        let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret.clone());
894

            
895
        let item = default_collection
896
            .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
897
            .await?;
898

            
899
        let items = default_collection.items().await?;
900
        assert_eq!(items.len(), 1, "Should have one item");
901

            
902
        let collection = setup
903
            .server
904
            .collection_from_path(default_collection.inner().path())
905
            .await
906
            .expect("Collection should exist");
907
        collection
908
            .set_locked(true, setup.keyring_secret.clone())
909
            .await?;
910

            
911
        assert!(item.is_locked().await?, "Item should be locked");
912

            
913
        item.delete(None).await?;
914

            
915
        let items = default_collection.items().await?;
916
        assert_eq!(items.len(), 0, "Item should be deleted after prompt");
917

            
918
        Ok(())
919
    }
920

            
921
    #[tokio::test]
922
    async fn locked_item_operations() -> Result<(), Box<dyn std::error::Error>> {
923
        let setup = TestServiceSetup::plain_session(true).await?;
924

            
925
        // Create an item
926
        let secret = oo7::Secret::text("test-password");
927
        let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret.clone());
928

            
929
        let item = setup.collections[0]
930
            .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None)
931
            .await?;
932

            
933
        // Verify item is unlocked initially
934
        assert!(!item.is_locked().await?, "Item should start unlocked");
935

            
936
        // Lock the collection (which locks the item)
937
        let collection = setup
938
            .server
939
            .collection_from_path(setup.collections[0].inner().path())
940
            .await
941
            .expect("Collection should exist");
942
        collection
943
            .set_locked(true, setup.keyring_secret.clone())
944
            .await?;
945

            
946
        // Verify item is now locked
947
        assert!(
948
            item.is_locked().await?,
949
            "Item should be locked after locking collection"
950
        );
951

            
952
        // Test 1: get_secret should fail with IsLocked
953
        let result = item.secret(&setup.session).await;
954
        assert!(
955
            matches!(
956
                result,
957
                Err(oo7::dbus::Error::Service(
958
                    oo7::dbus::ServiceError::IsLocked(_)
959
                ))
960
            ),
961
            "get_secret should fail with IsLocked error, got: {:?}",
962
            result
963
        );
964

            
965
        // Test 2: set_secret should fail with IsLocked
966
        let new_secret = oo7::Secret::text("new-password");
967
        let new_dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), new_secret);
968
        let result = item.set_secret(&new_dbus_secret).await;
969
        assert!(
970
            matches!(
971
                result,
972
                Err(oo7::dbus::Error::Service(
973
                    oo7::dbus::ServiceError::IsLocked(_)
974
                ))
975
            ),
976
            "set_secret should fail with IsLocked error, got: {:?}",
977
            result
978
        );
979

            
980
        // Test 3: set_attributes should fail with IsLocked
981
        let result = item.set_attributes(&[("app", "new-app")]).await;
982
        assert!(
983
            matches!(result, Err(oo7::dbus::Error::ZBus(zbus::Error::FDO(_)))),
984
            "set_attributes should fail with IsLocked error, got: {:?}",
985
            result
986
        );
987

            
988
        // Test 4: set_label should fail with IsLocked
989
        let result = item.set_label("New Label").await;
990
        assert!(
991
            matches!(result, Err(oo7::dbus::Error::ZBus(zbus::Error::FDO(_)))),
992
            "set_label should fail with IsLocked error, got: {:?}",
993
            result
994
        );
995

            
996
        // Test 5: Reading properties should also fail on locked items
997
        let result = item.label().await;
998
        assert!(
999
            matches!(result, Err(oo7::dbus::Error::ZBus(zbus::Error::FDO(_)))),
            "label should fail on locked item, got: {:?}",
            result
        );
        let result = item.attributes().await;
        assert!(
            matches!(result, Err(oo7::dbus::Error::ZBus(zbus::Error::FDO(_)))),
            "attributes should fail on locked item, got: {:?}",
            result
        );
        let result = item.created().await;
        assert!(
            matches!(result, Err(oo7::dbus::Error::ZBus(zbus::Error::FDO(_)))),
            "created should fail on locked item, got: {:?}",
            result
        );
        let result = item.modified().await;
        assert!(
            matches!(result, Err(oo7::dbus::Error::ZBus(zbus::Error::FDO(_)))),
            "modified should fail on locked item, got: {:?}",
            result
        );
        Ok(())
    }
}