462 lines
18 KiB
PHP
462 lines
18 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Notification;
|
|
|
|
use App\Domains\Attachable\Enums\AttachmentType;
|
|
use App\Domains\Attachable\Models\Attachment;
|
|
use App\Domains\Auth\Models\ResetPasswordAttempt;
|
|
use App\Domains\Auth\Models\User;
|
|
use App\Domains\Catalog\Models\CatalogItem;
|
|
use App\Domains\Integration\Models\Integration;
|
|
use App\Domains\Notification\Events\PasswordResetRequested;
|
|
use App\Domains\Notification\Services\NotificationMailService;
|
|
use App\Domains\Purchase\Models\Purchase;
|
|
use App\Domains\Tenant\Models\Tenant;
|
|
use App\Domains\Tenant\Models\WebsiteType;
|
|
use App\Domains\Ticket\Models\Ticket;
|
|
use Barryvdh\DomPDF\ServiceProvider as DomPdfServiceProvider;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Mail\Mailable;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Tests\TestCase;
|
|
|
|
class NotificationMailServiceTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private Tenant $tenant;
|
|
|
|
private User $user;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->app->register(DomPdfServiceProvider::class);
|
|
Mail::fake();
|
|
Integration::query()->create([
|
|
'integration_code' => 'email',
|
|
'name' => 'Email',
|
|
'url' => null,
|
|
'requires_configuration' => false,
|
|
'integration_data_schema' => [],
|
|
]);
|
|
$header = Attachment::query()->create([
|
|
'path' => 'test/mail-header.png',
|
|
'filename' => 'header.png',
|
|
'type' => AttachmentType::Image,
|
|
'mime_type' => 'image/png',
|
|
]);
|
|
$footer = Attachment::query()->create([
|
|
'path' => 'test/mail-footer.png',
|
|
'filename' => 'footer.png',
|
|
'type' => AttachmentType::Image,
|
|
'mime_type' => 'image/png',
|
|
]);
|
|
$this->tenant = Tenant::query()->create([
|
|
'codigo' => 'mail-tenant',
|
|
'nombre' => 'Mail Tenant',
|
|
'dominio' => 'mail.local',
|
|
'primary_color' => '#112233',
|
|
'secondary_color' => '#000000',
|
|
'danger_color' => '#000000',
|
|
'success_color' => '#000000',
|
|
'header_bg_color' => '#000000',
|
|
'footer_bg_color' => '#000000',
|
|
'header_logo_id' => $header->id,
|
|
'footer_logo_id' => $footer->id,
|
|
]);
|
|
$this->user = User::factory()->create([
|
|
'nombre_apellido' => 'Ada Lovelace',
|
|
'email' => 'ada@example.com',
|
|
]);
|
|
}
|
|
|
|
public function test_it_sends_a_branded_welcome_email(): void
|
|
{
|
|
$this->useWebsiteTypeBranding();
|
|
|
|
app(NotificationMailService::class)->sendWelcome($this->user->id, $this->tenant->codigo);
|
|
app(NotificationMailService::class)->sendWelcome($this->user->id, $this->tenant->codigo);
|
|
|
|
Mail::assertSent(Mailable::class, 1);
|
|
|
|
Mail::assertSent(Mailable::class, function (Mailable $mail): bool {
|
|
$mail->assertTo('ada@example.com');
|
|
$mail->assertHasSubject('Bienvenido a OnTicket');
|
|
|
|
return str_contains($mail->render(), 'Ada Lovelace')
|
|
&& str_contains($mail->render(), 'OnTicket')
|
|
&& ! str_contains($mail->render(), 'Mail Tenant')
|
|
&& str_contains($mail->render(), 'border-top: 4px solid #ff7006');
|
|
});
|
|
}
|
|
|
|
public function test_it_sends_a_branded_password_reset_email(): void
|
|
{
|
|
$this->useWebsiteTypeBranding();
|
|
$attempt = $this->user->resetPasswordAttempts()->create([
|
|
'codigo' => '0123',
|
|
]);
|
|
|
|
app(NotificationMailService::class)->sendPasswordResetCode(
|
|
$attempt->id,
|
|
$this->tenant->codigo,
|
|
);
|
|
app(NotificationMailService::class)->sendPasswordResetCode(
|
|
$attempt->id,
|
|
$this->tenant->codigo,
|
|
);
|
|
|
|
Mail::assertSent(Mailable::class, 1);
|
|
|
|
Mail::assertSent(Mailable::class, function (Mailable $mail): bool {
|
|
$mail->assertTo('ada@example.com');
|
|
$mail->assertHasSubject('Código para recuperar tu contraseña - OnTicket');
|
|
$rendered = $mail->render();
|
|
|
|
return str_contains($rendered, '0123')
|
|
&& str_contains($rendered, 'Ada Lovelace')
|
|
&& str_contains($rendered, 'OnTicket')
|
|
&& ! str_contains($rendered, 'Mail Tenant')
|
|
&& str_contains($rendered, '#ff7006')
|
|
&& ! str_contains($rendered, 'border: 2px solid #112233');
|
|
});
|
|
}
|
|
|
|
public function test_password_reset_idempotency_is_scoped_to_each_attempt(): void
|
|
{
|
|
$firstAttempt = $this->user->resetPasswordAttempts()->create(['codigo' => '0123']);
|
|
$secondAttempt = $this->user->resetPasswordAttempts()->create(['codigo' => '4567']);
|
|
$service = app(NotificationMailService::class);
|
|
|
|
$service->sendPasswordResetCode($firstAttempt->id, $this->tenant->codigo);
|
|
$service->sendPasswordResetCode($firstAttempt->id, $this->tenant->codigo);
|
|
$service->sendPasswordResetCode($secondAttempt->id, $this->tenant->codigo);
|
|
|
|
Mail::assertSent(Mailable::class, 2);
|
|
$this->assertDatabaseHas('email_deliveries', [
|
|
'idempotency_key' => "password-reset:{$firstAttempt->id}",
|
|
'status' => 'sent',
|
|
]);
|
|
$this->assertDatabaseHas('email_deliveries', [
|
|
'idempotency_key' => "password-reset:{$secondAttempt->id}",
|
|
'status' => 'sent',
|
|
]);
|
|
}
|
|
|
|
public function test_it_links_scanner_password_resets_to_the_scanner_domain(): void
|
|
{
|
|
$websiteType = WebsiteType::query()->create([
|
|
'codigo' => 'scanner-mail',
|
|
'nombre' => 'Scanner Mail',
|
|
'dominio' => 'admin.mail.local',
|
|
'scanner_domain' => 'scanner.mail.local',
|
|
]);
|
|
$this->tenant->update(['website_type_code' => $websiteType->codigo]);
|
|
$attempt = $this->user->resetPasswordAttempts()->create([
|
|
'codigo' => '0123',
|
|
'reason' => ResetPasswordAttempt::REASON_STAFF_CREATED,
|
|
]);
|
|
|
|
app(NotificationMailService::class)->sendPasswordResetCode(
|
|
$attempt->id,
|
|
$this->tenant->codigo,
|
|
PasswordResetRequested::CHANNEL_SCANNER,
|
|
);
|
|
|
|
Mail::assertSent(Mailable::class, function (Mailable $mail): bool {
|
|
$mail->assertHasSubject('Tu cuenta de escáner está lista - Scanner Mail');
|
|
$rendered = $mail->render();
|
|
|
|
return str_contains($rendered, 'https://scanner.mail.local/recuperar-contrasena/codigo')
|
|
&& str_contains($rendered, 'Tu cuenta de escáner está lista')
|
|
&& str_contains($rendered, 'comenzar a escanear entradas')
|
|
&& ! str_contains($rendered, 'Recuperá tu contraseña')
|
|
&& str_contains($rendered, 'email=ada%40example.com')
|
|
&& str_contains($rendered, 'code=0123')
|
|
&& str_contains($rendered, 'Crear mi');
|
|
});
|
|
}
|
|
|
|
public function test_administrator_creation_has_custom_copy_and_the_existing_code_and_link(): void
|
|
{
|
|
$websiteType = WebsiteType::query()->create([
|
|
'codigo' => 'admin-mail', 'nombre' => 'Admin Mail',
|
|
'dominio' => 'admin.mail.local', 'scanner_domain' => 'scanner.mail.local',
|
|
]);
|
|
$this->tenant->update(['website_type_code' => $websiteType->codigo]);
|
|
$attempt = $this->user->resetPasswordAttempts()->create([
|
|
'codigo' => '0456', 'reason' => ResetPasswordAttempt::REASON_ADMINISTRATOR_CREATED,
|
|
]);
|
|
app(NotificationMailService::class)->sendPasswordResetCode($attempt->id, $this->tenant->codigo, PasswordResetRequested::CHANNEL_ADMINAPP);
|
|
Mail::assertSent(Mailable::class, function (Mailable $mail): bool {
|
|
$mail->assertHasSubject('Tu cuenta de administrador está lista - Admin Mail');
|
|
$html = $mail->render();
|
|
|
|
return str_contains($html, 'Tu cuenta de administrador está lista')
|
|
&& str_contains($html, 'ingresar al panel de administración')
|
|
&& str_contains($html, '0456')
|
|
&& str_contains($html, 'Crear mi contraseña')
|
|
&& str_contains($html, 'https://admin.mail.local/recuperar-contrasena/codigo?email=ada%40example.com')
|
|
&& ! str_contains($html, 'scanner.mail.local')
|
|
&& ! str_contains($html, 'administrator_created')
|
|
&& ! str_contains($html, 'Recuperá tu contraseña');
|
|
});
|
|
}
|
|
|
|
public function test_locked_account_has_its_own_copy_and_the_shared_code_action(): void
|
|
{
|
|
$attempt = $this->user->resetPasswordAttempts()->create([
|
|
'codigo' => '0789', 'reason' => ResetPasswordAttempt::REASON_ACCOUNT_LOCKED,
|
|
]);
|
|
app(NotificationMailService::class)->sendPasswordResetCode($attempt->id, $this->tenant->codigo);
|
|
Mail::assertSent(Mailable::class, function (Mailable $mail): bool {
|
|
$mail->assertHasSubject('Desbloqueá tu cuenta - Mail Tenant');
|
|
$html = $mail->render();
|
|
|
|
return str_contains($html, 'Desbloqueá tu cuenta')
|
|
&& str_contains($html, 'bloqueamos el acceso temporalmente')
|
|
&& str_contains($html, '0789')
|
|
&& str_contains($html, 'Ingresar código ahora')
|
|
&& str_contains($html, 'https://mail.local/recuperar-contrasena/codigo')
|
|
&& ! str_contains($html, 'account_locked');
|
|
});
|
|
}
|
|
|
|
public function test_it_sends_one_purchase_confirmation_with_generated_tickets_attached(): void
|
|
{
|
|
$this->useWebsiteTypeBranding();
|
|
$purchase = Purchase::query()->create([
|
|
'tenant_codigo' => $this->tenant->codigo,
|
|
'user_id' => $this->user->id,
|
|
'status' => Purchase::STATUS_PAID,
|
|
'payment_method' => 'transfer',
|
|
'total' => 25,
|
|
'email' => 'checkout@example.com',
|
|
]);
|
|
$catalogItem = CatalogItem::query()->create([
|
|
'tenant_code' => $this->tenant->codigo,
|
|
'slug' => 'entrada',
|
|
'nombre' => 'Entrada',
|
|
'descripcion' => 'Entrada general',
|
|
'precio' => 25,
|
|
'has_tickets' => true,
|
|
]);
|
|
$purchaseItem = $purchase->items()->create([
|
|
'source_catalog_item_id' => $catalogItem->id,
|
|
'nombre' => 'Entrada',
|
|
'descripcion' => 'Entrada general',
|
|
'slug' => 'entrada',
|
|
'item_nombre' => 'Entrada general',
|
|
'variant_attributes' => [],
|
|
'cantidad' => 1,
|
|
'precio_unitario' => 25,
|
|
'total' => 25,
|
|
]);
|
|
$ticket = Ticket::query()->create([
|
|
'tenant_code' => $this->tenant->codigo,
|
|
'ticket' => fake()->uuid(),
|
|
'source_purchase_item_id' => $purchaseItem->id,
|
|
'source_catalog_item_id' => $catalogItem->id,
|
|
'user_id' => $this->user->id,
|
|
]);
|
|
$service = app(NotificationMailService::class);
|
|
|
|
$service->sendPurchaseConfirmed($purchase->id);
|
|
|
|
Mail::assertSent(Mailable::class, 1);
|
|
Mail::assertSent(Mailable::class, function (Mailable $mail) use ($purchase, $ticket): bool {
|
|
$mail->assertTo('checkout@example.com');
|
|
$attachment = collect($mail->rawAttachments)->firstWhere('name', "tickets_{$ticket->id}.pdf");
|
|
|
|
return $mail->subject === "Compra confirmada - Compra #{$purchase->id}"
|
|
&& str_contains($mail->render(), '¡Compra realizada con éxito!')
|
|
&& str_contains($mail->render(), 'Total pagado')
|
|
&& str_contains($mail->render(), 'Tus tickets ya están disponibles')
|
|
&& $attachment !== null
|
|
&& $attachment['options'] === ['mime' => 'application/pdf']
|
|
&& str_starts_with($attachment['data'], '%PDF-')
|
|
&& str_contains($mail->render(), 'border-top: 4px solid #112233')
|
|
&& ! str_contains($mail->render(), 'border-top: 4px solid #ff7006');
|
|
});
|
|
}
|
|
|
|
public function test_purchase_confirmation_omits_ticket_content_and_attachment_without_tickets(): void
|
|
{
|
|
$purchase = Purchase::query()->create([
|
|
'tenant_codigo' => $this->tenant->codigo,
|
|
'user_id' => $this->user->id,
|
|
'status' => Purchase::STATUS_PAID,
|
|
'payment_method' => 'transfer',
|
|
'total' => 25,
|
|
]);
|
|
|
|
app(NotificationMailService::class)->sendPurchaseConfirmed($purchase->id);
|
|
app(NotificationMailService::class)->sendPurchaseConfirmed($purchase->id);
|
|
|
|
Mail::assertSent(Mailable::class, 1);
|
|
|
|
Mail::assertSent(Mailable::class, function (Mailable $mail) use ($purchase): bool {
|
|
return $mail->subject === "Compra confirmada - Compra #{$purchase->id}"
|
|
&& str_contains($mail->render(), '¡Compra realizada con éxito!')
|
|
&& ! str_contains($mail->render(), 'Tus tickets ya están disponibles')
|
|
&& $mail->rawAttachments === [];
|
|
});
|
|
}
|
|
|
|
public function test_it_sends_one_rescheduling_email_per_purchase_and_does_not_duplicate_it(): void
|
|
{
|
|
$purchase = Purchase::query()->create([
|
|
'tenant_codigo' => $this->tenant->codigo,
|
|
'user_id' => $this->user->id,
|
|
'status' => Purchase::STATUS_PAID,
|
|
'payment_method' => 'transfer',
|
|
'total' => 25,
|
|
'email' => 'checkout@example.com',
|
|
]);
|
|
$purchaseItem = $purchase->items()->create([
|
|
'source_catalog_item_id' => $this->catalogItem()->id,
|
|
'nombre' => 'Entrada',
|
|
'item_nombre' => 'Entrada general',
|
|
'variant_attributes' => [],
|
|
'cantidad' => 2,
|
|
'precio_unitario' => 25,
|
|
'total' => 25,
|
|
]);
|
|
$firstTicket = $this->ticketFor($purchaseItem->id);
|
|
$secondTicket = $this->ticketFor($purchaseItem->id);
|
|
$purchaseTickets = [[
|
|
'purchase_id' => $purchase->id,
|
|
'ticket_ids' => [$firstTicket->id, $secondTicket->id],
|
|
]];
|
|
|
|
app(NotificationMailService::class)->sendEventDateRescheduled(
|
|
$this->tenant->codigo,
|
|
10,
|
|
20,
|
|
'09/10/2027',
|
|
'20/10/2027',
|
|
$purchaseTickets,
|
|
);
|
|
app(NotificationMailService::class)->sendEventDateRescheduled(
|
|
$this->tenant->codigo,
|
|
10,
|
|
20,
|
|
'09/10/2027',
|
|
'20/10/2027',
|
|
$purchaseTickets,
|
|
);
|
|
|
|
Mail::assertSent(Mailable::class, 1);
|
|
Mail::assertSent(Mailable::class, function (Mailable $mail) use ($purchase, $firstTicket, $secondTicket): bool {
|
|
$mail->assertTo('checkout@example.com');
|
|
|
|
return $mail->subject === "Tu evento fue reprogramado - Compra #{$purchase->id}"
|
|
&& str_contains($mail->render(), '09/10/2027')
|
|
&& str_contains($mail->render(), '20/10/2027')
|
|
&& str_contains($mail->render(), '#'.$firstTicket->id)
|
|
&& str_contains($mail->render(), '#'.$secondTicket->id);
|
|
});
|
|
$this->assertDatabaseHas('email_deliveries', [
|
|
'idempotency_key' => "event-date-rescheduled:10:20:{$purchase->id}",
|
|
'email_type' => 'event_date_rescheduled',
|
|
'status' => 'sent',
|
|
]);
|
|
}
|
|
|
|
public function test_suspension_email_uses_the_account_email_and_separates_disabled_tickets(): void
|
|
{
|
|
$purchase = Purchase::query()->create([
|
|
'tenant_codigo' => $this->tenant->codigo,
|
|
'user_id' => $this->user->id,
|
|
'status' => Purchase::STATUS_PAID,
|
|
'payment_method' => 'transfer',
|
|
'total' => 25,
|
|
'email' => null,
|
|
]);
|
|
$purchaseItem = $purchase->items()->create([
|
|
'source_catalog_item_id' => $this->catalogItem()->id,
|
|
'nombre' => 'Entrada',
|
|
'item_nombre' => 'Entrada general',
|
|
'variant_attributes' => [],
|
|
'cantidad' => 2,
|
|
'precio_unitario' => 25,
|
|
'total' => 25,
|
|
]);
|
|
$disabledTicket = $this->ticketFor($purchaseItem->id);
|
|
$disabledTicket->markAsDisabled();
|
|
$disabledTicket->save();
|
|
$activeTicket = $this->ticketFor($purchaseItem->id);
|
|
|
|
app(NotificationMailService::class)->sendEventDateSuspended(
|
|
$this->tenant->codigo,
|
|
10,
|
|
'09/10/2027',
|
|
[[
|
|
'purchase_id' => $purchase->id,
|
|
'ticket_ids' => [$disabledTicket->id, $activeTicket->id],
|
|
]],
|
|
);
|
|
app(NotificationMailService::class)->sendEventDateSuspended(
|
|
$this->tenant->codigo,
|
|
10,
|
|
'09/10/2027',
|
|
[[
|
|
'purchase_id' => $purchase->id,
|
|
'ticket_ids' => [$disabledTicket->id, $activeTicket->id],
|
|
]],
|
|
);
|
|
|
|
Mail::assertSent(Mailable::class, 1);
|
|
|
|
Mail::assertSent(Mailable::class, function (Mailable $mail) use ($disabledTicket, $activeTicket): bool {
|
|
$mail->assertTo('ada@example.com');
|
|
|
|
return str_contains($mail->render(), 'quedaron inhabilitados')
|
|
&& str_contains($mail->render(), 'conservan otras fechas disponibles')
|
|
&& str_contains($mail->render(), '#'.$disabledTicket->id)
|
|
&& str_contains($mail->render(), '#'.$activeTicket->id);
|
|
});
|
|
}
|
|
|
|
private function ticketFor(int $purchaseItemId): Ticket
|
|
{
|
|
return Ticket::query()->create([
|
|
'tenant_code' => $this->tenant->codigo,
|
|
'ticket' => fake()->uuid(),
|
|
'source_purchase_item_id' => $purchaseItemId,
|
|
'user_id' => $this->user->id,
|
|
]);
|
|
}
|
|
|
|
private function catalogItem(): CatalogItem
|
|
{
|
|
return CatalogItem::query()->create([
|
|
'tenant_code' => $this->tenant->codigo,
|
|
'slug' => fake()->unique()->slug(),
|
|
'nombre' => 'Entrada',
|
|
'descripcion' => 'Entrada general',
|
|
'precio' => 25,
|
|
'has_tickets' => true,
|
|
]);
|
|
}
|
|
|
|
private function useWebsiteTypeBranding(): void
|
|
{
|
|
$websiteType = WebsiteType::query()->create([
|
|
'codigo' => 'onticket',
|
|
'nombre' => 'OnTicket',
|
|
'dominio' => 'onticket.local',
|
|
'primary_color' => '#ff7006',
|
|
'body_color' => '#666666',
|
|
'background_color' => '#f8f8f8',
|
|
'surface_color' => '#ffffff',
|
|
'login_header_footer_color' => '#838383',
|
|
]);
|
|
|
|
$this->tenant->update(['website_type_code' => $websiteType->codigo]);
|
|
$this->tenant->unsetRelation('websiteType');
|
|
}
|
|
}
|