Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down
29 changes: 29 additions & 0 deletions test-data/api-details/README.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions test-data/api-details/test-case-08.json
Original file line number Diff line number Diff line change
@@ -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": {}
}
30 changes: 30 additions & 0 deletions wp_api/src/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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");

Expand All @@ -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<u8> =
Expand Down