withoutVite(); $this->f = receiptFixture(); supplyGrant($this->f['receiver'], ['audit_metadata_read']); $this->actingAs($this->f['receiver']); $this->ids = []; foreach (range(1, 30) as $index) { $this->ids[] = DB::table('audit_logs')->insertGetId(['tenant_id' => $this->f['tenant']->id, 'user_id' => $this->f['receiver']->id, 'action' => $index % 3 === 0 ? 'people.selected' : 'receipt.confirmed', 'created_at' => '2026-11-01 12:00:00', 'before' => json_encode(['bank' => 'PRIVATE-BEFORE']), 'after' => json_encode(['secret' => 'PRIVATE-AFTER'])]); } }); test('audit metadata block paginates oldest events and exposes only the four allowed metadata fields', function () { $before = DB::table('audit_logs')->orderBy('id')->get()->toArray(); $this->get('/auditoria/eventos')->assertOk()->assertHeader('Cache-Control', 'max-age=0, no-store, private') ->assertDontSee('PRIVATE-BEFORE')->assertDontSee('PRIVATE-AFTER') ->assertInertia(fn (Assert $p) => $p->component('audit/metadata')->where('events.total', 30)->has('events.data', 25) ->where('events.data.0.id', $this->ids[0])->where('events.data.24.id', $this->ids[24]) ->missing('events.data.0.before')->missing('events.data.0.after')->missing('events.data.0.tenant_id')); $this->get('/auditoria/eventos?page=2')->assertOk()->assertInertia(fn (Assert $p) => $p->has('events.data', 5)->where('events.data.0.id', $this->ids[25])); expect(DB::table('audit_logs')->orderBy('id')->get()->toArray())->toEqual($before); }); test('audit metadata block exports the complete filtered scope independently of the screen page and protects spreadsheet cells', function () { $query = '?action=people.selected&actor_id='.$this->f['receiver']->id.'&from=2026-11-01&to=2026-11-01'; $this->get('/auditoria/eventos'.$query)->assertOk()->assertInertia(fn (Assert $p) => $p->where('events.total', 10)); $csv = $this->get('/auditoria/eventos.csv'.$query.'&page=2')->assertOk()->streamedContent(); expect(substr_count($csv, 'people.selected'))->toBe(10)->and($csv)->not->toContain('receipt.confirmed', 'PRIVATE-BEFORE', 'PRIVATE-AFTER'); $this->get('/auditoria/eventos?to=2026-11-01')->assertOk(); $this->getJson('/auditoria/eventos?from=2026-11-02&to=2026-11-01')->assertUnprocessable(); DB::table('audit_logs')->insert(['tenant_id' => $this->f['tenant']->id, 'user_id' => $this->f['receiver']->id, 'action' => '=1+1', 'created_at' => now(), 'after' => '{}']); $formula = $this->get('/auditoria/eventos.csv?action='.urlencode('=1+1'))->assertOk()->streamedContent(); expect($formula)->toContain("'=1+1"); $all = $this->get('/auditoria/eventos.csv')->assertOk()->streamedContent(); expect(substr_count($all, 'receipt.confirmed'))->toBe(20)->and(substr_count($all, 'people.selected'))->toBe(10); }); test('audit metadata block requires an explicit current nonmaintenance grant even for administrators', function (string $mode) { if ($mode === 'administrator') { supplyGrant($this->f['issuer'], ['admin']); $this->actingAs($this->f['issuer']); } elseif ($mode === 'expired') { DB::table('organization_memberships')->where('user_id', $this->f['receiver']->id)->update(['expires_at' => now()->subMinute()]); } elseif ($mode === 'maintenance') { DB::table('organization_memberships')->where('user_id', $this->f['receiver']->id)->update(['maintenance' => true]); } else { DB::table('organization_profiles')->where('tenant_id', $this->f['tenant']->id)->update(['permissions' => '[]']); } $this->get('/auditoria/eventos')->assertForbidden(); $this->get('/auditoria/eventos.csv')->assertForbidden(); })->with(['administrator', 'expired', 'maintenance', 'revoked']); test('audit metadata block isolates companies and adds no grants or audit events during consultation', function () { $foreign = receiptFixture(); supplyGrant($foreign['receiver'], ['audit_metadata_read']); DB::table('audit_logs')->insert(['tenant_id' => $foreign['tenant']->id, 'user_id' => $foreign['receiver']->id, 'action' => 'FOREIGN-ONLY', 'created_at' => now(), 'after' => '{}']); $before = []; foreach (['audit_logs', 'users', 'organization_profiles', 'organization_memberships', 'approval_steps', 'stock_movements'] as $table) { $before[$table] = DB::table($table)->orderBy('id')->get()->toArray(); } $this->actingAs($this->f['receiver'])->get('/auditoria/eventos')->assertOk()->assertDontSee('FOREIGN-ONLY'); $this->actingAs($foreign['receiver'])->get('/auditoria/eventos')->assertOk()->assertInertia(fn (Assert $p) => $p->where('events.total', 1)->where('events.data.0.action', 'FOREIGN-ONLY')); expect($this->get('/auditoria/eventos.csv')->assertOk()->streamedContent())->toContain('FOREIGN-ONLY')->not->toContain('people.selected'); foreach ($before as $table => $rows) { expect(DB::table($table)->orderBy('id')->get()->toArray())->toEqual($rows); } expect(array_key_exists('audit_metadata_read', OrganizationAccess::PERMISSIONS))->toBeTrue(); });