ADconsole v1.0.0
CI / PHP 8.2 (push) Failing after 2m46s
CI / PHP 8.3 (push) Failing after 59s
CI / PHP 8.4 (push) Failing after 1m7s

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
This commit is contained in:
kzbkowskiandClaude Sonnet 5 committed 2026-09-08 23:14:39 +02:00
commit 1126d96271
174 files changed
+27113

No files matched your search

+391
View File
@@ -0,0 +1,391 @@
<?php
namespace Tests\Feature;
use App\Services\PanelSettingsService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AttributeConfigTest extends TestCase
{
// Konfiguracja panelu jest w tabeli `panel_settings` - każdy test startuje
// na czystej bazie, czyli na wbudowanych domyślnych.
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Panel jest za middleware 'auth:ldap' - loguj udawanego administratora.
$this->actingAsPanelAdmin();
}
public function test_config_page_renders_attributes_tab(): void
{
$this->get('/settings')
->assertOk()
->assertSee('pane-attributes')
->assertSee('Dodaj atrybut');
}
public function test_password_max_age_defaults_to_42_and_is_configurable(): void
{
$this->assertSame(42, app(PanelSettingsService::class)->passwordMaxAgeDays());
$this->get('/settings')->assertOk()->assertSee('Maksymalny wiek hasła');
$this->put('/settings', [
'reset_password' => ['must_change_password' => '1', 'unlock_account' => '1'],
'password_policy' => ['max_age_days' => '30'],
])->assertRedirect();
$this->assertSame(30, app(PanelSettingsService::class)->passwordMaxAgeDays());
}
public function test_encryption_key_is_configurable_and_marks_cloud_attributes_encrypted(): void
{
$settings = app(PanelSettingsService::class);
$this->assertSame('', $settings->encryptionKey());
// Domyślnie zaszyfrowane: msDS-cloudExtensionAttribute1/2.
$byKey = collect($settings->attributes())->keyBy('key');
$this->assertTrue($byKey['microsoft']['encrypted']);
$this->assertTrue($byKey['google']['encrypted']);
$this->assertFalse($byKey['firstName']['encrypted']);
$this->get('/settings')->assertOk()->assertSee('Klucz szyfrujący')->assertSee('Tekst szyfrowany');
$this->put('/settings', [
'reset_password' => ['must_change_password' => '1', 'unlock_account' => '1'],
'password_policy' => ['max_age_days' => '42'],
'encryption' => ['key' => 'Sekret!42'],
])->assertRedirect();
$this->assertSame('Sekret!42', app(PanelSettingsService::class)->encryptionKey());
}
public function test_attribute_type_drives_editability_and_encryption(): void
{
$settings = app(PanelSettingsService::class);
$payload = ['attributes' => []];
foreach ($settings->attributes() as $i => $attr) {
if ($attr['computed']) {
$payload['attributes'][$i] = ['key' => $attr['key'], 'label' => $attr['label'], 'column' => $attr['column']];
continue;
}
$row = [
'key' => $attr['key'],
'label' => $attr['label'],
'ldap' => $attr['ldap'],
'category' => $attr['category'],
'type' => $attr['type'],
'column' => $attr['column'],
];
if ($attr['key'] === 'microsoft') {
$row['type'] = 'text'; // z szyfrowanego na zwykły tekst
}
if ($attr['key'] === 'firstName') {
$row['type'] = 'encrypted'; // zwykły atrybut - typ respektowany
}
if ($attr['key'] === 'employeeNumber') {
$row['type'] = 'number'; // typ liczbowy
}
if ($attr['key'] === 'login') {
$row['type'] = 'text'; // sAMAccountName "na stałe" - ma być zignorowany
}
$payload['attributes'][$i] = $row;
}
$this->put('/settings/attributes', $payload)->assertRedirect();
$byKey = collect(app(PanelSettingsService::class)->attributes())->keyBy('key');
$this->assertSame('text', $byKey['microsoft']['type']);
$this->assertFalse($byKey['microsoft']['encrypted']);
$this->assertSame('encrypted', $byKey['firstName']['type']);
$this->assertTrue($byKey['firstName']['encrypted']);
$this->assertTrue($byKey['firstName']['editable']);
$this->assertSame('number', $byKey['employeeNumber']['type']);
$this->assertSame('number', $byKey['employeeNumber']['input']);
$this->assertTrue($byKey['employeeNumber']['editable']);
// sAMAccountName pozostaje systemowy niezależnie od przesłanego typu.
$this->assertSame('system', $byKey['login']['type']);
$this->assertFalse($byKey['login']['editable']);
$this->assertFalse($byKey['login']['encrypted']);
}
public function test_preview_modal_only_exposes_editable_configured_attributes(): void
{
$settings = app(PanelSettingsService::class);
$groups = $settings->editableAttributesByCategory();
$keys = collect($groups)->flatten(1)->pluck('key')->all();
// Edytowalne pozycje (w tym wygasanie konta - typ "data").
$this->assertContains('firstName', $keys);
$this->assertContains('email', $keys);
$this->assertContains('account_expiry', $keys);
// Pozycje systemowe (sAMAccountName, DN) nigdy nie są edytowalne;
// kolumny wyliczane też nie są polami.
$this->assertNotContains('login', $keys);
$this->assertNotContains('distinguishedName', $keys);
$this->assertNotContains('status', $keys);
// Kategoria "Konto AD" pojawia się dzięki edytowalnemu "wygasaniu konta".
$this->assertArrayHasKey('account', $groups);
}
public function test_system_attributes_are_never_editable_even_if_flagged(): void
{
$this->put('/settings/attributes', [
'attributes' => [
['key' => 'distinguishedName', 'label' => 'DN', 'ldap' => 'distinguishedName', 'category' => 'account', 'type' => 'text', 'column' => '0'],
['key' => 'login', 'label' => 'Login', 'ldap' => 'sAMAccountName', 'category' => 'account', 'type' => 'encrypted', 'column' => '1'],
],
])->assertRedirect();
$byKey = collect(app(PanelSettingsService::class)->attributes())->keyBy('key');
// Pozycje systemowe: mimo przesłania edytowalnego typu zostają systemowe.
$this->assertSame('system', $byKey['distinguishedName']['type']);
$this->assertSame('system', $byKey['login']['type']);
$this->assertFalse($byKey['distinguishedName']['editable']);
$this->assertFalse($byKey['login']['editable']);
$this->assertSame('sAMAccountName', $byKey['login']['ldap']);
}
public function test_it_persists_added_edited_and_removed_attributes(): void
{
$settings = app(PanelSettingsService::class);
// Wyjściowo: pełna lista domyślna (24 atrybuty AD + 2 kolumny wyliczane).
$this->assertCount(26, $settings->attributes());
// Domyślnie widoczne kolumny listy - wszystkie zarządzane z panelu.
$this->assertEqualsCanonicalizing(
['login', 'displayName', 'email', 'status', 'password_expiry'],
collect($settings->attributeColumns())->pluck('key')->all()
);
$payload = ['attributes' => []];
foreach ($settings->attributes() as $i => $attr) {
if ($attr['key'] === 'google') {
continue; // usunięcie atrybutu niesystemowego
}
if ($attr['computed']) {
// Formularz dla kolumn wyliczanych wysyła tylko klucz, etykietę i widoczność.
$payload['attributes'][$i] = [
'key' => $attr['key'],
'label' => $attr['label'],
'column' => $attr['key'] === 'password_expiry' ? false : true,
];
continue;
}
if ($attr['key'] === 'title') {
$attr['column'] = true; // pokaż jako kolumnę listy
$attr['category'] = 'address'; // przypnij do innej kategorii
}
if ($attr['key'] === 'login') {
$attr['ldap'] = 'tampered'; // próba zmiany atrybutu systemowego
}
$payload['attributes'][$i] = $attr;
}
// Nowy, własny atrybut.
$payload['attributes'][] = [
'key' => 'badgeId',
'label' => 'Numer identyfikatora',
'ldap' => 'extensionAttribute9',
'category' => 'hr',
'editable' => true,
'column' => true,
];
$this->put('/settings/attributes', $payload)
->assertRedirect();
$fresh = app(PanelSettingsService::class);
$byKey = collect($fresh->attributes())->keyBy('key');
// Dodany
$this->assertTrue($byKey->has('badgeId'));
$this->assertSame('extensionAttribute9', $byKey['badgeId']['ldap']);
// Usunięty
$this->assertFalse($byKey->has('google'));
// Zmiana kolumny + kategorii
$this->assertTrue($byKey['title']['column']);
$this->assertSame('address', $byKey['title']['category']);
// Atrybut systemowy: nazwa atrybutu AD wymuszona z powrotem, nadal obecny
$this->assertSame('sAMAccountName', $byKey['login']['ldap']);
$this->assertTrue($byKey['login']['system']);
// Kolumny wyliczane pozostają (nie da się ich usunąć); password_expiry
// wyłączony jako kolumna, doszły title + badgeId.
$this->assertEqualsCanonicalizing(
['login', 'displayName', 'email', 'status', 'title', 'badgeId'],
collect($fresh->attributeColumns())->pluck('key')->all()
);
// Kolumny wyliczane nie są edytowalne, mają computed=true i nadal istnieją.
$this->assertTrue($byKey['password_expiry']['computed']);
$this->assertFalse($byKey['password_expiry']['editable']);
$this->assertFalse($byKey['password_expiry']['column']);
$this->assertTrue($byKey['status']['computed']);
$this->assertFalse($byKey->has('last_logon'));
}
public function test_system_and_computed_attributes_cannot_be_deleted(): void
{
// Wysyłamy tylko jeden zwykły atrybut - reszta powinna zostać odtworzona.
$this->put('/settings/attributes', [
'attributes' => [
['key' => 'title', 'label' => 'Stanowisko', 'ldap' => 'title', 'category' => 'org', 'editable' => true, 'column' => false],
],
])->assertRedirect();
$keys = collect(app(PanelSettingsService::class)->attributes())->pluck('key');
foreach (PanelSettingsService::SYSTEM_ATTRIBUTE_KEYS as $systemKey) {
$this->assertTrue($keys->contains($systemKey), "Brakuje atrybutu systemowego: {$systemKey}");
}
foreach (PanelSettingsService::COMPUTED_ATTRIBUTE_KEYS as $computedKey) {
$this->assertTrue($keys->contains($computedKey), "Brakuje kolumny wyliczanej: {$computedKey}");
}
foreach (array_keys(PanelSettingsService::FIXED_ATTRIBUTE_KEYS) as $fixedKey) {
$this->assertTrue($keys->contains($fixedKey), "Brakuje pozycji „na stałe”: {$fixedKey}");
}
}
public function test_account_expiry_has_fixed_date_type_but_configurable_category(): void
{
// Domyślnie: pozycja "na stałe", typ "data" (edytowalna), nie wyliczana.
$byKey = collect(app(PanelSettingsService::class)->attributes())->keyBy('key');
$this->assertTrue($byKey['account_expiry']['fixed']);
$this->assertFalse($byKey['account_expiry']['computed']);
$this->assertSame('date', $byKey['account_expiry']['type']);
$this->assertSame('date', $byKey['account_expiry']['input']);
$this->assertTrue($byKey['account_expiry']['editable']);
$this->assertSame('accountExpires', $byKey['account_expiry']['ldap']);
// Zmieniamy kategorię; próba podmiany typu i atrybutu AD ma być zignorowana.
$this->put('/settings/attributes', [
'attributes' => [
['key' => 'account_expiry', 'label' => 'Wygasanie konta', 'ldap' => 'whenChanged', 'category' => 'hr', 'type' => 'encrypted', 'column' => '1'],
],
])->assertRedirect();
$byKey = collect(app(PanelSettingsService::class)->attributes())->keyBy('key');
$this->assertSame('date', $byKey['account_expiry']['type']);
$this->assertTrue($byKey['account_expiry']['editable']);
$this->assertSame('hr', $byKey['account_expiry']['category']);
$this->assertTrue($byKey['account_expiry']['column']);
$this->assertSame('accountExpires', $byKey['account_expiry']['ldap']);
$this->assertFalse($byKey['account_expiry']['encrypted']);
// Trafia do pól okna "Podgląd / Edycja".
$groups = app(PanelSettingsService::class)->editableAttributesByCategory();
$keys = collect($groups)->flatten(1)->pluck('key')->all();
$this->assertContains('account_expiry', $keys);
}
public function test_attribute_validation_rules_persist_and_are_forced_empty_for_locked_entries(): void
{
$settings = app(PanelSettingsService::class);
// Domyślnie puste.
$byKey = collect($settings->attributes())->keyBy('key');
$this->assertSame('', $byKey['title']['regex']);
$this->assertFalse($byKey['title']['required']);
$this->assertSame('', $byKey['title']['help']);
$payload = ['attributes' => []];
foreach ($settings->attributes() as $i => $attr) {
if ($attr['computed']) {
$payload['attributes'][$i] = ['key' => $attr['key'], 'label' => $attr['label'], 'column' => $attr['column']];
continue;
}
$row = [
'key' => $attr['key'],
'label' => $attr['label'],
'ldap' => $attr['ldap'],
'category' => $attr['category'],
'type' => $attr['type'],
'column' => $attr['column'],
];
if ($attr['key'] === 'employeeNumber') {
$row['regex'] = '/^\d{4}$/';
$row['required'] = '1';
$row['help'] = 'Cztery cyfry';
}
if ($attr['key'] === 'login') {
// Pozycja systemowa - reguły mają zostać wyzerowane.
$row['regex'] = '/x/';
$row['required'] = '1';
$row['help'] = 'ignorowane';
}
$payload['attributes'][$i] = $row;
}
$this->put('/settings/attributes', $payload)->assertRedirect()->assertSessionHasNoErrors();
$byKey = collect(app(PanelSettingsService::class)->attributes())->keyBy('key');
$this->assertSame('/^\d{4}$/', $byKey['employeeNumber']['regex']);
$this->assertTrue($byKey['employeeNumber']['required']);
$this->assertSame('Cztery cyfry', $byKey['employeeNumber']['help']);
// Pozycja systemowa: reguły wymuszone na puste.
$this->assertSame('', $byKey['login']['regex']);
$this->assertFalse($byKey['login']['required']);
$this->assertSame('', $byKey['login']['help']);
}
public function test_invalid_validation_regex_is_rejected(): void
{
$this->put('/settings/attributes', [
'attributes' => [
['key' => 'title', 'label' => 'Stanowisko', 'ldap' => 'title', 'category' => 'org', 'type' => 'text', 'column' => '0', 'regex' => '/[unclosed/'],
],
])->assertSessionHasErrors('attributes.0.regex');
}
public function test_require_reason_setting_persists(): void
{
$this->assertFalse(app(PanelSettingsService::class)->requireReason());
$this->get('/settings')->assertOk()->assertSee(__('config.general.require_reason'));
$this->put('/settings', [
'reset_password' => ['must_change_password' => '1', 'unlock_account' => '1'],
'password_policy' => ['max_age_days' => '42'],
'audit' => ['require_reason' => '1'],
])->assertRedirect();
$this->assertTrue(app(PanelSettingsService::class)->requireReason());
}
public function test_general_config_no_longer_exposes_column_picker(): void
{
$this->get('/settings')
->assertOk()
->assertDontSee('users_table[columns]', escape: false)
->assertDontSee('Domyślnie widoczne kolumny listy użytkowników');
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace Tests\Feature;
use App\Models\AuditLog;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AuditLogTest extends TestCase
{
use RefreshDatabase;
public function test_settings_change_is_recorded_with_the_acting_admin(): void
{
$this->actingAsPanelAdmin()->put('/settings', [
'reset_password' => ['must_change_password' => '1', 'unlock_account' => '1'],
'password_policy' => ['max_age_days' => '30'],
'audit' => ['require_reason' => '1'],
])->assertRedirect();
$entry = AuditLog::query()->where('action', 'config.settings')->latest('id')->first();
$this->assertNotNull($entry);
$this->assertSame('tester', $entry->actor);
$this->assertSame(30, $entry->context['max_age_days']);
$this->assertTrue($entry->context['require_reason']);
}
public function test_attribute_config_change_is_recorded(): void
{
$this->actingAsPanelAdmin()->put('/settings/attributes', [
'attributes' => [
['key' => 'title', 'label' => 'Stanowisko', 'ldap' => 'title', 'category' => 'org', 'type' => 'text', 'column' => '0'],
],
])->assertRedirect();
$this->assertDatabaseHas('panel_audit', ['action' => 'config.attributes', 'actor' => 'tester']);
}
public function test_audit_page_renders_and_filters_by_action(): void
{
AuditLog::create(['actor' => 'alice', 'actor_name' => 'Alice', 'action' => 'user.disable', 'target' => 'jkowalski', 'created_at' => now()]);
AuditLog::create(['actor' => 'bob', 'actor_name' => 'Bob', 'action' => 'user.move', 'target' => 'anowak', 'created_at' => now()]);
$this->actingAsPanelAdmin()->get('/audit')->assertOk()->assertSee('jkowalski')->assertSee('anowak');
$this->actingAsPanelAdmin()->get('/audit?action=user.disable')
->assertOk()
->assertSee('jkowalski')
->assertDontSee('anowak');
}
public function test_audit_page_requires_authentication(): void
{
$this->get('/audit')->assertRedirect('/login');
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace Tests\Feature\Auth;
use Tests\TestCase;
class LoginTest extends TestCase
{
public function test_guests_are_redirected_to_the_login_screen(): void
{
$this->get('/dashboard')->assertRedirect(route('login'));
$this->get('/users')->assertRedirect(route('login'));
$this->get('/settings')->assertRedirect(route('login'));
}
public function test_login_screen_renders(): void
{
$this->get('/login')
->assertOk()
->assertSee('Zaloguj')
->assertSee(__('auth.login_label'));
}
public function test_authenticated_admin_is_redirected_away_from_login(): void
{
$this->actingAsPanelAdmin()
->get('/login')
->assertRedirect('/dashboard');
}
public function test_login_requires_credentials(): void
{
$this->from('/login')
->post('/login', [])
->assertRedirect('/login')
->assertSessionHasErrors(['login', 'password']);
$this->assertGuest('ldap');
}
public function test_logout_clears_the_session(): void
{
$this->actingAsPanelAdmin()
->withSession(['ad_admin' => ['login' => 'tester']])
->post('/logout')
->assertRedirect(route('login'));
$this->assertGuest('ldap');
}
}
+179
View File
@@ -0,0 +1,179 @@
<?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 &amp;,
// 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);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
class LocalizationTest extends TestCase
{
public function test_default_locale_is_polish(): void
{
$this->get('/login')
->assertOk()
->assertSee('Zaloguj się kontem domenowym')
->assertSee('Zaloguj');
}
public function test_login_screen_has_language_and_theme_switchers(): void
{
$this->get('/login')
->assertOk()
->assertSee('id="themeToggle"', escape: false)
->assertSee(route('locale.switch', 'pl'), escape: false)
->assertSee(route('locale.switch', 'en'), escape: false);
}
public function test_switching_language_persists_and_translates_the_ui(): void
{
$this->get('/language/en')->assertRedirect();
$this->assertSame('en', session('locale'));
$this->get('/login')
->assertOk()
->assertSee('Sign in with your domain account')
->assertDontSee('Zaloguj się kontem domenowym');
$this->get('/language/pl')->assertRedirect();
$this->assertSame('pl', session('locale'));
$this->get('/login')->assertSee('Zaloguj się kontem domenowym');
}
public function test_unknown_locale_is_rejected(): void
{
$this->get('/language/de')->assertNotFound();
$this->get('/language/xx')->assertNotFound();
}
public function test_locale_switch_route_works_without_authentication(): void
{
$this->get('/language/en')->assertRedirect();
$this->assertGuest('ldap');
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace Tests\Feature\Pages;
use Tests\TestCase;
class AboutPageTest extends TestCase
{
public function test_about_page_is_available_to_guests_with_all_three_sections(): void
{
$this->get('/about')
->assertOk()
->assertSee('id="o-projekcie"', escape: false)
->assertSee('id="autor"', escape: false)
->assertSee('id="kwestie-prawne"', escape: false);
}
public function test_about_page_shows_author_and_mit_licence(): void
{
$this->get('/about')
->assertOk()
->assertSee('Kacper')
->assertSee('MIT License')
->assertSee('Copyright (c) '.now()->year.' Kacper')
->assertSee('Active Directory'); // trademark notice
}
public function test_about_page_follows_the_selected_language(): void
{
$this->get('/about')->assertSee('Kwestie prawne');
$this->get('/language/en')->assertRedirect();
$this->get('/about')
->assertOk()
->assertSee('Legal')
->assertSee('Trademarks')
->assertDontSee('Kwestie prawne');
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
class UserExportTest extends TestCase
{
public function test_export_route_is_behind_the_ldap_guard(): void
{
// Bez realnego LDAP nie sprawdzamy zawartości pliku - tylko to, że
// trasa istnieje i wymaga zalogowania (gość -> ekran logowania).
$this->get('/users/export')->assertRedirect('/login');
}
}