From d96b7b8667a0c5ade9c5474ef611328f593c426e Mon Sep 17 00:00:00 2001 From: Thomas Arrow Date: Mon, 19 Oct 2026 15:05:07 +0100 Subject: [PATCH 01/11] include latestSuspensionAction in response used on Wiki dashboard This commit adds a simple relationship to the Wiki model to include just the latest suspension type suspension action. It then includes this in the `/mine` endpoint for consumption by the UI Bug: T441452 --- app/Http/Controllers/WikisController.php | 1 + app/Wiki.php | 9 +++++++ tests/Routes/Wiki/MineTest.php | 33 ++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/app/Http/Controllers/WikisController.php b/app/Http/Controllers/WikisController.php index 92a7afb5..fa20d659 100644 --- a/app/Http/Controllers/WikisController.php +++ b/app/Http/Controllers/WikisController.php @@ -11,6 +11,7 @@ public function getWikisOwnedByCurrentUser(Request $request): Response { ->user() ->managesWikis() ->withExists('pendingReviewSubmission as has_pending_review') + ->with('latestSuspensionAction') ->get(); return response( diff --git a/app/Wiki.php b/app/Wiki.php index 4401256f..126678e8 100644 --- a/app/Wiki.php +++ b/app/Wiki.php @@ -2,6 +2,7 @@ namespace App; +use App\Enums\WikiSuspensionActionType; use App\Helper\DomainHelper; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Collection; @@ -97,6 +98,14 @@ public function pendingReviewSubmission(): HasOne { return $this->hasOne(ReviewSubmission::class)->pending(); } + public function latestSuspensionAction(): HasOne { + return $this->hasOne(WikiSuspensionAction::class) + ->latestOfMany() + // Don't expose unsuspension actions to the public. This should look and feel the same + // as if there were no suspensions on the Wiki. + ->where('suspension_type', WikiSuspensionActionType::SUSPEND); + } + public function wikiDbVersion() { return $this->hasOne(WikiDb::class)->select(['id', 'wiki_id', 'version']); } diff --git a/tests/Routes/Wiki/MineTest.php b/tests/Routes/Wiki/MineTest.php index 13943d2c..9b3c06a2 100644 --- a/tests/Routes/Wiki/MineTest.php +++ b/tests/Routes/Wiki/MineTest.php @@ -8,6 +8,7 @@ use App\User; use App\Wiki; use App\WikiManager; +use App\WikiSuspensionAction; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Config; use Tests\Routes\Traits\OptionsRequestAllowed; @@ -82,4 +83,36 @@ public function testMineIncludesPendingReviewSubmissions() { ['id' => $wikiWithApprovedSubmission->id, 'has_pending_review' => false], ]]); } + + public function testMineIncludesScheduledSuspensions() { + $user = User::factory()->create(['verified' => true]); + + $wikiWithoutSuspension = Wiki::factory()->create(); + + WikiManager::factory() + ->for($user) + ->for($wikiWithoutSuspension) + ->create(); + + $wikiWithSuspension = Wiki::factory()->create(); + + WikiManager::factory() + ->for($user) + ->for($wikiWithSuspension) + ->create(); + + $suspensionAction = WikiSuspensionAction::factory() + ->for($wikiWithSuspension) + ->create(); + + $this->actingAs($user, 'api') + ->json('POST', $this->route, []) + ->assertStatus(200) + ->assertJson(['wikis' => [ + ['id' => $wikiWithoutSuspension->id, 'latest_suspension_action' => null], + ['id' => $wikiWithSuspension->id, 'latest_suspension_action' => [ + 'id' => $suspensionAction->id, + ]], + ]]); + } } From 0498ba8a8f4806066930ba554514cec7d29941ea Mon Sep 17 00:00:00 2001 From: Ollie Date: Fri, 23 Oct 2026 16:44:29 +0100 Subject: [PATCH 02/11] Update docblock in Wiki.php --- app/Wiki.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Wiki.php b/app/Wiki.php index 126678e8..8e290c84 100644 --- a/app/Wiki.php +++ b/app/Wiki.php @@ -26,6 +26,7 @@ * @property int $is_featured * @property string|null $wiki_deletion_reason * @property-read mixed $domain_decoded + * @property-read WikiSuspensionAction|null $latestSuspensionAction * @property-read ReviewSubmission|null $pendingReviewSubmission * @property-read Collection $reviewSubmissions * @property-read int|null $review_submissions_count From e7f338c566e657352766eddce1b7651259776b4c Mon Sep 17 00:00:00 2001 From: Ollie Date: Fri, 23 Oct 2026 16:28:18 +0100 Subject: [PATCH 03/11] Add tests for the new `Wiki::latestSuspensionAction()` relationship --- tests/Feature/Models/WikiTest.php | 70 +++++++++++++++++++++++++++++++ tests/Routes/Wiki/MineTest.php | 38 +++++++++++------ 2 files changed, 95 insertions(+), 13 deletions(-) diff --git a/tests/Feature/Models/WikiTest.php b/tests/Feature/Models/WikiTest.php index 92ebacf9..4743a2cb 100644 --- a/tests/Feature/Models/WikiTest.php +++ b/tests/Feature/Models/WikiTest.php @@ -6,8 +6,11 @@ use App\Enums\ReviewSubmissionActionType; use App\Enums\UserRole; +use App\Enums\WikiSuspensionActionReason; +use App\Enums\WikiSuspensionActionType; use App\ReviewSubmission; use App\Wiki; +use App\WikiSuspensionAction; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; @@ -69,4 +72,71 @@ public function testCancelledReviewSubmissionIsNotPending(): void { $this->assertNull(Wiki::findOrFail($wiki->id)->pendingReviewSubmission); } + + public function testLatestSuspensionActionIsNullWithoutAnySuspensionActions(): void { + $wiki = Wiki::factory()->create(); + WikiSuspensionAction::factory()->create(); + + $this->assertNull(Wiki::findOrFail($wiki->id)->latestSuspensionAction); + } + + public function testLatestSuspensionActionReturnsLatestSuspensionForTheGivenWiki(): void { + $wiki = Wiki::factory()->create(); + // Create two suspension actions for $wiki + WikiSuspensionAction::factory()->for($wiki)->create(); + $latestSuspensionAction = WikiSuspensionAction::factory()->for($wiki)->create(); + // Create a suspension action for a different wiki + WikiSuspensionAction::factory()->create(); + + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspensionAction->is($latestSuspensionAction)); + } + + public function testLatestSuspensionActionReturnsSuspensionScheduledInTheFuture(): void { + $wiki = Wiki::factory()->create(); + WikiSuspensionAction::factory() + ->for($wiki) + ->create(['active_from' => now()->addWeek()]); + $latestSuspensionAction = WikiSuspensionAction::factory() + ->for($wiki) + ->create(['active_from' => now()->addMonth()]); + + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspensionAction->is($latestSuspensionAction)); + } + + public function testLatestSuspensionActionReturnsSuspensionScheduledInThePast(): void { + $wiki = Wiki::factory()->create(); + WikiSuspensionAction::factory()->for($wiki)->create(); + $latestSuspensionAction = WikiSuspensionAction::factory() + ->for($wiki) + ->create(['active_from' => now()->subMonth()]); + + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspensionAction->is($latestSuspensionAction)); + } + + public function testLatestSuspensionActionIsNullAfterUnsuspension(): void { + $wiki = Wiki::factory()->create(); + WikiSuspensionAction::factory()->for($wiki)->create(); + WikiSuspensionAction::factory() + ->for($wiki) + ->create([ + 'suspension_type' => WikiSuspensionActionType::UNSUSPEND, + 'reason' => WikiSuspensionActionReason::INDEFINITELY_ALLOWED, + ]); + + $this->assertNull(Wiki::findOrFail($wiki->id)->latestSuspensionAction); + } + + public function testLatestSuspensionActionReturnsSuspensionAfterUnsuspension(): void { + $wiki = Wiki::factory()->create(); + WikiSuspensionAction::factory()->for($wiki)->create(); + WikiSuspensionAction::factory() + ->for($wiki) + ->create([ + 'suspension_type' => WikiSuspensionActionType::UNSUSPEND, + 'reason' => WikiSuspensionActionReason::INDEFINITELY_ALLOWED, + ]); + $latestSuspensionAction = WikiSuspensionAction::factory()->for($wiki)->create(); + + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspensionAction->is($latestSuspensionAction)); + } } diff --git a/tests/Routes/Wiki/MineTest.php b/tests/Routes/Wiki/MineTest.php index 9b3c06a2..d112340b 100644 --- a/tests/Routes/Wiki/MineTest.php +++ b/tests/Routes/Wiki/MineTest.php @@ -4,6 +4,8 @@ use App\Enums\ReviewSubmissionActionType; use App\Enums\UserRole; +use App\Enums\WikiSuspensionActionReason; +use App\Enums\WikiSuspensionActionType; use App\ReviewSubmission; use App\User; use App\Wiki; @@ -85,31 +87,41 @@ public function testMineIncludesPendingReviewSubmissions() { } public function testMineIncludesScheduledSuspensions() { - $user = User::factory()->create(['verified' => true]); - - $wikiWithoutSuspension = Wiki::factory()->create(); + $wikiManagerUser = User::factory()->create(['verified' => true]); + $platformAdminUser = User::factory()->create(['verified' => true, 'is_admin' => true]); - WikiManager::factory() - ->for($user) - ->for($wikiWithoutSuspension) + $wikiWithoutSuspension = Wiki::factory() + ->has(WikiManager::factory()) + ->recycle($wikiManagerUser) ->create(); - $wikiWithSuspension = Wiki::factory()->create(); - - WikiManager::factory() - ->for($user) - ->for($wikiWithSuspension) + $wikiWithUnsuspension = Wiki::factory() + ->has(WikiManager::factory()) + ->recycle($wikiManagerUser) + ->create(); + WikiSuspensionAction::factory()->for($wikiWithUnsuspension)->create(); + WikiSuspensionAction::factory() + ->for($wikiWithUnsuspension) + ->create([ + 'suspension_type' => WikiSuspensionActionType::UNSUSPEND, + 'actor_user_id' => $platformAdminUser, + 'reason' => WikiSuspensionActionReason::INDEFINITELY_ALLOWED, + ]); + + $wikiWithSuspension = Wiki::factory() + ->has(WikiManager::factory()) + ->recycle($wikiManagerUser) ->create(); - $suspensionAction = WikiSuspensionAction::factory() ->for($wikiWithSuspension) ->create(); - $this->actingAs($user, 'api') + $this->actingAs($wikiManagerUser, 'api') ->json('POST', $this->route, []) ->assertStatus(200) ->assertJson(['wikis' => [ ['id' => $wikiWithoutSuspension->id, 'latest_suspension_action' => null], + ['id' => $wikiWithUnsuspension->id, 'latest_suspension_action' => null], ['id' => $wikiWithSuspension->id, 'latest_suspension_action' => [ 'id' => $suspensionAction->id, ]], From 691b58d844eb7917e4876374546b0d21624fe40b Mon Sep 17 00:00:00 2001 From: Ollie Date: Fri, 23 Oct 2026 17:12:14 +0100 Subject: [PATCH 04/11] Rename `latestSuspensionAction` to `latestSuspendSuspensionAction` --- app/Http/Controllers/WikisController.php | 2 +- app/Wiki.php | 9 +++---- tests/Feature/Models/WikiTest.php | 30 ++++++++++++------------ 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/app/Http/Controllers/WikisController.php b/app/Http/Controllers/WikisController.php index fa20d659..d1767b53 100644 --- a/app/Http/Controllers/WikisController.php +++ b/app/Http/Controllers/WikisController.php @@ -11,7 +11,7 @@ public function getWikisOwnedByCurrentUser(Request $request): Response { ->user() ->managesWikis() ->withExists('pendingReviewSubmission as has_pending_review') - ->with('latestSuspensionAction') + ->with('latestSuspendSuspensionAction') ->get(); return response( diff --git a/app/Wiki.php b/app/Wiki.php index 8e290c84..566859ce 100644 --- a/app/Wiki.php +++ b/app/Wiki.php @@ -26,7 +26,7 @@ * @property int $is_featured * @property string|null $wiki_deletion_reason * @property-read mixed $domain_decoded - * @property-read WikiSuspensionAction|null $latestSuspensionAction + * @property-read WikiSuspensionAction|null $latestSuspendSuspensionAction * @property-read ReviewSubmission|null $pendingReviewSubmission * @property-read Collection $reviewSubmissions * @property-read int|null $review_submissions_count @@ -99,11 +99,12 @@ public function pendingReviewSubmission(): HasOne { return $this->hasOne(ReviewSubmission::class)->pending(); } - public function latestSuspensionAction(): HasOne { + /** + * Get latest suspension action if it is of type 'suspend', otherwise `null`. + */ + public function latestSuspendSuspensionAction(): HasOne { return $this->hasOne(WikiSuspensionAction::class) ->latestOfMany() - // Don't expose unsuspension actions to the public. This should look and feel the same - // as if there were no suspensions on the Wiki. ->where('suspension_type', WikiSuspensionActionType::SUSPEND); } diff --git a/tests/Feature/Models/WikiTest.php b/tests/Feature/Models/WikiTest.php index 4743a2cb..548b570e 100644 --- a/tests/Feature/Models/WikiTest.php +++ b/tests/Feature/Models/WikiTest.php @@ -73,44 +73,44 @@ public function testCancelledReviewSubmissionIsNotPending(): void { $this->assertNull(Wiki::findOrFail($wiki->id)->pendingReviewSubmission); } - public function testLatestSuspensionActionIsNullWithoutAnySuspensionActions(): void { + public function testLatestSuspendSuspensionActionIsNullWithoutAnySuspensionActions(): void { $wiki = Wiki::factory()->create(); WikiSuspensionAction::factory()->create(); - $this->assertNull(Wiki::findOrFail($wiki->id)->latestSuspensionAction); + $this->assertNull(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction); } - public function testLatestSuspensionActionReturnsLatestSuspensionForTheGivenWiki(): void { + public function testLatestSuspendSuspensionActionReturnsLatestSuspensionForTheGivenWiki(): void { $wiki = Wiki::factory()->create(); // Create two suspension actions for $wiki WikiSuspensionAction::factory()->for($wiki)->create(); - $latestSuspensionAction = WikiSuspensionAction::factory()->for($wiki)->create(); + $latestSuspendSuspensionAction = WikiSuspensionAction::factory()->for($wiki)->create(); // Create a suspension action for a different wiki WikiSuspensionAction::factory()->create(); - $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspensionAction->is($latestSuspensionAction)); + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($latestSuspendSuspensionAction)); } - public function testLatestSuspensionActionReturnsSuspensionScheduledInTheFuture(): void { + public function testLatestSuspendSuspensionActionReturnsSuspensionScheduledInTheFuture(): void { $wiki = Wiki::factory()->create(); WikiSuspensionAction::factory() ->for($wiki) ->create(['active_from' => now()->addWeek()]); - $latestSuspensionAction = WikiSuspensionAction::factory() + $latestSuspendSuspensionAction = WikiSuspensionAction::factory() ->for($wiki) ->create(['active_from' => now()->addMonth()]); - $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspensionAction->is($latestSuspensionAction)); + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($latestSuspendSuspensionAction)); } - public function testLatestSuspensionActionReturnsSuspensionScheduledInThePast(): void { + public function testLatestSuspendSuspensionActionReturnsSuspensionScheduledInThePast(): void { $wiki = Wiki::factory()->create(); WikiSuspensionAction::factory()->for($wiki)->create(); - $latestSuspensionAction = WikiSuspensionAction::factory() + $latestSuspendSuspensionAction = WikiSuspensionAction::factory() ->for($wiki) ->create(['active_from' => now()->subMonth()]); - $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspensionAction->is($latestSuspensionAction)); + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($latestSuspendSuspensionAction)); } public function testLatestSuspensionActionIsNullAfterUnsuspension(): void { @@ -123,10 +123,10 @@ public function testLatestSuspensionActionIsNullAfterUnsuspension(): void { 'reason' => WikiSuspensionActionReason::INDEFINITELY_ALLOWED, ]); - $this->assertNull(Wiki::findOrFail($wiki->id)->latestSuspensionAction); + $this->assertNull(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction); } - public function testLatestSuspensionActionReturnsSuspensionAfterUnsuspension(): void { + public function testLatestSuspendSuspensionActionReturnsSuspensionAfterUnsuspension(): void { $wiki = Wiki::factory()->create(); WikiSuspensionAction::factory()->for($wiki)->create(); WikiSuspensionAction::factory() @@ -135,8 +135,8 @@ public function testLatestSuspensionActionReturnsSuspensionAfterUnsuspension(): 'suspension_type' => WikiSuspensionActionType::UNSUSPEND, 'reason' => WikiSuspensionActionReason::INDEFINITELY_ALLOWED, ]); - $latestSuspensionAction = WikiSuspensionAction::factory()->for($wiki)->create(); + $latestSuspendSuspensionAction = WikiSuspensionAction::factory()->for($wiki)->create(); - $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspensionAction->is($latestSuspensionAction)); + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($latestSuspendSuspensionAction)); } } From 3cd623418a3fe2c20b26ae5f36842351cb846ca4 Mon Sep 17 00:00:00 2001 From: Ollie Date: Fri, 23 Oct 2026 18:06:15 +0100 Subject: [PATCH 05/11] Added test and comments to prove an issue :/ --- tests/Feature/Models/WikiTest.php | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Models/WikiTest.php b/tests/Feature/Models/WikiTest.php index 548b570e..bcf6e3ea 100644 --- a/tests/Feature/Models/WikiTest.php +++ b/tests/Feature/Models/WikiTest.php @@ -113,7 +113,29 @@ public function testLatestSuspendSuspensionActionReturnsSuspensionScheduledInThe $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($latestSuspendSuspensionAction)); } - public function testLatestSuspensionActionIsNullAfterUnsuspension(): void { + public function testLatestSuspendSuspensionActionReturnsWHATWhenSuspensionIsAFutureUnsuspend(): void { + $wiki = Wiki::factory()->create(); + $suspendSuspensionAction = WikiSuspensionAction::factory()->for($wiki)->create(); + $unsuspendSuspensionAction = WikiSuspensionAction::factory() + ->for($wiki) + ->create([ + 'suspension_type' => WikiSuspensionActionType::UNSUSPEND, + 'reason' => WikiSuspensionActionReason::INDEFINITELY_ALLOWED, + 'active_from' => now()->addMonth(), + ]); + + // This is what is currently returned, will we end up making a Wiki available before $unsuspendSuspensionAction->active_from? + $this->assertNull(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction); + // This is what should be returned if you take the name of the relationship at face value + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($suspendSuspensionAction)); + // I'm not sure if returning this would help, we would then need to look at the next suspension action... + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($unsuspendSuspensionAction)); + // Things that might make it easier to deal with: + // * a wiki is suspended due to expiry unless marked otherwise + // * we don't allow suspension actions of type UNSUSPEND have an active_from date in the future - how best to enforce this? + } + + public function testLatestSuspendSuspensionActionIsNullAfterUnsuspension(): void { $wiki = Wiki::factory()->create(); WikiSuspensionAction::factory()->for($wiki)->create(); WikiSuspensionAction::factory() From ef829406e07dc6626b7d79d77d1bd5cfd3387551 Mon Sep 17 00:00:00 2001 From: Ollie Date: Tue, 27 Oct 2026 12:01:12 +0000 Subject: [PATCH 06/11] Remove 'unsuspend' logic --- app/Http/Controllers/WikisController.php | 2 +- app/Wiki.php | 11 ++- app/WikiSuspension.php | 1 + tests/Feature/Models/WikiTest.php | 91 ++++++------------------ tests/Routes/Wiki/MineTest.php | 29 ++------ 5 files changed, 35 insertions(+), 99 deletions(-) diff --git a/app/Http/Controllers/WikisController.php b/app/Http/Controllers/WikisController.php index d1767b53..e1a38e8a 100644 --- a/app/Http/Controllers/WikisController.php +++ b/app/Http/Controllers/WikisController.php @@ -11,7 +11,7 @@ public function getWikisOwnedByCurrentUser(Request $request): Response { ->user() ->managesWikis() ->withExists('pendingReviewSubmission as has_pending_review') - ->with('latestSuspendSuspensionAction') + ->with('latestSuspension') ->get(); return response( diff --git a/app/Wiki.php b/app/Wiki.php index 566859ce..7c5625a4 100644 --- a/app/Wiki.php +++ b/app/Wiki.php @@ -2,7 +2,6 @@ namespace App; -use App\Enums\WikiSuspensionActionType; use App\Helper\DomainHelper; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Collection; @@ -26,7 +25,7 @@ * @property int $is_featured * @property string|null $wiki_deletion_reason * @property-read mixed $domain_decoded - * @property-read WikiSuspensionAction|null $latestSuspendSuspensionAction + * @property-read WikiSuspension|null $latestSuspension * @property-read ReviewSubmission|null $pendingReviewSubmission * @property-read Collection $reviewSubmissions * @property-read int|null $review_submissions_count @@ -100,12 +99,10 @@ public function pendingReviewSubmission(): HasOne { } /** - * Get latest suspension action if it is of type 'suspend', otherwise `null`. + * Get latest suspension or `null`. */ - public function latestSuspendSuspensionAction(): HasOne { - return $this->hasOne(WikiSuspensionAction::class) - ->latestOfMany() - ->where('suspension_type', WikiSuspensionActionType::SUSPEND); + public function latestSuspension(): HasOne { + return $this->hasOne(WikiSuspension::class)->latestOfMany(); } public function wikiDbVersion() { diff --git a/app/WikiSuspension.php b/app/WikiSuspension.php index 15f9da95..57566c4c 100644 --- a/app/WikiSuspension.php +++ b/app/WikiSuspension.php @@ -24,6 +24,7 @@ * @property-read ReviewSubmissionAction|null $reviewSubmissionAction * @property-read Wiki|null $wiki * + * @method static \Database\Factories\WikiSuspensionFactory factory($count = null, $state = []) * @method static \Illuminate\Database\Eloquent\Builder|WikiSuspension newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|WikiSuspension newQuery() * @method static \Illuminate\Database\Eloquent\Builder|WikiSuspension query() diff --git a/tests/Feature/Models/WikiTest.php b/tests/Feature/Models/WikiTest.php index bcf6e3ea..d706e795 100644 --- a/tests/Feature/Models/WikiTest.php +++ b/tests/Feature/Models/WikiTest.php @@ -6,11 +6,9 @@ use App\Enums\ReviewSubmissionActionType; use App\Enums\UserRole; -use App\Enums\WikiSuspensionActionReason; -use App\Enums\WikiSuspensionActionType; use App\ReviewSubmission; use App\Wiki; -use App\WikiSuspensionAction; +use App\WikiSuspension; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; @@ -73,92 +71,49 @@ public function testCancelledReviewSubmissionIsNotPending(): void { $this->assertNull(Wiki::findOrFail($wiki->id)->pendingReviewSubmission); } - public function testLatestSuspendSuspensionActionIsNullWithoutAnySuspensionActions(): void { + public function testLatestSuspendSuspensionIsNullWithNoSuspensions(): void { $wiki = Wiki::factory()->create(); - WikiSuspensionAction::factory()->create(); - $this->assertNull(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction); + // Create a suspension for a different wiki to make sure it isn't returned by $wiki->latestSuspension + WikiSuspension::factory()->create(); + + $this->assertNull(Wiki::findOrFail($wiki->id)->latestSuspension); } - public function testLatestSuspendSuspensionActionReturnsLatestSuspensionForTheGivenWiki(): void { + public function testLatestSuspendSuspensionReturnsLatestSuspensionForTheGivenWiki(): void { $wiki = Wiki::factory()->create(); - // Create two suspension actions for $wiki - WikiSuspensionAction::factory()->for($wiki)->create(); - $latestSuspendSuspensionAction = WikiSuspensionAction::factory()->for($wiki)->create(); - // Create a suspension action for a different wiki - WikiSuspensionAction::factory()->create(); - $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($latestSuspendSuspensionAction)); + // Create two suspensions for $wiki + WikiSuspension::factory()->for($wiki)->create(); + $latestSuspension = WikiSuspension::factory()->for($wiki)->create(); + + // Create a suspension for a different wiki after the $latestSuspension + WikiSuspension::factory()->create(); + + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspension->is($latestSuspension)); } public function testLatestSuspendSuspensionActionReturnsSuspensionScheduledInTheFuture(): void { $wiki = Wiki::factory()->create(); - WikiSuspensionAction::factory() + + WikiSuspension::factory() ->for($wiki) ->create(['active_from' => now()->addWeek()]); - $latestSuspendSuspensionAction = WikiSuspensionAction::factory() + $latestSuspension = WikiSuspension::factory() ->for($wiki) ->create(['active_from' => now()->addMonth()]); - $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($latestSuspendSuspensionAction)); + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspension->is($latestSuspension)); } public function testLatestSuspendSuspensionActionReturnsSuspensionScheduledInThePast(): void { $wiki = Wiki::factory()->create(); - WikiSuspensionAction::factory()->for($wiki)->create(); - $latestSuspendSuspensionAction = WikiSuspensionAction::factory() - ->for($wiki) - ->create(['active_from' => now()->subMonth()]); - - $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($latestSuspendSuspensionAction)); - } - public function testLatestSuspendSuspensionActionReturnsWHATWhenSuspensionIsAFutureUnsuspend(): void { - $wiki = Wiki::factory()->create(); - $suspendSuspensionAction = WikiSuspensionAction::factory()->for($wiki)->create(); - $unsuspendSuspensionAction = WikiSuspensionAction::factory() + WikiSuspension::factory()->for($wiki)->create(); + $latestSuspension = WikiSuspension::factory() ->for($wiki) - ->create([ - 'suspension_type' => WikiSuspensionActionType::UNSUSPEND, - 'reason' => WikiSuspensionActionReason::INDEFINITELY_ALLOWED, - 'active_from' => now()->addMonth(), - ]); - - // This is what is currently returned, will we end up making a Wiki available before $unsuspendSuspensionAction->active_from? - $this->assertNull(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction); - // This is what should be returned if you take the name of the relationship at face value - $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($suspendSuspensionAction)); - // I'm not sure if returning this would help, we would then need to look at the next suspension action... - $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($unsuspendSuspensionAction)); - // Things that might make it easier to deal with: - // * a wiki is suspended due to expiry unless marked otherwise - // * we don't allow suspension actions of type UNSUSPEND have an active_from date in the future - how best to enforce this? - } - - public function testLatestSuspendSuspensionActionIsNullAfterUnsuspension(): void { - $wiki = Wiki::factory()->create(); - WikiSuspensionAction::factory()->for($wiki)->create(); - WikiSuspensionAction::factory() - ->for($wiki) - ->create([ - 'suspension_type' => WikiSuspensionActionType::UNSUSPEND, - 'reason' => WikiSuspensionActionReason::INDEFINITELY_ALLOWED, - ]); - - $this->assertNull(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction); - } - - public function testLatestSuspendSuspensionActionReturnsSuspensionAfterUnsuspension(): void { - $wiki = Wiki::factory()->create(); - WikiSuspensionAction::factory()->for($wiki)->create(); - WikiSuspensionAction::factory() - ->for($wiki) - ->create([ - 'suspension_type' => WikiSuspensionActionType::UNSUSPEND, - 'reason' => WikiSuspensionActionReason::INDEFINITELY_ALLOWED, - ]); - $latestSuspendSuspensionAction = WikiSuspensionAction::factory()->for($wiki)->create(); + ->create(['active_from' => now()->subMonth()]); - $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspendSuspensionAction->is($latestSuspendSuspensionAction)); + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspension->is($latestSuspension)); } } diff --git a/tests/Routes/Wiki/MineTest.php b/tests/Routes/Wiki/MineTest.php index d112340b..dce048a2 100644 --- a/tests/Routes/Wiki/MineTest.php +++ b/tests/Routes/Wiki/MineTest.php @@ -4,13 +4,11 @@ use App\Enums\ReviewSubmissionActionType; use App\Enums\UserRole; -use App\Enums\WikiSuspensionActionReason; -use App\Enums\WikiSuspensionActionType; use App\ReviewSubmission; use App\User; use App\Wiki; use App\WikiManager; -use App\WikiSuspensionAction; +use App\WikiSuspension; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Config; use Tests\Routes\Traits\OptionsRequestAllowed; @@ -88,31 +86,17 @@ public function testMineIncludesPendingReviewSubmissions() { public function testMineIncludesScheduledSuspensions() { $wikiManagerUser = User::factory()->create(['verified' => true]); - $platformAdminUser = User::factory()->create(['verified' => true, 'is_admin' => true]); - $wikiWithoutSuspension = Wiki::factory() + $wikiWithNoSuspensions = Wiki::factory() ->has(WikiManager::factory()) ->recycle($wikiManagerUser) ->create(); - $wikiWithUnsuspension = Wiki::factory() - ->has(WikiManager::factory()) - ->recycle($wikiManagerUser) - ->create(); - WikiSuspensionAction::factory()->for($wikiWithUnsuspension)->create(); - WikiSuspensionAction::factory() - ->for($wikiWithUnsuspension) - ->create([ - 'suspension_type' => WikiSuspensionActionType::UNSUSPEND, - 'actor_user_id' => $platformAdminUser, - 'reason' => WikiSuspensionActionReason::INDEFINITELY_ALLOWED, - ]); - $wikiWithSuspension = Wiki::factory() ->has(WikiManager::factory()) ->recycle($wikiManagerUser) ->create(); - $suspensionAction = WikiSuspensionAction::factory() + $suspension = WikiSuspension::factory() ->for($wikiWithSuspension) ->create(); @@ -120,10 +104,9 @@ public function testMineIncludesScheduledSuspensions() { ->json('POST', $this->route, []) ->assertStatus(200) ->assertJson(['wikis' => [ - ['id' => $wikiWithoutSuspension->id, 'latest_suspension_action' => null], - ['id' => $wikiWithUnsuspension->id, 'latest_suspension_action' => null], - ['id' => $wikiWithSuspension->id, 'latest_suspension_action' => [ - 'id' => $suspensionAction->id, + ['id' => $wikiWithNoSuspensions->id, 'latest_suspension' => null], + ['id' => $wikiWithSuspension->id, 'latest_suspension' => [ + 'id' => $suspension->id, ]], ]]); } From 59c9441cc2452dcedb64b11b9d5935b29ad3c982 Mon Sep 17 00:00:00 2001 From: Thomas Arrow Date: Tue, 27 Oct 2026 15:29:51 +0000 Subject: [PATCH 07/11] Add active accessor to WikiSuspension This adds an Accessor[0] to allow the `/mine` api to pass on if a given suspension is active or not for use in the front end. This prevents having to do date calculations in the UI. [0] https://laravel.com/framework/docs/11.x/eloquent-mutators#building-value-objects-from-multiple-attributes --- app/WikiSuspension.php | 9 +++++++++ tests/Feature/Models/WikiSuspensionTest.php | 8 ++++++++ tests/Routes/Wiki/MineTest.php | 1 + 3 files changed, 18 insertions(+) diff --git a/app/WikiSuspension.php b/app/WikiSuspension.php index 57566c4c..ceafa51e 100644 --- a/app/WikiSuspension.php +++ b/app/WikiSuspension.php @@ -7,6 +7,7 @@ use App\Enums\WikiSuspensionReason; use Carbon\CarbonImmutable; use Database\Factories\WikiSuspensionFactory; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -64,4 +65,12 @@ public function reviewSubmissionAction(): BelongsTo { public function wiki(): BelongsTo { return $this->belongsTo(Wiki::class); } + + protected function active(): Attribute { + return Attribute::make( + get: fn (mixed $value, array $attributes): bool => CarbonImmutable::now()->greaterThan($attributes['active_from']), + ); + } + + protected $appends = ['active']; } diff --git a/tests/Feature/Models/WikiSuspensionTest.php b/tests/Feature/Models/WikiSuspensionTest.php index 4bc602d2..63c856b4 100644 --- a/tests/Feature/Models/WikiSuspensionTest.php +++ b/tests/Feature/Models/WikiSuspensionTest.php @@ -130,4 +130,12 @@ public function testDatesAndTimesCastCorrectly(): void { $this->assertInstanceOf(CarbonImmutable::class, $suspension->created_at); $this->assertInstanceOf(CarbonImmutable::class, $suspension->updated_at); } + + public function testActiveAccessor(): void { + $suspensionAlreadyActive = WikiSuspension::factory()->create(['active_from' => now()->subDay()]); + $suspensionNotActive = WikiSuspension::factory()->create(['active_from' => now()->addDay()]); + + $this->assertTrue($suspensionAlreadyActive->active); + $this->assertFalse($suspensionNotActive->active); + } } diff --git a/tests/Routes/Wiki/MineTest.php b/tests/Routes/Wiki/MineTest.php index dce048a2..00332a9a 100644 --- a/tests/Routes/Wiki/MineTest.php +++ b/tests/Routes/Wiki/MineTest.php @@ -107,6 +107,7 @@ public function testMineIncludesScheduledSuspensions() { ['id' => $wikiWithNoSuspensions->id, 'latest_suspension' => null], ['id' => $wikiWithSuspension->id, 'latest_suspension' => [ 'id' => $suspension->id, + 'active' => true, ]], ]]); } From db7d051295834491cc962c008af5541506f95048 Mon Sep 17 00:00:00 2001 From: Ollie Date: Tue, 27 Oct 2026 23:03:51 +0000 Subject: [PATCH 08/11] Use greater than or equal to for 'active_from' datetime comparison --- app/WikiSuspension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/WikiSuspension.php b/app/WikiSuspension.php index ceafa51e..12f28040 100644 --- a/app/WikiSuspension.php +++ b/app/WikiSuspension.php @@ -68,7 +68,7 @@ public function wiki(): BelongsTo { protected function active(): Attribute { return Attribute::make( - get: fn (mixed $value, array $attributes): bool => CarbonImmutable::now()->greaterThan($attributes['active_from']), + get: fn (mixed $value, array $attributes): bool => CarbonImmutable::now()->gte($attributes['active_from']), ); } From be967dc9fb2c0bc58c7f9fec9ffc66c263e781ce Mon Sep 17 00:00:00 2001 From: Ollie Date: Tue, 27 Oct 2026 23:18:09 +0000 Subject: [PATCH 09/11] Reorder properties and methods in WikiSuspension * Put properties before methods * Put protected properties and methods before public ones --- app/WikiSuspension.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/WikiSuspension.php b/app/WikiSuspension.php index 12f28040..f8cb0efc 100644 --- a/app/WikiSuspension.php +++ b/app/WikiSuspension.php @@ -44,6 +44,8 @@ class WikiSuspension extends Model { /** @use HasFactory */ use HasFactory; + protected $appends = ['active']; + protected function casts() { return [ 'reason' => WikiSuspensionReason::class, @@ -54,6 +56,12 @@ protected function casts() { ]; } + protected function active(): Attribute { + return Attribute::make( + get: fn (mixed $value, array $attributes): bool => CarbonImmutable::now()->gte($attributes['active_from']), + ); + } + public function actor(): BelongsTo { return $this->belongsTo(User::class, 'actor_user_id'); } @@ -65,12 +73,4 @@ public function reviewSubmissionAction(): BelongsTo { public function wiki(): BelongsTo { return $this->belongsTo(Wiki::class); } - - protected function active(): Attribute { - return Attribute::make( - get: fn (mixed $value, array $attributes): bool => CarbonImmutable::now()->gte($attributes['active_from']), - ); - } - - protected $appends = ['active']; } From 363e2cef74af38b6e708b4c1c28cfc6b5ffdd197 Mon Sep 17 00:00:00 2001 From: Ollie Date: Tue, 27 Oct 2026 23:57:17 +0000 Subject: [PATCH 10/11] Run php artisan ide-helper:models 'App\WikiSuspension' --- app/WikiSuspension.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/WikiSuspension.php b/app/WikiSuspension.php index f8cb0efc..6e2e840c 100644 --- a/app/WikiSuspension.php +++ b/app/WikiSuspension.php @@ -21,6 +21,7 @@ * @property WikiSuspensionReason $reason * @property CarbonImmutable|null $created_at * @property CarbonImmutable|null $updated_at + * @property-read bool $active * @property-read User|null $actor * @property-read ReviewSubmissionAction|null $reviewSubmissionAction * @property-read Wiki|null $wiki From a2525412564ff18fe550891c14a0efa2bece8bf7 Mon Sep 17 00:00:00 2001 From: Ollie Date: Fri, 30 Oct 2026 14:53:18 +0000 Subject: [PATCH 11/11] Change `active` to `is_active` --- app/WikiSuspension.php | 6 +++--- tests/Feature/Models/WikiSuspensionTest.php | 4 ++-- tests/Routes/Wiki/MineTest.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/WikiSuspension.php b/app/WikiSuspension.php index 6e2e840c..00bbd2a6 100644 --- a/app/WikiSuspension.php +++ b/app/WikiSuspension.php @@ -21,8 +21,8 @@ * @property WikiSuspensionReason $reason * @property CarbonImmutable|null $created_at * @property CarbonImmutable|null $updated_at - * @property-read bool $active * @property-read User|null $actor + * @property-read bool $is_active * @property-read ReviewSubmissionAction|null $reviewSubmissionAction * @property-read Wiki|null $wiki * @@ -45,7 +45,7 @@ class WikiSuspension extends Model { /** @use HasFactory */ use HasFactory; - protected $appends = ['active']; + protected $appends = ['is_active']; protected function casts() { return [ @@ -57,7 +57,7 @@ protected function casts() { ]; } - protected function active(): Attribute { + protected function isActive(): Attribute { return Attribute::make( get: fn (mixed $value, array $attributes): bool => CarbonImmutable::now()->gte($attributes['active_from']), ); diff --git a/tests/Feature/Models/WikiSuspensionTest.php b/tests/Feature/Models/WikiSuspensionTest.php index 63c856b4..4f3e6de8 100644 --- a/tests/Feature/Models/WikiSuspensionTest.php +++ b/tests/Feature/Models/WikiSuspensionTest.php @@ -135,7 +135,7 @@ public function testActiveAccessor(): void { $suspensionAlreadyActive = WikiSuspension::factory()->create(['active_from' => now()->subDay()]); $suspensionNotActive = WikiSuspension::factory()->create(['active_from' => now()->addDay()]); - $this->assertTrue($suspensionAlreadyActive->active); - $this->assertFalse($suspensionNotActive->active); + $this->assertTrue($suspensionAlreadyActive->is_active); + $this->assertFalse($suspensionNotActive->is_active); } } diff --git a/tests/Routes/Wiki/MineTest.php b/tests/Routes/Wiki/MineTest.php index 00332a9a..b191e0c7 100644 --- a/tests/Routes/Wiki/MineTest.php +++ b/tests/Routes/Wiki/MineTest.php @@ -107,7 +107,7 @@ public function testMineIncludesScheduledSuspensions() { ['id' => $wikiWithNoSuspensions->id, 'latest_suspension' => null], ['id' => $wikiWithSuspension->id, 'latest_suspension' => [ 'id' => $suspension->id, - 'active' => true, + 'is_active' => true, ]], ]]); }