fromStripeEvent([ 'id' => 'evt_'.uniqid(), 'email' => 'kunde@example.com', 'name' => 'Beispiel GmbH', 'stripe_customer_id' => null, 'plan' => 'start', 'datacenter' => 'fsn', 'amount_cents' => 1900, 'currency' => 'EUR', ]); Mail::assertQueued(OrderConfirmationMail::class, function (OrderConfirmationMail $mail) { return $mail->hasTo('kunde@example.com'); }); }); it('issues an invoice and sends it, instead of a bare confirmation, once it can', function () { // ONE mail, not two. The invoice is issued the moment the money lands, so // an order confirmation and an invoice mail would otherwise go out in the // same second for the same event. Mail::fake(); Queue::fake(); App\Support\CompanyProfile::put([ 'name' => 'CluPilot Cloud e.U.', 'address' => 'Dreherstraße 66/1/8', 'postcode' => '1110', 'city' => 'Wien', 'vat_id' => 'ATU00000000', ]); app(StartCustomerProvisioning::class)->fromStripeEvent([ 'id' => 'evt_'.uniqid(), 'email' => 'kunde@example.com', 'name' => 'Beispiel GmbH', 'stripe_customer_id' => null, 'plan' => 'start', 'datacenter' => 'fsn', 'amount_cents' => 1900, 'currency' => 'EUR', ]); expect(App\Models\Invoice::query()->count())->toBe(1); Mail::assertQueued(App\Mail\InvoiceMail::class); Mail::assertNotQueued(OrderConfirmationMail::class); }); it('does not confirm the same order twice when Stripe delivers twice', function () { // Stripe retries until it gets a 2xx, so duplicate deliveries are ordinary // rather than exceptional. A second confirmation for one payment reads as a // second charge. Mail::fake(); Queue::fake(); $event = [ 'id' => 'evt_duplicate', 'email' => 'kunde@example.com', 'name' => 'Beispiel GmbH', 'stripe_customer_id' => null, 'plan' => 'start', 'datacenter' => 'fsn', 'amount_cents' => 1900, 'currency' => 'EUR', ]; app(StartCustomerProvisioning::class)->fromStripeEvent($event); app(StartCustomerProvisioning::class)->fromStripeEvent($event); Mail::assertQueuedCount(1); }); it('renders the order confirmation without a template error, and with the money in it', function () { // Rendering is the only way to find a Blade mistake in a mail: nothing else // ever opens these files, and the first person who would is a customer. $customer = Customer::factory()->create(['name' => 'Beispiel GmbH']); $order = Order::factory()->create([ 'customer_id' => $customer->id, 'amount_cents' => 1900, 'currency' => 'EUR', 'datacenter' => 'fsn', ]); $rendered = (new OrderConfirmationMail($order, 'Beispiel GmbH'))->render(); expect($rendered)->toContain('19,00 EUR') ->and($rendered)->toContain(__('orders.mail_heading')) // The footer every mail carries, from the shared layout. ->and($rendered)->toContain(__('mail.why_you_got_this')); }); it('keeps every mail template on the shared layout', function () { // Laravel's markdown component brings its own logo, button and footer. Two // templates were still wearing it, and a new one written in a hurry is one // copy-paste away from doing the same — at which point the product sends // mail that looks like two different companies. $offenders = []; foreach (File::allFiles(resource_path('views/mail')) as $file) { $contents = File::get($file->getPathname()); if (str_contains($contents, 'x-mail::message') || str_contains($contents, 'x-mail::')) { $offenders[] = $file->getFilename(); } } expect($offenders)->toBe([]); }); it('never puts a credential in the mail that announces a finished cloud', function () { // The initial admin password used to travel there in clear text, across // third-party mail servers and then into an inbox for ever. It waits behind // the sign-in now, and this is what stops it drifting back. // // Asserted against the data the mail is actually given, not against the // template's source. A source scan trips over the word in a comment and // passes a template that leaks the value through a variable — it tests the // prose, not the mail. $secret = 'Str3ng-Geheim-'.uniqid(); $customer = Customer::factory()->create(); $instance = App\Models\Instance::factory()->create([ 'customer_id' => $customer->id, 'admin_password' => $secret, 'subdomain' => 'beispiel', ]); $user = App\Models\User::factory()->create(['name' => 'Beispiel GmbH']); $message = (new App\Notifications\CloudReady($instance))->toMail($user); expect(json_encode($message->viewData))->not->toContain($secret); // And the same for what is rendered from it, so a template that reaches // past its data for the model cannot slip through either. expect(view($message->view, $message->viewData)->render())->not->toContain($secret); });