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_AUTH__CLIENT_ID") {
82 tracing::debug!(key = "ATRG_AUTH__CLIENT_ID", "applying env override");
83 config.auth.client_id = val;
84 }
85
86 if let Ok(val) = std::env::var("ATRG_AUTH__REDIRECT_URI") {
87 tracing::debug!(key = "ATRG_AUTH__REDIRECT_URI", "applying env override");
88 config.auth.redirect_uri = val;
89 }
90
91 if let Ok(val) = std::env::var("ATRG_AUTH__SCOPE") {
92 tracing::debug!(key = "ATRG_AUTH__SCOPE", "applying env override");
93 config.auth.scope = val;
94 }
95
96 if let Ok(val) = std::env::var("ATRG_DATABASE__URL") {
98 tracing::debug!(key = "ATRG_DATABASE__URL", "applying env override");
99 config.database.url = val;
100 }
101
102 if let Ok(val) = std::env::var("ATRG_FIREHOSE__RELAY") {
104 if let Some(ref mut fh) = config.firehose {
105 tracing::debug!(key = "ATRG_FIREHOSE__RELAY", "applying env override");
106 fh.relay = val;
107 }
108 }
109
110 if let Ok(val) = std::env::var("ATRG_FEED_GENERATOR__DID") {
112 if let Some(ref mut fg) = config.feed_generator {
113 tracing::debug!(key = "ATRG_FEED_GENERATOR__DID", "applying env override");
114 fg.did = val;
115 }
116 }
117
118 if let Ok(val) = std::env::var("ATRG_LABELER__DID") {
120 if let Some(ref mut lb) = config.labeler {
121 tracing::debug!(key = "ATRG_LABELER__DID", "applying env override");
122 lb.did = val;
123 }
124 }
125 if let Ok(val) = std::env::var("ATRG_LABELER__SIGNING_KEY_PATH") {
126 if let Some(ref mut lb) = config.labeler {
127 tracing::debug!(
128 key = "ATRG_LABELER__SIGNING_KEY_PATH",
129 "applying env override"
130 );
131 lb.signing_key_path = Some(val);
132 }
133 }
134 if let Ok(val) = std::env::var("ATRG_LABELER__SIGNING_KEY_BASE64") {
135 if let Some(ref mut lb) = config.labeler {
136 tracing::debug!(
137 key = "ATRG_LABELER__SIGNING_KEY_BASE64",
138 "applying env override"
139 );
140 lb.signing_key_base64 = Some(val);
141 }
142 }
143}
144
145#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::config::{AppConfig, AuthConfig, Config, DatabaseConfig};
153
154 fn base_config() -> Config {
156 Config {
157 app: AppConfig {
158 name: "test-app".into(),
159 host: "127.0.0.1".into(),
160 port: 3000,
161 secret_key: "abcdefghijklmnopqrstuvwxyz123456".into(),
162 cors_origins: vec![],
163 environment: "development".into(),
164 },
165 auth: AuthConfig {
166 client_id: "http://localhost:3000/client-metadata.json".into(),
167 redirect_uri: "http://localhost:3000/auth/callback".into(),
168 scope: "atproto transition:generic".into(),
169 },
170 database: DatabaseConfig {
171 url: "sqlite://atrg.db".into(),
172 },
173 jetstream: None,
174 firehose: None,
175 feed_generator: None,
176 labeler: None,
177 rate_limit: None,
178 }
179 }
180
181 fn with_env_var<F: FnOnce()>(key: &str, value: &str, f: F) {
184 std::env::set_var(key, value);
185 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
186 std::env::remove_var(key);
187 if let Err(e) = result {
188 std::panic::resume_unwind(e);
189 }
190 }
191
192 #[test]
193 fn override_port() {
194 let mut cfg = base_config();
195 with_env_var("ATRG_APP__PORT", "9090", || {
196 apply_env_overrides(&mut cfg);
197 });
198 assert_eq!(cfg.app.port, 9090);
199 }
200
201 #[test]
202 fn override_secret_key() {
203 let mut cfg = base_config();
204 with_env_var(
205 "ATRG_APP__SECRET_KEY",
206 "new-super-secret-key-for-prod!!",
207 || {
208 apply_env_overrides(&mut cfg);
209 },
210 );
211 assert_eq!(cfg.app.secret_key, "new-super-secret-key-for-prod!!");
212 }
213
214 #[test]
215 fn override_database_url() {
216 let mut cfg = base_config();
217 with_env_var("ATRG_DATABASE__URL", "sqlite://prod.db", || {
218 apply_env_overrides(&mut cfg);
219 });
220 assert_eq!(cfg.database.url, "sqlite://prod.db");
221 }
222
223 #[test]
224 fn override_cors_origins_comma_separated() {
225 let mut cfg = base_config();
226 with_env_var(
227 "ATRG_APP__CORS_ORIGINS",
228 "http://localhost:5173, https://example.com , https://app.example.com",
229 || {
230 apply_env_overrides(&mut cfg);
231 },
232 );
233 assert_eq!(
234 cfg.app.cors_origins,
235 vec![
236 "http://localhost:5173",
237 "https://example.com",
238 "https://app.example.com",
239 ]
240 );
241 }
242
243 #[test]
244 fn invalid_port_is_ignored() {
245 let mut cfg = base_config();
246 let original_port = cfg.app.port;
247 with_env_var("ATRG_APP__PORT", "not_a_number", || {
248 apply_env_overrides(&mut cfg);
249 });
250 assert_eq!(cfg.app.port, original_port);
251 }
252
253 #[test]
254 fn override_app_name() {
255 let mut cfg = base_config();
256 with_env_var("ATRG_APP__NAME", "overridden-app", || {
257 apply_env_overrides(&mut cfg);
258 });
259 assert_eq!(cfg.app.name, "overridden-app");
260 }
261
262 #[test]
263 fn override_auth_client_id() {
264 let mut cfg = base_config();
265 with_env_var(
266 "ATRG_AUTH__CLIENT_ID",
267 "https://prod.example.com/client-metadata.json",
268 || {
269 apply_env_overrides(&mut cfg);
270 },
271 );
272 assert_eq!(
273 cfg.auth.client_id,
274 "https://prod.example.com/client-metadata.json"
275 );
276 }
277
278 #[test]
279 fn empty_cors_origins_value_produces_empty_vec() {
280 let mut cfg = base_config();
281 cfg.app.cors_origins = vec!["http://existing.example.com".into()];
282 with_env_var("ATRG_APP__CORS_ORIGINS", "", || {
283 apply_env_overrides(&mut cfg);
284 });
285 assert!(cfg.app.cors_origins.is_empty());
286 }
287
288 #[test]
289 fn absent_env_vars_leave_config_unchanged() {
290 for key in &[
292 "ATRG_APP__NAME",
293 "ATRG_APP__HOST",
294 "ATRG_APP__PORT",
295 "ATRG_APP__SECRET_KEY",
296 "ATRG_APP__ENVIRONMENT",
297 "ATRG_APP__CORS_ORIGINS",
298 "ATRG_AUTH__CLIENT_ID",
299 "ATRG_AUTH__REDIRECT_URI",
300 "ATRG_AUTH__SCOPE",
301 "ATRG_DATABASE__URL",
302 ] {
303 std::env::remove_var(key);
304 }
305
306 let mut cfg = base_config();
307 let before = format!("{:?}", cfg);
308 apply_env_overrides(&mut cfg);
309 let after = format!("{:?}", cfg);
310 assert_eq!(before, after);
311 }
312}