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>
This commit is contained in:
2026-08-19 17:55:27 +02:00
commit 3897113313
534 changed files with 64050 additions and 0 deletions

View File

@@ -0,0 +1,218 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Api;
use App\Models\ContentView;
use App\Models\Page;
use App\Models\Project;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/**
* What the frontend read, reported back.
*
* Two things need holding still. The first is that this stays a tally: one row
* per record, per language, per day, however many reports arrive. The second
* is what it refuses to collect — the moment a visitor could be reconstructed
* out of this table it has become something nobody agreed to run.
*/
class ContentViewsTest extends TestCase
{
use RefreshDatabase;
protected string $token = 'ingest-secret';
protected function setUp(): void
{
parent::setUp();
config(['site.telemetry.token' => $this->token]);
$this->withHeaders(['Accept-Language' => 'es']);
}
/**
* @param array<string, mixed> $payload
*/
protected function report(array $payload): TestResponse
{
return $this->withHeaders(['X-Ingest-Token' => $this->token])
->postJson('/api/public/v1/telemetry/views', $payload);
}
protected function page(string $slug = 'quienes-somos'): Page
{
return Page::factory()->create([
'slug' => ['es' => $slug, 'en' => "{$slug}-en", 'ca' => "{$slug}-ca"],
]);
}
#[Test]
public function it_counts_a_read(): void
{
$page = $this->page();
$this->report(['type' => 'page', 'slug' => 'quienes-somos', 'locale' => 'es'])
->assertAccepted()
->assertJsonPath('data.recorded', 1)
->assertHeader('Cache-Control', 'no-store, private');
$this->assertDatabaseHas('content_views', [
'viewable_type' => $page->getMorphClass(),
'viewable_id' => $page->id,
'locale' => 'es',
'count' => 1,
]);
}
/**
* The property that keeps this from becoming a log.
*/
#[Test]
public function reports_for_the_same_day_add_up_in_one_row(): void
{
$this->page();
foreach (range(1, 4) as $ignored) {
$this->report(['type' => 'page', 'slug' => 'quienes-somos', 'locale' => 'es'])->assertAccepted();
}
$this->assertSame(1, ContentView::query()->count());
$this->assertSame(4, (int) ContentView::query()->first()->count);
}
#[Test]
public function a_batch_can_carry_its_own_counts(): void
{
$this->page();
Project::factory()->create(['slug' => ['es' => 'mi-web', 'en' => 'my-web', 'ca' => 'el-meu-web']]);
$this->report(['views' => [
['type' => 'page', 'slug' => 'quienes-somos', 'locale' => 'es', 'count' => 12],
['type' => 'project', 'slug' => 'mi-web', 'locale' => 'es', 'count' => 3],
]])
->assertAccepted()
->assertJsonPath('data.recorded', 2);
$this->assertSame(15, (int) ContentView::query()->sum('count'));
}
/**
* The same record read in two languages is two rows: which language people
* read something in is most of what the figures are for.
*/
#[Test]
public function each_language_is_counted_separately(): void
{
$page = $this->page();
$this->report(['type' => 'page', 'slug' => 'quienes-somos', 'locale' => 'es'])->assertAccepted();
$this->report(['type' => 'page', 'slug' => 'quienes-somos-ca', 'locale' => 'ca'])->assertAccepted();
$this->assertSame(2, ContentView::query()->count());
$this->assertSame(2, $page->viewsSince(30));
}
/**
* A slug that no longer resolves is a 404, and the endpoint next door
* exists for those. Counting it here would attach a read to nothing.
*/
#[Test]
public function a_slug_that_resolves_to_nothing_is_not_counted(): void
{
$this->report(['type' => 'page', 'slug' => 'no-existe', 'locale' => 'es'])
->assertAccepted()
->assertJsonPath('data.received', 1)
->assertJsonPath('data.recorded', 0);
$this->assertSame(0, ContentView::query()->count());
}
#[Test]
public function it_refuses_without_the_secret(): void
{
$this->page();
$this->postJson('/api/public/v1/telemetry/views', ['type' => 'page', 'slug' => 'quienes-somos'])
->assertForbidden();
$this->assertSame(0, ContentView::query()->count());
}
#[Test]
public function it_rejects_what_it_cannot_use(): void
{
$this->report(['type' => 'invento', 'slug' => 'x'])
->assertUnprocessable()
->assertJsonValidationErrors('views.0.type');
// The frontend counts its own renders; it does not get to invent an
// audience of any size it likes.
$this->page();
$this->report(['type' => 'page', 'slug' => 'quienes-somos', 'count' => 999999])
->assertUnprocessable()
->assertJsonValidationErrors('views.0.count');
}
/**
* The whole privacy argument for this table, as a test: there is nowhere
* to put an address, a session or a time of day, so no future change can
* quietly start keeping one.
*/
#[Test]
public function nothing_about_a_visitor_is_stored(): void
{
$this->page();
$this->report(['type' => 'page', 'slug' => 'quienes-somos', 'locale' => 'es'])->assertAccepted();
$columns = array_keys((array) ContentView::query()->first()->getAttributes());
$this->assertSame([
'id', 'viewable_type', 'viewable_id', 'locale', 'day', 'count',
'created_at', 'updated_at',
], $columns);
}
#[Test]
public function the_listing_figure_comes_from_one_query(): void
{
$page = $this->page();
ContentView::record($page, 'es', 5);
ContentView::record($page, 'ca', 2);
$row = Page::query()->withRecentViews()->find($page->id);
$this->assertSame(7, (int) $row->recent_views);
}
#[Test]
public function figures_older_than_the_window_do_not_count_as_recent(): void
{
$page = $this->page();
$page->views()->create([
'locale' => 'es',
'day' => now()->subDays(90)->toDateString(),
'count' => 40,
]);
$this->assertSame(0, $page->viewsSince(30));
}
#[Test]
public function pruning_forgets_figures_nobody_compares_against(): void
{
$page = $this->page();
$page->views()->create(['locale' => 'es', 'day' => now()->subDays(500)->toDateString(), 'count' => 9]);
$page->views()->create(['locale' => 'es', 'day' => now()->subDays(10)->toDateString(), 'count' => 3]);
$this->artisan('content:prune')->assertExitCode(0);
$this->assertSame(1, ContentView::query()->count());
$this->assertSame(3, (int) ContentView::query()->first()->count);
}
}