diff --git a/.buildkite/integration-tests.yml b/.buildkite/integration-tests.yml new file mode 100644 index 000000000..3f103b3f1 --- /dev/null +++ b/.buildkite/integration-tests.yml @@ -0,0 +1,16 @@ +steps: + - label: ":wordpress: :rust: WordPress.org API" + command: | + echo "--- :wordpress: Preparing" + ./wordpress_org_api_integration_tests/replace-test-parameters.sh + echo "--- :rust: Testing" + make test-rust-integration-wordpress-org-api + agents: + queue: tumblr-metal + +notify: + - slack: + channels: + - "#wordpress-rs" + message: "wordpress-org-api-integration-tests failed." + if: build.state == "failed" diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index ce8dfbb4a..1c8a87c4c 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -201,11 +201,6 @@ steps: WORDPRESS_VERSION: "{{matrix}}" matrix: *wordpress_version_matrix - - label: ":wordpress: :rust: WordPress.org API" - command: | - echo "--- :rust: Testing" - make test-rust-integration-wordpress-org-api - - label: ":wordpress: :kotlin: WordPress {{matrix}}" command: ".buildkite/commands/run-kotlin-integration-tests.sh" env: diff --git a/Cargo.lock b/Cargo.lock index f09531ecf..b0ce4b15b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3198,8 +3198,12 @@ dependencies = [ name = "wordpress_org_api_integration_tests" version = "0.1.0" dependencies = [ + "reqwest", "rstest", + "rstest_reuse", + "serde", "serde_json", + "tokio", "wordpress_org_api", ] diff --git a/Makefile b/Makefile index bfc6378c3..e2c7677f2 100644 --- a/Makefile +++ b/Makefile @@ -204,7 +204,6 @@ test-rust-integration: docker exec -i wordpress /bin/bash < ./scripts/run-rust-integration-tests.sh test-rust-integration-wordpress-org-api: - @test -d target/wordpress-org-plugin-directory || ./scripts/plugin-directory.sh download_from_s3 $(rust_docker_run) cargo test --package wordpress_org_api_integration_tests test-kotlin-integration: diff --git a/scripts/plugin-directory.sh b/scripts/plugin-directory.sh deleted file mode 100755 index bbd8c3232..000000000 --- a/scripts/plugin-directory.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -OUTPUT_DIR="target/wordpress-org-plugin-directory" - -fetch_plugin_data() { - slug=$1 - curl -s --output "$OUTPUT_DIR/$slug.json" "$API_URL?action=plugin_information&fields=icons%2Cbanners&slug=$slug" -} - -download_from_wp_org() { - echo "Downloading plugin data from WordPress.org." - echo "(Run this script within sandbox to get a much faster download speed.)" - - rm -rf "$OUTPUT_DIR" && mkdir -p "$OUTPUT_DIR" - - API_URL="https://api.wordpress.org/plugins/info/1.2/" - PARALLEL_JOBS=10 - - current_page=1 - - echo "Fetching the total number of pages..." - response=$(curl -s "$API_URL?action=query_plugins&request%5Bpage%5D=$current_page&request%5Bper_page%5D=100") - total_pages=$(echo "$response" | jq -r '.info.pages') - if [[ -z "$total_pages" || "$total_pages" -eq 0 ]]; then - echo "Failed to fetch pagination info or no pages available." - exit 1 - fi - - echo "Total pages to process: $total_pages" - - export -f fetch_plugin_data - export OUTPUT_DIR - export API_URL - - while [[ $current_page -le $total_pages ]]; do - echo "[$(date)] Processing page: $current_page" - - response=$(curl -s "$API_URL?action=query_plugins&request%5Bpage%5D=$current_page&request%5Bper_page%5D=100") - plugin_slugs=$(echo "$response" | jq -r '.plugins[].slug') - - echo "$plugin_slugs" | xargs -n 1 -P "$PARALLEL_JOBS" bash -c 'fetch_plugin_data "$@"' _ - - current_page=$((current_page + 1)) - done - - echo "All plugin data has been saved to the directory: $OUTPUT_DIR" -} - -S3_URI="s3://a8c-ci-cache/wordpress-rs-wordpress-org-plugin-directory.tar.gz" -S3_LOCAL_CACHE="target/wordpress-org-plugin-directory.tar.gz" - -upload_to_s3() { - echo "Uploading $(find "$OUTPUT_DIR" -type f | wc -l) plugins in $OUTPUT_DIR to S3..." - echo "Compressing ..." - tar -czf "$S3_LOCAL_CACHE" -C "$OUTPUT_DIR" . - aws s3 cp "$S3_LOCAL_CACHE" "$S3_URI" - rm "$S3_LOCAL_CACHE" -} - -download_from_s3() { - echo "Downloading plugin data from S3 cache..." - rm -rf "$OUTPUT_DIR" && mkdir -p "$OUTPUT_DIR" - aws s3 cp "$S3_URI" "$S3_LOCAL_CACHE" - echo "Unzip to $OUTPUT_DIR" - tar -xzf "$S3_LOCAL_CACHE" -C "$OUTPUT_DIR" - rm "$S3_LOCAL_CACHE" -} - -${1:-download_from_wp_org} diff --git a/wordpress_org_api/src/plugin_directory.rs b/wordpress_org_api/src/plugin_directory.rs index b11eed2a9..4b0912dc7 100644 --- a/wordpress_org_api/src/plugin_directory.rs +++ b/wordpress_org_api/src/plugin_directory.rs @@ -15,6 +15,7 @@ pub struct PluginInformation { #[serde(deserialize_with = "deserialize_default_values")] pub author_profile: String, #[serde(deserialize_with = "deserialize_default_values")] + #[serde(default)] pub contributors: HashMap, #[serde(deserialize_with = "deserialize_default_values")] pub requires: String, @@ -26,6 +27,7 @@ pub struct PluginInformation { pub rating: u32, pub ratings: Ratings, pub num_ratings: u32, + #[serde(default)] pub support_url: String, pub support_threads: u32, pub support_threads_resolved: u32, @@ -33,23 +35,32 @@ pub struct PluginInformation { pub last_updated: String, pub added: String, pub homepage: String, + #[serde(default)] pub sections: HashMap, pub download_link: String, #[serde(deserialize_with = "deserialize_default_values")] + #[serde(default)] pub upgrade_notice: HashMap, + #[serde(default)] pub screenshots: Screenshots, #[serde(deserialize_with = "deserialize_default_values")] pub tags: HashMap, #[serde(deserialize_with = "deserialize_default_values")] + #[serde(default)] pub versions: HashMap, #[serde(deserialize_with = "deserialize_default_values")] + #[serde(default)] pub business_model: String, + #[serde(default)] pub repository_url: String, + #[serde(default)] pub commercial_support_url: String, pub donate_link: String, #[serde(deserialize_with = "deserialize_default_values")] + #[serde(default)] pub banners: Banners, pub icons: Option, + #[serde(default)] pub preview_link: String, } @@ -82,6 +93,12 @@ pub enum Screenshots { List(Vec), } +impl Default for Screenshots { + fn default() -> Self { + Screenshots::List(vec![]) + } +} + #[derive(Deserialize, Debug)] pub struct Screenshot { pub src: String, @@ -105,3 +122,16 @@ pub struct Icons { pub svg: Option, pub default: Option, } + +#[derive(Deserialize, Debug)] +pub struct QueryPluginResponse { + pub info: QueryPluginResponseInfo, + pub plugins: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct QueryPluginResponseInfo { + pub page: i64, + pub pages: i64, + pub results: i64, +} diff --git a/wordpress_org_api_integration_tests/Cargo.toml b/wordpress_org_api_integration_tests/Cargo.toml index 103ec1266..f2a0d45b3 100644 --- a/wordpress_org_api_integration_tests/Cargo.toml +++ b/wordpress_org_api_integration_tests/Cargo.toml @@ -8,5 +8,9 @@ publish = false wordpress_org_api = { path = "../wordpress_org_api" } [dev-dependencies] +reqwest = { workspace = true, features = [ "json" ] } rstest = { workspace = true } +rstest_reuse = { workspace = true } +serde = { workspace = true, features = [ "derive" ] } serde_json = { workspace = true } +tokio = { workspace = true, features = [ "full" ] } diff --git a/wordpress_org_api_integration_tests/replace-test-parameters.sh b/wordpress_org_api_integration_tests/replace-test-parameters.sh new file mode 100755 index 000000000..73d079213 --- /dev/null +++ b/wordpress_org_api_integration_tests/replace-test-parameters.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +set -euo pipefail + +save_plugin_slug() { + url=$1 + curl -s "$url" | jq -r '.plugins[].slug' | xargs -I {} touch "$SLUG_DIR/{}" +} + +OUTPUT_FILE="wordpress_org_api_integration_tests/tests/plugin_directory_parameters.rs" + +echo "Prepare integration tests parameters" + +API_URL="https://api.wordpress.org/plugins/info/1.2/" +PAGE_SIZE=200 +PARALLEL_JOBS=20 + +current_page=1 +response=$(curl -s "$API_URL?action=query_plugins&request%5Bpage%5D=$current_page&request%5Bper_page%5D=$PAGE_SIZE") +total_pages=$(echo "$response" | jq -r '.info.pages') +if [[ -z "$total_pages" || "$total_pages" -eq 0 ]]; then + echo "Failed to fetch pagination info or no pages available." + exit 1 +fi +echo "Total pages to process: $total_pages" + +query_plugins_api_urls=() +current_page=1 +while [[ $current_page -le $total_pages ]]; do + query_plugins_api_urls+=("$API_URL?action=query_plugins&request%5Bpage%5D=$current_page&request%5Bper_page%5D=$PAGE_SIZE") + current_page=$((current_page + 1)) +done + +export -f save_plugin_slug +export API_URL +export SLUG_DIR="target/wordpress-org-plugin-directory/slugs" +echo "Saving plugin slugs to $SLUG_DIR" +rm -rf "$SLUG_DIR" && mkdir -p "$SLUG_DIR" +echo "${query_plugins_api_urls[@]}" | xargs -n 1 -P "$PARALLEL_JOBS" bash -c 'save_plugin_slug "$@"' _ + +slugs=() +while IFS='' read -r line; do slugs+=("$line"); done < <(ls -1 "$SLUG_DIR") + +{ + echo '#![allow(unused_macros)]' + echo '' + echo '#[rstest_reuse::template]' + echo '#[rstest::rstest]' + for url in "${query_plugins_api_urls[@]}"; do + echo "#[case(\"$url\")]" + done + echo 'pub fn query_plugins_api_url(#[case] url: &str) {}' + echo '' + + midpoint=$((${#slugs[@]} / 2)) + first_half=("${slugs[@]:0:midpoint}") + second_half=("${slugs[@]:midpoint}") + + echo '#[rstest_reuse::template]' + echo '#[rstest::rstest]' + for slug in "${first_half[@]}"; do + echo "#[case(\"$slug\")]" + done + echo 'pub fn plugin_information_slug_1(#[case] slug: &str) {}' + echo '' + + echo '#[rstest_reuse::template]' + echo '#[rstest::rstest]' + for slug in "${second_half[@]}"; do + echo "#[case(\"$slug\")]" + done + echo 'pub fn plugin_information_slug_2(#[case] slug: &str) {}' + echo '' +} > "$OUTPUT_FILE" diff --git a/wordpress_org_api_integration_tests/tests/plugin_directory_parameters.rs b/wordpress_org_api_integration_tests/tests/plugin_directory_parameters.rs new file mode 100644 index 000000000..bf710dea3 --- /dev/null +++ b/wordpress_org_api_integration_tests/tests/plugin_directory_parameters.rs @@ -0,0 +1,26 @@ +#![allow(unused_macros)] + +// Run replace-test-parameters.sh to replace the parameters defined in this file +// with the full plugin directory on wordpress.org. +// +// Please note: build time will increase significantly after running the script. +// +// IMPORTANT: DO NOT COMMIT THE CHANGES TO THIS FILE AFTER RUNNING THE SCRIPT. + +#[rstest_reuse::template] +#[rstest::rstest] +#[case("https://api.wordpress.org/plugins/info/1.2/?action=query_plugins&request%5Bpage%5D=1&request%5Bper_page%5D=200")] +#[case("https://api.wordpress.org/plugins/info/1.2/?action=query_plugins&request%5Bpage%5D=2&request%5Bper_page%5D=200")] +#[case("https://api.wordpress.org/plugins/info/1.2/?action=query_plugins&request%5Bpage%5D=273&request%5Bper_page%5D=200")] +#[case("https://api.wordpress.org/plugins/info/1.2/?action=query_plugins&request%5Bpage%5D=274&request%5Bper_page%5D=200")] +pub fn query_plugins_api_url(#[case] url: &str) {} + +#[rstest_reuse::template] +#[rstest::rstest] +#[case("jetpack")] +pub fn plugin_information_slug_1(#[case] slug: &str) {} + +#[rstest_reuse::template] +#[rstest::rstest] +#[case("woocommerce")] +pub fn plugin_information_slug_2(#[case] slug: &str) {} diff --git a/wordpress_org_api_integration_tests/tests/test_plugin_directory.rs b/wordpress_org_api_integration_tests/tests/test_plugin_directory.rs index 0037efc9d..335793121 100644 --- a/wordpress_org_api_integration_tests/tests/test_plugin_directory.rs +++ b/wordpress_org_api_integration_tests/tests/test_plugin_directory.rs @@ -1,246 +1,57 @@ -use rstest::*; -use std::collections::HashMap; -use std::path::PathBuf; +use rstest_reuse::*; use wordpress_org_api::plugin_directory::*; -#[fixture] -fn plugins_dir() -> PathBuf { - std::fs::canonicalize("../target/wordpress-org-plugin-directory").unwrap() -} - -#[fixture] -fn plugin_info_files(plugins_dir: PathBuf) -> Vec { - println!( - "Reading plugin information files from {:?}...", - &plugins_dir - ); - - let mut files = vec![]; - for entry in std::fs::read_dir(plugins_dir).unwrap() { - let entry = entry.unwrap(); - if entry.file_type().unwrap().is_file() - && entry.path().extension().and_then(|f| f.to_str()) == Some("json") - { - files.push(entry.path()); - } - } - files -} +mod plugin_directory_parameters; +use plugin_directory_parameters::*; -fn parse_plugin(slug: &str) -> PluginInformation { - let file = plugins_dir().join(format!("{}.json", slug)); - let content = std::fs::read_to_string(file).unwrap(); - let result = serde_json::from_str::(&content); +#[apply(query_plugins_api_url)] +#[tokio::test] +async fn test_parse_query_plugins_api(#[case] url: &str) { + let client = reqwest::Client::new(); + let response = client.get(url).send().await.unwrap(); + let result = response.json::().await; assert!( result.is_ok(), - "Failed to parse plugin {:?}: {:?}", - slug, + "Failed to parse plugin query {:?}: {:?}", + url, result.err() ); - result.unwrap() + let result = result.unwrap(); + assert!(result.plugins.len() > 0); } -#[rstest] -fn parse_plugin_info(plugin_info_files: Vec) { - let results: HashMap<&PathBuf, _> = - plugin_info_files - .iter() - .fold(HashMap::new(), |mut results, file| { - let info = std::fs::read_to_string(file).unwrap(); - let result = serde_json::from_str::(&info); - results.insert(file, result); - results - }); - - let success: Vec<_> = results - .iter() - .filter(|(_, result)| result.is_ok()) - .collect(); - - let failed: Vec<_> = results - .iter() - .filter(|(_, result)| result.is_err()) - .map(|(file, _)| file) - .collect(); - if !failed.is_empty() { - println!("Failed to parse the following files:"); - for file in &failed { - println!("- {:?}", file.file_name().unwrap()); - } - } - - assert_eq!( - success.len(), - results.len(), - "{} out of {} files parsed successfully", - success.len(), - results.len() - ); - assert!( - failed.is_empty(), - "Failed to parse {} out of {} files", - failed.len(), - results.len() +async fn test_parse_plugin_information_api(slug: &str) { + let url = format!( + "https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request[slug]={}&fields=icons", + slug ); -} + println!("Plugin information API URL: {}", url); -#[rstest] -#[case("jetpack", "https://profiles.wordpress.org/automattic/")] -#[case("appmysite", "https://profiles.wordpress.org/appmysite/")] -#[case("superlinks", "")] -fn test_property_author_profile(#[case] slug: &str, #[case] value: &str) { - let result = parse_plugin(slug); - assert_eq!(result.author_profile, value); -} + let client = reqwest::Client::new(); + let response = client.get(url).send().await.unwrap(); + let result = response.json::().await; -#[rstest] -#[case("jetpack", "automattic", "https://profiles.wordpress.org/automattic/")] -#[case("appmysite", "appmysite", "https://profiles.wordpress.org/appmysite/")] -fn test_property_contributors_profile( - #[case] slug: &str, - #[case] username: &str, - #[case] profile: &str, -) { - let result = parse_plugin(slug); - let contributors = result.contributors; - assert_eq!( - contributors.get(username).map(|f| f.profile.as_str()), - Some(profile) + assert!( + result.is_ok(), + "Failed to parse plugin information {:?}: {:?}", + slug, + result.err() ); -} - -#[rstest] -#[case("superlinks")] -fn test_property_no_contributors(#[case] slug: &str) { - let result = parse_plugin(slug); - assert!(result.contributors.is_empty()); -} -#[rstest] -#[case("timeline-express-no-icons-add-on", "")] -#[case("appmysite", "6.4")] -#[case("superlinks", "2.5")] -fn test_property_requires(#[case] slug: &str, #[case] value: &str) { - let result = parse_plugin(slug); - assert_eq!(result.requires, value); + let result = result.unwrap(); + assert_eq!(result.slug, slug); } -#[rstest] -#[case("jetpack", "6.7.1")] -#[case("add-rss", "")] -fn test_property_tested(#[case] slug: &str, #[case] value: &str) { - let result = parse_plugin(slug); - assert_eq!(result.tested, value); +#[apply(plugin_information_slug_1)] +#[tokio::test] +async fn test_parse_plugin_information_api_1(#[case] slug: &str) { + test_parse_plugin_information_api(slug).await; } -#[rstest] -#[case("about-author", "")] -#[case("accessibility-toolbar", "7.4")] -fn test_property_requires_php(#[case] slug: &str, #[case] value: &str) { - let result = parse_plugin(slug); - assert_eq!(result.requires_php, value); -} - -#[rstest] -#[case("accordion-archive-widget", "")] -#[case("abc-pricing-table", "commercial")] -fn test_property_business_model(#[case] slug: &str, #[case] value: &str) { - let result = parse_plugin(slug); - assert_eq!(result.business_model, value); -} - -#[rstest] -fn test_property_empty_upgrade_notice(#[values("1-click-close-store", "2em")] slug: &str) { - let result = parse_plugin(slug); - assert!(result.upgrade_notice.is_empty()); -} - -#[rstest] -fn test_property_nonempty_upgrade_notice( - #[values("ab-wp-security", "absolute-addons")] slug: &str, -) { - let result = parse_plugin(slug); - assert!(!result.upgrade_notice.is_empty()); -} - -#[rstest] -fn test_property_empty_tags(#[values("acf-rest", "add-rss")] slug: &str) { - let result = parse_plugin(slug); - assert!(result.tags.is_empty()); -} - -#[rstest] -fn test_property_nonempty_tags(#[values("appbanners", "seo-assistant")] slug: &str) { - let result = parse_plugin(slug); - assert!(!result.tags.is_empty()); -} - -#[rstest] -fn test_property_empty_versions(#[values("mos-faqs", "adjustly-collapse")] slug: &str) { - let result = parse_plugin(slug); - assert!(result.versions.is_empty()); -} - -#[rstest] -fn test_property_nonempty_versions(#[values("abcsubmit", "acf-views")] slug: &str) { - let result = parse_plugin(slug); - assert!(!result.versions.is_empty()); -} - -#[rstest] -#[case( - "appmysite", - "https://ps.w.org/appmysite/assets/banner-772x250.png?rev=2829272", - "https://ps.w.org/appmysite/assets/banner-1544x500.png?rev=2829272" -)] -#[case( - "jetpack", - "https://ps.w.org/jetpack/assets/banner-772x250.png?rev=2653649", - "https://ps.w.org/jetpack/assets/banner-1544x500.png?rev=2653649" -)] -#[case( - "1-click-migration", - "https://ps.w.org/1-click-migration/assets/banner-772x250.png?rev=2333853", - "" -)] -fn test_property_banners(#[case] slug: &str, #[case] low: String, #[case] high: String) { - let result = parse_plugin(slug); - let expected = Banners { low, high }; - let banners = result.banners; - assert_eq!(banners, expected); -} - -#[rstest] -#[case( - "contact-form-7", - Some("https://ps.w.org/contact-form-7/assets/icon.svg?rev=2339255"), - None, - Some("https://ps.w.org/contact-form-7/assets/icon.svg?rev=2339255"), - None -)] -#[case( - "adminimize", - None, - None, - None, - Some("https://s.w.org/plugins/geopattern-icon/adminimize_000000.svg") -)] -fn test_property_icons( - #[case] slug: &str, - #[case] low: Option<&str>, - #[case] high: Option<&str>, - #[case] svg: Option<&str>, - #[case] default: Option<&str>, -) { - let result = parse_plugin(slug); - let icons = result.icons; - assert!(icons.is_some()); - - let icons = icons.unwrap(); - assert_eq!(icons.low.as_deref(), low); - assert_eq!(icons.high.as_deref(), high); - assert_eq!(icons.svg.as_deref(), svg); - assert_eq!(icons.default.as_deref(), default); +#[apply(plugin_information_slug_2)] +#[tokio::test] +async fn test_parse_plugin_information_api_2(#[case] slug: &str) { + test_parse_plugin_information_api(slug).await; }