156 lines
5.7 KiB
PHP
156 lines
5.7 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 when no invoice can be issued', function () {
|
|
// The company details are not filled in here, so IssueInvoice refuses —
|
|
// correctly, because an invoice without a registered name or VAT number is
|
|
// not a valid invoice. The purchase still stands and the customer is still
|
|
// told.
|
|
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('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);
|
|
});
|