mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-04-15 19:09:36 +00:00
Fix creating parts from TME if the SPN contains percent signs (#1337)
* Fix creating TME parts with percent signs in SPN The SPN ends up in the URL, which later causes validation errors n the form. Solved by encoding the percent sign. * Add TME provider unit tests.
This commit is contained in:
parent
cee7e2a077
commit
d25ac2622e
2 changed files with 396 additions and 1 deletions
|
|
@ -280,9 +280,13 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf
|
|||
{
|
||||
//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;
|
||||
}
|
||||
|
||||
|
|
|
|||
391
tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php
Normal file
391
tests/Services/InfoProviderSystem/Providers/TMEProviderTest.php
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
<?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\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'));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue