From 40425233714ee9ff3e1a4884518b400f113c16d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E7=BF=BC=E7=BF=94?= <836250617@qq.com> Date: Mon, 18 Dec 2023 12:54:26 +0800 Subject: [PATCH] alpha1 (#10) * Update issue templates (#1) * Create test.yml (#2) * Create release.yml (#3) * Update test.yml (#4) * test * error handling (#6) * 7 action cache (#8) * action (#9) * test * test * test * test * test * test * test * test * modified: README.md description --- .github/ISSUE_TEMPLATE/bug_report.md | 27 +++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++ .github/workflows/release.yml | 45 +++++++ .github/workflows/test.yml | 52 ++++++++ Cargo.toml | 2 +- README.md | 10 +- tests/tests.rs => examples/example.rs | 23 ++-- src/config.rs | 50 ++++---- src/encrypt_utils.rs | 29 +++-- src/error.rs | 26 +++- src/lib.rs | 137 +++++++++++++++++----- tests/.gitkeep | 1 + 12 files changed, 346 insertions(+), 76 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml rename tests/tests.rs => examples/example.rs (84%) create mode 100644 tests/.gitkeep diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8ad4866 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[Bug]" +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior, or a Minimal Reproducible Code Snippet. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Information (please complete the following information):** +OS: [e.g. macOS] +Output of `rustc -V` + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..1ce1302 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[New Feature]" +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..72431cc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,45 @@ +name: Rust + +on: + pull_request: + branches: [release] + types: [closed] + +env: + CARGO_TERM_COLOR: always + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Extract version + id: extract-version + run: echo "version=$(grep -oP '^version = "\K[^"]+' Cargo.toml | awk '{$1=$1;print}')" >> $GITHUB_OUTPUT + + - name: Cache restore + uses: actions/cache/restore@v3 + id: cache-cargo-restore + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ steps.extract-version.outputs.version }} + + - name: Run clippy + run: cargo clippy --all-features --all-targets -- -D warnings + - name: Build + run: cargo build + - name: Run tests + run: cargo test + + - name: publish crates + uses: katyo/publish-crates@v2 + with: + # crates.io registry token + registry-token: ${{ secrets.crates_io }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..eae471e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,52 @@ +name: Rust + +on: + pull_request: + branches: [dev] + types: [opened] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Extract version + id: extract-version + run: echo "version=$(grep -oP '^version = "\K[^"]+' Cargo.toml | awk '{$1=$1;print}')" >> $GITHUB_OUTPUT + + - name: Cache restore + uses: actions/cache/restore@v3 + id: cache-cargo-restore + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ steps.extract-version.outputs.version }} + + - name: Run clippy + run: cargo clippy --all-features --all-targets -- -D warnings + - name: Build + run: cargo build + - name: Run tests + run: cargo test + + - name: Cache save + if: steps.cache-cargo-restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v3 + id: cache-cargo-save + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }} diff --git a/Cargo.toml b/Cargo.toml index adc4ca2..b9c7062 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "encrypt_config" version = "0.0.1-alpha1" authors = ["Louis <836250617@qq.com>"] -description = "A config manager, supporting encryption." +description = "A rust crate to manage, persist and encrypt your configurations." license = "MIT" edition = "2021" repository = "https://github.com/kingwingfly/encrypt-config" diff --git a/README.md b/README.md index b368f20..cba206c 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ A rust crate to manage, persist, encrypt configurations. ## Getting Started -Details here: [Example](tests/tests.rs) +Details here: [Example](example/eamples.rs)

(back to top)

@@ -95,8 +95,8 @@ Details here: [Example](tests/tests.rs) ## Usage -```rust -use encrypt_config::{Config, ConfigKey, ConfigResult, SecretSource}; +```rust no_run +use encrypt_config::{Config, ConfigResult, SecretSource}; #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)] struct Bar(String); @@ -108,7 +108,7 @@ impl SecretSource for SecretSourceImpl { type Value = Bar; // The key to query from `Config` - fn source_name(&self) -> ConfigKey { + fn source_name(&self) -> String { "secret_test".to_owned() } @@ -132,7 +132,7 @@ let v: Bar = config.get("secret_test").unwrap(); assert_eq!(v, Bar("world".to_owned())); // `upgrade` will return a `Patch` -let patch = SecretSourceImpl.upgrade(&Bar("Louis".to_owned())).unwrap(); +let patch = SecretSourceImpl.upgrade(&Bar("Louis".to_owned())); // No change will happen until the `Patch` is applied patch.apply(&mut config).unwrap(); let v: Bar = config.get("secret_test").unwrap(); diff --git a/tests/tests.rs b/examples/example.rs similarity index 84% rename from tests/tests.rs rename to examples/example.rs index 63fc0c5..fcb484d 100644 --- a/tests/tests.rs +++ b/examples/example.rs @@ -1,11 +1,11 @@ -use encrypt_config::{Config, ConfigKey, ConfigResult, PersistSource, SecretSource, Source}; +use encrypt_config::{Config, PersistSource, SecretSource, Source}; struct NormalSource; impl Source for NormalSource { type Value = String; type Map = Vec<(String, Self::Value)>; - fn collect(&self) -> ConfigResult { + fn collect(&self) -> Result> { Ok(vec![("key".to_owned(), "value".to_owned())]) } } @@ -17,7 +17,7 @@ struct PersistSourceImpl; impl PersistSource for PersistSourceImpl { type Value = Foo; - fn source_name(&self) -> ConfigKey { + fn source_name(&self) -> String { "test".to_owned() } @@ -37,7 +37,7 @@ struct SecretSourceImpl; impl SecretSource for SecretSourceImpl { type Value = Bar; - fn source_name(&self) -> ConfigKey { + fn source_name(&self) -> String { "secret_test".to_owned() } @@ -50,8 +50,7 @@ impl SecretSource for SecretSourceImpl { } } -#[test] -fn source_test() { +fn config_tests() { let mut config = Config::new("test"); config.add_source(NormalSource).unwrap(); config.add_persist_source(PersistSourceImpl).unwrap(); @@ -62,20 +61,22 @@ fn source_test() { assert_eq!(v, Foo("hello".to_owned())); let v: Bar = config.get("secret_test").unwrap(); assert_eq!(v, Bar("world".to_owned())); - let patch = NormalSource - .upgrade("key", &"new_value".to_owned()) - .unwrap(); + let patch = NormalSource.upgrade("key", &"new_value".to_owned()); patch.apply(&mut config).unwrap(); let v: String = config.get("key").unwrap(); assert_eq!(v, "new_value"); - let patch = PersistSourceImpl.upgrade(&Foo("hi".to_owned())).unwrap(); + let patch = PersistSourceImpl.upgrade(&Foo("hi".to_owned())); patch.apply(&mut config).unwrap(); let v: Foo = config.get("test").unwrap(); assert_eq!(v, Foo("hi".to_owned())); - let patch = SecretSourceImpl.upgrade(&Bar("Louis".to_owned())).unwrap(); + let patch = SecretSourceImpl.upgrade(&Bar("Louis".to_owned())); patch.apply(&mut config).unwrap(); let v: Bar = config.get("secret_test").unwrap(); assert_eq!(v, Bar("Louis".to_owned())); std::fs::remove_file("tests/secret_test").unwrap(); std::fs::remove_file("tests/test").unwrap(); } + +fn main() { + config_tests(); +} diff --git a/src/config.rs b/src/config.rs index 7bc9602..fe2b7e2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,16 +1,20 @@ //! # Config //! This module provides a `Config` struct that can be used to store configuration values. +use snafu::OptionExt; use std::collections::HashMap; -use crate::{encrypt_utils::Encrypter, ConfigResult, PersistSource, SecretSource, Source}; +use crate::{ + encrypt_utils::Encrypter, CollectFailed, ConfigNotFound, ConfigResult, PersistSource, + SecretSource, Source, +}; -pub type ConfigKey = String; +pub(crate) type ConfigKey = String; pub(crate) type ConfigValue = Vec; /// A struct that can be used to store configuration values. /// # Example -/// ``` +/// ```no_run /// use encrypt_config::{Config, Source, ConfigResult}; /// /// let mut config = Config::new("test"); @@ -20,7 +24,7 @@ pub(crate) type ConfigValue = Vec; /// type Value = String; /// type Map = Vec<(String, Self::Value)>; /// -/// fn collect(&self) -> ConfigResult { +/// fn collect(&self) -> Result> { /// Ok(vec![("key".to_owned(), "value".to_owned())]) /// } /// } @@ -56,7 +60,9 @@ impl Config { K: AsRef, R: serde::de::DeserializeOwned, { - let serded = self.inner.get(key.as_ref()).unwrap(); + let serded = self.inner.get(key.as_ref()).context(ConfigNotFound { + key: key.as_ref().to_owned(), + })?; Ok(serde_json::from_slice(serded).unwrap()) } @@ -64,7 +70,8 @@ impl Config { /// The source must implement [`Source`] trait, which is for normal config that does not need to be encrypted or persisted. pub fn add_source(&mut self, source: impl Source) -> ConfigResult<()> { let map = source - .collect()? + .collect() + .map_err(|_| CollectFailed.build())? .into_iter() .map(|(k, v)| (k, serde_json::to_vec(&v).unwrap())); self.inner.extend(map); @@ -74,7 +81,7 @@ impl Config { /// Add a persist source to the config. /// The source must implement [`PersistSource`] trait, which is for config that needs to be persisted. pub fn add_persist_source(&mut self, source: impl PersistSource) -> ConfigResult<()> { - let patch = source.collect()?; + let patch = source.collect(); patch.apply(self)?; Ok(()) } @@ -82,7 +89,7 @@ impl Config { /// Add a secret source to the config. /// The source must implement [`SecretSource`] trait, which is for config that needs to be encrypted and persisted. pub fn add_secret_source(&mut self, source: impl SecretSource) -> ConfigResult<()> { - let patch = source.collect()?; + let patch = source.collect(); patch.apply(self)?; Ok(()) } @@ -92,8 +99,8 @@ impl Config { /// You can get a `ConfigPatch` by calling [`PersistSource::upgrade`], and apply it by calling [`ConfigPatch::apply`] to a config. /// No change will happen until you call [`ConfigPatch::apply`]. /// # Example -/// ```rust -/// use encrypt_config::{Config, ConfigKey, PersistSource, ConfigResult}; +/// ```no_run +/// use encrypt_config::{Config, PersistSource, ConfigResult}; /// /// let mut config = Config::new("test"); /// @@ -104,7 +111,7 @@ impl Config { /// impl PersistSource for PersistSourceImpl { /// type Value = Foo; /// -/// fn source_name(&self) -> ConfigKey { +/// fn source_name(&self) -> String { /// "test".to_owned() /// } /// @@ -120,11 +127,10 @@ impl Config { /// config.add_persist_source(PersistSourceImpl).unwrap(); /// let v: Foo = config.get("test").unwrap(); /// assert_eq!(v, Foo("hello".to_owned())); -/// let patch = PersistSourceImpl.upgrade(&Foo("hi".to_owned())).unwrap(); +/// let patch = PersistSourceImpl.upgrade(&Foo("hi".to_owned())); /// patch.apply(&mut config).unwrap(); /// let v: Foo = config.get("test").unwrap(); /// assert_eq!(v, Foo("hi".to_owned())); -/// # std::fs::remove_file("tests/test").unwrap(); /// ``` pub struct ConfigPatch { key: ConfigKey, @@ -144,12 +150,14 @@ impl ConfigPatch { } } +type Func = Box ConfigResult>; + /// A patch that can be used to modify the config. /// You can get a `SecretConfigPatch` by calling [`SecretSource::upgrade`], and apply it by calling [`SecretConfigPatch::apply`] to a config. /// No change will happen until you call [`SecretConfigPatch::apply`]. /// # Example -/// ```rust -/// use encrypt_config::{Config, ConfigKey, SecretSource, ConfigResult}; +/// ```no_run +/// use encrypt_config::{Config, SecretSource, ConfigResult}; /// /// let mut config = Config::new("test"); /// @@ -160,7 +168,7 @@ impl ConfigPatch { /// impl SecretSource for SecretSourceImpl { /// type Value = Foo; /// -/// fn source_name(&self) -> ConfigKey { +/// fn source_name(&self) -> String { /// "secret_test".to_owned() /// } /// @@ -176,22 +184,18 @@ impl ConfigPatch { /// config.add_secret_source(SecretSourceImpl).unwrap(); /// let v: Foo = config.get("secret_test").unwrap(); /// assert_eq!(v, Foo("hello".to_owned())); -/// let patch = SecretSourceImpl.upgrade(&Foo("hi".to_owned())).unwrap(); +/// let patch = SecretSourceImpl.upgrade(&Foo("hi".to_owned())); /// patch.apply(&mut config).unwrap(); /// let v: Foo = config.get("secret_test").unwrap(); /// assert_eq!(v, Foo("hi".to_owned())); -/// # std::fs::remove_file("tests/secret_test").unwrap(); /// ``` pub struct SecretConfigPatch { key: ConfigKey, - func: Box ConfigResult>, + func: Func, } impl SecretConfigPatch { - pub(crate) fn new( - key: ConfigKey, - func: Box ConfigResult>>, - ) -> Self { + pub(crate) fn new(key: ConfigKey, func: Func) -> Self { Self { key, func } } diff --git a/src/encrypt_utils.rs b/src/encrypt_utils.rs index 3883f91..706af80 100644 --- a/src/encrypt_utils.rs +++ b/src/encrypt_utils.rs @@ -16,14 +16,13 @@ impl Encrypter { pub(crate) fn new(config_name: impl AsRef) -> ConfigResult { let entry = keyring_entry(config_name); match entry.get_password() { - Ok(serded_enc) => Ok(serde_json::from_str(&serded_enc).unwrap()), - Err(_) => { + Ok(serded_enc) => Ok(serde_json::from_str(&serded_enc)?), + Err(keyring::Error::NoEntry) => { let new_enc = Encrypter::build(); - entry - .set_password(&serde_json::to_string(&new_enc).unwrap()) - .unwrap(); + entry.set_password(&serde_json::to_string(&new_enc).unwrap())?; Ok(new_enc) } + Err(e) => Err(e)?, } } @@ -43,6 +42,22 @@ impl Encrypter { // self.encrypt_serded(&origin) // } + /// This is used to encrypt seriliazed Value. + /// # Arguments + /// * origin - The returning of `serde_json::to_vec` + /// # Example + /// ```ignore + /// let foo = Foo::new(); + /// let serded = serde_json::to_vec(&foo)?; + /// let encrypter = Encrypter::new("test")?; + /// let encrypted = encrypter.encrypt_serded(serded)?; + /// ``` + /// # Question + /// Q: Why not use `Foo` as origin more conviniently? + /// + /// A: The user passes `&Foo` to [`SecretSource::upgrade`] to upgrade the config, which returns a [`SecretConfigPatch`], + /// containing a [`Func`] as its field. `Func`, which is a boxed closure, should take the ownership of `Foo` if directly use + /// it. To avoid this, and due to we need seriliaze it anyway, we just move its serded `Vec` into the closure. pub(crate) fn encrypt_serded(&self, origin: &[u8]) -> ConfigResult { let mut rng = rand::thread_rng(); let chunk_size = if cfg!(not(target_os = "windows")) { @@ -53,7 +68,7 @@ impl Encrypter { let pub_key = RsaPublicKey::from(&self.priv_key); let mut encrypted = vec![]; for c in origin.chunks(chunk_size) { - encrypted.extend(pub_key.encrypt(&mut rng, Pkcs1v15Encrypt, c).unwrap()); + encrypted.extend(pub_key.encrypt(&mut rng, Pkcs1v15Encrypt, c)?); } Ok(encrypted) } @@ -66,7 +81,7 @@ impl Encrypter { 128 }; for c in encrypted.chunks(chunk_size) { - decrypted.extend(self.priv_key.decrypt(Pkcs1v15Encrypt, c).unwrap()); + decrypted.extend(self.priv_key.decrypt(Pkcs1v15Encrypt, c)?); } Ok(decrypted) } diff --git a/src/error.rs b/src/error.rs index 1b58a49..e4bff2e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,30 @@ use snafu::Snafu; #[derive(Snafu, Debug)] #[snafu(visibility(pub(crate)), context(suffix(false)))] -pub enum ConfigError {} +pub enum ConfigError { + #[snafu(display("The key `{}` not found in Config", key))] + ConfigNotFound { key: String }, + #[snafu( + display("Failed to deseriliaze encrypter from keyring."), + context(false) + )] + LoadEncrypterFailed { source: serde_json::Error }, + #[snafu( + display( + "Keyring Error.\nThis error may caused by OS' secret manager, the rsa private key cannot be saved or read." + ), + context(false) + )] + KeyringError { source: keyring::Error }, + #[snafu( + display("Encryption Error. Cannot encrypt or decrypt.\nIf it's a decrypt error, maybe it's the private key stored in keyring being incorrect, modified or recreated."), + context(false) + )] + EncryptionError { source: rsa::Error }, + #[snafu(display("IO error. Cannot operate the file."), context(false))] + IoError { source: std::io::Error }, + #[snafu(display("Cannot collect from Source"))] + CollectFailed, +} pub type ConfigResult = Result; diff --git a/src/lib.rs b/src/lib.rs index 8bebe28..0e7f709 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,10 @@ -//! # encrypt-config -//! A crate helping managing, persisting, encrypting configs. +#![doc = include_str!("../README.md")] mod config; mod encrypt_utils; mod error; -pub use config::{Config, ConfigKey, ConfigPatch, SecretConfigPatch}; +pub use config::{Config, ConfigPatch, SecretConfigPatch}; pub use error::*; use encrypt_utils::Encrypter; @@ -17,13 +16,12 @@ pub trait Source { type Value: serde::Serialize; type Map: IntoIterator; - fn collect(&self) -> ConfigResult; + fn collect(&self) -> Result>; - fn upgrade(&self, key: impl AsRef, new_value: &Self::Value) -> ConfigResult { + fn upgrade(&self, key: impl AsRef, new_value: &Self::Value) -> ConfigPatch { let serded = serde_json::to_vec(&new_value).unwrap(); let func = Box::new(move || Ok(serded)); - let patch = ConfigPatch::new(key.as_ref().to_owned(), func); - Ok(patch) + ConfigPatch::new(key.as_ref().to_owned(), func) } } @@ -33,39 +31,39 @@ pub trait Source { pub trait PersistSource { type Value: serde::Serialize + serde::de::DeserializeOwned; - fn source_name(&self) -> ConfigKey; + fn source_name(&self) -> String; /// This will be used to initialize the source if not existing. fn default(&self) -> Self::Value; #[cfg(feature = "default_config_dir")] fn path(&self) -> std::path::PathBuf { - dirs_next::config_dir().unwrap().join(self.source_name()) + dirs_next::config_dir() + .expect("Default config dir unknown, turn off feature `default_config_dir`") + .join(self.source_name()) } #[cfg(not(feature = "default_config_dir"))] fn path(&self) -> std::path::PathBuf; - fn collect(&self) -> ConfigResult { + fn collect(&self) -> ConfigPatch { match std::fs::read(self.path()) { Ok(serded) => { let func = Box::new(move || Ok(serded)); - let patch = ConfigPatch::new(self.source_name(), func); - Ok(patch) + ConfigPatch::new(self.source_name(), func) } - Err(_) => Ok(self.upgrade(&self.default()).unwrap()), + Err(_) => self.upgrade(&self.default()), } } - fn upgrade(&self, new_value: &Self::Value) -> ConfigResult { + fn upgrade(&self, new_value: &Self::Value) -> ConfigPatch { let path = self.path(); let serded = serde_json::to_vec(new_value).unwrap(); let func = Box::new(move || { - std::fs::write(path, &serded).unwrap(); + std::fs::write(path, &serded)?; Ok(serded) }); - let patch = ConfigPatch::new(self.source_name(), func); - Ok(patch) + ConfigPatch::new(self.source_name(), func) } } @@ -75,42 +73,125 @@ pub trait PersistSource { pub trait SecretSource { type Value: serde::Serialize + serde::de::DeserializeOwned; - fn source_name(&self) -> ConfigKey; + fn source_name(&self) -> String; /// This will be used to initialize the source if not existing. fn default(&self) -> Self::Value; #[cfg(feature = "default_config_dir")] fn path(&self) -> std::path::PathBuf { - dirs_next::config_dir().unwrap().join(self.source_name()) + dirs_next::config_dir() + .expect("Default config dir unknown, turn off feature `default_config_dir`") + .join(self.source_name()) } #[cfg(not(feature = "default_config_dir"))] fn path(&self) -> std::path::PathBuf; - fn collect(&self) -> ConfigResult { + fn collect(&self) -> SecretConfigPatch { match std::fs::read(self.path()) { Ok(encrypted) => { let func = Box::new(move |encrypter: &Encrypter| { let serded = encrypter.decrypt(&encrypted).unwrap(); Ok(serded) }); - let patch = SecretConfigPatch::new(self.source_name(), func); - Ok(patch) + SecretConfigPatch::new(self.source_name(), func) } - Err(_) => Ok(self.upgrade(&self.default()).unwrap()), + Err(_) => self.upgrade(&self.default()), } } - fn upgrade(&self, new_value: &Self::Value) -> ConfigResult { + fn upgrade(&self, new_value: &Self::Value) -> SecretConfigPatch { let path = self.path(); let new_value = serde_json::to_vec(new_value).unwrap(); let func = Box::new(move |encrypter: &Encrypter| { - let encrypted = encrypter.encrypt_serded(&new_value).unwrap(); - std::fs::write(path, &encrypted).unwrap(); + let encrypted = encrypter.encrypt_serded(&new_value)?; + std::fs::write(path, encrypted)?; Ok(new_value) }); - let patch = SecretConfigPatch::new(self.source_name(), func); - Ok(patch) + SecretConfigPatch::new(self.source_name(), func) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct NormalSource; + impl Source for NormalSource { + type Value = String; + type Map = Vec<(String, Self::Value)>; + + fn collect(&self) -> Result> { + Ok(vec![("key".to_owned(), "value".to_owned())]) + } + } + + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)] + struct Foo(String); + + struct PersistSourceImpl; + impl PersistSource for PersistSourceImpl { + type Value = Foo; + + fn source_name(&self) -> String { + "test".to_owned() + } + + fn default(&self) -> Self::Value { + Foo("hello".to_owned()) + } + + fn path(&self) -> std::path::PathBuf { + std::path::PathBuf::from("tests").join(self.source_name()) + } + } + + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)] + struct Bar(String); + + struct SecretSourceImpl; + impl SecretSource for SecretSourceImpl { + type Value = Bar; + + fn source_name(&self) -> String { + "secret_test".to_owned() + } + + fn default(&self) -> Self::Value { + Bar("world".to_owned()) + } + + fn path(&self) -> std::path::PathBuf { + std::path::PathBuf::from("tests").join(self.source_name()) + } + } + + #[test] + fn config_tests() { + let mut config = Config::new("test"); + config.add_source(NormalSource).unwrap(); + config.add_persist_source(PersistSourceImpl).unwrap(); + config.add_secret_source(SecretSourceImpl).unwrap(); + let v: String = config.get("key").unwrap(); + assert_eq!(v, "value"); + let v: Foo = config.get("test").unwrap(); + assert_eq!(v, Foo("hello".to_owned())); + let v: Bar = config.get("secret_test").unwrap(); + assert_eq!(v, Bar("world".to_owned())); + let patch = NormalSource.upgrade("key", &"new_value".to_owned()); + patch.apply(&mut config).unwrap(); + let v: String = config.get("key").unwrap(); + assert_eq!(v, "new_value"); + let patch = PersistSourceImpl.upgrade(&Foo("hi".to_owned())); + patch.apply(&mut config).unwrap(); + let v: Foo = config.get("test").unwrap(); + assert_eq!(v, Foo("hi".to_owned())); + let patch = SecretSourceImpl.upgrade(&Bar("Louis".to_owned())); + patch.apply(&mut config).unwrap(); + let v: Bar = config.get("secret_test").unwrap(); + assert_eq!(v, Bar("Louis".to_owned())); + std::fs::remove_file("tests/secret_test").unwrap(); + std::fs::remove_file("tests/test").unwrap(); } } diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/.gitkeep @@ -0,0 +1 @@ +