use axum::extract::State;
use axum::http::StatusCode;
use axum::response::Json;
use super::types::*;
use crate::config::Config;
fn gateways_of(c: &Config) -> Vec<GatewayInfo> {
let mut gateways: Vec<GatewayInfo> = c
.model_providers
.iter()
.map(|(name, p)| GatewayInfo {
name: name.clone(),
base_url: p.base_url.clone(),
has_api_key: p.api_key.is_some(),
script: p.script.clone(),
extra_keys: {
let mut keys: Vec<String> = p.extra.keys().cloned().collect();
keys.sort();
keys
},
})
.collect();
gateways.sort_by(|a, b| a.name.cmp(&b.name));
gateways
}
fn redact(c: &Config) -> RedactedConfig {
RedactedConfig {
default_provider: c.default_provider.clone(),
has_anthropic_key: c.providers.anthropic_api_key.is_some(),
has_openai_key: c.providers.openai_api_key.is_some(),
has_google_key: c.providers.google_api_key.is_some(),
has_openrouter_key: c.openrouter_api_key.is_some(),
ollama_base_url: c.ollama_base_url.clone(),
gateways: gateways_of(c),
agent_paths: c.agent_paths.clone(),
mcp_server_count: c.mcp_servers.len(),
api_version: API_VERSION.to_string(),
capabilities: API_CAPABILITIES.iter().map(|c| c.to_string()).collect(),
limits: ApiLimits::current(),
}
}
pub(super) async fn get_config(State(state): State<AppState>) -> Json<RedactedConfig> {
Json(redact(&state.config))
}
pub(super) async fn put_config(
State(state): State<AppState>,
Json(req): Json<WriteConfigReq>,
) -> Result<Json<RedactedConfig>, (StatusCode, Json<ErrorResponse>)> {
let path = &state.mcp.config_path;
let mut config = Config::load_from_path_public(path).map_err(|e| {
err(
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to read config: {e}"),
)
})?;
if let Some(v) = req.default_provider {
config.default_provider = v;
}
if let Some(v) = req.default_model {
config.default_model = Some(v);
}
if let Some(v) = req.anthropic_key {
config.providers.anthropic_api_key = Some(v);
}
if let Some(v) = req.openai_key {
config.providers.openai_api_key = Some(v);
}
if let Some(v) = req.google_key {
config.providers.google_api_key = Some(v);
}
if let Some(v) = req.openrouter_key {
config.openrouter_api_key = Some(v);
}
if let Some(v) = req.ollama_base_url {
config.ollama_base_url = Some(v);
}
for gateway in req.gateways.unwrap_or_default() {
let entry = config.model_providers.entry(gateway.name).or_default();
if let Some(v) = gateway.base_url {
entry.base_url = Some(v);
}
if let Some(v) = gateway.api_key {
entry.api_key = Some(v);
}
if let Some(v) = gateway.script {
entry.script = Some(v);
}
}
for name in req.remove_gateways.unwrap_or_default() {
config.model_providers.remove(&name);
}
config.save_to_path_public(path).map_err(|e| {
err(
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to write config: {e}"),
)
})?;
Ok(Json(redact(&config)))
}
fn validate_key_format(provider: &str, key: &str) -> (bool, Option<String>) {
match provider {
"anthropic" => {
if key.starts_with("sk-ant-") {
(true, None)
} else {
(
false,
Some("Anthropic keys start with `sk-ant-`.".to_string()),
)
}
}
"openai" => {
if key.starts_with("sk-") {
(true, None)
} else {
(false, Some("OpenAI keys start with `sk-`.".to_string()))
}
}
"google" | "openrouter" => {
if key.trim().is_empty() {
(false, Some("Key must not be empty.".to_string()))
} else {
(true, None)
}
}
_ => match key.trim().is_empty() {
true => (false, Some("Key must not be empty.".to_string())),
false => (true, None),
},
}
}
fn validate_base_url(url: &str) -> (bool, Option<String>) {
let trimmed = url.trim();
if trimmed.is_empty() {
return (false, Some("Base URL must not be empty.".to_string()));
}
match trimmed.starts_with("http://") || trimmed.starts_with("https://") {
true => (true, None),
false => (
false,
Some("Base URL must start with `http://` or `https://`.".to_string()),
),
}
}
pub(super) async fn validate_config_key(Json(req): Json<ValidateKeyReq>) -> Json<ValidateKeyResp> {
if let Some(base_url) = &req.base_url {
let (valid, message) = validate_base_url(base_url);
if !valid {
return Json(ValidateKeyResp { valid, message });
}
}
let (valid, message) = validate_key_format(&req.provider, &req.key);
Json(ValidateKeyResp { valid, message })
}
pub(super) async fn get_models(State(state): State<AppState>) -> Json<Vec<ModelEntry>> {
models_with(&state, &leviath_providers::provider::build_http_client).await
}
pub(super) async fn models_with(
state: &AppState,
build_client: leviath_providers::provider::HttpClientFactory<'_>,
) -> Json<Vec<ModelEntry>> {
let Ok(registry) = crate::commands::run::session::build_provider_registry_from_config_with(
&state.config,
build_client,
) else {
return Json(Vec::new());
};
let mut models = Vec::new();
for provider_name in registry.provider_names() {
let provider = registry
.get(provider_name)
.expect("provider_names returns registered names");
if let Ok(list) = provider.list_models().await {
for m in list {
models.push(ModelEntry {
id: m.id,
provider: m.provider,
display_name: m.display_name,
max_context_tokens: m.capabilities.max_context_tokens,
max_output_tokens: m.capabilities.max_output_tokens,
supports_tools: m.capabilities.supports_tools,
});
}
}
}
Json(models)
}
#[cfg(test)]
mod tests {
use super::*;
use axum::Router;
use axum::body::Body;
use axum::http::Request;
use axum::routing::get;
use std::sync::Arc;
use tokio::sync::broadcast;
use tower::ServiceExt;
use crate::commands::serve::types::ServerEvent;
use crate::config::Config;
fn state_without_a_reachable_ollama() -> AppState {
let (tx, _) = broadcast::channel::<ServerEvent>(64);
AppState {
config: Arc::new(Config {
ollama_base_url: Some("http://127.0.0.1:1".to_string()),
..Config::default()
}),
event_tx: tx,
control: crate::commands::serve::testutil::no_daemon_client(),
mcp: crate::commands::serve::mcp::McpAdmin::default(),
limits: Default::default(),
}
}
fn test_state() -> AppState {
let (tx, _) = broadcast::channel::<ServerEvent>(64);
AppState {
config: Arc::new(Config::default()),
event_tx: tx,
control: crate::commands::serve::testutil::no_daemon_client(),
mcp: crate::commands::serve::mcp::McpAdmin::default(),
limits: Default::default(),
}
}
fn test_state_with_keys() -> AppState {
let (tx, _) = broadcast::channel::<ServerEvent>(64);
AppState {
config: Arc::new(Config {
providers: crate::config::ProviderConfig {
anthropic_api_key: Some("sk-ant-test".to_string()),
openai_api_key: Some("sk-openai-test".to_string()),
google_api_key: None,
claude_code_enabled: false,
claude_code_binary: None,
claude_code_effort: None,
anthropic_cache_ttl: None,
fallback_order: Vec::new(),
},
openrouter_api_key: Some("sk-or-test".to_string()),
ollama_base_url: Some("http://localhost:11434".to_string()),
mcp_servers: vec![],
..Default::default()
}),
event_tx: tx,
control: crate::commands::serve::testutil::no_daemon_client(),
mcp: crate::commands::serve::mcp::McpAdmin::default(),
limits: Default::default(),
}
}
#[tokio::test]
async fn get_config_default_returns_ok() {
let app = Router::new()
.route("/api/config", get(get_config))
.with_state(test_state());
let req = Request::builder()
.uri("/api/config")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let config: RedactedConfig = serde_json::from_slice(&body).unwrap();
assert_eq!(config.default_provider, "anthropic");
assert!(!config.has_anthropic_key);
assert!(!config.has_openai_key);
assert!(!config.has_openrouter_key);
assert!(config.ollama_base_url.is_none());
}
#[tokio::test]
async fn get_config_advertises_the_api_version_capabilities_and_limits() {
let app = Router::new()
.route("/api/config", get(get_config))
.with_state(test_state());
let req = Request::builder()
.uri("/api/config")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let config: RedactedConfig = serde_json::from_slice(&body).unwrap();
assert_eq!(config.api_version, API_VERSION);
for expected in [
"runs.envelope",
"runs.search",
"runs.files.listing",
"blueprints.envelope",
"context.history.page",
] {
assert!(
config.capabilities.iter().any(|c| c == expected),
"missing capability {expected}"
);
}
assert_eq!(
config.limits.max_limit,
crate::commands::serve::runs::MAX_LIMIT
);
assert_eq!(
config.limits.max_file_bytes,
crate::commands::serve::agents::MAX_FILE_READ_BYTES
);
assert_eq!(
config.limits.max_tracked_modified_files,
leviath_core::run_meta::MAX_TRACKED_MODIFIED_FILES
);
}
#[tokio::test]
async fn get_config_with_keys_shows_has_key_true() {
let app = Router::new()
.route("/api/config", get(get_config))
.with_state(test_state_with_keys());
let req = Request::builder()
.uri("/api/config")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let config: RedactedConfig = serde_json::from_slice(&body).unwrap();
assert!(config.has_anthropic_key);
assert!(config.has_openai_key);
assert!(config.has_openrouter_key);
assert_eq!(
config.ollama_base_url.as_deref(),
Some("http://localhost:11434")
);
let raw = std::str::from_utf8(&body).unwrap();
assert!(!raw.contains("sk-ant-test"));
assert!(!raw.contains("sk-openai-test"));
}
#[tokio::test]
async fn get_config_agent_paths_included() {
let (tx, _) = broadcast::channel::<ServerEvent>(64);
let state = AppState {
config: Arc::new(Config {
agent_paths: vec![
std::path::PathBuf::from("/my/agents"),
std::path::PathBuf::from("/other/agents"),
],
..Default::default()
}),
event_tx: tx,
control: crate::commands::serve::testutil::no_daemon_client(),
mcp: crate::commands::serve::mcp::McpAdmin::default(),
limits: Default::default(),
};
let app = Router::new()
.route("/api/config", get(get_config))
.with_state(state);
let req = Request::builder()
.uri("/api/config")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let config: RedactedConfig = serde_json::from_slice(&body).unwrap();
assert_eq!(config.agent_paths.len(), 2);
}
fn test_state_listing_models() -> AppState {
let (tx, _) = broadcast::channel::<ServerEvent>(64);
AppState {
config: Arc::new(Config {
providers: crate::config::ProviderConfig {
claude_code_enabled: true,
..Config::default().providers
},
..Config::default()
}),
event_tx: tx,
control: crate::commands::serve::testutil::no_daemon_client(),
mcp: crate::commands::serve::mcp::McpAdmin::default(),
limits: Default::default(),
}
}
#[tokio::test]
async fn get_models_returns_ok() {
let app = Router::new()
.route("/api/models", get(get_models))
.with_state(state_without_a_reachable_ollama());
let req = Request::builder()
.uri("/api/models")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let models: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
let _ = models;
}
#[tokio::test]
async fn get_models_enumerates_when_a_provider_lists_models() {
let app = Router::new()
.route("/api/models", get(get_models))
.with_state(test_state_listing_models());
let req = Request::builder()
.uri("/api/models")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let models: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
assert!(!models.is_empty());
assert!(models.iter().any(|m| m["provider"] == "claude-code"));
assert!(models.iter().all(|m| m["id"].is_string()));
}
#[test]
fn redacted_config_hides_keys() {
let config = RedactedConfig {
default_provider: "anthropic".to_string(),
has_anthropic_key: true,
has_openai_key: false,
has_google_key: false,
has_openrouter_key: false,
ollama_base_url: None,
gateways: Vec::new(),
agent_paths: vec![],
mcp_server_count: 2,
api_version: API_VERSION.to_string(),
capabilities: API_CAPABILITIES.iter().map(|c| c.to_string()).collect(),
limits: ApiLimits::current(),
};
let json = serde_json::to_string(&config).unwrap();
assert!(!json.contains("sk-"));
assert!(json.contains("\"has_anthropic_key\":true"));
assert!(json.contains("\"has_openai_key\":false"));
assert!(json.contains("\"mcp_server_count\":2"));
}
#[test]
fn redacted_config_with_ollama_url() {
let config = RedactedConfig {
default_provider: "ollama".to_string(),
has_anthropic_key: false,
has_openai_key: false,
has_google_key: false,
has_openrouter_key: false,
ollama_base_url: Some("http://localhost:11434".to_string()),
gateways: Vec::new(),
agent_paths: vec![],
mcp_server_count: 0,
api_version: API_VERSION.to_string(),
capabilities: API_CAPABILITIES.iter().map(|c| c.to_string()).collect(),
limits: ApiLimits::current(),
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("\"ollama_base_url\":\"http://localhost:11434\""));
}
fn state_with_config_path(path: std::path::PathBuf) -> AppState {
let (tx, _) = broadcast::channel::<ServerEvent>(64);
AppState {
config: Arc::new(Config::default()),
event_tx: tx,
control: crate::commands::serve::testutil::no_daemon_client(),
mcp: crate::commands::serve::mcp::McpAdmin {
config_path: path,
..Default::default()
},
limits: Default::default(),
}
}
async fn put_config_request(state: AppState, body: &str) -> axum::http::Response<Body> {
let app = Router::new()
.route("/api/config", axum::routing::put(put_config))
.with_state(state);
let req = Request::builder()
.method("PUT")
.uri("/api/config")
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap();
app.oneshot(req).await.unwrap()
}
#[tokio::test]
async fn put_config_writes_all_present_fields_and_redacts() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
Config::default().save_to_path_public(&path).unwrap();
let body = serde_json::json!({
"default_provider": "openai",
"default_model": "gpt-5",
"anthropic_key": "sk-ant-x",
"openai_key": "sk-openai-x",
"google_key": "g-x",
"openrouter_key": "or-x",
"ollama_base_url": "http://ollama:11434"
})
.to_string();
let resp = put_config_request(state_with_config_path(path.clone()), &body).await;
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let raw = std::str::from_utf8(&bytes).unwrap();
assert!(!raw.contains("sk-ant-x"), "must not leak key values");
let rc: RedactedConfig = serde_json::from_slice(&bytes).unwrap();
assert!(
rc.has_anthropic_key && rc.has_openai_key && rc.has_google_key && rc.has_openrouter_key
);
assert_eq!(rc.default_provider, "openai");
let saved = Config::load_from_path_public(&path).unwrap();
assert_eq!(
saved.providers.anthropic_api_key.as_deref(),
Some("sk-ant-x")
);
assert_eq!(
saved.providers.openai_api_key.as_deref(),
Some("sk-openai-x")
);
assert_eq!(saved.providers.google_api_key.as_deref(), Some("g-x"));
assert_eq!(saved.openrouter_api_key.as_deref(), Some("or-x"));
assert_eq!(saved.default_model.as_deref(), Some("gpt-5"));
assert_eq!(
saved.ollama_base_url.as_deref(),
Some("http://ollama:11434")
);
}
#[tokio::test]
async fn put_config_empty_body_leaves_existing_config_untouched() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
let base = Config {
providers: crate::config::ProviderConfig {
anthropic_api_key: Some("sk-ant-keep".to_string()),
openai_api_key: None,
google_api_key: None,
claude_code_enabled: false,
claude_code_binary: None,
claude_code_effort: None,
anthropic_cache_ttl: None,
fallback_order: Vec::new(),
},
..Default::default()
};
base.save_to_path_public(&path).unwrap();
let resp = put_config_request(state_with_config_path(path.clone()), "{}").await;
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let saved = Config::load_from_path_public(&path).unwrap();
assert_eq!(
saved.providers.anthropic_api_key.as_deref(),
Some("sk-ant-keep")
);
}
#[tokio::test]
async fn put_config_edits_a_gateway_without_being_told_its_key() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
Config::default().save_to_path_public(&path).unwrap();
let state = || state_with_config_path(path.clone());
let resp = put_config_request(
state(),
r#"{"gateways":[{"name":"groq","base_url":"https://api.groq.com","api_key":"sk-secret","script":"groq.rhai"}]}"#,
)
.await;
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let saved = Config::load_from_path_public(&path).unwrap();
assert_eq!(
saved.model_providers["groq"].script.as_deref(),
Some("groq.rhai"),
"the script backing the gateway is written too"
);
let resp = put_config_request(
state(),
r#"{"gateways":[{"name":"groq","base_url":"https://eu.groq.com"}]}"#,
)
.await;
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let saved = Config::load_from_path_public(&path).unwrap();
let gateway = &saved.model_providers["groq"];
assert_eq!(gateway.base_url.as_deref(), Some("https://eu.groq.com"));
assert_eq!(
gateway.api_key.as_deref(),
Some("sk-secret"),
"an unsent key is left alone, not cleared"
);
let resp = put_config_request(state(), r#"{"gateways":[{"name":"other"}]}"#).await;
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let saved = Config::load_from_path_public(&path).unwrap();
assert_eq!(saved.model_providers.len(), 2);
let resp = put_config_request(state(), r#"{"remove_gateways":["other"]}"#).await;
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let saved = Config::load_from_path_public(&path).unwrap();
assert!(saved.model_providers.contains_key("groq"));
assert!(!saved.model_providers.contains_key("other"));
}
#[tokio::test]
async fn put_config_returns_the_gateway_redacted() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
Config::default().save_to_path_public(&path).unwrap();
let resp = put_config_request(
state_with_config_path(path),
r#"{"gateways":[{"name":"groq","api_key":"sk-secret"}]}"#,
)
.await;
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["gateways"][0]["name"], serde_json::json!("groq"));
assert_eq!(json["gateways"][0]["has_api_key"], serde_json::json!(true));
assert!(
!String::from_utf8_lossy(&body).contains("sk-secret"),
"the write's own response must not hand the key back"
);
}
#[tokio::test]
async fn put_config_read_failure_is_500() {
let dir = tempfile::tempdir().unwrap();
let resp = put_config_request(state_with_config_path(dir.path().to_path_buf()), "{}").await;
assert_eq!(resp.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn put_config_write_failure_is_500() {
let dir = tempfile::tempdir().unwrap();
let blocker = dir.path().join("blocker");
std::fs::write(&blocker, b"x").unwrap();
let path = blocker.join("config.toml");
let resp = put_config_request(state_with_config_path(path), "{}").await;
assert_eq!(resp.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
}
#[test]
fn validate_key_format_covers_every_provider() {
assert_eq!(validate_key_format("anthropic", "sk-ant-1"), (true, None));
assert!(!validate_key_format("anthropic", "nope").0);
assert_eq!(validate_key_format("openai", "sk-1"), (true, None));
assert!(!validate_key_format("openai", "nope").0);
assert_eq!(validate_key_format("google", "g"), (true, None));
assert!(!validate_key_format("google", " ").0);
assert_eq!(validate_key_format("openrouter", "or"), (true, None));
assert_eq!(validate_key_format("my-gateway", "anything"), (true, None));
assert!(!validate_key_format("my-gateway", " ").0);
}
#[test]
fn a_gateways_secrets_are_reported_as_presence_not_value() {
let mut config = Config::default();
config.model_providers.insert(
"groq".to_string(),
crate::config::ModelProviderConfig {
script: Some("groq.rhai".to_string()),
api_key: Some("sk-secret-value".to_string()),
base_url: Some("https://api.groq.com".to_string()),
rate_limit: None,
extra: [(
"signing_secret".to_string(),
toml::Value::String("hunter2".to_string()),
)]
.into_iter()
.collect(),
},
);
let redacted = redact(&config);
let gateway = &redacted.gateways[0];
assert_eq!(gateway.name, "groq");
assert_eq!(gateway.base_url.as_deref(), Some("https://api.groq.com"));
assert!(gateway.has_api_key);
assert_eq!(gateway.extra_keys, vec!["signing_secret".to_string()]);
let json = serde_json::to_string(&redacted).expect("serializes");
assert!(!json.contains("sk-secret-value"), "{json}");
assert!(!json.contains("hunter2"), "{json}");
}
#[test]
fn gateways_are_reported_in_a_stable_order() {
let mut config = Config::default();
for name in ["zulu", "alpha", "mike"] {
config
.model_providers
.insert(name.to_string(), Default::default());
}
let names: Vec<String> = gateways_of(&config).into_iter().map(|g| g.name).collect();
assert_eq!(names, vec!["alpha", "mike", "zulu"]);
}
#[test]
fn a_config_with_no_gateways_reports_none() {
assert_eq!(gateways_of(&Config::default()), Vec::new());
}
#[test]
fn a_base_url_is_checked_for_its_scheme() {
assert_eq!(validate_base_url("https://api.example.com"), (true, None));
assert_eq!(validate_base_url("http://localhost:11434"), (true, None));
assert!(!validate_base_url("api.example.com").0);
assert!(!validate_base_url(" ").0);
}
#[tokio::test]
async fn validate_config_key_endpoint_returns_result() {
let app = Router::new().route(
"/api/config/validate",
axum::routing::post(validate_config_key),
);
let req = Request::builder()
.method("POST")
.uri("/api/config/validate")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({"provider":"anthropic","key":"bad"}).to_string(),
))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), axum::http::StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let v: ValidateKeyResp = serde_json::from_slice(&bytes).unwrap();
assert!(!v.valid);
assert!(v.message.is_some());
}
#[tokio::test]
async fn validate_config_key_endpoint_checks_a_gateways_base_url() {
let check = |body: serde_json::Value| async move {
let app = Router::new().route(
"/api/config/validate",
axum::routing::post(validate_config_key),
);
let req = Request::builder()
.method("POST")
.uri("/api/config/validate")
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice::<ValidateKeyResp>(&bytes).unwrap()
};
let bad = check(serde_json::json!({
"provider": "my-gateway",
"key": "anything",
"base_url": "api.example.com",
}))
.await;
assert!(!bad.valid);
assert!(
bad.message.unwrap_or_default().contains("Base URL"),
"the address is what is wrong"
);
let good = check(serde_json::json!({
"provider": "my-gateway",
"key": "anything",
"base_url": "https://api.example.com",
}))
.await;
assert!(good.valid, "{:?}", good.message);
let empty = check(serde_json::json!({
"provider": "my-gateway",
"key": " ",
"base_url": "https://api.example.com",
}))
.await;
assert!(!empty.valid);
}
#[tokio::test]
async fn the_models_endpoint_is_empty_when_no_https_client_can_be_built() {
let state = test_state();
let Json(models) = super::models_with(&state, &|_t| {
Err(leviath_providers::provider::malformed_url_error())
})
.await;
assert!(models.is_empty());
}
}