CluPilotCloud/tests/Feature/MailTemplatesTest.php

126 lines
4.6 KiB
PHP

<?php
use App\Actions\StartCustomerProvisioning;
use App\Mail\OrderConfirmationMail;
use App\Models\Customer;
use App\Models\Order;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
/**
* The mails this product sends, and the one layout they all wear.
*/
it('sends an order confirmation the moment the payment lands', function () {
// Not when provisioning finishes. Those are minutes apart at best, and
// somebody who has just been charged and heard nothing assumes the worst
// about both the charge and the product.
Mail::fake();
Queue::fake();
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',
]);
Mail::assertQueued(OrderConfirmationMail::class, function (OrderConfirmationMail $mail) {
return $mail->hasTo('kunde@example.com');
});
});
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);
});