diff --git a/app/Http/Controllers/WikisController.php b/app/Http/Controllers/WikisController.php index 92a7afb5..e1a38e8a 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('latestSuspension') ->get(); return response( diff --git a/app/Wiki.php b/app/Wiki.php index 4401256f..7c5625a4 100644 --- a/app/Wiki.php +++ b/app/Wiki.php @@ -25,6 +25,7 @@ * @property int $is_featured * @property string|null $wiki_deletion_reason * @property-read mixed $domain_decoded + * @property-read WikiSuspension|null $latestSuspension * @property-read ReviewSubmission|null $pendingReviewSubmission * @property-read Collection $reviewSubmissions * @property-read int|null $review_submissions_count @@ -97,6 +98,13 @@ public function pendingReviewSubmission(): HasOne { return $this->hasOne(ReviewSubmission::class)->pending(); } + /** + * Get latest suspension or `null`. + */ + public function latestSuspension(): HasOne { + return $this->hasOne(WikiSuspension::class)->latestOfMany(); + } + public function wikiDbVersion() { return $this->hasOne(WikiDb::class)->select(['id', 'wiki_id', 'version']); } diff --git a/app/WikiSuspension.php b/app/WikiSuspension.php index 15f9da95..00bbd2a6 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; @@ -21,9 +22,11 @@ * @property CarbonImmutable|null $created_at * @property CarbonImmutable|null $updated_at * @property-read User|null $actor + * @property-read bool $is_active * @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() @@ -42,6 +45,8 @@ class WikiSuspension extends Model { /** @use HasFactory */ use HasFactory; + protected $appends = ['is_active']; + protected function casts() { return [ 'reason' => WikiSuspensionReason::class, @@ -52,6 +57,12 @@ protected function casts() { ]; } + protected function isActive(): 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'); } diff --git a/tests/Feature/Models/WikiSuspensionTest.php b/tests/Feature/Models/WikiSuspensionTest.php index 4bc602d2..4f3e6de8 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->is_active); + $this->assertFalse($suspensionNotActive->is_active); + } } diff --git a/tests/Feature/Models/WikiTest.php b/tests/Feature/Models/WikiTest.php index 92ebacf9..d706e795 100644 --- a/tests/Feature/Models/WikiTest.php +++ b/tests/Feature/Models/WikiTest.php @@ -8,6 +8,7 @@ use App\Enums\UserRole; use App\ReviewSubmission; use App\Wiki; +use App\WikiSuspension; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; @@ -69,4 +70,50 @@ public function testCancelledReviewSubmissionIsNotPending(): void { $this->assertNull(Wiki::findOrFail($wiki->id)->pendingReviewSubmission); } + + public function testLatestSuspendSuspensionIsNullWithNoSuspensions(): void { + $wiki = Wiki::factory()->create(); + + // 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 testLatestSuspendSuspensionReturnsLatestSuspensionForTheGivenWiki(): void { + $wiki = Wiki::factory()->create(); + + // 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(); + + WikiSuspension::factory() + ->for($wiki) + ->create(['active_from' => now()->addWeek()]); + $latestSuspension = WikiSuspension::factory() + ->for($wiki) + ->create(['active_from' => now()->addMonth()]); + + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspension->is($latestSuspension)); + } + + public function testLatestSuspendSuspensionActionReturnsSuspensionScheduledInThePast(): void { + $wiki = Wiki::factory()->create(); + + WikiSuspension::factory()->for($wiki)->create(); + $latestSuspension = WikiSuspension::factory() + ->for($wiki) + ->create(['active_from' => now()->subMonth()]); + + $this->assertTrue(Wiki::findOrFail($wiki->id)->latestSuspension->is($latestSuspension)); + } } diff --git a/tests/Routes/Wiki/MineTest.php b/tests/Routes/Wiki/MineTest.php index 13943d2c..b191e0c7 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\WikiSuspension; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Config; use Tests\Routes\Traits\OptionsRequestAllowed; @@ -82,4 +83,32 @@ public function testMineIncludesPendingReviewSubmissions() { ['id' => $wikiWithApprovedSubmission->id, 'has_pending_review' => false], ]]); } + + public function testMineIncludesScheduledSuspensions() { + $wikiManagerUser = User::factory()->create(['verified' => true]); + + $wikiWithNoSuspensions = Wiki::factory() + ->has(WikiManager::factory()) + ->recycle($wikiManagerUser) + ->create(); + + $wikiWithSuspension = Wiki::factory() + ->has(WikiManager::factory()) + ->recycle($wikiManagerUser) + ->create(); + $suspension = WikiSuspension::factory() + ->for($wikiWithSuspension) + ->create(); + + $this->actingAs($wikiManagerUser, 'api') + ->json('POST', $this->route, []) + ->assertStatus(200) + ->assertJson(['wikis' => [ + ['id' => $wikiWithNoSuspensions->id, 'latest_suspension' => null], + ['id' => $wikiWithSuspension->id, 'latest_suspension' => [ + 'id' => $suspension->id, + 'is_active' => true, + ]], + ]]); + } }