1#[cfg(feature = "real-consul")]
13pub mod consul_client;
14#[cfg(feature = "real-nacos")]
15pub mod nacos_client;
16#[cfg(feature = "prod-config-masking")]
17pub mod prod_ready;
18
19pub mod config_validator;
20pub mod hot_reload;
21
22pub use config_validator::{
23 ConfigSchema, ConfigValidationReport, FieldSchema, ValidationResult, ValidationRule,
24};
25
26use serde::{Deserialize, Serialize};
27use std::collections::HashMap;
28use std::sync::{Arc, Mutex};
29
30pub type ConfigChangeCallback = Arc<dyn Fn(&str, &str) + Send + Sync>;
33
34pub trait ConfigCenter: Send + Sync {
36 fn get(&self, key: &str) -> Option<String>;
37 fn set(&mut self, key: &str, value: &str);
38 fn delete(&mut self, key: &str) -> bool;
39 fn exists(&self, key: &str) -> bool;
40 fn list(&self) -> Vec<String>;
41 fn watch(&self, key: &str) -> bool;
44 fn subscribe(&mut self, key: &str, callback: ConfigChangeCallback);
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ConfigChangeEvent {
51 pub key: String,
52 pub value: String,
53 pub deleted: bool,
54}
55
56pub struct ConsulConfigCenter {
58 data: HashMap<String, String>,
59 subscribers: HashMap<String, Vec<ConfigChangeCallback>>,
60 events: Mutex<Vec<ConfigChangeEvent>>,
61}
62
63impl ConsulConfigCenter {
64 pub fn new() -> Self {
65 Self {
66 data: HashMap::new(),
67 subscribers: HashMap::new(),
68 events: Mutex::new(Vec::new()),
69 }
70 }
71
72 fn notify(&self, key: &str, value: &str, deleted: bool) {
73 if let Some(callbacks) = self.subscribers.get(key) {
74 for cb in callbacks {
75 cb(key, value);
76 }
77 }
78 if let Ok(mut events) = self.events.lock() {
79 events.push(ConfigChangeEvent {
80 key: key.to_string(),
81 value: value.to_string(),
82 deleted,
83 });
84 }
85 }
86
87 pub fn events(&self) -> Vec<ConfigChangeEvent> {
89 self.events.lock().map(|e| e.clone()).unwrap_or_default()
90 }
91
92 pub fn subscriber_count(&self, key: &str) -> usize {
93 self.subscribers.get(key).map(|cbs| cbs.len()).unwrap_or(0)
94 }
95}
96
97impl Default for ConsulConfigCenter {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103impl ConfigCenter for ConsulConfigCenter {
104 fn get(&self, key: &str) -> Option<String> {
105 self.data.get(key).cloned()
106 }
107
108 fn set(&mut self, key: &str, value: &str) {
109 self.data.insert(key.to_string(), value.to_string());
110 self.notify(key, value, false);
111 }
112
113 fn delete(&mut self, key: &str) -> bool {
114 let removed = self.data.remove(key).is_some();
115 if removed {
116 self.notify(key, "", true);
117 }
118 removed
119 }
120
121 fn exists(&self, key: &str) -> bool {
122 self.data.contains_key(key)
123 }
124
125 fn list(&self) -> Vec<String> {
126 let mut keys: Vec<String> = self.data.keys().cloned().collect();
127 keys.sort();
128 keys
129 }
130
131 fn watch(&self, _key: &str) -> bool {
132 true
133 }
134
135 fn subscribe(&mut self, key: &str, callback: ConfigChangeCallback) {
136 self.subscribers
137 .entry(key.to_string())
138 .or_default()
139 .push(callback);
140 }
141}
142
143pub struct NacosConfigCenter {
145 data: HashMap<String, String>,
146 subscribers: HashMap<String, Vec<ConfigChangeCallback>>,
147 events: Mutex<Vec<ConfigChangeEvent>>,
148}
149
150impl NacosConfigCenter {
151 pub fn new() -> Self {
152 Self {
153 data: HashMap::new(),
154 subscribers: HashMap::new(),
155 events: Mutex::new(Vec::new()),
156 }
157 }
158
159 fn notify(&self, key: &str, value: &str, deleted: bool) {
160 if let Some(callbacks) = self.subscribers.get(key) {
161 for cb in callbacks {
162 cb(key, value);
163 }
164 }
165 if let Ok(mut events) = self.events.lock() {
166 events.push(ConfigChangeEvent {
167 key: key.to_string(),
168 value: value.to_string(),
169 deleted,
170 });
171 }
172 }
173
174 pub fn events(&self) -> Vec<ConfigChangeEvent> {
175 self.events.lock().map(|e| e.clone()).unwrap_or_default()
176 }
177
178 pub fn subscriber_count(&self, key: &str) -> usize {
179 self.subscribers.get(key).map(|cbs| cbs.len()).unwrap_or(0)
180 }
181}
182
183impl Default for NacosConfigCenter {
184 fn default() -> Self {
185 Self::new()
186 }
187}
188
189impl ConfigCenter for NacosConfigCenter {
190 fn get(&self, key: &str) -> Option<String> {
191 self.data.get(key).cloned()
192 }
193
194 fn set(&mut self, key: &str, value: &str) {
195 self.data.insert(key.to_string(), value.to_string());
196 self.notify(key, value, false);
197 }
198
199 fn delete(&mut self, key: &str) -> bool {
200 let removed = self.data.remove(key).is_some();
201 if removed {
202 self.notify(key, "", true);
203 }
204 removed
205 }
206
207 fn exists(&self, key: &str) -> bool {
208 self.data.contains_key(key)
209 }
210
211 fn list(&self) -> Vec<String> {
212 let mut keys: Vec<String> = self.data.keys().cloned().collect();
213 keys.sort();
214 keys
215 }
216
217 fn watch(&self, _key: &str) -> bool {
218 true
219 }
220
221 fn subscribe(&mut self, key: &str, callback: ConfigChangeCallback) {
222 self.subscribers
223 .entry(key.to_string())
224 .or_default()
225 .push(callback);
226 }
227}
228
229pub struct ConfigWatcher {
241 last_snapshot: Mutex<HashMap<String, String>>,
243 pub poll_interval_ms: u64,
245 watchers: Mutex<Vec<(String, ConfigChangeCallback)>>,
247}
248
249impl ConfigWatcher {
250 pub fn new(poll_interval_ms: u64) -> Self {
251 Self {
252 last_snapshot: Mutex::new(HashMap::new()),
253 poll_interval_ms: poll_interval_ms.max(100),
254 watchers: Mutex::new(Vec::new()),
255 }
256 }
257
258 pub fn watch(&self, key: &str, callback: ConfigChangeCallback) {
260 if let Ok(mut watchers) = self.watchers.lock() {
261 watchers.push((key.to_string(), callback));
262 }
263 }
264
265 pub fn poll<C: ConfigCenter>(&self, center: &C) -> usize {
269 let current: HashMap<String, String> = center
270 .list()
271 .into_iter()
272 .filter_map(|k| center.get(&k).map(|v| (k, v)))
273 .collect();
274
275 let mut changes = Vec::new();
276 {
277 let Ok(mut snapshot) = self.last_snapshot.lock() else {
278 return 0;
279 };
280 for (k, v) in ¤t {
282 match snapshot.get(k) {
283 Some(old) if old == v => {}
284 _ => changes.push((k.clone(), v.clone(), false)),
285 }
286 }
287 for k in snapshot.keys() {
289 if !current.contains_key(k) {
290 changes.push((k.clone(), String::new(), true));
291 }
292 }
293 *snapshot = current;
294 }
295
296 let watcher_count = changes.len();
297 if let Ok(watchers) = self.watchers.lock() {
298 for (key, new_value, deleted) in &changes {
299 for (watch_key, cb) in watchers.iter() {
300 if watch_key == key {
301 cb(key, new_value);
302 }
303 }
304 let _ = deleted;
306 }
307 }
308 watcher_count
309 }
310
311 pub fn watcher_count(&self) -> usize {
313 self.watchers.lock().map(|w| w.len()).unwrap_or(0)
314 }
315}
316
317impl Default for ConfigWatcher {
318 fn default() -> Self {
319 Self::new(5000)
320 }
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
330pub enum ConfigSourcePriority {
331 File = 0,
333 Remote = 1,
335 Env = 2,
337}
338
339pub struct MultiSourceConfig {
343 file_config: HashMap<String, String>,
345 env_config: HashMap<String, String>,
347 remote_config: HashMap<String, String>,
349 merged: Mutex<Option<HashMap<String, String>>>,
351 hot_reload: hot_reload::HotReloadManager,
353}
354
355impl MultiSourceConfig {
356 pub fn new() -> Self {
357 Self {
358 file_config: HashMap::new(),
359 env_config: HashMap::new(),
360 remote_config: HashMap::new(),
361 merged: Mutex::new(None),
362 hot_reload: hot_reload::HotReloadManager::new(),
363 }
364 }
365
366 pub fn hot_reload_manager(&self) -> &hot_reload::HotReloadManager {
368 &self.hot_reload
369 }
370
371 pub fn apply_hot_reload(
373 &self,
374 changes: &HashMap<String, String>,
375 source: hot_reload::ReloadSource,
376 ) -> Result<hot_reload::ReloadEvent, Vec<String>> {
377 hot_reload::apply_with_validation(&self.hot_reload, changes, source)
378 }
379
380 pub fn set_file_config(&mut self, config: HashMap<String, String>) {
382 self.file_config = config;
383 self.invalidate_cache();
384 }
385
386 pub fn set_env_config(&mut self, config: HashMap<String, String>) {
388 self.env_config = config;
389 self.invalidate_cache();
390 }
391
392 pub fn set_remote_config(&mut self, config: HashMap<String, String>) {
394 self.remote_config = config;
395 self.invalidate_cache();
396 }
397
398 pub fn load_env_vars(&mut self, prefix: &str) {
400 for (key, value) in std::env::vars() {
401 if let Some(stripped) = key.strip_prefix(prefix) {
402 self.env_config.insert(stripped.to_lowercase(), value);
404 }
405 }
406 self.invalidate_cache();
407 }
408
409 pub fn load_json(&mut self, json: &str) -> Result<(), String> {
411 let parsed: HashMap<String, String> =
412 serde_json::from_str(json).map_err(|e| format!("parse json error: {}", e))?;
413 self.file_config = parsed;
414 self.invalidate_cache();
415 Ok(())
416 }
417
418 pub fn merge(&self) -> HashMap<String, String> {
420 if let Ok(cache) = self.merged.lock() {
421 if let Some(cached) = cache.as_ref() {
422 return cached.clone();
423 }
424 }
425 let mut result = HashMap::new();
426 for (k, v) in &self.file_config {
428 result.insert(k.clone(), v.clone());
429 }
430 for (k, v) in &self.remote_config {
431 result.insert(k.clone(), v.clone());
432 }
433 for (k, v) in &self.env_config {
434 result.insert(k.clone(), v.clone());
435 }
436 if let Ok(mut cache) = self.merged.lock() {
437 *cache = Some(result.clone());
438 }
439 result
440 }
441
442 pub fn get(&self, key: &str) -> Option<String> {
444 self.merge().get(key).cloned()
445 }
446
447 pub fn source_of(&self, key: &str) -> Option<ConfigSourcePriority> {
449 if self.env_config.contains_key(key) {
450 Some(ConfigSourcePriority::Env)
451 } else if self.remote_config.contains_key(key) {
452 Some(ConfigSourcePriority::Remote)
453 } else if self.file_config.contains_key(key) {
454 Some(ConfigSourcePriority::File)
455 } else {
456 None
457 }
458 }
459
460 fn invalidate_cache(&self) {
461 if let Ok(mut cache) = self.merged.lock() {
462 *cache = None;
463 }
464 }
465}
466
467impl Default for MultiSourceConfig {
468 fn default() -> Self {
469 Self::new()
470 }
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
479pub enum ConfigFieldType {
480 String,
481 Integer,
482 Float,
483 Boolean,
484 Url,
485 Email,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct ConfigFieldSchema {
491 pub name: String,
493 pub field_type: ConfigFieldType,
495 pub required: bool,
497 pub min: Option<f64>,
499 pub max: Option<f64>,
501 pub min_length: Option<usize>,
503 pub max_length: Option<usize>,
505 pub allowed_values: Option<Vec<String>>,
507}
508
509impl ConfigFieldSchema {
510 pub fn new(name: impl Into<String>, field_type: ConfigFieldType) -> Self {
511 Self {
512 name: name.into(),
513 field_type,
514 required: false,
515 min: None,
516 max: None,
517 min_length: None,
518 max_length: None,
519 allowed_values: None,
520 }
521 }
522
523 pub fn required(mut self) -> Self {
524 self.required = true;
525 self
526 }
527
528 pub fn with_range(mut self, min: f64, max: f64) -> Self {
529 self.min = Some(min);
530 self.max = Some(max);
531 self
532 }
533
534 pub fn with_length(mut self, min: usize, max: usize) -> Self {
535 self.min_length = Some(min);
536 self.max_length = Some(max);
537 self
538 }
539
540 pub fn with_allowed_values(mut self, values: Vec<String>) -> Self {
541 self.allowed_values = Some(values);
542 self
543 }
544}
545
546pub struct SchemaValidator {
549 fields: Vec<ConfigFieldSchema>,
550}
551
552#[derive(Debug, Clone, PartialEq, Eq)]
554pub enum ValidationError {
555 MissingRequired(String),
557 TypeMismatch(String, String),
559 OutOfRange(String, String),
561 LengthExceeded(String, String),
563 NotAllowed(String, String),
565 InvalidFormat(String, String),
567}
568
569impl std::fmt::Display for ValidationError {
570 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
571 match self {
572 ValidationError::MissingRequired(k) => write!(f, "missing required field: {}", k),
573 ValidationError::TypeMismatch(k, v) => write!(f, "type mismatch for {}: {}", k, v),
574 ValidationError::OutOfRange(k, v) => write!(f, "value out of range for {}: {}", k, v),
575 ValidationError::LengthExceeded(k, v) => {
576 write!(f, "length exceeded for {}: {}", k, v)
577 }
578 ValidationError::NotAllowed(k, v) => write!(f, "value not allowed for {}: {}", k, v),
579 ValidationError::InvalidFormat(k, v) => write!(f, "invalid format for {}: {}", k, v),
580 }
581 }
582}
583
584impl SchemaValidator {
585 pub fn new() -> Self {
586 Self { fields: Vec::new() }
587 }
588
589 pub fn add_field(&mut self, schema: ConfigFieldSchema) {
590 self.fields.push(schema);
591 }
592
593 pub fn validate(&self, config: &HashMap<String, String>) -> Vec<ValidationError> {
596 let mut errors = Vec::new();
597 for field in &self.fields {
598 match config.get(&field.name) {
599 None => {
600 if field.required {
601 errors.push(ValidationError::MissingRequired(field.name.clone()));
602 }
603 }
604 Some(value) => {
605 if let Err(e) = self.validate_field(field, value) {
606 errors.push(e);
607 }
608 }
609 }
610 }
611 errors
612 }
613
614 fn validate_field(
615 &self,
616 schema: &ConfigFieldSchema,
617 value: &str,
618 ) -> Result<(), ValidationError> {
619 if let Some(allowed) = &schema.allowed_values {
621 if !allowed.iter().any(|v| v == value) {
622 return Err(ValidationError::NotAllowed(
623 schema.name.clone(),
624 value.to_string(),
625 ));
626 }
627 }
628
629 match schema.field_type {
630 ConfigFieldType::String => {
631 if let Some(max_len) = schema.max_length {
632 if value.len() > max_len {
633 return Err(ValidationError::LengthExceeded(
634 schema.name.clone(),
635 format!("len={} > {}", value.len(), max_len),
636 ));
637 }
638 }
639 if let Some(min_len) = schema.min_length {
640 if value.len() < min_len {
641 return Err(ValidationError::LengthExceeded(
642 schema.name.clone(),
643 format!("len={} < {}", value.len(), min_len),
644 ));
645 }
646 }
647 }
648 ConfigFieldType::Integer => {
649 let n: i64 = value.parse().map_err(|_| {
650 ValidationError::TypeMismatch(schema.name.clone(), value.to_string())
651 })?;
652 if let Some(min) = schema.min {
653 if (n as f64) < min {
654 return Err(ValidationError::OutOfRange(
655 schema.name.clone(),
656 format!("{} < {}", n, min),
657 ));
658 }
659 }
660 if let Some(max) = schema.max {
661 if (n as f64) > max {
662 return Err(ValidationError::OutOfRange(
663 schema.name.clone(),
664 format!("{} > {}", n, max),
665 ));
666 }
667 }
668 }
669 ConfigFieldType::Float => {
670 let n: f64 = value.parse().map_err(|_| {
671 ValidationError::TypeMismatch(schema.name.clone(), value.to_string())
672 })?;
673 if let Some(min) = schema.min {
674 if n < min {
675 return Err(ValidationError::OutOfRange(
676 schema.name.clone(),
677 format!("{} < {}", n, min),
678 ));
679 }
680 }
681 if let Some(max) = schema.max {
682 if n > max {
683 return Err(ValidationError::OutOfRange(
684 schema.name.clone(),
685 format!("{} > {}", n, max),
686 ));
687 }
688 }
689 }
690 ConfigFieldType::Boolean => {
691 if value != "true" && value != "false" {
692 return Err(ValidationError::TypeMismatch(
693 schema.name.clone(),
694 value.to_string(),
695 ));
696 }
697 }
698 ConfigFieldType::Url => {
699 if !value.starts_with("http://") && !value.starts_with("https://") {
700 return Err(ValidationError::InvalidFormat(
701 schema.name.clone(),
702 "not a valid URL".to_string(),
703 ));
704 }
705 }
706 ConfigFieldType::Email => {
707 if !value.contains('@') || !value.contains('.') {
708 return Err(ValidationError::InvalidFormat(
709 schema.name.clone(),
710 "not a valid email".to_string(),
711 ));
712 }
713 }
714 }
715 Ok(())
716 }
717}
718
719impl Default for SchemaValidator {
720 fn default() -> Self {
721 Self::new()
722 }
723}
724
725pub struct ConfigEncryption {
736 key: Vec<u8>,
738}
739
740pub const ENCRYPTED_PREFIX: &str = "ENC(";
742pub const ENCRYPTED_SUFFIX: &str = ")";
743
744impl ConfigEncryption {
745 pub fn new(key: impl Into<String>) -> Self {
746 Self {
747 key: key.into().into_bytes(),
748 }
749 }
750
751 pub fn encrypt(&self, plaintext: &str) -> String {
753 let bytes = plaintext.as_bytes();
754 let encrypted: Vec<u8> = bytes
755 .iter()
756 .enumerate()
757 .map(|(i, &b)| b ^ self.key[i % self.key.len()])
758 .collect();
759 let encoded = base64_encode(&encrypted);
760 format!("{}{}{}", ENCRYPTED_PREFIX, encoded, ENCRYPTED_SUFFIX)
761 }
762
763 pub fn decrypt(&self, ciphertext: &str) -> Result<String, String> {
765 let inner = ciphertext
766 .strip_prefix(ENCRYPTED_PREFIX)
767 .and_then(|s| s.strip_suffix(ENCRYPTED_SUFFIX))
768 .ok_or_else(|| "invalid encrypted format, expected ENC(...)".to_string())?;
769 let decoded = base64_decode(inner)?;
770 let decrypted: Vec<u8> = decoded
771 .iter()
772 .enumerate()
773 .map(|(i, &b)| b ^ self.key[i % self.key.len()])
774 .collect();
775 String::from_utf8(decrypted).map_err(|e| format!("decrypt utf8 error: {}", e))
776 }
777
778 pub fn is_encrypted(value: &str) -> bool {
780 value.starts_with(ENCRYPTED_PREFIX) && value.ends_with(ENCRYPTED_SUFFIX)
781 }
782
783 pub fn decrypt_if_needed(&self, value: &str) -> Result<String, String> {
785 if Self::is_encrypted(value) {
786 self.decrypt(value)
787 } else {
788 Ok(value.to_string())
789 }
790 }
791
792 pub fn decrypt_config(
794 &self,
795 config: &HashMap<String, String>,
796 ) -> Result<HashMap<String, String>, String> {
797 let mut result = HashMap::new();
798 for (k, v) in config {
799 let decrypted = self.decrypt_if_needed(v)?;
800 result.insert(k.clone(), decrypted);
801 }
802 Ok(result)
803 }
804}
805
806fn base64_encode(data: &[u8]) -> String {
808 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
809 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
810 let mut i = 0;
811 while i + 3 <= data.len() {
812 let n = ((data[i] as u32) << 16) | ((data[i + 1] as u32) << 8) | (data[i + 2] as u32);
813 result.push(CHARS[((n >> 18) & 0x3F) as usize] as char);
814 result.push(CHARS[((n >> 12) & 0x3F) as usize] as char);
815 result.push(CHARS[((n >> 6) & 0x3F) as usize] as char);
816 result.push(CHARS[(n & 0x3F) as usize] as char);
817 i += 3;
818 }
819 let remaining = data.len() - i;
820 if remaining == 1 {
821 let n = (data[i] as u32) << 16;
822 result.push(CHARS[((n >> 18) & 0x3F) as usize] as char);
823 result.push(CHARS[((n >> 12) & 0x3F) as usize] as char);
824 result.push('=');
825 result.push('=');
826 } else if remaining == 2 {
827 let n = ((data[i] as u32) << 16) | ((data[i + 1] as u32) << 8);
828 result.push(CHARS[((n >> 18) & 0x3F) as usize] as char);
829 result.push(CHARS[((n >> 12) & 0x3F) as usize] as char);
830 result.push(CHARS[((n >> 6) & 0x3F) as usize] as char);
831 result.push('=');
832 }
833 result
834}
835
836fn base64_decode(s: &str) -> Result<Vec<u8>, String> {
838 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
839 fn char_to_val(c: u8) -> Result<u8, String> {
840 CHARS
841 .iter()
842 .position(|&ch| ch == c)
843 .map(|p| p as u8)
844 .ok_or_else(|| format!("invalid base64 char: {}", c as char))
845 }
846 let trimmed = s.trim_end_matches('=');
847 let bytes = trimmed.as_bytes();
848 let mut result = Vec::with_capacity(bytes.len() * 3 / 4);
849 let mut i = 0;
850 while i + 4 <= bytes.len() {
851 let a = char_to_val(bytes[i])? as u32;
852 let b = char_to_val(bytes[i + 1])? as u32;
853 let c = char_to_val(bytes[i + 2])? as u32;
854 let d = char_to_val(bytes[i + 3])? as u32;
855 let n = (a << 18) | (b << 12) | (c << 6) | d;
856 result.push((n >> 16) as u8);
857 result.push((n >> 8) as u8);
858 result.push(n as u8);
859 i += 4;
860 }
861 let remaining = bytes.len() - i;
862 if remaining == 2 {
863 let a = char_to_val(bytes[i])? as u32;
864 let b = char_to_val(bytes[i + 1])? as u32;
865 let n = (a << 18) | (b << 12);
866 result.push((n >> 16) as u8);
867 } else if remaining == 3 {
868 let a = char_to_val(bytes[i])? as u32;
869 let b = char_to_val(bytes[i + 1])? as u32;
870 let c = char_to_val(bytes[i + 2])? as u32;
871 let n = (a << 18) | (b << 12) | (c << 6);
872 result.push((n >> 16) as u8);
873 result.push((n >> 8) as u8);
874 } else if remaining != 0 {
875 return Err(format!("invalid base64 length, remainder: {}", remaining));
876 }
877 Ok(result)
878}
879
880#[cfg(test)]
881mod tests {
882 use super::*;
883 use std::sync::atomic::{AtomicU32, Ordering};
884
885 #[test]
886 fn test_consul_set_and_get() {
887 let mut c = ConsulConfigCenter::new();
888 c.set("k", "v");
889 assert_eq!(c.get("k"), Some("v".to_string()));
890 assert!(c.exists("k"));
891 assert!(!c.exists("missing"));
892 }
893
894 #[test]
895 fn test_consul_get_missing() {
896 let c = ConsulConfigCenter::new();
897 assert_eq!(c.get("missing"), None);
898 }
899
900 #[test]
901 fn test_consul_delete() {
902 let mut c = ConsulConfigCenter::new();
903 c.set("k", "v");
904 assert!(c.delete("k"));
905 assert!(!c.exists("k"));
906 assert_eq!(c.get("k"), None);
907 assert!(!c.delete("missing"));
909 }
910
911 #[test]
912 fn test_consul_list_sorted() {
913 let mut c = ConsulConfigCenter::new();
914 c.set("z", "1");
915 c.set("a", "2");
916 c.set("m", "3");
917 assert_eq!(c.list(), vec!["a", "m", "z"]);
918 }
919
920 #[test]
921 fn test_consul_watch_returns_true() {
922 let c = ConsulConfigCenter::new();
923 assert!(c.watch("any-key"));
924 }
925
926 #[test]
927 fn test_nacos_set_and_get() {
928 let mut c = NacosConfigCenter::new();
929 c.set("k", "v");
930 assert_eq!(c.get("k"), Some("v".to_string()));
931 }
932
933 #[test]
934 fn test_nacos_watch_returns_true() {
935 let c = NacosConfigCenter::new();
936 assert!(c.watch("k"));
937 }
938
939 #[test]
940 fn test_nacos_delete() {
941 let mut c = NacosConfigCenter::new();
942 c.set("k", "v");
943 assert!(c.delete("k"));
944 assert_eq!(c.get("k"), None);
945 }
946
947 #[test]
948 fn test_nacos_list_sorted() {
949 let mut c = NacosConfigCenter::new();
950 c.set("b", "1");
951 c.set("a", "2");
952 assert_eq!(c.list(), vec!["a", "b"]);
953 }
954
955 #[test]
958 fn test_consul_subscribe_receives_set_events() {
959 let mut c = ConsulConfigCenter::new();
960 let count = Arc::new(AtomicU32::new(0));
961 let last_value = Arc::new(Mutex::new(String::new()));
962
963 let cb_count = count.clone();
964 let cb_value = last_value.clone();
965 c.subscribe(
966 "app.config",
967 Arc::new(move |_key, value| {
968 cb_count.fetch_add(1, Ordering::SeqCst);
969 *cb_value.lock().unwrap() = value.to_string();
970 }),
971 );
972
973 c.set("app.config", "v1");
974 assert_eq!(count.load(Ordering::SeqCst), 1);
975 assert_eq!(*last_value.lock().unwrap(), "v1");
976
977 c.set("app.config", "v2");
978 assert_eq!(count.load(Ordering::SeqCst), 2);
979 assert_eq!(*last_value.lock().unwrap(), "v2");
980 }
981
982 #[test]
983 fn test_consul_subscribe_receives_delete_events() {
984 let mut c = ConsulConfigCenter::new();
985 let deleted = Arc::new(AtomicU32::new(0));
986 let last_value = Arc::new(Mutex::new(String::new()));
987
988 let d_count = deleted.clone();
989 let d_value = last_value.clone();
990 c.subscribe(
991 "k",
992 Arc::new(move |_key, value| {
993 d_count.fetch_add(1, Ordering::SeqCst);
994 *d_value.lock().unwrap() = value.to_string();
995 }),
996 );
997
998 c.set("k", "v");
999 c.delete("k");
1000 assert_eq!(deleted.load(Ordering::SeqCst), 2); assert_eq!(*last_value.lock().unwrap(), ""); }
1003
1004 #[test]
1005 fn test_consul_multiple_subscribers() {
1006 let mut c = ConsulConfigCenter::new();
1007 let c1 = Arc::new(AtomicU32::new(0));
1008 let c2 = Arc::new(AtomicU32::new(0));
1009
1010 let c1_clone = c1.clone();
1011 c.subscribe(
1012 "k",
1013 Arc::new(move |_key, _value| {
1014 c1_clone.fetch_add(1, Ordering::SeqCst);
1015 }),
1016 );
1017
1018 let c2_clone = c2.clone();
1019 c.subscribe(
1020 "k",
1021 Arc::new(move |_key, _value| {
1022 c2_clone.fetch_add(1, Ordering::SeqCst);
1023 }),
1024 );
1025
1026 assert_eq!(c.subscriber_count("k"), 2);
1027 c.set("k", "v");
1028 assert_eq!(c1.load(Ordering::SeqCst), 1);
1029 assert_eq!(c2.load(Ordering::SeqCst), 1);
1030 }
1031
1032 #[test]
1033 fn test_consul_subscribers_are_keyed() {
1034 let mut c = ConsulConfigCenter::new();
1035 let other_count = Arc::new(AtomicU32::new(0));
1036 let oc = other_count.clone();
1037 c.subscribe(
1038 "other",
1039 Arc::new(move |_key, _value| {
1040 oc.fetch_add(1, Ordering::SeqCst);
1041 }),
1042 );
1043
1044 c.set("this", "v");
1045 assert_eq!(other_count.load(Ordering::SeqCst), 0);
1047
1048 c.set("other", "v");
1049 assert_eq!(other_count.load(Ordering::SeqCst), 1);
1050 }
1051
1052 #[test]
1053 fn test_consul_events_record() {
1054 let mut c = ConsulConfigCenter::new();
1055 c.set("a", "1");
1056 c.set("b", "2");
1057 c.delete("a");
1058
1059 let events = c.events();
1060 assert_eq!(events.len(), 3);
1061 assert_eq!(events[0].key, "a");
1062 assert_eq!(events[0].value, "1");
1063 assert!(!events[0].deleted);
1064 assert_eq!(events[2].key, "a");
1065 assert!(events[2].deleted);
1066 }
1067
1068 #[test]
1069 fn test_nacos_subscribe_receives_events() {
1070 let mut c = NacosConfigCenter::new();
1071 let received = Arc::new(Mutex::new(Vec::<(String, String)>::new()));
1072 let r = received.clone();
1073 c.subscribe(
1074 "cfg",
1075 Arc::new(move |key, value| {
1076 r.lock().unwrap().push((key.to_string(), value.to_string()));
1077 }),
1078 );
1079 c.set("cfg", "v1");
1080 c.set("cfg", "v2");
1081 let received = received.lock().unwrap();
1082 assert_eq!(
1083 *received,
1084 vec![
1085 ("cfg".to_string(), "v1".to_string()),
1086 ("cfg".to_string(), "v2".to_string()),
1087 ]
1088 );
1089 }
1090
1091 #[test]
1092 fn test_nacos_events_record() {
1093 let mut c = NacosConfigCenter::new();
1094 c.set("k", "v");
1095 let events = c.events();
1096 assert_eq!(events.len(), 1);
1097 assert_eq!(events[0].key, "k");
1098 assert_eq!(events[0].value, "v");
1099 }
1100
1101 #[test]
1102 fn test_subscribe_via_trait_object() {
1103 let mut boxed: Box<dyn ConfigCenter> = Box::new(ConsulConfigCenter::new());
1105 let count = Arc::new(AtomicU32::new(0));
1106 let c = count.clone();
1107 boxed.subscribe(
1108 "k",
1109 Arc::new(move |_key, _value| {
1110 c.fetch_add(1, Ordering::SeqCst);
1111 }),
1112 );
1113 boxed.set("k", "v");
1114 assert_eq!(count.load(Ordering::SeqCst), 1);
1115 }
1116
1117 #[test]
1118 fn test_overwrite_existing_value_notifies() {
1119 let mut c = ConsulConfigCenter::new();
1120 let count = Arc::new(AtomicU32::new(0));
1121 let c1 = count.clone();
1122 c.subscribe(
1123 "k",
1124 Arc::new(move |_key, _value| {
1125 c1.fetch_add(1, Ordering::SeqCst);
1126 }),
1127 );
1128 c.set("k", "v1");
1129 c.set("k", "v2");
1130 c.set("k", "v3");
1131 assert_eq!(count.load(Ordering::SeqCst), 3);
1132 }
1133
1134 #[test]
1139 fn test_config_watcher_detects_new_key() {
1140 let mut center = ConsulConfigCenter::new();
1141 let watcher = ConfigWatcher::new(1000);
1142 let received = Arc::new(Mutex::new(String::new()));
1143 let r = received.clone();
1144 watcher.watch(
1145 "app.port",
1146 Arc::new(move |_k, v| {
1147 *r.lock().unwrap() = v.to_string();
1148 }),
1149 );
1150
1151 center.set("app.port", "8080");
1152 let changes = watcher.poll(¢er);
1153 assert_eq!(changes, 1);
1154 assert_eq!(*received.lock().unwrap(), "8080");
1155 }
1156
1157 #[test]
1158 fn test_config_watcher_detects_value_change() {
1159 let mut center = ConsulConfigCenter::new();
1160 center.set("k", "v1");
1161 let watcher = ConfigWatcher::new(1000);
1162 watcher.poll(¢er);
1164
1165 let received = Arc::new(AtomicU32::new(0));
1166 let r = received.clone();
1167 watcher.watch(
1168 "k",
1169 Arc::new(move |_, _| {
1170 r.fetch_add(1, Ordering::SeqCst);
1171 }),
1172 );
1173
1174 center.set("k", "v2");
1175 let changes = watcher.poll(¢er);
1176 assert_eq!(changes, 1);
1177 assert_eq!(received.load(Ordering::SeqCst), 1);
1178 }
1179
1180 #[test]
1181 fn test_config_watcher_detects_deletion() {
1182 let mut center = ConsulConfigCenter::new();
1183 center.set("k", "v");
1184 let watcher = ConfigWatcher::new(1000);
1185 watcher.poll(¢er);
1186
1187 let deleted_value = Arc::new(Mutex::new("not_empty".to_string()));
1188 let d = deleted_value.clone();
1189 watcher.watch(
1190 "k",
1191 Arc::new(move |_, v| {
1192 *d.lock().unwrap() = v.to_string();
1193 }),
1194 );
1195
1196 center.delete("k");
1197 let changes = watcher.poll(¢er);
1198 assert_eq!(changes, 1);
1199 assert_eq!(*deleted_value.lock().unwrap(), "");
1200 }
1201
1202 #[test]
1203 fn test_config_watcher_no_change_returns_zero() {
1204 let mut center = ConsulConfigCenter::new();
1205 center.set("k", "v");
1206 let watcher = ConfigWatcher::new(1000);
1207 watcher.poll(¢er);
1208 let changes = watcher.poll(¢er);
1210 assert_eq!(changes, 0);
1211 }
1212
1213 #[test]
1214 fn test_config_watcher_watcher_count() {
1215 let watcher = ConfigWatcher::new(1000);
1216 assert_eq!(watcher.watcher_count(), 0);
1217 watcher.watch("a", Arc::new(|_, _| {}));
1218 watcher.watch("b", Arc::new(|_, _| {}));
1219 assert_eq!(watcher.watcher_count(), 2);
1220 }
1221
1222 #[test]
1223 fn test_config_watcher_default_interval() {
1224 let watcher = ConfigWatcher::default();
1225 assert_eq!(watcher.poll_interval_ms, 5000);
1226 }
1227
1228 #[test]
1229 fn test_config_watcher_min_interval_clamped() {
1230 let watcher = ConfigWatcher::new(10);
1231 assert_eq!(watcher.poll_interval_ms, 100);
1232 }
1233
1234 #[test]
1239 fn test_multi_source_env_overrides_remote_overrides_file() {
1240 let mut ms = MultiSourceConfig::new();
1241 let mut file = HashMap::new();
1242 file.insert("k".to_string(), "file_val".to_string());
1243 ms.set_file_config(file);
1244
1245 let mut remote = HashMap::new();
1246 remote.insert("k".to_string(), "remote_val".to_string());
1247 ms.set_remote_config(remote);
1248
1249 let mut env = HashMap::new();
1250 env.insert("k".to_string(), "env_val".to_string());
1251 ms.set_env_config(env);
1252
1253 assert_eq!(ms.get("k"), Some("env_val".to_string()));
1254 assert_eq!(ms.source_of("k"), Some(ConfigSourcePriority::Env));
1255 }
1256
1257 #[test]
1258 fn test_multi_source_remote_overrides_file() {
1259 let mut ms = MultiSourceConfig::new();
1260 let mut file = HashMap::new();
1261 file.insert("k".to_string(), "file_val".to_string());
1262 ms.set_file_config(file);
1263
1264 let mut remote = HashMap::new();
1265 remote.insert("k".to_string(), "remote_val".to_string());
1266 ms.set_remote_config(remote);
1267
1268 assert_eq!(ms.get("k"), Some("remote_val".to_string()));
1269 assert_eq!(ms.source_of("k"), Some(ConfigSourcePriority::Remote));
1270 }
1271
1272 #[test]
1273 fn test_multi_source_file_only() {
1274 let mut ms = MultiSourceConfig::new();
1275 let mut file = HashMap::new();
1276 file.insert("k".to_string(), "file_val".to_string());
1277 ms.set_file_config(file);
1278
1279 assert_eq!(ms.get("k"), Some("file_val".to_string()));
1280 assert_eq!(ms.source_of("k"), Some(ConfigSourcePriority::File));
1281 }
1282
1283 #[test]
1284 fn test_multi_source_missing_key_returns_none() {
1285 let ms = MultiSourceConfig::new();
1286 assert_eq!(ms.get("missing"), None);
1287 assert_eq!(ms.source_of("missing"), None);
1288 }
1289
1290 #[test]
1291 fn test_multi_source_merge_combines_all_keys() {
1292 let mut ms = MultiSourceConfig::new();
1293 let mut file = HashMap::new();
1294 file.insert("file_key".to_string(), "fv".to_string());
1295 ms.set_file_config(file);
1296
1297 let mut remote = HashMap::new();
1298 remote.insert("remote_key".to_string(), "rv".to_string());
1299 ms.set_remote_config(remote);
1300
1301 let mut env = HashMap::new();
1302 env.insert("env_key".to_string(), "ev".to_string());
1303 ms.set_env_config(env);
1304
1305 let merged = ms.merge();
1306 assert_eq!(merged.len(), 3);
1307 assert_eq!(merged.get("file_key"), Some(&"fv".to_string()));
1308 assert_eq!(merged.get("remote_key"), Some(&"rv".to_string()));
1309 assert_eq!(merged.get("env_key"), Some(&"ev".to_string()));
1310 }
1311
1312 #[test]
1313 fn test_multi_source_load_json() {
1314 let mut ms = MultiSourceConfig::new();
1315 ms.load_json(r#"{"host":"localhost","port":"3306"}"#)
1316 .unwrap();
1317 assert_eq!(ms.get("host"), Some("localhost".to_string()));
1318 assert_eq!(ms.get("port"), Some("3306".to_string()));
1319 }
1320
1321 #[test]
1322 fn test_multi_source_load_json_invalid() {
1323 let mut ms = MultiSourceConfig::new();
1324 assert!(ms.load_json("not json").is_err());
1325 }
1326
1327 #[test]
1328 fn test_multi_source_cache_invalidated_on_update() {
1329 let mut ms = MultiSourceConfig::new();
1330 let mut file = HashMap::new();
1331 file.insert("k".to_string(), "v1".to_string());
1332 ms.set_file_config(file);
1333 assert_eq!(ms.get("k"), Some("v1".to_string()));
1334
1335 let mut file2 = HashMap::new();
1336 file2.insert("k".to_string(), "v2".to_string());
1337 ms.set_file_config(file2);
1338 assert_eq!(ms.get("k"), Some("v2".to_string()));
1339 }
1340
1341 #[test]
1346 fn test_schema_validates_integer_in_range() {
1347 let mut validator = SchemaValidator::new();
1348 validator.add_field(
1349 ConfigFieldSchema::new("port", ConfigFieldType::Integer)
1350 .required()
1351 .with_range(1.0, 65535.0),
1352 );
1353 let mut config = HashMap::new();
1354 config.insert("port".to_string(), "3306".to_string());
1355 let errors = validator.validate(&config);
1356 assert!(errors.is_empty(), "expected no errors, got: {:?}", errors);
1357 }
1358
1359 #[test]
1360 fn test_schema_missing_required_field() {
1361 let mut validator = SchemaValidator::new();
1362 validator.add_field(ConfigFieldSchema::new("port", ConfigFieldType::Integer).required());
1363 let config = HashMap::new();
1364 let errors = validator.validate(&config);
1365 assert_eq!(errors.len(), 1);
1366 assert_eq!(
1367 errors[0],
1368 ValidationError::MissingRequired("port".to_string())
1369 );
1370 }
1371
1372 #[test]
1373 fn test_schema_integer_out_of_range() {
1374 let mut validator = SchemaValidator::new();
1375 validator.add_field(
1376 ConfigFieldSchema::new("port", ConfigFieldType::Integer).with_range(1.0, 65535.0),
1377 );
1378 let mut config = HashMap::new();
1379 config.insert("port".to_string(), "99999".to_string());
1380 let errors = validator.validate(&config);
1381 assert_eq!(errors.len(), 1);
1382 assert!(matches!(errors[0], ValidationError::OutOfRange(_, _)));
1383 }
1384
1385 #[test]
1386 fn test_schema_type_mismatch() {
1387 let mut validator = SchemaValidator::new();
1388 validator.add_field(ConfigFieldSchema::new("port", ConfigFieldType::Integer));
1389 let mut config = HashMap::new();
1390 config.insert("port".to_string(), "not_a_number".to_string());
1391 let errors = validator.validate(&config);
1392 assert_eq!(errors.len(), 1);
1393 assert!(matches!(errors[0], ValidationError::TypeMismatch(_, _)));
1394 }
1395
1396 #[test]
1397 fn test_schema_boolean_valid() {
1398 let mut validator = SchemaValidator::new();
1399 validator.add_field(ConfigFieldSchema::new("debug", ConfigFieldType::Boolean));
1400 let mut config = HashMap::new();
1401 config.insert("debug".to_string(), "true".to_string());
1402 assert!(validator.validate(&config).is_empty());
1403
1404 config.insert("debug".to_string(), "false".to_string());
1405 assert!(validator.validate(&config).is_empty());
1406 }
1407
1408 #[test]
1409 fn test_schema_boolean_invalid() {
1410 let mut validator = SchemaValidator::new();
1411 validator.add_field(ConfigFieldSchema::new("debug", ConfigFieldType::Boolean));
1412 let mut config = HashMap::new();
1413 config.insert("debug".to_string(), "yes".to_string());
1414 let errors = validator.validate(&config);
1415 assert_eq!(errors.len(), 1);
1416 assert!(matches!(errors[0], ValidationError::TypeMismatch(_, _)));
1417 }
1418
1419 #[test]
1420 fn test_schema_string_length() {
1421 let mut validator = SchemaValidator::new();
1422 validator
1423 .add_field(ConfigFieldSchema::new("name", ConfigFieldType::String).with_length(2, 10));
1424 let mut config = HashMap::new();
1425 config.insert("name".to_string(), "ab".to_string());
1426 assert!(validator.validate(&config).is_empty());
1427
1428 config.insert("name".to_string(), "a".to_string());
1429 assert_eq!(validator.validate(&config).len(), 1);
1430
1431 config.insert("name".to_string(), "this_is_way_too_long".to_string());
1432 assert_eq!(validator.validate(&config).len(), 1);
1433 }
1434
1435 #[test]
1436 fn test_schema_allowed_values() {
1437 let mut validator = SchemaValidator::new();
1438 validator.add_field(
1439 ConfigFieldSchema::new("level", ConfigFieldType::String).with_allowed_values(vec![
1440 "info".into(),
1441 "warn".into(),
1442 "error".into(),
1443 ]),
1444 );
1445 let mut config = HashMap::new();
1446 config.insert("level".to_string(), "info".to_string());
1447 assert!(validator.validate(&config).is_empty());
1448
1449 config.insert("level".to_string(), "debug".to_string());
1450 let errors = validator.validate(&config);
1451 assert_eq!(errors.len(), 1);
1452 assert!(matches!(errors[0], ValidationError::NotAllowed(_, _)));
1453 }
1454
1455 #[test]
1456 fn test_schema_url_format() {
1457 let mut validator = SchemaValidator::new();
1458 validator.add_field(ConfigFieldSchema::new("endpoint", ConfigFieldType::Url));
1459 let mut config = HashMap::new();
1460 config.insert("endpoint".to_string(), "https://example.com".to_string());
1461 assert!(validator.validate(&config).is_empty());
1462
1463 config.insert("endpoint".to_string(), "ftp://bad".to_string());
1464 assert_eq!(validator.validate(&config).len(), 1);
1465 }
1466
1467 #[test]
1468 fn test_schema_email_format() {
1469 let mut validator = SchemaValidator::new();
1470 validator.add_field(ConfigFieldSchema::new("email", ConfigFieldType::Email));
1471 let mut config = HashMap::new();
1472 config.insert("email".to_string(), "user@example.com".to_string());
1473 assert!(validator.validate(&config).is_empty());
1474
1475 config.insert("email".to_string(), "not_an_email".to_string());
1476 assert_eq!(validator.validate(&config).len(), 1);
1477 }
1478
1479 #[test]
1480 fn test_schema_float_range() {
1481 let mut validator = SchemaValidator::new();
1482 validator.add_field(
1483 ConfigFieldSchema::new("ratio", ConfigFieldType::Float).with_range(0.0, 1.0),
1484 );
1485 let mut config = HashMap::new();
1486 config.insert("ratio".to_string(), "0.5".to_string());
1487 assert!(validator.validate(&config).is_empty());
1488
1489 config.insert("ratio".to_string(), "1.5".to_string());
1490 assert_eq!(validator.validate(&config).len(), 1);
1491 }
1492
1493 #[test]
1494 fn test_schema_multiple_errors() {
1495 let mut validator = SchemaValidator::new();
1496 validator.add_field(ConfigFieldSchema::new("a", ConfigFieldType::Integer).required());
1497 validator.add_field(ConfigFieldSchema::new("b", ConfigFieldType::Boolean).required());
1498 let config = HashMap::new();
1499 let errors = validator.validate(&config);
1500 assert_eq!(errors.len(), 2);
1501 }
1502
1503 #[test]
1504 fn test_schema_optional_field_missing_ok() {
1505 let mut validator = SchemaValidator::new();
1506 validator.add_field(ConfigFieldSchema::new("optional", ConfigFieldType::String));
1507 let config = HashMap::new();
1508 assert!(validator.validate(&config).is_empty());
1509 }
1510
1511 #[test]
1516 fn test_encryption_roundtrip() {
1517 let enc = ConfigEncryption::new("my_secret_key");
1518 let plaintext = "database_password_123";
1519 let ciphertext = enc.encrypt(plaintext);
1520 assert!(ConfigEncryption::is_encrypted(&ciphertext));
1521 let decrypted = enc.decrypt(&ciphertext).unwrap();
1522 assert_eq!(decrypted, plaintext);
1523 }
1524
1525 #[test]
1526 fn test_encryption_is_encrypted_detection() {
1527 assert!(ConfigEncryption::is_encrypted("ENC(abc123)"));
1528 assert!(!ConfigEncryption::is_encrypted("plaintext"));
1529 assert!(!ConfigEncryption::is_encrypted("ENC(incomplete"));
1530 }
1531
1532 #[test]
1533 fn test_encryption_decrypt_if_needed_for_plaintext() {
1534 let enc = ConfigEncryption::new("key");
1535 let result = enc.decrypt_if_needed("plain_value").unwrap();
1536 assert_eq!(result, "plain_value");
1537 }
1538
1539 #[test]
1540 fn test_encryption_decrypt_if_needed_for_ciphertext() {
1541 let enc = ConfigEncryption::new("key");
1542 let ciphertext = enc.encrypt("secret");
1543 let result = enc.decrypt_if_needed(&ciphertext).unwrap();
1544 assert_eq!(result, "secret");
1545 }
1546
1547 #[test]
1548 fn test_encryption_decrypt_invalid_format_errors() {
1549 let enc = ConfigEncryption::new("key");
1550 assert!(enc.decrypt("not_encrypted").is_err());
1551 assert!(enc.decrypt("ENC(incomplete").is_err());
1552 }
1553
1554 #[test]
1555 fn test_encryption_decrypt_config_batch() {
1556 let enc = ConfigEncryption::new("master_key");
1557 let mut config = HashMap::new();
1558 config.insert("host".to_string(), "localhost".to_string());
1559 config.insert("password".to_string(), enc.encrypt("s3cr3t"));
1560 config.insert("api_key".to_string(), enc.encrypt("abc123"));
1561
1562 let decrypted = enc.decrypt_config(&config).unwrap();
1563 assert_eq!(decrypted.get("host"), Some(&"localhost".to_string()));
1564 assert_eq!(decrypted.get("password"), Some(&"s3cr3t".to_string()));
1565 assert_eq!(decrypted.get("api_key"), Some(&"abc123".to_string()));
1566 }
1567
1568 #[test]
1569 fn test_encryption_different_keys_produce_different_output() {
1570 let enc1 = ConfigEncryption::new("key1");
1571 let enc2 = ConfigEncryption::new("key2");
1572 let plaintext = "same_secret";
1573 let c1 = enc1.encrypt(plaintext);
1574 let c2 = enc2.encrypt(plaintext);
1575 assert_ne!(c1, c2);
1576 }
1577
1578 #[test]
1579 fn test_encryption_same_key_same_plaintext_deterministic() {
1580 let enc = ConfigEncryption::new("key");
1581 let c1 = enc.encrypt("secret");
1582 let c2 = enc.encrypt("secret");
1583 assert_eq!(c1, c2);
1584 }
1585
1586 #[test]
1587 fn test_base64_encode_decode_roundtrip() {
1588 let data = b"hello world";
1589 let encoded = base64_encode(data);
1590 let decoded = base64_decode(&encoded).unwrap();
1591 assert_eq!(decoded, data);
1592 }
1593
1594 #[test]
1595 fn test_base64_encode_known_value() {
1596 assert_eq!(base64_encode(b"Man"), "TWFu");
1598 assert_eq!(base64_encode(b"Ma"), "TWE=");
1600 assert_eq!(base64_encode(b"M"), "TQ==");
1602 }
1603
1604 #[test]
1605 fn test_base64_decode_invalid_char_errors() {
1606 assert!(base64_decode("!!!").is_err());
1607 }
1608
1609 #[test]
1610 fn test_base64_decode_empty_string() {
1611 let result = base64_decode("").unwrap();
1612 assert!(result.is_empty());
1613 }
1614}