Download codex-rs/core-plugins/src/remote/share/tests.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 27.5 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core-plugins/src/remote/share/tests.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/core-plugins/src/remote/share/tests.rs
-
curl -L -o tests.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core-plugins/src/remote/share/tests.rs
27.5 kB
| use super::*; | |
| 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::PluginInstallPolicy; | |
| use codex_app_server_protocol::PluginInterface; | |
| use codex_login::CodexAuth; | |
| use codex_utils_absolute_path::AbsolutePathBuf; | |
| use pretty_assertions::assert_eq; | |
| use serde_json::json; | |
| use std::collections::BTreeMap; | |
| use std::fs; | |
| use std::io::Read; | |
| use std::path::Path; | |
| use std::path::PathBuf; | |
| use tempfile::TempDir; | |
| use wiremock::Mock; | |
| use wiremock::MockServer; | |
| use wiremock::ResponseTemplate; | |
| use wiremock::matchers::body_json; | |
| use wiremock::matchers::header; | |
| use wiremock::matchers::method; | |
| use wiremock::matchers::path; | |
| use wiremock::matchers::query_param; | |
| use wiremock::matchers::query_param_is_missing; | |
| fn test_config(server: &MockServer) -> RemotePluginServiceConfig { | |
| RemotePluginServiceConfig::new( | |
| format!("{}/backend-api", server.uri()), | |
| crate::test_support::test_http_client_factory(), | |
| ) | |
| } | |
| fn test_auth() -> CodexAuth { | |
| CodexAuth::create_dummy_chatgpt_auth_for_testing() | |
| } | |
| fn write_file(path: &Path, contents: &str) { | |
| fs::create_dir_all(path.parent().expect("file should have a parent")).unwrap(); | |
| fs::write(path, contents).unwrap(); | |
| } | |
| fn write_test_plugin(root: &Path, plugin_name: &str) -> PathBuf { | |
| let plugin_path = root.join(plugin_name); | |
| write_file( | |
| &plugin_path.join(".codex-plugin/plugin.json"), | |
| &format!(r#"{{"name":"{plugin_name}"}}"#), | |
| ); | |
| write_file( | |
| &plugin_path.join("skills/example/SKILL.md"), | |
| "# Example\n\nA test skill.\n", | |
| ); | |
| plugin_path | |
| } | |
| fn write_plugin_share_local_path_mapping( | |
| codex_home: &Path, | |
| remote_plugin_id: &str, | |
| plugin_path: &AbsolutePathBuf, | |
| ) { | |
| write_file( | |
| &codex_home.join(".tmp/plugin-share-local-paths-v1.json"), | |
| &format!( | |
| "{}\n", | |
| serde_json::to_string_pretty(&json!({ | |
| "localPluginPathsByRemotePluginId": { | |
| remote_plugin_id: plugin_path, | |
| }, | |
| })) | |
| .unwrap() | |
| ), | |
| ); | |
| } | |
| fn archive_file_entries(archive_bytes: &[u8]) -> BTreeMap<String, Vec<u8>> { | |
| let decoder = flate2::read::GzDecoder::new(archive_bytes); | |
| let mut archive = tar::Archive::new(decoder); | |
| archive | |
| .entries() | |
| .unwrap() | |
| .filter_map(|entry| { | |
| let mut entry = entry.unwrap(); | |
| if !entry.header().entry_type().is_file() { | |
| return None; | |
| } | |
| let path = entry.path().unwrap().to_string_lossy().into_owned(); | |
| let mut contents = Vec::new(); | |
| entry.read_to_end(&mut contents).unwrap(); | |
| Some((path, contents)) | |
| }) | |
| .collect() | |
| } | |
| fn remote_plugin_json(plugin_id: &str) -> serde_json::Value { | |
| json!({ | |
| "id": plugin_id, | |
| "name": "demo-plugin", | |
| "scope": "WORKSPACE", | |
| "discoverability": "PRIVATE", | |
| "installation_policy": "AVAILABLE", | |
| "authentication_policy": "ON_USE", | |
| "release": { | |
| "version": "0.1.0", | |
| "display_name": "Demo Plugin", | |
| "description": "Demo plugin description", | |
| "interface": { | |
| "short_description": "A demo plugin", | |
| "capabilities": ["Read", "Write"] | |
| }, | |
| "skills": [] | |
| } | |
| }) | |
| } | |
| fn remote_plugin_json_with_share_url_and_principals( | |
| plugin_id: &str, | |
| share_url: Option<&str>, | |
| share_principals: serde_json::Value, | |
| ) -> serde_json::Value { | |
| let mut plugin = remote_plugin_json(plugin_id); | |
| let serde_json::Value::Object(fields) = &mut plugin else { | |
| unreachable!("plugin json should be an object"); | |
| }; | |
| fields.insert("discoverability".to_string(), json!("PRIVATE")); | |
| fields.insert("share_url".to_string(), json!(share_url)); | |
| fields.insert("share_principals".to_string(), share_principals); | |
| plugin | |
| } | |
| fn installed_remote_plugin_json(plugin_id: &str) -> serde_json::Value { | |
| let mut plugin = remote_plugin_json(plugin_id); | |
| let serde_json::Value::Object(fields) = &mut plugin else { | |
| unreachable!("plugin json should be an object"); | |
| }; | |
| fields.insert("enabled".to_string(), json!(true)); | |
| fields.insert("disabled_skill_names".to_string(), json!([])); | |
| plugin | |
| } | |
| fn empty_pagination_json() -> serde_json::Value { | |
| json!({ | |
| "next_page_token": null | |
| }) | |
| } | |
| fn expected_plugin_interface() -> PluginInterface { | |
| PluginInterface { | |
| display_name: Some("Demo Plugin".to_string()), | |
| short_description: Some("A demo plugin".to_string()), | |
| long_description: None, | |
| developer_name: None, | |
| category: None, | |
| capabilities: vec!["Read".to_string(), "Write".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: None, | |
| logo_url_dark: None, | |
| screenshots: Vec::new(), | |
| screenshot_urls: Vec::new(), | |
| } | |
| } | |
| async fn save_remote_plugin_share_creates_workspace_plugin() { | |
| let codex_home = TempDir::new().unwrap(); | |
| let temp_dir = TempDir::new().unwrap(); | |
| let plugin_path = | |
| AbsolutePathBuf::try_from(write_test_plugin(temp_dir.path(), "demo-plugin")).unwrap(); | |
| let archive_size = archive_plugin_for_upload(plugin_path.as_path()) | |
| .unwrap() | |
| .len(); | |
| let server = MockServer::start().await; | |
| let (config, selected_urls) = | |
| recording_remote_plugin_service_config(format!("{}/backend-api", server.uri())); | |
| let auth = test_auth(); | |
| Mock::given(method("POST")) | |
| .and(path("/backend-api/public/plugins/workspace/upload-url")) | |
| .and(header("authorization", "Bearer Access Token")) | |
| .and(header("chatgpt-account-id", "account_id")) | |
| .and(body_json(json!({ | |
| "filename": "demo-plugin.tar.gz", | |
| "mime_type": "application/gzip", | |
| "size_bytes": archive_size, | |
| }))) | |
| .respond_with(ResponseTemplate::new(201).set_body_json(json!({ | |
| "file_id": "file_123", | |
| "upload_url": format!("{}/upload/file_123", server.uri()), | |
| "etag": "\"upload_etag_123\"", | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| Mock::given(method("PUT")) | |
| .and(path("/upload/file_123")) | |
| .and(header("x-ms-blob-type", "BlockBlob")) | |
| .and(header("content-type", "application/gzip")) | |
| .respond_with(ResponseTemplate::new(201).insert_header("etag", "\"blob_etag_123\"")) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| Mock::given(method("POST")) | |
| .and(path("/backend-api/public/plugins/workspace")) | |
| .and(header("authorization", "Bearer Access Token")) | |
| .and(header("chatgpt-account-id", "account_id")) | |
| .and(body_json(json!({ | |
| "file_id": "file_123", | |
| "etag": "\"upload_etag_123\"", | |
| "discoverability": "UNLISTED", | |
| "share_targets": [ | |
| { | |
| "principal_type": "user", | |
| "principal_id": "user-1", | |
| "role": "reader", | |
| }, | |
| { | |
| "principal_type": "workspace", | |
| "principal_id": "account_id", | |
| "role": "reader", | |
| }, | |
| ], | |
| }))) | |
| .respond_with(ResponseTemplate::new(201).set_body_json(json!({ | |
| "plugin_id": "plugins_123", | |
| "share_url": "https://chatgpt.example/plugins/share/share-key-1", | |
| "can_publish_to_workspace": true, | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| let result = save_remote_plugin_share( | |
| &config, | |
| Some(&auth), | |
| codex_home.path(), | |
| &plugin_path, | |
| /*remote_plugin_id*/ None, | |
| RemotePluginShareAccessPolicy { | |
| discoverability: Some(RemotePluginShareDiscoverability::Unlisted), | |
| share_targets: Some(vec![RemotePluginShareTarget { | |
| principal_type: RemotePluginSharePrincipalType::User, | |
| principal_id: "user-1".to_string(), | |
| role: RemotePluginShareTargetRole::Reader, | |
| }]), | |
| }, | |
| ) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| result, | |
| RemotePluginShareSaveResult { | |
| remote_plugin_id: "plugins_123".to_string(), | |
| share_url: Some("https://chatgpt.example/plugins/share/share-key-1".to_string()), | |
| can_publish_to_workspace: Some(true), | |
| } | |
| ); | |
| assert_eq!( | |
| local_paths::load_plugin_share_local_paths(codex_home.path()).unwrap(), | |
| BTreeMap::from([("plugins_123".to_string(), plugin_path)]) | |
| ); | |
| assert_eq!( | |
| recorded_http_client_urls(&selected_urls), | |
| vec![ | |
| format!( | |
| "{}/backend-api/public/plugins/workspace/upload-url", | |
| server.uri() | |
| ), | |
| format!("{}/upload/file_123", server.uri()), | |
| format!("{}/backend-api/public/plugins/workspace", server.uri()), | |
| ] | |
| ); | |
| let requests = server.received_requests().await.unwrap_or_default(); | |
| let upload_request = requests | |
| .iter() | |
| .find(|request| request.method == "PUT" && request.url.path() == "/upload/file_123") | |
| .unwrap(); | |
| let archive_files = archive_file_entries(&upload_request.body); | |
| assert_eq!( | |
| archive_files | |
| .get(".codex-plugin/plugin.json") | |
| .map(Vec::as_slice), | |
| Some(br#"{"name":"demo-plugin"}"#.as_slice()) | |
| ); | |
| assert_eq!( | |
| archive_files | |
| .get("skills/example/SKILL.md") | |
| .map(Vec::as_slice), | |
| Some(b"# Example\n\nA test skill.\n".as_slice()) | |
| ); | |
| } | |
| fn archive_plugin_for_upload_rejects_archives_over_limit() { | |
| let temp_dir = TempDir::new().unwrap(); | |
| let plugin_path = write_test_plugin(temp_dir.path(), "demo-plugin"); | |
| write_file( | |
| &plugin_path.join("large.txt"), | |
| &"0123456789abcdef".repeat(1024), | |
| ); | |
| let err = archive_plugin_for_upload_with_limit(&plugin_path, /*max_bytes*/ 16) | |
| .expect_err("oversized plugin archive should fail"); | |
| assert!(matches!( | |
| err, | |
| RemotePluginCatalogError::ArchiveTooLarge { .. } | |
| )); | |
| } | |
| fn archive_plugin_for_upload_places_manifest_at_archive_root() { | |
| let temp_dir = TempDir::new().unwrap(); | |
| let plugin_path = write_test_plugin(temp_dir.path(), "demo-plugin"); | |
| let archive_bytes = archive_plugin_for_upload(&plugin_path).unwrap(); | |
| let archive_files = archive_file_entries(&archive_bytes); | |
| assert_eq!( | |
| archive_files.keys().cloned().collect::<Vec<_>>(), | |
| vec![ | |
| ".codex-plugin/plugin.json".to_string(), | |
| "skills/example/SKILL.md".to_string() | |
| ] | |
| ); | |
| assert_eq!( | |
| archive_files | |
| .get(".codex-plugin/plugin.json") | |
| .map(Vec::as_slice), | |
| Some(br#"{"name":"demo-plugin"}"#.as_slice()) | |
| ); | |
| assert_eq!( | |
| archive_files | |
| .get("skills/example/SKILL.md") | |
| .map(Vec::as_slice), | |
| Some(b"# Example\n\nA test skill.\n".as_slice()) | |
| ); | |
| } | |
| fn archive_plugin_for_upload_round_trips_through_plugin_bundle_archive_with_long_paths() { | |
| let temp_dir = TempDir::new().unwrap(); | |
| let plugin_path = write_test_plugin(temp_dir.path(), "demo-plugin"); | |
| let long_skill_path = Path::new("skills") | |
| .join(["segment"; 40].join("/")) | |
| .join("SKILL.md"); | |
| write_file(&plugin_path.join(&long_skill_path), "# Long path skill\n"); | |
| let archive_bytes = archive_plugin_for_upload(&plugin_path).unwrap(); | |
| let destination = TempDir::new().unwrap(); | |
| crate::plugin_bundle_archive::unpack_plugin_bundle_tar_gz( | |
| &archive_bytes, | |
| destination.path(), | |
| /*max_total_bytes*/ 1024 * 1024, | |
| ) | |
| .expect("extract shared plugin archive"); | |
| assert_eq!( | |
| fs::read_to_string(destination.path().join(".codex-plugin/plugin.json")).unwrap(), | |
| r#"{"name":"demo-plugin"}"# | |
| ); | |
| assert_eq!( | |
| fs::read_to_string(destination.path().join(long_skill_path)).unwrap(), | |
| "# Long path skill\n" | |
| ); | |
| } | |
| async fn save_remote_plugin_share_updates_existing_workspace_plugin() { | |
| let codex_home = TempDir::new().unwrap(); | |
| let temp_dir = TempDir::new().unwrap(); | |
| let plugin_path = | |
| AbsolutePathBuf::try_from(write_test_plugin(temp_dir.path(), "demo-plugin")).unwrap(); | |
| let archive_size = archive_plugin_for_upload(plugin_path.as_path()) | |
| .unwrap() | |
| .len(); | |
| let server = MockServer::start().await; | |
| let config = test_config(&server); | |
| let auth = test_auth(); | |
| Mock::given(method("POST")) | |
| .and(path("/backend-api/public/plugins/workspace/upload-url")) | |
| .and(body_json(json!({ | |
| "filename": "demo-plugin.tar.gz", | |
| "mime_type": "application/gzip", | |
| "size_bytes": archive_size, | |
| "plugin_id": "plugins_123", | |
| }))) | |
| .respond_with(ResponseTemplate::new(201).set_body_json(json!({ | |
| "file_id": "file_456", | |
| "upload_url": format!("{}/upload/file_456", server.uri()), | |
| "etag": "\"upload_etag_456\"", | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| Mock::given(method("PUT")) | |
| .and(path("/upload/file_456")) | |
| .respond_with(ResponseTemplate::new(201).insert_header("etag", "\"blob_etag_456\"")) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| Mock::given(method("POST")) | |
| .and(path("/backend-api/public/plugins/workspace/plugins_123")) | |
| .and(body_json(json!({ | |
| "file_id": "file_456", | |
| "etag": "\"upload_etag_456\"", | |
| }))) | |
| .respond_with(ResponseTemplate::new(200).set_body_json(json!({ | |
| "plugin_id": "plugins_123", | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| let result = save_remote_plugin_share( | |
| &config, | |
| Some(&auth), | |
| codex_home.path(), | |
| &plugin_path, | |
| Some("plugins_123"), | |
| RemotePluginShareAccessPolicy::default(), | |
| ) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| result, | |
| RemotePluginShareSaveResult { | |
| remote_plugin_id: "plugins_123".to_string(), | |
| share_url: None, | |
| can_publish_to_workspace: None, | |
| } | |
| ); | |
| } | |
| async fn update_remote_plugin_share_targets_updates_targets() { | |
| let server = MockServer::start().await; | |
| let config = test_config(&server); | |
| let auth = test_auth(); | |
| Mock::given(method("PUT")) | |
| .and(path("/backend-api/ps/plugins/plugins_123/shares")) | |
| .and(header("authorization", "Bearer Access Token")) | |
| .and(header("chatgpt-account-id", "account_id")) | |
| .and(body_json(json!({ | |
| "discoverability": "UNLISTED", | |
| "targets": [ | |
| { | |
| "principal_type": "user", | |
| "principal_id": "user-1", | |
| "role": "editor", | |
| }, | |
| { | |
| "principal_type": "group", | |
| "principal_id": "group-1", | |
| "role": "reader", | |
| }, | |
| { | |
| "principal_type": "workspace", | |
| "principal_id": "account_id", | |
| "role": "reader", | |
| }, | |
| ], | |
| }))) | |
| .respond_with(ResponseTemplate::new(200).set_body_json(json!({ | |
| "principals": [ | |
| { | |
| "principal_type": "user", | |
| "principal_id": "user-1", | |
| "role": "editor", | |
| "name": "Gavin", | |
| }, | |
| { | |
| "principal_type": "group", | |
| "principal_id": "group-1", | |
| "role": "reader", | |
| "name": "Engineering", | |
| }, | |
| ], | |
| "discoverability": "UNLISTED", | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| let result = update_remote_plugin_share_targets( | |
| &config, | |
| Some(&auth), | |
| "plugins_123", | |
| vec![ | |
| RemotePluginShareTarget { | |
| principal_type: RemotePluginSharePrincipalType::User, | |
| principal_id: "user-1".to_string(), | |
| role: RemotePluginShareTargetRole::Editor, | |
| }, | |
| RemotePluginShareTarget { | |
| principal_type: RemotePluginSharePrincipalType::Group, | |
| principal_id: "group-1".to_string(), | |
| role: RemotePluginShareTargetRole::Reader, | |
| }, | |
| ], | |
| RemotePluginShareUpdateDiscoverability::Unlisted, | |
| ) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| result, | |
| RemotePluginShareUpdateTargetsResult { | |
| principals: vec![ | |
| RemotePluginSharePrincipal { | |
| principal_type: RemotePluginSharePrincipalType::User, | |
| principal_id: "user-1".to_string(), | |
| role: RemotePluginSharePrincipalRole::Editor, | |
| name: "Gavin".to_string(), | |
| }, | |
| RemotePluginSharePrincipal { | |
| principal_type: RemotePluginSharePrincipalType::Group, | |
| principal_id: "group-1".to_string(), | |
| role: RemotePluginSharePrincipalRole::Reader, | |
| name: "Engineering".to_string(), | |
| }, | |
| ], | |
| discoverability: RemotePluginShareDiscoverability::Unlisted, | |
| } | |
| ); | |
| } | |
| async fn list_remote_plugin_shares_fetches_created_workspace_plugins() { | |
| let codex_home = TempDir::new().unwrap(); | |
| let local_plugin_path = | |
| AbsolutePathBuf::try_from(codex_home.path().join("local-plugin")).unwrap(); | |
| write_plugin_share_local_path_mapping(codex_home.path(), "plugins_123", &local_plugin_path); | |
| let server = MockServer::start().await; | |
| let config = test_config(&server); | |
| let auth = test_auth(); | |
| Mock::given(method("GET")) | |
| .and(path("/backend-api/ps/plugins/workspace/created")) | |
| .and(header("authorization", "Bearer Access Token")) | |
| .and(header("chatgpt-account-id", "account_id")) | |
| .and(query_param( | |
| "limit", | |
| REMOTE_PLUGIN_LIST_PAGE_LIMIT.to_string(), | |
| )) | |
| .and(query_param_is_missing("pageToken")) | |
| .respond_with(ResponseTemplate::new(200).set_body_json(json!({ | |
| "plugins": [remote_plugin_json_with_share_url_and_principals( | |
| "plugins_123", | |
| Some("https://chatgpt.example/plugins/share/share-key-1"), | |
| json!([ | |
| { | |
| "principal_type": "user", | |
| "principal_id": "user-owner", | |
| "role": "owner", | |
| "name": "Owner", | |
| }, | |
| { | |
| "principal_type": "user", | |
| "principal_id": "user-reader", | |
| "role": "reader", | |
| "name": "Reader", | |
| }, | |
| ]), | |
| )], | |
| "pagination": { | |
| "next_page_token": "page-2" | |
| }, | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| Mock::given(method("GET")) | |
| .and(path("/backend-api/ps/plugins/workspace/created")) | |
| .and(header("authorization", "Bearer Access Token")) | |
| .and(header("chatgpt-account-id", "account_id")) | |
| .and(query_param( | |
| "limit", | |
| REMOTE_PLUGIN_LIST_PAGE_LIMIT.to_string(), | |
| )) | |
| .and(query_param("pageToken", "page-2")) | |
| .respond_with(ResponseTemplate::new(200).set_body_json(json!({ | |
| "plugins": [remote_plugin_json_with_share_url_and_principals( | |
| "plugins_456", | |
| /*share_url*/ None, | |
| json!([ | |
| { | |
| "principal_type": "user", | |
| "principal_id": "user-owner", | |
| "role": "owner", | |
| "name": "Owner", | |
| }, | |
| { | |
| "principal_type": "user", | |
| "principal_id": "user-editor", | |
| "role": "editor", | |
| "name": "Editor", | |
| }, | |
| ]), | |
| )], | |
| "pagination": empty_pagination_json(), | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| Mock::given(method("GET")) | |
| .and(path("/backend-api/ps/plugins/installed")) | |
| .and(query_param("scope", "WORKSPACE")) | |
| .respond_with(ResponseTemplate::new(200).set_body_json(json!({ | |
| "plugins": [installed_remote_plugin_json("plugins_456")], | |
| "pagination": empty_pagination_json(), | |
| }))) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| let result = list_remote_plugin_shares(&config, Some(&auth), codex_home.path()) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| result, | |
| vec![ | |
| RemotePluginShareSummary { | |
| summary: RemotePluginSummary { | |
| id: "demo-plugin@workspace-shared-with-me".to_string(), | |
| remote_plugin_id: "plugins_123".to_string(), | |
| version: Some("0.1.0".to_string()), | |
| local_version: None, | |
| name: "demo-plugin".to_string(), | |
| share_context: Some(RemotePluginShareContext { | |
| remote_plugin_id: "plugins_123".to_string(), | |
| remote_version: Some("0.1.0".to_string()), | |
| discoverability: RemotePluginShareDiscoverability::Private, | |
| share_url: Some( | |
| "https://chatgpt.example/plugins/share/share-key-1".to_string(), | |
| ), | |
| creator_account_user_id: None, | |
| creator_name: None, | |
| share_principals: Some(vec![ | |
| RemotePluginSharePrincipal { | |
| principal_type: RemotePluginSharePrincipalType::User, | |
| principal_id: "user-owner".to_string(), | |
| role: RemotePluginSharePrincipalRole::Owner, | |
| name: "Owner".to_string(), | |
| }, | |
| RemotePluginSharePrincipal { | |
| principal_type: RemotePluginSharePrincipalType::User, | |
| principal_id: "user-reader".to_string(), | |
| role: RemotePluginSharePrincipalRole::Reader, | |
| name: "Reader".to_string(), | |
| }, | |
| ]), | |
| can_publish_to_workspace: None, | |
| }), | |
| installed: false, | |
| installed_at: None, | |
| enabled: false, | |
| install_policy: PluginInstallPolicy::Available, | |
| install_policy_source: None, | |
| must_show_installation_interstitial: None, | |
| auth_policy: PluginAuthPolicy::OnUse, | |
| availability: PluginAvailability::Available, | |
| disabled_reason: None, | |
| eligible_plan_types: None, | |
| interface: Some(expected_plugin_interface()), | |
| keywords: Vec::new(), | |
| }, | |
| local_plugin_path: Some(local_plugin_path), | |
| }, | |
| RemotePluginShareSummary { | |
| summary: RemotePluginSummary { | |
| id: "demo-plugin@workspace-shared-with-me".to_string(), | |
| remote_plugin_id: "plugins_456".to_string(), | |
| version: Some("0.1.0".to_string()), | |
| local_version: Some("0.1.0".to_string()), | |
| name: "demo-plugin".to_string(), | |
| share_context: Some(RemotePluginShareContext { | |
| remote_plugin_id: "plugins_456".to_string(), | |
| remote_version: Some("0.1.0".to_string()), | |
| discoverability: RemotePluginShareDiscoverability::Private, | |
| share_url: None, | |
| creator_account_user_id: None, | |
| creator_name: None, | |
| share_principals: Some(vec![ | |
| RemotePluginSharePrincipal { | |
| principal_type: RemotePluginSharePrincipalType::User, | |
| principal_id: "user-owner".to_string(), | |
| role: RemotePluginSharePrincipalRole::Owner, | |
| name: "Owner".to_string(), | |
| }, | |
| RemotePluginSharePrincipal { | |
| principal_type: RemotePluginSharePrincipalType::User, | |
| principal_id: "user-editor".to_string(), | |
| role: RemotePluginSharePrincipalRole::Editor, | |
| name: "Editor".to_string(), | |
| }, | |
| ]), | |
| can_publish_to_workspace: None, | |
| }), | |
| installed: true, | |
| installed_at: None, | |
| enabled: true, | |
| install_policy: PluginInstallPolicy::Available, | |
| install_policy_source: None, | |
| must_show_installation_interstitial: None, | |
| auth_policy: PluginAuthPolicy::OnUse, | |
| availability: PluginAvailability::Available, | |
| disabled_reason: None, | |
| eligible_plan_types: None, | |
| interface: Some(expected_plugin_interface()), | |
| keywords: Vec::new(), | |
| }, | |
| local_plugin_path: None, | |
| } | |
| ] | |
| ); | |
| } | |
| async fn delete_remote_plugin_share_deletes_workspace_plugin() { | |
| let codex_home = TempDir::new().unwrap(); | |
| let local_plugin_path = | |
| AbsolutePathBuf::try_from(codex_home.path().join("local-plugin")).unwrap(); | |
| write_plugin_share_local_path_mapping(codex_home.path(), "plugins_123", &local_plugin_path); | |
| let server = MockServer::start().await; | |
| let config = test_config(&server); | |
| let auth = test_auth(); | |
| Mock::given(method("DELETE")) | |
| .and(path("/backend-api/public/plugins/workspace/plugins_123")) | |
| .and(header("authorization", "Bearer Access Token")) | |
| .and(header("chatgpt-account-id", "account_id")) | |
| .respond_with(ResponseTemplate::new(204)) | |
| .expect(1) | |
| .mount(&server) | |
| .await; | |
| delete_remote_plugin_share(&config, Some(&auth), codex_home.path(), "plugins_123") | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| local_paths::load_plugin_share_local_paths(codex_home.path()).unwrap(), | |
| BTreeMap::new() | |
| ); | |
| } | |