$this->token]); $this->withHeaders(['Accept-Language' => 'es']); } /** * @param array $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); } }