Files
device-management/tests/Feature/MikrotikProvisioningTest.php
Wian Drs 1e80be180e
tests / ci (push) Has been cancelled
Initial commit device-management
2026-08-25 09:42:00 +07:00

169 lines
7.8 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\TenantDeviceSetting;
use App\Models\User;
use App\Network\Clients\RouterOs\RouterOsApiClient;
use App\Network\Drivers\Mikrotik\MikrotikDriver;
use App\Services\TenantProvisioningService;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class MikrotikProvisioningTest extends TestCase
{
use RefreshDatabase;
public function test_tenant_admin_can_store_encrypted_base_device_password(): void
{
$this->seed(RolePermissionSeeder::class);
$tenant = app(TenantProvisioningService::class)->createWithOwner([
'name' => 'Tenant A', 'slug' => 'tenant-a', 'owner_name' => 'Admin',
'owner_email' => 'admin@test.local', 'owner_password' => 'Strong!Password123',
]);
$admin = User::withoutGlobalScope('tenant')->where('tenant_id', $tenant->id)->firstOrFail();
$this->actingAs($admin)->put(route('device-settings.update'), [
'base_username' => 'radiq-manager', 'base_password' => 'Device!Password123',
'base_password_confirmation' => 'Device!Password123', 'use_tls' => false,
'verify_tls' => true, 'connection_timeout' => 10,
])->assertRedirect();
$setting = TenantDeviceSetting::withoutGlobalScope('tenant')->firstOrFail();
$this->assertSame('Device!Password123', $setting->base_password);
$this->assertNotSame('Device!Password123', DB::table('tenant_device_settings')->value('base_password'));
$this->assertArrayNotHasKey('base_password', $setting->toArray());
}
public function test_tenant_admin_must_confirm_login_password_to_reveal_base_password(): void
{
$this->seed(RolePermissionSeeder::class);
$tenant = app(TenantProvisioningService::class)->createWithOwner([
'name' => 'Tenant A', 'slug' => 'tenant-a', 'owner_name' => 'Admin',
'owner_email' => 'admin@test.local', 'owner_password' => 'Strong!Password123',
]);
$admin = User::withoutGlobalScope('tenant')->where('tenant_id', $tenant->id)->firstOrFail();
TenantDeviceSetting::create([
'tenant_id' => $tenant->id, 'base_username' => 'radiq-manager',
'base_password' => 'Device!Password123', 'connection_timeout' => 10,
]);
$this->actingAs($admin)->postJson(route('device-settings.reveal-password'), [
'current_password' => 'wrong-password',
])->assertUnprocessable()->assertJsonValidationErrors('current_password');
$this->actingAs($admin)->postJson(route('device-settings.reveal-password'), [
'current_password' => 'Strong!Password123',
])->assertOk()->assertExactJson(['password' => 'Device!Password123'])->assertHeader('Cache-Control', 'no-store, private');
}
public function test_key_rotation_reencrypts_existing_secrets_without_changing_plaintext(): void
{
$this->seed(RolePermissionSeeder::class);
$tenant = app(TenantProvisioningService::class)->createWithOwner([
'name' => 'Tenant A', 'slug' => 'tenant-a', 'owner_name' => 'Admin',
'owner_email' => 'admin@test.local', 'owner_password' => 'Strong!Password123',
]);
$admin = User::withoutGlobalScope('tenant')->where('tenant_id', $tenant->id)->firstOrFail();
TenantDeviceSetting::create([
'tenant_id' => $tenant->id, 'base_username' => 'radiq-manager',
'base_password' => 'Device!Password123', 'connection_timeout' => 10,
]);
$before = DB::table('tenant_device_settings')->value('base_password');
$this->actingAs($admin)->post(route('device-settings.rotate-key'), [
'mode' => 'generated', 'current_password' => 'Strong!Password123',
])->assertRedirect();
$after = DB::table('tenant_device_settings')->value('base_password');
$this->assertNotSame($before, $after);
$this->assertSame('Device!Password123', TenantDeviceSetting::withoutGlobalScope('tenant')->firstOrFail()->base_password);
$this->assertDatabaseCount('tenant_encryption_keys', 2);
$this->assertDatabaseHas('tenant_encryption_keys', ['tenant_id' => $tenant->id, 'version' => 2, 'is_active' => true]);
}
public function test_mikrotik_groups_exclude_policy_and_base_user_uses_full(): void
{
$client = new FakeRouterOsClient;
(new MikrotikDriver($client))->provisionBaseAccess('radiq-manager', 'Device!Password123');
$groups = collect($client->commands)->filter(fn (array $command) => $command[0] === '/user/group/add');
foreach (['RADIQ-READ', 'RADIQ-WRITE', 'RADIQ-NOC'] as $name) {
$command = $groups->first(fn (array $item) => in_array('=name='.$name, $item, true));
$policy = collect($command)->first(fn (string $word) => str_starts_with($word, '=policy='));
$this->assertStringNotContainsString(',policy,', ','.str($policy)->after('=policy=').',');
}
$this->assertFalse($groups->contains(fn (array $item) => in_array('=name=RADIQ-MANAGER', $item, true)));
$baseUser = collect($client->commands)->first(fn (array $item) => $item[0] === '/user/add');
$this->assertContains('=group=full', $baseUser);
}
public function test_mikrotik_inventory_and_legacy_cleanup_preserve_full_users(): void
{
$client = new FakeRouterOsClient;
$client->responses['/system/identity/print'] = [['name' => 'CCR-Jakarta']];
$client->responses['/system/routerboard/print'] = [['model' => 'CCR2004', 'serial-number' => 'ABC123', 'current-firmware' => '7.20']];
$client->responses['/system/resource/print'] = [['version' => '7.20', 'architecture-name' => 'arm64']];
$client->responses['/user/print'] = [
['.id' => '*1', 'name' => 'radiq-manager', 'group' => 'full'],
['.id' => '*2', 'name' => 'emergency-admin', 'group' => 'full'],
['.id' => '*3', 'name' => 'old-noc', 'group' => 'read'],
];
$driver = new MikrotikDriver($client);
$this->assertSame('ABC123', $driver->getDeviceInfo()['serial-number']);
$result = $driver->cleanupLegacyUsers('radiq-manager');
$this->assertSame(['old-noc'], $result['deleted']);
$this->assertContains(['/user/remove', '=.id=*3'], $client->commands);
$this->assertNotContains(['/user/remove', '=.id=*2'], $client->commands);
}
public function test_unsupported_device_fact_does_not_cancel_other_inventory(): void
{
$client = new FakeRouterOsClient;
$client->responses['/system/identity/print'] = [['name' => 'Router ROS']];
$client->responses['/system/resource/print'] = [['version' => '6.49.17', 'uptime' => '1d']];
$client->failWhenProplistContains = 'cpu';
$facts = (new MikrotikDriver($client))->getDeviceInfo();
$this->assertSame('Router ROS', $facts['identity']);
$this->assertSame('6.49.17', $facts['version']);
$this->assertArrayNotHasKey('cpu', $facts);
}
}
class FakeRouterOsClient extends RouterOsApiClient
{
public array $commands = [];
public array $responses = [];
public ?string $failWhenProplistContains = null;
public function __construct() {}
public function connect(): void {}
public function disconnect(): void {}
public function command(array $words): array
{
$this->commands[] = $words;
if ($this->failWhenProplistContains && in_array('=.proplist='.$this->failWhenProplistContains, $words, true)) {
throw new \RuntimeException('CONNECTION_TIMEOUT: unsupported fact');
}
if (array_key_exists($words[0], $this->responses)) {
return $this->responses[$words[0]];
}
if (str_ends_with($words[0], '/print')) {
return [];
}
return [];
}
}