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 );
2026-02-14 23:32:43 +01:00
$translatorMock = $this -> createStub ( TranslatorInterface :: class );
$ipnSuggestSettings = $this -> createStub ( IpnSuggestSettings :: class );
2025-11-03 00:31:47 +01:00
$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 ;
});
2026-02-14 23:32:43 +01:00
$ipnSuggestSettings = $this -> createStub ( IpnSuggestSettings :: class );
2025-11-03 00:31:47 +01:00
$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 ;
});
2026-02-14 23:32:43 +01:00
$ipnSuggestSettings = $this -> createStub ( IpnSuggestSettings :: class );
2025-11-03 00:31:47 +01:00
$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' ]);
}
}