ADconsole v1.1.0
Config backup, a Docker dev/prod split, and a Gitea-built release image.
Added
- /settings -> General -> "Configuration backup": JSON export/import of the
panel's general settings + attribute definitions
(App\Services\ConfigBackupService); audited as config.export / config.import.
Export can leave out the attribute-encryption key. No AD data included.
- docker/compose.dev.yaml: live-reload dev stack that bind-mounts the working
tree. docker/php/Dockerfile is now multi-stage (base -> dev / prod).
- .github/workflows/release.yml: on every vX.Y.Z tag, builds the prod image and
pushes gitea.kzbikowski.pl/kzbkowski/adconsole:{X.Y.Z,X.Y,latest}.
Changed
- docker/compose.yaml: self-contained prod image (app code + composer install
--no-dev baked in via COPY); persistent state in docker/data/{app_storage,db};
defaults to the released image (ADCONSOLE_VERSION pins a tag, ADCONSOLE_IMAGE
replaces the ref). Apache now honours public/.htaccess (AllowOverride All).
Run from the repo root with --env-file .env.
- Repo root now holds only README.md; CHANGELOG / CONTRIBUTING / SECURITY /
CODE_OF_CONDUCT moved under docs/.
Upgrade notes: docs/upgrading.md#v110--docker-layout. Full log: docs/CHANGELOG.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JKT9p3bfP8RZesExdYCS52
This commit is contained in:
1 parent
1126d96271
commit
b313c126ce
35 files changed
+1174
-119
No files matched your search
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Services\ConfigBackupService;
|
||||
use App\Services\PanelSettingsService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ConfigBackupTest extends TestCase
|
||||
{
|
||||
// Konfiguracja panelu jest w bazie - każdy test startuje na czystej,
|
||||
// czyli na wbudowanych domyślnych.
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->actingAsPanelAdmin();
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $overrides */
|
||||
protected function backupFile(array $overrides = []): UploadedFile
|
||||
{
|
||||
$payload = array_replace(
|
||||
app(ConfigBackupService::class)->export(),
|
||||
$overrides,
|
||||
);
|
||||
|
||||
return UploadedFile::fake()->createWithContent(
|
||||
'adconsole-config.json',
|
||||
json_encode($payload, JSON_UNESCAPED_UNICODE),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_export_downloads_a_json_backup_with_the_expected_shape(): void
|
||||
{
|
||||
$res = $this->get('/settings/export')->assertOk();
|
||||
|
||||
$this->assertStringContainsString('attachment', $res->headers->get('content-disposition'));
|
||||
$this->assertStringContainsString('adconsole-config-', $res->headers->get('content-disposition'));
|
||||
|
||||
$res->assertJsonPath('format', 'adconsole-config')
|
||||
->assertJsonPath('version', 1)
|
||||
->assertJsonStructure(['format', 'version', 'exported_at', 'settings', 'attributes']);
|
||||
|
||||
$data = $res->json();
|
||||
$this->assertSame(42, $data['settings']['password_policy']['max_age_days']);
|
||||
// Pełna lista domyślna: 24 atrybuty AD + 2 kolumny wyliczane.
|
||||
$this->assertCount(26, $data['attributes']);
|
||||
}
|
||||
|
||||
public function test_export_can_leave_out_the_encryption_key(): void
|
||||
{
|
||||
$this->put('/settings', [
|
||||
'reset_password' => ['must_change_password' => '1', 'unlock_account' => '1'],
|
||||
'password_policy' => ['max_age_days' => '42'],
|
||||
'encryption' => ['key' => 'TopSecret!'],
|
||||
])->assertRedirect();
|
||||
|
||||
$this->get('/settings/export')
|
||||
->assertJsonPath('settings.encryption.key', 'TopSecret!')
|
||||
->assertJsonPath('encryption_key_included', true);
|
||||
|
||||
$this->get('/settings/export?exclude_key=1')
|
||||
->assertJsonPath('settings.encryption.key', '')
|
||||
->assertJsonPath('encryption_key_included', false);
|
||||
}
|
||||
|
||||
public function test_import_restores_general_settings_and_attributes(): void
|
||||
{
|
||||
// Zmień konfigurację, zrób kopię, przywróć domyślne, wczytaj kopię.
|
||||
$this->put('/settings', [
|
||||
'reset_password' => ['must_change_password' => '0', 'unlock_account' => '0'],
|
||||
'password_policy' => ['max_age_days' => '17'],
|
||||
'audit' => ['require_reason' => '1'],
|
||||
])->assertRedirect();
|
||||
|
||||
$this->put('/settings/attributes', [
|
||||
'attributes' => [
|
||||
['key' => 'title', 'label' => 'Stanowisko', 'ldap' => 'title', 'category' => 'org', 'type' => 'text', 'column' => '1'],
|
||||
['key' => 'badgeId', 'label' => 'Identyfikator', 'ldap' => 'extensionAttribute9', 'category' => 'hr', 'type' => 'text', 'column' => '1'],
|
||||
],
|
||||
])->assertRedirect();
|
||||
|
||||
$file = $this->backupFile();
|
||||
|
||||
// Wróć do domyślnych.
|
||||
app(PanelSettingsService::class)->save(app(PanelSettingsService::class)->defaults());
|
||||
app(PanelSettingsService::class)->saveAttributes([]);
|
||||
$this->assertSame(42, app(PanelSettingsService::class)->passwordMaxAgeDays());
|
||||
|
||||
$this->post('/settings/import', ['config_file' => $file])
|
||||
->assertRedirect()
|
||||
->assertSessionHas('status')
|
||||
->assertSessionHasNoErrors();
|
||||
|
||||
$fresh = app(PanelSettingsService::class);
|
||||
$this->assertSame(17, $fresh->passwordMaxAgeDays());
|
||||
$this->assertTrue($fresh->requireReason());
|
||||
|
||||
$byKey = collect($fresh->attributes())->keyBy('key');
|
||||
$this->assertTrue($byKey->has('badgeId'));
|
||||
$this->assertSame('extensionAttribute9', $byKey['badgeId']['ldap']);
|
||||
$this->assertTrue($byKey['title']['column']);
|
||||
|
||||
// Pozycje systemowe / wyliczane zostają odtworzone przez saveAttributes().
|
||||
$this->assertTrue($byKey->has('login'));
|
||||
$this->assertTrue($byKey->has('status'));
|
||||
}
|
||||
|
||||
public function test_import_writes_an_audit_entry(): void
|
||||
{
|
||||
$this->post('/settings/import', ['config_file' => $this->backupFile()])
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('panel_audit', ['action' => 'config.import']);
|
||||
}
|
||||
|
||||
public function test_import_rejects_a_file_that_is_not_an_adconsole_backup(): void
|
||||
{
|
||||
$file = UploadedFile::fake()->createWithContent('nope.json', json_encode(['hello' => 'world']));
|
||||
|
||||
$this->post('/settings/import', ['config_file' => $file])
|
||||
->assertRedirect()
|
||||
->assertSessionHasErrors('config_file');
|
||||
|
||||
$this->assertDatabaseMissing('panel_audit', ['action' => 'config.import']);
|
||||
}
|
||||
|
||||
public function test_import_rejects_a_file_that_is_not_valid_json(): void
|
||||
{
|
||||
$file = UploadedFile::fake()->createWithContent('broken.json', '{ not json ');
|
||||
|
||||
$this->post('/settings/import', ['config_file' => $file])
|
||||
->assertRedirect()
|
||||
->assertSessionHasErrors('config_file');
|
||||
}
|
||||
|
||||
public function test_import_rejects_a_backup_from_a_newer_schema_version(): void
|
||||
{
|
||||
$file = $this->backupFile(['version' => 999]);
|
||||
|
||||
$this->post('/settings/import', ['config_file' => $file])
|
||||
->assertRedirect()
|
||||
->assertSessionHasErrors('config_file');
|
||||
}
|
||||
|
||||
public function test_import_sanitises_out_of_range_values_from_a_hand_edited_file(): void
|
||||
{
|
||||
$file = $this->backupFile([
|
||||
'settings' => [
|
||||
'password_policy' => ['max_age_days' => 999999, 'generator_length' => 2],
|
||||
'audit' => ['require_reason' => 'yes'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->post('/settings/import', ['config_file' => $file])->assertRedirect()->assertSessionHasNoErrors();
|
||||
|
||||
$fresh = app(PanelSettingsService::class);
|
||||
$this->assertSame(3650, $fresh->passwordMaxAgeDays());
|
||||
$this->assertSame(8, $fresh->passwordGenerator()['length']);
|
||||
$this->assertTrue($fresh->requireReason());
|
||||
}
|
||||
|
||||
public function test_settings_page_shows_the_backup_card(): void
|
||||
{
|
||||
$this->get('/settings')
|
||||
->assertOk()
|
||||
->assertSee(__('config.backup.heading'))
|
||||
->assertSee(route('config.export'), escape: false);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user