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