1use crate::config::Config;
27
28pub fn apply_env_overrides(config: &mut Config) {
33 if let Ok(val) = std::env::var("ATRG_APP__NAME") {
35 tracing::debug!(key = "ATRG_APP__NAME", "applying env override");
36 config.app.name = val;
37 }
38
39 if let Ok(val) = std::env::var("ATRG_APP__HOST") {
40 tracing::debug!(key = "ATRG_APP__HOST", "applying env override");
41 config.app.host = val;
42 }
43
44 if let Ok(val) = std::env::var("ATRG_APP__PORT") {
45 match val.parse::<u16>() {
46 Ok(port) => {
47 tracing::debug!(key = "ATRG_APP__PORT", port, "applying env override");
48 config.app.port = port;
49 }
50 Err(e) => {
51 tracing::warn!(
52 key = "ATRG_APP__PORT",
53 value = %val,
54 error = %e,
55 "ignoring invalid ATRG_APP__PORT value"
56 );
57 }
58 }
59 }
60
61 if let Ok(val) = std::env::var("ATRG_APP__SECRET_KEY") {
62 tracing::debug!(key = "ATRG_APP__SECRET_KEY", "applying env override");
63 config.app.secret_key = val;
64 }
65
66 if let Ok(val) = std::env::var("ATRG_APP__ENVIRONMENT") {
67 tracing::debug!(key = "ATRG_APP__ENVIRONMENT", "applying env override");
68 config.app.environment = val;
69 }
70
71 if let Ok(val) = std::env::var("ATRG_APP__CORS_ORIGINS") {
72 tracing::debug!(key = "ATRG_APP__CORS_ORIGINS", "applying env override");
73 config.app.cors_origins = val
74 .split(',')
75 .map(|s| s.trim().to_string())
76 .filter(|s| !s.is_empty())
77 .collect();
78 }
79
80 if let Ok(val) = std::env::var("ATRG_APP__ADMIN_DIDS") {
81 tracing::debug!(key = "ATRG_APP__ADMIN_DIDS", "applying env override");
82 config.app.admin_dids = val
83 .split(',')
84 .map(|s| s.trim().to_string())
85 .filter(|s| !s.is_empty())
86 .collect();
87 }
88
89 if let Ok(val) = std::env::var("ATRG_AUTH__CLIENT_ID") {
91 tracing::debug!(key = "ATRG_AUTH__CLIENT_ID", "applying env override");
92 config.auth.client_id = val;
93 }
94
95 if let Ok(val) = std::env::var("ATRG_AUTH__REDIRECT_URI") {
96 tracing::debug!(key = "ATRG_AUTH__REDIRECT_URI", "applying env override");
97 config.auth.redirect_uri = val;
98 }
99
100 if let Ok(val) = std::env::var("ATRG_AUTH__SCOPE") {
101 tracing::debug!(key = "ATRG_AUTH__SCOPE", "applying env override");
102 config.auth.scope = val;
103 }
104
105 if let Ok(val) = std::env::var("ATRG_AUTH__POST_LOGIN_REDIRECT") {
106 tracing::debug!(
107 key = "ATRG_AUTH__POST_LOGIN_REDIRECT",
108 "applying env override"
109 );
110 config.auth.post_login_redirect = val;
111 }
112
113 if let Ok(val) = std::env::var("ATRG_DATABASE__URL") {
115 tracing::debug!(key = "ATRG_DATABASE__URL", "applying env override");
116 config.database.url = val;
117 }
118
119 if let Ok(val) = std::env::var("ATRG_FIREHOSE__RELAY") {
121 if let Some(ref mut fh) = config.firehose {
122 tracing::debug!(key = "ATRG_FIREHOSE__RELAY", "applying env override");
123 fh.relay = val;
124 }
125 }
126
127 if let Ok(val) = std::env::var("ATRG_FEED_GENERATOR__DID") {
129 if let Some(ref mut fg) = config.feed_generator {
130 tracing::debug!(key = "ATRG_FEED_GENERATOR__DID", "applying env override");
131 fg.did = val;
132 }
133 }
134
135 if let Ok(val) = std::env::var("ATRG_LABELER__DID") {
137 if let Some(ref mut lb) = config.labeler {
138 tracing::debug!(key = "ATRG_LABELER__DID", "applying env override");
139 lb.did = val;
140 }
141 }
142 if let Ok(val) = std::env::var("ATRG_LABELER__SIGNING_KEY_PATH") {
143 if let Some(ref mut lb) = config.labeler {
144 tracing::debug!(
145 key = "ATRG_LABELER__SIGNING_KEY_PATH",
146 "applying env override"
147 );
148 lb.signing_key_path = Some(val);
149 }
150 }
151 if let Ok(val) = std::env::var("ATRG_LABELER__SIGNING_KEY_BASE64") {
152 if let Some(ref mut lb) = config.labeler {
153 tracing::debug!(
154 key = "ATRG_LABELER__SIGNING_KEY_BASE64",
155 "applying env override"
156 );
157 lb.signing_key_base64 = Some(val);
158 }
159 }
160}
161
162#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::config::{AppConfig, AuthConfig, Config, DatabaseConfig};
170
171 fn base_config() -> Config {
173 Config {
174 app: AppConfig {
175 name: "test-app".into(),
176 host: "127.0.0.1".into(),
177 port: 3000,
178 secret_key: "abcdefghijklmnopqrstuvwxyz123456".into(),
179 cors_origins: vec![],
180 environment: "development".into(),
181 admin_dids: vec![],
182 },
183 auth: AuthConfig {
184 client_id: "http://localhost:3000/client-metadata.json".into(),
185 redirect_uri: "http://localhost:3000/auth/callback".into(),
186 scope: "atproto transition:generic".into(),
187 post_login_redirect: "/".into(),
188 },
189 database: DatabaseConfig {
190 url: "sqlite://atrg.db".into(),
191 },
192 jetstream: None,
193 firehose: None,
194 feed_generator: None,
195 labeler: None,
196 rate_limit: None,
197 }
198 }
199
200 fn with_env_var<F: FnOnce()>(key: &str, value: &str, f: F) {
203 std::env::set_var(key, value);
204 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
205 std::env::remove_var(key);
206 if let Err(e) = result {
207 std::panic::resume_unwind(e);
208 }
209 }
210
211 #[test]
212 fn override_port() {
213 let mut cfg = base_config();
214 with_env_var("ATRG_APP__PORT", "9090", || {
215 apply_env_overrides(&mut cfg);
216 });
217 assert_eq!(cfg.app.port, 9090);
218 }
219
220 #[test]
221 fn override_secret_key() {
222 let mut cfg = base_config();
223 with_env_var(
224 "ATRG_APP__SECRET_KEY",
225 "new-super-secret-key-for-prod!!",
226 || {
227 apply_env_overrides(&mut cfg);
228 },
229 );
230 assert_eq!(cfg.app.secret_key, "new-super-secret-key-for-prod!!");
231 }
232
233 #[test]
234 fn override_database_url() {
235 let mut cfg = base_config();
236 with_env_var("ATRG_DATABASE__URL", "sqlite://prod.db", || {
237 apply_env_overrides(&mut cfg);
238 });
239 assert_eq!(cfg.database.url, "sqlite://prod.db");
240 }
241
242 #[test]
243 fn override_cors_origins_comma_separated() {
244 let mut cfg = base_config();
245 with_env_var(
246 "ATRG_APP__CORS_ORIGINS",
247 "http://localhost:5173, https://example.com , https://app.example.com",
248 || {
249 apply_env_overrides(&mut cfg);
250 },
251 );
252 assert_eq!(
253 cfg.app.cors_origins,
254 vec![
255 "http://localhost:5173",
256 "https://example.com",
257 "https://app.example.com",
258 ]
259 );
260 }
261
262 #[test]
263 fn invalid_port_is_ignored() {
264 let mut cfg = base_config();
265 let original_port = cfg.app.port;
266 with_env_var("ATRG_APP__PORT", "not_a_number", || {
267 apply_env_overrides(&mut cfg);
268 });
269 assert_eq!(cfg.app.port, original_port);
270 }
271
272 #[test]
273 fn override_app_name() {
274 let mut cfg = base_config();
275 with_env_var("ATRG_APP__NAME", "overridden-app", || {
276 apply_env_overrides(&mut cfg);
277 });
278 assert_eq!(cfg.app.name, "overridden-app");
279 }
280
281 #[test]
282 fn override_auth_client_id() {
283 let mut cfg = base_config();
284 with_env_var(
285 "ATRG_AUTH__CLIENT_ID",
286 "https://prod.example.com/client-metadata.json",
287 || {
288 apply_env_overrides(&mut cfg);
289 },
290 );
291 assert_eq!(
292 cfg.auth.client_id,
293 "https://prod.example.com/client-metadata.json"
294 );
295 }
296
297 #[test]
298 fn empty_cors_origins_value_produces_empty_vec() {
299 let mut cfg = base_config();
300 cfg.app.cors_origins = vec!["http://existing.example.com".into()];
301 with_env_var("ATRG_APP__CORS_ORIGINS", "", || {
302 apply_env_overrides(&mut cfg);
303 });
304 assert!(cfg.app.cors_origins.is_empty());
305 }
306
307 #[test]
308 fn absent_env_vars_leave_config_unchanged() {
309 for key in &[
311 "ATRG_APP__NAME",
312 "ATRG_APP__HOST",
313 "ATRG_APP__PORT",
314 "ATRG_APP__SECRET_KEY",
315 "ATRG_APP__ENVIRONMENT",
316 "ATRG_APP__CORS_ORIGINS",
317 "ATRG_AUTH__CLIENT_ID",
318 "ATRG_AUTH__REDIRECT_URI",
319 "ATRG_AUTH__SCOPE",
320 "ATRG_DATABASE__URL",
321 ] {
322 std::env::remove_var(key);
323 }
324
325 let mut cfg = base_config();
326 let before = format!("{:?}", cfg);
327 apply_env_overrides(&mut cfg);
328 let after = format!("{:?}", cfg);
329 assert_eq!(before, after);
330 }
331}