Commit b07d4ae1312 for woocommerce

commit b07d4ae1312fc10aac17c7bb784625f947664c83
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Mon Sep 14 17:26:43 2026 +0300

    [tests] Move 3 Checkout Link error tests to PHPUnit and add a logged-in browser test (#68659)

    test(checkout): Move Checkout Link error handling below E2E

    The Checkout Link endpoint had five browser tests, three of which
    drove a real page load to find out what the endpoint does with a bad
    request: an invalid coupon, a list containing a missing product, and a
    malformed product list. Those are decisions the endpoint makes before
    anything renders, and CheckoutLinkTest can ask it directly.

    The spec goes from five titles to three. CheckoutLinkTest goes from
    two methods to nine.

    Two of the three removed titles are replaced by more than they
    asserted. The browser checked that an error notice appeared; the PHP
    tests check which notice, that the valid products survive alongside
    it, and that a missing product with no session takes the query-error
    path rather than the queued-notice one.

    The surviving guest title keeps the browser honest about the redirect
    itself, and gains a session-token assertion. A third title is added
    for the logged-in case, which had no working coverage before: the
    describe block that claimed to cover it ran without a storage state,
    so it exercised the guest path a second time. This one uses the real
    customer state and asserts the redirect carries no session token.

    test_products_and_coupon_are_added_and_token_in_url is renamed to
    ..._and_guest_token_in_url and rewritten. It is strictly stronger:
    three assertions become nine, assertEquals becomes assertSame, the
    stripped query arguments are checked individually, and the session
    token is validated through CartTokenUtils rather than matched as a
    substring.

    Consolidates the mega-branch slices:
    - Slice sf-r03-checkout-link

    Refs TESTOPS-288
    Refs #68046

    Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/testops-288-checkout-link b/plugins/woocommerce/changelog/testops-288-checkout-link
new file mode 100644
index 00000000000..643b2e15274
--- /dev/null
+++ b/plugins/woocommerce/changelog/testops-288-checkout-link
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Reduce Checkout Link E2E tests from 5 to 3 and rewrite CheckoutLinkTest from 2 methods to 9; PHPUnit owns the invalid coupon, invalid product and malformed list cases, and a new browser test covers the logged-in redirect.
diff --git a/plugins/woocommerce/tests/e2e/tests/checkout/checkout-link.spec.ts b/plugins/woocommerce/tests/e2e/tests/checkout/checkout-link.spec.ts
index 20fd95c2ac0..ebde075e104 100644
--- a/plugins/woocommerce/tests/e2e/tests/checkout/checkout-link.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/checkout/checkout-link.spec.ts
@@ -7,13 +7,10 @@ import { faker } from '@faker-js/faker';
 /**
  * Internal dependencies
  */
-import {
-	expect,
-	tags,
-	test as baseTest,
-	guestFile,
-} from '../../fixtures/fixtures';
+import { expect, tags, test as baseTest } from '../../fixtures/fixtures';
+import { CUSTOMER_STATE_PATH } from '../../playwright.config';
 import { getFakeProduct } from '../../utils/data';
+import { guestFile } from '../../utils/blocks/constants';

 const test = baseTest.extend( {
 	products: async ( { restApi }, use ) => {
@@ -74,120 +71,73 @@ test.describe( 'Checkout Link Endpoint', () => {
 			'Guest user redirected to checkout with correct cart',
 			{ tag: [ tags.PAYMENTS, tags.SERVICES ] },
 			async ( { page, baseURL, products, coupon } ) => {
-				// Visit checkout-link
 				const checkoutLink = `${ baseURL }/checkout-link?products=${ products[ 0 ].id },${ products[ 1 ].id }&coupon=${ coupon.code }`;
 				await page.goto( checkoutLink );

-				// Should redirect to checkout
 				await expect( page ).toHaveURL( /\/checkout/ );
-
-				// Assert both products are in the cart
-				const cartItems = page.locator(
-					'.wc-block-components-order-summary'
+				const session = new URL( page.url() ).searchParams.get(
+					'session'
 				);
-				await expect( cartItems ).toContainText( products[ 0 ].name );
-				await expect( cartItems ).toContainText( products[ 1 ].name );
-
-				// Assert coupon is applied
-				await expect(
-					page.getByText( `Coupon: ${ coupon.code }` )
-				).toBeVisible();
-			}
-		);
-
-		test(
-			'Guest user sees error when invalid coupon is applied',
-			{ tag: [ tags.PAYMENTS, tags.SERVICES ] },
-			async ( { page, baseURL, products } ) => {
-				// Visit checkout-link with invalid coupon
-				const checkoutLink = `${ baseURL }/checkout-link?products=${ products[ 0 ].id },${ products[ 1 ].id }&coupon=INVALID_COUPON`;
-				await page.goto( checkoutLink );
+				expect( session ).not.toBeNull();
+				expect( session ).not.toBe( '' );

-				// Should redirect to checkout
-				await expect( page ).toHaveURL( /\/checkout/ );
-
-				// Assert both products are in the cart
 				const cartItems = page.locator(
 					'.wc-block-components-order-summary'
 				);
 				await expect( cartItems ).toContainText( products[ 0 ].name );
 				await expect( cartItems ).toContainText( products[ 1 ].name );

-				// Assert error notice is shown for invalid coupon
 				await expect(
-					page.getByText(
-						'Coupon "INVALID_COUPON" cannot be applied because it does not exist.'
-					)
-				).toBeVisible();
-			}
-		);
-
-		test(
-			'Guest user sees error when invalid products are provided',
-			{ tag: [ tags.PAYMENTS, tags.SERVICES ] },
-			async ( { page, baseURL, products } ) => {
-				// Visit checkout-link with invalid product ID in list.
-				const checkoutLink = `${ baseURL }/checkout-link?products=${ products[ 0 ].id },999999`;
-				await page.goto( checkoutLink );
-
-				// Should redirect to checkout
-				await expect( page ).toHaveURL( /\/checkout/ );
-
-				// Assert error notice is shown for invalid product
-				await expect(
-					page.getByText(
-						'Product with ID "999999" was not found and cannot be added to the cart.'
-					)
+					page.getByText( `Coupon: ${ coupon.code }` )
 				).toBeVisible();
 			}
 		);

 		test(
-			'Guest user sees error when invalid product is provided',
+			'Guest user sees error when invalid link is provided',
 			{ tag: [ tags.PAYMENTS, tags.SERVICES ] },
 			async ( { page, baseURL } ) => {
-				// Visit checkout-link with invalid product ID only.
-				const checkoutLink = `${ baseURL }/checkout-link?products=999999`;
+				const checkoutLink = `${ baseURL }/checkout-link?products=abc`;
 				await page.goto( checkoutLink );

-				// Should redirect to cart if cart is empty.
 				await expect( page ).toHaveURL( /\/cart/ );

-				// Assert error notice is shown for invalid product
 				await expect(
 					page.getByText(
-						'Product with ID "999999" was not found and cannot be added to the cart.'
+						'The provided checkout link was out of date or invalid. No products were added to the cart.'
 					)
 				).toBeVisible();

-				// Cart should be empty
 				await expect(
 					page.getByText( 'Your cart is currently empty!' )
 				).toBeVisible();
 			}
 		);
+	} );
+
+	test.describe( 'Logged-in user', () => {
+		test.use( { storageState: CUSTOMER_STATE_PATH } );

 		test(
-			'Guest user sees error when invalid link is provided',
+			'Logged-in user redirected to checkout with correct cart',
 			{ tag: [ tags.PAYMENTS, tags.SERVICES ] },
-			async ( { page, baseURL } ) => {
-				// Visit checkout-link with invalid product ID only.
-				const checkoutLink = `${ baseURL }/checkout-link?products=abc`;
+			async ( { page, baseURL, products, coupon } ) => {
+				const checkoutLink = `${ baseURL }/checkout-link?products=${ products[ 0 ].id },${ products[ 1 ].id }&coupon=${ coupon.code }`;
 				await page.goto( checkoutLink );

-				// Should redirect to cart if cart is empty.
-				await expect( page ).toHaveURL( /\/cart/ );
+				await expect( page ).toHaveURL( /\/checkout/ );
+				expect(
+					new URL( page.url() ).searchParams.get( 'session' )
+				).toBeNull();

-				// Assert error notice is shown for invalid product
-				await expect(
-					page.getByText(
-						'The provided checkout link was out of date or invalid. No products were added to the cart.'
-					)
-				).toBeVisible();
+				const cartItems = page.locator(
+					'.wc-block-components-order-summary'
+				);
+				await expect( cartItems ).toContainText( products[ 0 ].name );
+				await expect( cartItems ).toContainText( products[ 1 ].name );

-				// Cart should be empty
 				await expect(
-					page.getByText( 'Your cart is currently empty!' )
+					page.getByText( `Coupon: ${ coupon.code }` )
 				).toBeVisible();
 			}
 		);
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutLinkTest.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutLinkTest.php
index 8773329b10a..bf30a56af7c 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutLinkTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutLinkTest.php
@@ -5,11 +5,167 @@ namespace Automattic\WooCommerce\Tests\Blocks\Domain\Services;

 use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutLink;
 use Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper;
+use Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils;

 /**
  * Unit tests for CheckoutLink.
  */
 class CheckoutLinkTest extends \WC_Unit_Test_Case {
+	/**
+	 * The System Under Test.
+	 *
+	 * @var CheckoutLink
+	 */
+	private $sut;
+
+	/**
+	 * Product IDs created by a test.
+	 *
+	 * @var int[]
+	 */
+	private $product_ids = array();
+
+	/**
+	 * Coupon IDs created by a test.
+	 *
+	 * @var int[]
+	 */
+	private $coupon_ids = array();
+
+	/**
+	 * Original request globals.
+	 *
+	 * @var array<string, mixed>
+	 */
+	private $original_get;
+
+	/**
+	 * Original cookies.
+	 *
+	 * @var array<string, mixed>
+	 */
+	private $original_cookie;
+
+	/**
+	 * Original query string.
+	 *
+	 * @var string|null
+	 */
+	private $original_query_string;
+
+	/**
+	 * Whether the original query string existed.
+	 *
+	 * @var bool
+	 */
+	private $had_query_string;
+
+	/**
+	 * Original current user ID.
+	 *
+	 * @var int
+	 */
+	private $original_user_id;
+
+	/**
+	 * Original WooCommerce session.
+	 *
+	 * @var \WC_Session
+	 */
+	private $original_session;
+
+	/**
+	 * Original WooCommerce cart.
+	 *
+	 * @var \WC_Cart
+	 */
+	private $original_cart;
+
+	/**
+	 * Filter callback that provides a deterministic Cart URL.
+	 *
+	 * @var \Closure
+	 */
+	private $cart_url_filter;
+
+	/**
+	 * Filter callback that provides a deterministic Checkout URL.
+	 *
+	 * @var \Closure
+	 */
+	private $checkout_url_filter;
+
+	/**
+	 * Set up an isolated checkout-link runtime.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->original_get          = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+		$this->original_cookie       = $_COOKIE;
+		$this->had_query_string      = array_key_exists( 'QUERY_STRING', $_SERVER );
+		$this->original_query_string = $this->had_query_string ? (string) $_SERVER['QUERY_STRING'] : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
+		$this->original_user_id      = get_current_user_id();
+		$this->original_session      = WC()->session;
+		$this->original_cart         = WC()->cart;
+		$this->sut                   = new CheckoutLink();
+		$this->cart_url_filter       = static function () {
+			return 'https://example.org/test-cart/';
+		};
+		$this->checkout_url_filter   = static function () {
+			return 'https://example.org/test-checkout/';
+		};
+
+		add_filter( 'woocommerce_get_cart_url', $this->cart_url_filter );
+		add_filter( 'woocommerce_get_checkout_url', $this->checkout_url_filter );
+
+		$this->reset_runtime();
+	}
+
+	/**
+	 * Restore global state and delete test fixtures.
+	 */
+	public function tearDown(): void {
+		try {
+			if ( WC()->cart ) {
+				WC()->cart->empty_cart();
+			}
+			wc_clear_notices();
+
+			foreach ( array_reverse( $this->coupon_ids ) as $coupon_id ) {
+				$coupon = new \WC_Coupon( $coupon_id );
+				if ( $coupon->get_id() ) {
+					$coupon->delete( true );
+				}
+			}
+
+			foreach ( array_reverse( $this->product_ids ) as $product_id ) {
+				$product = wc_get_product( $product_id );
+				if ( $product ) {
+					$product->delete( true );
+				}
+			}
+		} finally {
+			remove_filter( 'woocommerce_get_cart_url', $this->cart_url_filter );
+			remove_filter( 'woocommerce_get_checkout_url', $this->checkout_url_filter );
+
+			$_GET    = $this->original_get;
+			$_COOKIE = $this->original_cookie;
+
+			if ( $this->had_query_string ) {
+				$_SERVER['QUERY_STRING'] = $this->original_query_string;
+			} else {
+				unset( $_SERVER['QUERY_STRING'] );
+			}
+
+			wp_set_current_user( $this->original_user_id );
+			WC()->session = $this->original_session;
+			WC()->cart    = $this->original_cart;
+
+			parent::tearDown();
+		}
+	}
+
 	/**
 	 * @testdox Installing-mode requests queue the endpoint rewrite without replacing persisted rules.
 	 */
@@ -50,63 +206,341 @@ class CheckoutLinkTest extends \WC_Unit_Test_Case {
 	}

 	/**
-	 * Test that products and coupon are added and token in url.
+	 * @testdox Adds products and a coupon and includes a guest session token in the Checkout URL.
 	 */
-	public function test_products_and_coupon_are_added_and_token_in_url() {
-		$test_products = [
-			\WC_Helper_Product::create_simple_product(),
-			\WC_Helper_Product::create_simple_product(),
-			\WC_Helper_Product::create_simple_product(),
-		];
+	public function test_products_and_coupon_are_added_and_guest_token_in_url(): void {
+		$product_ids = $this->create_products( 3 );
+		$this->create_coupon( 'test-coupon' );
+		$this->set_request(
+			array(
+				'products'      => implode( ',', $product_ids ),
+				'coupon'        => 'test-coupon',
+				'checkout-link' => '1',
+				'utm_source'    => 'checkout-link-test',
+			)
+		);

-		$product_ids = array_map(
-			function ( $product ) {
-				return $product->get_id();
-			},
-			$test_products
+		$url   = $this->get_checkout_link();
+		$query = $this->get_url_query( $url );
+
+		$this->assertSame(
+			array_combine( $product_ids, array( 1, 1, 1 ) ),
+			$this->get_cart_product_quantities(),
+			'The guest cart should contain each requested product with its exact quantity.'
 		);
+		$this->assertSame( array( 'test-coupon' ), WC()->cart->get_applied_coupons(), 'The requested coupon should be applied.' );
+		$this->assertSame( wc_get_checkout_url(), remove_query_arg( array( 'utm_source', 'session' ), $url ), 'The redirect should use the Checkout URL.' );
+		$this->assertSame( 'checkout-link-test', $query['utm_source'] ?? null, 'Unrelated query arguments should be preserved.' );
+		$this->assertArrayNotHasKey( 'products', $query, 'The products argument should be stripped from the redirect.' );
+		$this->assertArrayNotHasKey( 'coupon', $query, 'The coupon argument should be stripped from the redirect.' );
+		$this->assertArrayNotHasKey( 'checkout-link', $query, 'The endpoint argument should be stripped from the redirect.' );
+		$this->assertNotSame( '', $query['session'] ?? '', 'Guest redirects should contain a non-empty session token.' );
+		$this->assertTrue( CartTokenUtils::validate_cart_token( (string) $query['session'] ), 'The guest session token should be valid.' );
+	}

-		$coupon = CouponHelper::create_coupon( 'test-coupon' );
+	/**
+	 * @testdox Parses quantities, lets the last duplicate win, and discards malformed or zero entries.
+	 */
+	public function test_parses_quantities_and_discards_invalid_entries(): void {
+		$product_ids = $this->create_products( 2 );
+		$this->set_request(
+			array(
+				'products' => sprintf( '%1$d:2,abc,0:4,%2$d:0,%1$d:5,%2$d:3', $product_ids[0], $product_ids[1] ),
+			)
+		);

-		$_GET['products'] = implode( ',', $product_ids );
-		$_GET['coupon']   = 'test-coupon';
+		$this->assertSame(
+			array(
+				$product_ids[0] => 5,
+				$product_ids[1] => 3,
+			),
+			$this->get_products_from_checkout_link(),
+			'Only valid products and quantities should remain, with the final duplicate quantity taking precedence.'
+		);
+	}

-		$service = new class() extends CheckoutLink {
-			/**
-			 * Get the checkout link for testing.
-			 *
-			 * @return string The checkout link.
-			 */
-			public function get_checkout_link_test() {
-				return parent::get_checkout_link();
-			}
-		};
+	/**
+	 * @testdox Rejects a product list containing only malformed or zero entries.
+	 */
+	public function test_rejects_malformed_product_list(): void {
+		$this->set_request( array( 'products' => 'abc,0,2:0' ) );
+
+		$this->assertFalse( $this->validate_checkout_link(), 'A checkout link without any valid product entries should be rejected.' );
+	}
+
+	/**
+	 * @testdox Keeps valid products and adds the exact notice when a coupon is invalid.
+	 */
+	public function test_invalid_coupon_keeps_valid_products_and_adds_exact_notice(): void {
+		$product_id = $this->create_products( 1 )[0];
+		$this->set_request(
+			array(
+				'products' => (string) $product_id,
+				'coupon'   => 'INVALID_COUPON',
+			)
+		);
+
+		$url = $this->get_checkout_link();
+
+		$this->assertSame( array( $product_id => 1 ), $this->get_cart_product_quantities(), 'The valid product should remain in the cart.' );
+		$this->assertSame( array(), WC()->cart->get_applied_coupons(), 'An invalid coupon should not be applied.' );
+		$this->assertSame( wc_get_checkout_url(), remove_query_arg( 'session', $url ), 'A valid cart should redirect to Checkout.' );
+		$this->assertSame(
+			array( 'Coupon &quot;invalid_coupon&quot; cannot be applied because it does not exist.' ),
+			$this->get_error_notice_messages(),
+			'The invalid coupon notice should be exact.'
+		);
+	}
+
+	/**
+	 * @testdox Keeps valid products and adds the exact notice when another product is missing.
+	 */
+	public function test_missing_product_keeps_valid_product_and_adds_exact_notice(): void {
+		$product_id = $this->create_products( 1 )[0];
+		$this->set_request( array( 'products' => $product_id . ',999999' ) );
+
+		$url = $this->get_checkout_link();
+
+		$this->assertSame( array( $product_id => 1 ), $this->get_cart_product_quantities(), 'The valid product should remain in the cart.' );
+		$this->assertSame( wc_get_checkout_url(), remove_query_arg( 'session', $url ), 'A valid cart should redirect to Checkout.' );
+		$this->assertSame(
+			array( 'Product with ID &quot;999999&quot; was not found and cannot be added to the cart.' ),
+			$this->get_error_notice_messages(),
+			'The missing-product notice should be exact.'
+		);
+	}
+
+	/**
+	 * @testdox Uses the query string for a missing-product error when no session exists.
+	 */
+	public function test_only_missing_product_without_session_uses_query_error(): void {
+		$this->set_request( array( 'products' => '999999' ) );
+
+		$url   = $this->get_checkout_link();
+		$query = $this->get_url_query( $url );
+
+		$this->assertSame( wc_get_cart_url(), remove_query_arg( 'wc_error', $url ), 'An empty cart should redirect to the Cart URL.' );
+		$this->assertSame(
+			'Product with ID &quot;999999&quot; was not found and cannot be added to the cart.',
+			$query['wc_error'] ?? null,
+			'The missing-product error should be transported in the URL without a session.'
+		);
+		$this->assertSame( array(), WC()->cart->get_cart(), 'The cart should remain empty.' );
+		$this->assertSame( array(), $this->get_error_notice_messages(), 'No notice should be queued without a session.' );
+	}
+
+	/**
+	 * @testdox Queues missing-product and generic errors when an established session exists.
+	 */
+	public function test_only_missing_product_with_session_queues_notices(): void {
+		$session = $this->get_session_handler();
+		$session->set_customer_session_cookie( true );
+		$this->set_request( array( 'products' => '999999' ) );

-		$url = $service->get_checkout_link_test();
+		$url = $this->get_checkout_link();

-		$cart_contents    = WC()->cart->get_cart();
-		$cart_product_ids = array_map(
-			function ( $item ) {
-				return $item['product_id'];
-			},
-			$cart_contents
+		$this->assertSame( wc_get_cart_url(), $url, 'An empty cart with a session should redirect to the Cart URL without query transport.' );
+		$this->assertArrayNotHasKey( 'wc_error', $this->get_url_query( $url ), 'The error should not be duplicated in the URL.' );
+		$this->assertSame( array(), WC()->cart->get_cart(), 'The cart should remain empty.' );
+		$this->assertSame(
+			array(
+				'Product with ID &quot;999999&quot; was not found and cannot be added to the cart.',
+				'The provided checkout link was out of date or invalid. No products were added to the cart.',
+			),
+			$this->get_error_notice_messages(),
+			'Both empty-cart errors should be queued in order.'
 		);
+	}
+
+	/**
+	 * @testdox Omits the session token for a logged-in customer while preserving products and coupon.
+	 */
+	public function test_logged_in_checkout_omits_session_token(): void {
+		$user_id = self::factory()->user->create( array( 'role' => 'customer' ) );
+		wp_set_current_user( $user_id );
+		$this->reset_runtime( $user_id );
+
+		$product_ids = $this->create_products( 2 );
+		$this->create_coupon( 'logged-in-coupon' );
+		$this->set_request(
+			array(
+				'products' => implode( ',', $product_ids ),
+				'coupon'   => 'logged-in-coupon',
+			)
+		);
+
+		$url = $this->get_checkout_link();

-		$applied_coupons      = WC()->cart->get_coupons();
-		$applied_coupon_codes = array_map(
-			function ( $coupon ) {
-				return $coupon->get_code();
-			},
-			$applied_coupons
+		$this->assertSame(
+			array_combine( $product_ids, array( 1, 1 ) ),
+			$this->get_cart_product_quantities(),
+			'The logged-in cart should contain each requested product.'
 		);
+		$this->assertSame( array( 'logged-in-coupon' ), WC()->cart->get_applied_coupons(), 'The requested coupon should be applied.' );
+		$this->assertSame( wc_get_checkout_url(), $url, 'A logged-in customer should receive the Checkout URL without a session token.' );
+		$this->assertArrayNotHasKey( 'session', $this->get_url_query( $url ), 'Logged-in redirects should not contain a session token.' );
+	}
+
+	/**
+	 * Reset WooCommerce cart and session state.
+	 *
+	 * @param int $user_id Current user ID.
+	 * @return void
+	 */
+	private function reset_runtime( int $user_id = 0 ): void {
+		wp_set_current_user( $user_id );
+		$_GET = array();
+
+		foreach ( array_keys( $_COOKIE ) as $cookie_name ) {
+			if ( str_starts_with( $cookie_name, 'wp_woocommerce_session_' ) ) {
+				unset( $_COOKIE[ $cookie_name ] );
+			}
+		}
+
+		WC()->session = new \WC_Session_Handler();
+		WC()->session->init_session_cookie();
+		WC()->cart = new \WC_Cart();
+		wc_clear_notices();
+	}
+
+	/**
+	 * Create simple products and track them for cleanup.
+	 *
+	 * @param int $count Number of products.
+	 * @return int[]
+	 */
+	private function create_products( int $count ): array {
+		$product_ids = array();
+
+		for ( $index = 0; $index < $count; $index++ ) {
+			$product_id          = \WC_Helper_Product::create_simple_product()->get_id();
+			$product_ids[]       = $product_id;
+			$this->product_ids[] = $product_id;
+		}
+
+		return $product_ids;
+	}
+
+	/**
+	 * Create a coupon and track it for cleanup.
+	 *
+	 * @param string $code Coupon code.
+	 * @return \WC_Coupon
+	 */
+	private function create_coupon( string $code ): \WC_Coupon {
+		$coupon             = CouponHelper::create_coupon( $code );
+		$this->coupon_ids[] = $coupon->get_id();
+
+		return $coupon;
+	}

-		$this->assertEquals( array_values( $product_ids ), array_values( $cart_product_ids ) );
-		$this->assertEquals( array_values( [ 'test-coupon' ] ), array_values( $applied_coupon_codes ) );
-		$this->assertStringContainsString( 'session=', $url );
+	/**
+	 * Set request globals from query arguments.
+	 *
+	 * @param array<string, string> $query Request arguments.
+	 * @return void
+	 */
+	private function set_request( array $query ): void {
+		$_GET                    = $query;
+		$_SERVER['QUERY_STRING'] = http_build_query( $query );
+	}
+
+	/**
+	 * Get product quantities from the cart.
+	 *
+	 * @return array<int, int>
+	 */
+	private function get_cart_product_quantities(): array {
+		$quantities = array();
+
+		foreach ( WC()->cart->get_cart() as $cart_item ) {
+			$quantities[ (int) $cart_item['product_id'] ] = (int) $cart_item['quantity'];
+		}
+
+		return $quantities;
+	}
+
+	/**
+	 * Get decoded query arguments from a URL.
+	 *
+	 * @param string $url URL to parse.
+	 * @return array<string, string>
+	 */
+	private function get_url_query( string $url ): array {
+		$query        = array();
+		$scalar_query = array();
+		parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query );

-		// Clean up.
-		foreach ( $test_products as $product ) {
-			wp_delete_post( $product->get_id(), true );
+		foreach ( $query as $key => $value ) {
+			if ( is_string( $key ) && is_string( $value ) ) {
+				$scalar_query[ $key ] = $value;
+			}
 		}
+
+		return $scalar_query;
+	}
+
+	/**
+	 * Get queued error notice messages.
+	 *
+	 * @return string[]
+	 */
+	private function get_error_notice_messages(): array {
+		return array_values( wp_list_pluck( wc_get_notices( 'error' ), 'notice' ) );
+	}
+
+	/**
+	 * Get the real isolated session handler.
+	 *
+	 * @return \WC_Session_Handler
+	 */
+	private function get_session_handler(): \WC_Session_Handler {
+		$session = WC()->session;
+
+		if ( ! $session instanceof \WC_Session_Handler ) {
+			throw new \RuntimeException( 'The isolated Checkout Link runtime requires WC_Session_Handler.' );
+		}
+
+		return $session;
+	}
+
+	/**
+	 * Invoke a protected CheckoutLink method through a reusable test seam.
+	 *
+	 * @param string $method Method name.
+	 * @return mixed
+	 */
+	private function invoke_checkout_link_method( string $method ) {
+		$reflection = new \ReflectionMethod( $this->sut, $method );
+		$reflection->setAccessible( true );
+
+		return $reflection->invoke( $this->sut );
+	}
+
+	/**
+	 * Get parsed checkout-link products.
+	 *
+	 * @return array<int, int>
+	 */
+	private function get_products_from_checkout_link(): array {
+		return (array) $this->invoke_checkout_link_method( 'get_products_from_checkout_link' );
+	}
+
+	/**
+	 * Validate the checkout-link request.
+	 *
+	 * @return bool
+	 */
+	private function validate_checkout_link(): bool {
+		return (bool) $this->invoke_checkout_link_method( 'validate_checkout_link' );
+	}
+
+	/**
+	 * Build the checkout-link redirect.
+	 *
+	 * @return string
+	 */
+	private function get_checkout_link(): string {
+		return (string) $this->invoke_checkout_link_method( 'get_checkout_link' );
 	}
 }