223 lines
10 KiB
PHP
223 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Http\Middleware\HandleInertiaRequests;
|
|
use App\Models\News;
|
|
use App\Services\VMangosAccountService;
|
|
use DOMDocument;
|
|
use DOMXPath;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\Request;
|
|
use Inertia\Testing\AssertableInertia as Assert;
|
|
use Tests\TestCase;
|
|
|
|
class SeoFeatureTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
config([
|
|
'app.url' => 'https://moon-well.online',
|
|
'seo.indexable' => true,
|
|
'seo.google_verification' => null,
|
|
'seo.yandex_verification' => null,
|
|
'moonwell.realm.server_name' => 'MoonWell',
|
|
'moonwell.realm.client_version' => '1.12.1',
|
|
'moonwell.realm.rates.experience' => 'x1',
|
|
]);
|
|
|
|
$this->mock(VMangosAccountService::class)
|
|
->shouldReceive('onlinePlayerCount')->andReturn(null);
|
|
}
|
|
|
|
public function test_landing_has_server_rendered_pvp_metadata_and_readable_content(): void
|
|
{
|
|
$response = $this->get('/?utm_source=google');
|
|
|
|
$response->assertOk()->assertSee('<html lang="ru">', false);
|
|
$html = $this->xpath($response->getContent());
|
|
|
|
$this->assertSame(1, $html->query('//head/title')->length);
|
|
$this->assertSame('MoonWell — PvP сервер WoW Vanilla 1.12.1 x1', $html->evaluate('string(//head/title)'));
|
|
$this->assertSame(1, $html->query('//head/meta[@name="description"]')->length);
|
|
$this->assertSame('https://moon-well.online/', $html->evaluate('string(//head/link[@rel="canonical"]/@href)'));
|
|
$this->assertSame('index, follow, max-image-preview:large', $html->evaluate('string(//head/meta[@name="robots"]/@content)'));
|
|
$this->assertSame(1, $html->query('//div[@id="app"]//h1')->length);
|
|
$this->assertStringContainsString('PvP сервер World of Warcraft Vanilla 1.12.1', $html->evaluate('string(//div[@id="app"])'));
|
|
$this->assertSame('https://moon-well.online/landing.png', $html->evaluate('string(//head/meta[@property="og:image"]/@content)'));
|
|
|
|
$schema = json_decode($html->evaluate('string(//head/script[@type="application/ld+json"])'), true, flags: JSON_THROW_ON_ERROR);
|
|
$this->assertSame('WebSite', $schema['@graph'][0]['@type']);
|
|
|
|
$response->assertInertia(fn (Assert $page) => $page
|
|
->where('seo.canonical', 'https://moon-well.online/')
|
|
->where('realm.mode', 'PvP · x1 rate · RU')
|
|
->has('seo.head')
|
|
->etc()
|
|
);
|
|
}
|
|
|
|
public function test_canonical_and_sitemap_use_the_configured_domain(): void
|
|
{
|
|
$response = $this->get('http://alternate.example/?utm_campaign=test');
|
|
$response->assertOk();
|
|
$this->assertSame('https://moon-well.online/', $this->xpath($response->getContent())->evaluate('string(//head/link[@rel="canonical"]/@href)'));
|
|
|
|
$this->get('http://alternate.example/robots.txt')
|
|
->assertOk()
|
|
->assertSee('Sitemap: https://moon-well.online/sitemap.xml', false)
|
|
->assertDontSee('alternate.example', false);
|
|
}
|
|
|
|
public function test_seo_uses_the_configured_version_and_rates(): void
|
|
{
|
|
config([
|
|
'moonwell.realm.server_name' => 'Moonwell',
|
|
'moonwell.realm.client_version' => '1.12.2',
|
|
'moonwell.realm.rates.experience' => 'x3',
|
|
]);
|
|
|
|
$response = $this->get('/');
|
|
$response->assertOk();
|
|
$this->assertSame('MoonWell — PvP сервер WoW Vanilla 1.12.2 x3', $this->xpath($response->getContent())->evaluate('string(//head/title)'));
|
|
$response->assertSee('PvP сервер World of Warcraft Vanilla 1.12.2', false);
|
|
}
|
|
|
|
public function test_news_has_full_html_content_and_safe_article_structured_data(): void
|
|
{
|
|
$article = $this->article([
|
|
'title' => "Обновление\n<script>alert(\"test\")</script>",
|
|
'body' => "Первый абзац.\n\nВторой абзац: рейды & подземелья.",
|
|
]);
|
|
|
|
$response = $this->get('/news/'.$article->id.'?utm_source=yandex');
|
|
$response->assertOk()->assertDontSee('<script>alert("test")</script>', false);
|
|
$this->assertStringContainsString(e($article->title), $response->viewData('page')['props']['seo']['head'][0]);
|
|
$html = $this->xpath($response->getContent());
|
|
$this->assertSame($article->title, $html->evaluate('string(//div[@id="app"]//h1)'));
|
|
$this->assertStringContainsString('Второй абзац: рейды & подземелья.', $html->evaluate('string(//div[@id="app"])'));
|
|
$this->assertSame('https://moon-well.online/news/'.$article->id, $html->evaluate('string(//head/link[@rel="canonical"]/@href)'));
|
|
|
|
$schema = json_decode($html->evaluate('string(//head/script[@type="application/ld+json"])'), true, flags: JSON_THROW_ON_ERROR);
|
|
$this->assertSame('NewsArticle', $schema['@graph'][2]['@type']);
|
|
$this->assertSame($article->title, $schema['@graph'][2]['headline']);
|
|
$this->assertSame($article->updated_at->toIso8601String(), $schema['@graph'][2]['dateModified']);
|
|
}
|
|
|
|
public function test_news_pagination_has_its_own_canonical_and_crawlable_links(): void
|
|
{
|
|
foreach (range(1, 10) as $number) {
|
|
$this->article(['title' => "Новость {$number}"]);
|
|
}
|
|
|
|
$response = $this->get('/news?page=2&utm_source=test');
|
|
$response->assertOk();
|
|
$html = $this->xpath($response->getContent());
|
|
$this->assertSame('https://moon-well.online/news?page=2', $html->evaluate('string(//head/link[@rel="canonical"]/@href)'));
|
|
$this->assertStringContainsString('страница 2', $html->evaluate('string(//head/title)'));
|
|
$this->assertGreaterThan(0, $html->query('//div[@id="app"]//nav//a[@href]')->length);
|
|
|
|
$first = $this->get('/news?page=1&utm_source=test')->assertOk();
|
|
$this->assertSame('https://moon-well.online/news', $this->xpath($first->getContent())->evaluate('string(//head/link[@rel="canonical"]/@href)'));
|
|
$this->get('/news?page=3')->assertNotFound();
|
|
}
|
|
|
|
public function test_sitemap_is_valid_xml_and_contains_only_published_public_pages(): void
|
|
{
|
|
$published = $this->article();
|
|
$draft = $this->article(['is_published' => false]);
|
|
|
|
$response = $this->get('/sitemap.xml');
|
|
$response->assertOk()->assertHeader('Content-Type', 'application/xml; charset=UTF-8');
|
|
$xml = simplexml_load_string($response->getContent());
|
|
$this->assertNotFalse($xml);
|
|
$xml->registerXPathNamespace('s', 'http://www.sitemaps.org/schemas/sitemap/0.9');
|
|
$locations = array_map('strval', $xml->xpath('//s:loc'));
|
|
$this->assertSame([
|
|
'https://moon-well.online/',
|
|
'https://moon-well.online/news',
|
|
'https://moon-well.online/news/'.$published->id,
|
|
], $locations);
|
|
$this->assertSame($published->updated_at->toAtomString(), (string) $xml->xpath('//s:lastmod')[0]);
|
|
$this->get('/news/'.$draft->id)->assertNotFound();
|
|
|
|
$published->update(['is_published' => false]);
|
|
$this->get('/sitemap.xml')->assertOk()->assertDontSee('/news/'.$published->id, false);
|
|
}
|
|
|
|
public function test_robots_allows_assets_and_advertises_the_sitemap(): void
|
|
{
|
|
$this->get('/robots.txt')
|
|
->assertOk()
|
|
->assertHeader('Content-Type', 'text/plain; charset=UTF-8')
|
|
->assertSee("User-agent: *\nAllow: /\n", false)
|
|
->assertSee('Sitemap: https://moon-well.online/sitemap.xml', false)
|
|
->assertDontSee('Disallow: /build', false)
|
|
->assertDontSee('Disallow: /storage', false)
|
|
->assertDontSee('Disallow: /login', false);
|
|
}
|
|
|
|
public function test_staging_disables_indexing(): void
|
|
{
|
|
config(['seo.indexable' => false]);
|
|
|
|
$this->get('/robots.txt')->assertOk()->assertContent("User-agent: *\nDisallow: /\n");
|
|
$response = $this->get('/');
|
|
$response->assertOk();
|
|
$this->assertSame('noindex, follow', $this->xpath($response->getContent())->evaluate('string(//head/meta[@name="robots"]/@content)'));
|
|
}
|
|
|
|
public function test_inertia_navigation_gets_fresh_metadata_including_private_page_noindex(): void
|
|
{
|
|
$this->withHeaders([
|
|
'X-Inertia' => 'true',
|
|
'X-Inertia-Version' => app(HandleInertiaRequests::class)->version(Request::create('/')),
|
|
])->get('/news')
|
|
->assertOk()
|
|
->assertJsonPath('props.seo.canonical', 'https://moon-well.online/news')
|
|
->assertJsonPath('props.seo.robots', 'index, follow, max-image-preview:large');
|
|
|
|
$this->get('/login')
|
|
->assertOk()
|
|
->assertJsonPath('props.seo.canonical', null)
|
|
->assertJsonPath('props.seo.robots', 'noindex, follow')
|
|
->assertJsonPath('props.seo.structured_data', null);
|
|
}
|
|
|
|
public function test_verification_tags_are_optional_and_escaped(): void
|
|
{
|
|
$response = $this->get('/login')->assertOk();
|
|
$response->assertDontSee('name="google-site-verification"', false)->assertDontSee('name="yandex-verification"', false);
|
|
|
|
config(['seo.google_verification' => 'google-token"<>', 'seo.yandex_verification' => 'yandex-token']);
|
|
$response = $this->get('/login')->assertOk();
|
|
$html = $this->xpath($response->getContent());
|
|
$this->assertSame('google-token"<>', $html->evaluate('string(//head/meta[@name="google-site-verification"]/@content)'));
|
|
$this->assertSame('yandex-token', $html->evaluate('string(//head/meta[@name="yandex-verification"]/@content)'));
|
|
$this->assertSame('noindex, follow', $html->evaluate('string(//head/meta[@name="robots"]/@content)'));
|
|
}
|
|
|
|
private function article(array $attributes = []): News
|
|
{
|
|
return News::query()->create([
|
|
'title' => 'Обновление сервера',
|
|
'body' => 'Новости классического Азерота.',
|
|
'sort_order' => 0,
|
|
'is_published' => true,
|
|
...$attributes,
|
|
]);
|
|
}
|
|
|
|
private function xpath(string $html): DOMXPath
|
|
{
|
|
$document = new DOMDocument;
|
|
@$document->loadHTML('<?xml encoding="UTF-8">'.$html);
|
|
|
|
return new DOMXPath($document);
|
|
}
|
|
}
|