206 lines
8.2 KiB
PHP
206 lines
8.2 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\CustomerDetail;
|
|
use App\Livewire\Admin\MailLog;
|
|
use App\Livewire\Admin\Overview;
|
|
use App\Mail\OperatorMessageMail;
|
|
use App\Models\Customer;
|
|
use App\Models\SentMail;
|
|
use App\Models\SupportRequest;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Livewire\Livewire;
|
|
|
|
/**
|
|
* What left the building, and who wrote it.
|
|
*
|
|
* "Hat der Kunde die Zugangsdaten je bekommen, und wann?" was answered on the
|
|
* mail server until now — a different machine, a different program, and nothing
|
|
* an operator can put in front of somebody who says nothing arrived.
|
|
*/
|
|
it('writes a row for every mail the application sends', function () {
|
|
$customer = Customer::factory()->create();
|
|
|
|
Mail::to($customer->email)->send(new OperatorMessageMail($customer, 'Betreff', 'Text'));
|
|
|
|
$row = SentMail::query()->latest('id')->first();
|
|
|
|
expect($row)->not->toBeNull()
|
|
->and($row->to)->toBe($customer->email)
|
|
->and($row->subject)->toBe('Betreff')
|
|
->and($row->mailable)->toBe(OperatorMessageMail::class)
|
|
// Attached to the customer, so the question "what did THIS customer
|
|
// get" does not mean reading the whole register.
|
|
->and($row->customer_id)->toBe($customer->id);
|
|
});
|
|
|
|
it('records the send, not the intention', function () {
|
|
// On MessageSent, not MessageSending: a row here has to mean the transport
|
|
// accepted it. Recording intentions would fill the register with mails that
|
|
// never left — worse than no register, because an operator would show a
|
|
// customer a line proving something was sent that was not.
|
|
expect(config('mail.default'))->not->toBeNull();
|
|
|
|
$before = SentMail::query()->count();
|
|
Mail::fake();
|
|
|
|
$customer = Customer::factory()->create();
|
|
Mail::to($customer->email)->send(new OperatorMessageMail($customer, 'Betreff', 'Text'));
|
|
|
|
// Mail::fake() never hands the message to a transport, so nothing was sent
|
|
// and nothing is claimed.
|
|
expect(SentMail::query()->count())->toBe($before);
|
|
});
|
|
|
|
it('keeps a mail to an address nobody owns, without inventing a customer for it', function () {
|
|
Mail::to('fremder@example.test')->send(
|
|
new OperatorMessageMail(Customer::factory()->create(), 'Betreff', 'Text')
|
|
);
|
|
|
|
$row = SentMail::query()->where('to', 'fremder@example.test')->first();
|
|
|
|
expect($row)->not->toBeNull()
|
|
->and($row->customer_id)->toBeNull();
|
|
});
|
|
|
|
it('does not let a stranger write rows by asking for a password reset', function () {
|
|
// The verification and reset mails go to addresses nobody has confirmed,
|
|
// and anyone who can type into a form can send them. Recording them would
|
|
// hand the operator's register to whoever wants to fill it.
|
|
$user = App\Models\User::factory()->create();
|
|
|
|
Mail::to($user->email)->send(new App\Mail\ResetPasswordMail($user, 'https://example.test/reset', 60));
|
|
|
|
expect(SentMail::query()->where('to', $user->email)->exists())->toBeFalse();
|
|
});
|
|
|
|
it('shows the register, newest first, with the customer it belongs to', function () {
|
|
$customer = Customer::factory()->create(['name' => 'Kanzlei Berger']);
|
|
|
|
SentMail::create([
|
|
'customer_id' => $customer->id,
|
|
'to' => $customer->email,
|
|
'subject' => 'Ihre Zugangsdaten',
|
|
'mailable' => App\Mail\OperatorMessageMail::class,
|
|
'sent_at' => now()->subHour(),
|
|
]);
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(MailLog::class)
|
|
->assertOk()
|
|
->assertSee('Ihre Zugangsdaten')
|
|
->assertSee('Kanzlei Berger');
|
|
});
|
|
|
|
// ---- Writing to a customer from the console ----
|
|
|
|
it('sends the operator message and records what was written', function () {
|
|
// The body is kept only for a mail a person typed: "what exactly did I
|
|
// write to them in March" is a question the mail server's log cannot
|
|
// answer either.
|
|
$customer = Customer::factory()->create();
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(CustomerDetail::class, ['uuid' => $customer->uuid])
|
|
->set('subject', 'Zur Datenübernahme')
|
|
->set('body', 'Wir haben uns Ihr System angesehen und können übernehmen.')
|
|
->call('send');
|
|
|
|
$row = SentMail::query()->where('customer_id', $customer->id)->latest('id')->first();
|
|
|
|
expect($row)->not->toBeNull()
|
|
->and($row->subject)->toBe('Zur Datenübernahme')
|
|
->and($row->from_operator)->toBeTrue()
|
|
->and($row->body)->toBe('Wir haben uns Ihr System angesehen und können übernehmen.');
|
|
});
|
|
|
|
it('closes the request it answers, which is the reason to answer from here', function () {
|
|
// Answering from a mail client leaves the request open for ever, because
|
|
// nothing ever told it otherwise.
|
|
$customer = Customer::factory()->create();
|
|
$request = SupportRequest::create([
|
|
'customer_id' => $customer->id,
|
|
'subject' => 'Können Sie unsere Daten übernehmen?',
|
|
'category' => 'other',
|
|
'body' => 'Wir haben ownCloud 10 im Haus.',
|
|
'status' => 'open',
|
|
'reported_by' => $customer->email,
|
|
]);
|
|
|
|
$page = Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(CustomerDetail::class, ['uuid' => $customer->uuid])
|
|
->call('answer', $request->uuid);
|
|
|
|
expect($page->get('subject'))->toContain('Können Sie unsere Daten übernehmen?');
|
|
|
|
$page->set('body', 'Ja, das geht — Angebot folgt.')->call('send');
|
|
|
|
expect($request->fresh()->status)->toBe('answered')
|
|
->and($request->fresh()->answered_at)->not->toBeNull();
|
|
});
|
|
|
|
it('shows the customer their own words, so nobody opens the mail client to read the question', function () {
|
|
$customer = Customer::factory()->create();
|
|
SupportRequest::create([
|
|
'customer_id' => $customer->id,
|
|
'subject' => 'Frage zur Migration',
|
|
'category' => 'other',
|
|
'body' => 'Wir haben 42 GB in ownCloud und wollen wechseln.',
|
|
'status' => 'open',
|
|
'reported_by' => $customer->email,
|
|
]);
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(CustomerDetail::class, ['uuid' => $customer->uuid])
|
|
->assertSee('Wir haben 42 GB in ownCloud und wollen wechseln.');
|
|
});
|
|
|
|
it('refuses an empty message rather than sending one', function () {
|
|
$customer = Customer::factory()->create();
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(CustomerDetail::class, ['uuid' => $customer->uuid])
|
|
->call('send')
|
|
->assertHasErrors(['subject', 'body']);
|
|
|
|
expect(SentMail::query()->where('customer_id', $customer->id)->exists())->toBeFalse();
|
|
});
|
|
|
|
it('keeps both pages to operators who may see customers', function () {
|
|
// Support KEEPS both, deliberately: answering customers is the job, and a
|
|
// support operator who cannot read what was already sent them is answering
|
|
// blind. Read-only is the role that may look at the console and change
|
|
// nothing — writing a mail is a change, and so is reading the register of
|
|
// what was written.
|
|
$customer = Customer::factory()->create();
|
|
|
|
expect(operator('Support')->can('customers.manage'))->toBeTrue();
|
|
|
|
Livewire::actingAs(operator('Read-only'), 'operator')
|
|
->test(CustomerDetail::class, ['uuid' => $customer->uuid])
|
|
->assertForbidden();
|
|
|
|
Livewire::actingAs(operator('Read-only'), 'operator')
|
|
->test(MailLog::class)
|
|
->assertForbidden();
|
|
});
|
|
|
|
// ---- The notices somebody can finally act on ----
|
|
|
|
it('gives every notice the page that shows the thing it is about', function () {
|
|
// The count in the header said "4 Hinweis(e)" and led nowhere: whoever read
|
|
// it had to know the list was a card further down the same page, and then
|
|
// go looking for the host or the run by hand.
|
|
App\Models\Host::factory()->create(['status' => 'error', 'name' => 'pve-broken']);
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Overview::class)
|
|
->assertViewHas('notices', fn (array $notices) => collect($notices)
|
|
->every(fn (array $n) => array_key_exists('route', $n) && $n['route'] !== null))
|
|
->assertSee(route('admin.hosts'), false);
|
|
});
|
|
|
|
it('counts notices in words, not in brackets', function () {
|
|
expect(trans_choice('admin.systems_notices', 1, ['n' => 1]))->toBe('1 Hinweis')
|
|
->and(trans_choice('admin.systems_notices', 4, ['n' => 4]))->toBe('4 Hinweise');
|
|
});
|