Files
shopit-back/tests/Feature/Auth/AdminAppEventScopeTest.php

237 lines
11 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_event_admins_of_the_same_tenant_receive_the_same_assigned_menus(): void
{
DB::table('menues')->insert([
'code' => 'onticket.adminapp.event',
'label' => 'Eventos',
'route' => '/admin/event',
'parent_menu_code' => 'main.adminapp',
]);
DB::table('roles_menues')->insert([
'rol_codigo' => 'adminapp',
'menu_codigo' => 'onticket.adminapp.event',
]);
DB::table('tenants_menues')->insert([
'tenant_code' => 'onticket',
'menu_code' => 'onticket.adminapp.event',
]);
$this->user->update(['admin_scope' => 'event', 'event_id' => 1]);
$token = $this->login()->assertOk()->json('token');
$firstMenus = $this->withToken($token)->getJson('/api/v1/adminapp/me')
->assertOk()->json('data.tenant.menues');
$this->user->update(['event_id' => 2]);
$secondMenus = $this->withToken($token)->getJson('/api/v1/adminapp/me')
->assertOk()->json('data.tenant.menues');
$this->assertSame($firstMenus, $secondMenus);
$this->assertSame(
['adminapp.ventas', 'onticket.adminapp.event'],
collect($firstMenus[0]['submenues'])->pluck('code')->sort()->values()->all(),
);
}
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']);
}
}