Commit 16fa8cce56e for woocommerce
commit 16fa8cce56e9f00d5b96e284638a4c340b40ce5f
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Mon Jul 13 18:05:42 2026 +0300
Speed up the PHPUnit suite while strengthening coverage (#66522)
* test: isolate V3 product REST routes
[Context]
V3 product tests dispatch only the production products controller across collection, CRUD, duplicate, and batch behavior.
[Problem]
The generic REST base registered every WooCommerce route and initialized payment gateways before each of 73 tests despite no inherited REST helper or unrelated route usage.
[Solution]
Register the V3 products controller on the shared scoped real server with WordPress defaults and explicitly restore global REST state during teardown.
* test: share V3 product administrator
[Context]
V3 product controller tests authenticate every request as the same administrator role.
[Problem]
Each of 73 methods inserted an unchanged user row even though product mutations and request state are transactionally isolated.
[Solution]
Create the administrator once through the existing WordPress class fixture setup and select that fixture per test while preserving all real authorization and product behavior.
* test: isolate V3 product variation routes
[Context]
V3 variation tests dispatch only nested variation collection, item, and batch routes.
[Problem]
The generic REST base initialized all WooCommerce routes and payment gateways, while the class also created an unused generic products controller that did not own the exercised routes.
[Solution]
Register the concrete production product variations controller on the shared scoped server and explicitly restore global REST state during teardown.
* test: isolate V4 email settings routes
[Context]
Email settings tests initialize one production controller after enabling V4, configuring the email-editor registry, and building its schema.
[Problem]
The generic REST base had already registered every WooCommerce route and initialized payment gateways before that late controller setup on each of 26 tests.
[Solution]
Create the shared scoped real server at the existing late initialization point, register only the email settings controller with WordPress defaults, and restore global REST state during teardown.
* test: isolate V4 payment gateway settings routes
[Context]
Payment gateway settings tests require a freshly initialized production gateway registry plus one injected mock gateway.
[Problem]
The generic REST base coupled that required gateway reset to registration of every WooCommerce route before each of 33 tests.
[Solution]
Preserve the exact gateway clear/init lifecycle locally, then register only the production payment gateway settings controller on the shared scoped server and restore REST state in teardown.
* test: isolate V4 tax settings routes
[Context]
Tax settings controller tests dispatch real REST requests through a WP_REST_Server instance.
[Problem]
Extending the generic REST test case initialized every registered REST route for each test even though this class only exercises the tax settings endpoint.
[Solution]
Use the WooCommerce unit test base and register only the tax settings controller routes while preserving the real server, controller, requests, assertions, user cleanup, and randomized neighboring-controller coverage.
* test: isolate V4 fulfillment routes
[Context]
The V4 fulfillments tests dispatch production requests against a real REST server and controller dependency graph.
[Problem]
The generic REST test base registered the entire application REST API and initialized unrelated gateway state before this class registered the one fulfillments controller it exercises.
[Solution]
Use the WooCommerce unit base and scoped REST helper to register only the production fulfillments controller with WordPress default filters. Keep all users, orders, fulfillments, permissions, cleanup, requests, and assertions unchanged.
* test: preserve fulfillment table transactions
[Context]
WordPress wraps each unit test in a database transaction, and the fulfillment and fulfillment-meta tables use InnoDB.
[Problem]
Truncating both tables after every V4 fulfillment controller test implicitly committed the transaction and reset auto-increment values. This added 48 schema-level operations and made isolation depend on destructive cleanup instead of the test harness.
[Solution]
Let the standard test transaction roll back fulfillment rows. Repeated execution leaves both tables empty while auto-increment values continue advancing, proving the tests remain isolated without relying on reset IDs.
* test: rely on rollback for fulfillment fixtures
[Context]
Each V4 fulfillment controller test creates fresh users, an order, and fulfillment data inside the WordPress test transaction.
[Problem]
The test teardown explicitly deleted both users and the order before the parent rolled the entire transaction back. Those extra writes and deletion hooks duplicated harness cleanup without contributing to any controller assertion.
[Solution]
Clear only the scoped REST server and let the standard parent teardown roll back all per-test fixtures. Repeated execution preserves exact user, order, fulfillment, and meta row counts, while every endpoint and permission assertion remains unchanged.
* test: isolate V4 shipping zone routes
[Context]
The V4 shipping-zone controller tests use a real REST server, production schema, service, permissions, shipping tables, and method settings.
[Problem]
The generic REST base registered every application route and initialized unrelated gateway state so that this one endpoint would be discovered through the V4 feature filter, despite the test already constructing its concrete controller.
[Solution]
Use the WooCommerce unit base and register the initialized production shipping-zone controller directly on the scoped real server with WordPress default REST filters. Preserve feature setup, per-test fixtures, cleanup, requests, and all response assertions.
* test: contain shipping zone feature filter
[Context]
The V4 shipping-zone controller tests enable the REST API V4 feature while exercising route registration and requests.
[Problem]
Setup added an anonymous enable filter before the parent hook snapshot, and teardown added a different anonymous disable filter after parent restoration. In-process instrumentation showed callback count growing from zero to two between selected methods, allowing feature state to leak into neighboring tests.
[Solution]
Use one named feature callback, add it after parent setup, and remove it before parent teardown. The feature remains enabled for every request, while repeated instrumentation shows zero residual callbacks before both methods.
* test: scope product segments to report fixtures
[Context]
The orders-stats segment test creates two sold products and one deliberately unused product, then asserts detailed product and variation totals across hourly intervals.
[Problem]
Its unbounded product segmentation query also returned unrelated bootstrap products as zero-order segments. The class passed only when earlier suite tests happened to delete those catalog rows.
[Solution]
Pass the three dynamically allocated fixture IDs through the production product_includes query argument. Keep all totals, variation roll-up, interval, and unused-product zero-segment assertions unchanged.
* test: align coupon report paid dates
[Context]
The multiple-coupons report test backdates orders into a chosen hourly interval and queries analytics by paid date.
[Problem]
Only date_created was backdated. If the test crossed an hour boundary, completing the orders assigned date_paid in the new wall-clock hour, so the intended previous-hour report correctly returned zero rows.
[Solution]
Assign each order the same explicit fixture timestamp for date_created and date_paid. The unchanged report assertion now passes both natural execution and a forced previous-hour boundary reproducer.
* test: scope Store API route initialization
[Context]
Seventeen Store API route test classes create a real spy REST server and execute the production rest_api_init lifecycle for every method.
[Problem]
With no request route context, WooCommerce conservatively initialized every known REST namespace, including unrelated WC versions, Admin, and Analytics APIs. The 243-test aggregate spent over six seconds on that unnecessary registration.
[Solution]
Temporarily expose wc/store/v1 as the current route while firing the real action, allowing the production namespace loader to skip unrelated known WC APIs. Restore the exact prior query-var state in a finally block so neighboring tests cannot observe the harness context.
* test: define agentic checkout phone fixture
[Context]
Agentic checkout completion tests provide buyer phone data separately from the fulfillment address and expect the session to complete successfully.
[Problem]
With no persisted checkout phone option, stores whose checkout page is not the default block treat Phone as a required address field. The class passed in full-suite order only when another test left the option optional, but failed seven success-path methods from a clean state.
[Solution]
Set the phone field to optional explicitly inside each test transaction before constructing session fixtures. Keep all buyer, address, payment, stock, totals, and error validation assertions unchanged.
* test: restore AdditionalFields transactions
[Context]
AdditionalFields creates products, shipping fixtures, checkout orders, options, sessions, hooks, and Mockery expectations for every method.
[Problem]
Extending Mockery plain PHPUnit TestCase bypassed WordPress transactions and hook snapshots. A 48-test run leaked 194 products and 9 HPOS orders, took 14 seconds, depended on Phone being optional, and skipped WordPress incorrect-usage auditing.
[Solution]
Use WP_Test_REST_TestCase with MockeryPHPUnitIntegration, define the phone fixture transactionally, and declare the deliberately asserted registration notices as expected WordPress incorrect usage. All fixtures now roll back while exact Mockery message checks remain.
* test: reuse scoped Store API setup
[Context]
ControllerTestCase and Mockery-backed Store API tests need the same real spy server and production namespace-scoped route lifecycle.
[Problem]
The proven request-context implementation lived only in the inherited base, leaving AdditionalFields to initialize every REST namespace and inviting duplicated save/restore logic as more classes adopt it.
[Solution]
Extract spy-server creation, wc/store namespace context, real rest_api_init, and exact finally restoration into a PSR-4 test trait. Reuse it from ControllerTestCase and AdditionalFields; the latter falls from 5.310s to 3.842s while remaining transactionally isolated.
* fix(tests): isolate Store API checkout fixtures
[Context] Checkout route tests create products, coupons, users, orders, shipping methods, sessions, options, and hooks for every method.
[Problem] The class inherited from Mockery's plain PHPUnit base, so WordPress transactions and hook restoration never ran. A clean class run leaked 149 product rows and 26 HPOS orders, while later methods depended on state left by earlier tests.
[Solution] Run the class through WP_Test_REST_TestCase with Mockery integration, declare the optional phone fixture explicitly, and finish class cleanup before the parent rollback and global restoration. Repeated runs retain all endpoint assertions while rows roll back and auto-increment values continue advancing.
* fix(tests): reset Store API checkout customer
[Context] Deferred checkout updates persist additional fields on WooCommerce's global session customer object.
[Problem] Destroying the session did not replace that already-mutated object. Checkout::test_put_order_update could therefore leak an order-only field into a later AdditionalFields test even though database transactions rolled back correctly.
[Solution] Rebuild the session-backed customer through WooCommerce's normal cart initialization after destroying each Checkout test session. The minimal reverse-order reproducer and the wider randomized Store API guard now pass without changing endpoint assertions.
* perf(tests): scope Store API checkout routes
[Context] Checkout tests create a real REST server and register their custom checkout routes for every method.
[Problem] Firing rest_api_init without a route context also initializes unrelated WooCommerce REST namespaces, adding repeated setup work that these tests never dispatch.
[Solution] Reuse the Store API test trait to provide the production wc/store/v1 namespace context while retaining WordPress REST initialization and the class's explicit checkout route registrations. The complete class and randomized cross-class guard remain green.
* perf(tests): reuse HPOS tables per class
[Context] OrdersTableDataStoreTests needs permanent HPOS tables because WordPress normally rewrites test-created tables as temporary.
[Problem] setup_cot dropped and recreated four permanent tables before every method. This issued 848 DDL statements and implicitly committed the per-test transaction, costing more than eight seconds in setup alone.
[Solution] Ensure and empty the owned HPOS tables once before the class, without resetting auto-increments, then reuse them inside normal per-test transactions. All data-store assertions and natural-order HPOS neighbor coverage pass while rollback leaves every table empty.
* fix(tests): restore HPOS state before removing guard
[Context] The REST data-store tests allow storage changes while their fixtures are intentionally pending synchronization.
[Problem] Child teardown removed that guard before parent teardown restored the HPOS state captured during setup. Standalone and repeated runs could therefore fail with an out-of-sync storage exception.
[Solution] Keep the guard registered through parent teardown and remove it afterward. All 32 tests pass from the previously failing state, and two repetitions complete all 498 assertions.
* perf(tests): reuse HPOS tables in REST tests
[Context] Both core data-store and REST data-store tests need permanent HPOS tables while running real order operations.
[Problem] The REST class dropped and recreated all HPOS tables for each of its 32 methods, repeating the same implicit-commit pattern removed from the main data-store class.
[Solution] Extract class-level ensure-and-empty behavior into HPOSToggleTrait and use it from both classes. The REST suite keeps all route and order assertions while per-test rows now roll back and auto-increment values are preserved.
* perf(tests): reuse HPOS tables in order queries
[Context] Order query tests create real HPOS orders with varied dates, statuses, customers, and query arguments.
[Problem] Their setup dropped and recreated every HPOS table before each of 27 methods, adding implicit commits and schema work unrelated to query behavior.
[Solution] Initialize and empty HPOS tables once for the class, then keep feature toggles and all order fixtures inside normal per-test transactions. Two complete repetitions pass and rollback leaves every HPOS table empty.
* perf(tests): reuse HPOS tables in capability tests
[Context] Capability tests create real HPOS orders and refunds under different roles and a private order status.
[Problem] Each small capability assertion first dropped and recreated all HPOS tables, making schema setup dominate the class runtime and implicitly commit its transaction.
[Solution] Initialize and empty HPOS tables once for the class while preserving per-test roles, users, statuses, orders, refunds, and sync settings. Two complete repetitions pass and all HPOS tables remain empty.
* fix(tests): scope Store API product collection
[Context] The collection test owns two simple products, one grouped product, and the grouped product's three children.
[Problem] It requested the unrestricted global catalog and hardcoded six results, so bootstrap or preceding product rows caused clean-state and order-dependent failures.
[Solution] Pass the six dynamically allocated fixture IDs through the production include parameter and assert that dynamic count. The real collection response and every product schema assertion remain covered.
* perf(tests): create product images only when needed
[Context] Store API product tests use common simple and grouped product fixtures, while a few focused methods exercise populated, missing, deleted, and category images.
[Problem] Common setup sideloaded two image files before all 41 methods even though only one common-fixture test asserted a populated image. Eighty uploads per class run dominated runtime.
[Solution] Keep common products image-free and sideload one attachment inside the populated-image test. Explicit missing, deleted, and category image fixtures remain unchanged, and the complete class retains all assertions.
* perf(tests): create cart item images only when needed
[Context] Cart item tests share simple and variable products, while two focused methods validate image filtering and warning behavior.
[Problem] Common setup sideloaded two images for every test even though non-image tests only require the image response key and the variable image is never asserted.
[Solution] Keep common products image-free and attach one image to both the stored product and cart-item object only in the two image tests. The focused tests remove their attachment, and all assertions plus the known PHPUnit warning remain.
* perf(tests): scope WooPayments settings routes
[Context] WooPayments settings controller tests initialize per-test service mocks and dispatch real REST requests to one production controller.
[Problem] The generic REST base registered the complete WooCommerce application route surface before the class manually added its own route, making unrelated route setup dominate 62 methods.
[Solution] Use the base unit case's scoped real server with WordPress default REST filters and only the initialized WooPayments controller callback. Per-test mocks, permissions, requests, and response assertions remain unchanged.
* perf(tests): scope WooPayments integration routes
[Context] WooPayments integration tests rebuild container mocks, payment services, gateways, and the production REST controller for every method.
[Problem] The generic REST base first registered every WooCommerce route, even though the test later re-registered one controller after injecting its mocks.
[Solution] Preserve the base payment-gateway clear/init lifecycle explicitly, then create a scoped real server with WordPress defaults and only the late initialized WooPayments controller. Integration behavior and mock isolation remain per-test.
* perf(tests): scope payment settings routes
[Context] Payment settings controller tests inject a mocked Payments service and dispatch real REST requests to one initialized controller.
[Problem] The generic REST base registered every WooCommerce route before the class manually registered its own controller, repeating unrelated setup for 43 methods.
[Solution] Use a scoped real server with WordPress default REST filters and only the initialized payment settings controller. Users, service mocks, permissions, requests, and response assertions remain per-test.
* perf(tests): scope payment integration routes
[Context] Payment settings integration tests rebuild gateways, proxy mocks, services, suggestions, and a production controller for every method.
[Problem] The generic REST base registered all WooCommerce routes before the class reinitialized its controller against the mocked dependency graph.
[Solution] Preserve payment-gateway initialization explicitly and have the late controller initializer create a scoped real server with WordPress defaults. All integration fixtures, gateway behavior, and response assertions remain per-test.
* perf(tests): initialize REST routes on demand
[Context]\nWC_REST_Unit_Test_Case creates a fresh REST server for each test method.\n\n[Problem]\nThe base fixture eagerly registers every WordPress and WooCommerce REST namespace even when a test dispatches requests to only one namespace. This repeats substantial controller construction across the suite.\n\n[Solution]\nUse a request-aware spy server that initializes routes with the production request context on first dispatch. Preserve full route discovery for tests that inspect the route map, guard nested dispatch during registration, and retain the existing payment gateway reset.
* fix(tests): preserve manual REST route setup
[Context]\nSome REST controller tests intentionally register only the controller under test after the shared parent setup.\n\n[Problem]\nDeferred global initialization treated those routes as missing, registered unrelated controllers later, and no longer established WordPress's default REST action state before child setup. This produced registration notices, duplicate routes, and changed focused-controller behavior.\n\n[Solution]\nInitialize only WordPress's default REST filters during parent setup and let already-registered routes dispatch directly. Keep lazy global registration for missing routes, support explicit full initialization, and fall back safely when no WordPress route context exists.
* fix(tests): retain eager REST registration timing
[Context]\nSettings and onboarding tests mutate REST schema and settings filters after their parent fixture is ready.\n\n[Problem]\nDeferring route registration until dispatch made controller construction observe those later mutations. That changed the behavior these tests intentionally exercise compared with the previous eager fixture.\n\n[Solution]\nUse the shared explicit initializer immediately after parent setup in the three timing-sensitive classes. Their routes retain the original registration order while other REST tests continue to initialize only what they request.
* perf(tests): isolate product filter fixtures
[Context]\nFiltererTest creates products, attributes, terms, and lookup rows inside WordPress test transactions.\n\n[Problem]\nIts teardown rolled back first, then queried and deleted every visible product and attribute and truncated the InnoDB lookup table. This repeated global work, reset table state, and made query assertions depend on deleting unrelated bootstrap products.\n\n[Solution]\nScope the simulated catalog query to test-owned product IDs through WooCommerce's production loop_shop_post_in hook. Unregister dynamic taxonomies and reset in-memory caches, while relying on the harness rollback for all database fixtures.
* perf(tests): share checkout session products
[Context]\nCheckout session tests use the same two physical products and one virtual product in every method.\n\n[Problem]\nRecreating those immutable rows for all 60 tests repeats product inserts and metadata writes. Only one test changes stock, and that update already runs inside the per-test transaction.\n\n[Solution]\nCreate the three product rows once through the WordPress class fixture and reload fresh product objects for each method. Keep shipping, routes, authentication, cart, customer, session, and the stock mutation in the per-test lifecycle.
* perf(tests): share cart catalog fixtures
[Context]\nStore API cart tests begin with the same four products, cross-sell relation, and fixed-cart coupon.\n\n[Problem]\nRecreating those unchanged catalog rows for every method repeats product, metadata, relationship, and coupon writes before the cart behavior is exercised.\n\n[Solution]\nCreate the immutable catalog rows once through the WordPress class fixture and reload fresh objects per method. Keep shipping options, cart contents, coupon application, checkout-draft order, customer, and session state in each test transaction.
* fix(tests): reset complete cart customer state
[Context]\nStore API cart tests reuse WooCommerce's in-memory customer object across requests.\n\n[Problem]\nThe fixture reset only cleared a subset of address fields. A customer update left address line 2 populated, causing a later repeated test to observe stale shipping data despite database rollback.\n\n[Solution]\nReset every billing and shipping field that cart requests can mutate while preserving the intended US default country. Repeated tests now start from the same customer state without changing endpoint assertions.
* perf(tests): isolate product filter fixtures
[Context]
Product filter tests build real products, terms, attributes, and lookup rows for every method.
[Problem]
Per-test CREATE TABLE and TRUNCATE statements implicitly committed WordPress's rollback transaction. Cleanup then globally deleted database fixtures and reset table identifiers after every assertion group.
[Solution]
Create the custom table at class scope, clear lookup data transactionally per test, and let WordPress roll back ordinary fixtures. Clean WooCommerce custom tables once after each class so no orphan lookup rows survive.
* perf(tests): preserve lookup fixture transactions
[Context]
Lookup data-store tests create real products and attribute rows while exercising direct and queued update paths.
[Problem]
Fixture-only TRUNCATE calls committed each test transaction, so database rows survived until broad class cleanup. Class-owned attributes were then deleted after that cleanup commit and rolled back at process exit.
[Solution]
Empty lookup rows with transactional DELETE, establish the optimized-update option inside each test transaction, and remove class attributes before WordPress commits class cleanup.
* fix(tests): reset lookup class fixtures
[Context]
PHPUnit can rebuild class fixtures when a test class is repeated in one process.
[Problem]
LookupDataStoreTest appended each new attribute set to static state. A second run still read deleted IDs from indexes zero through two, causing 28 failures against newly registered taxonomies.
[Solution]
Reset the class-owned fixture registry before allocating attributes so every run uses its current non-fixed IDs and terms.
* perf(tests): preserve stock notification transactions
[Context]
Stock-notification tests persist real notification and metadata rows across five focused classes.
[Problem]
Each method truncated both custom tables after WordPress had rolled back, committing test fixtures and resetting generated IDs. Several assertions consequently depended on IDs 1 through 3.
[Solution]
Delete child and parent rows before transactional teardown, then assert persisted and queried IDs against each saved fixture. This retains exact ordering coverage while allowing counters to advance normally.
* perf(tests): reuse synchronizer tables
[Context]
DataSynchronizer tests require permanent HPOS tables, including focused cases that explicitly remove and recreate the schema.
[Problem]
Common setup dropped and rebuilt every HPOS table before all 29 methods. The DDL committed WordPress's test transaction and repeated schema work even when the previous test left the tables intact.
[Solution]
Initialize empty tables once at class scope and retain the existing per-test existence check. Schema lifecycle tests still remove tables, while the following setup recreates them only when required.
* perf(tests): rely on refund fixture rollback
[Context]
V4 refund controller and preview tests create real orders, refunds, tax rows, and options inside WordPress test transactions.
[Problem]
Teardown reloaded and permanently deleted every tracked object before invoking the transaction rollback, adding redundant writes and cache churn.
[Solution]
Reset only the in-memory ownership lists and let WordPress roll back the database fixtures. Feature cleanup remains unchanged, and post-run order and tax tables stay empty.
* fix(tests): isolate lookup filter attributes
[Context]
Product attribute lookup tests require an empty attribute universe and register dynamic taxonomies throughout the class.
[Problem]
WordPress class cleanup does not delete WooCommerce attribute taxonomy rows. A stale size row caused the helper to skip taxonomy registration, producing 77 downstream errors after Filterer unregistered the stale taxonomy.
[Solution]
Remove leaked attribute terms, registrations, and rows once before Filterer class fixtures start. Per-test product and lookup behavior remains unchanged, and stale process state no longer poisons variation fixtures.
* perf(tests): tune wp-env phpunit runtime
[Context]
The isolated wp-env test containers use production-oriented CLI and database durability defaults even though their data is disposable.
[Problem]
PHP CLI left the available OPcache disabled, while MariaDB synced its redo log after every transaction. Both costs apply across the entire sequential PHPUnit suite.
[Solution]
Run wp-env PHPUnit through a dedicated script that enables CLI OPcache and sets InnoDB flush mode 2 on only the test database connection. Normal and HPOS-off npm commands share the runner; watch and non-container workflows are unchanged.
* fix(tests): isolate analytics attribute lookup updates
The analytics attribute-filter tests relied on queued product lookup updates. Pending actions from prior runs can target reused product IDs, suppressing fresh actions and making isolated runs omit attributed simple products.
Enable WooCommerce's synchronous lookup update mode for these fixtures and stop draining the global database-update queue. Product saves still exercise the production lookup path, while results no longer depend on scheduler history or unrelated pending work.
* perf(tests): scope custom attribute report fixtures
The custom attribute report test built generic taxonomy attributes and then materialized their Cartesian product with the custom attribute, despite ordering only two variations.
Create a variable product containing the same five-value custom attribute and persist explicit matching and nonmatching variations. The report still covers both variation orders, an unrelated order, and exact matching-order identity with substantially less database work.
* fix(tests): avoid orphaned product lookup actions
The V4 products controller creates and deletes seven shared products in class-level fixture hooks. Those hooks run outside WordPress test transactions, so asynchronous attribute lookup actions persisted after the products were gone and could collide with reused fixture IDs.
Temporarily expose the direct-update option only while class fixtures are created or deleted, removing the filter in a finally block. This keeps production lookup updates active while leaving no scheduled work or option state behind.
* fix(tests): isolate draft order cleanup storage
The draft-order cleanup tests inherited their harness from the standalone Blocks repository and implicitly relied on whichever order store earlier tests left authoritative. They also created fixtures before setup and deleted every visible order during teardown.\n\nRun the fixtures inside WC_Unit_Test_Case's transaction, explicitly select the CPT store required by the wp_posts assertions, and restore the prior HPOS state before rollback. Teardown now removes only this test's exception listener and relies on transactional cleanup instead of global order deletion.
* fix(tests): restore HPOS after user function tests
The user-function test class enabled HPOS through the shared toggle trait, but its teardown always disabled HPOS after rolling back the test transaction. Every later class therefore inherited a storage mode chosen by test order, and the focused class could also fail when pending synchronization blocked its explicit switch.\n\nCapture and restore the incoming HPOS mode around each test, keep the established storage-change guard scoped to that lifecycle, and restore temporary-table filters before transaction teardown. This preserves both HPOS and CPT scenarios without leaking either mode to neighboring tests.
* fix(tests): resolve the order cache helper
The order-cache test referenced WC_Helper_Order from inside a namespace without importing it. A preceding HPOS state leak kept the test skipped in the full suite, so restoring storage isolation exposed the class-resolution error.\n\nImport the global helper explicitly so the test exercises its real HPOS cache and duplicate-metadata assertions regardless of suite order.
* fix(tests): isolate post order migration state
PostsToOrdersMigrationControllerTest creates post-backed source orders and migrates them into HPOS tables, but it inherited whichever order store previous tests left authoritative. With HPOS active, source lookups returned false; the resulting failures also prevented SQL mode cleanup and caused unrelated scheduler inserts to fail later.\n\nDeclare CPT as the source store for each migration test, restore the incoming HPOS and transaction-option state, and guarantee session SQL mode restoration with finally. The real post fixtures, HPOS target tables, transaction paths, migration verification, and existing assertions remain unchanged.
* fix(tests): transact download permission fixtures
DownloadPermissionsAdjusterTest overrode setUp without invoking the WooCommerce parent, so every product fixture and deferred lookup action escaped transactional cleanup. Four tests also called setUp manually after registering data-store doubles, which would make adding the parent call unsafe on its own.\n\nRun the PHPUnit parent lifecycle exactly once and split SUT construction into a dedicated initializer for tests that replace dependencies. Product and download-permission behavior remains real, while fixture rows and lookup actions now roll back with each test.
* fix(tests): avoid orphaned migration lookup actions
The migration tests deliberately truncate HPOS target tables, which commits unrelated deferred product-lookup actions created by their source-order fixtures. Those pending actions survive after the products are removed and can collide with reused IDs in later tests.\n\nUse WooCommerce's supported direct lookup-update mode only for this test lifecycle. The production lookup callback still runs, while no deferred action can escape the migration tests' intentional transaction boundaries.
* chore(tests): satisfy branch lint
[Context] The PHPUnit harness performance work touched several legacy and modern test files.
[Problem] Branch-level PHPCS found formatting, deprecated call syntax, and intentional test-harness operations that needed explicit rationale.
[Solution] Normalize the affected statements, use the current get_terms argument form, and document the narrowly scoped test-only exceptions. This does not change the exercised behavior.
* perf(tests): narrow order count fixtures
[Context] The order-count test persists a triangular matrix across every registered status for both HPOS and CPT storage.
[Problem] The generic order helper also created products, items, shipping data, billing data, and payment state that the count assertions never inspect. This kept both datasets above the slow-test threshold.
[Solution] Persist the same real order/status matrix directly through wc_create_order. All 34 count assertions remain unchanged while the focused pair falls from 2.805 seconds to about 0.75 seconds.
* perf(tests): reuse refund store tables
[Context] Refund datastore tests require permanent HPOS tables and already run each method inside the WordPress transaction harness.
[Problem] Setup dropped and recreated all HPOS tables after every transaction started, adding repeated schema work and crossing the transaction boundary despite no schema-lifecycle coverage in this class.
[Solution] Create and empty the tables once before class transactions begin, then let rollback isolate each refund scenario. The eight-test class falls from 1.362 seconds to 0.471 seconds with unchanged assertions and empty post-run tables.
* fix(tests): register ability categories on hook
[Context] WordPress requires ability categories to be registered during wp_abilities_api_categories_init.
[Problem] The idempotence test called the registrar directly and depended on an earlier test leaving the hook action count nonzero. Repeating the class removed the category but left the next direct call outside the required lifecycle, causing an order-dependent failure.
[Solution] Invoke both idempotence calls from the required category-init hook. The same category assertions now pass across two complete class repetitions without relying on global action history.
* perf(tests): share abilities administrator
[Context] Every AbilitiesLoaderTest method starts from the same administrator while permission tests explicitly switch to their own users.
[Problem] Recreating that immutable default account for all 104 cases added user-table work unrelated to ability registration and execution coverage.
[Solution] Create the default administrator once through the WordPress class factory and reset it as the current user before every method. Two complete repetitions preserve all 846 assertions while reducing average class runtime by roughly one third.
* chore(tests): normalize branch lint context
[Context] Branch-level PHPCS evaluates the complete committed test-harness diff against trunk.
[Problem] A structural suppression separated the REST base class from its docblock, while one customer assignment inherited spacing from a statement group interrupted by a metadata write.
[Solution] Keep the REST docblock adjacent to its class while narrowly disabling the one-object rule, and align the isolated customer assignment independently. Runtime behavior is unchanged.
* perf(tests): avoid persisting product walker fixtures
Context: ProductWalker tests provide products through a mocked loader and only exercise walker pagination, mapping, validation, feed, and memory behavior.
Problem: Creating 165 persisted simple products added database writes and cleanup for state that the walker never reads.
Solution: Use real in-memory WC_Product_Simple objects with explicit IDs while preserving every page, interaction, and assertion.
* perf(tests): reuse REST customers administrator
Context: The V4 customers controller tests need an administrator only to authenticate each REST request.
Problem: Common setup inserted an identical administrator before all 33 methods even though permission-sensitive scenarios create their own users.
Solution: Create one immutable class-owned administrator and continue resetting the current user before every test.
* fix(tests): avoid database writes in data provider
Context: DownloadsWrapper needs real persisted products to exercise downloadable-product lookup behavior.
Problem: Its data provider created those products during PHPUnit discovery, outside any test transaction. Filtered unrelated suites evaluated the provider without running owner cleanup and inherited two foreign products.
Solution: Return boolean scenarios from the provider and create the same real products inside the consuming test lifecycle.
* fix(tests): avoid orphaned download fixture actions
Context: DownloadsWrapper now creates real products inside each test and deletes them during its explicit cleanup.
Problem: Asynchronous product-attribute lookup deletion callbacks outlived those deleted fixtures and left pending Action Scheduler rows.
Solution: Scope WooCommerce's supported direct lookup-update option through fixture creation and deletion, then restore it before inherited teardown.
* fix(tests): contain product lookup fixture updates
Context: Several WooCommerce test classes create or delete real products outside per-method transactions or after parent teardown.
Problem: Asynchronous product-attribute lookup callbacks survived fixture deletion, leaving 21 pending actions per full run and making clean-state verification depend on reused product IDs.
Solution: Add reusable test-only direct-update scoping and apply it only around the measured class and method fixture lifecycles, including global class cleanup.
* fix(tests): contain cart rounding fixture cleanup
Context: The cart shipping rounding test creates real product, tax, zone, and shipping method fixtures inside the WordPress test transaction.
Problem: Its teardown rolled back first and then attempted product deletion. In full-suite cache state, the rolled-back product could still resolve and enqueue a lookup callback in a new transaction that the next test committed.
Solution: Run the existing fixture cleanup before rollback and guarantee parent teardown with finally, keeping all scenarios and production callbacks intact.
* perf(tests): share checkout catalog fixtures
Context: Store API Checkout tests use the same three products and fixed coupon in every method.
Problem: Recreating immutable catalog rows for all 47 methods repeats product, metadata, and coupon persistence before checkout behavior is exercised.
Solution: Create the catalog once through the WordPress class fixture and reload fresh product objects per method. Stock and coupon mutations remain inside per-test transactions, while routes, shipping, carts, sessions, and customers keep their existing lifecycle.
* fix(tests): contain Store API catalog fixture updates
Context: Cart, Checkout, and CheckoutSessions keep immutable products in WordPress class fixtures to avoid recreating catalog rows for every method.
Problem: Class setup commits its transaction, so asynchronous product-lookup inserts can survive the suite. Historical pending actions masked this until Checkout changed product ID allocation.
Solution: Create class-owned Store API products through a shared helper that runs the production lookup callback synchronously and always restores the configured update mode.
* perf(tests): reuse refunds REST administrator
Context: The V4 refunds controller tests need an administrator only to authenticate REST requests.
Problem: Common setup inserted the same immutable administrator before each of the 61 test methods, adding database work unrelated to refund behavior.
Solution: Create the administrator through WordPress's class fixture lifecycle and reset the current user to it before each test. Orders, products, refunds, requests, and assertions remain method-scoped.
* perf(tests): reuse refunds order product
Context: Generic V4 refund order fixtures need the same immutable ten-dollar product, while custom line-item tests supply their own products.
Problem: The helper created and deleted a product for every order, including an entirely unused product when callers replaced line_items.
Solution: Reuse one class-owned product for default order payloads, skip it for caller-provided line items, and delete it through the WooCommerce data store under synchronous lookup updates so no lookup rows or actions survive.
* fix(tests): contain refunds product lookup updates
Context: The V4 refunds suite creates purpose-built products throughout the class in addition to its shared generic product.
Problem: Full-order product IDs exposed 43 asynchronous attribute-lookup callbacks that survived after their products were rolled back or deleted, while focused runs were collision-masked by historical actions.
Solution: Scope WooCommerce's supported synchronous lookup-update mode to class-product setup/cleanup and each method after WordPress hook restoration. The production callback still runs for every product operation without leaving orphan actions.
* fix(tests): restore product collection transactions
Context: Product collection handler, query-builder, REST, and SQL-generation tests override setup to construct block mocks; several persist real WooCommerce products.
Problem: The four classes skipped their WordPress parent setup, so tests ran outside the harness transaction and hook restoration. Product lookup actions could survive class cleanup, with historical product IDs masking leaks through Action Scheduler deduplication.
Solution: Run parent setup before constructing each block mock. Existing fixtures and assertions remain unchanged while WordPress restores database and hook state after every method.
* perf(tests): reuse products REST administrator
Context: The V4 products controller tests need an administrator only to authenticate their default REST requests.
Problem: Common setup inserted the same immutable administrator before each of 89 methods, adding database work unrelated to product behavior.
Solution: Create the administrator once through the existing class fixture lifecycle and reset the current user before every test. Permission-specific users, products, requests, and assertions remain method-scoped.
* perf(tests): reuse Store API product fixtures
Context: Store API product tests need the same two simple products and grouped catalog for their common endpoint assertions.
Problem: Setup recreated that six-row catalog before all 41 methods, while only one test mutates a shared product and WordPress bulk cleanup can orphan WooCommerce lookup rows.
Solution: Create the catalog once under synchronous lookup updates, reload top-level products before each method, and delete every owned product through WooCommerce data stores after the class. Purpose-built method fixtures and all assertions remain unchanged.
* perf(tests): reuse checkout field products
Context: Additional checkout field tests need the same physical, virtual, and downloadable products only as cart inputs.
Problem: Setup recreated four immutable products before all 48 methods, and class fixtures require WooCommerce-aware deletion to avoid orphan lookup rows.
Solution: Share product IDs across the class, reload objects per method, and centralize data-store teardown in the Store API fixture trait. Shipping, payment, session, field, cart, request, and assertion lifecycles remain method-scoped.
* perf(tests): reuse orders REST administrator
Context: V3 Orders controller tests and their HPOS subclass need an administrator only for default request authentication and order ownership.
Problem: Parent setup inserted the same immutable administrator before every method, and the HPOS subclass did not chain parent class-fixture setup.
Solution: Create one class-owned administrator, reset the current user before every test, and explicitly chain HPOS class setup before creating permanent tables. Orders, storage toggles, requests, and assertions remain method-scoped.
* fix(tests): clean up Store API cart products
[Context] The Store API Cart tests share four immutable products across the class to avoid recreating the catalog for every test method.\n\n[Problem] WordPress class cleanup removed the product posts directly but left their WooCommerce product lookup rows behind. Full-suite ordering masked that residue when later tests cleared the table.\n\n[Solution] Delete the class-owned products through the WooCommerce data stores during class teardown, matching the neighboring Store API fixture owners and keeping lookup maintenance synchronous.
* fix(tests): clean up checkout class products
[Context] The Store API Checkout and CheckoutSessions tests share immutable product catalogs across their test methods.\n\n[Problem] Generic WordPress class cleanup removed the product posts but left WooCommerce product lookup rows behind. Natural full-suite ordering masked this state leak when later tests cleared the lookup table.\n\n[Solution] Delete each class-owned catalog through WooCommerce data stores during class teardown, matching the fixture helper's explicit ownership contract.
* perf(tests): avoid redundant order item save
[Context] WC_Helper_Order builds persisted orders used by more than one hundred test files.\n\n[Problem] The helper saved each product item before adding it to the order, even though add_item() deliberately defers persistence until the subsequent order save. This wrote the item once without its final parent state and then updated it again.\n\n[Solution] Let the order save persist the newly added item in one lifecycle. The resulting order and item IDs remain unchanged from callers' perspective while high-volume fixture tests avoid two database writes per order.
* perf(tests): avoid duplicate order total saves
[Context] The multiple-interval analytics test builds 72 real orders covering product, coupon, status, customer, and item-count permutations.\n\n[Problem] Coupon orders were fully recalculated and saved inside apply_coupon(), then recalculated and saved again, followed by another explicit save. No-coupon orders also explicitly saved after calculate_totals() had already persisted them.\n\n[Solution] Use apply_coupon() as the single production calculation path for coupon orders and calculate_totals() for no-coupon orders. This preserves every persisted fixture and assertion while removing 2,580 duplicate database queries.
* perf(tests): avoid duplicate image metadata generation
The duplicate-SKU cleanup regression created both its setup image and its rollback image through the full media metadata pipeline. Only the failed upload exercises the behavior under test, making the first generation pure fixture overhead.\n\nCreate a valid local attachment for the original product and continue passing it through the REST endpoint by ID. The failed request still performs the real source upload, duplicate-SKU rejection, and orphan cleanup while the original attachment survival assertion remains intact.
* perf(tests): reuse product filter data fixtures
FilterDataTest rebuilt an immutable product, taxonomy, lookup, and review catalog for every method, making fixture setup about 94% of the class runtime. Moving that data outside transactions initially exposed asynchronous lookup actions, so class reuse must own the complete lifecycle.\n\nExpose narrow fixture lifecycle helpers from the abstract test and reuse them only for FilterDataTest. Re-register dynamic taxonomies inside each transaction, run lookup updates synchronously during class creation and deletion, and restore option state so the shared catalog remains isolated.
* perf(tests): reuse products within stock transitions
The stock transition matrix created a new managed-stock product for every independent order, even though product creation is not part of the behavior under test. This consumed substantially more time than the intentionally retained cart and checkout path.\n\nReuse one product within each test method and reset its quantity before creating the next order. Orders, status hooks, item saves, notes, and all transition combinations remain separate, while the existing per-test transaction preserves isolation.
* perf(tests): reuse query clause catalog
The query-clause data-provider cases rebuilt the same product, variation, taxonomy, and attribute catalog for every assertion. That repeated database setup dominated the class runtime even though the tests only read the fixtures.\n\nMove the proven class-fixture lifecycle into the shared product-filter test base and let read-only suites opt into it. Creation and deletion still execute WooCommerce lookup callbacks synchronously, options are restored, and each test reloads objects inside its own transaction.\n\nThis reduces the QueryClauses test from 1.629 seconds to 0.346 seconds while preserving all query paths and cleanup guarantees.
* perf(tests): reuse parameter test catalog
ParamsTest rebuilt the complete product-filter catalog for every parameter and cache assertion even though the test methods only inspect taxonomy and attribute registration.\n\nOpt the class into the shared immutable catalog lifecycle. Method-owned taxonomies and the Params static cache remain isolated and reset for every test.\n\nThis reduces the class from 1.503 seconds to about 0.316 seconds while preserving all 13 tests and exact database cleanup.
* perf(tests): reuse filter cache catalog
The cache-cap tests recreated the full product-filter catalog for each transient and filter assertion even though they only read product data.\n\nOpt the class into the shared immutable catalog lifecycle. Cache filters and transients remain method-scoped and are still removed during teardown.\n\nThis reduces the class from 0.650 seconds to 0.299 seconds with unchanged assertions and exact cleanup.
* perf(tests): defer refundability item saves
Refundability scenarios saved ten new product, fee, and shipping items with order ID 0 immediately before adding them to an order and saving that order. The second persistence was redundant because the order data store assigns IDs and saves new items.\n\nLet each order save persist its new items once. Every item is still saved before its ID is used by a refund or assertion, preserving the full status, tax, total, refund, and REST response matrix.\n\nThis removes deterministic database writes and reduces the class from 1.352 seconds to 1.157-1.208 seconds.
* perf(tests): scope CPT storage mode to class
Every CPT order data-store test disabled HPOS and restored it independently, causing 84 feature transitions, option writes, cache flushes, and data-store reloads around a class that never changes storage mode.\n\nSnapshot the incoming authoritative store once, use CPT for the class, and restore the original store after class teardown. Per-test transactions, COGS cleanup, filters, orders, and assertions remain isolated.\n\nThis reduces the 42-test class from 2.306 seconds to 1.736-1.878 seconds and restores HPOS before neighboring data-store tests.
* perf(tests): reuse review scheduler product
Generic scheduler scenarios created the same reviewable product 16 times even though each test only changes its own order, scheduler action, email state, and status transitions.\n\nCreate one class-owned reviewable product for the generic order helper while preserving purpose-built disabled and mixed products. Lookup callbacks run synchronously and teardown deletes the fixture through WooCommerce's data store.\n\nThis reduces SchedulerTest from 1.471 seconds to 1.186-1.204 seconds with no product or scheduled-action residue.
* perf(tests): reuse collection data products
ProductCollectionData recreated the same two priced products and reviews for every route and schema assertion. Only one test changes their taxonomy relationships, and those mutations remain inside its per-test transaction.\n\nCreate the immutable products and reviews once through the established Store API fixture lifecycle, then reload products by ID for each method. Purpose-built variable products, attributes, spies, and requests remain method-scoped.\n\nThis reduces the class from 0.931 seconds to 0.421-0.447 seconds with exact product, review, term, and lookup cleanup.
* perf(tests): reuse variable product data store fixture
Context: The variable product CPT data-store tests exercise tax, transient, metadata, hook, and object-cache behavior against the same standard variable catalog. WordPress already wraps each method in a transaction and restores hooks and caches between methods.
Problem: Data-provider expansion recreated one parent and six variations 21 times, spending most of the class runtime on identical fixture persistence and consuming auto-increment IDs that the assertions do not use.
Solution: Create the standard variable product once at the class boundary, reload it for each test transaction, and delete it through WooCommerce with synchronous lookup updates. This preserves every method mutation and assertion while keeping product, transient, lookup-table, and scheduler residue unchanged.
* perf(tests): reuse shopper list fixtures
Context: Shopper Lists route tests use the same immutable product, customer accounts, and feature flag while saved-list state is written to user meta inside each test transaction.
Problem: Recreating those fixtures for all 22 methods repeated 66 object inserts. The old teardown also deleted users and the feature option after the parent had rolled back the transaction, so cleanup did not belong to the test transaction.
Solution: Own the immutable fixtures at the class boundary, reload the product per method, and restore the incoming feature option after explicit WooCommerce product cleanup. User-meta mutations, carts, nonces, requests, and assertions remain isolated per test.
* perf(tests): reuse cart item catalog
Context: CartItems tests use the same simple product and global-attribute variable catalog while carts, invalid products, image attachments, filters, and requests are method-specific.
Problem: Rebuilding the parent products, taxonomies, terms, and variation for all 11 methods repeated persistence unrelated to most assertions. Class creation also needs valid synchronous lookup data so it cannot leave scheduler actions behind.
Solution: Create the real catalog once, align its owned term slugs with the existing variation values, and explicitly delete all owned products and taxonomies at class teardown. Each test still reloads products and exercises its original cart and image behavior.
* perf(tests): reuse Store API order fixtures
Context: Store API Order authorization tests use the same immutable product and two customer identities while creating a distinct order for each access-control scenario.
Problem: Recreating those three fixtures for all 16 methods repeated persistence unrelated to the authorization behavior. User cleanup also ran after the parent test transaction had already rolled back.
Solution: Own the product and customer accounts at the class boundary and reload the product per method. Guest/customer orders, billing data, current-user changes, requests, and assertions remain isolated inside each test transaction.
* perf(tests): reuse checkout session products
Context: Agentic checkout completion tests use the same two products while rebuilding shipping, sessions, customer state, authentication, gateways, routes, and requests per method.
Problem: Persisting both products for all 14 tests repeated catalog writes unrelated to most checkout assertions. Two stock scenarios mutate the first product, but those changes already run inside WordPress's method transaction.
Solution: Create the products once and reload them after each transaction starts. Stock changes still use real product saves, while all session, checkout, payment, and authentication behavior remains method-scoped.
* perf(tests): keep admin notes transactional
The Admin Notes fixture helper truncated both tables after WordPress had started each test transaction. MySQL implicitly committed those transactions, forcing durable fixture churn and making requests depend on IDs resetting to 1 through 4.\n\nUse transactional deletes and return the actual IDs created by the unchanged four-note fixture. The API tests now address those IDs directly, so rollback isolation remains active and advancing auto-increment values cannot change behavior.
* perf(tests): keep shipping zones transactional
The shared shipping-zone helper truncated three tables after WordPress started each test transaction. That implicitly committed test state and forced both callers to assume fixture IDs 1 through 4.\n\nUse transactional deletes and return the IDs of the same four zones. Both shipping-zone test classes now address their actual fixtures, preserving rollback isolation while retaining all zone, location, method, mutation, and matching coverage.
* perf(tests): rely on fulfillment transaction cleanup
The data-store hook tests rolled back their WordPress transaction and then truncated both fulfillment tables after every method. Those DDL statements were redundant, erased class-external state, and accounted for nearly half the class runtime.\n\nRemove the teardown override so the existing per-test transaction owns cleanup. All fulfillment and metadata writes, hooks, exceptions, mutations, and assertions remain method-scoped and unchanged.
* perf(tests): isolate downloadable product fixtures
DownloadsWrapper truncated the product lookup table during teardown, committing deletion of products owned by earlier test classes. Its empty-store assertions therefore depended on destructively altering global catalog state.\n\nClear visible products and lookup rows after each method transaction starts, then let WordPress rollback restore incoming state and remove the method fixture. The same real products, production queries, direct lookup callbacks, caches, and assertions remain exercised.
* fix(tests): scope fulfillment REST cleanup
The REST controller test tracked every class-owned order but truncated the global fulfillment tables during class cleanup. That removed unrelated fulfillments and metadata outside the fixture's ownership.\n\nUse the production data store to hard-delete fulfillments for each class-owned order before deleting the order. The complete 110-fulfillment REST fixture and all request, permission, mutation, and response coverage remain unchanged.
* perf(tests): reuse order log products
Order log cleanup tests created a new simple product for every otherwise-identical order, even though the processor only reads and removes order log metadata. This repeated unrelated catalog persistence across HPOS and CPT datasets.\n\nCreate one method-owned product and pass it through the unchanged full order helper. Every order still has a real line item, shipping data, storage-mode behavior, log metadata, synchronization path, and batch assertion.
* perf(tests): reuse order function products
Order function query tests created a separate simple product for every complete helper order, although they varied and asserted order fields rather than catalog identity. Multi-order methods therefore repeated unrelated product persistence.\n\nRoute helper orders through a lazy per-test product fixture while preserving purpose-built products passed explicitly. Every order still contains real items, shipping, payment, address, storage, query, refund, note, and coupon behavior.
* fix(tests): clean up customer report product fixture
The customer reports controller suite creates a shared product outside method transactions. Its deferred attribute lookup action was masked whenever the product ID matched historical scheduler data, and generic WordPress class cleanup could leave the WooCommerce product lookup row behind.\n\nRun lookup maintenance synchronously while creating the fixture, then delete the class-owned product through WooCommerce's data store under the same mode. This keeps the real product and report coverage while making scheduler and lookup-table cleanup independent of auto-increment history.
* perf(tests): reuse report import users
The report import suite recreated the same administrator and customer for every method even though their identities are stable and only one transactional test updates customer metadata.\n\nCreate both users through the WordPress class factory and assign their IDs during method setup. Orders, products, queue processing, analytics mutations, requests, and assertions remain method-scoped while avoiding twelve redundant user fixtures.
* fix(tests): keep order migration cleanup transactional
The posts-to-orders migration tests truncated HPOS tables after creating CPT fixtures. MySQL committed the surrounding WordPress test transaction, so generated users and customer analytics rows survived until class cleanup and left orphan lookup data.\n\nDelete the same table contents transactionally instead. Migration coverage and table-empty semantics remain unchanged, auto-increment values are no longer assumed, and normal test rollback now owns every generated fixture.
* perf(tests): initialize recovery mailer on demand
The abandoned-cart recovery suite rebuilt every WooCommerce email before each method even though only its registration test reads the global mailer. The remaining tests construct the concrete recovery email directly and exercise its real trigger, send, order, unsubscribe, and hook behavior.\n\nInitialize the production mailer in the registration test that requires it. This keeps registration coverage explicit while avoiding 46 unrelated global mailer rebuilds.
* perf(tests): Reduce order review fixture setup
The submission-handler tests used the broad REST order helper even though they immediately discarded its default product line. That helper also wrote shipping settings and created shipping and payment fixtures unrelated to review submission behavior.\n\nBuild a minimal persisted order locally with the same customer, billing identity, completed status, real products, and order items. This keeps the production handler and comment paths intact while avoiding unrelated database work.
* perf(tests): Reuse the V2 orders API user
The V2 orders REST suite inserted an identical administrator before every test, including route, schema, invalid-ID, and unauthenticated scenarios. The user is immutable and only supplies request permissions.\n\nCreate that administrator once through the WordPress class fixture factory and assign its ID during per-test setup. Orders, products, shipping, payment data, endpoint dispatches, and assertions remain method-scoped.
* perf(tests): Initialize product imports on demand
The product CSV importer suite parsed the full sample and cleared approved download directories before every test, even though nine tests never used that importer instance. This added database and parsing work to key, path, and validation checks.\n\nCreate the full importer and reset its authorization registry only in the two tests that execute complete imports. The real admin and shop-manager imports, image handling, products, permissions, and assertions remain unchanged.
* fix(tests): Refresh flat rate shipping in cart setup
The discount-and-tax cart test updated flat-rate settings but relied on the process-global shipping registry still being cold. In randomized order, an earlier cart test could leave instantiated methods behind, dropping shipping from the asserted total.\n\nInvalidate shipping rates and reload the real shipping methods after writing the fixture settings. This preserves the production calculation while making the test independent of method order.
* perf(tests): Reuse products across cart tax modes
The issue 11626 regression test inserted the same eight price fixtures once for a VAT-exempt customer and again for a normal customer. The products are immutable; only customer tax state and cart contents differ between the two calculations.\n\nCreate the eight real products once and re-add them after emptying the cart. Both tax modes, the coupon, tax rate, production totals calculation, distinct prices, and assertions remain unchanged.
* perf(tests): Reduce order report filter fixtures
The product-attribute report test created ten unattributed filler orders even though it does not exercise a pagination boundary. Those orders only enlarged the unfiltered and negative result sets around the three semantically relevant fixtures.\n\nKeep two filler orders and derive expected totals from that count. The test still covers multiple positive and negative rows, taxonomy-backed simple and variation products, every malformed filter, analytics synchronization, and both filter directions.
* perf(tests): Rely on rollback for product importer cleanup
The product importer test class already inherits WC_Unit_Test_Case's per-test database transaction lifecycle.
Running extra SKU, category, and tag scans before and after every test duplicated that cleanup and added avoidable database work.
Remove the redundant cleanup helper and rely on the parent rollback while preserving all importer fixtures and assertions.
* fix(tests): Isolate install role creation
The role creation test ran the full WooCommerce uninstaller even though it only needed the customer and shop manager roles to be absent.
Dropping tables implicitly committed the test transaction. Class teardown then committed the installation lock during table recreation and left its deletion to roll back, causing persistent lock rows and duplicate-key errors in later runs.
Use the production role-removal routine as the exact precondition and remove the unused full-uninstall helper. This preserves real role creation coverage without mutating unrelated tables and options.
* perf(tests): Use lightweight product image fixtures
The product image rendering tests used media_sideload_image even though remote upload behavior is outside their scope and covered by dedicated REST tests.
That path performed HTTP interception, file copying, and image subsize generation before each direct and inherited image assertion.
Create a real PNG attachment with the known fixture metadata instead. Product image resolution, parent fallback, dimensions, sizes, classes, persistence, and attachment cleanup remain covered without unrelated media processing.
* perf(tests): Use lightweight Store API images
Store API product and cart image tests used media_handle_sideload for a local fixture even though they only assert attachment-backed response and filter behavior.
That helper copied files and generated image metadata and subsizes four times, adding unrelated media processing to five API tests.
Replace it with a real PNG attachment carrying the known fixture metadata and rename the helper to reflect its purpose. Image objects, deleted-image handling, category responses, cart filters, logging, and cleanup remain covered.
* fix(tests): Start dashboard setup transaction first
Dashboard setup tests changed the default country and inserted their administrator before the parent test case started its database transaction.
The users were eventually removed by broader cleanup, but the JP country option persisted and silently influenced every downstream test.
Start the parent lifecycle first so all dashboard-specific users and options roll back to their incoming values while preserving the real widget and task-list behavior.
* fix(tests): Restore remote extension test state
Remote free extension tests called the parent teardown before their own cleanup, then hard-coded the store country to US outside the test transaction.
That value persisted after the class and became the full suite's final country, influencing later location-sensitive tests.
Run local transient and filter cleanup before parent teardown and rely on transaction rollback to restore the incoming country instead of writing a replacement value.
* fix(tests): Restore complete payment country option
The payment settings integration fixtures captured only the base country before their class-level tests. Restoring that projected value into the full default-country option discarded the state and leaked US over the installed US:CA value for every downstream test.\n\nCapture the exact stored option instead, so class cleanup restores the complete country-and-state value while preserving all existing endpoint fixtures and assertions.
* fix(tests): Restore complete mobile messaging country
The mobile messaging fixture captured only the base country before its class-level tests. Restoring that projected value into the full default-country option discarded the state and leaked US over the installed US:CA value for every later test.\n\nCapture the exact stored option so class cleanup restores the complete country-and-state value while preserving all mobile messaging scenarios and assertions.
* perf(tests): Reduce Orders Stats customer fixtures
The multiple-interval report test duplicated its complete product, coupon, status, and item-shape matrix for two customers even though it never filters by customer identity. Those 36 extra full orders spent most of the test runtime in order persistence hooks.\n\nUse one customer for the full remaining Cartesian matrix. Its first order still covers the new-customer path and its subsequent orders cover returning customers, while all 36 aggregate and filter assertions continue to exercise real analytics synchronization.
* perf(tests): Reuse PageController HPOS tables
The PageController tests dropped and recreated permanent HPOS tables after the WordPress per-test transaction started. Besides repeating DDL for every method, those implicit commits weakened rollback isolation.\n\nCreate the HPOS tables once for the class, restore the incoming storage state after the class, and keep the per-method CPT-to-HPOS toggles that exercise both screen-detection paths.
* fix(tests): Isolate order ID search fixtures
The order ID list-table test used complete helper orders whose default phone and postcode are also searched by wc_order_search(). A generated numeric order ID could therefore occur inside that metadata and make the exact-count assertion depend on table auto-increment history.\n\nCreate real address-empty orders for this ID-specific scenario. This keeps the production order CRUD and numeric search paths plus all negative-result assertions, while ensuring unrelated searchable metadata cannot legitimately match the target ID.
* perf(tests): Reuse order address search fixture
The address-field search test created a complete order, product, items, and shipping setup for each of sixteen field values even though every query only needs one uniquely searchable positive order and one negative order.\n\nPopulate all sixteen unique address values on one real order, save its search indexes once, and run the same field-specific production queries. All thirty-two positive and negative assertions remain while avoiding fifteen redundant full fixtures.
* fix(tests): Clean up fulfillment import actions
The fulfillment REST class creates eleven shared orders outside method transactions. Their analytics import actions survived class teardown even after the owned orders were deleted, while historical pending actions could hide the leak through scheduler deduplication.\n\nUnschedule the exact import action for each class-owned order during its existing cleanup. This preserves normal production scheduling and every fulfillment assertion while making final scheduler state independent of order IDs and prior database history.
* perf(tests): Reuse HPOS tables in user function tests
Each user-function test dropped and recreated the permanent HPOS tables, including the CPT-only download case and the user-meta-only last-active case. The recurring DDL dominated all three methods and could commit transaction state.\n\nCreate and empty the HPOS tables once before method transactions, restore the incoming authority immediately, and keep the existing per-method HPOS/CPT toggles. This preserves both storage-mode scenarios and every fixture/assertion while removing redundant table rebuilds.
* perf(tests): Avoid sample image filename scans
[Context] The HTTP test harness streams a shared local image fixture through WordPress\x27s real sideload and attachment pipeline.
[Problem] Native unique filename generation scans the uploads directory for every fixture upload, adding roughly 350ms to product and variation REST tests without covering behavior those tests assert.
[Solution] Supply a fixture-prefixed UUID through WordPress\x27s unique filename callback only for the pending mocked sample download. Remove the temporary override immediately after use and defensively during teardown, preserving upload validation, collision safety, attachment handling, and cleanup.
* perf(tests): Trim CSV importer role fixtures
[Context] The CSV importer role tests verify how an administrator and a shop manager handle one downloadable product from an unapproved location.
[Problem] Both tests imported all seven catalog rows even though five rows were unrelated to the role-specific authorization behavior, adding product, term, image, and persistence work to each run.
[Solution] Limit the role-test importer to one normal control row and the downloadable row. Assert the approved-directory state directly for both roles, preserving the real parser and import path while increasing authorization coverage.
* perf(tests): Remove tracker order line items
[Context] Tracker tests build 76 real orders to verify order aggregation and first/last snapshot data.
[Problem] Every order also created a product line item and recalculated totals solely to produce the fixed $10 total asserted by tracker output. This added unrelated product, order-item, and duplicate save work.
[Solution] Persist the fixed order total directly while retaining every order, status, date, gateway, creation source, production tracker query, and assertion. The fixture now matches the tracker behavior under test without exercising totals calculation.
* perf(tests): Isolate payment gateway notifications
The payment gateway notification test initialized the gateway registry twice, which registered duplicate settings callbacks while its assertions only inspected the final callback. It also loaded provider-specific payment settings details even though that integration has its own focused test suite.
Use the constructor's initialization once, replace provider-detail enrichment with an expectation-backed mock, and assert exact logger and action cardinality for all built-in gateways. Real option writes, transactional email dispatch, logging, activation hooks, and the Tracks entry point remain exercised.
* fix(tests): Clean imported image fixtures
WordPress database rollback removes imported attachment records but does not remove their physical upload files. The CSV role tests and onboarding sample import therefore left 28 new files after every full suite run, steadily increasing filename scans and coupling runtime to test history.
Track attachment IDs created by each test class and force-delete only those attachments before the database transaction rolls back. This preserves the real image import paths while cleaning originals and generated derivatives through WordPress APIs.
* fix(tests): Delete class-owned import actions
Two test classes create shared orders outside method transactions. Their analytics import actions survived class cleanup: customer reports left completed actions, while fulfillment cleanup only canceled its actions and added more durable scheduler history.
Track exact import action IDs while each class builds its shared orders, then delete only those IDs through Action Scheduler during class teardown. This keeps production scheduling and every shared fixture while preventing scheduler rows and logs from accumulating across suite runs.
* perf(tests): Isolate plugin installation
The plugin API test downloaded Hello Dolly from WordPress.org and left the installed files behind. This made the first run slow and network-dependent while later runs bypassed the actual installer.
Use a local package through WordPress's upgrader filters, suppress unrelated update callbacks, and remove the owned plugin after every test. The endpoint still exercises real Plugin_Upgrader extraction and installed-plugin response handling.
* perf(tests): Isolate invalid theme API failure
The invalid-theme endpoint test contacted WordPress.org on every run even though it only verifies WooCommerce's API-error branch. This made a deterministic controller path slow and network-dependent.
Return a controlled themes_api error while asserting the exact action, slug, and call count. Authentication, REST dispatch, sanitization, and WooCommerce response conversion remain real.
* perf(tests): Exercise Blueprint theme filtering locally
The Blueprint theme test sent an empty slug list to the live Theme API and passed only because the request failed. This skipped the successful filtering and cache path while adding external latency.
Use associative theme fixtures, a successful API expectation, and a serializable theme stub. The test now excludes a custom theme, verifies active ordering, exercises transient persistence, and cleans the owned transient.
* perf(tests): Reuse analytics filler product
The Orders Stats attribute-filter test created ten identical products solely to populate ten unrelated filler orders. The assertions depend on the separate orders and page-size pressure, not distinct product identities.
Reuse one real unrelated product across all ten orders. Attribute partitions, totals, analytics imports, REST requests, and the complete assertion set remain unchanged.
* perf(tests): Add new migration metadata directly
The variation gallery batching fixture used update_post_meta for 251 keys known not to exist, forcing an unnecessary existence read before every insert.
Use add_post_meta for those setup-only rows. The same 251 variations still exercise the 250-row boundary, remainder batch, production migration writes, and complete assertion set.
* perf(tests): Seed stock stats lookup rows directly
The stock stats endpoint test created complete WooCommerce products even though its data store reads only published posts and product lookup stock columns. Those unrelated metadata, taxonomy, hook, and lookup-generation writes dominated the fixture cost.\n\nCreate the same 20 published products through the transactional WordPress factory and insert their required lookup values in prepared batches. This preserves the REST, SQL count, cardinality, and cache-refresh behavior while leaving product CRUD coverage to its dedicated tests.
* perf(tests): Reduce variation pagination fixture
The pagination test created three identical variable-product catalogs even though a neighboring test already covers variations across multiple parents. Twelve extra variations and two parent products exercised no additional pagination behavior.\n\nKeep one complete six-variation product, which still spans three pages at two items per page. The real REST query, response size, total headers, and page calculation remain covered while cross-parent behavior stays in its dedicated test.
* perf(tests): Reuse HPOS tables in order note tests
Both order-note storage datasets dropped and recreated the permanent HPOS tables inside per-test setup. That transaction-breaking DDL dominated and destabilized tests whose behavior is adding a real note and persisting its metadata.\n\nCreate and empty the HPOS tables once before method transactions, restore the incoming authority immediately, and keep the independent CPT/HPOS toggle for each dataset. Both storage paths and all real order, comment, and metadata writes remain covered without recurring table rebuilds.
* perf(tests): Save product query fixtures once
Five wc_get_products fixtures were saved with defaults and then immediately saved again with their final status, SKU, or taxonomy properties. No query observes those intermediate states, so the duplicate CRUD writes and hooks add cost without coverage.\n\nReturn unsaved default simple products and preconfigure the variable parent before helper persistence. All nine products, six variations, query inputs, result cardinalities, and assertions remain identical while each changed fixture is stored once.
* perf(tests): Minimize product type fixtures
The V3 and V4 exclude_types tests created complete variable and grouped product catalogs even though their requests only inspect parent product types. That repeated variation, taxonomy, and child-product persistence across twelve tests without contributing to the asserted behavior.
Create one genuine persisted parent of each product type instead. The REST requests, type combinations, precedence checks, and canonical product-type assertions remain unchanged while avoiding unrelated child fixtures.
* perf(tests): Minimize stock variation fixtures
The V4 stock-range tests used the generic six-variation catalog with three attribute taxonomies even though they only assert a few stock quantities and one draft status. That persisted unrelated variation and taxonomy data on every run.
Create genuine variable parents with only the stock-managed variations each scenario needs, configured before their first save. The production lookup query, REST dispatch, matching and nonmatching controls, and published-status behavior remain unchanged.
* perf(tests): Reuse product type fixtures
The V3 and V4 include_types tests repeated the full variable and grouped product catalogs even though the accepted type-filter fixture already provides the same four genuine parent product types.
Reuse that fixture for the include_types scenarios. Their REST requests, grouped-type result, response cardinality, and assertions remain unchanged while avoiding unrelated child persistence.
* perf(tests): Minimize variation link fixtures
The V4 variation link and embed tests created full attribute taxonomies for behavior that only depends on persisted parent-child relationships. Two single-link scenarios also created five unused variations.
Create attribute-free real variations, retaining six children where every link and embedded response is asserted and one child for single-link checks. All REST link, embedding, ID, and parent assertions remain unchanged.
* perf(tests): Reuse multiple type fixtures
The V3 and V4 multiple include_types tests still built complete variable and grouped child catalogs despite exercising the same parent-type fixture as the neighboring type filters.
Reuse the accepted parent-only catalog. V3 retains its grouped and external results, while V4 retains the additional class-owned grouped result and exact canonical assertions.
* perf(tests): Reuse variation query fixture
The V4 variation global_unique_id test built six attributed variations and serialized availability data before setting and querying one child identifier.
Reuse the accepted one-child variable fixture and load that real variation directly. The production global-ID persistence, REST query, result cardinality, and name assertions remain unchanged.
* perf(tests): Reuse variation COGS fixture
The V4 variation Cost of Goods test created six attributed variations for two scenarios that persist and query COGS data on one child.
Reuse the accepted one-child variable fixture. Parent and child COGS persistence, additive and override modes, REST responses, and exact total assertions remain unchanged.
* perf(tests): Scope privacy exporter fixtures
Privacy exporter setup created thirteen full orders and a second customer before the customer-only export test, even though only the order exporter uses pagination and cross-customer isolation. The tracking arrays also retained rolled-back objects across repeated methods.
Reset class-owned fixture tracking per setup and create the second customer plus the exact 11+2 order matrix only for the order exporter. Both exporter payloads, pagination behavior, isolation coverage, and assertions remain unchanged.
* perf(tests): Minimize order collection fixtures
The V2 and V3 order collection tests created ten complete commerce orders with products, items, shipping, billing, and payment data, then asserted only the successful default ten-order page.
Persist ten real customer orders directly through wc_create_order. Collection cardinality, permissions, REST dispatch, and assertions remain unchanged while unrelated fixture writes are removed.
* perf(tests): Minimize order permission fixtures
The V2 and V3 order permission tests built complete orders even though unauthenticated requests return before order serialization.
Use minimal persisted orders so the tests retain real resource IDs and REST permission checks without unrelated products, items, addresses, and totals.
* fix(tests): Isolate product download files
Two filetype tests reused fixed upload paths and never removed them. Repeat runs skipped their creation assertions and left filesystem state behind.
Give each execution a unique real file and delete it in a finally block through the WordPress filesystem API so behavior and assertion counts remain stable.
* perf(tests): Reuse HPOS tables for legacy cleanup
[Context] Legacy data-handler tests need real HPOS tables, storage toggles, synchronized orders, and production cleanup queries.
[Problem] Recreating permanent HPOS tables before every method performs repeated DDL and implicitly commits the WordPress test transaction.
[Solution] Create the HPOS tables once before class transactions begin, then preserve per-test authority toggles, cleanup, and incoming storage state.
* perf(tests): Minimize HPOS total fixtures
[Context] The HPOS total-operator test persists nine orders to exercise duplicate values, boundaries, and all supported comparison operators.
[Problem] Its helper fixture also creates products, items, shipping, addresses, and payment data that total filtering never reads.
[Solution] Persist minimal real orders with the same totals while retaining every production save, query, and cardinality assertion.
* perf(tests): Minimize analytics order fixtures
[Context] The analytics number-filter test creates ten orders and selects the tenth ID through the real REST endpoint.
[Problem] Its helper fixtures also create products, items, shipping, billing, and gateway data that the number query never reads.
[Solution] Persist ten minimal real customer orders while preserving authentication, cardinality, endpoint dispatch, and identity assertions.
* perf(tests): Minimize order-again variation fixture
[Context] The sold-individually order-again regression needs a real variable product, variation metadata, completed order, and duplicate cart request.
[Problem] Its shared helper creates six variations across three global attributes even though the test selects and asserts only one child.
[Solution] Persist one global attribute and one real variation while retaining the complete order-again population and cart rejection behavior.
* perf(tests): Avoid duplicate product search saves
[Context] Eight product-search datasets each build ten real products covering name, SKU, partial matching, ambiguity, and argument precedence.
[Problem] Every fixture is first inserted with dummy name and SKU values, then immediately updated with its final searchable values.
[Solution] Configure the existing full helper object with its final name and SKU before its single persistence operation, preserving the complete search catalog and assertions.
* perf(tests): Minimize legacy cleanup fixtures
[Context] The legacy cleanup query test needs ten synchronized order IDs to cover individual, range, union, and invalid-range selection.
[Problem] Its helper also creates products, items, shipping, addresses, and payment data that cleanup selection never reads.
[Solution] Save ten bare real orders while synchronization is enabled, preserving both storage records, every query shape, and all assertions.
* perf(tests): Minimize CPT total fixtures
[Context] The CPT total-operator test persists nine orders to exercise duplicate values, boundaries, and all supported comparison operators.
[Problem] Its helper fixtures also create products, items, shipping, addresses, and payment data that total filtering never reads.
[Solution] Persist minimal real orders with the same totals while retaining every CPT save, production query, and cardinality assertion.
* perf(tests): Minimize created-via order fixtures
[Context] Four V3 collection-filter tests run under both CPT and HPOS controllers with checkout, admin, and invalid created-via values.
[Problem] Their helper orders also create products, items, shipping, addresses, and payment data that source filtering never reads.
[Solution] Persist minimal real orders with the same created-via values while retaining both storage modes, REST dispatches, and all response assertions.
* perf(tests): Minimize refunds collection fixtures
[Context] The standalone refunds endpoint test verifies that refunds from three distinct orders are returned by the collection route.
[Problem] Each order unnecessarily included a product, line items, fees, shipping, and matching refund items even though none of those fields were asserted.
[Solution] Persist minimal orders and real refunds, and assert the exact collection size before checking refund IDs. This preserves the datastore and REST integration while reducing unrelated database writes.
* perf(tests): Minimize order metadata fixture
[Context] The V3 order update regression verifies handling of incomplete metadata through both CPT and HPOS REST owners.
[Problem] Its order helper also persisted a product, line item, shipping configuration, address, payment method, and totals that the metadata path never reads.
[Solution] Use a minimal real order while preserving REST dispatch, persistence, both storage modes, malformed metadata inputs, and all assertions.
* perf(tests): Minimize HPOS type guard fixtures
[Context] Two V3 REST regressions verify that HPOS refund and custom-type records are rejected by the shop-order update endpoint.
[Problem] Both tests created complete product, shipping, address, payment, and total fixtures solely to obtain an HPOS order ID and type.
[Solution] Persist minimal real orders while retaining the real refund type, direct custom-type update, REST rejection, and immutability assertions.
* perf(tests): Minimize customer search order fixture
[Context] The HPOS customer search test verifies that coupon usage lookup returns only users whose billing email appears on an order.
[Problem] Its helper persisted unrelated product, item, shipping, address, payment, and total data, then saved again to set the queried email.
[Solution] Save one real order with only the customer ID and billing email consumed by the production SQL and user query.
* perf(tests): Reuse HPOS tables for order tracking
[Context] Order tracking tests exercise the same hooks and screen events under both CPT and HPOS authority.
[Problem] Every test dropped and recreated the HPOS schema, and setup mutations occurred before the WordPress harness snapshot, leaking tracking state and callbacks across tests.
[Solution] Create permanent HPOS tables once per class and enter the parent test harness before registering test-owned options, globals, and hooks. Per-test authority toggles, teardown restoration, real events, and assertions remain unchanged.
* fix(tests): Isolate tracking event state
[Context] Product, settings, and order tracking tests register event callbacks and inspect a shared Tracks footer-pixel buffer.
[Problem] Product and settings setup ran before the WordPress harness snapshot, leaking tracking options and hooks, while all three owners inherited recorded events from prior tests. Repeated runs failed after the first pass.
[Solution] Enter the parent harness before test-owned mutations and clear recorded Tracks events at each test boundary. Repeated and randomized owners now remain independent without changing event assertions.
* fix(tests): Isolate importer tracking state
[Context] Importer tracking tests register a callback and inspect the same shared Tracks event buffer as the other event-tracking owners.
[Problem] Setup ran before the WordPress harness snapshot and inherited prior recorded events, leaving tracking enabled and allowing state to accumulate silently across repeated runs.
[Solution] Enter the parent harness first and clear recorded Tracks events before registering the importer callback. The real import hook and assertion remain unchanged.
* fix(tests): Isolate remote logger test state
RemoteLoggerTest skipped the parent teardown, leaving WordPress transactions and hook snapshots unrestored. Its success case also counted any intercepted HTTP request, so leaked tracking state silently added McStats assertions.
Restore the parent lifecycle and make successful delivery assert one request to the Logstash endpoint. Use the supported error level to keep critical-stat traffic outside the delivery scenario while retaining dedicated critical handling coverage.
* perf(tests): Bulk insert variation migration fixtures
The batch-boundary test created 251 bare variations and their legacy metadata through 502 high-level calls. Fixture setup alone issued 3,774 queries and dominated the real migration work.
Insert the same posts and metadata through prepared bulk queries, then select their IDs by an explicit marker. This preserves the 250/1 boundary, real migration writes, and every compatibility assertion without relying on auto-increment reset state.
* perf(tests): Minimize product type fixtures
The product type test built four complete catalog objects and a variation child even though the datastore reads only post type and product_type taxonomy relationships.
Use shared WordPress factory posts with real product type terms instead. This preserves all datastore branches and strict type assertions while letting the test harness own cleanup.
* fix(tests): Reset reused stock product state
The stock transition owner retained its reused product object after WordPress rolled back the corresponding database row. PHPUnit repeat runs therefore reused a stale ID and created orders without line items.
Clear the test-owned property during teardown so every repeated method creates a valid product while preserving within-method reuse.
* perf(tests): Bulk insert order count fixtures
The order count test created a full order object for every status row even though OrderUtil reads only type and status from the authoritative persistence table.
Insert the same triangular status matrix in one prepared query per storage mode. HPOS IDs are allocated above the live maxima across both ID namespaces, avoiding any dependency on reset or auto-increment state.
* perf(tests): Limit onboarding sample import fixture
The onboarding endpoint test imported all nine sample rows while asserting only that multiple products succeeded. Seven rows added product and image work without increasing the tested contract.
Limit this test through the importer’s supported lines argument and assert exactly two imports with no skips. The real REST, CSV, product, taxonomy, and image paths remain covered.
* perf(tests): Minimize duplicate slug fixtures
The variable-product slug regression test exercises repeated child duplication, but its shared fixture also creates global attributes, terms, prices, and SKUs that the slug algorithm never reads.
Keep the real variable parent and all six persisted variation children with their exact collision-producing slugs. Create only the WordPress post records required by the duplication path so the three successive maximum-suffix checks retain their original depth with less database setup.
* perf(tests): Isolate database update policy coverage
The auto-update policy matrix called the full installer for every dataset, spending hundreds of queries on schema, roles, terms, files, pages, and other behavior unrelated to the scheduling policy under test.
Exercise the private policy boundary with real Action Scheduler persistence for all three states, and retain a dedicated public version-check integration guard. Use the penultimate registered migration so the fixture remains current without accumulating every future callback.
* perf(tests): Minimize add-to-cart subtype fixtures
The V4 add-to-cart response matrix built complete grouped and variable catalogs even though its contract reads only parent subtype methods. That setup added child products, variations, attributes, terms, prices, and SKUs unrelated to the response field.
Persist real grouped and variable parents without unrelated children, and assert the serialized response type explicitly. The controller dispatch and all four product add-to-cart methods remain real while the fixtures stay scoped to the behavior under test.
* perf(tests): Bulk insert tracker order matrix
The tracker aggregate test persisted 36 HPOS orders through high-level CRUD even though production reads a fixed set of order and operational columns. Its payment_method factory arguments were also ignored, collapsing the intended three-gateway matrix into an empty-gateway assertion.
Insert the complete HPOS persistence boundary in two prepared queries with collision-free IDs and explicit cache invalidation. Keep real CRUD for CPT, persist payment methods in both modes, and assert each gateway's count and revenue separately.
* perf(tests): Bulk insert tracker snapshot orders
The tracker snapshot test persisted 40 HPOS orders through high-level CRUD even though its production queries read a fixed set of order and operational columns. This repeated hooks, cache work, and object persistence unrelated to first/last ranking.
Reuse the tracker test's prepared HPOS persistence boundary for the exact dated snapshot rows while retaining real CRUD under CPT. All rank, currency, total, recorded-sales, and version assertions remain unchanged.
* perf(tests): Minimize variation creation fixtures
The datastore variation tests used arbitrary 27-variation matrices to exercise Cartesian creation and batch limits. This made two narrow unit tests perform 54 full variation writes without adding a distinct boundary.
Use smaller real multidimensional fixtures, assert every generated combination, and verify cumulative child counts across limited batches. This preserves the production datastore path while strengthening the tested contract with substantially fewer writes.
* perf(tests): Avoid remote rule option writes
The base-location rule tests persisted every input through update_option and reset three options after each test. The completed-profile case also fired onboarding completion hooks unrelated to the processor's read-only contract.
Provide scoped pre-option fixtures backed by per-test values and remove them before teardown. The production rule still reads each option normally while the tests avoid SQL writes and unrelated lifecycle hooks.
* fix(tests): Restore multi-customer order stats coverage
The order stats matrix fixture was shrunk from 144 to 36 orders for
suite speed, dropping the second customer and the second same-hour
order slot. Every total_customers assertion in the file then only ever
verified the value 1, so DISTINCT-customer aggregation and same-hour
interval merging were no longer exercised.
Add a dedicated test that creates two completed orders by two customers
inside one hourly interval and asserts both the totals and the interval
subtotals report two orders and two distinct customers. This restores
the lost coverage for the cost of two orders instead of re-inflating
the combinatorial matrix fourfold.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): Exercise stock stats lookup sync on save
The stock stats REST test was converted to seed all of its products by
inserting rows straight into wc_product_meta_lookup, so the only test
covering /wc-analytics/reports/stock/stats no longer went through the
production sync-on-save path that populates that table.
Route the single backorder product (created mid-test anyway to verify
cache refresh) through a real product save so the lookup-table sync is
exercised end to end, while keeping the 19 bulk-inserted rows that make
the test fast.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): Make order controller tests self-contained
OrderControllerTests failed when run in isolation (2 errors, 3
failures): its address fixtures do not provide phone numbers while the
WooCommerce default for woocommerce_checkout_phone_field is 'required'.
The class only passed inside the full suite by inheriting the
'optional' value that earlier Store API test classes set per test.
Set the option to 'optional' in setUp() like the other Store API test
classes do, so the class carries its own preconditions regardless of
execution order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): Assert unfiltered Store API product listing
test_get_items was changed to pre-filter the products request with
include=<fixture IDs>, which masked the default (unfiltered) listing
code path and reduced the assertion to a count of the requested IDs.
Dispatch the unfiltered request again and assert the returned ID set
equals the class fixtures exactly (assertEqualsCanonicalizing). This is
stronger than the original count-only assertion and doubles as a
guardrail against fixture leakage from other classes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): Bind breadcrumb font size assertion to declaration
The custom typography test asserted 'font-size:' and '2rem' as two
independent substrings. The '2rem' check was trivially satisfied by the
serialized data-style attribute, so the test could pass without the
size ever being applied as a style.
Assert the two together with /font-size:[^;"]*2rem/ so 2rem must appear
inside the font-size declaration itself, while still tolerating fluid
typography rendering the value as clamp(..., 2rem) depending on the
active theme.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): Restore per-test product filter cleanup
The shared-catalog refactor removed per-test fixture cleanup from
tearDown(), but class-level cleanup only runs for subclasses that opt
into uses_class_product_filter_fixtures(). A future subclass keeping
the default per-test fixtures would leak attribute taxonomies and
lookup rows between test methods with no teardown at all.
Call delete_product_filter_fixtures() from tearDown() for the per-test
fixture path. All four current subclasses use class fixtures, so this
is free today; it just closes the trap for the next subclass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): Drop shared fixture deletions in variable data store tests
Two tests still ended by deleting the variable product that is now a
class-shared fixture created in wpSetUpBeforeClass(). They only stay
harmless because they are declared last and the per-test transaction
rollback restores the rows — adding a test after them or reordering the
class would break every subsequent test.
Remove the delete() calls; fixture cleanup is owned by
wpTearDownAfterClass().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(tests): Remove dead HPOS revert scaffolding
wpSetUpBeforeClass() captured the HPOS usage state, allowed storage
changes via filter, and reverted the state after setup_cot_tables().
That helper only creates and empties the HPOS tables — it never toggles
usage — so the revert branch is unreachable and the filter unnecessary.
Call setup_cot_tables() directly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(tests): Clarify install version check test scope
test_version_check_schedules_db_auto_update() looks like it should
cover the same auto-update variations as test_db_auto_updates(), but
running check_version() per variation would trigger a full install()
three times. Document that it is deliberately a single end-to-end smoke
of the wiring while the decision logic is parametrized in the cheaper
test above.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(tests): Consolidate direct lookup update toggles
The "force synchronous attribute lookup updates around fixture work"
pattern was implemented four times: an enable/disable pair in
WC_Unit_Test_Case plus scoped filter callbacks re-created in
StoreApiRestTestCaseTrait, ProductsControllerTest, and
PostsToOrdersMigrationControllerTest.
Make WC_Unit_Test_Case the single owner. Its enable/disable pair is now
depth-counted, which also fixes a real nesting flaw: tearDownAfterClass
wraps parent teardown (including subclasses' wpTearDownAfterClass) in
enable/disable, and a subclass running its own balanced enable/disable
inside that window used to strip the outer filter early. A new public
with_direct_product_attribute_lookup_updates( callable ) provides the
scoped form; the three duplicates now delegate to it or use the
inherited pair.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(tests): Deduplicate REST test server plumbing
Two fiddly patterns were hand-rolled in multiple places: saving,
setting, and restoring $wp->query_vars['rest_route'] (which drives
WooCommerce's lazy REST namespace registration) in WC_Lazy_REST_Server
and StoreApiRestTestCaseTrait, and swapping the global rest_api_init
hook for an isolated one in create_rest_server_with_routes() and
initialize_rest_api_defaults().
Extract with_rest_route_context() and do_isolated_rest_api_init() into
WC_Unit_Test_Case and delegate all four call sites to them. Also
document the endpoint-count sentinel in has_registered_routes().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): Cover unfiltered product segment enumeration
Scoping the segmentby=product test with product_includes fixed a real
catalog order-dependence, but it left Segmenter::set_all_segments()'s
unfiltered branch — enumerating every store product, the default
merchant path — covered by no test at all.
Add a dedicated test without product_includes: one sold and one unsold
product, asserting a segment exists for every catalog product and that
the unsold product's segment carries zero subtotals. The expected ID
set is computed dynamically so unrelated catalog rows cannot reintroduce
order dependence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): Forward order data through test order wrapper
The private create_order() wrapper accepted only customer and product,
but test_wc_create_refund_28969 passed a third argument with
status => completed. PHP silently discards extra arguments, so the test
ran against a pending order instead of the completed-order scenario it
was written for (and the dead argument was a copy-paste trap).
Add an $order_data parameter and forward it to
WC_Helper_Order::create_order(), which already supports it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): Validate image schema in Store API schema tests
On trunk every fixture product carried a sideloaded image, so the
CartItems and Products schema tests validated the response's images[0]
against ImageAttachmentSchema. The shared class fixtures have no
images, and ValidateSchema::get_diff_from_object() skips nested
items.properties validation when the array is empty — the image schema
check silently stopped running while the tests stayed green.
Attach an image inside each schema test (per-test rollback contains the
mutation, and the physical file is removed explicitly) and add
assertNotEmpty guards so an imageless response can never silently skip
the nested validation again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): Exercise null stock guard in product stock filters
The minimized variable-product fixtures manage stock on every
variation, so the stock_quantity IS NOT NULL guards in the V4 products
stock-range filter were never data-exercised: no lookup row with a NULL
quantity existed in any fixture.
Let the fixture helper create unmanaged-stock variations, add one to
the non-matching variable product, and query a zero-inclusive range —
the case where a COALESCE(stock_quantity, 0)-style regression would
wrongly match the NULL row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(tests): Clean all attribute taxonomy caches
use_class_product_filter_fixtures() hardcoded
clean_taxonomy_cache( 'pa_color' ) — correct for today's shared catalog
but silently wrong for any subclass that adds attributes through
set_up_additional_class_product_filter_fixtures().
Iterate the registered attribute taxonomies instead, mirroring the
cleanup already done in tearDown().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): Restore uninstall helper for user meta test
The branch removed WC_Tests_Install::uninstall() when narrowing
test_create_roles to WC_Install::remove_roles(), while trunk's #66027
added test_uninstall_removes_experimental_user_meta_but_preserves_other_meta,
which drives the real uninstall.php full-data-removal path through that
helper. The merge combined both without a textual conflict, leaving the
new test calling an undefined method.
Restore the helper for the new test; test_create_roles keeps its narrow
remove_roles() scope.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(tests): Document performance and isolation principles
The suite-speedup work established a set of conventions (assertion-
sized fixtures, transaction-respecting cleanup, class-level immutable
fixtures, persistence-boundary seeding, lazy REST registration,
deterministic time/network) and shared framework helpers that embody
them. None of this was written down where test authors look.
Add a short "Performance and isolation principles" section to the
tests README so new tests stay on the runtime budget, including the
TRUNCATE-implicitly-commits pitfall and the SpeedTrap report as a
review flag.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: Guard direct product lookup fixture updates
The shared direct-update fixture helper had no focused assertion for its synchronous lookup contract. Downstream filter-count tests still passed when the helper was mutated to queue updates, while leaving hundreds of pending actions behind.\n\nAdd a focused lookup data-store test that saves an attributed product through the helper and verifies both the immediate lookup row and the absence of queued work. This makes a disabled direct-update mutation fail at the intended boundary without adding production changes.
* test: Verify nested lookup fixture scopes
The shared direct-update helper is nesting-aware and restores its option filter in a finally block, but neither contract had focused coverage. A premature inner cleanup or omitted outer cleanup could silently leak fixture mode across tests.\n\nAdd a focused nested-scope test that verifies direct mode remains active through the outer callback and that the original option value is restored afterward. The test kills both early-removal and missing-finally mutations without retaining production changes.
* test: Assert product image update succeeds
The product update test read response fields without first checking that the image upload request succeeded. When the local image fixture was mutated away, the test errored on a missing payload key instead of identifying the failed request.\n\nAssert the HTTP 200 response before inspecting the updated product payload. This makes invalid image-download mutations fail at the request boundary with a clear diagnostic while retaining the existing field and image assertions.
* test: Verify batch note delete undo
The batch undo test sent is_deleted=1 after deleting all notes and asserted only the response status and item count. A production no-op update therefore passed, so the test did not verify its stated behavior.\n\nSend the undelete value, assert both response objects are restored, and fetch both notes again to verify persisted state. This makes a skipped repository update fail while keeping all durable changes in test code.
* test: Verify async plugin install scheduling
The async install test accepted a null job ID and never checked Action Scheduler, so removing the scheduling call did not fail the test. It also left install callbacks behind after successful runs.\n\nRequire a non-empty job ID and the exact scheduled install callback, and clean matching actions in a finally block. This makes a skipped scheduler call fail while keeping the test environment isolated.
* test: Isolate order note query fixtures
Customer-note creation now records an additional private note through the transactional email logger. That unrelated side effect made the order-note query test depend on email logging state and invalidated its explicit three-note fixture.
Suppress only the email logger order note while creating the customer-note fixture, and always restore the filter. This keeps the query test focused on its controlled customer and internal notes.
* test: Verify privacy export pagination
The order privacy exporter test inspected only the first item on each page, so changing the production page size from ten to one remained green and left pagination behavior unguarded.
Assert the exact first- and second-page cardinalities and completion flags for the eleven matching orders.
* test: Correct shipping zone location type coverage
The test named for location-type validation duplicated the zone-order assertion, while the following add-location test only exercised location-type validation and never added a location.
Keep one accurately named validation test with the real built-in, invalid, and filtered-type assertions, removing the duplicate false signal.
* test: Correct base country rule coverage
The base-location country test claimed to exercise a non-array return, but wc_get_base_location() formats the empty option into an array with empty country and state values. The test therefore guarded the empty-country comparison path, not the unreachable shape guard.\n\nRename the test and its description to state the behavior it actually verifies. A production comparison mutation now fails this accurately named contract.
* test: Guard attribute lookup fixture scopes
Synchronous attribute lookup fixture mode was not directly observable, and the WC Admin helper left its setup scope enabled after product creation. That could hide queued lookup work and leak option state into later tests.\n\nScope product creation and deletion inside balanced lifecycle blocks, verify the prior option mode is restored, and assert that fixture-owned lookup actions are not queued. These contracts fail when direct updates are disabled or cleanup is omitted.
* test: Verify customer report reset isolation
Removing separate-process execution relies on report lookup cleanup to isolate customer tests in one PHPUnit process. Transaction rollback alone makes incidental stale-state checks nondeterministic.\n\nCreate a deliberate customer lookup sentinel before each report reset, prove that it exists, and assert that reset_stats_dbs removes it. The existing ten-customer expectations then remain independent of execution order.
* test: Assert canonical remote log endpoint
The successful remote logger test compared the request URL with the same production constant used to send it, so endpoint mutations remained self-consistent and survived. Assertions inside the HTTP interception callback could also be converted into an indirect handle failure.\n\nCapture the requested URL, allow the fake response to complete, and compare it with the canonical endpoint outside the callback. The endpoint mutation now fails at the URL oracle while request count, body, headers, and rate limiting remain covered.
* test: Distinguish prior order review slots
The prior-order scope test placed both the old review and current submission on variation A. Removing order scoping therefore still left variation B missing, allowing the mutation to survive for the wrong reason.\n\nPlace the older order's review on variation B while the current submission reviews variation A. Production still leaves the current order incomplete, but an unscoped query now combines both slots and is caught.
* test: Verify attribute lookup deletions
Direct-update deletion datasets started without the lookup rows they expected the SUT to remove. This allowed disabled deletion behavior to pass while providing a false signal of coverage.
Seed the target lookup rows for deletion scenarios across simple, variable, and variation products. Creation cases remain empty, while update and no-op fixtures retain their existing populated setup.
* test: Assert add-to-cart text contracts
The response test derived its expected text from the same product method used by the endpoint. A broken product-type label therefore changed both sides of the assertion and passed unnoticed.
Define locale-aware expected labels from each exact fixture and compare the response against those independent contracts.
* test: Assert product type filter results
The type-filter tests accepted incomplete collections: the exclude test passed on an empty response, and the include test inspected only the first item.
Compare the complete returned type sets against the fixture contracts so missing, extra, or wrongly typed products are observable.
* test: Restore variable add-to-cart coverage
The optimized fixture replaced a purchasable variable product with an empty parent, changing the exercised response from Select options to Read more.
Create one lightweight custom attribute and published priced variation, then sync and reload the parent. This preserves the original child-dependent behavior without restoring the expensive full variation catalog.
* test: Reset product controller fixtures
Class teardown deleted shared product fixtures but retained their objects in the static catalog. Repeating the class appended new fixtures to stale references, producing invalid include IDs and deleted product lookups.
Release the owned static catalog in a finally block after deletion so repeated and reverse-order runs start from a clean fixture inventory.
* test: Reset fulfillment controller fixtures
Class teardown deleted shared fulfillment fixtures but retained their static order, fulfillment, and user IDs. Repeating the class reused deleted first-pass records, causing widespread 404 responses and teardown errors.
Reset every class-owned fixture reference after cleanup, and guarantee option restoration, parent teardown, and direct-lookup balancing through nested finally blocks.
* test: Isolate block email settings state
The email settings controller tests enable the block email editor and rebuild the global email registry. Restoring the feature option before the parent teardown allowed the test transaction rollback to re-enable it, while the process-local email singleton could retain block-editor email objects. This changed email titles in later tests when class order was reversed.\n\nSnapshot the exact option and WC_Emails state, then restore both after the parent teardown through exception-safe cleanup. This keeps repeated and reverse-order runs independent without changing production code.
* test: Restore raw mobile messaging options
The mobile messaging tests saved derived country and currency strings before class setup. If either option was absent, teardown recreated it with a filtered default, and serialized values or autoload metadata could not be restored exactly.\n\nSnapshot option existence, stored bytes, and autoload state directly from the options table. Restore existing rows without changing their identity, insert only when a captured row disappeared, delete originally absent rows, and invalidate all related option caches.
* test: Isolate variation migration fixtures
Variation gallery batch fixtures were reselected using a shared post title. An ambient variation with that title could enter the owned ID set and receive mismatched fixture metadata.\n\nGive each fixture batch a unique title, assert its exact cardinality, and keep a same-title decoy outside the migration so the ownership boundary remains observable.
* test: Restore remote extension option state
Remote extension tests relied on transaction rollback for option and transient rows, but rollback left their caches mutated. Later tests could therefore observe stale marketplace or country state.\n\nCapture the exact raw rows before fixture mutation, restore them after parent teardown, invalidate caches unconditionally, and verify the resulting raw and decoded state independently. Cleanup now also removes only the specs callback owned by the fixture.
* test: Exercise product importer images
Importer image fixtures omitted the required original IDs, so the handler skipped every image and the tests passed even when image handling returned immediately. The disabled-option case also claimed behavior the importer does not implement.\n\nStream a checked-in image fixture through WordPress, assert featured, gallery, dry-run, limit, and explicit-feature semantics, and clean owned files exactly. Remove the false-confidence disabled-option test.
* test: Scope product filter fixtures
Product result queries were restricted to class-owned fixtures, but layered-navigation counts still included ambient matching products. Existing tests had no deliberate collision to expose that mismatch.\n\nScope count SQL to the same owned IDs and add matching/nonmatching ambient products in both lookup modes. The new oracle independently proves request and counter isolation.
* test: Reset download permission fixtures
Download permission tests left LegacyProxy replacements and the approved-directory option cache mutated until the next test setup. Immediate consumers could observe mocks or stale disabled mode even after database rollback.\n\nReset class-owned proxy mocks before parent cleanup, always run parent teardown, and invalidate the option caches after rollback.
* test: Restore remote logger state
Remote logger tests mutate Jetpack constant overrides, namespace mock globals, site-transient caches, and rate-limit cache entries while exercising failure paths.
The prior cleanup cleared unrelated constants, targeted the wrong transient API, and relied on broad or ineffective cache cleanup. That could leak state or erase ambient test state depending on execution order.
Snapshot and restore exact process state, let inherited teardown own hooks and transactional rows, then invalidate only the cache keys the class mutates. This keeps repeated and reverse-order runs isolated without touching production code.
* test: Restore WooPayments controller state
The WooPayments controller integration fixture changes gateway callbacks, container resolutions, REST globals, Jetpack constant overrides, and option caches across each test.
Its prior teardown broadly removed hooks, rewrote class options through filtered APIs, and stopped at the first cleanup failure. That could erase ambient state or leak fixture state into later tests.
Preserve raw option rows exactly, restore only the owned constant override, scope gateway cleanup to the owned callback, and run every teardown phase before surfacing failures. Targeted cache invalidation exposes the transaction-restored values without retaining redundant deletes.
* test: Restore payments controller state
The payments controller fixture changes PayPal settings, gateway callbacks, container resolutions, REST globals, WCPAY overrides, and several option caches while testing controller flows.
Its previous cleanup deleted ambient PayPal state, removed unrelated gateway callbacks, and could stop before inherited rollback. Invalid PayPal settings could also leave the canonical gateway collection incomplete after a test failure.
Preserve raw class options exactly, restore only owned process state, rebuild core PayPal without fake gateways, and run every teardown phase before surfacing errors. Invalidate only the option caches exercised by the fixture after rollback.
* test: Verify incomplete metadata absence
REST metadata tests compared missing-value and explicit-null entries through identical fallback values. If production dropped both entries before normalization, the assertion still passed and overstated endpoint coverage.
Assert that neither null-valued key appears in the final WC_Data metadata while retaining the complete and keyless-entry checks. This matches downstream persistence semantics and independently detects a missing value that becomes non-null metadata.
* test: Reset pattern request counts
Pattern route tests count PTK requests on an instance property, but PHPUnit repeat can reuse the same instance across executions. The stale counter made a correct single request look like a duplicate in repeated or reordered runs.
Reset the observation counter at each setup boundary before installing the scoped HTTP callback so every test measures only its own dispatch.
* test: Verify Store API image URLs
Store API product tests asserted image count and attachment identity but accepted empty source and thumbnail URLs because schema checks only enforced string types.
Require both image URL fields to contain a parseable host. This preserves environment-independent fixtures while rejecting unusable response URLs.
* test: Verify tracker fixture inserts
The tracker tests build minimal HPOS fixtures with direct bulk inserts, but previously ignored both database return values. A failed or partial insert could therefore surface later as a misleading tracker assertion failure.\n\nAssert the exact affected-row count for the orders and operational-data inserts before flushing caches or exercising tracker behavior. This makes fixture failures immediate and identifies which table was incomplete.
* test: Restore checkout phone option
OrderControllerTests forces the checkout phone field to optional, but it extends the plain PHPUnit base and had no transaction or teardown restoration. Running the class could therefore leak the option into later tests and hide missing-phone dependencies.\n\nCapture both the incoming option existence and value, then restore that exact state before guaranteeing the parent teardown runs. This preserves false-like stored values while removing rows that were originally absent.
* test: Restore agentic checkout options
The checkout-session completion tests change the phone and agentic feature options, then relied on transaction rollback while mutating option caches during teardown. This could leave stale cache values or delete an incoming feature setting after the parent restored it.\n\nSnapshot exact option existence and values before each test. After the parent rolls back database state, invalidate the relevant WordPress option caches and restore the captured state through the option API, with nested finally blocks guaranteeing cleanup.
* test: Remove agentic gateway filters
Checkout-session completion setup registers two callbacks bound to the current test instance, but teardown previously left both callbacks installed. Repeated tests could retain stale instances and mock gateway state.\n\nRemove both exact callbacks in the guaranteed teardown path before parent cleanup. This keeps normal and exceptional test runs isolated without changing Store API behavior.
* test: Delete shared Store API coupons
The Cart and Checkout route classes create coupons outside per-test transactions, but their class teardown only deleted the shared products. Those coupon rows survived the class and could influence later tests.\n\nTrack each class-owned coupon by its persisted ID and force-delete it after product cleanup. A finally block guarantees coupon cleanup even when product teardown fails, without searching by a potentially ambiguous coupon code.
* test: Delete shared report customers
The customer reports class creates three users outside per-test transactions, but class teardown previously dropped only the PHP references. Those users survived the class and could affect later customer-report fixtures.\n\nDelete the exact retained WC_Customer objects during class cleanup. Keep product deletion in a guaranteed finally block and continue balancing direct product lookup updates around that product operation.
* test: Restore dashboard option state
The dashboard setup tests force a non-US country and mutate task-list options inside a database transaction.
After rollback, WordPress could still serve the test values from individual or aggregate option caches, contaminating later tests.
Capture the incoming country state, invalidate every option cache touched by the class after parent teardown, then restore the exact prior country value or absence.
* test: Reset Store API cart notices
The Store API Cart fixture reset the cart and customer fields but left the session notice collection untouched.
A notice created by an earlier test could therefore remain visible to the next Cart test despite its otherwise clean setup.
Clear the notice session key before the route fixtures populate the cart.
* test: Restore Checkout shared state
Checkout tests mutate country, account, payment, and pickup options while fixture setup reinitializes the global payment gateway registry.
Database rollback restored option rows but left cache-visible values, the registry, and PayPal's separate singleton in their test state.
After guaranteed parent teardown, invalidate every touched option cache and restore the exact pre-fixture gateway objects without constructing new gateways or registering hooks.
* test: Persist refund timestamp fixture
The refund regression test replaced its wall-clock delay with a fixed modified date. WordPress overwrites that value during CPT updates, so HPOS-off CI could compare two timestamps from the same second and fail.\n\nPersist the past fixture timestamp through each datastore's authoritative path, reload it before the refund, and require the refund to advance the stored parent-order timestamp. The direct CPT update is limited to test setup and asserts that exactly one order row changed.
* docs(tests): Document the fixture ladder principle
The suite-speedup branch replaced over a hundred heavyweight
WC_Helper_Order::create_order() fixtures with wc_create_order() where
the assertions never touched line items, addresses, or totals — but the
principle behind choosing between the full helper, the minimal
production API call, and direct row seeding was only implied by the
existing bullets.
Spell out the ladder explicitly with orders as the canonical example,
including the wc_create_order() argument-handling gotcha that had let a
tracker test believe it covered payment gateways it never created.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tests): Initialize full route table after scoped dispatch
The lazy REST test server skipped full route initialization in
get_routes() whenever any route was already registered, so the first
scoped dispatch (which registers a single namespace) permanently
disabled the full-table fallback the method's docblock promises.
Route assertions made after a dispatch could then depend on which
namespace happened to be dispatched first.
Track whether scoped namespace initialization has run and complete
the route table on direct inspection in that case, while still
leaving servers that only hold manually registered routes untouched,
so focused-controller tests keep asserting on exactly the routes
they registered.
Includes regression coverage for both paths: dispatch-then-inspect
returns the complete table, and manual-only registration stays
preserved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: Cover production REST wiring for scoped suites
Scoped controller suites (mobile app QR login, V4 shipping zones)
register their controllers directly on an isolated server, so no
behavior test exercised those controllers' production registry
entries, feature gates, or DI wiring anymore.
Add framework-level smoke tests that run the real rest_api_init
cascade and assert those controllers' routes register through the
production path, including that the wc/v4 namespace stays gated
behind the rest-api-v4 feature.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(tests): Annotate new test methods with testdox
Four test methods added on this branch (category lookup, order
reports stats, WC install) were missing the @testdox annotation the
repository requires on new test methods.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json
index f2dd0693d54..b5571f890ad 100644
--- a/plugins/woocommerce/package.json
+++ b/plugins/woocommerce/package.json
@@ -97,8 +97,8 @@
"test:php:watch": "sh ./client/blocks/bin/copy-blocks-json.sh && ./vendor/bin/phpunit-watcher watch",
"test:metrics": "USE_WP_ENV=1 pnpm playwright test --config=tests/metrics/playwright.config.js",
"test:metrics:ci": "../../.github/workflows/scripts/run-metrics.sh",
- "test:php:env": "sh ./client/blocks/bin/copy-blocks-json.sh && pnpm wp-env:test run --env-cwd='wp-content/plugins/woocommerce' cli vendor/bin/phpunit -c phpunit.xml --verbose",
- "test:php:env:hpos-off": "sh ./client/blocks/bin/copy-blocks-json.sh && pnpm wp-env:test run --env-cwd='wp-content/plugins/woocommerce' cli env DISABLE_HPOS=1 vendor/bin/phpunit -c phpunit.xml --verbose",
+ "test:php:env": "sh ./client/blocks/bin/copy-blocks-json.sh && pnpm wp-env:test run --env-cwd='wp-content/plugins/woocommerce' cli tests/php/bin/run-phpunit.sh -c phpunit.xml --verbose",
+ "test:php:env:hpos-off": "sh ./client/blocks/bin/copy-blocks-json.sh && pnpm wp-env:test run --env-cwd='wp-content/plugins/woocommerce' cli env DISABLE_HPOS=1 tests/php/bin/run-phpunit.sh -c phpunit.xml --verbose",
"test:php:env:watch": "sh ./client/blocks/bin/copy-blocks-json.sh && pnpm wp-env:test run --env-cwd='wp-content/plugins/woocommerce' cli vendor/bin/phpunit-watcher watch --verbose",
"test:plugincheck": "pnpm wp-env:e2e run cli wp plugin check ${PLUGIN_SLUG:-woocommerce}",
"test:unit": "pnpm test:php",
diff --git a/plugins/woocommerce/tests/README.md b/plugins/woocommerce/tests/README.md
index 7514e0d8eef..087cc1515aa 100644
--- a/plugins/woocommerce/tests/README.md
+++ b/plugins/woocommerce/tests/README.md
@@ -12,6 +12,7 @@ This document discusses unit tests. See [the e2e README](https://github.com/wooc
- [Running Unit Tests](#running-unit-tests)
- [Troubleshooting](#troubleshooting)
- [Guide for Writing Unit Tests](#guide-for-writing-unit-tests)
+ - [Performance and isolation principles](#performance-and-isolation-principles)
- [Automated Tests](#automated-tests)
- [Code Coverage](#code-coverage)
@@ -177,6 +178,26 @@ General guidelines for all the unit tests:
- Filters persist between test cases so be sure to remove them in your test method or in the `tearDown()` method.
- Use data providers where possible. Be sure that their name is like `data_provider_function_to_test` (i.e. the data provider for `test_is_postcode` would be `data_provider_test_is_postcode`). Read more about data providers in the [PHPUnit manual](https://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers).
+### Performance and isolation principles
+
+The full suite runs in a few minutes because every test pays only for what it asserts. Please keep new tests on that budget:
+
+- **Size fixtures to the assertions, not to realism.** Derive the fixture from the production branches under test; if two variations cover the algorithm, don't create twenty-seven. When scale itself is the contract (pagination boundaries, batch limits), keep the boundary — and prefer strengthening assertions (exact values, per-branch checks) over enlarging fixtures.
+- **Climb down the fixture ladder.** Create each fixture on the cheapest rung that still exercises the production branches under test, using orders as the canonical example:
+ 1. `WC_Helper_Order::create_order()` (or `OrderHelper::create_order()`) when line items, addresses, shipping, or totals are part of the contract — it builds a complete commerce order (~29 writes plus hook cascades).
+ 2. `wc_create_order( array( 'customer_id' => ... ) )` when the test only needs orders to exist with basic properties (counts, collections, permissions, status transitions) — one write through the real production API. Note it only honors the documented argument keys; properties like `payment_method` must be set on the object and saved.
+ 3. Direct row inserts when the code under test only reads persisted rows (see "Seed at the boundary the code reads" below).
+
+ The same ladder applies to products and other entities: full helper → minimal production API call → boundary seeding.
+- **Respect the per-test transaction.** Every test runs inside a transaction that is rolled back, so most cleanup is free. Never use `TRUNCATE TABLE` in tests or helpers: it is DDL and implicitly commits the transaction, silently breaking isolation for everything after it. Use `DELETE FROM` instead.
+- **Share immutable fixtures at class level.** Catalogs that no test mutates belong in `wpSetUpBeforeClass()`, reloaded per test with fresh object instances (`wc_get_product()` etc.). Per-test mutations are contained by the transaction rollback and the per-test object-cache flush. Clean up class-created data in `wpTearDownAfterClass()`.
+- **Seed at the boundary the code reads.** If the code under test only reads persisted rows (aggregate SQL, lookup tables, migration sources), fixtures may insert those rows directly — with exactly the columns production writes and collision-safe IDs. Keep at least one real write-path (CRUD) test per area so the sync path stays covered.
+- **Register only the REST surface you exercise.** `WC_REST_Unit_Test_Case` registers routes lazily per namespace; scoped tests can use `WC_Unit_Test_Case::create_rest_server_with_routes()`. Avoid re-firing the full `rest_api_init` per test.
+- **Never sleep, never fetch.** Control timestamps through the object setters instead of `sleep()`, and mock HTTP through the framework's interception layer (`$this->http_responder`, local fixture files) so the suite is deterministic and passes without network access.
+- **Use the shared helpers instead of re-rolling them**, e.g. `WC_Unit_Test_Case::with_direct_product_attribute_lookup_updates()` for fixture work that would otherwise leave scheduled lookup actions behind.
+
+The `SpeedTrapListener` reports tests slower than one second at the end of each run — treat an entry there as a review flag.
+
## Automated Tests
Tests are automatically run with [GitHub Actions](https://github.com/woocommerce/woocommerce/actions/workflows/ci.yml) for each commit and pull request.
diff --git a/plugins/woocommerce/tests/legacy/data/sample-woo-plugin.zip b/plugins/woocommerce/tests/legacy/data/sample-woo-plugin.zip
new file mode 100644
index 00000000000..5a42dbb95ce
Binary files /dev/null and b/plugins/woocommerce/tests/legacy/data/sample-woo-plugin.zip differ
diff --git a/plugins/woocommerce/tests/legacy/framework/class-wc-rest-unit-test-case.php b/plugins/woocommerce/tests/legacy/framework/class-wc-rest-unit-test-case.php
index df7eec8f406..8b6bcd6e293 100644
--- a/plugins/woocommerce/tests/legacy/framework/class-wc-rest-unit-test-case.php
+++ b/plugins/woocommerce/tests/legacy/framework/class-wc-rest-unit-test-case.php
@@ -8,10 +8,171 @@
* @since 3.0
*/
+/**
+ * Base class for REST related unit test classes.
+ */
+class WC_Lazy_REST_Server extends WP_Test_Spy_REST_Server {
+
+ /**
+ * Route namespaces initialized on this server.
+ *
+ * @var array<string, bool>
+ */
+ private $initialized_namespaces = array();
+
+ /**
+ * Whether every REST route has been initialized.
+ *
+ * @var bool
+ */
+ private $all_routes_initialized = false;
+
+ /**
+ * Whether route initialization is currently running.
+ *
+ * @var bool
+ */
+ private $initializing = false;
+
+ /**
+ * Number of requests currently being dispatched.
+ *
+ * @var int
+ */
+ private $dispatch_depth = 0;
+
+ /**
+ * Initialize only the namespace needed by a request before dispatching it.
+ *
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response
+ */
+ public function dispatch( $request ) {
+ $route = $request->get_route();
+ $namespace = $this->get_route_namespace( $route );
+
+ if ( ! $this->initializing && ! $this->all_routes_initialized && ! isset( $this->initialized_namespaces[ $namespace ] ) && ! $this->has_route( $route ) ) {
+ $initialized_all_routes = $this->initialize_routes( $route );
+
+ if ( $initialized_all_routes || '' === $namespace ) {
+ $this->all_routes_initialized = true;
+ } else {
+ $this->initialized_namespaces[ $namespace ] = true;
+ }
+ }
+
+ ++$this->dispatch_depth;
+ try {
+ return parent::dispatch( $request );
+ } finally {
+ --$this->dispatch_depth;
+ }
+ }
+
+ /**
+ * Get registered routes, initializing all routes for direct inspection.
+ *
+ * A server holding only manually registered routes (never initialized by a
+ * dispatch) is left untouched so focused-controller tests can assert on
+ * exactly the routes they registered.
+ *
+ * @param string $route_namespace Optionally limit results to a namespace.
+ * @return array
+ */
+ public function get_routes( $route_namespace = '' ) {
+ $scoped_initialization_ran = array() !== $this->initialized_namespaces;
+
+ if ( 0 === $this->dispatch_depth && ! $this->initializing && ! $this->all_routes_initialized && ( $scoped_initialization_ran || ! $this->has_registered_routes() ) ) {
+ $this->initialize_all_routes();
+ }
+
+ return parent::get_routes( $route_namespace );
+ }
+
+ /**
+ * Initialize every REST route on this server.
+ */
+ public function initialize_all_routes(): void {
+ if ( ! $this->all_routes_initialized ) {
+ $this->initialize_routes();
+ $this->all_routes_initialized = true;
+ }
+ }
+
+ /**
+ * Run REST route registration with an optional production route context.
+ *
+ * @param string|null $route REST route being requested, or null to initialize all routes.
+ * @return bool Whether every route was initialized.
+ */
+ private function initialize_routes( $route = null ): bool {
+ $this->initializing = true;
+ try {
+ WC_Unit_Test_Case::with_rest_route_context(
+ $route,
+ static function () {
+ // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+ do_action( 'rest_api_init' );
+ }
+ );
+ } finally {
+ $this->initializing = false;
+ }
+
+ return ! is_object( $GLOBALS['wp'] ?? null ) || null === $route;
+ }
+
+ /**
+ * Check whether a non-core route has already been registered.
+ *
+ * A pristine server only holds the two endpoints WP_REST_Server registers
+ * in its constructor: `/` and `/batch/v1`.
+ *
+ * @return bool
+ */
+ private function has_registered_routes(): bool {
+ return count( $this->endpoints ) > 2;
+ }
+
+ /**
+ * Check whether a request can already be served without global initialization.
+ *
+ * @param string $request_route REST request route.
+ * @return bool
+ */
+ private function has_route( string $request_route ): bool {
+ if ( '/' === $request_route || '/batch/v1' === $request_route ) {
+ return false;
+ }
+
+ foreach ( array_keys( $this->endpoints ) as $route ) {
+ if ( preg_match( '@^' . $route . '$@i', $request_route ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get a stable namespace key from a REST route.
+ *
+ * @param string $route REST route.
+ * @return string
+ */
+ private function get_route_namespace( string $route ): string {
+ $segments = explode( '/', trim( $route, '/' ) );
+
+ return implode( '/', array_slice( $segments, 0, 2 ) );
+ }
+}
+
+// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound -- Test server and its base case are intentionally colocated.
/**
* Base class for REST related unit test classes.
*/
class WC_REST_Unit_Test_Case extends WC_Unit_Test_Case {
+ // phpcs:enable Generic.Files.OneObjectStructurePerFile.MultipleFound
/**
* @var WP_REST_Server
@@ -24,9 +185,9 @@ class WC_REST_Unit_Test_Case extends WC_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
global $wp_rest_server;
- $wp_rest_server = new WP_Test_Spy_REST_Server();
+ $wp_rest_server = new WC_Lazy_REST_Server();
$this->server = $wp_rest_server;
- do_action( 'rest_api_init' );
+ $this->initialize_rest_api_defaults();
// Reset payment gateways.
$gateways = WC_Payment_Gateways::instance();
@@ -34,6 +195,20 @@ class WC_REST_Unit_Test_Case extends WC_Unit_Test_Case {
$gateways->init();
}
+ /**
+ * Initialize all REST routes for tests that depend on eager registration timing.
+ */
+ protected function initialize_rest_api_routes(): void {
+ $this->server->initialize_all_routes();
+ }
+
+ /**
+ * Establish WordPress REST defaults without registering WooCommerce routes.
+ */
+ private function initialize_rest_api_defaults(): void {
+ self::do_isolated_rest_api_init( array( 'rest_api_default_filters' ) );
+ }
+
/**
* Unset the server.
*/
diff --git a/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
index e1c126f9622..21d45108e3d 100644
--- a/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
+++ b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
@@ -34,6 +34,65 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
*/
private static $code_hacker_temporary_disables_requested = 0;
+ /**
+ * Stable filter used to run product attribute lookup updates synchronously.
+ *
+ * @var Closure|null
+ */
+ private static $direct_product_attribute_lookup_updates_filter;
+
+ /**
+ * How many unbalanced enable_direct_product_attribute_lookup_updates() calls are active.
+ *
+ * @var int
+ */
+ private static $direct_product_attribute_lookup_updates_depth = 0;
+
+ /**
+ * Enable synchronous product attribute lookup updates for test fixtures.
+ *
+ * Calls can be nested; the filter is removed once every enable has been
+ * balanced by a disable_direct_product_attribute_lookup_updates() call.
+ */
+ protected static function enable_direct_product_attribute_lookup_updates(): void {
+ if ( null === self::$direct_product_attribute_lookup_updates_filter ) {
+ self::$direct_product_attribute_lookup_updates_filter = static function () {
+ return 'yes';
+ };
+ }
+
+ if ( 0 === self::$direct_product_attribute_lookup_updates_depth++ ) {
+ add_filter( 'pre_option_woocommerce_attribute_lookup_direct_updates', self::$direct_product_attribute_lookup_updates_filter );
+ }
+ }
+
+ /**
+ * Restore the product attribute lookup update mode after fixture work.
+ */
+ protected static function disable_direct_product_attribute_lookup_updates(): void {
+ if ( self::$direct_product_attribute_lookup_updates_depth > 0 && 0 === --self::$direct_product_attribute_lookup_updates_depth ) {
+ remove_filter( 'pre_option_woocommerce_attribute_lookup_direct_updates', self::$direct_product_attribute_lookup_updates_filter );
+ }
+ }
+
+ /**
+ * Run a callback with synchronous product attribute lookup updates enabled.
+ *
+ * Use for fixture creation/deletion so product writes update the lookup
+ * table directly instead of leaving scheduled actions behind.
+ *
+ * @param callable $callback Fixture lifecycle callback.
+ * @return mixed The callback's return value.
+ */
+ public static function with_direct_product_attribute_lookup_updates( callable $callback ) {
+ self::enable_direct_product_attribute_lookup_updates();
+ try {
+ return $callback();
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
+ }
+ }
+
/**
* Increase the count of Code Hacker disable requests, and effectively disable it if the count was zero.
* Does nothing if the code hacker wasn't enabled when the test suite started running.
@@ -88,6 +147,103 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
wc_get_container()->get( LegacyProxy::class )->reset();
}
+ /**
+ * Fire rest_api_init with only the provided callbacks attached.
+ *
+ * The global rest_api_init hook is stashed, replaced with a fresh hook
+ * holding just the callbacks, fired, and restored afterwards.
+ *
+ * @param callable[] $callbacks Callbacks to run on the isolated hook.
+ */
+ protected static function do_isolated_rest_api_init( array $callbacks ): void {
+ global $wp_filter;
+
+ $rest_api_init_hook = $wp_filter['rest_api_init'] ?? null;
+ $wp_filter['rest_api_init'] = new WP_Hook(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- The original hook is restored below.
+
+ foreach ( $callbacks as $callback ) {
+ add_action( 'rest_api_init', $callback );
+ }
+
+ try {
+ // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+ do_action( 'rest_api_init' );
+ } finally {
+ if ( null === $rest_api_init_hook ) {
+ unset( $wp_filter['rest_api_init'] );
+ } else {
+ $wp_filter['rest_api_init'] = $rest_api_init_hook; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Restore the original hook.
+ }
+ }
+ }
+
+ /**
+ * Run a callback with $wp->query_vars['rest_route'] temporarily set (or unset).
+ *
+ * WooCommerce registers REST namespaces lazily based on the requested route,
+ * so this lets fixture code scope rest_api_init work to one namespace, or
+ * force full registration by passing null.
+ *
+ * @param string|null $route REST route to expose during the callback, or null to unset it.
+ * @param callable $callback Callback to run.
+ * @return mixed The callback's return value.
+ */
+ public static function with_rest_route_context( ?string $route, callable $callback ) {
+ $wp = $GLOBALS['wp'] ?? null;
+ $has_wp_context = is_object( $wp );
+ $had_rest_route = $has_wp_context && array_key_exists( 'rest_route', $wp->query_vars );
+ $rest_route = $has_wp_context ? ( $wp->query_vars['rest_route'] ?? null ) : null;
+
+ if ( $has_wp_context ) {
+ if ( null === $route ) {
+ unset( $wp->query_vars['rest_route'] );
+ } else {
+ $wp->query_vars['rest_route'] = $route;
+ }
+ }
+
+ try {
+ return $callback();
+ } finally {
+ if ( $has_wp_context ) {
+ if ( $had_rest_route ) {
+ $wp->query_vars['rest_route'] = $rest_route;
+ } else {
+ unset( $wp->query_vars['rest_route'] );
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a REST server with only the requested route callbacks registered.
+ *
+ * @param callable[] $route_registration_callbacks Callbacks that register routes on rest_api_init.
+ * @param bool $register_default_filters Whether to register WordPress default REST filters.
+ * @return WP_REST_Server
+ */
+ protected function create_rest_server_with_routes( array $route_registration_callbacks, bool $register_default_filters = false ): WP_REST_Server {
+ global $wp_rest_server;
+
+ $wp_rest_server = new WP_REST_Server();
+
+ if ( $register_default_filters ) {
+ array_unshift( $route_registration_callbacks, 'rest_api_default_filters' );
+ }
+
+ self::do_isolated_rest_api_init( $route_registration_callbacks );
+
+ return $wp_rest_server;
+ }
+
+ /**
+ * Clear the global REST server created for a test.
+ */
+ protected function clear_rest_server(): void {
+ global $wp_rest_server;
+ $wp_rest_server = null;
+ }
+
/**
* Set up class unit test.
*
@@ -104,8 +260,13 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
* Clean up after tests have run.
*/
public static function tearDownAfterClass(): void {
- self::clear_hpos_orders();
- parent::tearDownAfterClass();
+ self::enable_direct_product_attribute_lookup_updates();
+ try {
+ self::clear_hpos_orders();
+ parent::tearDownAfterClass();
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
+ }
}
/**
@@ -481,7 +642,7 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
$existing_tables = array_unique( array_intersect( $order_tables, $wpdb->get_col( 'SHOW TABLES' ) ) );
foreach ( $existing_tables as $table ) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
- $wpdb->query( "TRUNCATE TABLE {$table}" );
+ $wpdb->query( "DELETE FROM {$table}" );
}
}
}
diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-admin-notes.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-admin-notes.php
index 88428915f22..0507d344b7f 100644
--- a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-admin-notes.php
+++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-admin-notes.php
@@ -19,12 +19,14 @@ class WC_Helper_Admin_Notes {
*/
public static function reset_notes_dbs() {
global $wpdb;
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_admin_notes" ); // @codingStandardsIgnoreLine.
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_admin_note_actions" ); // @codingStandardsIgnoreLine.
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_admin_notes" ); // @codingStandardsIgnoreLine.
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_admin_note_actions" ); // @codingStandardsIgnoreLine.
}
/**
- * Create four notes that we can use for notes REST API tests
+ * Create four notes that we can use for notes REST API tests.
+ *
+ * @return int[] Note IDs keyed by their fixture number.
*/
public static function add_notes_for_tests() {
$data_store = WC_Data_Store::load( 'admin-note' );
@@ -92,6 +94,13 @@ class WC_Helper_Admin_Notes {
'?s=PHPUNIT_TEST_NOTE_4_ACTION_2_URL'
);
$note_4->save();
+
+ return array(
+ 1 => $note_1->get_id(),
+ 2 => $note_2->get_id(),
+ 3 => $note_3->get_id(),
+ 4 => $note_4->get_id(),
+ );
}
/**
diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-order.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-order.php
index f5dff3c0d4f..5081b617850 100644
--- a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-order.php
+++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-order.php
@@ -75,7 +75,6 @@ class WC_Helper_Order {
'total' => wc_get_price_excluding_tax( $product, array( 'qty' => 4 ) ),
)
);
- $item->save();
$order->add_item( $item );
// Set billing address.
diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-reports.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-reports.php
index cfb31cdd8a1..6d2fcee85cc 100644
--- a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-reports.php
+++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-reports.php
@@ -21,6 +21,19 @@ class WC_Helper_Reports {
$wpdb->query( 'DELETE FROM ' . \Automattic\WooCommerce\Admin\API\Reports\Products\DataStore::get_db_table_name() ); // @codingStandardsIgnoreLine.
$wpdb->query( 'DELETE FROM ' . \Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore::get_db_table_name() ); // @codingStandardsIgnoreLine.
$wpdb->query( 'DELETE FROM ' . \Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore::get_db_table_name() ); // @codingStandardsIgnoreLine.
- \Automattic\WooCommerce\Internal\Admin\CategoryLookup::instance()->regenerate();
+ $wpdb->query( "DELETE FROM {$wpdb->wc_category_lookup}" ); // @codingStandardsIgnoreLine.
+
+ $category_lookup = \Automattic\WooCommerce\Internal\Admin\CategoryLookup::instance();
+ $category_ids = get_terms(
+ array(
+ 'taxonomy' => 'product_cat',
+ 'hide_empty' => false,
+ 'fields' => 'ids',
+ )
+ );
+
+ foreach ( $category_ids as $category_id ) {
+ $category_lookup->on_create( $category_id );
+ }
}
}
diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-shipping-zones.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-shipping-zones.php
index a3aa26d4431..cff50ab6953 100644
--- a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-shipping-zones.php
+++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-shipping-zones.php
@@ -8,10 +8,13 @@
class WC_Helper_Shipping_Zones {
/**
- * Create some mock shipping zones to test against
+ * Create some mock shipping zones to test against.
+ *
+ * @return int[] Zone IDs keyed by fixture name.
*/
public static function create_mock_zones() {
self::remove_mock_zones();
+ $zone_ids = array();
// Local zone
$zone = new WC_Shipping_Zone();
@@ -20,6 +23,7 @@ class WC_Helper_Shipping_Zones {
$zone->add_location( 'GB', 'country' );
$zone->add_location( 'CB*', 'postcode' );
$zone->save();
+ $zone_ids['local'] = $zone->get_id();
// Europe zone
$zone = new WC_Shipping_Zone();
@@ -27,6 +31,7 @@ class WC_Helper_Shipping_Zones {
$zone->set_zone_order( 2 );
$zone->add_location( 'EU', 'continent' );
$zone->save();
+ $zone_ids['europe'] = $zone->get_id();
// US california zone
$zone = new WC_Shipping_Zone();
@@ -34,6 +39,7 @@ class WC_Helper_Shipping_Zones {
$zone->set_zone_order( 3 );
$zone->add_location( 'US:CA', 'state' );
$zone->save();
+ $zone_ids['california'] = $zone->get_id();
// US zone
$zone = new WC_Shipping_Zone();
@@ -41,6 +47,9 @@ class WC_Helper_Shipping_Zones {
$zone->set_zone_order( 4 );
$zone->add_location( 'US', 'country' );
$zone->save();
+ $zone_ids['us'] = $zone->get_id();
+
+ return $zone_ids;
}
/**
@@ -48,9 +57,9 @@ class WC_Helper_Shipping_Zones {
*/
public static function remove_mock_zones() {
global $wpdb;
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods;" );
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_locations;" );
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_shipping_zones;" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_shipping_zone_methods;" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_shipping_zone_locations;" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_shipping_zones;" );
WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
}
}
diff --git a/plugins/woocommerce/tests/legacy/includes/wp-http-testcase.php b/plugins/woocommerce/tests/legacy/includes/wp-http-testcase.php
index d03277392e0..19ceb5cde01 100644
--- a/plugins/woocommerce/tests/legacy/includes/wp-http-testcase.php
+++ b/plugins/woocommerce/tests/legacy/includes/wp-http-testcase.php
@@ -16,6 +16,20 @@ use Automattic\Jetpack\Constants;
*/
abstract class WP_HTTP_TestCase extends WP_UnitTestCase {
+ /**
+ * Shared sample image URL used by WooCommerce tests.
+ *
+ * @var string
+ */
+ private const SAMPLE_IMAGE_URL = 'http://cldup.com/Dr1Bczxq4q.png';
+
+ /**
+ * Shared sample image fixture used by WooCommerce tests.
+ *
+ * @var string
+ */
+ private const SAMPLE_IMAGE_FILE = 'Dr1Bczxq4q.png';
+
/**
* The HTTP requests caught.
*
@@ -40,6 +54,13 @@ abstract class WP_HTTP_TestCase extends WP_UnitTestCase {
*/
protected $http_responder;
+ /**
+ * Whether a mocked sample image is waiting to be moved into uploads.
+ *
+ * @var bool
+ */
+ private $sample_image_download_pending = false;
+
/**
* Whether the class has been initialized.
*
@@ -181,8 +202,10 @@ abstract class WP_HTTP_TestCase extends WP_UnitTestCase {
parent::tearDown();
remove_filter( 'pre_http_request', array( $this, 'http_request_listner' ) );
+ remove_filter( 'wp_handle_sideload_overrides', array( $this, 'set_sample_image_unique_filename_callback' ) );
- $this->skip_cache_next = false;
+ $this->skip_cache_next = false;
+ $this->sample_image_download_pending = false;
}
//
@@ -213,9 +236,66 @@ abstract class WP_HTTP_TestCase extends WP_UnitTestCase {
$preempt = call_user_func( $this->http_responder, $request, $url );
}
+ if ( false === $preempt && self::SAMPLE_IMAGE_URL === $url ) {
+ $preempt = $this->mock_sample_image_response( $request );
+ }
+
return $preempt;
}
+ /**
+ * Mock the shared sample image download from a local fixture.
+ *
+ * @param array $request The request arguments.
+ * @return array
+ */
+ protected function mock_sample_image_response( $request ) {
+ $fixture_path = WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/' . self::SAMPLE_IMAGE_FILE;
+ $response = array(
+ 'headers' => array(
+ 'content-type' => 'image/png',
+ ),
+ 'body' => '',
+ 'response' => array(
+ 'code' => 200,
+ 'message' => 'OK',
+ ),
+ 'cookies' => array(),
+ );
+
+ if ( ! empty( $request['filename'] ) ) {
+ WC_Unit_Test_Case::file_copy( $fixture_path, $request['filename'] );
+ $this->sample_image_download_pending = true;
+ add_filter( 'wp_handle_sideload_overrides', array( $this, 'set_sample_image_unique_filename_callback' ), 10, 2 );
+ $response['filename'] = $request['filename'];
+ return $response;
+ }
+
+ $response['body'] = file_get_contents( $fixture_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a local test fixture.
+ return $response;
+ }
+
+ /**
+ * Use a unique filename for the pending sample image download.
+ *
+ * @param array $overrides Sideload overrides.
+ * @param array $file Sideloaded file data.
+ * @return array
+ */
+ public function set_sample_image_unique_filename_callback( $overrides, $file ) {
+ if ( ! $this->sample_image_download_pending || self::SAMPLE_IMAGE_FILE !== $file['name'] ) {
+ return $overrides;
+ }
+
+ $this->sample_image_download_pending = false;
+ remove_filter( 'wp_handle_sideload_overrides', array( $this, 'set_sample_image_unique_filename_callback' ) );
+ $overrides['unique_filename_callback'] = static function ( $_dir, $name, $ext ) {
+ return $name . '-' . wp_generate_uuid4() . $ext;
+ };
+
+ return $overrides;
+ }
+
/**
* Route a request through to a predefined host, with optional caching.
*
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-duplicate-product.php b/plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-duplicate-product.php
index 2ca26fb5ac6..05bc8f85c97 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-duplicate-product.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/admin/class-wc-tests-admin-duplicate-product.php
@@ -67,7 +67,30 @@ class WC_Tests_Admin_Duplicate_Product extends WC_Unit_Test_Case {
* Tests that the unique slugs generated by variation duplication are correct.
*/
public function test_variable_product_duplication_unique_slug_generation() {
- $product = WC_Helper_Product::create_variation_product();
+ $product = new WC_Product_Variable();
+ $product->set_name( 'Dummy Variable Product' );
+ $product->save();
+
+ $variation_slugs = array(
+ 'dummy-variable-product-small',
+ 'dummy-variable-product-large',
+ 'dummy-variable-product',
+ 'dummy-variable-product-2',
+ 'dummy-variable-product-3',
+ 'dummy-variable-product-4',
+ );
+ $variation_ids = array();
+ foreach ( $variation_slugs as $variation_slug ) {
+ $variation_ids[] = self::factory()->post->create(
+ array(
+ 'post_name' => $variation_slug,
+ 'post_parent' => $product->get_id(),
+ 'post_status' => ProductStatus::PUBLISH,
+ 'post_type' => 'product_variation',
+ )
+ );
+ }
+ $product->set_children( $variation_ids );
// We just want to make sure each successive duplication has the correct slugs.
$slug_matches = array(
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php b/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php
index 9b38797249d..333432179e4 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php
@@ -64,6 +64,8 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
'cost' => '9.59',
);
update_option( 'woocommerce_flat_rate_settings', $flat_rate_settings );
+ WC_Cache_Helper::get_transient_version( 'shipping', true );
+ WC()->shipping->load_shipping_methods();
WC_Helper_Shipping::force_customer_us_address();
@@ -373,7 +375,6 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
);
WC_Tax::_insert_tax_rate( $tax_rate );
- $product_ids = array();
$products_data = array(
'5.17',
'3.32',
@@ -384,16 +385,20 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
'5.99',
'5.51',
);
+ $products = array();
+
+ foreach ( $products_data as $price ) {
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_regular_price( $price );
+ $product->save();
+ $products[] = $product;
+ }
foreach ( $expected_values as $customer_tax_exempt => $value ) {
WC()->customer->set_is_vat_exempt( $customer_tax_exempt );
- foreach ( $products_data as $price ) {
- $loop_product = WC_Helper_Product::create_simple_product();
- $product_ids[] = $loop_product->get_id();
- $loop_product->set_regular_price( $price );
- $loop_product->save();
- WC()->cart->add_to_cart( $loop_product->get_id(), 1 );
+ foreach ( $products as $product ) {
+ WC()->cart->add_to_cart( $product->get_id(), 1 );
}
WC()->cart->add_discount( $coupon->get_code() );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/customer/crud.php b/plugins/woocommerce/tests/legacy/unit-tests/customer/crud.php
index b686dd51903..d28fffa4ee1 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/customer/crud.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/customer/crud.php
@@ -308,8 +308,9 @@ class WC_Tests_CustomerCRUD extends WC_Unit_Test_Case {
public function test_customer_get_date_modified_read() {
$customer = WC_Helper_Customer::create_customer();
$customer_id = $customer->get_id();
- $last = get_user_meta( $customer_id, 'last_update', true );
- sleep( 1 );
+ $last = (int) get_user_meta( $customer_id, 'last_update', true ) - 1;
+ update_user_meta( $customer_id, 'last_update', $last );
+ $customer = new WC_Customer( $customer_id );
$this->assertEquals( $last, $customer->get_date_modified()->getOffsetTimestamp() );
$customer->set_billing_address( '1234 Some St.' );
$customer->save();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php b/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php
index 749b9c216dc..3c9a21d293c 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php
@@ -24,15 +24,18 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
protected $csv_file = '';
/**
- * @var WC_Product_CSV_Importer
+ * Attachment IDs created by the current test.
+ *
+ * @var int[]
*/
- private $sut;
+ private $created_attachment_ids = array();
/**
* Load up the importer classes since they aren't loaded by default.
*/
public function setUp(): void {
parent::setUp();
+ add_action( 'add_attachment', array( $this, 'track_created_attachment' ) );
$bootstrap = WC_Unit_Tests_Bootstrap::instance();
require_once $bootstrap->plugin_dir . '/includes/import/class-wc-product-csv-importer.php';
@@ -48,17 +51,47 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
// Callback used by WP_HTTP_TestCase to decide whether to perform HTTP requests or to provide a mocked response.
$this->http_responder = array( $this, 'mock_http_responses' );
$this->csv_file = dirname( __FILE__ ) . '/sample.csv';
- $this->sut = new WC_Product_CSV_Importer(
+ }
+
+ /**
+ * Remove physical files for attachments created by the current test.
+ */
+ public function tearDown(): void {
+ remove_action( 'add_attachment', array( $this, 'track_created_attachment' ) );
+ foreach ( array_unique( $this->created_attachment_ids ) as $attachment_id ) {
+ wp_delete_attachment( $attachment_id, true );
+ }
+ $this->created_attachment_ids = array();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Track an attachment created by the current test.
+ *
+ * @param int $attachment_id Attachment ID.
+ */
+ public function track_created_attachment( $attachment_id ): void {
+ $this->created_attachment_ids[] = (int) $attachment_id;
+ }
+
+ /**
+ * Create the importer used by the full import tests.
+ *
+ * @return WC_Product_CSV_Importer
+ */
+ private function get_importer() {
+ wc_get_container()->get( \Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register::class )->delete_all();
+
+ return new WC_Product_CSV_Importer(
$this->csv_file,
array(
'mapping' => $this->get_csv_mapped_items(),
+ 'lines' => 2,
'parse' => true,
'prevent_timeouts' => false,
)
);
-
- // Clear list of approved download directories before running tests.
- wc_get_container()->get( \Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register::class )->delete_all();
}
/**
@@ -125,16 +158,19 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
public function test_import_for_admin_users() {
// In most cases, an admin user will run the import.
wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
- $results = $this->sut->import();
+ $results = $this->get_importer()->import();
$this->assertEquals( 0, count( $results['failed'] ) );
$this->assertEquals( 0, count( $results['updated'] ) );
$this->assertEquals( 0, count( $results['skipped'] ) );
$this->assertEquals(
- 7,
+ 2,
count( $results['imported'] ) + count( $results['imported_variations'] ),
'One import item references a downloadable file stored in an unapproved location: if the import is triggered by an admin user, that location will be automatically approved.'
);
+ $this->assertTrue(
+ wc_get_container()->get( \Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register::class )->is_valid_path( 'http://woo.dev/albums/album.flac' )
+ );
}
/**
@@ -143,16 +179,19 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case {
public function test_import_for_shop_managers() {
// In some cases, a shop manager may run the import.
wp_set_current_user( self::factory()->user->create( array( 'role' => 'shop_manager' ) ) );
- $results = $this->sut->import();
+ $results = $this->get_importer()->import();
$this->assertEquals( 0, count( $results['updated'] ) );
$this->assertEquals( 0, count( $results['skipped'] ) );
- $this->assertEquals( 6, count( $results['imported'] ) + count( $results['imported_variations'] ) );
+ $this->assertEquals( 1, count( $results['imported'] ) + count( $results['imported_variations'] ) );
$this->assertEquals(
1,
count( $results['failed'] ),
'One import item references a downloadable file stored in an unapproved location: if the import is triggered by a non-admin, that item cannot be imported.'
);
+ $this->assertFalse(
+ wc_get_container()->get( \Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register::class )->is_valid_path( 'http://woo.dev/albums/album.flac' )
+ );
}
/**
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php b/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php
index e78124d1cf2..a52124336e7 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php
@@ -19,6 +19,32 @@ use Automattic\WooCommerce\Caches\OrderCountCache;
*/
class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
+ /**
+ * Product shared by helper orders within the current test method.
+ *
+ * @var WC_Product|null
+ */
+ private $order_product;
+
+ /**
+ * Create a complete helper order with a method-owned product.
+ *
+ * @param int $customer_id Customer ID.
+ * @param WC_Product|null $product Optional purpose-built product.
+ * @param array $order_data Optional order properties to override the helper defaults.
+ * @return WC_Order
+ */
+ private function create_order( $customer_id = 1, $product = null, $order_data = array() ) {
+ if ( ! $product instanceof WC_Product ) {
+ if ( ! $this->order_product ) {
+ $this->order_product = WC_Helper_Product::create_simple_product();
+ }
+ $product = $this->order_product;
+ }
+
+ return WC_Helper_Order::create_order( $customer_id, $product, $order_data );
+ }
+
/**
* Test wc_get_order_statuses().
*
@@ -175,7 +201,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
$original_post = $post;
$original_theorder = $theorder;
- $order = WC_Helper_Order::create_order();
+ $order = $this->create_order();
// Assert that $order is a WC_Order object.
$this->assertInstanceOf( 'WC_Order', $order );
@@ -226,7 +252,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
if ( \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) {
$this->markTestSkipped( 'Test only works against Post Meta' );
}
- $order = WC_Helper_Order::create_order();
+ $order = $this->create_order();
$this->assertEmpty( $order->get_payment_tokens() );
$token = WC_Helper_Payment_Token::create_cc_token();
@@ -242,7 +268,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 2.6
*/
public function test_wc_order_add_payment_token() {
- $order = WC_Helper_Order::create_order();
+ $order = $this->create_order();
$this->assertEmpty( $order->get_payment_tokens() );
$token = WC_Helper_Payment_Token::create_cc_token();
@@ -257,10 +283,10 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.0
*/
public function test_wc_get_orders_customer_params() {
- $order1 = WC_Helper_Order::create_order( 0 );
- $order2 = WC_Helper_Order::create_order( 0 );
- $order3 = WC_Helper_Order::create_order( 1 );
- $order4 = WC_Helper_Order::create_order( 1 );
+ $order1 = $this->create_order( 0 );
+ $order2 = $this->create_order( 0 );
+ $order3 = $this->create_order( 1 );
+ $order4 = $this->create_order( 1 );
$order_1 = $order1->get_id();
$order_2 = $order2->get_id();
$order_3 = $order3->get_id();
@@ -306,15 +332,15 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.0
*/
public function test_wc_get_orders_date_params() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->set_date_created( '2015-01-01 05:20:30' );
$order1->save();
$order_1 = $order1->get_id();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->set_date_created( '2017-01-01' );
$order2->save();
$order_2 = $order2->get_id();
- $order3 = WC_Helper_Order::create_order();
+ $order3 = $this->create_order();
$order3->set_date_created( '2017-01-01' );
$order3->save();
$order_3 = $order3->get_id();
@@ -393,10 +419,10 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_status_param() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->set_status( OrderStatus::PENDING );
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->set_status( OrderStatus::COMPLETED );
$order2->save();
@@ -425,7 +451,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_type_param() {
- $order = WC_Helper_Order::create_order();
+ $order = $this->create_order();
$order->save();
$refund = new WC_Order_Refund();
$refund->save();
@@ -455,7 +481,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_version_param() {
- $order = WC_Helper_Order::create_order();
+ $order = $this->create_order();
$order->save();
$orders = wc_get_orders(
@@ -483,10 +509,10 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_created_via_param() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->set_created_via( 'rest-api' );
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->set_created_via( 'checkout' );
$order2->save();
@@ -515,13 +541,13 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_parent_param() {
- $parent = WC_Helper_Order::create_order();
+ $parent = $this->create_order();
$parent->save();
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->set_parent_id( $parent->get_id() );
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->save();
$orders = wc_get_orders(
@@ -540,13 +566,13 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_parent_exclude_param() {
- $parent = WC_Helper_Order::create_order();
+ $parent = $this->create_order();
$parent->save();
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->set_parent_id( $parent->get_id() );
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->save();
$orders = wc_get_orders(
@@ -566,9 +592,9 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_exclude_param() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->save();
$orders = wc_get_orders(
@@ -587,9 +613,9 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_limit_param() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->save();
$orders = wc_get_orders( array( 'limit' => 1 ) );
@@ -602,9 +628,9 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_paged_param() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->save();
$orders = wc_get_orders(
@@ -638,9 +664,9 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_offset_param() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->save();
$orders = wc_get_orders(
@@ -661,9 +687,9 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_paginate_param() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->save();
$orders = wc_get_orders( array( 'paginate' => true ) );
@@ -699,9 +725,9 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_order_param() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->save();
$orders = wc_get_orders(
@@ -731,10 +757,10 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_currency_param() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->set_currency( 'BRL' );
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->set_currency( 'USD' );
$order2->save();
@@ -763,10 +789,10 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_prices_include_tax_param() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->set_prices_include_tax( true );
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->set_prices_include_tax( false );
$order2->save();
@@ -795,10 +821,10 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_payment_method_param() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->set_payment_method( WC_Gateway_Cheque::ID );
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->set_payment_method( WC_Gateway_COD::ID );
$order2->save();
@@ -827,10 +853,10 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_get_order_payment_method_title_param() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->set_payment_method_title( 'Check payments' );
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->set_payment_method_title( 'PayPal' );
$order2->save();
@@ -945,11 +971,11 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
$customer2->set_billing_email( 'customer2@test.com' );
$customer2->save();
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->set_customer_id( $customer1->get_id() );
$order1->set_billing_email( $customer1->get_billing_email() );
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->set_customer_id( $customer2->get_id() );
$order2->set_billing_email( $customer2->get_billing_email() );
$order2->save();
@@ -999,10 +1025,10 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
$customer2->set_billing_email( 'customer2@test.com' );
$customer2->save();
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->set_customer_id( $customer1->get_id() );
$order1->save();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->set_customer_id( $customer2->get_id() );
$order2->save();
@@ -1031,7 +1057,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.1
*/
public function test_wc_get_order_address_params() {
- $order1 = WC_Helper_Order::create_order();
+ $order1 = $this->create_order();
$order1->set_props(
array(
'billing_email' => 'test1@test.com',
@@ -1059,7 +1085,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
$order1->save();
$order1_id = $order1->get_id();
- $order2 = WC_Helper_Order::create_order();
+ $order2 = $this->create_order();
$order2->set_props(
array(
'billing_email' => 'test2@test.com',
@@ -1295,7 +1321,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
$date_range = $start . '...' . $end;
// Populate orders.
- $us_now = WC_Helper_Order::create_order();
+ $us_now = $this->create_order();
$us_now->set_props(
array(
'shipping_country' => 'US',
@@ -1303,7 +1329,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
);
$us_now->save();
- $us_old = WC_Helper_Order::create_order();
+ $us_old = $this->create_order();
$us_old->set_props(
array(
'date_created' => $yesterday,
@@ -1312,7 +1338,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
);
$us_old->save();
- $mx_now = WC_Helper_Order::create_order();
+ $mx_now = $this->create_order();
$mx_now->set_props(
array(
'shipping_country' => 'MX',
@@ -1320,7 +1346,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
);
$mx_now->save();
- $mx_old = WC_Helper_Order::create_order();
+ $mx_old = $this->create_order();
$mx_old->set_props(
array(
'date_created' => $yesterday,
@@ -1420,12 +1446,12 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
}
/**
- * Test that order modified date is updated when a refund is created for it.
+ * @testdox Creating a refund advances the parent order modified date.
*
* @link https://github.com/woocommerce/woocommerce/issues/28969
*/
public function test_wc_create_refund_28969() {
- $order = WC_Helper_Order::create_order(
+ $order = $this->create_order(
1,
WC_Helper_Product::create_simple_product(),
array(
@@ -1433,10 +1459,32 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
)
);
// Ensure the order is a complete object with an initial modified date.
- $order = wc_get_order( $order->get_id() );
+ $order = wc_get_order( $order->get_id() );
+ $initial_date_modified = '2020-01-01 00:00:00';
- // Ensure the order's initial modified date is sufficiently in the past.
- sleep( 1 );
+ if ( \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) {
+ $order->set_date_modified( $initial_date_modified );
+ $order->save();
+ } else {
+ global $wpdb;
+
+ $this->assertSame(
+ 1,
+ $wpdb->update(
+ $wpdb->posts,
+ array(
+ 'post_modified' => $initial_date_modified,
+ 'post_modified_gmt' => $initial_date_modified,
+ ),
+ array( 'ID' => $order->get_id() )
+ ),
+ 'The CPT fixture must update exactly one order row.'
+ );
+ clean_post_cache( $order->get_id() );
+ }
+
+ $order = wc_get_order( $order->get_id() );
+ $initial_modified_timestamp = $order->get_date_modified()->getTimestamp();
$args = array(
'order_id' => $order->get_id(),
@@ -1446,7 +1494,11 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
wc_create_refund( $args );
$updated_order = wc_get_order( $order->get_id() );
- $this->assertNotEquals( $order->get_date_modified()->getTimestamp(), $updated_order->get_date_modified()->getTimestamp() );
+ $this->assertGreaterThan(
+ $initial_modified_timestamp,
+ $updated_order->get_date_modified()->getTimestamp(),
+ 'Creating a refund must advance the persisted parent order modified date.'
+ );
}
/**
@@ -1466,7 +1518,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
*/
public function test_wc_get_order_note() {
$note_content = 'Note content';
- $order = WC_Helper_Order::create_order();
+ $order = $this->create_order();
$note_id = (int) $order->add_order_note( $note_content );
$expected = array(
'id' => $note_id,
@@ -1487,8 +1539,13 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.2.0
*/
public function test_wc_get_order_notes() {
- $order = WC_Helper_Order::create_order();
- $order->add_order_note( 'Customer note', 1 );
+ $order = $this->create_order();
+ add_filter( 'woocommerce_email_log_add_order_note', '__return_false' );
+ try {
+ $order->add_order_note( 'Customer note', 1 );
+ } finally {
+ remove_filter( 'woocommerce_email_log_add_order_note', '__return_false' );
+ }
$order->add_order_note( 'Internal note' );
$order->add_order_note( 'Another internal note' );
@@ -1518,7 +1575,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.2.0
*/
public function test_wc_create_order_note() {
- $order = WC_Helper_Order::create_order();
+ $order = $this->create_order();
$note_id = wc_create_order_note( $order->get_id(), 'Note content', false, false );
$this->assertTrue( 0 < $note_id );
@@ -1530,7 +1587,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.2.0
*/
public function test_wc_delete_order_note() {
- $order = WC_Helper_Order::create_order();
+ $order = $this->create_order();
$note_id = $order->add_order_note( 'Note content' );
$this->assertTrue( wc_delete_order_note( $note_id ) );
@@ -1542,7 +1599,7 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case {
* @since 3.3.0
*/
public function test_wc_order_search() {
- $order = WC_Helper_Order::create_order();
+ $order = $this->create_order();
// Should find order searching by billing address name.
$this->assertEquals( array( $order->get_id() ), wc_order_search( 'Jeroen' ) );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/privacy/export.php b/plugins/woocommerce/tests/legacy/unit-tests/privacy/export.php
index 8aca20e4577..168af21f82c 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/privacy/export.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/privacy/export.php
@@ -29,32 +29,34 @@ class WC_Test_Privacy_Export extends WC_Unit_Test_Case {
*/
public function setUp(): void {
parent::setUp();
+ $this->orders = array();
+ $this->customers = array();
$customer1 = WC_Helper_Customer::create_customer( 'customer1', 'password', 'test1@test.com' );
$customer1->set_billing_email( 'customer1@test.com' );
$customer1->save();
+ $this->customers[] = $customer1;
+ }
+
+ /**
+ * Create customers and orders needed only by the order exporter pagination test.
+ */
+ private function create_order_export_fixtures(): void {
+ $customer1 = $this->customers[0];
$customer2 = WC_Helper_Customer::create_customer( 'customer2', 'password', 'test2@test.com' );
$customer2->set_billing_email( 'customer2@test.com' );
$customer2->save();
- $this->customers[] = $customer1;
$this->customers[] = $customer2;
- // Create a bunch of dummy orders for some users.
- $this->orders[] = WC_Helper_Order::create_order( $customer1->get_id() );
- $this->orders[] = WC_Helper_Order::create_order( $customer1->get_id() );
- $this->orders[] = WC_Helper_Order::create_order( $customer1->get_id() );
- $this->orders[] = WC_Helper_Order::create_order( $customer1->get_id() );
- $this->orders[] = WC_Helper_Order::create_order( $customer1->get_id() );
- $this->orders[] = WC_Helper_Order::create_order( $customer1->get_id() );
- $this->orders[] = WC_Helper_Order::create_order( $customer1->get_id() );
- $this->orders[] = WC_Helper_Order::create_order( $customer1->get_id() );
- $this->orders[] = WC_Helper_Order::create_order( $customer1->get_id() );
- $this->orders[] = WC_Helper_Order::create_order( $customer1->get_id() );
- $this->orders[] = WC_Helper_Order::create_order( $customer1->get_id() );
- $this->orders[] = WC_Helper_Order::create_order( $customer2->get_id() );
- $this->orders[] = WC_Helper_Order::create_order( $customer2->get_id() );
+ for ( $index = 0; $index < 11; $index++ ) {
+ $this->orders[] = WC_Helper_Order::create_order( $customer1->get_id() );
+ }
+
+ for ( $index = 0; $index < 2; $index++ ) {
+ $this->orders[] = WC_Helper_Order::create_order( $customer2->get_id() );
+ }
}
/**
@@ -139,8 +141,12 @@ class WC_Test_Privacy_Export extends WC_Unit_Test_Case {
* Test: Order data exporter.
*/
public function test_order_data_exporter() {
+ $this->create_order_export_fixtures();
+
$response = WC_Privacy_Exporters::order_data_exporter( 'test1@test.com', 1 );
+ $this->assertCount( 10, $response['data'] );
+ $this->assertFalse( $response['done'] );
$this->assertEquals( 'woocommerce_orders', $response['data'][0]['group_id'] );
$this->assertEquals( 'Orders', $response['data'][0]['group_label'] );
$this->assertStringContainsString( 'order-', $response['data'][0]['item_id'] );
@@ -149,6 +155,7 @@ class WC_Test_Privacy_Export extends WC_Unit_Test_Case {
// Next page should be orders.
$response = WC_Privacy_Exporters::order_data_exporter( 'test1@test.com', 2 );
+ $this->assertCount( 1, $response['data'] );
$this->assertTrue( $response['done'] );
$this->assertTrue( 8 === count( $response['data'][0]['data'] ), count( $response['data'][0]['data'] ) );
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/product/data-store.php b/plugins/woocommerce/tests/legacy/unit-tests/product/data-store.php
index a139f73d1d4..8a70b7dccea 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/product/data-store.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/product/data-store.php
@@ -1014,43 +1014,30 @@ class WC_Tests_Product_Data_Store extends WC_Unit_Test_Case {
* Test WC_Product_Data_Store_CPT::create_all_product_variations
*/
public function test_variable_create_all_product_variations() {
- $product = new WC_Product_Variable();
- $product->set_name( 'Test Variable Product' );
-
- $attribute_1 = new WC_Product_Attribute();
- $attribute_1->set_name( 'color' );
- $attribute_1->set_visible( true );
- $attribute_1->set_variation( true );
- $attribute_1->set_options( array( 'red', 'green', 'blue' ) );
-
- $attribute_2 = new WC_Product_Attribute();
- $attribute_2->set_name( 'size' );
- $attribute_2->set_visible( true );
- $attribute_2->set_variation( true );
- $attribute_2->set_options( array( 'small', 'medium', 'large' ) );
-
- $attribute_3 = new WC_Product_Attribute();
- $attribute_3->set_name( 'pattern' );
- $attribute_3->set_visible( true );
- $attribute_3->set_variation( true );
- $attribute_3->set_options( array( 'striped', 'polka-dot', 'plain' ) );
-
- $attributes = array(
- $attribute_1,
- $attribute_2,
- $attribute_3,
+ $product = $this->create_variable_product_with_attributes(
+ array(
+ 'color' => array( 'red', 'blue' ),
+ 'size' => array( 'small', 'large' ),
+ )
);
- $product->set_attributes( $attributes );
- $product_id = $product->save();
-
// Test all variations get linked.
$data_store = WC_Data_Store::load( 'product' );
- $count = $data_store->create_all_product_variations( wc_get_product( $product_id ) );
- $this->assertEquals( 27, $count );
+ $count = $data_store->create_all_product_variations( wc_get_product( $product->get_id() ) );
+ $this->assertEquals( 4, $count );
+
+ $created_combinations = array_map(
+ static function ( $variation_id ) {
+ $attributes = wc_get_product( $variation_id )->get_attributes();
+ return $attributes['color'] . ':' . $attributes['size'];
+ },
+ wc_get_product( $product->get_id() )->get_children()
+ );
+ sort( $created_combinations );
+ $this->assertSame( array( 'blue:large', 'blue:small', 'red:large', 'red:small' ), $created_combinations );
// Test duplicates are not created.
- $count = $data_store->create_all_product_variations( wc_get_product( $product_id ) );
+ $count = $data_store->create_all_product_variations( wc_get_product( $product->get_id() ) );
$this->assertEquals( 0, $count );
}
@@ -1100,46 +1087,56 @@ class WC_Tests_Product_Data_Store extends WC_Unit_Test_Case {
* Test WC_Product_Data_Store_CPT::create_all_product_variations
*/
public function test_variable_create_all_product_variations_limits() {
- $product = new WC_Product_Variable();
- $product->set_name( 'Test Variable Product' );
-
- $attribute_1 = new WC_Product_Attribute();
- $attribute_1->set_name( 'color' );
- $attribute_1->set_visible( true );
- $attribute_1->set_variation( true );
- $attribute_1->set_options( array( 'red', 'green', 'blue' ) );
+ $product = $this->create_variable_product_with_attributes(
+ array(
+ 'color' => array( 'red', 'blue' ),
+ 'size' => array( 'small', 'medium', 'large', 'extra-large' ),
+ )
+ );
- $attribute_2 = new WC_Product_Attribute();
- $attribute_2->set_name( 'size' );
- $attribute_2->set_visible( true );
- $attribute_2->set_variation( true );
- $attribute_2->set_options( array( 'small', 'medium', 'large' ) );
+ // Test creation across multiple limited batches and a partial final batch.
+ $data_store = WC_Data_Store::load( 'product' );
+ $count = $data_store->create_all_product_variations( wc_get_product( $product->get_id() ), 3 );
+ $this->assertEquals( 3, $count );
+ $this->assertCount( 3, wc_get_product( $product->get_id() )->get_children() );
- $attribute_3 = new WC_Product_Attribute();
- $attribute_3->set_name( 'pattern' );
- $attribute_3->set_visible( true );
- $attribute_3->set_variation( true );
- $attribute_3->set_options( array( 'striped', 'polka-dot', 'plain' ) );
+ $count = $data_store->create_all_product_variations( wc_get_product( $product->get_id() ), 3 );
+ $this->assertEquals( 3, $count );
+ $this->assertCount( 6, wc_get_product( $product->get_id() )->get_children() );
- $attributes = array(
- $attribute_1,
- $attribute_2,
- $attribute_3,
- );
+ $count = $data_store->create_all_product_variations( wc_get_product( $product->get_id() ), 3 );
+ $this->assertEquals( 2, $count );
+ $this->assertCount( 8, wc_get_product( $product->get_id() )->get_children() );
- $product->set_attributes( $attributes );
- $product_id = $product->save();
+ $count = $data_store->create_all_product_variations( wc_get_product( $product->get_id() ), 3 );
+ $this->assertEquals( 0, $count );
+ $this->assertCount( 8, wc_get_product( $product->get_id() )->get_children() );
+ }
- // Test creation with a limit of 10.
- $data_store = WC_Data_Store::load( 'product' );
- $count = $data_store->create_all_product_variations( wc_get_product( $product_id ), 10 );
- $this->assertEquals( 10, $count );
+ /**
+ * Create a variable product with local variation attributes.
+ *
+ * @param array $attribute_options Attribute names mapped to option lists.
+ * @return WC_Product_Variable
+ */
+ private function create_variable_product_with_attributes( array $attribute_options ) {
+ $attributes = array();
+
+ foreach ( $attribute_options as $name => $options ) {
+ $attribute = new WC_Product_Attribute();
+ $attribute->set_name( $name );
+ $attribute->set_visible( true );
+ $attribute->set_variation( true );
+ $attribute->set_options( $options );
+ $attributes[] = $attribute;
+ }
- $count = $data_store->create_all_product_variations( wc_get_product( $product_id ), 10 );
- $this->assertEquals( 10, $count );
+ $product = new WC_Product_Variable();
+ $product->set_name( 'Test Variable Product' );
+ $product->set_attributes( $attributes );
+ $product->save();
- $count = $data_store->create_all_product_variations( wc_get_product( $product_id ), 10 );
- $this->assertEquals( 7, $count );
+ return $product;
}
/**
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/product/data.php b/plugins/woocommerce/tests/legacy/unit-tests/product/data.php
index e904cd7a967..a07f48972fd 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/product/data.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/product/data.php
@@ -669,14 +669,27 @@ class WC_Tests_Product_Data extends WC_Unit_Test_Case {
* @return array Image ID and URL.
*/
protected function set_product_image( $product ) {
- global $wpdb;
-
- // TODO: find a way to set the product image without performing a HTTP request to make the tests faster.
- $image_url = media_sideload_image( 'http://cldup.com/Dr1Bczxq4q.png', $product->get_id(), '', 'src' );
+ $image_url = 'http://example.org/wp-content/uploads/Dr1Bczxq4q.png';
+ $image_id = self::factory()->attachment->create(
+ array(
+ 'file' => 'Dr1Bczxq4q.png',
+ 'guid' => $image_url,
+ 'post_mime_type' => 'image/png',
+ 'post_parent' => $product->get_id(),
+ )
+ );
- $this->assertNotWPError( $image_url );
+ $this->assertNotWPError( $image_id );
- $image_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE guid = %s", $image_url ) );
+ wp_update_attachment_metadata(
+ $image_id,
+ array(
+ 'width' => 186,
+ 'height' => 144,
+ 'file' => 'Dr1Bczxq4q.png',
+ )
+ );
+ $image_url = wp_get_attachment_url( $image_id );
$product->set_image_id( $image_id );
$product->save();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/product/factory.php b/plugins/woocommerce/tests/legacy/unit-tests/product/factory.php
index 8b589373127..c44ded7c787 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/product/factory.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/product/factory.php
@@ -15,23 +15,30 @@ class WC_Tests_Product_Factory extends WC_Unit_Test_Case {
* @since 3.0.0
*/
function test_get_product_type() {
- $simple = WC_Helper_Product::create_simple_product();
- $external = WC_Helper_Product::create_external_product();
- $grouped = WC_Helper_Product::create_grouped_product();
- $variable = WC_Helper_Product::create_variation_product();
- $children = $variable->get_children();
- $child_id = $children[0];
-
- $this->assertEquals( ProductType::SIMPLE, WC()->product_factory->get_product_type( $simple->get_id() ) );
- $this->assertEquals( ProductType::EXTERNAL, WC()->product_factory->get_product_type( $external->get_id() ) );
- $this->assertEquals( ProductType::GROUPED, WC()->product_factory->get_product_type( $grouped->get_id() ) );
- $this->assertEquals( ProductType::VARIABLE, WC()->product_factory->get_product_type( $variable->get_id() ) );
- $this->assertEquals( ProductType::VARIATION, WC()->product_factory->get_product_type( $child_id ) );
-
- $simple->delete( true );
- $external->delete( true );
- $grouped->delete( true );
- $variable->delete( true );
+ $product_ids = array();
+
+ foreach ( array( ProductType::SIMPLE, ProductType::EXTERNAL, ProductType::GROUPED, ProductType::VARIABLE ) as $product_type ) {
+ $product_ids[ $product_type ] = self::factory()->post->create(
+ array(
+ 'post_type' => 'product',
+ 'post_status' => 'publish',
+ )
+ );
+ wp_set_object_terms( $product_ids[ $product_type ], $product_type, 'product_type' );
+ }
+
+ $variation_id = self::factory()->post->create(
+ array(
+ 'post_type' => 'product_variation',
+ 'post_status' => 'publish',
+ )
+ );
+
+ $this->assertSame( ProductType::SIMPLE, WC()->product_factory->get_product_type( $product_ids[ ProductType::SIMPLE ] ) );
+ $this->assertSame( ProductType::EXTERNAL, WC()->product_factory->get_product_type( $product_ids[ ProductType::EXTERNAL ] ) );
+ $this->assertSame( ProductType::GROUPED, WC()->product_factory->get_product_type( $product_ids[ ProductType::GROUPED ] ) );
+ $this->assertSame( ProductType::VARIABLE, WC()->product_factory->get_product_type( $product_ids[ ProductType::VARIABLE ] ) );
+ $this->assertSame( ProductType::VARIATION, WC()->product_factory->get_product_type( $variation_id ) );
}
/**
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/product/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/product/functions.php
index 77b21f407e8..4acae40fb66 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/product/functions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/product/functions.php
@@ -29,32 +29,32 @@ class WC_Tests_Product_Functions extends WC_Unit_Test_Case {
$term_tag_1 = get_term_by( 'id', $test_tag_1['term_id'], 'product_tag' );
$term_tag_2 = get_term_by( 'id', $test_tag_2['term_id'], 'product_tag' );
- $product = WC_Helper_Product::create_simple_product();
+ $product = WC_Helper_Product::create_simple_product( false );
$product->set_tag_ids( array( $test_tag_1['term_id'] ) );
$product->set_category_ids( array( $test_cat_1['term_id'] ) );
$product->set_sku( 'GET TEST SKU SIMPLE' );
$product->save();
- $product_2 = WC_Helper_Product::create_simple_product();
+ $product_2 = WC_Helper_Product::create_simple_product( false );
$product_2->set_category_ids( array( $test_cat_1['term_id'] ) );
$product_2->save();
- $external = WC_Helper_Product::create_simple_product();
+ $external = WC_Helper_Product::create_simple_product( false );
$external->set_category_ids( array( $test_cat_1['term_id'] ) );
$external->set_sku( 'GET TEST SKU EXTERNAL' );
$external->save();
- $external_2 = WC_Helper_Product::create_simple_product();
+ $external_2 = WC_Helper_Product::create_simple_product( false );
$external_2->set_tag_ids( array( $test_tag_2['term_id'] ) );
$external_2->save();
$grouped = WC_Helper_Product::create_grouped_product();
- $variation = WC_Helper_Product::create_variation_product();
+ $variation = new WC_Product_Variable();
$variation->set_tag_ids( array( $test_tag_1['term_id'] ) );
- $variation->save();
+ $variation = WC_Helper_Product::create_variation_product( $variation );
- $draft = WC_Helper_Product::create_simple_product();
+ $draft = WC_Helper_Product::create_simple_product( false );
$draft->set_status( ProductStatus::DRAFT );
$draft->save();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/orders.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/orders.php
index 8c7d7c97357..2f0c7ca7267 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/orders.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/orders.php
@@ -13,6 +13,13 @@ use Automattic\WooCommerce\Enums\ProductTaxStatus;
*/
class WC_Tests_API_Orders_V2 extends WC_REST_Unit_Test_Case {
+ /**
+ * Administrator used to authenticate requests.
+ *
+ * @var int
+ */
+ protected static $administrator_user;
+
/**
* Array of order to track
* @var array
@@ -20,18 +27,27 @@ class WC_Tests_API_Orders_V2 extends WC_REST_Unit_Test_Case {
protected $orders = array();
/**
- * Setup our test server.
+ * Create the shared administrator.
+ *
+ * @param WP_UnitTest_Factory $factory WordPress test factory.
*/
- public function setUp(): void {
- parent::setUp();
- $this->endpoint = new WC_REST_Orders_Controller();
- $this->user = $this->factory->user->create(
+ public static function wpSetUpBeforeClass( $factory ) {
+ self::$administrator_user = $factory->user->create(
array(
'role' => 'administrator',
)
);
}
+ /**
+ * Setup our test server.
+ */
+ public function setUp(): void {
+ parent::setUp();
+ $this->endpoint = new WC_REST_Orders_Controller();
+ $this->user = self::$administrator_user;
+ }
+
/**
* Test route registration.
* @since 3.0.0
@@ -52,7 +68,7 @@ class WC_Tests_API_Orders_V2 extends WC_REST_Unit_Test_Case {
// Create 10 orders.
for ( $i = 0; $i < 10; $i++ ) {
- $this->orders[] = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order( $this->user );
+ $this->orders[] = wc_create_order( array( 'customer_id' => $this->user ) );
}
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/orders' ) );
@@ -69,7 +85,7 @@ class WC_Tests_API_Orders_V2 extends WC_REST_Unit_Test_Case {
*/
public function test_get_items_without_permission() {
wp_set_current_user( 0 );
- $this->orders[] = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
+ $this->orders[] = wc_create_order();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/orders' ) );
$this->assertEquals( 401, $response->get_status() );
}
@@ -153,7 +169,7 @@ class WC_Tests_API_Orders_V2 extends WC_REST_Unit_Test_Case {
*/
public function test_get_item_without_permission() {
wp_set_current_user( 0 );
- $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
+ $order = wc_create_order();
$this->orders[] = $order;
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/orders/' . $order->get_id() ) );
$this->assertEquals( 401, $response->get_status() );
@@ -652,7 +668,7 @@ class WC_Tests_API_Orders_V2 extends WC_REST_Unit_Test_Case {
*/
public function test_update_order_without_permission() {
wp_set_current_user( 0 );
- $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
+ $order = wc_create_order();
$request = new WP_REST_Request( 'PUT', '/wc/v2/orders/' . $order->get_id() );
$request->set_body_params(
array(
@@ -707,7 +723,7 @@ class WC_Tests_API_Orders_V2 extends WC_REST_Unit_Test_Case {
*/
public function test_delete_order_without_permission() {
wp_set_current_user( 0 );
- $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order();
+ $order = wc_create_order();
$request = new WP_REST_Request( 'DELETE', '/wc/v2/orders/' . $order->get_id() );
$request->set_param( 'force', true );
$response = $this->server->dispatch( $request );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/products.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/products.php
index 476f3eb2f1d..29f0a8bf189 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/products.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/products.php
@@ -49,9 +49,14 @@ class Products_API_V2 extends WC_REST_Unit_Test_Case {
*/
public function test_get_products() {
wp_set_current_user( $this->user );
- \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_external_product();
- sleep( 1 ); // So both products have different timestamps.
- \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
+ $external_product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_external_product();
+ $external_product->set_date_created( '2020-01-01 00:00:00' );
+ $external_product->save();
+
+ $simple_product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
+ $simple_product->set_date_created( '2020-01-02 00:00:00' );
+ $simple_product->save();
+
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products' ) );
$products = $response->get_data();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php
index 6c74b30154f..15e27f72e92 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php
@@ -20,6 +20,7 @@ class Settings_V2 extends WC_REST_Unit_Test_Case {
*/
public function setUp(): void {
parent::setUp();
+ $this->initialize_rest_api_routes();
$this->endpoint = new WC_REST_Setting_Options_Controller();
\Automattic\WooCommerce\RestApi\UnitTests\Helpers\SettingsHelper::register();
$this->user = $this->factory->user->create(
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/system-status.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/system-status.php
index a38834cc58d..0ac4bb79624 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/system-status.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version2/system-status.php
@@ -160,7 +160,9 @@ class WC_Tests_REST_System_Status_V2 extends WC_REST_Unit_Test_Case {
$actual_plugins = array( 'hello.php' );
update_option( 'active_plugins', $actual_plugins );
- $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status' ) );
+ $request = new WP_REST_Request( 'GET', '/wc/v2/system_status' );
+ $request->set_param( '_fields', 'active_plugins' );
+ $response = $this->server->dispatch( $request );
update_option( 'active_plugins', array() );
$data = $response->get_data();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/orders.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/orders.php
index 0fad429e516..fa45ad97373 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/orders.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/orders.php
@@ -60,7 +60,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
// Create 10 orders.
for ( $i = 0; $i < 10; $i++ ) {
- $this->orders[] = OrderHelper::create_order( $this->user );
+ $this->orders[] = wc_create_order( array( 'customer_id' => $this->user ) );
}
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders' ) );
@@ -80,9 +80,11 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
$order2 = OrderHelper::create_order( $this->user );
$order1->set_status( OrderStatus::COMPLETED );
+ $order1->set_date_modified( '2020-01-01 00:00:00' );
$order1->save();
- sleep( 1 );
+
$order2->set_status( OrderStatus::COMPLETED );
+ $order2->set_date_modified( '2020-01-02 00:00:00' );
$order2->save();
$request = new WP_REST_Request( 'GET', '/wc/v3/orders' );
@@ -114,7 +116,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_get_items_without_permission() {
wp_set_current_user( 0 );
- $this->orders[] = OrderHelper::create_order();
+ $this->orders[] = wc_create_order();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders' ) );
$this->assertEquals( 401, $response->get_status() );
}
@@ -149,7 +151,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_get_item_without_permission() {
wp_set_current_user( 0 );
- $order = OrderHelper::create_order();
+ $order = wc_create_order();
$this->orders[] = $order;
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders/' . $order->get_id() ) );
$this->assertEquals( 401, $response->get_status() );
@@ -1015,7 +1017,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_update_order_without_permission() {
wp_set_current_user( 0 );
- $order = OrderHelper::create_order();
+ $order = wc_create_order();
$request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
$request->set_body_params(
array(
@@ -1073,7 +1075,7 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
*/
public function test_delete_order_without_permission() {
wp_set_current_user( 0 );
- $order = OrderHelper::create_order();
+ $order = wc_create_order();
$request = new WP_REST_Request( 'DELETE', '/wc/v3/orders/' . $order->get_id() );
$request->set_param( 'force', true );
$response = $this->server->dispatch( $request );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/products.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/products.php
index 507fff62695..5461ea3631b 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/products.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/products.php
@@ -53,9 +53,14 @@ class WC_Tests_API_Product extends WC_REST_Unit_Test_Case {
*/
public function test_get_products() {
wp_set_current_user( $this->user );
- \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_external_product();
- sleep( 1 ); // So both products have different timestamps.
- \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
+ $external_product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_external_product();
+ $external_product->set_date_created( '2020-01-01 00:00:00' );
+ $external_product->save();
+
+ $simple_product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
+ $simple_product->set_date_created( '2020-01-02 00:00:00' );
+ $simple_product->save();
+
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products' ) );
$products = $response->get_data();
@@ -251,7 +256,8 @@ class WC_Tests_API_Product extends WC_REST_Unit_Test_Case {
)
);
$response = $this->server->dispatch( $request );
- $data = $response->get_data();
+ $this->assertSame( 200, $response->get_status(), 'The product image update request should succeed.' );
+ $data = $response->get_data();
$this->assertStringContainsString( 'Testing', $data['description'] );
$this->assertEquals( '8', $data['price'] );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php
index 53d72c33608..f99bb68c46f 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php
@@ -20,6 +20,7 @@ class Settings extends WC_REST_Unit_Test_Case {
*/
public function setUp(): void {
parent::setUp();
+ $this->initialize_rest_api_routes();
$this->endpoint = new WC_REST_Setting_Options_Controller();
\Automattic\WooCommerce\RestApi\UnitTests\Helpers\SettingsHelper::register();
$this->user = $this->factory->user->create(
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/shipping/shipping-zone.php b/plugins/woocommerce/tests/legacy/unit-tests/shipping/shipping-zone.php
index da5f7fbdff3..78052c0302c 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/shipping/shipping-zone.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/shipping/shipping-zone.php
@@ -11,13 +11,20 @@
*/
class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
+ /**
+ * Shipping zone fixture IDs keyed by fixture name.
+ *
+ * @var int[]
+ */
+ private $zone_ids;
+
/**
* Set up tests.
*/
public function setUp(): void {
parent::setUp();
- WC_Helper_Shipping_Zones::create_mock_zones();
+ $this->zone_ids = WC_Helper_Shipping_Zones::create_mock_zones();
}
/**
@@ -25,7 +32,7 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_get_data() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
$data = $zone->get_data();
// Assert.
@@ -37,10 +44,10 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_get_zone_id() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
// Assert.
- $this->assertEquals( $zone->get_id(), 1 );
+ $this->assertEquals( $zone->get_id(), $this->zone_ids['local'] );
}
/**
@@ -48,7 +55,7 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_get_zone_name() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
// Assert.
$this->assertEquals( $zone->get_zone_name(), 'Local' );
@@ -59,7 +66,7 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_get_zone_order() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
// Assert.
$this->assertEquals( $zone->get_zone_order(), 1 );
@@ -70,7 +77,7 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_get_zone_locations() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
// Assert.
$this->assertTrue( is_array( $zone->get_zone_locations() ) );
@@ -82,25 +89,25 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_get_formatted_location() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
// Assert.
$this->assertEquals( $zone->get_formatted_location(), 'United Kingdom (UK), CB*' );
// Test.
- $zone = WC_Shipping_Zones::get_zone( 2 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['europe'] );
// Assert.
$this->assertEquals( $zone->get_formatted_location(), 'Europe' );
// Test.
- $zone = WC_Shipping_Zones::get_zone( 3 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['california'] );
// Assert.
$this->assertEquals( $zone->get_formatted_location(), 'California' );
// Test.
- $zone = WC_Shipping_Zones::get_zone( 4 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['us'] );
// Assert.
$this->assertEquals( $zone->get_formatted_location(), 'United States (US)' );
@@ -111,7 +118,7 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_get_shipping_methods() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
$zone->add_shipping_method( 'flat_rate' );
$methods = $zone->get_shipping_methods();
@@ -125,7 +132,7 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_set_zone_name() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
$zone->set_zone_name( 'I am a fish' );
// Assert.
@@ -137,7 +144,7 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_set_zone_order() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
$zone->set_zone_order( 100 );
// Assert.
@@ -149,18 +156,7 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_is_valid_location_type() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
-
- // Assert.
- $this->assertEquals( $zone->get_zone_order(), 1 );
- }
-
- /**
- * Test: WC_Shipping_Zone::add_location
- */
- public function test_add_location() {
- // Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
// Assert.
$this->assertTrue( $zone->is_valid_location_type( 'state' ) );
@@ -188,7 +184,7 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_clear_locations() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
$zone->clear_locations();
// Assert.
@@ -201,7 +197,7 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_set_locations() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
$zone->clear_locations();
$zone->set_locations(
array(
@@ -237,10 +233,10 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_save() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
$zone->set_zone_name( 'I am a fish' );
$zone->save();
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
// Assert.
$this->assertEquals( $zone->get_zone_name(), 'I am a fish' );
@@ -251,7 +247,7 @@ class WC_Tests_Shipping_Zone extends WC_Unit_Test_Case {
*/
public function test_add_shipping_method() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
$zone->add_shipping_method( 'flat_rate' );
$zone->add_shipping_method( 'free_shipping' );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/shipping/shipping-zones.php b/plugins/woocommerce/tests/legacy/unit-tests/shipping/shipping-zones.php
index a203cb420e2..b12759f3c79 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/shipping/shipping-zones.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/shipping/shipping-zones.php
@@ -11,13 +11,20 @@
*/
class WC_Tests_Shipping_Zones extends WC_Unit_Test_Case {
+ /**
+ * Shipping zone fixture IDs keyed by fixture name.
+ *
+ * @var int[]
+ */
+ private $zone_ids;
+
/**
* Set up tests.
*/
public function setUp(): void {
parent::setUp();
- WC_Helper_Shipping_Zones::create_mock_zones();
+ $this->zone_ids = WC_Helper_Shipping_Zones::create_mock_zones();
}
/**
@@ -37,7 +44,7 @@ class WC_Tests_Shipping_Zones extends WC_Unit_Test_Case {
*/
public function test_get_zone() {
// Test.
- $zone = WC_Shipping_Zones::get_zone( 1 );
+ $zone = WC_Shipping_Zones::get_zone( $this->zone_ids['local'] );
// Assert that the first zone is our local zone.
$this->assertInstanceOf( 'WC_Shipping_Zone', $zone );
@@ -49,7 +56,7 @@ class WC_Tests_Shipping_Zones extends WC_Unit_Test_Case {
*/
public function test_get_zone_by() {
// Test.
- $zone = WC_Shipping_Zones::get_zone_by( 'zone_id', 2 );
+ $zone = WC_Shipping_Zones::get_zone_by( 'zone_id', $this->zone_ids['europe'] );
// Assert.
$this->assertInstanceOf( 'WC_Shipping_Zone', $zone );
@@ -70,7 +77,7 @@ class WC_Tests_Shipping_Zones extends WC_Unit_Test_Case {
*/
public function test_get_shipping_method() {
// Test.
- $zone = WC_Shipping_Zones::get_zone_by( 'zone_id', 1 );
+ $zone = WC_Shipping_Zones::get_zone_by( 'zone_id', $this->zone_ids['local'] );
$instance_id = $zone->add_shipping_method( 'flat_rate' );
$shipping_method = WC_Shipping_Zones::get_shipping_method( $instance_id );
@@ -82,7 +89,7 @@ class WC_Tests_Shipping_Zones extends WC_Unit_Test_Case {
* Test: WC_Shipping_Zones::get_shipping_method loads enabled and method_order from database
*/
public function test_get_shipping_method_loads_enabled_and_order() {
- $zone = WC_Shipping_Zones::get_zone_by( 'zone_id', 1 );
+ $zone = WC_Shipping_Zones::get_zone_by( 'zone_id', $this->zone_ids['local'] );
$instance_id = $zone->add_shipping_method( 'flat_rate' );
// Set enabled to false and order to 5 in database.
@@ -134,7 +141,7 @@ class WC_Tests_Shipping_Zones extends WC_Unit_Test_Case {
*/
public function test_delete_zone() {
// Test.
- WC_Shipping_Zones::delete_zone( 1 );
+ WC_Shipping_Zones::delete_zone( $this->zone_ids['local'] );
$zones = WC_Shipping_Zones::get_zones();
// Assert.
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/util/class-wc-rate-limiter.php b/plugins/woocommerce/tests/legacy/unit-tests/util/class-wc-rate-limiter.php
index 86fbe760083..7107849e588 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/util/class-wc-rate-limiter.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/util/class-wc-rate-limiter.php
@@ -42,8 +42,7 @@ class WC_Tests_Rate_Limiter extends WC_Unit_Test_Case {
$this->assertEquals( true, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_1 ), 'retried_too_soon allowed action to run too soon before the delay.' );
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_2 ), 'retried_too_soon did not allow action to run for another user before the delay.' );
- // As retired_too_soon bails if current time <= limit, the actual time needs to be at least 1 second after the limit.
- sleep( 1 );
+ WC_Rate_Limiter::set_rate_limit( $rate_limit_id_1, -1 );
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_1 ), 'retried_too_soon did not allow action to run after the designated delay.' );
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_2 ), 'retried_too_soon did not allow action to run for another user after the designated delay.' );
@@ -67,13 +66,12 @@ class WC_Tests_Rate_Limiter extends WC_Unit_Test_Case {
$this->assertEquals( true, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_1 ), 'retried_too_soon allowed action to run too soon before the delay.' );
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_2 ), 'retried_too_soon did not allow action to run for another user before the delay.' );
+ WC_Rate_Limiter::set_rate_limit( $rate_limit_id_1, -1 );
+
// Clear cached values for both users.
wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'rate_limit' . $rate_limit_id_1 ), WC_Rate_Limiter::CACHE_GROUP );
wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'rate_limit' . $rate_limit_id_2 ), WC_Rate_Limiter::CACHE_GROUP );
- // As retired_too_soon bails if current time <= limit, the actual time needs to be at least 1 second after the limit.
- sleep( 1 );
-
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_1 ), 'retried_too_soon did not allow action to run after the designated delay.' );
$this->assertEquals( false, WC_Rate_Limiter::retried_too_soon( $rate_limit_id_2 ), 'retried_too_soon did not allow action to run for another user after the designated delay.' );
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/util/install.php b/plugins/woocommerce/tests/legacy/unit-tests/util/install.php
index db00bbe4788..b7c1c7ffd8c 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/util/install.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/util/install.php
@@ -112,7 +112,7 @@ class WC_Tests_Install extends WC_Unit_Test_Case {
* Test - create roles.
*/
public function test_create_roles() {
- self::uninstall();
+ WC_Install::remove_roles();
WC_Install::create_roles();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/admin-notes.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/admin-notes.php
index 57efea0458f..49a59e54794 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/admin-notes.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/admin-notes.php
@@ -20,6 +20,13 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
*/
protected $endpoint = '/wc-analytics/admin/notes';
+ /**
+ * Note fixture IDs keyed by their fixture number.
+ *
+ * @var int[]
+ */
+ private $note_ids;
+
/**
* Setup test admin notes data. Called before every test.
*
@@ -35,7 +42,7 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
);
WC_Helper_Admin_Notes::reset_notes_dbs();
- WC_Helper_Admin_Notes::add_notes_for_tests();
+ $this->note_ids = WC_Helper_Admin_Notes::add_notes_for_tests();
}
/**
@@ -59,12 +66,12 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
public function test_get_note() {
wp_set_current_user( $this->user );
- $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/1' ) );
+ $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/' . $this->note_ids[1] ) );
$note = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
- $this->assertEquals( 1, $note['id'] );
+ $this->assertEquals( $this->note_ids[1], $note['id'] );
$this->assertEquals( 'PHPUNIT_TEST_NOTE_NAME', $note['name'] );
$this->assertEquals( Note::E_WC_ADMIN_NOTE_INFORMATIONAL, $note['type'] );
$this->assertArrayHasKey( 'locale', $note );
@@ -104,7 +111,7 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
$note->add_nonce_to_action( 'learn-more', 'foo', 'bar' );
$note->save();
- $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/1' ) );
+ $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/' . $note->get_id() ) );
$note = $response->get_data();
$expected_url = 'https://woocommerce.com/?bar=' . wp_create_nonce( 'foo' );
@@ -127,7 +134,7 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
$note->add_nonce_to_action( 'learn-more', 'foo', 'bar' );
$note->save();
- $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/1' ) );
+ $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/' . $note->get_id() ) );
$note = $response->get_data();
$expected_url = 'https://example.com/?x=1&y=2&bar=' . wp_create_nonce( 'foo' );
@@ -147,7 +154,7 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
// phpcs:ignore WordPress.PHP.IniSet.Risky
$log_file = ini_set( 'error_log', '/dev/null' );
- $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/999' ) );
+ $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/' . ( max( $this->note_ids ) + 1 ) ) );
$note = $response->get_data();
// phpcs:ignore WordPress.PHP.IniSet.Risky
@@ -162,7 +169,7 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
* @since 3.5.0
*/
public function test_get_note_without_permission() {
- $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/1' ) );
+ $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/' . $this->note_ids[1] ) );
$this->assertEquals( 401, $response->get_status() );
}
@@ -172,13 +179,13 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
public function test_update_note() {
wp_set_current_user( $this->user );
- $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/1' ) );
+ $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/' . $this->note_ids[1] ) );
$note = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 'unactioned', $note['status'] );
- $request = new WP_REST_Request( 'PUT', $this->endpoint . '/1' );
+ $request = new WP_REST_Request( 'PUT', $this->endpoint . '/' . $this->note_ids[1] );
$request->set_body_params(
array(
'status' => 'actioned',
@@ -195,7 +202,7 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
* Test updating a single note without permission. It should fail.
*/
public function test_update_note_without_permission() {
- $request = new WP_REST_Request( 'PUT', $this->endpoint . '/1' );
+ $request = new WP_REST_Request( 'PUT', $this->endpoint . '/' . $this->note_ids[1] );
$request->set_body_params(
array(
'status' => 'actioned',
@@ -436,13 +443,13 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
public function test_delete_single_note() {
wp_set_current_user( $this->user );
- $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/3' ) );
+ $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/' . $this->note_ids[3] ) );
$note = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( false, $note['is_deleted'] );
- $response = $this->server->dispatch( new WP_REST_Request( 'DELETE', $this->endpoint . '/delete/3' ) );
+ $response = $this->server->dispatch( new WP_REST_Request( 'DELETE', $this->endpoint . '/delete/' . $this->note_ids[3] ) );
$note = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
@@ -453,7 +460,7 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
* Test deleting a single note without permission. It should fail.
*/
public function test_delete_single_note_without_permission() {
- $response = $this->server->dispatch( new WP_REST_Request( 'DELETE', $this->endpoint . '/delete/3' ) );
+ $response = $this->server->dispatch( new WP_REST_Request( 'DELETE', $this->endpoint . '/delete/' . $this->note_ids[3] ) );
$note = $response->get_data();
$this->assertEquals( 401, $response->get_status() );
@@ -465,11 +472,11 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
public function test_undo_single_notes_delete() {
wp_set_current_user( $this->user );
- $response = $this->server->dispatch( new WP_REST_Request( 'DELETE', $this->endpoint . '/delete/3' ) );
+ $response = $this->server->dispatch( new WP_REST_Request( 'DELETE', $this->endpoint . '/delete/' . $this->note_ids[3] ) );
$note = $response->get_data();
$this->assertEquals( true, $note['is_deleted'] );
- $request = new WP_REST_Request( 'PUT', $this->endpoint . '/3' );
+ $request = new WP_REST_Request( 'PUT', $this->endpoint . '/' . $this->note_ids[3] );
$request->set_body_params(
array(
'is_deleted' => '0',
@@ -539,8 +546,8 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
$request = new WP_REST_Request( 'PUT', $this->endpoint . '/update' );
$request->set_body_params(
array(
- 'noteIds' => array( '1', '4' ),
- 'is_deleted' => '1',
+ 'noteIds' => array( (string) $this->note_ids[1], (string) $this->note_ids[4] ),
+ 'is_deleted' => '0',
)
);
@@ -549,5 +556,14 @@ class WC_Admin_Tests_API_Admin_Notes extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 2, count( $notes ) );
+ foreach ( $notes as $note ) {
+ $this->assertFalse( $note['is_deleted'] );
+ }
+
+ foreach ( array( $this->note_ids[1], $this->note_ids[4] ) as $note_id ) {
+ $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint . '/' . $note_id ) );
+ $note = $response->get_data();
+ $this->assertFalse( $note['is_deleted'] );
+ }
}
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-profile.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-profile.php
index 3328a2d2274..58fbd734254 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-profile.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-profile.php
@@ -29,6 +29,7 @@ class WC_Admin_Tests_API_Onboarding_Profiles extends WC_REST_Unit_Test_Case {
*/
public function setUp(): void {
parent::setUp();
+ $this->initialize_rest_api_routes();
$this->user = $this->factory->user->create(
array(
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-tasks.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-tasks.php
index 2fe1b494a10..69f64043e65 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-tasks.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-tasks.php
@@ -30,11 +30,19 @@ class WC_Admin_Tests_API_Onboarding_Tasks extends WC_REST_Unit_Test_Case {
*/
protected $endpoint = '/wc-admin/onboarding/tasks';
+ /**
+ * Attachment IDs created by the current test.
+ *
+ * @var int[]
+ */
+ private $created_attachment_ids = array();
+
/**
* Setup test data. Called before every test.
*/
public function setUp(): void {
parent::setUp();
+ add_action( 'add_attachment', array( $this, 'track_created_attachment' ) );
$this->user = $this->factory->user->create(
array(
@@ -58,12 +66,27 @@ class WC_Admin_Tests_API_Onboarding_Tasks extends WC_REST_Unit_Test_Case {
* Tear down.
*/
public function tearDown(): void {
+ remove_action( 'add_attachment', array( $this, 'track_created_attachment' ) );
+ foreach ( array_unique( $this->created_attachment_ids ) as $attachment_id ) {
+ wp_delete_attachment( $attachment_id, true );
+ }
+ $this->created_attachment_ids = array();
+
parent::tearDown();
$this->cleanup_test_product_attributes();
TaskLists::clear_lists();
TaskLists::init_default_lists();
}
+ /**
+ * Track an attachment created by the current test.
+ *
+ * @param int $attachment_id Attachment ID.
+ */
+ public function track_created_attachment( $attachment_id ): void {
+ $this->created_attachment_ids[] = (int) $attachment_id;
+ }
+
/**
* Clean up product attribute taxonomies created during tests.
*/
@@ -106,6 +129,13 @@ class WC_Admin_Tests_API_Onboarding_Tasks extends WC_REST_Unit_Test_Case {
10,
3
);
+ add_filter(
+ 'woocommerce_product_csv_importer_args',
+ function ( $args ) {
+ $args['lines'] = 2;
+ return $args;
+ }
+ );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/import_sample_products' );
$response = $this->server->dispatch( $request );
@@ -116,15 +146,14 @@ class WC_Admin_Tests_API_Onboarding_Tasks extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'failed', $data );
$this->assertEquals( 0, count( $data['failed'] ) );
$this->assertArrayHasKey( 'imported', $data );
+ $this->assertCount( 2, $data['imported'] );
$this->assertArrayHasKey( 'skipped', $data );
- // There might be previous products present.
- if ( 0 === count( $data['skipped'] ) ) {
- $this->assertGreaterThan( 1, count( $data['imported'] ) );
- }
+ $this->assertCount( 0, $data['skipped'] );
$this->assertArrayHasKey( 'updated', $data );
$this->assertEquals( 0, count( $data['updated'] ) );
remove_all_filters( 'pre_http_request' );
+ remove_all_filters( 'woocommerce_product_csv_importer_args' );
}
/**
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-themes.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-themes.php
index 02459372319..8c464b0d717 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-themes.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/onboarding-themes.php
@@ -66,16 +66,33 @@ class WC_Admin_Tests_API_Onboarding_Themes extends WC_REST_Unit_Test_Case {
public function test_install_invalid_theme() {
wp_set_current_user( $this->user );
+ $api_calls = 0;
+ $api_filter = function ( $result, $action, $args ) use ( &$api_calls ) {
+ ++$api_calls;
+ $this->assertSame( 'theme_information', $action );
+ $this->assertSame( 'invalid-theme-name', $args->slug );
+
+ return new WP_Error( 'themes_api_failed', 'Expected test failure.' );
+ };
+
+ add_filter( 'themes_api', $api_filter, 10, 3 );
+
$request = new WP_REST_Request( 'POST', $this->endpoint . '/install' );
$request->set_query_params(
array(
'theme' => 'invalid-theme-name',
)
);
- $response = $this->server->dispatch( $request );
- $data = $response->get_data();
+
+ try {
+ $response = $this->server->dispatch( $request );
+ $data = $response->get_data();
+ } finally {
+ remove_filter( 'themes_api', $api_filter, 10 );
+ }
$this->assertEquals( 'woocommerce_rest_theme_install', $data['code'] );
+ $this->assertSame( 1, $api_calls );
}
/**
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/orders.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/orders.php
index 2de6625e9fa..d6eb54a8265 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/orders.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/orders.php
@@ -59,7 +59,7 @@ class WC_Admin_Tests_API_Orders extends WC_REST_Unit_Test_Case {
$orders = array();
for ( $i = 0; $i < 10; $i++ ) {
- $orders[] = WC_Helper_Order::create_order( $this->user );
+ $orders[] = wc_create_order( array( 'customer_id' => $this->user ) );
}
$order = $orders[9];
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/plugins.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/plugins.php
index 04ae94ae797..46fdd451305 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/plugins.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/plugins.php
@@ -6,12 +6,23 @@
*/
use Automattic\WooCommerce\Admin\API\Plugins;
+use Automattic\WooCommerce\Admin\PluginsHelper;
/**
* WC Tests API Plugins
*/
class WC_Admin_Tests_API_Plugins extends WC_REST_Unit_Test_Case {
+ /**
+ * Test plugin slug.
+ */
+ private const TEST_PLUGIN_SLUG = 'sample-woo-plugin';
+
+ /**
+ * Test plugin file.
+ */
+ private const TEST_PLUGIN_FILE = 'sample-woo-plugin/sample-woo-plugin.php';
+
/**
* Endpoints.
*
@@ -24,6 +35,7 @@ class WC_Admin_Tests_API_Plugins extends WC_REST_Unit_Test_Case {
*/
public function setUp(): void {
parent::setUp();
+ $this->delete_test_plugin();
$this->user = $this->factory->user->create(
array(
@@ -37,10 +49,23 @@ class WC_Admin_Tests_API_Plugins extends WC_REST_Unit_Test_Case {
*/
public function tearDown(): void {
deactivate_plugins( 'akismet/akismet.php', true );
+ $this->delete_test_plugin();
parent::tearDown();
}
+ /**
+ * Delete files installed by the plugin installation test.
+ */
+ private function delete_test_plugin(): void {
+ if ( ! file_exists( WP_PLUGIN_DIR . '/' . self::TEST_PLUGIN_FILE ) ) {
+ return;
+ }
+
+ delete_plugins( array( self::TEST_PLUGIN_FILE ) );
+ wp_clean_plugins_cache( true );
+ }
+
/**
* Test that installation without permission is unauthorized.
*/
@@ -55,23 +80,75 @@ class WC_Admin_Tests_API_Plugins extends WC_REST_Unit_Test_Case {
public function test_install_plugin() {
wp_set_current_user( $this->user );
+ $plugin_package = wp_tempnam( 'sample-woo-plugin.zip' );
+ copy( WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/sample-woo-plugin.zip', $plugin_package );
+
+ $api_filter = static function ( $result, $action, $args ) {
+ if ( 'plugin_information' !== $action || self::TEST_PLUGIN_SLUG !== $args->slug ) {
+ return $result;
+ }
+
+ return (object) array(
+ 'slug' => self::TEST_PLUGIN_SLUG,
+ 'version' => '1.0.0',
+ 'download_link' => 'https://example.com/sample-woo-plugin.zip',
+ );
+ };
+
+ $download_filter = static function ( $reply, $package, $upgrader ) use ( $plugin_package ) {
+ if ( $upgrader instanceof Plugin_Upgrader ) {
+ return $plugin_package;
+ }
+
+ return $reply;
+ };
+
+ $upgrader_hooks = array(
+ array( array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20, 1 ),
+ array( 'wp_version_check', 10, 0 ),
+ array( 'wp_update_plugins', 10, 0 ),
+ array( 'wp_update_themes', 10, 0 ),
+ );
+
+ $removed_upgrader_hooks = array();
+
+ add_filter( 'plugins_api', $api_filter, 10, 3 );
+ add_filter( 'upgrader_pre_download', $download_filter, 10, 3 );
+ foreach ( $upgrader_hooks as $upgrader_hook ) {
+ if ( false !== has_action( 'upgrader_process_complete', $upgrader_hook[0] ) ) {
+ remove_action( 'upgrader_process_complete', $upgrader_hook[0], $upgrader_hook[1] );
+ $removed_upgrader_hooks[] = $upgrader_hook;
+ }
+ }
+
$request = new WP_REST_Request( 'POST', $this->endpoint . '/install' );
$request->set_query_params(
array(
- 'plugins' => 'hello-dolly',
+ 'plugins' => self::TEST_PLUGIN_SLUG,
)
);
- $response = $this->server->dispatch( $request );
- // TODO: This test should be skipped if WordPress.org's plugins API endpoint cannot be reached.
+ $this->assertFalse( PluginsHelper::is_plugin_installed( self::TEST_PLUGIN_SLUG ) );
- $data = $response->get_data();
- $plugins = get_plugins();
+ try {
+ $response = $this->server->dispatch( $request );
+ $data = $response->get_data();
+ $plugins = get_plugins();
- $this->assertEquals( 200, $response->get_status() );
- $this->assertEquals( array( 'hello-dolly' ), $data['data']['installed'] );
- $this->assertEquals( true, $data['success'] );
- $this->assertArrayHasKey( 'hello-dolly/hello.php', $plugins );
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEquals( array( self::TEST_PLUGIN_SLUG ), $data['data']['installed'] );
+ $this->assertEquals( true, $data['success'] );
+ $this->assertArrayHasKey( self::TEST_PLUGIN_FILE, $plugins );
+ } finally {
+ remove_filter( 'plugins_api', $api_filter, 10 );
+ remove_filter( 'upgrader_pre_download', $download_filter, 10 );
+ foreach ( $removed_upgrader_hooks as $upgrader_hook ) {
+ add_action( 'upgrader_process_complete', $upgrader_hook[0], $upgrader_hook[1], $upgrader_hook[2] );
+ }
+ if ( file_exists( $plugin_package ) ) {
+ wp_delete_file( $plugin_package );
+ }
+ }
}
/**
@@ -79,23 +156,31 @@ class WC_Admin_Tests_API_Plugins extends WC_REST_Unit_Test_Case {
*/
public function test_install_plugin_async() {
wp_set_current_user( $this->user );
+ as_unschedule_all_actions( 'woocommerce_plugins_install_callback' );
- $request = new WP_REST_Request( 'POST', $this->endpoint . '/install' );
- $request->set_query_params(
- array(
- 'async' => true,
- 'plugins' => 'hello-dolly',
- )
- );
- $response = $this->server->dispatch( $request );
+ try {
+ $request = new WP_REST_Request( 'POST', $this->endpoint . '/install' );
+ $request->set_query_params(
+ array(
+ 'async' => true,
+ 'plugins' => self::TEST_PLUGIN_SLUG,
+ )
+ );
+ $response = $this->server->dispatch( $request );
- // TODO: This test should be skipped if WordPress.org's plugins API endpoint cannot be reached.
+ $data = $response->get_data();
- $data = $response->get_data();
- $plugins = get_plugins();
-
- $this->assertEquals( 200, $response->get_status() );
- $this->assertArrayHasKey( 'job_id', $data['data'] );
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertNotEmpty( $data['data']['job_id'] );
+ $this->assertTrue(
+ as_has_scheduled_action(
+ 'woocommerce_plugins_install_callback',
+ array( array( self::TEST_PLUGIN_SLUG ) )
+ )
+ );
+ } finally {
+ as_unschedule_all_actions( 'woocommerce_plugins_install_callback' );
+ }
}
/**
@@ -172,5 +257,4 @@ class WC_Admin_Tests_API_Plugins extends WC_REST_Unit_Test_Case {
public function set_france_locale() {
return 'fr_FR';
}
-
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/product-attributes.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/product-attributes.php
index 5bcb9905296..7ba9f07feb4 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/product-attributes.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/product-attributes.php
@@ -9,6 +9,12 @@
* Class WC_Admin_Tests_API_Product_Attributes
*/
class WC_Admin_Tests_API_Product_Attributes extends WC_REST_Unit_Test_Case {
+ /**
+ * Product IDs created for the class fixtures.
+ *
+ * @var int[]
+ */
+ private static $fixture_product_ids = array();
/**
* Endpoints.
@@ -31,10 +37,18 @@ class WC_Admin_Tests_API_Product_Attributes extends WC_REST_Unit_Test_Case {
}
public static function tearDownAfterClass(): void {
- parent::tearDownAfterClass();
- global $wpdb;
- $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_attribute_taxonomies" );
- $wpdb->query('commit');
+ $direct_update_mode = get_option( 'woocommerce_attribute_lookup_direct_updates' );
+ self::enable_direct_product_attribute_lookup_updates();
+ try {
+ parent::tearDownAfterClass();
+ global $wpdb;
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_attribute_taxonomies" );
+ $wpdb->query( 'commit' );
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
+ }
+
+ self::assertSame( $direct_update_mode, get_option( 'woocommerce_attribute_lookup_direct_updates' ) );
}
/**
@@ -42,50 +56,81 @@ class WC_Admin_Tests_API_Product_Attributes extends WC_REST_Unit_Test_Case {
*/
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
+ $direct_update_mode = get_option( 'woocommerce_attribute_lookup_direct_updates' );
+ self::enable_direct_product_attribute_lookup_updates();
+ try {
+ // Use the test helper to populate some global attributes.
+ $product = \WC_Helper_Product::create_variation_product();
+ $attributes = $product->get_attributes();
+
+ // Add a custom attribute.
+ $custom_attr = new WC_Product_Attribute();
+ $custom_attr->set_name( 'Numeric Size' );
+ $custom_attr->set_options( array( '1', '2', '3', '4', '5' ) );
+ $custom_attr->set_visible( true );
+ $custom_attr->set_variation( true );
+ $attributes[] = $custom_attr;
+
+ $product->set_attributes( $attributes );
+ $product->save();
+
+ // Assign one variation to the '1' size.
+ $variation = $product->get_available_variations( 'objects' )[0];
+ $attributes = $variation->get_attributes();
+ $attributes[ sanitize_title( $custom_attr->get_name() ) ] = '1';
+ $variation->set_attributes( $attributes );
+ $variation->save();
+
+ // Add more custom Numeric Size values to another product.
+ $product_2 = new WC_Product_Variable();
+ $product_2->set_props(
+ array(
+ 'name' => 'Dummy Variable Product 2',
+ 'sku' => 'DUMMY VARIABLE SKU 2' . microtime(),
+ )
+ );
+
+ $custom_attr_2 = new WC_Product_Attribute();
+ $custom_attr_2->set_name( 'Numeric Size' );
+ $custom_attr_2->set_options( array( '6', '7', '8', '9', '10' ) );
+ $custom_attr_2->set_visible( true );
+ $custom_attr_2->set_variation( true );
+
+ $product_2->set_attributes(
+ array(
+ $custom_attr_2,
+ )
+ );
+ $product_2->save();
+ self::$fixture_product_ids = array_merge( array( $product->get_id() ), $product->get_children(), array( $product_2->get_id() ) );
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
+ }
- // Use the test helper to populate some global attributes.
- $product = \WC_Helper_Product::create_variation_product();
- $attributes = $product->get_attributes();
-
- // Add a custom attribute.
- $custom_attr = new WC_Product_Attribute();
- $custom_attr->set_name( 'Numeric Size' );
- $custom_attr->set_options( array( '1', '2', '3', '4', '5' ) );
- $custom_attr->set_visible( true );
- $custom_attr->set_variation( true );
- $attributes[] = $custom_attr;
-
- $product->set_attributes( $attributes );
- $product->save();
-
- // Assign one variation to the '1' size.
- $variation = $product->get_available_variations( 'objects' )[0];
- $attributes = $variation->get_attributes();
- $attributes[ sanitize_title( $custom_attr->get_name() ) ] = '1';
- $variation->set_attributes( $attributes );
- $variation->save();
-
- // Add more custom Numeric Size values to another product.
- $product_2 = new WC_Product_Variable();
- $product_2->set_props(
- array(
- 'name' => 'Dummy Variable Product 2',
- 'sku' => 'DUMMY VARIABLE SKU 2' . microtime(),
- )
- );
-
- $custom_attr_2 = new WC_Product_Attribute();
- $custom_attr_2->set_name( 'Numeric Size' );
- $custom_attr_2->set_options( array( '6', '7', '8', '9', '10' ) );
- $custom_attr_2->set_visible( true );
- $custom_attr_2->set_variation( true );
+ self::assertSame( $direct_update_mode, get_option( 'woocommerce_attribute_lookup_direct_updates' ) );
+ }
- $product_2->set_attributes(
- array(
- $custom_attr_2,
- )
- );
- $product_2->save();
+ /**
+ * Test that class fixtures do not schedule product attribute lookup updates.
+ *
+ * @testdox Class fixtures do not schedule product attribute lookup updates.
+ */
+ public function test_fixtures_do_not_schedule_attribute_lookup_updates(): void {
+ $queue = WC()->get_instance_of( WC_Queue::class );
+
+ foreach ( self::$fixture_product_ids as $product_id ) {
+ $this->assertEmpty(
+ $queue->search(
+ array(
+ 'hook' => 'woocommerce_run_product_attribute_lookup_update_callback',
+ 'args' => array( $product_id, \Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore::ACTION_INSERT ),
+ 'status' => ActionScheduler_Store::STATUS_PENDING,
+ ),
+ 'ids'
+ ),
+ "Product {$product_id} has a pending attribute lookup update."
+ );
+ }
}
/**
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-categories.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-categories.php
index 0bf5fc33c4e..c417c059417 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-categories.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-categories.php
@@ -133,18 +133,19 @@ class WC_Admin_Tests_API_Reports_Categories extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 2, count( $reports ) );
- $category_report = reset( $reports );
+ $reports_by_category = array_column( $reports, null, 'category_id' );
+ $this->assertArrayHasKey( $second_category_id, $reports_by_category );
+ $this->assertArrayHasKey( $uncategorized_term->term_id, $reports_by_category );
- $this->assertEquals( $second_category_id, $category_report['category_id'] );
+ $category_report = $reports_by_category[ $second_category_id ];
$this->assertEquals( 0, $category_report['items_sold'] );
$this->assertEquals( 0, $category_report['orders_count'] );
$this->assertEquals( 0, $category_report['products_count'] );
$this->assertArrayHasKey( '_links', $category_report );
$this->assertArrayHasKey( 'category', $category_report['_links'] );
- $category_report = next( $reports );
+ $category_report = $reports_by_category[ $uncategorized_term->term_id ];
- $this->assertEquals( $uncategorized_term->term_id, $category_report['category_id'] );
$this->assertEquals( 4, $category_report['items_sold'] );
$this->assertEquals( 1, $category_report['orders_count'] );
$this->assertEquals( 1, $category_report['products_count'] );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers-stats.php
index 028bbd5362e..fa0d3b04c21 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers-stats.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers-stats.php
@@ -7,12 +7,10 @@
*/
use Automattic\WooCommerce\Enums\OrderStatus;
+use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore as CustomersDataStore;
/**
* Reports Customers Stats REST API Test Class
- * @runTestsInSeparateProcesses
- * @preserveGlobalState disabled
- * @group run-in-separate-process
* @package WooCommerce\Admin\Tests\API
* @since 3.5.0
*/
@@ -92,7 +90,11 @@ class WC_Admin_Tests_API_Reports_Customers_Stats extends WC_REST_Unit_Test_Case
*/
public function test_get_reports() {
wp_set_current_user( $this->user );
+ $stale_customer = WC_Helper_Customer::create_customer( 'stale_stats_customer', 'password', 'stale-stats@example.com' );
+ $this->assertNotFalse( CustomersDataStore::update_registered_customer( $stale_customer->get_id() ) );
+ $this->assertNotFalse( CustomersDataStore::get_customer_id_by_user_id( $stale_customer->get_id() ) );
WC_Helper_Reports::reset_stats_dbs();
+ $this->assertFalse( CustomersDataStore::get_customer_id_by_user_id( $stale_customer->get_id() ) );
$test_customers = array();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php
index aa35d3fddac..4debbc40bac 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-customers.php
@@ -13,9 +13,6 @@ use Automattic\WooCommerce\Enums\OrderStatus;
/**
* Reports Customers REST API Test Class
- * @runTestsInSeparateProcesses
- * @preserveGlobalState disabled
- * @group run-in-separate-process
* @package WooCommerce\Admin\Tests\API
* @since 3.5.0
*/
@@ -213,7 +210,11 @@ class WC_Admin_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
global $wpdb;
wp_set_current_user( $this->user );
+ $stale_customer = WC_Helper_Customer::create_customer( 'stale_report_customer', 'password', 'stale-report@example.com' );
+ $this->assertNotFalse( CustomersDataStore::update_registered_customer( $stale_customer->get_id() ) );
+ $this->assertNotFalse( CustomersDataStore::get_customer_id_by_user_id( $stale_customer->get_id() ) );
WC_Helper_Reports::reset_stats_dbs();
+ $this->assertFalse( CustomersDataStore::get_customer_id_by_user_id( $stale_customer->get_id() ) );
$test_customers = array();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-import.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-import.php
index 2539e38376f..3b438d0fdf5 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-import.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-import.php
@@ -15,6 +15,20 @@ use Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore;
* @package WooCommerce\Admin\Tests\API
*/
class WC_Admin_Tests_API_Reports_Import extends WC_REST_Unit_Test_Case {
+ /**
+ * Administrator used to authenticate requests.
+ *
+ * @var int
+ */
+ protected static $administrator_user;
+
+ /**
+ * Customer used by imported orders.
+ *
+ * @var int
+ */
+ protected static $customer_user;
+
/**
* Endpoint.
*
@@ -23,18 +37,18 @@ class WC_Admin_Tests_API_Reports_Import extends WC_REST_Unit_Test_Case {
protected $endpoint = '/wc-analytics/reports/import';
/**
- * Setup test reports products data.
+ * Create shared report users.
+ *
+ * @param WP_UnitTest_Factory $factory WordPress test factory.
*/
- public function setUp(): void {
- parent::setUp();
-
- $this->user = $this->factory->user->create(
+ public static function wpSetUpBeforeClass( $factory ) {
+ self::$administrator_user = $factory->user->create(
array(
'role' => 'administrator',
)
);
- $this->customer = $this->factory->user->create(
+ self::$customer_user = $factory->user->create(
array(
'first_name' => 'Steve',
'last_name' => 'User',
@@ -43,6 +57,16 @@ class WC_Admin_Tests_API_Reports_Import extends WC_REST_Unit_Test_Case {
);
}
+ /**
+ * Setup test reports products data.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->user = self::$administrator_user;
+ $this->customer = self::$customer_user;
+ }
+
/**
* Test route registration.
*/
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders-stats.php
index a67dc11b200..9179737806d 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders-stats.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders-stats.php
@@ -141,9 +141,9 @@ class WC_Admin_Tests_API_Reports_Orders_Stats extends WC_REST_Unit_Test_Case {
$this->markTestSkipped( 'Skipped in older versions of WordPress due to a bug in WP when validating arrays.' );
}
- global $wpdb;
wp_set_current_user( $this->user );
WC_Helper_Reports::reset_stats_dbs();
+ update_option( 'woocommerce_attribute_lookup_direct_updates', 'yes' );
// Create a variable product.
$variable_product = WC_Helper_Product::create_variation_product( new WC_Product_Variable() );
@@ -185,14 +185,14 @@ class WC_Admin_Tests_API_Reports_Orders_Stats extends WC_REST_Unit_Test_Case {
$simple_product_order_1->save();
// Create more orders for simple products.
+ $unrelated_product = WC_Helper_Product::create_simple_product();
for ( $i = 0; $i < 10; $i++ ) {
- $order = WC_Helper_Order::create_order( $this->user );
+ $order = WC_Helper_Order::create_order( $this->user, $unrelated_product );
$order->set_status( OrderStatus::COMPLETED );
$order->save();
}
WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
- WC_Helper_Queue::run_all_pending( 'woocommerce-db-updates' );
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params( array( 'per_page' => 15 ) );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders.php
index 96b7fba005b..d8fe4a2eab4 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-orders.php
@@ -193,6 +193,7 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
wp_set_current_user( $this->user );
WC_Helper_Reports::reset_stats_dbs();
+ update_option( 'woocommerce_attribute_lookup_direct_updates', 'yes' );
// Create a variable product.
$variable_product = WC_Helper_Product::create_variation_product( new WC_Product_Variable() );
@@ -235,14 +236,14 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
$simple_product_order_1->save();
// Create more orders for simple products.
- for ( $i = 0; $i < 10; $i++ ) {
+ $unattributed_order_count = 2;
+ for ( $i = 0; $i < $unattributed_order_count; $i++ ) {
$order = WC_Helper_Order::create_order( $this->user );
$order->set_status( OrderStatus::COMPLETED );
$order->save();
}
WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
- WC_Helper_Queue::run_all_pending( 'woocommerce-db-updates' );
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params( array( 'per_page' => 15 ) );
@@ -251,7 +252,7 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
// Sanity check before filtering by attribute.
$this->assertEquals( 200, $response->get_status() );
- $this->assertEquals( 13, count( $response_orders ) );
+ $this->assertEquals( 3 + $unattributed_order_count, count( $response_orders ) );
// To filter by later.
$size_attr_id = wc_attribute_taxonomy_id_by_name( 'pa_size' );
@@ -277,7 +278,7 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
// We expect all results since the attribute param is malformed.
- $this->assertEquals( 13, count( $response_orders ) );
+ $this->assertEquals( 3 + $unattributed_order_count, count( $response_orders ) );
}
// Filter by the "size" attribute, with value "small".
@@ -317,7 +318,7 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
$response_orders = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
- $this->assertEquals( 11, count( $response_orders ) );
+ $this->assertEquals( 1 + $unattributed_order_count, count( $response_orders ) );
}
/**
@@ -332,30 +333,29 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
wp_set_current_user( $this->user );
WC_Helper_Reports::reset_stats_dbs();
- // Create a variable product.
- $variable_product = WC_Helper_Product::create_variation_product( new WC_Product_Variable() );
+ $variable_product = new WC_Product_Variable();
+ $variable_product->set_name( 'Custom attribute product' );
- // Add a custom attribute.
- $attributes = $variable_product->get_attributes();
$custom_attr = new WC_Product_Attribute();
$custom_attr->set_name( 'Numeric Size' );
$custom_attr->set_options( array( '1', '2', '3', '4', '5' ) );
$custom_attr->set_visible( true );
$custom_attr->set_variation( true );
- $attributes[] = $custom_attr;
- $variable_product->set_attributes( $attributes );
+ $variable_product->set_attributes( array( $custom_attr ) );
$variable_product->save();
- // Custom attribute terms can only be found once assigned to variations.
- $data_store = $variable_product->get_data_store();
- $data_store->create_all_product_variations( $variable_product );
-
- // Fetch the product to get new variations.
- $variable_product = wc_get_product( $variable_product->get_id() );
- $product_variations = $variable_product->get_children();
-
- $order_variation_1 = wc_get_product( $product_variations[0] ); // Variation: size = small.
- $order_variation_2 = wc_get_product( end( $product_variations ) ); // Variation: size = huge, colour = blue, number = 1, numeric-size = 5.
+ $order_variation_1 = WC_Helper_Product::create_product_variation_object(
+ $variable_product->get_id(),
+ 'DUMMY SKU CUSTOM ATTRIBUTE 1',
+ 10,
+ array( 'numeric-size' => '1' )
+ );
+ $order_variation_2 = WC_Helper_Product::create_product_variation_object(
+ $variable_product->get_id(),
+ 'DUMMY SKU CUSTOM ATTRIBUTE 5',
+ 10,
+ array( 'numeric-size' => '5' )
+ );
// Create orders for variations.
$variation_order_1 = WC_Helper_Order::create_order( $this->user, $order_variation_1 );
@@ -366,12 +366,9 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
$variation_order_2->set_status( OrderStatus::COMPLETED );
$variation_order_2->save();
- // Create more orders for simple products.
- for ( $i = 0; $i < 10; $i++ ) {
- $order = WC_Helper_Order::create_order( $this->user );
- $order->set_status( OrderStatus::COMPLETED );
- $order->save();
- }
+ $order = WC_Helper_Order::create_order( $this->user );
+ $order->set_status( OrderStatus::COMPLETED );
+ $order->save();
WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
@@ -382,7 +379,7 @@ class WC_Admin_Tests_API_Reports_Orders extends WC_REST_Unit_Test_Case {
// Sanity check before filtering by attribute.
$this->assertEquals( 200, $response->get_status() );
- $this->assertEquals( 12, count( $response_orders ) );
+ $this->assertEquals( 3, count( $response_orders ) );
// Filter by the "Numeric Size" custom attribute, with value "1".
$request = new WP_REST_Request( 'GET', $this->endpoint );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-stock-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-stock-stats.php
index 69f9d9f44f8..b1f9f79212b 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-stock-stats.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/api/reports-stock-stats.php
@@ -51,32 +51,13 @@ class WC_Admin_Tests_API_Reports_Stock_Stats extends WC_REST_Unit_Test_Case {
WC_Helper_Reports::reset_stats_dbs();
$number_of_low_stock = 3;
- for ( $i = 1; $i <= $number_of_low_stock; $i++ ) {
- $low_stock = new WC_Product_Simple();
- $low_stock->set_name( "Test low stock {$i}" );
- $low_stock->set_regular_price( 5 );
- $low_stock->set_manage_stock( true );
- $low_stock->set_stock_quantity( 1 );
- $low_stock->set_stock_status( ProductStockStatus::IN_STOCK );
- $low_stock->save();
- }
+ $this->create_stock_products( $number_of_low_stock, ProductStockStatus::IN_STOCK, 1 );
$number_of_out_of_stock = 6;
- for ( $i = 1; $i <= $number_of_out_of_stock; $i++ ) {
- $out_of_stock = new WC_Product_Simple();
- $out_of_stock->set_name( "Test out of stock {$i}" );
- $out_of_stock->set_regular_price( 5 );
- $out_of_stock->set_stock_status( ProductStockStatus::OUT_OF_STOCK );
- $out_of_stock->save();
- }
+ $this->create_stock_products( $number_of_out_of_stock, ProductStockStatus::OUT_OF_STOCK );
$number_of_in_stock = 10;
- for ( $i = 1; $i <= $number_of_in_stock; $i++ ) {
- $in_stock = new WC_Product_Simple();
- $in_stock->set_name( "Test in stock {$i}" );
- $in_stock->set_regular_price( 25 );
- $in_stock->save();
- }
+ $this->create_stock_products( $number_of_in_stock, ProductStockStatus::IN_STOCK );
$request = new WP_REST_Request( 'GET', $this->endpoint );
$response = $this->server->dispatch( $request );
@@ -91,12 +72,14 @@ class WC_Admin_Tests_API_Reports_Stock_Stats extends WC_REST_Unit_Test_Case {
$this->assertEquals( 3, $reports['totals'][ ProductStockStatus::LOW_STOCK ] );
$this->assertEquals( 13, $reports['totals'][ ProductStockStatus::IN_STOCK ] );
- // Test backorder and cache update.
- $backorder_stock = new WC_Product_Simple();
- $backorder_stock->set_name( 'Test backorder' );
- $backorder_stock->set_regular_price( 5 );
- $backorder_stock->set_stock_status( ProductStockStatus::ON_BACKORDER );
- $backorder_stock->save();
+ // Test backorder and cache update. Save a real product so the
+ // production lookup-table sync-on-save path is exercised as well.
+ WC_Helper_Product::create_simple_product(
+ true,
+ array(
+ 'stock_status' => ProductStockStatus::ON_BACKORDER,
+ )
+ );
// Clear caches.
delete_transient( 'wc_admin_stock_count_lowstock' );
@@ -119,6 +102,36 @@ class WC_Admin_Tests_API_Reports_Stock_Stats extends WC_REST_Unit_Test_Case {
$this->assertEquals( 13, $reports['totals'][ ProductStockStatus::IN_STOCK ] );
}
+ /**
+ * Create published products with the lookup data consumed by the stock stats queries.
+ *
+ * @param int $count Number of products to create.
+ * @param string $stock_status Product stock status.
+ * @param float|null $stock_quantity Product stock quantity.
+ */
+ private function create_stock_products( $count, $stock_status, $stock_quantity = null ) {
+ global $wpdb;
+
+ $product_ids = $this->factory->post->create_many(
+ $count,
+ array(
+ 'post_type' => 'product',
+ )
+ );
+ $rows = array();
+
+ foreach ( $product_ids as $product_id ) {
+ $rows[] = null === $stock_quantity
+ ? $wpdb->prepare( '(%d, NULL, %s)', $product_id, $stock_status )
+ : $wpdb->prepare( '(%d, %f, %s)', $product_id, $stock_quantity, $stock_status );
+ }
+
+ $query = $wpdb->prepare( 'INSERT INTO %i ( product_id, stock_quantity, stock_status ) VALUES ', $wpdb->wc_product_meta_lookup );
+ $query .= implode( ', ', $rows );
+
+ $wpdb->query( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared -- Table and row values are prepared above.
+ }
+
/**
* Test getting reports without valid permissions.
*/
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/batch-queue.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/batch-queue.php
index 7c381054ee5..99edc803bd9 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/batch-queue.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/batch-queue.php
@@ -143,16 +143,16 @@ class WC_Admin_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case
// Insert a blocking job.
CustomersScheduler::schedule_action( 'import_batch_init', array( 1, false ) );
// Verify that the action was properly blocked.
- $this->assertCount(
- 1,
- OrdersScheduler::queue()->search(
- array(
- 'hook' => OrdersScheduler::get_action( 'import_batch_init' ),
- )
+ $pending_import_batch_init_actions = OrdersScheduler::queue()->search(
+ array(
+ 'status' => OrderStatus::PENDING,
+ 'hook' => OrdersScheduler::get_action( 'import_batch_init' ),
)
);
+ $this->assertCount( 1, $pending_import_batch_init_actions );
// Verify that a second follow up action was queued.
- WC_Helper_Queue::run_all_pending();
+ $queue_runner = new ActionScheduler_QueueRunner();
+ $queue_runner->process_action( key( $pending_import_batch_init_actions ) );
$this->assertCount(
2,
OrdersScheduler::queue()->search(
@@ -161,21 +161,21 @@ class WC_Admin_Tests_Reports_Regenerate_Batching extends WC_REST_Unit_Test_Case
)
)
);
+ CustomersScheduler::clear_queued_actions();
+ OrdersScheduler::clear_queued_actions();
// Queue an action that isn't blocked.
OrdersScheduler::schedule_action( 'import', array( 0 ) );
// Verify that the dependent action was queued.
- $this->assertCount(
- 1,
- OrdersScheduler::queue()->search(
- array(
- 'status' => OrderStatus::PENDING,
- 'hook' => OrdersScheduler::get_action( 'import' ),
- )
+ $pending_import_actions = OrdersScheduler::queue()->search(
+ array(
+ 'status' => OrderStatus::PENDING,
+ 'hook' => OrdersScheduler::get_action( 'import' ),
)
);
+ $this->assertCount( 1, $pending_import_actions );
// Verify that no follow up action was queued.
- WC_Helper_Queue::run_all_pending();
+ $queue_runner->process_action( key( $pending_import_actions ) );
$this->assertCount(
0,
OrdersScheduler::queue()->search(
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/category-lookup.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/category-lookup.php
index 8b605ea98dd..a6fbf16c814 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/category-lookup.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/category-lookup.php
@@ -140,4 +140,22 @@ class WC_Admin_Tests_Category_Lookup extends WP_UnitTestCase {
$this->assertCount( 2, $parent_parent_ids );
$this->assertContains( $this->parent2_term_id, $parent_parent_ids );
}
+
+ /**
+ * Test resetting report data preserves the test transaction.
+ *
+ * @testdox Resetting report data preserves the test transaction.
+ */
+ public function test_reset_stats_dbs_preserves_transaction() {
+ global $wpdb;
+
+ $wpdb->query( 'SAVEPOINT before_reset_stats_dbs' );
+
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $child_parent_ids = $this->get_category_parent_id( $this->child_term_id );
+ $this->assertCount( 2, $child_parent_ids );
+ $this->assertContains( $this->parent_term_id, $child_parent_ids );
+ $this->assertNotFalse( $wpdb->query( 'ROLLBACK TO SAVEPOINT before_reset_stats_dbs' ) );
+ }
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/base-location-country-rule-processor.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/base-location-country-rule-processor.php
index 031da61b9f2..7f824991c4d 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/base-location-country-rule-processor.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/remote-specs/rule-processors/base-location-country-rule-processor.php
@@ -14,6 +14,60 @@ use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
* class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationCountryRuleProcessor
*/
class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationCountryRuleProcessor extends WC_Unit_Test_Case {
+ /**
+ * Store base country fixture.
+ *
+ * @var string
+ */
+ private $default_country;
+
+ /**
+ * Store address fixture.
+ *
+ * @var string
+ */
+ private $store_address;
+
+ /**
+ * Onboarding profile fixture.
+ *
+ * @var array
+ */
+ private $onboarding_profile;
+
+ /**
+ * Option filters installed for the current test.
+ *
+ * @var array
+ */
+ private $option_filters = array();
+
+ /**
+ * Set up option fixtures without triggering unrelated update hooks.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->default_country = 'US:CA';
+ $this->store_address = '';
+ $this->onboarding_profile = array();
+ $this->option_filters = array(
+ 'woocommerce_default_country' => function () {
+ return $this->default_country;
+ },
+ 'woocommerce_store_address' => function () {
+ return $this->store_address;
+ },
+ OnboardingProfile::DATA_OPTION => function () {
+ return $this->onboarding_profile;
+ },
+ );
+
+ foreach ( $this->option_filters as $option => $filter ) {
+ add_filter( 'pre_option_' . $option, $filter );
+ }
+ }
+
/**
* Get the publish_before rule.
*
@@ -33,19 +87,20 @@ class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationCountryRuleProcessor
* Tear down.
*/
public function tearDown(): void {
+ foreach ( $this->option_filters as $option => $filter ) {
+ remove_filter( 'pre_option_' . $option, $filter );
+ }
+
parent::tearDown();
- update_option( 'woocommerce_store_address', '' );
- update_option( 'woocommerce_default_country', 'US:CA' );
- update_option( OnboardingProfile::DATA_OPTION, array() );
}
/**
- * Tests that the processor returns false if not default country.
+ * Tests that the processor returns false if the base country is empty.
*
* @group fast
*/
- public function test_spec_fails_if_wc_get_base_location_is_not_an_array() {
- update_option( 'woocommerce_default_country', '' );
+ public function test_spec_fails_if_base_country_is_empty() {
+ $this->default_country = '';
$processor = new BaseLocationCountryRuleProcessor();
@@ -60,9 +115,6 @@ class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationCountryRuleProcessor
* @group fast
*/
public function test_spec_fails_if_base_location_is_default_and_onboarding_is_not_completed() {
- update_option( 'woocommerce_default_country', 'US:CA' );
- update_option( OnboardingProfile::DATA_OPTION, array() );
-
$processor = new BaseLocationCountryRuleProcessor();
$result = $processor->process( $this->get_rule(), new stdClass() );
@@ -76,8 +128,7 @@ class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationCountryRuleProcessor
* @group fast
*/
public function test_spec_succeeds_if_base_location_is_default_and_onboarding_is_completed() {
- update_option( 'woocommerce_default_country', 'US:CA' );
- update_option( OnboardingProfile::DATA_OPTION, array( 'completed' => true ) );
+ $this->onboarding_profile = array( 'completed' => true );
$processor = new BaseLocationCountryRuleProcessor();
@@ -92,8 +143,7 @@ class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationCountryRuleProcessor
* @group fast
*/
public function test_spec_succeeds_if_base_location_is_default_and_onboarding_is_skipped() {
- update_option( 'woocommerce_default_country', 'US:CA' );
- update_option( OnboardingProfile::DATA_OPTION, array( 'skipped' => true ) );
+ $this->onboarding_profile = array( 'skipped' => true );
$processor = new BaseLocationCountryRuleProcessor();
@@ -108,8 +158,7 @@ class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationCountryRuleProcessor
* @group fast
*/
public function test_spec_succeeds_if_base_location_is_not_default() {
- update_option( 'woocommerce_default_country', 'US:FL' );
- update_option( OnboardingProfile::DATA_OPTION, array() );
+ $this->default_country = 'US:FL';
$processor = new BaseLocationCountryRuleProcessor();
@@ -124,8 +173,7 @@ class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationCountryRuleProcessor
* @group fast
*/
public function test_spec_succeeds_if_base_location_is_default_and_is_store_country_set_is_true() {
- update_option( 'woocommerce_default_country', 'US:CA' );
- update_option( OnboardingProfile::DATA_OPTION, array( 'is_store_country_set' => true ) );
+ $this->onboarding_profile = array( 'is_store_country_set' => true );
$processor = new BaseLocationCountryRuleProcessor();
@@ -140,9 +188,7 @@ class WC_Admin_Tests_RemoteSpecs_RuleProcessors_BaseLocationCountryRuleProcessor
* @group fast
*/
public function test_spec_succeeds_if_store_address_is_updated() {
- update_option( 'woocommerce_store_address', 'updated' );
- update_option( 'woocommerce_default_country', 'US:CA' );
- update_option( OnboardingProfile::DATA_OPTION, array() );
+ $this->store_address = 'updated';
$processor = new BaseLocationCountryRuleProcessor();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php
index 0096dbcbb88..f67cb20f4c0 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php
@@ -876,8 +876,10 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$orders = array();
foreach ( range( 1, 3 ) as $order_number ) {
- $order = WC_Helper_Order::create_order( $customer->get_id(), $product );
- $order->set_date_created( $order_time++ );
+ $order = WC_Helper_Order::create_order( $customer->get_id(), $product );
+ $order_date = $order_time++;
+ $order->set_date_created( $order_date );
+ $order->set_date_paid( $order_date );
$order->set_status( OrderStatus::COMPLETED );
foreach ( $coupons as $amount => $coupon ) {
@@ -1004,17 +1006,13 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$order_status_2 = OrderStatus::PROCESSING;
$customer_1 = WC_Helper_Customer::create_customer( 'cust_1', 'pwd_1', 'user_1@mail.com' );
- $customer_2 = WC_Helper_Customer::create_customer( 'cust_2', 'pwd_2', 'user_2@mail.com' );
$order_1_datetime = new DateTime();
$order_1_hour = (int) $order_1_datetime->format( 'H' );
$order_1_datetime->setTime( $order_1_hour, 10, 0 ); // Set a time near the top of the hour.
$order_1_time = $order_1_datetime->format( 'U' );
- // One more order needs to fit into the same hour, but also be one second later than this one.
- $order_2_time = $order_1_time + 1;
-
- $this_['hour'] = array( 1, 2 );
+ $this_['hour'] = array( 1 );
$this_['day'] = array( 1, 2 );
$this_['week'] = array( 1, 2 );
$this_['month'] = array( 1, 2 );
@@ -1094,11 +1092,10 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
foreach ( array( $product_1, $product_2, $product_3 ) as $product ) {
foreach ( array( null, $coupon_1, $coupon_2 ) as $coupon ) {
foreach ( array( $order_status_1, $order_status_2 ) as $order_status ) {
- foreach ( array( $customer_1, $customer_2 ) as $customer ) {
+ foreach ( array( $customer_1 ) as $customer ) {
foreach (
array(
$order_1_time,
- $order_2_time,
) as $order_time
) { // As there are no tests for different timeframes, ignore these for now: $order_3_time, $order_4_time, $order_5_time, $order_6_time, $order_7_time
// One order with only 1 product.
@@ -1108,11 +1105,10 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
if ( $coupon ) {
$order->apply_coupon( $coupon );
+ } else {
+ $order->calculate_totals();
}
- $order->calculate_totals();
- $order->save();
-
$orders[] = $order;
// One order with 2 products: product_4 and selected product.
@@ -1134,11 +1130,10 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
if ( $coupon ) {
$order_2->apply_coupon( $coupon );
+ } else {
+ $order_2->calculate_totals();
}
- $order_2->calculate_totals();
- $order_2->save();
-
$orders[] = $order_2;
}
}
@@ -1162,8 +1157,8 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$current_hour_end->setTimestamp( $order_1_time + ( HOUR_IN_SECONDS - $order_1_seconds ) - 1 );
// All orders, no filters.
- // 72 orders in one batch (3 products * 3 coupon options * 2 order statuses * 2 customers * 2 orders), 4 items of each product per order
- // 24 orders without coupons, 48 with coupons: 24 with $1 coupon and 24 with $2 coupon.
+ // 36 orders in one batch (3 products * 3 coupon options * 2 order statuses * 2 order shapes), 4 items of each product per order.
+ // 12 orders without coupons, 24 with coupons: 12 with $1 coupon and 12 with $2 coupon.
// shipping is $10 per order.
$query_args = array(
'after' => $current_hour_start->format( TimeInterval::$sql_datetime_format ),
@@ -1171,9 +1166,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'interval' => 'hour',
);
- $order_permutations = 72;
- $order_w_coupon_1_perms = 24;
- $order_w_coupon_2_perms = 24;
+ $order_permutations = 36;
+ $order_w_coupon_1_perms = 12;
+ $order_w_coupon_2_perms = 12;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product + $orders_count / 2 * $qty_per_product * 2;
@@ -1189,7 +1184,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
- $coupons;
$total_sales = $net_revenue + $shipping;
$gross_sales = $net_revenue + $coupons;
- $total_customers = 2;
+ $total_customers = 1;
$expected_stats = array(
'totals' => array(
@@ -1264,9 +1259,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 36;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 12;
+ $order_permutations = 18;
+ $order_w_coupon_1_perms = 6;
+ $order_w_coupon_2_perms = 6;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -1343,9 +1338,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 36;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 12;
+ $order_permutations = 18;
+ $order_w_coupon_1_perms = 6;
+ $order_w_coupon_2_perms = 6;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -1486,9 +1481,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 36;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 12;
+ $order_permutations = 18;
+ $order_w_coupon_1_perms = 6;
+ $order_w_coupon_2_perms = 6;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -1567,9 +1562,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 48;
- $order_w_coupon_1_perms = 16;
- $order_w_coupon_2_perms = 16;
+ $order_permutations = 24;
+ $order_w_coupon_1_perms = 8;
+ $order_w_coupon_2_perms = 8;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -1644,9 +1639,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 24;
- $order_w_coupon_1_perms = 8;
- $order_w_coupon_2_perms = 8;
+ $order_permutations = 12;
+ $order_w_coupon_1_perms = 4;
+ $order_w_coupon_2_perms = 4;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -1674,7 +1669,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 2,
'segments' => array(),
// product 3 and product 4 (that is sometimes included in the orders with product 3).
@@ -1699,7 +1694,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -1720,9 +1715,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 48;
- $order_w_coupon_1_perms = 16;
- $order_w_coupon_2_perms = 16;
+ $order_permutations = 24;
+ $order_w_coupon_1_perms = 8;
+ $order_w_coupon_2_perms = 8;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -1752,7 +1747,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 3,
'segments' => array(),
),
@@ -1776,7 +1771,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -1798,9 +1793,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 24;
- $order_w_coupon_1_perms = 8;
- $order_w_coupon_2_perms = 8;
+ $order_permutations = 12;
+ $order_w_coupon_1_perms = 4;
+ $order_w_coupon_2_perms = 4;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -1828,7 +1823,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 2,
'segments' => array(),
),
@@ -1852,7 +1847,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -1877,9 +1872,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 24;
- $order_w_coupon_1_perms = 8;
- $order_w_coupon_2_perms = 8;
+ $order_permutations = 12;
+ $order_w_coupon_1_perms = 4;
+ $order_w_coupon_2_perms = 4;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -1907,7 +1902,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 2,
'segments' => array(),
),
@@ -1931,7 +1926,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -1954,9 +1949,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 48;
- $order_w_coupon_1_perms = 24;
- $order_w_coupon_2_perms = 24;
+ $order_permutations = 24;
+ $order_w_coupon_1_perms = 12;
+ $order_w_coupon_2_perms = 12;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -1988,7 +1983,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 4,
'segments' => array(),
),
@@ -2012,7 +2007,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -2033,8 +2028,8 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 24;
- $order_w_coupon_1_perms = 24;
+ $order_permutations = 12;
+ $order_w_coupon_1_perms = 12;
$order_w_coupon_2_perms = 0;
$orders_count = count( $this_['hour'] ) * $order_permutations;
@@ -2067,7 +2062,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 4,
'segments' => array(),
),
@@ -2091,7 +2086,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -2112,9 +2107,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 48;
+ $order_permutations = 24;
$order_w_coupon_1_perms = 0;
- $order_w_coupon_2_perms = 24;
+ $order_w_coupon_2_perms = 12;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -2192,7 +2187,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 24;
+ $order_permutations = 12;
$order_w_coupon_1_perms = 0;
$order_w_coupon_2_perms = 0;
@@ -2275,8 +2270,8 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 24;
- $order_w_coupon_1_perms = 24;
+ $order_permutations = 12;
+ $order_w_coupon_1_perms = 12;
$order_w_coupon_2_perms = 0;
$orders_count = count( $this_['hour'] ) * $order_permutations;
@@ -2309,7 +2304,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 4,
'segments' => array(),
),
@@ -2333,7 +2328,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -2353,7 +2348,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'customer_type' => 'new',
);
- $orders_count = 2;
+ $orders_count = 1;
$num_items_sold = $orders_count * $qty_per_product;
$coupons = 0;
$shipping = $orders_count * 10;
@@ -2418,10 +2413,10 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'customer_type' => 'returning',
);
- $total_orders_count = 144;
- $returning_orders_count = 2;
- $order_w_coupon_1_perms = 24;
- $order_w_coupon_2_perms = 24;
+ $total_orders_count = count( $this_['hour'] ) * 36;
+ $returning_orders_count = 1;
+ $order_w_coupon_1_perms = 12;
+ $order_w_coupon_2_perms = 12;
$orders_count = $total_orders_count - $returning_orders_count;
$num_items_sold = $total_orders_count * 6 - ( $returning_orders_count * 4 );
@@ -2503,9 +2498,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 12;
- $order_w_coupon_1_perms = 4;
- $order_w_coupon_2_perms = 4;
+ $order_permutations = 6;
+ $order_w_coupon_1_perms = 2;
+ $order_w_coupon_2_perms = 2;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -2581,8 +2576,8 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 12;
- $order_w_coupon_1_perms = 12;
+ $order_permutations = 6;
+ $order_w_coupon_1_perms = 6;
$order_w_coupon_2_perms = 0;
$orders_count = count( $this_['hour'] ) * $order_permutations;
@@ -2615,7 +2610,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 4,
'segments' => array(),
),
@@ -2639,7 +2634,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -2663,8 +2658,8 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 8;
- $order_w_coupon_1_perms = 8;
+ $order_permutations = 4;
+ $order_w_coupon_1_perms = 4;
$order_w_coupon_2_perms = 0;
$orders_count = count( $this_['hour'] ) * $order_permutations;
@@ -2693,7 +2688,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 2,
'segments' => array(),
),
@@ -2717,7 +2712,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -2744,8 +2739,8 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 4;
- $order_w_coupon_1_perms = 4;
+ $order_permutations = 2;
+ $order_w_coupon_1_perms = 2;
$order_w_coupon_2_perms = 0;
$orders_count = count( $this_['hour'] ) * $order_permutations;
@@ -2774,7 +2769,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 2,
'segments' => array(),
),
@@ -2798,7 +2793,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -2829,8 +2824,8 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 4;
- $order_w_coupon_1_perms = 4;
+ $order_permutations = 2;
+ $order_w_coupon_1_perms = 2;
$order_w_coupon_2_perms = 0;
$orders_count = count( $this_['hour'] ) * $order_permutations;
@@ -2859,7 +2854,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 2,
'segments' => array(),
),
@@ -2883,7 +2878,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -2915,9 +2910,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 12;
- $order_w_coupon_1_perms = 4;
- $order_w_coupon_2_perms = 4;
+ $order_permutations = 6;
+ $order_w_coupon_1_perms = 2;
+ $order_w_coupon_2_perms = 2;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count * $qty_per_product; // No 2-item-orders here.
@@ -3004,8 +2999,8 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 4;
- $order_w_coupon_1_perms = 4;
+ $order_permutations = 2;
+ $order_w_coupon_1_perms = 2;
$order_w_coupon_2_perms = 0;
$orders_count = count( $this_['hour'] ) * $order_permutations;
@@ -3033,7 +3028,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 2,
'segments' => array(),
),
@@ -3057,7 +3052,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -3096,8 +3091,8 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 4;
- $order_w_coupon_1_perms = 4;
+ $order_permutations = 2;
+ $order_w_coupon_1_perms = 2;
$order_w_coupon_2_perms = 0;
$orders_count = count( $this_['hour'] ) * $order_permutations;
@@ -3125,7 +3120,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'products' => 2,
'segments' => array(),
),
@@ -3149,7 +3144,7 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
'net_revenue' => $net_revenue,
'avg_items_per_order' => $num_items_sold / $orders_count,
'avg_order_value' => $net_revenue / $orders_count,
- 'total_customers' => 2,
+ 'total_customers' => $total_customers,
'segments' => array(),
),
),
@@ -3175,9 +3170,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 72;
- $order_w_coupon_1_perms = 24;
- $order_w_coupon_2_perms = 24;
+ $order_permutations = 36;
+ $order_w_coupon_1_perms = 12;
+ $order_w_coupon_2_perms = 12;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -3258,9 +3253,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 48;
- $order_w_coupon_1_perms = 16;
- $order_w_coupon_2_perms = 16;
+ $order_permutations = 24;
+ $order_w_coupon_1_perms = 8;
+ $order_w_coupon_2_perms = 8;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -3341,9 +3336,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 48;
- $order_w_coupon_1_perms = 24;
- $order_w_coupon_2_perms = 12;
+ $order_permutations = 24;
+ $order_w_coupon_1_perms = 12;
+ $order_w_coupon_2_perms = 6;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -3424,9 +3419,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 60;
- $order_w_coupon_1_perms = 12;
- $order_w_coupon_2_perms = 24;
+ $order_permutations = 30;
+ $order_w_coupon_1_perms = 6;
+ $order_w_coupon_2_perms = 12;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -3507,9 +3502,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 40;
- $order_w_coupon_1_perms = 24;
- $order_w_coupon_2_perms = 8;
+ $order_permutations = 20;
+ $order_w_coupon_1_perms = 12;
+ $order_w_coupon_2_perms = 4;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -3593,9 +3588,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 56;
- $order_w_coupon_1_perms = 24;
- $order_w_coupon_2_perms = 16;
+ $order_permutations = 28;
+ $order_w_coupon_1_perms = 12;
+ $order_w_coupon_2_perms = 8;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -3682,9 +3677,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 56;
- $order_w_coupon_1_perms = 24;
- $order_w_coupon_2_perms = 16;
+ $order_permutations = 28;
+ $order_w_coupon_1_perms = 12;
+ $order_w_coupon_2_perms = 8;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -3771,9 +3766,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 60;
- $order_w_coupon_1_perms = 20;
- $order_w_coupon_2_perms = 20;
+ $order_permutations = 30;
+ $order_w_coupon_1_perms = 10;
+ $order_w_coupon_2_perms = 10;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -3863,9 +3858,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 64;
- $order_w_coupon_1_perms = 24;
- $order_w_coupon_2_perms = 20;
+ $order_permutations = 32;
+ $order_w_coupon_1_perms = 12;
+ $order_w_coupon_2_perms = 10;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -3958,9 +3953,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
),
);
- $order_permutations = 68;
- $order_w_coupon_1_perms = 24;
- $order_w_coupon_2_perms = 20;
+ $order_permutations = 34;
+ $order_w_coupon_1_perms = 12;
+ $order_w_coupon_2_perms = 10;
$orders_count = count( $this_['hour'] ) * $order_permutations;
$num_items_sold = $orders_count / 2 * $qty_per_product
@@ -4028,6 +4023,60 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$this->assertEquals( $expected_stats, json_decode( wp_json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . $this->return_print_r( $query_args ) . "; query: {$wpdb->last_query}" );
}
+ /**
+ * Test that distinct customer counts and same-hour interval aggregation work with multiple customers.
+ *
+ * @testdox Distinct customer counts and same-hour interval aggregation work with multiple customers.
+ */
+ public function test_populate_and_query_multiple_customers_same_hour() {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $customer_1 = WC_Helper_Customer::create_customer( 'cust_multi_1', 'pwd_1', 'multi_user_1@mail.com' );
+ $customer_2 = WC_Helper_Customer::create_customer( 'cust_multi_2', 'pwd_2', 'multi_user_2@mail.com' );
+
+ // Two completed orders by different customers within the same hourly interval.
+ // Set a time near the top of the hour so both orders stay within it.
+ $order_datetime = new DateTime();
+ $order_datetime->setTime( (int) $order_datetime->format( 'H' ), 10, 0 );
+ $order_time = (int) $order_datetime->format( 'U' );
+
+ $order_1 = WC_Helper_Order::create_order( $customer_1->get_id() );
+ $order_1->set_date_created( $order_time );
+ $order_1->set_status( OrderStatus::COMPLETED );
+ $order_1->save();
+
+ // Offset by 1 second to keep both orders in the same hour but distinct.
+ $order_2 = WC_Helper_Order::create_order( $customer_2->get_id() );
+ $order_2->set_date_created( $order_time + 1 );
+ $order_2->set_status( OrderStatus::COMPLETED );
+ $order_2->save();
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ $data_store = new OrdersStatsDataStore();
+ $start_time = gmdate( 'Y-m-d H:00:00', $order_1->get_date_created()->getOffsetTimestamp() );
+ $end_time = gmdate( 'Y-m-d H:59:59', $order_1->get_date_created()->getOffsetTimestamp() );
+
+ $data = json_decode(
+ wp_json_encode(
+ $data_store->get_data(
+ array(
+ 'interval' => 'hour',
+ 'after' => $start_time,
+ 'before' => $end_time,
+ )
+ )
+ ),
+ true
+ );
+
+ $this->assertEquals( 2, $data['totals']['orders_count'] );
+ $this->assertEquals( 2, $data['totals']['total_customers'] );
+ $this->assertCount( 1, $data['intervals'] );
+ $this->assertEquals( 2, $data['intervals'][0]['subtotals']['orders_count'] );
+ $this->assertEquals( 2, $data['intervals'][0]['subtotals']['total_customers'] );
+ }
+
/**
* Test if lookup tables are cleaned after delete an order.
*
@@ -4255,10 +4304,11 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$i3_end->setTimestamp( $i3_end_timestamp );
$query_args = array(
- 'after' => $two_hours_back->format( TimeInterval::$sql_datetime_format ),
- 'before' => $now->format( TimeInterval::$sql_datetime_format ),
- 'interval' => 'hour',
- 'segmentby' => 'product',
+ 'after' => $two_hours_back->format( TimeInterval::$sql_datetime_format ),
+ 'before' => $now->format( TimeInterval::$sql_datetime_format ),
+ 'interval' => 'hour',
+ 'segmentby' => 'product',
+ 'product_includes' => array( $product_1->get_id(), $product_2->get_id(), $product_3->get_id() ),
);
$shipping_amnt = 10;
@@ -4651,6 +4701,64 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$this->assertEquals( $expected_stats, $actual, 'Segmenting by product, expected: ' . $this->return_print_r( $expected_stats ) . '; actual: ' . $this->return_print_r( $actual ) );
}
+ /**
+ * Test that product segmentation without product_includes enumerates the whole catalog.
+ *
+ * @testdox Product segmentation without product_includes enumerates the whole catalog.
+ */
+ public function test_segmenting_by_product_without_includes() {
+ WC_Helper_Reports::reset_stats_dbs();
+
+ $sold_product = new WC_Product_Simple();
+ $sold_product->set_name( 'Segmented Sold Product' );
+ $sold_product->set_regular_price( 10 );
+ $sold_product->save();
+
+ $unsold_product = new WC_Product_Simple();
+ $unsold_product->set_name( 'Segmented Unsold Product' );
+ $unsold_product->set_regular_price( 15 );
+ $unsold_product->save();
+
+ $order = WC_Helper_Order::create_order( 1, $sold_product );
+ $order->set_status( OrderStatus::COMPLETED );
+ $order->save();
+
+ WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+
+ $data_store = new OrdersStatsDataStore();
+ $start_time = gmdate( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() );
+ $end_time = gmdate( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() );
+
+ // Without product_includes the segmenter must return a segment for
+ // every store product, not only the ones with orders.
+ $data = json_decode(
+ wp_json_encode(
+ $data_store->get_data(
+ array(
+ 'after' => $start_time,
+ 'before' => $end_time,
+ 'segmentby' => 'product',
+ )
+ )
+ ),
+ true
+ );
+
+ $segments = array_column( $data['totals']['segments'], 'subtotals', 'segment_id' );
+ $expected_product_ids = wc_get_products(
+ array(
+ 'return' => 'ids',
+ 'limit' => -1,
+ )
+ );
+
+ $this->assertEqualsCanonicalizing( $expected_product_ids, array_keys( $segments ) );
+ $this->assertEquals( 1, $segments[ $sold_product->get_id() ]['orders_count'] );
+ $this->assertEquals( 4, $segments[ $sold_product->get_id() ]['num_items_sold'] );
+ $this->assertEquals( 0, $segments[ $unsold_product->get_id() ]['orders_count'] );
+ $this->assertEquals( 0, $segments[ $unsold_product->get_id() ]['num_items_sold'] );
+ }
+
/**
* Test zero filling when ordering by date in descending and ascending order.
*/
@@ -6371,11 +6479,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
$this->assertEquals( 1, $actual_data->totals->total_customers );
- // Wait a bit so that orders are not created at the same second.
- sleep( 1 );
-
$order_2 = WC_Helper_Order::create_order( 0, $product );
$order_2->set_date_created( $order_1_time );
+ $order_2->set_date_modified( $order_1_time + 1 );
$order_2->set_date_paid( $order_1_time );
$order_2->set_status( OrderStatus::PROCESSING );
$order_2->set_total( 100 );
@@ -6480,11 +6586,9 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
$actual_data = json_decode( wp_json_encode( $data_store->get_data( $query_args ) ) );
$this->assertEquals( 1, $actual_data->totals->total_customers );
- // Wait a bit so that orders are not created at the same second.
- sleep( 1 );
-
$order_2 = WC_Helper_Order::create_order( $customer_1->get_id(), $product );
$order_2->set_date_created( $order_1_time );
+ $order_2->set_date_modified( $order_1_time + 1 );
$order_2->set_date_paid( $order_1_time );
$order_2->set_status( OrderStatus::PROCESSING );
$order_2->set_total( 100 );
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/wc-admin-helper.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/wc-admin-helper.php
index bb0fbd6601c..74b9b9e0341 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/wc-admin-helper.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/wc-admin-helper.php
@@ -82,12 +82,23 @@ class WC_Admin_Tests_Admin_Helper extends WC_Unit_Test_Case {
*/
public function setUp(): void {
parent::setUp();
+ $direct_update_mode = get_option( 'woocommerce_attribute_lookup_direct_updates' );
+ self::enable_direct_product_attribute_lookup_updates();
+ try {
+ // Create a product.
+ $product = WC_Helper_Product::create_simple_product();
+ $product->set_status( 'publish' );
+ $product->save();
+ $this->product_id = $product->get_id();
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
+ }
- // Create a product.
- $product = WC_Helper_Product::create_simple_product();
- $product->set_status( 'publish' );
- $product->save();
- $this->product_id = $product->get_id();
+ $this->assertSame(
+ $direct_update_mode,
+ get_option( 'woocommerce_attribute_lookup_direct_updates' ),
+ 'The direct attribute lookup update mode was not restored after fixture creation.'
+ );
}
/**
@@ -96,10 +107,45 @@ class WC_Admin_Tests_Admin_Helper extends WC_Unit_Test_Case {
* @return void
*/
public function tearDown(): void {
- parent::tearDown();
+ $direct_update_mode = get_option( 'woocommerce_attribute_lookup_direct_updates' );
+ try {
+ // Clean up product.
+ self::enable_direct_product_attribute_lookup_updates();
+ try {
+ WC_Helper_Product::delete_product( $this->product_id );
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
+ }
+
+ $this->assertSame(
+ $direct_update_mode,
+ get_option( 'woocommerce_attribute_lookup_direct_updates' ),
+ 'The direct attribute lookup update mode was not restored after fixture cleanup.'
+ );
+ } finally {
+ parent::tearDown();
+ }
+ }
- // Clean up product.
- WC_Helper_Product::delete_product( $this->product_id );
+ /**
+ * Test that the product fixture does not schedule an attribute lookup update.
+ *
+ * @testdox Product fixture does not schedule an attribute lookup update.
+ */
+ public function test_product_fixture_does_not_schedule_attribute_lookup_update(): void {
+ $queue = WC()->get_instance_of( WC_Queue::class );
+
+ $this->assertEmpty(
+ $queue->search(
+ array(
+ 'hook' => 'woocommerce_run_product_attribute_lookup_update_callback',
+ 'args' => array( $this->product_id, \Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore::ACTION_INSERT ),
+ 'status' => ActionScheduler_Store::STATUS_PENDING,
+ ),
+ 'ids'
+ ),
+ "Product {$this->product_id} has a pending attribute lookup update."
+ );
}
/**
diff --git a/plugins/woocommerce/tests/php/bin/run-phpunit.sh b/plugins/woocommerce/tests/php/bin/run-phpunit.sh
new file mode 100755
index 00000000000..0d4fef985a1
--- /dev/null
+++ b/plugins/woocommerce/tests/php/bin/run-phpunit.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+set -eu
+
+# The wp-env database is disposable, so avoid syncing the redo log after every
+# transaction while retaining InnoDB's normal SQL and rollback behavior.
+wp db query "SET GLOBAL innodb_flush_log_at_trx_commit=2" >/dev/null
+
+exec php -d opcache.enable_cli=1 vendor/bin/phpunit "$@"
diff --git a/plugins/woocommerce/tests/php/framework/class-wc-lazy-rest-server-test.php b/plugins/woocommerce/tests/php/framework/class-wc-lazy-rest-server-test.php
new file mode 100644
index 00000000000..3c4e39aba84
--- /dev/null
+++ b/plugins/woocommerce/tests/php/framework/class-wc-lazy-rest-server-test.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Tests for the lazy REST server used by WC_REST_Unit_Test_Case.
+ *
+ * @package WooCommerce\Tests\Framework
+ */
+
+declare( strict_types = 1 );
+
+/**
+ * WC_Lazy_REST_Server_Test class.
+ */
+class WC_Lazy_REST_Server_Test extends WC_REST_Unit_Test_Case {
+
+ /**
+ * Filter callback currently enabling the rest-api-v4 feature, if any.
+ *
+ * @var callable|null
+ */
+ private $enable_v4_feature_callback = null;
+
+ /**
+ * Clean up the feature filter.
+ */
+ public function tearDown(): void {
+ if ( null !== $this->enable_v4_feature_callback ) {
+ remove_filter( 'woocommerce_admin_features', $this->enable_v4_feature_callback );
+ $this->enable_v4_feature_callback = null;
+ }
+ parent::tearDown();
+ }
+
+ /**
+ * @testdox get_routes() returns the complete route table after a scoped namespace dispatch.
+ */
+ public function test_get_routes_returns_full_route_table_after_scoped_dispatch() {
+ $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products' ) );
+
+ $routes = $this->server->get_routes();
+
+ $this->assertArrayHasKey( '/wc/v2/products', $routes );
+ $this->assertArrayHasKey( '/wc/v3/products', $routes );
+ $this->assertArrayHasKey( '/wc/v1/products', $routes );
+ }
+
+ /**
+ * @testdox get_routes() leaves a server holding only manually registered routes untouched.
+ */
+ public function test_get_routes_preserves_manual_route_registration() {
+ self::do_isolated_rest_api_init(
+ array(
+ static function () {
+ register_rest_route(
+ 'wc-lazy-test/v1',
+ '/ping',
+ array(
+ 'methods' => 'GET',
+ 'callback' => '__return_empty_array',
+ 'permission_callback' => '__return_true',
+ )
+ );
+ },
+ )
+ );
+
+ $routes = $this->server->get_routes();
+
+ $this->assertArrayHasKey( '/wc-lazy-test/v1/ping', $routes );
+ $this->assertArrayNotHasKey( '/wc/v3/products', $routes );
+ }
+
+ /**
+ * @testdox Full initialization registers controllers through the production registry, feature gates, and DI wiring.
+ */
+ public function test_full_initialization_wires_controllers_through_production_registry() {
+ $this->enable_rest_api_v4_feature();
+
+ $this->server->initialize_all_routes();
+ $routes = $this->server->get_routes();
+
+ // Controllers whose behavior suites run against scoped servers stay
+ // covered here through their real production registration paths.
+ $this->assertArrayHasKey( '/wc-admin/mobile-app/qr-login-token', $routes );
+ $this->assertArrayHasKey( '/wc-admin/mobile-app/qr-login-exchange', $routes );
+ $this->assertArrayHasKey( '/wc-admin/mobile-app/qr-login-status', $routes );
+ $this->assertArrayHasKey( '/wc/v4/shipping-zones', $routes );
+ }
+
+ /**
+ * @testdox The wc/v4 namespace stays gated behind the rest-api-v4 feature.
+ */
+ public function test_v4_namespace_stays_gated_without_feature() {
+ $this->server->initialize_all_routes();
+ $routes = $this->server->get_routes();
+
+ $this->assertArrayHasKey( '/wc/v3/products', $routes );
+ $this->assertArrayNotHasKey( '/wc/v4/shipping-zones', $routes );
+ }
+
+ /**
+ * Enable the REST API v4 feature for the current test.
+ */
+ private function enable_rest_api_v4_feature(): void {
+ $this->enable_v4_feature_callback = static function ( $features ) {
+ $features[] = 'rest-api-v4';
+ return $features;
+ };
+ add_filter( 'woocommerce_admin_features', $this->enable_v4_feature_callback );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/helpers/HPOSToggleTrait.php b/plugins/woocommerce/tests/php/helpers/HPOSToggleTrait.php
index a9b84a60f72..02c1b0fb939 100644
--- a/plugins/woocommerce/tests/php/helpers/HPOSToggleTrait.php
+++ b/plugins/woocommerce/tests/php/helpers/HPOSToggleTrait.php
@@ -4,6 +4,7 @@ namespace Automattic\WooCommerce\RestApi\UnitTests;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
+use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
/**
@@ -13,6 +14,24 @@ use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
*/
trait HPOSToggleTrait {
+ /**
+ * Ensure permanent HPOS tables exist and empty them without resetting IDs.
+ */
+ protected static function setup_cot_tables(): void {
+ OrderHelper::create_order_custom_table_if_not_exist();
+
+ global $wpdb;
+ $tables = array(
+ OrdersTableDataStore::get_meta_table_name(),
+ OrdersTableDataStore::get_operational_data_table_name(),
+ OrdersTableDataStore::get_addresses_table_name(),
+ OrdersTableDataStore::get_orders_table_name(),
+ );
+ foreach ( $tables as $table ) {
+ $wpdb->query( "DELETE FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names are provided by the data store.
+ }
+ }
+
/**
* Call in setUp to enable COT/HPOS.
*
diff --git a/plugins/woocommerce/tests/php/helpers/MetaDataAssertionTrait.php b/plugins/woocommerce/tests/php/helpers/MetaDataAssertionTrait.php
index 81a0443993d..84c6cc14e11 100644
--- a/plugins/woocommerce/tests/php/helpers/MetaDataAssertionTrait.php
+++ b/plugins/woocommerce/tests/php/helpers/MetaDataAssertionTrait.php
@@ -38,7 +38,7 @@ trait MetaDataAssertionTrait {
* Asserts that a WC_Data object processed incomplete meta_data entries correctly:
* - Complete entries are saved.
* - Entries without a key are not processed.
- * - Entries with a missing value behave the same as passing null explicitly.
+ * - Entries with a missing or explicit null value are not persisted.
*
* @param WC_Data $wc_data The object whose meta data to check.
*/
@@ -50,10 +50,7 @@ trait MetaDataAssertionTrait {
$this->assertEquals( 'complete_value', $meta_by_key['complete_key'] ?? null, 'Complete entry should be saved' );
$this->assertArrayNotHasKey( '', $meta_by_key, 'Entry without key should not create a meta data row' );
- $this->assertSame(
- $meta_by_key['key_missing_value'] ?? 'NOT_FOUND',
- $meta_by_key['key_explicit_null'] ?? 'NOT_FOUND',
- 'Missing value should be equivalent to explicit null'
- );
+ $this->assertArrayNotHasKey( 'key_missing_value', $meta_by_key, 'Entry without value should not create a meta data row' );
+ $this->assertArrayNotHasKey( 'key_explicit_null', $meta_by_key, 'Entry with null value should not create a meta data row' );
}
}
diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php
index d95279e32b1..e2d7f0d24a2 100644
--- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php
+++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php
@@ -17,6 +17,7 @@ class WC_Abstract_Product_Test extends WC_Unit_Test_Case {
parent::tearDown();
$this->disable_cogs_feature();
remove_all_filters( 'woocommerce_get_cogs_total_value' );
+ self::disable_direct_product_attribute_lookup_updates();
}
/**
@@ -43,6 +44,7 @@ class WC_Abstract_Product_Test extends WC_Unit_Test_Case {
* Setup items we need repeatedly across tests in this class.
*/
public function set_up() {
+ self::enable_direct_product_attribute_lookup_updates();
$this->admin_user = self::factory()->user->create( array( 'role' => 'administrator' ) );
$this->shop_manager_user = self::factory()->user->create( array( 'role' => 'shop_manager' ) );
$this->download_directories = wc_get_container()->get( Download_Directories::class );
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php
index 784dfd41713..ff707fe64a3 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php
@@ -12,10 +12,30 @@ use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskList;
*/
class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
+ /**
+ * Whether the default country option existed before the test.
+ *
+ * @var bool
+ */
+ private $default_country_option_existed = false;
+
+ /**
+ * Default country option value before the test.
+ *
+ * @var mixed
+ */
+ private $default_country_option_value;
+
/**
* Set up
*/
public function setUp(): void {
+ $missing_option = new stdClass();
+ $this->default_country_option_value = get_option( 'woocommerce_default_country', $missing_option );
+ $this->default_country_option_existed = $missing_option !== $this->default_country_option_value;
+
+ parent::setUp();
+
// Set default country to non-US so that 'payments' task gets added but 'woocommerce-payments' doesn't,
// by default it won't be considered completed but we can manually change that as needed.
update_option( 'woocommerce_default_country', 'JP' );
@@ -29,17 +49,46 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
)
);
wp_set_current_user( $this->admin );
-
- parent::setUp();
}
/**
* Tear down
*/
public function tearDown(): void {
- remove_all_filters( 'woocommerce_available_payment_gateways' );
+ try {
+ remove_all_filters( 'woocommerce_available_payment_gateways' );
+ } finally {
+ try {
+ parent::tearDown();
+ } finally {
+ $this->invalidate_dashboard_option_caches();
+
+ if ( $this->default_country_option_existed ) {
+ update_option( 'woocommerce_default_country', $this->default_country_option_value );
+ } else {
+ delete_option( 'woocommerce_default_country' );
+ }
+ }
+ }
+ }
- parent::tearDown();
+ /**
+ * Invalidate caches for options modified by dashboard tests.
+ */
+ private function invalidate_dashboard_option_caches(): void {
+ $option_names = array(
+ 'woocommerce_default_country',
+ 'woocommerce_default_homepage_layout',
+ 'woocommerce_onboarding_profile',
+ 'woocommerce_task_list_hidden',
+ 'woocommerce_task_list_hidden_lists',
+ );
+
+ foreach ( $option_names as $option_name ) {
+ wp_cache_delete( $option_name, 'options' );
+ }
+ wp_cache_delete( 'alloptions', 'options' );
+ wp_cache_delete( 'notoptions', 'options' );
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-reports-customers-controller-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-reports-customers-controller-test.php
index 6d53c3667bc..abd49855030 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-reports-customers-controller-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-reports-customers-controller-test.php
@@ -10,7 +10,21 @@ use Automattic\WooCommerce\Enums\OrderStatus;
*
* @package WooCommerce\Admin\Tests\API
*/
-class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case {
+class WC_Admin_Reports_Customers_Controller_Test extends WC_Unit_Test_Case {
+ /**
+ * REST server used to dispatch customer report requests.
+ *
+ * @var WP_REST_Server
+ */
+ private $server;
+
+ /**
+ * Customer reports controller registered on the test server.
+ *
+ * @var CustomersController
+ */
+ private $controller;
+
/**
* Endpoint.
*
@@ -30,42 +44,48 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
*
* @var WC_Product_Simple
*/
- protected $product;
+ protected static $product;
/**
* Registered customers.
*
* @var array
*/
- protected $registered_customers = array();
+ protected static $registered_customers = array();
/**
* Guest orders (no user_id).
*
* @var array
*/
- protected $guest_orders = array();
+ protected static $guest_orders = array();
/**
- * Setup test reports customers data.
+ * Report import action IDs created for the shared orders.
+ *
+ * @var int[]
*/
- public function setUp(): void {
- parent::setUp();
+ private static $import_action_ids = array();
- $this->user = $this->factory->user->create(
- array(
- 'role' => 'administrator',
- )
- );
-
- wp_set_current_user( $this->user );
+ /**
+ * Set up shared report customer data.
+ */
+ public static function wpSetUpBeforeClass() {
WC_Helper_Reports::reset_stats_dbs();
+ self::$registered_customers = array();
+ self::$guest_orders = array();
+ self::$import_action_ids = array();
// Create a test product.
- $this->product = new WC_Product_Simple();
- $this->product->set_name( 'Test Product' );
- $this->product->set_regular_price( 25 );
- $this->product->save();
+ self::$product = new WC_Product_Simple();
+ self::$product->set_name( 'Test Product' );
+ self::$product->set_regular_price( 25 );
+ self::enable_direct_product_attribute_lookup_updates();
+ try {
+ self::$product->save();
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
+ }
// Create registered customers with different names for search testing.
$customer1 = WC_Helper_Customer::create_customer( 'customer1', 'password', 'customer1@example.com' );
@@ -74,7 +94,7 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
$customer1->set_billing_state( 'CA' );
$customer1->set_billing_country( 'US' );
$customer1->save();
- $this->registered_customers[] = $customer1;
+ self::$registered_customers[] = $customer1;
$customer2 = WC_Helper_Customer::create_customer( 'customer2', 'password', 'customer2@example.com' );
$customer2->set_first_name( 'Jane' );
@@ -82,7 +102,7 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
$customer2->set_billing_state( 'NY' );
$customer2->set_billing_country( 'US' );
$customer2->save();
- $this->registered_customers[] = $customer2;
+ self::$registered_customers[] = $customer2;
$customer3 = WC_Helper_Customer::create_customer( 'customer3', 'password', 'customer3@example.com' );
$customer3->set_first_name( 'Bob' );
@@ -90,11 +110,13 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
$customer3->set_billing_state( 'CA' );
$customer3->set_billing_country( 'US' );
$customer3->save();
- $this->registered_customers[] = $customer3;
+ self::$registered_customers[] = $customer3;
+
+ add_action( 'action_scheduler_stored_action', array( self::class, 'track_import_action' ) );
// Create orders for registered customers with location data.
- foreach ( $this->registered_customers as $index => $customer ) {
- $order = WC_Helper_Order::create_order( $customer->get_id(), $this->product );
+ foreach ( self::$registered_customers as $index => $customer ) {
+ $order = WC_Helper_Order::create_order( $customer->get_id(), self::$product );
$order->set_status( OrderStatus::COMPLETED );
$order->set_total( 100 + ( $index * 50 ) );
$order->set_billing_state( $customer->get_billing_state() );
@@ -103,7 +125,7 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
}
// Create guest orders (no user_id) with different locations.
- $guest_order1 = WC_Helper_Order::create_order( 0, $this->product );
+ $guest_order1 = WC_Helper_Order::create_order( 0, self::$product );
$guest_order1->set_billing_email( 'guest1@example.com' );
$guest_order1->set_billing_first_name( 'Guest' );
$guest_order1->set_billing_last_name( 'Customer' );
@@ -112,9 +134,9 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
$guest_order1->set_status( OrderStatus::COMPLETED );
$guest_order1->set_total( 50 );
$guest_order1->save();
- $this->guest_orders[] = $guest_order1;
+ self::$guest_orders[] = $guest_order1;
- $guest_order2 = WC_Helper_Order::create_order( 0, $this->product );
+ $guest_order2 = WC_Helper_Order::create_order( 0, self::$product );
$guest_order2->set_billing_email( 'guest2@example.com' );
$guest_order2->set_billing_first_name( 'Guest' );
$guest_order2->set_billing_last_name( 'User' );
@@ -123,10 +145,81 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
$guest_order2->set_status( OrderStatus::COMPLETED );
$guest_order2->set_total( 75 );
$guest_order2->save();
- $this->guest_orders[] = $guest_order2;
+ self::$guest_orders[] = $guest_order2;
// Sync all data to lookup tables.
WC_Helper_Queue::run_all_pending( 'wc-admin-data' );
+ remove_action( 'action_scheduler_stored_action', array( self::class, 'track_import_action' ) );
+ }
+
+ /**
+ * Track report import actions created for shared order fixtures.
+ *
+ * @param int $action_id Action ID.
+ */
+ public static function track_import_action( $action_id ): void {
+ $action = ActionScheduler_Store::instance()->fetch_action( $action_id );
+ if ( 'wc-admin_import_orders' === $action->get_hook() ) {
+ self::$import_action_ids[] = (int) $action_id;
+ }
+ }
+
+ /**
+ * Clean up shared report customer data.
+ */
+ public static function wpTearDownAfterClass() {
+ remove_action( 'action_scheduler_stored_action', array( self::class, 'track_import_action' ) );
+ WC_Helper_Reports::reset_stats_dbs();
+ $action_store = ActionScheduler_Store::instance();
+ foreach ( self::$import_action_ids as $action_id ) {
+ $action_store->delete_action( $action_id );
+ }
+ try {
+ foreach ( self::$registered_customers as $customer ) {
+ $customer->delete();
+ }
+ } finally {
+ self::enable_direct_product_attribute_lookup_updates();
+ try {
+ self::$product->delete( true );
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
+ }
+ }
+ self::$product = null;
+ self::$registered_customers = array();
+ self::$guest_orders = array();
+ self::$import_action_ids = array();
+ }
+
+ /**
+ * Set up authentication for each test.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->controller = new CustomersController();
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->controller, 'register_routes' ) ),
+ true
+ );
+
+ $this->user = $this->factory->user->create(
+ array(
+ 'role' => 'administrator',
+ )
+ );
+
+ wp_set_current_user( $this->user );
+ }
+
+ /**
+ * Clear the scoped REST server.
+ */
+ public function tearDown(): void {
+ $this->clear_rest_server();
+ unset( $this->server, $this->controller );
+ parent::tearDown();
}
/**
@@ -804,7 +897,7 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
* @testdox Should consolidate numeric name_includes IDs into customers param.
*/
public function test_name_includes_with_customer_ids(): void {
- $customer_id = CustomersDataStore::get_customer_id_by_user_id( $this->registered_customers[0]->get_id() );
+ $customer_id = CustomersDataStore::get_customer_id_by_user_id( self::$registered_customers[0]->get_id() );
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params(
@@ -825,8 +918,8 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
* @testdox Should consolidate numeric email_includes IDs into customers param.
*/
public function test_email_includes_with_customer_ids(): void {
- $customer_id_1 = CustomersDataStore::get_customer_id_by_user_id( $this->registered_customers[0]->get_id() );
- $customer_id_2 = CustomersDataStore::get_customer_id_by_user_id( $this->registered_customers[1]->get_id() );
+ $customer_id_1 = CustomersDataStore::get_customer_id_by_user_id( self::$registered_customers[0]->get_id() );
+ $customer_id_2 = CustomersDataStore::get_customer_id_by_user_id( self::$registered_customers[1]->get_id() );
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params(
@@ -846,8 +939,8 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
* @testdox Should filter by customers_exclude param.
*/
public function test_customers_exclude(): void {
- $customer_id_1 = CustomersDataStore::get_customer_id_by_user_id( $this->registered_customers[0]->get_id() );
- $customer_id_2 = CustomersDataStore::get_customer_id_by_user_id( $this->registered_customers[1]->get_id() );
+ $customer_id_1 = CustomersDataStore::get_customer_id_by_user_id( self::$registered_customers[0]->get_id() );
+ $customer_id_2 = CustomersDataStore::get_customer_id_by_user_id( self::$registered_customers[1]->get_id() );
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params(
@@ -869,7 +962,7 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
* @testdox Should consolidate numeric exclude IDs into customers_exclude param.
*/
public function test_email_excludes_with_customer_ids(): void {
- $customer_id = CustomersDataStore::get_customer_id_by_user_id( $this->registered_customers[0]->get_id() );
+ $customer_id = CustomersDataStore::get_customer_id_by_user_id( self::$registered_customers[0]->get_id() );
$request = new WP_REST_Request( 'GET', $this->endpoint );
$request->set_query_params(
@@ -890,8 +983,8 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
* @testdox Should intersect include sets when match=all.
*/
public function test_consolidation_match_all_intersection(): void {
- $customer_id_1 = CustomersDataStore::get_customer_id_by_user_id( $this->registered_customers[0]->get_id() );
- $customer_id_2 = CustomersDataStore::get_customer_id_by_user_id( $this->registered_customers[1]->get_id() );
+ $customer_id_1 = CustomersDataStore::get_customer_id_by_user_id( self::$registered_customers[0]->get_id() );
+ $customer_id_2 = CustomersDataStore::get_customer_id_by_user_id( self::$registered_customers[1]->get_id() );
$args = CustomersController::consolidate_customer_id_filters(
array(
@@ -914,8 +1007,8 @@ class WC_Admin_Reports_Customers_Controller_Test extends WC_REST_Unit_Test_Case
* @testdox Should union include sets when match=any.
*/
public function test_consolidation_match_any_union(): void {
- $customer_id_1 = CustomersDataStore::get_customer_id_by_user_id( $this->registered_customers[0]->get_id() );
- $customer_id_2 = CustomersDataStore::get_customer_id_by_user_id( $this->registered_customers[1]->get_id() );
+ $customer_id_1 = CustomersDataStore::get_customer_id_by_user_id( self::$registered_customers[0]->get_id() );
+ $customer_id_2 = CustomersDataStore::get_customer_id_by_user_id( self::$registered_customers[1]->get_id() );
$args = CustomersController::consolidate_customer_id_filters(
array(
diff --git a/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-orders-test.php b/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-orders-test.php
index b3ccc320f62..c32fa74567a 100644
--- a/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-orders-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/list-tables/class-wc-admin-list-table-orders-test.php
@@ -119,12 +119,14 @@ class WC_Admin_List_Table_Orders_Test extends WC_Unit_Test_Case {
$dummy_order->set_billing_first_name( 'NotAMatch' );
$dummy_order->save();
+ $order = WC_Helper_Order::create_order();
foreach ( $fields as $field => $value ) {
- $order = WC_Helper_Order::create_order();
$setter = 'set_' . $field;
$order->$setter( $value );
- $order->save();
+ }
+ $order->save();
+ foreach ( $fields as $field => $value ) {
$_GET['s'] = $value;
$GLOBALS['pagenow'] = 'edit.php'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
@@ -151,8 +153,8 @@ class WC_Admin_List_Table_Orders_Test extends WC_Unit_Test_Case {
);
unset( $_GET['s'], $GLOBALS['pagenow'] );
- wp_delete_post( $order->get_id(), true );
}
+ wp_delete_post( $order->get_id(), true );
wp_delete_post( $dummy_order->get_id(), true );
}
@@ -163,11 +165,11 @@ class WC_Admin_List_Table_Orders_Test extends WC_Unit_Test_Case {
// Create several dummy orders.
$orders = array();
for ( $i = 0; $i < 3; $i++ ) {
- $orders[] = WC_Helper_Order::create_order();
+ $orders[] = wc_create_order();
}
// Create a dummy order that should NOT match.
- $dummy_order = WC_Helper_Order::create_order();
+ $dummy_order = wc_create_order();
$dummy_order->set_billing_first_name( 'NotAMatch' );
$dummy_order->save();
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
index db1872c54c5..668cadf5284 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
@@ -15,6 +15,19 @@ use Automattic\WooCommerce\Proxies\LegacyProxy;
*/
class WC_AJAX_Test extends \WP_Ajax_UnitTestCase {
+ /**
+ * Sets up the test fixture.
+ */
+ public function set_up() {
+ parent::set_up();
+
+ // The WP AJAX test case removes these before the class runs, but mixed
+ // test sequences can re-add core admin hooks before individual tests.
+ remove_action( 'admin_init', '_maybe_update_core' );
+ remove_action( 'admin_init', '_maybe_update_plugins' );
+ remove_action( 'admin_init', '_maybe_update_themes' );
+ }
+
/**
* Stock should not be reduced from AJAX when an item is added to an order.
*/
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-shipping-rounding-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-shipping-rounding-test.php
index 06eb6baa3fd..b47762379c0 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-shipping-rounding-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-shipping-rounding-test.php
@@ -41,30 +41,51 @@ class WC_Cart_Shipping_Rounding_Test extends WC_Unit_Test_Case {
private $flat_rate_id;
/**
- * Clean up after each test.
+ * Shipping enabled state before the test.
+ *
+ * @var bool
*/
- public function tearDown(): void {
- parent::tearDown();
- WC()->cart->empty_cart();
+ private $shipping_was_enabled;
- if ( $this->zone ) {
- $this->zone->delete();
- }
-
- if ( $this->flat_rate_id ) {
- delete_option( 'woocommerce_flat_rate_' . $this->flat_rate_id . '_settings' );
- }
+ /**
+ * Set up shipping state for the test.
+ */
+ public function setUp(): void {
+ parent::setUp();
- if ( $this->tax_rate_id ) {
- WC_Tax::_delete_tax_rate( $this->tax_rate_id );
- }
+ $this->shipping_was_enabled = WC()->shipping()->enabled;
+ WC()->shipping()->enabled = true;
+ }
- if ( $this->product ) {
- WC_Helper_Product::delete_product( $this->product->get_id() );
+ /**
+ * Clean up after each test.
+ */
+ public function tearDown(): void {
+ try {
+ WC()->cart->empty_cart();
+
+ if ( $this->zone ) {
+ $this->zone->delete();
+ }
+
+ if ( $this->flat_rate_id ) {
+ delete_option( 'woocommerce_flat_rate_' . $this->flat_rate_id . '_settings' );
+ }
+
+ if ( $this->tax_rate_id ) {
+ WC_Tax::_delete_tax_rate( $this->tax_rate_id );
+ }
+
+ if ( $this->product ) {
+ WC_Helper_Product::delete_product( $this->product->get_id() );
+ }
+
+ // Clear shipping caches.
+ WC_Cache_Helper::get_transient_version( 'shipping', true );
+ WC()->shipping()->enabled = $this->shipping_was_enabled;
+ } finally {
+ parent::tearDown();
}
-
- // Clear shipping caches.
- WC_Cache_Helper::get_transient_version( 'shipping', true );
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
index bda64ad40f1..783ce78c632 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
@@ -56,9 +56,20 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
WC()->cart->empty_cart();
WC()->session->set( 'wc_notices', null );
- $variable_product = WC_Helper_Product::create_variation_product();
+ $variable_product = new WC_Product_Variable();
+ $variable_product->set_name( 'Sold individually variable product' );
+ $variable_product->set_attributes(
+ array( WC_Helper_Product::create_product_attribute_object( 'size', array( 'small' ) ) )
+ );
$variable_product->set_sold_individually( true );
$variable_product->save();
+ WC_Helper_Product::create_product_variation_object(
+ $variable_product->get_id(),
+ 'SOLD INDIVIDUALLY VARIATION ' . microtime(),
+ 10,
+ array( 'pa_size' => 'small' )
+ );
+ $variable_product = new WC_Product_Variable( $variable_product->get_id() );
$variation_ids = $variable_product->get_children();
$this->assertNotEmpty( $variation_ids, 'Expected at least one variation.' );
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-totals-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-totals-test.php
index 0347b9617a1..27638079c3e 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-totals-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-totals-test.php
@@ -5,12 +5,30 @@
*/
class WC_Cart_Totals_Tests extends WC_Unit_Test_Case {
+ /**
+ * Shipping enabled state before the test.
+ *
+ * @var bool
+ */
+ private $shipping_was_enabled;
+
+ /**
+ * Set up non-shipping cart total tests.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->shipping_was_enabled = WC()->shipping()->enabled;
+ WC()->shipping()->enabled = false;
+ }
+
/**
* tearDown.
*/
public function tearDown(): void {
parent::tearDown();
WC()->cart->empty_cart();
+ WC()->shipping()->enabled = $this->shipping_was_enabled;
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php b/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
index cff3fc7ae0b..6fcfae352c8 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
@@ -21,6 +21,8 @@ class WC_Checkout_Test extends \WC_Unit_Test_Case {
* Runs before each test.
*/
public function setUp(): void {
+ parent::setUp();
+
// phpcs:disable Generic.CodeAnalysis, Squiz.Commenting
$this->sut = new class() extends WC_Checkout {
public function validate_posted_data( &$data, &$errors ) {
@@ -44,6 +46,8 @@ class WC_Checkout_Test extends \WC_Unit_Test_Case {
public function tearDown(): void {
remove_filter( 'woocommerce_checkout_registration_enabled', '__return_true' );
delete_option( 'woocommerce_calc_taxes' );
+
+ parent::tearDown();
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-importer-tracking-test.php b/plugins/woocommerce/tests/php/includes/class-wc-importer-tracking-test.php
index d443afb1cda..dca06944fd4 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-importer-tracking-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-importer-tracking-test.php
@@ -10,11 +10,13 @@ class WC_Importer_Tracking_Test extends \WC_Unit_Test_Case {
* @return void
*/
public function setUp(): void {
+ parent::setUp();
+ $this->clear_tracks_events();
+
include_once WC_ABSPATH . 'includes/tracks/events/class-wc-importer-tracking.php';
update_option( 'woocommerce_allow_tracking', 'yes' );
$importer_tracking = new WC_Importer_Tracking();
$importer_tracking->init();
- parent::setUp();
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-install-test.php b/plugins/woocommerce/tests/php/includes/class-wc-install-test.php
index 573143790c8..9af04e0da8d 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-install-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-install-test.php
@@ -281,18 +281,18 @@ class WC_Install_Test extends \WC_Unit_Test_Case {
* @param bool|null $auto_update Whether to enable auto-updates (TRUE) or not. NULL means use the defaults.
*/
public function test_db_auto_updates( ?bool $auto_update = null ): void {
- $options = array( 'woocommerce_db_version', 'woocommerce_version' );
+ $update_versions = array_keys( WC_Install::get_db_update_callbacks() );
+ $from_version = $update_versions[ count( $update_versions ) - 2 ];
+ $maybe_update_db = function () {
+ static::maybe_update_db_version();
+ };
if ( ! is_null( $auto_update ) ) {
add_filter( 'woocommerce_enable_auto_update_db', fn() => $auto_update );
}
- foreach ( $options as $option_name ) {
- update_option( $option_name, '9.4.0' );
- }
-
- // Trigger version check.
- \WC_Install::check_version();
+ update_option( 'woocommerce_db_version', $from_version );
+ $maybe_update_db->call( new WC_Install() );
// Did we schedule anything automatically?
$update_scheduled = ! is_null( WC()->queue()->get_next( 'woocommerce_run_update_callback', null, 'woocommerce-db-updates' ) );
@@ -304,6 +304,27 @@ class WC_Install_Test extends \WC_Unit_Test_Case {
}
}
+ /**
+ * Tests that the version check reaches the automatic database updater.
+ *
+ * This is a single end-to-end smoke test of the check_version() -> install() ->
+ * maybe_update_db_version() wiring; the auto-update on/off/default decision logic
+ * itself is covered for all variations by test_db_auto_updates() above.
+ *
+ * @testdox The version check schedules the automatic database update.
+ */
+ public function test_version_check_schedules_db_auto_update(): void {
+ $update_versions = array_keys( WC_Install::get_db_update_callbacks() );
+ $from_version = $update_versions[ count( $update_versions ) - 2 ];
+
+ update_option( 'woocommerce_db_version', $from_version );
+ update_option( 'woocommerce_version', $from_version );
+
+ WC_Install::check_version();
+
+ $this->assertNotNull( WC()->queue()->get_next( 'woocommerce_run_update_callback', null, 'woocommerce-db-updates' ) );
+ }
+
/**
* Ensures that the versions in `WC_Install::$db_update_callbacks` are correct.
*/
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-order-note-test.php b/plugins/woocommerce/tests/php/includes/class-wc-order-note-test.php
index c92bc4df7c5..67489b6fbf9 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-order-note-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-order-note-test.php
@@ -2,6 +2,8 @@
declare( strict_types = 1 );
use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
+use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
+use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* Class WC_Order_Note_Test.
@@ -10,6 +12,22 @@ class WC_Order_Note_Test extends \WC_Unit_Test_Case {
use HPOSToggleTrait;
+ /**
+ * Ensure permanent HPOS tables exist before per-test transactions start.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ $previous_hpos_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ try {
+ self::setup_cot_tables();
+ if ( OrderUtil::custom_orders_table_usage_is_enabled() !== $previous_hpos_state ) {
+ OrderHelper::toggle_cot_feature_and_usage( $previous_hpos_state );
+ }
+ } finally {
+ remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ }
+ }
+
/**
* @var bool Was HPOS enabled before the test?
*/
@@ -24,8 +42,10 @@ class WC_Order_Note_Test extends \WC_Unit_Test_Case {
parent::setUp();
add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
- $this->prev_hpos_enabled = \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
- $this->setup_cot();
+ $this->prev_hpos_enabled = OrderUtil::custom_orders_table_usage_is_enabled();
+ remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
+ remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
+ $this->toggle_cot_feature_and_usage( true );
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-orders-tracking-test.php b/plugins/woocommerce/tests/php/includes/class-wc-orders-tracking-test.php
index 0eb5c8810b1..faf114472b8 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-orders-tracking-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-orders-tracking-test.php
@@ -22,12 +22,24 @@ class WC_Orders_Tracking_Test extends \WC_Unit_Test_Case {
*/
private $prev_hpos_enabled;
+ /**
+ * Ensure permanent HPOS tables exist before per-test transactions start.
+ *
+ * @param \WP_UnitTest_Factory $factory WordPress unit test factory.
+ */
+ public static function wpSetUpBeforeClass( $factory ): void {
+ self::setup_cot_tables();
+ }
+
/**
* Set up test
*
* @return void
*/
public function setUp(): void {
+ parent::setUp();
+ $this->clear_tracks_events();
+
include_once WC_ABSPATH . 'includes/tracks/events/class-wc-orders-tracking.php';
update_option( 'woocommerce_allow_tracking', 'yes' );
@@ -40,11 +52,14 @@ class WC_Orders_Tracking_Test extends \WC_Unit_Test_Case {
$orders_tracking = new WC_Orders_Tracking();
$orders_tracking->init();
- parent::setUp();
add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
$this->prev_hpos_enabled = \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
- $this->setup_cot();
+
+ // Keep the permanent HPOS tables outside the per-test transaction.
+ remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
+ remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
+ $this->toggle_cot_feature_and_usage( true );
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-payment-gateways-test.php b/plugins/woocommerce/tests/php/includes/class-wc-payment-gateways-test.php
index 274f5620fdb..fe87fb9f3d9 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-payment-gateways-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-payment-gateways-test.php
@@ -27,7 +27,6 @@ class WC_Payment_Gateways_Test extends WC_Unit_Test_Case {
$container = wc_get_container();
$container->reset_all_resolved();
$this->sut = new WC_Payment_Gateways();
- $this->sut->init();
}
/**
@@ -42,6 +41,13 @@ class WC_Payment_Gateways_Test extends WC_Unit_Test_Case {
* @testdox Enabling a gateway fires the notification action and logs the event.
*/
public function test_wc_payment_gateway_enabled_notification(): void {
+ $gateways = $this->sut->payment_gateways();
+ $providers_service = $this->createMock( \Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders::class );
+ $providers_service->expects( $this->exactly( count( $gateways ) ) )
+ ->method( 'get_payment_gateway_details' )
+ ->willReturn( array() );
+ wc_get_container()->replace( \Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders::class, $providers_service );
+
// phpcs:disable Squiz.Commenting
$fake_logger = new class() {
public $infos = array();
@@ -68,26 +74,32 @@ class WC_Payment_Gateways_Test extends WC_Unit_Test_Case {
};
add_action( 'woocommerce_payment_gateway_enabled', $action_watcher );
- foreach ( $this->sut->payment_gateways() as $gateway ) {
- $gateway->settings['enabled'] = 'no';
- $gateway->settings['title'] = null;
- update_option( $gateway->get_option_key(), $gateway->settings );
-
- $gateway->settings['enabled'] = 'yes';
- update_option( $gateway->get_option_key(), $gateway->settings );
-
- $this->assertEquals(
- 'Payment gateway enabled: "' . $gateway->get_method_title() . '"',
- end( $fake_logger->infos )['message'],
- 'Logger should record the gateway enable event'
- );
-
- $last_fired = end( $action_fired );
- $this->assertInstanceOf( WC_Payment_Gateway::class, $last_fired, 'Action should fire with a gateway object' );
- $this->assertEquals( $gateway->id, $last_fired->id, 'Action should fire with the correct gateway' );
- }
+ try {
+ foreach ( $gateways as $gateway ) {
+ $gateway->settings['enabled'] = 'no';
+ $gateway->settings['title'] = null;
+ update_option( $gateway->get_option_key(), $gateway->settings );
- remove_action( 'woocommerce_payment_gateway_enabled', $action_watcher );
+ $gateway->settings['enabled'] = 'yes';
+ update_option( $gateway->get_option_key(), $gateway->settings );
+
+ $this->assertEquals(
+ 'Payment gateway enabled: "' . $gateway->get_method_title() . '"',
+ end( $fake_logger->infos )['message'],
+ 'Logger should record the gateway enable event'
+ );
+
+ $last_fired = end( $action_fired );
+ $this->assertInstanceOf( WC_Payment_Gateway::class, $last_fired, 'Action should fire with a gateway object' );
+ $this->assertEquals( $gateway->id, $last_fired->id, 'Action should fire with the correct gateway' );
+ }
+
+ $this->assertCount( count( $gateways ), $fake_logger->infos, 'Logger should run once per enabled gateway' );
+ $this->assertCount( count( $gateways ), $action_fired, 'Action should fire once per enabled gateway' );
+ } finally {
+ remove_action( 'woocommerce_payment_gateway_enabled', $action_watcher );
+ wc_get_container()->reset_replacement( \Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders::class );
+ }
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php b/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php
index 03464f72bb9..2c3727508b1 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php
@@ -1,6 +1,7 @@
<?php
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
+use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;
/**
* Class WC_Product_Download_Test
*/
@@ -9,31 +10,37 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case {
* Test for file without extension.
*/
public function test_is_allowed_filetype_with_no_extension() {
+ $filesystem = FilesystemUtil::get_wp_filesystem();
$upload_dir = trailingslashit( wp_upload_dir()['basedir'] );
- $file_path_with_no_extension = $upload_dir . 'upload_file';
- if ( ! file_exists( $file_path_with_no_extension ) ) {
- // Copy an existing file without extension.
- $this->assertTrue( touch( $file_path_with_no_extension ), 'Unable to create file without extension.' );
+ $file_path_with_no_extension = $upload_dir . 'upload_file-' . wp_generate_uuid4();
+ $this->assertTrue( $filesystem->touch( $file_path_with_no_extension ), 'Unable to create file without extension.' );
+
+ try {
+ $download = new WC_Product_Download();
+ $download->set_file( $file_path_with_no_extension );
+ $this->assertEquals( true, $download->is_allowed_filetype() );
+ } finally {
+ $filesystem->delete( $file_path_with_no_extension );
}
- $download = new WC_Product_Download();
- $download->set_file( $file_path_with_no_extension );
- $this->assertEquals( true, $download->is_allowed_filetype() );
}
/**
* Simulates test condition for windows when filename ends with a period.
*/
public function test_is_allowed_filetype_on_windows_with_period_at_end() {
+ $filesystem = FilesystemUtil::get_wp_filesystem();
$upload_dir = trailingslashit( wp_upload_dir()['basedir'] );
- $file_path_with_period_at_end = $upload_dir . 'upload_file.';
- if ( ! file_exists( $file_path_with_period_at_end ) ) {
- // Copy an existing file without extension.
- $this->assertTrue( touch( $file_path_with_period_at_end ), 'Unable to create file with period at the end.' );
+ $file_path_with_period_at_end = $upload_dir . 'upload_file-' . wp_generate_uuid4() . '.';
+ $this->assertTrue( $filesystem->touch( $file_path_with_period_at_end ), 'Unable to create file with period at the end.' );
+
+ try {
+ \Automattic\Jetpack\Constants::set_constant( 'PHP_OS', 'winnt' );
+ $download = new WC_Product_Download();
+ $download->set_file( $file_path_with_period_at_end );
+ $this->assertEquals( false, $download->is_allowed_filetype() );
+ } finally {
+ $filesystem->delete( $file_path_with_period_at_end );
}
- \Automattic\Jetpack\Constants::set_constant( 'PHP_OS', 'winnt' );
- $download = new WC_Product_Download();
- $download->set_file( $file_path_with_period_at_end );
- $this->assertEquals( false, $download->is_allowed_filetype() );
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-products-tracking-test.php b/plugins/woocommerce/tests/php/includes/class-wc-products-tracking-test.php
index 2574b60fb12..2d702434873 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-products-tracking-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-products-tracking-test.php
@@ -12,11 +12,13 @@ class WC_Products_Tracking_Test extends \WC_Unit_Test_Case {
* @return void
*/
public function setUp(): void {
+ parent::setUp();
+ $this->clear_tracks_events();
+
include_once WC_ABSPATH . 'includes/tracks/events/class-wc-products-tracking.php';
update_option( 'woocommerce_allow_tracking', 'yes' );
$products_tracking = new WC_Products_Tracking();
$products_tracking->init();
- parent::setUp();
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-settings-tracking-test.php b/plugins/woocommerce/tests/php/includes/class-wc-settings-tracking-test.php
index 398e1213766..f41e0a50719 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-settings-tracking-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-settings-tracking-test.php
@@ -10,11 +10,13 @@ class WC_Settings_Tracking_Test extends \WC_Unit_Test_Case {
* @return void
*/
public function setUp(): void {
+ parent::setUp();
+ $this->clear_tracks_events();
+
include_once WC_ABSPATH . 'includes/tracks/events/class-wc-settings-tracking.php';
update_option( 'woocommerce_allow_tracking', 'yes' );
$settings_tracking = new WC_Settings_Tracking();
$settings_tracking->init();
- parent::setUp();
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-tracker-test.php b/plugins/woocommerce/tests/php/includes/class-wc-tracker-test.php
index 7d1775325b9..d9213e4e30f 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-tracker-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-tracker-test.php
@@ -7,8 +7,11 @@
declare(strict_types=1);
+use Automattic\WooCommerce\Caches\OrderCountCache;
use Automattic\WooCommerce\Enums\OrderInternalStatus;
+use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
+use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Utilities\PluginUtil;
// phpcs:disable Squiz.Classes.ClassFileName.NoMatch, Squiz.Classes.ValidClassName.NotCamelCaps -- Backward compatibility.
@@ -184,29 +187,11 @@ class WC_Tracker_Test extends \WC_Unit_Test_Case {
* @testDox Test orders tracking data.
*/
public function test_get_tracking_data_orders() {
- $dummy_product = WC_Helper_Product::create_simple_product();
$status_entries = array( OrderInternalStatus::PROCESSING, OrderInternalStatus::COMPLETED, OrderInternalStatus::REFUNDED, OrderInternalStatus::PENDING );
$created_via_entries = array( 'api', 'checkout', 'admin' );
$payment_method_entries = array( WC_Gateway_Paypal::ID, 'stripe', WC_Gateway_COD::ID );
- $order_count = count( $status_entries ) * count( $created_via_entries ) * count( $payment_method_entries );
-
- foreach ( $status_entries as $status_entry ) {
- foreach ( $created_via_entries as $created_via_entry ) {
- foreach ( $payment_method_entries as $payment_method_entry ) {
- $order = wc_create_order(
- array(
- 'status' => $status_entry,
- 'created_via' => $created_via_entry,
- 'payment_method' => $payment_method_entry,
- )
- );
- $order->add_product( $dummy_product );
- $order->save();
- $order->calculate_totals();
- }
- }
- }
+ $order_count = $this->create_tracking_orders( $status_entries, $created_via_entries, $payment_method_entries );
$order_data = WC_Tracker::get_tracking_data()['orders'];
@@ -217,28 +202,128 @@ class WC_Tracker_Test extends \WC_Unit_Test_Case {
// Gross revenue is for wc-completed and wc-refunded status, so we calculate expected revenue per status, multiply by 2, and then multiply by 10 to account for the 10 USD per status.
$this->assertEquals( ( $order_count / count( $status_entries ) ) * 2 * 10, $order_data['gross'] );
- // Gross revenue is for wc-pending status, so we calculate expected revenue per status, multiply by 1, and then multiply by 10 to account for the 10 USD per status.
+ // Processing gross revenue covers one status, so multiply the orders per status by the fixed 10 USD total.
$this->assertEquals( ( $order_count / count( $status_entries ) ) * 1 * 10, $order_data['processing_gross'] );
- // Order count per gateway is calculated for three status (completed, processing and refunded) so we multiply order count by 3 and then divide by the number of status entries.
- $this->assertEquals( ( $order_count * 3 / count( $status_entries ) ), $order_data['gateway__USD_count'] );
-
- // Order revenue per gateway is calculated for three status (completed, processing and refunded) so we multiply order count by 3, then by 10 to account for 10 USD per order and then divide by the number of status entries.
- $this->assertEquals( ( $order_count * 3 * 10 / count( $status_entries ) ), $order_data['gateway__USD_total'] );
+ $orders_per_gateway = count( $created_via_entries ) * 3;
+ foreach ( $payment_method_entries as $payment_method_entry ) {
+ $gateway_key = 'gateway_' . $payment_method_entry . '_USD';
+ $this->assertEquals( $orders_per_gateway, $order_data[ $gateway_key . '_count' ] );
+ $this->assertEquals( $orders_per_gateway * 10, $order_data[ $gateway_key . '_total' ] );
+ }
foreach ( $created_via_entries as $created_via_entry ) {
$this->assertEquals( ( $order_count / count( $created_via_entries ) ), $order_data['created_via'][ $created_via_entry ] );
}
}
+ /**
+ * Persist the order matrix read by the tracker aggregate queries.
+ *
+ * @param string[] $statuses Order statuses.
+ * @param string[] $created_via Order origins.
+ * @param string[] $payment_methods Payment methods.
+ * @return int Number of inserted orders.
+ */
+ private function create_tracking_orders( array $statuses, array $created_via, array $payment_methods ): int {
+ if ( ! OrderUtil::custom_orders_table_usage_is_enabled() ) {
+ $order_count = 0;
+ foreach ( $statuses as $status ) {
+ foreach ( $created_via as $origin ) {
+ foreach ( $payment_methods as $payment_method ) {
+ $order = wc_create_order(
+ array(
+ 'status' => $status,
+ 'created_via' => $origin,
+ )
+ );
+ $order->set_payment_method( $payment_method );
+ $order->set_total( 10 );
+ $order->save();
+ ++$order_count;
+ }
+ }
+ }
+
+ return $order_count;
+ }
+
+ $order_date = gmdate( 'Y-m-d H:i:s' );
+ $orders = array();
+
+ foreach ( $statuses as $status ) {
+ foreach ( $created_via as $origin ) {
+ foreach ( $payment_methods as $payment_method ) {
+ $orders[] = array(
+ 'status' => $status,
+ 'date' => $order_date,
+ 'payment_method' => $payment_method,
+ 'created_via' => $origin,
+ 'recorded_sales' => 0,
+ );
+ }
+ }
+ }
+
+ return $this->insert_hpos_tracking_orders( $orders );
+ }
+
+ /**
+ * Insert minimal HPOS rows consumed by tracker queries.
+ *
+ * @param array[] $orders Order persistence data.
+ * @return int Number of inserted orders.
+ */
+ private function insert_hpos_tracking_orders( array $orders ): int {
+ global $wpdb;
+
+ $next_order_id = (int) $wpdb->get_var( "SELECT GREATEST(COALESCE((SELECT MAX(id) FROM {$wpdb->prefix}wc_orders), 0), COALESCE((SELECT MAX(ID) FROM {$wpdb->posts}), 0)) + 1" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names are provided by WordPress.
+ $order_rows = array();
+ $order_values = array();
+ $detail_rows = array();
+ $detail_values = array();
+
+ foreach ( $orders as $order ) {
+ $order_rows[] = '(%d, %s, %s, %s, %f, %s, %s, %s)';
+ array_push( $order_values, $next_order_id, $order['status'], 'USD', 'shop_order', 10, $order['date'], $order['date'], $order['payment_method'] );
+
+ $detail_rows[] = '(%d, %s, %s, %d)';
+ array_push( $detail_values, $next_order_id, $order['created_via'], WOOCOMMERCE_VERSION, $order['recorded_sales'] );
+
+ ++$next_order_id;
+ }
+
+ $order_table = OrdersTableDataStore::get_orders_table_name();
+ $order_columns = 'id, status, currency, type, total_amount, date_created_gmt, date_updated_gmt, payment_method';
+ $detail_table = OrdersTableDataStore::get_operational_data_table_name();
+ $detail_columns = 'order_id, created_via, woocommerce_version, recorded_sales';
+
+ $order_query = $wpdb->prepare(
+ "INSERT INTO {$order_table} ({$order_columns}) VALUES " . implode( ', ', $order_rows ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.NotPrepared -- Table and columns are selected above; placeholders are generated above.
+ $order_values
+ );
+ $order_rows_inserted = $wpdb->query( $order_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared immediately above.
+ $this->assertSame( count( $orders ), $order_rows_inserted, 'Expected every tracker order row to be inserted.' );
+
+ $detail_query = $wpdb->prepare(
+ "INSERT INTO {$detail_table} ({$detail_columns}) VALUES " . implode( ', ', $detail_rows ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.NotPrepared -- Table and columns are selected above; placeholders are generated above.
+ $detail_values
+ );
+ $detail_rows_inserted = $wpdb->query( $detail_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared immediately above.
+ $this->assertSame( count( $orders ), $detail_rows_inserted, 'Expected operational data for every tracker order row.' );
+
+ ( new OrderCountCache() )->flush( 'shop_order', array_keys( wc_get_order_statuses() ) );
+
+ return count( $orders );
+ }
+
/**
* @testDox Test order snapshot data.
*/
public function test_get_tracking_data_order_snapshot() {
- $dummy_product = WC_Helper_Product::create_simple_product();
- $year = gmdate( 'Y' );
- $first_20 = array();
- $last_20 = array();
+ $year = gmdate( 'Y' );
+ $first_20 = array();
+ $last_20 = array();
// Populate order dates.
for ( $i = 1; $i <= 20; $i++ ) {
@@ -246,18 +331,7 @@ class WC_Tracker_Test extends \WC_Unit_Test_Case {
$last_20[] = sprintf( '%d-02-%02d 12:00:00', $year + 2, $i );
}
- // Create set of orders.
- foreach ( array_merge( $first_20, $last_20 ) as $order_date ) {
- $order = wc_create_order(
- array(
- 'status' => OrderInternalStatus::COMPLETED,
- )
- );
- $order->add_product( $dummy_product );
- $order->set_date_created( $order_date );
- $order->save();
- $order->calculate_totals();
- }
+ $this->create_tracking_snapshot_orders( array_merge( $first_20, $last_20 ) );
$order_snapshot = WC_Tracker::get_tracking_data()['order_snapshot'];
@@ -285,6 +359,39 @@ class WC_Tracker_Test extends \WC_Unit_Test_Case {
}
}
+ /**
+ * Persist orders read by the first/last order snapshot queries.
+ *
+ * @param string[] $order_dates Order creation dates.
+ */
+ private function create_tracking_snapshot_orders( array $order_dates ): void {
+ if ( ! OrderUtil::custom_orders_table_usage_is_enabled() ) {
+ foreach ( $order_dates as $order_date ) {
+ $order = wc_create_order(
+ array(
+ 'status' => OrderInternalStatus::COMPLETED,
+ )
+ );
+ $order->set_date_created( $order_date );
+ $order->set_total( 10 );
+ $order->save();
+ }
+ return;
+ }
+
+ $orders = array_map(
+ static fn( $order_date ) => array(
+ 'status' => OrderInternalStatus::COMPLETED,
+ 'date' => $order_date,
+ 'payment_method' => '',
+ 'created_via' => 'admin',
+ 'recorded_sales' => 1,
+ ),
+ $order_dates
+ );
+ $this->insert_hpos_tracking_orders( $orders );
+ }
+
/**
* @testDox Test enabled features tracking data.
*/
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-test.php
index 040b0d41a83..0261cf4ca50 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-test.php
@@ -14,6 +14,16 @@ use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
*/
class WC_Customer_Data_Store_CPT_Test extends WC_Unit_Test_Case {
+ /**
+ * Runs before all tests in the class.
+ */
+ public static function setUpBeforeClass(): void {
+ parent::setUpBeforeClass();
+
+ OrderHelper::delete_order_custom_tables();
+ OrderHelper::create_order_custom_table_if_not_exist();
+ }
+
/**
* Runs before each test.
*/
@@ -21,23 +31,14 @@ class WC_Customer_Data_Store_CPT_Test extends WC_Unit_Test_Case {
parent::setUp();
add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
-
- // Remove the Test Suite’s use of temporary tables https://wordpress.stackexchange.com/a/220308.
- remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
- remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
- OrderHelper::delete_order_custom_tables();
- OrderHelper::create_order_custom_table_if_not_exist();
}
/**
- * Destroys system under test.
+ * Runs after each test.
*/
public function tearDown(): void {
- remove_all_filters( 'wc_allow_changing_orders_storage_while_sync_is_pending' );
+ remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
- // Add back removed filter.
- add_filter( 'query', array( $this, '_create_temporary_tables' ) );
- add_filter( 'query', array( $this, '_drop_temporary_tables' ) );
parent::tearDown();
}
@@ -171,7 +172,7 @@ class WC_Customer_Data_Store_CPT_Test extends WC_Unit_Test_Case {
$base_id + 5,
$customer_2_id
);
- $wpdb->query( $query );
+ $this->assertSame( 5, $wpdb->query( $query ), 'All custom order table fixtures should be inserted.' );
//phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
$sut = new WC_Customer_Data_Store();
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php
index 8c8f6a3d3f0..63e6902cfd2 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php
@@ -15,11 +15,26 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
use CogsAwareUnitTestSuiteTrait;
/**
- * Store the COT state before the test.
+ * Store the COT state before the test class.
*
* @var bool
*/
- private $prev_cot_state;
+ private static $previous_cot_state;
+
+ /**
+ * Use the CPT order data store for every test in this class.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ self::$previous_cot_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ OrderHelper::toggle_cot_feature_and_usage( false );
+ }
+
+ /**
+ * Restore the order data store used before this class.
+ */
+ public static function wpTearDownAfterClass(): void {
+ OrderHelper::toggle_cot_feature_and_usage( self::$previous_cot_state );
+ }
/**
* Store the COT state before the test.
@@ -28,8 +43,6 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
*/
public function setUp(): void {
parent::setUp();
- $this->prev_cot_state = OrderUtil::custom_orders_table_usage_is_enabled();
- OrderHelper::toggle_cot_feature_and_usage( false );
add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
}
@@ -39,7 +52,6 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
* @return void
*/
public function tearDown(): void {
- OrderHelper::toggle_cot_feature_and_usage( $this->prev_cot_state );
remove_all_filters( 'wc_allow_changing_orders_storage_while_sync_is_pending' );
$this->disable_cogs_feature();
parent::tearDown();
@@ -705,7 +717,7 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
public function test_total_filtering_with_operators() {
$order_totals_to_test = array( 5, 10, 50, 100.00, 100.00, 250.50, 250.50, 500.75, 1000.00 );
foreach ( $order_totals_to_test as $order_total ) {
- $order = OrderHelper::create_order();
+ $order = wc_create_order();
$order->set_total( $order_total );
$order->save();
}
diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variable-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variable-data-store-cpt-test.php
index ca578e846ac..6219e93abe8 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variable-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-variable-data-store-cpt-test.php
@@ -4,6 +4,91 @@
* Class WC_Product_Variable_Data_Store_CPT_Test
*/
class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
+ /**
+ * Variable product shared by the class.
+ *
+ * @var int
+ */
+ private static $product_id;
+
+ /**
+ * Attribute taxonomy IDs owned by the class fixture.
+ *
+ * @var int[]
+ */
+ private static $attribute_ids = array();
+
+ /**
+ * Whether an attribute deletion rewrite flush was already scheduled.
+ *
+ * @var bool
+ */
+ private static $had_scheduled_rewrite_flush;
+
+ /**
+ * Create the variable product fixture shared by all test methods.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ self::enable_direct_product_attribute_lookup_updates();
+
+ try {
+ $existing_attribute_ids = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_id' );
+ $product = WC_Helper_Product::create_variation_product();
+ self::$product_id = $product->get_id();
+ self::$attribute_ids = array_values( array_diff( wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_id' ), $existing_attribute_ids ) );
+ self::$had_scheduled_rewrite_flush = false !== wp_next_scheduled( 'woocommerce_flush_rewrite_rules' );
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
+ }
+ }
+
+ /**
+ * Delete the class-owned variable product through its data store.
+ */
+ public static function wpTearDownAfterClass(): void {
+ global $wc_product_attributes;
+
+ self::enable_direct_product_attribute_lookup_updates();
+
+ try {
+ $product = wc_get_product( self::$product_id );
+ if ( $product ) {
+ $product->delete( true );
+ }
+
+ foreach ( self::$attribute_ids as $attribute_id ) {
+ $attribute = wc_get_attribute( $attribute_id );
+ $taxonomy = $attribute ? $attribute->slug : '';
+
+ wc_delete_attribute( $attribute_id );
+
+ if ( $taxonomy && taxonomy_exists( $taxonomy ) ) {
+ unregister_taxonomy( $taxonomy );
+ }
+ unset( $wc_product_attributes[ $taxonomy ] );
+ }
+
+ if ( ! self::$had_scheduled_rewrite_flush ) {
+ wp_clear_scheduled_hook( 'woocommerce_flush_rewrite_rules' );
+ }
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
+ }
+ }
+
+ /**
+ * Reload the class-owned variable product for the current transaction.
+ *
+ * @return WC_Product_Variable
+ */
+ private function get_variation_product_fixture(): WC_Product_Variable {
+ $product = wc_get_product( self::$product_id );
+ if ( ! $product instanceof WC_Product_Variable ) {
+ throw new RuntimeException( 'Unable to load the variable product fixture.' );
+ }
+
+ return $product;
+ }
/**
* Helper filter to force prices inclusive of tax.
@@ -358,7 +443,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
);
// Create our variable product.
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->get_variation_product_fixture();
// Verify that a VAT exempt customer gets prices with tax removed.
WC()->customer->set_is_vat_exempt( true );
@@ -391,7 +476,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
*/
public function test_read_children() {
$data_store = new WC_Product_Variable_Data_Store_CPT();
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->get_variation_product_fixture();
// Set invalid transient data.
$invalid_data = 'not an array';
@@ -422,7 +507,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
*/
public function test_read_price_data() {
$data_store = new WC_Product_Variable_Data_Store_CPT();
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->get_variation_product_fixture();
// Get initial valid price data.
$initial_prices = $data_store->read_price_data( $product, false );
@@ -481,7 +566,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
WC()->customer->set_is_vat_exempt( $user_vat_exempt );
$data_store = new WC_Product_Variable_Data_Store_CPT();
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->get_variation_product_fixture();
$transient_name = 'wc_var_prices_' . $product->get_id();
delete_transient( $transient_name );
@@ -519,7 +604,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
add_filter( 'woocommerce_product_is_taxable', '__return_true' );
add_filter( 'woocommerce_matched_rates', array( $this, '__return_rates' ) );
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->get_variation_product_fixture();
$extended_data_store = $this->get_data_store_with_public_taxes_influence_price();
@@ -570,7 +655,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
};
add_filter( 'woocommerce_variable_product_taxes_influence_price', $filter_callback, 10, 2 );
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->get_variation_product_fixture();
$extended_data_store = $this->get_data_store_with_public_taxes_influence_price();
$this->assertSame( $expected, $extended_data_store->taxes_influence_price( $product ) );
@@ -598,7 +683,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
WC()->customer->set_is_vat_exempt( false );
$data_store = new WC_Product_Variable_Data_Store_CPT();
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->get_variation_product_fixture();
$transient_name = 'wc_var_prices_' . $product->get_id();
delete_transient( $transient_name );
@@ -643,7 +728,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
);
$data_store = new WC_Product_Variable_Data_Store_CPT();
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->get_variation_product_fixture();
$transient_name = 'wc_var_prices_' . $product->get_id();
delete_transient( $transient_name );
@@ -722,7 +807,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
*/
public function test_read_price_data_with_validation_failure() {
$data_store = new WC_Product_Variable_Data_Store_CPT();
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->get_variation_product_fixture();
// Get initial valid price data.
$initial_prices = $data_store->read_price_data( $product, false );
@@ -763,7 +848,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
*/
public function test_read_children_with_validation_failure() {
$data_store = new WC_Product_Variable_Data_Store_CPT();
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->get_variation_product_fixture();
// Get initial valid children data.
$initial_children = $data_store->read_children( $product, false );
@@ -800,7 +885,7 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
* Tests `read_attributes` for handling metas migration due to sanitize_title BC breaks.
*/
public function test_read_attributes_addresses_bc_break_in_sanitize(): void {
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->get_variation_product_fixture();
$product_id = $product->get_id();
$child_ids = array_values( $product->get_children() );
@@ -824,14 +909,13 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
$this->assertSame( $sizes[ $index ], get_post_meta( $child_id, 'attribute_size-size', true ) );
$this->assertSame( $sizes[ $index ], get_post_meta( $child_id, 'attribute_Size/Size', true ) );
}
- $product->delete();
}
/**
* @testdox read_product_data does not record variable product transient names in the notoptions cache when a persistent object cache is in use.
*/
public function test_read_product_data_does_not_prime_transients_with_object_cache() {
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->get_variation_product_fixture();
$product_id = $product->get_id();
$option_names = array(
@@ -868,7 +952,5 @@ class WC_Product_Variable_Data_Store_CPT_Test extends WC_Unit_Test_Case {
'Variable product transient option names must not be added to notoptions when a persistent object cache is active.'
);
}
-
- $product->delete();
}
}
diff --git a/plugins/woocommerce/tests/php/includes/emails/class-wc-email-customer-abandoned-cart-recovery-test.php b/plugins/woocommerce/tests/php/includes/emails/class-wc-email-customer-abandoned-cart-recovery-test.php
index 9f4829bcaf2..0eb82a2e8a8 100644
--- a/plugins/woocommerce/tests/php/includes/emails/class-wc-email-customer-abandoned-cart-recovery-test.php
+++ b/plugins/woocommerce/tests/php/includes/emails/class-wc-email-customer-abandoned-cart-recovery-test.php
@@ -51,8 +51,6 @@ class WC_Email_Customer_Abandoned_Cart_Recovery_Test extends \WC_Unit_Test_Case
require_once $bootstrap->plugin_dir . '/includes/emails/class-wc-email.php';
require_once $bootstrap->plugin_dir . '/includes/emails/class-wc-email-customer-abandoned-cart-recovery.php';
- WC()->mailer()->init();
-
$this->sut = new WC_Email_Customer_Abandoned_Cart_Recovery();
}
@@ -207,6 +205,7 @@ class WC_Email_Customer_Abandoned_Cart_Recovery_Test extends \WC_Unit_Test_Case
* @testdox Email is registered with WC_Emails when the feature flag is on so the WC Settings → Emails page renders it.
*/
public function test_is_registered_with_wc_emails(): void {
+ WC()->mailer()->init();
$emails = WC()->mailer()->get_emails();
$this->assertArrayHasKey( 'WC_Email_Customer_Abandoned_Cart_Recovery', $emails );
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-products-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-products-controller-tests.php
index bf072c76013..823574633c4 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-products-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version2/class-wc-rest-products-controller-tests.php
@@ -21,6 +21,7 @@ class WC_REST_Products_V2_Controller_Test extends WC_REST_Unit_Test_Case {
* @return void
*/
public static function wpSetUpBeforeClass() {
+ self::enable_direct_product_attribute_lookup_updates();
for ( $i = 1; $i <= 4; $i++ ) {
self::$products[] = WC_Helper_Product::create_simple_product();
}
@@ -30,6 +31,7 @@ class WC_REST_Products_V2_Controller_Test extends WC_REST_Unit_Test_Case {
$product->add_meta_data( 'test2', 'test2', true );
$product->save();
}
+ self::disable_direct_product_attribute_lookup_updates();
}
/**
@@ -38,9 +40,11 @@ class WC_REST_Products_V2_Controller_Test extends WC_REST_Unit_Test_Case {
* @return void
*/
public static function wpTearDownAfterClass() {
+ self::enable_direct_product_attribute_lookup_updates();
foreach ( self::$products as $product ) {
WC_Helper_Product::delete_product( $product->get_id() );
}
+ self::disable_direct_product_attribute_lookup_updates();
}
/**
@@ -407,9 +411,13 @@ class WC_REST_Products_V2_Controller_Test extends WC_REST_Unit_Test_Case {
);
foreach ( $skus_and_names as $sku => $name ) {
- $product = WC_Helper_Product::create_simple_product();
- $product->set_name( $name );
- $product->set_sku( $sku );
+ $product = WC_Helper_Product::create_simple_product(
+ false,
+ array(
+ 'name' => $name,
+ 'sku' => $sku,
+ )
+ );
$product->save();
}
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller-tests.php
index ccb31880ea0..18f63082d49 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller-tests.php
@@ -28,16 +28,32 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
private $cot_state;
/**
- * Setup our test server, endpoints, and user info.
+ * Administrator ID used to authenticate requests.
+ *
+ * @var int
*/
- public function setUp(): void {
- parent::setUp();
- $this->endpoint = new WC_REST_Orders_Controller();
- $this->user = $this->factory->user->create(
+ protected static $administrator_id;
+
+ /**
+ * Create immutable class fixtures.
+ *
+ * @param WP_UnitTest_Factory $factory WordPress unit test factory.
+ */
+ public static function wpSetUpBeforeClass( $factory ): void {
+ self::$administrator_id = $factory->user->create(
array(
'role' => 'administrator',
)
);
+ }
+
+ /**
+ * Setup our test server, endpoints, and user info.
+ */
+ public function setUp(): void {
+ parent::setUp();
+ $this->endpoint = new WC_REST_Orders_Controller();
+ $this->user = self::$administrator_id;
wp_set_current_user( $this->user );
add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
@@ -380,10 +396,10 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
$this->toggle_cot_feature_and_usage( true );
// A refund (type 'shop_order_refund') is used because it's a real in-core order type that shares the same table as orders.
- $order = OrderHelper::create_order();
+ $order = wc_create_order();
$refund = wc_create_refund(
array(
- 'amount' => 10,
+ 'amount' => 0,
'order_id' => $order->get_id(),
)
);
@@ -413,7 +429,7 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
$this->toggle_cot_feature_and_usage( true );
wc_register_order_type( 'shop_test' );
- $order = OrderHelper::create_order();
+ $order = wc_create_order();
$this->assertSame(
1,
$wpdb->update(
@@ -718,11 +734,11 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
public function test_created_via_param_is_filters_order_when_cot_is_enabled() {
update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'yes' );
- $order_checkout = WC_Helper_Order::create_order();
+ $order_checkout = wc_create_order();
$order_checkout->set_created_via( 'checkout' );
$order_checkout->save();
- $order_admin = WC_Helper_Order::create_order();
+ $order_admin = wc_create_order();
$order_admin->set_created_via( 'admin' );
$order_admin->save();
@@ -744,7 +760,7 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
public function test_get_orders_by_invalid_created_via_when_cot_is_enabled() {
update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'yes' );
- $order_checkout = WC_Helper_Order::create_order();
+ $order_checkout = wc_create_order();
$order_checkout->set_created_via( 'checkout' );
$order_checkout->save();
@@ -764,11 +780,11 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
public function test_created_via_param_is_filters_order_when_cot_is_disabled() {
update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
- $order_checkout = WC_Helper_Order::create_order();
+ $order_checkout = wc_create_order();
$order_checkout->set_created_via( 'checkout' );
$order_checkout->save();
- $order_admin = WC_Helper_Order::create_order();
+ $order_admin = wc_create_order();
$order_admin->set_created_via( 'admin' );
$order_admin->save();
@@ -790,7 +806,7 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
public function test_get_orders_by_invalid_created_via_when_cot_is_disabled() {
update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
- $order_checkout = WC_Helper_Order::create_order();
+ $order_checkout = wc_create_order();
$order_checkout->set_created_via( 'checkout' );
$order_checkout->save();
@@ -1012,7 +1028,7 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
* @testdox Updating an order with incomplete meta_data entries does not cause errors.
*/
public function test_update_meta_data_with_incomplete_entries(): void {
- $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order( $this->user );
+ $order = wc_create_order();
$request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() );
$request->set_header( 'content-type', 'application/json' );
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-custom-fields-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-custom-fields-controller-tests.php
index c6641c6d8a1..b7d845265a3 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-custom-fields-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-custom-fields-controller-tests.php
@@ -16,6 +16,7 @@ class WC_REST_Product_Custom_Fields_Controller_Tests extends WC_REST_Unit_Test_C
* @return void
*/
public static function wpSetUpBeforeClass() {
+ self::enable_direct_product_attribute_lookup_updates();
for ( $i = 1; $i <= 4; $i ++ ) {
self::$products[] = WC_Helper_Product::create_simple_product();
}
@@ -26,6 +27,7 @@ class WC_REST_Product_Custom_Fields_Controller_Tests extends WC_REST_Unit_Test_C
}
$product->save();
}
+ self::disable_direct_product_attribute_lookup_updates();
}
/**
@@ -34,9 +36,11 @@ class WC_REST_Product_Custom_Fields_Controller_Tests extends WC_REST_Unit_Test_C
* @return void
*/
public static function wpTearDownAfterClass() {
+ self::enable_direct_product_attribute_lookup_updates();
foreach ( self::$products as $product ) {
WC_Helper_Product::delete_product( $product->get_id() );
}
+ self::disable_direct_product_attribute_lookup_updates();
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller-tests.php
index 5a7bf1dce1a..d0a3cc4189d 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller-tests.php
@@ -7,17 +7,42 @@ use Automattic\WooCommerce\Tests\Helpers\MetaDataAssertionTrait;
/**
* Variations Controller tests for V3 REST API.
*/
-class WC_REST_Product_Variations_Controller_Tests extends WC_REST_Unit_Test_Case {
+class WC_REST_Product_Variations_Controller_Tests extends WC_Unit_Test_Case {
use CogsAwareUnitTestSuiteTrait;
use MetaDataAssertionTrait;
+ /**
+ * REST server used to dispatch variation requests.
+ *
+ * @var WP_REST_Server
+ */
+ protected $server;
+
+ /**
+ * Product variations controller registered on the test server.
+ *
+ * @var WC_REST_Product_Variations_Controller
+ */
+ protected $controller;
+
+ /**
+ * Administrator user ID.
+ *
+ * @var int
+ */
+ protected $user;
+
/**
* Runs before each test.
*/
public function setUp(): void {
parent::setUp();
- $this->endpoint = new WC_REST_Products_Controller();
- $this->user = $this->factory->user->create(
+ $this->controller = new WC_REST_Product_Variations_Controller();
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->controller, 'register_routes' ) ),
+ true
+ );
+ $this->user = $this->factory->user->create(
array(
'role' => 'administrator',
)
@@ -30,6 +55,8 @@ class WC_REST_Product_Variations_Controller_Tests extends WC_REST_Unit_Test_Case
*/
public function tearDown(): void {
parent::tearDown();
+ $this->clear_rest_server();
+ unset( $this->server, $this->controller );
$this->disable_cogs_feature();
}
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
index 896dd63fa95..f20b9ea1e34 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller-tests.php
@@ -9,10 +9,38 @@ use Automattic\WooCommerce\Tests\Helpers\MetaDataAssertionTrait;
* class WC_REST_Products_Controller_Tests.
* Product Controller tests for V3 REST API.
*/
-class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
+class WC_REST_Products_Controller_Tests extends WC_Unit_Test_Case {
use CogsAwareUnitTestSuiteTrait;
use MetaDataAssertionTrait;
+ /**
+ * REST server used to dispatch product requests.
+ *
+ * @var WP_REST_Server
+ */
+ protected $server;
+
+ /**
+ * Products controller registered on the test server.
+ *
+ * @var WC_REST_Products_Controller
+ */
+ protected $endpoint;
+
+ /**
+ * Administrator user ID.
+ *
+ * @var int
+ */
+ protected $user;
+
+ /**
+ * Administrator fixture user ID.
+ *
+ * @var int
+ */
+ protected static $fixture_user;
+
/**
* Saves the `woocommerce_hide_out_of_stock_items` option value for restoration after tests that modify it.
* @var mixed
@@ -24,6 +52,8 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
*/
public function tearDown(): void {
parent::tearDown();
+ $this->clear_rest_server();
+ unset( $this->server, $this->endpoint );
$this->disable_cogs_feature();
update_option( 'woocommerce_hide_out_of_stock_items', $this->original_hid_out_of_stock_value );
}
@@ -34,11 +64,15 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
protected static $products = array();
/**
- * Create products for tests.
+ * Create class fixtures for tests.
*
+ * @param WP_UnitTest_Factory $factory WordPress unit test factory.
* @return void
*/
- public static function wpSetUpBeforeClass() {
+ public static function wpSetUpBeforeClass( $factory ) {
+ self::enable_direct_product_attribute_lookup_updates();
+ self::$fixture_user = $factory->user->create( array( 'role' => 'administrator' ) );
+
self::$products[] = WC_Helper_Product::create_simple_product(
true,
array(
@@ -73,6 +107,7 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
$product->add_meta_data( 'test2', 'test2', true );
$product->save();
}
+ self::disable_direct_product_attribute_lookup_updates();
}
/**
@@ -81,9 +116,25 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
* @return void
*/
public static function wpTearDownAfterClass() {
+ self::enable_direct_product_attribute_lookup_updates();
foreach ( self::$products as $product ) {
WC_Helper_Product::delete_product( $product->get_id() );
}
+ self::disable_direct_product_attribute_lookup_updates();
+ }
+
+ /**
+ * Get the IDs of the products owned by this test class.
+ *
+ * @return int[]
+ */
+ private function get_fixture_product_ids(): array {
+ return array_map(
+ static function ( $product ) {
+ return $product->get_id();
+ },
+ self::$products
+ );
}
/**
@@ -92,11 +143,11 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
$this->endpoint = new WC_REST_Products_Controller();
- $this->user = $this->factory->user->create(
- array(
- 'role' => 'administrator',
- )
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->endpoint, 'register_routes' ) ),
+ true
);
+ $this->user = self::$fixture_user;
wp_set_current_user( $this->user );
$this->original_hid_out_of_stock_value = get_option( 'woocommerce_hide_out_of_stock_items' );
@@ -424,11 +475,12 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
public function test_collection_param_include_meta() {
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_param( 'include_meta', 'test1' );
+ $request->set_param( 'include', $this->get_fixture_product_ids() );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$response_data = $response->get_data();
- $this->assertCount( 4, $response_data );
+ $this->assertCount( count( self::$products ), $response_data );
foreach ( $response_data as $order ) {
$this->assertArrayHasKey( 'meta_data', $order );
@@ -449,11 +501,12 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
public function test_collection_param_include_meta_empty() {
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_param( 'include_meta', '' );
+ $request->set_param( 'include', $this->get_fixture_product_ids() );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$response_data = $response->get_data();
- $this->assertCount( 4, $response_data );
+ $this->assertCount( count( self::$products ), $response_data );
foreach ( $response_data as $order ) {
$this->assertArrayHasKey( 'meta_data', $order );
@@ -474,11 +527,12 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
public function test_collection_param_exclude_meta() {
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_param( 'exclude_meta', 'test1' );
+ $request->set_param( 'include', $this->get_fixture_product_ids() );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$response_data = $response->get_data();
- $this->assertCount( 4, $response_data );
+ $this->assertCount( count( self::$products ), $response_data );
foreach ( $response_data as $order ) {
$this->assertArrayHasKey( 'meta_data', $order );
@@ -500,11 +554,12 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_param( 'include_meta', 'test1' );
$request->set_param( 'exclude_meta', 'test1' );
+ $request->set_param( 'include', $this->get_fixture_product_ids() );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$response_data = $response->get_data();
- $this->assertCount( 4, $response_data );
+ $this->assertCount( count( self::$products ), $response_data );
foreach ( $response_data as $order ) {
$this->assertArrayHasKey( 'meta_data', $order );
@@ -965,10 +1020,7 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
* Test that the `include_types` parameter filters products by a single type.
*/
public function test_collection_filter_with_include_types() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_query_params(
@@ -989,10 +1041,7 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
* Test that the `include_types` parameter filters products by multiple types.
*/
public function test_collection_filter_with_multiple_include_types() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_query_params(
@@ -1028,13 +1077,27 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
}
/**
- * Test that `exclude_types` parameter correctly excludes a single type.
+ * Create one product of each parent product type for type-filter tests.
*/
- public function test_products_filter_with_single_exclude_types() {
+ private function create_products_for_type_filtering(): void {
WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
+
+ $variable = new WC_Product_Variable();
+ $variable->set_name( 'Variable product' );
+ $variable->save();
+
+ $grouped = new WC_Product_Grouped();
+ $grouped->set_name( 'Grouped product' );
+ $grouped->save();
+
WC_Helper_Product::create_external_product();
+ }
+
+ /**
+ * Test that `exclude_types` parameter correctly excludes a single type.
+ */
+ public function test_products_filter_with_single_exclude_types() {
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_query_params(
@@ -1056,10 +1119,7 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
* Test that `exclude_types` parameter correctly excludes multiple types.
*/
public function test_products_filter_with_multiple_exclude_types() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_query_params(
@@ -1087,10 +1147,7 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
* Test that empty `exclude_types` parameter returns all products.
*/
public function test_products_filter_with_empty_exclude_types() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_query_params(
@@ -1130,10 +1187,7 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
* Test that `exclude_types` with all types returns empty result.
*/
public function test_products_filter_exclude_types_with_all_types_returns_empty() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_query_params(
@@ -1151,10 +1205,7 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
* Test that `exclude_types` parameter takes precedence over `include_types`.
*/
public function test_products_filter_exclude_types_precedence_over_include() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_query_params(
@@ -1178,10 +1229,7 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
* Test that `exclude_types` works correctly with the `type` param.
*/
public function test_products_filter_exclude_types_with_type_param() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v3/products' );
$request->set_query_params(
@@ -1516,6 +1564,13 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
$original_product_sku = 'DUPLICATE_SKU_TEST_TRASHED';
// This image `src` is used in other product API tests, using here for consistency.
$shared_image_src = 'http://cldup.com/Dr1Bczxq4q.png';
+ // The failed request below exercises upload processing; setup only needs a valid image attachment.
+ $original_attachment_id = self::factory()->attachment->create(
+ array(
+ 'file' => WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/Dr1Bczxq4q.png',
+ 'post_mime_type' => 'image/png',
+ )
+ );
// 1. Create the original product with its image.
$request_original_product = new WP_REST_Request( 'POST', '/wc/v3/products' );
@@ -1527,7 +1582,7 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
'regular_price' => '10',
'images' => array(
array(
- 'src' => $shared_image_src,
+ 'id' => $original_attachment_id,
),
),
)
@@ -1536,9 +1591,8 @@ class WC_REST_Products_Controller_Tests extends WC_REST_Unit_Test_Case {
$this->assertEquals( 201, $response_original_product->get_status(), 'Failed to create the initial product with an image.' );
- $original_product_data = $response_original_product->get_data();
- $original_product_id = $original_product_data['id'];
- $original_attachment_id = $original_product_data['images'][0]['id'];
+ $original_product_data = $response_original_product->get_data();
+ $original_product_id = $original_product_data['id'];
// 2. Move the original product to trash.
wp_trash_post( $original_product_id );
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-refunds-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-refunds-controller-tests.php
index 56b5e3eddf2..09b64fd451d 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-refunds-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-refunds-controller-tests.php
@@ -14,31 +14,12 @@ class WC_REST_Refunds_Controller_Test extends WC_REST_Unit_Test_Case {
$refund_ids = array();
while ( $orders_and_refunds_count < 3 ) {
- $order = WC_Helper_Order::create_order_with_fees_and_shipping();
-
- $product_item = current( $order->get_items( 'line_item' ) );
- $fee_item = current( $order->get_items( 'fee' ) );
- $shipping_item = current( $order->get_items( 'shipping' ) );
+ $order = wc_create_order();
$refund = wc_create_refund(
array(
- 'order_id' => $order->get_id(),
- 'reason' => 'testing',
- 'line_items' => array(
- $product_item->get_id() =>
- array(
- 'qty' => 1,
- 'refund_total' => 1,
- ),
- $fee_item->get_id() =>
- array(
- 'refund_total' => 10,
- ),
- $shipping_item->get_id() =>
- array(
- 'refund_total' => 20,
- ),
- ),
+ 'order_id' => $order->get_id(),
+ 'reason' => 'testing',
)
);
@@ -53,6 +34,7 @@ class WC_REST_Refunds_Controller_Test extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
$this->assertIsArray( $data );
+ $this->assertCount( 3, $data );
foreach ( $data as $refund ) {
$this->assertContains( $refund['id'], $refund_ids );
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-variations-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-variations-controller-tests.php
index 56eecb62070..24af199e2ef 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-variations-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-variations-controller-tests.php
@@ -119,10 +119,8 @@ class WC_REST_Variations_Controller_Tests extends WC_REST_Unit_Test_Case {
// Given.
wp_set_current_user( $this->user );
- // Creates 3 variable products to have more variations.
- for ( $i = 0; $i < 3; $i++ ) {
- WC_Helper_Product::create_variation_product();
- }
+ // Creates a variable product with enough variations for multiple pages.
+ WC_Helper_Product::create_variation_product();
// When.
$request = new WP_REST_Request( 'GET', '/wc/v3/variations' );
@@ -140,8 +138,8 @@ class WC_REST_Variations_Controller_Tests extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'X-WP-TotalPages', $headers );
$total = (int) $headers['X-WP-Total'];
$total_pages = (int) $headers['X-WP-TotalPages'];
- $this->assertSame( 18, $total ); // 3 variable products * 6 variations per product.
- $this->assertSame( 9, $total_pages );
+ $this->assertSame( 6, $total );
+ $this->assertSame( 3, $total_pages );
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Customers/class-wc-rest-customers-v4-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Customers/class-wc-rest-customers-v4-controller-tests.php
index 8adf64de9a3..9caa504f907 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Customers/class-wc-rest-customers-v4-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Customers/class-wc-rest-customers-v4-controller-tests.php
@@ -30,11 +30,11 @@ class WC_REST_Customers_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
private $endpoint;
/**
- * Shared admin user ID used for REST authentication across the class.
+ * Administrator ID used to authenticate requests.
*
* @var int
*/
- protected static $user_id;
+ private static $administrator_id;
/**
* Customer schema instance.
@@ -43,6 +43,19 @@ class WC_REST_Customers_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
*/
private $customer_schema;
+ /**
+ * Create immutable class fixtures.
+ *
+ * @param WP_UnitTest_Factory $factory WordPress unit test factory.
+ */
+ public static function wpSetUpBeforeClass( $factory ): void {
+ self::$administrator_id = $factory->user->create(
+ array(
+ 'role' => 'administrator',
+ )
+ );
+ }
+
/**
* Runs after each test.
*/
@@ -78,15 +91,6 @@ class WC_REST_Customers_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
);
}
- /**
- * Create the shared admin user once for the whole class.
- *
- * @param object $factory Factory object.
- */
- public static function wpSetUpBeforeClass( $factory ) {
- self::$user_id = $factory->user->create( array( 'role' => 'administrator' ) );
- }
-
/**
* Setup our test server, endpoints, and user info.
*/
@@ -104,7 +108,7 @@ class WC_REST_Customers_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
$this->endpoint = new CustomersController();
$this->endpoint->init( $this->customer_schema, $collection_query, $update_utils );
- wp_set_current_user( self::$user_id );
+ wp_set_current_user( self::$administrator_id );
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Orders/class-wc-rest-orders-v4-can-be-refunded-test.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Orders/class-wc-rest-orders-v4-can-be-refunded-test.php
index c69de1e0b7f..05c0cff6d10 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Orders/class-wc-rest-orders-v4-can-be-refunded-test.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Orders/class-wc-rest-orders-v4-can-be-refunded-test.php
@@ -160,7 +160,6 @@ class WC_REST_Orders_V4_Can_Be_Refunded_Test extends WC_REST_Unit_Test_Case {
'total' => 20,
)
);
- $item_a->save();
$order->add_item( $item_a );
$item_b = new WC_Order_Item_Product();
@@ -172,7 +171,6 @@ class WC_REST_Orders_V4_Can_Be_Refunded_Test extends WC_REST_Unit_Test_Case {
'total' => 20,
)
);
- $item_b->save();
$order->add_item( $item_b );
$order->set_status( 'completed' );
@@ -294,7 +292,6 @@ class WC_REST_Orders_V4_Can_Be_Refunded_Test extends WC_REST_Unit_Test_Case {
'total' => 10,
)
);
- $item->save();
$order->add_item( $item );
$order->set_status( 'completed' );
$order->save();
@@ -462,7 +459,6 @@ class WC_REST_Orders_V4_Can_Be_Refunded_Test extends WC_REST_Unit_Test_Case {
'total' => array( 1 => '1.50' ),
)
);
- $shipping_item->save();
$order->add_item( $shipping_item );
$order->set_status( 'completed' );
$order->save();
@@ -509,7 +505,6 @@ class WC_REST_Orders_V4_Can_Be_Refunded_Test extends WC_REST_Unit_Test_Case {
'total' => array( 1 => '3.00' ),
)
);
- $fee_item->save();
$order->add_item( $fee_item );
$order->set_status( 'completed' );
$order->save();
@@ -560,7 +555,6 @@ class WC_REST_Orders_V4_Can_Be_Refunded_Test extends WC_REST_Unit_Test_Case {
'total' => array( 1 => '1.50' ),
)
);
- $shipping_item->save();
$order->add_item( $shipping_item );
$order->set_status( 'completed' );
$order->save();
@@ -609,7 +603,6 @@ class WC_REST_Orders_V4_Can_Be_Refunded_Test extends WC_REST_Unit_Test_Case {
'total' => array( 1 => '3.00' ),
)
);
- $fee_item->save();
$order->add_item( $fee_item );
$order->set_status( 'completed' );
$order->save();
@@ -657,7 +650,6 @@ class WC_REST_Orders_V4_Can_Be_Refunded_Test extends WC_REST_Unit_Test_Case {
'total' => 0,
)
);
- $item->save();
$order->add_item( $item );
$order->set_status( 'completed' );
$order->save();
@@ -691,7 +683,6 @@ class WC_REST_Orders_V4_Can_Be_Refunded_Test extends WC_REST_Unit_Test_Case {
'total' => 50,
)
);
- $item->save();
$order->add_item( $item );
$fee = new WC_Order_Item_Fee();
@@ -701,7 +692,6 @@ class WC_REST_Orders_V4_Can_Be_Refunded_Test extends WC_REST_Unit_Test_Case {
'total' => '-10.00',
)
);
- $fee->save();
$order->add_item( $fee );
$order->set_status( 'completed' );
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Refunds/class-wc-rest-refunds-v4-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Refunds/class-wc-rest-refunds-v4-controller-tests.php
index 9f25cda990f..a6e40c93428 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Refunds/class-wc-rest-refunds-v4-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Refunds/class-wc-rest-refunds-v4-controller-tests.php
@@ -27,11 +27,18 @@ class WC_REST_Refunds_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
private $endpoint;
/**
- * User ID.
+ * Administrator ID used to authenticate requests.
*
* @var int
*/
- private $user_id;
+ private static $administrator_id;
+
+ /**
+ * Product ID used by generic order fixtures.
+ *
+ * @var int
+ */
+ private static $product_id;
/**
* Refund schema instance.
@@ -55,37 +62,51 @@ class WC_REST_Refunds_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
private $created_refunds = array();
/**
- * Runs after each test.
+ * Create immutable class fixtures.
+ *
+ * @param WP_UnitTest_Factory $factory WordPress unit test factory.
*/
- public function tearDown(): void {
- // Clean up created refunds.
- foreach ( $this->created_refunds as $refund_id ) {
- $refund = wc_get_order( $refund_id );
- if ( $refund ) {
- $refund->delete( true );
- }
+ public static function wpSetUpBeforeClass( $factory ): void {
+ self::$administrator_id = $factory->user->create(
+ array(
+ 'user_login' => 'test_admin',
+ 'user_email' => 'test@example.com',
+ 'user_pass' => 'password',
+ 'role' => 'administrator',
+ )
+ );
+
+ self::enable_direct_product_attribute_lookup_updates();
+ try {
+ self::$product_id = WC_Helper_Product::create_simple_product()->get_id();
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
}
- $this->created_refunds = array();
+ }
- // Clean up created orders.
- foreach ( $this->created_orders as $order_id ) {
- $order = wc_get_order( $order_id );
- if ( $order ) {
- $order->delete( true );
+ /**
+ * Delete WooCommerce class fixtures through their data stores.
+ */
+ public static function wpTearDownAfterClass(): void {
+ self::enable_direct_product_attribute_lookup_updates();
+ try {
+ $product = wc_get_product( self::$product_id );
+ if ( $product ) {
+ $product->delete( true );
}
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
}
- $this->created_orders = array();
-
- // Clean up tax data.
- global $wpdb;
- $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations" );
- $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates" );
+ }
- // Reset tax-calculation options to their defaults. Several tests toggle these and
- // not all restore them individually; resetting here keeps the suite order-independent.
- update_option( 'woocommerce_calc_taxes', 'no' );
- update_option( 'woocommerce_prices_include_tax', 'no' );
+ /**
+ * Runs after each test.
+ */
+ public function tearDown(): void {
+ $this->created_refunds = array();
+ $this->created_orders = array();
+ self::disable_direct_product_attribute_lookup_updates();
parent::tearDown();
$this->disable_rest_api_v4_feature();
}
@@ -122,6 +143,7 @@ class WC_REST_Refunds_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
public function setUp(): void {
$this->enable_rest_api_v4_feature();
parent::setUp();
+ self::enable_direct_product_attribute_lookup_updates();
// Create schema instances with dependency injection.
$this->refund_schema = new RefundSchema();
@@ -134,15 +156,7 @@ class WC_REST_Refunds_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
$this->endpoint = new RefundsController();
$this->endpoint->init( $this->refund_schema, $preview_schema, $collection_query, $data_utils );
- $this->user_id = wp_insert_user(
- array(
- 'user_login' => 'test_admin',
- 'user_email' => 'test@example.com',
- 'user_pass' => 'password',
- 'role' => 'administrator',
- )
- );
- wp_set_current_user( $this->user_id );
+ wp_set_current_user( self::$administrator_id );
}
/**
@@ -152,13 +166,9 @@ class WC_REST_Refunds_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
* @return WC_Order
*/
private function create_test_order( array $order_data = array() ): WC_Order {
- $product = WC_Helper_Product::create_simple_product();
- $product->set_price( 10.00 );
- $product->save();
-
$default_data = array(
- 'status' => OrderStatus::COMPLETED,
- 'billing' => array(
+ 'status' => OrderStatus::COMPLETED,
+ 'billing' => array(
'first_name' => 'John',
'last_name' => 'Doe',
'email' => 'john.doe@example.com',
@@ -169,13 +179,16 @@ class WC_REST_Refunds_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
'postcode' => '12345',
'country' => 'US',
),
- 'line_items' => array(
+ );
+
+ if ( ! array_key_exists( 'line_items', $order_data ) ) {
+ $default_data['line_items'] = array(
array(
- 'product_id' => $product->get_id(),
+ 'product_id' => self::$product_id,
'quantity' => 1,
),
- ),
- );
+ );
+ }
$order_data = wp_parse_args( $order_data, $default_data );
@@ -189,8 +202,6 @@ class WC_REST_Refunds_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
$order = wc_get_order( $data['id'] );
$this->created_orders[] = $order->get_id();
- $product->delete( true );
-
return $order;
}
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Refunds/class-wc-rest-refunds-v4-preview-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Refunds/class-wc-rest-refunds-v4-preview-tests.php
index cdda235f5b4..c0acc4626db 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Refunds/class-wc-rest-refunds-v4-preview-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/Refunds/class-wc-rest-refunds-v4-preview-tests.php
@@ -96,21 +96,8 @@ class WC_REST_Refunds_V4_Preview_Tests extends WC_REST_Unit_Test_Case {
* Runs after each test.
*/
public function tearDown(): void {
- foreach ( $this->created_orders as $order_id ) {
- $order = wc_get_order( $order_id );
- if ( $order ) {
- foreach ( $order->get_refunds() as $refund ) {
- $refund->delete( true );
- }
- $order->delete( true );
- }
- }
$this->created_orders = array();
- global $wpdb;
- $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations" );
- $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates" );
-
parent::tearDown();
$this->disable_rest_api_v4_feature();
}
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/ShippingZones/class-wc-rest-shipping-zones-v4-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/ShippingZones/class-wc-rest-shipping-zones-v4-controller-tests.php
index f1d778c546b..6b1dbe19b9a 100644
--- a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/ShippingZones/class-wc-rest-shipping-zones-v4-controller-tests.php
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version4/ShippingZones/class-wc-rest-shipping-zones-v4-controller-tests.php
@@ -14,7 +14,14 @@ use Automattic\WooCommerce\Internal\RestApi\Routes\V4\ShippingZones\ShippingZone
/**
* Shipping Zones V4 Controller tests class.
*/
-class WC_REST_Shipping_Zones_V4_Controller_Tests extends WC_REST_Unit_Test_Case {
+class WC_REST_Shipping_Zones_V4_Controller_Tests extends WC_Unit_Test_Case {
+
+ /**
+ * REST server used to dispatch shipping zone requests.
+ *
+ * @var WP_REST_Server
+ */
+ protected $server;
/**
* Test endpoint.
@@ -38,29 +45,14 @@ class WC_REST_Shipping_Zones_V4_Controller_Tests extends WC_REST_Unit_Test_Case
protected $zones = array();
/**
- * Enable the REST API v4 feature.
- */
- public static function enable_rest_api_v4_feature() {
- add_filter(
- 'woocommerce_admin_features',
- function ( $features ) {
- $features[] = 'rest-api-v4';
- return $features;
- }
- );
- }
-
- /**
- * Disable the REST API v4 feature.
+ * Add the REST API v4 feature.
+ *
+ * @param array $features Enabled WooCommerce Admin features.
+ * @return array
*/
- public static function disable_rest_api_v4_feature() {
- add_filter(
- 'woocommerce_admin_features',
- function ( $features ) {
- $features = array_diff( $features, array( 'rest-api-v4' ) );
- return $features;
- }
- );
+ public static function enable_rest_api_v4_feature( $features ) {
+ $features[] = 'rest-api-v4';
+ return $features;
}
/**
@@ -83,10 +75,14 @@ class WC_REST_Shipping_Zones_V4_Controller_Tests extends WC_REST_Unit_Test_Case
* Setup our test server, endpoints, and user info.
*/
public function setUp(): void {
- $this->enable_rest_api_v4_feature();
parent::setUp();
+ add_filter( 'woocommerce_admin_features', array( self::class, 'enable_rest_api_v4_feature' ) );
$this->endpoint = new ShippingZonesController();
$this->endpoint->init( new ShippingZoneSchema(), new ShippingZoneService() );
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->endpoint, 'register_routes' ) ),
+ true
+ );
wp_set_current_user( self::$user );
$this->zones = array();
}
@@ -103,8 +99,9 @@ class WC_REST_Shipping_Zones_V4_Controller_Tests extends WC_REST_Unit_Test_Case
}
$this->zones = array();
+ $this->clear_rest_server();
+ remove_filter( 'woocommerce_admin_features', array( self::class, 'enable_rest_api_v4_feature' ) );
parent::tearDown();
- $this->disable_rest_api_v4_feature();
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-shipping-test.php b/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-shipping-test.php
index 5f91ea2ca00..d57a9ecfdcd 100644
--- a/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-shipping-test.php
+++ b/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-shipping-test.php
@@ -69,16 +69,27 @@ class WC_Settings_Shipping_Test extends WC_Settings_Unit_Test_Case {
$sut->method( 'get_shipping_methods' )->willReturn( $methods );
$sut->method( 'wc_is_installing' )->willReturn( false );
- $sections = $sut->get_sections();
-
- $expected = array(
- '' => 'Shipping zones',
- 'options' => 'Shipping settings',
- 'classes' => 'Classes',
- 'method_1_id' => 'Method_1_id',
- 'method_2_id' => 'method_2_title',
- );
- $this->assertEquals( $expected, $sections );
+ $previous_fulfillments_enabled = get_option( 'woocommerce_feature_fulfillments_enabled', null );
+ update_option( 'woocommerce_feature_fulfillments_enabled', 'no' );
+
+ try {
+ $sections = $sut->get_sections();
+
+ $expected = array(
+ '' => 'Shipping zones',
+ 'options' => 'Shipping settings',
+ 'classes' => 'Classes',
+ 'method_1_id' => 'Method_1_id',
+ 'method_2_id' => 'method_2_title',
+ );
+ $this->assertEquals( $expected, $sections );
+ } finally {
+ if ( null === $previous_fulfillments_enabled ) {
+ delete_option( 'woocommerce_feature_fulfillments_enabled' );
+ } else {
+ update_option( 'woocommerce_feature_fulfillments_enabled', $previous_fulfillments_enabled );
+ }
+ }
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php b/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php
index 98176bf7c47..0adf6efcf40 100644
--- a/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php
+++ b/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php
@@ -12,6 +12,12 @@ use Automattic\WooCommerce\Enums\OrderInternalStatus;
* Class WC_Stock_Functions_Tests.
*/
class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
+ /**
+ * Product reused by independent stock transitions within one test method.
+ *
+ * @var WC_Product|null
+ */
+ private $stock_product;
/**
* @var array List of statuses which reduces stock from inventory.
@@ -44,6 +50,7 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
public function tearDown(): void {
parent::tearDown();
WC()->cart->empty_cart();
+ $this->stock_product = null;
}
/**
@@ -52,13 +59,18 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
* @param string $status Status for the newly created order.
*/
private function create_order_from_cart_with_status( $status ) {
- $product = WC_Helper_Product::create_simple_product(
- true,
- array(
- 'manage_stock' => true,
- 'stock_quantity' => 10,
- )
- );
+ if ( ! $this->stock_product ) {
+ $this->stock_product = WC_Helper_Product::create_simple_product(
+ true,
+ array(
+ 'manage_stock' => true,
+ 'stock_quantity' => 10,
+ )
+ );
+ } else {
+ wc_update_product_stock( $this->stock_product, 10, 'set' );
+ }
+ $product = $this->stock_product;
WC()->cart->empty_cart();
WC()->cart->add_to_cart( $product->get_id(), 1 );
WC()->cart->calculate_totals();
diff --git a/plugins/woocommerce/tests/php/includes/wc-user-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-user-functions-test.php
index 465885cbe89..dea9cba1b09 100644
--- a/plugins/woocommerce/tests/php/includes/wc-user-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-user-functions-test.php
@@ -2,7 +2,9 @@
use Automattic\WooCommerce\Enums\OrderStatus;
use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
+use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
+use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* Tests for the WC_User class.
@@ -10,12 +12,38 @@ use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Registe
class WC_User_Functions_Tests extends WC_Unit_Test_Case {
use HPOSToggleTrait;
+ /**
+ * Ensure permanent HPOS tables exist before per-test transactions start.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ $previous_hpos_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ try {
+ self::setup_cot_tables();
+ if ( OrderUtil::custom_orders_table_usage_is_enabled() !== $previous_hpos_state ) {
+ OrderHelper::toggle_cot_feature_and_usage( $previous_hpos_state );
+ }
+ } finally {
+ remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ }
+ }
+
+ /**
+ * Whether HPOS was authoritative before the test.
+ *
+ * @var bool
+ */
+ private $previous_hpos_state;
+
/**
* Setup COT.
*/
public function setUp(): void {
parent::setUp();
- $this->setup_cot();
+ $this->previous_hpos_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
+ remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
$this->toggle_cot_feature_and_usage( true );
}
@@ -23,8 +51,10 @@ class WC_User_Functions_Tests extends WC_Unit_Test_Case {
* Clean COT specific things.
*/
public function tearDown(): void {
- parent::tearDown();
$this->clean_up_cot_setup();
+ $this->toggle_cot_feature_and_usage( $this->previous_hpos_state );
+ remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ parent::tearDown();
// In case `wc_update_user_last_active` test fail, clean the global state.
global $wp_current_filter;
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/MobileAppQRLoginTest.php b/plugins/woocommerce/tests/php/src/Admin/API/MobileAppQRLoginTest.php
index aeb5e4fa5e7..4a8a0e8febf 100644
--- a/plugins/woocommerce/tests/php/src/Admin/API/MobileAppQRLoginTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/API/MobileAppQRLoginTest.php
@@ -11,16 +11,53 @@ namespace Automattic\WooCommerce\Tests\Admin\API;
use Automattic\WooCommerce\Admin\API\MobileAppQRLogin;
use Automattic\WooCommerce\Admin\API\RateLimits\QRLoginRateLimits;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_Application_Passwords;
use WP_REST_Request;
+use WP_REST_Server;
+use WP_UnitTest_Factory;
/**
* MobileAppQRLogin API controller test.
*
* @class MobileAppQRLoginTest.
*/
-class MobileAppQRLoginTest extends WC_REST_Unit_Test_Case {
+class MobileAppQRLoginTest extends WC_Unit_Test_Case {
+
+ /**
+ * REST server used to dispatch QR login requests.
+ *
+ * @var WP_REST_Server
+ */
+ private $server;
+
+ /**
+ * QR login controller registered on the test server.
+ *
+ * @var MobileAppQRLogin
+ */
+ private $controller;
+
+ /**
+ * Administrator fixture user ID.
+ *
+ * @var int
+ */
+ private static $fixture_admin_id;
+
+ /**
+ * Shop manager fixture user ID.
+ *
+ * @var int
+ */
+ private static $fixture_shop_manager_id;
+
+ /**
+ * Subscriber fixture user ID.
+ *
+ * @var int
+ */
+ private static $fixture_subscriber_id;
/**
* Token generation endpoint.
@@ -140,15 +177,32 @@ class MobileAppQRLoginTest extends WC_REST_Unit_Test_Case {
*/
private $site_url_filters = array();
+ /**
+ * Create immutable users shared by the test class.
+ *
+ * @param WP_UnitTest_Factory $factory WordPress unit test factory.
+ */
+ public static function wpSetUpBeforeClass( $factory ): void {
+ self::$fixture_admin_id = $factory->user->create( array( 'role' => 'administrator' ) );
+ self::$fixture_shop_manager_id = $factory->user->create( array( 'role' => 'shop_manager' ) );
+ self::$fixture_subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) );
+ }
+
/**
* Set up test fixtures.
*/
public function setUp(): void {
parent::setUp();
- $this->admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
- $this->shop_manager_id = $this->factory->user->create( array( 'role' => 'shop_manager' ) );
- $this->subscriber_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );
+ $this->controller = new MobileAppQRLogin();
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->controller, 'register_routes' ) ),
+ true
+ );
+
+ $this->admin_id = self::$fixture_admin_id;
+ $this->shop_manager_id = self::$fixture_shop_manager_id;
+ $this->subscriber_id = self::$fixture_subscriber_id;
// Remember existing $_SERVER values so we can restore them in tearDown.
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Unit-test fixture: values are captured for restoration only, never used for processing.
@@ -178,10 +232,6 @@ class MobileAppQRLoginTest extends WC_REST_Unit_Test_Case {
public function tearDown(): void {
wp_set_current_user( 0 );
- wp_delete_user( $this->admin_id );
- wp_delete_user( $this->shop_manager_id );
- wp_delete_user( $this->subscriber_id );
-
// Clear any QR login data the tests may have written.
$this->delete_all_qr_login_data();
@@ -217,6 +267,8 @@ class MobileAppQRLoginTest extends WC_REST_Unit_Test_Case {
remove_filter( 'pre_option_siteurl', $filter, $priority );
}
$this->site_url_filters = array();
+ $this->clear_rest_server();
+ unset( $this->server, $this->controller );
parent::tearDown();
}
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Blueprint/InitTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Blueprint/InitTest.php
index af037f3b998..a5856a2ac74 100644
--- a/plugins/woocommerce/tests/php/src/Admin/Features/Blueprint/InitTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Blueprint/InitTest.php
@@ -5,7 +5,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Tests\Admin\Features\Blueprint;
use Automattic\WooCommerce\Admin\Features\Blueprint\Init;
-use Automattic\WooCommerce\Tests\Admin\Features\Blueprint\stubs\DummyExporter;
+use Automattic\WooCommerce\Tests\Admin\Features\Blueprint\Stubs\DummyExporter;
+use Automattic\WooCommerce\Tests\Admin\Features\Blueprint\Stubs\ThemeStub;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
@@ -78,13 +79,36 @@ class InitTest extends MockeryTestCase {
*/
public function test_get_themes_for_export_group() {
// Create mock themes that mimic WP_Theme.
- $mock_theme_1 = $this->createMockTheme( 'theme-one', 'Theme One' );
- $mock_theme_2 = $this->createMockTheme( 'theme-two', 'Theme Two' );
- $mock_active_theme = $this->createMockTheme( 'theme-one', 'Theme One' );
+ $mock_theme_1 = $this->createThemeStub( 'theme-one', 'Theme One' );
+ $mock_theme_2 = $this->createThemeStub( 'theme-two', 'Theme Two' );
+ $mock_theme_3 = $this->createThemeStub( 'custom-theme', 'Custom Theme' );
+ $mock_active_theme = $this->createThemeStub( 'theme-one', 'Theme One' );
+ $themes_api = (object) array(
+ 'theme-one' => array( 'download_link' => 'https://example.com/theme-one.zip' ),
+ 'theme-two' => array( 'download_link' => 'https://example.com/theme-two.zip' ),
+ );
// Mock methods.
- $this->init->shouldReceive( 'wp_get_themes' )->andReturn( array( $mock_theme_1, $mock_theme_2 ) );
+ $this->init->shouldReceive( 'wp_get_themes' )->andReturn(
+ array(
+ 'theme-one' => $mock_theme_1,
+ 'theme-two' => $mock_theme_2,
+ 'custom-theme' => $mock_theme_3,
+ )
+ );
$this->init->shouldReceive( 'wp_get_theme' )->andReturn( $mock_active_theme );
+ $this->init
+ ->shouldReceive( 'wp_themes_api' )
+ ->once()
+ ->with(
+ 'theme_information',
+ Mockery::on(
+ function ( $args ) {
+ return array( 'theme-one', 'theme-two', 'custom-theme' ) === $args['slugs'];
+ }
+ )
+ )
+ ->andReturn( $themes_api );
// Run the function.
$result = $this->init->get_themes_for_export_group();
@@ -166,19 +190,15 @@ class InitTest extends MockeryTestCase {
}
/**
- * Helper method to create a mock WP_Theme-like object.
+ * Helper method to create a WP_Theme-like object.
*
* @param string $stylesheet The stylesheet of the theme.
* @param string $name The name of the theme.
*
- * @return Mockery\MockInterface The mock WP_Theme object.
+ * @return ThemeStub The theme object.
*/
- private function createMockTheme( string $stylesheet, string $name ) {
- $mock_theme = Mockery::mock( 'stdClass' );
- $mock_theme->shouldReceive( 'get_stylesheet' )->andReturn( $stylesheet );
- $mock_theme->shouldReceive( 'get' )->with( 'Name' )->andReturn( $name );
-
- return $mock_theme;
+ private function createThemeStub( string $stylesheet, string $name ): ThemeStub {
+ return new ThemeStub( $stylesheet, $name );
}
/**
@@ -189,5 +209,6 @@ class InitTest extends MockeryTestCase {
parent::tearDown();
delete_transient( $this->init::INSTALLED_WP_ORG_PLUGINS_TRANSIENT );
+ delete_transient( $this->init::INSTALLED_WP_ORG_THEMES_TRANSIENT );
}
}
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Blueprint/Stubs/ThemeStub.php b/plugins/woocommerce/tests/php/src/Admin/Features/Blueprint/Stubs/ThemeStub.php
new file mode 100644
index 00000000000..77ba31da5df
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Blueprint/Stubs/ThemeStub.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Tests\Admin\Features\Blueprint\Stubs;
+
+/**
+ * Serializable WP_Theme-like test double.
+ */
+final class ThemeStub {
+ /**
+ * Theme stylesheet.
+ *
+ * @var string
+ */
+ private $stylesheet;
+
+ /**
+ * Theme name.
+ *
+ * @var string
+ */
+ private $name;
+
+ /**
+ * Initialize the theme stub.
+ *
+ * @param string $stylesheet Theme stylesheet.
+ * @param string $name Theme name.
+ */
+ public function __construct( string $stylesheet, string $name ) {
+ $this->stylesheet = $stylesheet;
+ $this->name = $name;
+ }
+
+ /**
+ * Get the theme stylesheet.
+ *
+ * @return string
+ */
+ public function get_stylesheet(): string {
+ return $this->stylesheet;
+ }
+
+ /**
+ * Get a theme header.
+ *
+ * @param string $header Theme header name.
+ * @return string
+ */
+ public function get( string $header ): string {
+ return 'Name' === $header ? $this->name : '';
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/DataStore/FulfillmentsDataStoreHookTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/DataStore/FulfillmentsDataStoreHookTest.php
index 5ba494fade6..fe443636755 100644
--- a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/DataStore/FulfillmentsDataStoreHookTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/DataStore/FulfillmentsDataStoreHookTest.php
@@ -55,21 +55,6 @@ class FulfillmentsDataStoreHookTest extends WC_Unit_Test_Case {
$this->order = WC_Helper_Order::create_order( get_current_user_id() );
}
- /**
- * Tear down test case.
- *
- * This method is called after each test method is executed.
- * It is used to clean up any data created during the tests.
- */
- public function tearDown(): void {
- parent::tearDown();
-
- // Remove any fulfillments added during the tests.
- global $wpdb;
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_order_fulfillments;" );
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_order_fulfillment_meta;" );
- }
-
/**
* Test that the fulfillment before create hook is called when creating a fulfillment.
*/
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/OrderFulfillmentsRestControllerTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/OrderFulfillmentsRestControllerTest.php
index 1d2d388abf1..d21485e6442 100644
--- a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/OrderFulfillmentsRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/OrderFulfillmentsRestControllerTest.php
@@ -3,6 +3,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Admin\Features\Fulfillments;
+use Automattic\WooCommerce\Admin\Features\Fulfillments\DataStore\FulfillmentsDataStore;
use Automattic\WooCommerce\Admin\Features\Fulfillments\OrderFulfillmentsRestController;
use Automattic\WooCommerce\Tests\Admin\Features\Fulfillments\Helpers\FulfillmentsHelper;
use WC_Helper_Order;
@@ -29,6 +30,13 @@ class OrderFulfillmentsRestControllerTest extends WC_REST_Unit_Test_Case {
*/
private static array $created_order_ids = array();
+ /**
+ * Import action IDs created for shared order fixtures.
+ *
+ * @var int[]
+ */
+ private static array $created_import_action_ids = array();
+
/**
* Created user ID for testing purposes.
*
@@ -73,6 +81,7 @@ class OrderFulfillmentsRestControllerTest extends WC_REST_Unit_Test_Case {
*/
public static function setupBeforeClass(): void {
parent::setupBeforeClass();
+ self::enable_direct_product_attribute_lookup_updates();
self::$original_fulfillments_flag = get_option( 'woocommerce_feature_fulfillments_enabled' );
update_option( 'woocommerce_feature_fulfillments_enabled', 'yes' );
@@ -81,6 +90,7 @@ class OrderFulfillmentsRestControllerTest extends WC_REST_Unit_Test_Case {
$controller->initialize_fulfillments();
self::$created_user_id = wp_create_user( 'test_user', 'password', 'nonadmin@example.com' );
+ add_action( 'action_scheduler_stored_action', array( self::class, 'track_import_action' ) );
for ( $order_number = 1; $order_number <= 10; $order_number++ ) {
$order = WC_Helper_Order::create_order( get_current_user_id() );
@@ -109,29 +119,64 @@ class OrderFulfillmentsRestControllerTest extends WC_REST_Unit_Test_Case {
);
self::$created_fulfillment_ids[ $customer_order->get_id() ][] = $f->get_id();
}
+ remove_action( 'action_scheduler_stored_action', array( self::class, 'track_import_action' ) );
+ self::disable_direct_product_attribute_lookup_updates();
+ }
+
+ /**
+ * Track import actions created for shared order fixtures.
+ *
+ * @param int $action_id Action ID.
+ */
+ public static function track_import_action( $action_id ): void {
+ $action = \ActionScheduler_Store::instance()->fetch_action( $action_id );
+ if ( 'wc-admin_import_orders' === $action->get_hook() ) {
+ self::$created_import_action_ids[] = (int) $action_id;
+ }
}
/**
* Destroys the test environment after all tests on this file are run.
*/
public static function tearDownAfterClass(): void {
- // Delete the created orders and their fulfillments.
- global $wpdb;
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_order_fulfillments;" );
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_order_fulfillment_meta;" );
- foreach ( self::$created_order_ids as $order_id ) {
- WC_Helper_Order::delete_order( $order_id );
- }
+ remove_action( 'action_scheduler_stored_action', array( self::class, 'track_import_action' ) );
+ self::enable_direct_product_attribute_lookup_updates();
+ try {
+ $fulfillments_data_store = new FulfillmentsDataStore();
+ $action_store = \ActionScheduler_Store::instance();
+ foreach ( self::$created_import_action_ids as $action_id ) {
+ $action_store->delete_action( $action_id );
+ }
- // Delete the created user.
- wp_delete_user( self::$created_user_id );
- if ( false === self::$original_fulfillments_flag ) {
- delete_option( 'woocommerce_feature_fulfillments_enabled' );
- } else {
- update_option( 'woocommerce_feature_fulfillments_enabled', self::$original_fulfillments_flag );
- }
+ // Delete the created orders and their fulfillments.
+ foreach ( self::$created_order_ids as $order_id ) {
+ $fulfillments_data_store->delete_by_entity( WC_Order::class, (string) $order_id );
+ WC_Helper_Order::delete_order( $order_id );
+ }
- parent::tearDownAfterClass();
+ // Delete the created user.
+ wp_delete_user( self::$created_user_id );
+ } finally {
+ try {
+ if ( false === self::$original_fulfillments_flag ) {
+ delete_option( 'woocommerce_feature_fulfillments_enabled' );
+ } else {
+ update_option( 'woocommerce_feature_fulfillments_enabled', self::$original_fulfillments_flag );
+ }
+ } finally {
+ try {
+ parent::tearDownAfterClass();
+ } finally {
+ self::$created_order_ids = array();
+ self::$created_import_action_ids = array();
+ self::$created_fulfillment_ids = array();
+ self::$created_user_id = -1;
+ self::$customer_order_id = -1;
+ self::$original_fulfillments_flag = null;
+ self::disable_direct_product_attribute_lookup_updates();
+ }
+ }
+ }
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/BreadcrumbsTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/BreadcrumbsTest.php
index 4c68325012d..3cb51dfbc06 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/BreadcrumbsTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/BreadcrumbsTest.php
@@ -106,7 +106,9 @@ class BreadcrumbsTest extends WP_UnitTestCase {
public function test_custom_typography_font_size(): void {
$markup = $this->render_breadcrumbs( '{"style":{"typography":{"fontSize":"2rem"}}}' );
- $this->assertStringContainsString( 'font-size: 2rem', $markup, 'Custom font size should be applied as inline style.' );
+ // Fluid typography may render the size as clamp(..., 2rem), so only
+ // require that 2rem appears within the font-size declaration itself.
+ $this->assertMatchesRegularExpression( '/font-size:[^;"]*2rem/', $markup, 'Custom font size should be applied as inline style.' );
$this->assertStringNotContainsString( 'has-small-font-size', $markup, 'Default small font size class should be removed for custom typography.' );
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/DownloadsWrapper.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/DownloadsWrapper.php
index 85d09a355f6..f44bb093cbb 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/DownloadsWrapper.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/OrderConfirmation/DownloadsWrapper.php
@@ -9,20 +9,38 @@ use Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation\DownloadsWrapper
*/
final class DownloadsWrapper extends \WP_UnitTestCase {
/**
- * Perform products/options/cache cleanup.
+ * Enable synchronous product attribute lookup updates for test fixtures.
+ *
+ * @return string
*/
- public function tear_down() {
+ public static function enable_direct_attribute_lookup_updates(): string {
+ return 'yes';
+ }
+
+ /**
+ * Set up test fixtures.
+ */
+ public function set_up() {
global $wpdb;
+ parent::set_up();
+ add_filter( 'pre_option_woocommerce_attribute_lookup_direct_updates', array( self::class, 'enable_direct_attribute_lookup_updates' ) );
+
/** @var \WC_Product[] $products */
$products = ( new \WC_Product_Query() )->get_products();
foreach ( $products as $product ) {
$product->delete();
}
- $wpdb->query( "TRUNCATE TABLE {$wpdb->wc_product_meta_lookup}" );
+ $wpdb->query( "DELETE FROM {$wpdb->wc_product_meta_lookup}" );
+ }
+ /**
+ * Perform products/options/cache cleanup.
+ */
+ public function tear_down() {
delete_option( 'woocommerce_product_lookup_table_is_generating' );
wp_cache_delete( 'woocommerce_has_downloadable_products', 'woocommerce' );
+ remove_filter( 'pre_option_woocommerce_attribute_lookup_direct_updates', array( self::class, 'enable_direct_attribute_lookup_updates' ) );
parent::tear_down();
}
@@ -31,10 +49,11 @@ final class DownloadsWrapper extends \WP_UnitTestCase {
* Test `store_has_downloadable_products`: query product meta lookup table.
*
* @dataProvider provider_downloadable_products
- * @param \WC_Product $product The product instance.
+ * @param bool $downloadable Whether the product is downloadable.
*/
- public function test_store_has_downloadable_products_via_product_meta_lookup_table_with_downloadable( \WC_Product $product ): void {
- $proxy = new class() extends DownloadsWrapperClass {
+ public function test_store_has_downloadable_products_via_product_meta_lookup_table_with_downloadable( bool $downloadable ): void {
+ $product = \WC_Helper_Product::create_simple_product( true, array( 'downloadable' => $downloadable ) );
+ $proxy = new class() extends DownloadsWrapperClass {
// phpcs:ignore Squiz.Commenting.FunctionComment.Missing
public function __construct() {
}
@@ -44,7 +63,7 @@ final class DownloadsWrapper extends \WP_UnitTestCase {
}
};
- $this->assertSame( $product->is_downloadable(), $proxy->store_has_downloadable_products_proxy() );
+ $this->assertSame( $downloadable, $proxy->store_has_downloadable_products_proxy() );
}
/**
@@ -54,8 +73,8 @@ final class DownloadsWrapper extends \WP_UnitTestCase {
*/
public function provider_downloadable_products(): array {
return array(
- array( \WC_Helper_Product::create_simple_product( true, array( 'downloadable' => true ) ) ),
- array( \WC_Helper_Product::create_simple_product( true, array( 'downloadable' => false ) ) ),
+ array( true ),
+ array( false ),
);
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/HandlerRegistry.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/HandlerRegistry.php
index 7f1383d52b2..eeac64986b2 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/HandlerRegistry.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/HandlerRegistry.php
@@ -25,6 +25,7 @@ class HandlerRegistry extends \WP_UnitTestCase {
* Initiate the mock object.
*/
protected function setUp(): void {
+ parent::setUp();
$this->block_instance = new ProductCollectionMock();
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
index 680aea66f10..0728e2bd3cc 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
@@ -28,6 +28,7 @@ class QueryBuilder extends \WP_UnitTestCase {
* Initiate the mock object.
*/
protected function setUp(): void {
+ parent::setUp();
$this->block_instance = new ProductCollectionMock();
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/RestApi.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/RestApi.php
index 854f3987cea..02763dc3a14 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/RestApi.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/RestApi.php
@@ -25,6 +25,7 @@ class RestApi extends \WP_UnitTestCase {
* Initiate the mock object.
*/
protected function setUp(): void {
+ parent::setUp();
$this->block_instance = new ProductCollectionMock();
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/SqlGeneration.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/SqlGeneration.php
index 0d4d1bb1632..d70b8bf5796 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/SqlGeneration.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/SqlGeneration.php
@@ -27,6 +27,7 @@ class SqlGeneration extends \WP_UnitTestCase {
* Initiate the mock object.
*/
protected function setUp(): void {
+ parent::setUp();
$this->block_instance = new ProductCollectionMock();
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/DeleteDraftOrders.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/DeleteDraftOrders.php
index 8534dc0c536..d50f6510afd 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/DeleteDraftOrders.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/DeleteDraftOrders.php
@@ -6,28 +6,59 @@ namespace Automattic\WooCommerce\Tests\Blocks\Domain\Services;
use Automattic\WooCommerce\Blocks\Domain\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders;
use Automattic\WooCommerce\Enums\OrderStatus;
+use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
+use Automattic\WooCommerce\Utilities\OrderUtil;
use WC_Order;
-use Yoast\PHPUnitPolyfills\TestCases\TestCase;
/**
* Tests Delete Draft Orders functionality
*
* @since $VID:$
*/
-class DeleteDraftOrders extends TestCase {
+class DeleteDraftOrders extends \WC_Unit_Test_Case {
+ /**
+ * Draft order service under test.
+ *
+ * @var DraftOrders|null
+ */
private $draft_orders_instance;
+
+ /**
+ * Exception caught by the service's error handler.
+ *
+ * @var \Throwable|null
+ */
private $caught_exception;
+
+ /**
+ * Original PHP error log destination.
+ *
+ * @var string|false
+ */
private $original_logging_destination;
+ /**
+ * Whether HPOS was authoritative before the test.
+ *
+ * @var bool
+ */
+ private $previous_hpos_state;
+
/**
* During setup create some draft orders.
*
* @return void
*/
- protected function setUp(): void {
+ public function setUp(): void {
global $wpdb;
+ parent::setUp();
+
+ $this->previous_hpos_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ OrderHelper::toggle_cot_feature_and_usage( false );
+
$this->draft_orders_instance = new DraftOrders( new Package( 'test', './' ) );
$order = new WC_Order();
@@ -77,30 +108,36 @@ class DeleteDraftOrders extends TestCase {
)
);
- // set listening for exceptions
- add_action( 'woocommerce_caught_exception', function($exception_object){
- $this->caught_exception = $exception_object;
- });
+ // Listen for exceptions.
+ add_action( 'woocommerce_caught_exception', array( $this, 'capture_exception' ) );
// temporarily hide error logging we don't care about (and keeps from polluting stdout)
$this->original_logging_destination = ini_get('error_log');
ini_set('error_log', '/dev/null');
- parent::setUp();
}
- protected function tearDown(): void {
+ /**
+ * Restore test state.
+ */
+ public function tearDown(): void {
$this->draft_orders_instance = null;
- // delete all orders
- $orders = wc_get_orders([]);
- foreach( $orders as $order ) {
- $order->delete( true );
- }
- remove_all_actions( 'woocommerce_caught_exception' );
+ remove_action( 'woocommerce_caught_exception', array( $this, 'capture_exception' ) );
//restore original logging destination
ini_set('error_log', $this->original_logging_destination);
+ OrderHelper::toggle_cot_feature_and_usage( $this->previous_hpos_state );
+ remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
parent::tearDown();
}
+ /**
+ * Capture an exception reported by the service.
+ *
+ * @param \Throwable $exception_object Reported exception.
+ */
+ public function capture_exception( $exception_object ): void {
+ $this->caught_exception = $exception_object;
+ }
+
/**
* Delete draft orders older than a day.
*
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Helpers/FixtureData.php b/plugins/woocommerce/tests/php/src/Blocks/Helpers/FixtureData.php
index 6a5adb63711..662f63fb745 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Helpers/FixtureData.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Helpers/FixtureData.php
@@ -354,15 +354,36 @@ class FixtureData {
}
/**
- * Upload a sample image and return it's ID.
+ * Create a sample image attachment and return its ID.
*
- * @param integer $product_id
- * @return void
+ * @param integer $product_id Parent product ID.
+ * @return int|\WP_Error
*/
- public function sideload_image( $product_id = 0 ) {
- global $wpdb;
- $image_url = media_sideload_image( 'http://cldup.com/Dr1Bczxq4q.png', $product_id, '', 'src' );
- return $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE guid = %s", $image_url ) )[0];
+ public function create_image_attachment( $product_id = 0 ) {
+ $attachment_id = wp_insert_attachment(
+ array(
+ 'guid' => 'http://example.org/wp-content/uploads/Dr1Bczxq4q.png',
+ 'post_mime_type' => 'image/png',
+ 'post_parent' => $product_id,
+ 'post_title' => 'Dr1Bczxq4q.png',
+ ),
+ 'Dr1Bczxq4q.png',
+ $product_id,
+ true
+ );
+
+ if ( ! is_wp_error( $attachment_id ) ) {
+ wp_update_attachment_metadata(
+ $attachment_id,
+ array(
+ 'width' => 186,
+ 'height' => 144,
+ 'file' => 'Dr1Bczxq4q.png',
+ )
+ );
+ }
+
+ return $attachment_id;
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php
index b5cc8ed1dae..38878ce3255 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/AdditionalFields.php
@@ -6,11 +6,11 @@
namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes;
use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
-use Mockery\Adapter\Phpunit\MockeryTestCase;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
use Automattic\WooCommerce\Blocks\Package;
use WC_Gateway_BACS;
use Automattic\WooCommerce\Enums\ProductStockStatus;
+use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
/**
* AdditionalFields Controller Tests.
@@ -18,7 +18,16 @@ use Automattic\WooCommerce\Enums\ProductStockStatus;
*
* phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_print_r, WooCommerce.Commenting.CommentHooks.MissingHookComment
*/
-class AdditionalFields extends MockeryTestCase {
+class AdditionalFields extends \WP_Test_REST_TestCase {
+ use MockeryPHPUnitIntegration;
+ use StoreApiRestTestCaseTrait;
+
+ /**
+ * Product IDs shared by the class.
+ *
+ * @var int[]
+ */
+ private static $product_ids = array();
/**
* Fields to register.
@@ -40,16 +49,61 @@ class AdditionalFields extends MockeryTestCase {
*/
protected $products;
+ /**
+ * Create immutable catalog rows shared by all test methods.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ self::$product_ids = array_map(
+ static fn( $product ) => $product->get_id(),
+ self::create_class_fixture_products(
+ array(
+ array(
+ 'name' => 'Test Product 1',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ 'weight' => 10,
+ ),
+ array(
+ 'name' => 'Test Product 2',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ 'weight' => 10,
+ ),
+ array(
+ 'name' => 'Virtual Test Product 3',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ 'weight' => 10,
+ 'virtual' => true,
+ ),
+ array(
+ 'name' => 'Downloadable Test Product 4',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ 'weight' => 10,
+ 'downloadable' => true,
+ ),
+ )
+ )
+ );
+ }
+
+ /**
+ * Delete class products through WooCommerce data stores.
+ */
+ public static function wpTearDownAfterClass(): void {
+ self::delete_class_fixture_products( self::$product_ids );
+ }
+
/**
* Setup products and a cart, as well as register fields.
*/
protected function setUp(): void {
parent::setUp();
+ update_option( 'woocommerce_checkout_phone_field', 'optional' );
add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
- global $wp_rest_server;
- $wp_rest_server = new \Spy_REST_Server();
- do_action( 'rest_api_init', $wp_rest_server );
+ $this->initialize_store_api_server();
$this->register_fields();
$this->controller = Package::container()->get( CheckoutFields::class );
@@ -57,41 +111,9 @@ class AdditionalFields extends MockeryTestCase {
$fixtures = new FixtureData();
$fixtures->shipping_add_flat_rate();
$fixtures->payments_enable_bacs();
- $this->products = array(
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 1',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- )
- ),
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 2',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- )
- ),
- $fixtures->get_simple_product(
- array(
- 'name' => 'Virtual Test Product 3',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- 'virtual' => true,
- )
- ),
- $fixtures->get_simple_product(
- array(
- 'name' => 'Downloadable Test Product 4',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- 'downloadable' => true,
- )
- ),
+ $this->products = array_map(
+ 'wc_get_product',
+ self::$product_ids
);
$this->reset_session();
}
@@ -347,6 +369,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a field is registered without an ID.
*/
public function test_missing_id_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
array(
@@ -388,6 +411,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a field is registered with an invalid ID.
*/
public function test_invalid_id_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'invalid-id';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -432,6 +456,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a field is registered without a label.
*/
public function test_missing_label_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/missing-label';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -474,6 +499,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a field is registered without a location key.
*/
public function test_missing_location_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/missing-location';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -515,6 +541,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a field is registered with an invalid location key (contact, address, additional).
*/
public function test_invalid_location_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/invalid-location';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -560,6 +587,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a field is registered with an existing id.
*/
public function test_already_registered_field() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/gov-id';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -602,6 +630,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a field is registered with an invalid type (text, select, checkbox).
*/
public function test_invalid_type_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/invalid-type';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -654,6 +683,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a field is registered with an invalid sanitize callback.
*/
public function test_invalid_sanitize_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/invalid-sanitize';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -700,6 +730,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a field is registered with an invalid validate callback.
*/
public function test_invalid_validate_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/invalid-validate';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -746,6 +777,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a field is registered with an invalid attributes prop.
*/
public function test_invalid_attribute_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/invalid-attribute';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -815,6 +847,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered if a field is registered with invalid attributes values.
*/
public function test_invalid_attributes_values_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/invalid-attribute-values';
$invalid_attributes = array( 'invalidAttribute' );
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
@@ -890,6 +923,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a select is registered without options prop.
*/
public function test_missing_select_options_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/missing-options';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -935,6 +969,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a select is registered with an invalid options array.
*/
public function test_invalid_select_options_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/invalid-options';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -982,6 +1017,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a select is registered with duplicate options.
*/
public function test_duplicate_select_options_in_registration() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/duplicate-options';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -1051,6 +1087,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a checkbox is registered with invalid required property.
*/
public function test_invalid_required_prop_checkbox() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/checkbox-bad-required-value';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -1162,6 +1199,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure a warning is triggered when a checkbox is registered with an error_message, but it is not required.
*/
public function test_error_message_non_required_checkbox() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/checkbox-non-required-error-message';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -1225,6 +1263,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure a warning is triggered when a checkbox is registered with an invalid required prop.
*/
public function test_error_message_bad_required_value_checkbox() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/checkbox-non-required-error-message';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -1284,6 +1323,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure a warning is triggered when a checkbox is registered with a non-string error_message.
*/
public function test_non_string_error_message_checkbox() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/checkbox-non-string-error-message';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -1347,6 +1387,7 @@ class AdditionalFields extends MockeryTestCase {
* Ensure an error is triggered when a field is registered with hidden set to true.
*/
public function test_register_hidden_field_error() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$id = 'plugin-namespace/hidden-field';
$doing_it_wrong_mocker = \Mockery::mock( 'ActionCallback' );
$doing_it_wrong_mocker->shouldReceive( 'doing_it_wrong_run' )->withArgs(
@@ -2548,6 +2589,7 @@ class AdditionalFields extends MockeryTestCase {
* Test for errors when providing the wrong rules schema.
*/
public function test_invalid_rules_schema() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$doing_it_wrong_mocker = $this->add_doing_it_wrong_error_mocker( 'woocommerce_register_additional_checkout_field', 'Unable to register field with id: "namespace/test-id". validation: Rules must be defined as an array.' );
\woocommerce_register_additional_checkout_field(
array(
@@ -2568,6 +2610,7 @@ class AdditionalFields extends MockeryTestCase {
* Test for errors when providing the wrong validation rules schema.
*/
public function test_invalid_validation_rules_schema() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$doing_it_wrong_mocker = $this->add_doing_it_wrong_error_mocker( 'woocommerce_register_additional_checkout_field', 'Unable to register field with id: "namespace/test-id". validation: The properties must match schema: {properties}' );
\woocommerce_register_additional_checkout_field(
array(
@@ -2626,6 +2669,7 @@ class AdditionalFields extends MockeryTestCase {
* Test for errors when providing the wrong required rules schema.
*/
public function test_invalid_required_rules_schema() {
+ $this->setExpectedIncorrectUsage( 'woocommerce_register_additional_checkout_field' );
$doing_it_wrong_mocker = $this->add_doing_it_wrong_error_mocker( 'woocommerce_register_additional_checkout_field', 'Unable to register field with id: "namespace/test-id". required: Rules must be defined as an array.' );
\woocommerce_register_additional_checkout_field(
array(
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
index f12deb2fdd5..86d42fd0e4f 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
@@ -19,66 +19,95 @@ use Automattic\WooCommerce\Enums\ProductStockStatus;
*/
class Cart extends ControllerTestCase {
+ /**
+ * Product IDs shared by the class.
+ *
+ * @var int[]
+ */
+ private static $product_ids = array();
/**
- * Setup test product data. Called before every test.
+ * Coupon ID shared by the class.
+ *
+ * @var int
*/
- protected function setUp(): void {
- parent::setUp();
+ private static $coupon_id;
+ /**
+ * Create immutable catalog rows shared by all test methods.
+ */
+ public static function wpSetUpBeforeClass(): void {
$fixtures = new FixtureData();
- $fixtures->shipping_add_flat_rate();
-
- $this->products = array(
- $fixtures->get_simple_product(
+ $products = self::create_class_fixture_products(
+ array(
array(
'name' => 'Test Product 1',
'stock_status' => ProductStockStatus::IN_STOCK,
'regular_price' => 10,
'weight' => 10,
- )
- ),
- $fixtures->get_simple_product(
+ ),
array(
'name' => 'Test Product 2',
'stock_status' => ProductStockStatus::IN_STOCK,
'regular_price' => 10,
'weight' => 10,
- )
- ),
- $fixtures->get_simple_product(
+ ),
array(
'name' => 'Test Product 3',
'stock_status' => ProductStockStatus::IN_STOCK,
'regular_price' => 10,
'weight' => 10,
- )
- ),
- $fixtures->get_simple_product(
+ ),
array(
'name' => 'Test Product 4',
'stock_status' => ProductStockStatus::IN_STOCK,
'regular_price' => 10,
'weight' => 10,
'virtual' => true,
- )
+ ),
),
);
- // Add product #3 as a cross-sell for product #1.
- $this->products[0]->set_cross_sell_ids( array( $this->products[2]->get_id() ) );
- $this->products[0]->save();
+ $products[0]->set_cross_sell_ids( array( $products[2]->get_id() ) );
+ $products[0]->save();
- $this->coupon = $fixtures->get_coupon(
+ self::$product_ids = array_map( fn( $product ) => $product->get_id(), $products );
+ self::$coupon_id = $fixtures->get_coupon(
array(
'code' => 'test_coupon',
'discount_type' => 'fixed_cart',
'amount' => 1,
)
- );
+ )->get_id();
+ }
+
+ /**
+ * Delete class products through WooCommerce data stores.
+ */
+ public static function wpTearDownAfterClass(): void {
+ try {
+ self::delete_class_fixture_products( self::$product_ids );
+ } finally {
+ $coupon = new \WC_Coupon( self::$coupon_id );
+ $coupon->delete( true );
+ }
+ }
+
+ /**
+ * Setup test product data. Called before every test.
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $fixtures = new FixtureData();
+ $fixtures->shipping_add_flat_rate();
+
+ $this->products = array_map( 'wc_get_product', self::$product_ids );
+ $this->coupon = new \WC_Coupon( self::$coupon_id );
wc_empty_cart();
$this->reset_customer_state();
+ wc()->session->set( 'wc_notices', null );
$this->keys = array();
$this->keys[] = wc()->cart->add_to_cart( $this->products[0]->get_id(), 2 );
$this->keys[] = wc()->cart->add_to_cart( $this->products[1]->get_id() );
@@ -95,16 +124,26 @@ class Cart extends ControllerTestCase {
* Resets customer state and remove any existing data from previous tests.
*/
private function reset_customer_state() {
- wc()->customer->set_billing_country( 'US' );
- wc()->customer->set_shipping_country( 'US' );
+ wc()->customer->set_billing_first_name( '' );
+ wc()->customer->set_billing_last_name( '' );
+ wc()->customer->set_billing_company( '' );
+ wc()->customer->set_billing_address_1( '' );
+ wc()->customer->set_billing_address_2( '' );
+ wc()->customer->set_billing_city( '' );
wc()->customer->set_billing_state( '' );
- wc()->customer->set_shipping_state( '' );
wc()->customer->set_billing_postcode( '' );
- wc()->customer->set_shipping_postcode( '' );
- wc()->customer->set_shipping_city( '' );
- wc()->customer->set_billing_city( '' );
+ wc()->customer->set_billing_country( 'US' );
+ wc()->customer->set_billing_email( '' );
+ wc()->customer->set_billing_phone( '' );
+ wc()->customer->set_shipping_first_name( '' );
+ wc()->customer->set_shipping_last_name( '' );
+ wc()->customer->set_shipping_company( '' );
wc()->customer->set_shipping_address_1( '' );
- wc()->customer->set_billing_address_1( '' );
+ wc()->customer->set_shipping_address_2( '' );
+ wc()->customer->set_shipping_city( '' );
+ wc()->customer->set_shipping_state( '' );
+ wc()->customer->set_shipping_postcode( '' );
+ wc()->customer->set_shipping_country( 'US' );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartItems.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartItems.php
index fec10713425..239d4e89390 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartItems.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CartItems.php
@@ -16,6 +16,123 @@ use Automattic\WooCommerce\Enums\ProductStockStatus;
* Cart Controller Tests.
*/
class CartItems extends ControllerTestCase {
+ /**
+ * Product IDs shared by the class.
+ *
+ * @var int[]
+ */
+ private static $product_ids = array();
+
+ /**
+ * Variation ID shared by the class.
+ *
+ * @var int
+ */
+ private static $variation_id;
+
+ /**
+ * All product IDs owned by the class.
+ *
+ * @var int[]
+ */
+ private static $owned_product_ids = array();
+
+ /**
+ * Attribute taxonomy IDs owned by the class.
+ *
+ * @var int[]
+ */
+ private static $attribute_ids = array();
+
+ /**
+ * Whether an attribute deletion rewrite flush was already scheduled.
+ *
+ * @var bool
+ */
+ private static $had_scheduled_rewrite_flush;
+
+ /**
+ * Create the immutable cart catalog shared by all test methods.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ $existing_attribute_ids = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_id' );
+ self::$had_scheduled_rewrite_flush = false !== wp_next_scheduled( 'woocommerce_flush_rewrite_rules' );
+
+ $fixtures = self::with_direct_product_attribute_lookup_updates(
+ static function () {
+ $fixture_data = new FixtureData();
+ $color = $fixture_data->get_product_attribute( 'color', array( 'red', 'green', 'blue' ) );
+ $size = $fixture_data->get_product_attribute( 'size', array( 'small', 'medium', 'large' ) );
+
+ foreach ( array( $color, $size ) as $attribute ) {
+ foreach ( $attribute['term_ids'] as $term_id ) {
+ $term = get_term( $term_id, $attribute['attribute_taxonomy'] );
+ wp_update_term( $term_id, $attribute['attribute_taxonomy'], array( 'slug' => sanitize_title( $term->name ) ) );
+ }
+ }
+
+ $simple = $fixture_data->get_simple_product(
+ array(
+ 'name' => 'Test Product 1',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ 'weight' => 10,
+ )
+ );
+ $variable = $fixture_data->get_variable_product(
+ array(
+ 'name' => 'Test Product 2',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ 'weight' => 10,
+ ),
+ array(
+ $color,
+ $size,
+ )
+ );
+ $variation = $fixture_data->get_variation_product(
+ $variable->get_id(),
+ array(
+ 'pa_color' => 'red',
+ 'pa_size' => 'small',
+ )
+ );
+
+ return array( $simple, $variable, $variation );
+ }
+ );
+
+ self::$product_ids = array( $fixtures[0]->get_id(), $fixtures[1]->get_id() );
+ self::$variation_id = $fixtures[2]->get_id();
+ self::$owned_product_ids = array( $fixtures[0]->get_id(), $fixtures[2]->get_id(), $fixtures[1]->get_id() );
+ self::$attribute_ids = array_values( array_diff( wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_id' ), $existing_attribute_ids ) );
+ }
+
+ /**
+ * Delete the class-owned catalog and global attributes.
+ */
+ public static function wpTearDownAfterClass(): void {
+ global $wc_product_attributes;
+
+ self::delete_class_fixture_products( self::$owned_product_ids );
+
+ foreach ( self::$attribute_ids as $attribute_id ) {
+ $attribute = wc_get_attribute( $attribute_id );
+ $taxonomy = $attribute ? $attribute->slug : '';
+
+ wc_delete_attribute( $attribute_id );
+
+ if ( $taxonomy && taxonomy_exists( $taxonomy ) ) {
+ unregister_taxonomy( $taxonomy );
+ }
+ unset( $wc_product_attributes[ $taxonomy ] );
+ }
+
+ if ( ! self::$had_scheduled_rewrite_flush ) {
+ wp_clear_scheduled_hook( 'woocommerce_flush_rewrite_rules' );
+ }
+ }
/**
* The mock logger.
@@ -30,42 +147,8 @@ class CartItems extends ControllerTestCase {
protected function setUp(): void {
parent::setUp();
- $fixtures = new FixtureData();
-
- $this->products = array(
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 1',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- 'image_id' => $fixtures->sideload_image(),
- )
- ),
- );
-
- $variable_product = $fixtures->get_variable_product(
- array(
- 'name' => 'Test Product 2',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- 'image_id' => $fixtures->sideload_image(),
- ),
- array(
- $fixtures->get_product_attribute( 'color', array( 'red', 'green', 'blue' ) ),
- $fixtures->get_product_attribute( 'size', array( 'small', 'medium', 'large' ) ),
- )
- );
- $variation = $fixtures->get_variation_product(
- $variable_product->get_id(),
- array(
- 'pa_color' => 'red',
- 'pa_size' => 'small',
- )
- );
-
- $this->products[] = $variable_product;
+ $this->products = array_map( 'wc_get_product', self::$product_ids );
+ $variation = wc_get_product( self::$variation_id );
wc_empty_cart();
$this->keys = array();
@@ -302,6 +385,9 @@ class CartItems extends ControllerTestCase {
* Tests schema of both products in cart to cover as much schema as possible.
*/
public function test_get_item_schema() {
+ // Give the simple product an image so the nested image schema is validated too.
+ $image_id = $this->add_image_to_product();
+
$routes = new \Automattic\WooCommerce\StoreApi\RoutesController( new \Automattic\WooCommerce\StoreApi\SchemaController( $this->mock_extend ) );
$controller = $routes->get( 'cart-items', 'v1' );
$schema = $controller->get_item_schema();
@@ -310,7 +396,8 @@ class CartItems extends ControllerTestCase {
// Simple product.
$response = $controller->prepare_item_for_response( current( $cart ), new \WP_REST_Request() );
- $diff = $validate->get_diff_from_object( $response->get_data() );
+ $this->assertNotEmpty( $response->get_data()['images'], 'The simple product response must include an image so its schema is exercised.' );
+ $diff = $validate->get_diff_from_object( $response->get_data() );
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
$this->assertEmpty( $diff, print_r( $diff, true ) );
@@ -319,6 +406,8 @@ class CartItems extends ControllerTestCase {
$diff = $validate->get_diff_from_object( $response->get_data() );
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
$this->assertEmpty( $diff, print_r( $diff, true ) );
+
+ wp_delete_attachment( $image_id, true );
}
/**
@@ -328,6 +417,7 @@ class CartItems extends ControllerTestCase {
* @throws \Exception When the images are not filtered correctly.
*/
public function test_cart_item_image_filtering() {
+ $image_id = $this->add_image_to_product();
$routes = new \Automattic\WooCommerce\StoreApi\RoutesController( new \Automattic\WooCommerce\StoreApi\SchemaController( $this->mock_extend ) );
$controller = $routes->get( 'cart-items', 'v1' );
$cart = WC()->cart->get_cart();
@@ -352,6 +442,7 @@ class CartItems extends ControllerTestCase {
$this->assertEquals( $image->src, 'https://example.com/image-1.jpg' );
$this->assertEquals( $image->thumbnail, 'https://example.com/image-1-thumbnail.jpg' );
remove_all_filters( 'woocommerce_store_api_cart_item_images' );
+ wp_delete_attachment( $image_id, true );
}
/**
@@ -361,6 +452,7 @@ class CartItems extends ControllerTestCase {
* @throws \Exception When the errors are not logged correctly.
*/
public function test_cart_item_image_filtering_logging() {
+ $image_id = $this->add_image_to_product();
$routes = new \Automattic\WooCommerce\StoreApi\RoutesController( new \Automattic\WooCommerce\StoreApi\SchemaController( $this->mock_extend ) );
$controller = $routes->get( 'cart-items', 'v1' );
$cart = WC()->cart->get_cart();
@@ -436,6 +528,23 @@ class CartItems extends ControllerTestCase {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
$this->assertEquals( $image->src, $expected_image );
$this->assertEquals( $image->thumbnail, $expected_image );
+ wp_delete_attachment( $image_id, true );
+ }
+
+ /**
+ * Add an image to the simple product used by image-filter tests.
+ *
+ * @return int Attachment ID.
+ */
+ private function add_image_to_product(): int {
+ $fixtures = new FixtureData();
+ $image_id = $fixtures->create_image_attachment( $this->products[0]->get_id() );
+ $this->products[0]->set_image_id( $image_id );
+ $this->products[0]->save();
+ $cart_item = WC()->cart->get_cart_item( $this->keys[0] );
+ $cart_item['data']->set_image_id( $image_id );
+
+ return $image_id;
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
index e95f92d79b4..9c3e38847bc 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
@@ -18,7 +18,7 @@ use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
use Automattic\WooCommerce\Enums\ProductStockStatus;
-use Mockery\Adapter\Phpunit\MockeryTestCase;
+use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use WC_Gateway_BACS;
/**
@@ -26,9 +26,90 @@ use WC_Gateway_BACS;
*
* phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_print_r, WooCommerce.Commenting.CommentHooks.MissingHookComment
*/
-class Checkout extends MockeryTestCase {
+class Checkout extends \WP_Test_REST_TestCase {
+ use MockeryPHPUnitIntegration;
+ use StoreApiRestTestCaseTrait;
const TEST_COUPON_CODE = 'test_coupon_code';
+
+ /**
+ * Product IDs shared by the class.
+ *
+ * @var int[]
+ */
+ private static $product_ids = array();
+
+ /**
+ * Coupon ID shared by the class.
+ *
+ * @var int
+ */
+ private static $coupon_id;
+
+ /**
+ * Payment gateway registry before the test.
+ *
+ * @var array
+ */
+ private $payment_gateways_before_test = array();
+
+ /**
+ * PayPal gateway singleton before the test.
+ *
+ * @var \WC_Gateway_Paypal
+ */
+ private $paypal_gateway_before_test;
+
+ /**
+ * Create immutable catalog rows shared by all test methods.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ self::$product_ids = array_map(
+ fn( $product ) => $product->get_id(),
+ self::create_class_fixture_products(
+ array(
+ array(
+ 'name' => 'Test Product 1',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ 'weight' => 10,
+ ),
+ array(
+ 'name' => 'Test Product 2',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ 'weight' => 10,
+ ),
+ array(
+ 'name' => 'Virtual Test Product 2',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ 'weight' => 10,
+ 'virtual' => true,
+ ),
+ ),
+ )
+ );
+
+ $coupon = new \WC_Coupon();
+ $coupon->set_code( self::TEST_COUPON_CODE );
+ $coupon->set_amount( 2 );
+ $coupon->save();
+ self::$coupon_id = $coupon->get_id();
+ }
+
+ /**
+ * Delete class products through WooCommerce data stores.
+ */
+ public static function wpTearDownAfterClass(): void {
+ try {
+ self::delete_class_fixture_products( self::$product_ids );
+ } finally {
+ $coupon = new \WC_Coupon( self::$coupon_id );
+ $coupon->delete( true );
+ }
+ }
+
/**
* Setup test product data. Called before every test.
*/
@@ -37,20 +118,14 @@ class Checkout extends MockeryTestCase {
add_filter( 'woocommerce_set_cookie_enabled', array( $this, 'filter_woocommerce_set_cookie_enabled' ), 10, 4 );
+ update_option( 'woocommerce_checkout_phone_field', 'optional' );
update_option( 'woocommerce_enable_guest_checkout', 'yes' );
update_option( 'woocommerce_enable_signup_and_login_from_checkout', 'yes' );
- global $wp_rest_server;
- $wp_rest_server = new \Spy_REST_Server();
- do_action( 'rest_api_init', $wp_rest_server );
+ $this->initialize_store_api_server();
wp_set_current_user( 0 );
- $coupon = new \WC_Coupon();
- $coupon->set_code( self::TEST_COUPON_CODE );
- $coupon->set_amount( 2 );
- $coupon->save();
-
$formatters = new Formatters();
$formatters->register( 'money', MoneyFormatter::class );
$formatters->register( 'html', HtmlFormatter::class );
@@ -77,38 +152,15 @@ class Checkout extends MockeryTestCase {
$order_route = new CheckoutOrderRoute( $schema_controller, $schema_controller->get( 'checkout-order' ) );
register_rest_route( $order_route->get_namespace(), $order_route->get_path(), $order_route->get_args(), true );
+ $this->payment_gateways_before_test = WC()->payment_gateways()->payment_gateways;
+ $this->paypal_gateway_before_test = \WC_Gateway_Paypal::get_instance();
+
$fixtures = new FixtureData();
$fixtures->payments_enable_bacs();
$fixtures->shipping_add_pickup_location();
$fixtures->shipping_add_flat_rate_instance();
- $this->products = array(
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 1',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- )
- ),
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 2',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- )
- ),
- $fixtures->get_simple_product(
- array(
- 'name' => 'Virtual Test Product 2',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- 'virtual' => true,
- )
- ),
- );
+ $this->products = array_map( 'wc_get_product', self::$product_ids );
wc_empty_cart();
wc()->cart->add_to_cart( $this->products[0]->get_id(), 2 );
wc()->cart->add_to_cart( $this->products[1]->get_id(), 1 );
@@ -118,39 +170,70 @@ class Checkout extends MockeryTestCase {
* Tear down Rest API server.
*/
protected function tearDown(): void {
- parent::tearDown();
-
- remove_filter( 'woocommerce_set_cookie_enabled', array( $this, 'filter_woocommerce_set_cookie_enabled' ) );
-
- remove_all_filters( 'woocommerce_get_country_locale' );
- remove_all_filters( 'woocommerce_register_shop_order_post_statuses' );
- remove_all_filters( 'wc_order_statuses' );
- remove_all_actions( 'woocommerce_checkout_validate_order_before_payment' );
- remove_all_actions( 'woocommerce_store_api_checkout_order_processed' );
- remove_all_actions( 'woocommerce_valid_order_statuses_for_payment' );
-
- update_option( 'woocommerce_ship_to_countries', 'all' );
- update_option( 'woocommerce_allowed_countries', 'all' );
- update_option( 'woocommerce_enable_guest_checkout', 'yes' );
- update_option( 'woocommerce_enable_signup_and_login_from_checkout', 'yes' );
-
- $fixtures = new FixtureData();
- $fixtures->shipping_remove_pickup_location();
- $fixtures->shipping_remove_methods_from_default_zone();
+ try {
+ remove_filter( 'woocommerce_set_cookie_enabled', array( $this, 'filter_woocommerce_set_cookie_enabled' ) );
+
+ remove_all_filters( 'woocommerce_get_country_locale' );
+ remove_all_filters( 'woocommerce_register_shop_order_post_statuses' );
+ remove_all_filters( 'wc_order_statuses' );
+ remove_all_actions( 'woocommerce_checkout_validate_order_before_payment' );
+ remove_all_actions( 'woocommerce_store_api_checkout_order_processed' );
+ remove_all_actions( 'woocommerce_valid_order_statuses_for_payment' );
+
+ update_option( 'woocommerce_ship_to_countries', 'all' );
+ update_option( 'woocommerce_allowed_countries', 'all' );
+ update_option( 'woocommerce_enable_guest_checkout', 'yes' );
+ update_option( 'woocommerce_enable_signup_and_login_from_checkout', 'yes' );
+
+ $fixtures = new FixtureData();
+ $fixtures->shipping_remove_pickup_location();
+ $fixtures->shipping_remove_methods_from_default_zone();
+
+ $customer_to_delete = get_user_by( 'email', 'testaccount@test.com' );
+ if ( $customer_to_delete ) {
+ wp_delete_user( $customer_to_delete->ID );
+ }
- $coupon_to_delete = new \WC_Coupon( self::TEST_COUPON_CODE );
- $coupon_to_delete->delete( true );
+ unset( WC()->countries->locale );
+ WC()->cart->empty_cart();
+ WC()->session->destroy_session();
+ WC()->customer = null;
+ WC()->initialize_cart();
- $customer_to_delete = get_user_by( 'email', 'testaccount@test.com' );
- if ( $customer_to_delete ) {
- wp_delete_user( $customer_to_delete->ID );
+ $GLOBALS['wp_rest_server'] = null;
+ } finally {
+ try {
+ parent::tearDown();
+ } finally {
+ $this->invalidate_checkout_option_caches();
+ WC()->payment_gateways()->payment_gateways = $this->payment_gateways_before_test;
+ \WC_Gateway_Paypal::set_instance( $this->paypal_gateway_before_test );
+ }
}
+ }
- unset( WC()->countries->locale );
- WC()->cart->empty_cart();
- WC()->session->destroy_session();
-
- $GLOBALS['wp_rest_server'] = null;
+ /**
+ * Invalidate caches for options modified by checkout tests.
+ */
+ private function invalidate_checkout_option_caches(): void {
+ $option_names = array(
+ 'woocommerce_checkout_phone_field',
+ 'woocommerce_enable_guest_checkout',
+ 'woocommerce_enable_signup_and_login_from_checkout',
+ 'woocommerce_ship_to_countries',
+ 'woocommerce_allowed_countries',
+ 'woocommerce_specific_ship_to_countries',
+ 'woocommerce_specific_allowed_countries',
+ 'woocommerce_bacs_settings',
+ 'woocommerce_pickup_location_settings',
+ 'pickup_location_pickup_locations',
+ );
+
+ foreach ( $option_names as $option_name ) {
+ wp_cache_delete( $option_name, 'options' );
+ }
+ wp_cache_delete( 'alloptions', 'options' );
+ wp_cache_delete( 'notoptions', 'options' );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessions.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessions.php
index 34afa0c1d80..49a4cc094b9 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessions.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessions.php
@@ -22,6 +22,13 @@ use Automattic\WooCommerce\Tests\Internal\Admin\Agentic\AgenticTestHelpers;
class CheckoutSessions extends ControllerTestCase {
use AgenticTestHelpers;
+ /**
+ * Product IDs shared by the class.
+ *
+ * @var int[]
+ */
+ private static $product_ids = array();
+
/**
* Products created for tests.
*
@@ -29,6 +36,44 @@ class CheckoutSessions extends ControllerTestCase {
*/
protected $products = array();
+ /**
+ * Create immutable product rows shared by all test methods.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ self::$product_ids = array_map(
+ fn( $product ) => $product->get_id(),
+ self::create_class_fixture_products(
+ array(
+ array(
+ 'name' => 'Test Product 1',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ 'weight' => 10,
+ ),
+ array(
+ 'name' => 'Test Product 2',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 20,
+ 'weight' => 5,
+ ),
+ array(
+ 'name' => 'Virtual Product',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 15,
+ 'virtual' => true,
+ ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Delete class products through WooCommerce data stores.
+ */
+ public static function wpTearDownAfterClass(): void {
+ self::delete_class_fixture_products( self::$product_ids );
+ }
+
/**
* Setup test product data. Called before every test.
*/
@@ -53,32 +98,7 @@ class CheckoutSessions extends ControllerTestCase {
$fixtures = new FixtureData();
$fixtures->shipping_add_flat_rate();
- $this->products = array(
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 1',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- )
- ),
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 2',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 20,
- 'weight' => 5,
- )
- ),
- $fixtures->get_simple_product(
- array(
- 'name' => 'Virtual Product',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 15,
- 'virtual' => true,
- )
- ),
- );
+ $this->products = array_map( 'wc_get_product', self::$product_ids );
wc_get_container()->get( RoutesController::class )->register_all_routes();
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessionsComplete.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessionsComplete.php
index 9b691f32181..99b81325157 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessionsComplete.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/CheckoutSessionsComplete.php
@@ -21,6 +21,44 @@ use Automattic\WooCommerce\Tests\Internal\Admin\Agentic\AgenticTestHelpers;
class CheckoutSessionsComplete extends ControllerTestCase {
use AgenticTestHelpers;
+ /**
+ * Product IDs shared by the class.
+ *
+ * @var int[]
+ */
+ private static $product_ids = array();
+
+ /**
+ * Create immutable products shared by all test methods.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ $products = self::create_class_fixture_products(
+ array(
+ array(
+ 'name' => 'Test Product 1',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ 'weight' => 10,
+ ),
+ array(
+ 'name' => 'Test Product 2',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 20,
+ 'weight' => 5,
+ ),
+ )
+ );
+
+ self::$product_ids = array_map( static fn( $product ) => $product->get_id(), $products );
+ }
+
+ /**
+ * Delete class products through WooCommerce data stores.
+ */
+ public static function wpTearDownAfterClass(): void {
+ self::delete_class_fixture_products( self::$product_ids );
+ }
+
/**
* Products created for tests.
*
@@ -35,11 +73,21 @@ class CheckoutSessionsComplete extends ControllerTestCase {
*/
protected $mock_gateway;
+ /**
+ * Option state to restore after each test.
+ *
+ * @var array<string, array{exists: bool, value: mixed}>
+ */
+ private $option_state = array();
+
/**
* Setup test product data. Called before every test.
*/
protected function setUp(): void {
parent::setUp();
+ $this->snapshot_option_state( 'woocommerce_checkout_phone_field' );
+ $this->snapshot_option_state( 'woocommerce_feature_agentic_checkout_enabled' );
+ update_option( 'woocommerce_checkout_phone_field', 'optional' );
// Reset customer and cart FIRST before anything else.
wc_empty_cart();
@@ -59,24 +107,7 @@ class CheckoutSessionsComplete extends ControllerTestCase {
$fixtures = new FixtureData();
$fixtures->shipping_add_flat_rate();
- $this->products = array(
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 1',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => 10,
- )
- ),
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 2',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 20,
- 'weight' => 5,
- )
- ),
- );
+ $this->products = array_map( 'wc_get_product', self::$product_ids );
// Register mock agentic payment gateway.
$this->mock_gateway = new MockAgenticPaymentGateway();
@@ -90,18 +121,59 @@ class CheckoutSessionsComplete extends ControllerTestCase {
* Tear down test.
*/
protected function tearDown(): void {
- parent::tearDown();
- delete_option( 'woocommerce_feature_agentic_checkout_enabled' );
+ try {
+ // Clear session data.
+ WC()->session->set( SessionKey::CHOSEN_SHIPPING_METHODS, null );
+ WC()->session->set( SessionKey::AGENTIC_CHECKOUT_SESSION_ID, null );
+
+ // Reset customer state to clean state.
+ $this->reset_customer_state();
+
+ // Reset Jetpack auth state.
+ $this->reset_jetpack_auth_state();
+
+ } finally {
+ remove_filter( 'woocommerce_payment_gateways', array( $this, 'add_mock_gateway' ) );
+ remove_filter( 'woocommerce_available_payment_gateways', array( $this, 'add_mock_gateway' ) );
+ try {
+ parent::tearDown();
+ } finally {
+ $this->restore_option_state();
+ }
+ }
+ }
- // Clear session data.
- WC()->session->set( SessionKey::CHOSEN_SHIPPING_METHODS, null );
- WC()->session->set( SessionKey::AGENTIC_CHECKOUT_SESSION_ID, null );
+ /**
+ * Capture an option's exact existence and value before changing it.
+ *
+ * @param string $option_name Option name.
+ */
+ private function snapshot_option_state( string $option_name ): void {
+ $missing_option = new \stdClass();
+ $value = get_option( $option_name, $missing_option );
- // Reset customer state to clean state.
- $this->reset_customer_state();
+ $this->option_state[ $option_name ] = array(
+ 'exists' => $missing_option !== $value,
+ 'value' => $value,
+ );
+ }
+
+ /**
+ * Restore options changed by the test, including their original absence.
+ */
+ private function restore_option_state(): void {
+ foreach ( $this->option_state as $option_name => $state ) {
+ wp_cache_delete( $option_name, 'options' );
+ wp_cache_delete( 'alloptions', 'options' );
+ wp_cache_delete( 'notoptions', 'options' );
+ if ( $state['exists'] ) {
+ update_option( $option_name, $state['value'] );
+ } else {
+ delete_option( $option_name );
+ }
+ }
- // Reset Jetpack auth state.
- $this->reset_jetpack_auth_state();
+ $this->option_state = array();
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ControllerTestCase.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ControllerTestCase.php
index e42f5798b3a..7144859071d 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ControllerTestCase.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ControllerTestCase.php
@@ -15,6 +15,8 @@ use Automattic\WooCommerce\StoreApi\Formatters\CurrencyFormatter;
* ControllerTestCase class.
*/
abstract class ControllerTestCase extends \WP_Test_REST_TestCase {
+ use StoreApiRestTestCaseTrait;
+
/**
* ExtendSchema class instance.
*
@@ -28,11 +30,7 @@ abstract class ControllerTestCase extends \WP_Test_REST_TestCase {
protected function setUp(): void {
parent::setUp();
- /** @var \WP_REST_Server $wp_rest_server */
- global $wp_rest_server;
- $wp_rest_server = new \Spy_REST_Server();
- // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
- do_action( 'rest_api_init', $wp_rest_server );
+ $this->initialize_store_api_server();
wp_set_current_user( 0 );
update_option( 'woocommerce_weight_unit', 'g' );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Order.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Order.php
index 7a523954894..2ea7d7db47f 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Order.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Order.php
@@ -7,14 +7,64 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes;
-use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
-
/**
* Order Route Tests.
*
* Tests for the /wc/store/v1/order endpoint, focusing on authorization.
*/
class Order extends ControllerTestCase {
+ /**
+ * Product ID shared by the class.
+ *
+ * @var int
+ */
+ private static $product_id;
+
+ /**
+ * Customer IDs shared by the class.
+ *
+ * @var int[]
+ */
+ private static $customer_ids = array();
+
+ /**
+ * Create immutable fixtures shared by all test methods.
+ *
+ * @param \WP_UnitTest_Factory $factory WordPress unit test factory.
+ */
+ public static function wpSetUpBeforeClass( $factory ): void {
+ $product = self::create_class_fixture_products(
+ array(
+ array(
+ 'name' => 'Test Product',
+ 'regular_price' => 10,
+ ),
+ )
+ )[0];
+ self::$product_id = $product->get_id();
+
+ self::$customer_ids = array(
+ $factory->user->create(
+ array(
+ 'role' => 'customer',
+ 'user_email' => 'customer1@test.com',
+ )
+ ),
+ $factory->user->create(
+ array(
+ 'role' => 'customer',
+ 'user_email' => 'customer2@test.com',
+ )
+ ),
+ );
+ }
+
+ /**
+ * Delete the class-owned product through its data store.
+ */
+ public static function wpTearDownAfterClass(): void {
+ self::delete_class_fixture_products( array( self::$product_id ) );
+ }
/**
* Test product.
@@ -43,41 +93,9 @@ class Order extends ControllerTestCase {
protected function setUp(): void {
parent::setUp();
- $fixtures = new FixtureData();
- $this->product = $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product',
- 'regular_price' => 10,
- )
- );
-
- // Create test customers.
- $this->customer_id = $this->factory->user->create(
- array(
- 'role' => 'customer',
- 'user_email' => 'customer1@test.com',
- )
- );
- $this->customer_id_2 = $this->factory->user->create(
- array(
- 'role' => 'customer',
- 'user_email' => 'customer2@test.com',
- )
- );
- }
-
- /**
- * Tear down test data.
- */
- protected function tearDown(): void {
- parent::tearDown();
-
- if ( $this->customer_id ) {
- wp_delete_user( $this->customer_id );
- }
- if ( $this->customer_id_2 ) {
- wp_delete_user( $this->customer_id_2 );
- }
+ $this->product = wc_get_product( self::$product_id );
+ $this->customer_id = self::$customer_ids[0];
+ $this->customer_id_2 = self::$customer_ids[1];
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Patterns.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Patterns.php
index a437a75f259..73cda1d2f7c 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Patterns.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Patterns.php
@@ -3,20 +3,39 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes;
+use Automattic\WooCommerce\Blocks\Patterns\PTKClient;
use Automattic\WooCommerce\Blocks\Patterns\PTKPatternsStore;
/**
* Patterns Controller Tests.
*/
class Patterns extends ControllerTestCase {
+ /**
+ * Number of PTK API requests made by the test.
+ *
+ * @var int
+ */
+ private $ptk_request_count = 0;
+
/**
* Set up user for tests.
*/
public function setUp(): void {
parent::setUp();
+ $this->ptk_request_count = 0;
$user = $this->factory->user->create( array( 'role' => 'administrator' ) );
wp_set_current_user( $user );
+
+ add_filter( 'pre_http_request', array( $this, 'mock_ptk_response' ), 10, 3 );
+ }
+
+ /**
+ * Remove the scoped PTK HTTP mock.
+ */
+ public function tearDown(): void {
+ remove_filter( 'pre_http_request', array( $this, 'mock_ptk_response' ), 10 );
+ parent::tearDown();
}
/**
@@ -35,6 +54,7 @@ class Patterns extends ControllerTestCase {
$patterns = get_option( PTKPatternsStore::OPTION_NAME );
$this->assertFalse( $patterns );
+ $this->assertSame( 0, $this->ptk_request_count );
}
/**
@@ -51,7 +71,66 @@ class Patterns extends ControllerTestCase {
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( true, $data['success'] );
- $patterns = get_option( PTKPatternsStore::OPTION_NAME );
- $this->assertNotFalse( $patterns );
+ $expected_patterns = array(
+ array(
+ 'ID' => 14870,
+ 'site_id' => 174455321,
+ 'title' => 'Review: A quote with scattered images',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
+ 'categories' => array(
+ 'reviews' => array(
+ 'slug' => 'reviews',
+ 'title' => 'Reviews',
+ ),
+ ),
+ ),
+ );
+
+ $this->assertSame( 1, $this->ptk_request_count );
+ $this->assertSame( $expected_patterns, get_option( PTKPatternsStore::OPTION_NAME ) );
+ }
+
+ /**
+ * Return a deterministic response for PTK API requests.
+ *
+ * @param mixed $preempt Whether to preempt the HTTP request.
+ * @param array $parsed_args HTTP request arguments.
+ * @param string $url Request URL.
+ * @return mixed
+ */
+ public function mock_ptk_response( $preempt, $parsed_args, $url ) {
+ if ( 0 !== strpos( $url, PTKClient::PATTERNS_TOOLKIT_URL ) ) {
+ return $preempt;
+ }
+
+ ++$this->ptk_request_count;
+
+ return array(
+ 'headers' => array(),
+ 'body' => wp_json_encode(
+ array(
+ array(
+ 'ID' => 14870,
+ 'site_id' => 174455321,
+ 'title' => 'Review: A quote with scattered images',
+ 'name' => 'review-a-quote-with-scattered-images',
+ 'html' => '<!-- /wp:spacer -->',
+ 'categories' => array(
+ 'testimonials' => array(
+ 'slug' => 'testimonials',
+ 'title' => 'Testimonials',
+ 'description' => 'Share reviews and feedback about your brand/business.',
+ ),
+ ),
+ ),
+ )
+ ),
+ 'response' => array(
+ 'code' => 200,
+ 'message' => 'OK',
+ ),
+ 'cookies' => array(),
+ );
}
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductCollectionData.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductCollectionData.php
index 6524ff25242..ff965fc6611 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductCollectionData.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ProductCollectionData.php
@@ -13,6 +13,42 @@ use Automattic\WooCommerce\Tests\Blocks\Helpers\ValidateSchema;
* Controller Tests.
*/
class ProductCollectionData extends ControllerTestCase {
+ /**
+ * Product IDs shared by the class.
+ *
+ * @var int[]
+ */
+ private static $product_ids = array();
+
+ /**
+ * Create immutable products and reviews shared by all test methods.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ $products = self::create_class_fixture_products(
+ array(
+ array(
+ 'name' => 'Test Product 1',
+ 'regular_price' => 10,
+ ),
+ array(
+ 'name' => 'Test Product 2',
+ 'regular_price' => 100,
+ ),
+ )
+ );
+ $fixtures = new FixtureData();
+ $fixtures->add_product_review( $products[0]->get_id(), 5 );
+ $fixtures->add_product_review( $products[1]->get_id(), 4 );
+
+ self::$product_ids = array_map( static fn( $product ) => $product->get_id(), $products );
+ }
+
+ /**
+ * Delete class products and their reviews through WooCommerce data stores.
+ */
+ public static function wpTearDownAfterClass(): void {
+ self::delete_class_fixture_products( self::$product_ids );
+ }
/**
* Product attributes created during a test.
@@ -28,26 +64,7 @@ class ProductCollectionData extends ControllerTestCase {
parent::setUp();
$this->created_product_attributes = array();
-
- $fixtures = new FixtureData();
-
- $this->products = array(
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 1',
- 'regular_price' => 10,
- )
- ),
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 2',
- 'regular_price' => 100,
- )
- ),
- );
-
- $fixtures->add_product_review( $this->products[0]->get_id(), 5 );
- $fixtures->add_product_review( $this->products[1]->get_id(), 4 );
+ $this->products = array_map( 'wc_get_product', self::$product_ids );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
index b6305546af5..6c3d6e87fa5 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Products.php
@@ -15,38 +15,75 @@ use Automattic\WooCommerce\Enums\ProductStockStatus;
* Products Controller Tests.
*/
class Products extends ControllerTestCase {
+ /**
+ * Product IDs shared by the class.
+ *
+ * @var int[]
+ */
+ private static $product_ids = array();
+
+ /**
+ * All product IDs owned by the class, including grouped children.
+ *
+ * @var int[]
+ */
+ private static $owned_product_ids = array();
+
+ /**
+ * Create immutable catalog rows shared by all test methods.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ $products = self::with_direct_product_attribute_lookup_updates(
+ static function () {
+ $fixtures = new FixtureData();
+
+ return array(
+ $fixtures->get_simple_product(
+ array(
+ 'name' => 'Test Product 1',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ 'weight' => '2.5',
+ 'length' => '10',
+ 'width' => '5',
+ 'height' => '3',
+ )
+ ),
+ $fixtures->get_simple_product(
+ array(
+ 'name' => 'Test Product 2',
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ 'regular_price' => 10,
+ )
+ ),
+ $fixtures->get_grouped_product( array() ),
+ );
+ }
+ );
+
+ self::$product_ids = array_map(
+ static fn( $product ) => $product->get_id(),
+ $products
+ );
+ self::$owned_product_ids = array_merge( self::$product_ids, $products[2]->get_children() );
+ }
+
+ /**
+ * Delete class products through WooCommerce data stores.
+ */
+ public static function wpTearDownAfterClass(): void {
+ self::delete_class_fixture_products( self::$owned_product_ids );
+ }
/**
- * Setup test product data. Called before every test.
+ * Reload shared test product data before every test.
*/
protected function setUp(): void {
parent::setUp();
- $fixtures = new FixtureData();
-
- $this->products = array(
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 1',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'weight' => '2.5',
- 'length' => '10',
- 'width' => '5',
- 'height' => '3',
- 'image_id' => $fixtures->sideload_image(),
- 'gallery_image_ids' => array(),
- )
- ),
- $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product 2',
- 'stock_status' => ProductStockStatus::IN_STOCK,
- 'regular_price' => 10,
- 'image_id' => $fixtures->sideload_image(),
- )
- ),
- $fixtures->get_grouped_product( array() ),
+ $this->products = array_map(
+ 'wc_get_product',
+ self::$product_ids
);
}
@@ -54,6 +91,11 @@ class Products extends ControllerTestCase {
* Test getting item.
*/
public function test_get_item() {
+ $fixtures = new FixtureData();
+ $image_id = $fixtures->create_image_attachment( $this->products[0]->get_id() );
+ $this->products[0]->set_image_id( $image_id );
+ $this->products[0]->save();
+
$response = rest_get_server()->dispatch( new \WP_REST_Request( 'GET', '/wc/store/v1/products/' . $this->products[0]->get_id() ) );
$data = $response->get_data();
@@ -84,6 +126,10 @@ class Products extends ControllerTestCase {
$this->assertCount( 1, $data['images'] );
$this->assertIsObject( $data['images'][0] );
$this->assertEquals( $this->products[0]->get_image_id(), $data['images'][0]->id );
+ $this->assertNotEmpty( wp_parse_url( $data['images'][0]->src, PHP_URL_HOST ) );
+ $this->assertNotEmpty( wp_parse_url( $data['images'][0]->thumbnail, PHP_URL_HOST ) );
+
+ wp_delete_attachment( $image_id, true );
}
/**
@@ -113,11 +159,21 @@ class Products extends ControllerTestCase {
* Test getting items.
*/
public function test_get_items() {
- $response = rest_get_server()->dispatch( new \WP_REST_Request( 'GET', '/wc/store/v1/products' ) );
- $data = $response->get_data();
+ $product_ids = array_merge(
+ array_map(
+ function ( $product ) {
+ return $product->get_id();
+ },
+ $this->products
+ ),
+ $this->products[2]->get_children()
+ );
+ $request = new \WP_REST_Request( 'GET', '/wc/store/v1/products' );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
- $this->assertEquals( 6, count( $data ) );
+ $this->assertEqualsCanonicalizing( $product_ids, array_column( $data, 'id' ) );
$this->assertArrayHasKey( 'id', $data[0] );
$this->assertArrayHasKey( 'name', $data[0] );
$this->assertArrayHasKey( 'variation', $data[0] );
@@ -231,14 +287,23 @@ class Products extends ControllerTestCase {
* Test schema matches responses.
*/
public function test_get_item_schema() {
+ // Give the product an image so the nested image schema is validated too.
+ $fixtures = new FixtureData();
+ $image_id = $fixtures->create_image_attachment( $this->products[0]->get_id() );
+ $this->products[0]->set_image_id( $image_id );
+ $this->products[0]->save();
+
$routes = new \Automattic\WooCommerce\StoreApi\RoutesController( new \Automattic\WooCommerce\StoreApi\SchemaController( $this->mock_extend ) );
$controller = $routes->get( 'products' );
$schema = $controller->get_item_schema();
$response = $controller->prepare_item_for_response( $this->products[0], new \WP_REST_Request() );
$validate = new ValidateSchema( $schema );
+ $this->assertNotEmpty( $response->get_data()['images'], 'The product response must include an image so its schema is exercised.' );
$diff = $validate->get_diff_from_object( $response->get_data() );
$this->assertEmpty( $diff, print_r( $diff, true ) ); // @phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
+
+ wp_delete_attachment( $image_id, true );
}
/**
@@ -262,7 +327,7 @@ class Products extends ControllerTestCase {
$this->assertIsArray( $data['images'] );
$this->assertCount( 0, $data['images'] );
- $image_id = $fixtures->sideload_image();
+ $image_id = $fixtures->create_image_attachment();
$product = $fixtures->get_simple_product(
array(
'name' => 'Test Product 1',
@@ -286,7 +351,7 @@ class Products extends ControllerTestCase {
*/
public function test_product_category_image_return_types() {
$fixtures = new FixtureData();
- $image_id = $fixtures->sideload_image();
+ $image_id = $fixtures->create_image_attachment();
$term = wp_insert_term( 'Test Category', 'product_cat' );
update_term_meta( $term['term_id'], 'thumbnail_id', $image_id );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ShopperLists.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ShopperLists.php
index 530e565f389..5013794ee11 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ShopperLists.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/ShopperLists.php
@@ -7,12 +7,86 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes;
-use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
-
/**
* Tests for the /wc/store/v1/shopper-lists/* endpoints.
*/
class ShopperLists extends ControllerTestCase {
+ /**
+ * Product ID shared by the class.
+ *
+ * @var int
+ */
+ private static $product_id;
+
+ /**
+ * Customer IDs shared by the class.
+ *
+ * @var int[]
+ */
+ private static $customer_ids = array();
+
+ /**
+ * Whether the feature option existed before class setup.
+ *
+ * @var bool
+ */
+ private static $had_feature_option;
+
+ /**
+ * Feature option value before class setup.
+ *
+ * @var mixed
+ */
+ private static $feature_option;
+
+ /**
+ * Create immutable fixtures before route registration starts.
+ *
+ * @param \WP_UnitTest_Factory $factory WordPress unit test factory.
+ */
+ public static function wpSetUpBeforeClass( $factory ): void {
+ self::$feature_option = get_option( 'woocommerce_cart_save_for_later_enabled' );
+ self::$had_feature_option = false !== self::$feature_option;
+ update_option( 'woocommerce_cart_save_for_later_enabled', 'yes' );
+
+ $product = self::create_class_fixture_products(
+ array(
+ array(
+ 'name' => 'Test Product',
+ 'regular_price' => 10,
+ ),
+ )
+ )[0];
+ self::$product_id = $product->get_id();
+
+ self::$customer_ids = array(
+ $factory->user->create(
+ array(
+ 'role' => 'customer',
+ 'user_email' => 'shopper-lists-1@test.com',
+ )
+ ),
+ $factory->user->create(
+ array(
+ 'role' => 'customer',
+ 'user_email' => 'shopper-lists-2@test.com',
+ )
+ ),
+ );
+ }
+
+ /**
+ * Delete class fixtures and restore the incoming feature option.
+ */
+ public static function wpTearDownAfterClass(): void {
+ self::delete_class_fixture_products( array( self::$product_id ) );
+
+ if ( self::$had_feature_option ) {
+ update_option( 'woocommerce_cart_save_for_later_enabled', self::$feature_option );
+ } else {
+ delete_option( 'woocommerce_cart_save_for_later_enabled' );
+ }
+ }
/**
* Test product.
@@ -39,49 +113,11 @@ class ShopperLists extends ControllerTestCase {
* Setup test data.
*/
protected function setUp(): void {
- // The shopper-lists routes are gated behind the `cart_save_for_later`
- // feature flag, which is read inside `do_action( 'rest_api_init' )`
- // fired by parent::setUp(). The option must be in place before then.
- update_option( 'woocommerce_cart_save_for_later_enabled', 'yes' );
-
parent::setUp();
- $fixtures = new FixtureData();
- $this->product = $fixtures->get_simple_product(
- array(
- 'name' => 'Test Product',
- 'regular_price' => 10,
- )
- );
-
- $this->customer_id = $this->factory->user->create(
- array(
- 'role' => 'customer',
- 'user_email' => 'shopper-lists-1@test.com',
- )
- );
- $this->other_customer_id = $this->factory->user->create(
- array(
- 'role' => 'customer',
- 'user_email' => 'shopper-lists-2@test.com',
- )
- );
- }
-
- /**
- * Tear down test data.
- */
- protected function tearDown(): void {
- parent::tearDown();
-
- if ( $this->customer_id ) {
- wp_delete_user( $this->customer_id );
- }
- if ( $this->other_customer_id ) {
- wp_delete_user( $this->other_customer_id );
- }
-
- delete_option( 'woocommerce_cart_save_for_later_enabled' );
+ $this->product = wc_get_product( self::$product_id );
+ $this->customer_id = self::$customer_ids[0];
+ $this->other_customer_id = self::$customer_ids[1];
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/StoreApiRestTestCaseTrait.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/StoreApiRestTestCaseTrait.php
new file mode 100644
index 00000000000..124df582945
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/StoreApiRestTestCaseTrait.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Shared REST server setup for Store API route tests.
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes;
+
+/**
+ * Provides namespace-scoped Store API route registration.
+ */
+trait StoreApiRestTestCaseTrait {
+ /**
+ * Run class fixture changes without leaving asynchronous lookup actions.
+ *
+ * @param callable $callback Fixture lifecycle callback.
+ * @return mixed
+ */
+ protected static function with_direct_product_attribute_lookup_updates( callable $callback ) {
+ return \WC_Unit_Test_Case::with_direct_product_attribute_lookup_updates( $callback );
+ }
+
+ /**
+ * Create class-owned products without leaving asynchronous lookup actions.
+ *
+ * @param array[] $product_properties Product properties for each fixture.
+ * @return \WC_Product[]
+ */
+ protected static function create_class_fixture_products( array $product_properties ): array {
+ return self::with_direct_product_attribute_lookup_updates(
+ static function () use ( $product_properties ) {
+ $fixtures = new \Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData();
+
+ return array_map(
+ static function ( array $properties ) use ( $fixtures ) {
+ return $fixtures->get_simple_product( $properties );
+ },
+ $product_properties
+ );
+ }
+ );
+ }
+
+ /**
+ * Delete class-owned products through WooCommerce data stores.
+ *
+ * @param int[] $product_ids Product IDs to delete.
+ */
+ protected static function delete_class_fixture_products( array $product_ids ): void {
+ self::with_direct_product_attribute_lookup_updates(
+ static function () use ( $product_ids ) {
+ foreach ( $product_ids as $product_id ) {
+ $product = wc_get_product( $product_id );
+ if ( $product ) {
+ $product->delete( true );
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Create a REST server with only the relevant WooCommerce namespace loaded.
+ */
+ protected function initialize_store_api_server(): void {
+ /** @var \WP_REST_Server $wp_rest_server */
+ global $wp_rest_server;
+ $wp_rest_server = new \Spy_REST_Server();
+
+ \WC_Unit_Test_Case::with_rest_route_context(
+ '/wc/store/v1',
+ static function () use ( $wp_rest_server ) {
+ // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+ do_action( 'rest_api_init', $wp_rest_server );
+ }
+ );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/OrderControllerTests.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/OrderControllerTests.php
index 504fb84163e..fd4e6f33bae 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/OrderControllerTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/OrderControllerTests.php
@@ -15,6 +15,20 @@ use Yoast\PHPUnitPolyfills\TestCases\TestCase;
* OrderControllerTests class.
*/
class OrderControllerTests extends TestCase {
+ /**
+ * Whether the checkout phone field option existed before the test.
+ *
+ * @var bool
+ */
+ private $checkout_phone_field_option_existed = false;
+
+ /**
+ * Checkout phone field option value before the test.
+ *
+ * @var mixed
+ */
+ private $checkout_phone_field_option_value;
+
/**
* The system under test.
*
@@ -29,6 +43,16 @@ class OrderControllerTests extends TestCase {
*/
public function setUp(): void {
parent::setUp();
+
+ $missing_option = new \stdClass();
+ $this->checkout_phone_field_option_value = get_option( 'woocommerce_checkout_phone_field', $missing_option );
+ $this->checkout_phone_field_option_existed = $missing_option !== $this->checkout_phone_field_option_value;
+
+ // The fixtures in this class do not provide phone numbers, so make the
+ // phone field optional as other Store API test classes do. Without this
+ // the class only passes when run after a class that already did so.
+ update_option( 'woocommerce_checkout_phone_field', 'optional' );
+
$this->sut = new class() extends OrderController {
/**
* Check all required address fields are set and return errors if not. Parent is protected.
@@ -47,9 +71,18 @@ class OrderControllerTests extends TestCase {
* Tear down after test.
*/
public function tearDown(): void {
- parent::tearDown();
- WC()->countries->locale = null;
- $this->sut = null;
+ try {
+ WC()->countries->locale = null;
+ $this->sut = null;
+
+ if ( $this->checkout_phone_field_option_existed ) {
+ update_option( 'woocommerce_checkout_phone_field', $this->checkout_phone_field_option_value );
+ } else {
+ delete_option( 'woocommerce_checkout_phone_field' );
+ }
+ } finally {
+ parent::tearDown();
+ }
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Caching/OrderCacheTest.php b/plugins/woocommerce/tests/php/src/Caching/OrderCacheTest.php
index dfe5af900d7..a3872fc7153 100644
--- a/plugins/woocommerce/tests/php/src/Caching/OrderCacheTest.php
+++ b/plugins/woocommerce/tests/php/src/Caching/OrderCacheTest.php
@@ -6,6 +6,7 @@ namespace Automattic\WooCommerce\Tests\Caching;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Utilities\OrderUtil;
+use WC_Helper_Order;
/**
* Class OrderCacheTest.
diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php
index e41e5d40b1a..bb6a09f9b79 100644
--- a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php
@@ -8,6 +8,7 @@ use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableControlle
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
use Automattic\WooCommerce\Testing\Tools\DynamicDecorator;
+use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Utilities\StringUtil;
use WC_DateTime;
@@ -31,24 +32,84 @@ class PostsToOrdersMigrationControllerTest extends \WC_Unit_Test_Case {
*/
private $executed_transaction_statements;
+ /**
+ * Whether HPOS was authoritative before the test.
+ *
+ * @var bool
+ */
+ private $previous_hpos_state;
+
+ /**
+ * Original migration transaction option.
+ *
+ * @var mixed
+ */
+ private $previous_transactions_option;
+
+ /**
+ * Original migration transaction isolation option.
+ *
+ * @var mixed
+ */
+ private $previous_isolation_option;
+
+ /**
+ * Original database session SQL mode.
+ *
+ * @var string
+ */
+ private $previous_sql_mode;
+
/**
* Setup data_store and sut.
*/
public function setUp(): void {
+ global $wpdb;
+
parent::setUp();
+
+ $this->previous_hpos_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ $this->previous_transactions_option = get_option( CustomOrdersTableController::USE_DB_TRANSACTIONS_OPTION, null );
+ $this->previous_isolation_option = get_option( CustomOrdersTableController::DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION, null );
+ $this->previous_sql_mode = $wpdb->get_var( 'SELECT @@SESSION.sql_mode' );
+
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ self::enable_direct_product_attribute_lookup_updates();
+ OrderHelper::toggle_cot_feature_and_usage( false );
OrderHelper::create_order_custom_table_if_not_exist();
$this->data_store = wc_get_container()->get( OrdersTableDataStore::class );
$this->sut = wc_get_container()->get( PostsToOrdersMigrationController::class );
- add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
}
/**
* Run after each test.
*/
public function tearDown(): void {
+ global $wpdb;
+
+ $wpdb->suppress_errors( false );
+ $wpdb->query( $wpdb->prepare( 'SET SESSION sql_mode = %s', $this->previous_sql_mode ) );
+ $this->restore_option( CustomOrdersTableController::USE_DB_TRANSACTIONS_OPTION, $this->previous_transactions_option );
+ $this->restore_option( CustomOrdersTableController::DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION, $this->previous_isolation_option );
+ OrderHelper::toggle_cot_feature_and_usage( $this->previous_hpos_state );
+ self::disable_direct_product_attribute_lookup_updates();
+ remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+
parent::tearDown();
- update_option( CustomOrdersTableController::USE_DB_TRANSACTIONS_OPTION, 'no' );
- remove_all_filters( 'wc_allow_changing_orders_storage_while_sync_is_pending' );
+ }
+
+ /**
+ * Restore an option to its value before the test, including nonexistence.
+ *
+ * @param string $option_name Option name.
+ * @param mixed $previous_value Original option value, or null when absent.
+ */
+ private function restore_option( string $option_name, $previous_value ): void {
+ if ( null === $previous_value ) {
+ delete_option( $option_name );
+ } else {
+ update_option( $option_name, $previous_value );
+ }
}
/**
@@ -369,14 +430,14 @@ WHERE order_id = {$order_id} AND meta_key = 'non_unique_key_1' AND meta_value in
}
/**
- * Helper method to clear checkout and truncate order tables.
+ * Helper method to clear checkout and order tables.
*/
private function clear_all_orders() {
global $wpdb;
$order_tables = $this->data_store->get_all_table_names();
foreach ( $order_tables as $table ) {
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
- $wpdb->query( "TRUNCATE table $table;" );
+ $wpdb->query( "DELETE FROM $table;" );
}
}
@@ -822,25 +883,28 @@ WHERE order_id = {$order_id} AND meta_key = 'non_unique_key_1' AND meta_value in
$sql_mode = $wpdb->get_var( 'SELECT @@sql_mode' );
- // Set SQL mode to strict to disallow 0 dates.
- $wpdb->query( "SET sql_mode = 'TRADITIONAL'" );
+ try {
+ // Set SQL mode to strict to disallow 0 dates.
+ $wpdb->query( "SET sql_mode = 'TRADITIONAL'" );
- // Assert that strict mode was indeed enabled, by trying to insert 0 date.
- $orders_table = OrdersTableDataStore::get_orders_table_name();
- $wpdb->suppress_errors();
+ // Assert that strict mode was indeed enabled, by trying to insert 0 date.
+ $orders_table = OrdersTableDataStore::get_orders_table_name();
+ $wpdb->suppress_errors();
- // phpcs:ignore -- Ignoring this error because we are testing for it.
- $result = $wpdb->query( "INSERT INTO $orders_table (date_created_gmt) VALUES ('0000-00-00 00:00:00')" );
- $this->assertFalse( $result );
- $wpdb->suppress_errors( false );
+ // phpcs:ignore -- Ignoring this error because we are testing for it.
+ $result = $wpdb->query( "INSERT INTO $orders_table (date_created_gmt) VALUES ('0000-00-00 00:00:00')" );
+ $this->assertFalse( $result );
+ $wpdb->suppress_errors( false );
- $this->sut->migrate_order( $order->get_id() );
-
- $errors = $this->sut->verify_migrated_orders( array( $order->get_id() ) );
- $this->assertEmpty( $errors ); // _customer_user_agent
+ $this->sut->migrate_order( $order->get_id() );
- // phpcs:ignore -- Hardcoded value.
- $wpdb->query( "SET sql_mode = '$sql_mode' " );
+ $errors = $this->sut->verify_migrated_orders( array( $order->get_id() ) );
+ $this->assertEmpty( $errors );
+ } finally {
+ $wpdb->suppress_errors( false );
+ // phpcs:ignore -- Hardcoded value.
+ $wpdb->query( "SET sql_mode = '$sql_mode' " );
+ }
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php b/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php
index 3daeac3fecb..52328a2d981 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php
@@ -25,6 +25,13 @@ class AbilitiesLoaderTest extends \WC_Unit_Test_Case {
use HPOSToggleTrait;
+ /**
+ * Shared administrator used as the default current user.
+ *
+ * @var int
+ */
+ private static $administrator_id;
+
private const CANONICAL_ABILITY_IDS = array(
'woocommerce/products-query',
'woocommerce/product-create',
@@ -84,6 +91,15 @@ class AbilitiesLoaderTest extends \WC_Unit_Test_Case {
*/
private $cot_setup_for_test = false;
+ /**
+ * Create immutable class fixtures.
+ *
+ * @param \WP_UnitTest_Factory $factory WordPress unit test factory.
+ */
+ public static function wpSetUpBeforeClass( $factory ): void {
+ self::$administrator_id = $factory->user->create( array( 'role' => 'administrator' ) );
+ }
+
/**
* Set up test fixtures.
*/
@@ -106,9 +122,7 @@ class AbilitiesLoaderTest extends \WC_Unit_Test_Case {
// WordPress 6.9+ requires init to have fired before the Abilities API registry can be initialized.
$wp_actions['init'] = max( 1, (int) ( $wp_actions['init'] ?? 0 ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
- wp_set_current_user(
- $this->factory->user->create( array( 'role' => 'administrator' ) )
- );
+ wp_set_current_user( self::$administrator_id );
$this->register_woocommerce_category();
$this->register_domain_abilities();
@@ -549,8 +563,14 @@ class AbilitiesLoaderTest extends \WC_Unit_Test_Case {
$this->markTestSkipped( 'Abilities API category registry is not available.' );
}
- AbilitiesCategories::register_categories();
- AbilitiesCategories::register_categories();
+ $callback = static function () {
+ AbilitiesCategories::register_categories();
+ AbilitiesCategories::register_categories();
+ };
+
+ add_action( 'wp_abilities_api_categories_init', $callback );
+ do_action( 'wp_abilities_api_categories_init' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Exercise category registration on its required lifecycle hook.
+ remove_action( 'wp_abilities_api_categories_init', $callback );
$this->assertTrue( wp_has_ability_category( 'woocommerce' ) );
$this->assertTrue( wp_has_ability_category( 'woocommerce-rest' ) );
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/OrderMilestoneEasterEggTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/OrderMilestoneEasterEggTest.php
index d3d29628657..40afa4ffcf6 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/OrderMilestoneEasterEggTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/OrderMilestoneEasterEggTest.php
@@ -6,14 +6,13 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin;
use Automattic\WooCommerce\Internal\Admin\OrderMilestoneEasterEgg;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
-use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
+use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* Unit tests for OrderMilestoneEasterEgg.
*/
class OrderMilestoneEasterEggTest extends \WC_Unit_Test_Case {
- use HPOSToggleTrait;
/** @var OrderMilestoneEasterEgg */
private OrderMilestoneEasterEgg $sut;
@@ -21,13 +20,48 @@ class OrderMilestoneEasterEggTest extends \WC_Unit_Test_Case {
/** @var int */
private int $admin_user_id;
+ /**
+ * Previous HPOS state.
+ *
+ * @var bool
+ */
+ private static bool $hpos_prev_state;
+
+ /**
+ * Set up the class fixtures.
+ */
+ public static function setUpBeforeClass(): void {
+ parent::setUpBeforeClass();
+
+ self::$hpos_prev_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ OrderHelper::create_order_custom_table_if_not_exist();
+
+ if ( ! self::$hpos_prev_state ) {
+ OrderHelper::toggle_cot_feature_and_usage( true );
+ }
+ }
+
+ /**
+ * Tear down the class fixtures.
+ */
+ public static function tearDownAfterClass(): void {
+ self::clear_hpos_orders();
+
+ if ( OrderUtil::custom_orders_table_usage_is_enabled() !== self::$hpos_prev_state ) {
+ OrderHelper::toggle_cot_feature_and_usage( self::$hpos_prev_state );
+ }
+
+ remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+
+ parent::tearDownAfterClass();
+ }
+
/**
* Set up the test case.
*/
public function setUp(): void {
parent::setUp();
- $this->setup_cot();
- $this->toggle_cot_feature_and_usage( true );
$this->sut = new OrderMilestoneEasterEgg();
$this->admin_user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
@@ -47,11 +81,8 @@ class OrderMilestoneEasterEggTest extends \WC_Unit_Test_Case {
remove_action( 'woocommerce_update_order', array( $this->sut, 'clear_milestone_cache' ) );
remove_action( 'woocommerce_delete_order', array( $this->sut, 'clear_milestone_cache' ) );
remove_action( 'woocommerce_trash_order', array( $this->sut, 'clear_milestone_cache' ) );
+ remove_all_filters( 'wc_order_milestone_egg_map' );
- // Drop HPOS tables before toggling off — avoids the "orders out of sync" exception
- // that fires when HPOS is disabled while the table still holds unsync'd rows.
- OrderHelper::delete_order_custom_tables();
- $this->clean_up_cot_setup();
wp_set_current_user( 0 );
parent::tearDown();
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/ListTableTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/ListTableTest.php
index b8516a068ab..6e0a44349dc 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/ListTableTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/ListTableTest.php
@@ -1,17 +1,17 @@
<?php
-declare( strict_types = 1);
+declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders;
use Automattic\WooCommerce\Enums\OrderStatus;
use Automattic\WooCommerce\Internal\Admin\Orders\ListTable;
-use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
+use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
+use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* Tests related to order list table in admin.
*/
class ListTableTest extends \WC_Unit_Test_Case {
- use HPOSToggleTrait;
/**
* @var ListTable
@@ -19,12 +19,47 @@ class ListTableTest extends \WC_Unit_Test_Case {
private $sut;
/**
- * Setup - enables HPOS.
+ * Previous HPOS state.
+ *
+ * @var bool
+ */
+ private static bool $hpos_prev_state;
+
+ /**
+ * Set up class fixtures.
+ */
+ public static function setUpBeforeClass(): void {
+ parent::setUpBeforeClass();
+
+ self::$hpos_prev_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ OrderHelper::create_order_custom_table_if_not_exist();
+
+ if ( ! self::$hpos_prev_state ) {
+ OrderHelper::toggle_cot_feature_and_usage( true );
+ }
+ }
+
+ /**
+ * Tear down class fixtures.
+ */
+ public static function tearDownAfterClass(): void {
+ self::clear_hpos_orders();
+
+ if ( OrderUtil::custom_orders_table_usage_is_enabled() !== self::$hpos_prev_state ) {
+ OrderHelper::toggle_cot_feature_and_usage( self::$hpos_prev_state );
+ }
+
+ remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+
+ parent::tearDownAfterClass();
+ }
+
+ /**
+ * Set up test fixtures.
*/
public function setUp(): void {
parent::setUp();
- $this->setup_cot();
- $this->toggle_cot_authoritative( true );
$this->sut = new ListTable();
$set_order_type = function ( $order_type ) {
$this->order_type = $order_type;
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/MetaBoxes/CustomerHistoryTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/MetaBoxes/CustomerHistoryTest.php
index ff2a3a5ea54..cfc33c8b632 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/MetaBoxes/CustomerHistoryTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/MetaBoxes/CustomerHistoryTest.php
@@ -6,7 +6,8 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders\MetaBoxes;
use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
use Automattic\WooCommerce\Admin\Overrides\Order as AdminOrder;
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomerHistory;
-use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
+use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
+use Automattic\WooCommerce\Utilities\OrderUtil;
use WC_Helper_Order;
use WC_Unit_Test_Case;
@@ -14,7 +15,6 @@ use WC_Unit_Test_Case;
* Tests for the CustomerHistory class.
*/
class CustomerHistoryTest extends WC_Unit_Test_Case {
- use HPOSToggleTrait;
/**
* The System Under Test.
@@ -23,13 +23,57 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
*/
private $sut;
+ /**
+ * Whether HPOS should be restored after this test.
+ *
+ * @var bool
+ */
+ private bool $restore_hpos_after_test = false;
+
+ /**
+ * Previous HPOS state.
+ *
+ * @var bool
+ */
+ private static bool $hpos_prev_state;
+
+ /**
+ * Set up class fixtures.
+ */
+ public static function setUpBeforeClass(): void {
+ parent::setUpBeforeClass();
+
+ self::$hpos_prev_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ OrderHelper::create_order_custom_table_if_not_exist();
+
+ if ( ! self::$hpos_prev_state ) {
+ OrderHelper::toggle_cot_feature_and_usage( true );
+ }
+ }
+
+ /**
+ * Tear down class fixtures.
+ */
+ public static function tearDownAfterClass(): void {
+ self::clear_hpos_orders();
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+
+ if ( OrderUtil::custom_orders_table_usage_is_enabled() !== self::$hpos_prev_state ) {
+ OrderHelper::toggle_cot_feature_and_usage( self::$hpos_prev_state );
+ }
+
+ remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+
+ parent::tearDownAfterClass();
+ }
+
/**
* Set up test fixtures.
*/
public function setUp(): void {
parent::setUp();
add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
- $this->setup_cot();
$this->sut = new CustomerHistory();
}
@@ -37,7 +81,13 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* Tear down test fixtures.
*/
public function tearDown(): void {
- $this->clean_up_cot_setup();
+ if ( $this->restore_hpos_after_test ) {
+ OrderHelper::toggle_cot_feature_and_usage( true );
+ $this->restore_hpos_after_test = false;
+ }
+
+ delete_option( 'woocommerce_excluded_report_order_statuses' );
+ remove_filter( 'woocommerce_order_class', array( AdminOrder::class, 'order_class_name' ) );
remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
parent::tearDown();
}
@@ -46,8 +96,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Should return correct count, total, and average for a registered customer with multiple orders (HPOS).
*/
public function test_registered_customer_with_multiple_orders(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$customer_id = $this->factory->user->create();
$order1 = WC_Helper_Order::create_order( $customer_id );
@@ -73,8 +121,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Should fetch data correctly for a guest customer matched by billing email (HPOS).
*/
public function test_guest_customer_by_email(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$email = 'guest-test@example.com';
$order1 = WC_Helper_Order::create_order( 0 );
@@ -102,8 +148,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Should not count orders with excluded statuses like pending, cancelled, and failed (HPOS).
*/
public function test_excluded_statuses_not_counted(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$customer_id = $this->factory->user->create();
$order_good = WC_Helper_Order::create_order( $customer_id );
@@ -154,8 +198,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Should show zero data for guest order with no billing email (HPOS).
*/
public function test_guest_with_no_email_shows_zero(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$order = WC_Helper_Order::create_order( 0 );
$order->set_billing_email( '' );
$order->set_status( 'completed' );
@@ -173,8 +215,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Should show zero data when no matching orders exist for the customer (HPOS).
*/
public function test_no_matching_orders_shows_zero(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$customer_id = $this->factory->user->create();
$order = WC_Helper_Order::create_order( $customer_id );
@@ -193,8 +233,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Should deduct partial refund from total spend (HPOS).
*/
public function test_partial_refund_deducted_from_total_spend(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$customer_id = $this->factory->user->create();
$order = WC_Helper_Order::create_order( $customer_id );
@@ -223,8 +261,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Should deduct full refund from total spend (HPOS).
*/
public function test_full_refund_deducted_from_total_spend(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$customer_id = $this->factory->user->create();
$order1 = WC_Helper_Order::create_order( $customer_id );
@@ -258,8 +294,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Should deduct refund from guest order total spend (HPOS).
*/
public function test_guest_order_refund_deducted_from_total_spend(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$email = 'guest-refund@example.com';
$order = WC_Helper_Order::create_order( 0 );
@@ -288,8 +322,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Should only count orders for the specific registered customer, not other customers (HPOS).
*/
public function test_registered_customer_isolation(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$customer_a = $this->factory->user->create();
$customer_b = $this->factory->user->create();
@@ -327,8 +359,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Should only count orders for the specific guest email, not other guest emails (HPOS).
*/
public function test_guest_customer_email_isolation(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$email_a = 'guest-a@example.com';
$email_b = 'guest-b@example.com';
@@ -369,8 +399,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Tooltip should list default excluded statuses (pending payment, failed, cancelled).
*/
public function test_tooltip_shows_default_excluded_statuses(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$customer_id = $this->factory->user->create();
$order = WC_Helper_Order::create_order( $customer_id );
@@ -390,8 +418,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Tooltip should reflect custom excluded statuses option.
*/
public function test_tooltip_reflects_custom_option(): void {
- $this->toggle_cot_feature_and_usage( true );
-
update_option( 'woocommerce_excluded_report_order_statuses', array( 'cancelled' ) );
$customer_id = $this->factory->user->create();
@@ -415,8 +441,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Tooltip should reflect statuses added via filter.
*/
public function test_tooltip_reflects_filter(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$add_on_hold = function ( $statuses ) {
$statuses[] = 'on-hold';
return $statuses;
@@ -442,8 +466,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Tooltip should not display internal statuses like auto-draft, trash, or checkout-draft.
*/
public function test_tooltip_excludes_internal_statuses(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$customer_id = $this->factory->user->create();
$order = WC_Helper_Order::create_order( $customer_id );
@@ -462,8 +484,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Tooltip should not display checkout-draft even when it is added via filter.
*/
public function test_tooltip_excludes_checkout_draft_status(): void {
- $this->toggle_cot_feature_and_usage( true );
-
$add_checkout_draft = function ( $statuses ) {
$statuses[] = 'checkout-draft';
return $statuses;
@@ -489,8 +509,6 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox Tooltip should show generic message when all statuses are removed from exclusion.
*/
public function test_tooltip_shows_no_exclusion_message_when_all_statuses_removed(): void {
- $this->toggle_cot_feature_and_usage( true );
-
add_filter( 'woocommerce_analytics_excluded_order_statuses', '__return_empty_array' );
$customer_id = $this->factory->user->create();
@@ -513,7 +531,7 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox CPT fallback should render correct customer history from analytics tables.
*/
public function test_cpt_fallback_renders_with_analytics_data(): void {
- $this->toggle_cot_feature_and_usage( false );
+ $this->use_cpt_orders();
\WC_Helper_Reports::reset_stats_dbs();
@@ -548,7 +566,7 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
* @testdox CPT fallback should render customer history from a base WC_Order without logging warnings.
*/
public function test_cpt_fallback_renders_with_base_order(): void {
- $this->toggle_cot_feature_and_usage( false );
+ $this->use_cpt_orders();
\WC_Helper_Reports::reset_stats_dbs();
@@ -584,4 +602,12 @@ class CustomerHistoryTest extends WC_Unit_Test_Case {
$this->assertMatchesRegularExpression( '/order-attribution-total-orders">\s*1\s*</', $output, 'Should show 1 order from analytics data' );
$this->assertMatchesRegularExpression( '/order-attribution-total-spend">\s*.*100\.00/', $output, 'Should show total spend of 100' );
}
+
+ /**
+ * Switches the order data store to CPT for fallback coverage.
+ */
+ private function use_cpt_orders(): void {
+ $this->restore_hpos_after_test = true;
+ OrderHelper::toggle_cot_feature_and_usage( false );
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/PageControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/PageControllerTest.php
index 2b9bccd66fe..a2a5daa8ced 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/PageControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/PageControllerTest.php
@@ -4,6 +4,8 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders {
use Automattic\WooCommerce\Internal\Admin\Orders\PageController;
use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
+ use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
+ use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* Tests related to the HPOS orders admin pages controller.
@@ -11,11 +13,46 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders {
class PageControllerTest extends \WC_Unit_Test_Case {
use HPOSToggleTrait;
+ /**
+ * Previous HPOS state.
+ *
+ * @var bool
+ */
+ private static bool $hpos_prev_state;
+
/**
* @var int ID of test admin user.
*/
private $user_admin;
+ /**
+ * Set up class fixtures.
+ */
+ public static function setUpBeforeClass(): void {
+ parent::setUpBeforeClass();
+
+ self::$hpos_prev_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ OrderHelper::create_order_custom_table_if_not_exist();
+
+ if ( self::$hpos_prev_state ) {
+ OrderHelper::toggle_cot_feature_and_usage( false );
+ }
+ }
+
+ /**
+ * Tear down class fixtures.
+ */
+ public static function tearDownAfterClass(): void {
+ if ( OrderUtil::custom_orders_table_usage_is_enabled() !== self::$hpos_prev_state ) {
+ OrderHelper::toggle_cot_feature_and_usage( self::$hpos_prev_state );
+ }
+
+ remove_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+
+ parent::tearDownAfterClass();
+ }
+
/**
* Set up before each test.
*
@@ -23,7 +60,6 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders {
*/
public function setUp(): void {
parent::setUp();
- $this->setup_cot();
$this->toggle_cot_feature_and_usage( false );
$this->user_admin = $this->factory->user->create( array( 'role' => 'administrator' ) );
@@ -33,16 +69,6 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders {
$mock_filter_input = false;
}
- /**
- * Tear down after each test.
- *
- * @return void
- */
- public function tearDown(): void {
- $this->clean_up_cot_setup();
- parent::tearDown();
- }
-
/**
* @testDox Basic order screen detection works.
*/
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/RemoteFreeExtensions/InitTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/RemoteFreeExtensions/InitTest.php
index 430af1ebe77..ce12e67199e 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/RemoteFreeExtensions/InitTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/RemoteFreeExtensions/InitTest.php
@@ -15,6 +15,18 @@ use WC_Unit_Test_Case;
* @covers \Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\Init
*/
class InitTest extends WC_Unit_Test_Case {
+ /**
+ * Raw option states before the test fixture mutates them.
+ *
+ * @var array<string, array{exists: bool, value: string|null, autoload: string|null}>
+ */
+ private array $initial_option_states = array();
+
+ /** @var callable|null Filter used to provide local remote-extension specs. */
+ private $specs_transient_filter;
+
+ /** @var bool Whether child fixture setup reached its mutation phase. */
+ private bool $fixture_setup_started = false;
/**
* Set up.
@@ -22,50 +34,59 @@ class InitTest extends WC_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
+ $this->assertFalse( (bool) wp_using_ext_object_cache(), 'This test requires database-backed transients for exact restoration.' );
+ $this->assert_option_cache_matches_raw_state( 'woocommerce_default_country' );
+ $this->assert_option_cache_matches_raw_state( 'woocommerce_show_marketplace_suggestions' );
+ foreach ( $this->get_restored_option_names() as $option_name ) {
+ $this->initial_option_states[ $option_name ] = $this->get_raw_option_state( $option_name );
+ }
+ $this->fixture_setup_started = true;
+
$this->user = $this->factory->user->create(
array(
'role' => 'administrator',
)
);
delete_option( 'woocommerce_show_marketplace_suggestions' );
- add_filter(
- 'transient_woocommerce_admin_' . RemoteFreeExtensionsDataSourcePoller::ID . '_specs',
- function( $value ) {
- if ( $value ) {
- return $value;
- }
+ $this->specs_transient_filter = function ( $value ) {
+ if ( $value ) {
+ return $value;
+ }
+
+ $locale = get_user_locale();
- $locale = get_user_locale();
-
- return array(
- $locale => array(
- array(
- 'key' => 'obw/basics',
- 'title' => __( 'Get the basics', 'woocommerce' ),
- 'plugins' => array(
- array(
- 'name' => 'mock-extension-1',
- 'key' => 'mock-extension-1',
- 'is_visible' => (object) array(
- 'type' => 'base_location_country',
- 'value' => 'ZA',
- 'operation' => '=',
- ),
+ return array(
+ $locale => array(
+ array(
+ 'key' => 'obw/basics',
+ 'title' => __( 'Get the basics', 'woocommerce' ),
+ 'plugins' => array(
+ array(
+ 'name' => 'mock-extension-1',
+ 'key' => 'mock-extension-1',
+ 'is_visible' => (object) array(
+ 'type' => 'base_location_country',
+ 'value' => 'ZA',
+ 'operation' => '=',
),
- array(
- 'name' => 'mock-extension-2',
- 'key' => 'mock-extension-2',
- 'is_visible' => (object) array(
- 'type' => 'base_location_country',
- 'value' => 'US',
- 'operation' => '=',
- ),
+ ),
+ array(
+ 'name' => 'mock-extension-2',
+ 'key' => 'mock-extension-2',
+ 'is_visible' => (object) array(
+ 'type' => 'base_location_country',
+ 'value' => 'US',
+ 'operation' => '=',
),
),
),
),
- );
- },
+ ),
+ );
+ };
+ add_filter(
+ 'transient_woocommerce_admin_' . RemoteFreeExtensionsDataSourcePoller::ID . '_specs',
+ $this->specs_transient_filter,
);
}
@@ -73,27 +94,197 @@ class InitTest extends WC_Unit_Test_Case {
* Tear down.
*/
public function tearDown(): void {
- parent::tearDown();
- RemoteFreeExtensions::delete_specs_transient();
- remove_all_filters( 'transient_woocommerce_admin_' . RemoteFreeExtensionsDataSourcePoller::ID . '_specs' );
- update_option( 'woocommerce_default_country', 'US' );
+ try {
+ if ( $this->fixture_setup_started ) {
+ if ( null !== $this->specs_transient_filter ) {
+ remove_filter( 'transient_' . $this->get_specs_transient_name(), $this->specs_transient_filter );
+ }
+ RemoteFreeExtensions::delete_specs_transient();
+ }
+ } finally {
+ try {
+ parent::tearDown();
+ } finally {
+ if ( $this->fixture_setup_started ) {
+ $failures = $this->restore_initial_option_states();
+ $this->assert_initial_option_states_restored( $failures );
+ }
+ }
+ }
+ }
+
+ /**
+ * Get option rows that must survive each fixture scope exactly.
+ *
+ * @return string[]
+ */
+ private function get_restored_option_names(): array {
+ $transient_name = $this->get_specs_transient_name();
+
+ return array(
+ 'woocommerce_default_country',
+ 'woocommerce_show_marketplace_suggestions',
+ '_transient_' . $transient_name,
+ '_transient_timeout_' . $transient_name,
+ );
+ }
+
+ /**
+ * Get the specs transient name.
+ *
+ * @return string
+ */
+ private function get_specs_transient_name(): string {
+ return 'woocommerce_admin_' . RemoteFreeExtensionsDataSourcePoller::ID . '_specs';
+ }
+
+ /**
+ * Read an option without default filters or value coercion.
+ *
+ * @param string $option_name Option name.
+ * @return array{exists: bool, value: string|null, autoload: string|null}
+ */
+ private function get_raw_option_state( string $option_name ): array {
+ global $wpdb;
+
+ $wpdb->last_error = '';
+
+ $row = $wpdb->get_row(
+ $wpdb->prepare( "SELECT option_value, autoload FROM {$wpdb->options} WHERE option_name = %s", $option_name ),
+ ARRAY_A
+ );
+ if ( '' !== $wpdb->last_error ) {
+ throw new \RuntimeException( esc_html( "Failed to read option {$option_name}: {$wpdb->last_error}" ) );
+ }
+
+ return null === $row
+ ? array(
+ 'exists' => false,
+ 'value' => null,
+ 'autoload' => null,
+ )
+ : array(
+ 'exists' => true,
+ 'value' => $row['option_value'],
+ 'autoload' => $row['autoload'],
+ );
+ }
+
+ /**
+ * Assert the option cache represents the row currently stored in the database.
+ *
+ * @param string $option_name Option name.
+ */
+ private function assert_option_cache_matches_raw_state( string $option_name ): void {
+ $state = $this->get_raw_option_state( $option_name );
+ $expected = $state['exists'] ? maybe_unserialize( $state['value'] ) : false;
+
+ $this->assertSame( $expected, get_option( $option_name ) );
+ }
+
+ /**
+ * Restore every captured option and invalidate its caches.
+ *
+ * @return string[] Restoration failures.
+ */
+ private function restore_initial_option_states(): array {
+ $failures = array();
+ foreach ( $this->initial_option_states as $option_name => $state ) {
+ try {
+ if ( ! $this->restore_raw_option_state( $option_name, $state ) ) {
+ $failures[] = "Database write failed for {$option_name}.";
+ }
+ } catch ( \Throwable $error ) {
+ $failures[] = $error->getMessage();
+ }
+ }
+
+ return $failures;
+ }
+
+ /**
+ * Verify raw rows and option caches after parent teardown and restoration.
+ *
+ * @param string[] $failures Existing restoration failures.
+ */
+ private function assert_initial_option_states_restored( array $failures ): void {
+ foreach ( $this->initial_option_states as $option_name => $state ) {
+ try {
+ if ( $state !== $this->get_raw_option_state( $option_name ) ) {
+ $failures[] = "Restored row does not match the captured state for {$option_name}.";
+ }
+ $expected = $state['exists'] ? maybe_unserialize( $state['value'] ) : false;
+ if ( maybe_serialize( get_option( $option_name ) ) !== maybe_serialize( $expected ) ) {
+ $failures[] = "Option cache does not match the captured state for {$option_name}.";
+ }
+ } catch ( \Throwable $error ) {
+ $failures[] = $error->getMessage();
+ }
+ }
+
+ $this->assertSame( array(), $failures, implode( ' ', $failures ) );
+ }
+
+ /**
+ * Restore an option without invoking setting sanitizers.
+ *
+ * @param string $option_name Option name.
+ * @param array{exists: bool, value: string|null, autoload: string|null} $state Raw option state.
+ * @return bool Whether the database operation succeeded.
+ */
+ private function restore_raw_option_state( string $option_name, array $state ): bool {
+ global $wpdb;
+
+ try {
+ if ( ! $state['exists'] ) {
+ $result = $wpdb->delete( $wpdb->options, array( 'option_name' => $option_name ) );
+ } elseif ( $this->get_raw_option_state( $option_name )['exists'] ) {
+ $result = $wpdb->update(
+ $wpdb->options,
+ array(
+ 'option_value' => $state['value'],
+ 'autoload' => $state['autoload'],
+ ),
+ array( 'option_name' => $option_name )
+ );
+ } else {
+ $result = $wpdb->insert(
+ $wpdb->options,
+ array(
+ 'option_name' => $option_name,
+ 'option_value' => $state['value'],
+ 'autoload' => $state['autoload'],
+ )
+ );
+ }
+
+ return false !== $result;
+ } finally {
+ wp_cache_delete( $option_name, 'options' );
+ wp_cache_delete( 'alloptions', 'options' );
+ wp_cache_delete( 'notoptions', 'options' );
+ if ( 0 === strpos( $option_name, '_transient_' ) ) {
+ wp_cache_delete( substr( $option_name, strlen( '_transient_' ) ), 'transient' );
+ }
+ }
}
/**
* Test that default extensions are provided when remote sources don't exist.
*/
public function test_get_default_specs() {
- remove_all_filters( 'transient_woocommerce_admin_' . RemoteFreeExtensionsDataSourcePoller::ID . '_specs' );
- add_filter(
- DataSourcePoller::FILTER_NAME,
- function() {
- return array();
- }
- );
- $specs = RemoteFreeExtensions::get_specs();
- $defaults = DefaultFreeExtensions::get_all();
- remove_all_filters( DataSourcePoller::FILTER_NAME );
- $this->assertEquals( $defaults, $specs );
+ remove_filter( 'transient_' . $this->get_specs_transient_name(), $this->specs_transient_filter );
+ $data_sources_filter = function () {
+ return array();
+ };
+ add_filter( DataSourcePoller::FILTER_NAME, $data_sources_filter );
+ try {
+ $specs = RemoteFreeExtensions::get_specs();
+ $defaults = DefaultFreeExtensions::get_all();
+ $this->assertEquals( $defaults, $specs );
+ } finally {
+ remove_filter( DataSourcePoller::FILTER_NAME, $data_sources_filter );
+ }
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsRestControllerIntegrationTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsRestControllerIntegrationTest.php
index 43b3204a201..2a805cd66c7 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsRestControllerIntegrationTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsRestControllerIntegrationTest.php
@@ -15,7 +15,7 @@ use Automattic\WooCommerce\Testing\Tools\DependencyManagement\MockableLegacyProx
use Automattic\WooCommerce\Testing\Tools\TestingContainer;
use Automattic\WooCommerce\Tests\Internal\Admin\Settings\Mocks\FakePaymentGateway;
use PHPUnit\Framework\MockObject\MockObject;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_REST_Request;
/**
@@ -23,7 +23,7 @@ use WP_REST_Request;
*
* @class WooPaymentsRestController
*/
-class WooPaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
+class WooPaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
/**
* Endpoint.
*
@@ -73,6 +73,13 @@ class WooPaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
*/
protected $store_admin_id;
+ /**
+ * REST server used by the controller tests.
+ *
+ * @var \WP_REST_Server
+ */
+ protected $server;
+
/**
* The current time in seconds.
*
@@ -85,38 +92,51 @@ class WooPaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
/**
* Gateways mock.
*
- * @var callable
+ * @var callable|null
*/
- private $gateways_mock_ref;
+ private $gateways_mock_ref = null;
/**
- * The initial country that is set before running tests in this test suite.
+ * Raw option states before the test class mutates them.
*
- * @var string $initial_country
+ * @var array<string, array{exists: bool, value: string|null, autoload: string|null}>
*/
- private static string $initial_country = '';
+ private static array $initial_option_states = array();
- /**
- * The initial currency that is set before running tests in this test suite.
- *
- * @var string $initial_currency
- */
- private static string $initial_currency = '';
+ /** @var bool Whether the WooPayments version override existed before the test. */
+ private bool $initial_wcpay_version_override_exists = false;
+
+ /** @var mixed WooPayments version override value before the test. */
+ private $initial_wcpay_version_override;
/**
* Saves values of initial country and currency before running test suite.
*/
public static function wpSetUpBeforeClass(): void {
- self::$initial_country = WC()->countries->get_base_country();
- self::$initial_currency = get_woocommerce_currency();
+ foreach ( self::get_restored_option_names() as $option_name ) {
+ self::$initial_option_states[ $option_name ] = self::get_raw_option_state( $option_name );
+ self::invalidate_option_cache( $option_name );
+ }
}
/**
* Restores initial values of country and currency after running test suite.
*/
public static function wpTearDownAfterClass(): void {
- update_option( 'woocommerce_default_country', self::$initial_country );
- update_option( 'woocommerce_currency', self::$initial_currency );
+ $failures = array();
+ foreach ( self::$initial_option_states as $option_name => $state ) {
+ try {
+ if ( ! self::restore_raw_option_state( $option_name, $state ) ) {
+ $failures[] = "Database write failed for {$option_name}.";
+ }
+ } catch ( \Throwable $error ) {
+ $failures[] = $error->getMessage();
+ }
+ }
+
+ if ( array() !== $failures ) {
+ throw new \RuntimeException( esc_html( implode( ' ', $failures ) ) );
+ }
}
/**
@@ -124,6 +144,12 @@ class WooPaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
*/
public function setUp(): void {
parent::setUp();
+ $this->initial_wcpay_version_override_exists = array_key_exists( 'WCPAY_VERSION_NUMBER', Constants::$set_constants );
+ $this->initial_wcpay_version_override = Constants::$set_constants['WCPAY_VERSION_NUMBER'] ?? null;
+
+ $gateways = \WC_Payment_Gateways::instance();
+ $gateways->payment_gateways = array();
+ $gateways->init();
$this->store_admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
wp_set_current_user( $this->store_admin_id );
@@ -279,32 +305,193 @@ class WooPaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
$this->woopayments_provider_service = $container->get( WooPaymentsService::class );
// Register the REST controller routes again to make sure the dependency tree is using our mocks.
- $sut = new WooPaymentsRestController();
- $sut->init( $container->get( Payments::class ), $this->woopayments_provider_service );
- $sut->register_routes( true );
+ $this->controller = new WooPaymentsRestController();
+ $this->controller->init( $container->get( Payments::class ), $this->woopayments_provider_service );
+ $this->server = $this->create_rest_server_with_routes(
+ array(
+ function () {
+ $this->controller->register_routes( true );
+ },
+ ),
+ true
+ );
}
/**
* Tear down.
*/
public function tearDown(): void {
- $this->unmock_payment_gateways();
- delete_option( 'woocommerce_gateway_order' );
+ $cleanup_errors = array();
+
+ $this->run_cleanup_step( fn() => $this->unmock_payment_gateways(), $cleanup_errors );
+ $this->run_cleanup_step(
+ function () {
+ if ( isset( $this->mockable_proxy ) ) {
+ $this->mockable_proxy->reset();
+ }
+ },
+ $cleanup_errors
+ );
+ $this->run_cleanup_step( fn() => wc_get_container()->reset_all_resolved(), $cleanup_errors );
+ $this->run_cleanup_step( fn() => $this->clear_rest_server(), $cleanup_errors );
+ $this->run_cleanup_step( fn() => $this->restore_wcpay_version_override(), $cleanup_errors );
+ $this->run_cleanup_step( fn() => parent::tearDown(), $cleanup_errors );
+ $this->run_cleanup_step( fn() => self::invalidate_test_option_caches(), $cleanup_errors );
+
+ if ( array() !== $cleanup_errors ) {
+ throw $cleanup_errors[0];
+ }
+ }
+
+ /**
+ * Run one teardown phase without preventing later phases.
+ *
+ * @param callable $callback Cleanup phase.
+ * @param \Throwable[] $errors Collected cleanup errors.
+ */
+ private function run_cleanup_step( callable $callback, array &$errors ): void {
+ try {
+ $callback();
+ } catch ( \Throwable $error ) {
+ $errors[] = $error;
+ }
+ }
- delete_option( WooPaymentsService::NOX_PROFILE_OPTION_KEY );
- delete_option( WooPaymentsService::NOX_ONBOARDING_LOCKED_KEY );
+ /**
+ * Restore only the Jetpack constant override owned by this fixture.
+ */
+ private function restore_wcpay_version_override(): void {
+ if ( $this->initial_wcpay_version_override_exists ) {
+ Constants::set_constant( 'WCPAY_VERSION_NUMBER', $this->initial_wcpay_version_override );
+ } else {
+ Constants::clear_single_constant( 'WCPAY_VERSION_NUMBER' );
+ }
+ }
- $this->mockable_proxy->reset();
+ /**
+ * Get option rows that must survive the class exactly.
+ *
+ * @return string[]
+ */
+ private static function get_restored_option_names(): array {
+ return array(
+ 'woocommerce_default_country',
+ 'woocommerce_currency',
+ );
+ }
- /**
- * TestingContainer instance.
- *
- * @var TestingContainer $container
- */
- $container = wc_get_container();
- $container->reset_all_resolved();
+ /**
+ * Get option caches mutated by the fixture or SUT.
+ *
+ * @return string[]
+ */
+ private static function get_test_option_names(): array {
+ return array_merge( self::get_restored_option_names(), self::get_transactional_option_names() );
+ }
+
+ /**
+ * Get option rows changed within the inherited transaction.
+ *
+ * @return string[]
+ */
+ private static function get_transactional_option_names(): array {
+ return array(
+ 'woocommerce_gateway_order',
+ WooPaymentsService::NOX_PROFILE_OPTION_KEY,
+ WooPaymentsService::NOX_ONBOARDING_LOCKED_KEY,
+ );
+ }
+
+ /**
+ * Read an option without default filters or value coercion.
+ *
+ * @param string $option_name Option name.
+ * @return array{exists: bool, value: string|null, autoload: string|null}
+ */
+ private static function get_raw_option_state( string $option_name ): array {
+ global $wpdb;
+
+ $wpdb->last_error = '';
+ $row = $wpdb->get_row(
+ $wpdb->prepare( "SELECT option_value, autoload FROM {$wpdb->options} WHERE option_name = %s", $option_name ),
+ ARRAY_A
+ );
+ if ( '' !== $wpdb->last_error ) {
+ throw new \RuntimeException( esc_html( "Failed to read option {$option_name}: {$wpdb->last_error}" ) );
+ }
+
+ return null === $row
+ ? array(
+ 'exists' => false,
+ 'value' => null,
+ 'autoload' => null,
+ )
+ : array(
+ 'exists' => true,
+ 'value' => $row['option_value'],
+ 'autoload' => $row['autoload'],
+ );
+ }
+
+ /**
+ * Restore an option without invoking setting sanitizers.
+ *
+ * @param string $option_name Option name.
+ * @param array{exists: bool, value: string|null, autoload: string|null} $state Raw option state.
+ * @return bool Whether the database operation succeeded.
+ */
+ private static function restore_raw_option_state( string $option_name, array $state ): bool {
+ global $wpdb;
+
+ try {
+ if ( ! $state['exists'] ) {
+ $result = $wpdb->delete( $wpdb->options, array( 'option_name' => $option_name ) );
+ } elseif ( self::get_raw_option_state( $option_name )['exists'] ) {
+ $result = $wpdb->update(
+ $wpdb->options,
+ array(
+ 'option_value' => $state['value'],
+ 'autoload' => $state['autoload'],
+ ),
+ array( 'option_name' => $option_name )
+ );
+ } else {
+ $result = $wpdb->insert(
+ $wpdb->options,
+ array(
+ 'option_name' => $option_name,
+ 'option_value' => $state['value'],
+ 'autoload' => $state['autoload'],
+ )
+ );
+ }
- parent::tearDown();
+ return false !== $result;
+ } finally {
+ self::invalidate_option_cache( $option_name );
+ }
+ }
+
+ /**
+ * Invalidate all WordPress option cache representations.
+ *
+ * @param string $option_name Option name.
+ */
+ private static function invalidate_option_cache( string $option_name ): void {
+ wp_cache_delete( $option_name, 'options' );
+ wp_cache_delete( 'alloptions', 'options' );
+ wp_cache_delete( 'notoptions', 'options' );
+ }
+
+ /**
+ * Invalidate every option cache representation touched by a test.
+ */
+ private static function invalidate_test_option_caches(): void {
+ foreach ( self::get_test_option_names() as $option_name ) {
+ wp_cache_delete( $option_name, 'options' );
+ }
+ wp_cache_delete( 'alloptions', 'options' );
+ wp_cache_delete( 'notoptions', 'options' );
}
/**
@@ -1393,12 +1580,16 @@ class WooPaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
* Unmock the WC payment gateways.
*/
private function unmock_payment_gateways() {
- remove_all_actions( 'wc_payment_gateways_initialized' );
+ if ( null !== $this->gateways_mock_ref ) {
+ remove_action( 'wc_payment_gateways_initialized', $this->gateways_mock_ref, 100 );
+ }
// Reinitialize the WC gateways.
WC()->payment_gateways()->payment_gateways = array();
WC()->payment_gateways()->init();
- $this->providers_service->reset_memo();
+ if ( isset( $this->providers_service ) ) {
+ $this->providers_service->reset_memo();
+ }
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsRestControllerTest.php
index 8d7b180add6..ec34256bec1 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsRestControllerTest.php
@@ -8,7 +8,7 @@ use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WooPayments
use Automattic\WooCommerce\Internal\Admin\Settings\Payments;
use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WooPayments\WooPaymentsRestController;
use PHPUnit\Framework\MockObject\MockObject;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_REST_Request;
/**
@@ -16,7 +16,7 @@ use WP_REST_Request;
*
* @class WooPaymentsRestController
*/
-class WooPaymentsRestControllerTest extends WC_REST_Unit_Test_Case {
+class WooPaymentsRestControllerTest extends WC_Unit_Test_Case {
/**
* Endpoint.
*
@@ -46,6 +46,13 @@ class WooPaymentsRestControllerTest extends WC_REST_Unit_Test_Case {
*/
protected $store_admin_id;
+ /**
+ * REST server used by the controller tests.
+ *
+ * @var \WP_REST_Server
+ */
+ protected $server;
+
/**
* Set up test.
*/
@@ -60,7 +67,22 @@ class WooPaymentsRestControllerTest extends WC_REST_Unit_Test_Case {
$this->sut = new WooPaymentsRestController();
$this->sut->init( $this->mock_payments_service, $this->mock_woopayments_service );
- $this->sut->register_routes( true );
+ $this->server = $this->create_rest_server_with_routes(
+ array(
+ function () {
+ $this->sut->register_routes( true );
+ },
+ ),
+ true
+ );
+ }
+
+ /**
+ * Tear down the REST server.
+ */
+ public function tearDown(): void {
+ $this->clear_rest_server();
+ parent::tearDown();
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php
index 35f769f5c87..29cf960457e 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php
@@ -19,7 +19,7 @@ use Automattic\WooCommerce\Testing\Tools\DependencyManagement\MockableLegacyProx
use Automattic\WooCommerce\Testing\Tools\TestingContainer;
use Automattic\WooCommerce\Tests\Internal\Admin\Settings\Mocks\FakePaymentGateway;
use PHPUnit\Framework\MockObject\MockObject;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_REST_Request;
use WC_Gateway_BACS;
use WC_Gateway_Cheque;
@@ -31,7 +31,7 @@ use WC_Gateway_Paypal;
*
* @class PaymentsRestController
*/
-class PaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
+class PaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
/**
* Endpoint.
*
@@ -61,6 +61,13 @@ class PaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
*/
protected $store_admin_id;
+ /**
+ * REST server used by the controller tests.
+ *
+ * @var \WP_REST_Server
+ */
+ protected $server;
+
/**
* The current time in seconds.
*
@@ -98,47 +105,58 @@ class PaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
/**
* Gateways mock.
*
- * @var callable
+ * @var callable|null
*/
- private $gateways_mock_ref;
+ private $gateways_mock_ref = null;
/**
* Incentives WPCOM endpoint response mock.
*
- * @var callable
+ * @var callable|null
*/
- private $woopayments_incentives_response_mock_ref;
+ private $woopayments_incentives_response_mock_ref = null;
/**
- * The initial country that is set before running tests in this test suite.
+ * Raw option states before the test class mutates them.
*
- * @var string $initial_country
+ * @var array<string, array{exists: bool, value: string|null, autoload: string|null}>
*/
- private static string $initial_country = '';
+ private static array $initial_option_states = array();
- /**
- * The initial currency that is set before running tests in this test suite.
- *
- * @var string $initial_currency
- */
- private static string $initial_currency = '';
+ /** @var bool Whether the WooPayments version override existed before the test. */
+ private bool $initial_wcpay_version_override_exists = false;
+
+ /** @var mixed WooPayments version override value before the test. */
+ private $initial_wcpay_version_override;
/**
* Saves values of initial country and currency before running test suite.
*/
public static function wpSetUpBeforeClass(): void {
- self::$initial_country = WC()->countries->get_base_country();
- self::$initial_currency = get_woocommerce_currency();
+ foreach ( self::get_restored_option_names() as $option_name ) {
+ self::$initial_option_states[ $option_name ] = self::get_raw_option_state( $option_name );
+ self::invalidate_option_cache( $option_name );
+ }
}
/**
* Restores initial values of country and currency after running test suite.
*/
public static function wpTearDownAfterClass(): void {
- update_option( 'woocommerce_default_country', self::$initial_country );
- update_option( 'woocommerce_currency', self::$initial_currency );
+ $failures = array();
+ foreach ( self::$initial_option_states as $option_name => $state ) {
+ try {
+ if ( ! self::restore_raw_option_state( $option_name, $state ) ) {
+ $failures[] = "Database write failed for {$option_name}.";
+ }
+ } catch ( \Throwable $error ) {
+ $failures[] = $error->getMessage();
+ }
+ }
- delete_option( 'woocommerce_paypal_settings' );
+ if ( array() !== $failures ) {
+ throw new \RuntimeException( esc_html( implode( ' ', $failures ) ) );
+ }
}
/**
@@ -146,6 +164,12 @@ class PaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
*/
public function setUp(): void {
parent::setUp();
+ $this->initial_wcpay_version_override_exists = array_key_exists( 'WCPAY_VERSION_NUMBER', Constants::$set_constants );
+ $this->initial_wcpay_version_override = Constants::$set_constants['WCPAY_VERSION_NUMBER'] ?? null;
+
+ $gateways = \WC_Payment_Gateways::instance();
+ $gateways->payment_gateways = array();
+ $gateways->init();
$this->store_admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
wp_set_current_user( $this->store_admin_id );
@@ -165,22 +189,173 @@ class PaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
* Tear down.
*/
public function tearDown(): void {
- remove_filter( 'pre_http_request', $this->woopayments_incentives_response_mock_ref );
+ $cleanup_errors = array();
+
+ $this->run_cleanup_step( fn() => $this->unmock_payment_gateways(), $cleanup_errors );
+ $this->run_cleanup_step(
+ function () {
+ if ( isset( $this->mockable_proxy ) ) {
+ $this->mockable_proxy->reset();
+ }
+ },
+ $cleanup_errors
+ );
+ $this->run_cleanup_step( fn() => wc_get_container()->reset_all_resolved(), $cleanup_errors );
+ $this->run_cleanup_step( fn() => $this->clear_rest_server(), $cleanup_errors );
+ $this->run_cleanup_step( fn() => $this->restore_wcpay_version_override(), $cleanup_errors );
+ $this->run_cleanup_step( fn() => parent::tearDown(), $cleanup_errors );
+ $this->run_cleanup_step( fn() => self::invalidate_test_option_caches(), $cleanup_errors );
+
+ if ( array() !== $cleanup_errors ) {
+ throw $cleanup_errors[0];
+ }
+ }
- $this->unmock_payment_gateways();
- delete_option( 'woocommerce_gateway_order' );
+ /**
+ * Run one teardown phase without preventing later phases.
+ *
+ * @param callable $callback Cleanup phase.
+ * @param \Throwable[] $errors Collected cleanup errors.
+ */
+ private function run_cleanup_step( callable $callback, array &$errors ): void {
+ try {
+ $callback();
+ } catch ( \Throwable $error ) {
+ $errors[] = $error;
+ }
+ }
- $this->mockable_proxy->reset();
+ /**
+ * Restore only the Jetpack constant override owned by this fixture.
+ */
+ private function restore_wcpay_version_override(): void {
+ if ( $this->initial_wcpay_version_override_exists ) {
+ Constants::set_constant( 'WCPAY_VERSION_NUMBER', $this->initial_wcpay_version_override );
+ } else {
+ Constants::clear_single_constant( 'WCPAY_VERSION_NUMBER' );
+ }
+ }
- /**
- * TestingContainer instance.
- *
- * @var TestingContainer $container
- */
- $container = wc_get_container();
- $container->reset_all_resolved();
+ /**
+ * Get option rows that must survive the class exactly.
+ *
+ * @return string[]
+ */
+ private static function get_restored_option_names(): array {
+ return array(
+ 'woocommerce_default_country',
+ 'woocommerce_currency',
+ 'woocommerce_paypal_settings',
+ );
+ }
- parent::tearDown();
+ /**
+ * Get option caches mutated by the fixture or SUT.
+ *
+ * @return string[]
+ */
+ private static function get_test_option_names(): array {
+ return array_merge(
+ self::get_restored_option_names(),
+ array(
+ 'woocommerce_gateway_order',
+ OnboardingProfile::DATA_OPTION,
+ Payments::PAYMENTS_PROVIDER_STATE_SNAPSHOTS_KEY,
+ 'woocommerce_paypal_branded',
+ )
+ );
+ }
+
+ /**
+ * Read an option without default filters or value coercion.
+ *
+ * @param string $option_name Option name.
+ * @return array{exists: bool, value: string|null, autoload: string|null}
+ */
+ private static function get_raw_option_state( string $option_name ): array {
+ global $wpdb;
+
+ $wpdb->last_error = '';
+ $row = $wpdb->get_row(
+ $wpdb->prepare( "SELECT option_value, autoload FROM {$wpdb->options} WHERE option_name = %s", $option_name ),
+ ARRAY_A
+ );
+ if ( '' !== $wpdb->last_error ) {
+ throw new \RuntimeException( esc_html( "Failed to read option {$option_name}: {$wpdb->last_error}" ) );
+ }
+
+ return null === $row
+ ? array(
+ 'exists' => false,
+ 'value' => null,
+ 'autoload' => null,
+ )
+ : array(
+ 'exists' => true,
+ 'value' => $row['option_value'],
+ 'autoload' => $row['autoload'],
+ );
+ }
+
+ /**
+ * Restore an option without invoking setting sanitizers.
+ *
+ * @param string $option_name Option name.
+ * @param array{exists: bool, value: string|null, autoload: string|null} $state Raw option state.
+ * @return bool Whether the database operation succeeded.
+ */
+ private static function restore_raw_option_state( string $option_name, array $state ): bool {
+ global $wpdb;
+
+ try {
+ if ( ! $state['exists'] ) {
+ $result = $wpdb->delete( $wpdb->options, array( 'option_name' => $option_name ) );
+ } elseif ( self::get_raw_option_state( $option_name )['exists'] ) {
+ $result = $wpdb->update(
+ $wpdb->options,
+ array(
+ 'option_value' => $state['value'],
+ 'autoload' => $state['autoload'],
+ ),
+ array( 'option_name' => $option_name )
+ );
+ } else {
+ $result = $wpdb->insert(
+ $wpdb->options,
+ array(
+ 'option_name' => $option_name,
+ 'option_value' => $state['value'],
+ 'autoload' => $state['autoload'],
+ )
+ );
+ }
+
+ return false !== $result;
+ } finally {
+ self::invalidate_option_cache( $option_name );
+ }
+ }
+
+ /**
+ * Invalidate all WordPress option cache representations.
+ *
+ * @param string $option_name Option name.
+ */
+ private static function invalidate_option_cache( string $option_name ): void {
+ wp_cache_delete( $option_name, 'options' );
+ wp_cache_delete( 'alloptions', 'options' );
+ wp_cache_delete( 'notoptions', 'options' );
+ }
+
+ /**
+ * Invalidate every option cache representation touched by a test.
+ */
+ private static function invalidate_test_option_caches(): void {
+ foreach ( self::get_test_option_names() as $option_name ) {
+ wp_cache_delete( $option_name, 'options' );
+ }
+ wp_cache_delete( 'alloptions', 'options' );
+ wp_cache_delete( 'notoptions', 'options' );
}
/**
@@ -1729,9 +1904,16 @@ class PaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
$this->service = $container->get( Payments::class );
// Register the REST controller routes again to make sure the dependency tree is using our mocks.
- $sut = new PaymentsRestController();
- $sut->init( $this->service );
- $sut->register_routes( true );
+ $this->controller = new PaymentsRestController();
+ $this->controller->init( $this->service );
+ $this->server = $this->create_rest_server_with_routes(
+ array(
+ function () {
+ $this->controller->register_routes( true );
+ },
+ ),
+ true
+ );
}
/**
@@ -1900,12 +2082,16 @@ class PaymentsRestControllerIntegrationTest extends WC_REST_Unit_Test_Case {
* Unmock the WC payment gateways.
*/
private function unmock_payment_gateways() {
- remove_all_actions( 'wc_payment_gateways_initialized' );
+ if ( null !== $this->gateways_mock_ref ) {
+ remove_action( 'wc_payment_gateways_initialized', $this->gateways_mock_ref, 100 );
+ }
// Reinitialize the WC gateways.
WC()->payment_gateways()->payment_gateways = array();
- WC()->payment_gateways()->init();
-
- $this->providers_service->reset_memo();
+ if ( isset( $this->providers_service ) ) {
+ $this->load_core_paypal_pg();
+ } else {
+ WC()->payment_gateways()->init();
+ }
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerTest.php
index 75e3a571306..ead71daa19b 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerTest.php
@@ -7,7 +7,7 @@ use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders;
use Automattic\WooCommerce\Internal\Admin\Settings\Payments;
use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsRestController;
use PHPUnit\Framework\MockObject\MockObject;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_REST_Request;
use WC_Gateway_BACS;
use WC_Gateway_Cheque;
@@ -19,7 +19,7 @@ use WC_Gateway_PayPal;
*
* @class PaymentsRestController
*/
-class PaymentsRestControllerTest extends WC_REST_Unit_Test_Case {
+class PaymentsRestControllerTest extends WC_Unit_Test_Case {
/**
* Endpoint.
*
@@ -44,6 +44,13 @@ class PaymentsRestControllerTest extends WC_REST_Unit_Test_Case {
*/
protected $store_admin_id;
+ /**
+ * REST server used by the controller tests.
+ *
+ * @var \WP_REST_Server
+ */
+ protected $server;
+
/**
* Set up test.
*/
@@ -57,7 +64,22 @@ class PaymentsRestControllerTest extends WC_REST_Unit_Test_Case {
$this->sut = new PaymentsRestController();
$this->sut->init( $this->mock_service );
- $this->sut->register_routes( true );
+ $this->server = $this->create_rest_server_with_routes(
+ array(
+ function () {
+ $this->sut->register_routes( true );
+ },
+ ),
+ true
+ );
+ }
+
+ /**
+ * Tear down the REST server.
+ */
+ public function tearDown(): void {
+ $this->clear_rest_server();
+ parent::tearDown();
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/CLI/Migrator/Core/WooCommerceProductImporterTest.php b/plugins/woocommerce/tests/php/src/Internal/CLI/Migrator/Core/WooCommerceProductImporterTest.php
index d358b4746f1..f976a6f80ae 100644
--- a/plugins/woocommerce/tests/php/src/Internal/CLI/Migrator/Core/WooCommerceProductImporterTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/CLI/Migrator/Core/WooCommerceProductImporterTest.php
@@ -26,6 +26,27 @@ class WooCommerceProductImporterTest extends \WC_Unit_Test_Case {
*/
private WooCommerceProductImporter $importer;
+ /**
+ * Unique filename prefix for image attachments owned by the current test.
+ *
+ * @var string
+ */
+ private string $image_filename_prefix;
+
+ /**
+ * Number of local image downloads handled during the current test.
+ *
+ * @var int
+ */
+ private int $image_download_count = 0;
+
+ /**
+ * Streamed temporary paths that may need cleanup after a failed sideload.
+ *
+ * @var string[]
+ */
+ private array $streamed_image_paths = array();
+
/**
* Set up before each test.
*/
@@ -33,18 +54,91 @@ class WooCommerceProductImporterTest extends \WC_Unit_Test_Case {
parent::setUp();
// Create importer with default options.
- $this->importer = new WooCommerceProductImporter();
-
- // Clean up any existing products.
- $this->clean_up_products();
+ $this->importer = new WooCommerceProductImporter();
+ $this->image_filename_prefix = 'wc-importer-' . wp_generate_uuid4();
+ $this->image_download_count = 0;
+ $this->streamed_image_paths = array();
+ add_filter( 'pre_http_request', array( $this, 'provide_local_image_response' ), 10, 3 );
}
/**
- * Clean up after each test.
+ * Remove owned upload files and HTTP interception.
*/
public function tearDown(): void {
- $this->clean_up_products();
- parent::tearDown();
+ remove_filter( 'pre_http_request', array( $this, 'provide_local_image_response' ), 10 );
+
+ try {
+ try {
+ global $wpdb;
+ $attachment_ids = $wpdb->get_col(
+ $wpdb->prepare(
+ "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_name LIKE %s",
+ $wpdb->esc_like( $this->image_filename_prefix ) . '%'
+ )
+ );
+ foreach ( $attachment_ids as $attachment_id ) {
+ wp_delete_attachment( (int) $attachment_id, true );
+ }
+ } finally {
+ foreach ( $this->streamed_image_paths as $streamed_image_path ) {
+ if ( file_exists( $streamed_image_path ) ) {
+ wp_delete_file( $streamed_image_path );
+ }
+ }
+ }
+ } finally {
+ parent::tearDown();
+ }
+ }
+
+ /**
+ * Serve the checked-in PNG fixture for importer-owned image URLs.
+ *
+ * @param false|array|\WP_Error $preempt Short-circuit response.
+ * @param array $args Request arguments.
+ * @param string $url Request URL.
+ * @return false|array|\WP_Error
+ */
+ public function provide_local_image_response( $preempt, array $args, string $url ) {
+ if ( false === strpos( $url, 'https://example.com/' . $this->image_filename_prefix ) ) {
+ return $preempt;
+ }
+
+ if ( empty( $args['filename'] ) ) {
+ return new \WP_Error( 'missing_stream_filename', 'Image request did not provide a stream filename.' );
+ }
+
+ $fixture_path = \WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/Dr1Bczxq4q.png';
+ if ( ! self::file_copy( $fixture_path, $args['filename'] ) ) {
+ return new \WP_Error( 'fixture_copy_failed', 'Unable to copy the local image fixture.' );
+ }
+
+ $this->streamed_image_paths[] = $args['filename'];
+ ++$this->image_download_count;
+
+ return array(
+ 'headers' => array(
+ 'content-type' => 'image/png',
+ 'content-length' => (string) filesize( $fixture_path ),
+ ),
+ 'body' => '',
+ 'response' => array(
+ 'code' => 200,
+ 'message' => 'OK',
+ ),
+ 'cookies' => array(),
+ 'filename' => $args['filename'],
+ );
+ }
+
+ /**
+ * Build a unique importer-owned image URL.
+ *
+ * @param string $suffix Filename suffix.
+ * @return string
+ */
+ private function get_image_url( string $suffix ): string {
+ return 'https://example.com/' . $this->image_filename_prefix . '-' . $suffix . '.png';
}
/**
@@ -370,34 +464,50 @@ class WooCommerceProductImporterTest extends \WC_Unit_Test_Case {
}
/**
- * Test image import logic (without requiring actual network access).
+ * Test featured and gallery image import without external network access.
*/
public function test_image_import_with_featured_and_gallery(): void {
$product_data = MockShopifyData::get_mock_wc_product_data( 70 );
$product_data['images'] = array(
array(
- 'src' => 'https://via.placeholder.com/600x400/FF0000/FFFFFF?text=Featured',
- 'alt' => 'Featured Image',
- 'is_featured' => true,
+ 'original_id' => 'gallery-image-1',
+ 'src' => $this->get_image_url( 'gallery-1' ),
+ 'alt' => 'Gallery Image 1',
+ 'is_featured' => false,
),
array(
- 'src' => 'https://via.placeholder.com/600x400/00FF00/000000?text=Gallery1',
- 'alt' => 'Gallery Image 1',
+ 'original_id' => 'featured-image',
+ 'src' => $this->get_image_url( 'featured' ),
+ 'alt' => 'Featured Image',
+ 'is_featured' => true,
),
array(
- 'src' => 'https://via.placeholder.com/600x400/0000FF/FFFFFF?text=Gallery2',
- 'alt' => 'Gallery Image 2',
+ 'original_id' => 'gallery-image-2',
+ 'src' => $this->get_image_url( 'gallery-2' ),
+ 'alt' => 'Gallery Image 2',
+ 'is_featured' => false,
),
);
$result = $this->importer->import_product( $product_data );
$this->assertEquals( 'success', $result['status'] );
- $this->assertNotEmpty( $result['product_id'] );
-
$product = wc_get_product( $result['product_id'] );
- $this->assertNotNull( $product );
- $this->assertEquals( $product_data['name'], $product->get_name() );
+ $this->assertInstanceOf( WC_Product_Simple::class, $product );
+
+ $featured_id = $product->get_image_id();
+ $gallery_ids = $product->get_gallery_image_ids();
+ $this->assertGreaterThan( 0, $featured_id );
+ $this->assertCount( 2, $gallery_ids );
+ $this->assertSame( 3, $this->image_download_count );
+ $this->assertSame( 3, $this->importer->get_import_stats()['images_processed'] );
+ $this->assertSame( 'Featured Image', get_post_meta( $featured_id, '_wp_attachment_image_alt', true ) );
+ $this->assertSame( 'Gallery Image 1', get_post_meta( $gallery_ids[0], '_wp_attachment_image_alt', true ) );
+ $this->assertSame( 'Gallery Image 2', get_post_meta( $gallery_ids[1], '_wp_attachment_image_alt', true ) );
+ foreach ( array_merge( array( $featured_id ), $gallery_ids ) as $attachment_id ) {
+ $this->assertTrue( wp_attachment_is_image( $attachment_id ) );
+ $this->assertSame( $result['product_id'], (int) get_post_field( 'post_parent', $attachment_id ) );
+ }
}
/**
@@ -410,7 +520,8 @@ class WooCommerceProductImporterTest extends \WC_Unit_Test_Case {
$product_data = MockShopifyData::get_mock_wc_product_data( 71 );
$product_data['images'] = array(
array(
- 'src' => 'https://via.placeholder.com/600x400/FF0000/FFFFFF?text=DryRun',
+ 'original_id' => 'dry-run-image',
+ 'src' => $this->get_image_url( 'dry-run' ),
'alt' => 'Dry Run Image',
'is_featured' => true,
),
@@ -422,6 +533,8 @@ class WooCommerceProductImporterTest extends \WC_Unit_Test_Case {
// In dry run mode, no actual images should be imported.
$featured_image_id = get_post_thumbnail_id( $result['product_id'] );
$this->assertEmpty( $featured_image_id );
+ $this->assertSame( 0, $this->image_download_count );
+ $this->assertSame( 0, $dry_run_importer->get_import_stats()['images_processed'] );
}
/**
@@ -429,57 +542,46 @@ class WooCommerceProductImporterTest extends \WC_Unit_Test_Case {
*/
public function test_image_import_with_max_limit(): void {
$limited_importer = new WooCommerceProductImporter();
- $limited_importer->configure( array( 'max_images_per_product' => 2 ) );
+ $limited_importer->configure(
+ array(
+ 'max_images_per_product' => 2,
+ 'skip_duplicate_images' => true,
+ )
+ );
$product_data = MockShopifyData::get_mock_wc_product_data( 72 );
$product_data['images'] = array(
array(
- 'src' => 'https://via.placeholder.com/600x400/FF0000/FFFFFF?text=1',
+ 'original_id' => 'limited-image-1',
+ 'src' => $this->get_image_url( 'limited-1' ),
'is_featured' => true,
),
- array( 'src' => 'https://via.placeholder.com/600x400/00FF00/000000?text=2' ),
- array( 'src' => 'https://via.placeholder.com/600x400/0000FF/FFFFFF?text=3' ),
- array( 'src' => 'https://via.placeholder.com/600x400/FFFF00/000000?text=4' ),
- );
-
- $result = $limited_importer->import_product( $product_data );
- $this->assertEquals( 'success', $result['status'] );
-
- // Test passes if product is created successfully.
- // The max_images_per_product logic is tested (images processed up to limit).
- $this->assertNotEmpty( $result['product_id'] );
-
- // Verify the product was created with correct basic data.
- $product = wc_get_product( $result['product_id'] );
- $this->assertNotNull( $product );
- $this->assertEquals( $product_data['name'], $product->get_name() );
- }
-
- /**
- * Test image import disabled.
- */
- public function test_image_import_disabled(): void {
- $no_images_importer = new WooCommerceProductImporter();
- $no_images_importer->configure( array( 'import_images' => false ) );
-
- $product_data = MockShopifyData::get_mock_wc_product_data( 73 );
- $product_data['images'] = array(
array(
- 'src' => 'https://via.placeholder.com/600x400/FF0000/FFFFFF?text=Disabled',
- 'alt' => 'Should Not Import',
- 'is_featured' => true,
+ 'original_id' => 'limited-image-2',
+ 'src' => $this->get_image_url( 'limited-2' ),
+ 'is_featured' => false,
+ ),
+ array(
+ 'original_id' => 'limited-image-3',
+ 'src' => $this->get_image_url( 'limited-3' ),
+ 'is_featured' => false,
+ ),
+ array(
+ 'original_id' => 'limited-image-4',
+ 'src' => $this->get_image_url( 'limited-4' ),
+ 'is_featured' => false,
),
);
- $result = $no_images_importer->import_product( $product_data );
+ $result = $limited_importer->import_product( $product_data );
$this->assertEquals( 'success', $result['status'] );
- // No images should be imported when disabled.
- $featured_image_id = get_post_thumbnail_id( $result['product_id'] );
- $this->assertEmpty( $featured_image_id );
-
- $gallery = get_post_meta( $result['product_id'], '_product_image_gallery', true );
- $this->assertEmpty( $gallery );
+ $product = wc_get_product( $result['product_id'] );
+ $this->assertInstanceOf( WC_Product_Simple::class, $product );
+ $this->assertGreaterThan( 0, $product->get_image_id() );
+ $this->assertCount( 1, $product->get_gallery_image_ids() );
+ $this->assertSame( 2, $this->image_download_count );
+ $this->assertSame( 2, $limited_importer->get_import_stats()['images_processed'] );
}
/**
@@ -849,49 +951,4 @@ class WooCommerceProductImporterTest extends \WC_Unit_Test_Case {
$this->assertSame( '', $product->get_width() );
$this->assertSame( '', $product->get_height() );
}
-
- /**
- * Helper method to clean up test products.
- */
- private function clean_up_products(): void {
- global $wpdb;
-
- // Delete test products by SKU pattern.
- $test_skus = $wpdb->get_col(
- "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_sku' AND meta_value LIKE 'TEST-SKU-%'"
- );
-
- foreach ( $test_skus as $sku ) {
- $product_id = wc_get_product_id_by_sku( $sku );
- if ( $product_id ) {
- wp_delete_post( $product_id, true );
- }
- }
-
- // Clean up test categories.
- $test_categories = get_terms(
- array(
- 'taxonomy' => 'product_cat',
- 'name__like' => 'Test Category',
- 'hide_empty' => false,
- )
- );
-
- foreach ( $test_categories as $category ) {
- wp_delete_term( $category->term_id, 'product_cat' );
- }
-
- // Clean up test tags.
- $test_tags = get_terms(
- array(
- 'taxonomy' => 'product_tag',
- 'name__like' => 'test-tag',
- 'hide_empty' => false,
- )
- );
-
- foreach ( $test_tags as $tag ) {
- wp_delete_term( $tag->term_id, 'product_tag' );
- }
- }
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Customers/SearchServiceTest.php b/plugins/woocommerce/tests/php/src/Internal/Customers/SearchServiceTest.php
index fcc692faf5a..bc6abbedf57 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Customers/SearchServiceTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Customers/SearchServiceTest.php
@@ -6,7 +6,6 @@ namespace Automattic\WooCommerce\Tests\Internal\Customers;
use Automattic\WooCommerce\Internal\Customers\SearchService as CustomersSearchService;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper;
-use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
use Automattic\WooCommerce\Utilities\OrderUtil;
@@ -124,7 +123,8 @@ class SearchServiceTest extends \WC_Unit_Test_Case {
* @return \WC_Order
*/
private function create_order_for( \WC_Customer $customer ): \WC_Order {
- $order = OrderHelper::create_order( $customer->get_id() );
+ $order = new \WC_Order();
+ $order->set_customer_id( $customer->get_id() );
$order->set_billing_email( $customer->get_billing_email( 'edit' ) );
$order->save();
return $order;
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/DataSynchronizerTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/DataSynchronizerTests.php
index 797ff7e107b..2ab5d1945f0 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/DataSynchronizerTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/DataSynchronizerTests.php
@@ -28,6 +28,13 @@ class DataSynchronizerTests extends \HposTestCase {
*/
private $sut;
+ /**
+ * Ensure permanent HPOS tables exist before per-test transactions start.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ self::setup_cot_tables();
+ }
+
/**
* Initializes system under test.
*/
@@ -41,7 +48,6 @@ class DataSynchronizerTests extends \HposTestCase {
// Remove the Test Suite’s use of temporary tables https://wordpress.stackexchange.com/a/220308.
remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
- OrderHelper::delete_order_custom_tables(); // We need this since non-temporary tables won't drop automatically.
OrderHelper::create_order_custom_table_if_not_exist();
OrderHelper::toggle_cot_feature_and_usage( false );
$this->sut = $container->get( DataSynchronizer::class );
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/HposOrderCapabilityHelperTest.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/HposOrderCapabilityHelperTest.php
index 5506ac7b77a..1eb702e096e 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/HposOrderCapabilityHelperTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/HposOrderCapabilityHelperTest.php
@@ -14,6 +14,13 @@ use WC_Unit_Test_Case;
class HposOrderCapabilityHelperTest extends WC_Unit_Test_Case {
use HPOSToggleTrait;
+ /**
+ * Ensure permanent HPOS tables exist before per-test transactions start.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ self::setup_cot_tables();
+ }
+
/**
* Custom roles registered during a test.
*
@@ -35,7 +42,9 @@ class HposOrderCapabilityHelperTest extends WC_Unit_Test_Case {
'private' => true,
)
);
- $this->setup_cot();
+ remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
+ remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
+ $this->toggle_cot_authoritative( true );
$this->disable_cot_sync();
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/LegacyDataHandlerTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/LegacyDataHandlerTests.php
index 7e052aa226a..e1bfadbfff5 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/LegacyDataHandlerTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/LegacyDataHandlerTests.php
@@ -8,6 +8,7 @@ use Automattic\WooCommerce\Internal\DataStores\Orders\LegacyDataHandler;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
use Automattic\WooCommerce\Enums\OrderStatus;
+use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* Class OrdersTableQueryTests.
@@ -15,11 +16,25 @@ use Automattic\WooCommerce\Enums\OrderStatus;
class LegacyDataHandlerTests extends \WC_Unit_Test_Case {
use HPOSToggleTrait;
+ /**
+ * Ensure permanent HPOS tables exist before per-test transactions start.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ self::setup_cot_tables();
+ }
+
/**
* @var LegacyDataHandler
*/
private $sut;
+ /**
+ * Whether HPOS was authoritative before the test.
+ *
+ * @var bool
+ */
+ private $previous_hpos_state;
+
/**
* Initializes system under test.
*/
@@ -27,7 +42,10 @@ class LegacyDataHandlerTests extends \WC_Unit_Test_Case {
parent::setUp();
add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
- $this->setup_cot();
+ $this->previous_hpos_state = OrderUtil::custom_orders_table_usage_is_enabled();
+ remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
+ remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
+ OrderHelper::toggle_cot_feature_and_usage( true );
$this->sut = wc_get_container()->get( LegacyDataHandler::class );
}
@@ -36,9 +54,10 @@ class LegacyDataHandlerTests extends \WC_Unit_Test_Case {
* Destroys system under test.
*/
public function tearDown(): void {
- parent::tearDown();
$this->clean_up_cot_setup();
+ OrderHelper::toggle_cot_feature_and_usage( $this->previous_hpos_state );
remove_all_filters( 'wc_allow_changing_orders_storage_while_sync_is_pending' );
+ parent::tearDown();
}
/**
@@ -105,7 +124,9 @@ class LegacyDataHandlerTests extends \WC_Unit_Test_Case {
$this->enable_cot_sync();
$order_ids = array();
for ( $i = 0; $i < 10; $i++ ) {
- $order_id = OrderHelper::create_order()->get_id();
+ $order = new \WC_Order();
+ $order->save();
+ $order_id = $order->get_id();
$order_ids[] = $order_id;
}
$this->disable_cot_sync();
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreRestOrdersControllerTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreRestOrdersControllerTests.php
index b4edfd6573c..081bda2ba29 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreRestOrdersControllerTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreRestOrdersControllerTests.php
@@ -21,6 +21,15 @@ if ( ! class_exists( 'WC_REST_Orders_Controller_Tests' ) ) {
* Test for REST support in the OrdersTableDataStore class.
*/
class OrdersTableDataStoreRestOrdersControllerTests extends \WC_REST_Orders_Controller_Tests {
+ /**
+ * Ensure permanent HPOS tables exist before per-test transactions start.
+ *
+ * @param \WP_UnitTest_Factory $factory WordPress unit test factory.
+ */
+ public static function wpSetUpBeforeClass( $factory ): void {
+ parent::wpSetUpBeforeClass( $factory );
+ self::setup_cot_tables();
+ }
/**
* Initializes system under test.
@@ -33,8 +42,6 @@ class OrdersTableDataStoreRestOrdersControllerTests extends \WC_REST_Orders_Cont
// Remove the Test Suite’s use of temporary tables https://wordpress.stackexchange.com/a/220308.
remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
- OrderHelper::delete_order_custom_tables();
- OrderHelper::create_order_custom_table_if_not_exist();
$this->toggle_cot_feature_and_usage( true );
}
@@ -49,9 +56,9 @@ class OrdersTableDataStoreRestOrdersControllerTests extends \WC_REST_Orders_Cont
add_filter( 'query', array( $this, '_create_temporary_tables' ) );
add_filter( 'query', array( $this, '_drop_temporary_tables' ) );
- remove_all_filters( 'wc_allow_changing_orders_storage_while_sync_is_pending' );
-
parent::tearDown();
+
+ remove_all_filters( 'wc_allow_changing_orders_storage_while_sync_is_pending' );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
index 659ef9bbef1..61cd4e474e3 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
@@ -40,6 +40,13 @@ class OrdersTableDataStoreTests extends \HposTestCase {
use HPOSToggleTrait;
use CogsAwareUnitTestSuiteTrait;
+ /**
+ * Ensure permanent HPOS tables exist before per-test transactions start.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ self::setup_cot_tables();
+ }
+
/**
* Original timezone before this test started.
* @var string
@@ -86,7 +93,9 @@ class OrdersTableDataStoreTests extends \HposTestCase {
//phpcs:ignore WordPress.DateTime.RestrictedFunctions.timezone_change_date_default_timezone_set -- We need to change the timezone to test the date sync fields.
update_option( 'timezone_string', 'Asia/Kolkata' );
// Remove the Test Suite’s use of temporary tables https://wordpress.stackexchange.com/a/220308.
- $this->setup_cot();
+ remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
+ remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
+ $this->toggle_cot_authoritative( true );
$this->cot_state = OrderUtil::custom_orders_table_usage_is_enabled();
$this->toggle_cot_feature_and_usage( false );
$container = wc_get_container();
@@ -3756,6 +3765,8 @@ class OrdersTableDataStoreTests extends \HposTestCase {
* @param string $datastore_to_use Which datastore to use. Either 'hpos' or 'posts'.
*/
public function test_order_util_get_count_for_type( $datastore_to_use ) {
+ global $wpdb;
+
$this->disable_cot_sync();
if ( 'hpos' === $datastore_to_use ) {
@@ -3767,17 +3778,43 @@ class OrdersTableDataStoreTests extends \HposTestCase {
// Create a few orders in various states.
$order_statuses = array_keys( wc_get_order_statuses() );
- $expected_counts = array_combine( $order_statuses, array_fill( 0, count( $order_statuses ), 0 ) );
+ $expected_counts = array_combine( $order_statuses, array_fill( 0, count( $order_statuses ), 0 ) );
+ $order_placeholders = array();
+ $order_values = array();
+ $next_order_id = null;
+
+ if ( 'hpos' === $datastore_to_use ) {
+ $next_order_id = (int) $wpdb->get_var( "SELECT GREATEST(COALESCE((SELECT MAX(id) FROM {$wpdb->prefix}wc_orders), 0), COALESCE((SELECT MAX(ID) FROM {$wpdb->posts}), 0)) + 1" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names are provided by WordPress.
+ }
+
foreach ( $order_statuses as $i => $status ) {
foreach ( range( 0, $i ) as $_ ) {
$expected_counts[ $status ] = $i + 1;
- $order = WC_Helper_Order::create_order();
- $order->set_status( $status );
- $order->save();
+ if ( null !== $next_order_id ) {
+ $order_placeholders[] = '(%d, %s, %s)';
+ array_push( $order_values, $next_order_id++, 'shop_order', $status );
+ } else {
+ $order_placeholders[] = '(%s, %s)';
+ array_push( $order_values, 'shop_order', $status );
+ }
}
}
+ if ( 'hpos' === $datastore_to_use ) {
+ $table_name = $wpdb->prefix . 'wc_orders';
+ $columns = 'id, type, status';
+ } else {
+ $table_name = $wpdb->posts;
+ $columns = 'post_type, post_status';
+ }
+
+ $insert_query = $wpdb->prepare(
+ "INSERT INTO {$table_name} ({$columns}) VALUES " . implode( ', ', $order_placeholders ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.NotPrepared -- Table and columns are selected from constants above; placeholders are generated above.
+ $order_values
+ );
+ $wpdb->query( $insert_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared immediately above.
+
$real_counts = OrderUtil::get_count_for_type( 'shop_order' );
foreach ( $expected_counts as $status => $count ) {
$this->assertArrayHasKey( $status, $real_counts );
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableQueryTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableQueryTests.php
index 9ab17195de0..e9bd85aad73 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableQueryTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableQueryTests.php
@@ -20,6 +20,13 @@ use WC_Order;
class OrdersTableQueryTests extends \WC_Unit_Test_Case {
use HPOSToggleTrait;
+ /**
+ * Ensure permanent HPOS tables exist before per-test transactions start.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ self::setup_cot_tables();
+ }
+
/**
* Stores the original COT state.
*
@@ -34,7 +41,8 @@ class OrdersTableQueryTests extends \WC_Unit_Test_Case {
parent::setUp();
add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
$this->cot_state = OrderUtil::custom_orders_table_usage_is_enabled();
- $this->setup_cot();
+ remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
+ remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
$this->toggle_cot_feature_and_usage( true );
}
@@ -619,7 +627,7 @@ class OrdersTableQueryTests extends \WC_Unit_Test_Case {
public function test_total_filtering_with_operators() {
$order_totals_to_test = array( 5, 10, 50, 100.00, 100.00, 250.50, 250.50, 500.75, 1000.00 );
foreach ( $order_totals_to_test as $order_total ) {
- $order = OrderHelper::create_order();
+ $order = wc_create_order();
$order->set_total( $order_total );
$order->save();
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableRefundDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableRefundDataStoreTests.php
index 3372af9659e..989b3af5467 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableRefundDataStoreTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableRefundDataStoreTests.php
@@ -16,6 +16,13 @@ use WC_Order_Refund_Data_Store_CPT;
class OrdersTableRefundDataStoreTests extends \WC_Unit_Test_Case {
use \Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
+ /**
+ * Ensure permanent HPOS tables exist before per-test transactions start.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ self::setup_cot_tables();
+ }
+
/**
* @var PostsToOrdersMigrationController
*/
@@ -44,8 +51,6 @@ class OrdersTableRefundDataStoreTests extends \WC_Unit_Test_Case {
// Remove the Test Suite’s use of temporary tables https://wordpress.stackexchange.com/a/220308.
remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
- OrderHelper::delete_order_custom_tables(); // We need this since non-temporary tables won't drop automatically.
- OrderHelper::create_order_custom_table_if_not_exist();
$this->sut = wc_get_container()->get( OrdersTableRefundDataStore::class );
$this->order_data_store = wc_get_container()->get( OrdersTableDataStore::class );
$this->migrator = wc_get_container()->get( PostsToOrdersMigrationController::class );
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/StockNotifications/StockNotificationsDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/StockNotifications/StockNotificationsDataStoreTests.php
index 08774d58752..3ece68a6b83 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/StockNotifications/StockNotificationsDataStoreTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/StockNotifications/StockNotificationsDataStoreTests.php
@@ -33,11 +33,11 @@ class StockNotificationsDataStoreTests extends \WC_Unit_Test_Case {
* Tear down the test.
*/
public function tearDown(): void {
- parent::tearDown();
// Clean up all notifications.
global $wpdb;
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_stock_notifications" );
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_stock_notificationmeta" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_stock_notificationmeta" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_stock_notifications" );
+ parent::tearDown();
}
/**
@@ -71,7 +71,7 @@ class StockNotificationsDataStoreTests extends \WC_Unit_Test_Case {
$notification->save();
// Verify all properties were saved correctly.
- $this->assertEquals( 1, $notification->get_id() );
+ $this->assertGreaterThan( 0, $notification->get_id() );
$this->assertEquals( 1, $notification->get_product_id() );
$this->assertEquals( 1, $notification->get_user_id() );
$this->assertEquals( 'test@test.com', $notification->get_user_email() );
@@ -125,7 +125,7 @@ class StockNotificationsDataStoreTests extends \WC_Unit_Test_Case {
$notification->set_user_id( 1 );
$notification->save();
- $this->assertEquals( 1, $notification->get_id() );
+ $this->assertGreaterThan( 0, $notification->get_id() );
$this->assertEquals( 1, $notification->get_product_id() );
$this->assertEquals( 1, $notification->get_user_id() );
$this->assertEquals( null, $notification->get_user_email() );
@@ -533,8 +533,8 @@ class StockNotificationsDataStoreTests extends \WC_Unit_Test_Case {
);
$this->assertCount( 2, $notifications );
- $this->assertEquals( 2, $notifications[0] );
- $this->assertEquals( 3, $notifications[1] );
+ $this->assertEquals( $notification_2->get_id(), $notifications[0] );
+ $this->assertEquals( $notification_3->get_id(), $notifications[1] );
}
/**
@@ -579,7 +579,7 @@ class StockNotificationsDataStoreTests extends \WC_Unit_Test_Case {
);
$this->assertCount( 1, $notifications );
- $this->assertEquals( 1, $notifications[0] );
+ $this->assertEquals( $notification->get_id(), $notifications[0] );
// Check the return type is ids.
$notifications = $this->data_store->query(
@@ -590,7 +590,7 @@ class StockNotificationsDataStoreTests extends \WC_Unit_Test_Case {
)
);
$this->assertCount( 1, $notifications );
- $this->assertEquals( 1, $notifications[0] );
+ $this->assertEquals( $notification->get_id(), $notifications[0] );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/DownloadPermissionsAdjusterTest.php b/plugins/woocommerce/tests/php/src/Internal/DownloadPermissionsAdjusterTest.php
index 65ac309d8f2..3c050dd79ac 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DownloadPermissionsAdjusterTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DownloadPermissionsAdjusterTest.php
@@ -25,8 +25,8 @@ class DownloadPermissionsAdjusterTest extends \WC_Unit_Test_Case {
* Runs before each test.
*/
public function setUp(): void {
- $this->sut = new DownloadPermissionsAdjuster();
- $this->sut->init();
+ parent::setUp();
+ $this->initialize_subject();
// This is needed for "product->set_downloads" to work without actual files.
add_filter(
@@ -48,13 +48,38 @@ class DownloadPermissionsAdjusterTest extends \WC_Unit_Test_Case {
wc_get_container()->get( Download_Directories::class )->set_mode( Download_Directories::MODE_DISABLED );
}
+ /**
+ * Reset non-transactional test doubles and option caches.
+ */
+ public function tearDown(): void {
+ try {
+ $this->reset_legacy_proxy_mocks();
+ } finally {
+ try {
+ parent::tearDown();
+ } finally {
+ wp_cache_delete( 'wc_downloads_approved_directories_mode', 'options' );
+ wp_cache_delete( 'alloptions', 'options' );
+ wp_cache_delete( 'notoptions', 'options' );
+ }
+ }
+ }
+
+ /**
+ * Initialize the subject after test doubles have been registered.
+ */
+ private function initialize_subject(): void {
+ $this->sut = new DownloadPermissionsAdjuster();
+ $this->sut->init();
+ }
+
/**
* @testdox DownloadPermissionsAdjuster class hooks on 'adjust_download_permissions' on initialization.
*/
public function test_class_hooks_on_adjust_download_permissions() {
remove_all_actions( 'adjust_download_permissions' );
$this->assertFalse( has_action( 'adjust_download_permissions' ) );
- $this->setUp();
+ $this->initialize_subject();
$this->assertTrue( has_action( 'adjust_download_permissions' ) );
}
@@ -192,7 +217,7 @@ class DownloadPermissionsAdjusterTest extends \WC_Unit_Test_Case {
$data_store = $this->create_mock_data_store( $data_for_data_store );
- $this->setUp();
+ $this->initialize_subject();
$this->sut->adjust_download_permissions( $product->get_id() );
$expected_created_data = array(
@@ -254,7 +279,7 @@ class DownloadPermissionsAdjusterTest extends \WC_Unit_Test_Case {
$data_store = $this->create_mock_data_store( $data_for_data_store );
- $this->setUp();
+ $this->initialize_subject();
$this->sut->adjust_download_permissions( $product->get_id() );
$this->assertEmpty( $data_store->created_data );
@@ -311,7 +336,7 @@ class DownloadPermissionsAdjusterTest extends \WC_Unit_Test_Case {
$data_store = $this->create_mock_data_store( $data_for_data_store );
- $this->setUp();
+ $this->initialize_subject();
$this->sut->adjust_download_permissions( $product->get_id() );
$expected = array(
diff --git a/plugins/woocommerce/tests/php/src/Internal/Logging/OrderLogsCleanupTest.php b/plugins/woocommerce/tests/php/src/Internal/Logging/OrderLogsCleanupTest.php
index 8ddc5755ca9..ac2d286da6b 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Logging/OrderLogsCleanupTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Logging/OrderLogsCleanupTest.php
@@ -12,6 +12,7 @@ use Automattic\WooCommerce\Internal\Logging\OrderLogsCleanupHelper;
use Automattic\WooCommerce\Internal\Logging\OrderLogsDeletionProcessor;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
+use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper;
use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
use Automattic\WooCommerce\Testing\Tools\TestingContainer;
@@ -447,9 +448,10 @@ class OrderLogsCleanupTest extends \WC_Unit_Test_Case {
*/
private function create_orders_with_logs( int $count ): array {
$order_ids = array();
+ $product = ProductHelper::create_simple_product();
for ( $i = 0; $i < $count; $i++ ) {
- $order = OrderHelper::create_order();
+ $order = OrderHelper::create_order( 1, $product );
$order->add_meta_data( '_debug_log_source_pending_deletion', 'place-order-debug-' . $i );
$order->save();
$order_ids[] = $order->get_id();
diff --git a/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php b/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php
index 46eed93d380..4add9e455f2 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php
@@ -9,8 +9,8 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging {
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\Logging\RemoteLogger;
use Automattic\WooCommerce\Utilities\StringUtil;
- use WC_Rate_Limiter;
use WC_Cache_Helper;
+ use WC_Rate_Limiter;
/**
* Class RemoteLoggerTest.
*/
@@ -22,6 +22,12 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging {
*/
private $sut;
+ /** @var array<string, mixed> Jetpack constant overrides before the test. */
+ private array $initial_constant_overrides = array();
+
+ /** @var array<string, array{exists: bool, value: mixed}> Namespace mock globals before the test. */
+ private array $initial_mock_globals = array();
+
/**
* Set up test
*
@@ -29,6 +35,13 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging {
*/
public function setUp(): void {
parent::setUp();
+ $this->initial_constant_overrides = Constants::$set_constants;
+ foreach ( array( 'mock_filter_input', 'mock_return' ) as $global_name ) {
+ $this->initial_mock_globals[ $global_name ] = array(
+ 'exists' => array_key_exists( $global_name, $GLOBALS ),
+ 'value' => $GLOBALS[ $global_name ] ?? null,
+ );
+ }
$this->sut = wc_get_container()->get( RemoteLogger::class );
}
@@ -38,33 +51,36 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging {
* @return void
*/
public function tearDown(): void {
- $this->cleanup_filters();
- delete_option( 'woocommerce_feature_remote_logging_enabled' );
- delete_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT );
- global $wpdb;
- $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_rate_limits" );
- WC_Cache_Helper::invalidate_cache_group( WC_Rate_Limiter::CACHE_GROUP );
- Constants::clear_constants();
- }
-
- /**
- * Cleanup filters used in tests.
- *
- * @return void
- */
- private function cleanup_filters() {
- $filters = array(
- 'option_woocommerce_admin_remote_feature_enabled',
- 'option_woocommerce_allow_tracking',
- 'option_woocommerce_version',
- 'plugins_api',
- 'pre_http_request',
- 'woocommerce_remote_logger_formatted_log_data',
- 'pre_site_transient_update_plugins',
- 'woocommerce_remote_logger_request_uri_whitelist',
- );
- foreach ( $filters as $filter ) {
- remove_all_filters( $filter );
+ try {
+ Constants::$set_constants = $this->initial_constant_overrides;
+ foreach ( $this->initial_mock_globals as $global_name => $state ) {
+ if ( $state['exists'] ) {
+ $GLOBALS[ $global_name ] = $state['value'];
+ } else {
+ unset( $GLOBALS[ $global_name ] );
+ }
+ }
+ } finally {
+ try {
+ parent::tearDown();
+ } finally {
+ wp_cache_delete( 'woocommerce_feature_remote_logging_enabled', 'options' );
+ wp_cache_delete( 'alloptions', 'options' );
+ wp_cache_delete( 'notoptions', 'options' );
+
+ $transient_name = RemoteLogger::WC_NEW_VERSION_TRANSIENT;
+ $network_id = get_current_network_id();
+ wp_cache_delete( $transient_name, 'site-transient' );
+ wp_cache_delete( '_site_transient_' . $transient_name, 'options' );
+ wp_cache_delete( '_site_transient_timeout_' . $transient_name, 'options' );
+ wp_cache_delete( $network_id . ':_site_transient_' . $transient_name, 'site-options' );
+ wp_cache_delete( $network_id . ':_site_transient_timeout_' . $transient_name, 'site-options' );
+ wp_cache_delete( $network_id . ':notoptions', 'site-options' );
+ wp_cache_delete(
+ WC_Cache_Helper::get_cache_prefix( 'rate_limit' . RemoteLogger::RATE_LIMIT_ID ),
+ WC_Rate_Limiter::CACHE_GROUP
+ );
+ }
}
}
@@ -409,7 +425,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging {
}
/**
- * @testdox handle method successfully sends log
+ * @testdox handle method successfully sends log.
*/
public function test_handle_successful() {
$this->sut = $this->getMockBuilder( RemoteLoggerWithEnvironmentOverride::class )
@@ -418,10 +434,15 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging {
$this->sut->set_is_dev_or_local( false );
$this->sut->method( 'is_remote_logging_allowed' )->willReturn( true );
+ $request_count = 0;
+ $request_url = null;
+ add_filter( 'option_woocommerce_allow_tracking', fn() => 'yes' );
add_filter(
'pre_http_request',
- function ( $preempt, $args ) {
+ function ( $preempt, $args, $url ) use ( &$request_count, &$request_url ) {
+ ++$request_count;
+ $request_url = $url;
$this->assertArrayHasKey( 'body', $args );
$this->assertArrayHasKey( 'headers', $args );
return array(
@@ -436,8 +457,10 @@ namespace Automattic\WooCommerce\Tests\Internal\Logging {
3
);
- $this->assertTrue( $this->sut->handle( time(), 'critical', 'Test message', array( 'remote-logging' => true ) ) );
+ $this->assertTrue( $this->sut->handle( time(), 'error', 'Test message', array( 'remote-logging' => true ) ) );
+ $this->assertSame( 'https://public-api.wordpress.com/rest/v1.1/logstash', $request_url );
$this->assertTrue( WC_Rate_Limiter::retried_too_soon( RemoteLogger::RATE_LIMIT_ID ) );
+ $this->assertSame( 1, $request_count );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SchedulerTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SchedulerTest.php
index 30f86987020..bdd64de6b27 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SchedulerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SchedulerTest.php
@@ -17,6 +17,41 @@ use WC_Unit_Test_Case;
* @covers \Automattic\WooCommerce\Internal\OrderReviews\Scheduler
*/
class SchedulerTest extends WC_Unit_Test_Case {
+ /**
+ * Product ID shared by generic reviewable-order fixtures.
+ *
+ * @var int
+ */
+ private static $reviewable_product_id;
+
+ /**
+ * Create the immutable reviewable product before per-test transactions begin.
+ */
+ public static function wpSetUpBeforeClass(): void {
+ self::enable_direct_product_attribute_lookup_updates();
+
+ try {
+ self::$reviewable_product_id = WC_Helper_Product::create_simple_product()->get_id();
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
+ }
+ }
+
+ /**
+ * Delete the shared product through the WooCommerce data store.
+ */
+ public static function wpTearDownAfterClass(): void {
+ self::enable_direct_product_attribute_lookup_updates();
+
+ try {
+ $product = wc_get_product( self::$reviewable_product_id );
+ if ( $product ) {
+ $product->delete( true );
+ }
+ } finally {
+ self::disable_direct_product_attribute_lookup_updates();
+ }
+ }
/**
* Prepare the mailer and enable the review-request email.
@@ -316,7 +351,7 @@ class SchedulerTest extends WC_Unit_Test_Case {
* Create an order in a non-completed status so transitioning to completed fires the hook cleanly.
*/
private function create_pending_order(): WC_Order {
- $order = OrderHelper::create_order();
+ $order = OrderHelper::create_order( 1, wc_get_product( self::$reviewable_product_id ) );
$order->set_status( 'pending' );
$order->save();
return $order;
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SubmissionHandlerTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SubmissionHandlerTest.php
index f4971d87f37..0201095fcaa 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SubmissionHandlerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SubmissionHandlerTest.php
@@ -6,7 +6,6 @@ namespace Automattic\WooCommerce\Tests\Internal\OrderReviews;
use Automattic\WooCommerce\Enums\OrderStatus;
use Automattic\WooCommerce\Internal\OrderReviews\ItemEligibility;
use Automattic\WooCommerce\Internal\OrderReviews\SubmissionHandler;
-use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
use WC_Helper_Product;
use WC_Order;
use WC_Unit_Test_Case;
@@ -41,6 +40,21 @@ class SubmissionHandlerTest extends WC_Unit_Test_Case {
parent::tearDown();
}
+ /**
+ * Build an empty completed order for review submissions.
+ */
+ private function make_empty_order(): WC_Order {
+ $order = new WC_Order();
+ $order->set_customer_id( 1 );
+ $order->set_billing_first_name( 'Jane' );
+ $order->set_billing_last_name( 'Doe' );
+ $order->set_billing_email( 'jane@example.test' );
+ $order->set_status( OrderStatus::COMPLETED );
+ $order->save();
+
+ return $order;
+ }
+
/**
* Build a completed order with the given number of products.
*
@@ -48,15 +62,7 @@ class SubmissionHandlerTest extends WC_Unit_Test_Case {
* @return array{order:WC_Order, product_ids:int[], item_ids:int[]}
*/
private function make_order( int $product_count = 1 ): array {
- $order = OrderHelper::create_order();
- // Wipe the default item.
- foreach ( $order->get_items() as $item ) {
- $order->remove_item( $item->get_id() );
- }
- $order->set_billing_first_name( 'Jane' );
- $order->set_billing_last_name( 'Doe' );
- $order->set_billing_email( 'jane@example.test' );
- $order->set_status( OrderStatus::COMPLETED );
+ $order = $this->make_empty_order();
$product_ids = array();
for ( $i = 0; $i < $product_count; $i++ ) {
@@ -226,12 +232,8 @@ class SubmissionHandlerTest extends WC_Unit_Test_Case {
$variation_a = wc_get_product( $variation_ids[0] );
$variation_b = wc_get_product( $variation_ids[1] );
- $order = OrderHelper::create_order();
- foreach ( $order->get_items() as $item ) {
- $order->remove_item( $item->get_id() );
- }
+ $order = $this->make_empty_order();
$order->set_billing_email( 'shopper@example.test' );
- $order->set_status( OrderStatus::COMPLETED );
$order->add_product( $variation_a, 1 );
$order->add_product( $variation_b, 1 );
$order->save();
@@ -542,9 +544,9 @@ class SubmissionHandlerTest extends WC_Unit_Test_Case {
$variation_ids = $variable->get_children();
$variation_a = wc_get_product( $variation_ids[0] );
$variation_b = wc_get_product( $variation_ids[1] );
- $other_order = OrderHelper::create_order();
+ $other_order = $this->make_empty_order();
- // Pre-existing approved review from an older order, same parent, variation A.
+ // Pre-existing approved review from an older order, same parent, variation B.
$prior_comment_id = (int) wp_insert_comment(
array(
'comment_post_ID' => $variable->get_id(),
@@ -557,14 +559,9 @@ class SubmissionHandlerTest extends WC_Unit_Test_Case {
);
add_comment_meta( $prior_comment_id, 'rating', 4, true );
add_comment_meta( $prior_comment_id, ItemEligibility::ORDER_META_KEY, (int) $other_order->get_id(), true );
- add_comment_meta( $prior_comment_id, ItemEligibility::VARIATION_META_KEY, (int) $variation_a->get_id(), true );
+ add_comment_meta( $prior_comment_id, ItemEligibility::VARIATION_META_KEY, (int) $variation_b->get_id(), true );
- $order = OrderHelper::create_order();
- foreach ( $order->get_items() as $item ) {
- $order->remove_item( $item->get_id() );
- }
- $order->set_billing_email( 'jane@example.test' );
- $order->set_status( OrderStatus::COMPLETED );
+ $order = $this->make_empty_order();
$order->add_product( $variation_a, 1 );
$order->add_product( $variation_b, 1 );
$order->save();
@@ -608,12 +605,7 @@ class SubmissionHandlerTest extends WC_Unit_Test_Case {
$variation_a = wc_get_product( $variation_ids[0] );
$variation_b = wc_get_product( $variation_ids[1] );
- $order = OrderHelper::create_order();
- foreach ( $order->get_items() as $item ) {
- $order->remove_item( $item->get_id() );
- }
- $order->set_billing_email( 'jane@example.test' );
- $order->set_status( OrderStatus::COMPLETED );
+ $order = $this->make_empty_order();
$order->add_product( $variation_a, 1 );
$order->add_product( $variation_b, 1 );
$order->save();
diff --git a/plugins/woocommerce/tests/php/src/Internal/Orders/MobileMessagingHandlerTest.php b/plugins/woocommerce/tests/php/src/Internal/Orders/MobileMessagingHandlerTest.php
index abdcb0f9a3b..1400aed0c54 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Orders/MobileMessagingHandlerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Orders/MobileMessagingHandlerTest.php
@@ -18,29 +18,93 @@ class MobileMessagingHandlerTest extends \WC_Unit_Test_Case {
const ORDER_ID = 5;
const DOMAIN = 'sample-domain.com';
- /**
- * @var string $initial_country that is set on site which is a platform for unit tests, before running tests in this test suite.
- */
- private static $initial_country = '';
- /**
- * @var string $initial_currency that is set on site which is a platform for unit tests, before running tests in this test suite.
- */
- private static $initial_currency = '';
+ /** @var array{exists: bool, value: string|null, autoload: string|null} Initial raw country option state. */
+ private static array $initial_country = array();
+
+ /** @var array{exists: bool, value: string|null, autoload: string|null} Initial raw currency option state. */
+ private static array $initial_currency = array();
/**
* Saves values of initial country and currency before running test suite.
*/
public static function wpSetUpBeforeClass(): void {
- self::$initial_country = WC()->countries->get_base_country();
- self::$initial_currency = get_woocommerce_currency();
+ self::$initial_country = self::get_raw_option_state( 'woocommerce_default_country' );
+ self::$initial_currency = self::get_raw_option_state( 'woocommerce_currency' );
}
/**
* Restores initial values of country and currency after running test suite.
*/
public static function wpTearDownAfterClass(): void {
- update_option( 'woocommerce_default_country', self::$initial_country );
- update_option( 'woocommerce_currency', self::$initial_currency );
+ try {
+ self::restore_raw_option_state( 'woocommerce_default_country', self::$initial_country );
+ } finally {
+ self::restore_raw_option_state( 'woocommerce_currency', self::$initial_currency );
+ }
+ }
+
+ /**
+ * Read an option without default-option filters or value coercion.
+ *
+ * @param string $option_name Option name.
+ * @return array{exists: bool, value: string|null, autoload: string|null}
+ */
+ private static function get_raw_option_state( string $option_name ): array {
+ global $wpdb;
+
+ $row = $wpdb->get_row(
+ $wpdb->prepare( "SELECT option_value, autoload FROM {$wpdb->options} WHERE option_name = %s", $option_name ),
+ ARRAY_A
+ );
+
+ return null === $row
+ ? array(
+ 'exists' => false,
+ 'value' => null,
+ 'autoload' => null,
+ )
+ : array(
+ 'exists' => true,
+ 'value' => $row['option_value'],
+ 'autoload' => $row['autoload'],
+ );
+ }
+
+ /**
+ * Restore an option without invoking setting sanitizers.
+ *
+ * @param string $option_name Option name.
+ * @param array{exists: bool, value: string|null, autoload: string|null} $state Raw option state.
+ */
+ private static function restore_raw_option_state( string $option_name, array $state ): void {
+ global $wpdb;
+
+ if ( ! $state['exists'] ) {
+ $result = $wpdb->delete( $wpdb->options, array( 'option_name' => $option_name ) );
+ } elseif ( self::get_raw_option_state( $option_name )['exists'] ) {
+ $result = $wpdb->update(
+ $wpdb->options,
+ array(
+ 'option_value' => $state['value'],
+ 'autoload' => $state['autoload'],
+ ),
+ array( 'option_name' => $option_name )
+ );
+ } else {
+ $result = $wpdb->insert(
+ $wpdb->options,
+ array(
+ 'option_name' => $option_name,
+ 'option_value' => $state['value'],
+ 'autoload' => $state['autoload'],
+ )
+ );
+ }
+ \PHPUnit\Framework\Assert::assertNotFalse( $result, "Failed to restore option {$option_name}." );
+
+ wp_cache_delete( $option_name, 'options' );
+ wp_cache_delete( 'alloptions', 'options' );
+ wp_cache_delete( 'notoptions', 'options' );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Orders/OrderActionsRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Orders/OrderActionsRestControllerTest.php
index ecc4475621e..752a8cd2368 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Orders/OrderActionsRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Orders/OrderActionsRestControllerTest.php
@@ -5,25 +5,51 @@ namespace Automattic\WooCommerce\Tests\Internal\Orders;
use Automattic\WooCommerce\Internal\Orders\OrderActionsRestController;
use WC_Helper_Order;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_REST_Request;
+use WP_REST_Server;
+use WP_UnitTest_Factory;
/**
* OrderActionsRestController API controller test.
*
* @class OrderActionsRestController
*/
-class OrderActionsRestControllerTest extends WC_REST_Unit_Test_Case {
+class OrderActionsRestControllerTest extends WC_Unit_Test_Case {
/**
* @var OrderActionsRestController
*/
protected $controller;
+ /**
+ * REST server used to dispatch order action requests.
+ *
+ * @var WP_REST_Server
+ */
+ private $server;
+
+ /**
+ * User fixture IDs keyed by role.
+ *
+ * @var int[]
+ */
+ private static $fixture_user = array();
+
/**
* @var int[] Associative array of user IDs.
*/
private $user = array();
+ /**
+ * Create immutable users shared by the test class.
+ *
+ * @param WP_UnitTest_Factory $factory WordPress unit test factory.
+ */
+ public static function wpSetUpBeforeClass( $factory ): void {
+ self::$fixture_user['shop_manager'] = $factory->user->create( array( 'role' => 'shop_manager' ) );
+ self::$fixture_user['customer'] = $factory->user->create( array( 'role' => 'customer' ) );
+ }
+
/**
* Set up test.
*/
@@ -31,10 +57,12 @@ class OrderActionsRestControllerTest extends WC_REST_Unit_Test_Case {
parent::setUp();
$this->controller = new OrderActionsRestController();
- $this->controller->register_routes();
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->controller, 'register_routes' ) ),
+ true
+ );
- $this->user['shop_manager'] = $this->factory->user->create( array( 'role' => 'shop_manager' ) );
- $this->user['customer'] = $this->factory->user->create( array( 'role' => 'customer' ) );
+ $this->user = self::$fixture_user;
// Load and instantiate POS email classes to register their filter hooks.
// WP_UnitTestCase restores hooks between tests, so this must run each setUp().
@@ -45,6 +73,16 @@ class OrderActionsRestControllerTest extends WC_REST_Unit_Test_Case {
new \WC_Email_Customer_POS_Refunded_Order();
}
+ /**
+ * Tear down test.
+ */
+ public function tearDown(): void {
+ $this->clear_rest_server();
+ unset( $this->server, $this->controller );
+
+ parent::tearDown();
+ }
+
/**
* Create a partial refund for an order.
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php
index e113eb87835..4403ffda408 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php
@@ -5,7 +5,6 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\ProductAttributesLookup;
use Automattic\WooCommerce\Enums\ProductTaxStatus;
-use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\Internal\AttributesHelper;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper;
@@ -17,6 +16,13 @@ use Automattic\WooCommerce\Enums\ProductStockStatus;
*/
class FiltererTest extends \WC_Unit_Test_Case {
+ /**
+ * Product IDs owned by the current test.
+ *
+ * @var int[]
+ */
+ private $product_ids = array();
+
/**
* Counter to insert unique SKU for concurrent tests.
* The starting value ensures no conflicts between existing generators.
@@ -33,6 +39,25 @@ class FiltererTest extends \WC_Unit_Test_Case {
parent::setUpBeforeClass();
+ foreach ( wc_get_attribute_taxonomy_ids() as $attribute_name => $attribute_id ) {
+ $taxonomy_name = wc_attribute_taxonomy_name( wc_sanitize_taxonomy_name( $attribute_name ) );
+ $terms = get_terms(
+ array(
+ 'taxonomy' => $taxonomy_name,
+ 'hide_empty' => false,
+ )
+ );
+
+ if ( ! is_wp_error( $terms ) ) {
+ foreach ( $terms as $term ) {
+ wp_delete_term( $term->term_id, $taxonomy_name );
+ }
+ }
+
+ unregister_taxonomy( $taxonomy_name );
+ wc_delete_attribute( $attribute_id );
+ }
+
$wpdb->query(
"
CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wc_product_attributes_lookup (
@@ -56,50 +81,18 @@ class FiltererTest extends \WC_Unit_Test_Case {
* Runs after each test.
*/
public function tearDown(): void {
- global $wpdb;
-
remove_all_filters( 'woocommerce_layered_nav_count_cache_max_entries' );
- parent::tearDown();
-
- // Unregister all product attributes.
-
$attribute_ids_by_name = wc_get_attribute_taxonomy_ids();
- foreach ( $attribute_ids_by_name as $attribute_name => $attribute_id ) {
+ foreach ( array_keys( $attribute_ids_by_name ) as $attribute_name ) {
$attribute_name = wc_sanitize_taxonomy_name( $attribute_name );
$taxonomy_name = wc_attribute_taxonomy_name( $attribute_name );
unregister_taxonomy( $taxonomy_name );
-
- wc_delete_attribute( $attribute_id );
- }
-
- // Remove all products.
-
- $product_ids = wc_get_products( array( 'return' => 'ids' ) );
- foreach ( $product_ids as $product_id ) {
- $product = wc_get_product( $product_id );
- $is_variable = $product->is_type( ProductType::VARIABLE );
-
- foreach ( $product->get_children() as $child_id ) {
- $child = wc_get_product( $child_id );
- if ( empty( $child ) ) {
- continue;
- }
-
- if ( $is_variable ) {
- $child->delete( true );
- } else {
- $child->set_parent_id( 0 );
- $this->save( $child );
- }
- }
-
- $product->delete( true );
}
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_product_attributes_lookup" );
-
+ \WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );
\WC_Query::reset_chosen_attributes();
+ parent::tearDown();
}
/**
@@ -246,6 +239,7 @@ class FiltererTest extends \WC_Unit_Test_Case {
$product->set_stock_status( $in_stock ? ProductStockStatus::IN_STOCK : ProductStockStatus::OUT_OF_STOCK );
$this->save( $product );
+ $this->product_ids[] = $product->get_id();
if ( empty( $attribute_terms_by_name ) ) {
return $product;
@@ -318,6 +312,8 @@ class FiltererTest extends \WC_Unit_Test_Case {
$product_id = $product->get_id();
+ $this->product_ids[] = $product_id;
+
// * Now create the variations.
$variation_ids = array();
@@ -490,12 +486,44 @@ class FiltererTest extends \WC_Unit_Test_Case {
$_GET[ 'query_type_' . wc_sanitize_taxonomy_name( $name ) ] = $value;
}
- return $wp_the_query->query(
- array(
- 'post_type' => 'product',
- 'fields' => 'ids',
- )
+ $include_owned_products = fn() => $this->product_ids;
+ add_filter( 'loop_shop_post_in', $include_owned_products );
+
+ try {
+ return $wp_the_query->query(
+ array(
+ 'post_type' => 'product',
+ 'fields' => 'ids',
+ )
+ );
+ } finally {
+ remove_filter( 'loop_shop_post_in', $include_owned_products );
+ }
+ }
+
+ /**
+ * @testdox Product requests and counters are scoped to fixtures owned by the current test.
+ *
+ * @testWith [true]
+ * [false]
+ *
+ * @param bool $using_lookup_table Whether attribute filtering uses the lookup table.
+ */
+ public function test_product_requests_are_scoped_to_owned_products( bool $using_lookup_table ): void {
+ $this->set_use_lookup_table( $using_lookup_table );
+ $this->create_product_attribute( 'Color', array( 'Blue', 'Green' ) );
+
+ $owned_product = $this->create_simple_product( array( 'Color' => array( 'Blue' ) ), true );
+ $this->create_simple_product( array( 'Color' => array( 'Blue' ) ), true );
+ $this->create_simple_product( array( 'Color' => array( 'Green' ) ), true );
+
+ $this->product_ids = array( $owned_product->get_id() );
+
+ $this->assertSame(
+ array( $owned_product->get_id() ),
+ $this->do_product_request( array( 'Color' => array( 'Blue' ) ), array( 'Color' => 'or' ) )
);
+ $this->assert_counters( 'Color', array( 'Blue' ), 'or' );
}
/**
@@ -507,6 +535,8 @@ class FiltererTest extends \WC_Unit_Test_Case {
* @param string $filter_type The filter type in use, "and" or "or".
*/
private function assert_counters( $attribute_name, $expected_terms, $filter_type = 'and' ) {
+ global $wpdb;
+
$widget = new class() extends \WC_Widget_Layered_Nav {
// phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod, Squiz.Commenting.FunctionComment
public function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) {
@@ -523,7 +553,19 @@ class FiltererTest extends \WC_Unit_Test_Case {
$expected[ $term_ids_by_name[ $term ] ] = 1;
}
- $term_counts = $widget->get_filtered_term_product_counts( $term_ids_by_name, $taxonomy, $filter_type );
+ $scope_to_owned_products = function ( $query ) use ( $wpdb ) {
+ $owned_product_ids = implode( ',', array_map( 'absint', $this->product_ids ) );
+ $query['where'] .= empty( $owned_product_ids ) ? ' AND 1=0' : " AND {$wpdb->posts}.ID IN ({$owned_product_ids})";
+
+ return $query;
+ };
+ add_filter( 'woocommerce_get_filtered_term_product_counts_query', $scope_to_owned_products );
+
+ try {
+ $term_counts = $widget->get_filtered_term_product_counts( $term_ids_by_name, $taxonomy, $filter_type );
+ } finally {
+ remove_filter( 'woocommerce_get_filtered_term_product_counts_query', $scope_to_owned_products );
+ }
$this->assertEqualsCanonicalizing( $expected, $term_counts );
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductAttributesLookup/LookupDataStoreTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductAttributesLookup/LookupDataStoreTest.php
index e4288c9cc88..fe0d65004b4 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductAttributesLookup/LookupDataStoreTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductAttributesLookup/LookupDataStoreTest.php
@@ -7,7 +7,6 @@ namespace Automattic\WooCommerce\Tests\Internal\ProductAttributesLookup;
use Automattic\WooCommerce\Enums\CatalogVisibility;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper;
-use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
use Automattic\WooCommerce\Testing\Tools\FakeQueue;
use Automattic\WooCommerce\Enums\ProductStockStatus;
@@ -45,6 +44,8 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
+ self::$attributes = array();
+
for ( $i = 1; $i <= 3; $i++ ) {
$taxonomy_id = wc_create_attribute(
array(
@@ -70,12 +71,12 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
* Runs after all the tests.
*/
public static function tearDownAfterClass(): void {
- parent::tearDownAfterClass();
-
foreach ( self::$attributes as $attribute ) {
wc_delete_attribute( $attribute['id'] );
unregister_taxonomy( $attribute['name'] );
}
+
+ parent::tearDownAfterClass();
}
/**
@@ -87,7 +88,8 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
parent::setUp();
$this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
- $this->sut = new LookupDataStore();
+ delete_option( 'woocommerce_attribute_lookup_optimized_updates' );
+ $this->sut = new LookupDataStore();
$this->reset_legacy_proxy_mocks();
$this->register_legacy_proxy_class_mocks(
@@ -96,16 +98,7 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
)
);
- $this->get_instance_of( DataRegenerator::class )->truncate_lookup_table();
- }
-
- /**
- * Runs after each test.
- */
- public function tearDown(): void {
- parent::tearDown();
-
- delete_option( 'woocommerce_attribute_lookup_optimized_updates' );
+ $this->empty_lookup_table();
}
/**
@@ -803,6 +796,56 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
$this->assertEquals( $expected, $queue_calls[0] );
}
+ /**
+ * @testdox The shared fixture helper updates the lookup table synchronously without scheduling an action.
+ */
+ public function test_direct_fixture_helper_updates_lookup_table_without_scheduling(): void {
+ $attribute = self::$attributes[0];
+ $product = new \WC_Product_Simple();
+ $product->set_stock_status( ProductStockStatus::IN_STOCK );
+ $this->set_product_attributes(
+ $product,
+ array(
+ $attribute['name'] => array(
+ 'id' => $attribute['id'],
+ 'options' => array( $attribute['term_ids'][0] ),
+ ),
+ )
+ );
+
+ self::with_direct_product_attribute_lookup_updates(
+ static function () use ( $product ) {
+ $product->save();
+ }
+ );
+
+ $this->assertCount( 1, $this->get_lookup_table_data() );
+ $this->assertCount( 0, WC()->get_instance_of( \WC_Queue::class )->get_methods_called() );
+ }
+
+ /**
+ * @testdox The shared fixture helper preserves nested direct-update scopes and restores the option afterward.
+ */
+ public function test_direct_fixture_helper_restores_nested_update_mode(): void {
+ update_option( 'woocommerce_attribute_lookup_direct_updates', 'no' );
+
+ self::with_direct_product_attribute_lookup_updates(
+ function () {
+ $this->assertSame( 'yes', get_option( 'woocommerce_attribute_lookup_direct_updates' ) );
+
+ self::with_direct_product_attribute_lookup_updates(
+ function () {
+ $this->assertSame( 'yes', get_option( 'woocommerce_attribute_lookup_direct_updates' ) );
+ }
+ );
+
+ $this->assertSame( 'yes', get_option( 'woocommerce_attribute_lookup_direct_updates' ) );
+ }
+ );
+
+ $this->assertSame( 'no', get_option( 'woocommerce_attribute_lookup_direct_updates' ) );
+ }
+
/**
* Data provider for on_product_changed tests with direct update option set.
*
@@ -897,10 +940,10 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
$another_product_id = $product_id + 100;
// The product creation will have populated the table, but we want to start clean.
- $this->get_instance_of( DataRegenerator::class )->truncate_lookup_table();
+ $this->empty_lookup_table();
$this->insert_lookup_table_data( $another_product_id, $another_product_id, $another_attribute['name'], $another_attribute['term_ids'][0], false, true );
- if ( 'creation' !== $expected_action && 'deletion' !== $expected_action ) {
+ if ( 'creation' !== $expected_action ) {
$this->insert_lookup_table_data( $product_id, $product_id, $attribute['name'], $attribute['term_ids'][0], false, false );
}
@@ -995,10 +1038,10 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
$another_product_id = $variation_id + 100;
// The product creation will have populated the table, but we want to start clean.
- $this->get_instance_of( DataRegenerator::class )->truncate_lookup_table();
+ $this->empty_lookup_table();
$this->insert_lookup_table_data( $another_product_id, $another_product_id, $another_attribute['name'], $another_attribute['term_ids'][0], false, true );
- if ( 'creation' !== $expected_action && 'deletion' !== $expected_action ) {
+ if ( 'creation' !== $expected_action ) {
$this->insert_lookup_table_data( $product_id, $product_id, $non_variation_attribute['name'], $non_variation_attribute['term_ids'][0], false, false );
$this->insert_lookup_table_data( $variation_id, $product_id, $variation_attribute['name'], $variation_attribute['term_ids'][0], true, false );
}
@@ -1104,10 +1147,10 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
$another_product_id = $variation_id + 100;
// The product creation will have populated the table, but we want to start clean.
- $this->get_instance_of( DataRegenerator::class )->truncate_lookup_table();
+ $this->empty_lookup_table();
$this->insert_lookup_table_data( $another_product_id, $another_product_id, $another_attribute['name'], $another_attribute['term_ids'][0], false, true );
- if ( 'creation' !== $expected_action && 'deletion' !== $expected_action ) {
+ if ( 'creation' !== $expected_action ) {
$this->insert_lookup_table_data( $variation_id, $product_id, $variation_attribute['name'], $variation_attribute['term_ids'][0], true, false );
}
@@ -1228,6 +1271,15 @@ class LookupDataStoreTest extends \WC_Unit_Test_Case {
$queue->clear_methods_called();
}
+ /**
+ * Empty the lookup table inside the current test transaction.
+ */
+ private function empty_lookup_table(): void {
+ global $wpdb;
+
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_product_attributes_lookup" );
+ }
+
/**
* Insert one entry in the lookup table.
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductFeed/Feed/ProductWalkerTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductFeed/Feed/ProductWalkerTest.php
index ac19e34a07c..4fe814c8206 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductFeed/Feed/ProductWalkerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductFeed/Feed/ProductWalkerTest.php
@@ -3,8 +3,8 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\ProductFeed\Feed;
-use WC_Helper_Product;
use WC_Product;
+use WC_Product_Simple;
use Automattic\WooCommerce\Internal\ProductFeed\Integrations\IntegrationInterface;
use Automattic\WooCommerce\Internal\ProductFeed\Utils\MemoryManager;
use Automattic\WooCommerce\Internal\ProductFeed\Feed\ProductLoader;
@@ -42,6 +42,19 @@ class ProductWalkerTest extends \WC_Unit_Test_Case {
$this->test_container->reset_all_replacements();
}
+ /**
+ * Create a product object without persisting data that the walker never reads.
+ *
+ * @param int $id Product ID.
+ * @return WC_Product
+ */
+ private function create_in_memory_product( int $id ): WC_Product {
+ $product = new WC_Product_Simple();
+ $product->set_id( $id );
+
+ return $product;
+ }
+
/**
* Data provider for walker tests.
*
@@ -100,7 +113,7 @@ class ProductWalkerTest extends \WC_Unit_Test_Case {
for ( $i = 0; $i < $expected_iterations; $i++ ) {
$page = array();
for ( $j = 1; $j <= $batch_size && $generated_products++ < $number_of_products; $j++ ) {
- $page[] = WC_Helper_Product::create_simple_product();
+ $page[] = $this->create_in_memory_product( $generated_products );
}
$loader_results[] = (object) array(
@@ -288,7 +301,7 @@ class ProductWalkerTest extends \WC_Unit_Test_Case {
for ( $page = 1; $page <= $total_pages; $page++ ) {
$products = array();
for ( $i = 0; $i < $batch_size; $i++ ) {
- $products[] = WC_Helper_Product::create_simple_product();
+ $products[] = $this->create_in_memory_product( ( ( $page - 1 ) * $batch_size ) + $i + 1 );
}
$pages[ $page ] = (object) array(
'products' => $products,
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/AbstractProductFiltersTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/AbstractProductFiltersTest.php
index 10d701ff49b..0a1075a66f8 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/AbstractProductFiltersTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/AbstractProductFiltersTest.php
@@ -12,6 +12,20 @@ use Automattic\WooCommerce\Enums\ProductStockStatus;
* Tests related to FilterClausesGenerator service.
*/
abstract class AbstractProductFiltersTest extends \WC_Unit_Test_Case {
+ /**
+ * Class-owned product filter fixture state, keyed by test class.
+ *
+ * @var array
+ */
+ private static $class_fixture_state = array();
+
+ /**
+ * Option values to restore after class fixture setup and teardown.
+ *
+ * @var array
+ */
+ private static $class_fixture_option_values = array();
+
/**
* FixtureData instance.
*
@@ -48,25 +62,11 @@ abstract class AbstractProductFiltersTest extends \WC_Unit_Test_Case {
protected $product_tags;
/**
- * Backup options.
- *
- * @var array
- */
- protected $backup_options;
-
- /**
- * Runs before each test.
+ * Ensure the lookup table exists before per-test transactions start.
*/
- public function setUp(): void {
- parent::setUp();
-
- $this->fixture_data = new FixtureData();
-
- /**
- * Create the lookup table if it doesn't exist.
- * Unit tests can be run selectively, so we need to ensure the lookup table exists.
- */
+ public static function wpSetUpBeforeClass(): void {
global $wpdb;
+
$wpdb->query(
"
CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wc_product_attributes_lookup (
@@ -80,11 +80,61 @@ abstract class AbstractProductFiltersTest extends \WC_Unit_Test_Case {
"
);
- $this->backup_options = array(
- 'woocommerce_attribute_lookup_enabled' => get_option( 'woocommerce_attribute_lookup_enabled' ),
- 'woocommerce_calc_taxes' => get_option( 'woocommerce_calc_taxes' ),
- 'woocommerce_tax_display_shop' => get_option( 'woocommerce_tax_display_shop' ),
- );
+ if ( static::uses_class_product_filter_fixtures() ) {
+ static::set_up_class_product_filter_fixtures();
+ }
+ }
+
+ /**
+ * Remove custom-table rows not covered by WordPress's class cleanup.
+ */
+ public static function wpTearDownAfterClass(): void {
+ global $wpdb;
+
+ if ( static::uses_class_product_filter_fixtures() ) {
+ static::delete_class_product_filter_fixtures();
+ }
+
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_product_meta_lookup" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_product_attributes_lookup" );
+
+ if ( static::uses_class_product_filter_fixtures() ) {
+ static::restore_class_product_filter_fixture_options();
+ }
+ }
+
+ /**
+ * Runs before each test.
+ */
+ public function setUp(): void {
+ $this->set_up_test_case();
+
+ if ( static::uses_class_product_filter_fixtures() ) {
+ $this->use_class_product_filter_fixtures();
+ } else {
+ $this->set_up_product_filter_fixtures();
+ }
+ }
+
+ /**
+ * Whether this test class owns one immutable catalog shared by all its methods.
+ */
+ protected static function uses_class_product_filter_fixtures(): bool {
+ return false;
+ }
+
+ /**
+ * Start the standard per-test transaction and reset WordPress state.
+ */
+ protected function set_up_test_case(): void {
+ parent::setUp();
+ }
+
+ /**
+ * Create the product filter catalog used by a test.
+ */
+ protected function set_up_product_filter_fixtures(): void {
+ $this->fixture_data = new FixtureData();
update_option( 'woocommerce_attribute_lookup_enabled', 'yes' );
update_option( 'woocommerce_calc_taxes', 'no' );
@@ -214,35 +264,134 @@ abstract class AbstractProductFiltersTest extends \WC_Unit_Test_Case {
}
/**
- * Runs after each test.
+ * Create an immutable product filter catalog before per-test transactions begin.
*/
- public function tearDown(): void {
- parent::tearDown();
+ protected static function set_up_class_product_filter_fixtures(): void {
+ $class_name = static::class;
+ self::$class_fixture_option_values[ $class_name ] = array(
+ 'woocommerce_attribute_lookup_enabled' => get_option( 'woocommerce_attribute_lookup_enabled', null ),
+ 'woocommerce_calc_taxes' => get_option( 'woocommerce_calc_taxes', null ),
+ 'woocommerce_tax_display_shop' => get_option( 'woocommerce_tax_display_shop', null ),
+ );
+ static::enable_direct_product_attribute_lookup_updates();
+
+ try {
+ $fixture_owner = new static();
+ $fixture_owner->set_up_product_filter_fixtures();
+ $fixture_owner->set_up_additional_class_product_filter_fixtures();
+
+ self::$class_fixture_state[ $class_name ] = array(
+ 'product_ids' => array_map(
+ static function ( $product ) {
+ return $product->get_id();
+ },
+ $fixture_owner->products
+ ),
+ 'products_data' => $fixture_owner->products_data,
+ 'product_categories' => $fixture_owner->product_categories,
+ 'product_tags' => $fixture_owner->product_tags,
+ );
+ } finally {
+ static::restore_class_product_filter_fixture_options();
+ static::disable_direct_product_attribute_lookup_updates();
+ }
+ }
+
+ /**
+ * Add fixtures needed by a class after creating its shared catalog.
+ */
+ protected function set_up_additional_class_product_filter_fixtures(): void {
+ }
+
+ /**
+ * Rehydrate the class-owned catalog inside a per-test transaction.
+ */
+ protected function use_class_product_filter_fixtures(): void {
+ $fixture_state = self::$class_fixture_state[ static::class ];
+
+ update_option( 'woocommerce_attribute_lookup_enabled', 'yes' );
+ update_option( 'woocommerce_calc_taxes', 'no' );
+ update_option( 'woocommerce_tax_display_shop', 'excl' );
+ \WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );
+ unregister_taxonomy( 'product_type' );
+ \WC_Post_Types::register_taxonomies();
+ foreach ( array_keys( wc_get_attribute_taxonomy_ids() ) as $attribute_name ) {
+ clean_taxonomy_cache( wc_attribute_taxonomy_name( wc_sanitize_taxonomy_name( $attribute_name ) ) );
+ }
+
+ $this->fixture_data = new FixtureData();
+ $this->products_data = $fixture_state['products_data'];
+ $this->product_categories = $fixture_state['product_categories'];
+ $this->product_tags = $fixture_state['product_tags'];
+ $this->products = array_map( 'wc_get_product', $fixture_state['product_ids'] );
+ }
+
+ /**
+ * Delete a class-owned catalog through WooCommerce data stores.
+ */
+ protected static function delete_class_product_filter_fixtures(): void {
+ static::enable_direct_product_attribute_lookup_updates();
+
+ try {
+ $fixture_owner = new static();
+ $fixture_owner->delete_product_filter_fixtures();
+ } finally {
+ static::disable_direct_product_attribute_lookup_updates();
+ }
+
+ unset( self::$class_fixture_state[ static::class ] );
+ }
+
+ /**
+ * Restore options changed while creating or deleting a class-owned catalog.
+ */
+ protected static function restore_class_product_filter_fixture_options(): void {
+ $class_name = static::class;
+
+ foreach ( self::$class_fixture_option_values[ $class_name ] ?? array() as $option_name => $option_value ) {
+ if ( null === $option_value ) {
+ delete_option( $option_name );
+ } else {
+ update_option( $option_name, $option_value );
+ }
+ }
+ }
+
+ /**
+ * Delete product filter fixtures through WooCommerce data stores.
+ */
+ protected function delete_product_filter_fixtures(): void {
$this->remove_all_attributes();
$this->remove_all_products();
- $this->remove_all_product_categories();
- $this->remove_all_product_tags();
$this->empty_lookup_tables();
+ }
- foreach ( $this->backup_options as $option => $value ) {
- if ( false === $value ) {
- delete_option( $option );
- } else {
- update_option( $option, $value );
- }
+ /**
+ * Runs after each test.
+ */
+ public function tearDown(): void {
+ if ( ! static::uses_class_product_filter_fixtures() ) {
+ $this->delete_product_filter_fixtures();
}
- wp_cache_flush();
+ foreach ( array_keys( wc_get_attribute_taxonomy_ids() ) as $attribute_name ) {
+ $taxonomy_name = wc_attribute_taxonomy_name( wc_sanitize_taxonomy_name( $attribute_name ) );
+ unregister_taxonomy( $taxonomy_name );
+ }
+
+ \WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );
+ \WC_Query::reset_chosen_attributes();
+ parent::tearDown();
}
/**
- * Truncate the lookup table.
+ * Empty the lookup tables inside the current test transaction.
*/
private function empty_lookup_tables() {
global $wpdb;
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_product_meta_lookup" );
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_product_attributes_lookup" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_product_meta_lookup" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_product_attributes_lookup" );
}
/**
@@ -292,24 +441,6 @@ abstract class AbstractProductFiltersTest extends \WC_Unit_Test_Case {
}
}
- /**
- * Remove all product taxonomies.
- */
- private function remove_all_product_categories() {
- foreach ( $this->product_categories as $term ) {
- wp_delete_term( $term['term_id'], 'product_cat' );
- }
- }
-
- /**
- * Remove all product tags.
- */
- private function remove_all_product_tags() {
- foreach ( $this->product_tags as $term ) {
- wp_delete_term( $term['term_id'], 'product_tag' );
- }
- }
-
/**
* Get data from results of wc_get_product(), default to return the product name.
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataCacheCapTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataCacheCapTest.php
index 4f0f8347817..81972079b1e 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataCacheCapTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataCacheCapTest.php
@@ -12,6 +12,12 @@ use Automattic\WooCommerce\Internal\ProductFilters\QueryClauses;
* Tests for the filter-combination cache entry cap in FilterData.
*/
class FilterDataCacheCapTest extends AbstractProductFiltersTest {
+ /**
+ * This class only reads its product catalog inside per-test transactions.
+ */
+ protected static function uses_class_product_filter_fixtures(): bool {
+ return true;
+ }
/**
* System under test.
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataTest.php
index 5f59fd3a0c9..19603cf6e6e 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/FilterDataTest.php
@@ -10,6 +10,22 @@ use Automattic\WooCommerce\Internal\ProductFilters\TaxonomyHierarchyData;
* Tests related to Counts service.
*/
class FilterDataTest extends AbstractProductFiltersTest {
+ /**
+ * This class only reads its product catalog inside per-test transactions.
+ */
+ protected static function uses_class_product_filter_fixtures(): bool {
+ return true;
+ }
+
+ /**
+ * Add product reviews used by the filter-count assertions.
+ */
+ protected function set_up_additional_class_product_filter_fixtures(): void {
+ $this->fixture_data->add_product_review( $this->products[0]->get_id(), 5 );
+ $this->fixture_data->add_product_review( $this->products[1]->get_id(), 3 );
+ $this->fixture_data->add_product_review( $this->products[3]->get_id(), 5 );
+ }
+
/**
* The system under test.
*
@@ -34,10 +50,6 @@ class FilterDataTest extends AbstractProductFiltersTest {
$this->sut = $container->get( FilterDataProvider::class )->with( $container->get( QueryClauses::class ) );
$this->taxonomy_hierarchy_data = $container->get( TaxonomyHierarchyData::class );
-
- $this->fixture_data->add_product_review( $this->products[0]->get_id(), 5 );
- $this->fixture_data->add_product_review( $this->products[1]->get_id(), 3 );
- $this->fixture_data->add_product_review( $this->products[3]->get_id(), 5 );
}
/**
@@ -91,8 +103,8 @@ class FilterDataTest extends AbstractProductFiltersTest {
*/
public function test_get_stock_status_counts_with_default_query_using_postmeta_table() {
global $wpdb;
- // Truncate the lookup table to confirm that the underlying query is targeting the correct postmeta table.
- $wpdb->query( "TRUNCATE TABLE {$wpdb->wc_product_meta_lookup}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ // Empty the lookup table to confirm that the underlying query is targeting the correct postmeta table.
+ $wpdb->query( "DELETE FROM {$wpdb->wc_product_meta_lookup}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
update_option( 'woocommerce_product_lookup_table_is_generating', '1' );
$this->test_get_stock_status_counts_with( new \WP_Query( array( 'post_type' => 'product' ) ) );
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/ParamsTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/ParamsTest.php
index 28849187f63..9062e3826ae 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/ParamsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/ParamsTest.php
@@ -11,6 +11,13 @@ require_once WC_ABSPATH . '/includes/class-wc-brands.php';
* Tests for the Params class.
*/
class ParamsTest extends AbstractProductFiltersTest {
+ /**
+ * This class only reads its product catalog inside per-test transactions.
+ */
+ protected static function uses_class_product_filter_fixtures(): bool {
+ return true;
+ }
+
/**
* The system under test.
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/QueryClausesTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/QueryClausesTest.php
index d827d12bc79..0e6e988de71 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/QueryClausesTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/QueryClausesTest.php
@@ -9,6 +9,13 @@ use Automattic\WooCommerce\Internal\ProductFilters\QueryClauses;
* Tests related to QueryClauses service.
*/
class QueryClausesTest extends AbstractProductFiltersTest {
+ /**
+ * This class only reads its product catalog inside per-test transactions.
+ */
+ protected static function uses_class_product_filter_fixtures(): bool {
+ return true;
+ }
+
/**
* The system under test.
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/NotificationPreferencesRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/NotificationPreferencesRestControllerTest.php
index f6c6e8adf29..62c01fe56f7 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/NotificationPreferencesRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/NotificationPreferencesRestControllerTest.php
@@ -11,18 +11,41 @@ use Automattic\WooCommerce\Internal\PushNotifications\Services\NotificationPrefe
use Automattic\WooCommerce\Internal\Utilities\Users;
use Automattic\WooCommerce\Tests\Internal\PushNotifications\Helpers\PushNotificationsTestTrait;
use WC_Data_Exception;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_Http;
use WP_REST_Request;
+use WP_REST_Server;
+use WP_UnitTest_Factory;
/**
* Tests for the NotificationPreferencesRestController class.
*
* @package WooCommerce\Tests\PushNotifications
*/
-class NotificationPreferencesRestControllerTest extends WC_REST_Unit_Test_Case {
+class NotificationPreferencesRestControllerTest extends WC_Unit_Test_Case {
use PushNotificationsTestTrait;
+ /**
+ * REST server used to dispatch notification preference requests.
+ *
+ * @var WP_REST_Server
+ */
+ private $server;
+
+ /**
+ * Shop manager fixture user ID.
+ *
+ * @var int
+ */
+ private static $fixture_user_id;
+
+ /**
+ * Subscriber fixture user ID.
+ *
+ * @var int
+ */
+ private static $fixture_subscriber_id;
+
/**
* Shop manager user ID for testing.
*
@@ -37,6 +60,16 @@ class NotificationPreferencesRestControllerTest extends WC_REST_Unit_Test_Case {
*/
private $subscriber_id;
+ /**
+ * Create immutable users shared by the test class.
+ *
+ * @param WP_UnitTest_Factory $factory WordPress unit test factory.
+ */
+ public static function wpSetUpBeforeClass( $factory ): void {
+ self::$fixture_user_id = $factory->user->create( array( 'role' => 'shop_manager' ) );
+ self::$fixture_subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) );
+ }
+
/**
* Set up test.
*/
@@ -45,8 +78,8 @@ class NotificationPreferencesRestControllerTest extends WC_REST_Unit_Test_Case {
$this->reset_push_notifications_cache();
- $this->user_id = $this->factory->user->create( array( 'role' => 'shop_manager' ) );
- $this->subscriber_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );
+ $this->user_id = self::$fixture_user_id;
+ $this->subscriber_id = self::$fixture_subscriber_id;
}
/**
@@ -55,7 +88,11 @@ class NotificationPreferencesRestControllerTest extends WC_REST_Unit_Test_Case {
* mocks they need (e.g. replacing the service) so the resolved controller picks them up.
*/
private function register_routes(): void {
- wc_get_container()->get( NotificationPreferencesRestController::class )->register_routes();
+ $controller = wc_get_container()->get( NotificationPreferencesRestController::class );
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $controller, 'register_routes' ) ),
+ true
+ );
}
/**
@@ -65,11 +102,11 @@ class NotificationPreferencesRestControllerTest extends WC_REST_Unit_Test_Case {
wp_set_current_user( 0 );
Users::delete_site_user_meta( $this->user_id, NotificationPreferencesDataStore::META_KEY );
- wp_delete_user( $this->user_id );
- wp_delete_user( $this->subscriber_id );
$this->reset_container_replacements();
wc_get_container()->reset_all_resolved();
+ $this->clear_rest_server();
+ unset( $this->server );
parent::tearDown();
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushNotificationRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushNotificationRestControllerTest.php
index a07ddea3390..52ae66a432f 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushNotificationRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushNotificationRestControllerTest.php
@@ -6,14 +6,21 @@ namespace Automattic\WooCommerce\Tests\Internal\PushNotifications\Controllers;
use Automattic\WooCommerce\Internal\PushNotifications\Controllers\PushNotificationRestController;
use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_REST_Request;
use WP_REST_Server;
/**
* Tests for the PushNotificationRestController class.
*/
-class PushNotificationRestControllerTest extends WC_REST_Unit_Test_Case {
+class PushNotificationRestControllerTest extends WC_Unit_Test_Case {
+
+ /**
+ * REST server used to verify route registration.
+ *
+ * @var WP_REST_Server
+ */
+ private $server;
/**
* The System Under Test.
@@ -28,15 +35,28 @@ class PushNotificationRestControllerTest extends WC_REST_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
- $this->sut = new PushNotificationRestController();
- $this->sut->register_routes();
+ $this->sut = new PushNotificationRestController();
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->sut, 'register_routes' ) ),
+ true
+ );
+ }
+
+ /**
+ * Tear down test fixtures.
+ */
+ public function tearDown(): void {
+ $this->clear_rest_server();
+ unset( $this->server, $this->sut );
+
+ parent::tearDown();
}
/**
* @testdox Should register the send route.
*/
public function test_register_routes_adds_send_endpoint(): void {
- $routes = rest_get_server()->get_routes();
+ $routes = $this->server->get_routes();
$this->assertArrayHasKey(
'/wc-push-notifications/send',
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
index cfa2ef153e6..512ccc47deb 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
@@ -15,19 +15,63 @@ use Exception;
use RuntimeException;
use ReflectionClass;
use WC_Data_Exception;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_Error;
use WP_Http;
use WP_REST_Request;
+use WP_REST_Server;
+use WP_UnitTest_Factory;
/**
* Tests for the PushTokenRestController class.
*
* @package WooCommerce\Tests\PushNotifications
*/
-class PushTokenRestControllerTest extends WC_REST_Unit_Test_Case {
+class PushTokenRestControllerTest extends WC_Unit_Test_Case {
use PushNotificationsTestTrait;
+ /**
+ * REST server used to dispatch push token requests.
+ *
+ * @var WP_REST_Server
+ */
+ private $server;
+
+ /**
+ * Push token controller registered on the test server.
+ *
+ * @var PushTokenRestController
+ */
+ private $controller;
+
+ /**
+ * Shop manager fixture user ID.
+ *
+ * @var int
+ */
+ private static $fixture_user_id;
+
+ /**
+ * Customer fixture user ID.
+ *
+ * @var int
+ */
+ private static $fixture_customer_id;
+
+ /**
+ * Another shop manager fixture user ID.
+ *
+ * @var int
+ */
+ private static $fixture_other_shop_manager_id;
+
+ /**
+ * Subscriber fixture user ID.
+ *
+ * @var int
+ */
+ private static $fixture_subscriber_id;
+
/**
* Shop manager user ID for testing.
*
@@ -56,6 +100,18 @@ class PushTokenRestControllerTest extends WC_REST_Unit_Test_Case {
*/
private $subscriber_id;
+ /**
+ * Create immutable users shared by the test class.
+ *
+ * @param WP_UnitTest_Factory $factory WordPress unit test factory.
+ */
+ public static function wpSetUpBeforeClass( $factory ): void {
+ self::$fixture_user_id = $factory->user->create( array( 'role' => 'shop_manager' ) );
+ self::$fixture_customer_id = $factory->user->create( array( 'role' => 'customer' ) );
+ self::$fixture_other_shop_manager_id = $factory->user->create( array( 'role' => 'shop_manager' ) );
+ self::$fixture_subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) );
+ }
+
/**
* Set up test.
*/
@@ -64,12 +120,16 @@ class PushTokenRestControllerTest extends WC_REST_Unit_Test_Case {
$this->reset_push_notifications_cache();
- ( new PushTokenRestController() )->register_routes();
+ $this->controller = new PushTokenRestController();
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->controller, 'register_routes' ) ),
+ true
+ );
- $this->user_id = $this->factory->user->create( array( 'role' => 'shop_manager' ) );
- $this->customer_id = $this->factory->user->create( array( 'role' => 'customer' ) );
- $this->other_shop_manager_id = $this->factory->user->create( array( 'role' => 'shop_manager' ) );
- $this->subscriber_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );
+ $this->user_id = self::$fixture_user_id;
+ $this->customer_id = self::$fixture_customer_id;
+ $this->other_shop_manager_id = self::$fixture_other_shop_manager_id;
+ $this->subscriber_id = self::$fixture_subscriber_id;
}
/**
@@ -78,13 +138,10 @@ class PushTokenRestControllerTest extends WC_REST_Unit_Test_Case {
public function tearDown(): void {
wp_set_current_user( 0 );
- wp_delete_user( $this->user_id );
- wp_delete_user( $this->customer_id );
- wp_delete_user( $this->other_shop_manager_id );
- wp_delete_user( $this->subscriber_id );
-
$this->reset_container_replacements();
wc_get_container()->reset_all_resolved();
+ $this->clear_rest_server();
+ unset( $this->server, $this->controller );
parent::tearDown();
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Fulfillments/ControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Fulfillments/ControllerTest.php
index b4d98afe3ba..9dcf9afe5df 100644
--- a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Fulfillments/ControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Fulfillments/ControllerTest.php
@@ -8,15 +8,15 @@ use Automattic\WooCommerce\Admin\Features\Fulfillments\OrderFulfillmentsRestCont
use Automattic\WooCommerce\Internal\RestApi\Routes\V4\Fulfillments\Controller as FulfillmentsController;
use Automattic\WooCommerce\Internal\RestApi\Routes\V4\Fulfillments\Schema\FulfillmentSchema;
use Automattic\WooCommerce\Tests\Admin\Features\Fulfillments\Helpers\FulfillmentsHelper;
-use WC_REST_Unit_Test_Case;
use WC_Helper_Order;
use WC_Order;
+use WC_Unit_Test_Case;
use WP_REST_Request;
/**
* Fulfillments Controller test class
*/
-class ControllerTest extends WC_REST_Unit_Test_Case {
+class ControllerTest extends WC_Unit_Test_Case {
/**
* Controller instance
@@ -111,7 +111,10 @@ class ControllerTest extends WC_REST_Unit_Test_Case {
$this->controller = new FulfillmentsController();
$this->controller->init( new FulfillmentSchema(), new OrderFulfillmentsRestController() );
- $this->controller->register_routes();
+ $this->create_rest_server_with_routes(
+ array( array( $this->controller, 'register_routes' ) ),
+ true
+ );
$this->test_order = WC_Helper_Order::create_order( self::$customer_user_id );
$this->test_fulfillment = FulfillmentsHelper::create_fulfillment(
@@ -125,12 +128,7 @@ class ControllerTest extends WC_REST_Unit_Test_Case {
* Teardown test environment
*/
public function tearDown(): void {
- // Delete the created orders and their fulfillments.
- WC_Helper_Order::delete_order( $this->test_order->get_id() );
- global $wpdb;
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_order_fulfillments;" );
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_order_fulfillment_meta;" );
-
+ $this->clear_rest_server();
parent::tearDown();
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Products/ProductsAddToCartTest.php b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Products/ProductsAddToCartTest.php
index 68435a2897e..55f0ac10d4b 100644
--- a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Products/ProductsAddToCartTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Products/ProductsAddToCartTest.php
@@ -76,16 +76,43 @@ class ProductsAddToCartTest extends WC_REST_Unit_Test_Case {
public function test_add_to_cart_response_data( string $product_type ) {
switch ( $product_type ) {
case 'simple':
- $product = WC_Helper_Product::create_simple_product();
+ $product = WC_Helper_Product::create_simple_product();
+ $expected_add_to_cart_text = __( 'Add to cart', 'woocommerce' );
break;
case 'grouped':
- $product = WC_Helper_Product::create_grouped_product();
+ $product = new \WC_Product_Grouped();
+ $expected_add_to_cart_text = __( 'View products', 'woocommerce' );
+ $product->set_name( 'Dummy Grouped Product' );
+ $product->save();
break;
case 'external':
- $product = WC_Helper_Product::create_external_product();
+ $product = WC_Helper_Product::create_external_product();
+ $expected_add_to_cart_text = 'Buy external product';
break;
case 'variable':
- $product = WC_Helper_Product::create_variation_product();
+ $product = new \WC_Product_Variable();
+ $attribute = new \WC_Product_Attribute();
+ $attribute->set_name( 'size' );
+ $attribute->set_options( array( 'small' ) );
+ $attribute->set_visible( true );
+ $attribute->set_variation( true );
+ $product->set_name( 'Dummy Variable Product' );
+ $product->set_attributes( array( $attribute ) );
+ $product->save();
+
+ $variation = new \WC_Product_Variation();
+ $variation->set_parent_id( $product->get_id() );
+ $variation->set_regular_price( 10 );
+ $variation->set_status( 'publish' );
+ $variation->set_attributes( array( 'size' => 'small' ) );
+ $variation->save();
+
+ \WC_Product_Variable::sync( $product->get_id() );
+ $product = wc_get_product( $product->get_id() );
+ $this->assertNotEmpty( $product->get_children(), 'Variable fixture should expose its child.' );
+ $this->assertNotSame( '', $product->get_price(), 'Variable fixture should have a synced price.' );
+ $this->assertTrue( $product->is_purchasable(), 'Variable fixture should be purchasable.' );
+ $expected_add_to_cart_text = __( 'Select options', 'woocommerce' );
break;
}
@@ -94,6 +121,7 @@ class ProductsAddToCartTest extends WC_REST_Unit_Test_Case {
$data = $response->get_data();
+ $this->assertSame( $product_type, $data['type'] );
$this->assertArrayHasKey( 'add_to_cart', $data );
$add_to_cart = $data['add_to_cart'];
@@ -105,7 +133,7 @@ class ProductsAddToCartTest extends WC_REST_Unit_Test_Case {
$this->assertEquals( $product->add_to_cart_url(), $add_to_cart['url'] );
$this->assertEquals( $product->add_to_cart_description(), $add_to_cart['description'] );
- $this->assertEquals( $product->add_to_cart_text(), $add_to_cart['text'] );
+ $this->assertSame( $expected_add_to_cart_text, $add_to_cart['text'] );
$this->assertEquals( $product->single_add_to_cart_text(), $add_to_cart['single_text'] );
switch ( $product_type ) {
diff --git a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Products/ProductsControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Products/ProductsControllerTest.php
index 5f96d785d87..3412f18e071 100644
--- a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Products/ProductsControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Products/ProductsControllerTest.php
@@ -10,8 +10,10 @@ use Automattic\WooCommerce\Internal\RestApi\Routes\V4\Products\Controller as Pro
use Automattic\WooCommerce\Internal\CostOfGoodsSold\CogsAwareUnitTestSuiteTrait;
use Automattic\WooCommerce\Tests\Helpers\MetaDataAssertionTrait;
use WC_Helper_Product;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
+use WC_Unit_Tests_Bootstrap;
use WP_REST_Request;
+use WP_REST_Server;
@@ -19,15 +21,24 @@ use WP_REST_Request;
* class ProductsControllerTest.
* Product Controller tests for V4 REST API.
*/
-class ProductsControllerTest extends WC_REST_Unit_Test_Case {
+class ProductsControllerTest extends WC_Unit_Test_Case {
use CogsAwareUnitTestSuiteTrait;
use MetaDataAssertionTrait;
+ /**
+ * REST server used to dispatch product requests.
+ *
+ * @var WP_REST_Server
+ */
+ private $server;
/**
* Runs after each test.
*/
public function tearDown(): void {
+ $this->clear_rest_server();
+ unset( $this->server );
+
parent::tearDown();
$this->disable_cogs_feature();
$this->disable_rest_api_v4_feature();
@@ -65,67 +76,70 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
protected static $products = array();
/**
- * Admin user ID, shared across all tests in the class for REST authentication.
+ * Administrator ID used to authenticate requests.
*
* @var int
*/
- protected static $user;
+ private static $administrator_id;
/**
- * Create the shared admin user and products once for the whole class.
+ * Create products for tests.
*
- * @param object $factory Factory object.
* @return void
*/
- public static function wpSetUpBeforeClass( $factory ) {
- self::$user = $factory->user->create(
+ public static function wpSetUpBeforeClass() {
+ self::$administrator_id = self::factory()->user->create(
array(
'role' => 'administrator',
)
);
- self::$products[] = WC_Helper_Product::create_simple_product(
- true,
- array(
- 'name' => 'Pancake',
- 'sku' => 'pancake-1',
- )
- );
- self::$products[] = WC_Helper_Product::create_simple_product(
- true,
- array(
- 'name' => 'Waffle 1',
- 'sku' => 'pancake-2',
- )
- );
- self::$products[] = WC_Helper_Product::create_simple_product(
- true,
- array(
- 'name' => 'French Toast',
- 'sku' => 'waffle-2',
- )
- );
- self::$products[] = WC_Helper_Product::create_simple_product(
- true,
- array(
- 'name' => 'Waffle 3',
- 'sku' => 'waffle-3',
- )
- );
+ self::with_direct_product_attribute_lookup_updates(
+ static function () {
+ self::$products[] = WC_Helper_Product::create_simple_product(
+ true,
+ array(
+ 'name' => 'Pancake',
+ 'sku' => 'pancake-1',
+ )
+ );
+ self::$products[] = WC_Helper_Product::create_simple_product(
+ true,
+ array(
+ 'name' => 'Waffle 1',
+ 'sku' => 'pancake-2',
+ )
+ );
+ self::$products[] = WC_Helper_Product::create_simple_product(
+ true,
+ array(
+ 'name' => 'French Toast',
+ 'sku' => 'waffle-2',
+ )
+ );
+ self::$products[] = WC_Helper_Product::create_simple_product(
+ true,
+ array(
+ 'name' => 'Waffle 3',
+ 'sku' => 'waffle-3',
+ )
+ );
- $grouped_product = WC_Helper_Product::create_grouped_product();
- $children_products_ids = $grouped_product->get_children();
+ $grouped_product = WC_Helper_Product::create_grouped_product();
+ $children_products_ids = $grouped_product->get_children();
- foreach ( $children_products_ids as $child_product_id ) {
- self::$products[] = wc_get_product( $child_product_id );
- }
- self::$products[] = $grouped_product;
+ foreach ( $children_products_ids as $child_product_id ) {
+ self::$products[] = wc_get_product( $child_product_id );
+ }
+ self::$products[] = $grouped_product;
- foreach ( self::$products as $product ) {
- $product->add_meta_data( 'test1', 'test1', true );
- $product->add_meta_data( 'test2', 'test2', true );
- $product->save();
- }
+ foreach ( self::$products as $product ) {
+ $product->add_meta_data( 'test1', 'test1', true );
+ $product->add_meta_data( 'test2', 'test2', true );
+ $product->save();
+ }
+ }
+ );
}
/**
@@ -134,11 +148,43 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
* @return void
*/
public static function wpTearDownAfterClass() {
- foreach ( self::$products as $product ) {
- WC_Helper_Product::delete_product( $product->get_id() );
+ try {
+ self::with_direct_product_attribute_lookup_updates(
+ static function () {
+ foreach ( self::$products as $product ) {
+ WC_Helper_Product::delete_product( $product->get_id() );
+ }
+ }
+ );
+ } finally {
+ self::$products = array();
}
}
+ /**
+ * Get the IDs of the products owned by this test class.
+ *
+ * @return int[]
+ */
+ private function get_fixture_product_ids(): array {
+ return array_map(
+ static function ( $product ) {
+ return $product->get_id();
+ },
+ self::$products
+ );
+ }
+
+ /**
+ * Register only the product routes used by this test class.
+ */
+ private function reset_rest_server(): void {
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->endpoint, 'register_routes' ) ),
+ true
+ );
+ }
+
/**
* Setup our test server, endpoints, and user info.
*/
@@ -146,7 +192,8 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
$this->enable_rest_api_v4_feature();
parent::setUp();
$this->endpoint = new ProductsController();
- wp_set_current_user( self::$user );
+ $this->reset_rest_server();
+ wp_set_current_user( self::$administrator_id );
// Reset tax settings to ensure consistent product pricing.
// Some tests (like OrderHelper::create_complex_wp_post_order) modify tax settings globally,
@@ -315,11 +362,35 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
}
}
+ /**
+ * Create a variable product with the requested number of variations.
+ *
+ * @param int $variation_count Number of variations to create.
+ * @return \WC_Product_Variable
+ */
+ private function create_variable_product_with_variations( int $variation_count ): \WC_Product_Variable {
+ $product = new \WC_Product_Variable();
+ $product->set_name( 'Variable product' );
+ $product->save();
+ $variation_ids = array();
+
+ for ( $index = 0; $index < $variation_count; $index++ ) {
+ $variation = new \WC_Product_Variation();
+ $variation->set_parent_id( $product->get_id() );
+ $variation->set_regular_price( 10 + $index );
+ $variation->save();
+ $variation_ids[] = $variation->get_id();
+ }
+ $product->set_children( $variation_ids );
+
+ return $product;
+ }
+
/**
* @testdox Variable product responses include embeddable variation links.
*/
public function test_variable_product_response_includes_embeddable_variation_links(): void {
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->create_variable_product_with_variations( 6 );
$variation_ids = $product->get_children();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products/' . $product->get_id() ) );
@@ -344,7 +415,7 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
* @testdox Variable product responses embed variations when requested.
*/
public function test_variable_product_response_embeds_variations_when_requested(): void {
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->create_variable_product_with_variations( 6 );
$variation_ids = $product->get_children();
$request = new WP_REST_Request( 'GET', '/wc/v4/products/' . $product->get_id() );
$request->set_param( '_embed', 1 );
@@ -373,7 +444,7 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
* @testdox Variation embed links do not propagate parent requested fields.
*/
public function test_variation_embed_links_do_not_propagate_parent_requested_fields(): void {
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->create_variable_product_with_variations( 1 );
$request = new WP_REST_Request( 'GET', '/wc/v4/products/' . $product->get_id() );
$request->set_param( '_fields', 'id,name,_links,_embedded' );
@@ -407,7 +478,7 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
* @testdox Variation parent links are not embeddable.
*/
public function test_variation_parent_link_is_not_embeddable(): void {
- $product = WC_Helper_Product::create_variation_product();
+ $product = $this->create_variable_product_with_variations( 1 );
$variation_id = $product->get_children()[0];
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v4/products/' . $variation_id ) );
@@ -586,9 +657,8 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
* @return void
*/
public function test_products_query_by_global_unique_id_param_for_variations() {
- $parent_product = WC_Helper_Product::create_variation_product();
- $variation = $parent_product->get_available_variations()[0];
- $variation = wc_get_product( $variation['variation_id'] );
+ $parent_product = $this->create_variable_product_with_variations( 1 );
+ $variation = wc_get_product( $parent_product->get_children()[0] );
$unique_id = $parent_product->get_id() . '-1';
$variation->set_global_unique_id( $unique_id );
$variation->save();
@@ -612,11 +682,12 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
public function test_collection_param_include_meta() {
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_param( 'include_meta', 'test1' );
+ $request->set_param( 'include', $this->get_fixture_product_ids() );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$response_data = $response->get_data();
- $this->assertCount( 7, $response_data );
+ $this->assertCount( count( self::$products ), $response_data );
foreach ( $response_data as $order ) {
$this->assertArrayHasKey( 'meta_data', $order );
@@ -637,11 +708,12 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
public function test_collection_param_include_meta_empty() {
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_param( 'include_meta', '' );
+ $request->set_param( 'include', $this->get_fixture_product_ids() );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$response_data = $response->get_data();
- $this->assertCount( 7, $response_data );
+ $this->assertCount( count( self::$products ), $response_data );
foreach ( $response_data as $order ) {
$this->assertArrayHasKey( 'meta_data', $order );
@@ -662,11 +734,12 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
public function test_collection_param_exclude_meta() {
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_param( 'exclude_meta', 'test1' );
+ $request->set_param( 'include', $this->get_fixture_product_ids() );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$response_data = $response->get_data();
- $this->assertCount( 7, $response_data );
+ $this->assertCount( count( self::$products ), $response_data );
foreach ( $response_data as $order ) {
$this->assertArrayHasKey( 'meta_data', $order );
@@ -688,11 +761,12 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_param( 'include_meta', 'test1' );
$request->set_param( 'exclude_meta', 'test1' );
+ $request->set_param( 'include', $this->get_fixture_product_ids() );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$response_data = $response->get_data();
- $this->assertCount( 7, $response_data );
+ $this->assertCount( count( self::$products ), $response_data );
foreach ( $response_data as $order ) {
$this->assertArrayHasKey( 'meta_data', $order );
@@ -718,7 +792,8 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
$response_data = $this->server->response_to_data( $response, false );
$encoded_data_string = wp_json_encode( $response_data );
- $decoded_data_object = json_decode( $encoded_data_string, false ); // Ensure object instead of associative array.
+ // Ensure object instead of associative array.
+ $decoded_data_object = json_decode( $encoded_data_string, false );
$this->assertIsArray( $decoded_data_object[0]->meta_data );
}
@@ -1153,10 +1228,7 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
* Test that the `include_types` parameter filters products by a single type.
*/
public function test_collection_filter_with_include_types() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_query_params(
@@ -1170,17 +1242,17 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
$response_products = $response->get_data();
$this->assertCount( 2, $response_products );
- $this->assertEquals( ProductType::GROUPED, $response_products[0]['type'] );
+ $this->assertEqualsCanonicalizing(
+ array( ProductType::GROUPED, ProductType::GROUPED ),
+ wp_list_pluck( $response_products, 'type' )
+ );
}
/**
* Test that the `include_types` parameter filters products by multiple types.
*/
public function test_collection_filter_with_multiple_include_types() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_query_params(
@@ -1409,36 +1481,60 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
WC_Helper_Product::delete_product( $high_stock_product->get_id() );
}
+ /**
+ * Create a variable product with stock-managed variations.
+ *
+ * A null quantity creates an unmanaged-stock variation, i.e. a NULL
+ * stock_quantity row in the product meta lookup table.
+ *
+ * @param array<int, array{quantity: int|null, status?: string}> $variation_data Variation stock and status data.
+ * @return \WC_Product_Variable
+ */
+ private function create_variable_product_with_stock( array $variation_data ): \WC_Product_Variable {
+ $product = new \WC_Product_Variable();
+ $product->set_name( 'Variable product' );
+ $product->save();
+
+ foreach ( $variation_data as $data ) {
+ $variation = new \WC_Product_Variation();
+ $variation->set_parent_id( $product->get_id() );
+ $variation->set_regular_price( 10 );
+ if ( isset( $data['quantity'] ) ) {
+ $variation->set_manage_stock( true );
+ $variation->set_stock_quantity( $data['quantity'] );
+ } else {
+ $variation->set_manage_stock( false );
+ }
+ $variation->set_status( $data['status'] ?? ProductStatus::PUBLISH );
+ $variation->save();
+ }
+
+ return $product;
+ }
+
/**
* @testdox Stock quantity parameters include variable products when a variation matches.
*/
public function test_products_filter_with_stock_quantity_range_matches_variable_product_variations(): void {
- $variable_product = WC_Helper_Product::create_variation_product();
- $variation_ids = $variable_product->get_children();
- $matching_variation = wc_get_product( $variation_ids[0] );
- $non_matching_variation = wc_get_product( $variation_ids[1] );
- $non_matching_product = WC_Helper_Product::create_simple_product(
+ $variable_product = $this->create_variable_product_with_stock(
+ array(
+ array( 'quantity' => 5 ),
+ array( 'quantity' => 8 ),
+ )
+ );
+ $non_matching_product = WC_Helper_Product::create_simple_product(
true,
array(
'manage_stock' => true,
'stock_quantity' => 2,
)
);
- $non_matching_variable = WC_Helper_Product::create_variation_product();
- $non_matching_variation_id = $non_matching_variable->get_children()[0];
- $non_matching_variation_2 = wc_get_product( $non_matching_variation_id );
-
- $matching_variation->set_manage_stock( true );
- $matching_variation->set_stock_quantity( 5 );
- $matching_variation->save();
-
- $non_matching_variation->set_manage_stock( true );
- $non_matching_variation->set_stock_quantity( 8 );
- $non_matching_variation->save();
-
- $non_matching_variation_2->set_manage_stock( true );
- $non_matching_variation_2->set_stock_quantity( 10 );
- $non_matching_variation_2->save();
+ $non_matching_variable = $this->create_variable_product_with_stock(
+ array(
+ array( 'quantity' => 10 ),
+ array( 'quantity' => null ),
+ )
+ );
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_query_params(
@@ -1458,6 +1554,26 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( array( $variable_product->get_id() ), wp_list_pluck( $response->get_data(), 'id' ) );
+ // A zero-inclusive range must not match the unmanaged (NULL stock)
+ // variation of the non-matching variable product.
+ $request = new WP_REST_Request( 'GET', '/wc/v4/products' );
+ $request->set_query_params(
+ array(
+ 'min_stock_quantity' => 0,
+ 'max_stock_quantity' => 2,
+ 'include' => array(
+ $variable_product->get_id(),
+ $non_matching_product->get_id(),
+ $non_matching_variable->get_id(),
+ ),
+ )
+ );
+
+ $response = $this->server->dispatch( $request );
+
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEquals( array( $non_matching_product->get_id() ), wp_list_pluck( $response->get_data(), 'id' ) );
+
WC_Helper_Product::delete_product( $variable_product->get_id() );
WC_Helper_Product::delete_product( $non_matching_product->get_id() );
WC_Helper_Product::delete_product( $non_matching_variable->get_id() );
@@ -1467,19 +1583,15 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
* @testdox Stock quantity parameters ignore unpublished variations when matching variable products.
*/
public function test_products_filter_with_stock_quantity_range_ignores_unpublished_variations(): void {
- $variable_product = WC_Helper_Product::create_variation_product();
- $variation_ids = $variable_product->get_children();
- $draft_variation = wc_get_product( $variation_ids[0] );
- $non_matching_variation = wc_get_product( $variation_ids[1] );
-
- $draft_variation->set_status( ProductStatus::DRAFT );
- $draft_variation->set_manage_stock( true );
- $draft_variation->set_stock_quantity( 5 );
- $draft_variation->save();
-
- $non_matching_variation->set_manage_stock( true );
- $non_matching_variation->set_stock_quantity( 8 );
- $non_matching_variation->save();
+ $variable_product = $this->create_variable_product_with_stock(
+ array(
+ array(
+ 'quantity' => 5,
+ 'status' => ProductStatus::DRAFT,
+ ),
+ array( 'quantity' => 8 ),
+ )
+ );
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_query_params(
@@ -1627,13 +1739,27 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
}
/**
- * Test that `exclude_types` parameter correctly excludes a single type.
+ * Create one product of each parent product type for type-filter tests.
*/
- public function test_products_filter_with_single_exclude_types() {
+ private function create_products_for_type_filtering(): void {
WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
+
+ $variable = new \WC_Product_Variable();
+ $variable->set_name( 'Variable product' );
+ $variable->save();
+
+ $grouped = new \WC_Product_Grouped();
+ $grouped->set_name( 'Grouped product' );
+ $grouped->save();
+
WC_Helper_Product::create_external_product();
+ }
+
+ /**
+ * Test that `exclude_types` parameter correctly excludes a single type.
+ */
+ public function test_products_filter_with_single_exclude_types() {
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_query_params(
@@ -1646,19 +1772,17 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
$data = $response->get_data();
- $types = array_unique( array_column( $data, 'type' ) );
-
- $this->assertNotContains( ProductType::SIMPLE, $types );
+ $this->assertEqualsCanonicalizing(
+ array( ProductType::VARIABLE, ProductType::GROUPED, ProductType::EXTERNAL ),
+ array_values( array_unique( array_column( $data, 'type' ) ) )
+ );
}
/**
* Test that `exclude_types` parameter correctly excludes multiple types.
*/
public function test_products_filter_with_multiple_exclude_types() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_query_params(
@@ -1686,10 +1810,7 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
* Test that empty `exclude_types` parameter returns all products.
*/
public function test_products_filter_with_empty_exclude_types() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_query_params(
@@ -1729,10 +1850,7 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
* Test that `exclude_types` with all types returns empty result.
*/
public function test_products_filter_exclude_types_with_all_types_returns_empty() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_query_params(
@@ -1750,10 +1868,7 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
* Test that `exclude_types` parameter takes precedence over `include_types`.
*/
public function test_products_filter_exclude_types_precedence_over_include() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_query_params(
@@ -1777,10 +1892,7 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
* Test that `exclude_types` works correctly with the `type` param.
*/
public function test_products_filter_exclude_types_with_type_param() {
- WC_Helper_Product::create_simple_product();
- WC_Helper_Product::create_variation_product();
- WC_Helper_Product::create_grouped_product();
- WC_Helper_Product::create_external_product();
+ $this->create_products_for_type_filtering();
$request = new WP_REST_Request( 'GET', '/wc/v4/products' );
$request->set_query_params(
@@ -2049,7 +2161,7 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
public function test_cogs_values_received_for_variation_product( bool $set_additive_flag ) {
$this->enable_cogs_feature();
- $parent_product = WC_Helper_Product::create_variation_product();
+ $parent_product = $this->create_variable_product_with_variations( 1 );
$parent_product->set_cogs_value( 12.34 );
$parent_product->save();
@@ -2115,6 +2227,13 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
$original_product_sku = 'DUPLICATE_SKU_TEST_TRASHED';
// This image `src` is used in other product API tests, using here for consistency.
$shared_image_src = 'http://cldup.com/Dr1Bczxq4q.png';
+ // The failed request below exercises upload processing; setup only needs a valid image attachment.
+ $original_attachment_id = self::factory()->attachment->create(
+ array(
+ 'file' => WC_Unit_Tests_Bootstrap::instance()->tests_dir . '/data/Dr1Bczxq4q.png',
+ 'post_mime_type' => 'image/png',
+ )
+ );
// 1. Create the original product with its image.
$request_original_product = new WP_REST_Request( 'POST', '/wc/v4/products' );
@@ -2126,7 +2245,7 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
'regular_price' => '10',
'images' => array(
array(
- 'src' => $shared_image_src,
+ 'id' => $original_attachment_id,
),
),
)
@@ -2135,9 +2254,8 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
$this->assertEquals( 201, $response_original_product->get_status(), 'Failed to create the initial product with an image.' );
- $original_product_data = $response_original_product->get_data();
- $original_product_id = $original_product_data['id'];
- $original_attachment_id = $original_product_data['images'][0]['id'];
+ $original_product_data = $response_original_product->get_data();
+ $original_product_id = $original_product_data['id'];
// 2. Move the original product to trash.
wp_trash_post( $original_product_id );
@@ -2157,7 +2275,8 @@ class ProductsControllerTest extends WC_REST_Unit_Test_Case {
$create_request_for_failure->set_body_params(
array(
'name' => 'New Product Attempt That Fails',
- 'sku' => $original_product_sku, // Duplicate SKU.
+ // Duplicate SKU.
+ 'sku' => $original_product_sku,
'type' => 'simple',
'regular_price' => '20',
'images' => array(
diff --git a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/Emails/EmailsSettingsControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/Emails/EmailsSettingsControllerTest.php
index acf6c7bb58a..d0807aa2a4e 100644
--- a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/Emails/EmailsSettingsControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/Emails/EmailsSettingsControllerTest.php
@@ -15,8 +15,9 @@ use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransact
use Automattic\WooCommerce\EmailEditor\Email_Editor_Container;
use Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tags_Registry;
use Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tag;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_REST_Request;
+use WP_REST_Server;
use WC_Emails;
/**
@@ -24,7 +25,21 @@ use WC_Emails;
*
* @class EmailsSettingsControllerTest
*/
-class EmailsSettingsControllerTest extends WC_REST_Unit_Test_Case {
+class EmailsSettingsControllerTest extends WC_Unit_Test_Case {
+
+ /**
+ * REST server used to dispatch email settings requests.
+ *
+ * @var WP_REST_Server
+ */
+ private $server;
+
+ /**
+ * Email settings controller registered on the test server.
+ *
+ * @var Controller
+ */
+ private $controller;
/**
* Sample email ID for testing.
@@ -40,6 +55,20 @@ class EmailsSettingsControllerTest extends WC_REST_Unit_Test_Case {
*/
private $email;
+ /**
+ * Email registry to restore after each test.
+ *
+ * @var array<string, \WC_Email>
+ */
+ private array $previous_emails = array();
+
+ /**
+ * Email singleton to restore after each test.
+ *
+ * @var WC_Emails|null
+ */
+ private ?WC_Emails $previous_emails_instance = null;
+
/**
* Shared shop_manager user ID used for REST auth across the class.
*
@@ -81,6 +110,11 @@ class EmailsSettingsControllerTest extends WC_REST_Unit_Test_Case {
* Setup.
*/
public function setUp(): void {
+ $instance_property = new \ReflectionProperty( WC_Emails::class, 'instance' );
+ $instance_property->setAccessible( true );
+ $this->previous_emails_instance = $instance_property->getValue();
+ $this->previous_emails = $this->previous_emails_instance ? $this->previous_emails_instance->emails : array();
+
// Enable the v4 REST API feature before bootstrapping.
$this->feature_filter = function ( $features ) {
$features[] = 'rest-api-v4';
@@ -88,13 +122,13 @@ class EmailsSettingsControllerTest extends WC_REST_Unit_Test_Case {
};
add_filter( 'woocommerce_admin_features', $this->feature_filter );
-
- // Enable block email editor feature.
$this->prev_options['woocommerce_feature_block_email_editor_enabled'] = get_option( 'woocommerce_feature_block_email_editor_enabled', null );
- update_option( 'woocommerce_feature_block_email_editor_enabled', 'yes' );
parent::setUp();
+ // Enable block email editor feature.
+ update_option( 'woocommerce_feature_block_email_editor_enabled', 'yes' );
+
// Register personalization tags for testing.
$container = Email_Editor_Container::container();
$this->registry = $container->get( Personalization_Tags_Registry::class );
@@ -104,11 +138,14 @@ class EmailsSettingsControllerTest extends WC_REST_Unit_Test_Case {
// Manually initialize controller with schema that has the registry.
// We do this to ensure personalization tags wrapping/unwrapping uses the registry.
- $controller = new Controller();
- $schema = new EmailsSettingsSchema();
+ $this->controller = new Controller();
+ $schema = new EmailsSettingsSchema();
$schema->init();
- $controller->init( $schema );
- $controller->register_routes();
+ $this->controller->init( $schema );
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->controller, 'register_routes' ) ),
+ true
+ );
// Snapshot current option values to restore on tearDown.
$option_key = 'woocommerce_' . self::SAMPLE_EMAIL_ID . '_settings';
@@ -127,23 +164,50 @@ class EmailsSettingsControllerTest extends WC_REST_Unit_Test_Case {
* Tear down.
*/
public function tearDown(): void {
- if ( isset( $this->feature_filter ) ) {
- remove_filter( 'woocommerce_admin_features', $this->feature_filter );
- }
+ $block_email_editor_option = $this->prev_options['woocommerce_feature_block_email_editor_enabled'];
+ unset( $this->prev_options['woocommerce_feature_block_email_editor_enabled'] );
- // Restore previous option values.
- foreach ( $this->prev_options as $key => $value ) {
- if ( null === $value ) {
- delete_option( (string) $key );
- } else {
- update_option( (string) $key, $value );
+ try {
+ if ( isset( $this->feature_filter ) ) {
+ remove_filter( 'woocommerce_admin_features', $this->feature_filter );
}
- }
- // Clean up email template posts transient.
- delete_transient( 'wc_email_editor_initial_templates_generated' );
+ // Restore previous option values.
+ foreach ( $this->prev_options as $key => $value ) {
+ if ( null === $value ) {
+ delete_option( (string) $key );
+ } else {
+ update_option( (string) $key, $value );
+ }
+ }
- parent::tearDown();
+ // Clean up email template posts transient.
+ delete_transient( 'wc_email_editor_initial_templates_generated' );
+ $this->clear_rest_server();
+ unset( $this->server, $this->controller );
+ } finally {
+ try {
+ parent::tearDown();
+ } finally {
+ try {
+ if ( $this->previous_emails_instance ) {
+ $this->previous_emails_instance->emails = $this->previous_emails;
+ }
+ $instance_property = new \ReflectionProperty( WC_Emails::class, 'instance' );
+ $instance_property->setAccessible( true );
+ $instance_property->setValue( null, $this->previous_emails_instance );
+ } finally {
+ if ( null === $block_email_editor_option ) {
+ delete_option( 'woocommerce_feature_block_email_editor_enabled' );
+ } else {
+ update_option( 'woocommerce_feature_block_email_editor_enabled', $block_email_editor_option );
+ }
+ $this->previous_emails = array();
+ $this->previous_emails_instance = null;
+ $this->prev_options = array();
+ }
+ }
+ }
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/PaymentGateways/PaymentGatewaysSettingsControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/PaymentGateways/PaymentGatewaysSettingsControllerTest.php
index c8d2873e259..964ade9dafb 100644
--- a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/PaymentGateways/PaymentGatewaysSettingsControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/PaymentGateways/PaymentGatewaysSettingsControllerTest.php
@@ -8,15 +8,16 @@ use WC_Gateway_BACS;
use WC_Gateway_Cheque;
use WC_Gateway_COD;
use WC_Payment_Gateway;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_REST_Request;
+use WP_REST_Server;
/**
* Tests for the Payment Gateways Settings REST API controller.
*
* @class PaymentGatewaysSettingsControllerTest
*/
-class PaymentGatewaysSettingsControllerTest extends WC_REST_Unit_Test_Case {
+class PaymentGatewaysSettingsControllerTest extends WC_Unit_Test_Case {
/**
* Endpoint.
*
@@ -29,6 +30,13 @@ class PaymentGatewaysSettingsControllerTest extends WC_REST_Unit_Test_Case {
*/
protected Controller $sut;
+ /**
+ * REST server used to dispatch payment gateway settings requests.
+ *
+ * @var WP_REST_Server
+ */
+ private $server;
+
/**
* Shared admin user used for REST authentication across all tests in the class.
*
@@ -51,13 +59,30 @@ class PaymentGatewaysSettingsControllerTest extends WC_REST_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
+ $gateways = \WC_Payment_Gateways::instance();
+ $gateways->payment_gateways = array();
+ $gateways->init();
+
wp_set_current_user( self::$store_admin_id );
// Inject the mock gateway directly — avoids filter pollution and a second init() call.
WC()->payment_gateways()->payment_gateways[] = new WCGatewayMockPassword();
- $this->sut = new Controller();
- $this->sut->register_routes();
+ $this->sut = new Controller();
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->sut, 'register_routes' ) ),
+ true
+ );
+ }
+
+ /**
+ * Tear down test.
+ */
+ public function tearDown(): void {
+ $this->clear_rest_server();
+ unset( $this->server, $this->sut );
+
+ parent::tearDown();
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/Tax/TaxControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/Tax/TaxControllerTest.php
index c3b001a398f..69028ef2fd8 100644
--- a/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/Tax/TaxControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/RestApi/Routes/V4/Settings/Tax/TaxControllerTest.php
@@ -5,15 +5,16 @@ namespace Automattic\WooCommerce\Tests\Internal\RestApi\Routes\V4\Settings\Tax;
use Automattic\WooCommerce\Internal\RestApi\Routes\V4\Settings\Tax\Controller;
use Automattic\WooCommerce\Internal\RestApi\Routes\V4\Settings\Tax\Schema\TaxSettingsSchema;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_REST_Request;
+use WP_REST_Server;
/**
* Tests for the Tax Settings REST API controller.
*
* @class TaxControllerTest
*/
-class TaxControllerTest extends WC_REST_Unit_Test_Case {
+class TaxControllerTest extends WC_Unit_Test_Case {
/**
* Endpoint.
*
@@ -26,6 +27,13 @@ class TaxControllerTest extends WC_REST_Unit_Test_Case {
*/
protected $sut;
+ /**
+ * REST server used to dispatch tax settings requests.
+ *
+ * @var WP_REST_Server
+ */
+ private $server;
+
/**
* The ID of the store admin user, shared across all tests in the class for REST authentication.
*
@@ -53,7 +61,19 @@ class TaxControllerTest extends WC_REST_Unit_Test_Case {
$schema = new TaxSettingsSchema();
$this->sut = new Controller();
$this->sut->init( $schema );
- $this->sut->register_routes();
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->sut, 'register_routes' ) ),
+ true
+ );
+ }
+
+ /**
+ * Tear down test.
+ */
+ public function tearDown(): void {
+ $this->clear_rest_server();
+ unset( $this->server, $this->sut );
+ parent::tearDown();
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessorTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessorTests.php
index eef3c0ce7a8..d0f05e11860 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessorTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/AsyncTasks/NotificationsProcessorTests.php
@@ -46,12 +46,12 @@ class NotificationsProcessorTests extends \WC_Unit_Test_Case {
* Clean up after tests
*/
public function tearDown(): void {
- parent::tearDown();
unset( $this->sut );
// Clean up all notifications.
global $wpdb;
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_stock_notifications" );
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_stock_notificationmeta" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_stock_notificationmeta" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_stock_notifications" );
+ parent::tearDown();
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/NotificationTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/NotificationTests.php
index 865fd720557..41e0257f03a 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/NotificationTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/NotificationTests.php
@@ -15,11 +15,11 @@ class NotificationTests extends \WC_Unit_Test_Case {
* @after
*/
public function tearDown(): void {
- parent::tearDown();
// Clean up all notifications.
global $wpdb;
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_stock_notifications" );
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_stock_notificationmeta" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_stock_notificationmeta" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_stock_notifications" );
+ parent::tearDown();
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Utilities/EligibilityServiceTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Utilities/EligibilityServiceTests.php
index 88209e789ab..ad54e7e2d22 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Utilities/EligibilityServiceTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Utilities/EligibilityServiceTests.php
@@ -34,12 +34,12 @@ class EligibilityServiceTests extends \WC_Unit_Test_Case {
* @after
*/
public function tearDown(): void {
- parent::tearDown();
unset( $this->sut );
// Clean up all notifications.
global $wpdb;
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_stock_notifications" );
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_stock_notificationmeta" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_stock_notificationmeta" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_stock_notifications" );
+ parent::tearDown();
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Utilities/StockManagementHelperTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Utilities/StockManagementHelperTests.php
index 06a931ea1df..704b7d9bf2f 100644
--- a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Utilities/StockManagementHelperTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/Utilities/StockManagementHelperTests.php
@@ -28,12 +28,12 @@ class StockManagementHelperTests extends \WC_Unit_Test_Case {
* @after
*/
public function tearDown(): void {
- parent::tearDown();
unset( $this->sut );
// Clean up all notifications.
global $wpdb;
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_stock_notifications" );
- $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_stock_notificationmeta" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_stock_notificationmeta" );
+ $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_stock_notifications" );
+ parent::tearDown();
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Traits/RestApiCacheTest.php b/plugins/woocommerce/tests/php/src/Internal/Traits/RestApiCacheTest.php
index 391fe8e3c09..e8a66c74708 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Traits/RestApiCacheTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Traits/RestApiCacheTest.php
@@ -10,7 +10,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Traits;
use Automattic\WooCommerce\Internal\Caches\VersionStringGenerator;
use Automattic\WooCommerce\Internal\Traits\RestApiCache;
use Automattic\WooCommerce\Proxies\LegacyProxy;
-use WC_REST_Unit_Test_Case;
+use WC_Unit_Test_Case;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
@@ -20,10 +20,17 @@ use WP_Error;
/**
* Tests for the simplified RestApiCache trait.
*/
-class RestApiCacheTest extends WC_REST_Unit_Test_Case {
+class RestApiCacheTest extends WC_Unit_Test_Case {
private const CACHE_GROUP = 'woocommerce_rest_api_cache';
+ /**
+ * REST server used to dispatch requests to the test controller.
+ *
+ * @var WP_REST_Server
+ */
+ private $server;
+
/**
* System under test.
*
@@ -81,8 +88,7 @@ class RestApiCacheTest extends WC_REST_Unit_Test_Case {
* Tear down after each test.
*/
public function tearDown(): void {
- global $wp_rest_server;
- $wp_rest_server = null;
+ $this->clear_rest_server();
remove_all_filters( 'woocommerce_rest_api_not_modified_response' );
@@ -911,12 +917,9 @@ class RestApiCacheTest extends WC_REST_Unit_Test_Case {
* on the same server instance.
*/
private function reset_rest_server() {
- global $wp_rest_server;
- $wp_rest_server = new WP_REST_Server();
- $this->server = $wp_rest_server;
- // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
- do_action( 'rest_api_init' );
- $this->sut->register_routes();
+ $this->server = $this->create_rest_server_with_routes(
+ array( array( $this->sut, 'register_routes' ) )
+ );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/VariationGallery/MigrationTest.php b/plugins/woocommerce/tests/php/src/Internal/VariationGallery/MigrationTest.php
index 403c5fca2aa..781599fa93a 100644
--- a/plugins/woocommerce/tests/php/src/Internal/VariationGallery/MigrationTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/VariationGallery/MigrationTest.php
@@ -79,13 +79,20 @@ class MigrationTest extends \WC_Unit_Test_Case {
$wpdb->delete( $wpdb->postmeta, array( 'meta_key' => LegacyVariationGalleryCompatibility::get_core_managed_meta_key() ) );
// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
- $variation_ids = array();
-
- for ( $index = 0; $index < 251; ++$index ) {
- $variation_id = $this->create_variation_post();
- $variation_ids[] = $variation_id;
- update_post_meta( $variation_id, '_wc_additional_variation_images', (string) ( $index + 1 ) );
- }
+ $this->assertSame(
+ 1,
+ $wpdb->insert(
+ $wpdb->posts,
+ array(
+ 'post_type' => 'product_variation',
+ 'post_status' => 'publish',
+ 'post_title' => 'Variation gallery migration batch fixture',
+ )
+ )
+ );
+ $decoy_id = (int) $wpdb->insert_id;
+ $variation_ids = $this->create_variation_posts( 251 );
+ $this->assertNotContains( $decoy_id, $variation_ids );
$this->assertTrue( Migration::run() );
@@ -105,6 +112,9 @@ class MigrationTest extends \WC_Unit_Test_Case {
foreach ( $variation_ids as $variation_id ) {
$this->assertTrue( LegacyVariationGalleryCompatibility::is_variation_id_core_managed( $variation_id ) );
}
+ $this->assertFalse( LegacyVariationGalleryCompatibility::is_variation_id_core_managed( $decoy_id ) );
+ $this->assertSame( '', get_post_meta( $decoy_id, '_wc_additional_variation_images', true ) );
+ $this->assertSame( '', get_post_meta( $decoy_id, '_product_image_gallery', true ) );
}
/**
@@ -117,15 +127,57 @@ class MigrationTest extends \WC_Unit_Test_Case {
}
/**
- * Create a bare variation post for migration batching tests.
+ * Create bare variation posts and legacy gallery metadata for migration batching tests.
+ *
+ * The migration reads these rows directly, so inserting the fixture in bulk avoids
+ * running unrelated product hooks hundreds of times.
+ *
+ * @param int $count Number of variations to create.
+ * @return int[] Variation post IDs.
*/
- private function create_variation_post(): int {
- return self::factory()->post->create(
- array(
- 'post_type' => 'product_variation',
- 'post_status' => 'publish',
+ private function create_variation_posts( int $count ): array {
+ global $wpdb;
+
+ $post_title = 'Variation gallery migration batch fixture ' . wp_generate_uuid4();
+ $post_placeholders = array_fill( 0, $count, '(%s, %s, %s)' );
+ $post_values = array();
+
+ for ( $index = 0; $index < $count; ++$index ) {
+ array_push( $post_values, 'product_variation', 'publish', $post_title );
+ }
+
+ $posts_query = $wpdb->prepare(
+ "INSERT INTO {$wpdb->posts} (post_type, post_status, post_title) VALUES " . implode( ', ', $post_placeholders ), // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.NotPrepared -- Placeholders are generated above.
+ $post_values
+ );
+ $wpdb->query( $posts_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared immediately above.
+
+ $variation_ids = array_map(
+ 'intval',
+ $wpdb->get_col(
+ $wpdb->prepare(
+ "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_title = %s ORDER BY ID ASC",
+ 'product_variation',
+ $post_title
+ )
)
);
+ $this->assertCount( $count, $variation_ids );
+
+ $meta_placeholders = array_fill( 0, $count, '(%d, %s, %s)' );
+ $meta_values = array();
+
+ foreach ( $variation_ids as $index => $variation_id ) {
+ array_push( $meta_values, $variation_id, '_wc_additional_variation_images', (string) ( $index + 1 ) );
+ }
+
+ $meta_query = $wpdb->prepare(
+ "INSERT INTO {$wpdb->postmeta} (post_id, meta_key, meta_value) VALUES " . implode( ', ', $meta_placeholders ), // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.NotPrepared -- Placeholders are generated above.
+ $meta_values
+ );
+ $wpdb->query( $meta_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared immediately above.
+
+ return $variation_ids;
}
/**