ADconsole v1.0.0
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:
commit
1126d96271
174 files changed
+27113
No files matched your search
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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 &,
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user