openzeppelin_relayer/config/config_file/signer/
local.rs1use serde::{Deserialize, Serialize};
12
13use crate::{config::ConfigFileError, models::PlainOrEnvValue};
14
15use super::SignerConfigValidate;
16use std::path::Path;
17
18#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
19#[serde(deny_unknown_fields)]
20pub struct LocalSignerFileConfig {
21 pub path: String,
22 pub passphrase: PlainOrEnvValue,
23}
24
25impl LocalSignerFileConfig {
26 fn validate_path(&self) -> Result<(), ConfigFileError> {
27 if self.path.is_empty() {
28 return Err(ConfigFileError::InvalidIdLength(
29 "Signer path cannot be empty".into(),
30 ));
31 }
32
33 let path = Path::new(&self.path);
34 if !path.exists() {
35 return Err(ConfigFileError::FileNotFound(format!(
36 "Signer file not found at path: {}",
37 path.display()
38 )));
39 }
40
41 if !path.is_file() {
42 return Err(ConfigFileError::InvalidFormat(format!(
43 "Path exists but is not a file: {}",
44 path.display()
45 )));
46 }
47
48 Ok(())
49 }
50
51 fn validate_passphrase(&self) -> Result<(), ConfigFileError> {
52 match &self.passphrase {
53 PlainOrEnvValue::Env { value } => {
54 if value.is_empty() {
55 return Err(ConfigFileError::MissingField(
56 "Passphrase environment variable name cannot be empty".into(),
57 ));
58 }
59 if std::env::var(value).is_err() {
60 return Err(ConfigFileError::MissingEnvVar(format!(
61 "Environment variable {} not found",
62 value
63 )));
64 }
65 }
66 PlainOrEnvValue::Plain { value } => {
67 if value.is_empty() {
68 return Err(ConfigFileError::InvalidFormat(
69 "Passphrase value cannot be empty".into(),
70 ));
71 }
72 }
73 }
74
75 Ok(())
76 }
77}
78
79impl SignerConfigValidate for LocalSignerFileConfig {
80 fn validate(&self) -> Result<(), ConfigFileError> {
81 self.validate_path()?;
82 self.validate_passphrase()?;
83
84 Ok(())
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::models::SecretString;
92 use std::env;
93 use std::fs::File;
94 use std::io::Write;
95 use tempfile::tempdir;
96
97 #[test]
98 fn test_valid_local_signer_config() {
99 let temp_dir = tempdir().unwrap();
100 let file_path = temp_dir.path().join("test-keystore.json");
101 let mut file = File::create(&file_path).unwrap();
102 writeln!(file, "{{\"mock\": \"keystore\"}}").unwrap();
103
104 let config = LocalSignerFileConfig {
105 path: file_path.to_str().unwrap().to_string(),
106 passphrase: PlainOrEnvValue::Plain {
107 value: SecretString::new("password123"),
108 },
109 };
110
111 assert!(config.validate().is_ok());
112 }
113
114 #[test]
115 fn test_empty_path() {
116 let config = LocalSignerFileConfig {
117 path: "".to_string(),
118 passphrase: PlainOrEnvValue::Plain {
119 value: SecretString::new("password123"),
120 },
121 };
122
123 let result = config.validate();
124 assert!(result.is_err());
125 assert!(matches!(result, Err(ConfigFileError::InvalidIdLength(_))));
126 }
127
128 #[test]
129 fn test_nonexistent_path() {
130 let config = LocalSignerFileConfig {
131 path: "/tmp/definitely-doesnt-exist-12345.json".to_string(),
132 passphrase: PlainOrEnvValue::Plain {
133 value: SecretString::new("password123"),
134 },
135 };
136
137 let result = config.validate();
138 assert!(result.is_err());
139 assert!(matches!(result, Err(ConfigFileError::FileNotFound(_))));
140 }
141
142 #[test]
143 fn test_path_is_directory() {
144 let temp_dir = tempdir().unwrap();
145
146 let config = LocalSignerFileConfig {
147 path: temp_dir.path().to_str().unwrap().to_string(),
148 passphrase: PlainOrEnvValue::Plain {
149 value: SecretString::new("password123"),
150 },
151 };
152
153 let result = config.validate();
154 assert!(result.is_err());
155 assert!(matches!(result, Err(ConfigFileError::InvalidFormat(_))));
156 }
157
158 #[test]
159 fn test_empty_plain_passphrase() {
160 let temp_dir = tempdir().unwrap();
161 let file_path = temp_dir.path().join("test-keystore.json");
162 let mut file = File::create(&file_path).unwrap();
163 writeln!(file, "{{\"mock\": \"keystore\"}}").unwrap();
164
165 let config = LocalSignerFileConfig {
166 path: file_path.to_str().unwrap().to_string(),
167 passphrase: PlainOrEnvValue::Plain {
168 value: SecretString::new(""),
169 },
170 };
171
172 let result = config.validate();
173 assert!(result.is_err());
174 assert!(matches!(result, Err(ConfigFileError::InvalidFormat(_))));
175 }
176
177 #[test]
178 fn test_empty_env_name() {
179 let temp_dir = tempdir().unwrap();
180 let file_path = temp_dir.path().join("test-keystore.json");
181 let mut file = File::create(&file_path).unwrap();
182 writeln!(file, "{{\"mock\": \"keystore\"}}").unwrap();
183
184 let config = LocalSignerFileConfig {
185 path: file_path.to_str().unwrap().to_string(),
186 passphrase: PlainOrEnvValue::Env {
187 value: "".to_string(),
188 },
189 };
190
191 let result = config.validate();
192 assert!(result.is_err());
193 assert!(matches!(result, Err(ConfigFileError::MissingField(_))));
194 }
195
196 #[test]
197 fn test_missing_env_var() {
198 let temp_dir = tempdir().unwrap();
199 let file_path = temp_dir.path().join("test-keystore.json");
200 let mut file = File::create(&file_path).unwrap();
201 writeln!(file, "{{\"mock\": \"keystore\"}}").unwrap();
202
203 env::remove_var("TEST_SIGNER_PASSPHRASE_THAT_DOESNT_EXIST");
205
206 let config = LocalSignerFileConfig {
207 path: file_path.to_str().unwrap().to_string(),
208 passphrase: PlainOrEnvValue::Env {
209 value: "TEST_SIGNER_PASSPHRASE_THAT_DOESNT_EXIST".to_string(),
210 },
211 };
212
213 let result = config.validate();
214 assert!(result.is_err());
215 assert!(matches!(result, Err(ConfigFileError::MissingEnvVar(_))));
216 }
217
218 #[test]
219 fn test_valid_env_var_passphrase() {
220 let temp_dir = tempdir().unwrap();
221 let file_path = temp_dir.path().join("test-keystore.json");
222 let mut file = File::create(&file_path).unwrap();
223 writeln!(file, "{{\"mock\": \"keystore\"}}").unwrap();
224
225 env::set_var("TEST_SIGNER_PASSPHRASE", "super-secret-passphrase");
226
227 let config = LocalSignerFileConfig {
228 path: file_path.to_str().unwrap().to_string(),
229 passphrase: PlainOrEnvValue::Env {
230 value: "TEST_SIGNER_PASSPHRASE".to_string(),
231 },
232 };
233
234 assert!(config.validate().is_ok());
235
236 env::remove_var("TEST_SIGNER_PASSPHRASE");
237 }
238
239 #[test]
240 fn test_serialize_deserialize() {
241 let config = LocalSignerFileConfig {
242 path: "/path/to/keystore.json".to_string(),
243 passphrase: PlainOrEnvValue::Plain {
244 value: SecretString::new("password123"),
245 },
246 };
247
248 let serialized = serde_json::to_string(&config).unwrap();
249 let deserialized: LocalSignerFileConfig = serde_json::from_str(&serialized).unwrap();
250
251 assert_eq!(config.path, deserialized.path);
252 assert_ne!(config.passphrase, deserialized.passphrase);
253 }
254
255 #[test]
256 fn test_deserialize_from_json() {
257 let json = r#"{
258 "path": "/path/to/keystore.json",
259 "passphrase": {
260 "type": "plain",
261 "value": "password123"
262 }
263 }"#;
264
265 let config: LocalSignerFileConfig = serde_json::from_str(json).unwrap();
266 assert_eq!(config.path, "/path/to/keystore.json");
267
268 if let PlainOrEnvValue::Plain { value } = &config.passphrase {
269 assert_eq!(value.to_str().as_str(), "password123");
270 } else {
271 panic!("Expected Plain passphrase variant");
272 }
273 }
274
275 #[test]
276 fn test_deserialize_env_passphrase() {
277 let json = r#"{
278 "path": "/path/to/keystore.json",
279 "passphrase": {
280 "type": "env",
281 "value": "KEYSTORE_PASSPHRASE"
282 }
283 }"#;
284
285 let config: LocalSignerFileConfig = serde_json::from_str(json).unwrap();
286 assert_eq!(config.path, "/path/to/keystore.json");
287
288 if let PlainOrEnvValue::Env { value } = &config.passphrase {
289 assert_eq!(value, "KEYSTORE_PASSPHRASE");
290 } else {
291 panic!("Expected Env passphrase variant");
292 }
293 }
294
295 #[test]
296 fn test_reject_unknown_fields() {
297 let json = r#"{
298 "path": "/path/to/keystore.json",
299 "passphrase": {
300 "type": "plain",
301 "value": "password123"
302 },
303 "unexpected_field": "should cause error"
304 }"#;
305
306 let result: Result<LocalSignerFileConfig, _> = serde_json::from_str(json);
307 assert!(result.is_err());
308 }
309
310 #[test]
311 fn test_validate_path_and_passphrase_methods() {
312 let temp_dir = tempdir().unwrap();
313 let file_path = temp_dir.path().join("test-keystore.json");
314 let mut file = File::create(&file_path).unwrap();
315 writeln!(file, "{{\"mock\": \"keystore\"}}").unwrap();
316
317 let config1 = LocalSignerFileConfig {
318 path: file_path.to_str().unwrap().to_string(),
319 passphrase: PlainOrEnvValue::Plain {
320 value: SecretString::new("password123"),
321 },
322 };
323 assert!(config1.validate_path().is_ok());
324
325 assert!(config1.validate_passphrase().is_ok());
326
327 let config2 = LocalSignerFileConfig {
328 path: "/nonexistent/path.json".to_string(),
329 passphrase: PlainOrEnvValue::Plain {
330 value: SecretString::new("password123"),
331 },
332 };
333 assert!(config2.validate_path().is_err());
334
335 let config3 = LocalSignerFileConfig {
336 path: file_path.to_str().unwrap().to_string(),
337 passphrase: PlainOrEnvValue::Plain {
338 value: SecretString::new(""),
339 },
340 };
341 assert!(config3.validate_passphrase().is_err());
342 }
343
344 #[test]
345 fn test_env_var_passphrase_with_special_chars() {
346 let temp_dir = tempdir().unwrap();
347 let file_path = temp_dir.path().join("test-keystore.json");
348 let mut file = File::create(&file_path).unwrap();
349 writeln!(file, "{{\"mock\": \"keystore\"}}").unwrap();
350
351 let env_path = temp_dir.path().join(".env");
353 let mut env_file = File::create(&env_path).unwrap();
354 writeln!(
355 env_file,
356 "TEST_SIGNER_PASSPHRASE_SPECIAL=super#secret#passphrase"
357 )
358 .unwrap();
359
360 dotenvy::from_path(&env_path).unwrap();
362
363 let config = LocalSignerFileConfig {
364 path: file_path.to_str().unwrap().to_string(),
365 passphrase: PlainOrEnvValue::Env {
366 value: "TEST_SIGNER_PASSPHRASE_SPECIAL".to_string(),
367 },
368 };
369
370 assert!(config.validate().is_ok());
371 if let PlainOrEnvValue::Env { value } = &config.passphrase {
373 assert_eq!(
374 std::env::var(value).unwrap(),
375 "super#secret#passphrase",
376 "Environment variable value should match the expected value"
377 );
378 } else {
379 panic!("Expected Env passphrase variant");
380 }
381 }
382}