204 lines
9.6 KiB
PHP
204 lines
9.6 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Auth;
|
|
|
|
use App\Domains\Core\Auth\Models\User;
|
|
use Illuminate\Database\Migrations\Migration;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Testing\TestResponse;
|
|
use Tests\TestCase;
|
|
|
|
class AdminAppEventScopeTest extends TestCase
|
|
{
|
|
private User $user;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// Focused pre-migration schema: do not replay unrelated provisioning migrations.
|
|
// Tests\TestCase and tests/bootstrap.php enforce SQLite :memory:.
|
|
Schema::create('tenants', function (Blueprint $table): void {
|
|
$table->id();
|
|
$table->string('codigo')->unique();
|
|
$table->string('nombre');
|
|
$table->string('dominio');
|
|
$table->string('search_product_layout')->default('column_with_image');
|
|
$table->string('search_group_layout')->default('paginated');
|
|
});
|
|
Schema::create('events', function (Blueprint $table): void {
|
|
$table->id();
|
|
$table->string('tenant_code');
|
|
$table->string('title');
|
|
$table->timestamps();
|
|
});
|
|
Schema::create('users', function (Blueprint $table): void {
|
|
$table->id();
|
|
$table->string('nombre_apellido');
|
|
$table->string('email');
|
|
$table->string('active_email')->virtualAs('CASE WHEN deleted_at IS NULL THEN lower(email) ELSE NULL END');
|
|
$table->string('password');
|
|
$table->string('rol_codigo')->default('user');
|
|
$table->string('tenant_codigo')->nullable();
|
|
$table->softDeletes();
|
|
$table->timestamps();
|
|
$table->unique(['active_email', 'rol_codigo']);
|
|
});
|
|
Schema::create('menues', function (Blueprint $table): void {
|
|
$table->id();
|
|
$table->string('code')->unique();
|
|
$table->string('label');
|
|
$table->string('route');
|
|
$table->string('parent_menu_code')->nullable();
|
|
$table->string('content_type')->default('dynamic');
|
|
});
|
|
Schema::create('roles_menues', function (Blueprint $table): void {
|
|
$table->string('rol_codigo');
|
|
$table->string('menu_codigo');
|
|
});
|
|
Schema::create('tenants_menues', function (Blueprint $table): void {
|
|
$table->string('tenant_code');
|
|
$table->string('menu_code');
|
|
$table->json('static_content')->nullable();
|
|
$table->timestamps();
|
|
});
|
|
foreach ([
|
|
'2026_06_18_130006_create_personal_access_tokens_table.php',
|
|
'2026_07_28_000000_create_roles_and_permissions_tables.php',
|
|
'2026_07_29_000000_add_login_security_fields_to_users_table.php',
|
|
'2026_07_29_000100_create_login_attempts_table.php',
|
|
] as $file) {
|
|
(require database_path('migrations/'.$file))->up();
|
|
}
|
|
|
|
DB::table('roles')->insert(['codigo' => 'adminapp', 'nombre' => 'AdminApp']);
|
|
DB::table('tenants')->insert([
|
|
['codigo' => 'onticket', 'nombre' => 'OnTicket', 'dominio' => 'onticket.test'],
|
|
['codigo' => 'other', 'nombre' => 'Other', 'dominio' => 'other.test'],
|
|
]);
|
|
DB::table('events')->insert([
|
|
['id' => 1, 'tenant_code' => 'onticket', 'title' => 'Evento A'],
|
|
['id' => 2, 'tenant_code' => 'onticket', 'title' => 'Evento B'],
|
|
['id' => 3, 'tenant_code' => 'other', 'title' => 'Evento ajeno'],
|
|
]);
|
|
// An existing administrator must remain general after applying the new migration.
|
|
DB::table('users')->insert([
|
|
'id' => 1, 'nombre_apellido' => 'Admin', 'email' => 'admin@example.test',
|
|
'password' => Hash::make('secret123'), 'rol_codigo' => 'adminapp', 'tenant_codigo' => 'onticket',
|
|
]);
|
|
$this->scopeMigration()->up();
|
|
$this->user = User::query()->findOrFail(1);
|
|
DB::table('menues')->insert([
|
|
['code' => 'main.adminapp', 'label' => 'Administración', 'route' => '/', 'parent_menu_code' => null],
|
|
['code' => 'adminapp.ventas', 'label' => 'Ventas', 'route' => '/admin/ventas', 'parent_menu_code' => 'main.adminapp'],
|
|
]);
|
|
foreach (['main.adminapp', 'adminapp.ventas'] as $code) {
|
|
DB::table('roles_menues')->insert(['rol_codigo' => 'adminapp', 'menu_codigo' => $code]);
|
|
DB::table('tenants_menues')->insert(['tenant_code' => 'onticket', 'menu_code' => $code]);
|
|
}
|
|
}
|
|
|
|
private function scopeMigration(): Migration
|
|
{
|
|
return require database_path('migrations/2026_09_30_000000_add_admin_scope_to_users.php');
|
|
}
|
|
|
|
private function login(array $extra = []): TestResponse
|
|
{
|
|
return $this->postJson('/api/v1/adminapp/login', [
|
|
'email' => ' ADMIN@EXAMPLE.TEST ', 'password' => 'secret123', ...$extra,
|
|
]);
|
|
}
|
|
|
|
public function test_existing_admin_keeps_general_access_after_migration(): void
|
|
{
|
|
$this->assertTrue($this->user->isTenantAdministrator());
|
|
$token = $this->login()->assertOk()->assertJsonPath('user.admin_scope', 'tenant')
|
|
->assertJsonPath('user.event_id', null)->json('token');
|
|
$this->withToken($token)->getJson('/api/v1/adminapp/me')->assertOk()
|
|
->assertJsonPath('data.event', null)->assertJsonCount(1, 'data.tenant.menues.0.submenues');
|
|
}
|
|
|
|
public function test_event_admin_logs_in_and_restores_only_its_assigned_context(): void
|
|
{
|
|
$this->user->update(['admin_scope' => 'event', 'event_id' => 1]);
|
|
$this->assertSame(1, $this->user->event->id);
|
|
// Scope cannot be chosen by the caller during login.
|
|
$token = $this->login(['admin_scope' => 'tenant', 'event_id' => 2])->assertOk()
|
|
->assertJsonPath('user.admin_scope', 'event')->assertJsonPath('user.event_id', 1)->json('token');
|
|
$this->withToken($token)->getJson('/api/v1/adminapp/me')->assertOk()
|
|
->assertJsonPath('data.event.id', 1)->assertJsonPath('data.event.title', 'Evento A')
|
|
->assertJsonPath('data.tenant.codigo', 'onticket')
|
|
->assertJsonCount(1, 'data.tenant.menues.0.submenues');
|
|
$this->assertSame(['adminapp'], $this->user->tokens()->sole()->abilities);
|
|
}
|
|
|
|
public function test_invalid_scopes_never_issue_tokens_or_increment_password_failure_counts(): void
|
|
{
|
|
foreach ([
|
|
['admin_scope' => 'event', 'event_id' => null],
|
|
['admin_scope' => 'event', 'event_id' => 3],
|
|
['admin_scope' => 'tenant', 'event_id' => 1],
|
|
['admin_scope' => 'unknown', 'event_id' => null],
|
|
['admin_scope' => 'event', 'event_id' => 1, 'tenant_codigo' => null],
|
|
] as $attributes) {
|
|
$this->user->update($attributes);
|
|
$this->login()->assertUnprocessable()->assertJsonValidationErrors('email');
|
|
}
|
|
$this->assertDatabaseCount('personal_access_tokens', 0);
|
|
$this->assertSame(0, $this->user->refresh()->failed_login_attempts);
|
|
}
|
|
|
|
public function test_event_admin_cannot_access_tenant_operations_but_can_logout(): void
|
|
{
|
|
$this->user->update(['admin_scope' => 'event', 'event_id' => 1]);
|
|
$token = $this->login()->assertOk()->json('token');
|
|
foreach (['tenant/tickets', 'tenant/administrators', 'tenant/website-extras', 'forms/tickets-filter'] as $path) {
|
|
$this->withToken($token)->getJson('/api/v1/adminapp/'.$path)->assertForbidden();
|
|
}
|
|
$this->withToken($token)->postJson('/api/v1/adminapp/tenant/administrators', [])->assertForbidden();
|
|
$this->withToken($token)->postJson('/api/logout')->assertOk();
|
|
$this->assertDatabaseCount('personal_access_tokens', 0);
|
|
}
|
|
|
|
public function test_deleting_the_event_invalidates_an_existing_token_without_promoting_the_user(): void
|
|
{
|
|
$this->user->update(['admin_scope' => 'event', 'event_id' => 1]);
|
|
$token = $this->login()->assertOk()->json('token');
|
|
DB::table('events')->where('id', 1)->delete();
|
|
$this->assertNull($this->user->refresh()->event_id);
|
|
$this->assertSame('event', $this->user->admin_scope);
|
|
$this->withToken($token)->getJson('/api/v1/adminapp/me')->assertForbidden();
|
|
$this->withToken($token)->getJson('/api/v1/adminapp/tenant/administrators')->assertForbidden();
|
|
}
|
|
|
|
public function test_event_reassignment_and_tenant_changes_apply_to_existing_tokens(): void
|
|
{
|
|
$this->user->update(['admin_scope' => 'event', 'event_id' => 1]);
|
|
$token = $this->login()->assertOk()->json('token');
|
|
$this->user->update(['event_id' => 2]);
|
|
$this->withToken($token)->getJson('/api/v1/adminapp/me')->assertOk()->assertJsonPath('data.event.id', 2);
|
|
DB::table('events')->where('id', 2)->update(['tenant_code' => 'other']);
|
|
$this->withToken($token)->getJson('/api/v1/adminapp/me')->assertForbidden();
|
|
}
|
|
|
|
public function test_wrong_password_still_counts_as_a_failed_attempt(): void
|
|
{
|
|
$this->user->update(['admin_scope' => 'event', 'event_id' => 1]);
|
|
$this->login(['password' => 'wrong'])->assertUnprocessable()->assertJsonValidationErrors('email');
|
|
$this->assertSame(1, $this->user->refresh()->failed_login_attempts);
|
|
$this->assertDatabaseCount('personal_access_tokens', 0);
|
|
}
|
|
|
|
public function test_scope_migration_can_be_rolled_back_without_removing_users(): void
|
|
{
|
|
$this->scopeMigration()->down();
|
|
$this->assertFalse(Schema::hasColumn('users', 'admin_scope'));
|
|
$this->assertFalse(Schema::hasColumn('users', 'event_id'));
|
|
$this->assertDatabaseHas('users', ['id' => 1, 'email' => 'admin@example.test']);
|
|
}
|
|
}
|