Download codex-rs/core-plugins/src/remote/search_tests.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 17.1 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core-plugins/src/remote/search_tests.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/core-plugins/src/remote/search_tests.rs
-
curl -L -o search_tests.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core-plugins/src/remote/search_tests.rs
17.1 kB
| use super::*; | |
| use crate::remote::REMOTE_CREATED_BY_ME_MARKETPLACE_NAME; | |
| use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME; | |
| use crate::remote::REMOTE_WORKSPACE_MARKETPLACE_NAME; | |
| use crate::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME; | |
| use crate::remote::RemotePluginShareDiscoverability; | |
| use crate::test_support::recorded_http_client_urls; | |
| use crate::test_support::recording_remote_plugin_service_config; | |
| use codex_app_server_protocol::PluginAuthPolicy; | |
| use codex_app_server_protocol::PluginAvailability; | |
| use codex_app_server_protocol::PluginDisabledReason; | |
| use codex_app_server_protocol::PluginInstallPolicy; | |
| use codex_app_server_protocol::PluginInstallPolicySource; | |
| use codex_app_server_protocol::PluginInterface; | |
| use http::StatusCode; | |
| use pretty_assertions::assert_eq; | |
| use serde_json::json; | |
| use wiremock::Mock; | |
| use wiremock::MockServer; | |
| use wiremock::ResponseTemplate; | |
| use wiremock::matchers::header; | |
| use wiremock::matchers::header_exists; | |
| use wiremock::matchers::method; | |
| use wiremock::matchers::path; | |
| use wiremock::matchers::query_param; | |
| use wiremock::matchers::query_param_is_missing; | |
| fn remote_plugin_json(remote_plugin_id: &str, plugin_name: &str, scope: &str) -> serde_json::Value { | |
| let discoverability = (scope == "WORKSPACE").then_some("LISTED"); | |
| json!({ | |
| "id": remote_plugin_id, | |
| "name": plugin_name, | |
| "scope": scope, | |
| "discoverability": discoverability, | |
| "installation_policy": "AVAILABLE", | |
| "authentication_policy": "ON_USE", | |
| "release": { | |
| "display_name": plugin_name, | |
| "description": format!("{plugin_name} description"), | |
| "interface": {}, | |
| }, | |
| }) | |
| } | |
| async fn search_remote_plugins_forwards_parameters_and_converts_results() { | |
| let server = MockServer::start().await; | |
| let remote_plugin = json!({ | |
| "id": "plugins~Plugin_linear", | |
| "name": "linear", | |
| "scope": "GLOBAL", | |
| "installation_policy": "NOT_AVAILABLE", | |
| "installation_policy_source": "WORKSPACE_SETTING", | |
| "must_show_installation_interstitial": true, | |
| "authentication_policy": "ON_INSTALL", | |
| "status": "DISABLED_BY_ADMIN", | |
| "disabled_reason": "plan_not_eligible", | |
| "eligible_plan_types": ["pro"], | |
| "release": { | |
| "version": "1.2.3", | |
| "display_name": "Linear", | |
| "description": "Track issues", | |
| "keywords": ["issues"], | |
| "interface": { | |
| "short_description": "Issue tracking", | |
| "category": "Productivity", | |
| "capabilities": ["Create issues"], | |
| "logo_url": "https://example.com/linear.png", | |
| }, | |
| }, | |
| }); | |
| Mock::given(method("GET")) | |
| .and(path("/backend-api/ps/plugins/search")) | |
| .and(query_param("q", "linear & docs/+")) | |
| .and(query_param("scope", "GLOBAL")) | |
| .and(query_param("limit", "16")) | |
| .and(query_param("pageToken", "next page/+")) | |
| .and(header("authorization", "Bearer Access Token")) | |
| .and(header("chatgpt-account-id", "account_id")) | |
| .and(header("oai-product-sku", "codex")) | |
| .and(header_exists("user-agent")) | |
| .and(header_exists("originator")) | |
| .respond_with(ResponseTemplate::new(200).set_body_json(json!({ | |
| "plugins": [remote_plugin], | |
| "pagination": {"next_page_token": "later page/+"}, | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| let (config, selected_urls) = | |
| recording_remote_plugin_service_config(format!("{}/backend-api/", server.uri())); | |
| let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); | |
| let result = search_remote_plugins( | |
| &config, | |
| Some(&auth), | |
| RemotePluginSearchRequest { | |
| query: "linear & docs/+", | |
| scope: Some(RemotePluginScope::Global), | |
| limit: 16, | |
| page_token: Some("next page/+"), | |
| }, | |
| ) | |
| .await | |
| .expect("plugin search should succeed"); | |
| assert_eq!( | |
| result, | |
| RemotePluginSearchPage { | |
| plugins: vec![RemotePluginSummary { | |
| id: format!("linear@{REMOTE_GLOBAL_MARKETPLACE_NAME}"), | |
| remote_plugin_id: "plugins~Plugin_linear".to_string(), | |
| version: Some("1.2.3".to_string()), | |
| local_version: None, | |
| name: "linear".to_string(), | |
| share_context: None, | |
| installed: false, | |
| installed_at: None, | |
| enabled: false, | |
| install_policy: PluginInstallPolicy::NotAvailable, | |
| install_policy_source: Some(PluginInstallPolicySource::WorkspaceSetting), | |
| must_show_installation_interstitial: Some(true), | |
| auth_policy: PluginAuthPolicy::OnInstall, | |
| availability: PluginAvailability::DisabledByAdmin, | |
| disabled_reason: Some(PluginDisabledReason::PlanNotEligible), | |
| eligible_plan_types: Some(vec!["pro".to_string()]), | |
| interface: Some(PluginInterface { | |
| display_name: Some("Linear".to_string()), | |
| short_description: Some("Issue tracking".to_string()), | |
| long_description: None, | |
| developer_name: None, | |
| category: Some("Productivity".to_string()), | |
| capabilities: vec!["Create issues".to_string()], | |
| website_url: None, | |
| privacy_policy_url: None, | |
| terms_of_service_url: None, | |
| default_prompt: None, | |
| brand_color: None, | |
| composer_icon: None, | |
| composer_icon_url: None, | |
| logo: None, | |
| logo_dark: None, | |
| logo_url: Some("https://example.com/linear.png".to_string()), | |
| logo_url_dark: None, | |
| screenshots: Vec::new(), | |
| screenshot_urls: Vec::new(), | |
| }), | |
| keywords: vec!["issues".to_string()], | |
| }], | |
| next_page_token: Some("later page/+".to_string()), | |
| } | |
| ); | |
| assert_eq!( | |
| recorded_http_client_urls(&selected_urls), | |
| vec![format!( | |
| "{}/backend-api/ps/plugins/search?q=linear+%26+docs%2F%2B&scope=GLOBAL&limit=16&pageToken=next+page%2F%2B", | |
| server.uri() | |
| )] | |
| ); | |
| } | |
| async fn search_remote_plugins_omits_optional_scope_and_page_token() { | |
| let server = MockServer::start().await; | |
| Mock::given(method("GET")) | |
| .and(path("/backend-api/ps/plugins/search")) | |
| .and(query_param("q", "calendar")) | |
| .and(query_param("limit", "25")) | |
| .and(query_param_is_missing("scope")) | |
| .and(query_param_is_missing("pageToken")) | |
| .respond_with(ResponseTemplate::new(200).set_body_json(json!({ | |
| "plugins": [], | |
| "pagination": {"next_page_token": null}, | |
| }))) | |
| .expect(2) | |
| .mount(&server) | |
| .await; | |
| let (config, _) = | |
| recording_remote_plugin_service_config(format!("{}/backend-api", server.uri())); | |
| let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); | |
| for _ in 0..2 { | |
| let result = search_remote_plugins( | |
| &config, | |
| Some(&auth), | |
| RemotePluginSearchRequest { | |
| query: "calendar", | |
| scope: None, | |
| limit: 25, | |
| page_token: None, | |
| }, | |
| ) | |
| .await | |
| .expect("unscoped plugin search should succeed"); | |
| assert_eq!( | |
| result, | |
| RemotePluginSearchPage { | |
| plugins: Vec::new(), | |
| next_page_token: None, | |
| } | |
| ); | |
| } | |
| } | |
| async fn search_remote_plugins_forwards_each_supported_scope() { | |
| let server = MockServer::start().await; | |
| let (config, _) = | |
| recording_remote_plugin_service_config(format!("{}/backend-api", server.uri())); | |
| let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); | |
| for (scope, expected_scope) in [ | |
| (RemotePluginScope::Global, "GLOBAL"), | |
| (RemotePluginScope::User, "USER"), | |
| (RemotePluginScope::Workspace, "WORKSPACE"), | |
| ] { | |
| Mock::given(method("GET")) | |
| .and(path("/backend-api/ps/plugins/search")) | |
| .and(query_param("scope", expected_scope)) | |
| .respond_with(ResponseTemplate::new(200).set_body_json(json!({ | |
| "plugins": [], | |
| "pagination": {"next_page_token": null}, | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| search_remote_plugins( | |
| &config, | |
| Some(&auth), | |
| RemotePluginSearchRequest { | |
| query: "calendar", | |
| scope: Some(scope), | |
| limit: 16, | |
| page_token: None, | |
| }, | |
| ) | |
| .await | |
| .expect("scoped plugin search should succeed"); | |
| } | |
| } | |
| async fn search_remote_plugins_preserves_order_and_canonical_marketplaces() { | |
| let server = MockServer::start().await; | |
| let mut shared_plugin = remote_plugin_json("plugin-shared", "shared", "WORKSPACE"); | |
| shared_plugin["discoverability"] = json!("PRIVATE"); | |
| Mock::given(method("GET")) | |
| .and(path("/backend-api/ps/plugins/search")) | |
| .respond_with(ResponseTemplate::new(200).set_body_json(json!({ | |
| "plugins": [ | |
| remote_plugin_json("plugin-user", "personal", "USER"), | |
| remote_plugin_json("plugin-global", "global", "GLOBAL"), | |
| remote_plugin_json("plugin-workspace", "workspace", "WORKSPACE"), | |
| shared_plugin, | |
| ], | |
| "pagination": {"next_page_token": null}, | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| let (config, _) = | |
| recording_remote_plugin_service_config(format!("{}/backend-api", server.uri())); | |
| let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); | |
| let result = search_remote_plugins( | |
| &config, | |
| Some(&auth), | |
| RemotePluginSearchRequest { | |
| query: "plugin", | |
| scope: None, | |
| limit: 16, | |
| page_token: None, | |
| }, | |
| ) | |
| .await | |
| .expect("mixed-scope plugin search should succeed"); | |
| let identities = result | |
| .plugins | |
| .into_iter() | |
| .map(|plugin| { | |
| ( | |
| plugin.id, | |
| plugin.remote_plugin_id, | |
| plugin.share_context.map(|context| context.discoverability), | |
| ) | |
| }) | |
| .collect::<Vec<_>>(); | |
| assert_eq!( | |
| identities, | |
| vec![ | |
| ( | |
| format!("personal@{REMOTE_CREATED_BY_ME_MARKETPLACE_NAME}"), | |
| "plugin-user".to_string(), | |
| None, | |
| ), | |
| ( | |
| format!("global@{REMOTE_GLOBAL_MARKETPLACE_NAME}"), | |
| "plugin-global".to_string(), | |
| None, | |
| ), | |
| ( | |
| format!("workspace@{REMOTE_WORKSPACE_MARKETPLACE_NAME}"), | |
| "plugin-workspace".to_string(), | |
| Some(RemotePluginShareDiscoverability::Listed), | |
| ), | |
| ( | |
| format!("shared@{REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME}"), | |
| "plugin-shared".to_string(), | |
| Some(RemotePluginShareDiscoverability::Private), | |
| ), | |
| ] | |
| ); | |
| } | |
| async fn search_remote_plugins_requires_chatgpt_authentication() { | |
| let (config, selected_urls) = | |
| recording_remote_plugin_service_config("https://chatgpt.example/backend-api".to_string()); | |
| let result = search_remote_plugins( | |
| &config, | |
| /*auth*/ None, | |
| RemotePluginSearchRequest { | |
| query: "calendar", | |
| scope: None, | |
| limit: 16, | |
| page_token: None, | |
| }, | |
| ) | |
| .await; | |
| assert!(matches!( | |
| result, | |
| Err(RemotePluginCatalogError::AuthRequired) | |
| )); | |
| assert_eq!( | |
| recorded_http_client_urls(&selected_urls), | |
| Vec::<String>::new() | |
| ); | |
| } | |
| async fn search_remote_plugins_rejects_api_key_authentication() { | |
| let (config, selected_urls) = | |
| recording_remote_plugin_service_config("https://chatgpt.example/backend-api".to_string()); | |
| let auth = CodexAuth::from_api_key("test-api-key"); | |
| let result = search_remote_plugins( | |
| &config, | |
| Some(&auth), | |
| RemotePluginSearchRequest { | |
| query: "calendar", | |
| scope: Some(RemotePluginScope::Global), | |
| limit: 16, | |
| page_token: None, | |
| }, | |
| ) | |
| .await; | |
| assert!(matches!( | |
| result, | |
| Err(RemotePluginCatalogError::UnsupportedAuthMode) | |
| )); | |
| assert_eq!( | |
| recorded_http_client_urls(&selected_urls), | |
| Vec::<String>::new() | |
| ); | |
| } | |
| async fn search_remote_plugins_redacts_sensitive_parameters_from_transport_errors() { | |
| let listener = std::net::TcpListener::bind("127.0.0.1:0") | |
| .expect("test listener should bind to a local port"); | |
| let address = listener | |
| .local_addr() | |
| .expect("test listener should have a local address"); | |
| let connection = std::thread::spawn(move || { | |
| let (stream, _) = listener | |
| .accept() | |
| .expect("test listener should accept the plugin search request"); | |
| drop(stream); | |
| }); | |
| let (config, _) = | |
| recording_remote_plugin_service_config(format!("http://{address}/backend-api")); | |
| let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); | |
| let error = search_remote_plugins( | |
| &config, | |
| Some(&auth), | |
| RemotePluginSearchRequest { | |
| query: "sensitive search term", | |
| scope: Some(RemotePluginScope::Global), | |
| limit: 16, | |
| page_token: Some("sensitive pagination token"), | |
| }, | |
| ) | |
| .await | |
| .expect_err("closed connection should fail the plugin search request"); | |
| connection | |
| .join() | |
| .expect("test listener should close the accepted connection"); | |
| let error_message = error.to_string(); | |
| assert!(!error_message.contains("sensitive search term")); | |
| assert!(!error_message.contains("sensitive pagination token")); | |
| let RemotePluginCatalogError::Request { url, source } = error else { | |
| panic!("expected transport request error"); | |
| }; | |
| assert_eq!( | |
| url, | |
| format!("http://{address}/backend-api/ps/plugins/search") | |
| ); | |
| assert!(!source.to_string().contains("sensitive")); | |
| } | |
| async fn search_remote_plugins_preserves_upstream_http_errors() { | |
| let server = MockServer::start().await; | |
| Mock::given(method("GET")) | |
| .and(path("/backend-api/ps/plugins/search")) | |
| .respond_with(ResponseTemplate::new(503).set_body_string("plugin search unavailable")) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| let (config, _) = | |
| recording_remote_plugin_service_config(format!("{}/backend-api", server.uri())); | |
| let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); | |
| let result = search_remote_plugins( | |
| &config, | |
| Some(&auth), | |
| RemotePluginSearchRequest { | |
| query: "sensitive search term", | |
| scope: Some(RemotePluginScope::Global), | |
| limit: 16, | |
| page_token: Some("sensitive pagination token"), | |
| }, | |
| ) | |
| .await; | |
| let error = result.expect_err("upstream HTTP status should fail"); | |
| let error_message = error.to_string(); | |
| assert!(!error_message.contains("sensitive search term")); | |
| assert!(!error_message.contains("sensitive pagination token")); | |
| let RemotePluginCatalogError::UnexpectedStatus { url, status, body } = error else { | |
| panic!("expected upstream HTTP status error"); | |
| }; | |
| assert_eq!( | |
| (url, status, body), | |
| ( | |
| format!("{}/backend-api/ps/plugins/search", server.uri()), | |
| StatusCode::SERVICE_UNAVAILABLE, | |
| "plugin search unavailable".to_string(), | |
| ) | |
| ); | |
| } | |
| async fn search_remote_plugins_preserves_response_decode_errors() { | |
| let server = MockServer::start().await; | |
| Mock::given(method("GET")) | |
| .and(path("/backend-api/ps/plugins/search")) | |
| .respond_with(ResponseTemplate::new(200).set_body_string("not-json")) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| let (config, _) = | |
| recording_remote_plugin_service_config(format!("{}/backend-api", server.uri())); | |
| let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); | |
| let result = search_remote_plugins( | |
| &config, | |
| Some(&auth), | |
| RemotePluginSearchRequest { | |
| query: "calendar", | |
| scope: None, | |
| limit: 16, | |
| page_token: None, | |
| }, | |
| ) | |
| .await; | |
| assert!(matches!( | |
| result, | |
| Err(RemotePluginCatalogError::Decode { .. }) | |
| )); | |
| } | |