First public release. - docs: document the audit log, user offboarding, CSV export and the "require a reason for destructive actions" setting across README, docs/ and CLAUDE/AGENTS.md; note retention for panel_audit - docs: fill the publishing placeholders (repo URL, security contact, issue-template links) and add a semantic-versioning policy - move agent/contributor context into CLAUDE/ (AGENTS.md + a README); root CLAUDE.md is now a thin @-import pointer - add a favicon (shield-lock, matching the login screen) to the login page and the app layout - CHANGELOG: promote [Unreleased] to [1.0.0]; keep the earlier internal build as [0.1.0] Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfuvfwexztdS7LDpGuKsuK
180 lines
7.0 KiB
PHP
180 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use Illuminate\Cookie\Middleware\EncryptCookies;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Renderowanie pulpitu na podstawie gotowego cache (bez realnego LDAP).
|
|
* Kontroler czyta wynik z Cache::store('file') pod kluczem 'dashboard:summary',
|
|
* więc podstawiamy go ręcznie i sprawdzamy sam widok.
|
|
*/
|
|
class DashboardTest extends TestCase
|
|
{
|
|
protected function tearDown(): void
|
|
{
|
|
Cache::store('file')->forget('dashboard:summary');
|
|
Cache::store('file')->forget('dashboard:summary_ts');
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
private function seedSummary(array $overrides = []): void
|
|
{
|
|
$payload = array_merge([
|
|
'stats' => [
|
|
'users_total' => 128,
|
|
'users_disabled' => 4,
|
|
'users_locked' => 2,
|
|
'pwd_never_expires' => 6,
|
|
'pwd_expiring_7d' => 3,
|
|
'pwd_expired' => 5,
|
|
'inactive_90d' => 9,
|
|
'account_expired' => 1,
|
|
'users_no_manager' => 7,
|
|
'computers_inactive_90d' => 3,
|
|
],
|
|
'pwd_age' => ['lt30' => 40, 'd30_60' => 30, 'd60_90' => 20, 'gt90' => 30, 'unknown' => 8],
|
|
'computers_total' => 44,
|
|
'groups_total' => 17,
|
|
'expiring_soon' => [['cn' => 'Jan Kowalski', 'sam' => 'jkowalski', 'date' => '2026-09-10']],
|
|
'inactive' => [],
|
|
'recent' => [],
|
|
'pwd_never_list' => [['cn' => 'Anna Nowak', 'sam' => 'anowak']],
|
|
'locked_list' => [['cn' => 'Piotr Lis', 'sam' => 'plis', 'since' => '2026-09-08 07:30']],
|
|
'no_manager_list' => [['cn' => 'Ewa Mak', 'sam' => 'emak']],
|
|
'computers_inactive' => [['cn' => 'WS-OLD-01', 'last' => '2026-01-02']],
|
|
'max_password_age_days' => 42,
|
|
], $overrides);
|
|
|
|
Cache::store('file')->put('dashboard:summary', $payload, 300);
|
|
Cache::store('file')->put('dashboard:summary_ts', now()->subMinutes(2)->format('Y-m-d H:i:s'), 300);
|
|
}
|
|
|
|
public function test_it_renders_clickable_and_alerting_tiles(): void
|
|
{
|
|
$this->seedSummary();
|
|
|
|
$res = $this->actingAsPanelAdmin()->get('/dashboard')->assertOk();
|
|
|
|
// Klikalne kafelki prowadzą do przefiltrowanej listy (href w HTML ma &,
|
|
// więc porównujemy z domyślnym escapowaniem oczekiwanego łańcucha).
|
|
$res->assertSee(route('users.index', ['all_ou' => 1, 'filter' => 'locked']));
|
|
$res->assertSee(route('users.index', ['all_ou' => 1, 'filter' => 'pwd_expired']));
|
|
$res->assertSee(route('users.index', ['all_ou' => 1, 'status' => 'disabled']));
|
|
$res->assertSee(route('computers.index', ['all_ou' => 1]));
|
|
|
|
// Kafelek "z problemem" (locked > 0) dostaje wyróżniającą klasę.
|
|
$res->assertSee('dashboard-tile-alert', escape: false);
|
|
|
|
// Histogram wieku hasła i wiek danych.
|
|
$res->assertSee(__('dashboard.pwd_age.heading'));
|
|
$res->assertSee(__('dashboard.reports.expiring_date'));
|
|
}
|
|
|
|
public function test_it_shows_a_global_empty_state_when_ldap_returned_nothing(): void
|
|
{
|
|
$this->seedSummary([
|
|
'stats' => [
|
|
'users_total' => 0, 'users_disabled' => 0, 'users_locked' => 0,
|
|
'pwd_never_expires' => 0, 'pwd_expiring_7d' => 0, 'pwd_expired' => 0,
|
|
'inactive_90d' => 0, 'account_expired' => 0,
|
|
],
|
|
'pwd_age' => ['lt30' => 0, 'd30_60' => 0, 'd60_90' => 0, 'gt90' => 0, 'unknown' => 0],
|
|
'computers_total' => 0,
|
|
'groups_total' => 0,
|
|
'expiring_soon' => [],
|
|
]);
|
|
|
|
$this->actingAsPanelAdmin()->get('/dashboard')
|
|
->assertOk()
|
|
->assertSee(__('dashboard.empty'))
|
|
->assertDontSee('dashboard-tile', escape: false);
|
|
}
|
|
|
|
public function test_it_renders_the_new_signal_cards_and_actions(): void
|
|
{
|
|
$this->seedSummary();
|
|
|
|
$res = $this->actingAsPanelAdmin()->get('/dashboard')->assertOk();
|
|
|
|
// Nowe kafelki + ich linki do przefiltrowanych list.
|
|
$res->assertSee(route('users.index', ['all_ou' => 1, 'filter' => 'no_manager']));
|
|
$res->assertSee(route('computers.index', ['all_ou' => 1, 'inactive' => 1]));
|
|
|
|
// Nowe karty raportów.
|
|
$res->assertSee(__('dashboard.reports.pwd_never'));
|
|
$res->assertSee(__('dashboard.reports.locked'));
|
|
$res->assertSee(__('dashboard.reports.no_manager'));
|
|
$res->assertSee(__('dashboard.reports.computers_inactive'));
|
|
|
|
// Wiersze z list.
|
|
$res->assertSee('anowak')->assertSee('plis')->assertSee('WS-OLD-01');
|
|
|
|
// Akcja "odblokuj" w karcie zablokowanych + "resetuj" w karcie wygasających.
|
|
$res->assertSee(route('users.unlock', 'plis'));
|
|
$res->assertSee(route('users.index', ['all_ou' => 1, 'q' => 'jkowalski', 'open' => 'reset']));
|
|
}
|
|
|
|
public function test_preferences_endpoint_stores_layout_cookie(): void
|
|
{
|
|
$this->actingAsPanelAdmin()->post('/dashboard/preferences', [
|
|
'section_tiles' => '0',
|
|
'section_pwd_age' => '1',
|
|
'report_order' => ['r_recent', 'r_expiring', 'r_locked'],
|
|
'visible' => ['r_recent', 'r_expiring'],
|
|
])
|
|
->assertRedirect(route('dashboard.index'))
|
|
->assertCookie('dashboard_layout');
|
|
}
|
|
|
|
public function test_layout_cookie_hides_and_reorders_report_cards(): void
|
|
{
|
|
$this->seedSummary([
|
|
'recent' => [['cn' => 'Nowe Konto', 'sam' => 'nowe', 'created' => '2026-09-07']],
|
|
]);
|
|
|
|
$layout = json_encode([
|
|
'tiles' => false,
|
|
'pwd_age' => true,
|
|
'reports' => [
|
|
['key' => 'r_recent', 'visible' => true],
|
|
['key' => 'r_expiring', 'visible' => true],
|
|
['key' => 'r_locked', 'visible' => false],
|
|
],
|
|
]);
|
|
|
|
$res = $this->actingAsPanelAdmin()
|
|
->withoutMiddleware(EncryptCookies::class)
|
|
->withUnencryptedCookie('dashboard_layout', $layout)
|
|
->get('/dashboard')
|
|
->assertOk();
|
|
|
|
// Ukryta sekcja kafelków.
|
|
$res->assertDontSee('dashboard-tile', escape: false);
|
|
// Ukryta karta "zablokowane" - jej wiersz i nagłówek kolumny nie występują
|
|
// (sama nazwa raportu jest jeszcze na liście w oknie preferencji).
|
|
$res->assertDontSee('Piotr Lis');
|
|
$res->assertDontSee(__('dashboard.reports.locked_since'));
|
|
|
|
// Karta "Nowe konta" przed "Hasła wygasające" - nagłówki kolumn dat
|
|
// występują tylko w kartach (nie w oknie preferencji).
|
|
$res->assertSeeInOrder([
|
|
__('dashboard.reports.recent_date'),
|
|
__('dashboard.reports.expiring_date'),
|
|
]);
|
|
}
|
|
|
|
public function test_stale_cache_is_flagged(): void
|
|
{
|
|
$this->seedSummary();
|
|
Cache::store('file')->put('dashboard:summary_ts', now()->subHour()->format('Y-m-d H:i:s'), 300);
|
|
|
|
$this->actingAsPanelAdmin()->get('/dashboard')
|
|
->assertOk()
|
|
->assertSee('text-bg-warning', escape: false);
|
|
}
|
|
}
|