diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 0e73fe15e..22cb1c188 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -54,23 +54,6 @@ jobs: run: | cargo build --manifest-path ./portal/Cargo.toml - - name: Setup D-Bus and Secret Service for local tests - run: | - sudo apt-get update - sudo apt-get install -y gnome-keyring dbus-x11 - - # Start D-Bus session - mkdir -p ~/.local/share/keyrings - eval $(dbus-launch --sh-syntax) - export DBUS_SESSION_BUS_ADDRESS - echo "DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS" >> $GITHUB_ENV - - # Initialize and unlock the default keyring - printf '\n' | gnome-keyring-daemon --unlock --daemonize --login - - # Give the service time to start - sleep 3 - - name: Test (native) run: | cargo test --manifest-path ./client/Cargo.toml --no-default-features --features tokio --features native_crypto --features schema @@ -111,11 +94,11 @@ jobs: with: components: clippy - name: Clippy workspace (default features) - run: cargo clippy --workspace -- -D warnings + run: cargo clippy --workspace --tests -- -D warnings - name: Clippy client (tracing / async-std / native crypto) run: cargo clippy -p oo7 --no-default-features --features tracing,async-std,native_crypto,schema -- -D warnings - name: Clippy client (tracing / tokio / OpenSSL) - run: cargo clippy -p oo7 --no-default-features --features tracing,tokio,openssl_crypto,schema -- -D warnings + run: cargo clippy -p oo7 --no-default-features --features tracing,tokio,openssl_crypto,schema --tests -- -D warnings meson: name: Meson diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 73f7d1f22..49956f636 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -53,23 +53,6 @@ jobs: with: tool: grcov - - name: Install system dependencies and setup D-Bus - run: | - sudo apt-get update - sudo apt-get install -y libssl-dev pkg-config gnome-keyring dbus-x11 jq - - # Start D-Bus session - mkdir -p ~/.local/share/keyrings - eval $(dbus-launch --sh-syntax) - export DBUS_SESSION_BUS_ADDRESS - echo "DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS" >> $GITHUB_ENV - - # Initialize and unlock the default keyring - printf '\n' | gnome-keyring-daemon --unlock --daemonize --login - - # Give the service time to start - sleep 3 - - name: Run coverage script run: | chmod +x coverage.sh diff --git a/Cargo.lock b/Cargo.lock index 67c6085d6..f5ebbc03f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -589,17 +589,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - [[package]] name = "futures-io" version = "0.3.32" @@ -1093,12 +1082,12 @@ dependencies = [ "md-5", "num", "num-bigint-dig", + "oo7-daemon", "oo7-macros", "openssl", "pbkdf2", "serde", "serde_bytes", - "serial_test", "sha2", "subtle", "tempfile", @@ -1146,7 +1135,6 @@ dependencies = [ "rustix", "serde", "serde_repr", - "serial_test", "sha2", "tempfile", "tokio", @@ -1578,27 +1566,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "semver" version = "1.0.27" @@ -1669,32 +1642,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serial_test" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" -dependencies = [ - "futures-executor", - "futures-util", - "log", - "once_cell", - "parking_lot", - "scc", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "sha2" version = "0.10.9" diff --git a/client/Cargo.toml b/client/Cargo.toml index a9e06a1b8..60f81b3f7 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -47,7 +47,7 @@ zvariant.workspace = true zeroize.workspace = true [dev-dependencies] -serial_test = "3.4" +oo7-daemon = { path = "../server", features = ["test-util"], default-features = false, version = "0.6.0-alpha" } tempfile.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } @@ -74,8 +74,12 @@ native_crypto = [ "dep:pbkdf2", "dep:sha2", "dep:subtle", + "oo7-daemon/native_crypto", +] +openssl_crypto = [ + "dep:openssl", + "oo7-daemon/openssl_crypto", ] -openssl_crypto = ["dep:openssl"] tracing = ["dep:tracing", "ashpd/tracing"] schema = ["dep:oo7-macros"] diff --git a/client/src/dbus/service.rs b/client/src/dbus/service.rs index 725939c29..9bd86495b 100644 --- a/client/src/dbus/service.rs +++ b/client/src/dbus/service.rs @@ -49,11 +49,25 @@ impl Service { /// Create a new instance of the Service, an encrypted communication would /// be attempted first and would fall back to a plain one if that fails. pub async fn new() -> Result { - let service = match Self::encrypted().await { + let cnx = zbus::connection::Builder::session()? + .method_timeout(std::time::Duration::from_secs(30)) + .build() + .await?; + Self::new_with_connection(&cnx).await + } + + /// Create a new instance of the Service with a custom connection. + /// + /// An encrypted communication would be attempted first and would fall back + /// to a plain one if that fails. + pub async fn new_with_connection(cnx: &zbus::Connection) -> Result { + let service = match Self::encrypted_with_connection(cnx).await { Ok(service) => Ok(service), - Err(Error::ZBus(zbus::Error::MethodError(..))) => Self::plain().await, + Err(Error::ZBus(zbus::Error::MethodError(..))) => { + Self::plain_with_connection(cnx).await + } Err(Error::Service(ServiceError::ZBus(zbus::Error::MethodError(..)))) => { - Self::plain().await + Self::plain_with_connection(cnx).await } Err(e) => Err(e), }?; @@ -62,22 +76,41 @@ impl Service { /// Create a new instance of the Service with plain algorithm. pub async fn plain() -> Result { - Self::with_algorithm(Algorithm::Plain).await + let cnx = zbus::connection::Builder::session()? + .method_timeout(std::time::Duration::from_secs(30)) + .build() + .await?; + Self::plain_with_connection(&cnx).await } - /// Create a new instance of the Service with encrypted algorithm. - pub async fn encrypted() -> Result { - Self::with_algorithm(Algorithm::Encrypted).await + /// Create a new instance of the Service with plain algorithm and a custom + /// connection. + pub async fn plain_with_connection(cnx: &zbus::Connection) -> Result { + Self::with_algorithm_and_connection(Algorithm::Plain, cnx).await } - /// Create a new instance of the Service. - async fn with_algorithm(algorithm: Algorithm) -> Result { + /// Create a new instance of the Service with encrypted algorithm. + pub async fn encrypted() -> Result { let cnx = zbus::connection::Builder::session()? .method_timeout(std::time::Duration::from_secs(30)) .build() .await?; + Self::encrypted_with_connection(&cnx).await + } + + /// Create a new instance of the Service with encrypted algorithm and a + /// custom connection. + pub async fn encrypted_with_connection(cnx: &zbus::Connection) -> Result { + Self::with_algorithm_and_connection(Algorithm::Encrypted, cnx).await + } - let service = Arc::new(api::Service::new(&cnx).await?); + /// Create a new instance of the Service with a specific algorithm and + /// connection. + async fn with_algorithm_and_connection( + algorithm: Algorithm, + cnx: &zbus::Connection, + ) -> Result { + let service = Arc::new(api::Service::new(cnx).await?); let (aes_key, session) = match algorithm { Algorithm::Plain => { diff --git a/client/src/file/api/mod.rs b/client/src/file/api/mod.rs index 707e33970..86c2fd29d 100644 --- a/client/src/file/api/mod.rs +++ b/client/src/file/api/mod.rs @@ -280,6 +280,24 @@ impl Keyring { } } + /// Construct a keyring path within a specific data directory. + /// + /// This is useful for tests and cases where you want explicit control over + /// where keyrings are stored, avoiding the default XDG_DATA_HOME location. + pub(crate) fn path_at( + data_dir: impl AsRef, + name: &str, + version: u8, + ) -> PathBuf { + let mut path = data_dir.as_ref().to_path_buf(); + path.push("keyrings"); + if version > 0 { + path.push(format!("v{version}")); + } + path.push(format!("{name}.keyring")); + path + } + pub fn default_path() -> Result { Self::path("default", LEGACY_MAJOR_VERSION) } diff --git a/client/src/file/locked_keyring.rs b/client/src/file/locked_keyring.rs index aa522632d..6bbab6bea 100644 --- a/client/src/file/locked_keyring.rs +++ b/client/src/file/locked_keyring.rs @@ -177,4 +177,20 @@ impl LockedKeyring { let v1_path = api::Keyring::path(name, api::MAJOR_VERSION)?; Self::load(v1_path).await } + + /// Open a locked keyring at a specific data directory. + /// + /// This is useful for tests and cases where you want explicit control over + /// where keyrings are stored, avoiding the default XDG_DATA_HOME location. + /// + /// # Arguments + /// + /// * `data_dir` - Base data directory (keyrings stored in + /// `data_dir/keyrings/v1/`) + /// * `name` - The name of the keyring. + #[cfg_attr(feature = "tracing", tracing::instrument(fields(data_dir = ?data_dir.as_ref())))] + pub async fn open_at(data_dir: impl AsRef, name: &str) -> Result { + let path = api::Keyring::path_at(data_dir, name, api::MAJOR_VERSION); + Self::load(path).await + } } diff --git a/client/src/file/unlocked_keyring.rs b/client/src/file/unlocked_keyring.rs index 653765868..60640378e 100644 --- a/client/src/file/unlocked_keyring.rs +++ b/client/src/file/unlocked_keyring.rs @@ -156,25 +156,20 @@ impl UnlockedKeyring { } } - /// Open a keyring with given name from the default directory. - /// - /// This function will automatically migrate the keyring to the - /// latest format. - /// - /// # Arguments + /// Helper for opening/creating keyrings with explicit paths. /// - /// * `name` - The name of the keyring. - /// * `secret` - The service key, usually retrieved from the Secrets portal. - #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret)))] - pub async fn open(name: &str, secret: Secret) -> Result { - let v1_path = api::Keyring::path(name, api::MAJOR_VERSION)?; + /// Handles v0 -> v1 migration automatically. + async fn open_with_paths( + v1_path: PathBuf, + v0_path: PathBuf, + secret: Secret, + ) -> Result { if v1_path.exists() { #[cfg(feature = "tracing")] tracing::debug!("Loading v1 keyring file"); return Self::load(v1_path, secret).await; } - let v0_path = api::Keyring::path(name, api::LEGACY_MAJOR_VERSION)?; if v0_path.exists() { #[cfg(feature = "tracing")] tracing::debug!("Trying to load keyring file at {:?}", v0_path); @@ -195,6 +190,64 @@ impl UnlockedKeyring { } } + /// Open a keyring with given name from the default directory. + /// + /// This function will automatically migrate the keyring to the + /// latest format. + /// + /// # Arguments + /// + /// * `name` - The name of the keyring. + /// * `secret` - The service key, usually retrieved from the Secrets portal. + #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret)))] + pub async fn open(name: &str, secret: Secret) -> Result { + let v1_path = api::Keyring::path(name, api::MAJOR_VERSION)?; + let v0_path = api::Keyring::path(name, api::LEGACY_MAJOR_VERSION)?; + Self::open_with_paths(v1_path, v0_path, secret).await + } + + /// Open or create a keyring at a specific data directory. + /// + /// This is useful for tests and cases where you want explicit control over + /// where keyrings are stored, avoiding the default XDG_DATA_HOME location. + /// + /// This function will automatically migrate the keyring to the latest + /// format. + /// + /// # Arguments + /// + /// * `data_dir` - Base data directory (keyrings stored in + /// `data_dir/keyrings/v1/`) + /// * `name` - The name of the keyring. + /// * `secret` - The service key, usually retrieved from the Secrets portal. + /// + /// # Example + /// + /// ```no_run + /// # use oo7::{Secret, file::UnlockedKeyring}; + /// # async fn example() -> Result<(), Box> { + /// let temp_dir = tempfile::tempdir()?; + /// let keyring = + /// UnlockedKeyring::open_at(temp_dir.path(), "test-keyring", Secret::from("password")).await?; + /// keyring + /// .create_item("item", &[("attr", "value")], Secret::text("secret"), false) + /// .await?; + /// keyring.write().await?; // Writes to temp_dir/keyrings/v1/test-keyring.keyring + /// // + /// # Ok(()) + /// # } + /// ``` + #[cfg_attr(feature = "tracing", tracing::instrument(skip(secret), fields(data_dir = ?data_dir.as_ref())))] + pub async fn open_at( + data_dir: impl AsRef, + name: &str, + secret: Secret, + ) -> Result { + let v1_path = api::Keyring::path_at(&data_dir, name, api::MAJOR_VERSION); + let v0_path = api::Keyring::path_at(&data_dir, name, api::LEGACY_MAJOR_VERSION); + Self::open_with_paths(v1_path, v0_path, secret).await + } + /// Lock the keyring. pub fn lock(self) -> LockedKeyring { LockedKeyring { diff --git a/client/src/keyring.rs b/client/src/keyring.rs index 81fabb197..5676c900c 100644 --- a/client/src/keyring.rs +++ b/client/src/keyring.rs @@ -31,11 +31,7 @@ impl Keyring { #[cfg(feature = "tracing")] tracing::debug!("Application is sandboxed, using the file backend"); - let secret = Secret::from( - ashpd::desktop::secret::retrieve() - .await - .map_err(crate::file::Error::from)?, - ); + let secret = Secret::sandboxed().await?; match file::UnlockedKeyring::load( crate::file::api::Keyring::default_path()?, secret.clone(), @@ -81,12 +77,7 @@ impl Keyring { tracing::debug!("Unlocking file backend keyring"); // Retrieve secret from portal - let secret = Secret::from( - ashpd::desktop::secret::retrieve() - .await - .map_err(crate::file::Error::from)?, - ); - + let secret = Secret::sandboxed().await?; let unlocked = locked.unlock(secret).await.map_err(crate::Error::File)?; *kg = Some(file::Keyring::Unlocked(unlocked)); } else { diff --git a/client/src/migration.rs b/client/src/migration.rs index 678c60d63..753f52a95 100644 --- a/client/src/migration.rs +++ b/client/src/migration.rs @@ -7,11 +7,7 @@ use crate::{AsAttributes, Result, dbus::Service, file::UnlockedKeyring}; /// Secret Service. pub async fn migrate(attributes: Vec, replace: bool) -> Result<()> { let service = Service::new().await?; - let secret = crate::Secret::from( - ashpd::desktop::secret::retrieve() - .await - .map_err(crate::file::Error::from)?, - ); + let secret = crate::Secret::sandboxed().await?; let file_backend = match UnlockedKeyring::load(crate::file::api::Keyring::default_path()?, secret).await { Ok(file) => Ok(file), diff --git a/client/src/secret.rs b/client/src/secret.rs index 392fa9ac1..aacb8157f 100644 --- a/client/src/secret.rs +++ b/client/src/secret.rs @@ -79,6 +79,16 @@ impl Secret { Ok(Self::blob(secret)) } + /// Get the sandboxed app secret if the app is sandboxed using + /// org.freedesktop.portal.Secret portal. + pub async fn sandboxed() -> Result { + Ok(Self::blob( + ashpd::desktop::secret::retrieve() + .await + .map_err(crate::file::Error::from)?, + )) + } + /// Create a text secret, stored with `text/plain` content type. pub fn text(value: impl AsRef) -> Self { Self::Text(value.as_ref().to_owned()) diff --git a/client/tests/dbus_collection.rs b/client/tests/dbus_collection.rs index 59de6e297..f9f330215 100644 --- a/client/tests/dbus_collection.rs +++ b/client/tests/dbus_collection.rs @@ -1,3 +1,4 @@ +use futures_util::StreamExt; use oo7::dbus::Service; async fn create_item(service: Service, encrypted: bool) { @@ -38,21 +39,36 @@ async fn create_item(service: Service, encrypted: bool) { #[tokio::test] #[cfg(feature = "tokio")] async fn create_plain_item() { - let service = Service::plain().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); create_item(service, false).await; } #[tokio::test] #[cfg(feature = "tokio")] async fn create_encrypted_item() { - let service = Service::encrypted().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::encrypted_session(true) + .await + .unwrap(); + let service = Service::encrypted_with_connection(&setup.client_conn) + .await + .unwrap(); create_item(service, true).await; } #[tokio::test] #[cfg(feature = "tokio")] async fn attribute_search_patterns() { - let service = Service::plain().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); let collection = service.default_collection().await.unwrap(); let secret = oo7::Secret::text("search test"); @@ -128,7 +144,12 @@ async fn attribute_search_patterns() { #[tokio::test] #[cfg(feature = "tokio")] async fn items() { - let service = Service::plain().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); let collection = service.default_collection().await.unwrap(); let secret = oo7::Secret::text("items test"); @@ -172,7 +193,12 @@ async fn items() { #[tokio::test] #[cfg(feature = "tokio")] async fn label_mutation() { - let service = Service::plain().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); let collection = service.session_collection().await.unwrap(); let initial_label = collection.label().await.unwrap(); @@ -185,3 +211,346 @@ async fn label_mutation() { collection.set_label(&initial_label).await.unwrap(); assert_eq!(collection.label().await.unwrap(), initial_label); } + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn collections_list() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + + let collection1 = service + .create_collection("Collection One", None, None) + .await + .unwrap(); + let collection2 = service + .create_collection("Collection Two", None, None) + .await + .unwrap(); + + let collections = service.collections().await.unwrap(); + // Should have at least our 2 collections plus the default collection + assert!(collections.len() >= 3); + + // Verify our collections are in the list + let labels: Vec = futures_util::future::join_all(collections.iter().map(|c| c.label())) + .await + .into_iter() + .filter_map(Result::ok) + .collect(); + + assert!(labels.contains(&"Collection One".to_string())); + assert!(labels.contains(&"Collection Two".to_string())); + + collection1.delete(None).await.unwrap(); + collection2.delete(None).await.unwrap(); +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn with_alias() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + + // Default alias should exist + let default_collection = service + .with_alias(Service::DEFAULT_COLLECTION) + .await + .unwrap(); + assert!(default_collection.is_some()); + + // Create a collection with a custom alias + let collection = service + .create_collection("Aliased Collection", Some("custom-alias"), None) + .await + .unwrap(); + + // Should be able to find it by alias + let found = service.with_alias("custom-alias").await.unwrap(); + assert!(found.is_some()); + assert_eq!(found.unwrap().label().await.unwrap(), "Aliased Collection"); + + // Non-existent alias should return None + let not_found = service.with_alias("nonexistent-alias").await.unwrap(); + assert!(not_found.is_none()); + + collection.delete(None).await.unwrap(); +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn timestamps() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + + let collection = service + .create_collection("Timestamp Test", None, None) + .await + .unwrap(); + + let created = collection.created().await.unwrap(); + let modified = collection.modified().await.unwrap(); + + // Created timestamp should be a valid UNIX timestamp + assert!(created.as_secs() > 0); + // Modified should be >= created + assert!(modified >= created); + + // Modify the collection + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + collection.set_label("Modified Label").await.unwrap(); + + let new_modified = collection.modified().await.unwrap(); + // Modified timestamp should have increased + assert!(new_modified >= modified); + + collection.delete(None).await.unwrap(); +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn deleted_error() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + + let collection = service + .create_collection("Delete Error Test", None, None) + .await + .unwrap(); + + // Verify collection works before deletion + assert!(collection.label().await.is_ok()); + + // Delete the collection + collection.delete(None).await.unwrap(); + + // All operations should now return Error::Deleted + assert!(matches!( + collection.label().await, + Err(oo7::dbus::Error::Deleted) + )); + assert!(matches!( + collection.set_label("New").await, + Err(oo7::dbus::Error::Deleted) + )); + assert!(matches!( + collection.items().await, + Err(oo7::dbus::Error::Deleted) + )); + assert!(matches!( + collection.search_items(&[("test", "test")]).await, + Err(oo7::dbus::Error::Deleted) + )); + assert!(matches!( + collection + .create_item("test", &[("test", "test")], "secret", true, None) + .await, + Err(oo7::dbus::Error::Deleted) + )); + assert!(matches!( + collection.created().await, + Err(oo7::dbus::Error::Deleted) + )); + assert!(matches!( + collection.modified().await, + Err(oo7::dbus::Error::Deleted) + )); + assert!(matches!( + collection.is_locked().await, + Err(oo7::dbus::Error::Deleted) + )); + assert!(matches!( + collection.lock(None).await, + Err(oo7::dbus::Error::Deleted) + )); + assert!(matches!( + collection.unlock(None).await, + Err(oo7::dbus::Error::Deleted) + )); + assert!(matches!( + collection.delete(None).await, + Err(oo7::dbus::Error::Deleted) + )); +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn lock_unlock() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(false) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + + let collection = service + .create_collection("Lock Collection", None, None) + .await + .unwrap(); + + // Collection should start unlocked + assert!(!collection.is_locked().await.unwrap()); + + // Lock the collection + collection.lock(None).await.unwrap(); + assert!(collection.is_locked().await.unwrap()); + + // Unlock the collection + collection.unlock(None).await.unwrap(); + assert!(!collection.is_locked().await.unwrap()); + + collection.delete(None).await.unwrap(); +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn item_created_signal() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + let collection = service.default_collection().await.unwrap(); + + // Setup signal stream before creating item + let created_stream = collection.receive_item_created().await.unwrap(); + tokio::pin!(created_stream); + + // Create an item in a separate task + let collection_clone = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap() + .default_collection() + .await + .unwrap(); + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + let _ = collection_clone + .create_item( + "Signal Test", + &[("test", "signals")], + oo7::Secret::text("test"), + true, + None, + ) + .await; + }); + + // Wait for the signal + tokio::select! { + Some(item) = created_stream.next() => { + assert_eq!(item.label().await.unwrap(), "Signal Test"); + item.delete(None).await.unwrap(); + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)) => { + panic!("Timeout waiting for item created signal"); + } + } +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn item_changed_signal() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + let collection = service.default_collection().await.unwrap(); + + let item = collection + .create_item( + "Change Test", + &[("test", "change-signal")], + oo7::Secret::text("test"), + true, + None, + ) + .await + .unwrap(); + + // Setup signal stream + let changed_stream = collection.receive_item_changed().await.unwrap(); + tokio::pin!(changed_stream); + + // Get a path reference before moving + let item_path = item.path().to_owned(); + + // Modify the item in a separate task + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + let _ = item.set_label("Modified Label").await; + }); + + // Wait for the signal + tokio::select! { + Some(changed_item) = changed_stream.next() => { + assert_eq!(changed_item.label().await.unwrap(), "Modified Label"); + assert_eq!(changed_item.path(), &item_path); + changed_item.delete(None).await.unwrap(); + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)) => { + panic!("Timeout waiting for item changed signal"); + } + } +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn item_deleted_signal() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + let collection = service.default_collection().await.unwrap(); + + let item = collection + .create_item( + "Delete Test", + &[("test", "delete-signal")], + oo7::Secret::text("test"), + true, + None, + ) + .await + .unwrap(); + + // Setup signal stream + let deleted_stream = collection.receive_item_deleted().await.unwrap(); + tokio::pin!(deleted_stream); + + // Delete the item in a separate task + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + let _ = item.delete(None).await; + }); + + // Wait for the signal + tokio::select! { + Some(_deleted_path) = deleted_stream.next() => { + // Signal received successfully + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)) => { + panic!("Timeout waiting for item deleted signal"); + } + } +} diff --git a/client/tests/dbus_item.rs b/client/tests/dbus_item.rs index d5fc6acc8..8f5265174 100644 --- a/client/tests/dbus_item.rs +++ b/client/tests/dbus_item.rs @@ -1,9 +1,15 @@ +use futures_util::StreamExt; use oo7::dbus::Service; #[tokio::test] #[cfg(feature = "tokio")] async fn label_mutation() { - let service = Service::plain().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); let collection = service.default_collection().await.unwrap(); let secret = oo7::Secret::text("test secret"); @@ -33,7 +39,12 @@ async fn label_mutation() { #[tokio::test] #[cfg(feature = "tokio")] async fn secret_mutation() { - let service = Service::plain().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); let collection = service.default_collection().await.unwrap(); let original_secret = oo7::Secret::text("original secret"); @@ -62,7 +73,12 @@ async fn secret_mutation() { #[tokio::test] #[cfg(feature = "tokio")] async fn secret_mutation_encrypted() { - let service = Service::encrypted().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::encrypted_session(true) + .await + .unwrap(); + let service = Service::encrypted_with_connection(&setup.client_conn) + .await + .unwrap(); let collection = service.default_collection().await.unwrap(); let original_secret = oo7::Secret::text("original encrypted secret"); @@ -91,7 +107,12 @@ async fn secret_mutation_encrypted() { #[tokio::test] #[cfg(feature = "tokio")] async fn attributes_mutation() { - let service = Service::plain().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); let collection = service.default_collection().await.unwrap(); let secret = oo7::Secret::text("test secret"); @@ -134,7 +155,12 @@ async fn attributes_mutation() { #[tokio::test] #[cfg(feature = "tokio")] async fn text_secret_type() { - let service = Service::plain().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); let collection = service.default_collection().await.unwrap(); let text_secret = oo7::Secret::text("text password"); @@ -156,7 +182,12 @@ async fn text_secret_type() { #[tokio::test] #[cfg(feature = "tokio")] async fn blob_secret_type() { - let service = Service::plain().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); let collection = service.default_collection().await.unwrap(); let blob_secret = oo7::Secret::blob(b"binary data"); @@ -182,7 +213,12 @@ async fn blob_secret_type() { #[tokio::test] #[cfg(feature = "tokio")] async fn timestamps() { - let service = Service::plain().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); let collection = service.default_collection().await.unwrap(); let secret = oo7::Secret::text("timestamp test"); @@ -220,7 +256,12 @@ async fn timestamps() { #[tokio::test] #[cfg(feature = "tokio")] async fn deleted_error() { - let service = Service::plain().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); let collection = service.default_collection().await.unwrap(); let attributes = &[("test", "deleted-error")]; @@ -284,3 +325,172 @@ async fn deleted_error() { Err(oo7::dbus::Error::Deleted) )); } + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn lock_unlock() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + let collection = service.default_collection().await.unwrap(); + + let secret = oo7::Secret::text("test secret"); + let item = collection + .create_item("Lock Test", &[("test", "lock-unlock")], secret, true, None) + .await + .unwrap(); + + // Item should start unlocked + assert!(!item.is_locked().await.unwrap()); + + // Lock the item + item.lock(None).await.unwrap(); + assert!(item.is_locked().await.unwrap()); + + // Unlock the item + item.unlock(None).await.unwrap(); + assert!(!item.is_locked().await.unwrap()); + + item.delete(None).await.unwrap(); +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn item_created_signal() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + let collection = service.default_collection().await.unwrap(); + + // Setup signal stream before creating item + let created_stream = collection.receive_item_created().await.unwrap(); + tokio::pin!(created_stream); + + // Create an item in a separate task + let collection_clone = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap() + .default_collection() + .await + .unwrap(); + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + let _ = collection_clone + .create_item( + "Signal Test", + &[("test", "signals")], + oo7::Secret::text("test"), + true, + None, + ) + .await; + }); + + // Wait for the signal + tokio::select! { + Some(item) = created_stream.next() => { + assert_eq!(item.label().await.unwrap(), "Signal Test"); + item.delete(None).await.unwrap(); + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)) => { + panic!("Timeout waiting for item created signal"); + } + } +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn item_changed_signal() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + let collection = service.default_collection().await.unwrap(); + + let item = collection + .create_item( + "Change Test", + &[("test", "change-signal")], + oo7::Secret::text("test"), + true, + None, + ) + .await + .unwrap(); + + // Setup signal stream + let changed_stream = collection.receive_item_changed().await.unwrap(); + tokio::pin!(changed_stream); + + // Get a path reference before moving + let item_path = item.path().to_owned(); + + // Modify the item in a separate task + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + let _ = item.set_label("Modified Label").await; + }); + + // Wait for the signal + tokio::select! { + Some(changed_item) = changed_stream.next() => { + assert_eq!(changed_item.label().await.unwrap(), "Modified Label"); + assert_eq!(changed_item.path(), &item_path); + changed_item.delete(None).await.unwrap(); + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)) => { + panic!("Timeout waiting for item changed signal"); + } + } +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn item_deleted_signal() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + let collection = service.default_collection().await.unwrap(); + + let item = collection + .create_item( + "Delete Test", + &[("test", "delete-signal")], + oo7::Secret::text("test"), + true, + None, + ) + .await + .unwrap(); + + // Setup signal stream + let deleted_stream = collection.receive_item_deleted().await.unwrap(); + tokio::pin!(deleted_stream); + + // Delete the item in a separate task + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + let _ = item.delete(None).await; + }); + + // Wait for the signal + tokio::select! { + Some(_deleted_path) = deleted_stream.next() => { + // Signal received successfully + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)) => { + panic!("Timeout waiting for item deleted signal"); + } + } +} diff --git a/client/tests/dbus_service.rs b/client/tests/dbus_service.rs index e5a001df3..c7a195ea8 100644 --- a/client/tests/dbus_service.rs +++ b/client/tests/dbus_service.rs @@ -1,10 +1,15 @@ +use futures_util::StreamExt; use oo7::dbus::Service; #[tokio::test] #[cfg(feature = "tokio")] -#[ignore = "Requires prompting"] async fn create_collection() { - let service = Service::new().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); let collection = service .create_collection("somelabel", None, None) .await @@ -27,7 +32,12 @@ async fn create_collection() { #[tokio::test] #[cfg(feature = "tokio")] async fn default_collections() { - let service = Service::new().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); assert!(service.default_collection().await.is_ok()); assert!(service.session_collection().await.is_ok()); @@ -36,13 +46,140 @@ async fn default_collections() { #[tokio::test] #[cfg(feature = "tokio")] async fn encrypted_session() { - let service = Service::encrypted().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::encrypted_session(true) + .await + .unwrap(); + let service = Service::encrypted_with_connection(&setup.client_conn) + .await + .unwrap(); assert!(service.default_collection().await.is_ok()); } #[tokio::test] #[cfg(feature = "tokio")] async fn plain_session() { - let service = Service::plain().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); assert!(service.default_collection().await.is_ok()); } + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn collection_created_signal() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + + // Setup signal stream before creating collection + let created_stream = service.receive_collection_created().await.unwrap(); + tokio::pin!(created_stream); + + // Create a collection in a separate task + let service_clone = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + let _ = service_clone + .create_collection("Test Collection", None, None) + .await; + }); + + // Wait for the signal + tokio::select! { + Some(collection) = created_stream.next() => { + assert_eq!(collection.label().await.unwrap(), "Test Collection"); + collection.delete(None).await.unwrap(); + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)) => { + panic!("Timeout waiting for collection created signal"); + } + } +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn collection_changed_signal() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + + let collection = service + .create_collection("Change Signal Test", None, None) + .await + .unwrap(); + + // Setup signal stream + let changed_stream = service.receive_collection_changed().await.unwrap(); + tokio::pin!(changed_stream); + + // Get the collection path for comparison + let collection_path = collection.path().to_owned(); + + // Modify the collection in a separate task + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + let _ = collection.set_label("Modified Collection Label").await; + }); + + // Wait for the signal + tokio::select! { + Some(changed_collection) = changed_stream.next() => { + assert_eq!(changed_collection.label().await.unwrap(), "Modified Collection Label"); + assert_eq!(changed_collection.path(), &collection_path); + changed_collection.delete(None).await.unwrap(); + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)) => { + panic!("Timeout waiting for collection changed signal"); + } + } +} + +#[tokio::test] +#[cfg(feature = "tokio")] +async fn collection_deleted_signal() { + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); + + let collection = service + .create_collection("Delete Signal Test", None, None) + .await + .unwrap(); + + // Setup signal stream + let deleted_stream = service.receive_collection_deleted().await.unwrap(); + tokio::pin!(deleted_stream); + + // Get the collection path for comparison + let collection_path = collection.path().to_owned(); + + // Delete the collection in a separate task + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + let _ = collection.delete(None).await; + }); + + // Wait for the signal + tokio::select! { + Some(deleted_path) = deleted_stream.next() => { + assert_eq!(deleted_path.as_str(), collection_path.as_str()); + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(2)) => { + panic!("Timeout waiting for collection deleted signal"); + } + } +} diff --git a/client/tests/file_unlocked_keyring.rs b/client/tests/file_unlocked_keyring.rs index 5b3add61f..69dcb414a 100644 --- a/client/tests/file_unlocked_keyring.rs +++ b/client/tests/file_unlocked_keyring.rs @@ -118,7 +118,6 @@ async fn check_items(keyring: &UnlockedKeyring) -> Result<(), Error> { } #[tokio::test] -#[serial_test::serial] async fn migrate_from_legacy() -> Result<(), Error> { let data_dir = tempdir()?; let v0_dir = data_dir.path().join("keyrings"); @@ -130,14 +129,10 @@ async fn migrate_from_legacy() -> Result<(), Error> { .join("legacy.keyring"); fs::copy(&fixture_path, &v0_dir.join("default.keyring")).await?; - unsafe { - std::env::set_var("XDG_DATA_HOME", data_dir.path()); - } - assert!(!v1_dir.join("default.keyring").exists()); let secret = Secret::blob("test"); - let keyring = UnlockedKeyring::open("default", secret).await?; + let keyring = UnlockedKeyring::open_at(data_dir.path(), "default", secret).await?; check_items(&keyring).await?; @@ -148,7 +143,6 @@ async fn migrate_from_legacy() -> Result<(), Error> { } #[tokio::test] -#[serial_test::serial] async fn migrate() -> Result<(), Error> { let data_dir = tempdir()?; let v0_dir = data_dir.path().join("keyrings"); @@ -160,12 +154,8 @@ async fn migrate() -> Result<(), Error> { .join("default.keyring"); fs::copy(&fixture_path, &v0_dir.join("default.keyring")).await?; - unsafe { - std::env::set_var("XDG_DATA_HOME", data_dir.path()); - } - let secret = Secret::blob("test"); - let keyring = UnlockedKeyring::open("default", secret).await?; + let keyring = UnlockedKeyring::open_at(data_dir.path(), "default", secret).await?; assert!(!v1_dir.join("default.keyring").exists()); @@ -178,7 +168,6 @@ async fn migrate() -> Result<(), Error> { } #[tokio::test] -#[serial_test::serial] async fn open_wrong_password() -> Result<(), Error> { let data_dir = tempdir()?; let v0_dir = data_dir.path().join("keyrings"); @@ -190,18 +179,14 @@ async fn open_wrong_password() -> Result<(), Error> { .join("default.keyring"); fs::copy(&fixture_path, &v1_dir.join("default.keyring")).await?; - unsafe { - std::env::set_var("XDG_DATA_HOME", data_dir.path()); - } - let secret = Secret::blob("wrong"); - let keyring = UnlockedKeyring::open("default", secret).await; + let keyring = UnlockedKeyring::open_at(data_dir.path(), "default", secret).await; assert!(keyring.is_err()); assert!(matches!(keyring.unwrap_err(), Error::IncorrectSecret)); let secret = Secret::blob("test"); - let keyring = UnlockedKeyring::open("default", secret).await; + let keyring = UnlockedKeyring::open_at(data_dir.path(), "default", secret).await; assert!(keyring.is_ok()); @@ -209,7 +194,6 @@ async fn open_wrong_password() -> Result<(), Error> { } #[tokio::test] -#[serial_test::serial] async fn open() -> Result<(), Error> { let data_dir = tempdir()?; let v0_dir = data_dir.path().join("keyrings"); @@ -221,12 +205,8 @@ async fn open() -> Result<(), Error> { .join("default.keyring"); fs::copy(&fixture_path, &v1_dir.join("default.keyring")).await?; - unsafe { - std::env::set_var("XDG_DATA_HOME", data_dir.path()); - } - let secret = Secret::blob("test"); - let keyring = UnlockedKeyring::open("default", secret).await?; + let keyring = UnlockedKeyring::open_at(data_dir.path(), "default", secret).await?; assert!(v1_dir.join("default.keyring").exists()); @@ -239,19 +219,14 @@ async fn open() -> Result<(), Error> { } #[tokio::test] -#[serial_test::serial] async fn open_nonexistent() -> Result<(), Error> { let data_dir = tempdir()?; let v0_dir = data_dir.path().join("keyrings"); let v1_dir = v0_dir.join("v1"); fs::create_dir_all(&v1_dir).await?; - unsafe { - std::env::set_var("XDG_DATA_HOME", data_dir.path()); - } - let secret = Secret::blob("test"); - let keyring = UnlockedKeyring::open("default", secret).await?; + let keyring = UnlockedKeyring::open_at(data_dir.path(), "default", secret).await?; assert!(!v1_dir.join("default.keyring").exists()); @@ -435,7 +410,7 @@ async fn comprehensive_search_patterns() -> Result<(), Error> { let keyring = UnlockedKeyring::load(&keyring_path, strong_key()).await?; // Create diverse test data - let test_items = vec![ + let test_items = [ ( "Email Password", vec![ @@ -621,7 +596,7 @@ async fn secret_types_handling() -> Result<(), Error> { .create_item( "Binary Secret", &[("type", "binary")], - Secret::blob(&[0x00, 0x01, 0x02, 0xFF]), + Secret::blob([0x00, 0x01, 0x02, 0xFF]), false, ) .await?; diff --git a/client/tests/keyring.rs b/client/tests/keyring.rs index 84637ddf0..4fdd9d404 100644 --- a/client/tests/keyring.rs +++ b/client/tests/keyring.rs @@ -7,7 +7,9 @@ use tempfile::tempdir; #[cfg(feature = "tokio")] use tokio::sync::RwLock; -async fn all_backends(temp_dir: tempfile::TempDir) -> Vec { +async fn all_backends( + temp_dir: tempfile::TempDir, +) -> (oo7_server::tests::TestServiceSetup, Vec) { let mut backends = Vec::new(); let keyring_path = temp_dir.path().join("test.keyring"); @@ -21,19 +23,24 @@ async fn all_backends(temp_dir: tempfile::TempDir) -> Vec { backends.push(keyring); - let service = dbus::Service::new().await.unwrap(); + let setup = oo7_server::tests::TestServiceSetup::plain_session(true) + .await + .unwrap(); + let service = dbus::Service::plain_with_connection(&setup.client_conn) + .await + .unwrap(); if let Ok(collection) = service.default_collection().await { backends.push(Keyring::DBus(collection)); } - backends + (setup, backends) } #[tokio::test] #[cfg(feature = "tokio")] async fn create_and_retrieve_items() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; for (idx, keyring) in backends.iter().enumerate() { println!("Running test on backend {}", idx); @@ -87,7 +94,7 @@ async fn create_and_retrieve_items() { #[cfg(feature = "tokio")] async fn delete_items() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; for (idx, keyring) in backends.iter().enumerate() { println!("Running test on backend {}", idx); @@ -134,7 +141,7 @@ async fn delete_items() { #[cfg(feature = "tokio")] async fn item_update_label() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; for (idx, keyring) in backends.iter().enumerate() { println!("Running test on backend {}", idx); @@ -177,7 +184,7 @@ async fn item_update_label() { #[cfg(feature = "tokio")] async fn item_update_attributes() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; for (idx, keyring) in backends.iter().enumerate() { println!("Running test on backend {}", idx); @@ -234,7 +241,7 @@ async fn item_update_attributes() { #[cfg(feature = "tokio")] async fn item_update_secret() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; for (idx, keyring) in backends.iter().enumerate() { println!("Running test on backend {}", idx); @@ -271,7 +278,7 @@ async fn item_update_secret() { #[cfg(feature = "tokio")] async fn item_delete() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; for (idx, keyring) in backends.iter().enumerate() { println!("Running test on backend {}", idx); @@ -320,7 +327,7 @@ async fn item_delete() { #[cfg(feature = "tokio")] async fn item_replace() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; for (idx, keyring) in backends.iter().enumerate() { println!("Running test on backend {}", idx); @@ -355,7 +362,7 @@ async fn item_replace() { #[cfg(feature = "tokio")] async fn item_timestamps() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; for (idx, keyring) in backends.iter().enumerate() { println!("Running test on backend {}", idx); @@ -391,7 +398,7 @@ async fn item_timestamps() { #[cfg(feature = "tokio")] async fn item_is_locked() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; for (idx, keyring) in backends.iter().enumerate() { println!("Running test on backend {}", idx); @@ -424,7 +431,7 @@ async fn item_is_locked() { #[cfg(feature = "tokio")] async fn file_keyring_lock_unlock() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; let keyring = &backends[0]; assert!(!keyring.is_locked().await.unwrap()); @@ -458,7 +465,7 @@ async fn file_keyring_lock_unlock() { #[cfg(feature = "tokio")] async fn file_item_lock_unlock() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; let keyring = &backends[0]; keyring @@ -496,7 +503,7 @@ async fn file_item_lock_unlock() { #[cfg(feature = "tokio")] async fn file_locked_item_operations_fail() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; let keyring = &backends[0]; keyring @@ -551,7 +558,7 @@ async fn file_locked_item_operations_fail() { #[cfg(feature = "tokio")] async fn file_locked_keyring_operations_fail() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; let keyring = &backends[0]; keyring @@ -605,7 +612,7 @@ async fn file_locked_keyring_operations_fail() { #[cfg(feature = "tokio")] async fn file_item_lock_with_locked_keyring_fails() { let temp_dir = tempdir().unwrap(); - let backends = all_backends(temp_dir).await; + let (_setup, backends) = all_backends(temp_dir).await; let keyring = &backends[0]; keyring diff --git a/server/Cargo.toml b/server/Cargo.toml index db5053992..8220be302 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,6 +10,15 @@ repository.workspace = true rust-version.workspace = true version.workspace = true +[[bin]] +name = "oo7-daemon" +path = "src/main.rs" + +[lib] +name = "oo7_server" +path = "src/lib.rs" +test = false + [dependencies] ashpd = {workspace = true, features = ["backend", "secret", "tracing"]} base64 = {version = "0.22", optional = true} @@ -34,8 +43,10 @@ tracing = "0.1" tracing-subscriber.workspace = true zbus = { workspace = true, features = ["p2p"] } zeroize.workspace = true +tempfile = { workspace = true, optional = true } [features] +test-util = ["dep:tempfile"] default = ["native_crypto"] native_crypto = ["gnome_native_crypto", "plasma_native_crypto"] openssl_crypto = ["gnome_openssl_crypto", "plasma_openssl_crypto"] @@ -59,5 +70,4 @@ plasma_openssl_crypto = [ [dev-dependencies] rustix = { version = "1.1", default-features = false, features = ["net"] } -serial_test = "3.4" tempfile.workspace = true diff --git a/server/src/collection/tests.rs b/server/src/collection/tests.rs index 0d1c43219..1a4ae7f48 100644 --- a/server/src/collection/tests.rs +++ b/server/src/collection/tests.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use oo7::dbus; use tokio_stream::StreamExt; -use crate::tests::{TestServiceSetup, gnome_prompter_test, plasma_prompter_test}; +use crate::{service::PrompterType, tests::TestServiceSetup}; #[tokio::test] async fn create_item_plain() -> Result<(), Box> { @@ -15,17 +15,13 @@ async fn create_item_plain() -> Result<(), Box> { // Wait to ensure timestamp will be different tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - // Create an item using the proper API - let secret = oo7::Secret::text("my-secret-password"); - let dbus_secret = dbus::api::DBusSecret::new(setup.session, secret.clone()); - - let item = setup.collections[0] + // Create an item + let item = setup .create_item( "Test Item", &[("application", "test-app"), ("type", "password")], - &dbus_secret, + "my-secret-password", false, - None, ) .await?; @@ -51,19 +47,14 @@ async fn create_item_plain() -> Result<(), Box> { #[tokio::test] async fn create_item_encrypted() -> Result<(), Box> { let setup = TestServiceSetup::encrypted_session(true).await?; - let aes_key = setup.aes_key.unwrap(); - // Create an encrypted item using the proper API - let secret = oo7::Secret::text("my-encrypted-secret"); - let dbus_secret = dbus::api::DBusSecret::new_encrypted(setup.session, secret, &aes_key)?; - - let item = setup.collections[0] + // Create an encrypted item using the helper (automatically handles encryption) + let item = setup .create_item( "Test Encrypted Item", &[("application", "test-app"), ("type", "encrypted-password")], - &dbus_secret, + "my-encrypted-secret", false, - None, ) .await?; @@ -80,29 +71,21 @@ async fn search_items_after_creation() -> Result<(), Box> let setup = TestServiceSetup::plain_session(true).await?; // Create two items with different attributes - let secret1 = oo7::Secret::text("password1"); - let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1); - - setup.collections[0] + setup .create_item( "Firefox Password", &[("application", "firefox"), ("username", "user1")], - &dbus_secret1, + "password1", false, - None, ) .await?; - let secret2 = oo7::Secret::text("password2"); - let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2); - - setup.collections[0] + setup .create_item( "Chrome Password", &[("application", "chrome"), ("username", "user2")], - &dbus_secret2, + "password2", false, - None, ) .await?; @@ -138,16 +121,12 @@ async fn search_items_subset_matching() -> Result<(), Box let setup = TestServiceSetup::plain_session(true).await?; // Create an item with multiple attributes (url and username) - let secret = oo7::Secret::text("my-password"); - let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret); - - setup.collections[0] + setup .create_item( "Zed Login", &[("url", "https://zed.dev"), ("username", "alice")], - &dbus_secret, + "my-password", false, - None, ) .await?; @@ -208,15 +187,12 @@ async fn create_item_with_replace() -> Result<(), Box> { // Create first item let secret1 = oo7::Secret::text("original-password"); - let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1.clone()); - - let item1 = setup.collections[0] + let item1 = setup .create_item( "Test Item", &[("application", "myapp"), ("username", "user")], - &dbus_secret1, + secret1.clone(), false, - None, ) .await?; @@ -230,15 +206,12 @@ async fn create_item_with_replace() -> Result<(), Box> { // Create second item with same attributes and replace=true let secret2 = oo7::Secret::text("replaced-password"); - let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2.clone()); - - let item2 = setup.collections[0] + let item2 = setup .create_item( "Test Item", &[("application", "myapp"), ("username", "user")], - &dbus_secret2, + secret2.clone(), true, // replace=true - None, ) .await?; @@ -356,11 +329,8 @@ async fn item_created_signal() -> Result<(), Box> { tokio::pin!(signal_stream); // Create an item - let secret = oo7::Secret::text("test-secret"); - let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret); - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item("Test Item", &[("app", "test")], "test-secret", false) .await?; // Wait for signal with timeout @@ -386,11 +356,8 @@ async fn item_deleted_signal() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; // Create an item - let secret = oo7::Secret::text("test-secret"); - let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret); - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item("Test Item", &[("app", "test")], "test-secret", false) .await?; let item_path = item.inner().path().to_owned(); @@ -459,18 +426,12 @@ async fn delete_collection() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; // Create some items in the collection - let secret1 = oo7::Secret::text("password1"); - let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1); - - setup.collections[0] - .create_item("Item 1", &[("app", "test")], &dbus_secret1, false, None) + setup + .create_item("Item 1", &[("app", "test")], "password1", false) .await?; - let secret2 = oo7::Secret::text("password2"); - let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2); - - setup.collections[0] - .create_item("Item 2", &[("app", "test")], &dbus_secret2, false, None) + setup + .create_item("Item 2", &[("app", "test")], "password2", false) .await?; // Verify items were created @@ -545,26 +506,13 @@ async fn collection_deleted_signal() -> Result<(), Box> { Ok(()) } -gnome_prompter_test!( - create_item_in_locked_collection_gnome, - create_item_in_locked_collection -); -plasma_prompter_test!( - create_item_in_locked_collection_plasma, - create_item_in_locked_collection -); - -async fn create_item_in_locked_collection() -> Result<(), Box> { +async fn create_item_in_locked_collection_impl( + prompter_type: PrompterType, +) -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; + setup.server.set_prompter_type(prompter_type).await; - let collection = setup - .server - .collection_from_path(setup.collections[0].inner().path()) - .await - .expect("Collection should exist"); - collection - .set_locked(true, setup.keyring_secret.clone()) - .await?; + setup.lock_collection(&setup.collections[0]).await?; assert!( setup.collections[0].is_locked().await?, @@ -572,15 +520,12 @@ async fn create_item_in_locked_collection() -> Result<(), Box Result<(), Box Result<(), Box> { + create_item_in_locked_collection_impl(PrompterType::GNOME).await +} + +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +#[tokio::test] +async fn create_item_in_locked_collection_plasma() -> Result<(), Box> { + create_item_in_locked_collection_impl(PrompterType::Plasma).await +} -async fn delete_locked_collection_with_prompt() -> Result<(), Box> { +async fn delete_locked_collection_with_prompt_impl( + prompter_type: PrompterType, +) -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; + setup.server.set_prompter_type(prompter_type).await; let default_collection = setup.default_collection().await?; - let collection = setup - .server - .collection_from_path(default_collection.inner().path()) - .await - .expect("Collection should exist"); - collection - .set_locked(true, setup.keyring_secret.clone()) - .await?; + setup.lock_collection(default_collection).await?; assert!( default_collection.is_locked().await?, @@ -670,27 +614,29 @@ async fn delete_locked_collection_with_prompt() -> Result<(), Box Result<(), Box> { + delete_locked_collection_with_prompt_impl(PrompterType::GNOME).await +} + +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +#[tokio::test] +async fn delete_locked_collection_with_prompt_plasma() -> Result<(), Box> { + delete_locked_collection_with_prompt_impl(PrompterType::Plasma).await +} -async fn unlock_retry() -> Result<(), Box> { +async fn unlock_retry_impl(prompter_type: PrompterType) -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; + setup.server.set_prompter_type(prompter_type).await; let default_collection = setup.default_collection().await?; - let secret = oo7::Secret::text("test-secret-data"); - let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret); + let dbus_secret = setup.create_dbus_secret("test-secret-data")?; default_collection .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) .await?; - let collection = setup - .server - .collection_from_path(default_collection.inner().path()) - .await - .expect("Collection should exist"); - collection - .set_locked(true, setup.keyring_secret.clone()) - .await?; + setup.lock_collection(default_collection).await?; assert!( default_collection.is_locked().await?, @@ -724,6 +670,18 @@ async fn unlock_retry() -> Result<(), Box> { Ok(()) } +#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] +#[tokio::test] +async fn unlock_retry_gnome() -> Result<(), Box> { + unlock_retry_impl(PrompterType::GNOME).await +} + +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +#[tokio::test] +async fn unlock_retry_plasma() -> Result<(), Box> { + unlock_retry_impl(PrompterType::Plasma).await +} + #[tokio::test] async fn locked_collection_operations() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; @@ -735,14 +693,7 @@ async fn locked_collection_operations() -> Result<(), Box ); // Lock the collection - let collection = setup - .server - .collection_from_path(setup.collections[0].inner().path()) - .await - .expect("Collection should exist"); - collection - .set_locked(true, setup.keyring_secret.clone()) - .await?; + setup.lock_collection(&setup.collections[0]).await?; // Verify collection is now locked assert!( diff --git a/server/src/gnome/internal.rs b/server/src/gnome/internal.rs index f924914ee..f581cbc6c 100644 --- a/server/src/gnome/internal.rs +++ b/server/src/gnome/internal.rs @@ -217,8 +217,6 @@ impl InternalInterface { #[cfg(test)] mod tests { - use std::sync::Arc; - use oo7::{Secret, dbus}; use zbus::zvariant::{ObjectPath, OwnedObjectPath}; @@ -272,13 +270,7 @@ mod tests { let properties = oo7::dbus::api::Properties::for_collection(label); // Prepare the master password secret - let master_secret = Secret::text("my-master-password"); - let aes_key = setup.aes_key.as_ref().unwrap(); - let dbus_secret = oo7::dbus::api::DBusSecret::new_encrypted( - Arc::clone(&setup.session), - master_secret, - aes_key, - )?; + let dbus_secret = setup.create_dbus_secret("my-master-password")?; let dbus_secret_inner = dbus_secret.into(); // Call CreateWithMasterPassword via D-Bus @@ -319,7 +311,7 @@ mod tests { // Lock the collection setup .service_api - .lock(&[collection_path.clone()], None) + .lock(std::slice::from_ref(&collection_path), None) .await?; // Verify it's locked @@ -330,12 +322,7 @@ mod tests { // Prepare the unlock secret (use the keyring secret) let unlock_secret = setup.keyring_secret.clone().unwrap(); - let aes_key = setup.aes_key.as_ref().unwrap(); - let dbus_secret = oo7::dbus::api::DBusSecret::new_encrypted( - Arc::clone(&setup.session), - unlock_secret, - aes_key, - )?; + let dbus_secret = setup.create_dbus_secret(unlock_secret)?; let dbus_secret_inner = dbus_secret.into(); // Call UnlockWithMasterPassword via D-Bus @@ -367,17 +354,8 @@ mod tests { let original_secret = setup.keyring_secret.clone().unwrap(); let new_secret = Secret::text("new-master-password"); - let aes_key = setup.aes_key.as_ref().unwrap(); - let original_dbus = dbus::api::DBusSecret::new_encrypted( - Arc::clone(&setup.session), - original_secret, - aes_key, - )?; - let new_dbus = dbus::api::DBusSecret::new_encrypted( - Arc::clone(&setup.session), - new_secret.clone(), - aes_key, - )?; + let original_dbus = setup.create_dbus_secret(original_secret)?; + let new_dbus = setup.create_dbus_secret(new_secret.clone())?; // Call ChangeWithMasterPassword via D-Bus internal_proxy @@ -391,7 +369,7 @@ mod tests { // Verify the password was changed by locking and unlocking with new password setup .service_api - .lock(&[collection_path.clone()], None) + .lock(std::slice::from_ref(&collection_path), None) .await?; assert!( default_collection.is_locked().await?, @@ -399,8 +377,7 @@ mod tests { ); // Unlock with new password via D-Bus - let unlock_dbus = - dbus::api::DBusSecret::new_encrypted(Arc::clone(&setup.session), new_secret, aes_key)?; + let unlock_dbus = setup.create_dbus_secret(new_secret)?; internal_proxy .unlock_with_master_password(&collection_path.as_ref(), unlock_dbus.into()) .await?; @@ -416,6 +393,8 @@ mod tests { #[tokio::test] async fn test_change_with_prompt() -> Result<(), Box> { let setup = TestServiceSetup::encrypted_session(true).await?; + setup.set_password_accept(true).await; + let internal_proxy = InternalInterfaceProxyProxy::builder(&setup.client_conn) .build() .await?; @@ -435,8 +414,38 @@ mod tests { "Prompt path should not be empty" ); - // Verify the prompt exists and is accessible via D-Bus - let _prompt_proxy = dbus::api::Prompt::new(&setup.client_conn, prompt_path).await?; + // Get the prompt and complete it + let prompt_proxy = dbus::api::Prompt::new(&setup.client_conn, prompt_path) + .await? + .unwrap(); + let new_password = Secret::text("new-password-from-prompt"); + setup.set_password_queue(vec![new_password.clone()]).await; + + prompt_proxy.prompt(None).await?; + + // Wait for prompt to complete + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + // Verify the password was changed by locking and unlocking with new password + setup + .service_api + .lock(std::slice::from_ref(&collection_path), None) + .await?; + assert!( + default_collection.is_locked().await?, + "Collection should be locked" + ); + + // Unlock with new password via D-Bus + let unlock_dbus = setup.create_dbus_secret(new_password)?; + internal_proxy + .unlock_with_master_password(&collection_path.as_ref(), unlock_dbus.into()) + .await?; + + assert!( + !default_collection.is_locked().await?, + "Collection should be unlocked with new password" + ); Ok(()) } @@ -453,10 +462,7 @@ mod tests { default_collection.inner().path().to_owned().into(); // Create an item first so that the unlock validation has something to validate - let aes_key = setup.aes_key.as_ref().unwrap(); - let item_secret = Secret::text("item-secret"); - let dbus_secret = - dbus::api::DBusSecret::new_encrypted(Arc::clone(&setup.session), item_secret, aes_key)?; + let dbus_secret = setup.create_dbus_secret("item-secret")?; let mut attributes = std::collections::HashMap::new(); attributes.insert("test".to_string(), "value".to_string()); @@ -468,7 +474,7 @@ mod tests { // Lock the collection setup .service_api - .lock(&[collection_path.clone()], None) + .lock(std::slice::from_ref(&collection_path), None) .await?; // Verify it's locked before attempting unlock @@ -478,12 +484,7 @@ mod tests { ); // Try to unlock with wrong password via D-Bus - let wrong_secret = Secret::text("wrong-password"); - let wrong_dbus_secret = dbus::api::DBusSecret::new_encrypted( - Arc::clone(&setup.session), - wrong_secret, - aes_key, - )?; + let wrong_dbus_secret = setup.create_dbus_secret("wrong-password")?; let result = internal_proxy .unlock_with_master_password(&collection_path.as_ref(), wrong_dbus_secret.into()) diff --git a/server/src/item/tests.rs b/server/src/item/tests.rs index d0ef2d910..5e51a039a 100644 --- a/server/src/item/tests.rs +++ b/server/src/item/tests.rs @@ -3,23 +3,14 @@ use std::sync::Arc; use oo7::dbus; use tokio_stream::StreamExt; -use crate::tests::{TestServiceSetup, gnome_prompter_test, plasma_prompter_test}; +use crate::{service::PrompterType, tests::TestServiceSetup}; #[tokio::test] async fn label_property() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; - let secret = oo7::Secret::text("test-secret"); - let dbus_secret = dbus::api::DBusSecret::new(setup.session, secret); - - let item = setup.collections[0] - .create_item( - "Original Label", - &[("app", "test")], - &dbus_secret, - false, - None, - ) + let item = setup + .create_item("Original Label", &[("app", "test")], "test-secret", false) .await?; // Get label @@ -56,16 +47,12 @@ async fn label_property() -> Result<(), Box> { async fn attributes_property() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; - let secret = oo7::Secret::text("test-secret"); - let dbus_secret = dbus::api::DBusSecret::new(setup.session, secret); - - let item = setup.collections[0] + let item = setup .create_item( "Test Item", &[("app", "firefox"), ("username", "user@example.com")], - &dbus_secret, + "test-secret", false, - None, ) .await?; @@ -103,12 +90,8 @@ async fn attributes_property() -> Result<(), Box> { async fn timestamps() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; - let collections = setup.service_api.collections().await?; - let secret = oo7::Secret::text("test-secret"); - let dbus_secret = dbus::api::DBusSecret::new(setup.session, secret); - - let item = collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item("Test Item", &[("app", "test")], "test-secret", false) .await?; // Get created timestamp @@ -134,10 +117,8 @@ async fn secret_retrieval_plain() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; let secret = oo7::Secret::blob(b"my-secret-password"); - let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret.clone()); - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item("Test Item", &[("app", "test")], secret.clone(), false) .await?; // Retrieve secret @@ -159,11 +140,8 @@ async fn secret_retrieval_encrypted() -> Result<(), Box> let aes_key = setup.aes_key.as_ref().unwrap(); let secret = oo7::Secret::text("my-encrypted-secret"); - let dbus_secret = - dbus::api::DBusSecret::new_encrypted(Arc::clone(&setup.session), secret.clone(), aes_key)?; - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item("Test Item", &[("app", "test")], secret.clone(), false) .await?; // Retrieve secret @@ -188,11 +166,8 @@ async fn secret_retrieval_encrypted() -> Result<(), Box> async fn delete_item() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; - let secret = oo7::Secret::text("test-secret"); - let dbus_secret = dbus::api::DBusSecret::new(setup.session, secret); - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item("Test Item", &[("app", "test")], "test-secret", false) .await?; // Verify item exists @@ -213,11 +188,13 @@ async fn set_secret_plain() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; let original_secret = oo7::Secret::text("original-password"); - let dbus_secret = - dbus::api::DBusSecret::new(Arc::clone(&setup.session), original_secret.clone()); - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item( + "Test Item", + &[("app", "test")], + original_secret.clone(), + false, + ) .await?; // Verify original secret @@ -237,8 +214,7 @@ async fn set_secret_plain() -> Result<(), Box> { // Update the secret let new_secret = oo7::Secret::blob(b"new-password"); - let new_dbus_secret = - dbus::api::DBusSecret::new(Arc::clone(&setup.session), new_secret.clone()); + let new_dbus_secret = setup.create_dbus_secret(new_secret.clone())?; item.set_secret(&new_dbus_secret).await?; // Verify updated secret @@ -263,17 +239,16 @@ async fn set_secret_plain() -> Result<(), Box> { #[tokio::test] async fn set_secret_encrypted() -> Result<(), Box> { let setup = TestServiceSetup::encrypted_session(true).await?; - let aes_key = setup.aes_key.unwrap(); + let aes_key = setup.aes_key.as_ref().unwrap().clone(); let original_secret = oo7::Secret::text("original-encrypted-password"); - let dbus_secret = dbus::api::DBusSecret::new_encrypted( - Arc::clone(&setup.session), - original_secret.clone(), - &aes_key, - )?; - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item( + "Test Item", + &[("app", "test")], + original_secret.clone(), + false, + ) .await?; // Verify original secret @@ -291,11 +266,7 @@ async fn set_secret_encrypted() -> Result<(), Box> { // Update the secret let new_secret = oo7::Secret::text("new-encrypted-password"); - let new_dbus_secret = dbus::api::DBusSecret::new_encrypted( - Arc::clone(&setup.session), - new_secret.clone(), - &aes_key, - )?; + let new_dbus_secret = setup.create_dbus_secret(new_secret.clone())?; item.set_secret(&new_dbus_secret).await?; // Verify updated secret @@ -319,11 +290,8 @@ async fn set_secret_encrypted() -> Result<(), Box> { async fn get_secret_invalid_session() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; - let secret = oo7::Secret::text("test-secret"); - let dbus_secret = dbus::api::DBusSecret::new(setup.session, secret); - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item("Test Item", &[("app", "test")], "test-secret", false) .await?; // Try to get secret with invalid session path @@ -348,11 +316,8 @@ async fn get_secret_invalid_session() -> Result<(), Box> async fn set_secret_invalid_session() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; - let secret = oo7::Secret::text("test-secret"); - let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret); - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item("Test Item", &[("app", "test")], "test-secret", false) .await?; let new_secret = oo7::Secret::text("new-secret"); @@ -381,11 +346,8 @@ async fn set_secret_invalid_session() -> Result<(), Box> async fn item_changed_signal() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; - let secret = oo7::Secret::text("test-secret"); - let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret); - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item("Test Item", &[("app", "test")], "test-secret", false) .await?; // Subscribe to ItemChanged signal @@ -425,8 +387,7 @@ async fn item_changed_signal() -> Result<(), Box> { ); // Change secret and verify signal again - let new_secret = oo7::Secret::text("new-secret"); - let new_dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), new_secret); + let new_dbus_secret = setup.create_dbus_secret("new-secret")?; item.set_secret(&new_dbus_secret).await?; let signal_result = @@ -440,21 +401,14 @@ async fn item_changed_signal() -> Result<(), Box> { Ok(()) } -gnome_prompter_test!( - delete_locked_item_with_prompt_gnome, - delete_locked_item_with_prompt -); -plasma_prompter_test!( - delete_locked_item_with_prompt_plasma, - delete_locked_item_with_prompt -); - -async fn delete_locked_item_with_prompt() -> Result<(), Box> { +async fn delete_locked_item_with_prompt_impl( + prompter_type: PrompterType, +) -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; + setup.server.set_prompter_type(prompter_type).await; let default_collection = setup.default_collection().await?; - let secret = oo7::Secret::text("test-password"); - let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret.clone()); + let dbus_secret = setup.create_dbus_secret("test-password")?; let item = default_collection .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) @@ -463,14 +417,7 @@ async fn delete_locked_item_with_prompt() -> Result<(), Box Result<(), Box Result<(), Box> { + delete_locked_item_with_prompt_impl(PrompterType::GNOME).await +} + +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +#[tokio::test] +async fn delete_locked_item_with_prompt_plasma() -> Result<(), Box> { + delete_locked_item_with_prompt_impl(PrompterType::Plasma).await +} + #[tokio::test] async fn locked_item_operations() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; // Create an item - let secret = oo7::Secret::text("test-password"); - let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret.clone()); - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item("Test Item", &[("app", "test")], "test-password", false) .await?; // Verify item is unlocked initially assert!(!item.is_locked().await?, "Item should start unlocked"); // Lock the collection (which locks the item) - let collection = setup - .server - .collection_from_path(setup.collections[0].inner().path()) - .await - .expect("Collection should exist"); - collection - .set_locked(true, setup.keyring_secret.clone()) - .await?; + setup.lock_collection(&setup.collections[0]).await?; // Verify item is now locked assert!( @@ -527,8 +476,7 @@ async fn locked_item_operations() -> Result<(), Box> { ); // Test 2: set_secret should fail with IsLocked - let new_secret = oo7::Secret::text("new-password"); - let new_dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), new_secret); + let new_dbus_secret = setup.create_dbus_secret("new-password")?; let result = item.set_secret(&new_dbus_secret).await; assert!( matches!( diff --git a/server/src/lib.rs b/server/src/lib.rs new file mode 100644 index 000000000..9c7c5e1c3 --- /dev/null +++ b/server/src/lib.rs @@ -0,0 +1,23 @@ +// Library interface for oo7_server +// Only expose test utilities when the test-util feature is enabled +#![allow(unused)] + +pub(crate) mod collection; +pub(crate) mod error; +#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] +pub(crate) mod gnome; +pub(crate) mod item; +pub(crate) mod pam_listener; +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +pub(crate) mod plasma; +pub(crate) mod prompt; +pub(crate) mod service; +pub(crate) mod session; + +pub(crate) use service::Service; + +#[cfg(feature = "test-util")] +pub mod tests; + +#[cfg(all(test, not(feature = "test-util")))] +mod tests; diff --git a/server/src/pam_listener/mod.rs b/server/src/pam_listener/mod.rs index 4f7ad544e..ef959a5b2 100644 --- a/server/src/pam_listener/mod.rs +++ b/server/src/pam_listener/mod.rs @@ -45,7 +45,7 @@ impl PamMessage { /// PAM listener that receives authentication secrets from the PAM module #[derive(Clone)] pub struct PamListener { - socket_path: PathBuf, + pub(crate) socket_path: PathBuf, service: Service, /// Current user's secret, used to unlock their keyring user_secrets: Arc>>, @@ -54,9 +54,10 @@ pub struct PamListener { impl PamListener { pub fn new(service: Service) -> Self { let uid = unsafe { libc::getuid() }; - let socket_path = std::env::var("OO7_PAM_SOCKET") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from(format!("/run/user/{uid}/oo7-pam.sock"))); + let socket_path = service + .pam_socket + .clone() + .unwrap_or_else(|| PathBuf::from(format!("/run/user/{uid}/oo7-pam.sock"))); Self { socket_path, diff --git a/server/src/pam_listener/tests.rs b/server/src/pam_listener/tests.rs index 4c6d31c5e..8bcdc4c0c 100644 --- a/server/src/pam_listener/tests.rs +++ b/server/src/pam_listener/tests.rs @@ -39,11 +39,8 @@ async fn send_pam_message( } #[tokio::test] -#[serial_test::serial(xdg_env)] async fn pam_migrates_v0_keyrings() -> Result<(), Box> { let temp_dir = tempfile::tempdir()?; - unsafe { std::env::set_var("XDG_DATA_HOME", temp_dir.path()) }; - unsafe { std::env::set_var("OO7_PAM_SOCKET", temp_dir.path().join("pam.sock")) }; let keyrings_dir = temp_dir.path().join("keyrings"); let v1_dir = keyrings_dir.join("v1"); @@ -57,7 +54,13 @@ async fn pam_migrates_v0_keyrings() -> Result<(), Box> { let v0_path = keyrings_dir.join("legacy.keyring"); tokio::fs::copy(&fixture_path, &v0_path).await?; - let setup = crate::tests::TestServiceSetup::with_disk_keyrings(None).await?; + let pam_socket_path = temp_dir.path().join("pam.sock"); + let setup = crate::tests::TestServiceSetup::with_disk_keyrings( + temp_dir.path().to_path_buf(), + Some(pam_socket_path), + None, + ) + .await?; assert_eq!( setup.server.pending_migrations.lock().await.len(), @@ -76,13 +79,23 @@ async fn pam_migrates_v0_keyrings() -> Result<(), Box> { let message = create_pam_message(PamOperation::Unlock, "testuser", &[], v0_secret.as_bytes()); send_pam_message(&socket_path, &message).await?; - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + for i in 0..10 { + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + let pending = setup.server.pending_migrations.lock().await.len(); + eprintln!("Attempt {}: pending_migrations = {}", i + 1, pending); + if pending == 0 { + break; + } + } + let pending = setup.server.pending_migrations.lock().await; assert_eq!( - setup.server.pending_migrations.lock().await.len(), + pending.len(), 0, - "V0 keyring should be migrated" + "V0 keyring should be migrated. Remaining: {:?}", + pending.keys().collect::>() ); + drop(pending); let collections = setup.server.collections.lock().await; let mut legacy_collection = None; @@ -105,21 +118,16 @@ async fn pam_migrates_v0_keyrings() -> Result<(), Box> { "V0 file should be removed after migration" ); - unsafe { std::env::remove_var("XDG_DATA_HOME") }; - unsafe { std::env::remove_var("OO7_PAM_SOCKET") }; Ok(()) } #[tokio::test] -#[serial_test::serial(xdg_env)] async fn pam_unlocks_locked_collections() -> Result<(), Box> { let temp_dir = tempfile::tempdir()?; - unsafe { std::env::set_var("XDG_DATA_HOME", temp_dir.path()) }; - unsafe { std::env::set_var("OO7_PAM_SOCKET", temp_dir.path().join("pam.sock")) }; // Create a v1 keyring with a known password let secret = Secret::from("my-secure-password"); - let keyring = UnlockedKeyring::open("work", secret.clone()).await?; + let keyring = UnlockedKeyring::open_at(temp_dir.path(), "work", secret.clone()).await?; keyring .create_item( "Work Item", @@ -130,7 +138,13 @@ async fn pam_unlocks_locked_collections() -> Result<(), Box Result<(), Box Result<(), Box> { let temp_dir = tempfile::tempdir()?; - unsafe { std::env::set_var("XDG_DATA_HOME", temp_dir.path()) }; - unsafe { std::env::set_var("OO7_PAM_SOCKET", temp_dir.path().join("pam.sock")) }; let old_secret = Secret::from("old-password"); - let keyring = UnlockedKeyring::open("work", old_secret.clone()).await?; + let keyring = UnlockedKeyring::open_at(temp_dir.path(), "work", old_secret.clone()).await?; keyring .create_item( "Work Item", @@ -199,7 +208,13 @@ async fn pam_change_password() -> Result<(), Box> { .await?; keyring.write().await?; - let setup = crate::tests::TestServiceSetup::with_disk_keyrings(None).await?; + let pam_socket_path = temp_dir.path().join("pam.sock"); + let setup = crate::tests::TestServiceSetup::with_disk_keyrings( + temp_dir.path().to_path_buf(), + Some(pam_socket_path), + None, + ) + .await?; let collections = setup.server.collections.lock().await; let mut work_collection = None; @@ -265,8 +280,6 @@ async fn pam_change_password() -> Result<(), Box> { "New password should unlock collection" ); - unsafe { std::env::remove_var("XDG_DATA_HOME") }; - unsafe { std::env::remove_var("OO7_PAM_SOCKET") }; Ok(()) } diff --git a/server/src/plasma/prompter.rs b/server/src/plasma/prompter.rs index b63dfd0bb..1ac56ebdc 100644 --- a/server/src/plasma/prompter.rs +++ b/server/src/plasma/prompter.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2025 Harald Sitter -use std::{env, os::fd::AsFd}; +use std::os::fd::AsFd; use ashpd::WindowIdentifierType; use gettextrs::gettext; @@ -26,38 +26,32 @@ pub enum CallbackAction { } #[must_use] -pub async fn in_plasma_environment(_connection: &zbus::Connection) -> bool { - #[cfg(test)] - return env::var("OO7_DAEMON_PROMPTER_TEST").is_ok_and(|v| v.to_lowercase() == "plasma"); - - #[cfg(not(test))] - { - static IS_PLASMA: std::sync::OnceLock = std::sync::OnceLock::new(); - if let Some(cached_value) = IS_PLASMA.get() { - return *cached_value; - } - - let is_plasma = async { - if !env::var("XDG_CURRENT_DESKTOP").is_ok_and(|v| v.to_lowercase() == "kde") { - return false; - } +pub async fn in_plasma_environment(connection: &zbus::Connection) -> bool { + static IS_PLASMA: std::sync::OnceLock = std::sync::OnceLock::new(); + if let Some(cached_value) = IS_PLASMA.get() { + return *cached_value; + } - let proxy = match zbus::fdo::DBusProxy::new(_connection).await { - Ok(proxy) => proxy, - Err(_) => return false, - }; - let activatable_names = match proxy.list_activatable_names().await { - Ok(names) => names, - Err(_) => return false, - }; - activatable_names - .iter() - .any(|name| name.as_str() == "org.kde.secretprompter") + let is_plasma = async { + if !std::env::var("XDG_CURRENT_DESKTOP").is_ok_and(|v| v.to_lowercase() == "kde") { + return false; } - .await; - *IS_PLASMA.get_or_init(|| is_plasma) + let proxy = match zbus::fdo::DBusProxy::new(connection).await { + Ok(proxy) => proxy, + Err(_) => return false, + }; + let activatable_names = match proxy.list_activatable_names().await { + Ok(names) => names, + Err(_) => return false, + }; + activatable_names + .iter() + .any(|name| name.as_str() == "org.kde.secretprompter") } + .await; + + *IS_PLASMA.get_or_init(|| is_plasma) } #[zbus::proxy( diff --git a/server/src/prompt/mod.rs b/server/src/prompt/mod.rs index afce2dc0c..c801bd87d 100644 --- a/server/src/prompt/mod.rs +++ b/server/src/prompt/mod.rs @@ -13,8 +13,11 @@ use zbus::{ #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] use crate::gnome::prompter::{GNOMEPrompterCallback, GNOMEPrompterProxy}; #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] -use crate::plasma::prompter::{PlasmaPrompterCallback, in_plasma_environment}; -use crate::{error::custom_service_error, service::Service}; +use crate::plasma::prompter::PlasmaPrompterCallback; +use crate::{ + error::custom_service_error, + service::{PrompterType, Service}, +}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PromptRole { @@ -95,63 +98,70 @@ impl std::fmt::Debug for Prompt { impl Prompt { pub async fn prompt(&self, window_id: Optional<&str>) -> Result<(), ServiceError> { let window_id = (*window_id).and_then(|w| ashpd::WindowIdentifierType::from_str(w).ok()); - #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] - if in_plasma_environment(self.service.connection()).await { - if self.plasma_callback.get().is_some() { - return Err(custom_service_error( - "A prompt callback is ongoing already.", - )); - } - let callback = - PlasmaPrompterCallback::new(self.service.clone(), self.path.clone()).await; - let path = OwnedObjectPath::from(callback.path().clone()); + let prompter_type = self.service.prompter_type().await; - self.plasma_callback - .set(callback.clone()) - .expect("A prompt callback is only set once"); - self.service - .object_server() - .at(&path, callback.clone()) - .await?; - tracing::debug!("Prompt `{}` created.", self.path); - - return callback.start(&self.role, window_id, &self.label).await; + #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] + { + if prompter_type == PrompterType::Plasma { + if self.plasma_callback.get().is_some() { + return Err(custom_service_error( + "A prompt callback is ongoing already.", + )); + } + + let callback = + PlasmaPrompterCallback::new(self.service.clone(), self.path.clone()).await; + let path = OwnedObjectPath::from(callback.path().clone()); + + self.plasma_callback + .set(callback.clone()) + .expect("A prompt callback is only set once"); + self.service + .object_server() + .at(&path, callback.clone()) + .await?; + tracing::debug!("Prompt `{}` created.", self.path); + + return callback.start(&self.role, window_id, &self.label).await; + } } #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] { - if self.gnome_callback.get().is_some() { - return Err(custom_service_error( - "A GNOME prompt callback is ongoing already.", - )); - }; - - let callback = - GNOMEPrompterCallback::new(window_id, self.service.clone(), self.path.clone()) - .await - .map_err(|err| { - custom_service_error(&format!( - "Failed to create GNOMEPrompterCallback {err}." - )) - })?; - - let path = OwnedObjectPath::from(callback.path().clone()); - - self.gnome_callback - .set(callback.clone()) - .expect("A prompt callback is only set once"); - - self.service.object_server().at(&path, callback).await?; - tracing::debug!("Prompt `{}` created.", self.path); - - // Starts GNOME System Prompting. - // Spawned separately to avoid blocking the early return of the current - // execution. - let prompter = GNOMEPrompterProxy::new(self.service.connection()).await?; - tokio::spawn(async move { prompter.begin_prompting(&path).await }); - - return Ok(()); + if prompter_type == PrompterType::GNOME { + if self.gnome_callback.get().is_some() { + return Err(custom_service_error( + "A GNOME prompt callback is ongoing already.", + )); + }; + + let callback = + GNOMEPrompterCallback::new(window_id, self.service.clone(), self.path.clone()) + .await + .map_err(|err| { + custom_service_error(&format!( + "Failed to create GNOMEPrompterCallback {err}." + )) + })?; + + let path = OwnedObjectPath::from(callback.path().clone()); + + self.gnome_callback + .set(callback.clone()) + .expect("A prompt callback is only set once"); + + self.service.object_server().at(&path, callback).await?; + tracing::debug!("Prompt `{}` created.", self.path); + + // Starts GNOME System Prompting. + // Spawned separately to avoid blocking the early return of the current + // execution. + let prompter = GNOMEPrompterProxy::new(self.service.connection()).await?; + tokio::spawn(async move { prompter.begin_prompting(&path).await }); + + return Ok(()); + } } #[allow(unreachable_code)] diff --git a/server/src/prompt/tests.rs b/server/src/prompt/tests.rs index c7a7cefc9..2a7948903 100644 --- a/server/src/prompt/tests.rs +++ b/server/src/prompt/tests.rs @@ -1,20 +1,13 @@ -use crate::tests::{TestServiceSetup, gnome_prompter_test, plasma_prompter_test}; +use crate::{service::PrompterType, tests::TestServiceSetup}; -gnome_prompter_test!(prompt_called_twice_error_gnome, prompt_called_twice_error); -plasma_prompter_test!(prompt_called_twice_error_plasma, prompt_called_twice_error); - -async fn prompt_called_twice_error() -> Result<(), Box> { +async fn prompt_called_twice_error_impl( + prompter_type: PrompterType, +) -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; + setup.server.set_prompter_type(prompter_type).await; // Lock the collection to create a prompt scenario - let collection = setup - .server - .collection_from_path(setup.collections[0].inner().path()) - .await - .expect("Collection should exist"); - collection - .set_locked(true, setup.keyring_secret.clone()) - .await?; + setup.lock_collection(&setup.collections[0]).await?; // Get a prompt path by calling unlock (which creates a prompt but doesn't // auto-trigger it) @@ -45,20 +38,25 @@ async fn prompt_called_twice_error() -> Result<(), Box> { Ok(()) } +#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] +#[tokio::test] +async fn prompt_called_twice_error_gnome() -> Result<(), Box> { + prompt_called_twice_error_impl(PrompterType::GNOME).await +} + +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +#[tokio::test] +async fn prompt_called_twice_error_plasma() -> Result<(), Box> { + prompt_called_twice_error_impl(PrompterType::Plasma).await +} + #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] #[tokio::test] async fn prompt_not_found_error() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; // Lock the collection to create a prompt scenario - let collection = setup - .server - .collection_from_path(setup.collections[0].inner().path()) - .await - .expect("Collection should exist"); - collection - .set_locked(true, setup.keyring_secret.clone()) - .await?; + setup.lock_collection(&setup.collections[0]).await?; // Create a prompt using server API let (_unlocked, prompt_path) = setup @@ -111,14 +109,7 @@ async fn dismiss_prompt_cleanup() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; // Lock the collection to create a prompt scenario - let collection = setup - .server - .collection_from_path(setup.collections[0].inner().path()) - .await - .expect("Collection should exist"); - collection - .set_locked(true, setup.keyring_secret.clone()) - .await?; + setup.lock_collection(&setup.collections[0]).await?; // Get a prompt path by calling unlock let (_unlocked, prompt_path) = setup diff --git a/server/src/service/mod.rs b/server/src/service/mod.rs index 44e55c235..5275397b4 100644 --- a/server/src/service/mod.rs +++ b/server/src/service/mod.rs @@ -24,6 +24,8 @@ use zbus::{ #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] pub use crate::gnome::internal::{INTERNAL_INTERFACE_PATH, InternalInterface}; +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +use crate::plasma::prompter::in_plasma_environment; use crate::{ collection::Collection, error::{Error, custom_service_error}, @@ -34,7 +36,15 @@ use crate::{ const DEFAULT_COLLECTION_ALIAS_PATH: ObjectPath<'static> = ObjectPath::from_static_str_unchecked("/org/freedesktop/secrets/aliases/default"); -#[derive(Debug, Default, Clone)] +/// Prompter type +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PrompterType { + #[allow(clippy::upper_case_acronyms)] + GNOME, + Plasma, +} + +#[derive(Debug, Clone)] pub struct Service { // Properties pub(crate) collections: Arc>>, @@ -52,6 +62,12 @@ pub struct Service { #[allow(clippy::type_complexity)] pub(crate) pending_migrations: Arc>>, + // Data directory for keyrings (e.g., ~/.local/share or test temp dir) + data_dir: std::path::PathBuf, + // PAM socket path (None for tests that don't need PAM listener) + pub(crate) pam_socket: Option, + // Override for prompter type (mainly for tests) + pub(crate) prompter_type_override: Arc>>, } #[zbus::interface(name = "org.freedesktop.Secret.Service")] @@ -91,13 +107,13 @@ impl Service { let sender = if let Some(s) = header.sender() { s.to_owned() } else { - #[cfg(test)] + #[cfg(any(test, feature = "test-util"))] { // For p2p test connections, use a dummy sender since p2p connections // don't have a bus to assign unique names - UniqueName::try_from(":p2p.test").unwrap().into() + UniqueName::try_from(":p2p.test").unwrap() } - #[cfg(not(test))] + #[cfg(not(any(test, feature = "test-util")))] { return Err(custom_service_error("Failed to get sender from header.")); } @@ -391,8 +407,70 @@ impl Service { impl Service { const LOGIN_ALIAS: &str = "login"; + /// Set the prompter type override + #[cfg(test)] + pub(crate) async fn set_prompter_type(&self, prompter_type: PrompterType) { + *self.prompter_type_override.lock().await = Some(prompter_type); + } + + /// Get the prompter type to use + pub(crate) async fn prompter_type(&self) -> PrompterType { + if let Some(override_type) = self.prompter_type_override.lock().await.as_ref() { + return *override_type; + } + + #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] + { + if in_plasma_environment(self.connection()).await { + return PrompterType::Plasma; + } + } + + PrompterType::GNOME + } + + pub(crate) fn new( + data_dir: std::path::PathBuf, + pam_socket: Option, + ) -> Self { + Self { + collections: Arc::new(Mutex::new(HashMap::new())), + connection: Arc::new(OnceLock::new()), + sessions: Arc::new(Mutex::new(HashMap::new())), + session_index: Arc::new(RwLock::new(0)), + prompts: Arc::new(Mutex::new(HashMap::new())), + prompt_index: Arc::new(RwLock::new(0)), + pending_collections: Arc::new(Mutex::new(HashMap::new())), + pending_migrations: Arc::new(Mutex::new(HashMap::new())), + data_dir, + pam_socket, + prompter_type_override: Arc::new(Mutex::new(None)), + } + } + pub async fn run(secret: Option, request_replacement: bool) -> Result<(), Error> { - let service = Self::default(); + // Compute data directory from environment variables + let data_dir = std::env::var_os("XDG_DATA_HOME") + .and_then(|h| if h.is_empty() { None } else { Some(h) }) + .map(std::path::PathBuf::from) + .and_then(|p| if p.is_absolute() { Some(p) } else { None }) + .or_else(|| { + std::env::var_os("HOME") + .and_then(|h| if h.is_empty() { None } else { Some(h) }) + .map(std::path::PathBuf::from) + .map(|p| p.join(".local/share")) + }) + .ok_or_else(|| { + Error::IO(std::io::Error::new( + std::io::ErrorKind::NotFound, + "No data directory found (XDG_DATA_HOME or HOME)", + )) + })?; + + // Compute PAM socket path from environment variable + let pam_socket = std::env::var_os("OO7_PAM_SOCKET").map(std::path::PathBuf::from); + + let service = Self::new(data_dir, pam_socket); let connection = zbus::connection::Builder::session()? .allow_name_replacements(true) @@ -433,12 +511,14 @@ impl Service { Ok(()) } - #[cfg(test)] + #[cfg(any(test, feature = "test-util"))] pub async fn run_with_connection( connection: zbus::Connection, + data_dir: std::path::PathBuf, + pam_socket: Option, secret: Option, ) -> Result { - let service = Self::default(); + let service = Self::new(data_dir, pam_socket); // Serve the service at the standard path connection @@ -482,24 +562,7 @@ impl Service { ) -> Result, Error> { let mut discovered = Vec::new(); - // Get data directory using the same logic as oo7::file::api::data_dir() - let data_dir = std::env::var_os("XDG_DATA_HOME") - .and_then(|h| if h.is_empty() { None } else { Some(h) }) - .map(std::path::PathBuf::from) - .and_then(|p| if p.is_absolute() { Some(p) } else { None }) - .or_else(|| { - std::env::var_os("HOME") - .and_then(|h| if h.is_empty() { None } else { Some(h) }) - .map(std::path::PathBuf::from) - .map(|p| p.join(".local/share")) - }); - - let Some(data_dir) = data_dir else { - tracing::warn!("No data directory found, skipping keyring discovery"); - return Ok(discovered); - }; - - let keyrings_dir = data_dir.join("keyrings"); + let keyrings_dir = self.data_dir.join("keyrings"); // Scan for v1 keyrings first let v1_dir = keyrings_dir.join("v1"); @@ -627,7 +690,7 @@ impl Service { if let Some(secret) = secret { tracing::debug!("Attempting immediate migration of v0 keyring '{name}'",); - match UnlockedKeyring::open(name, secret.clone()).await { + match UnlockedKeyring::open_at(&self.data_dir, name, secret.clone()).await { Ok(unlocked) => { tracing::info!("Successfully migrated v0 keyring '{name}' to v1",); @@ -703,11 +766,11 @@ impl Service { tracing::info!("No default collection found, creating 'Login' keyring"); let keyring = if let Some(secret) = secret { - UnlockedKeyring::open(Self::LOGIN_ALIAS, secret) + UnlockedKeyring::open_at(&self.data_dir, Self::LOGIN_ALIAS, secret) .await .map(Keyring::Unlocked) } else { - LockedKeyring::open(Self::LOGIN_ALIAS) + LockedKeyring::open_at(&self.data_dir, Self::LOGIN_ALIAS) .await .map(Keyring::Locked) }; @@ -944,7 +1007,7 @@ impl Service { secret: Secret, ) -> Result { // Create a persistent keyring with the provided secret - let keyring = UnlockedKeyring::open(&label.to_lowercase(), secret) + let keyring = UnlockedKeyring::open_at(&self.data_dir, &label.to_lowercase(), secret) .await .map_err(|err| custom_service_error(&format!("Failed to create keyring: {err}")))?; @@ -1088,7 +1151,7 @@ impl Service { for (name, (path, label, alias)) in pending.iter() { tracing::debug!("Attempting to migrate pending v0 keyring: {}", name); - match UnlockedKeyring::open(name, secret.clone()).await { + match UnlockedKeyring::open_at(&self.data_dir, name, secret.clone()).await { Ok(unlocked) => { tracing::info!("Successfully migrated v0 keyring '{}' to v1", name); diff --git a/server/src/service/tests.rs b/server/src/service/tests.rs index 6c0748f42..495b8b70c 100644 --- a/server/src/service/tests.rs +++ b/server/src/service/tests.rs @@ -1,7 +1,7 @@ use oo7::dbus; use super::*; -use crate::tests::{TestServiceSetup, gnome_prompter_test, plasma_prompter_test}; +use crate::tests::TestServiceSetup; #[tokio::test] async fn open_session_plain() -> Result<(), Box> { @@ -77,48 +77,17 @@ async fn search_items() -> Result<(), Box> { // Test with both locked and unlocked items // Create items in default collection (unlocked) - let secret1 = Secret::text("password1"); - let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1); - - setup.collections[0] - .create_item( - "Unlocked Item", - &[("app", "testapp")], - &dbus_secret1, - false, - None, - ) + setup + .create_item("Unlocked Item", &[("app", "testapp")], "password1", false) .await?; // Create item in default collection and lock it - let secret2 = Secret::text("password2"); - let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2); - - let locked_item = setup.collections[0] - .create_item( - "Locked Item", - &[("app", "testapp")], - &dbus_secret2, - false, - None, - ) + let locked_item = setup + .create_item("Locked Item", &[("app", "testapp")], "password2", false) .await?; // Lock just this item (not the whole collection) - let collection = setup - .server - .collection_from_path(setup.collections[0].inner().path()) - .await - .expect("Collection should exist"); - - let keyring = collection.keyring.read().await; - let unlocked_keyring = keyring.as_ref().unwrap().as_unlocked(); - - let locked_item = collection - .item_from_path(locked_item.inner().path()) - .await - .unwrap(); - locked_item.set_locked(true, unlocked_keyring).await?; + setup.lock_item(&locked_item).await?; // Search for items with the shared attribute let (unlocked, locked) = setup @@ -137,7 +106,8 @@ async fn get_secrets() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; // Test with empty items list - edge case - let secrets = setup.service_api.secrets(&vec![], &setup.session).await?; + #[allow(clippy::mutable_key_type)] + let secrets = setup.service_api.secrets(&[], &setup.session).await?; assert!( secrets.is_empty(), "Should return empty secrets for empty items list" @@ -145,21 +115,18 @@ async fn get_secrets() -> Result<(), Box> { // Create two items with different secrets let secret1 = Secret::text("password1"); - let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1.clone()); - - let item1 = setup.collections[0] - .create_item("Item 1", &[("app", "test1")], &dbus_secret1, false, None) + let item1 = setup + .create_item("Item 1", &[("app", "test1")], secret1.clone(), false) .await?; let secret2 = Secret::text("password2"); - let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2.clone()); - - let item2 = setup.collections[0] - .create_item("Item 2", &[("app", "test2")], &dbus_secret2, false, None) + let item2 = setup + .create_item("Item 2", &[("app", "test2")], secret2.clone(), false) .await?; // Get secrets for both items let item_paths = vec![item1.clone(), item2.clone()]; + #[allow(clippy::mutable_key_type)] let secrets = setup .service_api .secrets(&item_paths, &setup.session) @@ -188,21 +155,18 @@ async fn get_secrets_multiple_collections() -> Result<(), Box Result<(), Box Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; // Create items in default collection - let secret1 = Secret::text("password1"); - let dbus_secret1 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret1); - - setup.collections[0] + setup .create_item( "Firefox Login", &[("application", "firefox"), ("type", "login")], - &dbus_secret1, + "password1", false, - None, ) .await?; - let secret2 = Secret::text("password2"); - let dbus_secret2 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret2); - - setup.collections[0] + setup .create_item( "Chrome Login", &[("application", "chrome"), ("type", "login")], - &dbus_secret2, + "password2", false, - None, ) .await?; // Create item in session collection - let secret3 = Secret::text("password3"); - let dbus_secret3 = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret3); + let dbus_secret3 = setup.create_dbus_secret("password3")?; setup.collections[1] .create_item( @@ -378,11 +334,8 @@ async fn get_secrets_invalid_session() -> Result<(), Box> let setup = TestServiceSetup::plain_session(true).await?; // Create an item - let secret = Secret::text("test-password"); - let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret); - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item("Test Item", &[("app", "test")], "test-password", false) .await?; // Try to get secrets with invalid session path @@ -436,11 +389,8 @@ async fn get_secrets_with_non_existent_items() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { ); // Test 3: Already unlocked objects - let secret = Secret::text("test-password"); - let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret); - - let item = setup.collections[0] - .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) + let item = setup + .create_item("Test Item", &[("app", "test")], "test-password", false) .await?; // Verify item is unlocked @@ -632,21 +578,14 @@ async fn lock_non_existent_objects() -> Result<(), Box> { Ok(()) } -gnome_prompter_test!(unlock_collection_prompt_gnome, unlock_collection_prompt); -plasma_prompter_test!(unlock_collection_prompt_plasma, unlock_collection_prompt); - -async fn unlock_collection_prompt() -> Result<(), Box> { +async fn unlock_collection_prompt_impl( + prompter_type: PrompterType, +) -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; + setup.server.set_prompter_type(prompter_type).await; // Lock the collection using server-side API - let collection = setup - .server - .collection_from_path(setup.collections[0].inner().path()) - .await - .expect("Collection should exist"); - collection - .set_locked(true, setup.keyring_secret.clone()) - .await?; + setup.lock_collection(&setup.collections[0]).await?; assert!( setup.collections[0].is_locked().await?, @@ -671,9 +610,7 @@ async fn unlock_collection_prompt() -> Result<(), Box> { ); // Lock the collection again for dismiss test - collection - .set_locked(true, setup.keyring_secret.clone()) - .await?; + setup.lock_collection(&setup.collections[0]).await?; assert!( setup.collections[0].is_locked().await?, "Collection should be locked again" @@ -698,29 +635,33 @@ async fn unlock_collection_prompt() -> Result<(), Box> { Ok(()) } -gnome_prompter_test!(unlock_item_prompt_gnome, unlock_item_prompt); -plasma_prompter_test!(unlock_item_prompt_plasma, unlock_item_prompt); +#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] +#[tokio::test] +async fn unlock_collection_prompt_gnome() -> Result<(), Box> { + unlock_collection_prompt_impl(PrompterType::GNOME).await +} + +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +#[tokio::test] +async fn unlock_collection_prompt_plasma() -> Result<(), Box> { + unlock_collection_prompt_impl(PrompterType::Plasma).await +} -async fn unlock_item_prompt() -> Result<(), Box> { +async fn unlock_item_prompt_impl( + prompter_type: PrompterType, +) -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; + setup.server.set_prompter_type(prompter_type).await; // Create an item - let secret = Secret::text("test-password"); - let dbus_secret = dbus::api::DBusSecret::new(Arc::clone(&setup.session), secret); + let dbus_secret = setup.create_dbus_secret("test-password")?; let default_collection = setup.service_api.read_alias("default").await?.unwrap(); let item = default_collection .create_item("Test Item", &[("app", "test")], &dbus_secret, false, None) .await?; // Lock the collection (which locks the item) - let collection = setup - .server - .collection_from_path(default_collection.inner().path()) - .await - .expect("Collection should exist"); - collection - .set_locked(true, setup.keyring_secret.clone()) - .await?; + setup.lock_collection(&default_collection).await?; assert!( item.is_locked().await?, @@ -745,9 +686,7 @@ async fn unlock_item_prompt() -> Result<(), Box> { ); // Lock the item again for dismiss test - collection - .set_locked(true, setup.keyring_secret.clone()) - .await?; + setup.lock_collection(&default_collection).await?; assert!(item.is_locked().await?, "Item should be locked again"); // Test 2: Unlock with dismiss @@ -771,10 +710,8 @@ async fn lock_item_in_unlocked_collection() -> Result<(), Box Result<(), Box> { ); // Unlock the collection - let collection = setup - .server - .collection_from_path(setup.collections[0].inner().path()) - .await - .expect("Collection should exist"); - collection - .set_locked(false, setup.keyring_secret.clone()) - .await?; + setup.unlock_collection(&setup.collections[0]).await?; assert!( !setup.collections[0].is_locked().await?, "Collection should be unlocked" @@ -867,19 +797,23 @@ async fn lock_collection_no_prompt() -> Result<(), Box> { Ok(()) } -gnome_prompter_test!( - create_collection_basic_gnome, - create_collection_basic, - serial_test::serial(xdg_env) -); -plasma_prompter_test!( - create_collection_basic_plasma, - create_collection_basic, - serial_test::serial(xdg_env) -); - -async fn create_collection_basic() -> Result<(), Box> { +#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] +#[tokio::test] +async fn unlock_item_prompt_gnome() -> Result<(), Box> { + unlock_item_prompt_impl(PrompterType::GNOME).await +} + +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +#[tokio::test] +async fn unlock_item_prompt_plasma() -> Result<(), Box> { + unlock_item_prompt_impl(PrompterType::Plasma).await +} + +async fn create_collection_basic_impl( + prompter_type: PrompterType, +) -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; + setup.server.set_prompter_type(prompter_type).await; // Get initial collection count let initial_collections = setup.service_api.collections().await?; @@ -938,19 +872,23 @@ async fn create_collection_basic() -> Result<(), Box> { Ok(()) } -gnome_prompter_test!( - create_collection_signal_gnome, - create_collection_signal, - serial_test::serial(xdg_env) -); -plasma_prompter_test!( - create_collection_signal_plasma, - create_collection_signal, - serial_test::serial(xdg_env) -); - -async fn create_collection_signal() -> Result<(), Box> { +#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] +#[tokio::test] +async fn create_collection_basic_gnome() -> Result<(), Box> { + create_collection_basic_impl(PrompterType::GNOME).await +} + +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +#[tokio::test] +async fn create_collection_basic_plasma() -> Result<(), Box> { + create_collection_basic_impl(PrompterType::Plasma).await +} + +async fn create_collection_signal_impl( + prompter_type: PrompterType, +) -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; + setup.server.set_prompter_type(prompter_type).await; // Subscribe to CollectionCreated signal let signal_stream = setup.service_api.receive_collection_created().await?; @@ -991,19 +929,23 @@ async fn create_collection_signal() -> Result<(), Box> { Ok(()) } -gnome_prompter_test!( - create_collection_and_add_items_gnome, - create_collection_and_add_items, - serial_test::serial(xdg_env) -); -plasma_prompter_test!( - create_collection_and_add_items_plasma, - create_collection_and_add_items, - serial_test::serial(xdg_env) -); - -async fn create_collection_and_add_items() -> Result<(), Box> { +#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] +#[tokio::test] +async fn create_collection_signal_gnome() -> Result<(), Box> { + create_collection_signal_impl(PrompterType::GNOME).await +} + +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +#[tokio::test] +async fn create_collection_signal_plasma() -> Result<(), Box> { + create_collection_signal_impl(PrompterType::Plasma).await +} + +async fn create_collection_and_add_items_impl( + prompter_type: PrompterType, +) -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; + setup.server.set_prompter_type(prompter_type).await; // Create a new collection let collection = setup @@ -1019,7 +961,7 @@ async fn create_collection_and_add_items() -> Result<(), Box Result<(), Box Result<(), Box> { +#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] +#[tokio::test] +async fn create_collection_and_add_items_gnome() -> Result<(), Box> { + create_collection_and_add_items_impl(PrompterType::GNOME).await +} + +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +#[tokio::test] +async fn create_collection_and_add_items_plasma() -> Result<(), Box> { + create_collection_and_add_items_impl(PrompterType::Plasma).await +} + +async fn create_collection_dismissed_impl( + prompter_type: PrompterType, +) -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; + setup.server.set_prompter_type(prompter_type).await; // Get initial collection count let initial_collections = setup.service_api.collections().await?; @@ -1104,6 +1050,18 @@ async fn create_collection_dismissed() -> Result<(), Box> Ok(()) } +#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] +#[tokio::test] +async fn create_collection_dismissed_gnome() -> Result<(), Box> { + create_collection_dismissed_impl(PrompterType::GNOME).await +} + +#[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] +#[tokio::test] +async fn create_collection_dismissed_plasma() -> Result<(), Box> { + create_collection_dismissed_impl(PrompterType::Plasma).await +} + #[tokio::test] async fn complete_collection_creation_no_pending() -> Result<(), Box> { let setup = TestServiceSetup::plain_session(true).await?; @@ -1127,13 +1085,10 @@ async fn complete_collection_creation_no_pending() -> Result<(), Box Result<(), Box> { - 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()) }; + let service = Service::new(temp_dir.path().to_path_buf(), None); // Create v1 keyrings directory let v1_dir = temp_dir.path().join("keyrings/v1"); @@ -1149,7 +1104,7 @@ async fn discover_v1_keyrings() -> Result<(), Box> { // 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?; + let keyring1 = UnlockedKeyring::open_at(temp_dir.path(), "work", secret1.clone()).await?; keyring1 .create_item( "Work Item", @@ -1161,7 +1116,7 @@ async fn discover_v1_keyrings() -> Result<(), Box> { keyring1.write().await?; let secret2 = Secret::from("password-for-personal"); - let keyring2 = UnlockedKeyring::open("personal", secret2.clone()).await?; + let keyring2 = UnlockedKeyring::open_at(temp_dir.path(), "personal", secret2.clone()).await?; keyring2 .create_item( "Personal Item", @@ -1174,7 +1129,7 @@ async fn discover_v1_keyrings() -> Result<(), Box> { // 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?; + let keyring3 = UnlockedKeyring::open_at(temp_dir.path(), "login", secret3.clone()).await?; keyring3 .create_item( "Login Item", @@ -1252,17 +1207,13 @@ async fn discover_v1_keyrings() -> Result<(), Box> { "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> { - let service = Service::default(); let temp_dir = tempfile::tempdir()?; - unsafe { std::env::set_var("XDG_DATA_HOME", temp_dir.path()) }; + let service = Service::new(temp_dir.path().to_path_buf(), None); let keyrings_dir = temp_dir.path().join("keyrings"); let v1_dir = keyrings_dir.join("v1"); @@ -1280,7 +1231,7 @@ async fn discover_v0_keyrings() -> Result<(), Box> { // Create a v1 keyring for mixed scenario let v1_secret = Secret::from("v1-password"); - let v1_keyring = UnlockedKeyring::open("modern", v1_secret.clone()).await?; + let v1_keyring = UnlockedKeyring::open_at(temp_dir.path(), "modern", v1_secret.clone()).await?; v1_keyring .create_item( "V1 Item", @@ -1338,6 +1289,5 @@ async fn discover_v0_keyrings() -> Result<(), Box> { "V0 should be pending with wrong password" ); - unsafe { std::env::remove_var("XDG_DATA_HOME") }; Ok(()) } diff --git a/server/src/tests.rs b/server/src/tests.rs index 07fb27caf..8d7372445 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -14,51 +14,6 @@ use crate::gnome::{ }; use crate::service::Service; -macro_rules! gnome_prompter_test { - ($name:tt, $test_function:tt $(, $meta:meta)*) => { -#[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] - #[tokio::test] - #[serial_test::serial(prompter_env)] - $( - #[$meta] - )* - async fn $name() -> Result<(), Box> { - unsafe { - std::env::set_var("OO7_DAEMON_PROMPTER_TEST", "gnome"); - } - let ret = $test_function().await; - unsafe { - std::env::remove_var("OO7_DAEMON_PROMPTER_TEST"); - } - ret - } - } -} -pub(crate) use gnome_prompter_test; - -macro_rules! plasma_prompter_test { - ($name:tt, $test_function:tt $(, $meta:meta)*) => { - #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] - - #[tokio::test] - #[serial_test::serial(prompter_env)] - $( - #[$meta] - )* - async fn $name() -> Result<(), Box> { - unsafe { - std::env::set_var("OO7_DAEMON_PROMPTER_TEST", "plasma"); - } - let ret = $test_function().await; - unsafe { - std::env::remove_var("OO7_DAEMON_PROMPTER_TEST"); - } - ret - } - } -} -pub(crate) use plasma_prompter_test; - /// Helper to create a peer-to-peer connection pair using Unix socket async fn create_p2p_connection() -> Result<(zbus::Connection, zbus::Connection), Box> { @@ -78,7 +33,7 @@ async fn create_p2p_connection() Ok((server_conn, client_conn)) } -pub(crate) struct TestServiceSetup { +pub struct TestServiceSetup { pub server: Service, pub client_conn: zbus::Connection, pub service_api: dbus::api::Service, @@ -88,14 +43,16 @@ pub(crate) struct TestServiceSetup { pub keyring_secret: Option, pub aes_key: Option>, #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] - pub mock_prompter: MockPrompterService, + pub(crate) mock_prompter: MockPrompterService, #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] - pub mock_prompter_plasma: MockPrompterServicePlasma, + pub(crate) mock_prompter_plasma: MockPrompterServicePlasma, + // Keep temp dir alive for duration of test + _temp_dir: tempfile::TempDir, } impl TestServiceSetup { /// Get the default/Login collection - pub(crate) async fn default_collection( + pub async fn default_collection( &self, ) -> Result<&dbus::api::Collection, Box> { for collection in &self.collections { @@ -107,7 +64,7 @@ impl TestServiceSetup { Err("Default collection not found".into()) } - pub(crate) async fn plain_session( + pub async fn plain_session( with_default_collection: bool, ) -> Result> { let (server_conn, client_conn) = create_p2p_connection().await?; @@ -118,7 +75,14 @@ impl TestServiceSetup { None }; - let server = Service::run_with_connection(server_conn.clone(), secret.clone()).await?; + let temp_dir = tempfile::TempDir::new()?; + let server = Service::run_with_connection( + server_conn.clone(), + temp_dir.path().to_path_buf(), + None, + secret.clone(), + ) + .await?; // Create and serve the mock prompter #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] @@ -163,10 +127,11 @@ impl TestServiceSetup { mock_prompter, #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] mock_prompter_plasma, + _temp_dir: temp_dir, }) } - pub(crate) async fn encrypted_session( + pub async fn encrypted_session( with_default_collection: bool, ) -> Result> { let (server_conn, client_conn) = create_p2p_connection().await?; @@ -177,7 +142,14 @@ impl TestServiceSetup { None }; - let server = Service::run_with_connection(server_conn.clone(), secret.clone()).await?; + let temp_dir = tempfile::TempDir::new()?; + let server = Service::run_with_connection( + server_conn.clone(), + temp_dir.path().to_path_buf(), + None, + secret.clone(), + ) + .await?; // Create and serve the mock prompter #[cfg(any(feature = "gnome_native_crypto", feature = "gnome_openssl_crypto"))] @@ -214,7 +186,7 @@ impl TestServiceSetup { let session = Arc::new(session); let aes_key = - oo7::Key::generate_aes_key(&client_private_key, &server_public_key.as_ref().unwrap())?; + oo7::Key::generate_aes_key(&client_private_key, server_public_key.as_ref().unwrap())?; let collections = service_api.collections().await?; @@ -231,19 +203,23 @@ impl TestServiceSetup { mock_prompter, #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] mock_prompter_plasma, + _temp_dir: temp_dir, }) } /// Create a test setup that discovers keyrings from disk /// This is useful for PAM tests that need to create keyrings on disk first pub(crate) async fn with_disk_keyrings( + data_dir: std::path::PathBuf, + pam_socket: Option, secret: Option, ) -> Result> { use zbus::proxy::Defaults; let (server_conn, client_conn) = create_p2p_connection().await?; - let service = crate::Service::default(); + let temp_dir = tempfile::TempDir::new()?; + let service = crate::Service::new(data_dir, pam_socket); server_conn .object_server() @@ -300,6 +276,7 @@ impl TestServiceSetup { mock_prompter, #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] mock_prompter_plasma, + _temp_dir: temp_dir, }) } @@ -320,6 +297,103 @@ impl TestServiceSetup { .set_password_queue(passwords) .await; } + + /// Helper to create a DBusSecret + /// + /// Automatically handles plain vs encrypted based on whether aes_key is + /// set. + pub(crate) fn create_dbus_secret( + &self, + secret: impl Into, + ) -> Result> { + let secret = secret.into(); + let dbus_secret = if let Some(ref aes_key) = self.aes_key { + dbus::api::DBusSecret::new_encrypted(Arc::clone(&self.session), secret, aes_key)? + } else { + dbus::api::DBusSecret::new(Arc::clone(&self.session), secret) + }; + Ok(dbus_secret) + } + + /// Helper to create a test item in the default collection (index 0) + /// + /// Automatically handles plain vs encrypted sessions based on whether + /// aes_key is set. + pub(crate) async fn create_item( + &self, + label: &str, + attributes: &impl oo7::AsAttributes, + secret: impl Into, + replace: bool, + ) -> Result> { + let dbus_secret = self.create_dbus_secret(secret)?; + + let item = self.collections[0] + .create_item(label, attributes, &dbus_secret, replace, None) + .await?; + + Ok(item) + } + + /// Helper to lock a collection + /// + /// Gets the server-side collection and locks it with the keyring secret. + pub(crate) async fn lock_collection( + &self, + collection: &dbus::api::Collection, + ) -> Result<(), Box> { + let server_collection = self + .server + .collection_from_path(collection.inner().path()) + .await + .expect("Collection should exist"); + server_collection + .set_locked(true, self.keyring_secret.clone()) + .await?; + Ok(()) + } + + /// Helper to unlock a collection + /// + /// Gets the server-side collection and unlocks it with the keyring secret. + pub(crate) async fn unlock_collection( + &self, + collection: &dbus::api::Collection, + ) -> Result<(), Box> { + let server_collection = self + .server + .collection_from_path(collection.inner().path()) + .await + .expect("Collection should exist"); + server_collection + .set_locked(false, self.keyring_secret.clone()) + .await?; + Ok(()) + } + + /// Helper to lock an item + /// + /// Gets the server-side collection and item, then locks the item. + pub(crate) async fn lock_item( + &self, + item: &dbus::api::Item, + ) -> Result<(), Box> { + let collection = self + .server + .collection_from_path(self.collections[0].inner().path()) + .await + .expect("Collection should exist"); + + let keyring = collection.keyring.read().await; + let unlocked_keyring = keyring.as_ref().unwrap().as_unlocked(); + + let server_item = collection + .item_from_path(item.inner().path()) + .await + .unwrap(); + server_item.set_locked(true, unlocked_keyring).await?; + Ok(()) + } } /// Mock implementation of org.gnome.keyring.internal.Prompter @@ -571,7 +645,6 @@ pub(crate) struct MockPrompterServicePlasma { } #[cfg(any(feature = "plasma_native_crypto", feature = "plasma_openssl_crypto"))] - impl MockPrompterServicePlasma { pub fn new() -> Self { Self { @@ -660,7 +733,7 @@ impl MockPrompterServicePlasma { let connection = connection.clone(); // Reject case - if *self.should_accept.lock().await == false { + if !*self.should_accept.lock().await { tokio::spawn(async move { tracing::debug!( "MockPrompterServicePlasma: dismissing prompt for {}",