diff --git a/src/Services/InfoProviderSystem/Providers/TMEProvider.php b/src/Services/InfoProviderSystem/Providers/TMEProvider.php index 938bc7b37..932225175 100644 --- a/src/Services/InfoProviderSystem/Providers/TMEProvider.php +++ b/src/Services/InfoProviderSystem/Providers/TMEProvider.php @@ -280,9 +280,13 @@ private function normalizeURL(string $url): string { //If a URL starts with // we assume that it is a relative URL and we add the protocol if (str_starts_with($url, '//')) { - return 'https:' . $url; + $url = 'https:' . $url; } + //Encode bare % signs that are not already part of a valid percent-encoded sequence + //Fixes part numbers with % in them e.g. SMD0603-5K1-1% + $url = preg_replace('/%(?![0-9A-Fa-f]{2})/', '%25', $url); + return $url; } diff --git a/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php b/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php new file mode 100644 index 000000000..20fba5231 --- /dev/null +++ b/tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php @@ -0,0 +1,391 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\Services\InfoProviderSystem\Providers; + +use App\Entity\Parts\ManufacturingStatus; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use App\Services\InfoProviderSystem\Providers\ProviderCapabilities; +use App\Services\InfoProviderSystem\Providers\TMEClient; +use App\Services\InfoProviderSystem\Providers\TMEProvider; +use App\Settings\InfoProviderSystem\TMESettings; +use App\Tests\SettingsTestHelper; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; + +final class TMEProviderTest extends TestCase +{ + private TMESettings $settings; + private TMEProvider $provider; + private MockHttpClient $httpClient; + + protected function setUp(): void + { + $this->httpClient = new MockHttpClient(); + $this->settings = SettingsTestHelper::createSettingsDummy(TMESettings::class); + // Use a short (anonymous-style) token so grossPrices is read from settings + $this->settings->apiToken = 'test_token_000000000000000000000000000000000000000'; + $this->settings->apiSecret = 'test_secret'; + $this->settings->currency = 'EUR'; + $this->settings->language = 'en'; + $this->settings->country = 'DE'; + $this->settings->grossPrices = false; + $this->provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings), $this->settings); + } + + // --- Mock response helpers --- + // Only fields actually read by TMEProvider are included. + + private function mockProductList(array $products): MockResponse + { + return new MockResponse(json_encode([ + 'Status' => 'OK', + 'Data' => ['ProductList' => $products], + ])); + } + + private function mockFilesList(array $products): MockResponse + { + return new MockResponse(json_encode([ + 'Status' => 'OK', + 'Data' => ['ProductList' => $products], + ])); + } + + private function mockParametersList(array $products): MockResponse + { + return new MockResponse(json_encode([ + 'Status' => 'OK', + 'Data' => ['ProductList' => $products], + ])); + } + + private function mockPrices(string $currency, string $priceType, array $products): MockResponse + { + return new MockResponse(json_encode([ + 'Status' => 'OK', + 'Data' => [ + 'Currency' => $currency, + 'PriceType' => $priceType, + 'ProductList' => $products, + ], + ])); + } + + // --- Mock data --- + + private function smd0603Products(): MockResponse + { + return $this->mockProductList([[ + 'Symbol' => 'SMD0603-5K1-1%', + 'OriginalSymbol' => '0603SAF5101T5E', + 'Producer' => 'ROYALOHM', + 'Description' => 'Resistor: thick film; SMD; 0603; 5.1kΩ; 0.1W; ±1%; 50V; -55÷155°C', + 'Category' => 'SMD resistors', + 'Photo' => '//ce8dc832c.cloudimg.io/v7/_cdn_/E9/C2/B0/00/0/732318_1.jpg', + 'ProductStatusList' => [], + 'ProductInformationPage' => '//www.tme.eu/en/details/smd0603-5k1-1%/smd-resistors/royalohm/0603saf5101t5e/', + 'Weight' => 0.021, + 'WeightUnit' => 'g', + ]]); + } + + private function smd0603Files(): MockResponse + { + return $this->mockFilesList([[ + 'Symbol' => 'SMD0603-5K1-1%', + 'Files' => [ + 'AdditionalPhotoList' => [], + 'DocumentList' => [ + ['DocumentUrl' => '//www.tme.eu/Document/b315665a56acbc42df513c99b390ad98/ROYALOHM-THICKFILM.pdf'], + ['DocumentUrl' => '//www.tme.eu/Document/c283990e907c122bb808207d1578ac7f/POWER_RATING-DTE.pdf'], + ], + ], + ]]); + } + + private function smd0603Parameters(): MockResponse + { + return $this->mockParametersList([[ + 'Symbol' => 'SMD0603-5K1-1%', + 'ParameterList' => [ + ['ParameterId' => 34, 'ParameterName' => 'Type of resistor', 'ParameterValue' => 'thick film'], + ['ParameterId' => 35, 'ParameterName' => 'Case - mm', 'ParameterValue' => '1608'], + ['ParameterId' => 38, 'ParameterName' => 'Resistance', 'ParameterValue' => '5.1kΩ'], + ['ParameterId' => 39, 'ParameterName' => 'Tolerance', 'ParameterValue' => '±1%'], + ['ParameterId' => 120, 'ParameterName' => 'Operating voltage', 'ParameterValue' => '50V'], + ], + ]]); + } + + private function smd0603Prices(): MockResponse + { + return $this->mockPrices('EUR', 'NET', [[ + 'Symbol' => 'SMD0603-5K1-1%', + 'PriceList' => [ + ['Amount' => 100, 'PriceValue' => 0.01077], + ['Amount' => 1000, 'PriceValue' => 0.00291], + ['Amount' => 5000, 'PriceValue' => 0.00150], + ], + ]]); + } + + private function etqp3mProducts(): MockResponse + { + return $this->mockProductList([[ + 'Symbol' => 'ETQP3M6R8KVP', + 'OriginalSymbol' => 'ETQP3M6R8KVP', + 'Producer' => 'PANASONIC', + 'Description' => 'Inductor: wire; SMD; 6.8uH; 2.9A; R: 65.7mΩ; ±20%; ETQP3M; 5.5x5x3mm', + 'Category' => 'Inductors', + 'Photo' => '//ce8dc832c.cloudimg.io/v7/_cdn_/9E/27/A0/00/0/684777_1.jpg', + 'ProductStatusList' => [], + 'ProductInformationPage' => '//www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/', + 'Weight' => 0.44, + 'WeightUnit' => 'g', + ]]); + } + + private function etqp3mFiles(): MockResponse + { + return $this->mockFilesList([[ + 'Symbol' => 'ETQP3M6R8KVP', + 'Files' => [ + 'AdditionalPhotoList' => [], + 'DocumentList' => [ + ['DocumentUrl' => '//www.tme.eu/Document/50a845881f09d8a2248350946e11df38/AGL0000C63.pdf'], + ['DocumentUrl' => '//www.tme.eu/Document/8480690a42fa577214e35e33d3fc8d77/ETQP3M100KVN-LNK.txt'], + ], + ], + ]]); + } + + private function etqp3mParameters(): MockResponse + { + return $this->mockParametersList([[ + 'Symbol' => 'ETQP3M6R8KVP', + 'ParameterList' => [ + ['ParameterId' => 566, 'ParameterName' => 'Inductance', 'ParameterValue' => '6.8µH'], + ['ParameterId' => 370, 'ParameterName' => 'Operating current', 'ParameterValue' => '2.9A'], + ['ParameterId' => 39, 'ParameterName' => 'Tolerance', 'ParameterValue' => '±20%'], + ], + ]]); + } + + private function etqp3mPrices(): MockResponse + { + return $this->mockPrices('EUR', 'NET', [[ + 'Symbol' => 'ETQP3M6R8KVP', + 'PriceList' => [ + ['Amount' => 1, 'PriceValue' => 0.589], + ['Amount' => 5, 'PriceValue' => 0.429], + ['Amount' => 10, 'PriceValue' => 0.399], + ], + ]]); + } + + // --- Tests --- + + public function testGetProviderInfo(): void + { + $info = $this->provider->getProviderInfo(); + + $this->assertIsArray($info); + $this->assertArrayHasKey('name', $info); + $this->assertArrayHasKey('description', $info); + $this->assertArrayHasKey('url', $info); + $this->assertEquals('TME', $info['name']); + $this->assertEquals('https://tme.eu/', $info['url']); + } + + public function testGetProviderKey(): void + { + $this->assertSame('tme', $this->provider->getProviderKey()); + } + + public function testIsActiveWithCredentials(): void + { + $this->assertTrue($this->provider->isActive()); + } + + public function testIsActiveWithoutCredentials(): void + { + $this->settings->apiToken = null; + $provider = new TMEProvider(new TMEClient($this->httpClient, $this->settings), $this->settings); + $this->assertFalse($provider->isActive()); + } + + public function testGetCapabilities(): void + { + $capabilities = $this->provider->getCapabilities(); + + $this->assertIsArray($capabilities); + $this->assertContains(ProviderCapabilities::BASIC, $capabilities); + $this->assertContains(ProviderCapabilities::PICTURE, $capabilities); + $this->assertContains(ProviderCapabilities::DATASHEET, $capabilities); + $this->assertContains(ProviderCapabilities::PRICE, $capabilities); + $this->assertContains(ProviderCapabilities::FOOTPRINT, $capabilities); + } + + public function testGetHandledDomains(): void + { + $this->assertContains('tme.eu', $this->provider->getHandledDomains()); + } + + public function testGetIDFromURL(): void + { + $this->assertSame('fi321_se', $this->provider->getIDFromURL('https://www.tme.eu/de/details/fi321_se/kuhler/alutronic/')); + $this->assertSame('smd0603-5k1-1%25', $this->provider->getIDFromURL('https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/')); + $this->assertNull($this->provider->getIDFromURL('https://www.tme.eu/en/')); + } + + public function testSearchByKeyword(): void + { + $this->httpClient->setResponseFactory([$this->smd0603Products()]); + + $results = $this->provider->searchByKeyword('SMD0603-5K1-1%'); + + $this->assertIsArray($results); + $this->assertCount(1, $results); + $this->assertInstanceOf(SearchResultDTO::class, $results[0]); + $this->assertSame('SMD0603-5K1-1%', $results[0]->provider_id); + $this->assertSame('0603SAF5101T5E', $results[0]->name); + $this->assertSame('ROYALOHM', $results[0]->manufacturer); + $this->assertSame('SMD resistors', $results[0]->category); + $this->assertSame(ManufacturingStatus::ACTIVE, $results[0]->manufacturing_status); + $this->assertSame( + 'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/', + $results[0]->provider_url + ); + } + + public function testGetDetailsWithPercentInPartNumber(): void + { + $this->httpClient->setResponseFactory([ + $this->smd0603Products(), + $this->smd0603Files(), + $this->smd0603Parameters(), + $this->smd0603Prices(), + ]); + + $result = $this->provider->getDetails('SMD0603-5K1-1%'); + + $this->assertInstanceOf(PartDetailDTO::class, $result); + $this->assertSame('SMD0603-5K1-1%', $result->provider_id); + $this->assertSame('0603SAF5101T5E', $result->name); + $this->assertSame('Resistor: thick film; SMD; 0603; 5.1kΩ; 0.1W; ±1%; 50V; -55÷155°C', $result->description); + $this->assertSame('ROYALOHM', $result->manufacturer); + $this->assertSame('0603SAF5101T5E', $result->mpn); + $this->assertSame('SMD resistors', $result->category); + $this->assertSame(ManufacturingStatus::ACTIVE, $result->manufacturing_status); + $this->assertSame(0.021, $result->mass); + $this->assertSame('1608', $result->footprint); + $this->assertSame( + 'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/', + $result->provider_url + ); + + $this->assertCount(2, $result->datasheets); + $this->assertSame('https://www.tme.eu/Document/b315665a56acbc42df513c99b390ad98/ROYALOHM-THICKFILM.pdf', $result->datasheets[0]->url); + $this->assertCount(0, $result->images); + + $this->assertCount(1, $result->vendor_infos); + $vendorInfo = $result->vendor_infos[0]; + $this->assertInstanceOf(PurchaseInfoDTO::class, $vendorInfo); + $this->assertSame('TME', $vendorInfo->distributor_name); + $this->assertSame('SMD0603-5K1-1%', $vendorInfo->order_number); + $this->assertSame( + 'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/', + $vendorInfo->product_url + ); + $this->assertCount(3, $vendorInfo->prices); + $this->assertSame(100.0, $vendorInfo->prices[0]->minimum_discount_amount); + $this->assertSame('0.01077', $vendorInfo->prices[0]->price); + $this->assertSame('EUR', $vendorInfo->prices[0]->currency_iso_code); + $this->assertFalse($vendorInfo->prices[0]->includes_tax); + + $this->assertCount(5, $result->parameters); + } + + public function testGetDetailsForEtqp3m6r8kvp(): void + { + $this->httpClient->setResponseFactory([ + $this->etqp3mProducts(), + $this->etqp3mFiles(), + $this->etqp3mParameters(), + $this->etqp3mPrices(), + ]); + + $result = $this->provider->getDetails('ETQP3M6R8KVP'); + + $this->assertInstanceOf(PartDetailDTO::class, $result); + $this->assertSame('ETQP3M6R8KVP', $result->provider_id); + $this->assertSame('ETQP3M6R8KVP', $result->name); + $this->assertSame('Inductor: wire; SMD; 6.8uH; 2.9A; R: 65.7mΩ; ±20%; ETQP3M; 5.5x5x3mm', $result->description); + $this->assertSame('PANASONIC', $result->manufacturer); + $this->assertSame('ETQP3M6R8KVP', $result->mpn); + $this->assertSame('Inductors', $result->category); + $this->assertSame(ManufacturingStatus::ACTIVE, $result->manufacturing_status); + $this->assertSame(0.44, $result->mass); + $this->assertNull($result->footprint); + $this->assertSame('https://www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/', $result->provider_url); + + $this->assertCount(2, $result->datasheets); + $this->assertSame('https://www.tme.eu/Document/50a845881f09d8a2248350946e11df38/AGL0000C63.pdf', $result->datasheets[0]->url); + $this->assertCount(0, $result->images); + + $this->assertCount(1, $result->vendor_infos); + $vendorInfo = $result->vendor_infos[0]; + $this->assertSame('TME', $vendorInfo->distributor_name); + $this->assertSame('ETQP3M6R8KVP', $vendorInfo->order_number); + $this->assertSame('https://www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/', $vendorInfo->product_url); + $this->assertCount(3, $vendorInfo->prices); + $this->assertSame(1.0, $vendorInfo->prices[0]->minimum_discount_amount); + $this->assertSame('0.589', $vendorInfo->prices[0]->price); + $this->assertSame('EUR', $vendorInfo->prices[0]->currency_iso_code); + $this->assertFalse($vendorInfo->prices[0]->includes_tax); + + $this->assertCount(3, $result->parameters); + } + + public function testNormalizeURLEncodesBarePctSign(): void + { + $method = (new \ReflectionClass($this->provider))->getMethod('normalizeURL'); + + $this->assertSame( + 'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/', + $method->invoke($this->provider, '//www.tme.eu/en/details/smd0603-5k1-1%/smd-resistors/royalohm/0603saf5101t5e/') + ); + $this->assertSame( + 'https://www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/', + $method->invoke($this->provider, '//www.tme.eu/en/details/smd0603-5k1-1%25/smd-resistors/royalohm/0603saf5101t5e/') + ); + $this->assertSame( + 'https://www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/', + $method->invoke($this->provider, '//www.tme.eu/en/details/etqp3m6r8kvp/inductors/panasonic/') + ); + $this->assertSame('https://example.com/path', $method->invoke($this->provider, 'https://example.com/path')); + } +}