Add PHPUnit test suite, PSR-4 refactor, lint+test CI jobs (v1.3.1)
Some checks failed
Create Release Package / PHP Lint (push) Successful in 47s
Create Release Package / test (push) Failing after 53s
Create Release Package / build-release (push) Has been skipped

- 57 unit tests covering ProductType, StockManager, CartHandler, Plugin,
  Admin/ProductData, Admin/Settings using Brain Monkey + Mockery
- WooCommerce class stubs for testing without WP installation
- PHP lint and test jobs in release workflow (test gate blocks release)
- PSR-4 namespace change: WC_Composable_Product -> Magdev\WcComposableProduct
- PascalCase filenames for all classes under includes/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 13:08:22 +01:00
parent ea2261d8d7
commit a7d6a57f01
24 changed files with 3415 additions and 12 deletions

View File

@@ -0,0 +1,108 @@
<?php
/**
* Admin ProductData Tests
*
* @package Magdev\WcComposableProduct\Tests
*/
namespace Magdev\WcComposableProduct\Tests\Unit\Admin;
use Magdev\WcComposableProduct\Tests\TestCase;
use Magdev\WcComposableProduct\Admin\ProductData;
use Brain\Monkey\Functions;
class ProductDataTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$_POST = [];
}
protected function tearDown(): void
{
$_POST = [];
parent::tearDown();
}
public function testConstructor_RegistersExpectedHooks(): void
{
$productData = new ProductData();
self::assertNotFalse(has_filter('woocommerce_product_data_tabs', 'Magdev\WcComposableProduct\Admin\ProductData->add_product_data_tab()'));
self::assertNotFalse(has_action('woocommerce_product_data_panels', 'Magdev\WcComposableProduct\Admin\ProductData->add_product_data_panel()'));
self::assertNotFalse(has_action('woocommerce_process_product_meta_composable', 'Magdev\WcComposableProduct\Admin\ProductData->save_product_data()'));
self::assertNotFalse(has_action('woocommerce_product_options_general_product_data', 'Magdev\WcComposableProduct\Admin\ProductData->add_general_fields()'));
}
public function testAddProductDataTab_AddsComposableTab(): void
{
$productData = new ProductData();
$tabs = $productData->add_product_data_tab([]);
$this->assertArrayHasKey('composable', $tabs);
$this->assertSame('composable_product_data', $tabs['composable']['target']);
$this->assertContains('show_if_composable', $tabs['composable']['class']);
$this->assertSame(21, $tabs['composable']['priority']);
}
public function testSaveProductData_SavesAllFields(): void
{
$_POST = [
'_composable_selection_limit' => '5',
'_composable_pricing_mode' => 'fixed',
'_composable_include_unpublished' => 'yes',
'_composable_criteria_type' => 'tag',
'_composable_categories' => ['1', '2'],
'_composable_tags' => ['3', '4'],
'_composable_skus' => 'SKU-1, SKU-2',
];
Functions\expect('absint')->andReturnUsing(function ($val) {
return abs((int) $val);
});
Functions\expect('sanitize_text_field')->andReturnUsing(function ($val) {
return $val;
});
Functions\expect('sanitize_textarea_field')->andReturnUsing(function ($val) {
return $val;
});
Functions\expect('update_post_meta')->times(7);
$productData = new ProductData();
$productData->save_product_data(42);
}
public function testSaveProductData_DefaultsWhenPostEmpty(): void
{
// No POST data at all
Functions\expect('absint')->andReturnUsing(function ($val) {
return abs((int) $val);
});
Functions\expect('sanitize_text_field')->andReturnUsing(function ($val) {
return $val;
});
Functions\expect('sanitize_textarea_field')->andReturnUsing(function ($val) {
return $val;
});
Functions\expect('update_post_meta')
->with(42, '_composable_selection_limit', \Mockery::any())->once();
Functions\expect('update_post_meta')
->with(42, '_composable_pricing_mode', '')->once();
Functions\expect('update_post_meta')
->with(42, '_composable_include_unpublished', '')->once();
Functions\expect('update_post_meta')
->with(42, '_composable_criteria_type', 'category')->once();
Functions\expect('update_post_meta')
->with(42, '_composable_categories', [])->once();
Functions\expect('update_post_meta')
->with(42, '_composable_tags', [])->once();
Functions\expect('update_post_meta')
->with(42, '_composable_skus', '')->once();
$productData = new ProductData();
$productData->save_product_data(42);
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* Admin Settings Tests
*
* @package Magdev\WcComposableProduct\Tests
*/
namespace Magdev\WcComposableProduct\Tests\Unit\Admin;
use Magdev\WcComposableProduct\Tests\TestCase;
use Magdev\WcComposableProduct\Admin\Settings;
use Brain\Monkey\Functions;
class SettingsTest extends TestCase
{
public function testConstructor_SetsIdAndLabel(): void
{
$settings = new Settings();
$this->assertSame('composable_products', $settings->get_id());
}
public function testGetSettings_ReturnsExpectedFieldIds(): void
{
Functions\expect('apply_filters')
->once()
->with('wc_composable_settings', \Mockery::type('array'))
->andReturnUsing(function ($hook, $settings) {
return $settings;
});
$settings = new Settings();
$fields = $settings->get_settings();
// Extract all field IDs
$ids = array_column($fields, 'id');
$this->assertContains('wc_composable_settings', $ids);
$this->assertContains('wc_composable_default_limit', $ids);
$this->assertContains('wc_composable_default_pricing', $ids);
$this->assertContains('wc_composable_include_unpublished', $ids);
$this->assertContains('wc_composable_show_images', $ids);
$this->assertContains('wc_composable_show_prices', $ids);
$this->assertContains('wc_composable_show_total', $ids);
}
public function testGetSettings_HasCorrectFieldTypes(): void
{
Functions\expect('apply_filters')
->once()
->andReturnUsing(function ($hook, $settings) {
return $settings;
});
$settings = new Settings();
$fields = $settings->get_settings();
// Index fields by ID for easy lookup
$indexed = [];
foreach ($fields as $field) {
if (isset($field['id'])) {
$indexed[$field['id']] = $field;
}
}
$this->assertSame('number', $indexed['wc_composable_default_limit']['type']);
$this->assertSame('select', $indexed['wc_composable_default_pricing']['type']);
$this->assertSame('checkbox', $indexed['wc_composable_include_unpublished']['type']);
$this->assertSame('checkbox', $indexed['wc_composable_show_images']['type']);
}
public function testGetSettings_AppliesFilter(): void
{
Functions\expect('apply_filters')
->once()
->with('wc_composable_settings', \Mockery::type('array'))
->andReturnUsing(function ($hook, $settings) {
return $settings;
});
$settings = new Settings();
$settings->get_settings();
}
}

View File

@@ -0,0 +1,184 @@
<?php
/**
* CartHandler Tests
*
* @package Magdev\WcComposableProduct\Tests
*/
namespace Magdev\WcComposableProduct\Tests\Unit;
use Magdev\WcComposableProduct\Tests\TestCase;
use Magdev\WcComposableProduct\CartHandler;
use Brain\Monkey\Functions;
use Brain\Monkey\Actions;
use Brain\Monkey\Filters;
class CartHandlerTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// Clean up POST superglobal between tests
$_POST = [];
}
protected function tearDown(): void
{
$_POST = [];
parent::tearDown();
}
public function testConstructor_RegistersExpectedHooks(): void
{
$handler = new CartHandler();
self::assertNotFalse(has_filter('woocommerce_add_to_cart_validation', 'Magdev\WcComposableProduct\CartHandler->validate_add_to_cart()'));
self::assertNotFalse(has_filter('woocommerce_add_cart_item_data', 'Magdev\WcComposableProduct\CartHandler->add_cart_item_data()'));
self::assertNotFalse(has_filter('woocommerce_get_cart_item_from_session', 'Magdev\WcComposableProduct\CartHandler->get_cart_item_from_session()'));
self::assertNotFalse(has_filter('woocommerce_get_item_data', 'Magdev\WcComposableProduct\CartHandler->display_cart_item_data()'));
self::assertNotFalse(has_action('woocommerce_before_calculate_totals', 'Magdev\WcComposableProduct\CartHandler->calculate_cart_item_price()'));
self::assertNotFalse(has_action('woocommerce_single_product_summary', 'Magdev\WcComposableProduct\CartHandler->render_product_selector()'));
self::assertNotFalse(has_filter('woocommerce_is_purchasable', 'Magdev\WcComposableProduct\CartHandler->hide_default_add_to_cart()'));
}
public function testHideDefaultAddToCart_ReturnsFalseForComposable(): void
{
$handler = new CartHandler();
$product = $this->createProductMock(['get_type' => 'composable']);
$result = $handler->hide_default_add_to_cart(true, $product);
$this->assertFalse($result);
}
public function testHideDefaultAddToCart_PassesThroughForSimple(): void
{
$handler = new CartHandler();
$product = $this->createProductMock(['get_type' => 'simple']);
$result = $handler->hide_default_add_to_cart(true, $product);
$this->assertTrue($result);
}
public function testValidateAddToCart_PassesThroughForNonComposable(): void
{
$product = $this->createProductMock(['get_type' => 'simple']);
Functions\expect('wc_get_product')->with(1)->andReturn($product);
$handler = new CartHandler();
$result = $handler->validate_add_to_cart(true, 1, 1);
$this->assertTrue($result);
}
public function testValidateAddToCart_ReturnsFalseWhenNoProductsSelected(): void
{
$product = $this->createProductMock(['get_type' => 'composable']);
Functions\expect('wc_get_product')->with(1)->andReturn($product);
Functions\expect('wc_add_notice')->once();
$_POST['composable_products'] = [];
$handler = new CartHandler();
$result = $handler->validate_add_to_cart(true, 1, 1);
$this->assertFalse($result);
}
public function testValidateAddToCart_ReturnsFalseWhenNoPostData(): void
{
$product = $this->createProductMock(['get_type' => 'composable']);
Functions\expect('wc_get_product')->with(1)->andReturn($product);
Functions\expect('wc_add_notice')->once();
// No $_POST['composable_products'] at all
$handler = new CartHandler();
$result = $handler->validate_add_to_cart(true, 1, 1);
$this->assertFalse($result);
}
public function testAddCartItemData_AddsSelectionsForComposable(): void
{
$product = $this->createProductMock(['get_type' => 'composable']);
Functions\expect('wc_get_product')->with(1)->andReturn($product);
Functions\expect('absint')->andReturnUsing(function ($val) {
return abs((int) $val);
});
$_POST['composable_products'] = ['101', '102'];
$handler = new CartHandler();
$result = $handler->add_cart_item_data([], 1);
$this->assertArrayHasKey('composable_products', $result);
$this->assertSame([101, 102], $result['composable_products']);
$this->assertArrayHasKey('unique_key', $result);
}
public function testAddCartItemData_PassesThroughForNonComposable(): void
{
$product = $this->createProductMock(['get_type' => 'simple']);
Functions\expect('wc_get_product')->with(1)->andReturn($product);
$handler = new CartHandler();
$result = $handler->add_cart_item_data(['existing' => 'data'], 1);
$this->assertSame(['existing' => 'data'], $result);
}
public function testGetCartItemFromSession_RestoresComposableProducts(): void
{
$handler = new CartHandler();
$result = $handler->get_cart_item_from_session(
['data' => 'test'],
['composable_products' => [101, 102]]
);
$this->assertSame([101, 102], $result['composable_products']);
}
public function testGetCartItemFromSession_PassesThroughWithoutComposableData(): void
{
$handler = new CartHandler();
$result = $handler->get_cart_item_from_session(
['data' => 'test'],
[]
);
$this->assertArrayNotHasKey('composable_products', $result);
}
public function testDisplayCartItemData_FormatsProductNames(): void
{
$mock1 = $this->createProductMock(['get_name' => 'Product A']);
$mock2 = $this->createProductMock(['get_name' => 'Product B']);
Functions\expect('wc_get_product')
->andReturnUsing(function ($id) use ($mock1, $mock2) {
return match ($id) {
101 => $mock1,
102 => $mock2,
default => false,
};
});
$handler = new CartHandler();
$result = $handler->display_cart_item_data(
[],
['composable_products' => [101, 102]]
);
$this->assertCount(1, $result);
$this->assertStringContainsString('Product A', $result[0]['value']);
$this->assertStringContainsString('Product B', $result[0]['value']);
}
public function testDisplayCartItemData_ReturnsEmptyForNonComposable(): void
{
$handler = new CartHandler();
$result = $handler->display_cart_item_data([], []);
$this->assertSame([], $result);
}
}

72
tests/Unit/PluginTest.php Normal file
View File

@@ -0,0 +1,72 @@
<?php
/**
* Plugin Tests
*
* @package Magdev\WcComposableProduct\Tests
*/
namespace Magdev\WcComposableProduct\Tests\Unit;
use Magdev\WcComposableProduct\Tests\TestCase;
use Magdev\WcComposableProduct\Plugin;
use Brain\Monkey\Functions;
class PluginTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// Reset the singleton instance between tests
$reflection = new \ReflectionClass(Plugin::class);
$property = $reflection->getProperty('instance');
$property->setAccessible(true);
$property->setValue(null, null);
}
public function testInstance_ReturnsSingleton(): void
{
$instance1 = Plugin::instance();
$instance2 = Plugin::instance();
$this->assertSame($instance1, $instance2);
}
public function testInstance_ReturnsPluginClass(): void
{
$instance = Plugin::instance();
$this->assertInstanceOf(Plugin::class, $instance);
}
public function testAddProductType_AddsComposableToTypes(): void
{
$plugin = Plugin::instance();
$types = $plugin->add_product_type([]);
$this->assertArrayHasKey('composable', $types);
}
public function testProductClass_ReturnsCustomClassForComposable(): void
{
$plugin = Plugin::instance();
$class = $plugin->product_class('WC_Product', 'composable');
$this->assertSame('Magdev\WcComposableProduct\ProductType', $class);
}
public function testProductClass_PassesThroughForOtherTypes(): void
{
$plugin = Plugin::instance();
$class = $plugin->product_class('WC_Product', 'simple');
$this->assertSame('WC_Product', $class);
}
public function testGetTwig_ReturnsTwigEnvironment(): void
{
$plugin = Plugin::instance();
$this->assertInstanceOf(\Twig\Environment::class, $plugin->get_twig());
}
}

View File

@@ -0,0 +1,218 @@
<?php
/**
* ProductType Tests
*
* @package Magdev\WcComposableProduct\Tests
*/
namespace Magdev\WcComposableProduct\Tests\Unit;
use Magdev\WcComposableProduct\Tests\TestCase;
use Magdev\WcComposableProduct\ProductType;
use Brain\Monkey\Functions;
class ProductTypeTest extends TestCase
{
private function createProductType(array $meta = []): ProductType
{
$product = new ProductType();
foreach ($meta as $key => $value) {
$product->update_meta_data($key, $value);
}
return $product;
}
public function testGetType_ReturnsComposable(): void
{
$product = $this->createProductType();
$this->assertSame('composable', $product->get_type());
}
public function testIsPurchasable_ReturnsTrue(): void
{
$product = $this->createProductType();
$this->assertTrue($product->is_purchasable());
}
public function testIsSoldIndividually_ReturnsTrue(): void
{
$product = $this->createProductType();
$this->assertTrue($product->is_sold_individually());
}
public function testGetSelectionLimit_UsesProductMeta(): void
{
Functions\expect('absint')->once()->andReturnUsing(function ($val) {
return abs((int) $val);
});
$product = $this->createProductType(['_composable_selection_limit' => '3']);
$this->assertSame(3, $product->get_selection_limit());
}
public function testGetSelectionLimit_FallsBackToGlobalDefault(): void
{
Functions\expect('get_option')
->once()
->with('wc_composable_default_limit', 5)
->andReturn(7);
Functions\expect('absint')->once()->andReturnUsing(function ($val) {
return abs((int) $val);
});
$product = $this->createProductType();
$this->assertSame(7, $product->get_selection_limit());
}
public function testGetSelectionLimit_FallsBackToHardDefault(): void
{
Functions\expect('get_option')
->once()
->with('wc_composable_default_limit', 5)
->andReturn(5);
Functions\expect('absint')->once()->andReturnUsing(function ($val) {
return abs((int) $val);
});
$product = $this->createProductType();
$this->assertSame(5, $product->get_selection_limit());
}
public function testGetPricingMode_UsesProductMeta(): void
{
$product = $this->createProductType(['_composable_pricing_mode' => 'fixed']);
$this->assertSame('fixed', $product->get_pricing_mode());
}
public function testGetPricingMode_FallsBackToGlobalDefault(): void
{
Functions\expect('get_option')
->once()
->with('wc_composable_default_pricing', 'sum')
->andReturn('sum');
$product = $this->createProductType();
$this->assertSame('sum', $product->get_pricing_mode());
}
public function testShouldIncludeUnpublished_PerProductYes(): void
{
$product = $this->createProductType(['_composable_include_unpublished' => 'yes']);
$this->assertTrue($product->should_include_unpublished());
}
public function testShouldIncludeUnpublished_PerProductNo(): void
{
$product = $this->createProductType(['_composable_include_unpublished' => 'no']);
$this->assertFalse($product->should_include_unpublished());
}
public function testShouldIncludeUnpublished_FallsBackToGlobalYes(): void
{
Functions\expect('get_option')
->once()
->with('wc_composable_include_unpublished', 'no')
->andReturn('yes');
$product = $this->createProductType();
$this->assertTrue($product->should_include_unpublished());
}
public function testShouldIncludeUnpublished_FallsBackToGlobalNo(): void
{
Functions\expect('get_option')
->once()
->with('wc_composable_include_unpublished', 'no')
->andReturn('no');
$product = $this->createProductType();
$this->assertFalse($product->should_include_unpublished());
}
public function testGetSelectionCriteria_DefaultsToCategory(): void
{
$product = $this->createProductType();
$criteria = $product->get_selection_criteria();
$this->assertSame('category', $criteria['type']);
$this->assertSame([], $criteria['categories']);
$this->assertSame([], $criteria['tags']);
$this->assertSame('', $criteria['skus']);
}
public function testGetSelectionCriteria_UsesProductMeta(): void
{
$product = $this->createProductType([
'_composable_criteria_type' => 'tag',
'_composable_tags' => [5, 10],
]);
$criteria = $product->get_selection_criteria();
$this->assertSame('tag', $criteria['type']);
$this->assertSame([5, 10], $criteria['tags']);
}
public function testCalculateComposedPrice_FixedMode(): void
{
$product = $this->createProductType(['_composable_pricing_mode' => 'fixed']);
// Set regular price via the stub's data property
$reflection = new \ReflectionClass($product);
$dataProp = $reflection->getProperty('data');
$dataProp->setAccessible(true);
$data = $dataProp->getValue($product);
$data['regular_price'] = '25.00';
$dataProp->setValue($product, $data);
$price = $product->calculate_composed_price([101, 102]);
$this->assertSame(25.0, $price);
}
public function testCalculateComposedPrice_SumMode(): void
{
Functions\expect('get_option')
->with('wc_composable_default_pricing', 'sum')
->andReturn('sum');
$mock1 = $this->createProductMock(['get_price' => '5.00']);
$mock2 = $this->createProductMock(['get_price' => '7.50']);
Functions\expect('wc_get_product')
->andReturnUsing(function ($id) use ($mock1, $mock2) {
return match ($id) {
101 => $mock1,
102 => $mock2,
default => false,
};
});
$product = $this->createProductType();
$price = $product->calculate_composed_price([101, 102]);
$this->assertSame(12.5, $price);
}
public function testCalculateComposedPrice_SumMode_SkipsInvalidProducts(): void
{
Functions\expect('get_option')
->with('wc_composable_default_pricing', 'sum')
->andReturn('sum');
$mock1 = $this->createProductMock(['get_price' => '5.00']);
Functions\expect('wc_get_product')
->andReturnUsing(function ($id) use ($mock1) {
return match ($id) {
101 => $mock1,
default => false,
};
});
$product = $this->createProductType();
$price = $product->calculate_composed_price([101, 999]);
$this->assertSame(5.0, $price);
}
}

View File

@@ -0,0 +1,226 @@
<?php
/**
* StockManager Tests
*
* @package Magdev\WcComposableProduct\Tests
*/
namespace Magdev\WcComposableProduct\Tests\Unit;
use Magdev\WcComposableProduct\Tests\TestCase;
use Magdev\WcComposableProduct\StockManager;
use Brain\Monkey\Functions;
class StockManagerTest extends TestCase
{
private StockManager $manager;
protected function setUp(): void
{
parent::setUp();
$this->manager = new StockManager();
}
// --- validate_stock_availability ---
public function testValidateStock_ReturnsTrueWhenAllInStock(): void
{
$mock = $this->createProductMock([
'managing_stock' => false,
'is_in_stock' => true,
]);
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
Functions\expect('wc_get_product')->with(2)->andReturn($mock);
$result = $this->manager->validate_stock_availability([1, 2]);
$this->assertTrue($result);
}
public function testValidateStock_ReturnsTrueWhenNotManagingStock(): void
{
$mock = $this->createProductMock(['managing_stock' => false]);
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
$result = $this->manager->validate_stock_availability([1]);
$this->assertTrue($result);
}
public function testValidateStock_ReturnsErrorForOutOfStock(): void
{
$mock = $this->createProductMock([
'managing_stock' => true,
'is_in_stock' => false,
'get_name' => 'Widget',
]);
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
$result = $this->manager->validate_stock_availability([1]);
$this->assertIsString($result);
$this->assertStringContainsString('Widget', $result);
}
public function testValidateStock_ReturnsErrorForInsufficientQuantity(): void
{
$mock = $this->createProductMock([
'managing_stock' => true,
'is_in_stock' => true,
'get_stock_quantity' => 1,
'get_name' => 'Widget',
'backorders_allowed' => false,
]);
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
$result = $this->manager->validate_stock_availability([1], 5);
$this->assertIsString($result);
$this->assertStringContainsString('Widget', $result);
}
public function testValidateStock_PassesWhenBackordersAllowed(): void
{
// When stock_quantity is null the insufficient-stock check is skipped,
// and the backorders_allowed() branch is reached.
$mock = $this->createProductMock([
'managing_stock' => true,
'is_in_stock' => true,
'get_stock_quantity' => null,
'backorders_allowed' => true,
]);
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
$result = $this->manager->validate_stock_availability([1], 5);
$this->assertTrue($result);
}
public function testValidateStock_SkipsNullProducts(): void
{
Functions\expect('wc_get_product')->with(999)->andReturn(false);
$result = $this->manager->validate_stock_availability([999]);
$this->assertTrue($result);
}
// --- get_product_stock_info ---
public function testGetProductStockInfo_ReturnsCorrectStructure(): void
{
$mock = $this->createProductMock([
'is_in_stock' => true,
'get_stock_quantity' => 10,
'backorders_allowed' => false,
'get_stock_status' => 'instock',
'managing_stock' => true,
]);
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
$info = $this->manager->get_product_stock_info(1);
$this->assertTrue($info['in_stock']);
$this->assertSame(10, $info['stock_quantity']);
$this->assertFalse($info['backorders_allowed']);
$this->assertSame('instock', $info['stock_status']);
$this->assertTrue($info['managing_stock']);
$this->assertTrue($info['has_enough_stock']);
}
public function testGetProductStockInfo_ReturnsFallbackForInvalidProduct(): void
{
Functions\expect('wc_get_product')->with(999)->andReturn(false);
$info = $this->manager->get_product_stock_info(999);
$this->assertFalse($info['in_stock']);
$this->assertSame(0, $info['stock_quantity']);
$this->assertFalse($info['backorders_allowed']);
$this->assertSame('outofstock', $info['stock_status']);
}
public function testGetProductStockInfo_HasEnoughStockTrueWhenNotManaging(): void
{
$mock = $this->createProductMock([
'managing_stock' => false,
'get_stock_quantity' => null,
]);
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
$info = $this->manager->get_product_stock_info(1, 100);
$this->assertTrue($info['has_enough_stock']);
}
public function testGetProductStockInfo_HasEnoughStockFalseWhenInsufficient(): void
{
$mock = $this->createProductMock([
'managing_stock' => true,
'get_stock_quantity' => 2,
]);
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
$info = $this->manager->get_product_stock_info(1, 5);
$this->assertFalse($info['has_enough_stock']);
}
// --- prevent_composable_stock_reduction ---
public function testPreventStockReduction_ReturnsFalseForComposableItem(): void
{
$productMock = $this->createProductMock(['get_type' => 'composable']);
$itemMock = \Mockery::mock('WC_Order_Item_Product');
$itemMock->shouldReceive('get_product')->andReturn($productMock);
$orderMock = \Mockery::mock('WC_Order');
$orderMock->shouldReceive('get_items')->andReturn([$itemMock]);
$result = $this->manager->prevent_composable_stock_reduction(true, $orderMock);
$this->assertFalse($result);
}
public function testPreventStockReduction_PassesThroughForNonComposable(): void
{
$productMock = $this->createProductMock(['get_type' => 'simple']);
$itemMock = \Mockery::mock('WC_Order_Item_Product');
$itemMock->shouldReceive('get_product')->andReturn($productMock);
$orderMock = \Mockery::mock('WC_Order');
$orderMock->shouldReceive('get_items')->andReturn([$itemMock]);
$result = $this->manager->prevent_composable_stock_reduction(true, $orderMock);
$this->assertTrue($result);
}
// --- store_selected_products_in_order ---
public function testStoreSelectedProducts_AddsMetaWhenPresent(): void
{
$itemMock = \Mockery::mock('WC_Order_Item_Product');
$itemMock->shouldReceive('add_meta_data')
->once()
->with('_composable_products', [1, 2], true);
$this->manager->store_selected_products_in_order(
$itemMock,
'cart_key',
['composable_products' => [1, 2]]
);
}
public function testStoreSelectedProducts_DoesNothingWithoutData(): void
{
$itemMock = \Mockery::mock('WC_Order_Item_Product');
$itemMock->shouldNotReceive('add_meta_data');
$this->manager->store_selected_products_in_order(
$itemMock,
'cart_key',
[]
);
}
}