Commit d30e5e8d98 for woocommerce
commit d30e5e8d98fb8b361710fce3833ac30b96ef2e20
Author: Luigi Teschio <gigitux@gmail.com>
Date: Thu May 29 17:27:25 2025 +0200
Add schema validation for Patterns Toolkit in PTKClient and update PTKPatternsStore to utilize validation (#57809)
* Add schema validation for Patterns Toolkit in PTKClient and update PTKPatternsStore to utilize validation. Enhance tests for error handling with invalid payloads
* Add changefile(s) from automation for the following project(s): woocommerce
* improve unit test
* improve logic
* improve naming
* fix schema
* improve unit test
* fix schema
* update IDs
* define categories object
---------
Co-authored-by: github-actions <github-actions@github.com>
diff --git a/plugins/woocommerce/changelog/57809-fix-improve-patterns-validation b/plugins/woocommerce/changelog/57809-fix-improve-patterns-validation
new file mode 100644
index 0000000000..75d14e312b
--- /dev/null
+++ b/plugins/woocommerce/changelog/57809-fix-improve-patterns-validation
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Add schema validation for Patterns Toolkit in PTKClient and update PTKPatternsStore to utilize validation.
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Blocks/BlockPatterns.php b/plugins/woocommerce/src/Blocks/BlockPatterns.php
index 7b83fd4570..65b83cc35d 100644
--- a/plugins/woocommerce/src/Blocks/BlockPatterns.php
+++ b/plugins/woocommerce/src/Blocks/BlockPatterns.php
@@ -251,10 +251,7 @@ class BlockPatterns {
* @param array $patterns The patterns to parse.
* @return array The parsed patterns.
*/
- private function parse_categories( $patterns ) {
- if ( ! is_array( $patterns ) ) {
- return array();
- }
+ private function parse_categories( array $patterns ) {
return array_map(
function ( $pattern ) {
if ( ! isset( $pattern['categories'] ) ) {
diff --git a/plugins/woocommerce/src/Blocks/Patterns/PTKClient.php b/plugins/woocommerce/src/Blocks/Patterns/PTKClient.php
index 2b86b3c857..995d21255c 100644
--- a/plugins/woocommerce/src/Blocks/Patterns/PTKClient.php
+++ b/plugins/woocommerce/src/Blocks/Patterns/PTKClient.php
@@ -14,6 +14,67 @@ class PTKClient {
*/
const PATTERNS_TOOLKIT_URL = 'https://public-api.wordpress.com/rest/v1/ptk/patterns/';
+ /**
+ * The schema for the patterns toolkit.
+ *
+ * @var array
+ */
+ private $schema;
+
+ /**
+ * Constructor.
+ */
+ public function __construct() {
+ $this->schema = [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'ID' => [
+ 'type' => 'integer',
+ 'required' => true,
+ ],
+ 'site_id' => [
+ 'type' => 'integer',
+ 'required' => true,
+ ],
+ 'title' => [
+ 'type' => 'string',
+ 'required' => true,
+ ],
+ 'name' => [
+ 'type' => 'string',
+ 'required' => true,
+ ],
+ 'html' => [
+ 'type' => 'string',
+ 'required' => true,
+ ],
+ 'categories' => [
+ 'type' => 'object',
+ 'additionalProperties' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'slug' => [
+ 'type' => 'string',
+ 'required' => true,
+ ],
+ 'title' => [
+ 'type' => 'string',
+ 'required' => true,
+ ],
+ 'description' => [
+ 'type' => 'string',
+ 'required' => true,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+
/**
* Fetch the WooCommerce patterns from the Patterns Toolkit (PTK) API.
*
@@ -57,7 +118,9 @@ class PTKClient {
$decoded_body = json_decode( $body, true );
- if ( ! is_array( $decoded_body ) ) {
+ $is_pattern_payload_valid = $this->is_valid_schema( $decoded_body );
+
+ if ( ! $is_pattern_payload_valid ) {
return new WP_Error(
'patterns_toolkit_api_error',
__( 'Wrong response received from the Patterns Toolkit API: try again later.', 'woocommerce' )
@@ -66,4 +129,16 @@ class PTKClient {
return $decoded_body;
}
+
+ /**
+ * Validate the patterns toolkit patterns.
+ *
+ * @param array $patterns The patterns to validate.
+ * @return bool
+ */
+ public function is_valid_schema( $patterns ) {
+ $is_pattern_payload_valid = rest_validate_value_from_schema( $patterns, $this->schema );
+
+ return ! is_wp_error( $is_pattern_payload_valid );
+ }
}
diff --git a/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php b/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php
index dd2c473577..66ce83dff2 100644
--- a/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php
+++ b/plugins/woocommerce/src/Blocks/Patterns/PTKPatternsStore.php
@@ -115,7 +115,7 @@ class PTKPatternsStore {
$patterns = get_transient( self::TRANSIENT_NAME );
// Only if the transient is not set, we schedule fetching the patterns from the PTK.
- if ( false === $patterns ) {
+ if ( false === $patterns || ! $this->ptk_client->is_valid_schema( $patterns ) ) {
$this->schedule_fetch_patterns();
return array();
}
@@ -209,6 +209,7 @@ class PTKPatternsStore {
),
)
);
+
if ( is_wp_error( $patterns ) ) {
wc_get_logger()->warning(
sprintf(
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Patterns/PTKClientTest.php b/plugins/woocommerce/tests/php/src/Blocks/Patterns/PTKClientTest.php
index 7591dbc556..c685701e36 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Patterns/PTKClientTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Patterns/PTKClientTest.php
@@ -147,6 +147,102 @@ class PTKClientTest extends \WP_UnitTestCase {
remove_filter( 'pre_http_request', array( $this, 'return_request_failed' ) );
}
+ /**
+ * Test fetch_patterns returns an WP_Error when the request is successful with one item with invalid payload.
+ */
+ public function test_fetch_patterns_returns_an_wp_error_when_the_request_is_successful_with_invalid_payload() {
+ add_filter(
+ 'pre_http_request',
+ function () {
+ return array(
+ 'body' => '[
+ {
+ "ID": 14870,
+ "site_id": 174455321,
+ "title": "Review: A quote with scattered images",
+ "name": "review-a-quote-with-scattered-images",
+ "html": "<!-- /wp:spacer -->",
+ "categories": {
+ "testimonials": {
+ "slug": null,
+ "title": null,
+ "description": null
+ }
+ }
+ },
+ {
+ "ID": 14871,
+ "site_id": 174455322,
+ "title": "Review: A quote with scattered images",
+ "name": "review-a-quote-with-scattered-images",
+ "html": "<!-- /wp:spacer -->",
+ "categories": {
+ "testimonials": {
+ "slug": null,
+ "title": null,
+ "description": null
+ }
+ }
+ }
+ ]',
+ 'response' => array(
+ 'code' => 200,
+ ),
+ );
+ },
+ );
+
+ $response = $this->client->fetch_patterns();
+ $this->assertErrorResponse( $response, 'Wrong response received from the Patterns Toolkit API: try again later.' );
+
+ remove_filter( 'pre_http_request', array( $this, 'return_request_failed' ) );
+ }
+
+ /**
+ * Test fetch_patterns returns an WP_Error when the request is successful with one item with invalid payload.
+ */
+ public function test_fetch_patterns_returns_an_wp_error_when_the_request_is_successful_with_one_item_with_invalid_payload() {
+ add_filter(
+ 'pre_http_request',
+ function () {
+ return array(
+ 'body' => '[
+ {
+ "ID": 14870,
+ "site_id": 174455321,
+ "title": "Review: A quote with scattered images",
+ "name": "review-a-quote-with-scattered-images",
+ "html": "<!-- /wp:spacer -->",
+ "categories": {
+ "testimonials": {
+ "slug": "testimonials",
+ "title": "Testimonials",
+ "description": "Share reviews and feedback about your brand/business."
+ }
+ }
+ }
+ {
+ "ID": 14871,
+ "site_id": 174455322,
+ "title": "Review: A quote with scattered images",
+ "name": "review-a-quote-with-scattered-images",
+ "html": "<!-- /wp:spacer -->",
+ "categories": null,
+ }
+ ]',
+ 'response' => array(
+ 'code' => 200,
+ ),
+ );
+ },
+ );
+
+ $response = $this->client->fetch_patterns();
+ $this->assertErrorResponse( $response, 'Wrong response received from the Patterns Toolkit API: try again later.' );
+
+ remove_filter( 'pre_http_request', array( $this, 'return_request_failed' ) );
+ }
+
/**
* Asserts that the response is an error with the expected error code and message.
*
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Patterns/PTKPatternsStoreTest.php b/plugins/woocommerce/tests/php/src/Blocks/Patterns/PTKPatternsStoreTest.php
index d27310dc4e..972ec60558 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Patterns/PTKPatternsStoreTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Patterns/PTKPatternsStoreTest.php
@@ -41,8 +41,18 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
public function test_get_patterns_should_come_from_the_cache_when_the_transient_is_set() {
$expected_patterns = array(
array(
- 'title' => 'My pattern',
- 'slug' => 'my-pattern',
+ 'ID' => 14870,
+ 'site_id' => 174455321,
+ 'title' => 'Review: A quote with scattered images',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
+ 'dependencies' => [],
+ 'categories' => array(
+ 'testimonials' => array(
+ 'slug' => 'testimonials',
+ 'title' => 'Testimonials',
+ ),
+ ),
),
);
@@ -52,6 +62,11 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
->expects( $this->never() )
->method( 'fetch_patterns' );
+ $this->ptk_client
+ ->expects( $this->once() )
+ ->method( 'is_valid_schema' )
+ ->willReturn( true );
+
$patterns = $this->pattern_store->get_patterns();
$this->assertEquals( $expected_patterns, $patterns );
@@ -78,8 +93,19 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
public function test_patterns_cache_is_empty_after_flushing_it() {
$expected_patterns = array(
array(
- 'title' => 'My pattern',
- 'slug' => 'my-pattern',
+ 'ID' => 14870,
+ 'site_id' => 174455321,
+ 'title' => 'Review: A quote with scattered images',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
+ 'dependencies' => [],
+ 'categories' => array(
+ 'testimonials' => array(
+ 'slug' => 'testimonials',
+ 'title' => 'Testimonials',
+ 'description' => 'Share reviews and feedback about your brand/business.',
+ ),
+ ),
),
);
@@ -98,8 +124,19 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
update_option( 'woocommerce_allow_tracking', 'no' );
$expected_patterns = array(
array(
- 'title' => 'My pattern',
- 'slug' => 'my-pattern',
+ 'ID' => 14870,
+ 'site_id' => 174455321,
+ 'title' => 'Review: A quote with scattered images',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
+ 'dependencies' => [],
+ 'categories' => array(
+ 'testimonials' => array(
+ 'slug' => 'testimonials',
+ 'title' => 'Testimonials',
+ 'description' => 'Share reviews and feedback about your brand/business.',
+ ),
+ ),
),
);
set_transient( PTKPatternsStore::TRANSIENT_NAME, $expected_patterns );
@@ -117,8 +154,17 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
update_option( 'woocommerce_allow_tracking', 'yes' );
$expected_patterns = array(
array(
- 'title' => 'My pattern',
- 'slug' => 'my-pattern',
+ 'ID' => 14870,
+ 'site_id' => 174455321,
+ 'title' => 'Review: A quote with scattered images',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
+ 'categories' => array(
+ 'reviews' => array(
+ 'slug' => 'reviews',
+ 'title' => 'Reviews',
+ ),
+ ),
),
);
set_transient( PTKPatternsStore::TRANSIENT_NAME, $expected_patterns );
@@ -151,10 +197,21 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
update_option( 'woocommerce_allow_tracking', 'yes' );
$expected_patterns = array(
array(
- 'title' => 'My pattern',
- 'slug' => 'my-pattern',
+ 'ID' => 14870,
+ 'site_id' => 174455321,
+ 'title' => 'Review: A quote with scattered images',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
+ 'categories' => array(
+ 'reviews' => array(
+ 'slug' => 'reviews',
+ 'title' => 'Reviews',
+ ),
+ ),
+ 'dependencies' => [],
),
);
+
$this->ptk_client
->expects( $this->once() )
->method( 'fetch_patterns' )
@@ -173,27 +230,35 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
update_option( 'woocommerce_allow_tracking', 'yes' );
$ptk_patterns = array(
array(
- 'title' => 'My pattern',
- 'slug' => 'my-pattern',
- 'categories' => array(
+ 'ID' => 14870,
+ 'site_id' => 174455321,
+ 'title' => 'Review: A quote with scattered images',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
+ 'categories' => array(
'testimonials' => array(
'slug' => 'testimonials',
'title' => 'Testimonials',
),
),
+ 'dependencies' => [],
),
);
$expected_patterns = array(
array(
- 'title' => 'My pattern',
- 'slug' => 'my-pattern',
- 'categories' => array(
+ 'ID' => 14870,
+ 'site_id' => 174455321,
+ 'title' => 'Review: A quote with scattered images',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
+ 'categories' => array(
'reviews' => array(
'slug' => 'reviews',
'title' => 'Reviews',
),
),
+ 'dependencies' => [],
),
);
@@ -220,45 +285,109 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
update_option( 'woocommerce_allow_tracking', 'yes' );
$ptk_patterns = array(
array(
- 'ID' => 1,
- 'title' => 'No deps',
+ 'ID' => 1,
+ 'title' => 'No deps',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
+ 'categories' => array(
+ 'reviews' => array(
+ 'slug' => 'reviews',
+ 'title' => 'Reviews',
+ ),
+ ),
),
array(
'ID' => 2,
'title' => 'Jetpack dep',
+ 'name' => 'review-a-quote-with-scattered-images',
'dependencies' => [ 'jetpack' ],
+ 'html' => '<!-- /wp:spacer -->',
+ 'categories' => array(
+ 'reviews' => array(
+ 'slug' => 'reviews',
+ 'title' => 'Reviews',
+ ),
+ ),
),
array(
'ID' => 3,
'title' => 'Jetpack and WooCommerce dep',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
'dependencies' => [ 'woocommerce', 'jetpack' ],
+ 'categories' => array(
+ 'reviews' => array(
+ 'slug' => 'reviews',
+ 'title' => 'Reviews',
+ ),
+ ),
),
array(
'ID' => 4,
'title' => 'WooCommerce dep',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
'dependencies' => [ 'woocommerce' ],
+ 'categories' => array(
+ 'reviews' => array(
+ 'slug' => 'reviews',
+ 'title' => 'Reviews',
+ ),
+ ),
),
array(
'ID' => 5,
'title' => 'Empty deps',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
'dependencies' => [],
+ 'categories' => array(
+ 'reviews' => array(
+ 'slug' => 'reviews',
+ 'title' => 'Reviews',
+ ),
+ ),
),
);
$expected_patterns = array(
array(
- 'ID' => 1,
- 'title' => 'No deps',
+ 'ID' => 1,
+ 'title' => 'No deps',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
+ 'categories' => array(
+ 'reviews' => array(
+ 'slug' => 'reviews',
+ 'title' => 'Reviews',
+ ),
+ ),
),
array(
'ID' => 4,
'title' => 'WooCommerce dep',
+ 'name' => 'review-a-quote-with-scattered-images',
'dependencies' => [ 'woocommerce' ],
+ 'html' => '<!-- /wp:spacer -->',
+ 'categories' => array(
+ 'reviews' => array(
+ 'slug' => 'reviews',
+ 'title' => 'Reviews',
+ ),
+ ),
),
array(
'ID' => 5,
'title' => 'Empty deps',
+ 'name' => 'review-a-quote-with-scattered-images',
'dependencies' => [],
+ 'html' => '<!-- /wp:spacer -->',
+ 'categories' => array(
+ 'reviews' => array(
+ 'slug' => 'reviews',
+ 'title' => 'Reviews',
+ ),
+ ),
),
);
@@ -277,21 +406,4 @@ class PTKPatternsStoreTest extends \WP_UnitTestCase {
$this->assertEquals( $expected_patterns, $patterns );
$this->assertEquals( $expected_patterns, get_transient( PTKPatternsStore::TRANSIENT_NAME ) );
}
-
- /**
- * Asserts that the response is an error with the expected error code and message.
- *
- * @param array|WP_Error $response The response to assert.
- * @param string $expected_error_message The expected error message.
- * @return void
- */
- private function assertErrorResponse( $response, $expected_error_message ) {
- $this->assertInstanceOf( WP_Error::class, $response );
-
- $error_code = $response->get_error_code();
- $this->assertEquals( 'patterns_store_error', $error_code );
-
- $error_message = $response->get_error_message();
- $this->assertEquals( $expected_error_message, $error_message );
- }
}