mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-06 11:09:29 +00:00
Add tests to cover new additions
This commit is contained in:
parent
d0f2422e0d
commit
7c1ab6460d
3 changed files with 847 additions and 0 deletions
4
makefile
4
makefile
|
|
@ -73,6 +73,10 @@ test-run:
|
||||||
@echo "🧪 Running tests..."
|
@echo "🧪 Running tests..."
|
||||||
php bin/phpunit
|
php bin/phpunit
|
||||||
|
|
||||||
|
test-typecheck:
|
||||||
|
@echo "🧪 Running type checks..."
|
||||||
|
COMPOSER_MEMORY_LIMIT=-1 composer phpstan
|
||||||
|
|
||||||
# Quick test reset (clean + migrate + fixtures, skip DB creation)
|
# Quick test reset (clean + migrate + fixtures, skip DB creation)
|
||||||
test-reset: test-cache-clear test-db-migrate test-fixtures
|
test-reset: test-cache-clear test-db-migrate test-fixtures
|
||||||
@echo "✅ Test environment reset complete!"
|
@echo "✅ Test environment reset complete!"
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,12 @@ declare(strict_types=1);
|
||||||
*/
|
*/
|
||||||
namespace App\Tests\Services\ImportExportSystem;
|
namespace App\Tests\Services\ImportExportSystem;
|
||||||
|
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
|
use App\Entity\Parts\Supplier;
|
||||||
use App\Entity\ProjectSystem\Project;
|
use App\Entity\ProjectSystem\Project;
|
||||||
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
use App\Entity\ProjectSystem\ProjectBOMEntry;
|
||||||
use App\Services\ImportExportSystem\BOMImporter;
|
use App\Services\ImportExportSystem\BOMImporter;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
use Symfony\Component\HttpFoundation\File\File;
|
use Symfony\Component\HttpFoundation\File\File;
|
||||||
|
|
||||||
|
|
@ -36,11 +39,17 @@ class BOMImporterTest extends WebTestCase
|
||||||
*/
|
*/
|
||||||
protected $service;
|
protected $service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var EntityManagerInterface
|
||||||
|
*/
|
||||||
|
protected $entityManager;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
//Get a service instance.
|
//Get a service instance.
|
||||||
self::bootKernel();
|
self::bootKernel();
|
||||||
$this->service = self::getContainer()->get(BOMImporter::class);
|
$this->service = self::getContainer()->get(BOMImporter::class);
|
||||||
|
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testImportFileIntoProject(): void
|
public function testImportFileIntoProject(): void
|
||||||
|
|
@ -119,4 +128,489 @@ class BOMImporterTest extends WebTestCase
|
||||||
|
|
||||||
$this->service->stringToBOMEntries($input, ['type' => 'kicad_pcbnew']);
|
$this->service->stringToBOMEntries($input, ['type' => 'kicad_pcbnew']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testDetectFields(): void
|
||||||
|
{
|
||||||
|
$input = <<<CSV
|
||||||
|
"Reference","Value","Footprint","Quantity","MPN","Manufacturer","LCSC SPN","Mouser SPN"
|
||||||
|
CSV;
|
||||||
|
|
||||||
|
$fields = $this->service->detectFields($input);
|
||||||
|
|
||||||
|
$this->assertIsArray($fields);
|
||||||
|
$this->assertCount(8, $fields);
|
||||||
|
$this->assertContains('Reference', $fields);
|
||||||
|
$this->assertContains('Value', $fields);
|
||||||
|
$this->assertContains('Footprint', $fields);
|
||||||
|
$this->assertContains('Quantity', $fields);
|
||||||
|
$this->assertContains('MPN', $fields);
|
||||||
|
$this->assertContains('Manufacturer', $fields);
|
||||||
|
$this->assertContains('LCSC SPN', $fields);
|
||||||
|
$this->assertContains('Mouser SPN', $fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDetectFieldsWithQuotes(): void
|
||||||
|
{
|
||||||
|
$input = <<<CSV
|
||||||
|
"Reference","Value","Footprint","Quantity","MPN","Manufacturer","LCSC SPN","Mouser SPN"
|
||||||
|
CSV;
|
||||||
|
|
||||||
|
$fields = $this->service->detectFields($input);
|
||||||
|
|
||||||
|
$this->assertIsArray($fields);
|
||||||
|
$this->assertCount(8, $fields);
|
||||||
|
$this->assertEquals('Reference', $fields[0]);
|
||||||
|
$this->assertEquals('Value', $fields[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDetectFieldsWithSemicolon(): void
|
||||||
|
{
|
||||||
|
$input = <<<CSV
|
||||||
|
"Reference";"Value";"Footprint";"Quantity";"MPN";"Manufacturer";"LCSC SPN";"Mouser SPN"
|
||||||
|
CSV;
|
||||||
|
|
||||||
|
$fields = $this->service->detectFields($input, ';');
|
||||||
|
|
||||||
|
$this->assertIsArray($fields);
|
||||||
|
$this->assertCount(8, $fields);
|
||||||
|
$this->assertEquals('Reference', $fields[0]);
|
||||||
|
$this->assertEquals('Value', $fields[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAvailableFieldTargets(): void
|
||||||
|
{
|
||||||
|
$targets = $this->service->getAvailableFieldTargets();
|
||||||
|
|
||||||
|
$this->assertIsArray($targets);
|
||||||
|
$this->assertArrayHasKey('Designator', $targets);
|
||||||
|
$this->assertArrayHasKey('Quantity', $targets);
|
||||||
|
$this->assertArrayHasKey('Value', $targets);
|
||||||
|
$this->assertArrayHasKey('Package', $targets);
|
||||||
|
$this->assertArrayHasKey('MPN', $targets);
|
||||||
|
$this->assertArrayHasKey('Manufacturer', $targets);
|
||||||
|
$this->assertArrayHasKey('Part-DB ID', $targets);
|
||||||
|
$this->assertArrayHasKey('Comment', $targets);
|
||||||
|
|
||||||
|
// Check structure of a target
|
||||||
|
$this->assertArrayHasKey('label', $targets['Designator']);
|
||||||
|
$this->assertArrayHasKey('description', $targets['Designator']);
|
||||||
|
$this->assertArrayHasKey('required', $targets['Designator']);
|
||||||
|
$this->assertArrayHasKey('multiple', $targets['Designator']);
|
||||||
|
|
||||||
|
$this->assertTrue($targets['Designator']['required']);
|
||||||
|
$this->assertTrue($targets['Quantity']['required']);
|
||||||
|
$this->assertFalse($targets['Value']['required']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAvailableFieldTargetsWithSuppliers(): void
|
||||||
|
{
|
||||||
|
// Create test suppliers
|
||||||
|
$supplier1 = new Supplier();
|
||||||
|
$supplier1->setName('LCSC');
|
||||||
|
$supplier2 = new Supplier();
|
||||||
|
$supplier2->setName('Mouser');
|
||||||
|
|
||||||
|
$this->entityManager->persist($supplier1);
|
||||||
|
$this->entityManager->persist($supplier2);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$targets = $this->service->getAvailableFieldTargets();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('LCSC SPN', $targets);
|
||||||
|
$this->assertArrayHasKey('Mouser SPN', $targets);
|
||||||
|
|
||||||
|
$this->assertEquals('LCSC SPN', $targets['LCSC SPN']['label']);
|
||||||
|
$this->assertEquals('Mouser SPN', $targets['Mouser SPN']['label']);
|
||||||
|
$this->assertFalse($targets['LCSC SPN']['required']);
|
||||||
|
$this->assertTrue($targets['LCSC SPN']['multiple']);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
$this->entityManager->remove($supplier1);
|
||||||
|
$this->entityManager->remove($supplier2);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSuggestedFieldMapping(): void
|
||||||
|
{
|
||||||
|
$detected_fields = [
|
||||||
|
'Reference',
|
||||||
|
'Value',
|
||||||
|
'Footprint',
|
||||||
|
'Quantity',
|
||||||
|
'MPN',
|
||||||
|
'Manufacturer',
|
||||||
|
'LCSC',
|
||||||
|
'Mouser',
|
||||||
|
'Part-DB ID',
|
||||||
|
'Comment'
|
||||||
|
];
|
||||||
|
|
||||||
|
$suggestions = $this->service->getSuggestedFieldMapping($detected_fields);
|
||||||
|
|
||||||
|
$this->assertIsArray($suggestions);
|
||||||
|
$this->assertEquals('Designator', $suggestions['Reference']);
|
||||||
|
$this->assertEquals('Value', $suggestions['Value']);
|
||||||
|
$this->assertEquals('Package', $suggestions['Footprint']);
|
||||||
|
$this->assertEquals('Quantity', $suggestions['Quantity']);
|
||||||
|
$this->assertEquals('MPN', $suggestions['MPN']);
|
||||||
|
$this->assertEquals('Manufacturer', $suggestions['Manufacturer']);
|
||||||
|
$this->assertEquals('Part-DB ID', $suggestions['Part-DB ID']);
|
||||||
|
$this->assertEquals('Comment', $suggestions['Comment']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSuggestedFieldMappingWithSuppliers(): void
|
||||||
|
{
|
||||||
|
// Create test suppliers
|
||||||
|
$supplier1 = new Supplier();
|
||||||
|
$supplier1->setName('LCSC');
|
||||||
|
$supplier2 = new Supplier();
|
||||||
|
$supplier2->setName('Mouser');
|
||||||
|
|
||||||
|
$this->entityManager->persist($supplier1);
|
||||||
|
$this->entityManager->persist($supplier2);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$detected_fields = [
|
||||||
|
'Reference',
|
||||||
|
'LCSC',
|
||||||
|
'Mouser',
|
||||||
|
'lcsc_part',
|
||||||
|
'mouser_spn'
|
||||||
|
];
|
||||||
|
|
||||||
|
$suggestions = $this->service->getSuggestedFieldMapping($detected_fields);
|
||||||
|
|
||||||
|
$this->assertIsArray($suggestions);
|
||||||
|
$this->assertEquals('Designator', $suggestions['Reference']);
|
||||||
|
// Note: The exact mapping depends on the pattern matching logic
|
||||||
|
// We just check that supplier fields are mapped to something
|
||||||
|
$this->assertArrayHasKey('LCSC', $suggestions);
|
||||||
|
$this->assertArrayHasKey('Mouser', $suggestions);
|
||||||
|
$this->assertArrayHasKey('lcsc_part', $suggestions);
|
||||||
|
$this->assertArrayHasKey('mouser_spn', $suggestions);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
$this->entityManager->remove($supplier1);
|
||||||
|
$this->entityManager->remove($supplier2);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateFieldMappingValid(): void
|
||||||
|
{
|
||||||
|
$field_mapping = [
|
||||||
|
'Reference' => 'Designator',
|
||||||
|
'Quantity' => 'Quantity',
|
||||||
|
'Value' => 'Value'
|
||||||
|
];
|
||||||
|
|
||||||
|
$detected_fields = ['Reference', 'Quantity', 'Value', 'MPN'];
|
||||||
|
|
||||||
|
$result = $this->service->validateFieldMapping($field_mapping, $detected_fields);
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertArrayHasKey('errors', $result);
|
||||||
|
$this->assertArrayHasKey('warnings', $result);
|
||||||
|
$this->assertArrayHasKey('is_valid', $result);
|
||||||
|
|
||||||
|
$this->assertTrue($result['is_valid']);
|
||||||
|
$this->assertEmpty($result['errors']);
|
||||||
|
$this->assertNotEmpty($result['warnings']); // Should warn about unmapped MPN
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateFieldMappingMissingRequired(): void
|
||||||
|
{
|
||||||
|
$field_mapping = [
|
||||||
|
'Value' => 'Value',
|
||||||
|
'MPN' => 'MPN'
|
||||||
|
];
|
||||||
|
|
||||||
|
$detected_fields = ['Value', 'MPN'];
|
||||||
|
|
||||||
|
$result = $this->service->validateFieldMapping($field_mapping, $detected_fields);
|
||||||
|
|
||||||
|
$this->assertFalse($result['is_valid']);
|
||||||
|
$this->assertNotEmpty($result['errors']);
|
||||||
|
$this->assertContains("Required field 'Designator' is not mapped from any CSV column.", $result['errors']);
|
||||||
|
$this->assertContains("Required field 'Quantity' is not mapped from any CSV column.", $result['errors']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateFieldMappingInvalidTarget(): void
|
||||||
|
{
|
||||||
|
$field_mapping = [
|
||||||
|
'Reference' => 'Designator',
|
||||||
|
'Quantity' => 'Quantity',
|
||||||
|
'Value' => 'InvalidTarget'
|
||||||
|
];
|
||||||
|
|
||||||
|
$detected_fields = ['Reference', 'Quantity', 'Value'];
|
||||||
|
|
||||||
|
$result = $this->service->validateFieldMapping($field_mapping, $detected_fields);
|
||||||
|
|
||||||
|
$this->assertFalse($result['is_valid']);
|
||||||
|
$this->assertNotEmpty($result['errors']);
|
||||||
|
$this->assertContains("Invalid target field 'InvalidTarget' for CSV field 'Value'.", $result['errors']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStringToBOMEntriesKiCADSchematic(): void
|
||||||
|
{
|
||||||
|
$input = <<<CSV
|
||||||
|
"Reference","Value","Footprint","Quantity","MPN","Manufacturer","LCSC SPN","Mouser SPN"
|
||||||
|
"R1,R2","10k","R_0805_2012Metric",2,"CRCW080510K0FKEA","Vishay","C123456","123-M10K"
|
||||||
|
"C1","100nF","C_0805_2012Metric",1,"CL21A104KOCLRNC","Samsung","C789012","80-CL21A104KOCLRNC"
|
||||||
|
CSV;
|
||||||
|
|
||||||
|
$field_mapping = [
|
||||||
|
'Reference' => 'Designator',
|
||||||
|
'Value' => 'Value',
|
||||||
|
'Footprint' => 'Package',
|
||||||
|
'Quantity' => 'Quantity',
|
||||||
|
'MPN' => 'MPN',
|
||||||
|
'Manufacturer' => 'Manufacturer',
|
||||||
|
'LCSC SPN' => 'LCSC SPN',
|
||||||
|
'Mouser SPN' => 'Mouser SPN'
|
||||||
|
];
|
||||||
|
|
||||||
|
$bom_entries = $this->service->stringToBOMEntries($input, [
|
||||||
|
'type' => 'kicad_schematic',
|
||||||
|
'field_mapping' => $field_mapping,
|
||||||
|
'delimiter' => ','
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries);
|
||||||
|
$this->assertCount(2, $bom_entries);
|
||||||
|
|
||||||
|
// Check first entry
|
||||||
|
$this->assertEquals('R1,R2', $bom_entries[0]->getMountnames());
|
||||||
|
$this->assertEquals(2.0, $bom_entries[0]->getQuantity());
|
||||||
|
$this->assertEquals('CRCW080510K0FKEA (R_0805_2012Metric)', $bom_entries[0]->getName());
|
||||||
|
$this->assertStringContainsString('Value: 10k', $bom_entries[0]->getComment());
|
||||||
|
$this->assertStringContainsString('MPN: CRCW080510K0FKEA', $bom_entries[0]->getComment());
|
||||||
|
$this->assertStringContainsString('Manf: Vishay', $bom_entries[0]->getComment());
|
||||||
|
|
||||||
|
// Check second entry
|
||||||
|
$this->assertEquals('C1', $bom_entries[1]->getMountnames());
|
||||||
|
$this->assertEquals(1.0, $bom_entries[1]->getQuantity());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStringToBOMEntriesKiCADSchematicWithPriority(): void
|
||||||
|
{
|
||||||
|
$input = <<<CSV
|
||||||
|
"Reference","Value","MPN1","MPN2","Quantity"
|
||||||
|
"R1,R2","10k","CRCW080510K0FKEA","","2"
|
||||||
|
"C1","100nF","","CL21A104KOCLRNC","1"
|
||||||
|
CSV;
|
||||||
|
|
||||||
|
$field_mapping = [
|
||||||
|
'Reference' => 'Designator',
|
||||||
|
'Value' => 'Value',
|
||||||
|
'MPN1' => 'MPN',
|
||||||
|
'MPN2' => 'MPN',
|
||||||
|
'Quantity' => 'Quantity'
|
||||||
|
];
|
||||||
|
|
||||||
|
$field_priorities = [
|
||||||
|
'MPN1' => 1,
|
||||||
|
'MPN2' => 2
|
||||||
|
];
|
||||||
|
|
||||||
|
$bom_entries = $this->service->stringToBOMEntries($input, [
|
||||||
|
'type' => 'kicad_schematic',
|
||||||
|
'field_mapping' => $field_mapping,
|
||||||
|
'field_priorities' => $field_priorities,
|
||||||
|
'delimiter' => ','
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries);
|
||||||
|
$this->assertCount(2, $bom_entries);
|
||||||
|
|
||||||
|
// First entry should use MPN1 (higher priority)
|
||||||
|
$this->assertEquals('CRCW080510K0FKEA', $bom_entries[0]->getName());
|
||||||
|
|
||||||
|
// Second entry should use MPN2 (MPN1 is empty)
|
||||||
|
$this->assertEquals('CL21A104KOCLRNC', $bom_entries[1]->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStringToBOMEntriesKiCADSchematicWithPartDBID(): void
|
||||||
|
{
|
||||||
|
// Create a test part with required fields
|
||||||
|
$part = new Part();
|
||||||
|
$part->setName('Test Part');
|
||||||
|
$part->setCategory($this->getDefaultCategory($this->entityManager));
|
||||||
|
$this->entityManager->persist($part);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$input = <<<CSV
|
||||||
|
"Reference","Value","Part-DB ID","Quantity"
|
||||||
|
"R1,R2","10k","{$part->getID()}","2"
|
||||||
|
CSV;
|
||||||
|
|
||||||
|
$field_mapping = [
|
||||||
|
'Reference' => 'Designator',
|
||||||
|
'Value' => 'Value',
|
||||||
|
'Part-DB ID' => 'Part-DB ID',
|
||||||
|
'Quantity' => 'Quantity'
|
||||||
|
];
|
||||||
|
|
||||||
|
$bom_entries = $this->service->stringToBOMEntries($input, [
|
||||||
|
'type' => 'kicad_schematic',
|
||||||
|
'field_mapping' => $field_mapping,
|
||||||
|
'delimiter' => ','
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries);
|
||||||
|
$this->assertCount(1, $bom_entries);
|
||||||
|
|
||||||
|
$this->assertEquals('Test Part', $bom_entries[0]->getName());
|
||||||
|
$this->assertSame($part, $bom_entries[0]->getPart());
|
||||||
|
$this->assertStringContainsString("Part-DB ID: {$part->getID()}", $bom_entries[0]->getComment());
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
$this->entityManager->remove($part);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStringToBOMEntriesKiCADSchematicWithInvalidPartDBID(): void
|
||||||
|
{
|
||||||
|
$input = <<<CSV
|
||||||
|
"Reference","Value","Part-DB ID","Quantity"
|
||||||
|
"R1,R2","10k","99999","2"
|
||||||
|
CSV;
|
||||||
|
|
||||||
|
$field_mapping = [
|
||||||
|
'Reference' => 'Designator',
|
||||||
|
'Value' => 'Value',
|
||||||
|
'Part-DB ID' => 'Part-DB ID',
|
||||||
|
'Quantity' => 'Quantity'
|
||||||
|
];
|
||||||
|
|
||||||
|
$bom_entries = $this->service->stringToBOMEntries($input, [
|
||||||
|
'type' => 'kicad_schematic',
|
||||||
|
'field_mapping' => $field_mapping,
|
||||||
|
'delimiter' => ','
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries);
|
||||||
|
$this->assertCount(1, $bom_entries);
|
||||||
|
|
||||||
|
$this->assertEquals('10k', $bom_entries[0]->getName()); // Should use Value as name
|
||||||
|
$this->assertNull($bom_entries[0]->getPart()); // Should not link to part
|
||||||
|
$this->assertStringContainsString("Part-DB ID: 99999 (NOT FOUND)", $bom_entries[0]->getComment());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStringToBOMEntriesKiCADSchematicMergeDuplicates(): void
|
||||||
|
{
|
||||||
|
$input = <<<CSV
|
||||||
|
"Reference","Value","MPN","Quantity"
|
||||||
|
"R1","10k","CRCW080510K0FKEA","1"
|
||||||
|
"R2","10k","CRCW080510K0FKEA","1"
|
||||||
|
CSV;
|
||||||
|
|
||||||
|
$field_mapping = [
|
||||||
|
'Reference' => 'Designator',
|
||||||
|
'Value' => 'Value',
|
||||||
|
'MPN' => 'MPN',
|
||||||
|
'Quantity' => 'Quantity'
|
||||||
|
];
|
||||||
|
|
||||||
|
$bom_entries = $this->service->stringToBOMEntries($input, [
|
||||||
|
'type' => 'kicad_schematic',
|
||||||
|
'field_mapping' => $field_mapping,
|
||||||
|
'delimiter' => ','
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries);
|
||||||
|
$this->assertCount(1, $bom_entries); // Should merge into one entry
|
||||||
|
|
||||||
|
$this->assertEquals('R1,R2', $bom_entries[0]->getMountnames());
|
||||||
|
$this->assertEquals(2.0, $bom_entries[0]->getQuantity());
|
||||||
|
$this->assertEquals('CRCW080510K0FKEA', $bom_entries[0]->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStringToBOMEntriesKiCADSchematicMissingRequired(): void
|
||||||
|
{
|
||||||
|
$input = <<<CSV
|
||||||
|
"Value","MPN"
|
||||||
|
"10k","CRCW080510K0FKEA"
|
||||||
|
CSV;
|
||||||
|
|
||||||
|
$field_mapping = [
|
||||||
|
'Value' => 'Value',
|
||||||
|
'MPN' => 'MPN'
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->expectException(\UnexpectedValueException::class);
|
||||||
|
$this->expectExceptionMessage('Required field "Designator" is missing or empty');
|
||||||
|
|
||||||
|
$this->service->stringToBOMEntries($input, [
|
||||||
|
'type' => 'kicad_schematic',
|
||||||
|
'field_mapping' => $field_mapping,
|
||||||
|
'delimiter' => ','
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStringToBOMEntriesKiCADSchematicQuantityMismatch(): void
|
||||||
|
{
|
||||||
|
$input = <<<CSV
|
||||||
|
"Reference","Value","Quantity"
|
||||||
|
"R1,R2,R3","10k","2"
|
||||||
|
CSV;
|
||||||
|
|
||||||
|
$field_mapping = [
|
||||||
|
'Reference' => 'Designator',
|
||||||
|
'Value' => 'Value',
|
||||||
|
'Quantity' => 'Quantity'
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->expectException(\UnexpectedValueException::class);
|
||||||
|
$this->expectExceptionMessage('Mismatch between quantity and component references');
|
||||||
|
|
||||||
|
$this->service->stringToBOMEntries($input, [
|
||||||
|
'type' => 'kicad_schematic',
|
||||||
|
'field_mapping' => $field_mapping,
|
||||||
|
'delimiter' => ','
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStringToBOMEntriesKiCADSchematicWithBOM(): void
|
||||||
|
{
|
||||||
|
// Test with BOM (Byte Order Mark)
|
||||||
|
$input = "\xEF\xBB\xBF" . <<<CSV
|
||||||
|
"Reference","Value","Quantity"
|
||||||
|
"R1,R2","10k","2"
|
||||||
|
CSV;
|
||||||
|
|
||||||
|
$field_mapping = [
|
||||||
|
'Reference' => 'Designator',
|
||||||
|
'Value' => 'Value',
|
||||||
|
'Quantity' => 'Quantity'
|
||||||
|
];
|
||||||
|
|
||||||
|
$bom_entries = $this->service->stringToBOMEntries($input, [
|
||||||
|
'type' => 'kicad_schematic',
|
||||||
|
'field_mapping' => $field_mapping,
|
||||||
|
'delimiter' => ','
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertContainsOnlyInstancesOf(ProjectBOMEntry::class, $bom_entries);
|
||||||
|
$this->assertCount(1, $bom_entries);
|
||||||
|
$this->assertEquals('R1,R2', $bom_entries[0]->getMountnames());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDefaultCategory(EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
// Get the first available category or create a default one
|
||||||
|
$categoryRepo = $entityManager->getRepository(\App\Entity\Parts\Category::class);
|
||||||
|
$categories = $categoryRepo->findAll();
|
||||||
|
|
||||||
|
if (empty($categories)) {
|
||||||
|
// Create a default category if none exists
|
||||||
|
$category = new \App\Entity\Parts\Category();
|
||||||
|
$category->setName('Default Category');
|
||||||
|
$entityManager->persist($category);
|
||||||
|
$entityManager->flush();
|
||||||
|
return $category;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $categories[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
349
tests/Services/ImportExportSystem/BOMValidationServiceTest.php
Normal file
349
tests/Services/ImportExportSystem/BOMValidationServiceTest.php
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
namespace App\Tests\Services\ImportExportSystem;
|
||||||
|
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
|
use App\Services\ImportExportSystem\BOMValidationService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see \App\Services\ImportExportSystem\BOMValidationService
|
||||||
|
*/
|
||||||
|
class BOMValidationServiceTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private BOMValidationService $validationService;
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
private TranslatorInterface $translator;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
$this->translator = self::getContainer()->get(TranslatorInterface::class);
|
||||||
|
$this->validationService = new BOMValidationService($this->entityManager, $this->translator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithValidData(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'Designator' => 'R1,C2,R3',
|
||||||
|
'Quantity' => '3',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
'Package' => '0603',
|
||||||
|
'Value' => '10k',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertTrue($result['is_valid']);
|
||||||
|
$this->assertEmpty($result['errors']);
|
||||||
|
$this->assertEquals(1, $result['line_number']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithMissingRequiredFields(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
'Package' => '0603',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertFalse($result['is_valid']);
|
||||||
|
$this->assertCount(2, $result['errors']);
|
||||||
|
$this->assertStringContainsString('Designator', (string) $result['errors'][0]);
|
||||||
|
$this->assertStringContainsString('Quantity', (string) $result['errors'][1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithQuantityMismatch(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'Designator' => 'R1,C2,R3,C4',
|
||||||
|
'Quantity' => '3',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertFalse($result['is_valid']);
|
||||||
|
$this->assertCount(1, $result['errors']);
|
||||||
|
$this->assertStringContainsString('Mismatch between quantity and component references', (string) $result['errors'][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithInvalidQuantity(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'Designator' => 'R1',
|
||||||
|
'Quantity' => 'abc',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertFalse($result['is_valid']);
|
||||||
|
$this->assertGreaterThanOrEqual(1, count($result['errors']));
|
||||||
|
$this->assertStringContainsString('not a valid number', implode(' ', array_map('strval', $result['errors'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithZeroQuantity(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'Designator' => 'R1',
|
||||||
|
'Quantity' => '0',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertFalse($result['is_valid']);
|
||||||
|
$this->assertGreaterThanOrEqual(1, count($result['errors']));
|
||||||
|
$this->assertStringContainsString('must be greater than 0', implode(' ', array_map('strval', $result['errors'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithDuplicateDesignators(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'Designator' => 'R1,R1,C2',
|
||||||
|
'Quantity' => '3',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertFalse($result['is_valid']);
|
||||||
|
$this->assertCount(1, $result['errors']);
|
||||||
|
$this->assertStringContainsString('Duplicate component references', (string) $result['errors'][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithInvalidDesignatorFormat(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'Designator' => 'R1,invalid,C2',
|
||||||
|
'Quantity' => '3',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertTrue($result['is_valid']); // Warnings don't make it invalid
|
||||||
|
$this->assertCount(1, $result['warnings']);
|
||||||
|
$this->assertStringContainsString('unusual format', (string) $result['warnings'][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithEmptyDesignator(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'Designator' => '',
|
||||||
|
'Quantity' => '1',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertFalse($result['is_valid']);
|
||||||
|
$this->assertGreaterThanOrEqual(1, count($result['errors']));
|
||||||
|
$this->assertStringContainsString('Required field "Designator" is missing or empty', implode(' ', array_map('strval', $result['errors'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithInvalidPartDBID(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'Designator' => 'R1',
|
||||||
|
'Quantity' => '1',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
'Part-DB ID' => 'abc',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertFalse($result['is_valid']);
|
||||||
|
$this->assertGreaterThanOrEqual(1, count($result['errors']));
|
||||||
|
$this->assertStringContainsString('not a valid number', implode(' ', array_map('strval', $result['errors'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithNonExistentPartDBID(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'Designator' => 'R1',
|
||||||
|
'Quantity' => '1',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
'Part-DB ID' => '999999', // Use very high ID that doesn't exist
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertTrue($result['is_valid']); // Warnings don't make it invalid
|
||||||
|
$this->assertCount(1, $result['warnings']);
|
||||||
|
$this->assertStringContainsString('not found in database', (string) $result['warnings'][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithNoComponentName(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'Designator' => 'R1',
|
||||||
|
'Quantity' => '1',
|
||||||
|
'Package' => '0603',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertTrue($result['is_valid']); // Warnings don't make it invalid
|
||||||
|
$this->assertCount(1, $result['warnings']);
|
||||||
|
$this->assertStringContainsString('No component name/designation', (string) $result['warnings'][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithLongPackageName(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'Designator' => 'R1',
|
||||||
|
'Quantity' => '1',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
'Package' => str_repeat('A', 150), // Very long package name
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertTrue($result['is_valid']); // Warnings don't make it invalid
|
||||||
|
$this->assertCount(1, $result['warnings']);
|
||||||
|
$this->assertStringContainsString('unusually long', (string) $result['warnings'][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntryWithLibraryPrefix(): void
|
||||||
|
{
|
||||||
|
$entry = [
|
||||||
|
'Designator' => 'R1',
|
||||||
|
'Quantity' => '1',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
'Package' => 'Resistor_SMD:R_0603_1608Metric',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntry($entry, 1);
|
||||||
|
|
||||||
|
$this->assertTrue($result['is_valid']);
|
||||||
|
$this->assertCount(1, $result['info']);
|
||||||
|
$this->assertStringContainsString('library prefix', $result['info'][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntriesWithMultipleEntries(): void
|
||||||
|
{
|
||||||
|
$entries = [
|
||||||
|
[
|
||||||
|
'Designator' => 'R1',
|
||||||
|
'Quantity' => '1',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Designator' => 'C1,C2',
|
||||||
|
'Quantity' => '2',
|
||||||
|
'MPN' => 'CAP-100nF',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntries($entries);
|
||||||
|
|
||||||
|
$this->assertTrue($result['is_valid']);
|
||||||
|
$this->assertEquals(2, $result['total_entries']);
|
||||||
|
$this->assertEquals(2, $result['valid_entries']);
|
||||||
|
$this->assertEquals(0, $result['invalid_entries']);
|
||||||
|
$this->assertCount(2, $result['line_results']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateBOMEntriesWithMixedResults(): void
|
||||||
|
{
|
||||||
|
$entries = [
|
||||||
|
[
|
||||||
|
'Designator' => 'R1',
|
||||||
|
'Quantity' => '1',
|
||||||
|
'MPN' => 'RES-10K',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Designator' => 'C1,C2',
|
||||||
|
'Quantity' => '1', // Mismatch
|
||||||
|
'MPN' => 'CAP-100nF',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->validationService->validateBOMEntries($entries);
|
||||||
|
|
||||||
|
$this->assertFalse($result['is_valid']);
|
||||||
|
$this->assertEquals(2, $result['total_entries']);
|
||||||
|
$this->assertEquals(1, $result['valid_entries']);
|
||||||
|
$this->assertEquals(1, $result['invalid_entries']);
|
||||||
|
$this->assertCount(1, $result['errors']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetValidationStats(): void
|
||||||
|
{
|
||||||
|
$validation_result = [
|
||||||
|
'total_entries' => 10,
|
||||||
|
'valid_entries' => 8,
|
||||||
|
'invalid_entries' => 2,
|
||||||
|
'errors' => ['Error 1', 'Error 2'],
|
||||||
|
'warnings' => ['Warning 1'],
|
||||||
|
'info' => ['Info 1', 'Info 2'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$stats = $this->validationService->getValidationStats($validation_result);
|
||||||
|
|
||||||
|
$this->assertEquals(10, $stats['total_entries']);
|
||||||
|
$this->assertEquals(8, $stats['valid_entries']);
|
||||||
|
$this->assertEquals(2, $stats['invalid_entries']);
|
||||||
|
$this->assertEquals(2, $stats['error_count']);
|
||||||
|
$this->assertEquals(1, $stats['warning_count']);
|
||||||
|
$this->assertEquals(2, $stats['info_count']);
|
||||||
|
$this->assertEquals(80.0, $stats['success_rate']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetErrorMessage(): void
|
||||||
|
{
|
||||||
|
$validation_result = [
|
||||||
|
'is_valid' => false,
|
||||||
|
'errors' => ['Error 1', 'Error 2'],
|
||||||
|
'warnings' => ['Warning 1'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$message = $this->validationService->getErrorMessage($validation_result);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('Errors:', $message);
|
||||||
|
$this->assertStringContainsString('• Error 1', $message);
|
||||||
|
$this->assertStringContainsString('• Error 2', $message);
|
||||||
|
$this->assertStringContainsString('Warnings:', $message);
|
||||||
|
$this->assertStringContainsString('• Warning 1', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetErrorMessageWithValidResult(): void
|
||||||
|
{
|
||||||
|
$validation_result = [
|
||||||
|
'is_valid' => true,
|
||||||
|
'errors' => [],
|
||||||
|
'warnings' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$message = $this->validationService->getErrorMessage($validation_result);
|
||||||
|
|
||||||
|
$this->assertEquals('', $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue