Merge pull request 'feature/event_scoped_admin' (#13) from feature/event_scoped_admin into dev
Reviewed-on: https://gitea.quo.ar/tbianchini/shopit-back/pulls/13
This commit is contained in:
@@ -17,6 +17,7 @@ class UserAuthorizationRelationsTest extends TestCase
|
||||
$this->assertTrue(Schema::hasColumns('users', [
|
||||
'rol_codigo',
|
||||
'tenant_codigo',
|
||||
'event_id',
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -28,5 +29,7 @@ class UserAuthorizationRelationsTest extends TestCase
|
||||
$this->assertNull($user->tenant_codigo);
|
||||
$this->assertSame(RoleCode::User->value, $user->role->codigo);
|
||||
$this->assertNull($user->tenant);
|
||||
$this->assertNull($user->event_id);
|
||||
$this->assertNull($user->event);
|
||||
}
|
||||
}
|
||||
|
||||
76
tests/Feature/Menu/TicketMenuAccessTest.php
Normal file
76
tests/Feature/Menu/TicketMenuAccessTest.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Menu;
|
||||
|
||||
use App\Domains\Core\Auth\Models\User;
|
||||
use App\Http\Middleware\EnsureTenantHasMenu;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TicketMenuAccessTest extends TestCase
|
||||
{
|
||||
public static function menuAssignments(): array
|
||||
{
|
||||
return [
|
||||
'OnTicket' => ['onticket.adminapp.tickets', 'current', true],
|
||||
'old code' => ['adminapp.tickets', 'current', false],
|
||||
'another tenant' => ['onticket.adminapp.tickets', 'other', false],
|
||||
'unrelated menu' => ['adminapp.ventas', 'current', false],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('menuAssignments')]
|
||||
public function test_ticket_menu_requires_an_association_with_the_authenticated_tenant(string $menuCode, string $assignedTenant, bool $allowed): void
|
||||
{
|
||||
Schema::create('tenants', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('codigo');
|
||||
});
|
||||
Schema::create('menues', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('code');
|
||||
});
|
||||
Schema::create('tenants_menues', function (Blueprint $table): void {
|
||||
$table->string('tenant_code');
|
||||
$table->string('menu_code');
|
||||
});
|
||||
DB::table('tenants')->insert(['codigo' => 'current']);
|
||||
DB::table('menues')->insert(['code' => $menuCode]);
|
||||
DB::table('tenants_menues')->insert(['tenant_code' => $assignedTenant, 'menu_code' => $menuCode]);
|
||||
|
||||
$user = new User(['tenant_codigo' => 'current']);
|
||||
$request = Request::create('/api/v1/adminapp/tenant/tickets');
|
||||
$request->setUserResolver(fn () => $user);
|
||||
if (! $allowed) {
|
||||
$this->expectException(HttpException::class);
|
||||
$this->expectExceptionCode(0);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = (new EnsureTenantHasMenu)->handle($request, fn () => response('allowed'), 'onticket.adminapp.tickets');
|
||||
$this->assertSame('allowed', $response->getContent());
|
||||
} catch (HttpException $exception) {
|
||||
$this->assertSame(404, $exception->getStatusCode());
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
public function test_all_ticket_routes_and_filter_form_use_the_updated_menu_codes(): void
|
||||
{
|
||||
foreach ([
|
||||
'adminapp.tickets.index', 'adminapp.tickets.cancel',
|
||||
'adminapp.tickets.calculate-refund', 'adminapp.tickets.refund',
|
||||
'adminapp.tickets.pdf', 'adminapp.tickets.excel', 'adminapp.forms.tickets-filter',
|
||||
] as $name) {
|
||||
$route = Route::getRoutes()->getByName($name);
|
||||
$this->assertNotNull($route);
|
||||
$this->assertContains('tenant.menu:onticket.adminapp.tickets', $route->gatherMiddleware());
|
||||
}
|
||||
}
|
||||
}
|
||||
57
tests/Feature/Migrations/AddEventIdToUsersTest.php
Normal file
57
tests/Feature/Migrations/AddEventIdToUsersTest.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Migrations;
|
||||
|
||||
use App\Domains\Core\Administrator\Resources\AdministratorResource;
|
||||
use App\Domains\Core\Auth\Models\User;
|
||||
use App\Domains\Core\Auth\Resources\UserResource;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AddEventIdToUsersTest extends TestCase
|
||||
{
|
||||
public function test_event_assignment_preserves_existing_users_and_restricts_event_deletion(): void
|
||||
{
|
||||
Schema::create('events', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
});
|
||||
Schema::create('users', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_codigo')->nullable();
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
});
|
||||
DB::table('users')->insert(['id' => 1, 'tenant_codigo' => 'legacy']);
|
||||
DB::table('events')->insert(['id' => 42]);
|
||||
|
||||
$migration = require database_path('migrations/2026_10_01_000000_add_event_id_to_users_table.php');
|
||||
$migration->up();
|
||||
|
||||
$legacy = User::query()->findOrFail(1);
|
||||
$this->assertNull($legacy->event_id);
|
||||
$this->assertNull($legacy->event);
|
||||
$this->assertSame('legacy', $legacy->tenant_codigo);
|
||||
|
||||
$legacy->update(['event_id' => '42']);
|
||||
$user = $legacy->fresh();
|
||||
$this->assertSame(42, $user->event_id);
|
||||
$this->assertSame(42, $user->event->id);
|
||||
$this->assertSame(42, UserResource::make($user)->resolve(new Request)['event_id']);
|
||||
$this->assertSame(42, AdministratorResource::make($user)->resolve(new Request)['event_id']);
|
||||
|
||||
try {
|
||||
DB::table('events')->where('id', 42)->delete();
|
||||
$this->fail('An assigned event must not be deleted.');
|
||||
} catch (QueryException $exception) {
|
||||
$this->assertStringContainsString('FOREIGN KEY', $exception->getMessage());
|
||||
}
|
||||
|
||||
$migration->down();
|
||||
$this->assertFalse(Schema::hasColumn('users', 'event_id'));
|
||||
$this->assertSame('legacy', DB::table('users')->where('id', 1)->value('tenant_codigo'));
|
||||
}
|
||||
}
|
||||
183
tests/Feature/Sale/AdminAppSaleEventScopeTest.php
Normal file
183
tests/Feature/Sale/AdminAppSaleEventScopeTest.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Sale;
|
||||
|
||||
use App\Domains\Commerce\Purchase\Models\Purchase;
|
||||
use App\Domains\Commerce\Purchase\Services\CheckoutService;
|
||||
use App\Domains\Commerce\Sale\Services\AdminAppSaleExcelService;
|
||||
use App\Domains\Commerce\Sale\Services\AdminAppSalePdfService;
|
||||
use App\Domains\Core\Auth\Models\User;
|
||||
use App\Domains\Core\Authorization\Enums\RoleCode;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Mockery\MockInterface;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminAppSaleEventScopeTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Isolated schema: the full legacy migration chain cannot run on SQLite.
|
||||
Schema::create('tenants', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('codigo');
|
||||
});
|
||||
Schema::create('users', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->softDeletes();
|
||||
});
|
||||
Schema::create('compras', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_codigo');
|
||||
$table->unsignedBigInteger('event_id')->nullable();
|
||||
$table->string('nombre_apellido');
|
||||
$table->string('status');
|
||||
$table->decimal('total', 12, 2);
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('compra_items', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('compra_id');
|
||||
$table->integer('cantidad');
|
||||
});
|
||||
Schema::create('tickets', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('source_purchase_item_id');
|
||||
});
|
||||
Schema::create('ticket_refunds', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('purchase_item_id');
|
||||
$table->decimal('amount', 12, 2);
|
||||
});
|
||||
Schema::create('value_changes', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_code');
|
||||
$table->string('trackable_type');
|
||||
$table->unsignedBigInteger('trackable_id');
|
||||
$table->string('attribute');
|
||||
$table->string('old_value');
|
||||
$table->string('new_value');
|
||||
$table->timestamp('changed_at');
|
||||
$table->string('actor_type');
|
||||
$table->unsignedBigInteger('user_id')->nullable();
|
||||
});
|
||||
|
||||
DB::table('tenants')->insert(['codigo' => 'onticket']);
|
||||
foreach ([1 => ['onticket', 10], 2 => ['onticket', 20], 3 => ['onticket', null], 4 => ['other', 10]] as $id => [$tenant, $event]) {
|
||||
DB::table('compras')->insert([
|
||||
'id' => $id,
|
||||
'tenant_codigo' => $tenant,
|
||||
'event_id' => $event,
|
||||
'nombre_apellido' => 'Cliente',
|
||||
'status' => Purchase::STATUS_PAID,
|
||||
'total' => $id * 100,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
DB::table('compra_items')->insert(['id' => $id, 'compra_id' => $id, 'cantidad' => 1]);
|
||||
DB::table('ticket_refunds')->insert(['purchase_item_id' => $id, 'amount' => $id * 10]);
|
||||
DB::table('value_changes')->insert([
|
||||
'id' => $id,
|
||||
'tenant_code' => $tenant,
|
||||
'trackable_type' => (new Purchase)->getMorphClass(),
|
||||
'trackable_id' => $id,
|
||||
'attribute' => 'status',
|
||||
'old_value' => Purchase::STATUS_PENDING_PAYMENT,
|
||||
'new_value' => Purchase::STATUS_PAID,
|
||||
'changed_at' => now(),
|
||||
'actor_type' => 'system',
|
||||
]);
|
||||
}
|
||||
$this->actingAsAdministrator(10);
|
||||
}
|
||||
|
||||
public function test_list_totals_and_filters_cannot_escape_the_authenticated_event(): void
|
||||
{
|
||||
$this->getJson('/api/v1/adminapp/tenant/sales?event_id=20&q=Cliente')
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.id', 1)
|
||||
->assertJsonPath('confirmed_sales_total', '100.00')
|
||||
->assertJsonPath('refunded_total', '10.00');
|
||||
$this->getJson('/api/v1/adminapp/tenant/sales?id=2')->assertOk()->assertJsonCount(0, 'data');
|
||||
$this->getJson('/api/v1/adminapp/tenant/sales/modifications?q=Cliente')
|
||||
->assertOk()->assertJsonCount(1, 'data')->assertJsonPath('data.0.sale_id', 1);
|
||||
}
|
||||
|
||||
public function test_unscoped_administrators_keep_access_to_all_sales_in_their_tenant(): void
|
||||
{
|
||||
$this->actingAsAdministrator(null);
|
||||
$this->getJson('/api/v1/adminapp/tenant/sales')->assertOk()->assertJsonCount(3, 'data')
|
||||
->assertJsonPath('confirmed_sales_total', '600.00')->assertJsonPath('refunded_total', '60.00');
|
||||
$this->getJson('/api/v1/adminapp/tenant/sales/modifications')->assertOk()->assertJsonCount(3, 'data');
|
||||
$this->getJson('/api/v1/adminapp/tenant/sales/2')->assertOk();
|
||||
$this->getJson('/api/v1/adminapp/tenant/sales/3')->assertOk();
|
||||
}
|
||||
|
||||
public function test_foreign_and_unassigned_sales_are_inaccessible_before_any_checkout_action(): void
|
||||
{
|
||||
$this->mock(CheckoutService::class, function (MockInterface $mock): void {
|
||||
$mock->shouldNotReceive('confirmPaidPurchase');
|
||||
$mock->shouldNotReceive('cancelPurchaseFromAdmin');
|
||||
});
|
||||
foreach ([2, 3, 4] as $id) {
|
||||
$this->getJson("/api/v1/adminapp/tenant/sales/{$id}")->assertNotFound();
|
||||
$this->getJson("/api/v1/adminapp/tenant/sales/{$id}/tickets")->assertNotFound();
|
||||
$this->postJson("/api/v1/adminapp/tenant/sales/{$id}/confirm", ['event_id' => 20])->assertNotFound();
|
||||
$this->postJson("/api/v1/adminapp/tenant/sales/{$id}/cancel", ['event_id' => 20])->assertNotFound();
|
||||
}
|
||||
$this->assertSame(Purchase::STATUS_PAID, DB::table('compras')->where('id', 2)->value('status'));
|
||||
}
|
||||
|
||||
public function test_own_event_allows_detail_tickets_and_checkout_actions(): void
|
||||
{
|
||||
// Empty snapshots keep this fixture focused on authorization.
|
||||
DB::table('compra_items')->where('compra_id', 1)->delete();
|
||||
$this->mock(CheckoutService::class, function (MockInterface $mock): void {
|
||||
foreach (['confirmPaidPurchase', 'cancelPurchaseFromAdmin'] as $method) {
|
||||
$mock->shouldReceive($method)->once()->withArgs(fn (Purchase $sale): bool => $sale->id === 1)
|
||||
->andReturnUsing(fn (Purchase $sale): Purchase => $sale);
|
||||
}
|
||||
});
|
||||
$this->getJson('/api/v1/adminapp/tenant/sales/1')->assertOk()->assertJsonPath('data.id', 1);
|
||||
$this->getJson('/api/v1/adminapp/tenant/sales/1/tickets')->assertOk();
|
||||
$this->postJson('/api/v1/adminapp/tenant/sales/1/confirm')->assertOk()->assertJsonPath('data.id', 1);
|
||||
$this->postJson('/api/v1/adminapp/tenant/sales/1/cancel')->assertOk()->assertJsonPath('data.id', 1);
|
||||
}
|
||||
|
||||
public function test_pdf_and_excel_exports_only_receive_sales_and_history_for_the_own_event(): void
|
||||
{
|
||||
foreach ([AdminAppSalePdfService::class, AdminAppSaleExcelService::class] as $class) {
|
||||
$this->mock($class, function (MockInterface $mock) use ($class): void {
|
||||
foreach (['downloadSales', 'downloadModifications'] as $method) {
|
||||
$mock->shouldReceive($method)->once()->withArgs(
|
||||
fn ($tenant, Collection $rows, $timezone): bool => $tenant->codigo === 'onticket'
|
||||
&& $rows->pluck('id')->all() === [1] && $timezone === 'UTC'
|
||||
)->andReturn($class === AdminAppSalePdfService::class
|
||||
? response('pdf') : new StreamedResponse(fn () => print ('excel')));
|
||||
}
|
||||
});
|
||||
}
|
||||
foreach (['pdf', 'excel', 'modifications/pdf', 'modifications/excel'] as $path) {
|
||||
$this->getJson("/api/v1/adminapp/tenant/sales/{$path}?timezone=UTC&event_id=20")->assertOk();
|
||||
}
|
||||
}
|
||||
|
||||
private function actingAsAdministrator(?int $eventId): void
|
||||
{
|
||||
$user = new User;
|
||||
$user->setRawAttributes([
|
||||
'id' => 1,
|
||||
'rol_codigo' => RoleCode::AdminApp->value,
|
||||
'tenant_codigo' => 'onticket',
|
||||
'event_id' => $eventId,
|
||||
]);
|
||||
Sanctum::actingAs($user);
|
||||
}
|
||||
}
|
||||
158
tests/Feature/Staff/StaffEventScopeTest.php
Normal file
158
tests/Feature/Staff/StaffEventScopeTest.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Staff;
|
||||
|
||||
use App\Domains\Core\Auth\Models\User;
|
||||
use App\Domains\Core\Auth\Services\ResetPasswordAttemptService;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StaffEventScopeTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Exercise HTTP authorization on SQLite without the incompatible legacy migrations.
|
||||
Schema::create('tenants', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('codigo');
|
||||
$table->boolean('scanner_category_validation_enabled')->default(false);
|
||||
});
|
||||
Schema::create('roles', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('codigo');
|
||||
$table->string('nombre');
|
||||
});
|
||||
Schema::create('users', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('rol_codigo');
|
||||
$table->string('tenant_codigo');
|
||||
$table->unsignedBigInteger('event_id')->nullable();
|
||||
$table->string('nombre_apellido');
|
||||
$table->string('dni');
|
||||
$table->string('email');
|
||||
$table->string('active_email')->nullable();
|
||||
$table->string('password')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
Schema::create('categorias', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('nombre');
|
||||
$table->string('tenant_code')->nullable();
|
||||
$table->unsignedBigInteger('categoria_id')->nullable();
|
||||
});
|
||||
Schema::create('catalog_items', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('category_id');
|
||||
$table->string('tenant_code');
|
||||
$table->softDeletes();
|
||||
});
|
||||
Schema::create('category_scanners', function (Blueprint $table): void {
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->unsignedBigInteger('categoria_id');
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tokenable_type');
|
||||
$table->unsignedBigInteger('tokenable_id');
|
||||
});
|
||||
|
||||
DB::table('tenants')->insert(['codigo' => 'onticket']);
|
||||
foreach (['adminapp', 'scanner'] as $role) {
|
||||
DB::table('roles')->insert(['codigo' => $role, 'nombre' => $role]);
|
||||
foreach ([10, 20, null] as $eventId) {
|
||||
$this->insertUser($role, 'onticket', $eventId);
|
||||
}
|
||||
$this->insertUser($role, 'other', 10);
|
||||
}
|
||||
Sanctum::actingAs(User::query()->findOrFail(1));
|
||||
}
|
||||
|
||||
public function test_lists_and_searches_are_restricted_to_the_authenticated_event(): void
|
||||
{
|
||||
foreach (['administrators' => 1, 'staff' => 5] as $path => $id) {
|
||||
foreach (['', '?search=Persona&event_id=20'] as $query) {
|
||||
$this->getJson("/api/v1/adminapp/tenant/{$path}{$query}")
|
||||
->assertOk()->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.id', $id)->assertJsonPath('data.0.event_id', 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function test_creation_inherits_the_actor_event_even_if_the_client_supplies_another(): void
|
||||
{
|
||||
$this->mock(ResetPasswordAttemptService::class, function (MockInterface $mock): void {
|
||||
$mock->shouldReceive('createForAdminAppEmail')->once();
|
||||
$mock->shouldReceive('createForScannerEmail')->once();
|
||||
});
|
||||
foreach (['administrators' => 'adminapp', 'staff' => 'scanner'] as $path => $role) {
|
||||
$this->postJson("/api/v1/adminapp/tenant/{$path}", [
|
||||
...$this->payload("new-{$role}@example.com"), 'event_id' => 20,
|
||||
])->assertSuccessful()->assertJsonPath('data.event_id', 10);
|
||||
$this->assertDatabaseHas('users', ['email' => "new-{$role}@example.com", 'event_id' => 10, 'rol_codigo' => $role]);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_foreign_and_unassigned_staff_cannot_be_edited_deleted_or_inspected(): void
|
||||
{
|
||||
foreach (['administrators' => [2, 3, 4], 'staff' => [6, 7, 8]] as $path => $ids) {
|
||||
foreach ($ids as $id) {
|
||||
$this->putJson("/api/v1/adminapp/tenant/{$path}/{$id}", $this->payload("update-{$id}@example.com"))->assertNotFound();
|
||||
$this->deleteJson("/api/v1/adminapp/tenant/{$path}/{$id}")->assertNotFound();
|
||||
$this->assertDatabaseHas('users', ['id' => $id, 'deleted_at' => null, 'email' => "person-{$id}@example.com"]);
|
||||
if ($path === 'staff') {
|
||||
$this->getJson("/api/v1/adminapp/tenant/staff/{$id}/scan-attempts")->assertNotFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function test_own_staff_can_be_edited_and_deleted_without_changing_its_event(): void
|
||||
{
|
||||
$adminId = $this->insertUser('adminapp', 'onticket', 10);
|
||||
foreach (['administrators' => $adminId, 'staff' => 5] as $path => $id) {
|
||||
$this->putJson("/api/v1/adminapp/tenant/{$path}/{$id}", [
|
||||
...$this->payload("updated-{$id}@example.com"), 'event_id' => 20,
|
||||
])->assertOk()->assertJsonPath('data.event_id', 10);
|
||||
$this->deleteJson("/api/v1/adminapp/tenant/{$path}/{$id}")->assertNoContent();
|
||||
$this->assertSoftDeleted('users', ['id' => $id]);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_unscoped_actor_keeps_tenant_lists_and_creates_without_an_event(): void
|
||||
{
|
||||
Sanctum::actingAs(User::query()->findOrFail(3));
|
||||
$this->mock(ResetPasswordAttemptService::class, function (MockInterface $mock): void {
|
||||
$mock->shouldReceive('createForAdminAppEmail')->once();
|
||||
$mock->shouldReceive('createForScannerEmail')->once();
|
||||
});
|
||||
foreach (['administrators', 'staff'] as $path) {
|
||||
$this->getJson("/api/v1/adminapp/tenant/{$path}")->assertOk()->assertJsonCount(3, 'data');
|
||||
$this->postJson("/api/v1/adminapp/tenant/{$path}", $this->payload("legacy-{$path}@example.com"))
|
||||
->assertSuccessful()->assertJsonPath('data.event_id', null);
|
||||
}
|
||||
}
|
||||
|
||||
private function insertUser(string $role, string $tenant, ?int $eventId): int
|
||||
{
|
||||
$id = DB::table('users')->count() + 1;
|
||||
|
||||
return DB::table('users')->insertGetId([
|
||||
'id' => $id, 'rol_codigo' => $role, 'tenant_codigo' => $tenant,
|
||||
'event_id' => $eventId, 'nombre_apellido' => 'Persona', 'dni' => '12345678',
|
||||
'email' => "person-{$id}@example.com", 'active_email' => "person-{$id}@example.com",
|
||||
]);
|
||||
}
|
||||
|
||||
private function payload(string $email): array
|
||||
{
|
||||
return ['nombre_apellido' => 'Persona editada', 'dni' => '87654321', 'email' => $email];
|
||||
}
|
||||
}
|
||||
178
tests/Feature/Ticket/AdminAppTicketEventScopeTest.php
Normal file
178
tests/Feature/Ticket/AdminAppTicketEventScopeTest.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Ticket;
|
||||
|
||||
use App\Domains\Core\Auth\Models\User;
|
||||
use App\Domains\Ticketing\Ticket\Services\AdminAppTicketExcelService;
|
||||
use App\Domains\Ticketing\Ticket\Services\AdminAppTicketPdfService;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Mockery\MockInterface;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminAppTicketEventScopeTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Keep HTTP tests isolated from legacy migrations incompatible with SQLite.
|
||||
Schema::create('tenants', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('codigo');
|
||||
$table->string('timezone')->default('UTC');
|
||||
$table->boolean('allow_ticket_refund')->default(false);
|
||||
$table->boolean('allow_ticket_total_refund')->default(false);
|
||||
$table->boolean('allow_ticket_partial_refund')->default(false);
|
||||
});
|
||||
Schema::create('menues', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('code');
|
||||
});
|
||||
Schema::create('tenants_menues', function (Blueprint $table): void {
|
||||
$table->string('tenant_code');
|
||||
$table->string('menu_code');
|
||||
});
|
||||
Schema::create('users', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('nombre_apellido');
|
||||
$table->softDeletes();
|
||||
});
|
||||
Schema::create('tickets', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_code');
|
||||
$table->unsignedBigInteger('event_id')->nullable();
|
||||
$table->string('ticket');
|
||||
foreach (['source_variant_id', 'source_catalog_item_id', 'source_purchase_item_id', 'scanner_user_id', 'user_id'] as $column) {
|
||||
$table->unsignedBigInteger($column)->nullable();
|
||||
}
|
||||
foreach (['used_at', 'disabled_at', 'cancelled_at', 'refunded_at'] as $column) {
|
||||
$table->timestamp($column)->nullable();
|
||||
}
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('compras', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_codigo');
|
||||
$table->unsignedBigInteger('event_id')->nullable();
|
||||
$table->string('nombre_apellido');
|
||||
});
|
||||
Schema::create('compra_items', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('compra_id');
|
||||
$table->decimal('precio_unitario', 12, 2);
|
||||
});
|
||||
Schema::create('ticket_refunds', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('ticket_id')->nullable();
|
||||
$table->unsignedBigInteger('purchase_item_id');
|
||||
$table->decimal('amount', 12, 2);
|
||||
});
|
||||
Schema::create('desfile_entry_reservations', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('ticket_id');
|
||||
$table->softDeletes();
|
||||
});
|
||||
Schema::create('value_changes', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_code');
|
||||
$table->string('trackable_type');
|
||||
$table->unsignedBigInteger('trackable_id');
|
||||
$table->string('attribute');
|
||||
$table->string('old_value')->nullable();
|
||||
$table->string('new_value')->nullable();
|
||||
$table->timestamp('changed_at');
|
||||
$table->string('actor_type');
|
||||
$table->unsignedBigInteger('user_id')->nullable();
|
||||
});
|
||||
|
||||
DB::table('tenants')->insert(['codigo' => 'onticket']);
|
||||
DB::table('menues')->insert(['code' => 'onticket.adminapp.tickets']);
|
||||
DB::table('tenants_menues')->insert(['tenant_code' => 'onticket', 'menu_code' => 'onticket.adminapp.tickets']);
|
||||
foreach ([1 => ['onticket', 10], 2 => ['onticket', 20], 3 => ['onticket', null], 4 => ['other', 10]] as $id => [$tenant, $event]) {
|
||||
DB::table('tickets')->insert([
|
||||
'id' => $id, 'tenant_code' => $tenant, 'event_id' => $event,
|
||||
'ticket' => "ticket-{$id}", 'used_at' => now(),
|
||||
]);
|
||||
DB::table('compras')->insert(['id' => $id, 'tenant_codigo' => $tenant, 'event_id' => $event, 'nombre_apellido' => 'Cliente']);
|
||||
DB::table('compra_items')->insert(['id' => $id, 'compra_id' => $id, 'precio_unitario' => 100]);
|
||||
DB::table('ticket_refunds')->insert(['purchase_item_id' => $id, 'amount' => $id * 10]);
|
||||
}
|
||||
$this->actingAsAdmin(10);
|
||||
}
|
||||
|
||||
public function test_list_counts_refunded_total_and_search_cannot_escape_the_user_event(): void
|
||||
{
|
||||
$this->getJson('/api/v1/adminapp/tenant/tickets?sort_by=id&event_id=20')
|
||||
->assertOk()->assertJsonCount(1, 'data')->assertJsonPath('data.0.id', 1)
|
||||
->assertJsonPath('scanned_tickets', 1)->assertJsonPath('total_tickets', 1)
|
||||
->assertJsonPath('refunded_total', '10.00');
|
||||
$this->getJson('/api/v1/adminapp/tenant/tickets?q=2')->assertOk()->assertJsonCount(0, 'data');
|
||||
// Status uses sorting in memory rather than the database.
|
||||
$this->getJson('/api/v1/adminapp/tenant/tickets?sort_by=status')
|
||||
->assertOk()->assertJsonCount(1, 'data')->assertJsonPath('data.0.id', 1);
|
||||
DB::table('tickets')->where('id', 1)->update(['used_at' => null]);
|
||||
$this->getJson('/api/v1/adminapp/tenant/tickets?status=active')
|
||||
->assertOk()->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('scanned_tickets', 0)->assertJsonPath('total_tickets', 1);
|
||||
}
|
||||
|
||||
public function test_foreign_unassigned_and_other_tenant_tickets_cannot_be_modified_or_refunded(): void
|
||||
{
|
||||
foreach ([2, 3, 4] as $id) {
|
||||
$this->postJson("/api/v1/adminapp/tenant/tickets/{$id}/cancel", ['event_id' => 20])->assertNotFound();
|
||||
$this->getJson("/api/v1/adminapp/tenant/tickets/{$id}/refund")->assertNotFound();
|
||||
$this->postJson("/api/v1/adminapp/tenant/tickets/{$id}/refund", ['refund_type' => 'total', 'event_id' => 20])->assertNotFound();
|
||||
$this->assertDatabaseHas('tickets', ['id' => $id, 'cancelled_at' => null, 'refunded_at' => null]);
|
||||
}
|
||||
$this->assertDatabaseCount('value_changes', 0);
|
||||
$this->assertDatabaseCount('ticket_refunds', 4);
|
||||
}
|
||||
|
||||
public function test_own_ticket_can_be_cancelled_and_refund_requests_reach_business_validation(): void
|
||||
{
|
||||
DB::table('tickets')->where('id', 1)->update(['used_at' => null]);
|
||||
$this->getJson('/api/v1/adminapp/tenant/tickets/1/refund')->assertUnprocessable();
|
||||
$this->postJson('/api/v1/adminapp/tenant/tickets/1/refund', ['refund_type' => 'total'])->assertUnprocessable();
|
||||
$this->postJson('/api/v1/adminapp/tenant/tickets/1/cancel')->assertOk();
|
||||
$this->assertNotNull(DB::table('tickets')->where('id', 1)->value('cancelled_at'));
|
||||
}
|
||||
|
||||
public function test_unscoped_admin_keeps_the_tenant_scope(): void
|
||||
{
|
||||
$this->actingAsAdmin(null);
|
||||
$this->getJson('/api/v1/adminapp/tenant/tickets?sort_by=id')
|
||||
->assertOk()->assertJsonCount(3, 'data')->assertJsonPath('total_tickets', 3)
|
||||
->assertJsonPath('refunded_total', '60.00');
|
||||
DB::table('tickets')->where('id', 3)->update(['used_at' => null]);
|
||||
$this->postJson('/api/v1/adminapp/tenant/tickets/3/cancel')->assertOk();
|
||||
$this->postJson('/api/v1/adminapp/tenant/tickets/4/cancel')->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_pdf_and_excel_receive_only_the_tickets_of_the_user_event(): void
|
||||
{
|
||||
foreach ([AdminAppTicketPdfService::class, AdminAppTicketExcelService::class] as $class) {
|
||||
$this->mock($class, function (MockInterface $mock) use ($class): void {
|
||||
$mock->shouldReceive('download')->once()->withArgs(
|
||||
fn ($tenant, Collection $tickets, $timezone): bool => $tenant->codigo === 'onticket'
|
||||
&& $tickets->pluck('id')->all() === [1] && $timezone === 'UTC'
|
||||
)->andReturn($class === AdminAppTicketPdfService::class
|
||||
? response('pdf') : new StreamedResponse(fn () => print ('excel')));
|
||||
});
|
||||
}
|
||||
foreach (['pdf', 'excel'] as $format) {
|
||||
$this->getJson("/api/v1/adminapp/tenant/tickets/{$format}?timezone=UTC&event_id=20")->assertOk();
|
||||
}
|
||||
}
|
||||
|
||||
private function actingAsAdmin(?int $eventId): void
|
||||
{
|
||||
$user = new User;
|
||||
$user->setRawAttributes(['id' => 1, 'rol_codigo' => 'adminapp', 'tenant_codigo' => 'onticket', 'event_id' => $eventId]);
|
||||
Sanctum::actingAs($user);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user