diff --git a/Cargo.lock b/Cargo.lock index f67cdf3..3d95328 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2222,6 +2222,7 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ + "serde", "zeroize_derive", ] diff --git a/crates/idfoto-core/Cargo.toml b/crates/idfoto-core/Cargo.toml index 2372e51..0ae3a70 100644 --- a/crates/idfoto-core/Cargo.toml +++ b/crates/idfoto-core/Cargo.toml @@ -16,7 +16,7 @@ ed25519-dalek = { version = "2", features = ["rand_core"] } image = { version = "0.25", default-features = false, features = ["jpeg"] } # Typed-item additions -zeroize = { version = "1", features = ["zeroize_derive"] } +zeroize = { version = "1", features = ["zeroize_derive", "serde"] } zxcvbn = { version = "3", default-features = false } bip39 = { version = "2", default-features = false, features = ["std"] } unicode-normalization = "0.1" diff --git a/crates/idfoto-core/src/item_types/login.rs b/crates/idfoto-core/src/item_types/login.rs index 4ab7756..d7a4ce7 100644 --- a/crates/idfoto-core/src/item_types/login.rs +++ b/crates/idfoto-core/src/item_types/login.rs @@ -1,3 +1,63 @@ +//! Login item core: username, password (Zeroizing), URL, optional TOTP. + use serde::{Deserialize, Serialize}; +use url::Url; +use zeroize::Zeroizing; + +use crate::item_types::TotpConfig; + #[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct LoginCore {} +pub struct LoginCore { + #[serde(skip_serializing_if = "Option::is_none")] + pub username: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub password: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub totp: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_login_round_trips() { + let login = LoginCore::default(); + let json = serde_json::to_string(&login).unwrap(); + let parsed: LoginCore = serde_json::from_str(&json).unwrap(); + assert!(parsed.username.is_none()); + assert!(parsed.password.is_none()); + } + + #[test] + fn full_login_round_trips() { + let login = LoginCore { + username: Some("alice".into()), + password: Some(Zeroizing::new("hunter2".into())), + url: Some(Url::parse("https://github.com/login").unwrap()), + totp: None, + }; + let json = serde_json::to_string(&login).unwrap(); + let parsed: LoginCore = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.username.as_deref(), Some("alice")); + assert_eq!(parsed.password.as_deref().map(String::as_str), Some("hunter2")); + assert_eq!(parsed.url.as_ref().map(Url::as_str), Some("https://github.com/login")); + } + + #[test] + fn omitted_fields_dont_appear_in_json() { + let login = LoginCore { + username: Some("alice".into()), + password: None, + url: None, + totp: None, + }; + let json = serde_json::to_string(&login).unwrap(); + assert!(!json.contains("password")); + assert!(!json.contains("url")); + assert!(!json.contains("totp")); + assert!(json.contains("alice")); + } +}