* Disable authentication for the Store API completely.

This may also resolve woocommerce/woocommerce-blocks#1991

* Add nonce handling to the abstract route

* Default state

* Add shared controls including nonce api fetch

* Use shared controls

* Hydrate inital nonce

* Update data stores

* Update nonce validation

* Fix tests by setting nonces

* Remove print_r debug

* Revert useStoreCart change

* Add nonce middleware

Co-Authored-By: Darren Ethier <darren@roughsmootheng.in>

* Switch back to apiFetchWithHeaders

* Docs

Co-authored-by: Darren Ethier <darren@roughsmootheng.in>
This commit is contained in:
Mike Jolley 2020-03-19 11:50:51 +00:00 committed by GitHub
parent cba3b9712f
commit 2ff854e212
15 changed files with 265 additions and 119 deletions

View File

@ -1,13 +1,14 @@
/**
* External dependencies
*/
import { apiFetch, select } from '@wordpress/data-controls';
import { select } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { ACTION_TYPES as types } from './action-types';
import { STORE_KEY as CART_STORE_KEY } from './constants';
import { apiFetchWithHeaders } from '../shared-controls';
/**
* Returns an action object used in updating the store with the provided items
@ -69,80 +70,6 @@ export function receiveRemovingCoupon( couponCode ) {
};
}
/**
* Applies a coupon code and either invalidates caches, or receives an error if
* the coupon cannot be applied.
*
* @throws Will throw an error if there is an API problem.
* @param {string} couponCode The coupon code to apply to the cart.
*/
export function* applyCoupon( couponCode ) {
yield receiveApplyingCoupon( couponCode );
try {
const result = yield apiFetch( {
path: '/wc/store/cart/apply-coupon',
method: 'POST',
data: {
code: couponCode,
},
cache: 'no-store',
} );
if ( result ) {
yield receiveCart( result );
}
// Finished handling the coupon.
yield receiveApplyingCoupon( '' );
} catch ( error ) {
// Store the error message in state.
yield receiveError( error );
// Re-throw the error.
throw error;
}
return true;
}
/**
* Removes a coupon code and either invalidates caches, or receives an error if
* the coupon cannot be removed.
*
* @throws Will throw an error if there is an API problem.
* @param {string} couponCode The coupon code to remove from the cart.
*/
export function* removeCoupon( couponCode ) {
yield receiveRemovingCoupon( couponCode );
try {
const result = yield apiFetch( {
path: '/wc/store/cart/remove-coupon',
method: 'POST',
data: {
code: couponCode,
},
cache: 'no-store',
} );
if ( result ) {
yield receiveCart( result );
}
// Finished handling the coupon.
yield receiveRemovingCoupon( '' );
} catch ( error ) {
// Store the error message in state.
yield receiveError( error );
// Finished handling the coupon.
yield receiveRemovingCoupon( '' );
// Re-throw the error.
throw error;
}
return true;
}
/**
* Returns an action object for updating a single cart item in the store.
*
@ -185,6 +112,85 @@ export function receiveRemovedItem( cartItemKey ) {
};
}
/**
* Returns an action object used to track what shipping address are we updating to.
*
* @param {boolean} isResolving if we're loading shipping address or not.
* @return {Object} Object for action.
*/
export function shippingRatesAreResolving( isResolving ) {
return {
type: types.UPDATING_SHIPPING_ADDRESS,
isResolving,
};
}
/**
* Applies a coupon code and either invalidates caches, or receives an error if
* the coupon cannot be applied.
*
* @throws Will throw an error if there is an API problem.
* @param {string} couponCode The coupon code to apply to the cart.
*/
export function* applyCoupon( couponCode ) {
yield receiveApplyingCoupon( couponCode );
try {
const { response } = yield apiFetchWithHeaders( {
path: '/wc/store/cart/apply-coupon',
method: 'POST',
data: {
code: couponCode,
},
cache: 'no-store',
} );
yield receiveCart( response );
yield receiveApplyingCoupon( '' );
} catch ( error ) {
// Store the error message in state.
yield receiveError( error );
// Re-throw the error.
throw error;
}
return true;
}
/**
* Removes a coupon code and either invalidates caches, or receives an error if
* the coupon cannot be removed.
*
* @throws Will throw an error if there is an API problem.
* @param {string} couponCode The coupon code to remove from the cart.
*/
export function* removeCoupon( couponCode ) {
yield receiveRemovingCoupon( couponCode );
try {
const { response } = yield apiFetchWithHeaders( {
path: '/wc/store/cart/remove-coupon',
method: 'POST',
data: {
code: couponCode,
},
cache: 'no-store',
} );
yield receiveCart( response );
yield receiveRemovingCoupon( '' );
} catch ( error ) {
// Store the error message in state.
yield receiveError( error );
// Finished handling the coupon.
yield receiveRemovingCoupon( '' );
// Re-throw the error.
throw error;
}
return true;
}
/**
* Removes specified item from the cart:
* - Calls API to remove item.
@ -198,13 +204,13 @@ export function* removeItemFromCart( cartItemKey ) {
yield itemQuantityPending( cartItemKey, true );
try {
const cart = yield apiFetch( {
const { response } = yield apiFetchWithHeaders( {
path: `/wc/store/cart/remove-item/?key=${ cartItemKey }`,
method: 'POST',
cache: 'no-store',
} );
yield receiveCart( cart );
yield receiveCart( response );
} catch ( error ) {
yield receiveError( error );
}
@ -228,8 +234,8 @@ export function* changeCartItemQuantity( cartItemKey, quantity ) {
return;
}
try {
const cart = yield apiFetch( {
path: `/wc/store/cart/update-item`,
const { response } = yield apiFetchWithHeaders( {
path: '/wc/store/cart/update-item',
method: 'POST',
data: {
key: cartItemKey,
@ -238,7 +244,7 @@ export function* changeCartItemQuantity( cartItemKey, quantity ) {
cache: 'no-store',
} );
yield receiveCart( cart );
yield receiveCart( response );
} catch ( error ) {
yield receiveError( error );
}
@ -252,7 +258,7 @@ export function* changeCartItemQuantity( cartItemKey, quantity ) {
*/
export function* selectShippingRate( rateId, packageId = 0 ) {
try {
const result = yield apiFetch( {
const { response } = yield apiFetchWithHeaders( {
path: `/wc/store/cart/select-shipping-rate/${ packageId }`,
method: 'POST',
data: {
@ -261,9 +267,7 @@ export function* selectShippingRate( rateId, packageId = 0 ) {
cache: 'no-store',
} );
if ( result ) {
yield receiveCart( result );
}
yield receiveCart( response );
} catch ( error ) {
yield receiveError( error );
// Re-throw the error.
@ -272,19 +276,6 @@ export function* selectShippingRate( rateId, packageId = 0 ) {
return true;
}
/**
* Returns an action object used to track what shipping address are we updating to.
*
* @param {boolean} isResolving if we're loading shipping address or not.
* @return {Object} Object for action.
*/
export function shippingRatesAreResolving( isResolving ) {
return {
type: types.UPDATING_SHIPPING_ADDRESS,
isResolving,
};
}
/**
* Applies a coupon code and either invalidates caches, or receives an error if
the coupon cannot be applied.
@ -294,16 +285,14 @@ the coupon cannot be applied.
export function* updateShippingAddress( address ) {
yield shippingRatesAreResolving( true );
try {
const result = yield apiFetch( {
const { response } = yield apiFetchWithHeaders( {
path: '/wc/store/cart/update-shipping',
method: 'POST',
data: address,
cache: 'no-store',
} );
if ( result ) {
yield receiveCart( result );
}
yield receiveCart( response );
} catch ( error ) {
yield receiveError( error );
yield shippingRatesAreResolving( false );

View File

@ -2,7 +2,7 @@
* External dependencies
*/
import { registerStore } from '@wordpress/data';
import { controls } from '@wordpress/data-controls';
import { controls as dataControls } from '@wordpress/data-controls';
/**
* Internal dependencies
@ -12,11 +12,12 @@ import * as selectors from './selectors';
import * as actions from './actions';
import * as resolvers from './resolvers';
import reducer from './reducers';
import { controls } from '../shared-controls';
registerStore( STORE_KEY, {
reducer,
actions,
controls,
controls: { ...dataControls, ...controls },
selectors,
resolvers,
} );

View File

@ -12,7 +12,7 @@ import * as selectors from './selectors';
import * as actions from './actions';
import * as resolvers from './resolvers';
import reducer from './reducers';
import { controls } from './controls';
import { controls } from '../shared-controls';
registerStore( STORE_KEY, {
reducer,

View File

@ -10,7 +10,7 @@ import { addQueryArgs } from '@wordpress/url';
import { receiveCollection, receiveCollectionError } from './actions';
import { STORE_KEY as SCHEMA_STORE_KEY } from '../schema/constants';
import { STORE_KEY, DEFAULT_EMPTY_ARRAY } from './constants';
import { apiFetchWithHeaders } from './controls';
import { apiFetchWithHeaders } from '../shared-controls';
/**
* Check if the store needs invalidating due to a change in last modified headers.
@ -52,9 +52,9 @@ export function* getCollection( namespace, resourceName, query, ids ) {
try {
const {
items = DEFAULT_EMPTY_ARRAY,
response = DEFAULT_EMPTY_ARRAY,
headers,
} = yield apiFetchWithHeaders( route + queryString );
} = yield apiFetchWithHeaders( { path: route + queryString } );
if ( headers && headers.get && headers.has( 'last-modified' ) ) {
// Do any invalidation before the collection is received to prevent
@ -65,7 +65,7 @@ export function* getCollection( namespace, resourceName, query, ids ) {
}
yield receiveCollection( namespace, resourceName, queryString, ids, {
items,
items: response,
headers,
} );
} catch ( error ) {

View File

@ -10,7 +10,7 @@ import { getCollection, getCollectionHeader } from '../resolvers';
import { receiveCollection } from '../actions';
import { STORE_KEY as SCHEMA_STORE_KEY } from '../../schema/constants';
import { STORE_KEY } from '../constants';
import { apiFetchWithHeaders } from '../controls';
import { apiFetchWithHeaders } from '../../shared-controls';
jest.mock( '@wordpress/data-controls' );
@ -73,7 +73,9 @@ describe( 'getCollection', () => {
fulfillment.next();
const { value } = fulfillment.next( 'https://example.org' );
expect( value ).toEqual(
apiFetchWithHeaders( 'https://example.org?foo=bar' )
apiFetchWithHeaders( {
path: 'https://example.org?foo=bar',
} )
);
}
);
@ -101,7 +103,7 @@ describe( 'getCollection', () => {
fulfillment.next();
fulfillment.next( 'https://example.org' );
const { value } = fulfillment.next( {
items: [ '42', 'cheeseburgers' ],
response: [ '42', 'cheeseburgers' ],
headers: { foo: 'bar' },
} );
expect( value ).toEqual(

View File

@ -7,14 +7,14 @@ import triggerFetch from '@wordpress/api-fetch';
* Dispatched a control action for triggering an api fetch call with no parsing.
* Typically this would be used in scenarios where headers are needed.
*
* @param {string} path The path for the request.
* @param {Object} options The options for the API request.
*
* @return {Object} The control action descriptor.
*/
export const apiFetchWithHeaders = ( path ) => {
export const apiFetchWithHeaders = ( options ) => {
return {
type: 'API_FETCH_WITH_HEADERS',
path,
options,
};
};
@ -25,12 +25,13 @@ export const apiFetchWithHeaders = ( path ) => {
* the controls property of the registration object.
*/
export const controls = {
API_FETCH_WITH_HEADERS( { path } ) {
API_FETCH_WITH_HEADERS( { options } ) {
return new Promise( ( resolve, reject ) => {
triggerFetch( { path, parse: false } )
.then( ( response ) => {
response.json().then( ( items ) => {
resolve( { items, headers: response.headers } );
triggerFetch( { ...options, parse: false } )
.then( ( fetchResponse ) => {
fetchResponse.json().then( ( response ) => {
resolve( { response, headers: fetchResponse.headers } );
triggerFetch.setNonce( fetchResponse.headers );
} );
} )
.catch( ( error ) => {

View File

@ -1,2 +1,3 @@
export * from './store-api-nonce';
export * from './constants';
export { ENDPOINTS } from './endpoints';

View File

@ -0,0 +1,61 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { getSetting } from '../shared';
// Cache for the initial nonce initialized from hydration.
let nonce = getSetting( 'storeApiNonce' );
/**
* Returns whether or not this is a non GET wc/store API request.
*
* @param {Object} options Fetch options.
*
* @return {boolean} Returns true if this is a store request.
*/
const isStoreApiGetRequest = ( options ) => {
const url = options.url || options.path;
if ( ! url || ! options.method || options.method === 'GET' ) {
return false;
}
return /wc\/store\//.exec( url ) !== null;
};
/**
* Set the current nonce from a header object.
*
* @param {Object} headers Headers object.
*/
const setNonce = ( headers ) => {
const newNonce = headers?.get( 'X-WC-Store-API-Nonce' );
if ( newNonce ) {
nonce = newNonce;
}
};
/**
* Nonce middleware which updates the nonce after a request, if given.
*
* @param {Object} options Fetch options.
* @param {Function} next The next middleware or fetchHandler to call.
*
* @return {*} The evaluated result of the remaining middleware chain.
*/
export const storeNonceMiddleware = ( options, next ) => {
if ( isStoreApiGetRequest( options ) ) {
const existingHeaders = options.headers || {};
options.headers = {
...existingHeaders,
'X-WC-Store-API-Nonce': nonce,
};
}
return next( options, next );
};
apiFetch.use( storeNonceMiddleware );
apiFetch.setNonce = setNonce;

View File

@ -141,6 +141,7 @@ class Assets {
'restApiRoutes' => [
'/wc/store' => array_keys( \Automattic\WooCommerce\Blocks\RestApi::get_routes_from_namespace( 'wc/store' ) ),
],
'storeApiNonce' => wp_create_nonce( 'wc_store_api' ),
'homeUrl' => esc_url( home_url( '/' ) ),
'storePages' => [
'shop' => self::format_page_resource( $page_ids['shop'] ),

View File

@ -21,6 +21,7 @@ class RestApi {
add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ), 10 );
add_action( 'rest_api_init', array( '\Automattic\WooCommerce\Blocks\RestApi\StoreApi\RoutesController', 'register_routes' ), 10 );
add_filter( 'rest_authentication_errors', array( __CLASS__, 'maybe_init_cart_session' ), 1 );
add_filter( 'rest_authentication_errors', array( __CLASS__, 'store_api_authentication' ) );
}
/**
@ -55,6 +56,37 @@ class RestApi {
return isset( $response_data['routes'] ) ? $response_data['routes'] : null;
}
/**
* Check if is request to the Store API.
*
* @return bool
*/
protected static function is_request_to_store_api() {
if ( empty( $_SERVER['REQUEST_URI'] ) ) {
return false;
}
$rest_prefix = trailingslashit( rest_get_url_prefix() );
$request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
return false !== strpos( $request_uri, $rest_prefix . 'wc/store' );
}
/**
* The Store API does not require authentication.
*
* @param \WP_Error|mixed $result Error from another authentication handler, null if we should handle it, or another value if not.
* @return \WP_Error|null|bool
*/
public static function store_api_authentication( $result ) {
// Pass through errors from other authentication methods used before this one.
if ( ! empty( $result ) || ! self::is_request_to_store_api() ) {
return $result;
}
return true;
}
/**
* If we're making a cart request, we may need to load some additional classes from WC Core so we're ready to deal with requests.
*

View File

@ -50,6 +50,9 @@ abstract class AbstractRoute implements RouteInterface {
public function get_response( \WP_REST_Request $request ) {
$response = null;
try {
if ( 'GET' !== $request->get_method() ) {
$this->check_nonce( $request );
}
switch ( $request->get_method() ) {
case 'POST':
$response = $this->get_route_post_response( $request );
@ -65,6 +68,9 @@ abstract class AbstractRoute implements RouteInterface {
$response = $this->get_route_response( $request );
break;
}
if ( 'GET' !== $request->get_method() && ! is_wp_error( $response ) ) {
$response->header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
}
} catch ( RouteException $error ) {
$response = new \WP_Error( $error->getErrorCode(), $error->getMessage(), [ 'status' => $error->getCode() ] );
} catch ( Exception $error ) {
@ -73,6 +79,30 @@ abstract class AbstractRoute implements RouteInterface {
return $response;
}
/**
* For non-GET endpoints, require and validate a nonce to prevent CSRF attacks.
*
* Nonces will mismatch if the logged in session cookie is different! If using a client to test, set this cookie
* to match the logged in cookie in your browser.
*
* @throws RouteException On error.
*
* @param \WP_REST_Request $request Request object.
*/
protected function check_nonce( \WP_REST_Request $request ) {
$nonce = $request->get_header( 'X-WC-Store-API-Nonce' );
if ( null === $nonce ) {
throw new RouteException( 'woocommerce_rest_missing_nonce', __( 'Missing the X-WC-Store-API-Nonce header. This endpoint requires a valid nonce.', 'woo-gutenberg-products-block' ), 403 );
}
$valid_nonce = wp_verify_nonce( $nonce, 'wc_store_api' );
if ( ! $valid_nonce ) {
throw new RouteException( 'woocommerce_rest_invalid_nonce', __( 'X-WC-Store-API-Nonce is invalid.', 'woo-gutenberg-products-block' ), 403 );
}
}
/**
* Get route response for GET requests.
*

View File

@ -98,6 +98,7 @@ class Cart extends TestCase {
public function test_remove_bad_cart_item() {
// Test removing a bad cart item - should return 404.
$request = new WP_REST_Request( 'POST', '/wc/store/cart/remove-item' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'key' => 'bad_item_key_123',
@ -116,6 +117,7 @@ class Cart extends TestCase {
public function test_remove_cart_item() {
// Test removing a valid cart item - should return updated cart.
$request = new WP_REST_Request( 'POST', '/wc/store/cart/remove-item' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'key' => $this->keys[0],
@ -143,6 +145,7 @@ class Cart extends TestCase {
*/
public function test_update_item() {
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-item' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'key' => $this->keys[0],
@ -163,6 +166,7 @@ class Cart extends TestCase {
*/
public function test_update_shipping() {
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'country' => 'US',
@ -188,6 +192,7 @@ class Cart extends TestCase {
public function test_get_items_address_validation() {
// US address.
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'address_1' => 'Test address 1',
@ -211,6 +216,7 @@ class Cart extends TestCase {
// Address with empty country.
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'country' => ''
@ -223,6 +229,7 @@ class Cart extends TestCase {
// Address with invalid country.
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'country' => 'ZZZZZZZZ'
@ -235,6 +242,7 @@ class Cart extends TestCase {
// US address with named state.
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'state' =>'Alabama',
@ -250,6 +258,7 @@ class Cart extends TestCase {
// US address with invalid state.
$request = new WP_REST_Request( 'POST', '/wc/store/cart/update-shipping' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'state' =>'ZZZZZZZZ',
@ -270,6 +279,7 @@ class Cart extends TestCase {
wc()->cart->remove_coupon( $this->coupon->get_code() );
$request = new WP_REST_Request( 'POST', '/wc/store/cart/apply-coupon' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'code' => $this->coupon->get_code(),
@ -283,6 +293,7 @@ class Cart extends TestCase {
// Test coupons with different case.
$newcoupon = CouponHelper::create_coupon( 'testCoupon' );
$request = new WP_REST_Request( 'POST', '/wc/store/cart/apply-coupon' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'code' => 'testCoupon',
@ -295,6 +306,7 @@ class Cart extends TestCase {
// Test coupons with special chars in the code.
$newcoupon = CouponHelper::create_coupon( '$5 off' );
$request = new WP_REST_Request( 'POST', '/wc/store/cart/apply-coupon' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'code' => '$5 off',
@ -311,6 +323,7 @@ class Cart extends TestCase {
public function test_remove_coupon() {
// Invalid coupon.
$request = new WP_REST_Request( 'POST', '/wc/store/cart/remove-coupon' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'code' => 'doesnotexist',
@ -322,6 +335,7 @@ class Cart extends TestCase {
// Applied coupon.
$request = new WP_REST_Request( 'POST', '/wc/store/cart/remove-coupon' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'code' => $this->coupon->get_code(),

View File

@ -74,6 +74,7 @@ class CartCoupons extends TestCase {
wc()->cart->remove_coupons();
$request = new WP_REST_Request( 'POST', '/wc/store/cart/coupons' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'code' => $this->coupon->get_code(),
@ -93,6 +94,7 @@ class CartCoupons extends TestCase {
wc()->cart->remove_coupons();
$request = new WP_REST_Request( 'POST', '/wc/store/cart/coupons' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'code' => 'IDONOTEXIST',
@ -109,6 +111,7 @@ class CartCoupons extends TestCase {
*/
public function test_delete_item() {
$request = new WP_REST_Request( 'DELETE', '/wc/store/cart/coupons/' . $this->coupon->get_code() );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
@ -116,12 +119,14 @@ class CartCoupons extends TestCase {
$this->assertEmpty( $data );
$request = new WP_REST_Request( 'DELETE', '/wc/store/cart/coupons/' . $this->coupon->get_code() );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 404, $response->get_status() );
$request = new WP_REST_Request( 'DELETE', '/wc/store/cart/coupons/i-do-not-exist' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
@ -133,6 +138,7 @@ class CartCoupons extends TestCase {
*/
public function test_delete_items() {
$request = new WP_REST_Request( 'DELETE', '/wc/store/cart/coupons' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$response = $this->server->dispatch( $request );
$data = $response->get_data();

View File

@ -63,6 +63,7 @@ class CartCreateOrder extends TestCase {
*/
public function test_create_item() {
$request = new WP_REST_Request( 'POST', '/wc/store/cart/create-order' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_param(
'billing_address',
[

View File

@ -92,6 +92,7 @@ class CartItems extends TestCase {
$this->assertEquals( '2000', $data['totals']->line_total );
$request = new WP_REST_Request( 'DELETE', '/wc/store/cart/items/XXX815416f775098fe977004015c6193' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
@ -105,6 +106,7 @@ class CartItems extends TestCase {
wc_empty_cart();
$request = new WP_REST_Request( 'POST', '/wc/store/cart/items' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'id' => $this->products[0]->get_id(),
@ -137,6 +139,7 @@ class CartItems extends TestCase {
$invalid_product->save();
$request = new WP_REST_Request( 'POST', '/wc/store/cart/items' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'id' => $invalid_product->get_id(),
@ -153,6 +156,7 @@ class CartItems extends TestCase {
*/
public function test_update_item() {
$request = new WP_REST_Request( 'PUT', '/wc/store/cart/items/' . $this->keys[0] );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$request->set_body_params(
array(
'quantity' => '10',
@ -170,6 +174,7 @@ class CartItems extends TestCase {
*/
public function test_delete_item() {
$request = new WP_REST_Request( 'DELETE', '/wc/store/cart/items/' . $this->keys[0] );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
@ -177,6 +182,7 @@ class CartItems extends TestCase {
$this->assertEmpty( $data );
$request = new WP_REST_Request( 'DELETE', '/wc/store/cart/items/' . $this->keys[0] );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
@ -188,6 +194,7 @@ class CartItems extends TestCase {
*/
public function test_delete_items() {
$request = new WP_REST_Request( 'DELETE', '/wc/store/cart/items' );
$request->set_header( 'X-WC-Store-API-Nonce', wp_create_nonce( 'wc_store_api' ) );
$response = $this->server->dispatch( $request );
$data = $response->get_data();