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 );
-	}
 }