281 lines
24 KiB
PHP
281 lines
24 KiB
PHP
<?php
|
||
namespace Tests\Feature;
|
||
use App\Models\Articles;
|
||
use App\Services\Seo;
|
||
use App\Services\ArticlesService;
|
||
use App\Services\ResourcePublication;
|
||
use App\Services\SeoNotifications;
|
||
use Illuminate\Database\Schema\Blueprint;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Illuminate\Support\Facades\Http;
|
||
use Tests\TestCase;
|
||
#[\PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses]
|
||
#[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)]
|
||
class SeoContractTest extends TestCase
|
||
{
|
||
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,'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'));
|
||
Schema::create('categories',function(Blueprint $t){$t->id();$t->string('sSlug');$t->string('sName');$t->integer('iParentId')->default(0);$t->integer('iStatus')->default(1);$t->integer('iSort')->default(0);$t->integer('iShowChildren')->default(0);$t->string('sHtmlClass')->nullable();$t->text('aOpt')->nullable();$t->text('sDescription')->nullable();$t->text('aBanner')->nullable();});
|
||
Schema::create('articles',function(Blueprint $t){$t->id();$t->integer('iCategoryId');$t->integer('iUserId')->default(1);$t->string('sSlug')->unique();$t->string('sTitle');$t->text('sTitleSub')->nullable();$t->text('sContent');$t->string('sThumb')->nullable();$t->integer('iStatus')->default(1);$t->boolean('bPageThumb')->default(false);foreach(['iViews','iLikeCount','i7like','i365like','iIsTop'] as $f)$t->integer($f)->default(0);$t->timestamp('sLastLikedAt')->nullable();$t->timestamps();});
|
||
Schema::create('tags',function(Blueprint $t){$t->id();$t->string('sSlug');$t->string('sName');});
|
||
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],[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(); }
|
||
private function article(string $slug, array $extra=[]): int
|
||
{
|
||
return DB::table('articles')->insertGetId($extra+['sSlug'=>$slug,'sTitle'=>'教程 '.$slug,'sTitleSub'=>'<b>示例</b> & 摘要','sContent'=>'<h2>操作步骤</h2><p>示例正文</p>','iCategoryId'=>3,'created_at'=>'2026-10-30 10:00:00','updated_at'=>'2026-10-31 01:00:00']);
|
||
}
|
||
public function test_category_aliases_pagination_and_parameters(): void
|
||
{
|
||
foreach(range(1,5) as $n)$this->article('guide'.$n);
|
||
$this->get('/windows?page=2&utm_source=x')->assertRedirect('https://example.org/client/windows?page=2')->assertStatus(301);
|
||
foreach(['/wrong/windows','/wrong/freenode','/client/freenode','/client/windows?page=0','/client/windows?page=-1','/client/windows?page=abc','/client/windows?page=99','/client/windows?page[]=2','/client/windows?page=2&page=3'] as $url)$this->get($url)->assertNotFound();
|
||
$this->get('/client/windows?page=1')->assertStatus(301)->assertRedirect('https://example.org/client/windows');
|
||
$html=$this->get('/client/windows?page=2&utm_source=x')->assertOk()->getContent();
|
||
$this->assertStringContainsString('rel="canonical" href="https://example.org/client/windows?page=2"',$html);
|
||
$this->assertStringContainsString('rel="next" href="https://example.org/client/windows?page=3"',$html);
|
||
$this->assertStringContainsString('rel="prev" href="https://example.org/client/windows"',$html);
|
||
$this->assertStringNotContainsString('guide5.html',$html);
|
||
$dom=new \DOMDocument();@$dom->loadHTML($html);$xp=new \DOMXPath($dom);
|
||
$this->assertSame(1,$xp->query('//head/link[@class="pagination-next"]')->length);
|
||
$this->assertSame(0,$xp->query('//body/nav[@class="seo-pagination"]')->length);
|
||
$this->assertSame(0,$xp->query('//*[contains(@class,"seo-card-image")]')->length);
|
||
$this->get('/client/windows?page=3')->assertOk()->assertDontSee('class="pagination-next"',false);
|
||
$this->get('/robots.txt')->assertOk()->assertDontSee('Disallow: /*?*',false);
|
||
}
|
||
public function test_hidden_and_future_content_are_absent_from_every_public_query(): void
|
||
{
|
||
$good=$this->article('good');
|
||
foreach(['draft'=>['iStatus'=>0],'future'=>['created_at'=>'2026-11-01 00:00:00']] as $slug=>$extra){$id=$this->article($slug,$extra);DB::table('article_tag')->insert(['iArticleId'=>$id,'iTagId'=>1]);$this->get('/a/'.$slug.'.html')->assertNotFound();}
|
||
$this->get('/search/'.rawurlencode('教程'))->assertOk()->assertSee('good.html',false)->assertDontSee('draft.html',false)->assertDontSee('future.html',false)->assertSee('noindex, follow',false);
|
||
$this->get('/?s='.rawurlencode('教程'))->assertStatus(301)->assertRedirect(Seo::url('/search/'.rawurlencode('教程')));
|
||
$this->get('/search/'.rawurlencode('不存在的内容'))->assertOk()->assertSee('看起来这里没有任何东西…');
|
||
$this->get('/tag/clash')->assertOk()->assertDontSee('draft.html',false);
|
||
DB::table('categories')->where('id',2)->update(['iStatus'=>0]);
|
||
$this->get('/a/good.html')->assertNotFound();$this->get('/client/windows')->assertNotFound();
|
||
$this->get('/sitemap.xml')->assertOk()->assertDontSee('good.html',false);
|
||
$this->get('/search/'.rawurlencode('教程'))->assertOk()->assertDontSee('good.html',false);
|
||
}
|
||
public function test_home_is_stable_and_failed_refresh_does_not_replace_it(): void
|
||
{
|
||
$this->article('fn-good',['iCategoryId'=>1,'sTitle'=>'日期标题','sTitleSub'=>'共20个可用节点']);
|
||
$this->article('fn-zero',['iCategoryId'=>1,'sTitleSub'=>'本次更新共0个可用节点','created_at'=>'2026-10-31 11:00:00']);
|
||
$this->assertSame('fn-good',app(ArticlesService::class)->getLatestFreeNodeSlug());
|
||
$html=$this->get('/')->assertOk()->getContent();
|
||
$this->assertStringContainsString('<h1 class="mb-3">日期标题</h1>',$html);
|
||
$this->assertStringContainsString('rel="canonical" href="https://example.org/"',$html);
|
||
$this->assertStringContainsString('property="og:type" content="website"',$html);
|
||
$this->assertStringContainsString('示例正文',$html);
|
||
$this->assertStringNotContainsString('resource-context',$html);
|
||
$this->assertStringNotContainsString('/css/seo.css',$html);
|
||
$this->assertStringContainsString('发布于',$html);
|
||
$this->travel(10)->days();$this->get('/')->assertOk()->assertSee('content="index, follow"',false);
|
||
$this->get('/a/fn-good.html')->assertOk()->assertSee('content="noindex, follow"',false)->assertDontSee('resource-context',false);
|
||
DB::table('articles')->delete();$this->get('/')->assertStatus(503)->assertHeader('Retry-After','3600');
|
||
}
|
||
public function test_new_snapshots_are_archives_and_content_clock_ignores_likes(): void
|
||
{
|
||
$id=$this->article('old',['iCategoryId'=>1]);$old=Articles::find($id);
|
||
$this->assertFalse(Seo::indexable($old));$this->assertNull(Seo::modified($old));
|
||
$new=Articles::create(['sSlug'=>'new','sTitle'=>'新的记录','sTitleSub'=>'共10个可用节点','sContent'=>'<p>有效资源</p>','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='<p>更新资源内容</p>';$new->save();$this->assertNotSame($clock,Seo::modified($new->fresh()));
|
||
$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);
|
||
}
|
||
public function test_metadata_and_structured_data_are_clean_and_share_article_url(): void
|
||
{
|
||
$this->article('clean',['sTitle'=>'标题 </script> & "引用"']);
|
||
$html=$this->get('/a/clean.html?utm_source=test')->assertOk()->getContent();
|
||
$dom=new \DOMDocument();@$dom->loadHTML($html);$xp=new \DOMXPath($dom);
|
||
$this->assertSame('示例 & 摘要',$xp->query('//meta[@name="description"]')->item(0)->getAttribute('content'));
|
||
$this->assertSame('https://example.org/a/clean.html',$xp->query('//a[@id="urlCopy"]')->item(0)->getAttribute('data-clipboard-text'));
|
||
foreach($xp->query('//script[@type="application/ld+json"]') as $script)$this->assertIsArray(json_decode($script->textContent,true,512,JSON_THROW_ON_ERROR));
|
||
$this->assertStringNotContainsString('dateModified',$html);$this->assertStringNotContainsString('AggregateRating',$html);
|
||
$this->assertStringContainsString('href="https://example.org/client/windows"',$html);
|
||
}
|
||
public function test_rejected_resource_does_not_change_articles(): void
|
||
{
|
||
$id=$this->article('last',['iCategoryId'=>1]);
|
||
$payload=['title'=>'更新','title_sub'=>'共12个可用节点','content'=>'<p>内容</p>','tags'=>[]];
|
||
$this->assertSame(12,ResourcePublication::validate($payload)['iResourceCount']);
|
||
$this->assertSame('unknown',ResourcePublication::validate($payload)['sResourceStatus']);
|
||
foreach([['check_status'=>'failed'],['valid_count'=>0],['title_sub'=>'本次更新共0个可用节点'],['content'=>'']] as $bad){
|
||
try { ResourcePublication::validate(array_replace($payload,$bad));$this->fail('Invalid refresh accepted'); } catch(\Symfony\Component\HttpKernel\Exception\HttpException|\Illuminate\Validation\ValidationException $e){$this->assertNotEmpty($e->getMessage());}
|
||
}
|
||
$this->assertSame('last',Articles::find($id)->sSlug);$this->assertSame(1,Articles::count());
|
||
}
|
||
public function test_durable_home_notifications_retry_and_deletions_need_no_http_probe(): void
|
||
{
|
||
config(['editorial.indexnow_enabled'=>true,'key.indexnow'=>'test-key-12345678']);
|
||
Http::fake(['api.indexnow.org/*'=>Http::sequence()->push('',429)->push('',202)->push('',200)]);
|
||
$id=$this->article('snapshot',['iCategoryId'=>1,'sSeoPolicy'=>'archive']);$service=app(SeoNotifications::class);
|
||
$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/']);
|
||
$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']));
|
||
}
|
||
public function test_cleanup_retains_old_resource_urls_and_get_cannot_like(): void
|
||
{
|
||
$id=$this->article('historical',['iCategoryId'=>1,'created_at'=>'2025-01-01 00:00:00']);
|
||
$this->artisan('clear_db')->assertExitCode(0);$this->assertNotNull(Articles::find($id));
|
||
$this->get('/like/'.$id)->assertNotFound();$this->assertSame(0,Articles::find($id)->iLikeCount);
|
||
}
|
||
public function test_commercial_attributes_and_zero_string_validation(): void
|
||
{
|
||
$html='<a href="https://xn--123-9i9dj97q.com/" rel="nofollow">推广</a><a href="https://example.net/doc">参考</a>';
|
||
$body=Seo::body($html);$this->assertStringContainsString('rel="nofollow sponsored noopener"',$body);
|
||
$this->assertStringContainsString('<a href="https://example.net/doc">参考</a>',$body);
|
||
$this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class);
|
||
ResourcePublication::validate(['title'=>'资源','title_sub'=>'数量未知','content'=>'内容','tags'=>[],'valid_count'=>'0']);
|
||
}
|
||
|
||
private function appleBody(): string
|
||
{
|
||
return '<h2>免费苹果账号</h2><p>账号:<a data-clipboard-text="test@example.invalid">test@example.invalid</a>,密码:<a data-clipboard-text="synthetic-test">synthetic-test</a>,地区:美区,检测时间:<span>2026-10-30 10:00:00</span></p>';
|
||
}
|
||
|
||
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'=>'<h2>免费苹果账号</h2>'], 'unknown'=>['sContent'=>'<p>格式未知</p>'],
|
||
'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():'<p>节点记录</p>']);
|
||
$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('<loc>https://example.org/shadowrocketids</loc>',$xml);
|
||
$this->assertStringContainsString('<loc>https://example.org/</loc>',$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']);
|
||
}
|
||
|
||
}
|