test: unit tests for OAuthConfig merge + get_oauth_provider_for_client
This commit is contained in:
@@ -651,3 +651,248 @@ pub(crate) fn client_config_info(
|
||||
ClientConfig::Unknown => ("unknown", "unknown", None),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::client::{ModelData, ProviderModels};
|
||||
use crate::client::openai_compatible::OpenAICompatibleConfig;
|
||||
|
||||
fn base_config() -> OAuthConfig {
|
||||
OAuthConfig {
|
||||
client_id: "base-id".into(),
|
||||
token_url: "https://base.example/token".into(),
|
||||
flow: OAuthFlow::Pkce,
|
||||
client_secret: Some("base-secret".into()),
|
||||
authorize_url: Some("https://base.example/authorize".into()),
|
||||
redirect_uri: None,
|
||||
redirect_port: Some(1234),
|
||||
scopes: vec!["a".into(), "b".into()],
|
||||
token_request_format: Some(TokenRequestFormat::FormUrlEncoded),
|
||||
extra_authorize_params: IndexMap::from([("plan".into(), "base".into())]),
|
||||
extra_token_headers: IndexMap::new(),
|
||||
extra_request_headers: IndexMap::new(),
|
||||
echo_pkce_in_token_exchange: false,
|
||||
include_state_in_token_exchange: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_user_override(client_id: &str, token_url: &str) -> OAuthConfig {
|
||||
OAuthConfig {
|
||||
client_id: client_id.into(),
|
||||
token_url: token_url.into(),
|
||||
flow: OAuthFlow::Pkce,
|
||||
client_secret: None,
|
||||
authorize_url: None,
|
||||
redirect_uri: None,
|
||||
redirect_port: None,
|
||||
scopes: vec![],
|
||||
token_request_format: None,
|
||||
extra_authorize_params: IndexMap::new(),
|
||||
extra_token_headers: IndexMap::new(),
|
||||
extra_request_headers: IndexMap::new(),
|
||||
echo_pkce_in_token_exchange: false,
|
||||
include_state_in_token_exchange: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_config_merge_user_wins_per_field() {
|
||||
let base = base_config();
|
||||
let mut user = empty_user_override("user-id", "https://user.example/token");
|
||||
user.client_secret = Some("user-secret".into());
|
||||
user.scopes = vec!["c".into()];
|
||||
user.extra_authorize_params = IndexMap::from([("plan".into(), "user".into())]);
|
||||
|
||||
let merged = base.merge(user);
|
||||
|
||||
assert_eq!(merged.client_id, "user-id");
|
||||
assert_eq!(merged.token_url, "https://user.example/token");
|
||||
assert_eq!(merged.client_secret.as_deref(), Some("user-secret"));
|
||||
assert_eq!(merged.authorize_url.as_deref(), Some("https://base.example/authorize"));
|
||||
assert_eq!(merged.redirect_port, Some(1234));
|
||||
assert_eq!(merged.scopes, vec!["c"]);
|
||||
assert_eq!(merged.extra_authorize_params.get("plan").map(String::as_str), Some("user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_config_merge_empty_user_keeps_base_optionals() {
|
||||
let base = base_config();
|
||||
let user = empty_user_override("user-id", "https://user.example/token");
|
||||
|
||||
let merged = base.merge(user);
|
||||
|
||||
assert_eq!(merged.client_id, "user-id");
|
||||
assert_eq!(merged.token_url, "https://user.example/token");
|
||||
assert_eq!(merged.client_secret.as_deref(), Some("base-secret"));
|
||||
assert_eq!(merged.authorize_url.as_deref(), Some("https://base.example/authorize"));
|
||||
assert_eq!(merged.redirect_port, Some(1234));
|
||||
assert_eq!(merged.scopes, vec!["a", "b"]);
|
||||
assert!(matches!(merged.token_request_format, Some(TokenRequestFormat::FormUrlEncoded)));
|
||||
assert_eq!(merged.extra_authorize_params.get("plan").map(String::as_str), Some("base"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_config_serde_roundtrip_from_yaml() {
|
||||
let yaml = r#"
|
||||
client_id: xai-client
|
||||
token_url: https://auth.x.ai/oauth2/token
|
||||
authorize_url: https://auth.x.ai/oauth2/authorize
|
||||
scopes:
|
||||
- openid
|
||||
- profile
|
||||
- api:access
|
||||
redirect_port: 56121
|
||||
flow: pkce
|
||||
token_request_format: form_url_encoded
|
||||
extra_authorize_params:
|
||||
plan: generic
|
||||
referrer: coyote
|
||||
echo_pkce_in_token_exchange: true
|
||||
"#;
|
||||
let cfg: OAuthConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(cfg.client_id, "xai-client");
|
||||
assert_eq!(cfg.token_url, "https://auth.x.ai/oauth2/token");
|
||||
assert_eq!(cfg.scopes.len(), 3);
|
||||
assert_eq!(cfg.redirect_port, Some(56121));
|
||||
assert!(matches!(cfg.flow, OAuthFlow::Pkce));
|
||||
assert!(matches!(cfg.token_request_format, Some(TokenRequestFormat::FormUrlEncoded)));
|
||||
assert!(cfg.echo_pkce_in_token_exchange);
|
||||
assert!(cfg.include_state_in_token_exchange);
|
||||
assert_eq!(cfg.extra_authorize_params.get("plan").map(String::as_str), Some("generic"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_flow_defaults_to_pkce_when_missing() {
|
||||
let yaml = "client_id: x\ntoken_url: y";
|
||||
let cfg: OAuthConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(matches!(cfg.flow, OAuthFlow::Pkce));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_flow_client_credentials_parses() {
|
||||
let yaml = "client_id: x\ntoken_url: y\nflow: client_credentials";
|
||||
let cfg: OAuthConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(matches!(cfg.flow, OAuthFlow::ClientCredentials));
|
||||
}
|
||||
|
||||
fn make_provider_models(provider: &str, oauth: Option<OAuthConfig>) -> ProviderModels {
|
||||
ProviderModels {
|
||||
provider: provider.into(),
|
||||
oauth,
|
||||
models: vec![ModelData::new("some-model")],
|
||||
}
|
||||
}
|
||||
|
||||
fn make_openai_compat_client(
|
||||
name: &str,
|
||||
auth: Option<&str>,
|
||||
oauth: Option<OAuthConfig>,
|
||||
) -> ClientConfig {
|
||||
ClientConfig::OpenAICompatibleConfig(OpenAICompatibleConfig {
|
||||
name: Some(name.into()),
|
||||
api_base: Some("https://api.example/v1".into()),
|
||||
api_key: None,
|
||||
auth: auth.map(str::to_string),
|
||||
oauth: oauth.map(Box::new),
|
||||
models: vec![],
|
||||
patch: None,
|
||||
extra: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_oauth_provider_for_client_merges_defaults_with_user_override() {
|
||||
let base = base_config();
|
||||
let mut user = empty_user_override("user-id", "https://user.example/token");
|
||||
user.echo_pkce_in_token_exchange = true;
|
||||
let models = vec![make_provider_models("acme", Some(base))];
|
||||
let cc = make_openai_compat_client("acme", Some("oauth"), Some(user));
|
||||
|
||||
let provider = get_oauth_provider_for_client(&cc, &models).unwrap();
|
||||
assert_eq!(provider.client_id(), "user-id");
|
||||
assert_eq!(provider.token_url(), "https://user.example/token");
|
||||
assert!(provider.echo_pkce_in_token_exchange());
|
||||
assert_eq!(
|
||||
provider.fixed_redirect_uri().as_deref(),
|
||||
Some("http://127.0.0.1:1234/callback")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_oauth_provider_for_client_uses_bundled_defaults_only() {
|
||||
let base = base_config();
|
||||
let models = vec![make_provider_models("bundled-only", Some(base))];
|
||||
let cc = make_openai_compat_client("bundled-only", Some("oauth"), None);
|
||||
|
||||
let provider = get_oauth_provider_for_client(&cc, &models).unwrap();
|
||||
assert_eq!(provider.client_id(), "base-id");
|
||||
assert_eq!(provider.token_url(), "https://base.example/token");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_oauth_provider_for_client_uses_inline_only() {
|
||||
let user = OAuthConfig {
|
||||
client_id: "inline-id".into(),
|
||||
token_url: "https://inline.example/token".into(),
|
||||
..empty_user_override("inline-id", "https://inline.example/token")
|
||||
};
|
||||
let cc = make_openai_compat_client("inline-only", Some("oauth"), Some(user));
|
||||
|
||||
let provider = get_oauth_provider_for_client(&cc, &[]).unwrap();
|
||||
assert_eq!(provider.client_id(), "inline-id");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_oauth_provider_for_client_returns_none_when_no_config_anywhere() {
|
||||
let cc = make_openai_compat_client("nothing", Some("oauth"), None);
|
||||
assert!(get_oauth_provider_for_client(&cc, &[]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_oauth_provider_for_client_returns_none_when_auth_not_oauth() {
|
||||
let base = base_config();
|
||||
let models = vec![make_provider_models("api-key-client", Some(base))];
|
||||
let cc = make_openai_compat_client("api-key-client", None, None);
|
||||
assert!(get_oauth_provider_for_client(&cc, &models).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_compatible_provider_joins_scopes_with_spaces() {
|
||||
let mut cfg = base_config();
|
||||
cfg.scopes = vec!["one".into(), "two".into(), "three".into()];
|
||||
let provider = super::super::openai_compatible_oauth::OpenAICompatibleOAuthProvider {
|
||||
config: cfg,
|
||||
client_name: "test".into(),
|
||||
};
|
||||
assert_eq!(provider.scopes(), "one two three");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_compatible_provider_prefers_redirect_uri_over_port() {
|
||||
let mut cfg = base_config();
|
||||
cfg.redirect_uri = Some("https://custom.example/cb".into());
|
||||
cfg.redirect_port = Some(9999);
|
||||
let provider = super::super::openai_compatible_oauth::OpenAICompatibleOAuthProvider {
|
||||
config: cfg,
|
||||
client_name: "test".into(),
|
||||
};
|
||||
assert_eq!(
|
||||
provider.fixed_redirect_uri().as_deref(),
|
||||
Some("https://custom.example/cb")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_compatible_provider_ephemeral_when_no_redirect() {
|
||||
let mut cfg = base_config();
|
||||
cfg.redirect_uri = None;
|
||||
cfg.redirect_port = None;
|
||||
let provider = super::super::openai_compatible_oauth::OpenAICompatibleOAuthProvider {
|
||||
config: cfg,
|
||||
client_name: "test".into(),
|
||||
};
|
||||
assert!(provider.uses_localhost_redirect());
|
||||
assert!(provider.fixed_redirect_uri().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user