@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Administration;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TenantManagementTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
}
|
||||
|
||||
public function test_platform_admin_can_create_tenant_with_owner_and_default_roles(): void
|
||||
{
|
||||
$admin = User::factory()->create(['is_platform_admin' => true]);
|
||||
|
||||
$response = $this->actingAs($admin)->post('/administration/tenants', [
|
||||
'name' => 'ISP Jakarta',
|
||||
'slug' => 'isp-jakarta',
|
||||
'is_active' => true,
|
||||
'owner_name' => 'Tenant Owner',
|
||||
'owner_email' => 'owner@example.test',
|
||||
'owner_password' => 'Strong!Password123',
|
||||
'owner_password_confirmation' => 'Strong!Password123',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::where('slug', 'isp-jakarta')->firstOrFail();
|
||||
$owner = User::withoutGlobalScope('tenant')->where('email', 'owner@example.test')->firstOrFail();
|
||||
|
||||
$response->assertRedirect(route('administration.tenants.edit', $tenant));
|
||||
$this->assertSame($tenant->id, $owner->tenant_id);
|
||||
$this->assertTrue(Hash::check('Strong!Password123', $owner->password));
|
||||
$this->assertNotNull($owner->email_verified_at);
|
||||
$this->assertDatabaseCount('roles', 3);
|
||||
$this->assertDatabaseHas('roles', ['tenant_id' => $tenant->id, 'name' => 'TENANT ADMIN']);
|
||||
$this->assertDatabaseHas('roles', ['tenant_id' => $tenant->id, 'name' => 'TENANT USER']);
|
||||
$this->assertDatabaseHas('model_has_roles', ['tenant_id' => $tenant->id, 'model_id' => $owner->id]);
|
||||
}
|
||||
|
||||
public function test_normal_user_cannot_open_platform_tenant_administration(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create();
|
||||
$user = User::factory()->for($tenant)->create();
|
||||
|
||||
$this->actingAs($user)->get('/administration/tenants')->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_tenant_with_users_cannot_be_deleted(): void
|
||||
{
|
||||
$admin = User::factory()->create(['is_platform_admin' => true]);
|
||||
$tenant = Tenant::factory()->create();
|
||||
User::factory()->for($tenant)->create();
|
||||
|
||||
$this->actingAs($admin)->delete(route('administration.tenants.destroy', $tenant))->assertStatus(422);
|
||||
$this->assertDatabaseHas('tenants', ['id' => $tenant->id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Administration;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\TenantProvisioningService;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserManagementTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
}
|
||||
|
||||
public function test_tenant_owner_can_create_update_reset_and_disable_user(): void
|
||||
{
|
||||
[$tenant, $owner] = $this->tenantWithOwner('tenant-a');
|
||||
$this->actingAs($owner)->post(route('administration.users.store', $tenant), [
|
||||
'name' => 'NOC Operator', 'email' => 'noc@example.test', 'password' => 'Strong!Password123',
|
||||
'password_confirmation' => 'Strong!Password123',
|
||||
])->assertRedirect(route('administration.users.index', $tenant));
|
||||
|
||||
$user = User::withoutGlobalScope('tenant')->where('email', 'noc@example.test')->firstOrFail();
|
||||
$this->assertSame($tenant->id, $user->tenant_id);
|
||||
setPermissionsTeamId($tenant->id);
|
||||
$this->assertTrue($user->hasRole('TENANT USER'));
|
||||
|
||||
$this->actingAs($owner)->put(route('administration.users.password', [$tenant, $user]), [
|
||||
'password' => 'Changed!Password123', 'password_confirmation' => 'Changed!Password123',
|
||||
])->assertRedirect();
|
||||
$this->assertTrue(Hash::check('Changed!Password123', $user->fresh()->password));
|
||||
|
||||
$this->actingAs($owner)->patch(route('administration.users.toggle-active', [$tenant, $user]))->assertRedirect();
|
||||
$this->assertFalse($user->fresh()->is_active);
|
||||
}
|
||||
|
||||
public function test_tenant_owner_cannot_access_other_tenant_administration(): void
|
||||
{
|
||||
[, $ownerA] = $this->tenantWithOwner('tenant-a');
|
||||
[$tenantB] = $this->tenantWithOwner('tenant-b');
|
||||
|
||||
$this->actingAs($ownerA)->get(route('administration.users.index', $tenantB))->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_tenant_admin_is_protected_from_deletion(): void
|
||||
{
|
||||
[$tenant, $owner] = $this->tenantWithOwner('tenant-a');
|
||||
|
||||
$this->actingAs(User::factory()->create(['is_platform_admin' => true]))
|
||||
->delete(route('administration.users.destroy', [$tenant, $owner]))
|
||||
->assertStatus(422);
|
||||
}
|
||||
|
||||
public function test_tenant_admin_cannot_manage_another_tenant_admin(): void
|
||||
{
|
||||
[$tenant, $admin] = $this->tenantWithOwner('tenant-a');
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get(route('administration.users.edit', [$tenant, $admin]))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
/** @return array{Tenant, User} */
|
||||
private function tenantWithOwner(string $slug): array
|
||||
{
|
||||
$tenant = app(TenantProvisioningService::class)->createWithOwner([
|
||||
'name' => strtoupper($slug), 'slug' => $slug, 'owner_name' => 'Owner '.$slug,
|
||||
'owner_email' => $slug.'@example.test', 'owner_password' => 'Strong!Password123',
|
||||
]);
|
||||
|
||||
return [$tenant, User::withoutGlobalScope('tenant')->where('tenant_id', $tenant->id)->firstOrFail()];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthenticationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_login_screen_can_be_rendered()
|
||||
{
|
||||
$response = $this->get(route('login'));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_users_can_authenticate_using_the_login_screen()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
public function test_users_with_two_factor_enabled_are_redirected_to_two_factor_challenge()
|
||||
{
|
||||
$this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
|
||||
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->withTwoFactor()->create();
|
||||
|
||||
$response = $this->post(route('login'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('two-factor.login'));
|
||||
$response->assertSessionHas('login.id', $user->id);
|
||||
$this->assertGuest();
|
||||
}
|
||||
|
||||
public function test_users_can_not_authenticate_with_invalid_password()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$this->assertGuest();
|
||||
}
|
||||
|
||||
public function test_users_can_logout()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post(route('logout'));
|
||||
|
||||
$response->assertRedirect(route('home'));
|
||||
|
||||
$this->assertGuest();
|
||||
}
|
||||
|
||||
public function test_users_are_rate_limited()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
RateLimiter::increment(md5('login'.implode('|', [$user->email, '127.0.0.1'])), amount: 5);
|
||||
|
||||
$response = $this->post(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$response->assertTooManyRequests();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EmailVerificationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->skipUnlessFortifyHas(Features::emailVerification());
|
||||
}
|
||||
|
||||
public function test_email_verification_screen_can_be_rendered()
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('verification.notice'));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_email_can_be_verified()
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)],
|
||||
);
|
||||
|
||||
$response = $this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
Event::assertDispatched(Verified::class);
|
||||
|
||||
$this->assertTrue($user->fresh()->hasVerifiedEmail());
|
||||
$response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
public function test_email_is_not_verified_with_invalid_hash()
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1('wrong-email')],
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
Event::assertNotDispatched(Verified::class);
|
||||
$this->assertFalse($user->fresh()->hasVerifiedEmail());
|
||||
}
|
||||
|
||||
public function test_email_is_not_verified_with_invalid_user_id(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => 123, 'hash' => sha1($user->email)],
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
Event::assertNotDispatched(Verified::class);
|
||||
$this->assertFalse($user->fresh()->hasVerifiedEmail());
|
||||
}
|
||||
|
||||
public function test_verified_user_is_redirected_to_dashboard_from_verification_prompt(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('verification.notice'));
|
||||
|
||||
Event::assertNotDispatched(Verified::class);
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
public function test_already_verified_user_visiting_verification_link_is_redirected_without_firing_event_again(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)],
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl)
|
||||
->assertRedirect(route('dashboard', absolute: false).'?verified=1');
|
||||
|
||||
Event::assertNotDispatched(Verified::class);
|
||||
$this->assertTrue($user->fresh()->hasVerifiedEmail());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordConfirmationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_confirm_password_screen_can_be_rendered()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('password.confirm'));
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('auth/confirm-password'),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_password_confirmation_requires_authentication()
|
||||
{
|
||||
$response = $this->get(route('password.confirm'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordResetTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->skipUnlessFortifyHas(Features::resetPasswords());
|
||||
}
|
||||
|
||||
public function test_reset_password_link_screen_can_be_rendered()
|
||||
{
|
||||
$response = $this->get(route('password.request'));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_reset_password_link_can_be_requested()
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post(route('password.email'), ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class);
|
||||
}
|
||||
|
||||
public function test_reset_password_screen_can_be_rendered()
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post(route('password.email'), ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
|
||||
$response = $this->get(route('password.reset', $notification->token));
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_password_can_be_reset_with_valid_token()
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post(route('password.email'), ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
|
||||
$response = $this->post(route('password.update'), [
|
||||
'token' => $notification->token,
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('login'));
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_password_cannot_be_reset_with_invalid_token(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post(route('password.update'), [
|
||||
'token' => 'invalid-token',
|
||||
'email' => $user->email,
|
||||
'password' => 'newpassword123',
|
||||
'password_confirmation' => 'newpassword123',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RegistrationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->skipUnlessFortifyHas(Features::registration());
|
||||
}
|
||||
|
||||
public function test_registration_screen_can_be_rendered()
|
||||
{
|
||||
$response = $this->get(route('register'));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_new_users_can_register()
|
||||
{
|
||||
$response = $this->post(route('register.store'), [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$response->assertRedirect(route('dashboard', absolute: false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TwoFactorChallengeTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
|
||||
}
|
||||
|
||||
public function test_two_factor_challenge_redirects_to_login_when_not_authenticated(): void
|
||||
{
|
||||
$response = $this->get(route('two-factor.login'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
}
|
||||
|
||||
public function test_two_factor_challenge_can_be_rendered(): void
|
||||
{
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->withTwoFactor()->create();
|
||||
|
||||
$this->post(route('login'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->get(route('two-factor.login'))
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('auth/two-factor-challenge'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class VerificationNotificationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->skipUnlessFortifyHas(Features::emailVerification());
|
||||
}
|
||||
|
||||
public function test_sends_verification_notification(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('verification.send'))
|
||||
->assertRedirect(route('home'));
|
||||
|
||||
Notification::assertSentTo($user, VerifyEmail::class);
|
||||
}
|
||||
|
||||
public function test_does_not_send_verification_notification_if_email_is_verified(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('verification.send'))
|
||||
->assertRedirect(route('dashboard', absolute: false));
|
||||
|
||||
Notification::assertNothingSent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\TenantProvisioningService;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DashboardTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
}
|
||||
|
||||
public function test_guests_are_redirected_to_the_login_page()
|
||||
{
|
||||
$response = $this->get(route('dashboard'));
|
||||
$response->assertRedirect(route('login'));
|
||||
}
|
||||
|
||||
public function test_authenticated_users_can_visit_the_dashboard()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('dashboard'));
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_master_admin_receives_platform_dashboard(): void
|
||||
{
|
||||
$admin = User::factory()->create(['is_platform_admin' => true]);
|
||||
|
||||
$this->actingAs($admin)->get(route('dashboard'))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('dashboard')
|
||||
->where('role', 'MASTER ADMIN')
|
||||
->where('tenant', null)
|
||||
->has('stats.tenants')
|
||||
->has('stats.activeLicenses'));
|
||||
}
|
||||
|
||||
public function test_tenant_admin_receives_tenant_admin_dashboard(): void
|
||||
{
|
||||
[$tenant, $admin] = $this->tenantWithAdmin();
|
||||
|
||||
$this->actingAs($admin)->get(route('dashboard'))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->where('role', 'TENANT ADMIN')
|
||||
->where('tenant.id', $tenant->id)
|
||||
->where('stats.users', 1));
|
||||
}
|
||||
|
||||
public function test_tenant_user_receives_tenant_user_dashboard(): void
|
||||
{
|
||||
[$tenant, $admin] = $this->tenantWithAdmin();
|
||||
setPermissionsTeamId($tenant->id);
|
||||
$user = User::factory()->for($tenant)->create();
|
||||
$user->assignRole('TENANT USER');
|
||||
|
||||
$this->actingAs($user)->get(route('dashboard'))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->where('role', 'TENANT USER')
|
||||
->where('tenant.id', $tenant->id)
|
||||
->where('stats.canAddDevice', true));
|
||||
}
|
||||
|
||||
private function tenantWithAdmin(): array
|
||||
{
|
||||
$tenant = app(TenantProvisioningService::class)->createWithOwner([
|
||||
'name' => 'Tenant A', 'slug' => 'tenant-a', 'owner_name' => 'Admin Tenant',
|
||||
'owner_email' => 'admin@tenant.test', 'owner_password' => 'Strong!Password123',
|
||||
]);
|
||||
|
||||
return [$tenant, User::withoutGlobalScope('tenant')->where('tenant_id', $tenant->id)->firstOrFail()];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Jobs\SyncDeviceUser;
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceAccessUser;
|
||||
use App\Models\DeviceType;
|
||||
use App\Models\DeviceUserAssignment;
|
||||
use App\Models\DeviceVendor;
|
||||
use App\Models\User;
|
||||
use App\Services\TenantProvisioningService;
|
||||
use App\Support\TenantContext;
|
||||
use Database\Seeders\DeviceCatalogSeeder;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DeviceAccessUserTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed([RolePermissionSeeder::class, DeviceCatalogSeeder::class]);
|
||||
}
|
||||
|
||||
public function test_one_central_user_can_be_queued_for_multiple_devices_without_password_in_job(): void
|
||||
{
|
||||
Queue::fake();
|
||||
[$tenant, $admin] = $this->tenant('tenant-a');
|
||||
$devices = collect([$this->device($tenant->id, $admin->id), $this->device($tenant->id, $admin->id, '10.0.0.2')]);
|
||||
|
||||
$this->actingAs($admin)->post(route('device-users.store'), [
|
||||
'display_name' => 'Operator NOC', 'username' => 'noc.operator',
|
||||
'password' => 'Device!Password123', 'password_confirmation' => 'Device!Password123',
|
||||
'group_name' => 'RADIQ-NOC', 'device_ids' => $devices->pluck('id')->all(), 'is_enabled' => true,
|
||||
])->assertRedirect(route('device-users.index'));
|
||||
|
||||
$accessUser = DeviceAccessUser::withoutGlobalScope('tenant')->firstOrFail();
|
||||
$this->assertSame('Device!Password123', $accessUser->password);
|
||||
$this->assertNotSame('Device!Password123', DB::table('device_access_users')->value('password'));
|
||||
$this->assertDatabaseCount('device_user_assignments', 2);
|
||||
Queue::assertPushed(SyncDeviceUser::class, 2);
|
||||
Queue::assertPushed(SyncDeviceUser::class, fn (SyncDeviceUser $job) => $job->tenantId === $tenant->id && ! property_exists($job, 'password'));
|
||||
}
|
||||
|
||||
public function test_unsupported_vendor_is_recorded_without_losing_pending_work(): void
|
||||
{
|
||||
[$tenant, $admin] = $this->tenant('tenant-a');
|
||||
$device = $this->device($tenant->id, $admin->id);
|
||||
$user = DeviceAccessUser::create(['tenant_id' => $tenant->id, 'display_name' => 'Operator', 'username' => 'operator', 'password' => 'Device!Password123']);
|
||||
$assignment = DeviceUserAssignment::create(['tenant_id' => $tenant->id, 'device_access_user_id' => $user->id, 'device_id' => $device->id, 'group_name' => 'RADIQ-NOC']);
|
||||
|
||||
(new SyncDeviceUser($tenant->id, $assignment->id))->handle(app(TenantContext::class));
|
||||
|
||||
$this->assertSame('unsupported', $assignment->fresh()->sync_status);
|
||||
$this->assertSame('DRIVER_NOT_AVAILABLE', $assignment->fresh()->error_code);
|
||||
}
|
||||
|
||||
private function tenant(string $slug): array
|
||||
{
|
||||
$tenant = app(TenantProvisioningService::class)->createWithOwner(['name' => strtoupper($slug), 'slug' => $slug, 'owner_name' => 'Admin', 'owner_email' => $slug.'@test.local', 'owner_password' => 'Strong!Password123']);
|
||||
|
||||
return [$tenant, User::withoutGlobalScope('tenant')->where('tenant_id', $tenant->id)->firstOrFail()];
|
||||
}
|
||||
|
||||
private function device(int $tenantId, int $adminId, string $address = '10.0.0.1'): Device
|
||||
{
|
||||
return Device::create(['tenant_id' => $tenantId, 'name' => 'Device '.$address, 'device_vendor_id' => DeviceVendor::where('slug', 'zte')->first()->id, 'device_type_id' => DeviceType::first()->id, 'management_address' => $address, 'management_port' => 22, 'connection_type' => 'ssh', 'created_by' => $adminId]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Enums\SystemRole;
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceCredential;
|
||||
use App\Models\DeviceType;
|
||||
use App\Models\DeviceVendor;
|
||||
use App\Models\User;
|
||||
use App\Services\TenantProvisioningService;
|
||||
use Database\Seeders\DeviceCatalogSeeder;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DeviceInventoryTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed([RolePermissionSeeder::class, DeviceCatalogSeeder::class]);
|
||||
}
|
||||
|
||||
public function test_tenant_admin_can_create_device_and_other_tenant_cannot_see_it(): void
|
||||
{
|
||||
[$tenantA, $adminA] = $this->tenant('tenant-a');
|
||||
[, $adminB] = $this->tenant('tenant-b');
|
||||
$vendor = DeviceVendor::where('slug', 'mikrotik')->firstOrFail();
|
||||
$type = DeviceType::where('slug', 'router')->firstOrFail();
|
||||
|
||||
$this->actingAs($adminA)->post(route('devices.store'), [
|
||||
'name' => 'Router Jakarta', 'management_address' => 'router.example.test',
|
||||
'management_port' => 8728, 'connection_type' => 'routeros_api',
|
||||
'device_vendor_id' => $vendor->id, 'device_type_id' => $type->id,
|
||||
'initial_username' => 'admin', 'initial_password' => '',
|
||||
'remove_legacy_users_on_activation' => true,
|
||||
])->assertRedirect(route('devices.index'));
|
||||
|
||||
$device = Device::withoutGlobalScope('tenant')->firstOrFail();
|
||||
$this->assertSame($tenantA->id, $device->tenant_id);
|
||||
$credential = DeviceCredential::withoutGlobalScope('tenant')->where('device_id', $device->id)->firstOrFail();
|
||||
$this->assertSame('admin', $credential->username);
|
||||
$this->assertSame('', $credential->password);
|
||||
$this->assertTrue($device->remove_legacy_users_on_activation);
|
||||
$this->actingAs($adminB)->get(route('devices.show', $device))->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_tenant_user_create_access_follows_tenant_policy(): void
|
||||
{
|
||||
[$tenant, $admin] = $this->tenant('tenant-a');
|
||||
setPermissionsTeamId($tenant->id);
|
||||
$user = User::factory()->for($tenant)->create();
|
||||
$user->assignRole(SystemRole::TenantUser->value);
|
||||
$tenant->devicePolicy()->update(['tenant_user_can_create' => false]);
|
||||
|
||||
$this->actingAs($user)->get(route('devices.create'))->assertForbidden();
|
||||
$this->actingAs($admin)->put(route('device-policy.update'), [
|
||||
'tenant_user_can_create' => true, 'tenant_user_can_update_own' => true,
|
||||
'tenant_user_can_delete_own' => false,
|
||||
])->assertRedirect();
|
||||
$this->actingAs($user)->get(route('devices.create'))->assertOk();
|
||||
}
|
||||
|
||||
public function test_device_password_is_encrypted_and_hidden(): void
|
||||
{
|
||||
[$tenant, $admin] = $this->tenant('tenant-a');
|
||||
$device = Device::create([
|
||||
'tenant_id' => $tenant->id, 'name' => 'Router', 'device_vendor_id' => DeviceVendor::first()->id,
|
||||
'device_type_id' => DeviceType::first()->id, 'management_address' => '10.0.0.1',
|
||||
'management_port' => 22, 'connection_type' => 'ssh', 'created_by' => $admin->id,
|
||||
]);
|
||||
$credential = DeviceCredential::create([
|
||||
'tenant_id' => $tenant->id, 'device_id' => $device->id, 'name' => 'Management',
|
||||
'username' => 'admin', 'password' => 'DeviceSecret!123', 'connection_type' => 'ssh',
|
||||
]);
|
||||
|
||||
$this->assertNotSame('DeviceSecret!123', DB::table('device_credentials')->value('password'));
|
||||
$this->assertSame('DeviceSecret!123', $credential->password);
|
||||
$this->assertArrayNotHasKey('password', $credential->toArray());
|
||||
}
|
||||
|
||||
private function tenant(string $slug): array
|
||||
{
|
||||
$tenant = app(TenantProvisioningService::class)->createWithOwner([
|
||||
'name' => strtoupper($slug), 'slug' => $slug, 'owner_name' => 'Admin '.$slug,
|
||||
'owner_email' => $slug.'@test.local', 'owner_password' => 'Strong!Password123',
|
||||
]);
|
||||
|
||||
return [$tenant, User::withoutGlobalScope('tenant')->where('tenant_id', $tenant->id)->firstOrFail()];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_returns_a_successful_response()
|
||||
{
|
||||
$response = $this->get(route('home'));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Enums\DeploymentMode;
|
||||
use App\Enums\LicenseStatus;
|
||||
use App\Models\DeploymentInstallation;
|
||||
use App\Models\License;
|
||||
use App\Models\Tenant;
|
||||
use App\Support\TenantContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LicensingArchitectureTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_license_and_installation_are_tenant_scoped(): void
|
||||
{
|
||||
$tenantA = Tenant::factory()->create();
|
||||
$tenantB = Tenant::factory()->create();
|
||||
$installation = DeploymentInstallation::create([
|
||||
'tenant_id' => $tenantA->id,
|
||||
'name' => 'ISP A Server',
|
||||
'deployment_type' => DeploymentMode::SelfHosted,
|
||||
'instance_key_hash' => hash('sha256', 'instance-secret'),
|
||||
]);
|
||||
License::create([
|
||||
'tenant_id' => $tenantA->id,
|
||||
'deployment_installation_id' => $installation->id,
|
||||
'license_key_hash' => hash('sha256', 'RNDM-RAW-KEY'),
|
||||
'plan' => 'professional',
|
||||
'status' => LicenseStatus::Active,
|
||||
'starts_at' => now(),
|
||||
'expires_at' => now()->addYear(),
|
||||
]);
|
||||
|
||||
app(TenantContext::class)->set($tenantB->id);
|
||||
|
||||
$this->assertCount(0, License::all());
|
||||
$this->assertCount(0, DeploymentInstallation::all());
|
||||
}
|
||||
|
||||
public function test_self_hosted_installation_cannot_create_platform_admin(): void
|
||||
{
|
||||
config()->set('deployment.mode', DeploymentMode::SelfHosted->value);
|
||||
|
||||
$this->artisan('app:create-platform-admin')
|
||||
->expectsOutput('Platform Super Admin hanya dibuat pada RADIQ control plane atau managed cloud.')
|
||||
->assertFailed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?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 [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PermissionTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_role_assignments_are_tenant_scoped(): void
|
||||
{
|
||||
$tenantA = Tenant::factory()->create();
|
||||
$tenantB = Tenant::factory()->create();
|
||||
$userA = User::factory()->for($tenantA)->create();
|
||||
$userB = User::factory()->for($tenantB)->create();
|
||||
$permission = Permission::create(['name' => 'device.view', 'guard_name' => 'web']);
|
||||
|
||||
setPermissionsTeamId($tenantA->id);
|
||||
$roleA = Role::create(['name' => 'NOC', 'guard_name' => 'web']);
|
||||
$roleA->givePermissionTo($permission);
|
||||
$userA->assignRole($roleA);
|
||||
|
||||
setPermissionsTeamId($tenantB->id);
|
||||
$roleB = Role::create(['name' => 'NOC', 'guard_name' => 'web']);
|
||||
$userB->assignRole($roleB);
|
||||
|
||||
setPermissionsTeamId($tenantA->id);
|
||||
$this->assertTrue($userA->fresh()->can('device.view'));
|
||||
setPermissionsTeamId($tenantB->id);
|
||||
$this->assertFalse($userB->fresh()->can('device.view'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PlatformAdminTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_command_creates_platform_admin_without_a_tenant(): void
|
||||
{
|
||||
$this->artisan('app:create-platform-admin', [
|
||||
'--name' => 'Platform Administrator',
|
||||
'--email' => 'admin@example.test',
|
||||
])
|
||||
->expectsQuestion('Password (minimum 12 characters)', 'Strong!Password123')
|
||||
->expectsQuestion('Confirm password', 'Strong!Password123')
|
||||
->expectsOutput('Platform Master Admin created successfully.')
|
||||
->assertSuccessful();
|
||||
|
||||
$admin = User::withoutGlobalScope('tenant')->firstOrFail();
|
||||
|
||||
$this->assertNull($admin->tenant_id);
|
||||
$this->assertTrue($admin->is_platform_admin);
|
||||
$this->assertTrue(Gate::forUser($admin)->allows('tenant.access-any'));
|
||||
}
|
||||
|
||||
public function test_normal_tenant_admin_does_not_receive_platform_access(): void
|
||||
{
|
||||
$user = User::factory()->create(['is_platform_admin' => false]);
|
||||
|
||||
$this->assertFalse(Gate::forUser($user)->allows('tenant.access-any'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Settings;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProfileUpdateTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_profile_page_is_displayed()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->get(route('profile.edit'));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_profile_information_can_be_updated()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->patch(route('profile.update'), [
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('profile.edit'));
|
||||
|
||||
$user->refresh();
|
||||
|
||||
$this->assertSame('Test User', $user->name);
|
||||
$this->assertSame('test@example.com', $user->email);
|
||||
$this->assertNull($user->email_verified_at);
|
||||
}
|
||||
|
||||
public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->patch(route('profile.update'), [
|
||||
'name' => 'Test User',
|
||||
'email' => $user->email,
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('profile.edit'));
|
||||
|
||||
$this->assertNotNull($user->refresh()->email_verified_at);
|
||||
}
|
||||
|
||||
public function test_user_can_delete_their_account()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->delete(route('profile.destroy'), [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('home'));
|
||||
|
||||
$this->assertGuest();
|
||||
$this->assertNull($user->fresh());
|
||||
}
|
||||
|
||||
public function test_correct_password_must_be_provided_to_delete_account()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->from(route('profile.edit'))
|
||||
->delete(route('profile.destroy'), [
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasErrors('password')
|
||||
->assertRedirect(route('profile.edit'));
|
||||
|
||||
$this->assertNotNull($user->fresh());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Settings;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SecurityTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_security_page_is_displayed()
|
||||
{
|
||||
$this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
|
||||
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
Features::passkeys([
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession(['auth.password_confirmed_at' => time()])
|
||||
->get(route('security.edit'))
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('settings/security')
|
||||
->where('canManagePasskeys', true)
|
||||
->where('passkeys', [])
|
||||
->where('canManageTwoFactor', true)
|
||||
->where('twoFactorEnabled', false),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_security_page_requires_password_confirmation_when_enabled()
|
||||
{
|
||||
$this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(route('security.edit'));
|
||||
|
||||
$response->assertRedirect(route('password.confirm'));
|
||||
}
|
||||
|
||||
public function test_security_page_renders_without_two_factor_when_feature_is_disabled()
|
||||
{
|
||||
$this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
|
||||
|
||||
config(['fortify.features' => []]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession(['auth.password_confirmed_at' => time()])
|
||||
->get(route('security.edit'))
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('settings/security')
|
||||
->where('canManagePasskeys', false)
|
||||
->where('passkeys', [])
|
||||
->where('canManageTwoFactor', false)
|
||||
->missing('twoFactorEnabled')
|
||||
->missing('requiresConfirmation'),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_password_can_be_updated()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->from(route('security.edit'))
|
||||
->put(route('user-password.update'), [
|
||||
'current_password' => 'password',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('security.edit'));
|
||||
|
||||
$this->assertTrue(Hash::check('new-password', $user->refresh()->password));
|
||||
}
|
||||
|
||||
public function test_correct_password_must_be_provided_to_update_password()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this
|
||||
->actingAs($user)
|
||||
->from(route('security.edit'))
|
||||
->put(route('user-password.update'), [
|
||||
'current_password' => 'wrong-password',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasErrors('current_password')
|
||||
->assertRedirect(route('security.edit'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Support\TenantContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TenantIsolationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_user_queries_are_scoped_to_current_tenant(): void
|
||||
{
|
||||
$tenantA = Tenant::factory()->create();
|
||||
$tenantB = Tenant::factory()->create();
|
||||
User::factory()->for($tenantA)->create();
|
||||
User::factory()->for($tenantB)->create();
|
||||
app(TenantContext::class)->set($tenantA->id);
|
||||
|
||||
$this->assertCount(1, User::all());
|
||||
$this->assertTrue(User::first()->tenant->is($tenantA));
|
||||
}
|
||||
|
||||
public function test_tenant_id_is_assigned_from_context_on_create(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create();
|
||||
app(TenantContext::class)->set($tenant->id);
|
||||
|
||||
$user = User::factory()->create(['tenant_id' => null]);
|
||||
|
||||
$this->assertSame($tenant->id, $user->tenant_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
protected function skipUnlessFortifyHas(string $feature, ?string $message = null): void
|
||||
{
|
||||
if (! Features::enabled($feature)) {
|
||||
$this->markTestSkipped($message ?? "Fortify feature [{$feature}] is not enabled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_that_true_is_true()
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user