mirror of
https://github.com/openai/codex.git
synced 2026-05-23 20:44:50 +00:00
Resolve remote plugin template app ids
This commit is contained in:
@@ -1176,6 +1176,12 @@ impl PluginRequestProcessor {
|
||||
}
|
||||
|
||||
let plugin_apps = load_plugin_apps(result.installed_path.as_path()).await;
|
||||
let plugin_apps = codex_core_plugins::remote::resolve_remote_plugin_app_ids(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
&plugin_apps,
|
||||
)
|
||||
.await;
|
||||
let apps_needing_auth = self
|
||||
.plugin_apps_needing_auth_for_install(
|
||||
&config,
|
||||
|
||||
@@ -723,6 +723,56 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_resolves_remote_bundle_template_app_ids() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
let bundle_url = mount_remote_plugin_bundle(
|
||||
&server,
|
||||
/*status_code*/ 200,
|
||||
remote_plugin_bundle_tar_gz_bytes_with_apps(
|
||||
"linear",
|
||||
&["templated_apps_GitHubEnterprise"],
|
||||
)?,
|
||||
)
|
||||
.await;
|
||||
configure_remote_plugin_test(codex_home.path(), &server)?;
|
||||
mount_remote_plugin_detail(&server, REMOTE_PLUGIN_ID, "1.2.3", Some(&bundle_url)).await;
|
||||
mount_empty_remote_installed_plugins(&server).await;
|
||||
mount_remote_plugin_install(&server, REMOTE_PLUGIN_ID).await;
|
||||
mount_remote_template_connector_ids(
|
||||
&server,
|
||||
"templated_apps_GitHubEnterprise",
|
||||
&["asdk_app_ghe"],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginInstallResponse = to_response(response)?;
|
||||
assert_eq!(response.apps_needing_auth, Vec::<AppSummary>::new());
|
||||
|
||||
wait_for_remote_plugin_request_count(
|
||||
&server,
|
||||
"GET",
|
||||
"/ps/connectors/by_template_id/templated_apps_GitHubEnterprise",
|
||||
/*expected_count*/ 1,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_install_tracks_remote_plugin_analytics_event() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -1429,6 +1479,24 @@ async fn mount_remote_plugin_install(server: &MockServer, remote_plugin_id: &str
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mount_remote_template_connector_ids(
|
||||
server: &MockServer,
|
||||
template_id: &str,
|
||||
connector_ids: &[&str],
|
||||
) {
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"/backend-api/ps/connectors/by_template_id/{template_id}"
|
||||
)))
|
||||
.and(header("authorization", "Bearer chatgpt-token"))
|
||||
.and(header("chatgpt-account-id", "account-123"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"connector_ids": connector_ids,
|
||||
})))
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CacheManifestExists {
|
||||
manifest_path: std::path::PathBuf,
|
||||
@@ -1575,8 +1643,20 @@ fn write_plugin_source(
|
||||
}
|
||||
|
||||
fn remote_plugin_bundle_tar_gz_bytes(plugin_name: &str) -> Result<Vec<u8>> {
|
||||
remote_plugin_bundle_tar_gz_bytes_with_apps(plugin_name, &[])
|
||||
}
|
||||
|
||||
fn remote_plugin_bundle_tar_gz_bytes_with_apps(
|
||||
plugin_name: &str,
|
||||
app_ids: &[&str],
|
||||
) -> Result<Vec<u8>> {
|
||||
let manifest = format!(r#"{{"name":"{plugin_name}"}}"#);
|
||||
let skill = "# Plan Work\n\nTrack work in Linear.\n";
|
||||
let apps = app_ids
|
||||
.iter()
|
||||
.map(|app_id| ((*app_id).to_string(), json!({ "id": app_id })))
|
||||
.collect::<serde_json::Map<_, _>>();
|
||||
let apps = serde_json::to_vec_pretty(&json!({ "apps": apps }))?;
|
||||
let encoder = GzEncoder::new(Vec::new(), Compression::default());
|
||||
let mut tar = tar::Builder::new(encoder);
|
||||
for (path, contents, mode) in [
|
||||
@@ -1590,6 +1670,7 @@ fn remote_plugin_bundle_tar_gz_bytes(plugin_name: &str) -> Result<Vec<u8>> {
|
||||
skill.as_bytes(),
|
||||
/*mode*/ 0o644,
|
||||
),
|
||||
(".app.json", apps.as_slice(), /*mode*/ 0o644),
|
||||
] {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(contents.len() as u64);
|
||||
|
||||
@@ -110,6 +110,7 @@ struct PluginAppConfig {
|
||||
pub async fn load_plugins_from_layer_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
extra_plugins: HashMap<String, PluginConfig>,
|
||||
app_overrides: HashMap<String, Vec<AppConnectorId>>,
|
||||
store: &PluginStore,
|
||||
restriction_product: Option<Product>,
|
||||
plugin_hooks_enabled: bool,
|
||||
@@ -123,7 +124,7 @@ pub async fn load_plugins_from_layer_stack(
|
||||
let mut plugins = Vec::with_capacity(configured_plugins.len());
|
||||
let mut seen_mcp_server_names = HashMap::<String, String>::new();
|
||||
for (configured_name, plugin) in configured_plugins {
|
||||
let loaded_plugin = load_plugin(
|
||||
let mut loaded_plugin = load_plugin(
|
||||
configured_name.clone(),
|
||||
&plugin,
|
||||
store,
|
||||
@@ -132,6 +133,9 @@ pub async fn load_plugins_from_layer_stack(
|
||||
plugin_hooks_enabled,
|
||||
)
|
||||
.await;
|
||||
if let Some(apps) = app_overrides.get(&configured_name) {
|
||||
loaded_plugin.apps = apps.clone();
|
||||
}
|
||||
for name in loaded_plugin.mcp_servers.keys() {
|
||||
if let Some(previous_plugin) =
|
||||
seen_mcp_server_names.insert(name.clone(), configured_name.clone())
|
||||
|
||||
@@ -493,6 +493,7 @@ impl PluginsManager {
|
||||
let outcome = load_plugins_from_layer_stack(
|
||||
&config.config_layer_stack,
|
||||
self.remote_installed_plugin_configs(config),
|
||||
self.remote_installed_plugin_app_overrides(config),
|
||||
&self.store,
|
||||
self.restriction_product,
|
||||
plugin_hooks_enabled,
|
||||
@@ -541,6 +542,7 @@ impl PluginsManager {
|
||||
load_plugins_from_layer_stack(
|
||||
config_layer_stack,
|
||||
self.remote_installed_plugin_configs(config),
|
||||
self.remote_installed_plugin_app_overrides(config),
|
||||
&self.store,
|
||||
self.restriction_product,
|
||||
plugin_hooks_feature_enabled,
|
||||
@@ -602,6 +604,32 @@ impl PluginsManager {
|
||||
remote_installed_plugins_to_config(plugins, &self.store)
|
||||
}
|
||||
|
||||
fn remote_installed_plugin_app_overrides(
|
||||
&self,
|
||||
config: &PluginsConfigInput,
|
||||
) -> HashMap<String, Vec<AppConnectorId>> {
|
||||
if !config.remote_plugin_enabled {
|
||||
return HashMap::new();
|
||||
}
|
||||
|
||||
let cache = match self.remote_installed_plugins_cache.read() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
};
|
||||
let Some(plugins) = cache.as_ref() else {
|
||||
return HashMap::new();
|
||||
};
|
||||
|
||||
plugins
|
||||
.iter()
|
||||
.filter_map(|plugin| {
|
||||
PluginId::new(plugin.name.clone(), plugin.marketplace_name.clone())
|
||||
.ok()
|
||||
.map(|plugin_id| (plugin_id.as_key(), plugin.app_connector_ids.clone()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn write_remote_installed_plugins_cache(&self, plugins: Vec<RemoteInstalledPlugin>) -> bool {
|
||||
let mut cache = match self.remote_installed_plugins_cache.write() {
|
||||
Ok(cache) => cache,
|
||||
@@ -1756,7 +1784,26 @@ impl PluginsManager {
|
||||
)
|
||||
.await;
|
||||
match installed_plugins {
|
||||
Ok(installed_plugins) => {
|
||||
Ok(mut installed_plugins) => {
|
||||
for installed_plugin in &mut installed_plugins {
|
||||
let Ok(plugin_id) = PluginId::new(
|
||||
installed_plugin.name.clone(),
|
||||
installed_plugin.marketplace_name.clone(),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let Some(plugin_root) = self.store.active_plugin_root(&plugin_id) else {
|
||||
continue;
|
||||
};
|
||||
let bundle_app_ids = load_plugin_apps(plugin_root.as_path()).await;
|
||||
installed_plugin.app_connector_ids =
|
||||
crate::remote::resolve_remote_plugin_app_ids(
|
||||
&request.service_config,
|
||||
request.auth.as_ref(),
|
||||
&bundle_app_ids,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
// TODO(remote plugins): reconcile missing or stale local bundles before
|
||||
// publishing remote installed state as effective local plugin config.
|
||||
let changed = self.write_remote_installed_plugins_cache(installed_plugins);
|
||||
|
||||
@@ -352,6 +352,7 @@ remote_plugin = true
|
||||
id: "plugins~Plugin_linear".to_string(),
|
||||
name: "linear".to_string(),
|
||||
enabled: true,
|
||||
app_connector_ids: Vec::new(),
|
||||
}]);
|
||||
|
||||
let outcome = manager.plugins_for_config(&config).await;
|
||||
@@ -363,6 +364,52 @@ remote_plugin = true
|
||||
assert_eq!(outcome.plugins()[0].config_name, "linear@chatgpt-global");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_installed_cache_uses_resolved_bundle_app_ids_for_runtime_loading() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let plugin_base = codex_home
|
||||
.path()
|
||||
.join("plugins/cache/chatgpt-global/linear");
|
||||
write_plugin(&plugin_base, "local", "linear");
|
||||
write_file(
|
||||
&plugin_base.join("local/.app.json"),
|
||||
r#"{
|
||||
"apps": {
|
||||
"github-enterprise": {
|
||||
"id": "templated_apps_GitHubEnterprise"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
write_file(
|
||||
&codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
remote_plugin = true
|
||||
"#,
|
||||
);
|
||||
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
manager.write_remote_installed_plugins_cache(vec![RemoteInstalledPlugin {
|
||||
marketplace_name: "chatgpt-global".to_string(),
|
||||
id: "plugins~Plugin_linear".to_string(),
|
||||
name: "linear".to_string(),
|
||||
enabled: true,
|
||||
app_connector_ids: vec![AppConnectorId("asdk_app_ghe".to_string())],
|
||||
}]);
|
||||
|
||||
let outcome = manager.plugins_for_config(&config).await;
|
||||
assert_eq!(
|
||||
outcome.effective_apps(),
|
||||
vec![AppConnectorId("asdk_app_ghe".to_string())]
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.capability_summaries()[0].app_connector_ids,
|
||||
vec![AppConnectorId("asdk_app_ghe".to_string())]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_installed_cache_ignores_plugins_missing_local_cache() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
@@ -381,6 +428,7 @@ remote_plugin = true
|
||||
id: "plugins~Plugin_linear".to_string(),
|
||||
name: "linear".to_string(),
|
||||
enabled: true,
|
||||
app_connector_ids: Vec::new(),
|
||||
}]);
|
||||
|
||||
let outcome = manager.plugins_for_config(&config).await;
|
||||
@@ -3666,6 +3714,7 @@ async fn load_plugins_ignores_project_config_files() {
|
||||
let outcome = load_plugins_from_layer_stack(
|
||||
&stack,
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
&PluginStore::new(codex_home.path().to_path_buf()),
|
||||
Some(Product::Codex),
|
||||
/*plugin_hooks_enabled*/ false,
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_app_server_protocol::PluginInterface;
|
||||
use codex_app_server_protocol::SkillInterface;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
use codex_plugin::AppConnectorId;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use reqwest::RequestBuilder;
|
||||
@@ -18,6 +19,7 @@ use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
mod remote_installed_plugin_sync;
|
||||
@@ -56,6 +58,7 @@ const REMOTE_PLUGIN_CATALOG_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const REMOTE_PLUGIN_LIST_PAGE_LIMIT: u32 = 200;
|
||||
const MAX_REMOTE_DEFAULT_PROMPT_LEN: usize = 128;
|
||||
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
const TEMPLATE_APP_ID_PREFIX: &str = "templated_apps_";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RemotePluginServiceConfig {
|
||||
@@ -82,6 +85,7 @@ pub struct RemoteInstalledPlugin {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub enabled: bool,
|
||||
pub app_connector_ids: Vec<AppConnectorId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -441,6 +445,12 @@ struct RemotePluginMutationResponse {
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct RemoteTemplateConnectorIdsResponse {
|
||||
#[serde(default)]
|
||||
connector_ids: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn fetch_remote_marketplaces(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
@@ -588,6 +598,67 @@ pub async fn fetch_remote_installed_plugins(
|
||||
Ok(installed_plugins)
|
||||
}
|
||||
|
||||
pub async fn resolve_remote_plugin_app_ids(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
app_ids: &[AppConnectorId],
|
||||
) -> Vec<AppConnectorId> {
|
||||
let mut resolved_app_ids = Vec::new();
|
||||
let mut seen_app_ids = HashSet::new();
|
||||
let mut template_connector_ids = BTreeMap::<String, Option<Vec<String>>>::new();
|
||||
|
||||
for app_id in app_ids {
|
||||
if !app_id.0.starts_with(TEMPLATE_APP_ID_PREFIX) {
|
||||
if seen_app_ids.insert(app_id.clone()) {
|
||||
resolved_app_ids.push(app_id.clone());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let connector_ids = if let Some(connector_ids) = template_connector_ids.get(&app_id.0) {
|
||||
connector_ids.clone()
|
||||
} else {
|
||||
let connector_ids = match ensure_chatgpt_auth(auth) {
|
||||
Ok(auth) => {
|
||||
match fetch_template_connector_ids(config, auth, app_id.0.as_str()).await {
|
||||
Ok(connector_ids) => Some(connector_ids),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
template_app_id = %app_id.0,
|
||||
error = %err,
|
||||
"failed to resolve remote plugin template app id; dropping it"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
template_app_id = %app_id.0,
|
||||
error = %err,
|
||||
"cannot resolve remote plugin template app id without ChatGPT auth; dropping it"
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
template_connector_ids.insert(app_id.0.clone(), connector_ids.clone());
|
||||
connector_ids
|
||||
};
|
||||
|
||||
let Some(connector_ids) = connector_ids else {
|
||||
continue;
|
||||
};
|
||||
for connector_id in connector_ids {
|
||||
let connector_id = AppConnectorId(connector_id);
|
||||
if seen_app_ids.insert(connector_id.clone()) {
|
||||
resolved_app_ids.push(connector_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolved_app_ids
|
||||
}
|
||||
|
||||
pub async fn fetch_remote_plugin_detail(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
@@ -929,6 +1000,7 @@ fn remote_installed_plugin_to_info(
|
||||
id: plugin.id.clone(),
|
||||
name: plugin.name.clone(),
|
||||
enabled: installed_plugin.enabled,
|
||||
app_connector_ids: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1163,6 +1235,18 @@ async fn fetch_plugin_detail(
|
||||
send_and_decode(request, &url).await
|
||||
}
|
||||
|
||||
async fn fetch_template_connector_ids(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: &CodexAuth,
|
||||
template_id: &str,
|
||||
) -> Result<Vec<String>, RemotePluginCatalogError> {
|
||||
let url = remote_template_connector_ids_url(config, template_id)?;
|
||||
let client = build_reqwest_client();
|
||||
let request = authenticated_request(client.get(&url), auth)?;
|
||||
let response: RemoteTemplateConnectorIdsResponse = send_and_decode(request, &url).await?;
|
||||
Ok(response.connector_ids)
|
||||
}
|
||||
|
||||
fn remote_plugin_skill_detail_url(
|
||||
config: &RemotePluginServiceConfig,
|
||||
plugin_id: &str,
|
||||
@@ -1184,6 +1268,25 @@ fn remote_plugin_skill_detail_url(
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
||||
fn remote_template_connector_ids_url(
|
||||
config: &RemotePluginServiceConfig,
|
||||
template_id: &str,
|
||||
) -> Result<String, RemotePluginCatalogError> {
|
||||
let mut url = Url::parse(config.chatgpt_base_url.trim_end_matches('/'))
|
||||
.map_err(RemotePluginCatalogError::InvalidBaseUrl)?;
|
||||
{
|
||||
let mut segments = url
|
||||
.path_segments_mut()
|
||||
.map_err(|()| RemotePluginCatalogError::InvalidBaseUrlPath)?;
|
||||
segments.pop_if_empty();
|
||||
segments.push("ps");
|
||||
segments.push("connectors");
|
||||
segments.push("by_template_id");
|
||||
segments.push(template_id);
|
||||
}
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
||||
fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth, RemotePluginCatalogError> {
|
||||
let Some(auth) = auth else {
|
||||
return Err(RemotePluginCatalogError::AuthRequired);
|
||||
@@ -1229,3 +1332,6 @@ async fn send_and_decode<T: for<'de> Deserialize<'de>>(
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
105
codex-rs/core-plugins/src/remote/tests.rs
Normal file
105
codex-rs/core-plugins/src/remote/tests.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use super::*;
|
||||
use codex_login::CodexAuth;
|
||||
use pretty_assertions::assert_eq;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::header;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
fn test_config(server: &MockServer) -> RemotePluginServiceConfig {
|
||||
RemotePluginServiceConfig {
|
||||
chatgpt_base_url: format!("{}/backend-api", server.uri()),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_auth() -> CodexAuth {
|
||||
CodexAuth::create_dummy_chatgpt_auth_for_testing()
|
||||
}
|
||||
|
||||
fn app(id: &str) -> AppConnectorId {
|
||||
AppConnectorId(id.to_string())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_remote_plugin_app_ids_expands_templates_and_dedupes_stably() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path(
|
||||
"/backend-api/ps/connectors/by_template_id/templated_apps_GitHubEnterprise",
|
||||
))
|
||||
.and(header("authorization", "Bearer Access Token"))
|
||||
.and(header("chatgpt-account-id", "account_id"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(
|
||||
r#"{"connector_ids":["connector_ghe","asdk_app_ghe","connector_ghe"]}"#,
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let resolved = resolve_remote_plugin_app_ids(
|
||||
&test_config(&server),
|
||||
Some(&test_auth()),
|
||||
&[
|
||||
app("asdk_app_linear"),
|
||||
app("templated_apps_GitHubEnterprise"),
|
||||
app("asdk_app_linear"),
|
||||
app("asdk_app_ghe"),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
resolved,
|
||||
vec![
|
||||
app("asdk_app_linear"),
|
||||
app("connector_ghe"),
|
||||
app("asdk_app_ghe"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_remote_plugin_app_ids_drops_missing_template_mappings() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path(
|
||||
"/backend-api/ps/connectors/by_template_id/templated_apps_GitHubEnterprise",
|
||||
))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(r#"{"connector_ids":[]}"#))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let resolved = resolve_remote_plugin_app_ids(
|
||||
&test_config(&server),
|
||||
Some(&test_auth()),
|
||||
&[app("templated_apps_GitHubEnterprise")],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(resolved, Vec::<AppConnectorId>::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_remote_plugin_app_ids_drops_templates_when_lookup_fails() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path(
|
||||
"/backend-api/ps/connectors/by_template_id/templated_apps_GitHubEnterprise",
|
||||
))
|
||||
.respond_with(ResponseTemplate::new(500).set_body_string("lookup failed"))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let resolved = resolve_remote_plugin_app_ids(
|
||||
&test_config(&server),
|
||||
Some(&test_auth()),
|
||||
&[
|
||||
app("asdk_app_linear"),
|
||||
app("templated_apps_GitHubEnterprise"),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(resolved, vec![app("asdk_app_linear")]);
|
||||
}
|
||||
Reference in New Issue
Block a user