CluPilotCloud/tests/Feature/Mail/MailboxTesterTest.php

234 lines
9.9 KiB
PHP

<?php
use App\Models\Mailbox;
use App\Services\Mail\MailboxTester;
use App\Support\Settings;
use Tests\Support\FakeSmtpServer;
beforeEach(function () {
config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));
config()->set('mail.default', 'log'); // as on a development machine
Settings::set('mail.host', '127.0.0.1');
Settings::set('mail.port', 1);
// 'none', not 'tls': FakeSmtpServer only ever speaks plain text (see its
// own docblock) and never advertises STARTTLS. Since the P1 fix makes
// 'tls' REQUIRE a STARTTLS upgrade, keeping this default at 'tls' would
// have broken every success/rejection test below that needs the fake
// server to complete a full plaintext dialogue — none of which are
// actually about the TLS policy. The tests that ARE about it set their
// own encryption explicitly, below.
Settings::set('mail.encryption', 'none');
});
/*
| The brief points this beforeEach at 'mail.example.invalid' and asserts the
| error mentions it. Measured by hand in this container (php artisan tinker
| equivalent, three runs each): a lookup for 'mail.example.invalid' fails in
| 0.7-7ms via NXDOMAIN, but that number depends on a resolver being reachable
| at all — nothing here guarantees that in every environment this suite might
| run in. 127.0.0.1:1 (nothing listens on port 1) refused every one of those
| three runs in ~0.2ms with zero DNS involved, and it is the convention this
| codebase already settled on for the identical reason — see
| MailboxTransportTest's "LOAD-BEARING for the MAIL_MAILER=log guard" comment.
| Using it here keeps both tests fast AND deterministic instead of merely fast
| today.
*/
it('does NOT quietly succeed through the log mailer', function () {
// The whole point: on dev the default mailer is the log, and a tester
// that honoured it would report success while writing to a file — a
// test that proves nothing. The log "transport" never fails, so only a
// real attempt (which a closed port refuses) can make this assertion
// meaningful.
$result = app(MailboxTester::class)->run(
Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']),
'ziel@example.com',
);
expect($result['ok'])->toBeFalse()
->and($result['error'])->not->toBeNull();
});
it('passes the SMTP error through verbatim, because that is the whole diagnosis', function () {
$result = app(MailboxTester::class)->run(
Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']),
'ziel@example.com',
);
// Not "Fehler beim Senden" — the operating system's own sentence, naming
// the actual host:port and the actual reason.
expect($result['error'])->toContain('127.0.0.1:1')
->and($result['error'])->toContain('Connection refused');
});
it('refuses a mailbox without a password rather than pretending to try', function () {
$box = Mailbox::factory()->create(['password' => null]);
$result = app(MailboxTester::class)->run($box, 'ziel@example.com');
expect($result['ok'])->toBeFalse()
->and($result['error'])->toContain('Passwort');
});
it('stamps last_verified_at only on success', function () {
$box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']);
app(MailboxTester::class)->run($box, 'ziel@example.com');
expect($box->fresh()->last_verified_at)->toBeNull();
});
it('gives a readable message instead of throwing when SECRETS_KEY is unusable', function () {
// Facts the brief could not know: SECRETS_KEY is not set on this
// installation at all (the mail settings page's own banner says so).
// $box->password decrypts on read, so touching it throws a
// RuntimeException out of SecretCipher — Task 7's EditMailbox::save()
// shipped a Critical fix for exactly this shape of bug. The tester
// touches the very same cipher and needs the very same guard.
$box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']);
config()->set('admin_access.secrets_key', '');
$result = app(MailboxTester::class)->run($box, 'ziel@example.com');
expect($result['ok'])->toBeFalse()
->and($result['error'])->toBe(__('mail_settings.no_key'));
});
/*
| LOAD-BEARING for the success path. Every test above only proves a FAILURE
| is reported correctly — none of them ever reaches
| `$box->forceFill(['last_verified_at' => now()])->save()`, so all of them
| would stay green even if that line were deleted outright. A real success
| needs something real to answer on the other end of the socket; see
| FakeSmtpServer for why that has to be a subprocess rather than an in-process
| fake.
*/
it('actually delivers and stamps last_verified_at when the server accepts the message', function () {
$server = FakeSmtpServer::succeeding();
try {
Settings::set('mail.host', '127.0.0.1');
Settings::set('mail.port', $server->port);
$box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']);
$result = app(MailboxTester::class)->run($box, 'ziel@example.com');
expect($result['ok'])->toBeTrue()
->and($result['error'])->toBeNull()
->and($box->fresh()->last_verified_at)->not->toBeNull();
} finally {
$server->stop();
}
});
it('passes a real SMTP rejection through verbatim too, not only a refused connection', function () {
// The strongest version of "the server's own sentence": an actual
// protocol-level 5xx from an actual SMTP dialogue, not merely the
// operating system's "connection refused". Proves MailboxTester's catch
// block does not paraphrase or replace what Symfony Mailer reports.
$server = FakeSmtpServer::rejectingAt('rcpt', '550 5.1.1 No such user ziel@example.com');
try {
Settings::set('mail.host', '127.0.0.1');
Settings::set('mail.port', $server->port);
$box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']);
$result = app(MailboxTester::class)->run($box, 'ziel@example.com');
expect($result['ok'])->toBeFalse()
->and($result['error'])->toContain('550 5.1.1 No such user ziel@example.com')
->and($box->fresh()->last_verified_at)->toBeNull();
} finally {
$server->stop();
}
});
it('actually attempts TLS when SSL is configured on a non-standard port, instead of silently going out in the clear', function () {
// The exact trap this task names: Laravel 13's MailManager reads
// 'scheme', not 'encryption' (MailManager.php:196). An 'encryption' key
// is silently ignored, and MailManager falls back to choosing the scheme
// from the port number alone — smtps only for port 465, smtp otherwise.
// Configured for SSL on a non-465 port (a real EU hoster's submission
// port with implicit TLS, say), that bug would connect in PLAIN TEXT
// while the console still reported TLS.
//
// FakeSmtpServer only ever speaks plain text, so it can tell the two
// apart: a correct 'scheme' => 'smtps' opens an implicit-TLS connection
// this server cannot answer and the send FAILS; the buggy 'encryption'
// key would default to plain 'smtp' on this non-465 port and let it
// succeed instead. Verified by hand this fails in ~0.2s, not a hang —
// OpenSSL rejects the server's plain-text greeting as an invalid TLS
// record immediately.
$server = FakeSmtpServer::succeeding();
try {
Settings::set('mail.host', '127.0.0.1');
Settings::set('mail.port', $server->port); // deliberately not 465
Settings::set('mail.encryption', 'ssl');
$box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']);
$result = app(MailboxTester::class)->run($box, 'ziel@example.com');
expect($result['ok'])->toBeFalse();
} finally {
$server->stop();
}
});
/*
| P1: the test-send button must enforce the exact same TLS policy real
| sending does (MailboxTransportTest carries the identical pair of tests for
| the real path) — otherwise a green test-send proves nothing about what a
| queued mail would actually do. Before the fix, MailboxTester built its
| transport with the default autoTls/requireTls Symfony ships (autoTls on,
| requireTls off): against a server that never offers STARTTLS, autoTls found
| nothing to upgrade to, nothing else objected, and the send completed in the
| clear while reporting success.
*/
it('fails, rather than sending in the clear, when TLS is required but the server never offers STARTTLS', function () {
$server = FakeSmtpServer::succeeding();
try {
Settings::set('mail.host', '127.0.0.1');
Settings::set('mail.port', $server->port);
Settings::set('mail.encryption', 'tls');
$box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']);
$result = app(MailboxTester::class)->run($box, 'ziel@example.com');
expect($result['ok'])->toBeFalse()
->and($result['error'])->toContain('TLS required but neither TLS or STARTTLS are in use.')
->and($box->fresh()->last_verified_at)->toBeNull();
} finally {
$server->stop();
}
});
it('still delivers over a plain connection when encryption is none, against a server that never offers TLS', function () {
// The control case for the test above: requireTls must stay OFF for
// 'none', or a legitimately plaintext-only relay would break the moment
// the P1 fix landed.
$server = FakeSmtpServer::succeeding();
try {
Settings::set('mail.host', '127.0.0.1');
Settings::set('mail.port', $server->port);
Settings::set('mail.encryption', 'none');
$box = Mailbox::factory()->create(['address' => 'no-reply@clupilot.com']);
$result = app(MailboxTester::class)->run($box, 'ziel@example.com');
expect($result['ok'])->toBeTrue()
->and($result['error'])->toBeNull()
->and($box->fresh()->last_verified_at)->not->toBeNull();
} finally {
$server->stop();
}
});