fn_b/tests/Feature/OneYearArticleLikesTest.php

137 lines
6.7 KiB
PHP

<?php
namespace Tests\Feature;
use App\Services\OneYearArticleLikes;
use Carbon\CarbonImmutable;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
#[\PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses]
#[\PHPUnit\Framework\Attributes\PreserveGlobalState(false)]
class OneYearArticleLikesTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config(['database.default' => 'sqlite', 'database.connections.sqlite.database' => ':memory:',
'scout.driver' => null, 'cache.default' => 'array']);
DB::purge('sqlite');
Schema::create('articles', function (Blueprint $t) {
$t->id();
$t->integer('iStatus')->default(1);
foreach (['iLikeCount', 'i7like', 'i365like'] as $field) {
$t->integer($field)->default(0);
}
$t->timestamp('sLastLikedAt')->nullable();
$t->timestamps();
});
(require database_path('migrations/2026_10_29_200000_add_one_year_article_likes.php'))->up();
$this->travelTo(CarbonImmutable::parse('2026-01-01 12:00:00'));
}
protected function tearDown(): void
{
$this->travelBack();
parent::tearDown();
}
private function article(array $values = []): int
{
return DB::table('articles')->insertGetId($values + [
'iYearLikeTarget' => 200, 'sYearLikeStartedAt' => now(),
'created_at' => now(), 'updated_at' => now(),
]);
}
public function test_quota_is_paid_gradually_exactly_once_and_stops(): void
{
$id = $this->article(['iLikeCount' => 17]);
$service = app(OneYearArticleLikes::class);
$this->assertSame(0, $service->sync());
$this->travel(183)->days();
$this->assertSame(100, $service->sync());
$this->assertSame(0, $service->sync());
DB::table('articles')->where('id', $id)->increment('iLikeCount', 3);
$this->travelTo(CarbonImmutable::parse('2027-01-01 12:00:00'));
$this->assertSame(100, $service->sync());
$this->assertSame(200, (int) DB::table('articles')->value('iYearLikeAdded'));
$this->assertSame(220, (int) DB::table('articles')->value('iLikeCount'));
$this->assertSame('2026-01-01 12:00:00', DB::table('articles')->value('updated_at'));
$this->travel(400)->days();
$this->assertSame(0, $service->sync());
$this->assertSame(220, (int) DB::table('articles')->value('iLikeCount'));
}
public function test_deadline_catches_up_after_downtime_but_ignores_disabled_and_future_plans(): void
{
$this->article(['iYearLikeTarget' => 500]);
$this->article(['iYearLikeTarget' => 0]);
$this->article(['iStatus' => 0]);
$this->article(['sYearLikeStartedAt' => '2030-01-01 00:00:00']);
$this->travel(370)->days();
$this->assertSame(500, app(OneYearArticleLikes::class)->sync());
$this->assertSame(500, (int) DB::table('articles')->sum('iYearLikeAdded'));
$this->assertSame(0, app(OneYearArticleLikes::class)->sync());
}
public function test_leap_day_uses_calendar_year_and_null_start_is_initialized_without_backfill(): void
{
$this->travelTo(CarbonImmutable::parse('2024-02-29 12:00:00'));
$id = $this->article(['iYearLikeTarget' => 365]);
$this->travelTo(CarbonImmutable::parse('2025-02-28 12:00:00'));
$this->assertSame(365, app(OneYearArticleLikes::class)->syncArticle($id));
$new = $this->article(['sYearLikeStartedAt' => null, 'created_at' => '2020-01-01 00:00:00']);
$this->assertSame(0, app(OneYearArticleLikes::class)->syncArticle($new));
$this->assertSame('2025-02-28 12:00:00', DB::table('articles')->where('id', $new)->value('sYearLikeStartedAt'));
}
public function test_backfill_is_limited_to_editorial_missing_settings_and_is_idempotent(): void
{
Schema::table('articles', fn (Blueprint $t) => $t->string('sSlug')->nullable());
Schema::create('editorial_documents', function (Blueprint $t) {
$t->integer('article_id');
$t->integer('published_revision')->nullable();
});
$blank = $this->article(['iYearLikeTarget' => 0, 'sYearLikeStartedAt' => null, 'iLikeCount' => 12]);
$legacy = $this->article(['iYearLikeTarget' => 0, 'sYearLikeStartedAt' => null, 'i365like' => 30]);
$resource = $this->article(['iYearLikeTarget' => 0, 'sYearLikeStartedAt' => null]);
DB::table('editorial_documents')->insert([
['article_id' => $blank, 'published_revision' => 1],
['article_id' => $legacy, 'published_revision' => 1],
]);
$this->artisan('article:backfill-editorial-likes')->assertSuccessful();
$this->assertSame(0, (int) DB::table('articles')->sum('iYearLikeTarget'));
$this->artisan('article:backfill-editorial-likes --apply')->assertSuccessful();
$a = DB::table('articles')->where('id', $blank)->first();
$this->assertGreaterThanOrEqual(5, (int) $a->i7like);
$this->assertLessThanOrEqual(35, (int) $a->i7like);
$this->assertSame(50, (int) $a->i365like);
$this->assertSame(12, (int) $a->iLikeCount);
$this->assertGreaterThanOrEqual(100, (int) $a->iYearLikeTarget);
$this->assertLessThanOrEqual(500, (int) $a->iYearLikeTarget);
$this->assertSame(30, (int) DB::table('articles')->where('id', $legacy)->value('i365like'));
$this->assertSame(0, (int) DB::table('articles')->where('id', $legacy)->value('i7like'));
$this->assertSame(0, (int) DB::table('articles')->where('id', $resource)->value('iYearLikeTarget'));
$before = DB::table('articles')->orderBy('id')->get()->toJson();
$this->artisan('article:backfill-editorial-likes --apply')->assertSuccessful();
$this->assertSame($before, DB::table('articles')->orderBy('id')->get()->toJson());
}
public function test_scheduled_command_runs_the_quota_without_resetting_other_settings(): void
{
$id = $this->article(['i365like' => 365]);
$this->travel(365)->days();
$this->artisan('article:sync-likes')->assertSuccessful();
$this->artisan('article:sync-likes')->assertSuccessful();
$this->assertSame(201, (int) DB::table('articles')->where('id', $id)->value('iLikeCount'));
$this->travel(1)->days();
$this->artisan('article:sync-likes')->assertSuccessful();
$this->assertSame(202, (int) DB::table('articles')->where('id', $id)->value('iLikeCount'));
$this->assertSame(365, (int) DB::table('articles')->where('id', $id)->value('i365like'));
$this->assertSame(200, (int) DB::table('articles')->where('id', $id)->value('iYearLikeAdded'));
}
}