From 069c041dbedb1ff78c0f2675b8c626e391193b5d Mon Sep 17 00:00:00 2001 From: Dat Date: Tue, 19 May 2026 17:25:33 +0200 Subject: [PATCH 1/7] Handle Carbon 3 timestamp parsing and add coverage for timestamp handling Bug: T426592 --- app/Helper/MWTimestampHelper.php | 9 +++++++-- tests/Helper/MWTimestampHelperTest.php | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/Helper/MWTimestampHelper.php b/app/Helper/MWTimestampHelper.php index a5d03f71e..bab92e404 100644 --- a/app/Helper/MWTimestampHelper.php +++ b/app/Helper/MWTimestampHelper.php @@ -15,8 +15,13 @@ class MWTimestampHelper { private const MWTimestampFormat = 'YmdHis'; public static function getCarbonFromMWTimestamp(string $MWTimestamp): CarbonImmutable { - $carbon = CarbonImmutable::createFromFormat(self::MWTimestampFormat, $MWTimestamp); - if ($carbon === null) { + try { + $carbon = CarbonImmutable::createFromFormat(self::MWTimestampFormat, $MWTimestamp); + } catch (InvalidFormatException $exception) { + throw new InvalidFormatException('Unable to create Carbon object', 0, $exception); + } + + if (!$carbon instanceof CarbonImmutable) { throw new InvalidFormatException('Unable to create Carbon object'); } diff --git a/tests/Helper/MWTimestampHelperTest.php b/tests/Helper/MWTimestampHelperTest.php index ed054cdb4..a82d81f9d 100644 --- a/tests/Helper/MWTimestampHelperTest.php +++ b/tests/Helper/MWTimestampHelperTest.php @@ -19,6 +19,7 @@ public function testGetCarbonFromMWTimestamp() { public function testGetCarbonFromMWTimestampWithInvalidTimestamp() { $this->expectException(InvalidFormatException::class); + $this->expectExceptionMessage('Unable to create Carbon object'); $invalidMwTimestamp = 'invalid_timestamp'; MWTimestampHelper::getCarbonFromMWTimestamp($invalidMwTimestamp); From a51fd9c792cf66367b51ea09c6c3885a0fa3a306 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 20 May 2026 19:55:48 +0200 Subject: [PATCH 2/7] Add test case to cover wiki noti threshold boundary --- .../SendEmptyWikiNotificationsJobTest.php | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/Jobs/SendEmptyWikiNotificationsJobTest.php b/tests/Jobs/SendEmptyWikiNotificationsJobTest.php index 7fb09ba36..dd07b544c 100644 --- a/tests/Jobs/SendEmptyWikiNotificationsJobTest.php +++ b/tests/Jobs/SendEmptyWikiNotificationsJobTest.php @@ -38,7 +38,7 @@ public function testEmptyWikiNotificationsSendNotification() { Notification::fake(); $user = User::factory()->create(['verified' => true]); $wiki = Wiki::factory()->create(['created_at' => $thresholdDaysAgo]); - $manager = WikiManager::factory()->create(['wiki_id' => $wiki->id, 'user_id' => $user->id]); + WikiManager::factory()->create(['wiki_id' => $wiki->id, 'user_id' => $user->id]); $wiki->wikiLifecycleEvents()->updateOrCreate(['first_edited' => null]); $job = new SendEmptyWikiNotificationsJob; @@ -50,6 +50,24 @@ public function testEmptyWikiNotificationsSendNotification() { ); } + // empty wikis, that are almost old enough (29 days and 23 hrs) + public function testEmptyWikiNotificationsNotSendNotification() { + $thresholdDaysAgo = Carbon::now() + ->subDays((config('wbstack.wiki_empty_notification_threshold') - 1)) + ->subHours(23) + ->toDateTimeString(); + + Notification::fake(); + $user = User::factory()->create(['verified' => true]); + $wiki = Wiki::factory()->create(['created_at' => $thresholdDaysAgo]); + WikiManager::factory()->create(['wiki_id' => $wiki->id, 'user_id' => $user->id]); + $wiki->wikiLifecycleEvents()->updateOrCreate(['first_edited' => null]); + + $job = new SendEmptyWikiNotificationsJob; + $this->assertFalse($job->checkIfWikiIsOldAndEmpty($wiki)); + $job->handle(); + } + // fresh wiki that does not have lifecycle event records yet public function testEmptyWikiNotificationsFreshWiki() { $now = Carbon::now()->toDateTimeString(); From d86dba61e21bd9b2f7e1d2b6b44c87e9e52550bc Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 20 May 2026 20:44:28 +0200 Subject: [PATCH 3/7] Add test second-precision lastEdit threshold in platform stats summary --- tests/Jobs/PlatformStatsSummaryJobTest.php | 46 ++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/Jobs/PlatformStatsSummaryJobTest.php b/tests/Jobs/PlatformStatsSummaryJobTest.php index 4a954200f..d94e970dc 100644 --- a/tests/Jobs/PlatformStatsSummaryJobTest.php +++ b/tests/Jobs/PlatformStatsSummaryJobTest.php @@ -274,4 +274,50 @@ public function testCreationStats() { ); } + + public function testPrepareStatsTreatsSecondPrecisionTimestampAtThresholdAsActive() { + $currentTime = CarbonImmutable::now(); + + $wiki = Wiki::factory()->create(['deleted_at' => null, 'domain' => 'thresholdtest.com']); + WikiDb::create([ + 'name' => 'mwdb_threshold_' . $wiki->id, + 'user' => 'user', + 'password' => 'password', + 'version' => 'version', + 'prefix' => 'prefix', + 'wiki_id' => $wiki->id, + ]); + + Http::fake([ + $this->mwBackendHost . '/w/api.php?action=query&list=allpages&apnamespace=122&apcontinue=&aplimit=max&format=json' => Http::response([ + 'query' => ['allpages' => []], + ], 200), + $this->mwBackendHost . '/w/api.php?action=query&list=allpages&apnamespace=120&apcontinue=&aplimit=max&format=json' => Http::response([ + 'query' => ['allpages' => []], + ], 200), + ]); + + $job = new PlatformStatsSummaryJob; + (function ($resolver): void { + $this->mwHostResolver = $resolver; + })->call($job, $this->mockMwHostResolver); + + $groups = $job->prepareStats([ + [ + 'wiki' => 'thresholdtest.com', + 'edits' => 1, + 'pages' => 1, + 'users' => 1, + 'active_users' => 1, + 'lastEdit' => MWTimestampHelper::getMWTimestampFromCarbon( + $currentTime->subSeconds(config('wbstack.platform_summary_inactive_threshold')) + ), + 'first100UsingOauth' => '0', + 'platform_summary_version' => 'v1', + ], + ], [$wiki]); + + $this->assertSame(1, $groups['edited_last_90_days']); + $this->assertSame(0, $groups['not_edited_last_90_days']); + } } From 842a701732c308e5a0f5cd95c1ec0b84e6982655 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 20 May 2026 20:51:40 +0200 Subject: [PATCH 4/7] Add test for fractional day diffs in conversion metrics --- tests/Routes/Wiki/ConversionMetricTest.php | 41 +++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/tests/Routes/Wiki/ConversionMetricTest.php b/tests/Routes/Wiki/ConversionMetricTest.php index a1c479401..800d41c83 100644 --- a/tests/Routes/Wiki/ConversionMetricTest.php +++ b/tests/Routes/Wiki/ConversionMetricTest.php @@ -58,10 +58,10 @@ private function createTestWiki($name, $createdWeeksAgo, $firstEditedWeeksAgo, $ $wiki->created_at = $current_date->subWeeks($createdWeeksAgo); $events = $wiki->wikiLifecycleEvents(); $update = []; - if ($lastEditedWeeksAgo) { + if ($lastEditedWeeksAgo !== null) { $update['last_edited'] = $current_date->subWeeks($lastEditedWeeksAgo); } - if ($firstEditedWeeksAgo) { + if ($firstEditedWeeksAgo !== null) { $update['first_edited'] = $current_date->subWeeks($firstEditedWeeksAgo); } $events->updateOrCreate($update); @@ -118,7 +118,7 @@ public function testDownloadJson() { ); $response->assertJsonFragment( [ - 'domain' => 'unused.for.a.year.but.now.active.wikibase.cloud', + 'domain' => 'acvtively.used.for.the.last.year.wikibase.cloud', 'time_to_engage_days' => 0, 'time_before_wiki_abandoned_days' => null, 'number_of_active_editors' => 5, @@ -134,9 +134,42 @@ public function testDownloadJson() { ); } + public function testDownloadJsonTruncatesFractionalDayDiffs() { + $currentDate = CarbonImmutable::now(); + $createdAt = $currentDate->subDays(200)->subHours(12); //200.5 days ago + $firstEditedAt = $createdAt->addDays(1)->addHours(12); //1.5 days after + $lastEditedAt = $currentDate->subDays(100); //100 days ago + + $wiki = Wiki::factory()->create([ + 'domain' => 'fractional.days.cloud', + 'sitename' => 'Fractional Days Site', + ]); + WikiSiteStats::factory()->create([ + 'wiki_id' => $wiki->id, + 'pages' => 77, + 'activeusers' => 2, + ]); + $wiki->created_at = $createdAt; + $wiki->wikiLifecycleEvents()->updateOrCreate([ + 'first_edited' => $firstEditedAt, + 'last_edited' => $lastEditedAt, + ]); + $wiki->save(); + + $response = $this->getJson($this->route); + + $response->assertStatus(200); + $response->assertJsonFragment([ + 'domain' => 'fractional.days.cloud', + 'time_to_engage_days' => 1, + 'time_before_wiki_abandoned_days' => 100, + 'number_of_active_editors' => 2, + ]); + } + public function testFunctionalWithMissingLifecycleEventsandStats() { $wiki = Wiki::factory()->create([ - 'domain' => 'very.new.wikibase.cloud', 'sitename' => 'bsite', + 'domain' => 'very.new.cloud', 'sitename' => 'Very New Site', ]); $response = $this->get($this->route); From e81d3e87017d583572962705aceeed4fee184cae Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 20 May 2026 20:58:41 +0200 Subject: [PATCH 5/7] fix linting error --- tests/Routes/Wiki/ConversionMetricTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Routes/Wiki/ConversionMetricTest.php b/tests/Routes/Wiki/ConversionMetricTest.php index 800d41c83..9188bdb9f 100644 --- a/tests/Routes/Wiki/ConversionMetricTest.php +++ b/tests/Routes/Wiki/ConversionMetricTest.php @@ -136,9 +136,9 @@ public function testDownloadJson() { public function testDownloadJsonTruncatesFractionalDayDiffs() { $currentDate = CarbonImmutable::now(); - $createdAt = $currentDate->subDays(200)->subHours(12); //200.5 days ago - $firstEditedAt = $createdAt->addDays(1)->addHours(12); //1.5 days after - $lastEditedAt = $currentDate->subDays(100); //100 days ago + $createdAt = $currentDate->subDays(200)->subHours(12); // 200.5 days ago + $firstEditedAt = $createdAt->addDays(1)->addHours(12); // 1.5 days after + $lastEditedAt = $currentDate->subDays(100); // 100 days ago $wiki = Wiki::factory()->create([ 'domain' => 'fractional.days.cloud', From 5d81db8f8b8e072e4e0df979b25d4c9a6921f333 Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 20 May 2026 21:01:56 +0200 Subject: [PATCH 6/7] remove unwanted changes --- bootstrap/providers.php | 17 +++++++++++++++++ tests/Routes/Wiki/ConversionMetricTest.php | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 bootstrap/providers.php diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 000000000..97b8e7ebd --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,17 @@ +assertJsonFragment( [ - 'domain' => 'acvtively.used.for.the.last.year.wikibase.cloud', + 'domain' => 'unused.for.a.year.but.now.active.wikibase.cloud', 'time_to_engage_days' => 0, 'time_before_wiki_abandoned_days' => null, 'number_of_active_editors' => 5, @@ -169,7 +169,7 @@ public function testDownloadJsonTruncatesFractionalDayDiffs() { public function testFunctionalWithMissingLifecycleEventsandStats() { $wiki = Wiki::factory()->create([ - 'domain' => 'very.new.cloud', 'sitename' => 'Very New Site', + 'domain' => 'very.new.wikibase.cloud', 'sitename' => 'bsite', ]); $response = $this->get($this->route); From da79c7e1fb64db2d1795c7c95b8840ea595c6d9a Mon Sep 17 00:00:00 2001 From: Dat Date: Wed, 20 May 2026 21:04:27 +0200 Subject: [PATCH 7/7] Remove accidental bootstrap/providers.php from branch --- bootstrap/providers.php | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 bootstrap/providers.php diff --git a/bootstrap/providers.php b/bootstrap/providers.php deleted file mode 100644 index 97b8e7ebd..000000000 --- a/bootstrap/providers.php +++ /dev/null @@ -1,17 +0,0 @@ -