fn_b/tests/Feature/QuietModeTest.php

144 lines
7.2 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Controllers\Api\v1\ArticlesController;
use App\Models\Articles;
use App\Models\Categories;
use App\Services\ArticleRecommendations;
use App\Services\SiteMode;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
#[\PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses]
#[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)]
final class QuietModeTest extends TestCase
{
private string $state;
protected function setUp(): void
{
parent::setUp();
$this->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'=>'<h2>机场推荐 - 福云:</h2><p>优惠</p><p><a href="https://example.invalid/">机场官网:福云</a></p><h2>订阅</h2><a data-clipboard-text="example-sub">复制</a>']);
$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);
}
}