Part-DB-server/tests/Repository/PartRepositoryTest.php

298 lines
15 KiB
PHP
Raw Permalink Normal View History

Added feature for part IPN suggest with category prefixes (#1054) * Erweiterungstätigkeiten zur IPN-Vorschlagsliste anhand von Präfixen aus den Kategorien * Umstellung Migrationen bzgl. Multi-Plattform-Support. Zunächst MySQL, SQLite Statements integrieren. * Postgre Statements integrieren * SQL-Formatierung in Migration verbessern * Erweitere IPN-Suggest um Bauteilbeschreibung. Die Implementierung berücksichtigt nun zusätzlich die Bauteilbeschreibung zu maximal 150 Zeichen Länge für die Generierung von IPN-Vorschlägen und Inkrementen. * Anpassungen aus Analyse vornehmen * IPN-Validierung für Parts überarbeiten * IPN-Vorschlagslogik um Konfiguration erweitert * Anpassungen aus phpstan Analyse * IPN-Vorschlagslogik erweitert und Bauteil-IPN vereindeutigt Die IPN-Logik wurde um eine Konfiguration zur automatischen Suffix-Anfügung und die Berücksichtigung von doppelten Beschreibungen bei Bedarf ergänzt. Zudem wurde das Datenmodell angepasst, um eine eindeutige Speicherung der IPN zu gewährleisten. * Regex-Konfigurationsmöglichkeit für IPN-Vorschläge einführen Die Einstellungen für die IPN-Vorschlagslogik wurden um eine Regex-Validierung und eine Hilfetext-Konfiguration erweitert. Tests und Änderungen an den Formularoptionen wurden implementiert. * Match range assert and form limits in suggestPartDigits * Keep existing behavior with autoAppend suffix by default * Show the regex hint in the browser validation notice. * Improved translations * Removed unnecessary service definition * Removed german comments --------- Co-authored-by: Marcel Diegelmann <marcel.diegelmann@gmail.com> Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2025-11-03 00:31:47 +01:00
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 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\Repository;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use App\Settings\MiscSettings\IpnSuggestSettings;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use PHPUnit\Framework\TestCase;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Symfony\Contracts\Translation\TranslatorInterface;
use App\Repository\PartRepository;
final class PartRepositoryTest extends TestCase
{
public function test_autocompleteSearch_builds_expected_query_without_db(): void
{
$qb = $this->getMockBuilder(QueryBuilder::class)
->disableOriginalConstructor()
->onlyMethods([
'select', 'leftJoin', 'where', 'orWhere',
'setParameter', 'setMaxResults', 'orderBy', 'getQuery'
])->getMock();
$qb->expects(self::once())->method('select')->with('part')->willReturnSelf();
$qb->expects(self::exactly(2))->method('leftJoin')->with($this->anything(), $this->anything())->willReturnSelf();
$qb->expects(self::atLeastOnce())->method('where')->with($this->anything())->willReturnSelf();
$qb->method('orWhere')->with($this->anything())->willReturnSelf();
$searchQuery = 'res';
$qb->expects(self::once())->method('setParameter')->with('query', '%'.$searchQuery.'%')->willReturnSelf();
$qb->expects(self::once())->method('setMaxResults')->with(10)->willReturnSelf();
$qb->expects(self::once())->method('orderBy')->with('NATSORT(part.name)', 'ASC')->willReturnSelf();
$emMock = $this->createMock(EntityManagerInterface::class);
$classMetadata = new ClassMetadata(Part::class);
$emMock->method('getClassMetadata')->with(Part::class)->willReturn($classMetadata);
$translatorMock = $this->createMock(TranslatorInterface::class);
$ipnSuggestSettings = $this->createMock(IpnSuggestSettings::class);
$repo = $this->getMockBuilder(PartRepository::class)
->setConstructorArgs([$emMock, $translatorMock, $ipnSuggestSettings])
->onlyMethods(['createQueryBuilder'])
->getMock();
$repo->expects(self::once())
->method('createQueryBuilder')
->with('part')
->willReturn($qb);
$part = new Part(); // create found part, because it is not saved in DB
$part->setName('Resistor');
$queryMock = $this->getMockBuilder(Query::class)
->disableOriginalConstructor()
->onlyMethods(['getResult'])
->getMock();
$queryMock->expects(self::once())->method('getResult')->willReturn([$part]);
$qb->method('getQuery')->willReturn($queryMock);
$result = $repo->autocompleteSearch($searchQuery, 10);
// Check one part found and returned
self::assertIsArray($result);
self::assertCount(1, $result);
self::assertSame($part, $result[0]);
}
public function test_autoCompleteIpn_with_unsaved_part_and_category_without_part_description(): void
{
$qb = $this->getMockBuilder(QueryBuilder::class)
->disableOriginalConstructor()
->onlyMethods([
'select', 'leftJoin', 'where', 'andWhere', 'orWhere',
'setParameter', 'setMaxResults', 'orderBy', 'getQuery'
])->getMock();
$qb->method('select')->willReturnSelf();
$qb->method('leftJoin')->willReturnSelf();
$qb->method('where')->willReturnSelf();
$qb->method('andWhere')->willReturnSelf();
$qb->method('orWhere')->willReturnSelf();
$qb->method('setParameter')->willReturnSelf();
$qb->method('setMaxResults')->willReturnSelf();
$qb->method('orderBy')->willReturnSelf();
$emMock = $this->createMock(EntityManagerInterface::class);
$classMetadata = new ClassMetadata(Part::class);
$emMock->method('getClassMetadata')->with(Part::class)->willReturn($classMetadata);
$translatorMock = $this->createMock(TranslatorInterface::class);
$translatorMock->method('trans')
->willReturnCallback(static function (string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string {
return $id;
});
$ipnSuggestSettings = $this->createMock(IpnSuggestSettings::class);
$ipnSuggestSettings->suggestPartDigits = 4;
$ipnSuggestSettings->useDuplicateDescription = false;
$repo = $this->getMockBuilder(PartRepository::class)
->setConstructorArgs([$emMock, $translatorMock, $ipnSuggestSettings])
->onlyMethods(['createQueryBuilder'])
->getMock();
$repo->expects(self::atLeastOnce())
->method('createQueryBuilder')
->with('part')
->willReturn($qb);
$queryMock = $this->getMockBuilder(Query::class)
->disableOriginalConstructor()
->onlyMethods(['getResult'])
->getMock();
$categoryParent = new Category();
$categoryParent->setName('Passive components');
$categoryParent->setPartIpnPrefix('PCOM');
$categoryChild = new Category();
$categoryChild->setName('Resistors');
$categoryChild->setPartIpnPrefix('RES');
$categoryChild->setParent($categoryParent);
$partForSuggestGeneration = new Part(); // create found part, because it is not saved in DB
$partForSuggestGeneration->setIpn('RES-0001');
$partForSuggestGeneration->setCategory($categoryChild);
$queryMock->method('getResult')->willReturn([$partForSuggestGeneration]);
$qb->method('getQuery')->willReturn($queryMock);
$suggestions = $repo->autoCompleteIpn($partForSuggestGeneration, '', 4);
// Check structure available
self::assertIsArray($suggestions);
self::assertArrayHasKey('commonPrefixes', $suggestions);
self::assertArrayHasKey('prefixesPartIncrement', $suggestions);
self::assertNotEmpty($suggestions['commonPrefixes']);
self::assertNotEmpty($suggestions['prefixesPartIncrement']);
// Check expected values
self::assertSame('RES-', $suggestions['commonPrefixes'][0]['title']);
self::assertSame('part.edit.tab.advanced.ipn.prefix.direct_category', $suggestions['commonPrefixes'][0]['description']);
self::assertSame('PCOM-RES-', $suggestions['commonPrefixes'][1]['title']);
self::assertSame('part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment', $suggestions['commonPrefixes'][1]['description']);
self::assertSame('RES-0002', $suggestions['prefixesPartIncrement'][0]['title']); // next possible free increment for given part category
self::assertSame('part.edit.tab.advanced.ipn.prefix.direct_category.increment', $suggestions['prefixesPartIncrement'][0]['description']);
self::assertSame('PCOM-RES-0002', $suggestions['prefixesPartIncrement'][1]['title']); // next possible free increment for given part category
self::assertSame('part.edit.tab.advanced.ipn.prefix.hierarchical.increment', $suggestions['prefixesPartIncrement'][1]['description']);
}
public function test_autoCompleteIpn_with_unsaved_part_and_category_with_part_description(): void
{
$qb = $this->getMockBuilder(QueryBuilder::class)
->disableOriginalConstructor()
->onlyMethods([
'select', 'leftJoin', 'where', 'andWhere', 'orWhere',
'setParameter', 'setMaxResults', 'orderBy', 'getQuery'
])->getMock();
$qb->method('select')->willReturnSelf();
$qb->method('leftJoin')->willReturnSelf();
$qb->method('where')->willReturnSelf();
$qb->method('andWhere')->willReturnSelf();
$qb->method('orWhere')->willReturnSelf();
$qb->method('setParameter')->willReturnSelf();
$qb->method('setMaxResults')->willReturnSelf();
$qb->method('orderBy')->willReturnSelf();
$emMock = $this->createMock(EntityManagerInterface::class);
$classMetadata = new ClassMetadata(Part::class);
$emMock->method('getClassMetadata')->with(Part::class)->willReturn($classMetadata);
$translatorMock = $this->createMock(TranslatorInterface::class);
$translatorMock->method('trans')
->willReturnCallback(static function (string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string {
return $id;
});
$ipnSuggestSettings = $this->createMock(IpnSuggestSettings::class);
$ipnSuggestSettings->suggestPartDigits = 4;
$ipnSuggestSettings->useDuplicateDescription = false;
$repo = $this->getMockBuilder(PartRepository::class)
->setConstructorArgs([$emMock, $translatorMock, $ipnSuggestSettings])
->onlyMethods(['createQueryBuilder'])
->getMock();
$repo->expects(self::atLeastOnce())
->method('createQueryBuilder')
->with('part')
->willReturn($qb);
$queryMock = $this->getMockBuilder(Query::class)
->disableOriginalConstructor()
->onlyMethods(['getResult'])
->getMock();
$categoryParent = new Category();
$categoryParent->setName('Passive components');
$categoryParent->setPartIpnPrefix('PCOM');
$categoryChild = new Category();
$categoryChild->setName('Resistors');
$categoryChild->setPartIpnPrefix('RES');
$categoryChild->setParent($categoryParent);
$partForSuggestGeneration = new Part(); // create found part, because it is not saved in DB
$partForSuggestGeneration->setCategory($categoryChild);
$partForSuggestGeneration->setIpn('1810-1679_1');
$partForSuggestGeneration->setDescription('NETWORK-RESISTOR 4 0 OHM +5PCT 0.063W TKF SMT');
$queryMock->method('getResult')->willReturn([$partForSuggestGeneration]);
$qb->method('getQuery')->willReturn($queryMock);
$suggestions = $repo->autoCompleteIpn($partForSuggestGeneration, 'NETWORK-RESISTOR 4 0 OHM +5PCT 0.063W TKF SMT', 4);
// Check structure available
self::assertIsArray($suggestions);
self::assertArrayHasKey('commonPrefixes', $suggestions);
self::assertArrayHasKey('prefixesPartIncrement', $suggestions);
self::assertNotEmpty($suggestions['commonPrefixes']);
self::assertCount(2, $suggestions['commonPrefixes']);
self::assertNotEmpty($suggestions['prefixesPartIncrement']);
self::assertCount(2, $suggestions['prefixesPartIncrement']);
// Check expected values without any increment, for user to decide
self::assertSame('RES-', $suggestions['commonPrefixes'][0]['title']);
self::assertSame('part.edit.tab.advanced.ipn.prefix.direct_category', $suggestions['commonPrefixes'][0]['description']);
self::assertSame('PCOM-RES-', $suggestions['commonPrefixes'][1]['title']);
self::assertSame('part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment', $suggestions['commonPrefixes'][1]['description']);
// Check expected values with next possible increment at category level
self::assertSame('RES-0001', $suggestions['prefixesPartIncrement'][0]['title']); // next possible free increment for given part category
self::assertSame('part.edit.tab.advanced.ipn.prefix.direct_category.increment', $suggestions['prefixesPartIncrement'][0]['description']);
self::assertSame('PCOM-RES-0001', $suggestions['prefixesPartIncrement'][1]['title']); // next possible free increment for given part category
self::assertSame('part.edit.tab.advanced.ipn.prefix.hierarchical.increment', $suggestions['prefixesPartIncrement'][1]['description']);
$ipnSuggestSettings->useDuplicateDescription = true;
$suggestionsWithSameDescription = $repo->autoCompleteIpn($partForSuggestGeneration, 'NETWORK-RESISTOR 4 0 OHM +5PCT 0.063W TKF SMT', 4);
// Check structure available
self::assertIsArray($suggestionsWithSameDescription);
self::assertArrayHasKey('commonPrefixes', $suggestionsWithSameDescription);
self::assertArrayHasKey('prefixesPartIncrement', $suggestionsWithSameDescription);
self::assertNotEmpty($suggestionsWithSameDescription['commonPrefixes']);
self::assertCount(2, $suggestionsWithSameDescription['commonPrefixes']);
self::assertNotEmpty($suggestionsWithSameDescription['prefixesPartIncrement']);
self::assertCount(4, $suggestionsWithSameDescription['prefixesPartIncrement']);
// Check expected values without any increment, for user to decide
self::assertSame('RES-', $suggestionsWithSameDescription['commonPrefixes'][0]['title']);
self::assertSame('part.edit.tab.advanced.ipn.prefix.direct_category', $suggestionsWithSameDescription['commonPrefixes'][0]['description']);
self::assertSame('PCOM-RES-', $suggestionsWithSameDescription['commonPrefixes'][1]['title']);
self::assertSame('part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment', $suggestionsWithSameDescription['commonPrefixes'][1]['description']);
// Check expected values with next possible increment at part description level
self::assertSame('1810-1679_1', $suggestionsWithSameDescription['prefixesPartIncrement'][0]['title']); // current given value
self::assertSame('part.edit.tab.advanced.ipn.prefix.description.current-increment', $suggestionsWithSameDescription['prefixesPartIncrement'][0]['description']);
self::assertSame('1810-1679_2', $suggestionsWithSameDescription['prefixesPartIncrement'][1]['title']); // next possible value
self::assertSame('part.edit.tab.advanced.ipn.prefix.description.increment', $suggestionsWithSameDescription['prefixesPartIncrement'][1]['description']);
// Check expected values with next possible increment at category level
self::assertSame('RES-0001', $suggestionsWithSameDescription['prefixesPartIncrement'][2]['title']); // next possible free increment for given part category
self::assertSame('part.edit.tab.advanced.ipn.prefix.direct_category.increment', $suggestionsWithSameDescription['prefixesPartIncrement'][2]['description']);
self::assertSame('PCOM-RES-0001', $suggestionsWithSameDescription['prefixesPartIncrement'][3]['title']); // next possible free increment for given part category
self::assertSame('part.edit.tab.advanced.ipn.prefix.hierarchical.increment', $suggestionsWithSameDescription['prefixesPartIncrement'][3]['description']);
}
}