Files
Gnuboard7/tests/Feature/Seo/JaLocaleSeoIntegrationTest.php
HeuJung 124433148c feat(seo): 사이트맵 재생성 알림·폴링 폴백 + 코어 7.0.6 확정
이슈 후속(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.
2026-07-19 18:57:51 +09:00

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();
}
}