get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table, 'activation should have created it via dbDelta()' ); } wc_edm_is( 'the recorded DB version matches the plugin', WC_EDM_VERSION, get_option( 'wc_edm_db_version' ) ); /* -------------------------------------------------------------- matching -- */ $match = WC_EDM_Helpers::find_email_match( 'exact@company.com' ); wc_edm_is( 'an exact pattern beats the domain pattern that also matches', 'custom', $match ? $match->assignment_type : null ); $match = WC_EDM_Helpers::find_email_match( 'someone@company.com' ); wc_edm_is( 'a domain pattern matches everyone on the domain', 'group', $match ? $match->assignment_type : null ); $match = WC_EDM_Helpers::find_email_match( 'partialperson@elsewhere.test' ); wc_edm_is( 'a partial pattern matches anywhere in the address', '5.00', $match ? $match->custom_discount : null ); wc_edm_ok( 'an unknown address matches nothing', null === WC_EDM_Helpers::find_email_match( 'nobody@nowhere.test' ) ); wc_edm_ok( 'a deactivated assignment is ignored', null === WC_EDM_Helpers::find_email_match( 'switched-off@company.dk' ) ); wc_edm_ok( 'a malformed address matches nothing', null === WC_EDM_Helpers::find_email_match( 'not-an-email' ) ); /* ------------------------------------------------------------------ cart -- */ // wc_load_cart() is WooCommerce's own way of getting a session, a customer and // a cart outside a web request. Without it WC()->cart is null in WP-CLI and // every cart assertion below would quietly skip. wc_load_cart(); wc_edm_ok( 'WooCommerce gives us a cart outside a web request', WC()->cart instanceof WC_Cart ); /** * Empty the cart, set who the customer is, and recalculate. * * @param string $email Billing email. * @param array $products product_id => quantity. */ function wc_edm_cart_for( $email, $products ) { WC()->cart->empty_cart(); WC()->cart->remove_coupons(); WC()->session->set( WC_EDM_Frontend::SESSION_EMAIL_KEY, null ); WC()->customer->set_billing_email( $email ); foreach ( $products as $product_id => $quantity ) { WC()->cart->add_to_cart( $product_id, $quantity ); } WC()->cart->calculate_totals(); } /** * Total of the negative fees on the cart, as a positive number. * * @return float */ function wc_edm_discount_total() { $total = 0; foreach ( WC()->cart->get_fees() as $fee ) { if ( $fee->amount < 0 ) { $total += -$fee->amount; } } return round( $total, 2 ); } wc_edm_cart_for( 'someone@company.com', array( $fixtures['product_in'] => 1, $fixtures['product_out'] => 1 ) ); wc_edm_is( 'a group discount takes 20% off the eligible item only, not the whole cart', 20.0, wc_edm_discount_total() ); wc_edm_cart_for( 'someone@company.com', array( $fixtures['product_out'] => 1 ) ); wc_edm_is( 'a cart with nothing in the group gets no discount', 0.0, wc_edm_discount_total() ); wc_edm_cart_for( 'exact@company.com', array( $fixtures['product_in'] => 1, $fixtures['product_out'] => 1 ) ); wc_edm_is( 'a custom percent applies to the whole cart', 75.0, wc_edm_discount_total() ); wc_edm_cart_for( 'nobody@nowhere.test', array( $fixtures['product_in'] => 1 ) ); wc_edm_is( 'an unknown customer pays full price', 0.0, wc_edm_discount_total() ); wc_edm_cart_for( 'buyer@coupons.test', array( $fixtures['product_in'] => 1 ) ); wc_edm_ok( 'a coupon assignment applies the coupon', in_array( 'ciflat10', WC()->cart->get_applied_coupons(), true ), 'applied: ' . implode( ', ', WC()->cart->get_applied_coupons() ) ); wc_edm_is( 'and the coupon actually discounts the cart', 10.0, round( WC()->cart->get_discount_total(), 2 ) ); /* -------------------------------------------------------------- shipping -- */ WC_EDM_DB::replace_shipping_rules( (int) $fixtures['group_id'], // The admin stores the zone *instance* key ("flat_rate:1"), not the method // id, and the filter compares against the rate id — which is the same // shape. A rule holding a bare method id silently matches nothing, and the // "never leave the customer without shipping" fallback then hands back // every rate, so the misconfiguration looks exactly like no rule at all. array( array( 'shipping_method_id' => 'flat_rate:1', 'action' => 'show', 'sort_order' => 0 ) ) ); $rates = array( 'flat_rate:1' => new WC_Shipping_Rate( 'flat_rate:1', 'Flat rate', 10, array(), 'flat_rate' ), 'local_pickup:2' => new WC_Shipping_Rate( 'local_pickup:2', 'Local pickup', 0, array(), 'local_pickup' ), ); WC()->customer->set_billing_email( 'someone@company.com' ); $filtered = wc_edm()->frontend->filter_shipping_rates( $rates, array() ); wc_edm_is( 'a "show" rule hides every method it does not name', array( 'flat_rate:1' ), array_keys( $filtered ) ); WC()->customer->set_billing_email( 'nobody@nowhere.test' ); $filtered = wc_edm()->frontend->filter_shipping_rates( $rates, array() ); wc_edm_is( 'an unmatched customer sees every method', 2, count( $filtered ) ); WC_EDM_DB::replace_shipping_rules( (int) $fixtures['group_id'], array() ); /* ------------------------------------------------------------------ rest -- */ $routes = rest_get_server()->get_routes(); $ours = array_filter( array_keys( $routes ), static function ( $route ) { return 0 === strpos( $route, '/' . WC_EDM_REST_API::REST_NAMESPACE ); } ); wc_edm_ok( 'the REST routes are registered', count( $ours ) > 0, 'namespace ' . WC_EDM_REST_API::REST_NAMESPACE ); $request = new WP_REST_Request( 'GET', '/' . WC_EDM_REST_API::REST_NAMESPACE . '/groups' ); $response = rest_get_server()->dispatch( $request ); wc_edm_is( 'and they refuse an unauthenticated caller', 401, $response->get_status() ); /* ------------------------------------------------------------ woocommerce -- */ $compatible = \Automattic\WooCommerce\Utilities\FeaturesUtil::get_compatible_plugins_for_feature( 'custom_order_tables' ); wc_edm_ok( 'HPOS compatibility is declared', in_array( plugin_basename( WC_EDM_PLUGIN_FILE ), $compatible['compatible'], true ), 'without the declaration WooCommerce shows this plugin as incompatible with High-Performance Order Storage in wp-admin' ); /* --------------------------------------------------------------- updates -- */ // get_plugin_data() lives in the admin includes, which are not loaded in a // WP-CLI context by default. require_once ABSPATH . 'wp-admin/includes/plugin.php'; $update_uri = WC_EDM_Updater::update_uri(); wc_edm_ok( 'the plugin header names an update source', '' !== $update_uri && 0 === strpos( $update_uri, 'https://' ), 'without "Update URI:" WordPress offers no updates at all — and worse, it falls back to asking wordpress.org about this slug' ); wc_edm_ok( 'the update filter is hooked for that host', has_filter( 'update_plugins_' . wp_parse_url( $update_uri, PHP_URL_HOST ) ) !== false, 'the filter name is built from the host in the header, so a typo in either silently disables updates' ); // The API address is derived from the header rather than configured, so a // change to either has to keep producing a reachable endpoint. $derive = new ReflectionMethod( 'WC_EDM_Updater', 'api_url' ); $derive->setAccessible( true ); wc_edm_is( 'the release API address is derived from the repository address', 'https://gitea.nolimit.dk/api/v1/repos/kw/demo/releases/latest', $derive->invoke( new WC_EDM_Updater(), 'https://gitea.nolimit.dk/kw/demo' ) ); // The forge builds browser_download_url from its own configured root URL, // which is not necessarily the address the outside world reaches it on. This // is the real response gitea.nolimit.dk returned for v1.4.0: a plain-HTTP raw // IP that bypasses the Cloudflare tunnel in front of it. A shop handed that // would be fetching plugin code over an unencrypted connection to an address // that is free to change, so the download address is rebuilt from the // repository address in the plugin header instead. $parse = new ReflectionMethod( 'WC_EDM_Updater', 'parse_release' ); $parse->setAccessible( true ); $parsed = $parse->invoke( new WC_EDM_Updater(), array( 'tag_name' => 'v1.4.0', 'body' => "notes\n\nTested up to: 7.1\nWC tested up to: 11.1.0\n", 'html_url' => 'http://57.129.89.117:25001/kw/woocommerce-email-discount-manager/releases/tag/v1.4.0', 'assets' => array( array( 'name' => 'woocommerce-email-discount-manager.zip', 'browser_download_url' => 'http://57.129.89.117:25001/kw/woocommerce-email-discount-manager/releases/download/v1.4.0/woocommerce-email-discount-manager.zip', ), ), ) ); wc_edm_is( 'the version comes from the tag, without the v', '1.4.0', $parsed['version'] ); wc_edm_is( 'the download address is rebuilt on the host in the plugin header', $update_uri . '/releases/download/v1.4.0/woocommerce-email-discount-manager.zip', $parsed['package'] ); wc_edm_ok( 'so it is never the raw address the forge happened to report', false === strpos( $parsed['package'], '57.129.89.117' ) && 0 === strpos( $parsed['package'], 'https://' ), 'got ' . $parsed['package'] ); wc_edm_is( 'and the tested-up-to line is read out of the release notes', '7.1', $parsed['tested'] ); wc_edm_ok( 'a release with no zip attached is not offered at all', null === $parse->invoke( new WC_EDM_Updater(), array( 'tag_name' => 'v9.9.9', 'assets' => array() ) ) ); // Feed the updater a known release through its own cache instead of the // network: what matters here is the shape handed to WordPress, and a test // that needs the internet fails for reasons that have nothing to do with it. set_transient( WC_EDM_Updater::TRANSIENT, array( 'version' => '99.0.0', 'package' => 'https://gitea.nolimit.dk/kw/demo/releases/download/v99.0.0/demo.zip', 'url' => 'https://gitea.nolimit.dk/kw/demo/releases/tag/v99.0.0', 'tested' => '7.1', ), 60 ); $offered = wc_edm()->updater->offer_update( false, get_plugin_data( WC_EDM_PLUGIN_FILE, false, false ) ); wc_edm_is( 'a newer release is offered as an update', '99.0.0', is_array( $offered ) ? $offered['version'] : null ); wc_edm_is( 'and it carries the folder name WordPress will unpack into', 'woocommerce-email-discount-manager', is_array( $offered ) ? $offered['slug'] : null ); wc_edm_ok( 'the offer names a downloadable package', is_array( $offered ) && ! empty( $offered['package'] ), 'an offer without a package is an update WordPress cannot install' ); // The same version already installed must still be answered, not skipped: // WordPress files that away under no_update, and that list is what the // per-plugin "Enable auto-updates" link on the plugins screen is built from. set_transient( WC_EDM_Updater::TRANSIENT, array( 'version' => WC_EDM_VERSION, 'package' => 'https://gitea.nolimit.dk/kw/demo/releases/download/v1/demo.zip', 'url' => 'https://gitea.nolimit.dk/kw/demo', 'tested' => '7.1', ), 60 ); $offered = wc_edm()->updater->offer_update( false, get_plugin_data( WC_EDM_PLUGIN_FILE, false, false ) ); wc_edm_is( 'the current version is answered too, not passed over', WC_EDM_VERSION, is_array( $offered ) ? $offered['version'] : null ); delete_transient( WC_EDM_Updater::TRANSIENT ); /* --------------------------------------------------------------- verdict -- */ echo "\n"; if ( $wc_edm_failures > 0 ) { printf( "%d assertion(s) failed on WordPress %s.\n", $wc_edm_failures, get_bloginfo( 'version' ) ); exit( 1 ); } printf( "All assertions passed on WordPress %s with WooCommerce %s.\n", get_bloginfo( 'version' ), defined( 'WC_VERSION' ) ? WC_VERSION : '?' );