Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
226 lines
6.8 KiB
PHP
226 lines
6.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Site;
|
|
|
|
use App\Enums\CallStatus;
|
|
use App\Enums\PageStatus;
|
|
use App\Models\Call;
|
|
use App\Models\Page;
|
|
use App\Models\Project;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Content that is only true between two dates.
|
|
*
|
|
* The window is enforced by the scopes rather than by a scheduled job, so it
|
|
* holds even when nothing is running — a queue worker being down must not put
|
|
* last month's notice back on the site.
|
|
*/
|
|
class PublicationWindowTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->withHeaders(['Accept-Language' => 'es']);
|
|
Queue::fake();
|
|
}
|
|
|
|
#[Test]
|
|
public function a_page_stays_off_the_site_until_its_date(): void
|
|
{
|
|
Page::factory()->create([
|
|
'status' => PageStatus::Published,
|
|
'title' => ['es' => 'El anuncio'],
|
|
'slug' => ['es' => 'el-anuncio'],
|
|
'published_at' => now()->addDay(),
|
|
]);
|
|
|
|
$this->getJson('/api/public/v1/pages/el-anuncio')->assertNotFound();
|
|
|
|
$this->travel(2)->days();
|
|
|
|
$this->getJson('/api/public/v1/pages/el-anuncio')->assertOk();
|
|
}
|
|
|
|
#[Test]
|
|
public function a_page_drops_off_the_site_on_its_end_date(): void
|
|
{
|
|
Page::factory()->create([
|
|
'status' => PageStatus::Published,
|
|
'title' => ['es' => 'Taller de octubre'],
|
|
'slug' => ['es' => 'taller'],
|
|
'published_at' => now()->subWeek(),
|
|
'unpublish_at' => now()->addDay(),
|
|
]);
|
|
|
|
$this->getJson('/api/public/v1/pages/taller')->assertOk();
|
|
|
|
$this->travel(2)->days();
|
|
|
|
// Nobody has to remember to take it down, which is the whole point:
|
|
// nobody ever does.
|
|
$this->getJson('/api/public/v1/pages/taller')->assertNotFound();
|
|
}
|
|
|
|
#[Test]
|
|
public function the_window_holds_without_anything_running(): void
|
|
{
|
|
Page::factory()->create([
|
|
'status' => PageStatus::Published,
|
|
'title' => ['es' => 'Caducada'],
|
|
'slug' => ['es' => 'caducada'],
|
|
'published_at' => now()->subMonth(),
|
|
'unpublish_at' => now()->subDay(),
|
|
]);
|
|
|
|
// No command, no queue, no scheduler — the scope is what decides.
|
|
$this->getJson('/api/public/v1/pages')
|
|
->assertOk()
|
|
->assertJsonCount(0, 'data');
|
|
}
|
|
|
|
#[Test]
|
|
public function an_expired_project_leaves_the_url_list_too(): void
|
|
{
|
|
Project::factory()->published()->create([
|
|
'title' => ['es' => 'Terminado'],
|
|
'slug' => ['es' => 'terminado'],
|
|
'unpublish_at' => now()->subDay(),
|
|
]);
|
|
|
|
$slugs = $this->getJson('/api/public/v1/slugs')->assertOk()->json('data');
|
|
|
|
// Otherwise the frontend would keep it in the sitemap and point search
|
|
// engines at a 404.
|
|
$this->assertEmpty($slugs['projects'] ?? []);
|
|
}
|
|
|
|
#[Test]
|
|
public function preview_still_reaches_content_outside_its_window(): void
|
|
{
|
|
config()->set('site.preview_token', 'secreto');
|
|
|
|
Page::factory()->create([
|
|
'status' => PageStatus::Published,
|
|
'title' => ['es' => 'Aún no'],
|
|
'slug' => ['es' => 'aun-no'],
|
|
'published_at' => now()->addWeek(),
|
|
]);
|
|
|
|
// Checking a scheduled page before its date is exactly what preview is
|
|
// for.
|
|
$this->withHeaders(['X-Preview-Token' => 'secreto'])
|
|
->getJson('/api/public/v1/pages/aun-no')
|
|
->assertOk();
|
|
}
|
|
|
|
#[Test]
|
|
public function the_command_closes_a_call_whose_date_has_passed(): void
|
|
{
|
|
$call = Call::factory()->create([
|
|
'project_id' => null,
|
|
'status' => CallStatus::Open,
|
|
'closes_at' => now()->subWeek(),
|
|
]);
|
|
|
|
$this->artisan('content:schedule')->assertSuccessful();
|
|
|
|
// The site already stopped offering it; this is about the panel not
|
|
// claiming otherwise, and a counter that stays worth reading.
|
|
$this->assertSame(CallStatus::Closed, $call->refresh()->status);
|
|
}
|
|
|
|
#[Test]
|
|
public function the_command_leaves_a_call_that_is_still_open(): void
|
|
{
|
|
$open = Call::factory()->create([
|
|
'project_id' => null,
|
|
'status' => CallStatus::Open,
|
|
'closes_at' => now()->addWeek(),
|
|
]);
|
|
|
|
$undated = Call::factory()->create([
|
|
'project_id' => null,
|
|
'status' => CallStatus::Open,
|
|
'closes_at' => null,
|
|
]);
|
|
|
|
$this->artisan('content:schedule')->assertSuccessful();
|
|
|
|
$this->assertSame(CallStatus::Open, $open->refresh()->status);
|
|
$this->assertSame(CallStatus::Open, $undated->refresh()->status);
|
|
}
|
|
|
|
#[Test]
|
|
public function a_dry_run_changes_nothing(): void
|
|
{
|
|
$call = Call::factory()->create([
|
|
'project_id' => null,
|
|
'status' => CallStatus::Open,
|
|
'closes_at' => now()->subWeek(),
|
|
]);
|
|
|
|
$this->artisan('content:schedule', ['--dry-run' => true])->assertSuccessful();
|
|
|
|
$this->assertSame(CallStatus::Open, $call->refresh()->status);
|
|
}
|
|
|
|
#[Test]
|
|
public function the_panel_can_tell_the_three_states_apart(): void
|
|
{
|
|
$scheduled = Page::factory()->create([
|
|
'status' => PageStatus::Published,
|
|
'published_at' => now()->addDays(3),
|
|
]);
|
|
|
|
$expired = Page::factory()->create([
|
|
'status' => PageStatus::Published,
|
|
'published_at' => now()->subMonth(),
|
|
'unpublish_at' => now()->subDay(),
|
|
]);
|
|
|
|
$live = Page::factory()->create([
|
|
'status' => PageStatus::Published,
|
|
'published_at' => now()->subDay(),
|
|
]);
|
|
|
|
// A panel that reports "Published" for something the site is not
|
|
// showing is lying to whoever wrote it.
|
|
$this->assertTrue($scheduled->isScheduled());
|
|
$this->assertFalse($scheduled->hasExpired());
|
|
|
|
$this->assertTrue($expired->hasExpired());
|
|
$this->assertFalse($expired->isScheduled());
|
|
|
|
$this->assertFalse($live->isScheduled());
|
|
$this->assertFalse($live->hasExpired());
|
|
}
|
|
|
|
#[Test]
|
|
public function content_about_to_go_can_be_spotted_in_advance(): void
|
|
{
|
|
$soon = Page::factory()->create([
|
|
'status' => PageStatus::Published,
|
|
'published_at' => now()->subMonth(),
|
|
'unpublish_at' => now()->addDays(2),
|
|
]);
|
|
|
|
$later = Page::factory()->create([
|
|
'status' => PageStatus::Published,
|
|
'published_at' => now()->subMonth(),
|
|
'unpublish_at' => now()->addMonths(2),
|
|
]);
|
|
|
|
$this->assertTrue($soon->expiresSoon());
|
|
$this->assertFalse($later->expiresSoon());
|
|
}
|
|
}
|