fix: linki produktow z permutacja atrybutow w feedzie Google (v0.350)
Separator URL miedzy parami attr-val zmieniony z "/" na "_" w generatorze feedu (ProductRepository::appendCombinationToXml). Wzorzec routingu pp_routes rozszerzony do [0-9_-]+ w Helpers::htacces (oba warianty: seo_link i fallback p-id-name). LayoutEngine konwertuje "_" -> "|" przed wywolaniem ProductRepository::findCached — format DB pozostaje "|". Partial product-attribute.php preselectuje wartosc z permutation_hash URL (forced_value_id), co poprawia UX wejscia z linka feedu. Suita: 834 -> 841 testow (+7), 2330 assertions. Wymagane akcje na produkcji po deployu: regeneracja pp_routes (Helpers::htacces), wyczyszczenie klucza pp_routes:all w Redis, regeneracja google-feed.xml, resubmit feedu w GMC. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
151
tests/Unit/Domain/Product/ProductFeedLinkTest.php
Normal file
151
tests/Unit/Domain/Product/ProductFeedLinkTest.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
namespace Tests\Unit\Domain\Product;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Domain\Product\ProductRepository;
|
||||
|
||||
/**
|
||||
* Phase 18 — testy generatora linku do feedu Google.
|
||||
*
|
||||
* ProductRepository::appendCombinationToXml buduje <link> dla pozycji
|
||||
* feedu Google. permutation_hash w bazie ma format "attr-val|attr-val".
|
||||
* W URL feedu separator między parami to "_" (nie "/"), żeby URL był
|
||||
* jednym segmentem dopasowywalnym przez routing pp_routes.
|
||||
*
|
||||
* Test wywołuje prywatną metodę przez ReflectionMethod z minimalnymi
|
||||
* danymi produktu i sprawdza zawartość wynikowego DOMDocument.
|
||||
*/
|
||||
class ProductFeedLinkTest extends TestCase
|
||||
{
|
||||
private function buildRepoWithMocks(): ProductRepository
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->method('select')->willReturn([]);
|
||||
$mockDb->method('get')->willReturn(null);
|
||||
|
||||
$repo = new ProductRepository($mockDb);
|
||||
|
||||
// appendShippingToXml wywołuje $this->transportRepoForXml->lowestTransportPrice().
|
||||
// Inicjalizacja w generateGoogleXmlFeed(); dla unit testu wstrzykujemy mock dynamicznie.
|
||||
$transportMock = $this->getMockBuilder(\Domain\Transport\TransportRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$transportMock->method('lowestTransportPrice')->willReturn(0.0);
|
||||
$repo->transportRepoForXml = $transportMock;
|
||||
|
||||
return $repo;
|
||||
}
|
||||
|
||||
private function invokeAppendCombination(ProductRepository $repo, array $product, array $combination): string
|
||||
{
|
||||
$doc = new \DOMDocument('1.0', 'UTF-8');
|
||||
$channelNode = $doc->appendChild($doc->createElement('channel'));
|
||||
|
||||
$method = new \ReflectionMethod(ProductRepository::class, 'appendCombinationToXml');
|
||||
$method->setAccessible(true);
|
||||
$method->invoke($repo, $doc, $channelNode, $product, $combination, 'https', 'shop.example.com');
|
||||
|
||||
return $doc->saveXML();
|
||||
}
|
||||
|
||||
private function baseProduct(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'id' => 123,
|
||||
'ean' => '5901234567890',
|
||||
'language' => [
|
||||
'name' => 'Produkt testowy',
|
||||
'xml_name' => '',
|
||||
'short_description' => 'Opis',
|
||||
'meta_title' => '',
|
||||
'seo_link' => 'sukienka-czerwona',
|
||||
],
|
||||
'price_brutto' => 100,
|
||||
'price_brutto_promo' => 0,
|
||||
'quantity' => 10,
|
||||
'stock_0_buy' => 0,
|
||||
'wp' => 1,
|
||||
'images' => [],
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
public function testCombinationLinkUsesUnderscoreInSeoLinkBranch()
|
||||
{
|
||||
$repo = $this->buildRepoWithMocks();
|
||||
$product = $this->baseProduct();
|
||||
$combination = [
|
||||
'id' => 555,
|
||||
'permutation_hash' => '20-170|21-175',
|
||||
'price_brutto' => 120,
|
||||
'price_brutto_promo' => 0,
|
||||
'quantity' => 5,
|
||||
'stock_0_buy' => 0,
|
||||
];
|
||||
|
||||
$xml = $this->invokeAppendCombination($repo, $product, $combination);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'<link>https://shop.example.com/sukienka-czerwona/20-170_21-175</link>',
|
||||
$xml,
|
||||
'Link feedu z seo_link musi używać "_" jako separatora par attr-val'
|
||||
);
|
||||
$this->assertStringNotContainsString(
|
||||
'20-170/21-175',
|
||||
$xml,
|
||||
'Link feedu nie może zawierać starego separatora "/" między parami atrybutów'
|
||||
);
|
||||
}
|
||||
|
||||
public function testCombinationLinkUsesUnderscoreInFallbackBranch()
|
||||
{
|
||||
$repo = $this->buildRepoWithMocks();
|
||||
$product = $this->baseProduct([
|
||||
'language' => [
|
||||
'name' => 'Sukienka czerwona',
|
||||
'xml_name' => '',
|
||||
'short_description' => 'Opis',
|
||||
'meta_title' => '',
|
||||
'seo_link' => '',
|
||||
],
|
||||
]);
|
||||
$combination = [
|
||||
'id' => 555,
|
||||
'permutation_hash' => '20-170|21-175',
|
||||
'price_brutto' => 120,
|
||||
'price_brutto_promo' => 0,
|
||||
'quantity' => 5,
|
||||
'stock_0_buy' => 0,
|
||||
];
|
||||
|
||||
$xml = $this->invokeAppendCombination($repo, $product, $combination);
|
||||
|
||||
// Fallback uses "p-{id}-{seo(name)}/...". Helpers::seo stub returns input unchanged.
|
||||
$this->assertStringContainsString(
|
||||
'<link>https://shop.example.com/p-123-Sukienka czerwona/20-170_21-175</link>',
|
||||
$xml,
|
||||
'Link fallback (bez seo_link) musi używać "_" jako separatora par attr-val'
|
||||
);
|
||||
}
|
||||
|
||||
public function testCombinationLinkWithSinglePair()
|
||||
{
|
||||
$repo = $this->buildRepoWithMocks();
|
||||
$product = $this->baseProduct();
|
||||
$combination = [
|
||||
'id' => 555,
|
||||
'permutation_hash' => '20-170',
|
||||
'price_brutto' => 120,
|
||||
'price_brutto_promo' => 0,
|
||||
'quantity' => 5,
|
||||
'stock_0_buy' => 0,
|
||||
];
|
||||
|
||||
$xml = $this->invokeAppendCombination($repo, $product, $combination);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'<link>https://shop.example.com/sukienka-czerwona/20-170</link>',
|
||||
$xml,
|
||||
'Pojedyncza para attr-val pozostaje bez zmian (str_replace nie ma co podmieniać)'
|
||||
);
|
||||
}
|
||||
}
|
||||
83
tests/Unit/Shared/Helpers/HelpersRoutingTest.php
Normal file
83
tests/Unit/Shared/Helpers/HelpersRoutingTest.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
namespace Tests\Unit\Shared\Helpers;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Phase 18 — testy regex routingu pp_routes dla URL produktów z permutacją.
|
||||
*
|
||||
* Helpers::htacces() generuje pattern dla każdego produktu z permutacją.
|
||||
* Pattern używa klasy znakowej [0-9_-]+, żeby dopasować segment "20-170_21-175"
|
||||
* w jednym kawałku (separator pomiędzy parami atrybutów to "_", nie "/").
|
||||
*
|
||||
* Testy nie wywołują htacces() (zbyt duże zależności), tylko weryfikują:
|
||||
* 1. Wzorzec literałem [0-9_-]+ występuje w generatorze pp_routes (file content)
|
||||
* 2. Wzorzec przyjmuje URL z "_" i odrzuca wariant ze "/"
|
||||
*/
|
||||
class HelpersRoutingTest extends TestCase
|
||||
{
|
||||
private $helpersSource;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->helpersSource = file_get_contents(
|
||||
__DIR__ . '/../../../../autoload/Shared/Helpers/Helpers.php'
|
||||
);
|
||||
}
|
||||
|
||||
public function testHelpersGeneratorUsesPermutationCharClassWithUnderscore()
|
||||
{
|
||||
// Liczba miejsc, gdzie pattern produktu z permutacją używa nowej klasy znaków.
|
||||
$newPattern = substr_count($this->helpersSource, '/([0-9_-]+)$');
|
||||
$this->assertGreaterThanOrEqual(
|
||||
2,
|
||||
$newPattern,
|
||||
'Helpers.php musi zawierać dwa wystąpienia /([0-9_-]+)$ (gałąź seo_link i fallback p-id-name)'
|
||||
);
|
||||
|
||||
// Stary wzorzec [0-9-]+ nie powinien już występować jako finalny segment URL.
|
||||
$this->assertStringNotContainsString(
|
||||
'/([0-9-]+)$',
|
||||
$this->helpersSource,
|
||||
'Stary wzorzec /([0-9-]+)$ został zastąpiony przez /([0-9_-]+)$ — nie powinno go już być w generatorze pp_routes'
|
||||
);
|
||||
}
|
||||
|
||||
public function testRegexMatchesUrlWithUnderscoreSeparator()
|
||||
{
|
||||
$pattern = '#^slug-produktu/([0-9_-]+)$#';
|
||||
$matches = [];
|
||||
|
||||
$this->assertSame(
|
||||
1,
|
||||
preg_match($pattern, 'slug-produktu/20-170_21-175', $matches),
|
||||
'Nowy wzorzec musi dopasować URL z "_" jako separatorem par atrybutów'
|
||||
);
|
||||
$this->assertSame('20-170_21-175', $matches[1]);
|
||||
}
|
||||
|
||||
public function testRegexRejectsLegacyUrlWithSlashSeparator()
|
||||
{
|
||||
$pattern = '#^slug-produktu/([0-9_-]+)$#';
|
||||
|
||||
$this->assertSame(
|
||||
0,
|
||||
preg_match($pattern, 'slug-produktu/20-170/21-175'),
|
||||
'Wzorzec NIE powinien dopasować starego URL ze "/" — taki URL ma trafiać do innego routingu lub 404'
|
||||
);
|
||||
}
|
||||
|
||||
public function testRegexMatchesSinglePairUrl()
|
||||
{
|
||||
$pattern = '#^slug-produktu/([0-9_-]+)$#';
|
||||
$matches = [];
|
||||
|
||||
$this->assertSame(
|
||||
1,
|
||||
preg_match($pattern, 'slug-produktu/20-170', $matches),
|
||||
'Wzorzec dopasowuje też URL z jedną parą attr-val'
|
||||
);
|
||||
$this->assertSame('20-170', $matches[1]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user