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.
This commit is contained in:
1 parent
3dd34104cc
commit
124433148c
35 files changed
+698
-53
No files matched your search
@@ -47,6 +47,20 @@ class GenerateSitemapJobTest extends TestCase
|
||||
$this->assertSame(120, $job->backoff);
|
||||
}
|
||||
|
||||
/**
|
||||
* 전용 'sitemap' 큐로 라우팅되는지 확인합니다.
|
||||
*
|
||||
* default 큐와 분리해, 긴 생성 작업이 방송/알림을 막지 않고(실시간 진행상황),
|
||||
* 이 큐 워커 1개 제약으로 동시 중복 생성을 차단합니다.
|
||||
*/
|
||||
public function test_job_routes_to_dedicated_sitemap_queue(): void
|
||||
{
|
||||
$job = new GenerateSitemapJob;
|
||||
|
||||
$this->assertSame('sitemap', GenerateSitemapJob::QUEUE);
|
||||
$this->assertSame('sitemap', $job->queue);
|
||||
}
|
||||
|
||||
/**
|
||||
* ShouldBeUnique 를 구현하여 동시 실행을 막는지 확인합니다.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Listeners;
|
||||
|
||||
use App\Listeners\SeoNotificationDataListener;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* SeoNotificationDataListener 단위 테스트.
|
||||
*
|
||||
* 사이트맵 재생성 알림의 핵심 정책 — 관리자 수동 실행(triggered_by 존재)만 발송하고,
|
||||
* 스케줄러/증분/봇(triggered_by null)은 context.skip 으로 제외 — 을 검증합니다.
|
||||
*/
|
||||
class SeoNotificationDataListenerTest extends TestCase
|
||||
{
|
||||
private SeoNotificationDataListener $listener;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->listener = new SeoNotificationDataListener;
|
||||
}
|
||||
|
||||
private function default(): array
|
||||
{
|
||||
return ['notifiable' => null, 'notifiables' => null, 'data' => [], 'context' => []];
|
||||
}
|
||||
|
||||
public function test_manual_completion_extracts_data_and_trigger_user(): void
|
||||
{
|
||||
$result = ['status' => 'updated', 'success' => true, 'data' => ['url_count' => 720, 'child_count' => 3]];
|
||||
|
||||
$out = $this->listener->extractData($this->default(), 'sitemap_regenerated', [$result, 42]);
|
||||
|
||||
$this->assertSame(42, $out['context']['trigger_user_id']);
|
||||
$this->assertArrayNotHasKey('skip', $out['context']);
|
||||
$this->assertSame('720', $out['data']['url_count']);
|
||||
$this->assertSame('3', $out['data']['child_count']);
|
||||
$this->assertArrayHasKey('action_url', $out['data']);
|
||||
}
|
||||
|
||||
public function test_non_manual_completion_is_skipped(): void
|
||||
{
|
||||
$result = ['status' => 'updated', 'success' => true, 'data' => ['url_count' => 720]];
|
||||
|
||||
// triggered_by 없음(스케줄러/증분/봇) → skip
|
||||
$out = $this->listener->extractData($this->default(), 'sitemap_regenerated', [$result, null]);
|
||||
|
||||
$this->assertTrue($out['context']['skip']);
|
||||
$this->assertSame([], $out['data']);
|
||||
}
|
||||
|
||||
public function test_manual_failure_extracts_error_and_trigger_user(): void
|
||||
{
|
||||
$result = ['status' => 'failed', 'success' => false, 'message' => 'disk full'];
|
||||
|
||||
$out = $this->listener->extractData($this->default(), 'sitemap_regenerate_failed', [$result, 7]);
|
||||
|
||||
$this->assertSame(7, $out['context']['trigger_user_id']);
|
||||
$this->assertSame('disk full', $out['data']['error']);
|
||||
}
|
||||
|
||||
public function test_non_manual_failure_is_skipped(): void
|
||||
{
|
||||
$out = $this->listener->extractData($this->default(), 'sitemap_regenerate_failed', [['message' => 'x'], null]);
|
||||
|
||||
$this->assertTrue($out['context']['skip']);
|
||||
}
|
||||
|
||||
public function test_unknown_type_returns_default_untouched(): void
|
||||
{
|
||||
$out = $this->listener->extractData($this->default(), 'some_other_type', [[], 1]);
|
||||
|
||||
$this->assertSame($this->default(), $out);
|
||||
}
|
||||
|
||||
public function test_subscribes_to_seo_extract_data_filter(): void
|
||||
{
|
||||
$hooks = SeoNotificationDataListener::getSubscribedHooks();
|
||||
|
||||
$this->assertArrayHasKey('core.seo.notification.extract_data', $hooks);
|
||||
$this->assertSame('filter', $hooks['core.seo.notification.extract_data']['type']);
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,12 @@ class SitemapGeneratorTest extends TestCase
|
||||
Config::set('app.locale', 'ko');
|
||||
Config::set('app.supported_locales', ['ko']);
|
||||
|
||||
// hreflang alternate 방출을 기본값(true)으로 고정한다. SitemapXmlRenderer::fromConfig()
|
||||
// 는 g7_core_settings('seo.sitemap_hreflang_enabled') 를 읽는데, 이 값은 부팅 시
|
||||
// storage/app/settings/seo.json 으로 적재되며 settings 디스크 fake 의 영향을 받지 않는다.
|
||||
// 실행 머신의 영속 설정에 다국어 hreflang 검증 결과가 좌우되지 않도록 명시 고정한다.
|
||||
Config::set('g7_settings.core.seo.sitemap_hreflang_enabled', true);
|
||||
|
||||
$this->routeResolver = Mockery::mock(TemplateRouteResolver::class);
|
||||
$this->templateService = Mockery::mock(TemplateService::class);
|
||||
|
||||
|
||||
@@ -121,10 +121,33 @@ class SitemapProgressTest extends TestCase
|
||||
$this->progress->start('full');
|
||||
|
||||
Event::assertDispatched(GenericBroadcastEvent::class, function ($event) {
|
||||
// payload 는 상태 API 봉투와 동형: 최상위 data 안에 progress/realtime_enabled
|
||||
// (target_source 가 대상 소스를 통째 교체해도 `sitemap_status.data.*` 바인딩 유지)
|
||||
return $event->channel === 'core.admin.seo.sitemap'
|
||||
&& $event->eventName === 'sitemap.progress.updated'
|
||||
&& ($event->payload['realtime_enabled'] ?? null) === true
|
||||
&& ($event->payload['progress']['status'] ?? null) === 'queued';
|
||||
&& ($event->payload['data']['realtime_enabled'] ?? null) === true
|
||||
&& ($event->payload['data']['progress']['status'] ?? null) === 'queued'
|
||||
&& array_key_exists('last_updated_at', $event->payload['data']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료 방송은 방금 커밋된 생성 시각을 data.last_updated_at 으로 실어 "마지막 생성"을 실시간 갱신한다.
|
||||
*/
|
||||
public function test_complete_broadcast_carries_new_last_updated_at(): void
|
||||
{
|
||||
$this->enableReverb();
|
||||
Event::fake([GenericBroadcastEvent::class]);
|
||||
|
||||
$this->progress->complete([
|
||||
'url_count' => 720,
|
||||
'child_count' => 1,
|
||||
'generated_at' => '2026-07-19T00:00:00+00:00',
|
||||
]);
|
||||
|
||||
Event::assertDispatched(GenericBroadcastEvent::class, function ($event) {
|
||||
return ($event->payload['data']['last_updated_at'] ?? null) === '2026-07-19T00:00:00+00:00'
|
||||
&& ($event->payload['data']['progress']['status'] ?? null) === 'completed';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace Tests\Unit\Upgrades;
|
||||
|
||||
require_once __DIR__.'/../../../upgrades/data/7.1.0/migrations/01_NullifyUntouchedSeoCacheOverrides.php';
|
||||
require_once __DIR__.'/../../../upgrades/data/7.0.6/migrations/01_NullifyUntouchedSeoCacheOverrides.php';
|
||||
|
||||
use App\Extension\UpgradeContext;
|
||||
use App\Upgrades\Data\V7_1_0\Migrations\NullifyUntouchedSeoCacheOverrides;
|
||||
use App\Upgrades\Data\V7_0_6\Migrations\NullifyUntouchedSeoCacheOverrides;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 7.1.0 업그레이드 스텝 — SEO 캐시 오버라이드 이행 (결정 D19)
|
||||
* 7.0.6 업그레이드 스텝 — SEO 캐시 오버라이드 이행 (결정 D19)
|
||||
*
|
||||
* 판정 규칙:
|
||||
* 옛 기본값과 같으면 "안 건드린 것" → null(미설정)로 비움
|
||||
@@ -40,7 +40,7 @@ class NullifyUntouchedSeoCacheOverridesTest extends TestCase
|
||||
*/
|
||||
private function context(): UpgradeContext
|
||||
{
|
||||
return new UpgradeContext('7.0.5', '7.1.0', '7.1.0');
|
||||
return new UpgradeContext('7.0.5', '7.0.6', '7.0.6');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user