Files
landing-api/tests/Feature/Site/PublicationWindowTest.php
Josep Piguillem 3897113313 Initial commit: Pireon Admin — headless admin panel and public API
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>
2026-08-19 17:55:27 +02:00

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());
}
}