이슈 후속(S7) 마감 + 릴리스 버전 확정. 기능(S7): - 사이트맵 전체 재생성 완료/실패 시 실행 관리자에게 알림(수동 실행 한정). 스케줄러·증분·봇 재생성은 알림 없음(triggered_by 부재 시 skip). - 최종 실패 전용 알림 + 재생성 전용 큐 + WS 진행상황 payload 봉투 정정. - Reverb OFF 폴링 폴백 경로 수정(실브라우저 폴링에서만 드러난 결함). 버전(D28 — 정정, D20 번복): - 릴리스를 7.0.5→7.0.6(patch)로 확정. 7.1.0(minor)은 다른 브랜치에서 별도 진행. - config/app.php + .env + README + INSTALL + CHANGELOG + 모듈 3 requires 를 7.0.6 로. - D19 SEO 캐시 이행 업그레이드 스텝을 Upgrade_7_0_6 로 리네임. 이유: 스텝 버전 게이팅이 version<=toVersion 이라, 7.1.0 이름이면 7.0.5→7.0.6 업그레이드에서 이행이 실행되지 않는다. 검증: composer test-smoke OK(2·29) · 재생성 알림/업그레이드 이행/진행상황 테스트 green · version-sync/version-bump(new-bump)/extension-require green · audit 0 error.
192 lines
7.6 KiB
PHP
192 lines
7.6 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Seo;
|
|
|
|
use App\Seo\Contracts\SitemapContributorInterface;
|
|
use App\Seo\SitemapGenerator;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Mockery;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* 일본어(ja) 언어팩 활성화 시 SEO/sitemap 전 영역에 다국어 이슈가 없는지 검증.
|
|
*
|
|
* `LanguagePackServiceProvider::refreshSupportedLocales()` 가 활성 코어 locale 을
|
|
* `config('app.supported_locales')` 에 동적 주입하면 SitemapGenerator/SeoRenderer/
|
|
* SeoMiddleware 가 자동으로 ja 를 소비해야 한다. 본 테스트는 supported_locales 에 ja 가
|
|
* 포함된 상태에서 다음을 보장한다:
|
|
*
|
|
* 1. Sitemap XML 의 모든 contributor URL 이 ja 로케일 URL 과 xhtml:link hreflang ja 로 확장된다.
|
|
* 2. SitemapGenerator 의 다국어 분기가 활성화된다 (xhtml 네임스페이스 + 로케일별 <url>).
|
|
* 3. SeoMiddleware 가 ?locale=ja 를 유효 로케일로 받아들이고 fallback 하지 않는다.
|
|
* 4. SeoMiddleware 가 ?locale=ja 캐시를 ko/en 캐시와 격리한다.
|
|
*
|
|
* 이 테스트는 ja 가 supported_locales 에 추가되기만 하면 — 실제 ja LanguagePack DB row
|
|
* 유무와 무관하게 — 다운스트림 SEO 계층이 ja 를 인식해야 한다는 계약을 굳힌다.
|
|
*/
|
|
class JaLocaleSeoIntegrationTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// ja 가 supported_locales 에 포함된 상태를 직접 주입 (LanguagePackServiceProvider
|
|
// 의 refreshSupportedLocales() 결과와 동등). bot 감지 + 캐시 격리 검증을 위해
|
|
// 봇 UA 가 사용 가능한 상태로 둔다.
|
|
Config::set('app.locale', 'ko');
|
|
Config::set('app.supported_locales', ['ko', 'en', 'ja']);
|
|
Config::set('app.locale_names', [
|
|
'ko' => '한국어',
|
|
'en' => 'English',
|
|
'ja' => '日本語',
|
|
]);
|
|
Config::set('app.translatable_locales', ['ko', 'en', 'ja']);
|
|
|
|
// hreflang alternate 방출을 기본값(true)으로 고정한다. fromConfig() 가 읽는
|
|
// g7_core_settings('seo.sitemap_hreflang_enabled') 는 부팅 시 영속 설정에서 적재되어
|
|
// settings 디스크 fake 의 영향을 받지 않으므로, 다국어 hreflang 검증이 실행 머신의
|
|
// 설정 상태에 좌우되지 않도록 명시 고정한다.
|
|
Config::set('g7_settings.core.seo.sitemap_hreflang_enabled', true);
|
|
}
|
|
|
|
public function test_sitemap_xml_includes_ja_locale_urls(): void
|
|
{
|
|
/** @var SitemapGenerator $generator */
|
|
$generator = app(SitemapGenerator::class);
|
|
|
|
$contributor = Mockery::mock(SitemapContributorInterface::class);
|
|
$contributor->shouldReceive('getIdentifier')->andReturn('ja-test');
|
|
$contributor->shouldReceive('getUrls')->andReturn([
|
|
['url' => '/about', 'changefreq' => 'monthly'],
|
|
]);
|
|
|
|
$generator->registerContributor($contributor);
|
|
$xml = $generator->generate();
|
|
|
|
$base = url('/about');
|
|
|
|
// 다국어 모드 → xhtml 네임스페이스 활성화
|
|
$this->assertStringContainsString(
|
|
'xmlns:xhtml="http://www.w3.org/1999/xhtml"',
|
|
$xml,
|
|
'supported_locales 가 1개 초과면 xhtml 네임스페이스가 sitemap 에 포함되어야 함'
|
|
);
|
|
|
|
// ja 로케일 <url> 항목 — ko 가 default 이므로 ja 는 ?locale=ja 형태
|
|
$this->assertStringContainsString(
|
|
'<loc>'.$base.'?locale=ja</loc>',
|
|
$xml,
|
|
'ja 가 supported_locales 에 있으면 sitemap 에 ja 로케일 <url> 이 생성되어야 함'
|
|
);
|
|
|
|
// ja hreflang alternate (다른 로케일들의 <url> 안에 ja 가 alternate 로 노출)
|
|
$this->assertStringContainsString(
|
|
'<xhtml:link rel="alternate" hreflang="ja" href="'.$base.'?locale=ja"/>',
|
|
$xml,
|
|
'ja hreflang alternate 가 모든 <url> 항목에 노출되어야 함'
|
|
);
|
|
|
|
// <url> 태그가 3개 이상 (ko/en/ja)
|
|
$this->assertGreaterThanOrEqual(
|
|
3,
|
|
substr_count($xml, '<url>'),
|
|
'ko/en/ja 3개 로케일 <url> 항목이 모두 생성되어야 함'
|
|
);
|
|
}
|
|
|
|
public function test_sitemap_xml_default_locale_url_has_no_query_string(): void
|
|
{
|
|
/** @var SitemapGenerator $generator */
|
|
$generator = app(SitemapGenerator::class);
|
|
|
|
$contributor = Mockery::mock(SitemapContributorInterface::class);
|
|
$contributor->shouldReceive('getIdentifier')->andReturn('default-loc-test');
|
|
$contributor->shouldReceive('getUrls')->andReturn([
|
|
['url' => '/contact'],
|
|
]);
|
|
|
|
$generator->registerContributor($contributor);
|
|
$xml = $generator->generate();
|
|
|
|
// 기본 ko 로케일은 ?locale=ko 가 아니라 clean URL
|
|
$this->assertStringContainsString(
|
|
'<loc>'.url('/contact').'</loc>',
|
|
$xml,
|
|
'기본 로케일(ko) URL 은 ?locale 파라미터 없이 clean URL 이어야 함'
|
|
);
|
|
$this->assertStringNotContainsString(
|
|
'?locale=ko',
|
|
$xml,
|
|
'기본 로케일 URL 에는 ?locale=ko 가 추가되지 않아야 함 (회피 — clean URL 정책)'
|
|
);
|
|
}
|
|
|
|
public function test_seo_middleware_accepts_ja_locale_query(): void
|
|
{
|
|
// 봇 요청으로 SEO 렌더링 트리거
|
|
Cache::flush();
|
|
|
|
$response = $this->get('/?locale=ja', [
|
|
'User-Agent' => 'Googlebot/2.1 (+http://www.google.com/bot.html)',
|
|
]);
|
|
|
|
// 200 응답이거나 (렌더링 성공) — 401/403/404 가 아니어야 함 (ja 가 거부되지 않았음)
|
|
$this->assertContains(
|
|
$response->status(),
|
|
[200, 301, 302],
|
|
'ja 가 supported_locales 에 있으면 SeoMiddleware 가 거부하지 않아야 함 (현재 status='.$response->status().')'
|
|
);
|
|
}
|
|
|
|
public function test_unsupported_locale_still_falls_back_when_ja_active(): void
|
|
{
|
|
// 등록되지 않은 'fr' 은 여전히 거부되고 default 로 fallback
|
|
Cache::flush();
|
|
|
|
$response = $this->get('/?locale=fr', [
|
|
'User-Agent' => 'Googlebot/2.1 (+http://www.google.com/bot.html)',
|
|
]);
|
|
|
|
$this->assertContains(
|
|
$response->status(),
|
|
[200, 301, 302],
|
|
'미지원 locale(fr) 도 fallback 으로 정상 응답해야 함'
|
|
);
|
|
}
|
|
|
|
public function test_seo_cache_isolated_for_ja_locale(): void
|
|
{
|
|
Cache::flush();
|
|
|
|
// ja 로케일과 ko 로케일이 같은 URL 에서 별도 캐시 키를 사용하는지 검증
|
|
$cacheManager = app(\App\Seo\SeoCacheManager::class);
|
|
|
|
$reflection = new \ReflectionClass($cacheManager);
|
|
if (! $reflection->hasMethod('buildKey')) {
|
|
$this->markTestSkipped('SeoCacheManager::buildKey not accessible');
|
|
}
|
|
|
|
$build = $reflection->getMethod('buildKey');
|
|
$build->setAccessible(true);
|
|
|
|
$url = 'https://example.test/products';
|
|
$keyKo = $build->invoke($cacheManager, $url, 'ko');
|
|
$keyEn = $build->invoke($cacheManager, $url, 'en');
|
|
$keyJa = $build->invoke($cacheManager, $url, 'ja');
|
|
|
|
$this->assertNotSame($keyKo, $keyJa, 'ja 캐시 키는 ko 와 격리되어야 함');
|
|
$this->assertNotSame($keyEn, $keyJa, 'ja 캐시 키는 en 과 격리되어야 함');
|
|
$this->assertNotSame($keyKo, $keyEn, 'ko/en 캐시 키도 격리되어 있어야 함 (회귀 가드)');
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Mockery::close();
|
|
parent::tearDown();
|
|
}
|
|
}
|