mailService = Mockery::mock(MailService::class); $this->service = new NotificationMailService( $this->mailService, Mockery::mock(TicketPdfService::class), ); } public function test_it_logs_and_swallows_a_missing_purchase(): void { $this->createEmptyPurchasesTable(); Log::shouldReceive('channel')->once()->with('emails')->andReturnSelf(); Log::shouldReceive('warning')->once()->with( 'Notification email skipped.', [ 'purchase_id' => 123, 'reason' => 'purchase_not_found', 'missing_model' => Purchase::class, 'email_type' => 'purchase_paid', ], ); $this->service->sendPurchasePaid(123); } public function test_missing_purchase_log_includes_the_requested_ticket_ids(): void { $this->createEmptyPurchasesTable(); Log::shouldReceive('channel')->once()->with('emails')->andReturnSelf(); Log::shouldReceive('warning')->once()->with( 'Notification email skipped.', [ 'purchase_id' => 123, 'requested_ticket_count' => 2, 'requested_ticket_ids' => [10, 11], 'reason' => 'purchase_not_found', 'missing_model' => Purchase::class, 'email_type' => 'tickets_available', ], ); $this->service->sendTicketsAvailable(123, [10, 11]); } public function test_it_logs_successful_delivery_with_the_mailer(): void { $this->mailService->shouldReceive('mailerName')->once()->andReturn('smtp'); Log::shouldReceive('channel')->once()->with('emails')->andReturnSelf(); Log::shouldReceive('info')->once()->with( 'Notification email sent.', [ 'user_id' => 10, 'tenant_code' => 'tenant-test', 'email_type' => 'welcome', 'mailer' => 'smtp', ], ); $this->sendLogged( 'welcome', ['user_id' => 10], fn (): array => ['tenant_code' => 'tenant-test'], ); } public function test_it_logs_and_rethrows_delivery_failures(): void { $exception = new RuntimeException('SMTP unavailable.'); Log::shouldReceive('channel')->once()->with('emails')->andReturnSelf(); Log::shouldReceive('error')->once()->with( 'Notification email delivery failed.', Mockery::on(fn (array $context): bool => $context['purchase_id'] === 123 && $context['email_type'] === 'purchase_paid' && $context['exception'] === $exception), ); $this->expectExceptionObject($exception); $this->sendLogged('purchase_paid', ['purchase_id' => 123], function () use ($exception): array { throw $exception; }); } /** @param array $context */ private function sendLogged(string $emailType, array $context, callable $send): void { (new ReflectionMethod($this->service, 'sendLogged'))->invoke( $this->service, $emailType, $context, $send, ); } private function createEmptyPurchasesTable(): void { config([ 'database.default' => 'sqlite', 'database.connections.sqlite.database' => ':memory:', ]); Schema::connection('sqlite')->create('compras', function (Blueprint $table): void { $table->id(); }); } }