Skip to content

Commit 660b254

Browse files
committed
Add resolver seam so the Throwable catch path is testable (WOOTAX-303)
The catch ( Throwable ) in StoreApiExtendSchema - the core WOOTAX-303 fix - had no test because the container call was a hard-coded static inside a private constructor with no injection point. Extract the container resolution into a protected static resolve_extend_schema() seam and instantiate via new static(), so a test double can override it to throw. The constructor is now protected (external instantiation is still blocked). Behavior is unchanged for the base class: new static() resolves to self there. Add test_instance_returns_null_when_container_throws(), which drives a subclass whose resolver throws a TypeError - a Throwable that is NOT an Exception, so it would propagate (and fail the test) against the pre-fix catch ( Exception ). This locks in the widening and asserts the failure is logged once.
1 parent 2f36257 commit 660b254

3 files changed

Lines changed: 97 additions & 3 deletions

File tree

src/StoreApi/StoreApiExtendSchema.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,32 @@ class StoreApiExtendSchema {
4747

4848
/**
4949
* ExtendSchemaService constructor.
50+
*
51+
* Protected rather than private so tests can subclass and override
52+
* resolve_extend_schema() to exercise the resolution-failure path.
5053
*/
51-
private function __construct() {
54+
protected function __construct() {
5255
self::$attempted = true;
5356

5457
try {
55-
self::$instance = StoreApi::container()->get( ExtendSchema::class );
58+
self::$instance = static::resolve_extend_schema();
5659
} catch ( Throwable $e ) {
5760
wc_get_logger()->debug( 'Failed to get ExtendSchema instance.', array( 'exception' => $e ) );
5861
}
5962
}
6063

64+
/**
65+
* Resolve the ExtendSchema instance from the Store API container.
66+
*
67+
* Extracted as a seam so a broken container can be simulated in tests (subclass
68+
* and override to throw) without needing a genuinely partial WooCommerce install.
69+
*
70+
* @return ExtendSchema
71+
*/
72+
protected static function resolve_extend_schema(): ExtendSchema {
73+
return StoreApi::container()->get( ExtendSchema::class );
74+
}
75+
6176
/**
6277
* Returns the ExtendSchema instance, or null when it cannot be resolved.
6378
*
@@ -67,7 +82,7 @@ private function __construct() {
6782
*/
6883
public static function instance(): ?ExtendSchema {
6984
if ( ! self::$attempted ) {
70-
new self();
85+
new static();
7186
}
7287

7388
return self::$instance;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
/**
3+
* Test double for StoreApiExtendSchema that simulates container resolution failure.
4+
*
5+
* @package Automattic/WCServices
6+
*/
7+
8+
// No direct access please.
9+
if ( ! defined( 'ABSPATH' ) ) {
10+
exit;
11+
}
12+
13+
if ( ! class_exists( 'WCServices_Throwing_Store_Api_Extend_Schema' ) ) {
14+
15+
/**
16+
* Forces StoreApiExtendSchema's container resolution to fail with a TypeError, so the
17+
* catch ( Throwable ) path in instance() can be exercised without a broken WooCommerce
18+
* install (WOOTAX-303).
19+
*/
20+
class WCServices_Throwing_Store_Api_Extend_Schema extends \Automattic\WCServices\StoreApi\StoreApiExtendSchema {
21+
22+
// phpcs:disable Squiz.Commenting.FunctionComment.InvalidNoReturn -- Test double intentionally always throws to simulate a resolution failure.
23+
/**
24+
* Simulate a container that cannot resolve ExtendSchema.
25+
*
26+
* @throws \TypeError Always, to mimic a container resolution failure.
27+
* @return \Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema
28+
*/
29+
protected static function resolve_extend_schema(): \Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema {
30+
throw new \TypeError( 'Simulated container resolution failure.' );
31+
}
32+
// phpcs:enable Squiz.Commenting.FunctionComment.InvalidNoReturn
33+
}
34+
}

tests/php/test-woocommerce-connect-client.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
require_once __DIR__ . '/class-wcservices-throwing-store-api-extend-schema.php';
4+
35
class WP_Test_WC_Connect_Loader extends WC_Unit_Test_Case {
46

57
const SERVICE_SCRIPT_HANDLE = 'wc_connect_admin';
@@ -470,4 +472,47 @@ public function test_instance_returns_cached_instance_without_reresolving() {
470472
$instance->setValue( $orig_instance );
471473
}
472474
}
475+
476+
/**
477+
* When the container throws while resolving ExtendSchema, instance() must catch it
478+
* and return null instead of fataling. A TypeError is used deliberately: it is a
479+
* Throwable but not an Exception, so pre-fix code (catch Exception) would let it
480+
* propagate. The failure must also be logged once (WOOTAX-303).
481+
*
482+
* @testdox instance() returns null and logs when the container throws a non-Exception Throwable.
483+
* @covers Automattic\WCServices\StoreApi\StoreApiExtendSchema::instance
484+
*/
485+
public function test_instance_returns_null_when_container_throws() {
486+
$class = '\Automattic\WCServices\StoreApi\StoreApiExtendSchema';
487+
$attempted = new ReflectionProperty( $class, 'attempted' );
488+
$instance = new ReflectionProperty( $class, 'instance' );
489+
$attempted->setAccessible( true );
490+
$instance->setAccessible( true );
491+
492+
$orig_attempted = $attempted->getValue();
493+
$orig_instance = $instance->getValue();
494+
495+
$logger = $this->getMockBuilder( 'WC_Logger_Interface' )->getMock();
496+
$logger->expects( $this->once() )
497+
->method( 'debug' )
498+
->with( 'Failed to get ExtendSchema instance.', $this->anything() );
499+
500+
$inject_logger = function () use ( $logger ) {
501+
return $logger;
502+
};
503+
add_filter( 'woocommerce_logging_class', $inject_logger );
504+
505+
try {
506+
$attempted->setValue( false );
507+
$instance->setValue( null );
508+
509+
$result = WCServices_Throwing_Store_Api_Extend_Schema::instance();
510+
511+
$this->assertNull( $result, 'instance() must return null - not fatal - when the container throws a non-Exception Throwable.' );
512+
} finally {
513+
remove_filter( 'woocommerce_logging_class', $inject_logger );
514+
$attempted->setValue( $orig_attempted );
515+
$instance->setValue( $orig_instance );
516+
}
517+
}
473518
}

0 commit comments

Comments
 (0)