products. * * @param int $product_count Number of products to create. */ protected function create_products( $product_count = 30 ) { $this->products = array(); for ( $i = 0; $i < $product_count; $i++ ) { $product = WC_Helper_Product::create_simple_product( false ); $product->set_name( 'Dummy Product ' . $i ); $this->products[] = $product; } // To test limit length properly, we need a product with a name that is shorter than 127 chars, // but longer than 127 chars when URL-encoded. if ( array_key_exists( 0, $this->products ) ) { $this->products[0]->set_name( 'Dummy Product 😎😎😎😎😎😎😎😎😎😎😎' ); } } /** * Add products from $this->products to $order as items, clearing existing order items. * * @param WC_Order $order Order to which the products should be added. * @param array $prices Array of prices to use for created products. Leave empty for default prices. */ protected function add_products_to_order( &$order, $prices = array() ) { // Remove previous items. foreach ( $order->get_items() as $item ) { $order->remove_item( $item->get_id() ); } // Add new products. $prod_count = 0; foreach ( $this->products as $product ) { $item = new WC_Order_Item_Product(); $item->set_props( array( 'product' => $product, 'quantity' => 3, 'subtotal' => $prices ? $prices[ $prod_count ] : wc_get_price_excluding_tax( $product, array( 'qty' => 3 ) ), 'total' => $prices ? $prices[ $prod_count ] : wc_get_price_excluding_tax( $product, array( 'qty' => 3 ) ), ) ); $item->save(); $order->add_item( $item ); $prod_count++; } } /** * Initialize the Paypal gateway and Request objects. */ public function setUp() { parent::setUp(); $bootstrap = WC_Unit_Tests_Bootstrap::instance(); include_once $bootstrap->plugin_dir . '/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php'; $this->paypal_gateway = new WC_Gateway_Paypal(); $this->paypal_request = new WC_Gateway_Paypal_Request( $this->paypal_gateway ); } /** * Create Paypal request URL for $product_count number of products. * * @param int $product_count Number of products to include in the order. * @param bool $testmode Whether to test using sandbox or not. * @param array $product_prices Array of prices to use for created products. Leave empty for default prices. * @param bool $calc_order_totals Whether the WC_Order::calculate_totals() should be triggered when creating order. * @return string * @throws WC_Data_Exception Exception on failure. */ protected function get_request_url( $product_count, $testmode, $product_prices = array(), $calc_order_totals = true ) { // Create products. $this->create_products( $product_count ); $this->order = WC_Helper_Order::create_order( $this->user ); $this->add_products_to_order( $this->order, $product_prices ); // Set payment method to Paypal. $payment_gateways = WC()->payment_gateways->payment_gateways(); $this->order->set_payment_method( $payment_gateways['paypal'] ); // Add tax. if ( wc_tax_enabled() ) { $tax_rate = array( 'tax_rate_country' => '', 'tax_rate_state' => '', 'tax_rate' => '11.0000', 'tax_rate_name' => 'TAX', 'tax_rate_priority' => '1', 'tax_rate_compound' => '0', 'tax_rate_shipping' => '1', 'tax_rate_order' => '1', 'tax_rate_class' => '', ); WC_Tax::_insert_tax_rate( $tax_rate ); $tax_item = new WC_Order_Item_Tax(); $tax_item->set_rate( 100 ); $tax_item->set_tax_total( 100 ); $tax_item->set_shipping_tax_total( 100 ); $this->order->add_item( $tax_item ); $this->order->save(); } $this->order->calculate_shipping(); if ( $calc_order_totals ) { $this->order->calculate_totals(); } return $this->paypal_request->get_request_url( $this->order, $testmode ); } /** * Check if the shipping tax is included in the total according to $shipping_tax_included. * * @param array $query_array Request URL parsed into associative array. * @param bool $shipping_tax_included Whether the shipping tax should be included or not. */ protected function check_shipping_tax( $query_array, $shipping_tax_included ) { $shipping_total = $this->order->get_shipping_total(); if ( $shipping_tax_included ) { $shipping_total += $this->order->get_shipping_tax(); } $epsilon = 0.01; $this->assertTrue( abs( $shipping_total - floatval( $query_array['shipping_1'] ) ) < $epsilon, 'Shipping tax mismatch: shipping total=' . $shipping_total . ' vs request shipping=' . $query_array['shipping_1'] ); } /** * Test common order asserts. * * @param string $request_url Paypal request URL. * @param bool $testmode Whether Paypal sandbox is used or not. */ protected function check_order_common_props( $request_url, $testmode ) { if ( $testmode ) { $this->assertEquals( 'https://www.sandbox.paypal.com', substr( $request_url, 0, 30 ) ); } else { $this->assertEquals( 'https://www.paypal.com', substr( $request_url, 0, 22 ) ); } $this->assertLessThanOrEqual( 2083, strlen( $request_url ) ); } /** * Test large order with 30 items, URL length > 2083 characters. * * @param bool $shipping_tax_included Whether the shipping tax should be included or not. * @param bool $testmode Whether to use Paypal sandbox. * @throws WC_Data_Exception Exception on failure. */ protected function check_large_order( $shipping_tax_included, $testmode ) { $request_url = $this->get_request_url( 30, $testmode ); $this->check_order_common_props( $request_url, $testmode ); // Check fields limited to 127 characters for length. $fields_limited_to_127_chars = array( 'invoice', 'item_name_1', 'item_number_1', ); $query_string = wp_parse_url( $request_url, PHP_URL_QUERY ) ? wp_parse_url( $request_url, PHP_URL_QUERY ) : ''; $query_array = array(); parse_str( $query_string, $query_array ); foreach ( $fields_limited_to_127_chars as $field_name ) { $this->assertLessThanOrEqual( 127, mb_strlen( $query_array[ $field_name ] ) ); } // Check that there is actually only one item for order with URL length > limit. $this->assertFalse( array_key_exists( 'item_name_2', $query_array ) ); // Check that non-line item parameters are included. $this->assertTrue( array_key_exists( 'cmd', $query_array ) ); $this->assertEquals( '_cart', $query_array['cmd'] ); $this->check_shipping_tax( $query_array, $shipping_tax_included ); } /** * Return true if value is < 0, false otherwise. * * @param int|float $value Tested value. * @return bool */ protected function is_negative( $value ) { return $value < 0; } /** * Test small order with fewer items, URL length should be < 2083 characters. * * @param int $product_count Number of products to include in the order. * @param bool $shipping_tax_included Whether the shipping tax should be included or not. * @param bool $testmode Whether to use Paypal sandbox. * @param array $product_prices Array of prices to use for created products. Leave empty for default prices. * @param bool $calc_order_totals Whether the WC_Order::calculate_totals() should be triggered when creating order. * @throws WC_Data_Exception Exception on failure. */ protected function check_small_order( $product_count, $shipping_tax_included, $testmode, $product_prices = array(), $calc_order_totals = true ) { $request_url = $this->get_request_url( $product_count, $testmode, $product_prices, $calc_order_totals ); $this->check_order_common_props( $request_url, $testmode ); $query_string = wp_parse_url( $request_url, PHP_URL_QUERY ) ? wp_parse_url( $request_url, PHP_URL_QUERY ) : ''; $query_array = array(); parse_str( $query_string, $query_array ); // Check that there are $product_count line items in the request URL. // However, if shipping tax is included, there is only one item. if ( $shipping_tax_included || array_filter( $product_prices, array( $this, 'is_negative' ) ) || ! $calc_order_totals ) { $product_count = 1; } for ( $i = 1; $i <= $product_count; $i++ ) { $this->assertTrue( array_key_exists( 'item_name_' . $i, $query_array ), 'Item name ' . $i . ' does not exist' ); $this->assertTrue( array_key_exists( 'quantity_' . $i, $query_array ) ); $this->assertTrue( array_key_exists( 'amount_' . $i, $query_array ) ); $this->assertTrue( array_key_exists( 'item_number_' . $i, $query_array ) ); // Check that non-line item parameters are included. $this->assertTrue( array_key_exists( 'cmd', $query_array ) ); $this->assertEquals( '_cart', $query_array['cmd'] ); $this->assertLessThanOrEqual( 127, mb_strlen( $query_array[ 'item_name_' . $i ] ) ); $this->assertLessThanOrEqual( 127, mb_strlen( $query_array[ 'item_number_' . $i ] ) ); } $this->check_shipping_tax( $query_array, $shipping_tax_included ); } /** * Test order with one product having negative amount. * Amount < 0 forces tax inclusion and single line item, since WC_Gateway_Paypal_Request::prepare_line_items() * will return false. * * @param bool $testmode Whether PayPal request should point to sandbox or live production. * @throws WC_Data_Exception Exception on failure. */ protected function check_negative_amount( $testmode ) { $shipping_tax_included = true; $product_prices = array( 6, 6, 6, 6, -3 ); $this->check_small_order( count( $product_prices ), $shipping_tax_included, $testmode, $product_prices ); } /** * Test order with totals mismatched. * This forces tax inclusion and single line item, since WC_Gateway_Paypal_Request::prepare_line_items() * will return false. * * @param bool $testmode Whether PayPal request should point to sandbox or live production. * @throws WC_Data_Exception Exception on failure. */ protected function check_totals_mismatch( $testmode ) { // totals mismatch forces tax inclusion and single line item. $shipping_tax_included = true; $this->check_small_order( 5, $shipping_tax_included, $testmode, array(), false ); } /** * Test for request_url() method. * * @group timeout * @throws WC_Data_Exception Exception on failure. */ public function test_request_url() { // User set up. $this->user = $this->factory->user->create( array( 'role' => 'administrator', ) ); wp_set_current_user( $this->user ); // wc_tax_enabled(), wc_prices_include_tax() and WC_Gateway_Paypal_Request::prepare_line_items() determine if // shipping tax should be included, these are the correct options. // Note that prepare_line_items() can return false in 2 cases, tested separately below: // - order totals mismatch and // - item amount < 0. $correct_options = array( // woocommerce_calc_taxes, woocommerce_prices_include_tax, $shipping_tax_included values. array( 'no', 'no', false ), array( 'yes', 'no', false ), // array( 'no', 'yes', false ), // this is not a valid option due to definition of wc_prices_include_tax(). array( 'yes', 'yes', true ), ); // One test without sandbox. $testmode = false; update_option( 'woocommerce_calc_taxes', 'no' ); update_option( 'woocommerce_prices_include_tax', 'no' ); $shipping_tax_included = false; $this->check_small_order( 5, $shipping_tax_included, $testmode ); // Other tests with sandbox active. $testmode = true; foreach ( $correct_options as $values ) { update_option( 'woocommerce_calc_taxes', $values[0] ); update_option( 'woocommerce_prices_include_tax', $values[1] ); $shipping_tax_included = $values[2]; // Test order with < 9 items (URL shorter than limit). $this->check_small_order( 5, $shipping_tax_included, $testmode ); // Test order with >9 items with URL shorter than limit. $this->check_small_order( 11, $shipping_tax_included, $testmode ); // Test order with URL longer than limit. // Many items in order -> forced to use one line item -> shipping tax included. $this->check_large_order( true, $testmode ); // Test amount < 0. $this->check_negative_amount( $testmode ); // Check order totals mismatch. $this->check_totals_mismatch( $testmode ); } } }