Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
3 changes: 3 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
*** WooCommerce Tax Changelog ***

= 3.6.13 - 2026-xx-xx =
* Tweak - Stop registering a redundant copy of the continents REST endpoint on requests that cannot reach it.

= 3.6.12 - 2026-08-10 =
* Fix - Restore tax rate lookups for local pickup orders, which could return no rates at all.
* Fix - Preserve apostrophes in city and street when taxes are recalculated from the order edit screen.
Expand Down
3 changes: 3 additions & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ This plugin relies on the following external services:

== Changelog ==

= 3.6.13 - 2026-xx-xx =
* Tweak - Stop registering a redundant copy of the continents REST endpoint on requests that cannot reach it.

= 3.6.12 - 2026-08-10 =
* Fix - Restore tax rate lookups for local pickup orders, which could return no rates at all.
* Fix - Preserve apostrophes in city and street when taxes are recalculated from the order edit screen.
Expand Down
77 changes: 77 additions & 0 deletions tests/php/test-woocommerce-connect-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,83 @@ public function test_class_exists() {
$this->assertTrue( class_exists( 'WC_Connect_Loader' ) );
}

/**
* The bundled wc-api-dev copy of /wc/v3/data/continents is a fallback for WooCommerce versions
* that predate the endpoint. Core has shipped it since WC 3.5, so on any supported WooCommerce
* the fallback must stay dormant.
*
* This is the regression guard for the lazy-namespace interaction: since WC 9.2 core skips
* registering the whole wc/v3 namespace on requests aimed at another namespace, so the route
* table alone cannot tell us whether core provides the endpoint.
*
* Asserted against the route table rather than against class-load state: whether the bundled
* class has been required is process-global and outlives whichever test loaded it, so it cannot
* carry this contract under an arbitrary execution order.
*
* @covers WC_Connect_Loader::wc_api_dev_init
*/
public function test_wc_api_dev_init_defers_to_core_continents_controller() {
global $wp, $wp_rest_server;

if ( ! function_exists( 'wc_rest_should_load_namespace' ) ) {
$this->markTestSkipped( 'This WooCommerce does not register REST namespaces lazily.' );
}

$this->assertTrue(
class_exists( 'WC_REST_Data_Continents_Controller' ),
'WooCommerce core is expected to provide WC_REST_Data_Continents_Controller.'
);

$original_server = $wp_rest_server;
$original_route = isset( $wp->query_vars['rest_route'] ) ? $wp->query_vars['rest_route'] : null;

// Reproduce a Store API request: core then skips the whole wc/v3 namespace, so
// /wc/v3/data/continents is missing from the route table even though core provides it.
$wp->query_vars['rest_route'] = '/wc/store/v1/cart';
$wp_rest_server = null;

try {
$this->assertArrayNotHasKey(
'/wc/v3/data/continents',
rest_get_server()->get_routes(),
'Expected WooCommerce to defer the wc/v3 namespace on a wc/store request.'
);

$this->mockLoader()->wc_api_dev_init();

$this->assertArrayNotHasKey(
'/wc/v3/data/continents',
rest_get_server()->get_routes(),
'The bundled wc-api-dev continents controller must not register a route when WooCommerce core provides its own.'
);
} finally {
$wp_rest_server = $original_server;

if ( null === $original_route ) {
unset( $wp->query_vars['rest_route'] );
} else {
$wp->query_vars['rest_route'] = $original_route;
}
}
}

/**
* Pins the fix shipped in 2.5.1: the bundled controller used to call parent::__construct(),
* but no class in its ancestry (WC_REST_Dev_Data_Controller, WC_REST_Controller,
* WP_REST_Controller) declares a constructor, so that call raised
* "Uncaught Error: Cannot call constructor" every time the fallback branch was reached.
*/
public function test_bundled_continents_controller_is_constructible() {

require_once __DIR__ . '/../../classes/class-wc-connect-continents.php';
require_once __DIR__ . '/../../classes/wc-api-dev/class-wc-rest-dev-data-controller.php';
require_once __DIR__ . '/../../classes/wc-api-dev/class-wc-rest-dev-data-continents-controller.php';
Comment on lines +239 to +241

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

woocommerce-services.php:1321 adds require_once __DIR__ . '/classes/class-wc-connect-continents.php'; to the fallback branch, presented in the PR body as the fix for the observed Class "WC_Connect_Continents" not found error. But test_bundled_continents_controller_is_constructible requires that same file itself before constructing the controller, and test_wc_api_dev_init_defers_to_core_continents_controller returns at the new class_exists() guard and never enters the fallback branch. Deleting line 1321 from production leaves the entire suite green, so the test proves the controller is constructible GIVEN its dependency rather than that the fallback loads it.

Suggested change
require_once __DIR__ . '/../../classes/class-wc-connect-continents.php';
require_once __DIR__ . '/../../classes/wc-api-dev/class-wc-rest-dev-data-controller.php';
require_once __DIR__ . '/../../classes/wc-api-dev/class-wc-rest-dev-data-continents-controller.php';
require_once __DIR__ . '/../../classes/wc-api-dev/class-wc-rest-dev-data-controller.php';
require_once __DIR__ . '/../../classes/wc-api-dev/class-wc-rest-dev-data-continents-controller.php';

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that nothing pinned that require. I checked it the way you did and got the same result: delete line 1321 and the whole suite stays green, because neither test ever enters the fallback branch. Fixed in 8a1691a.

I did not apply the suggested diff, though, because I don't think it gets there. I tried it:

  • on its own, composer test -- --filter test_bundled_continents_controller_is_constructible fails with Class "WC_Connect_Continents" not found
  • the full composer test passes

It passes in the suite only because tests/php/classes/test-class-wc-rest-connect-shipping-label-controller.php sorts earlier and loads that class for its own setup. So the test would be relying on process-global class-load state again — the thing your first comment (correctly) got rid of — and it still never enters the fallback branch, so line 1321 would stay unpinned either way.

What I did instead was make the branch reachable. wc_api_dev_init()'s "does core supply the controller?" question now lives in a small protected method, so a test can answer it "no" on a WooCommerce that does supply it. Everything else in the method runs for real: the three require_once calls, the controller construction, and register_routes(). The new test asserts both bundled routes show up on a route table where core's wc/v3 namespace is absent, so they can only have come from our fallback.

Mutation results, all with --filter test_wc_api_dev_init:

Mutation Result
drop $continents->register_routes(); fails
drop either wc-api-dev require fails, class not found
drop the WC_Connect_Continents require fails, class not found
guard back to route-table-only (the bug this PR fixes) fails, on the sibling test

One honest limit: that third row only holds when the file runs alone. In a whole-suite run an earlier file has already loaded WC_Connect_Continents, so the missing require is invisible. Loading a file is process-global and monotonic, so the only way around it is a fresh process per test, and that is not affordable here — I measured PHPUnit's process-isolation annotation at ~26s for a single test (the child re-runs the WP/WC bootstrap), and that bootstrap reinstalls the shared test database underneath the tests that run after it. I wrote that limit into the test's docblock rather than leave it implied.

Suite is 404 tests / 985 assertions green, and PHPCS is unchanged on both files (206 violations / 29 sources before and after, same path).


$controller = new WC_REST_Dev_Data_Continents_Controller();

$this->assertInstanceOf( 'WC_REST_Dev_Data_Continents_Controller', $controller );
}

/**
* @covers WC_Connect_Loader::__construct
*/
Expand Down
18 changes: 18 additions & 0 deletions woocommerce-services.php
Original file line number Diff line number Diff line change
Expand Up @@ -1298,9 +1298,27 @@ public function rest_api_init() {
* Delete this when the "v3" REST API is included in all the WC versions we support.
*/
public function wc_api_dev_init() {
/*
* Ask whether WooCommerce *provides* the endpoint, not whether it happens to be in the
* route table for this request.
*
* Since WooCommerce 9.2 the wc/v3 namespace is registered lazily: Server::get_rest_namespaces()
* consults wc_rest_should_load_namespace(), which returns false whenever the current request
* targets a different known namespace (wc/store, wc-analytics, wc-admin, ...). On all of those
* requests /wc/v3/data/continents is legitimately absent even though core supplies it, so
* registering our bundled copy there is dead work: the request cannot dispatch to the route,
* and route registrations do not outlive the request that made them.
*/
if ( class_exists( 'WC_REST_Data_Continents_Controller' ) ) {
return;
}

$rest_server = rest_get_server();
$existing_routes = $rest_server->get_routes();
if ( ! isset( $existing_routes['/wc/v3/data/continents'] ) ) {
// The controller below constructs WC_Connect_Continents; keep the fallback self-contained
// rather than relying on load_dependencies() having already required it.
require_once __DIR__ . '/classes/class-wc-connect-continents.php';
require_once __DIR__ . '/classes/wc-api-dev/class-wc-rest-dev-data-controller.php';
require_once __DIR__ . '/classes/wc-api-dev/class-wc-rest-dev-data-continents-controller.php';
$continents = new WC_REST_Dev_Data_Continents_Controller();
Expand Down
Loading