169 lines
7.8 KiB
PHP
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 [];
|
|
}
|
|
}
|