'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')); } }