diff --git a/rust/src/environments/mod.rs b/rust/src/environments/mod.rs index 0e3e0313..78004413 100644 --- a/rust/src/environments/mod.rs +++ b/rust/src/environments/mod.rs @@ -18,6 +18,7 @@ //! ```toml //! [dev] //! api_url = "https://api.dev-godaddy.com" +//! devx_core_url = "https://api.developer.commerce.dev-godaddy.com" //! min_stage = "experimental" //! //! [staging.feature_overrides] @@ -31,11 +32,12 @@ use cli_engine::{ConfigSource, EnvConfig, SourceChain}; pub const DEFAULT_ENV: &str = "prod"; -/// The two fields a compiled-in environment actually sets. +/// The public, non-secret values a compiled-in environment sets. #[derive(Debug, Clone, EnvConfig)] struct BaseEnvConfig { api_url: String, client_id: String, + devx_core_url: String, } /// The compiled-in `ote`/`prod` environments. @@ -46,6 +48,7 @@ static BUILTIN_ENVS: LazyLock> = LazyLock::ne BaseEnvConfig { api_url: "https://api.ote-godaddy.com".to_owned(), client_id: "91660d79-c909-426c-b5c8-e0f575e8fcd2".to_owned(), + devx_core_url: "https://api.developer.commerce.ote-godaddy.com".to_owned(), }, ), ( @@ -53,6 +56,7 @@ static BUILTIN_ENVS: LazyLock> = LazyLock::ne BaseEnvConfig { api_url: "https://api.godaddy.com".to_owned(), client_id: "bc87f347-af82-4892-833f-818f54a0e79e".to_owned(), + devx_core_url: "https://api.developer.commerce.godaddy.com".to_owned(), }, ), ] @@ -67,13 +71,6 @@ pub const DEFAULT_OAUTH_SCOPES: &[&str] = &[ pub const REDIRECT_URI: &str = "http://localhost:7443/callback"; pub const APP_ID: &str = "gddy"; -/// DevX Core API gateway base URL for each compiled-in builtin, consulted by -/// [`devx_core_url_with`] only after both env-var override tiers miss. -const BUILTIN_DEVX_CORE_URLS: &[(&str, &str)] = &[ - ("ote", "https://api.developer.commerce.ote-godaddy.com"), - ("prod", "https://api.developer.commerce.godaddy.com"), -]; - /// A fully-resolved environment config #[derive(Debug, Clone, Default, EnvConfig)] pub struct GddyEnvConfig { @@ -127,6 +124,17 @@ pub struct GddyEnvConfig { default_fn = default_account_url )] pub account_url: String, + + /// Base URL for the DevX Core API gateway used by onboarding. Custom + /// environments set this in `environments.toml`; `prod` and `ote` receive + /// their defaults through [`BUILTIN_ENVS`]. Shell overrides are applied + /// separately by [`devx_core_url`] so their legacy names and precedence + /// are preserved. + #[env_config( + from_toml = parse_url_from_toml, + default_fn = default_devx_core_url + )] + pub devx_core_url: String, } pub fn env_prefix(name: &str) -> String { @@ -172,6 +180,13 @@ fn default_account_url(sources: &SourceChain<'_>) -> String { derive_account_url(sources.env_name().unwrap_or_default()) } +fn default_devx_core_url(_sources: &SourceChain<'_>) -> String { + // A custom environment must configure this value explicitly. The empty + // default keeps the field optional for unrelated CLI commands; callers + // that require DevX Core use `devx_core_url()` and report it as missing. + String::new() +} + fn derive_account_url(env_name: &str) -> String { if env_name == "prod" { return "https://account.godaddy.com".to_owned(); @@ -289,24 +304,28 @@ fn clean_url(raw: &str) -> Option { /// Base URL for the DevX Core API gateway for the given environment. /// -/// Custom environments must set `_DEVX_CORE_URL` (for example, -/// `DEV_DEVX_CORE_URL`) or the global `DEVX_CORE_URL`. `prod` and `ote` use -/// their compiled-in endpoints unless either variable overrides them. +/// The configured `devx_core_url` from `environments.toml` is the default for +/// custom environments. `_DEVX_CORE_URL` (for example, +/// `DEV_DEVX_CORE_URL`) and the global `DEVX_CORE_URL` shell variable retain +/// precedence over that file value. `prod` and `ote` use compiled-in defaults +/// unless the file or either shell override supplies another URL. pub fn devx_core_url(name: &str) -> Option { - devx_core_url_with(name, |key| std::env::var(key).ok()) + let configured = resolve(name) + .ok() + .and_then(|config| clean_url(&config.devx_core_url)); + devx_core_url_with(name, configured.as_deref(), |key| std::env::var(key).ok()) } -fn devx_core_url_with(name: &str, var: impl Fn(&str) -> Option) -> Option { +fn devx_core_url_with( + name: &str, + configured: Option<&str>, + var: impl Fn(&str) -> Option, +) -> Option { let prefix = env_prefix(name); var(&format!("{prefix}_DEVX_CORE_URL")) .and_then(|value| clean_url(&value)) .or_else(|| var("DEVX_CORE_URL").and_then(|value| clean_url(&value))) - .or_else(|| { - BUILTIN_DEVX_CORE_URLS - .iter() - .find(|(n, _)| *n == name) - .map(|(_, url)| (*url).to_owned()) - }) + .or_else(|| configured.and_then(clean_url)) } /// Validates a candidate URL string. `EnvConfig` `from_env` shared by every @@ -452,6 +471,7 @@ mod tests { [dev] api_url = "https://api.dev-godaddy.com" client_id = "dev-client" +devx_core_url = "https://api.developer.commerce.dev-godaddy.com" "#, ) .expect("write file"); @@ -461,6 +481,68 @@ client_id = "dev-client" assert_eq!(resolved.domains_api_url, "https://api.dev-godaddy.com"); assert_eq!(resolved.account_url, "https://account.dev-godaddy.com"); + assert_eq!( + resolved.devx_core_url, + "https://api.developer.commerce.dev-godaddy.com" + ); + } + + #[test] + fn register_resolves_builtin_devx_core_urls() { + let _g = ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let dir = tempfile::tempdir().expect("tempdir"); + let missing_file = dir.path().join("environments.toml"); + let envs = register(Environments::new("prod").with_config_file_path_override(missing_file)); + + assert_eq!( + envs.resolve::("ote") + .expect("ote resolves") + .devx_core_url, + "https://api.developer.commerce.ote-godaddy.com" + ); + assert_eq!( + envs.resolve::("prod") + .expect("prod resolves") + .devx_core_url, + "https://api.developer.commerce.godaddy.com" + ); + } + + #[test] + fn register_file_layer_overrides_builtin_devx_core_url() { + let _g = ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let dir = tempfile::tempdir().expect("tempdir"); + let file = dir.path().join("environments.toml"); + std::fs::write( + &file, + r#" +[ote] +devx_core_url = "https://core.override.example.test" + +[prod] +devx_core_url = "https://core.prod-override.example.test" +"#, + ) + .expect("write file"); + + let envs = register(Environments::new("prod").with_config_file_path_override(file)); + + assert_eq!( + envs.resolve::("ote") + .expect("ote resolves") + .devx_core_url, + "https://core.override.example.test" + ); + assert_eq!( + envs.resolve::("prod") + .expect("prod resolves") + .devx_core_url, + "https://core.prod-override.example.test" + ); } #[test] @@ -487,6 +569,31 @@ client_id = "dev-client" assert!(err.to_string().contains("api_url")); } + #[test] + fn register_rejects_a_malformed_file_layer_devx_core_url() { + let _g = ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let dir = tempfile::tempdir().expect("tempdir"); + let file = dir.path().join("environments.toml"); + std::fs::write( + &file, + r#" +[dev] +api_url = "https://api.dev-godaddy.com" +client_id = "dev-client" +devx_core_url = "not-a-url" +"#, + ) + .expect("write file"); + + let envs = register(Environments::new("prod").with_config_file_path_override(file)); + let err = envs + .resolve::("dev") + .expect_err("a malformed devx_core_url must be a hard error"); + assert!(err.to_string().contains("devx_core_url")); + } + #[test] fn register_rejects_a_malformed_file_layer_auth_url_override_for_a_builtin() { let _g = ENV_LOCK @@ -919,21 +1026,42 @@ auth_url = "not-a-url" #[test] fn devx_core_url_uses_prod_and_ote_builtins() { assert_eq!( - devx_core_url_with("prod", |_| None).as_deref(), + devx_core_url_with( + "prod", + Some("https://api.developer.commerce.godaddy.com"), + |_| None, + ) + .as_deref(), Some("https://api.developer.commerce.godaddy.com") ); assert_eq!( - devx_core_url_with("ote", |_| None).as_deref(), + devx_core_url_with( + "ote", + Some("https://api.developer.commerce.ote-godaddy.com"), + |_| None, + ) + .as_deref(), Some("https://api.developer.commerce.ote-godaddy.com") ); } #[test] - fn devx_core_url_global_override_wins() { + fn devx_core_url_uses_the_environments_toml_value_for_a_custom_env() { + assert_eq!( + devx_core_url_with("dev", Some(" https://dev-core.example.test/ "), |_| None,) + .as_deref(), + Some("https://dev-core.example.test") + ); + } + + #[test] + fn devx_core_url_global_override_wins_over_the_environments_toml_value() { assert_eq!( - devx_core_url_with("prod", |key| { - (key == "DEVX_CORE_URL").then(|| " http://localhost:4000/ ".to_owned()) - }) + devx_core_url_with( + "prod", + Some("https://configured-core.example.test"), + |key| { (key == "DEVX_CORE_URL").then(|| " http://localhost:4000/ ".to_owned()) }, + ) .as_deref(), Some("http://localhost:4000") ); @@ -942,11 +1070,15 @@ auth_url = "not-a-url" #[test] fn devx_core_url_per_environment_override_wins_over_global() { assert_eq!( - devx_core_url_with("dev", |key| match key { - "DEV_DEVX_CORE_URL" => Some("https://dev-core.example.test/".to_owned()), - "DEVX_CORE_URL" => Some("https://shared-core.example.test".to_owned()), - _ => None, - }) + devx_core_url_with( + "dev", + Some("https://configured-core.example.test"), + |key| match key { + "DEV_DEVX_CORE_URL" => Some("https://dev-core.example.test/".to_owned()), + "DEVX_CORE_URL" => Some("https://shared-core.example.test".to_owned()), + _ => None, + }, + ) .as_deref(), Some("https://dev-core.example.test") ); @@ -954,6 +1086,6 @@ auth_url = "not-a-url" #[test] fn devx_core_url_custom_env_requires_override() { - assert_eq!(devx_core_url_with("dev", |_| None), None); + assert_eq!(devx_core_url_with("dev", None, |_| None), None); } }