mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-03-01 21:09:35 +00:00
Merge 4427ba8ca6 into 1650ade338
This commit is contained in:
commit
123399ef64
30 changed files with 3582 additions and 67 deletions
478
tests/Command/PopulateKicadCommandTest.php
Normal file
478
tests/Command/PopulateKicadCommandTest.php
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Command;
|
||||
|
||||
use App\Command\PopulateKicadCommand;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
final class PopulateKicadCommandTest extends KernelTestCase
|
||||
{
|
||||
private CommandTester $commandTester;
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$application = new Application(self::$kernel);
|
||||
|
||||
$command = $application->find('partdb:kicad:populate');
|
||||
$this->commandTester = new CommandTester($command);
|
||||
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
||||
}
|
||||
|
||||
public function testListOption(): void
|
||||
{
|
||||
$this->commandTester->execute(['--list' => true]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
// Should show footprints and categories tables
|
||||
$this->assertStringContainsString('Current Footprint KiCad Values', $output);
|
||||
$this->assertStringContainsString('Current Category KiCad Values', $output);
|
||||
$this->assertStringContainsString('ID', $output);
|
||||
$this->assertStringContainsString('Name', $output);
|
||||
|
||||
$this->assertEquals(0, $this->commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDryRunDoesNotModifyDatabase(): void
|
||||
{
|
||||
// Create a test footprint without KiCad value
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('SOT-23');
|
||||
$this->entityManager->persist($footprint);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
|
||||
// Run in dry-run mode
|
||||
$this->commandTester->execute(['--dry-run' => true, '--footprints' => true]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('DRY RUN MODE', $output);
|
||||
$this->assertStringContainsString('SOT-23', $output);
|
||||
|
||||
// Clear entity manager to force reload from DB
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Verify footprint was NOT updated in the database
|
||||
$reloadedFootprint = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertNull($reloadedFootprint->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloadedFootprint);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testFootprintMappingUpdatesCorrectly(): void
|
||||
{
|
||||
// Create test footprints
|
||||
$footprint1 = new Footprint();
|
||||
$footprint1->setName('SOT-23');
|
||||
|
||||
$footprint2 = new Footprint();
|
||||
$footprint2->setName('0805');
|
||||
|
||||
$footprint3 = new Footprint();
|
||||
$footprint3->setName('DIP-8');
|
||||
|
||||
$this->entityManager->persist($footprint1);
|
||||
$this->entityManager->persist($footprint2);
|
||||
$this->entityManager->persist($footprint3);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$ids = [$footprint1->getId(), $footprint2->getId(), $footprint3->getId()];
|
||||
|
||||
// Run the command
|
||||
$this->commandTester->execute(['--footprints' => true]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals(0, $this->commandTester->getStatusCode());
|
||||
|
||||
// Clear and reload
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Verify mappings were applied
|
||||
$reloaded1 = $this->entityManager->find(Footprint::class, $ids[0]);
|
||||
$this->assertEquals('Package_TO_SOT_SMD:SOT-23', $reloaded1->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
$reloaded2 = $this->entityManager->find(Footprint::class, $ids[1]);
|
||||
$this->assertEquals('Resistor_SMD:R_0805_2012Metric', $reloaded2->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
$reloaded3 = $this->entityManager->find(Footprint::class, $ids[2]);
|
||||
$this->assertEquals('Package_DIP:DIP-8_W7.62mm', $reloaded3->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded1);
|
||||
$this->entityManager->remove($reloaded2);
|
||||
$this->entityManager->remove($reloaded3);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testSkipsExistingValuesWithoutForce(): void
|
||||
{
|
||||
// Create footprint with existing value
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('SOT-23');
|
||||
$footprint->getEdaInfo()->setKicadFootprint('Custom:MyFootprint');
|
||||
$this->entityManager->persist($footprint);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
|
||||
// Run without --force
|
||||
$this->commandTester->execute(['--footprints' => true]);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Should keep original value
|
||||
$reloaded = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertEquals('Custom:MyFootprint', $reloaded->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testForceOptionOverwritesExistingValues(): void
|
||||
{
|
||||
// Create footprint with existing value
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('SOT-23');
|
||||
$footprint->getEdaInfo()->setKicadFootprint('Custom:MyFootprint');
|
||||
$this->entityManager->persist($footprint);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
|
||||
// Run with --force
|
||||
$this->commandTester->execute(['--footprints' => true, '--force' => true]);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Should overwrite with mapped value
|
||||
$reloaded = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertEquals('Package_TO_SOT_SMD:SOT-23', $reloaded->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testCategoryMappingUpdatesCorrectly(): void
|
||||
{
|
||||
// Create test categories
|
||||
$category1 = new Category();
|
||||
$category1->setName('Resistors');
|
||||
|
||||
$category2 = new Category();
|
||||
$category2->setName('LED Indicators');
|
||||
|
||||
$category3 = new Category();
|
||||
$category3->setName('Zener Diodes');
|
||||
|
||||
$this->entityManager->persist($category1);
|
||||
$this->entityManager->persist($category2);
|
||||
$this->entityManager->persist($category3);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$ids = [$category1->getId(), $category2->getId(), $category3->getId()];
|
||||
|
||||
// Run the command
|
||||
$this->commandTester->execute(['--categories' => true]);
|
||||
|
||||
$this->assertEquals(0, $this->commandTester->getStatusCode());
|
||||
|
||||
// Clear and reload
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Verify mappings were applied (using pattern matching)
|
||||
$reloaded1 = $this->entityManager->find(Category::class, $ids[0]);
|
||||
$this->assertEquals('Device:R', $reloaded1->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
$reloaded2 = $this->entityManager->find(Category::class, $ids[1]);
|
||||
$this->assertEquals('Device:LED', $reloaded2->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
$reloaded3 = $this->entityManager->find(Category::class, $ids[2]);
|
||||
$this->assertEquals('Device:D_Zener', $reloaded3->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded1);
|
||||
$this->entityManager->remove($reloaded2);
|
||||
$this->entityManager->remove($reloaded3);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testUnmappedFootprintsAreListed(): void
|
||||
{
|
||||
// Create footprint with no mapping
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('CustomPackage-XYZ');
|
||||
$this->entityManager->persist($footprint);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
|
||||
// Run the command
|
||||
$this->commandTester->execute(['--footprints' => true]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
// Should list the unmapped footprint
|
||||
$this->assertStringContainsString('No mapping found', $output);
|
||||
$this->assertStringContainsString('CustomPackage-XYZ', $output);
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->clear();
|
||||
$reloaded = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testMappingFileOverridesDefaults(): void
|
||||
{
|
||||
// Create a footprint that has a built-in mapping (SOT-23 -> Package_TO_SOT_SMD:SOT-23)
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('SOT-23');
|
||||
$this->entityManager->persist($footprint);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
|
||||
// Create a temporary JSON mapping file that overrides SOT-23
|
||||
$mappingFile = sys_get_temp_dir() . '/partdb_test_mappings_' . uniqid() . '.json';
|
||||
file_put_contents($mappingFile, json_encode([
|
||||
'footprints' => [
|
||||
'SOT-23' => 'Custom_Library:Custom_SOT-23',
|
||||
],
|
||||
]));
|
||||
|
||||
try {
|
||||
// Run with mapping file
|
||||
$this->commandTester->execute(['--footprints' => true, '--mapping-file' => $mappingFile]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals(0, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString('custom footprint mappings', $output);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Should use the custom mapping, not the built-in one
|
||||
$reloaded = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertEquals('Custom_Library:Custom_SOT-23', $reloaded->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
} finally {
|
||||
@unlink($mappingFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testMappingFileInvalidJsonReturnsFailure(): void
|
||||
{
|
||||
$mappingFile = sys_get_temp_dir() . '/partdb_test_invalid_' . uniqid() . '.json';
|
||||
file_put_contents($mappingFile, 'not valid json{{{');
|
||||
|
||||
try {
|
||||
$this->commandTester->execute(['--mapping-file' => $mappingFile]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals(1, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString('Invalid JSON', $output);
|
||||
} finally {
|
||||
@unlink($mappingFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testMappingFileNotFoundReturnsFailure(): void
|
||||
{
|
||||
$this->commandTester->execute(['--mapping-file' => '/nonexistent/path/mappings.json']);
|
||||
|
||||
$this->assertEquals(1, $this->commandTester->getStatusCode());
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Mapping file not found', $output);
|
||||
}
|
||||
|
||||
public function testFootprintAlternativeNameMatching(): void
|
||||
{
|
||||
// Create a footprint with a primary name that has no mapping,
|
||||
// but an alternative name that does
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('MyCustomSOT23');
|
||||
$footprint->setAlternativeNames('SOT-23, SOT23-3L');
|
||||
$this->entityManager->persist($footprint);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
|
||||
$this->commandTester->execute(['--footprints' => true]);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Should match via alternative name "SOT-23"
|
||||
$reloaded = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertEquals('Package_TO_SOT_SMD:SOT-23', $reloaded->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testCategoryAlternativeNameMatching(): void
|
||||
{
|
||||
// Create a category with a primary name that has no mapping,
|
||||
// but an alternative name that matches a pattern
|
||||
$category = new Category();
|
||||
$category->setName('SMD Components');
|
||||
$category->setAlternativeNames('Resistor SMD, Chip Resistors');
|
||||
$this->entityManager->persist($category);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$categoryId = $category->getId();
|
||||
|
||||
$this->commandTester->execute(['--categories' => true]);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Should match via alternative name "Resistor SMD" matching pattern "Resistor"
|
||||
$reloaded = $this->entityManager->find(Category::class, $categoryId);
|
||||
$this->assertEquals('Device:R', $reloaded->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testBothFootprintsAndCategoriesUpdatedByDefault(): void
|
||||
{
|
||||
// Create one of each
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('TO-220');
|
||||
$this->entityManager->persist($footprint);
|
||||
|
||||
$category = new Category();
|
||||
$category->setName('Capacitors');
|
||||
$this->entityManager->persist($category);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
$categoryId = $category->getId();
|
||||
|
||||
// Run without specific options (should do both)
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Updating Footprint Entities', $output);
|
||||
$this->assertStringContainsString('Updating Category Entities', $output);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
// Both should be updated
|
||||
$reloadedFootprint = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertEquals('Package_TO_SOT_THT:TO-220-3_Vertical', $reloadedFootprint->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
$reloadedCategory = $this->entityManager->find(Category::class, $categoryId);
|
||||
$this->assertEquals('Device:C', $reloadedCategory->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloadedFootprint);
|
||||
$this->entityManager->remove($reloadedCategory);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function testMappingFileWithBothFootprintsAndCategories(): void
|
||||
{
|
||||
$footprint = new Footprint();
|
||||
$footprint->setName('CustomPkg');
|
||||
$this->entityManager->persist($footprint);
|
||||
|
||||
$category = new Category();
|
||||
$category->setName('CustomType');
|
||||
$this->entityManager->persist($category);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$footprintId = $footprint->getId();
|
||||
$categoryId = $category->getId();
|
||||
|
||||
$mappingFile = sys_get_temp_dir() . '/partdb_test_both_' . uniqid() . '.json';
|
||||
file_put_contents($mappingFile, json_encode([
|
||||
'footprints' => [
|
||||
'CustomPkg' => 'Custom:Footprint',
|
||||
],
|
||||
'categories' => [
|
||||
'CustomType' => 'Custom:Symbol',
|
||||
],
|
||||
]));
|
||||
|
||||
try {
|
||||
$this->commandTester->execute(['--mapping-file' => $mappingFile]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals(0, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString('custom footprint mappings', $output);
|
||||
$this->assertStringContainsString('custom category mappings', $output);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
$reloadedFp = $this->entityManager->find(Footprint::class, $footprintId);
|
||||
$this->assertEquals('Custom:Footprint', $reloadedFp->getEdaInfo()->getKicadFootprint());
|
||||
|
||||
$reloadedCat = $this->entityManager->find(Category::class, $categoryId);
|
||||
$this->assertEquals('Custom:Symbol', $reloadedCat->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
// Cleanup
|
||||
$this->entityManager->remove($reloadedFp);
|
||||
$this->entityManager->remove($reloadedCat);
|
||||
$this->entityManager->flush();
|
||||
} finally {
|
||||
@unlink($mappingFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testMappingFileWithOnlyCategoriesSection(): void
|
||||
{
|
||||
$category = new Category();
|
||||
$category->setName('OnlyCatType');
|
||||
$this->entityManager->persist($category);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$categoryId = $category->getId();
|
||||
|
||||
$mappingFile = sys_get_temp_dir() . '/partdb_test_catonly_' . uniqid() . '.json';
|
||||
file_put_contents($mappingFile, json_encode([
|
||||
'categories' => [
|
||||
'OnlyCatType' => 'Custom:CatSymbol',
|
||||
],
|
||||
]));
|
||||
|
||||
try {
|
||||
$this->commandTester->execute(['--categories' => true, '--mapping-file' => $mappingFile]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals(0, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString('custom category mappings', $output);
|
||||
// Should NOT mention footprint mappings since they weren't in the file
|
||||
$this->assertStringNotContainsString('custom footprint mappings', $output);
|
||||
|
||||
$this->entityManager->clear();
|
||||
|
||||
$reloaded = $this->entityManager->find(Category::class, $categoryId);
|
||||
$this->assertEquals('Custom:CatSymbol', $reloaded->getEdaInfo()->getKicadSymbol());
|
||||
|
||||
$this->entityManager->remove($reloaded);
|
||||
$this->entityManager->flush();
|
||||
} finally {
|
||||
@unlink($mappingFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
171
tests/Controller/BatchEdaControllerTest.php
Normal file
171
tests/Controller/BatchEdaControllerTest.php
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\UserSystem\User;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
#[Group("slow")]
|
||||
#[Group("DB")]
|
||||
final class BatchEdaControllerTest extends WebTestCase
|
||||
{
|
||||
private function loginAsUser($client, string $username): void
|
||||
{
|
||||
$entityManager = $client->getContainer()->get('doctrine')->getManager();
|
||||
$userRepository = $entityManager->getRepository(User::class);
|
||||
$user = $userRepository->findOneBy(['name' => $username]);
|
||||
|
||||
if (!$user) {
|
||||
$this->markTestSkipped("User {$username} not found");
|
||||
}
|
||||
|
||||
$client->loginUser($user);
|
||||
}
|
||||
|
||||
public function testBatchEdaPageLoads(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$client->request('GET', '/en/tools/batch_eda_edit', ['ids' => '1,2,3']);
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testBatchEdaPageWithoutPartsRedirects(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$client->request('GET', '/en/tools/batch_eda_edit');
|
||||
|
||||
self::assertResponseRedirects();
|
||||
}
|
||||
|
||||
public function testBatchEdaPageWithoutPartsRedirectsToCustomUrl(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
// Empty IDs with a custom redirect URL
|
||||
$client->request('GET', '/en/tools/batch_eda_edit', [
|
||||
'ids' => '',
|
||||
'_redirect' => '/en/parts',
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/en/parts');
|
||||
}
|
||||
|
||||
public function testBatchEdaFormSubmission(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$crawler = $client->request('GET', '/en/tools/batch_eda_edit', ['ids' => '1,2']);
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('batch_eda[submit]')->form();
|
||||
$form['batch_eda[apply_reference_prefix]'] = true;
|
||||
$form['batch_eda[reference_prefix]'] = 'R';
|
||||
|
||||
$client->submit($form);
|
||||
|
||||
self::assertResponseRedirects();
|
||||
}
|
||||
|
||||
public function testBatchEdaFormSubmissionAppliesAllFields(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$crawler = $client->request('GET', '/en/tools/batch_eda_edit', ['ids' => '1,2']);
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('batch_eda[submit]')->form();
|
||||
|
||||
// Apply all text fields
|
||||
$form['batch_eda[apply_reference_prefix]'] = true;
|
||||
$form['batch_eda[reference_prefix]'] = 'C';
|
||||
$form['batch_eda[apply_value]'] = true;
|
||||
$form['batch_eda[value]'] = '100nF';
|
||||
$form['batch_eda[apply_kicad_symbol]'] = true;
|
||||
$form['batch_eda[kicad_symbol]'] = 'Device:C';
|
||||
$form['batch_eda[apply_kicad_footprint]'] = true;
|
||||
$form['batch_eda[kicad_footprint]'] = 'Capacitor_SMD:C_0402';
|
||||
|
||||
// Apply all tri-state checkboxes
|
||||
$form['batch_eda[apply_visibility]'] = true;
|
||||
$form['batch_eda[apply_exclude_from_bom]'] = true;
|
||||
$form['batch_eda[apply_exclude_from_board]'] = true;
|
||||
$form['batch_eda[apply_exclude_from_sim]'] = true;
|
||||
|
||||
$client->submit($form);
|
||||
|
||||
// All field branches in the controller are now exercised; redirect confirms success
|
||||
self::assertResponseRedirects();
|
||||
}
|
||||
|
||||
public function testBatchEdaFormSubmissionWithRedirectUrl(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$crawler = $client->request('GET', '/en/tools/batch_eda_edit', [
|
||||
'ids' => '1',
|
||||
'_redirect' => '/en/parts',
|
||||
]);
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('batch_eda[submit]')->form();
|
||||
$form['batch_eda[apply_reference_prefix]'] = true;
|
||||
$form['batch_eda[reference_prefix]'] = 'U';
|
||||
|
||||
$client->submit($form);
|
||||
|
||||
// Should redirect to the custom URL, not the default route
|
||||
self::assertResponseRedirects('/en/parts');
|
||||
}
|
||||
|
||||
public function testBatchEdaFormWithPartialFields(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$this->loginAsUser($client, 'admin');
|
||||
|
||||
$crawler = $client->request('GET', '/en/tools/batch_eda_edit', ['ids' => '3']);
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('batch_eda[submit]')->form();
|
||||
// Only apply value and kicad_footprint, leave other apply checkboxes unchecked
|
||||
$form['batch_eda[apply_value]'] = true;
|
||||
$form['batch_eda[value]'] = 'TestValue';
|
||||
$form['batch_eda[apply_kicad_footprint]'] = true;
|
||||
$form['batch_eda[kicad_footprint]'] = 'Package_SO:SOIC-8';
|
||||
|
||||
$client->submit($form);
|
||||
|
||||
// Redirect confirms the partial submission was processed
|
||||
self::assertResponseRedirects();
|
||||
}
|
||||
}
|
||||
|
|
@ -148,6 +148,11 @@ final class KiCadApiControllerTest extends WebTestCase
|
|||
'value' => 'http://localhost/en/part/1/info',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Part-DB URL' =>
|
||||
array(
|
||||
'value' => 'http://localhost/en/part/1/info',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'description' =>
|
||||
array(
|
||||
'value' => '',
|
||||
|
|
@ -168,6 +173,11 @@ final class KiCadApiControllerTest extends WebTestCase
|
|||
'value' => '1',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Stock' =>
|
||||
array(
|
||||
'value' => '0',
|
||||
'visible' => 'False',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -177,20 +187,19 @@ final class KiCadApiControllerTest extends WebTestCase
|
|||
public function testPartDetailsPart2(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/parts/1.json');
|
||||
$client->request('GET', self::BASE_URL.'/parts/2.json');
|
||||
|
||||
//Response should still be successful, but the result should be empty
|
||||
self::assertResponseIsSuccessful();
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertJson($content);
|
||||
|
||||
$data = json_decode($content, true);
|
||||
|
||||
//For part 2 things info should be taken from the category and footprint
|
||||
//For part 2, EDA info should be inherited from category and footprint (no part-level overrides)
|
||||
$expected = array (
|
||||
'id' => '1',
|
||||
'name' => 'Part 1',
|
||||
'symbolIdStr' => 'Part:1',
|
||||
'id' => '2',
|
||||
'name' => 'Part 2',
|
||||
'symbolIdStr' => 'Category:1',
|
||||
'exclude_from_bom' => 'False',
|
||||
'exclude_from_board' => 'True',
|
||||
'exclude_from_sim' => 'False',
|
||||
|
|
@ -198,27 +207,32 @@ final class KiCadApiControllerTest extends WebTestCase
|
|||
array (
|
||||
'footprint' =>
|
||||
array (
|
||||
'value' => 'Part:1',
|
||||
'value' => 'Footprint:1',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'reference' =>
|
||||
array (
|
||||
'value' => 'P',
|
||||
'value' => 'C',
|
||||
'visible' => 'True',
|
||||
),
|
||||
'value' =>
|
||||
array (
|
||||
'value' => 'Part 1',
|
||||
'value' => 'Part 2',
|
||||
'visible' => 'True',
|
||||
),
|
||||
'keywords' =>
|
||||
array (
|
||||
'value' => '',
|
||||
'value' => 'test, Test, Part2',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'datasheet' =>
|
||||
array (
|
||||
'value' => 'http://localhost/en/part/1/info',
|
||||
'value' => 'http://localhost/en/part/2/info',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Part-DB URL' =>
|
||||
array (
|
||||
'value' => 'http://localhost/en/part/2/info',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'description' =>
|
||||
|
|
@ -231,14 +245,44 @@ final class KiCadApiControllerTest extends WebTestCase
|
|||
'value' => 'Node 1',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Manufacturer' =>
|
||||
array (
|
||||
'value' => 'Node 1',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Manufacturing Status' =>
|
||||
array (
|
||||
'value' => '',
|
||||
'value' => 'Active',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Part-DB Footprint' =>
|
||||
array (
|
||||
'value' => 'Node 1',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Mass' =>
|
||||
array (
|
||||
'value' => '100.2 g',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Part-DB ID' =>
|
||||
array (
|
||||
'value' => '1',
|
||||
'value' => '2',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Part-DB IPN' =>
|
||||
array (
|
||||
'value' => 'IPN123',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'manf' =>
|
||||
array (
|
||||
'value' => 'Node 1',
|
||||
'visible' => 'False',
|
||||
),
|
||||
'Stock' =>
|
||||
array (
|
||||
'value' => '0',
|
||||
'visible' => 'False',
|
||||
),
|
||||
),
|
||||
|
|
@ -247,4 +291,31 @@ final class KiCadApiControllerTest extends WebTestCase
|
|||
self::assertEquals($expected, $data);
|
||||
}
|
||||
|
||||
public function testCategoriesHasCacheHeaders(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/categories.json');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
$response = $client->getResponse();
|
||||
self::assertNotNull($response->headers->get('ETag'));
|
||||
self::assertStringContainsString('max-age=', $response->headers->get('Cache-Control'));
|
||||
}
|
||||
|
||||
public function testConditionalRequestReturns304(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/categories.json');
|
||||
|
||||
$etag = $client->getResponse()->headers->get('ETag');
|
||||
self::assertNotNull($etag);
|
||||
|
||||
//Make a conditional request with the ETag
|
||||
$client->request('GET', self::BASE_URL.'/categories.json', [], [], [
|
||||
'HTTP_IF_NONE_MATCH' => $etag,
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(304);
|
||||
}
|
||||
|
||||
}
|
||||
180
tests/Controller/KiCadApiV2ControllerTest.php
Normal file
180
tests/Controller/KiCadApiV2ControllerTest.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\DataFixtures\APITokenFixtures;
|
||||
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
final class KiCadApiV2ControllerTest extends WebTestCase
|
||||
{
|
||||
private const BASE_URL = '/en/kicad-api/v2';
|
||||
|
||||
protected function createClientWithCredentials(string $token = APITokenFixtures::TOKEN_READONLY): KernelBrowser
|
||||
{
|
||||
return static::createClient([], ['headers' => ['authorization' => 'Bearer '.$token]]);
|
||||
}
|
||||
|
||||
public function testRootReturnsEndpointLinks(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertJson($content);
|
||||
|
||||
$array = json_decode($content, true);
|
||||
self::assertArrayHasKey('categories', $array);
|
||||
self::assertArrayHasKey('parts', $array);
|
||||
|
||||
// Root endpoint should return link to categories endpoint
|
||||
self::assertStringContainsString('categories.json', $array['categories']);
|
||||
}
|
||||
|
||||
public function testCategories(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/categories.json');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertJson($content);
|
||||
|
||||
$data = json_decode($content, true);
|
||||
self::assertCount(1, $data);
|
||||
|
||||
$category = $data[0];
|
||||
self::assertArrayHasKey('name', $category);
|
||||
self::assertArrayHasKey('id', $category);
|
||||
}
|
||||
|
||||
public function testCategoryParts(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/parts/category/1.json');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertJson($content);
|
||||
|
||||
$data = json_decode($content, true);
|
||||
self::assertCount(3, $data);
|
||||
|
||||
$part = $data[0];
|
||||
self::assertArrayHasKey('name', $part);
|
||||
self::assertArrayHasKey('id', $part);
|
||||
self::assertArrayHasKey('description', $part);
|
||||
}
|
||||
|
||||
public function testCategoryPartsMinimal(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/parts/category/1.json?minimal=true');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertJson($content);
|
||||
|
||||
$data = json_decode($content, true);
|
||||
self::assertCount(3, $data);
|
||||
}
|
||||
|
||||
public function testPartDetailsHasVolatileFields(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/parts/1.json');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertJson($content);
|
||||
|
||||
$data = json_decode($content, true);
|
||||
|
||||
// V2 should have volatile flag on Stock field
|
||||
self::assertArrayHasKey('fields', $data);
|
||||
self::assertArrayHasKey('Stock', $data['fields']);
|
||||
self::assertArrayHasKey('volatile', $data['fields']['Stock']);
|
||||
self::assertEquals('True', $data['fields']['Stock']['volatile']);
|
||||
}
|
||||
|
||||
public function testPartDetailsV2VsV1Difference(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
|
||||
// Get v1 response
|
||||
$client->request('GET', '/en/kicad-api/v1/parts/1.json');
|
||||
self::assertResponseIsSuccessful();
|
||||
$v1Data = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
// Get v2 response
|
||||
$client->request('GET', self::BASE_URL.'/parts/1.json');
|
||||
self::assertResponseIsSuccessful();
|
||||
$v2Data = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
// V1 should NOT have volatile on Stock
|
||||
self::assertArrayNotHasKey('volatile', $v1Data['fields']['Stock']);
|
||||
|
||||
// V2 should have volatile on Stock
|
||||
self::assertArrayHasKey('volatile', $v2Data['fields']['Stock']);
|
||||
|
||||
// Both should have the same stock value
|
||||
self::assertEquals($v1Data['fields']['Stock']['value'], $v2Data['fields']['Stock']['value']);
|
||||
}
|
||||
|
||||
public function testCategoriesHasCacheHeaders(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/categories.json');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
$response = $client->getResponse();
|
||||
self::assertNotNull($response->headers->get('ETag'));
|
||||
self::assertStringContainsString('max-age=', $response->headers->get('Cache-Control'));
|
||||
}
|
||||
|
||||
public function testConditionalRequestReturns304(): void
|
||||
{
|
||||
$client = $this->createClientWithCredentials();
|
||||
$client->request('GET', self::BASE_URL.'/categories.json');
|
||||
|
||||
$etag = $client->getResponse()->headers->get('ETag');
|
||||
self::assertNotNull($etag);
|
||||
|
||||
$client->request('GET', self::BASE_URL.'/categories.json', [], [], [
|
||||
'HTTP_IF_NONE_MATCH' => $etag,
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(304);
|
||||
}
|
||||
|
||||
public function testUnauthenticatedAccessDenied(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', self::BASE_URL.'/categories.json');
|
||||
|
||||
// Anonymous user has default read permissions in Part-DB,
|
||||
// so this returns 200 rather than a redirect
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
||||
|
|
@ -136,4 +136,44 @@ final class PartNormalizerTest extends WebTestCase
|
|||
$this->assertEqualsWithDelta(1.0, $priceDetail->getPriceRelatedQuantity(), PHP_FLOAT_EPSILON);
|
||||
$this->assertEqualsWithDelta(1.0, $priceDetail->getMinDiscountQuantity(), PHP_FLOAT_EPSILON);
|
||||
}
|
||||
|
||||
public function testDenormalizeEdaFields(): void
|
||||
{
|
||||
$input = [
|
||||
'name' => 'EDA Test Part',
|
||||
'kicad_symbol' => 'Device:R',
|
||||
'kicad_footprint' => 'Resistor_SMD:R_0805_2012Metric',
|
||||
'kicad_reference' => 'R',
|
||||
'kicad_value' => '10k',
|
||||
'eda_exclude_bom' => 'true',
|
||||
'eda_exclude_board' => 'false',
|
||||
];
|
||||
|
||||
$part = $this->service->denormalize($input, Part::class, 'json', ['groups' => ['import'], 'partdb_import' => true]);
|
||||
$this->assertInstanceOf(Part::class, $part);
|
||||
$this->assertSame('EDA Test Part', $part->getName());
|
||||
|
||||
$edaInfo = $part->getEdaInfo();
|
||||
$this->assertSame('Device:R', $edaInfo->getKicadSymbol());
|
||||
$this->assertSame('Resistor_SMD:R_0805_2012Metric', $edaInfo->getKicadFootprint());
|
||||
$this->assertSame('R', $edaInfo->getReferencePrefix());
|
||||
$this->assertSame('10k', $edaInfo->getValue());
|
||||
$this->assertTrue($edaInfo->getExcludeFromBom());
|
||||
$this->assertFalse($edaInfo->getExcludeFromBoard());
|
||||
}
|
||||
|
||||
public function testDenormalizeEdaFieldsEmptyValuesIgnored(): void
|
||||
{
|
||||
$input = [
|
||||
'name' => 'Part Without EDA',
|
||||
'kicad_symbol' => '',
|
||||
'kicad_footprint' => '',
|
||||
];
|
||||
|
||||
$part = $this->service->denormalize($input, Part::class, 'json', ['groups' => ['import'], 'partdb_import' => true]);
|
||||
|
||||
$edaInfo = $part->getEdaInfo();
|
||||
$this->assertNull($edaInfo->getKicadSymbol());
|
||||
$this->assertNull($edaInfo->getKicadFootprint());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
604
tests/Services/EDA/KiCadHelperTest.php
Normal file
604
tests/Services/EDA/KiCadHelperTest.php
Normal file
|
|
@ -0,0 +1,604 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 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/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Services\EDA;
|
||||
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Attachments\PartAttachment;
|
||||
use App\Entity\Parameters\PartParameter;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\Parts\PartLot;
|
||||
use App\Entity\Parts\StorageLocation;
|
||||
use App\Entity\Parts\Supplier;
|
||||
use App\Entity\PriceInformations\Orderdetail;
|
||||
use App\Services\EDA\KiCadHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
#[Group('DB')]
|
||||
final class KiCadHelperTest extends KernelTestCase
|
||||
{
|
||||
private KiCadHelper $helper;
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->helper = self::getContainer()->get(KiCadHelper::class);
|
||||
$this->em = self::getContainer()->get(EntityManagerInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 1 (from fixtures) has no stock lots. Stock should be 0.
|
||||
*/
|
||||
public function testPartWithoutStockHasZeroStock(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 1);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayHasKey('Stock', $result['fields']);
|
||||
self::assertSame('0', $result['fields']['Stock']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 3 (from fixtures) has a lot with amount=1.0 in StorageLocation 1.
|
||||
*/
|
||||
public function testPartWithStockShowsCorrectQuantity(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 3);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayHasKey('Stock', $result['fields']);
|
||||
self::assertSame('1', $result['fields']['Stock']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 3 has a lot with amount > 0 in StorageLocation "Node 1".
|
||||
*/
|
||||
public function testPartWithStorageLocationShowsLocation(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 3);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayHasKey('Storage Location', $result['fields']);
|
||||
self::assertSame('Node 1', $result['fields']['Storage Location']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 1 has no stock lots, so no storage location should be shown.
|
||||
*/
|
||||
public function testPartWithoutStorageLocationOmitsField(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 1);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayNotHasKey('Storage Location', $result['fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* All parts should have a "Part-DB URL" field pointing to the part info page.
|
||||
*/
|
||||
public function testPartDbUrlFieldIsPresent(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 1);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayHasKey('Part-DB URL', $result['fields']);
|
||||
self::assertStringContainsString('/part/1/info', $result['fields']['Part-DB URL']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 1 has no attachments, so the datasheet should fall back to the Part-DB page URL.
|
||||
*/
|
||||
public function testDatasheetFallbackToPartUrlWhenNoAttachments(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 1);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// With no attachments, datasheet should equal Part-DB URL
|
||||
self::assertSame(
|
||||
$result['fields']['Part-DB URL']['value'],
|
||||
$result['fields']['datasheet']['value']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 3 has attachments but none named "datasheet" and none are PDFs,
|
||||
* so the datasheet should fall back to the Part-DB page URL.
|
||||
*/
|
||||
public function testDatasheetFallbackWhenNoMatchingAttachments(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 3);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// "TestAttachment" (url: www.foo.bar) and "Test2" (internal: invalid) don't match datasheet patterns
|
||||
self::assertSame(
|
||||
$result['fields']['Part-DB URL']['value'],
|
||||
$result['fields']['datasheet']['value']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an attachment with type name containing "Datasheet" is found.
|
||||
*/
|
||||
public function testDatasheetFoundByAttachmentTypeName(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
// Create an attachment type named "Datasheets"
|
||||
$datasheetType = new AttachmentType();
|
||||
$datasheetType->setName('Datasheets');
|
||||
$this->em->persist($datasheetType);
|
||||
|
||||
// Create a part with a datasheet attachment
|
||||
$part = new Part();
|
||||
$part->setName('Part with Datasheet Type');
|
||||
$part->setCategory($category);
|
||||
|
||||
$attachment = new PartAttachment();
|
||||
$attachment->setName('Component Spec');
|
||||
$attachment->setURL('https://example.com/spec.pdf');
|
||||
$attachment->setAttachmentType($datasheetType);
|
||||
$part->addAttachment($attachment);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertSame('https://example.com/spec.pdf', $result['fields']['datasheet']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an attachment named "Datasheet" is found (regardless of type).
|
||||
*/
|
||||
public function testDatasheetFoundByAttachmentName(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
$attachmentType = $this->em->find(AttachmentType::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Named Datasheet');
|
||||
$part->setCategory($category);
|
||||
|
||||
$attachment = new PartAttachment();
|
||||
$attachment->setName('Datasheet BC547');
|
||||
$attachment->setURL('https://example.com/bc547-datasheet.pdf');
|
||||
$attachment->setAttachmentType($attachmentType);
|
||||
$part->addAttachment($attachment);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertSame('https://example.com/bc547-datasheet.pdf', $result['fields']['datasheet']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a PDF attachment is used as fallback when no "datasheet" match exists.
|
||||
*/
|
||||
public function testDatasheetFallbackToFirstPdfAttachment(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
$attachmentType = $this->em->find(AttachmentType::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with PDF');
|
||||
$part->setCategory($category);
|
||||
|
||||
// Non-PDF attachment first
|
||||
$attachment1 = new PartAttachment();
|
||||
$attachment1->setName('Photo');
|
||||
$attachment1->setURL('https://example.com/photo.jpg');
|
||||
$attachment1->setAttachmentType($attachmentType);
|
||||
$part->addAttachment($attachment1);
|
||||
|
||||
// PDF attachment second
|
||||
$attachment2 = new PartAttachment();
|
||||
$attachment2->setName('Specifications');
|
||||
$attachment2->setURL('https://example.com/specs.pdf');
|
||||
$attachment2->setAttachmentType($attachmentType);
|
||||
$part->addAttachment($attachment2);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// Should find the .pdf file as fallback
|
||||
self::assertSame('https://example.com/specs.pdf', $result['fields']['datasheet']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a "data sheet" variant (with space) is also matched by name.
|
||||
*/
|
||||
public function testDatasheetMatchesDataSheetWithSpace(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
$attachmentType = $this->em->find(AttachmentType::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Data Sheet');
|
||||
$part->setCategory($category);
|
||||
|
||||
$attachment = new PartAttachment();
|
||||
$attachment->setName('Data Sheet v1.2');
|
||||
$attachment->setURL('https://example.com/data-sheet.pdf');
|
||||
$attachment->setAttachmentType($attachmentType);
|
||||
$part->addAttachment($attachment);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertSame('https://example.com/data-sheet.pdf', $result['fields']['datasheet']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test stock calculation excludes expired lots.
|
||||
*/
|
||||
public function testStockExcludesExpiredLots(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Expired Stock');
|
||||
$part->setCategory($category);
|
||||
|
||||
// Active lot
|
||||
$lot1 = new PartLot();
|
||||
$lot1->setAmount(10.0);
|
||||
$part->addPartLot($lot1);
|
||||
|
||||
// Expired lot
|
||||
$lot2 = new PartLot();
|
||||
$lot2->setAmount(5.0);
|
||||
$lot2->setExpirationDate(new \DateTimeImmutable('-1 day'));
|
||||
$part->addPartLot($lot2);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// Only the active lot should be counted
|
||||
self::assertSame('10', $result['fields']['Stock']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test stock calculation excludes lots with unknown stock.
|
||||
*/
|
||||
public function testStockExcludesUnknownLots(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Unknown Stock');
|
||||
$part->setCategory($category);
|
||||
|
||||
// Known lot
|
||||
$lot1 = new PartLot();
|
||||
$lot1->setAmount(7.0);
|
||||
$part->addPartLot($lot1);
|
||||
|
||||
// Unknown lot
|
||||
$lot2 = new PartLot();
|
||||
$lot2->setInstockUnknown(true);
|
||||
$part->addPartLot($lot2);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertSame('7', $result['fields']['Stock']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test stock sums across multiple lots.
|
||||
*/
|
||||
public function testStockSumsMultipleLots(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
$location1 = $this->em->find(StorageLocation::class, 1);
|
||||
$location2 = $this->em->find(StorageLocation::class, 2);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part in Multiple Locations');
|
||||
$part->setCategory($category);
|
||||
|
||||
$lot1 = new PartLot();
|
||||
$lot1->setAmount(15.0);
|
||||
$lot1->setStorageLocation($location1);
|
||||
$part->addPartLot($lot1);
|
||||
|
||||
$lot2 = new PartLot();
|
||||
$lot2->setAmount(25.0);
|
||||
$lot2->setStorageLocation($location2);
|
||||
$part->addPartLot($lot2);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertSame('40', $result['fields']['Stock']['value']);
|
||||
self::assertArrayHasKey('Storage Location', $result['fields']);
|
||||
// Both locations should be listed
|
||||
self::assertStringContainsString('Node 1', $result['fields']['Storage Location']['value']);
|
||||
self::assertStringContainsString('Node 2', $result['fields']['Storage Location']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the Stock field visibility is "False" (not visible in schematic by default).
|
||||
*/
|
||||
public function testStockFieldIsNotVisible(): void
|
||||
{
|
||||
$part = $this->em->find(Part::class, 1);
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertSame('False', $result['fields']['Stock']['visible']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a parameter with eda_visibility=true appears in the KiCad fields.
|
||||
*/
|
||||
public function testParameterWithEdaVisibilityAppearsInFields(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Exported Parameter');
|
||||
$part->setCategory($category);
|
||||
|
||||
$param = new PartParameter();
|
||||
$param->setName('Voltage Rating');
|
||||
$param->setValueTypical(3.3);
|
||||
$param->setUnit('V');
|
||||
$param->setEdaVisibility(true);
|
||||
$part->addParameter($param);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayHasKey('Voltage Rating', $result['fields']);
|
||||
self::assertSame('3.3 V', $result['fields']['Voltage Rating']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a parameter with eda_visibility=false does NOT appear in the KiCad fields.
|
||||
*/
|
||||
public function testParameterWithoutEdaVisibilityDoesNotAppear(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Non-exported Parameter');
|
||||
$part->setCategory($category);
|
||||
|
||||
$param = new PartParameter();
|
||||
$param->setName('Internal Note');
|
||||
$param->setValueText('for testing only');
|
||||
$param->setEdaVisibility(false);
|
||||
$part->addParameter($param);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayNotHasKey('Internal Note', $result['fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a parameter with eda_visibility=null (system default) does NOT appear in the KiCad fields.
|
||||
*/
|
||||
public function testParameterWithNullEdaVisibilityDoesNotAppear(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Default Parameter');
|
||||
$part->setCategory($category);
|
||||
|
||||
$param = new PartParameter();
|
||||
$param->setName('Default Param');
|
||||
$param->setValueText('some value');
|
||||
// eda_visibility is null by default
|
||||
$part->addParameter($param);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayNotHasKey('Default Param', $result['fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an exported parameter named "description" does NOT overwrite the hardcoded description field.
|
||||
*/
|
||||
public function testExportedParameterDoesNotOverwriteHardcodedField(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Conflicting Parameter');
|
||||
$part->setDescription('The real description');
|
||||
$part->setCategory($category);
|
||||
|
||||
$param = new PartParameter();
|
||||
$param->setName('description');
|
||||
$param->setValueText('should not overwrite');
|
||||
$param->setEdaVisibility(true);
|
||||
$part->addParameter($param);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// The hardcoded description should win
|
||||
self::assertSame('The real description', $result['fields']['description']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that orderdetails without explicit eda_visibility are all exported (backward compat).
|
||||
*/
|
||||
public function testOrderdetailsExportedWhenNoEdaVisibilitySet(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$supplier = new Supplier();
|
||||
$supplier->setName('TestSupplier');
|
||||
$this->em->persist($supplier);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Supplier');
|
||||
$part->setCategory($category);
|
||||
|
||||
$od = new Orderdetail();
|
||||
$od->setSupplier($supplier);
|
||||
$od->setSupplierpartnr('TS-001');
|
||||
// eda_visibility is null (default)
|
||||
$part->addOrderdetail($od);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// Should export since no explicit flags are set (backward compat)
|
||||
self::assertArrayHasKey('TestSupplier SPN', $result['fields']);
|
||||
self::assertSame('TS-001', $result['fields']['TestSupplier SPN']['value']);
|
||||
// KiCost field should also be present
|
||||
self::assertArrayHasKey('testsupplier#', $result['fields']);
|
||||
self::assertSame('TS-001', $result['fields']['testsupplier#']['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that only orderdetails with eda_visibility=true are exported when explicit flags exist.
|
||||
*/
|
||||
public function testOrderdetailsFilteredByExplicitEdaVisibility(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$supplier1 = new Supplier();
|
||||
$supplier1->setName('VisibleSupplier');
|
||||
$this->em->persist($supplier1);
|
||||
|
||||
$supplier2 = new Supplier();
|
||||
$supplier2->setName('HiddenSupplier');
|
||||
$this->em->persist($supplier2);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Mixed Visibility');
|
||||
$part->setCategory($category);
|
||||
|
||||
$od1 = new Orderdetail();
|
||||
$od1->setSupplier($supplier1);
|
||||
$od1->setSupplierpartnr('VIS-001');
|
||||
$od1->setEdaVisibility(true);
|
||||
$part->addOrderdetail($od1);
|
||||
|
||||
$od2 = new Orderdetail();
|
||||
$od2->setSupplier($supplier2);
|
||||
$od2->setSupplierpartnr('HID-001');
|
||||
$od2->setEdaVisibility(false);
|
||||
$part->addOrderdetail($od2);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// Visible supplier should be exported
|
||||
self::assertArrayHasKey('VisibleSupplier SPN', $result['fields']);
|
||||
self::assertSame('VIS-001', $result['fields']['VisibleSupplier SPN']['value']);
|
||||
|
||||
// Hidden supplier should NOT be exported
|
||||
self::assertArrayNotHasKey('HiddenSupplier SPN', $result['fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that manufacturer fields (manf, manf#) are always exported.
|
||||
*/
|
||||
public function testManufacturerFieldsExported(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$manufacturer = new Manufacturer();
|
||||
$manufacturer->setName('Acme Corp');
|
||||
$this->em->persist($manufacturer);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Acme Widget');
|
||||
$part->setCategory($category);
|
||||
$part->setManufacturer($manufacturer);
|
||||
$part->setManufacturerProductNumber('ACM-1234');
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
self::assertArrayHasKey('manf', $result['fields']);
|
||||
self::assertSame('Acme Corp', $result['fields']['manf']['value']);
|
||||
self::assertArrayHasKey('manf#', $result['fields']);
|
||||
self::assertSame('ACM-1234', $result['fields']['manf#']['value']);
|
||||
self::assertArrayHasKey('Manufacturer', $result['fields']);
|
||||
self::assertArrayHasKey('MPN', $result['fields']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a parameter with empty name is not exported even with eda_visibility=true.
|
||||
*/
|
||||
public function testParameterWithEmptyNameIsSkipped(): void
|
||||
{
|
||||
$category = $this->em->find(Category::class, 1);
|
||||
|
||||
$part = new Part();
|
||||
$part->setName('Part with Empty Param Name');
|
||||
$part->setCategory($category);
|
||||
|
||||
$param = new PartParameter();
|
||||
$param->setName('');
|
||||
$param->setValueText('some value');
|
||||
$param->setEdaVisibility(true);
|
||||
$part->addParameter($param);
|
||||
|
||||
$this->em->persist($part);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->helper->getKiCADPart($part);
|
||||
|
||||
// Empty-named parameter should not appear
|
||||
self::assertArrayNotHasKey('', $result['fields']);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue