Files
ADconsle/tests/Feature/AttributeConfigTest.php
T
kzbkowskiandClaude Sonnet 5 1126d96271
CI / PHP 8.2 (push) Failing after 2m46s
CI / PHP 8.3 (push) Failing after 59s
CI / PHP 8.4 (push) Failing after 1m7s
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
2026-09-08 23:14:39 +02:00

392 lines
16 KiB
PHP

<?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');
}
}