diff --git a/tests/Helpers/AttributeHelperTest.php b/tests/Helpers/AttributeHelperTest.php new file mode 100644 index 000000000..1090e9fe4 --- /dev/null +++ b/tests/Helpers/AttributeHelperTest.php @@ -0,0 +1,127 @@ +assertEquals('attribute_value', getAttributeDataType('TEXT')); + } + + /** + * Test getAttributeDataType for DECIMAL type + * + * @return void + */ + public function testGetAttributeDataTypeForDecimal(): void + { + $this->assertEquals('attribute_decimal', getAttributeDataType('DECIMAL')); + } + + /** + * Test getAttributeDataType for DATE type + * + * @return void + */ + public function testGetAttributeDataTypeForDate(): void + { + $this->assertEquals('attribute_date', getAttributeDataType('DATE')); + } + + /** + * Test getAttributeDataType for DROPDOWN type + * + * @return void + */ + public function testGetAttributeDataTypeForDropdown(): void + { + // Note: DROPDOWN is a special case that uses attribute_value + // This test verifies the expected behavior + $this->assertEquals('attribute_value', getAttributeDataType('DROPDOWN')); + } + + /** + * Test getAttributeDataType for invalid type returns fallback + * + * @return void + */ + public function testGetAttributeDataTypeForInvalidType(): void + { + // Invalid types should return 'attribute_value' as fallback + $this->assertEquals('attribute_value', getAttributeDataType('INVALID_TYPE')); + } + + /** + * Test getAttributeDataType for checkbox type + * + * @return void + */ + public function testGetAttributeDataTypeForCheckbox(): void + { + // CHECKBOX values are stored as '0' or '1' in attribute_value + $this->assertEquals('attribute_value', getAttributeDataType('CHECKBOX')); + } + + /** + * Test that validateAttributeValueType throws exception for invalid type + * + * @return void + */ + public function testValidateAttributeValueTypeInvalid(): void + { + $this->expectException(\InvalidArgumentException::class); + validateAttributeValueType('INVALID_TYPE'); + } + + /** + * Test that validateAttributeValueType does not throw for valid types + * + * @return void + */ + public function testValidateAttributeValueTypeValidText(): void + { + // Should not throw exception + validateAttributeValueType('attribute_value'); + } + + /** + * Test that validateAttributeValueType does not throw for decimal type + * + * @return void + */ + public function testValidateAttributeValueTypeValidDecimal(): void + { + // Should not throw exception + validateAttributeValueType('attribute_decimal'); + } + + /** + * Test that validateAttributeValueType does not throw for date type + * + * @return void + */ + public function testValidateAttributeValueTypeValidDate(): void + { + // Should not throw exception + validateAttributeValueType('attribute_date'); + } +} \ No newline at end of file diff --git a/tests/Helpers/ImportFileHelperTest.php b/tests/Helpers/ImportFileHelperTest.php new file mode 100644 index 000000000..2a2d01b28 --- /dev/null +++ b/tests/Helpers/ImportFileHelperTest.php @@ -0,0 +1,88 @@ +assertEquals(0, strcasecmp('_DELETE_', '_DELETE_'), + 'Exact match should return 0'); + $this->assertEquals(0, strcasecmp('_DELETE_', '_delete_'), + 'Lowercase should match'); + $this->assertEquals(0, strcasecmp('_DELETE_', '_Delete_'), + 'Mixed case should match'); + + // Test that non-matching strings return non-zero + $this->assertNotEquals(0, strcasecmp('_DELETE_', 'DELETE'), + 'Without underscore should not match'); + $this->assertNotEquals(0, strcasecmp('_DELETE_', 'test'), + 'Random text should not match'); + } + + /** + * Test that _DELETE_ does not match similar-looking strings + * + * @return void + */ + public function testDeleteMagicWordNotConfusedWithSimilar(): void + { + // These should NOT match + $this->assertNotEquals(0, strcasecmp('_DELETE_', '__DELETE__'), + 'Double underscore should not match'); + $this->assertNotEquals(0, strcasecmp('_DELETE_', 'DELETE_'), + 'Without underscore should not match'); + $this->assertNotEquals(0, strcasecmp('_DELETE_', '_DELETE '), + 'With trailing space should not match'); + $this->assertNotEquals(0, strcasecmp('_DELETE_', ' _DELETE_'), + 'With leading space should not match'); + } + + /** + * Test empty string does not match _DELETE_ + * + * @return void + */ + public function testEmptyStringNotDelete(): void + { + $this->assertNotEquals(0, strcasecmp('_DELETE_', ''), + 'Empty string should not match _DELETE_'); + } + + /** + * Test null safety with strcasecmp + * + * @return void + */ + public function testDeleteMagicWordNullSafety(): void + { + // strcasecmp with null would cause a warning + // This test documents the need for null checking in the controller + $testString = '_DELETE_'; + $this->assertIsString($testString, 'Test string should not be null'); + + // In the actual code, empty() checks would be done before strcasecmp + $this->assertTrue(!empty($testString), 'Empty check should pass'); + } +} \ No newline at end of file diff --git a/tests/Models/AttributeTest.php b/tests/Models/AttributeTest.php new file mode 100644 index 000000000..1d0dcddf0 --- /dev/null +++ b/tests/Models/AttributeTest.php @@ -0,0 +1,823 @@ +attribute = model(Attribute::class); + $this->item = model(Item::class); + helper('attribute'); + } + + protected function tearDown(): void + { + parent::tearDown(); + } + + /** + * Test that case-sensitive attribute value updates work correctly + * + * @return void + */ + public function testCaseSensitiveAttributeValueUpdate(): void + { + // Create a text definition + $definitionData = [ + 'definition_name' => 'Color', + 'definition_type' => TEXT, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + // Create an item + $itemData = [ + 'item_id' => null, + 'name' => 'Test Item', + 'category' => 0, + 'supplier_id' => null, + 'item_number' => 'TEST001', + 'description' => 'Test item description', + 'cost_price' => 10.00, + 'unit_price' => 15.00, + 'reorder_level' => 0, + 'receiving_quantity' => 1, + 'allow_alt_description' => NEW_ENTRY, + 'is_serialized' => NEW_ENTRY, + 'deleted' => 0 + ]; + $itemId = $this->item->saveValue($itemData)['item_id']; + + // Save initial attribute value with uppercase + $attributeValue = 'RED'; + $attributeId1 = $this->attribute->saveAttributeValue( + $attributeValue, + $definitionId, + $itemId, + false, + TEXT + ); + + // Update with lowercase + $attributeValueLower = 'red'; + $attributeId2 = $this->attribute->saveAttributeValue( + $attributeValueLower, + $definitionId, + $itemId, + $attributeId1, + TEXT + ); + + // Verify the value was updated to lowercase + $result = $this->attribute->getAttributeValue($itemId, $definitionId); + $this->assertEquals('red', strtolower($result->attribute_value), + 'Attribute value should be updated from RED to red'); + + // The attribute_value table should have been updated, not duplicated + $builder = $this->db->table('attribute_values'); + $builder->where('attribute_id', $attributeId1); + $query = $builder->get(); + $rows = $query->getResult(); + + $this->assertCount(1, $rows, 'Should only have one attribute_value row'); + } + + /** + * Test that attribute link deletion works correctly + * + * @return void + */ + public function testDeleteAttributeLinks(): void + { + // Create an item + $itemData = [ + 'item_id' => null, + 'name' => 'Test Item', + 'category' => 0, + 'supplier_id' => null, + 'item_number' => 'TEST002', + 'description' => 'Test item description', + 'cost_price' => 10.00, + 'unit_price' => 15.00, + 'reorder_level' => 0, + 'receiving_quantity' => 1, + 'allow_alt_description' => NEW_ENTRY, + 'is_serialized' => NEW_ENTRY, + 'deleted' => 0 + ]; + $itemId = $this->item->saveValue($itemData)['item_id']; + + // Create a definition + $definitionData = [ + 'definition_name' => 'Size', + 'definition_type' => TEXT, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + // Save an attribute link + $attributeValue = 'Medium'; + $attributeId = $this->attribute->saveAttributeValue( + $attributeValue, + $definitionId, + $itemId, + false, + TEXT + ); + + // Verify the link exists + $this->attribute->saveAttributeLink($itemId, $definitionId, $attributeId); + + $builder = $this->db->table('attribute_links'); + $builder->where('item_id', $itemId); + $builder->where('definition_id', $definitionId); + $query = $builder->get(); + $result = $query->getResult(); + + $this->assertCount(1, $result, 'Attribute link should exist before deletion'); + + // Delete the attribute link + $deleted = $this->attribute->deleteAttributeLinks($itemId, $definitionId); + + // Verify the link is deleted + $this->assertTrue($deleted, 'deleteAttributeLinks should return true'); + + $builder = $this->db->table('attribute_links'); + $builder->where('item_id', $itemId); + $builder->where('definition_id', $definitionId); + $query = $builder->get(); + $result = $query->getResult(); + + $this->assertCount(0, $result, 'Attribute link should be deleted'); + } + + /** + * Test that category dropdown can be enabled (bug fix from PR) + * + * @return void + */ + public function testCategoryDropdownCanBeEnabled(): void + { + // Create a dropdown definition for category + $definitionData = [ + 'definition_name' => 'category_dropdown', + 'definition_type' => DROPDOWN, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + // The bug was that definition_flags == 1 check prevented dropdowns + // Now it should use truthy check + $builder = $this->db->table('attribute_definitions'); + $builder->where('definition_id', $definitionId); + $query = $builder->get(); + $result = $query->getRow(); + + $this->assertEquals(DROPDOWN, $result->definition_type, 'Definition type should be DROPDOWN'); + $this->assertEquals(0, $result->definition_flags, 'Definition flags should be 0'); + } + + /** + * Test DROPDOWN attribute value saving + * + * @return void + */ + public function testDropdownAttributeValueSave(): void + { + $itemData = [ + 'item_id' => null, + 'name' => 'Test Item', + 'category' => 0, + 'supplier_id' => null, + 'item_number' => 'TEST003', + 'description' => 'Test item description', + 'cost_price' => 10.00, + 'unit_price' => 15.00, + 'reorder_level' => 0, + 'receiving_quantity' => 1, + 'allow_alt_description' => NEW_ENTRY, + 'is_serialized' => NEW_ENTRY, + 'deleted' => 0 + ]; + $itemId = $this->item->saveValue($itemData)['item_id']; + + // Create a dropdown definition + $definitionData = [ + 'definition_name' => 'Material', + 'definition_type' => DROPDOWN, + 'definition_flags' => 0, + 'definition_unit' => null, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + // Add dropdown values + $dropdownValues = ['Cotton', 'Polyester', 'Wool']; + foreach ($dropdownValues as $i => $value) { + $valueData = [ + 'attribute_value' => $value, + 'definition_id' => $definitionId, + 'definition_type' => DROPDOWN, + 'attribute_group' => $i, + 'deleted' => 0 + ]; + $this->db->table('attribute_values')->insert($valueData); + } + + // Save attribute with dropdown value + $attributeValue = 'Cotton'; + $attributeId = $this->attribute->saveAttributeValue( + $attributeValue, + $definitionId, + $itemId, + false, + DROPDOWN + ); + + // Verify the dropdown value was saved + $result = $this->attribute->getAttributeValue($itemId, $definitionId); + $this->assertEquals('Cotton', $result->attribute_value); + + // Verify the attribute link was created + $builder = $this->db->table('attribute_links'); + $builder->where('item_id', $itemId); + $builder->where('definition_id', $definitionId); + $query = $builder->get(); + $linkResult = $query->getRow(); + + $this->assertNotNull($linkResult->attribute_id, 'Attribute link should be created for dropdown'); + } + + /** + * Test DATE attribute value saving + * + * @return void + */ + public function testDateAttributeValueSave(): void + { + $itemData = [ + 'item_id' => null, + 'name' => 'Test Item', + 'category' => 0, + 'supplier_id' => null, + 'item_number' => 'TEST004', + 'description' => 'Test item', + 'cost_price' => 10.00, + 'unit_price' => 15.00, + 'reorder_level' => 0, + 'receiving_quantity' => 1, + 'allow_alt_description' => NEW_ENTRY, + 'is_serialized' => NEW_ENTRY, + 'deleted' => 0 + ]; + $itemId = $this->item->saveValue($itemData)['item_id']; + + $definitionData = [ + 'definition_name' => 'Manufacture Date', + 'definition_type' => DATE, + 'definition_flags' => 0, + 'definition_unit' => null, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + // Save date attribute + $dateValue = date('Y-m-d'); + $attributeId = $this->attribute->saveAttributeValue( + $dateValue, + $definitionId, + $itemId, + false, + DATE + ); + + // Verify the date was saved + $result = $this->attribute->getAttributeValue($itemId, $definitionId); + $this->assertEquals($dateValue, $result->attribute_date); + } + + /** + * Test DATE attribute value case update + * + * @return void + */ + public function testDateAttributeValueCaseUpdate(): void + { + $itemData = [ + 'item_id' => null, + 'name' => 'Test Item', + 'category' => 0, + 'supplier_id' => null, + 'item_number' => 'TEST005', + 'description' => 'Test item', + 'cost_price' => 10.00, + 'unit_price' => 15.00, + 'reorder_level' => 0, + 'receiving_quantity' => 1, + 'allow_alt_description' => NEW_ENTRY, + 'is_serialized' => NEW_ENTRY, + 'deleted' => 0 + ]; + $itemId = $this->item->saveValue($itemData)['item_id']; + + $definitionData = [ + 'definition_name' => 'Expiration Date', + 'definition_type' => DATE, + 'definition_flags' => 0, + 'definition_unit' => null, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + // Save initial date + $dateValue = date('Y-m-d', strtotime('2025-01-01')); + $attributeId1 = $this->attribute->saveAttributeValue( + $dateValue, + $definitionId, + $itemId, + false, + DATE + ); + + // Date format doesn't have case, but this test verifies the logic path + $result = $this->attribute->getAttributeValue($itemId, $definitionId); + $this->assertEquals($dateValue, $result->attribute_date); + } + + /** + * Test DECIMAL attribute value saving + * + * @return void + */ + public function testDecimalAttributeValueSave(): void + { + $itemData = [ + 'item_id' => null, + 'name' => 'Test Item', + 'category' => 0, + 'supplier_id' => null, + 'item_number' => 'TEST006', + 'description' => 'Test item', + 'cost_price' => 10.00, + 'unit_price' => 15.00, + 'reorder_level' => 0, + 'receiving_quantity' => 1, + 'allow_alt_description' => NEW_ENTRY, + 'is_serialized' => NEW_ENTRY, + 'deleted' => 0 + ]; + $itemId = $this->item->saveValue($itemData)['item_id']; + + $definitionData = [ + 'definition_name' => 'Weight', + 'definition_type' => DECIMAL, + 'definition_flags' => 0, + 'definition_unit' => 'kg', + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + // Save decimal attribute + $decimalValue = '2.5'; + $attributeId = $this->attribute->saveAttributeValue( + $decimalValue, + $definitionId, + $itemId, + false, + DECIMAL + ); + + // Verify the decimal was saved + $result = $this->attribute->getAttributeValue($itemId, $definitionId); + $this->assertEquals(2.5, (float)$result->attribute_decimal); + } + + /** + * Test CHECKBOX attribute value saving + * + * @return void + */ + public function testCheckboxAttributeValueSave(): void + { + $itemData = [ + 'item_id' => null, + 'name' => 'Test Item', + 'category' => 0, + 'supplier_id' => null, + 'item_number' => 'TEST007', + 'description' => 'Test item', + 'cost_price' => 10.00, + 'unit_price' => 15.00, + 'reorder_level' => 0, + 'receiving_quantity' => 1, + 'allow_alt_description' => NEW_ENTRY, + 'is_serialized' => NEW_ENTRY, + 'deleted' => 0 + ]; + $itemId = $this->item->saveValue($itemData)['item_id']; + + $definitionData = [ + 'definition_name' => 'Available', + 'definition_type' => CHECKBOX, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + // Save checkbox attribute (checked) + $attributeId1 = $this->attribute->saveAttributeValue( + 'true', + $definitionId, + $itemId, + false, + CHECKBOX + ); + + $result = $this->attribute->getAttributeValue($itemId, $definitionId); + $this->assertEquals('1', $result->attribute_value); + + // Update to unchecked + $attributeId2 = $this->attribute->saveAttributeValue( + 'false', + $definitionId, + $itemId, + $attributeId1, + CHECKBOX + ); + + $result = $this->attribute->getAttributeValue($itemId, $definitionId); + $this->assertEquals('0', $result->attribute_value); + } + + /** + * Test category dropdown with CATEGORY_DEFINITION_ID + * + * @return void + */ + public function testCategoryDropdownWithConstant(): void + { + // Use the CATEGORY_DEFINITION_ID constant instead of -1 + $definitionData = [ + 'definition_id' => CATEGORY_DEFINITION_ID, + 'definition_name' => 'category_dropdown', + 'definition_type' => DROPDOWN, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + + $this->assertEquals(CATEGORY_DEFINITION_ID, -1, 'CATEGORY_DEFINITION_ID constant should equal -1'); + + $definitionId = $this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID); + + $this->assertEquals(CATEGORY_DEFINITION_ID, $definitionId, + 'Category definition ID should remain CATEGORY_DEFINITION_ID'); + } + + /** + * Test that attribute links with sale_id or receiving_id are not deleted + * + * @return void + */ + public function testDeleteAttributeLinksPreservesSalesAndReceivings(): void + { + $itemData = [ + 'item_id' => null, + 'name' => 'Test Item', + 'category' => 0, + 'supplier_id' => null, + 'item_number' => 'TEST008', + 'description' => 'Test item', + 'cost_price' => 10.00, + 'unit_price' => 15.00, + 'reorder_level' => 0, + 'receiving_quantity' => 1, + 'allow_alt_description' => NEW_ENTRY, + 'is_serialized' => NEW_ENTRY, + 'deleted' => 0 + ]; + $itemId = $this->item->saveValue($itemData)['item_id']; + + $definitionData = [ + 'definition_name' => 'Color', + 'definition_type' => TEXT, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + // Create attribute value + $attributeValue = 'Blue'; + $attributeId = $this->attribute->saveAttributeValue( + $attributeValue, + $definitionId, + $itemId, + false, + TEXT + ); + + // Create attribute link WITHOUT sale_id/receiving_id + $this->attribute->saveAttributeLink($itemId, $definitionId, $attributeId); + + // Create attribute link WITH sale_id (simulation) + $builder = $this->db->table('attribute_links'); + $builder->insert([ + 'attribute_id' => $attributeId, + 'item_id' => $itemId, + 'definition_id' => $definitionId, + 'sale_id' => 1, // Has a sale reference + 'receiving_id' => null + ]); + + // Delete attribute links + $this->attribute->deleteAttributeLinks($itemId, $definitionId); + + // Verify link WITH sale_id was NOT deleted + $builder = $this->db->table('attribute_links'); + $builder->where('item_id', $itemId); + $builder->where('definition_id', $definitionId); + $builder->where('attribute_id', $attributeId); + $query = $builder->get(); + $result = $query->getResult(); + + $this->assertCount(1, $result, 'Link with sale_id should not be deleted'); + } + + /** + * Test orphaned value deletion works correctly + * + * @return void + */ + public function testDeleteOrphanedValues(): void + { + $definitionData = [ + 'definition_name' => 'Temp Attribute', + 'definition_type' => TEXT, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + // Create an orphaned attribute value (no links) + $builder = $this->db->table('attribute_values'); + $builder->insert([ + 'attribute_value' => 'Orphan Value', + 'definition_id' => $definitionId, + 'definition_type' => TEXT, + 'attribute_group' => 0, + 'deleted' => 0 + ]); + + // Delete orphaned values + $this->attribute->deleteOrphanedValues(); + + // Verify orphan was deleted + $builder = $this->db->table('attribute_values'); + $builder->where('attribute_value', 'Orphan Value'); + $builder->where('definition_id', $definitionId); + $query = $builder->get(); + $result = $query->getResult(); + + $this->assertCount(0, $result, 'Orphaned value should be deleted'); + } + + /** + * Test Unicode case comparison for attribute values + * + * @return void + */ + public function testUnicodeCaseComparison(): void + { + $itemData = [ + 'item_id' => null, + 'name' => 'Test Item', + 'category' => 0, + 'supplier_id' => null, + 'item_number' => 'TEST009', + 'description' => 'Test item', + 'cost_price' => 10.00, + 'unit_price' => 15.00, + 'reorder_level' => 0, + 'receiving_quantity' => 1, + 'allow_alt_description' => NEW_ENTRY, + 'is_serialized' => NEW_ENTRY, + 'deleted' => 0 + ]; + $itemId = $this->item->saveValue($itemData)['item_id']; + + $definitionData = [ + 'definition_name' => 'Name', + 'definition_type' => TEXT, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + // Test with Unicode characters that have case + $unicodeValue = 'ÄÄÖÜß'; // German umlauts + $attributeId1 = $this->attribute->saveAttributeValue( + $unicodeValue, + $definitionId, + $itemId, + false, + TEXT + ); + + // Update with lowercase + $unicodeLower = 'äöüß'; + $attributeId2 = $this->attribute->saveAttributeValue( + $unicodeLower, + $definitionId, + $itemId, + $attributeId1, + TEXT + ); + + $result = $this->attribute->getAttributeValue($itemId, $definitionId); + // The value should be updated due to case difference + $this->assertNotEquals($unicodeValue, $result->attribute_value, + 'Unicode case should be detected and updated'); + } + + /** + * Test getAttributeValueByAttributeId method + * + * @return void + */ + public function testGetAttributeValueByAttributeId(): void + { + $itemData = [ + 'item_id' => null, + 'name' => 'Test Item', + 'category' => 0, + 'supplier_id' => null, + 'item_number' => 'TEST010', + 'description' => 'Test item', + 'cost_price' => 10.00, + 'unit_price' => 15.00, + 'reorder_level' => 0, + 'receiving_quantity' => 1, + 'allow_alt_description' => NEW_ENTRY, + 'is_serialized' => NEW_ENTRY, + 'deleted' => 0 + ]; + $itemId = $this->item->saveValue($itemData)['item_id']; + + $definitionData = [ + 'definition_name' => 'Quality', + 'definition_type' => TEXT, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + $attributeValue = 'Premium'; + $attributeId = $this->attribute->saveAttributeValue( + $attributeValue, + $definitionId, + $itemId, + false, + TEXT + ); + + // Test getting value by attribute ID + $result = $this->attribute->getAttributeValueByAttributeId($attributeId, TEXT); + + $this->assertNotNull($result); + $this->assertEquals('Premium', $result); + } + + /** + * Test attribute link with null attribute_id + * + * @return void + */ + public function testAttributeLinkWithNullAttributeId(): void + { + $itemData = [ + 'item_id' => null, + 'name' => 'Test Item', + 'category' => 0, + 'supplier_id' => null, + 'item_number' => 'TEST011', + 'description' => 'Test item', + 'cost_price' => 10.00, + 'unit_price' => 15.00, + 'reorder_level' => 0, + 'receiving_quantity' => 1, + 'allow_alt_description' => NEW_ENTRY, + 'is_serialized' => NEW_ENTRY, + 'deleted' => 0 + ]; + $itemId = $this->item->saveValue($itemData)['item_id']; + + $definitionData = [ + 'definition_name' => 'Brand', + 'definition_type' => TEXT, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData); + + // Save attribute link with null attribute_id (should be inserted as null) + $saved = $this->attribute->saveAttributeLink($itemId, $definitionId, null); + + $this->assertTrue($saved, 'saveAttributeLink should succeed with null attribute_id'); + + // Verify it was saved as null + $builder = $this->db->table('attribute_links'); + $builder->where('item_id', $itemId); + $builder->where('definition_id', $definitionId); + $query = $builder->get(); + $result = $query->getRow(); + + $this->assertNull($result->attribute_id, 'attribute_id should be null'); + } + + /** + * Test category dropdown updates item category + * + * @return void + */ + public function testCategoryDropdownUpdatesItemCategory(): void + { + // Create a category + $categoryId = 1; // Assuming category with ID 1 exists + + // Create item with category + $itemData = [ + 'item_id' => null, + 'name' => 'Test Item', + 'category' => $categoryId, + 'supplier_id' => null, + 'item_number' => 'TEST012', + 'description' => 'Test item', + 'cost_price' => 10.00, + 'unit_price' => 15.00, + 'reorder_level' => 0, + 'receiving_quantity' => 1, + 'allow_alt_description' => NEW_ENTRY, + 'is_serialized' => NEW_ENTRY, + 'deleted' => 0 + ]; + $itemId = $this->item->saveValue($itemData)['item_id']; + + // Create category dropdown definition + $definitionData = [ + 'definition_id' => CATEGORY_DEFINITION_ID, + 'definition_name' => 'category_dropdown', + 'definition_type' => DROPDOWN, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + $definitionId = $this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID); + + // Add dropdown value matching category name + $builder = $this->db->table('attribute_values'); + $builder->insert([ + 'attribute_value' => 'Electronics', + 'definition_id' => CATEGORY_DEFINITION_ID, + 'definition_type' => DROPDOWN, + 'attribute_group' => 0, + 'deleted' => 0 + ]); + + // Verify the definition was created with CATEGORY_DEFINITION_ID + $this->assertEquals(CATEGORY_DEFINITION_ID, $definitionId, + 'Category dropdown should use CATEGORY_DEFINITION_ID constant'); + } +} \ No newline at end of file diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 7c20c1a2d..7f75a099d 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -10,6 +10,12 @@ helpers + + Models + + + Controllers +