Files
Gnuboard7/modules/_bundled/sirsoft-ecommerce/tests/Feature/Seo/EcommerceSitemapIndexTest.php
T
HeuJung 1629684156 feat(seo): 사이트맵 증분 저장소 + 리소스 리스너 재배선
사이트맵을 리소스 단위로 영속화(sitemap_urls)해 진정한 증분 갱신과
도메인 재쿼리 없는 파일 재작성을 구현했다. 리소스 공개/비공개/삭제 시
해당 항목만 색인이 갱신되고, 관리자=full·스케줄러=auto·리소스 변경=증분으로 동작한다.

- sitemap_urls 마이그레이션 + SitemapUrl 모델. loc(2048) 를 unique 에 직접 넣으면
 InnoDB 키 길이를 초과하므로 loc_hash(sha256) 컬럼을 identity 로 사용
- SitemapUrlRepository(멱등 upsert / remove / 스트리밍) + SitemapIndexer
 (+ 제3자용 sitemap.index.collect_for_resource 필터 훅)
- 4 리스너 재배선: 공개→색인 / 비공개·삭제→색인 해제 + 재생성 잡 디바운스 디스패치.
 3 기여자가 resource_type/resource_id 를 함께 emit 해 전체·증분 identity 를 일치
- 재생성 모드를 SitemapGenerationMode Backed Enum 으로 통일(Command→Job→Manager)
- seo:generate-sitemap 에 --rebuild / --mode 옵션 추가
2026-07-18 16:19:49 +09:00

136 lines
5.7 KiB
PHP

<?php
namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Seo;
use App\Jobs\GenerateSitemapJob;
use App\Models\SitemapUrl;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Config;
use Modules\Sirsoft\Ecommerce\Enums\ProductDisplayStatus;
use Modules\Sirsoft\Ecommerce\Listeners\SeoCategoryCacheListener;
use Modules\Sirsoft\Ecommerce\Listeners\SeoProductCacheListener;
use Tests\TestCase;
/**
* Ecommerce 리스너 사이트맵 증분 색인 테스트 (S4 ⑲)
*
* 검증 목적 (DoD: 공개→append / 비공개→remove):
* - 전시 상품 생성/수정 → 색인(append), 숨김/삭제 → 색인 제거
* - 활성 카테고리 변경 → 색인, 비활성/삭제 → 색인 제거
* - 리스너가 사이트맵 재생성 잡을 디스패치
*/
class EcommerceSitemapIndexTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Bus::fake([GenerateSitemapJob::class]);
}
/**
* 전시 상품 생성 시 사이트맵에 색인(append)되고 잡이 디스패치된다.
*/
public function test_visible_product_create_appends_to_sitemap(): void
{
$product = (object) ['id' => 42, 'display_status' => ProductDisplayStatus::VISIBLE, 'updated_at' => now()];
(new SeoProductCacheListener)->onProductCreate($product);
$row = SitemapUrl::where('resource_type', 'product')->where('resource_id', '42')->first();
$this->assertNotNull($row, '전시 상품은 사이트맵에 색인돼야 합니다.');
$this->assertStringEndsWith('/products/42', $row->loc);
$this->assertSame('sirsoft-ecommerce', $row->contributor);
Bus::assertDispatched(GenerateSitemapJob::class);
}
/**
* 숨김 상품으로 수정 시 사이트맵 색인이 제거(remove)된다.
*/
public function test_hidden_product_update_removes_from_sitemap(): void
{
// 먼저 전시로 색인
(new SeoProductCacheListener)->onProductCreate(
(object) ['id' => 7, 'display_status' => ProductDisplayStatus::VISIBLE, 'updated_at' => now()]
);
$this->assertSame(1, SitemapUrl::where('resource_id', '7')->count());
// 숨김으로 전환
(new SeoProductCacheListener)->onProductUpdate(
(object) ['id' => 7, 'display_status' => ProductDisplayStatus::HIDDEN, 'updated_at' => now()]
);
$this->assertSame(0, SitemapUrl::where('resource_id', '7')->count(), '숨김 상품은 색인에서 제거돼야 합니다.');
}
/**
* 상품 삭제 시 사이트맵 색인이 제거된다.
*/
public function test_product_delete_removes_from_sitemap(): void
{
(new SeoProductCacheListener)->onProductCreate(
(object) ['id' => 9, 'display_status' => ProductDisplayStatus::VISIBLE, 'updated_at' => now()]
);
$this->assertSame(1, SitemapUrl::where('resource_id', '9')->count());
(new SeoProductCacheListener)->onProductDelete((object) ['id' => 9]);
$this->assertSame(0, SitemapUrl::where('resource_type', 'product')->where('resource_id', '9')->count());
}
/**
* 활성 카테고리 변경 시 색인, 비활성 시 색인 제거.
*/
public function test_category_visibility_toggles_index(): void
{
$listener = new SeoCategoryCacheListener;
$listener->onCategoryChange((object) ['id' => 3, 'slug' => 'shoes', 'is_active' => true, 'updated_at' => now()]);
$row = SitemapUrl::where('resource_type', 'category')->where('resource_id', '3')->first();
$this->assertNotNull($row);
$this->assertStringContainsString('/category/shoes', $row->loc);
$listener->onCategoryChange((object) ['id' => 3, 'slug' => 'shoes', 'is_active' => false, 'updated_at' => now()]);
$this->assertSame(0, SitemapUrl::where('resource_type', 'category')->where('resource_id', '3')->count());
}
/**
* 카테고리 삭제(ID 전달) 시 색인이 제거된다.
*/
public function test_category_delete_removes_index(): void
{
$listener = new SeoCategoryCacheListener;
$listener->onCategoryChange((object) ['id' => 4, 'slug' => 'bags', 'is_active' => true, 'updated_at' => now()]);
$this->assertSame(1, SitemapUrl::where('resource_type', 'category')->where('resource_id', '4')->count());
// after_delete 훅은 모델이 아닌 ID(int)를 전달
$listener->onCategoryDelete(4);
$this->assertSame(0, SitemapUrl::where('resource_type', 'category')->where('resource_id', '4')->count());
}
/**
* 'SEO 제공 페이지(상품 상세)' 토글이 OFF 면 전시 상품이어도 색인이 제거된다.
*
* 리스너의 공개상태 판정은 전시 여부 AND 토글을 함께 본다(기여자 getUrlsLazy 와 일치).
*/
public function test_product_detail_toggle_off_removes_index_even_when_visible(): void
{
$listener = new SeoProductCacheListener;
// 토글 ON 상태로 먼저 색인
$listener->onProductCreate((object) ['id' => 50, 'display_status' => ProductDisplayStatus::VISIBLE, 'updated_at' => now()]);
$this->assertSame(1, SitemapUrl::where('resource_id', '50')->count());
// 토글 OFF → 전시 상태여도 색인 제거
Config::set('g7_settings.modules.sirsoft-ecommerce.seo.seo_product_detail', false);
$listener->onProductUpdate((object) ['id' => 50, 'display_status' => ProductDisplayStatus::VISIBLE, 'updated_at' => now()]);
$this->assertSame(0, SitemapUrl::where('resource_id', '50')->count(), 'seo_product_detail OFF 면 전시 상품도 색인에서 제거돼야 합니다.');
}
}