From 4faa68cbd1245b4f4b22597d0e9d12248d307e72 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:33:30 -0600 Subject: [PATCH] Add a parse test for the minimal private-site API root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Locks in that `WpApiDetails` reads the stripped-down `/wp-json/` index a private site serves — application-passwords auth only, empty namespaces and routes. Also adds a README cataloguing the `api-details` fixtures. No library behavior change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 1 + test-data/api-details/README.md | 29 ++++++++++++++++++++++++ test-data/api-details/test-case-08.json | 16 +++++++++++++ wp_api/src/login.rs | 30 +++++++++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 test-data/api-details/README.md create mode 100644 test-data/api-details/test-case-08.json diff --git a/CHANGELOG.md b/CHANGELOG.md index eec14956f..7d8ffcae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Internal:** Add a parsing test (`test_parse_private_site_api_root`) for the minimal REST API root a private site advertises — populated `application-passwords` authentication, empty `namespaces`/`routes`, and no `timezone_string`/`site_icon_url`. No library behavior change. - **Internal:** Require pull requests to update `CHANGELOG.md` and warn when code changes do not update `## [Unreleased]`. - **Internal:** Build Rust test binaries with `debug = "line-tables-only"` instead of full debug info, via `[profile.test]` in the workspace `Cargo.toml`. This caps the memory a `cargo test` link consumes on CI, where full debug info was losing Buildkite agents. Backtraces still resolve to file and line; a debugger can no longer print locals. Override with `CARGO_PROFILE_TEST_DEBUG=full`. - **Internal:** Bumped the pinned stable Rust toolchain from `1.97.1` to `1.98.0` in lockstep across local development, CI, and the web image. No new clippy lints surfaced ([#1436](https://github.com/Automattic/wordpress-rs/issues/1436)). diff --git a/test-data/api-details/README.md b/test-data/api-details/README.md new file mode 100644 index 000000000..595044456 --- /dev/null +++ b/test-data/api-details/README.md @@ -0,0 +1,29 @@ +# `api-details` fixtures + +Sample WordPress REST API index responses (`GET /wp-json/`), used to test `WpApiDetails` parsing. + +Each `test-case-NN.json` is loaded by the `test_json` helper in `wp_api/src/login.rs` and fed through `test_api_details_json`, a smoke test asserting the body deserializes into `WpApiDetails`. Some cases also drive targeted tests, noted below. + +These fixtures deliberately cover the shapes WordPress emits in the wild — including the PHP-isms a naive JSON model gets wrong. The recurring ones: + +- **`authentication` and `routes` as `[]` vs `{}`** — PHP's `json_encode([])` renders an empty map as `[]`, not `{}`. An empty `authentication` (or `routes`) therefore arrives as an array. `WpApiDetails.authentication` tolerates both via `deserialize_empty_array_or_hashmap`; `routes` is a strict object. +- **`gmt_offset` as a string or a number** — real sites emit both `"0"` and `0`. +- **`site_icon_url` as `false`, a string, or absent** — handled by `deserialize_false_or_string`. +- **A UTF-8 BOM** prefixing the body — stripped in `WpApiDetails::try_from`. + +## Catalogue + +| File | Size | What it exercises | +| --- | --- | --- | +| `test-case-01.json` | 340 B | UTF-8 **BOM** prefix; `authentication: []` (empty-array form); `site_icon_url: false`; `gmt_offset: -3` (number). Covers BOM stripping, the `false` site-icon case, and empty-auth-as-array. | +| `test-case-02.json` | 906 B | `site_icon_url` field **absent**; `authentication: []`; carries a `_links` block. Covers the missing-optional-field path. | +| `test-case-03.json` | 788 KB | Real self-hosted Jetpack dump (`jetpack.wpmt.co`); `authentication: {application-passwords}`; `gmt_offset: "0"` (**string**); 19 namespaces, 498 routes. Also drives `test_has_namespace`, `test_route_args`, `test_has_route`, and `test_has_route_for_endpoint_with_wp_org_fixture`. | +| `test-case-04.json` | 1.9 MB | Real WordPress.com dump (`Mobile.blog`); `authentication: []` — an **empty array from a real, live site**, proving core itself emits `[]`; 18 namespaces, 1632 routes. | +| `test-case-05.json` | 2 KB | Compact fixture (`Example WordPress Site`); `authentication: {application-passwords}`; `gmt_offset: 13`; 20 namespaces but a single route. | +| `test-case-06.json` | 1.4 MB | Real WordPress.com dump; `authentication: {oauth2}` — the **OAuth2 scheme**; 21 namespaces, 1384 routes. | +| `test-case-07.json` | 344 KB | Real dump (`oauth-testing`); `authentication: {application-passwords, oauth2}` — **both schemes at once**; `gmt_offset: "0"` (string); 5 namespaces, 127 routes. | +| `test-case-08.json` | 391 B | **Minimal API root a private site advertises.** Only `application-passwords` authentication; empty `namespaces` and `routes` (`{}`); `gmt_offset: 0`; no `timezone_string` or `site_icon_url`. Mirrors what a private site emits so a client can still discover the application-password login endpoint. Also drives `test_parse_private_site_api_root`. | + +## Adding a fixture + +Add the `test-case-NN.json` file, a `#[case(...)]` line to `test_api_details_json` in `wp_api/src/login.rs`, and a row here describing what the new case covers. diff --git a/test-data/api-details/test-case-08.json b/test-data/api-details/test-case-08.json new file mode 100644 index 000000000..e946dfc28 --- /dev/null +++ b/test-data/api-details/test-case-08.json @@ -0,0 +1,16 @@ +{ + "name": "Private Site", + "description": "", + "url": "https://example.com", + "home": "https://example.com", + "gmt_offset": 0, + "namespaces": [], + "authentication": { + "application-passwords": { + "endpoints": { + "authorization": "https://example.com/wp-admin/authorize-application.php" + } + } + }, + "routes": {} +} diff --git a/wp_api/src/login.rs b/wp_api/src/login.rs index d567464cd..977afb5ab 100644 --- a/wp_api/src/login.rs +++ b/wp_api/src/login.rs @@ -630,6 +630,8 @@ mod tests { ); } + // WordPress REST API index (`/wp-json/`) fixtures; each must deserialize into + // `WpApiDetails`. See `test-data/api-details/README.md` for what each covers. #[rstest] #[case("api-details/test-case-01.json")] #[case("api-details/test-case-02.json")] @@ -638,6 +640,7 @@ mod tests { #[case("api-details/test-case-05.json")] #[case("api-details/test-case-06.json")] #[case("api-details/test-case-07.json")] + #[case("api-details/test-case-08.json")] // minimal private-site root fn test_api_details_json(#[case] input: &str) { let json = test_json(input).expect("Failed to read test resource"); @@ -649,6 +652,33 @@ mod tests { ); } + // Validates that the minimal API root a private site emits parses correctly: + // empty `namespaces` and `routes`, no `timezone_string`/`site_icon_url`, and + // only the application-passwords authentication endpoint advertised. + #[test] + fn test_parse_private_site_api_root() { + let json = + test_json("api-details/test-case-08.json").expect("Failed to read test resource"); + let result = WpApiDetails::try_from(json.as_slice()); + + let api_details = + result.unwrap_or_else(|e| panic!("Failed to parse json as `WpApiDetails`: {e:#?}")); + + assert_eq!(api_details.name, "Private Site"); + assert_eq!(api_details.description, ""); + assert_eq!(api_details.url, "https://example.com"); + assert_eq!(api_details.home, "https://example.com"); + assert_eq!(api_details.gmt_offset, Some(0.0)); + assert!(api_details.namespaces.is_empty()); + assert!(api_details.routes.is_empty()); + + assert!(api_details.has_application_passwords_authentication_url()); + assert_eq!( + api_details.find_application_passwords_authentication_url(), + Some("https://example.com/wp-admin/authorize-application.php".to_string()) + ); + } + #[test] fn test_has_namespace() { let json: Vec =