+ @if(!$freeNodeLayout && !$appleIdLayout) + @include('components.resource-history-link') + @endif @if($freeNodeLayout) @include('pages.articles.freenode.content') @elseif($appleIdLayout) @@ -88,6 +91,8 @@ @if($oArticle->oCategory?->sSlug === 'freenode') @include('components.newcomer-guide-link') + @elseif($oArticle->oCategory?->sSlug === 'shadowrocketids') + @include('components.resource-explainer-link', ['resourceKind' => 'shadowrocketids']) @endif @if ($oArticle->bPageThumb && $oArticle->sThumb)

diff --git a/resources/views/pages/articles/default/unavailable.blade.php b/resources/views/pages/articles/default/unavailable.blade.php index 4e99455a..6042d07b 100644 --- a/resources/views/pages/articles/default/unavailable.blade.php +++ b/resources/views/pages/articles/default/unavailable.blade.php @@ -1 +1 @@ -

资源暂不可用

暂时没有可展示的节点记录,请稍后再试。

查看新手教程
+

资源暂不可用

暂时没有可展示的{{ ($resourceType ?? 'freenode') === 'shadowrocketids' ? '账号' : '节点' }}记录,请稍后再试。

查看新手教程
diff --git a/resources/views/pages/articles/freenode/content.blade.php b/resources/views/pages/articles/freenode/content.blade.php index c62edb19..5dc95c1b 100644 --- a/resources/views/pages/articles/freenode/content.blade.php +++ b/resources/views/pages/articles/freenode/content.blade.php @@ -1,15 +1,18 @@ @if($freeNodeLayout['intro'])
{!! nl2br(\App\Services\Seo::body($freeNodeLayout['intro'])) !!}
@endif +@include('components.newcomer-guide-link')
-

获取免费订阅

- @include('components.newcomer-guide-link') +

获取免费订阅

按你使用的软件,复制对应订阅。

@if($freeNodeLayout['count'] === 0)

这篇更新记录的可用节点为 0,导入前可先查看最新更新。

@endif
@foreach($freeNodeLayout['subscriptions'] as $subscription)
-

适用于:{{ $subscription['title'] }}

-
{{ $subscription['url'] }}
+
+

{{ $subscription['title'] }}

+
{{ $subscription['url'] }}
+
+
@endforeach
diff --git a/resources/views/pages/articles/freenode/header.blade.php b/resources/views/pages/articles/freenode/header.blade.php index 634056cd..0835908a 100644 --- a/resources/views/pages/articles/freenode/header.blade.php +++ b/resources/views/pages/articles/freenode/header.blade.php @@ -6,11 +6,16 @@

{{ $oArticle->created_at?->format('n月j日') }}免费节点订阅

-

支持 Clash / V2ray / Shadowrocket / Singbox / Hiddify

@if($oArticle->sThumb) - 猫咪主题插画 + 猫咪主题插画 @endif +

支持 Clash / V2ray / Shadowrocket / Singbox / Hiddify

+ @unless($bEditorialPreview ?? false) + + @endunless
@if($freeNodeLayout['count'] !== null) @@ -21,4 +26,3 @@ @endif
覆盖地区
@forelse($freeNodeLayout['regions'] as $region){{ $region }}@empty以订阅内容为准@endforelse
- diff --git a/routes/web.php b/routes/web.php index 39bd3a19..92cdd257 100644 --- a/routes/web.php +++ b/routes/web.php @@ -47,6 +47,11 @@ Route::get('/a/{sArticleSlug}.html', [ArticlesController::class, 'show']) ->name('article.show'); +Route::get('/freenode', [ArticlesController::class, 'resource'])->defaults('type', 'freenode')->name('resources.freenode'); +Route::get('/shadowrocketids', [ArticlesController::class, 'resource'])->defaults('type', 'shadowrocketids')->name('resources.shadowrocketids'); +Route::get('/history/{type?}', [ArticlesController::class, 'history']) + ->where('type', 'freenode|shadowrocketids')->name('resources.history'); + Route::get('/{sSlug1}', [ArticlesController::class, 'list']) ->name('articles.list'); Route::get('/{sSlug1}/{sSlug2?}', [ArticlesController::class, 'list']) diff --git a/tests/Feature/QuietModeTest.php b/tests/Feature/QuietModeTest.php new file mode 100644 index 00000000..c36d60c0 --- /dev/null +++ b/tests/Feature/QuietModeTest.php @@ -0,0 +1,143 @@ +state = sys_get_temp_dir().'/fna-quiet-'.bin2hex(random_bytes(8)).'.json'; + config(['site_mode.state_path'=>$this->state, 'site_mode.quiet'=>false, + 'cache.default'=>'array', 'responsecache.enabled'=>false, + 'database.default'=>'sqlite', 'database.connections.sqlite.database'=>':memory:', + 'scout.driver'=>null, 'editorial.indexnow_enabled'=>false]); + DB::purge('sqlite'); Http::preventStrayRequests(); + Schema::create('categories', function (Blueprint $t) { + $t->id(); $t->string('sSlug'); $t->string('sName'); + }); + DB::table('categories')->insert([ + ['id'=>1,'sSlug'=>'freenode','sName'=>'免费节点'], + ['id'=>2,'sSlug'=>'shadowrocketids','sName'=>'免费小火箭ID'], + ['id'=>3,'sSlug'=>'client','sName'=>'新手教程'], + ]); + Schema::create('articles', function (Blueprint $t) { + $t->id(); $t->integer('iCategoryId'); $t->string('sSlug'); + $t->text('sContent'); $t->integer('iLikeCount')->default(0); $t->timestamps(); + }); + DB::table('articles')->insert(['id'=>7,'iCategoryId'=>1,'sSlug'=>'m7','sContent'=>'original']); + } + + protected function tearDown(): void + { + if (isset($this->state) && is_file($this->state)) unlink($this->state); + parent::tearDown(); + } + + private function paused(callable $action): void + { + try { $action(); $this->fail('Expected paused response.'); } + catch (HttpResponseException $e) { + $this->assertSame(503, $e->getResponse()->getStatusCode()); + $this->assertSame('3600', $e->getResponse()->headers->get('Retry-After')); + $this->assertSame('FREE_NODE_UPDATES_PAUSED', json_decode($e->getResponse()->getContent(), true)['code']); + } + } + + public function test_command_switch_is_persistent_reversible_and_private(): void + { + $this->assertFalse(SiteMode::quiet()); + $this->artisan('site:quiet on')->assertExitCode(0); + $this->assertTrue(SiteMode::quiet()); + $this->assertSame(0600, fileperms($this->state) & 0777); + $this->artisan('site:quiet status')->assertExitCode(0); + $this->assertTrue(json_decode(file_get_contents($this->state), true)['quiet']); + $this->artisan('site:quiet off')->assertExitCode(0); + $this->assertFalse(SiteMode::quiet()); + $this->artisan('site:quiet invalid')->assertExitCode(1); + $this->assertFalse(SiteMode::quiet()); + } + + public function test_invalid_state_stays_paused_and_environment_fallback_works(): void + { + config(['site_mode.quiet'=>true]); $this->assertTrue(SiteMode::quiet()); + config(['site_mode.quiet'=>false]); + file_put_contents($this->state, 'invalid'); $this->assertTrue(SiteMode::quiet()); + file_put_contents($this->state, '{"quiet":"false"}'); $this->assertTrue(SiteMode::quiet()); + } + + public function test_receivers_pause_before_images_flows_and_any_database_write(): void + { + config(['site_mode.quiet'=>true]); + $controller = new ArticlesController(); + $this->paused(fn () => $controller->receiveFreenode(new Request())); + $this->paused(fn () => $controller->receiveFreenode_laji(new Request())); + $this->paused(fn () => $controller->receive(new Request(['_raw'=>[ + 'id'=>8, 'opt'=>json_encode(['sCategoryName'=>'免费节点']), + ]]))); + // Moving an existing node article to another category must not bypass the pause. + $this->paused(fn () => $controller->receive(new Request(['_raw'=>[ + 'id'=>7, 'opt'=>json_encode(['sCategoryName'=>'新手教程']), + ]]))); + $this->assertSame(1, DB::table('articles')->count()); + $this->assertSame('original', DB::table('articles')->where('id',7)->value('sContent')); + Http::assertNothingSent(); + } + + public function test_model_rejects_publication_changes_but_allows_metrics_and_other_types(): void + { + config(['site_mode.quiet'=>true]); + $existing = Articles::find(7); $existing->sContent = 'replacement'; + $this->paused(fn () => $existing->save()); + $this->assertSame('original', DB::table('articles')->where('id',7)->value('sContent')); + $new = new Articles(['iCategoryId'=>1,'sSlug'=>'fn-new','sContent'=>'new']); + $this->paused(fn () => $new->save()); + $metrics = Articles::find(7); $metrics->iLikeCount = 10; $metrics->save(); + $this->assertSame(10, DB::table('articles')->where('id',7)->value('iLikeCount')); + foreach ([2,3] as $id) SiteMode::guardArticle(new Articles(['iCategoryId'=>$id,'sContent'=>'unchanged scope'])); + SiteMode::assertUpdatesAllowed('shadowrocketids'); + $this->assertSame(1, DB::table('articles')->count()); + config(['site_mode.quiet'=>false]); SiteMode::assertUpdatesAllowed('freenode'); + } + + public function test_promotions_disappear_without_mutating_original_body_or_copy_payload(): void + { + $category = new Categories(['sSlug'=>'freenode']); + $article = new Articles(['sContent'=>'

机场推荐 - 福云:

优惠

机场官网:福云

订阅

复制']); + $article->setRelation('oCategory', $category); $original = $article->sContent; + config(['site_mode.quiet'=>true]); + $result = ArticleRecommendations::prepare($article); + $this->assertFalse($result['enabled']); + $this->assertStringNotContainsString('福云', $result['body']); + $this->assertStringContainsString('data-clipboard-text="example-sub"', $result['body']); + $this->assertSame($original, $article->sContent); + $this->assertSame([], ArticleRecommendations::providers()); + $this->assertStringNotContainsString('data-random-article-ad', view('components.article-ads')->render()); + $this->assertStringNotContainsString('sponsored', view('components.article-ads')->render()); + foreach (['dpl','hn','fuc'] as $provider) { + $this->assertStringNotContainsString('sponsored', view('components.'.$provider.'-article-ad')->render()); + } + $this->assertStringNotContainsString('airport-recommendations', view('components.airport-recommendations')->render()); + config(['site_mode.quiet'=>false]); + $this->assertTrue(ArticleRecommendations::prepare($article)['enabled']); + $this->assertStringContainsString('data-random-article-ad', view('components.article-ads')->render()); + $this->assertSame($original, $article->sContent); + } +} diff --git a/tests/Feature/SeoContractTest.php b/tests/Feature/SeoContractTest.php index a34fca13..52e01eb0 100644 --- a/tests/Feature/SeoContractTest.php +++ b/tests/Feature/SeoContractTest.php @@ -19,7 +19,7 @@ protected function setUp(): void parent::setUp(); config(['database.default'=>'sqlite','database.connections.sqlite.database'=>':memory:','scout.driver'=>'collection', 'app.url'=>'https://example.org','editorial.base_url'=>'https://example.org','app.site_code'=>'example.org', - 'app.site_code_short'=>'cfn','seo.brand'=>'Example','responsecache.enabled'=>false,'seo.page_size'=>2, + 'app.site_code_short'=>'cfn','seo.brand'=>'Example','responsecache.enabled'=>false,'seo.page_size'=>2,'seo.history_page_size'=>2, 'illustrations.enabled'=>false,'editorial.indexnow_enabled'=>false]); DB::purge('sqlite'); Http::preventStrayRequests(); $this->withoutVite(); $this->travelTo(\Carbon\Carbon::parse('2026-10-31 12:00:00','UTC')); @@ -29,7 +29,7 @@ protected function setUp(): void Schema::create('article_tag',function(Blueprint $t){$t->integer('iArticleId');$t->integer('iTagId');}); Schema::create('article_likes',function(Blueprint $t){$t->id();$t->integer('iArticleId');$t->string('sIp');$t->string('sFingerprint');$t->timestamps();}); (require database_path('migrations/2026_10_31_160000_add_article_seo_state.php'))->up(); - foreach([[1,'freenode','免费节点',0],[2,'client','入门教程',0],[3,'windows','Windows',2],[4,'advanced','综合教程',0]] as [$id,$slug,$name,$parent]) DB::table('categories')->insert(['id'=>$id,'sSlug'=>$slug,'sName'=>$name,'iParentId'=>$parent,'iShowChildren'=>$id===2?1:0,'aOpt'=>$id===1?' {"noindexday":7}':null]); + foreach([[1,'freenode','免费节点',0],[2,'client','入门教程',0],[3,'windows','Windows',2],[4,'advanced','综合教程',0],[5,'shadowrocketids','免费小火箭ID',0]] as [$id,$slug,$name,$parent]) DB::table('categories')->insert(['id'=>$id,'sSlug'=>$slug,'sName'=>$name,'iParentId'=>$parent,'iShowChildren'=>$id===2?1:0,'aOpt'=>$id===1?' {"noindexday":7}':null]); DB::table('tags')->insert(['id'=>1,'sSlug'=>'clash','sName'=>'Clash']); } protected function tearDown(): void { $this->travelBack(); parent::tearDown(); } @@ -88,12 +88,12 @@ public function test_home_is_stable_and_failed_refresh_does_not_replace_it(): vo public function test_new_snapshots_are_archives_and_content_clock_ignores_likes(): void { $id=$this->article('old',['iCategoryId'=>1]);$old=Articles::find($id); - $this->assertTrue(Seo::indexable($old));$this->assertNull(Seo::modified($old)); + $this->assertFalse(Seo::indexable($old));$this->assertNull(Seo::modified($old)); $new=Articles::create(['sSlug'=>'new','sTitle'=>'新的记录','sTitleSub'=>'共10个可用节点','sContent'=>'

有效资源

','iCategoryId'=>1,'iStatus'=>1]); $this->assertSame('archive',$new->sSeoPolicy);$this->assertFalse(Seo::indexable($new)); $clock=Seo::modified($new);$this->travel(1)->hours();$new->increment('iLikeCount');$this->assertSame($clock,Seo::modified($new->fresh())); $new->sContent='

更新资源内容

';$new->save();$this->assertNotSame($clock,Seo::modified($new->fresh())); - $this->get('/sitemap.xml')->assertOk()->assertDontSee('/a/new.html',false)->assertSee('/a/old.html',false); + $this->get('/sitemap.xml')->assertOk()->assertDontSee('/a/new.html',false)->assertDontSee('/a/old.html',false); $this->travelTo(\Carbon\Carbon::parse('2026-11-06 10:00:01','UTC')); $this->get('/sitemap.xml')->assertOk()->assertDontSee('/a/old.html',false); } @@ -127,7 +127,7 @@ public function test_durable_home_notifications_retry_and_deletions_need_no_http $service->changed(Articles::find($id));$service->changed(Articles::find($id));$this->assertSame(1,DB::table('seo_notifications')->count()); $service->retry();Http::assertSentCount(1);$service->retry();Http::assertSentCount(1); $this->travel(6)->minutes();$service->retry();Http::assertSentCount(2); - Http::assertSent(fn($req)=>$req['urlList']===['https://example.org/','https://example.org/freenode']); + Http::assertSent(fn($req)=>$req['urlList']===['https://example.org/']); $service->changed(Articles::find($id),true);DB::table('articles')->where('id',$id)->delete();$service->retry();Http::assertSentCount(3); Http::assertSent(fn($req)=>in_array('https://example.org/a/snapshot.html',$req['urlList'])); } @@ -146,4 +146,135 @@ public function test_commercial_attributes_and_zero_string_validation(): void ResourcePublication::validate(['title'=>'资源','title_sub'=>'数量未知','content'=>'内容','tags'=>[],'valid_count'=>'0']); } + private function appleBody(): string + { + return '

免费苹果账号

账号:test@example.invalid,密码:synthetic-test,地区:美区,检测时间:2026-10-30 10:00:00

'; + } + + private function xpath(string $html): \DOMXPath + { + $dom=new \DOMDocument();@$dom->loadHTML($html);return new \DOMXPath($dom); + } + + public function test_id_entrance_uses_latest_nonempty_snapshot_without_changing_records(): void + { + $good=$this->article('id-good',['iCategoryId'=>5,'sContent'=>$this->appleBody()]); + $this->article('id-pinned',['iCategoryId'=>5,'sContent'=>$this->appleBody(),'iIsTop'=>1,'created_at'=>'2026-10-01 10:00:00']); + foreach(['empty'=>['sContent'=>'

免费苹果账号

'], 'unknown'=>['sContent'=>'

格式未知

'], + 'failed'=>['sResourceStatus'=>'failed'], 'draft'=>['iStatus'=>0], 'future'=>['created_at'=>'2026-11-01 00:00:00']] as $slug=>$extra) { + $this->article('id-'.$slug,$extra+['iCategoryId'=>5,'sContent'=>$this->appleBody(),'created_at'=>'2026-10-31 11:00:00']); + } + $before=DB::table('articles')->orderBy('id')->get()->toJson(); + $this->assertSame('id-good',app(ArticlesService::class)->getLatestResourceSlug('shadowrocketids')); + $html=$this->get('/shadowrocketids')->assertOk()->assertSee('查看往期资源(免费小火箭ID)')->getContent(); + $xp=$this->xpath($html); + $this->assertSame('https://example.org/shadowrocketids',$xp->query('//link[@rel="canonical"]')->item(0)->getAttribute('href')); + $this->assertSame('index, follow',$xp->query('//meta[@name="robots"]')->item(0)->getAttribute('content')); + $this->assertSame('https://example.org/shadowrocketids',$xp->query('//a[@id="urlCopy"]')->item(0)->getAttribute('data-clipboard-text')); + $this->assertSame('10月30日免费小火箭ID',trim($xp->query('//h1')->item(0)->textContent)); + $this->assertSame($before,DB::table('articles')->orderBy('id')->get()->toJson()); + DB::table('categories')->where('id',5)->update(['iStatus'=>0]); + $this->assertNull(app(ArticlesService::class)->getLatestResourceSlug('shadowrocketids')); + $this->get('/shadowrocketids')->assertStatus(503)->assertHeader('Retry-After','3600'); + $this->get('/history/shadowrocketids')->assertNotFound(); + } + + public function test_dated_resources_keep_their_urls_and_self_canonical_but_are_noindex(): void + { + foreach(['freenode'=>1,'shadowrocketids'=>5] as $type=>$category) { + $id=$this->article($type.'-date',['iCategoryId'=>$category,'sSeoPolicy'=>'index','sContent'=>$category===5?$this->appleBody():'

节点记录

']); + $html=$this->get('/a/'.$type.'-date.html')->assertOk()->assertSee('查看最新')->assertSee('查看往期')->getContent(); + $xp=$this->xpath($html); + $this->assertSame('往期资源', trim($xp->query('//a[contains(@class,"resource-history-action") and contains(@href,"/history/")]')->item(0)->textContent)); + $this->assertSame('noindex, follow',$xp->query('//meta[@name="robots"]')->item(0)->getAttribute('content')); + $this->assertSame('https://example.org/a/'.$type.'-date.html',$xp->query('//link[@rel="canonical"]')->item(0)->getAttribute('href')); + $this->assertFalse(Seo::indexable(Articles::find($id))); + } + $this->article('guide');$this->get('/a/guide.html')->assertOk()->assertSee('content="index, follow"',false)->assertDontSee('aria-label="资源更新记录"',false); + } + + public function test_history_overview_combines_both_types_and_category_links_filter_history(): void + { + config(['seo.history_page_size'=>10]); + $this->article('latest-free',['iCategoryId'=>1,'created_at'=>'2026-10-31 10:00:00']); + $this->article('latest-id',['iCategoryId'=>5,'sContent'=>$this->appleBody(),'created_at'=>'2026-10-31 10:00:00']); + $a=$this->article('free-a',['iCategoryId'=>1,'created_at'=>'2026-10-29 10:00:00']); + $b=$this->article('free-b',['iCategoryId'=>1,'created_at'=>'2026-10-28 10:00:00']); + $c=$this->article('id-a',['iCategoryId'=>5,'sContent'=>$this->appleBody(),'created_at'=>'2026-10-27 10:00:00']); + $xp=$this->xpath($this->get('/history')->assertOk()->getContent()); + $this->assertSame([(string)$a,(string)$b,(string)$c],array_map(fn($n)=>$n->getAttribute('data-article-id'),iterator_to_array($xp->query('//*[@data-article-id]')))); + foreach(['freenode','shadowrocketids'] as $type) { + $this->assertGreaterThan(0,$xp->query('//div[contains(@class,"list-home")]//div[contains(@class,"list-footer")]//a[@href="https://example.org/history/'.$type.'"]')->length); + } + $this->assertSame(2,$this->xpath($this->get('/history/freenode')->assertOk()->getContent())->query('//*[@data-article-id]')->length); + $this->assertSame(1,$this->xpath($this->get('/history/shadowrocketids')->assertOk()->getContent())->query('//*[@data-article-id]')->length); + } + + public function test_history_excludes_current_entries_and_preserves_date_order_and_pagination(): void + { + $this->article('current-free',['iCategoryId'=>1,'created_at'=>'2026-10-31 10:00:00']); + $this->article('current-id',['iCategoryId'=>5,'sContent'=>$this->appleBody(),'created_at'=>'2026-10-31 10:00:00']); + $ids=[];foreach(range(1,5) as $n)$ids[]=$this->article('past-'.$n,['iCategoryId'=>1,'iIsTop'=>$n===1?1:0,'created_at'=>'2026-10-2'.$n.' 10:00:00']); + $this->article('old-id',['iCategoryId'=>5,'sContent'=>$this->appleBody(),'created_at'=>'2026-10-20 10:00:00']); + $this->article('hidden-free',['iCategoryId'=>1,'iStatus'=>0]); + $this->article('future-free',['iCategoryId'=>1,'created_at'=>'2026-11-01 00:00:00']); + $this->article('not-resource'); + foreach(['/history','/history/freenode'] as $path) { + $xp=$this->xpath($this->get($path)->assertOk()->getContent()); + $this->assertSame(0, $xp->query('//nav[@aria-label="往期内容分类"]')->length); + $this->assertSame(0, $xp->query('//main//h1')->length); + $cards=$xp->query('//*[@data-article-id]'); + $this->assertSame([(string)$ids[4],(string)$ids[3]],array_map(fn($n)=>$n->getAttribute('data-article-id'),iterator_to_array($cards))); + $this->assertSame('noindex, follow',$xp->query('//meta[@name="robots"]')->item(0)->getAttribute('content')); + } + $html=$this->get('/history/freenode?page=2&utm_source=test')->assertOk()->getContent(); + $xp=$this->xpath($html);$cards=$xp->query('//*[@data-article-id]'); + $this->assertSame([(string)$ids[2],(string)$ids[1]],array_map(fn($n)=>$n->getAttribute('data-article-id'),iterator_to_array($cards))); + $this->assertSame('https://example.org/history/freenode?page=2',$xp->query('//link[@rel="canonical"]')->item(0)->getAttribute('href')); + $this->assertSame('https://example.org/history/freenode?page=3',$xp->query('//link[@rel="next"]')->item(0)->getAttribute('href')); + $xp=$this->xpath($this->get('/history/shadowrocketids')->assertOk()->getContent()); + $this->assertSame(1,$xp->query('//*[@data-article-id]')->length); + $this->assertStringContainsString('old-id',$xp->query('//a[contains(@class,"list-title")]')->item(0)->getAttribute('href')); + $this->get('/history/freenode?page=3')->assertOk()->assertDontSee('class="pagination-next"',false); + foreach(['/history/freenode?page=4','/history/freenode?page=0','/history?page=abc','/history?page[]=2','/history?page=2&page=3','/history/client'] as $path)$this->get($path)->assertNotFound(); + $this->get('/history/freenode?page=1')->assertStatus(301)->assertRedirect('https://example.org/history/freenode'); + } + + public function test_resource_navigation_redirects_and_sitemap_use_stable_entrances(): void + { + $this->article('current-free',['iCategoryId'=>1]); + $this->article('current-id',['iCategoryId'=>5,'sContent'=>$this->appleBody()]); + $this->article('guide'); + $html=$this->get('/')->assertOk()->assertSee('查看往期资源(免费节点)')->getContent(); + $xp=$this->xpath($html); + foreach(['往期资源'=>'/history','免费节点'=>'/','免费小火箭ID'=>'/shadowrocketids'] as $name=>$path) { + $links=$xp->query('//header//a[span[text()="'.$name.'"]]');$this->assertSame(2,$links->length); + foreach($links as $link)$this->assertSame($path,parse_url($link->getAttribute('href'),PHP_URL_PATH) ?: '/'); + } + foreach(['/freenode'=>'/','/freenode?page=1'=>'/','/?page=2'=>'/history/freenode?page=2', + '/freenode?page=2'=>'/history/freenode?page=2','/shadowrocketids?page=2'=>'/history/shadowrocketids?page=2', + '/shadowrocketids?page=1'=>'/shadowrocketids'] as $old=>$target)$this->get($old)->assertStatus(301)->assertRedirect('https://example.org'.$target); + $this->get('/shadowrocketids?page=-1')->assertNotFound(); + $xml=$this->get('/sitemap.xml')->assertOk()->getContent(); + $this->assertStringContainsString('https://example.org/shadowrocketids',$xml); + $this->assertStringContainsString('https://example.org/',$xml); + foreach(['/freenode','/history','/a/current-free.html','/a/current-id.html'] as $path)$this->assertStringNotContainsString('https://example.org'.$path,$xml); + $this->assertStringContainsString('/a/guide.html',$xml); + DB::table('articles')->where('iCategoryId',5)->delete(); + $this->get('/sitemap.xml')->assertOk()->assertDontSee('/shadowrocketids',false); + $this->get('/history/shadowrocketids')->assertOk()->assertSee('看起来这里没有任何东西…'); + } + + public function test_id_updates_notify_only_the_entrance_and_drop_queued_archive_updates(): void + { + config(['editorial.indexnow_enabled'=>true,'key.indexnow'=>'test-key-12345678']);Http::fake(['api.indexnow.org/*'=>Http::response('',200)]); + $id=$this->article('ids',['iCategoryId'=>5,'sContent'=>$this->appleBody()]);$service=app(SeoNotifications::class); + $service->changed(Articles::find($id));$service->retry(); + Http::assertSentCount(1);Http::assertSent(fn($req)=>$req['urlList']===['https://example.org/shadowrocketids']); + $service->enqueue('legacy-queue',['https://example.org/freenode','https://example.org/history','https://example.org/history/shadowrocketids','https://example.org/a/ids.html']); + $service->retry();Http::assertSentCount(1); + $state=json_decode(DB::table('seo_notifications')->where('event_key','legacy-queue')->value('state'),true); + $this->assertSame('skipped',$state['status']); + } + } diff --git a/tests/TestCase.php b/tests/TestCase.php old mode 100755 new mode 100644 index fe1ffc2f..60bf6ebe --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,5 +6,10 @@ abstract class TestCase extends BaseTestCase { - // + protected function setUp(): void + { + parent::setUp(); + // Tests must never inherit the running site's operational switch. + config(['site_mode.quiet' => false, 'site_mode.state_path' => null]); + } }