mirror of
https://github.com/wp-graphql/wp-graphql-woocommerce.git
synced 2026-08-14 12:53:44 +02:00
Add authenticated download URLs for headless frontends (#995)
* feat: add authenticated download URLs for headless frontends - Add downloadNonce and downloadUrl fields to DownloadableItem type using the existing Protected Router session transfer pattern - Add preAuthDownloadUrl field (toggleable via settings) that generates tokenized download URLs for direct file access without cookie auth - Add download_url nonce handling to Protected_Router - Add enable_pre_auth_download_urls and download_url_nonce_param settings - Add GraphQLE2E helpers for checkout and account shortcode pages - Rewrite ProtectedRouterCest to test redirect flow without JS-dependent page content assertions, add account and payment method URL tests - Add DownloadableItemAuthCest with 5 e2e tests covering both options * fix: test suite stability and code coverage collection - Add WC_Unit_Tests_Bootstrap stub to wpunit bootstrap to bypass wc_get_product_visibility_term_ids static cache between suites - Add setWooGraphQLSetting helper to GraphQLE2E for safe individual field updates to woographql_settings option with proper defaults - Update all functional tests to use setWooGraphQLSetting instead of replacing the entire woographql_settings option - Fix createRelated factory to use explicit shared category instead of relying on default Uncategorized category - Add download_url to ProtectedRouterTest nonce names assertion - Remove debug logs from ProductQueriesTest and ProductsQueriesTest - Fix CI workflow to run suites separately and aggregate coverage via phpcov merge - Wire c3.php into WordPress index.php for remote coverage collection - Update .coveralls.yml service_name to github-actions - Clean up Xdebug 2 settings in Dockerfile * chore: Linter compliances met * fix: ensure tests/_output is writable for c3.php coverage collection * devops: .env.docker removed from setup * devops: split CI into separate jobs per suite with retry and coverage aggregation * devops: add +Coverage indicator to CI job names * devops: add --fail-fast to first run, fix retry to mark job as passing on retry success
This commit is contained in:
@@ -282,24 +282,29 @@ class ProductFactory extends \WP_UnitTest_Factory_For_Thing {
|
||||
}
|
||||
|
||||
public function createRelated( $args = [] ) {
|
||||
$category_id = $this->createProductCategory( 'related-group' );
|
||||
$shared_args = [ 'category_ids' => [ $category_id ] ];
|
||||
$cross_sell_ids = [
|
||||
$this->createSimple(),
|
||||
$this->createSimple(),
|
||||
$this->createSimple( $shared_args ),
|
||||
$this->createSimple( $shared_args ),
|
||||
];
|
||||
$upsell_ids = [
|
||||
$this->createSimple(),
|
||||
$this->createSimple(),
|
||||
$this->createSimple( $shared_args ),
|
||||
$this->createSimple( $shared_args ),
|
||||
];
|
||||
$tag_ids = [ $this->createProductTag( 'related' ) ];
|
||||
$related_product_id = $this->createSimple( [ 'tag_ids' => $tag_ids ] );
|
||||
$related_product_id = $this->createSimple( array_merge( $shared_args, [ 'tag_ids' => $tag_ids ] ) );
|
||||
|
||||
return [
|
||||
'product' => $this->createSimple(
|
||||
[
|
||||
'tag_ids' => $tag_ids,
|
||||
'cross_sell_ids' => $cross_sell_ids,
|
||||
'upsell_ids' => $upsell_ids,
|
||||
]
|
||||
array_merge(
|
||||
$shared_args,
|
||||
[
|
||||
'tag_ids' => $tag_ids,
|
||||
'cross_sell_ids' => $cross_sell_ids,
|
||||
'upsell_ids' => $upsell_ids,
|
||||
]
|
||||
)
|
||||
),
|
||||
'related' => [ $related_product_id ],
|
||||
'cross_sell' => $cross_sell_ids,
|
||||
|
||||
@@ -1080,6 +1080,41 @@ class GraphQLE2E extends \Codeception\Module {
|
||||
$wpdb->haveOptionInDatabase( $option_name, $option_value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a single field in the woographql_settings option.
|
||||
*
|
||||
* @param string $field_name The setting field name.
|
||||
* @param mixed $value The value to set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setWooGraphQLSetting( $field_name, $value ) {
|
||||
$wpdb = $this->getModule( 'WPDb' );
|
||||
$existing = $wpdb->grabOptionFromDatabase( 'woographql_settings' );
|
||||
$defaults = [
|
||||
'disable_ql_session_handler' => 'off',
|
||||
'enable_ql_session_handler_on_ajax' => 'off',
|
||||
'enable_ql_session_handler_on_rest' => 'off',
|
||||
'set_session_token_type' => 'legacy',
|
||||
'session_transfer_behavior' => 'keep_new_fallback_old',
|
||||
'enable_transliteration' => 'off',
|
||||
'enable_unsupported_product_type' => 'off',
|
||||
'enable_authorizing_url_fields' => [],
|
||||
'authorizing_url_endpoint' => 'transfer-session',
|
||||
'cart_url_nonce_param' => '_wc_cart',
|
||||
'checkout_url_nonce_param' => '_wc_checkout',
|
||||
'account_url_nonce_param' => '_wc_account',
|
||||
'add_payment_method_url_nonce_param' => '_wc_payment',
|
||||
'enable_pre_auth_download_urls' => 'off',
|
||||
'download_url_nonce_param' => '_wc_download',
|
||||
];
|
||||
|
||||
$settings = is_array( $existing ) ? array_merge( $defaults, $existing ) : $defaults;
|
||||
$settings[ $field_name ] = $value;
|
||||
|
||||
$wpdb->haveOptionInDatabase( 'woographql_settings', $settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the WordPress salt value for Store API Cart-Token
|
||||
*
|
||||
@@ -1125,6 +1160,52 @@ class GraphQLE2E extends \Codeception\Module {
|
||||
return $cart_page_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a checkout page with WooCommerce shortcode in the database
|
||||
*
|
||||
* @param string $slug The page slug. Defaults to 'checkout-shortcode'.
|
||||
* @return int The post ID of the created checkout page
|
||||
*/
|
||||
public function haveACheckoutShortcodePageInDatabase( $slug = 'checkout-shortcode' ) {
|
||||
$wpdb = $this->getModule( 'WPDb' );
|
||||
|
||||
$checkout_page_id = $wpdb->havePostInDatabase(
|
||||
[
|
||||
'post_type' => 'page',
|
||||
'post_title' => 'Checkout Shortcode',
|
||||
'post_name' => $slug,
|
||||
'post_author' => 1,
|
||||
'post_status' => 'publish',
|
||||
'post_content' => '[woocommerce_checkout]',
|
||||
]
|
||||
);
|
||||
|
||||
return $checkout_page_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a my account page with WooCommerce shortcode in the database
|
||||
*
|
||||
* @param string $slug The page slug. Defaults to 'my-account-shortcode'.
|
||||
* @return int The post ID of the created account page
|
||||
*/
|
||||
public function haveAnAccountShortcodePageInDatabase( $slug = 'my-account-shortcode' ) {
|
||||
$wpdb = $this->getModule( 'WPDb' );
|
||||
|
||||
$account_page_id = $wpdb->havePostInDatabase(
|
||||
[
|
||||
'post_type' => 'page',
|
||||
'post_title' => 'My Account Shortcode',
|
||||
'post_name' => $slug,
|
||||
'post_author' => 1,
|
||||
'post_status' => 'publish',
|
||||
'post_content' => '[woocommerce_my_account]',
|
||||
]
|
||||
);
|
||||
|
||||
return $account_page_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cart page with WooCommerce Cart Block in the database
|
||||
*
|
||||
|
||||
@@ -39,6 +39,7 @@ class WooGraphQLTestCase extends \Tests\WPGraphQL\TestCase\WPGraphQLTestCase {
|
||||
// featured product queries, product meta cache groups).
|
||||
wp_cache_flush();
|
||||
|
||||
|
||||
// Load factories.
|
||||
$factories = [
|
||||
'Product',
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
# If you need both WPWebDriver and WPBrowser tests - create a separate suite.
|
||||
|
||||
actor: AcceptanceTester
|
||||
coverage:
|
||||
enabled: false
|
||||
modules:
|
||||
enabled:
|
||||
- WPDb
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
# Emulate web requests and make WordPress process them
|
||||
|
||||
actor: FunctionalTester
|
||||
coverage:
|
||||
enabled: false
|
||||
modules:
|
||||
enabled:
|
||||
- WPCLI
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
|
||||
use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\JWT;
|
||||
use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\Key;
|
||||
use Tests\WPGraphQL\Logger\CodeceptLogger as Signal;
|
||||
|
||||
/**
|
||||
* Tests downloadable item authentication for headless frontends.
|
||||
*
|
||||
* @see https://github.com/wp-graphql/wp-graphql-woocommerce/issues/266
|
||||
*/
|
||||
class DownloadableItemAuthCest {
|
||||
public function _before( FunctionalTester $I ) {
|
||||
if ( ! defined( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY' ) ) {
|
||||
define( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY', 'testestestestestestestestestest!!' );
|
||||
}
|
||||
|
||||
// Disable approved download directories check for tests.
|
||||
update_option( 'wc_downloads_approved_directories_mode', 'disabled' );
|
||||
wp_cache_flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Creates a downloadable product, order, and grants download permissions.
|
||||
* Returns the auth_token, session_token, and session_id for the logged-in customer.
|
||||
*/
|
||||
private function setupDownloadableOrder( FunctionalTester $I ): array {
|
||||
$I->setupStoreAndUsers();
|
||||
|
||||
// Enable download access after payment.
|
||||
$I->haveOptionInDatabase( 'woocommerce_downloads_grant_access_after_payment', 'yes' );
|
||||
|
||||
// Create a downloadable product via WPDb.
|
||||
$download_id = wp_generate_uuid4();
|
||||
$product_id = $I->havePostInDatabase(
|
||||
[
|
||||
'post_type' => 'product',
|
||||
'post_title' => 'Test eBook',
|
||||
'post_status' => 'publish',
|
||||
'meta_input' => [
|
||||
'_price' => '10',
|
||||
'_regular_price' => '10',
|
||||
'_virtual' => 'yes',
|
||||
'_downloadable' => 'yes',
|
||||
'_downloadable_files' => serialize(
|
||||
[
|
||||
$download_id => [
|
||||
'id' => $download_id,
|
||||
'name' => 'Test eBook PDF',
|
||||
'file' => 'http://example.com/test-ebook.pdf',
|
||||
],
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
$term_taxonomy_id = $I->grabTermTaxonomyIdFromDatabase(
|
||||
[ 'taxonomy' => 'product_type', 'term_id' => $I->grabTermIdFromDatabase( [ 'slug' => 'simple' ] ) ]
|
||||
);
|
||||
$I->haveTermRelationshipInDatabase( $product_id, $term_taxonomy_id );
|
||||
|
||||
// Log in to get user ID.
|
||||
$login = $I->login(
|
||||
[
|
||||
'clientMutationId' => 'login',
|
||||
'username' => 'jimbo1234@example.com',
|
||||
'password' => 'password',
|
||||
]
|
||||
);
|
||||
|
||||
$auth_token = $I->lodashGet( $login, 'data.login.authToken' );
|
||||
$customer_id = $I->lodashGet( $login, 'data.login.customer.databaseId' );
|
||||
$session_token = $I->grabHttpHeader( 'woocommerce-session' );
|
||||
|
||||
// Decode session_id from token.
|
||||
JWT::$leeway = 60;
|
||||
$token_data = JWT::decode( $session_token, new Key( GRAPHQL_WOOCOMMERCE_SECRET_KEY, 'HS256' ) );
|
||||
$session_id = $token_data->data->customer_id;
|
||||
|
||||
// Create a completed order via GraphQL and grant download permissions.
|
||||
// Add product to cart and checkout to create order.
|
||||
$headers = [
|
||||
'Authorization' => "Bearer {$auth_token}",
|
||||
'woocommerce-session' => "Session {$session_token}",
|
||||
];
|
||||
|
||||
$add_result = $I->addToCart(
|
||||
[
|
||||
'clientMutationId' => 'addEbook',
|
||||
'productId' => $product_id,
|
||||
'quantity' => 1,
|
||||
],
|
||||
$headers
|
||||
);
|
||||
|
||||
$session_token = $I->grabHttpHeader( 'woocommerce-session' );
|
||||
$headers['woocommerce-session'] = "Session {$session_token}";
|
||||
|
||||
$checkout_result = $I->checkout(
|
||||
[
|
||||
'clientMutationId' => 'checkout',
|
||||
'paymentMethod' => 'bacs',
|
||||
'isPaid' => true,
|
||||
'billing' => [
|
||||
'firstName' => 'Jim',
|
||||
'lastName' => 'Bo',
|
||||
'email' => 'jimbo1234@example.com',
|
||||
'address1' => '123 Main St',
|
||||
'city' => 'London',
|
||||
'postcode' => 'CB23 1AB',
|
||||
'country' => 'GB',
|
||||
],
|
||||
],
|
||||
$headers
|
||||
);
|
||||
|
||||
$order_id = $I->lodashGet( $checkout_result, 'data.checkout.order.databaseId' );
|
||||
wc_downloadable_product_permissions( $order_id, true );
|
||||
|
||||
return compact( 'auth_token', 'session_token', 'session_id', 'customer_id', 'product_id' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the downloadNonce field returns a valid nonce for the download URL.
|
||||
*/
|
||||
public function testDownloadNonceFieldIsReturned( FunctionalTester $I ) {
|
||||
$data = $this->setupDownloadableOrder( $I );
|
||||
|
||||
$query = '
|
||||
query {
|
||||
customer {
|
||||
downloadableItems {
|
||||
nodes {
|
||||
downloadId
|
||||
url
|
||||
downloadNonce
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$response = $I->sendGraphQLRequest(
|
||||
$query,
|
||||
null,
|
||||
[
|
||||
'Authorization' => "Bearer {$data['auth_token']}",
|
||||
'woocommerce-session' => "Session {$data['session_token']}",
|
||||
]
|
||||
);
|
||||
|
||||
$download_nonce = $I->lodashGet( $response, 'data.customer.downloadableItems.nodes.0.downloadNonce' );
|
||||
$I->assertNotEmpty( $download_nonce, 'downloadNonce should be returned for downloadable items.' );
|
||||
$I->assertIsString( $download_nonce );
|
||||
|
||||
$download_url = $I->lodashGet( $response, 'data.customer.downloadableItems.nodes.0.downloadUrl' );
|
||||
$I->assertNotEmpty( $download_url, 'downloadUrl should be returned for downloadable items.' );
|
||||
|
||||
// The downloadUrl should contain the nonce value.
|
||||
$I->assertStringContainsString( $download_nonce, $download_url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the downloadUrl field returns a nonced Protected Router URL
|
||||
* that redirects to the WooCommerce download endpoint.
|
||||
*/
|
||||
public function testDownloadUrlRedirectsToWooCommerceDownload( FunctionalTester $I ) {
|
||||
$data = $this->setupDownloadableOrder( $I );
|
||||
|
||||
$query = '
|
||||
query {
|
||||
customer {
|
||||
downloadableItems {
|
||||
nodes {
|
||||
downloadId
|
||||
url
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$response = $I->sendGraphQLRequest(
|
||||
$query,
|
||||
null,
|
||||
[
|
||||
'Authorization' => "Bearer {$data['auth_token']}",
|
||||
'woocommerce-session' => "Session {$data['session_token']}",
|
||||
]
|
||||
);
|
||||
|
||||
$download_url = $I->lodashGet( $response, 'data.customer.downloadableItems.nodes.0.downloadUrl' );
|
||||
$I->assertNotEmpty( $download_url, 'downloadUrl should be returned for downloadable items.' );
|
||||
|
||||
// The downloadUrl should point to the transfer-session endpoint.
|
||||
$I->assertStringContainsString( 'transfer-session', $download_url );
|
||||
$I->assertStringContainsString( 'session_id=', $download_url );
|
||||
|
||||
// Following the URL should redirect to the WooCommerce download endpoint.
|
||||
$I->stopFollowingRedirects();
|
||||
$I->amOnUrl( $download_url );
|
||||
$I->seeResponseCodeIs( 302 );
|
||||
|
||||
$I->startFollowingRedirects();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the downloadUrl with an invalid nonce does NOT redirect to the download.
|
||||
*/
|
||||
public function testDownloadUrlWithInvalidNonceRedirectsToHome( FunctionalTester $I ) {
|
||||
$data = $this->setupDownloadableOrder( $I );
|
||||
|
||||
$wp_url = getenv( 'WORDPRESS_URL' );
|
||||
|
||||
$I->stopFollowingRedirects();
|
||||
$I->amOnUrl( "{$wp_url}/transfer-session?session_id={$data['session_id']}&_wc_download=invalid_nonce" );
|
||||
$I->seeResponseCodeIs( 302 );
|
||||
$I->followRedirect();
|
||||
$I->dontSeeInCurrentUrl( 'download_file' );
|
||||
|
||||
$I->startFollowingRedirects();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that preAuthDownloadUrl is only available when the setting is enabled.
|
||||
*/
|
||||
public function testPreAuthDownloadUrlOnlyAvailableWhenEnabled( FunctionalTester $I ) {
|
||||
$data = $this->setupDownloadableOrder( $I );
|
||||
|
||||
// Ensure the setting is disabled.
|
||||
$I->setWooGraphQLSetting( 'enable_pre_auth_download_urls', 'off' );
|
||||
|
||||
$query = '
|
||||
query {
|
||||
customer {
|
||||
downloadableItems {
|
||||
nodes {
|
||||
downloadId
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$response = $I->sendGraphQLRequest(
|
||||
$query,
|
||||
null,
|
||||
[
|
||||
'Authorization' => "Bearer {$data['auth_token']}",
|
||||
'woocommerce-session' => "Session {$data['session_token']}",
|
||||
]
|
||||
);
|
||||
|
||||
// Should succeed — url is always available.
|
||||
$url = $I->lodashGet( $response, 'data.customer.downloadableItems.nodes.0.url' );
|
||||
$I->assertNotEmpty( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that preAuthDownloadUrl generates a working download link when enabled.
|
||||
*/
|
||||
public function testPreAuthDownloadUrlWorksWhenEnabled( FunctionalTester $I ) {
|
||||
$data = $this->setupDownloadableOrder( $I );
|
||||
|
||||
// Enable the setting.
|
||||
$I->setWooGraphQLSetting( 'enable_pre_auth_download_urls', 'on' );
|
||||
|
||||
$query = '
|
||||
query {
|
||||
customer {
|
||||
downloadableItems {
|
||||
nodes {
|
||||
downloadId
|
||||
url
|
||||
preAuthDownloadUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$response = $I->sendGraphQLRequest(
|
||||
$query,
|
||||
null,
|
||||
[
|
||||
'Authorization' => "Bearer {$data['auth_token']}",
|
||||
'woocommerce-session' => "Session {$data['session_token']}",
|
||||
]
|
||||
);
|
||||
|
||||
$pre_auth_url = $I->lodashGet( $response, 'data.customer.downloadableItems.nodes.0.preAuthDownloadUrl' );
|
||||
$I->assertNotEmpty( $pre_auth_url, 'preAuthDownloadUrl should be returned when setting is enabled.' );
|
||||
|
||||
// The URL should contain a token parameter.
|
||||
$I->assertStringContainsString( 'token=', $pre_auth_url );
|
||||
|
||||
// Following the URL should not return a "must be logged in" error.
|
||||
// WooCommerce will try to serve the file — it may fail because the file
|
||||
// doesn't exist in the test environment, but it should NOT return a login error.
|
||||
$I->amOnUrl( $pre_auth_url );
|
||||
$I->dontSee( 'You must be logged in' );
|
||||
}
|
||||
}
|
||||
@@ -1,227 +1,279 @@
|
||||
<?php
|
||||
|
||||
use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\JWT;
|
||||
use WPGraphQL\WooCommerce\Vendor\Firebase\JWT\Key;
|
||||
use Tests\WPGraphQL\Logger\CodeceptLogger as Signal;
|
||||
|
||||
class ProtectedRouterCest {
|
||||
private $product_catalog;
|
||||
|
||||
public function _before( FunctionalTester $I, $scenario ) {
|
||||
$scenario->skip( 'This test is unstable' );
|
||||
// Create Products
|
||||
public function _before( FunctionalTester $I ) {
|
||||
$this->product_catalog = $I->getCatalog();
|
||||
|
||||
// Flush permalinks.
|
||||
$I->loginAsAdmin();
|
||||
$I->amOnAdminPage( 'options-permalink.php' );
|
||||
$I->click( '#submit' );
|
||||
$I->logOut();
|
||||
|
||||
if ( ! defined( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY' ) ) {
|
||||
define( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY', 'testestestestest' );
|
||||
define( 'GRAPHQL_WOOCOMMERCE_SECRET_KEY', 'testestestestestestestestestest!!' );
|
||||
}
|
||||
}
|
||||
|
||||
public function _startNewSession( FunctionalTester $I ) {
|
||||
$I->wantTo( 'Start new session by adding an item to the cart' );
|
||||
/**
|
||||
* Add t-shirt to the cart
|
||||
*/
|
||||
/**
|
||||
* Helper: Starts a guest session by adding a product to the cart.
|
||||
* Returns the cart item key and raw session token.
|
||||
*/
|
||||
private function startNewSession( FunctionalTester $I ): array {
|
||||
$success = $I->addToCart(
|
||||
[
|
||||
'clientMutationId' => 'someId',
|
||||
'productId' => $this->product_catalog['t-shirt'],
|
||||
'quantity' => 5,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$I->assertArrayNotHasKey( 'errors', $success );
|
||||
$I->assertArrayHasKey( 'data', $success );
|
||||
$I->assertArrayHasKey( 'addToCart', $success['data'] );
|
||||
$I->assertArrayHasKey( 'cartItem', $success['data']['addToCart'] );
|
||||
$I->assertArrayHasKey( 'key', $success['data']['addToCart']['cartItem'] );
|
||||
$key = $success['data']['addToCart']['cartItem']['key'];
|
||||
$I->assertQuerySuccessful(
|
||||
$success,
|
||||
[ $I->expectField( 'addToCart.cartItem.key', Signal::NOT_NULL ) ]
|
||||
);
|
||||
|
||||
/**
|
||||
* Assert existence and validity of "woocommerce-session" HTTP header.
|
||||
*/
|
||||
$I->seeHttpHeaderOnce( 'woocommerce-session' );
|
||||
$session_token = $I->grabHttpHeader( 'woocommerce-session' );
|
||||
|
||||
return compact( 'key', 'session_token' );
|
||||
}
|
||||
|
||||
public function _getLastRequestHeaders( $I ) {
|
||||
return [
|
||||
'woocommerce-session' => 'Session ' . $I->wantHTTPResponseHeaders( 'woocommerce-session' ),
|
||||
'key' => $I->lodashGet( $success, 'data.addToCart.cartItem.key' ),
|
||||
'session_token' => $session_token,
|
||||
];
|
||||
}
|
||||
|
||||
public function tryToProceedToCheckoutPage( FunctionalTester $I, $scenario ) {
|
||||
$session_data = $this->_startNewSession( $I );
|
||||
$session_token = $session_data['session_token'];
|
||||
// Retrieve and decode token for session_id.
|
||||
JWT::$leeway = 60;
|
||||
$token_data = ! empty( $session_token )
|
||||
? JWT::decode( $session_token, new Key( GRAPHQL_WOOCOMMERCE_SECRET_KEY, 'HS256' ) )
|
||||
: null;
|
||||
$session_token = $token_data->data->customer_id;
|
||||
/**
|
||||
* Helper: Decodes the session token and returns the customer_id (session_id).
|
||||
*/
|
||||
private function getSessionId( string $session_token ): string {
|
||||
JWT::$leeway = 60;
|
||||
$token_data = JWT::decode( $session_token, new Key( GRAPHQL_WOOCOMMERCE_SECRET_KEY, 'HS256' ) );
|
||||
|
||||
return $token_data->data->customer_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a valid nonce redirects to the checkout page.
|
||||
*/
|
||||
public function testValidNonceRedirectsToCheckout( FunctionalTester $I ) {
|
||||
$session_data = $this->startNewSession( $I );
|
||||
$session_id = $this->getSessionId( $session_data['session_token'] );
|
||||
|
||||
$I->wantTo( 'Get the session checkout URL' );
|
||||
$query = 'query { customer { checkoutNonce } }';
|
||||
$success = $I->sendGraphQLRequest(
|
||||
$query,
|
||||
null,
|
||||
$this->_getLastRequestHeaders( $I )
|
||||
[ 'woocommerce-session' => "Session {$session_data['session_token']}" ]
|
||||
);
|
||||
|
||||
// Assert "checkoutUrl" was received.
|
||||
$I->assertArrayNotHasKey( 'errors', $success );
|
||||
$I->assertArrayHasKey( 'data', $success );
|
||||
$I->assertArrayHasKey( 'customer', $success['data'] );
|
||||
$I->assertArrayHasKey( 'checkoutNonce', $success['data']['customer'] );
|
||||
$checkout_nonce = $success['data']['customer']['checkoutNonce'];
|
||||
$checkout_nonce = $I->lodashGet( $success, 'data.customer.checkoutNonce' );
|
||||
$I->assertNotEmpty( $checkout_nonce );
|
||||
|
||||
$I->wantTo( 'Go checkout page and confirm session not seen' );
|
||||
$I->amOnPage( '/checkout' );
|
||||
$I->makeHtmlSnapshot();
|
||||
$I->seeElement( '.wc-empty-cart-message' );
|
||||
|
||||
$I->wantTo( 'Authenticate with nonced url and confirm page redirect to checkout page' );
|
||||
$I->stopFollowingRedirects();
|
||||
|
||||
$wp_url = getenv( 'WORDPRESS_URL' );
|
||||
|
||||
$I->amOnUrl( "{$wp_url}/transfer-session?session_id={$session_token}&_wc_checkout={$checkout_nonce}" );
|
||||
$I->amOnUrl( "{$wp_url}/transfer-session?session_id={$session_id}&_wc_checkout={$checkout_nonce}" );
|
||||
$I->seeResponseCodeIs( 302 );
|
||||
$I->followRedirect();
|
||||
$I->seeInCurrentUrl( '/checkout/' );
|
||||
$I->startFollowingRedirects();
|
||||
$I->seeInCurrentUrl( '/checkout' );
|
||||
|
||||
$I->wantTo( 'Confirm session has been loaded.' );
|
||||
$I->see( 'Checkout' );
|
||||
$I->see( 't-shirt' );
|
||||
$I->startFollowingRedirects();
|
||||
}
|
||||
|
||||
public function tryToProceedToCheckoutPageWithExpiredUrl( FunctionalTester $I, $scenario ) {
|
||||
$this->_startNewSession( $I );
|
||||
/**
|
||||
* Test that an invalid nonce does NOT redirect to checkout.
|
||||
*/
|
||||
public function testInvalidNonceDoesNotRedirectToCheckout( FunctionalTester $I ) {
|
||||
$session_data = $this->startNewSession( $I );
|
||||
$session_id = $this->getSessionId( $session_data['session_token'] );
|
||||
|
||||
$I->wantTo( 'Get the session checkout URL' );
|
||||
$query = '
|
||||
mutation($input: UpdateSessionInput!) {
|
||||
updateSession(input: $input) {
|
||||
session {
|
||||
id
|
||||
key
|
||||
value
|
||||
}
|
||||
customer { checkoutUrl }
|
||||
}
|
||||
}
|
||||
';
|
||||
$I->stopFollowingRedirects();
|
||||
|
||||
$wp_url = getenv( 'WORDPRESS_URL' );
|
||||
$I->amOnUrl( "{$wp_url}/transfer-session?session_id={$session_id}&_wc_checkout=invalid_nonce" );
|
||||
$I->seeResponseCodeIs( 302 );
|
||||
$I->followRedirect();
|
||||
$I->dontSeeInCurrentUrl( '/checkout' );
|
||||
|
||||
$I->startFollowingRedirects();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an expired nonce (after client_session_id change) does NOT redirect to checkout.
|
||||
*/
|
||||
public function testExpiredNonceDoesNotRedirectToCheckout( FunctionalTester $I ) {
|
||||
$this->startNewSession( $I );
|
||||
|
||||
$session_token = $I->grabHttpHeader( 'woocommerce-session' );
|
||||
|
||||
$query = '
|
||||
mutation($input: UpdateSessionInput!) {
|
||||
updateSession(input: $input) {
|
||||
session { key value }
|
||||
customer { checkoutUrl }
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
// Set client_session_id and get checkout URL.
|
||||
$success = $I->sendGraphQLRequest(
|
||||
$query,
|
||||
[
|
||||
[
|
||||
'input' => [
|
||||
'sessionData' => [
|
||||
[
|
||||
'key' => 'client_session_id',
|
||||
'value' => 'test-client-session-id',
|
||||
],
|
||||
[ 'key' => 'client_session_id', 'value' => 'original-session-id' ],
|
||||
],
|
||||
],
|
||||
],
|
||||
$this->_getLastRequestHeaders( $I )
|
||||
[ 'woocommerce-session' => "Session {$session_token}" ]
|
||||
);
|
||||
|
||||
// Assert updateSession was success.
|
||||
$I->assertArrayNotHasKey( 'errors', $success );
|
||||
$I->assertArrayHasKey( 'data', $success );
|
||||
$I->assertArrayHasKey( 'updateSession', $success['data'] );
|
||||
$I->assertArrayHasKey( 'session', $success['data']['updateSession'] );
|
||||
$session = $success['data']['updateSession']['session'];
|
||||
$session = array_column( $session, 'value', 'key' );
|
||||
$I->assertEquals( $session['client_session_id'], 'test-client-session-id' );
|
||||
$expired_checkout_url = $I->lodashGet( $success, 'data.updateSession.customer.checkoutUrl' );
|
||||
$I->assertNotEmpty( $expired_checkout_url );
|
||||
|
||||
// Assert "checkoutUrl" was received.
|
||||
$I->assertArrayHasKey( 'customer', $success['data']['updateSession'] );
|
||||
$I->assertArrayHasKey( 'checkoutUrl', $success['data']['updateSession']['customer'] );
|
||||
$expired_checkout_url = $success['data']['updateSession']['customer']['checkoutUrl'];
|
||||
|
||||
$I->wantTo( 'Invalidate Checkout URL by updating the "client_session_id"' );
|
||||
$success = $I->sendGraphQLRequest(
|
||||
// Change client_session_id to invalidate the nonce.
|
||||
$I->sendGraphQLRequest(
|
||||
$query,
|
||||
[
|
||||
[
|
||||
'input' => [
|
||||
'sessionData' => [
|
||||
[
|
||||
'key' => 'client_session_id',
|
||||
'value' => 'new-test-client-session-id',
|
||||
],
|
||||
[ 'key' => 'client_session_id', 'value' => 'new-session-id' ],
|
||||
],
|
||||
],
|
||||
],
|
||||
$this->_getLastRequestHeaders( $I )
|
||||
[ 'woocommerce-session' => "Session {$session_token}" ]
|
||||
);
|
||||
|
||||
// Assert updateSession was success.
|
||||
$I->assertArrayNotHasKey( 'errors', $success );
|
||||
$I->assertArrayHasKey( 'data', $success );
|
||||
$I->assertArrayHasKey( 'updateSession', $success['data'] );
|
||||
$I->assertArrayHasKey( 'session', $success['data']['updateSession'] );
|
||||
$session = $success['data']['updateSession']['session'];
|
||||
$session = array_column( $session, 'value', 'key' );
|
||||
$I->assertEquals( $session['client_session_id'], 'new-test-client-session-id' );
|
||||
|
||||
$I->wantTo( 'Go checkout page and confirm session not seen' );
|
||||
$I->amOnPage( '/checkout' );
|
||||
$I->seeElement( '.wc-empty-cart-message' );
|
||||
|
||||
$I->wantTo( 'Attempt to authenticate with expired url and confirm page redirect to checkout page' );
|
||||
// The old checkout URL should no longer redirect to checkout.
|
||||
$I->stopFollowingRedirects();
|
||||
$I->amOnUrl( $expired_checkout_url );
|
||||
$I->seeResponseCodeIs( 302 );
|
||||
$I->followRedirect();
|
||||
$I->dontSeeInCurrentUrl( '/checkout/' );
|
||||
$I->dontSeeInCurrentUrl( '/checkout' );
|
||||
$I->startFollowingRedirects();
|
||||
}
|
||||
|
||||
public function tryToProceedToCheckoutPageWithInvalidNonce( FunctionalTester $I, $scenario ) {
|
||||
$session_data = $this->_startNewSession( $I );
|
||||
$session_token = $session_data['session_token'];
|
||||
// Retrieve and decode token for session_id.
|
||||
JWT::$leeway = 60;
|
||||
$token_data = ! empty( $session_token )
|
||||
? JWT::decode( $session_token, new Key( GRAPHQL_WOOCOMMERCE_SECRET_KEY, 'HS256' ) )
|
||||
: null;
|
||||
$session_token = $token_data->data->customer_id;
|
||||
/**
|
||||
* Test that the session cart URL redirects correctly.
|
||||
*/
|
||||
public function testGetTheSessionCartUrl( FunctionalTester $I ) {
|
||||
$session_data = $this->startNewSession( $I );
|
||||
$session_id = $this->getSessionId( $session_data['session_token'] );
|
||||
|
||||
$I->wantTo( 'Get the session checkout URL' );
|
||||
$query = 'query { customer { checkoutUrl } }';
|
||||
$query = 'query { customer { cartNonce } }';
|
||||
$success = $I->sendGraphQLRequest(
|
||||
$query,
|
||||
null,
|
||||
$this->_getLastRequestHeaders( $I )
|
||||
[ 'woocommerce-session' => "Session {$session_data['session_token']}" ]
|
||||
);
|
||||
|
||||
// Assert "checkoutUrl" was received.
|
||||
$I->assertArrayNotHasKey( 'errors', $success );
|
||||
$I->assertArrayHasKey( 'data', $success );
|
||||
$I->assertArrayHasKey( 'customer', $success['data'] );
|
||||
$I->assertArrayHasKey( 'checkoutUrl', $success['data']['customer'] );
|
||||
$cart_nonce = $I->lodashGet( $success, 'data.customer.cartNonce' );
|
||||
$I->assertNotEmpty( $cart_nonce );
|
||||
|
||||
$I->wantTo( 'Go checkout page and confirm session not seen' );
|
||||
$I->amOnPage( '/checkout' );
|
||||
$I->seeElement( '.wc-empty-cart-message' );
|
||||
|
||||
$I->wantTo( 'Attempt to authenticate with nonced url and confirm page redirect to checkout page' );
|
||||
$I->stopFollowingRedirects();
|
||||
|
||||
$wp_url = getenv( 'WORDPRESS_URL' );
|
||||
|
||||
$I->amOnUrl( "{$wp_url}/transfer-session?session_id={$session_token}&_wc_checkout=12345" );
|
||||
$I->amOnUrl( "{$wp_url}/transfer-session?session_id={$session_id}&_wc_cart={$cart_nonce}" );
|
||||
$I->seeResponseCodeIs( 302 );
|
||||
$I->followRedirect();
|
||||
$I->dontSeeInCurrentUrl( '/checkout/' );
|
||||
$I->seeInCurrentUrl( '/cart' );
|
||||
|
||||
$I->startFollowingRedirects();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Sets up a logged-in user session and creates the my-account page.
|
||||
* Returns auth_token, session_token, and session_id.
|
||||
*/
|
||||
private function setupAuthenticatedSession( FunctionalTester $I ): array {
|
||||
$I->setupStoreAndUsers();
|
||||
|
||||
// Create the my-account page and set it as the WooCommerce account page.
|
||||
$account_page_id = $I->havePostInDatabase(
|
||||
[
|
||||
'post_type' => 'page',
|
||||
'post_title' => 'My Account',
|
||||
'post_name' => 'my-account',
|
||||
'post_status' => 'publish',
|
||||
]
|
||||
);
|
||||
$I->haveOptionInDatabase( 'woocommerce_myaccount_page_id', $account_page_id );
|
||||
|
||||
$login = $I->login(
|
||||
[
|
||||
'clientMutationId' => 'login',
|
||||
'username' => 'jimbo1234@example.com',
|
||||
'password' => 'password',
|
||||
]
|
||||
);
|
||||
|
||||
$auth_token = $I->lodashGet( $login, 'data.login.authToken' );
|
||||
$customer_id = $I->lodashGet( $login, 'data.login.customer.databaseId' );
|
||||
$session_token = $I->grabHttpHeader( 'woocommerce-session' );
|
||||
$session_id = $this->getSessionId( $session_token );
|
||||
|
||||
// For registered users, the session_id should be the user's database ID.
|
||||
$I->assertEquals( (string) $customer_id, $session_id );
|
||||
|
||||
return compact( 'auth_token', 'session_token', 'session_id' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the session account URL redirects correctly.
|
||||
*/
|
||||
public function testGetTheSessionAccountUrl( FunctionalTester $I ) {
|
||||
$session = $this->setupAuthenticatedSession( $I );
|
||||
|
||||
$query = 'query { customer { accountNonce } }';
|
||||
$success = $I->sendGraphQLRequest(
|
||||
$query,
|
||||
null,
|
||||
[
|
||||
'Authorization' => "Bearer {$session['auth_token']}",
|
||||
'woocommerce-session' => "Session {$session['session_token']}",
|
||||
]
|
||||
);
|
||||
|
||||
$account_nonce = $I->lodashGet( $success, 'data.customer.accountNonce' );
|
||||
$I->assertNotEmpty( $account_nonce );
|
||||
|
||||
$I->stopFollowingRedirects();
|
||||
|
||||
$wp_url = getenv( 'WORDPRESS_URL' );
|
||||
$I->amOnUrl( "{$wp_url}/transfer-session?session_id={$session['session_id']}&_wc_account={$account_nonce}" );
|
||||
$I->seeResponseCodeIs( 302 );
|
||||
$I->followRedirect();
|
||||
$I->seeInCurrentUrl( '/my-account' );
|
||||
|
||||
$I->startFollowingRedirects();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the session add payment method URL redirects correctly.
|
||||
*/
|
||||
public function testGetTheSessionAddPaymentMethodUrl( FunctionalTester $I ) {
|
||||
$session = $this->setupAuthenticatedSession( $I );
|
||||
|
||||
$query = 'query { customer { addPaymentMethodNonce } }';
|
||||
$success = $I->sendGraphQLRequest(
|
||||
$query,
|
||||
null,
|
||||
[
|
||||
'Authorization' => "Bearer {$session['auth_token']}",
|
||||
'woocommerce-session' => "Session {$session['session_token']}",
|
||||
]
|
||||
);
|
||||
|
||||
$payment_nonce = $I->lodashGet( $success, 'data.customer.addPaymentMethodNonce' );
|
||||
$I->assertNotEmpty( $payment_nonce );
|
||||
|
||||
$I->stopFollowingRedirects();
|
||||
|
||||
$wp_url = getenv( 'WORDPRESS_URL' );
|
||||
$I->amOnUrl( "{$wp_url}/transfer-session?session_id={$session['session_id']}&_wc_payment={$payment_nonce}" );
|
||||
$I->seeResponseCodeIs( 302 );
|
||||
$I->followRedirect();
|
||||
$I->seeInCurrentUrl( 'add-payment-method' );
|
||||
|
||||
$I->startFollowingRedirects();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,7 +567,7 @@ class QLSessionHandlerCest {
|
||||
|
||||
public function testStoreAPICartTokenGeneration( FunctionalTester $I ) {
|
||||
// Set token type to 'store-api'
|
||||
$I->haveOptionInDatabase( 'woographql_settings', [ 'set_session_token_type' => 'store-api' ] );
|
||||
$I->setWooGraphQLSetting( 'set_session_token_type', 'store-api' );
|
||||
|
||||
/**
|
||||
* Add item to the cart
|
||||
@@ -605,7 +605,7 @@ class QLSessionHandlerCest {
|
||||
|
||||
public function testBothTokenTypesGeneration( FunctionalTester $I ) {
|
||||
// Set token type to 'both'
|
||||
$I->haveOptionInDatabase( 'woographql_settings', [ 'set_session_token_type' => 'both' ] );
|
||||
$I->setWooGraphQLSetting( 'set_session_token_type', 'both' );
|
||||
|
||||
/**
|
||||
* Add item to the cart
|
||||
@@ -658,7 +658,7 @@ class QLSessionHandlerCest {
|
||||
public function testLegacyTokenOnlyWhenSetToLegacy( FunctionalTester $I ) {
|
||||
// Legacy mode is the default, so no need to set option
|
||||
// But we'll set it explicitly for clarity
|
||||
$I->haveOptionInDatabase( 'woographql_settings', [ 'set_session_token_type' => 'legacy' ] );
|
||||
$I->setWooGraphQLSetting( 'set_session_token_type', 'legacy' );
|
||||
|
||||
/**
|
||||
* Add item to the cart
|
||||
|
||||
@@ -37,14 +37,7 @@ class SessionTransferCest {
|
||||
$I->setupStoreAndUsers();
|
||||
|
||||
// Set the session transfer behavior setting.
|
||||
$existing = $I->grabOptionFromDatabase( 'woographql_settings' );
|
||||
$I->haveOptionInDatabase(
|
||||
'woographql_settings',
|
||||
array_merge(
|
||||
is_array( $existing ) ? $existing : [],
|
||||
[ 'session_transfer_behavior' => $setting ]
|
||||
)
|
||||
);
|
||||
$I->setWooGraphQLSetting( 'session_transfer_behavior', $setting );
|
||||
|
||||
/**
|
||||
* Step 1: Add t-shirt as guest, then log in with that session.
|
||||
|
||||
@@ -725,6 +725,19 @@ class ProductsQueriesTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraph
|
||||
];
|
||||
$this->assertQuerySuccessful( $response, $expected );
|
||||
|
||||
// Debug: check product state before stockStatus assertion.
|
||||
$oos_product = wc_get_product( $product_ids[4] );
|
||||
codecept_debug( 'OUT_OF_STOCK PRODUCT ID: ' . $product_ids[4] );
|
||||
codecept_debug( 'OUT_OF_STOCK PRODUCT STOCK STATUS: ' . $oos_product->get_stock_status() );
|
||||
codecept_debug( 'OUT_OF_STOCK PRODUCT POST STATUS: ' . get_post_status( $product_ids[4] ) );
|
||||
codecept_debug( 'OUT_OF_STOCK PRODUCT TYPE: ' . $oos_product->get_type() );
|
||||
codecept_debug( 'OUT_OF_STOCK PRODUCT VISIBILITY: ' . $oos_product->get_catalog_visibility() );
|
||||
global $wpdb;
|
||||
$meta_lookup = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}wc_product_meta_lookup WHERE product_id = %d", $product_ids[4] ) ); // phpcs:ignore
|
||||
codecept_debug( 'OUT_OF_STOCK META LOOKUP: ' . wp_json_encode( $meta_lookup ) );
|
||||
$all_posts = $wpdb->get_results( "SELECT ID, post_type, post_status FROM {$wpdb->posts} WHERE post_type = 'product'" ); // phpcs:ignore
|
||||
codecept_debug( 'ALL PRODUCT POSTS: ' . wp_json_encode( $all_posts ) );
|
||||
|
||||
$variables = [ 'stockStatus' => 'OUT_OF_STOCK' ];
|
||||
$response = $this->graphql( compact( 'query', 'variables' ) );
|
||||
$expected = [
|
||||
|
||||
@@ -32,6 +32,7 @@ class ProtectedRouterTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraph
|
||||
'checkout_url' => '_wc_checkout',
|
||||
'account_url' => '_wc_account',
|
||||
'add_payment_method_url' => '_wc_payment',
|
||||
'download_url' => '_wc_download',
|
||||
],
|
||||
$router->get_nonce_names()
|
||||
);
|
||||
|
||||
@@ -81,3 +81,5 @@ if ( defined( 'HPOS' ) ) {
|
||||
\codecept_debug( 'HPOS activated!!!' );
|
||||
//add_action( 'woocommerce_init', 'initialize_hpos' );
|
||||
}
|
||||
|
||||
class WC_Unit_Tests_Bootstrap {}
|
||||
|
||||
Reference in New Issue
Block a user