281 lines
11 KiB
PHP
281 lines
11 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\Mail as MailPage;
|
|
use App\Livewire\EditMailbox;
|
|
use App\Models\Mailbox;
|
|
use App\Models\User;
|
|
use App\Services\Mail\MailPurpose;
|
|
use App\Support\Settings;
|
|
use Livewire\Livewire;
|
|
|
|
beforeEach(function () {
|
|
config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));
|
|
|
|
// Blank slate, not the migration's seeded five: every other file in this
|
|
// directory does the same (see tests/Pest.php). Two of the tests below
|
|
// pin a mailbox to the key 'support' and another to 'no-reply' — both
|
|
// collide with mailboxes.key's unique constraint against the seeded
|
|
// baseline RefreshDatabase leaves in place. Clearing keeps this file
|
|
// deterministic regardless of what the seed migration happens to contain.
|
|
clearMailboxSeed();
|
|
});
|
|
|
|
it('is closed to an operator without mail.manage', function () {
|
|
$user = User::factory()->operator('Read-only')->create();
|
|
|
|
Livewire::actingAs($user)->test(MailPage::class)->assertForbidden();
|
|
});
|
|
|
|
it('opens for an operator who has it', function () {
|
|
$user = User::factory()->operator('Owner')->create();
|
|
|
|
Livewire::actingAs($user)->test(MailPage::class)->assertOk()->assertSet('usable', true);
|
|
});
|
|
|
|
it('grants mail.manage to Admin and Support as well as Owner', function () {
|
|
// The migration hands the capability to three roles, not one. Nothing
|
|
// above proves the other two actually received it — an Owner-only test
|
|
// would stay green even if the migration's role loop silently lost a name.
|
|
expect(User::factory()->operator('Admin')->create()->can('mail.manage'))->toBeTrue()
|
|
->and(User::factory()->operator('Support')->create()->can('mail.manage'))->toBeTrue();
|
|
});
|
|
|
|
it('is not opened by secrets.manage alone — that is the point of splitting them', function () {
|
|
// Billing holds secrets-adjacent capabilities but has no business changing
|
|
// the support sender, and vice versa. If this ever passes, the two
|
|
// capabilities have quietly been merged again.
|
|
$user = User::factory()->operator('Billing')->create();
|
|
|
|
expect($user->can('mail.manage'))->toBeFalse();
|
|
|
|
Livewire::actingAs($user)->test(MailPage::class)->assertForbidden();
|
|
});
|
|
|
|
it('edits a mailbox in a modal, never in the row (R20)', function () {
|
|
$box = Mailbox::factory()->create(['key' => 'support']);
|
|
|
|
$blade = file_get_contents(resource_path('views/livewire/admin/mail.blade.php'));
|
|
|
|
expect($blade)->not->toMatch('/<td[^>]*>(?:(?!<\/td>).)*<input/s')
|
|
->and($blade)->toContain('openModal');
|
|
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(EditMailbox::class, ['uuid' => $box->uuid])
|
|
->set('address', 'neu@clupilot.com')
|
|
->call('save');
|
|
|
|
expect($box->fresh()->address)->toBe('neu@clupilot.com');
|
|
});
|
|
|
|
it('never hands a stored password back to the browser', function () {
|
|
$box = Mailbox::factory()->create(['password' => 'streng-geheim']);
|
|
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(EditMailbox::class, ['uuid' => $box->uuid])
|
|
->assertSet('password', '')
|
|
->assertDontSee('streng-geheim');
|
|
});
|
|
|
|
it('keeps the old password when the field is left empty', function () {
|
|
$box = Mailbox::factory()->create(['password' => 'bleibt']);
|
|
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(EditMailbox::class, ['uuid' => $box->uuid])
|
|
->set('address', 'neu@clupilot.com')
|
|
->call('save');
|
|
|
|
expect($box->fresh()->password)->toBe('bleibt');
|
|
});
|
|
|
|
it('says so when SECRETS_KEY is missing, instead of crashing on the first password', function () {
|
|
config()->set('admin_access.secrets_key', '');
|
|
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(MailPage::class)
|
|
->assertOk()
|
|
->assertSet('usable', false)
|
|
->assertSee(__('mail_settings.no_key'));
|
|
});
|
|
|
|
it('refuses to leave the system purpose empty, because it is the fallback', function () {
|
|
Mailbox::factory()->create(['key' => 'no-reply']);
|
|
Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');
|
|
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(MailPage::class)
|
|
->set('purposes.system', '')
|
|
->call('savePurposes')
|
|
->assertHasErrors('purposes.system');
|
|
|
|
expect(Settings::get(MailPurpose::settingKey(MailPurpose::SYSTEM)))->toBe('no-reply');
|
|
});
|
|
|
|
// --- Coverage beyond the brief's list: behaviours the page and modal claim
|
|
// but the tests above never exercise. Each was verified to fail for the
|
|
// right reason before the implementation existed, and again by mutation
|
|
// afterwards (see the task report).
|
|
|
|
it('lists every mailbox on the page', function () {
|
|
Mailbox::factory()->create(['key' => 'support', 'address' => 'support@clupilot.com']);
|
|
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(MailPage::class)
|
|
->assertSee('support@clupilot.com')
|
|
->assertSee('support');
|
|
});
|
|
|
|
it('shows the currently configured mailbox for each purpose', function () {
|
|
Mailbox::factory()->create(['key' => 'support']);
|
|
Settings::set(MailPurpose::settingKey(MailPurpose::SUPPORT), 'support');
|
|
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(MailPage::class)
|
|
->assertSet('purposes.support', 'support');
|
|
});
|
|
|
|
it('saves the mail server settings', function () {
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(MailPage::class)
|
|
->set('host', 'smtp.example.com')
|
|
->set('port', 2525)
|
|
->set('encryption', 'ssl')
|
|
->call('saveServer')
|
|
->assertHasNoErrors();
|
|
|
|
expect(Settings::get('mail.host'))->toBe('smtp.example.com')
|
|
->and(Settings::get('mail.port'))->toBe(2525)
|
|
->and(Settings::get('mail.encryption'))->toBe('ssl');
|
|
});
|
|
|
|
it('rejects an empty mail server host', function () {
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(MailPage::class)
|
|
->set('host', '')
|
|
->call('saveServer')
|
|
->assertHasErrors('host');
|
|
});
|
|
|
|
it('saves the purpose-to-mailbox mapping', function () {
|
|
Mailbox::factory()->create(['key' => 'support']);
|
|
Mailbox::factory()->create(['key' => 'no-reply']);
|
|
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(MailPage::class)
|
|
->set('purposes.system', 'no-reply')
|
|
->set('purposes.support', 'support')
|
|
->call('savePurposes')
|
|
->assertHasNoErrors();
|
|
|
|
expect(Settings::get(MailPurpose::settingKey(MailPurpose::SUPPORT)))->toBe('support')
|
|
->and(Settings::get(MailPurpose::settingKey(MailPurpose::SYSTEM)))->toBe('no-reply');
|
|
});
|
|
|
|
it('re-checks mail.manage on every page action, not only at mount', function () {
|
|
// mount() alone would pass every test above: an Owner has the capability
|
|
// for the whole test, so nothing so far proves saveServer()/savePurposes()
|
|
// carry their own authorize() rather than relying on mount() having run
|
|
// once. Swap the authenticated user between mount and the action — same
|
|
// shape as SecretsPageTest's "refuses every action while locked", applied
|
|
// to the capability gate instead of the password gate.
|
|
$owner = User::factory()->operator('Owner')->create();
|
|
$billing = User::factory()->operator('Billing')->create();
|
|
|
|
// A fresh mount per action, like SecretsPageTest's equivalent: a 403
|
|
// response carries no usable Livewire snapshot, so a second ->call() on
|
|
// the same already-forbidden instance fails on the snapshot itself
|
|
// rather than on the authorization this test means to check.
|
|
foreach (['saveServer', 'savePurposes'] as $action) {
|
|
$page = Livewire::actingAs($owner)->test(MailPage::class)->assertOk();
|
|
|
|
Livewire::actingAs($billing);
|
|
|
|
$page->call($action)->assertForbidden();
|
|
}
|
|
});
|
|
|
|
it('re-checks mail.manage on modal save, not only at mount', function () {
|
|
// Same gap as above, for the modal: mount() re-reads the record instead of
|
|
// trusting the browser (R20), but that is a different guarantee from
|
|
// save() re-authorising instead of trusting a mount that already happened.
|
|
$box = Mailbox::factory()->create(['key' => 'support']);
|
|
$owner = User::factory()->operator('Owner')->create();
|
|
$billing = User::factory()->operator('Billing')->create();
|
|
|
|
$modal = Livewire::actingAs($owner)->test(EditMailbox::class, ['uuid' => $box->uuid])->assertOk();
|
|
|
|
Livewire::actingAs($billing);
|
|
|
|
$modal->set('address', 'neu@clupilot.com')->call('save')->assertForbidden();
|
|
});
|
|
|
|
it('refuses to edit a mailbox without mail.manage, even though the modal bypasses the page route', function () {
|
|
$box = Mailbox::factory()->create(['key' => 'support']);
|
|
|
|
Livewire::actingAs(User::factory()->operator('Billing')->create())
|
|
->test(EditMailbox::class, ['uuid' => $box->uuid])
|
|
->assertForbidden();
|
|
});
|
|
|
|
it('updates the password when a new one is entered', function () {
|
|
// The mirror of "keeps the old password when the field is left empty":
|
|
// that test alone would stay green even if save() stopped writing the
|
|
// password entirely, since "never changes" also satisfies "kept the old
|
|
// one". This proves the other half — a new value actually lands.
|
|
$box = Mailbox::factory()->create(['password' => 'alt']);
|
|
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(EditMailbox::class, ['uuid' => $box->uuid])
|
|
->set('password', 'neu-und-sicher')
|
|
->call('save');
|
|
|
|
expect($box->fresh()->password)->toBe('neu-und-sicher');
|
|
});
|
|
|
|
it('clears last_verified_at when the password changes, since it no longer proves anything', function () {
|
|
$box = Mailbox::factory()->create(['password' => 'alt', 'last_verified_at' => now()]);
|
|
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(EditMailbox::class, ['uuid' => $box->uuid])
|
|
->set('password', 'neu-und-sicher')
|
|
->call('save');
|
|
|
|
expect($box->fresh()->last_verified_at)->toBeNull();
|
|
});
|
|
|
|
it('saves display name, username, no-reply and active together', function () {
|
|
$box = Mailbox::factory()->create(['no_reply' => false, 'active' => true]);
|
|
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(EditMailbox::class, ['uuid' => $box->uuid])
|
|
->set('displayName', 'Support-Team')
|
|
->set('username', 'smtp-user')
|
|
->set('noReply', true)
|
|
->set('active', false)
|
|
->call('save');
|
|
|
|
$box->refresh();
|
|
|
|
expect($box->display_name)->toBe('Support-Team')
|
|
->and($box->username)->toBe('smtp-user')
|
|
->and($box->no_reply)->toBeTrue()
|
|
->and($box->active)->toBeFalse();
|
|
});
|
|
|
|
it('pre-fills the form from the existing record', function () {
|
|
$box = Mailbox::factory()->create([
|
|
'address' => 'support@clupilot.com',
|
|
'display_name' => 'Support-Team',
|
|
'username' => 'smtp-support',
|
|
'no_reply' => true,
|
|
'active' => false,
|
|
]);
|
|
|
|
Livewire::actingAs(User::factory()->operator('Owner')->create())
|
|
->test(EditMailbox::class, ['uuid' => $box->uuid])
|
|
->assertSet('address', 'support@clupilot.com')
|
|
->assertSet('displayName', 'Support-Team')
|
|
->assertSet('username', 'smtp-support')
|
|
->assertSet('noReply', true)
|
|
->assertSet('active', false);
|
|
});
|