//! Integration coverage for the client paste-request contract. //! //! Scope: validate key loading, encryption metadata, and truncation behavior //! through the public paste helper. //! Targets: `client/src/paste.rs`. //! Why: these checks are pure policy and crypto framing, so they belong in the //! centralized `testing/tests` contract suite. use chacha20poly1305::aead::Aead; use chacha20poly1305::{ChaCha20Poly1305, Key, KeyInit, Nonce}; use lesavka_client::paste::build_paste_request; use serial_test::serial; use temp_env::with_var; use tempfile::tempdir; const TEST_KEY_HEX: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; const TEST_KEY_RAW: &str = "0123456789abcdef0123456789abcdef"; #[test] #[serial] fn build_paste_request_requires_a_shared_key() { let dir = tempdir().expect("tempdir"); with_var("LESAVKA_PASTE_KEY", None::<&str>, || { with_var("LESAVKA_PASTE_KEY_FILE", None::<&str>, || { with_var("HOME", Some(dir.path().as_os_str()), || { let err = build_paste_request("hello").expect_err("missing key should fail"); let rendered = format!("{err:#}"); assert!( rendered.contains("paste key file") || rendered.contains("LESAVKA_PASTE_KEY") ); }); }); }); } #[test] #[serial] fn build_paste_request_requires_explicit_key_when_home_is_unset() { with_var("LESAVKA_PASTE_KEY", None::<&str>, || { with_var("LESAVKA_PASTE_KEY_FILE", None::<&str>, || { with_var("HOME", None::<&str>, || { let err = build_paste_request("hello").expect_err("missing key should fail"); let rendered = format!("{err:#}"); assert!(rendered.contains("LESAVKA_PASTE_KEY")); }); }); }); } #[test] #[serial] fn build_paste_request_sets_encryption_fields() { with_var("LESAVKA_PASTE_KEY", Some(TEST_KEY_HEX), || { let req = build_paste_request("hello world").expect("build request"); assert!(req.encrypted); assert_eq!(req.nonce.len(), 12); assert!(!req.data.is_empty()); }); } #[test] #[serial] fn build_paste_request_accepts_raw_32_byte_shared_key() { with_var("LESAVKA_PASTE_KEY", Some(TEST_KEY_RAW), || { let req = build_paste_request("hello raw key").expect("build request"); assert!(req.encrypted); assert_eq!(req.nonce.len(), 12); assert!(!req.data.is_empty()); }); } #[test] #[serial] fn build_paste_request_truncates_plaintext_before_encryption() { with_var("LESAVKA_PASTE_KEY", Some(TEST_KEY_HEX), || { with_var("LESAVKA_PASTE_MAX", Some("5"), || { let req = build_paste_request("hello-from-lesavka").expect("build truncated request"); let key = lesavka_common::paste::decode_shared_key(TEST_KEY_HEX).expect("decode key"); let cipher = ChaCha20Poly1305::new(Key::from_slice(&key)); let nonce = Nonce::from_slice(&req.nonce); let plaintext = cipher .decrypt(nonce, req.data.as_ref()) .expect("decrypt request data"); assert_eq!(std::str::from_utf8(&plaintext).expect("utf8"), "hello"); }); }); } #[test] #[serial] fn build_paste_request_loads_shared_key_from_file() { let dir = tempdir().expect("tempdir"); let path = dir.path().join("paste-key"); std::fs::write(&path, TEST_KEY_HEX).expect("write key"); with_var("LESAVKA_PASTE_KEY", None::<&str>, || { with_var("LESAVKA_PASTE_KEY_FILE", Some(path.as_os_str()), || { let req = build_paste_request("hello world").expect("build request"); assert!(req.encrypted); assert_eq!(req.nonce.len(), 12); assert!(!req.data.is_empty()); }); }); } #[test] #[serial] fn build_paste_request_uses_default_key_path_under_home() { let dir = tempdir().expect("tempdir"); let path = dir.path().join(".config/lesavka/paste-key"); std::fs::create_dir_all(path.parent().expect("paste key dir")).expect("create config dir"); std::fs::write(&path, TEST_KEY_HEX).expect("write key"); with_var("LESAVKA_PASTE_KEY", None::<&str>, || { with_var("LESAVKA_PASTE_KEY_FILE", None::<&str>, || { with_var("HOME", Some(dir.path().as_os_str()), || { let req = build_paste_request("hello default path").expect("build request"); assert!(req.encrypted); assert_eq!(req.nonce.len(), 12); assert!(!req.data.is_empty()); }); }); }); } #[test] #[serial] fn build_paste_request_rejects_empty_default_key_file() { let dir = tempdir().expect("tempdir"); let path = dir.path().join(".config/lesavka/paste-key"); std::fs::create_dir_all(path.parent().expect("paste key dir")).expect("create config dir"); std::fs::write(&path, "").expect("write empty key"); with_var("LESAVKA_PASTE_KEY", None::<&str>, || { with_var("LESAVKA_PASTE_KEY_FILE", None::<&str>, || { with_var("HOME", Some(dir.path().as_os_str()), || { let err = build_paste_request("hello").expect_err("empty key file should fail"); assert!(format!("{err:#}").contains("is empty")); }); }); }); }