Skip to content

Commit 0ed486f

Browse files
committed
Verify plain-permalinks endpoint URLs against server-sourced oracles
Back the plain_permalinks_url_tests golden table with two independent checks so its hard-coded values can't silently encode a resolver bug: - index_self_href_url_tests: for the 22 endpoints WordPress advertises as non-parameterized routes, assert our resolver reproduces the server's own published URL (the _links.self.href captured in the real-site index fixture test-data/api-details/test-case-03.json). Independent full-URL oracle, no server needed. - test_login_plain_permalinks_mut: add a second serial test that fetches a real object through a parameterized route (/wp/v2/users/<id>) over ?rest_route= — the case the index publishes no URL for — proving ID-bearing endpoints round-trip end-to-end on a plain-permalink site. Factored the discover-and-build-admin-client setup into a shared helper.
1 parent dd42ef1 commit 0ed486f

4 files changed

Lines changed: 364 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3939
- **Internal:** Documented how to pick an xcframework build for local work, and added `make help` descriptions for the `xcframework-only-*` targets. Verifying a UniFFI change needs only `make xcframework-only-macos`, not the full 11-target `make xcframework`.
4040
- **Internal:** Fixed `make xcframework-only-<platform>` building the per-target libraries but never assembling them into the xcframework. The `@# Help:` comments added for those `make help` descriptions became each rule's recipe and silently shadowed the shared `xcframework-only-%` pattern rule that ran the assemble step; each rule now runs the assemble step directly.
4141
- **Internal:** Update translations.
42-
- **Internal:** Added a golden URL table (`plain_permalinks_url_tests`) asserting all 45 self-hosted endpoints resolve to the correct `?rest_route=` URL on a plain-permalinks site, by driving each endpoint's real URL builder through a `rest_route`-seeded `WpOrgSiteApiUrlResolver`. Complements the per-endpoint `wp-json` golden assertions and the `test_login_plain_permalinks_mut` end-to-end test.
42+
- **Internal:** Added a golden URL table (`plain_permalinks_url_tests`) asserting all 45 self-hosted endpoints resolve to the correct `?rest_route=` URL on a plain-permalinks site, by driving each endpoint's real URL builder through a `rest_route`-seeded `WpOrgSiteApiUrlResolver`. To guard the table's accuracy against an independent source, the 22 non-parameterized routes are also checked (`index_self_href_url_tests`) against WordPress's own published URL — the `_links.self.href` captured in a real-site REST index fixture — and the parameterized (ID-bearing) routes, which the index never publishes a URL for, are exercised end-to-end by a new plain-permalinks integration test that fetches a real object by numeric id over `?rest_route=`.
4343

4444
### Fixed
4545

wp_api/src/request/endpoint.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub mod widgets_endpoint;
4949
pub mod wp_block_editor_endpoint;
5050
pub mod wp_site_health_tests_endpoint;
5151

52+
#[cfg(test)]
53+
mod index_self_href_url_tests;
5254
#[cfg(test)]
5355
mod plain_permalinks_url_tests;
5456

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
//! Full-URL oracle: for every self-hosted endpoint that WordPress advertises as
2+
//! a non-parameterized route, assert our resolver builds *exactly* the URL the
3+
//! server publishes in the REST index's `_links.self.href`.
4+
//!
5+
//! Each expected URL is read at run time from the committed real-site index
6+
//! fixture `test-data/api-details/test-case-03.json` (a self-hosted `wp-json`
7+
//! site) — WordPress's own `rest_url()` output, looked up by route with
8+
//! [`published_self_href`]. Nothing here is hand-transcribed: the only literal
9+
//! each test carries is the route key it looks up, and a mistyped key fails
10+
//! loudly because the fixture won't contain it. The fixture — i.e. the server —
11+
//! is the source of truth, not a golden copied into this file.
12+
//!
13+
//! The REST index only emits a concrete `self` href for routes without path
14+
//! parameters, so this covers the 22 collection/singleton endpoints; the
15+
//! ID-bearing ones are covered end-to-end by the plain-permalinks integration
16+
//! tests instead.
17+
//!
18+
//! The query string our representative call appends (`?context=edit`, etc.) is
19+
//! stripped before comparison, since the advertised `self` href is the bare
20+
//! route URL.
21+
22+
use super::{ApiUrlResolver, WpOrgSiteApiUrlResolver};
23+
use crate::parsed_url::ParsedUrl;
24+
use serde_json::Value;
25+
use std::path::PathBuf;
26+
use std::sync::Arc;
27+
28+
const API_ROOT: &str = "https://jetpack.wpmt.co/wp-json";
29+
30+
fn resolver() -> Arc<dyn ApiUrlResolver> {
31+
Arc::new(WpOrgSiteApiUrlResolver::new(
32+
ParsedUrl::parse(API_ROOT).expect("valid url").into(),
33+
))
34+
}
35+
36+
fn strip_query(url: &str) -> &str {
37+
url.split('?').next().unwrap_or(url)
38+
}
39+
40+
/// The exact URL WordPress published for `route` in the committed REST index
41+
/// fixture (`test-data/api-details/test-case-03.json`) — its own `rest_url()`
42+
/// output, read from that route's `_links.self[0].href`.
43+
///
44+
/// Panics if the fixture has no such route, so a mistyped route key fails the
45+
/// test loudly instead of silently asserting against a value invented here.
46+
fn published_self_href(route: &str) -> String {
47+
let mut path = PathBuf::from(env!("CARGO_WORKSPACE_DIR"));
48+
path.push("test-data");
49+
path.push("api-details");
50+
path.push("test-case-03.json");
51+
let json = std::fs::read_to_string(&path)
52+
.unwrap_or_else(|e| panic!("failed to read REST index fixture {}: {e}", path.display()));
53+
let index: Value = serde_json::from_str(&json).expect("REST index fixture is valid JSON");
54+
index["routes"][route]["_links"]["self"][0]["href"]
55+
.as_str()
56+
.unwrap_or_else(|| panic!("fixture publishes no `_links.self[0].href` for route `{route}`"))
57+
.to_string()
58+
}
59+
60+
#[test]
61+
fn block_directory() {
62+
let endpoint = super::block_directory_endpoint::BlockDirectoryRequestEndpoint::new(resolver());
63+
let built = endpoint
64+
.search(&crate::block_directory::BlockDirectorySearchParams::new(
65+
"coblocks".to_string(),
66+
))
67+
.as_str()
68+
.to_string();
69+
assert_eq!(
70+
strip_query(&built),
71+
published_self_href("/wp/v2/block-directory/search")
72+
);
73+
}
74+
75+
#[test]
76+
fn block_pattern_categories() {
77+
let endpoint =
78+
super::block_pattern_categories_endpoint::BlockPatternCategoriesRequestEndpoint::new(
79+
resolver(),
80+
);
81+
let built = endpoint.list_with_edit_context().as_str().to_string();
82+
assert_eq!(
83+
strip_query(&built),
84+
published_self_href("/wp/v2/block-patterns/categories")
85+
);
86+
}
87+
88+
#[test]
89+
fn block_patterns() {
90+
let endpoint = super::block_patterns_endpoint::BlockPatternsRequestEndpoint::new(resolver());
91+
let built = endpoint.list_with_edit_context().as_str().to_string();
92+
assert_eq!(
93+
strip_query(&built),
94+
published_self_href("/wp/v2/block-patterns/patterns")
95+
);
96+
}
97+
98+
#[test]
99+
fn block_types() {
100+
let endpoint = super::block_types_endpoint::BlockTypesRequestEndpoint::new(resolver());
101+
let built = endpoint.list_with_edit_context().as_str().to_string();
102+
assert_eq!(
103+
strip_query(&built),
104+
published_self_href("/wp/v2/block-types")
105+
);
106+
}
107+
108+
#[test]
109+
fn blocks() {
110+
let endpoint = super::blocks_endpoint::BlocksRequestEndpoint::new(resolver());
111+
let built = endpoint
112+
.list_with_edit_context(&crate::blocks::BlockListParams::default())
113+
.as_str()
114+
.to_string();
115+
assert_eq!(strip_query(&built), published_self_href("/wp/v2/blocks"));
116+
}
117+
118+
#[test]
119+
fn comments() {
120+
let endpoint = super::comments_endpoint::CommentsRequestEndpoint::new(resolver());
121+
let built = endpoint
122+
.list_with_edit_context(&crate::comments::CommentListParams::default())
123+
.as_str()
124+
.to_string();
125+
assert_eq!(strip_query(&built), published_self_href("/wp/v2/comments"));
126+
}
127+
128+
#[test]
129+
fn media() {
130+
let endpoint = super::media_endpoint::MediaRequestEndpoint::new(resolver());
131+
let built = endpoint
132+
.list_with_edit_context(&crate::media::MediaListParams::default())
133+
.as_str()
134+
.to_string();
135+
assert_eq!(strip_query(&built), published_self_href("/wp/v2/media"));
136+
}
137+
138+
#[test]
139+
fn menu_locations() {
140+
let endpoint = super::menu_locations_endpoint::MenuLocationsRequestEndpoint::new(resolver());
141+
let built = endpoint.list_with_edit_context().as_str().to_string();
142+
assert_eq!(
143+
strip_query(&built),
144+
published_self_href("/wp/v2/menu-locations")
145+
);
146+
}
147+
148+
#[test]
149+
fn navigations() {
150+
let endpoint = super::navigations_endpoint::NavigationsRequestEndpoint::new(resolver());
151+
let built = endpoint
152+
.list_with_edit_context(&crate::navigations::NavigationListParams::default())
153+
.as_str()
154+
.to_string();
155+
assert_eq!(
156+
strip_query(&built),
157+
published_self_href("/wp/v2/navigation")
158+
);
159+
}
160+
161+
#[test]
162+
fn pattern_directory() {
163+
let endpoint =
164+
super::pattern_directory_endpoint::PatternDirectoryRequestEndpoint::new(resolver());
165+
let params = crate::pattern_directory::PatternDirectoryListParams {
166+
per_page: Some(10),
167+
category: Some(crate::pattern_directory::PatternDirectoryCategoryId(5)),
168+
..Default::default()
169+
};
170+
let built = endpoint
171+
.list_with_view_context(&params)
172+
.as_str()
173+
.to_string();
174+
assert_eq!(
175+
strip_query(&built),
176+
published_self_href("/wp/v2/pattern-directory/patterns")
177+
);
178+
}
179+
180+
#[test]
181+
fn post_statuses() {
182+
let endpoint = super::post_statuses_endpoint::PostStatusesRequestEndpoint::new(resolver());
183+
let built = endpoint.list_with_edit_context().as_str().to_string();
184+
assert_eq!(strip_query(&built), published_self_href("/wp/v2/statuses"));
185+
}
186+
187+
#[test]
188+
fn post_types() {
189+
let endpoint = super::post_types_endpoint::PostTypesRequestEndpoint::new(resolver());
190+
let built = endpoint.list_with_edit_context().as_str().to_string();
191+
assert_eq!(strip_query(&built), published_self_href("/wp/v2/types"));
192+
}
193+
194+
#[test]
195+
fn posts() {
196+
let endpoint = super::posts_endpoint::PostsRequestEndpoint::new(resolver());
197+
let built = endpoint
198+
.list_with_edit_context(
199+
&crate::request::endpoint::posts_endpoint::PostEndpointType::Posts,
200+
&crate::posts::PostListParams::default(),
201+
)
202+
.as_str()
203+
.to_string();
204+
assert_eq!(strip_query(&built), published_self_href("/wp/v2/posts"));
205+
}
206+
207+
#[test]
208+
fn search() {
209+
let endpoint = super::search_endpoint::SearchRequestEndpoint::new(resolver());
210+
let built = endpoint
211+
.list_with_embed_context(&crate::search_results::SearchListParams::default())
212+
.as_str()
213+
.to_string();
214+
assert_eq!(strip_query(&built), published_self_href("/wp/v2/search"));
215+
}
216+
217+
#[test]
218+
fn sidebars() {
219+
let endpoint = super::sidebars_endpoint::SidebarsRequestEndpoint::new(resolver());
220+
let built = endpoint.list_with_edit_context().as_str().to_string();
221+
assert_eq!(strip_query(&built), published_self_href("/wp/v2/sidebars"));
222+
}
223+
224+
#[test]
225+
fn site_settings() {
226+
let endpoint = super::site_settings_endpoint::SiteSettingsRequestEndpoint::new(resolver());
227+
let built = endpoint.retrieve_with_edit_context().as_str().to_string();
228+
assert_eq!(strip_query(&built), published_self_href("/wp/v2/settings"));
229+
}
230+
231+
#[test]
232+
fn taxonomies() {
233+
let endpoint = super::taxonomies_endpoint::TaxonomiesRequestEndpoint::new(resolver());
234+
let built = endpoint
235+
.list_with_edit_context(&crate::taxonomies::TaxonomyListParams::default())
236+
.as_str()
237+
.to_string();
238+
assert_eq!(
239+
strip_query(&built),
240+
published_self_href("/wp/v2/taxonomies")
241+
);
242+
}
243+
244+
#[test]
245+
fn users() {
246+
let endpoint = super::users_endpoint::UsersRequestEndpoint::new(resolver());
247+
let built = endpoint
248+
.list_with_edit_context(&crate::UserListParams::default())
249+
.as_str()
250+
.to_string();
251+
assert_eq!(strip_query(&built), published_self_href("/wp/v2/users"));
252+
}
253+
254+
#[test]
255+
fn widget_types() {
256+
let endpoint = super::widget_types_endpoint::WidgetTypesRequestEndpoint::new(resolver());
257+
let built = endpoint.list_with_edit_context().as_str().to_string();
258+
assert_eq!(
259+
strip_query(&built),
260+
published_self_href("/wp/v2/widget-types")
261+
);
262+
}
263+
264+
#[test]
265+
fn widgets() {
266+
let endpoint = super::widgets_endpoint::WidgetsRequestEndpoint::new(resolver());
267+
let built = endpoint
268+
.list_with_edit_context(&crate::widgets::WidgetListParams::default())
269+
.as_str()
270+
.to_string();
271+
assert_eq!(strip_query(&built), published_self_href("/wp/v2/widgets"));
272+
}
273+
274+
#[test]
275+
fn wp_block_editor() {
276+
let endpoint = super::wp_block_editor_endpoint::WpBlockEditorRequestEndpoint::new(resolver());
277+
let params = crate::wp_block_editor::WpBlockEditorSettingsParams {
278+
context: Some(crate::wp_block_editor::WpBlockEditorSettingsContext::WidgetsEditor),
279+
};
280+
let built = endpoint.retrieve_settings(&params).as_str().to_string();
281+
assert_eq!(
282+
strip_query(&built),
283+
published_self_href("/wp-block-editor/v1/settings")
284+
);
285+
}
286+
287+
#[test]
288+
fn wp_site_health_tests() {
289+
let endpoint =
290+
super::wp_site_health_tests_endpoint::WpSiteHealthTestsRequestEndpoint::new(resolver());
291+
let built = endpoint
292+
.filter_background_updates(&[
293+
crate::wp_site_health_tests::SparseWpSiteHealthTestField::Actions,
294+
crate::wp_site_health_tests::SparseWpSiteHealthTestField::Badge,
295+
])
296+
.as_str()
297+
.to_string();
298+
assert_eq!(
299+
strip_query(&built),
300+
published_self_href("/wp-site-health/v1/tests/background-updates")
301+
);
302+
}

0 commit comments

Comments
 (0)