Windows 는 하위 트리에 열린 핸들이 하나라도 있으면 디렉토리 rename 을 막는다. 잠금 프로세스를 찾아 종료하던 기존 대응은 디렉토리 핸들을 감지하지 못했고 사용 중인 편집기를 예고 없이 죽였다. rename 이 막히면 파일 단위 연산으로 폴백해 어떤 프로세스도 건드리지 않고 교체를 끝낸다. 커밋 전 점검에서 pint.json 이 코어 업데이트 대상에 빠져 있던 것도 함께 등재했다. 이커머스는 기본 제공 통화 삭제가 저장 응답에서 즉시 부활했다. 항목 단위 보충 병합이 "소실" 과 "의도적 삭제" 를 구분하지 못한 탓이라, 삭제 의도를 서버가 도출해 저장본에 기록하고 병합이 그 기록을 존중하게 했다. 그 통화로 결제된 과거 주문의 표기가 흔들리지 않도록 소수 자릿수 해석도 스냅샷 우선으로 바꿨다 — 금액은 원래 스냅샷 환율을 써 안전했으나 자릿수만 현재 설정을 봤다.
133 lines
5.6 KiB
PHP
133 lines
5.6 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Seo;
|
|
|
|
use App\Jobs\GenerateSitemapJob;
|
|
use App\Models\SitemapUrl;
|
|
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 Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
|
|
|
|
/**
|
|
* Ecommerce 리스너 사이트맵 증분 색인 테스트 (S4 ⑲)
|
|
*
|
|
* 검증 목적 (DoD: 공개→append / 비공개→remove):
|
|
* - 전시 상품 생성/수정 → 색인(append), 숨김/삭제 → 색인 제거
|
|
* - 활성 카테고리 변경 → 색인, 비활성/삭제 → 색인 제거
|
|
* - 리스너가 사이트맵 재생성 잡을 디스패치
|
|
*/
|
|
class EcommerceSitemapIndexTest extends ModuleTestCase
|
|
{
|
|
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 면 전시 상품도 색인에서 제거돼야 합니다.');
|
|
}
|
|
}
|