'sqlite', 'database.connections.sqlite.database' => ':memory:', 'cache.default' => 'array', ]); DB::purge(); DB::reconnect(); Cache::flush(); Carbon::setTestNow('2026-07-20 14:37:00'); Schema::create('users', function (Blueprint $table): void { $table->id(); $table->dateTime('deleted_at')->nullable(); }); Schema::create('settings', function (Blueprint $table): void { $table->string('name')->primary(); $table->text('value')->nullable(); }); Schema::create('user_requests', function (Blueprint $table): void { $table->id(); $table->unsignedBigInteger('users_id'); $table->text('request'); $table->dateTime('timestamp'); }); Schema::create('user_downloads', function (Blueprint $table): void { $table->id(); $table->unsignedBigInteger('users_id'); $table->dateTime('timestamp'); }); Schema::create('user_activity_stats', function (Blueprint $table): void { $table->id(); $table->date('stat_date')->unique(); $table->integer('downloads_count')->default(0); $table->integer('api_hits_count')->default(0); $table->timestamps(); }); Schema::create('user_activity_stats_hourly', function (Blueprint $table): void { $table->id(); $table->dateTime('stat_hour')->unique(); $table->integer('downloads_count')->default(0); $table->integer('api_hits_count')->default(0); $table->timestamps(); }); DB::table('users')->insert(['id' => 1]); DB::table('settings')->insert([ ['name' => 'categorizeforeign', 'value' => '0'], ['name' => 'catwebdl', 'value' => '0'], ['name' => 'delaytime', 'value' => '0'], ['name' => 'innerfileblacklist', 'value' => ''], ]); } protected function tearDown(): void { Carbon::setTestNow(); parent::tearDown(); } public function test_summary_and_charts_combine_hourly_api_and_rss_hits_with_the_live_hour(): void { $this->insertHourly('2026-07-14 09:00:00', 3, 7); $this->insertHourly('2026-07-20 13:00:00', 2, 4); DB::table('user_activity_stats')->insert([ 'stat_date' => '2026-07-19', 'downloads_count' => 999, 'api_hits_count' => 999, ]); DB::table('user_requests')->insert([ ['users_id' => 1, 'request' => '/api/v2/search?id=one', 'timestamp' => '2026-07-20 14:37:00'], ['users_id' => 1, 'request' => '/rss/full-feed?api_token=test', 'timestamp' => '2026-07-20 14:37:30'], ]); DB::table('user_downloads')->insert([ 'users_id' => 1, 'timestamp' => '2026-07-20 14:36:00', ]); $service = new UserStatsService; $summary = $service->getSummaryStats(); $this->assertSame(6, $summary['api_hits_today']); $this->assertSame(13, $summary['api_hits_week']); $this->assertSame(3, $summary['downloads_today']); $this->assertSame(6, $summary['downloads_week']); $hourly = $service->getApiHitsPerHour(2); $this->assertSame([4, 2], array_column($hourly, 'count')); $daily = $service->getApiHitsPerDay(7); $this->assertSame(7, $daily[0]['count']); $this->assertSame(6, $daily[6]['count']); $perMinute = $service->getApiHitsPerMinute(60); $this->assertSame(2, $perMinute[59]['count']); } public function test_daily_rollup_remains_stable_after_raw_rows_are_pruned(): void { $this->insertHourly('2026-07-19 01:00:00', 2, 3); $this->insertHourly('2026-07-19 23:00:00', 4, 5); DB::table('user_requests')->insert([ 'users_id' => 1, 'request' => '/api/v1/api?t=search', 'timestamp' => '2026-07-19 23:30:00', ]); UserActivityStat::collectDailyStats('2026-07-19'); DB::table('user_requests')->delete(); UserActivityStat::collectDailyStats('2026-07-19'); $daily = DB::table('user_activity_stats')->where('stat_date', '2026-07-19')->first(); $this->assertSame(6, $daily->downloads_count); $this->assertSame(8, $daily->api_hits_count); } public function test_forced_daily_backfill_rebuilds_retained_history_from_hourly_totals(): void { $this->insertHourly('2026-07-19 10:00:00', 11, 17); DB::table('user_activity_stats')->insert([ 'stat_date' => '2026-07-19', 'downloads_count' => 1, 'api_hits_count' => 1, ]); $this->artisan('nntmux:backfill-user-activity-stats', [ '--type' => 'daily', '--days' => 2, '--force' => true, ])->assertSuccessful(); $daily = DB::table('user_activity_stats')->where('stat_date', '2026-07-19')->first(); $this->assertSame(11, $daily->downloads_count); $this->assertSame(17, $daily->api_hits_count); } private function insertHourly(string $hour, int $downloads, int $apiHits): void { DB::table('user_activity_stats_hourly')->insert([ 'stat_hour' => $hour, 'downloads_count' => $downloads, 'api_hits_count' => $apiHits, ]); } }