File: /home/xedaptot/ai.naniguide.com/tests/Unit/SendingServers/DriverSendTest.php
<?php
use App\Model\SendingServer;
use App\SendingServers\Drivers\Vendors\Generic\SmtpDriver;
use App\SendingServers\Drivers\Vendors\Mailgun\MailgunSmtpDriver;
use App\SendingServers\Drivers\Vendors\SendGrid\SendGridSmtpDriver;
use App\SendingServers\Drivers\Vendors\SparkPost\SparkPostSmtpDriver;
use Tests\TestCase;
uses(TestCase::class);
/**
* Driver send() smoke tests — verify each driver's send() method:
* 1. Reads config keys correctly (no undefined attribute errors).
* 2. Builds Symfony Mailer transport / vendor SDK request without exception
* until the actual network call.
* 3. Returns a SendResult DTO (typed getters: getRuntimeMessageId / getStatus).
*
* For each variant: assert NO undefined-method / undefined-property error
* occurs before transport->start() / vendor SDK call.
*/
function buildTestMessage(string $msgId = 'test-msg-001'): \Symfony\Component\Mime\Email
{
$email = new \Symfony\Component\Mime\Email();
$email->subject('Test');
$email->text('Body');
$email->from('[email protected]');
$email->to('[email protected]');
$email->getHeaders()->addTextHeader('X-Acelle-Message-Id', $msgId);
return $email;
}
test('SmtpDriver send() reads config without undefined-attribute errors', function () {
$server = new SendingServer(['type' => 'smtp', 'name' => 'fixture']);
$server->setConfigMany([
'host' => 'localhost', // unreachable port → expected SMTP error
'smtp_port' => '1',
'smtp_protocol' => 'tls',
'smtp_username' => 'u',
'smtp_password' => 'p',
]);
$driver = new SmtpDriver($server);
try {
$driver->send(buildTestMessage());
} catch (\Throwable $e) {
// Accept network/transport failures. Reject undefined-method/property errors.
expect($e)->not->toBeInstanceOf(\BadMethodCallException::class);
expect($e->getMessage())->not->toContain('undefined method');
expect($e->getMessage())->not->toContain('Undefined property');
}
});
test('SendGridSmtpDriver send() builds transport from config + tags message with Smtpapi header', function () {
$server = new SendingServer(['type' => 'sendgrid-smtp', 'name' => 'fixture']);
$server->setConfigMany([
'host' => 'localhost',
'smtp_port' => '1',
'smtp_protocol' => 'tls',
'smtp_username' => 'u',
'smtp_password' => 'p',
'api_key' => 'sg-test',
]);
$driver = new SendGridSmtpDriver($server);
$message = buildTestMessage('sg-msg-001');
try {
$driver->send($message);
} catch (\Throwable $e) {
expect($e)->not->toBeInstanceOf(\BadMethodCallException::class);
}
// Verify the Smtpapi header was added to the message before transport call.
expect($message->getHeaders()->has(\Smtpapi\Header::NAME))->toBeTrue();
});
test('MailgunSmtpDriver send() builds transport from config (no msgId tagging — legacy parity)', function () {
$server = new SendingServer(['type' => 'mailgun-smtp', 'name' => 'fixture']);
$server->setConfigMany([
'host' => 'localhost',
'smtp_port' => '1',
'smtp_protocol' => 'tls',
'smtp_username' => 'u',
'smtp_password' => 'p',
'api_key' => 'mg-test',
'aws_region' => 'us',
]);
$driver = new MailgunSmtpDriver($server);
try {
$driver->send(buildTestMessage());
} catch (\Throwable $e) {
expect($e)->not->toBeInstanceOf(\BadMethodCallException::class);
}
});
test('SparkPostSmtpDriver send() tags message with X-MSYS-API metadata header', function () {
$server = new SendingServer(['type' => 'sparkpost-smtp', 'name' => 'fixture']);
$server->setConfigMany([
'host' => 'localhost',
'smtp_port' => '1',
'smtp_protocol' => 'tls',
'smtp_username' => 'u',
'smtp_password' => 'p',
'api_key' => 'sp-test',
]);
$driver = new SparkPostSmtpDriver($server);
$message = buildTestMessage('sp-msg-001');
try {
$driver->send($message);
} catch (\Throwable $e) {
expect($e)->not->toBeInstanceOf(\BadMethodCallException::class);
}
// X-MSYS-API header must contain the runtime_message_id metadata.
$header = $message->getHeaders()->get('X-MSYS-API');
expect($header)->not->toBeNull();
$value = $header->getBodyAsString();
$decoded = json_decode($value, true);
expect($decoded['metadata']['runtime_message_id'])->toBe('sp-msg-001');
});
test('SendmailDriver send() returns failure result when path missing (defensive test)', function () {
$server = new SendingServer(['type' => 'sendmail', 'name' => 'fixture']);
$server->setConfig('sendmail_path', '/path/does/not/exist/sendmail');
$driver = new \App\SendingServers\Drivers\Vendors\Generic\SendmailDriver($server);
expect(fn () => $driver->send(buildTestMessage()))
->toThrow(\RuntimeException::class);
});
test('All drivers implement SendingDriver interface (no missing required method)', function () {
foreach (\App\SendingServers\DriverRegistry::all() as $type => $class) {
$reflection = new ReflectionClass($class);
expect($reflection->isInstantiable())->toBeTrue();
expect($reflection->implementsInterface(\App\SendingServers\Drivers\SendingDriver::class))->toBeTrue();
// Required interface methods all defined (not abstract).
foreach (['send', 'test', 'getServiceName', 'setupBeforeSend', 'getMessageId'] as $method) {
$m = $reflection->getMethod($method);
expect($m->isAbstract())->toBeFalse();
}
}
});
// ─── Regression: DKIM-signed message must remain typed as Email ──────────────
//
// Bug history: an older Acelle version had dkim_sign() return the DkimSigner
// output directly — that's typed Mime\Message (Email's parent). It then arrived
// at driver->send(Email) and raised TypeError deep in the worker pipeline.
//
// These tests pin the contract: dkim_sign + the SendingServer::send shim both
// preserve the Email type. PHP's own type-system enforces them at runtime; the
// tests assert the declared signatures match the contract.
test('dkim_sign() helper return type is Symfony\\Mime\\Email (parent regression guard)', function () {
$reflection = new ReflectionFunction('App\\Helpers\\dkim_sign');
expect((string) $reflection->getReturnType())->toBe('Symfony\\Component\\Mime\\Email');
});
test('SendingServer::send shim accepts Email and rejects bare Mime\\Message (parent)', function () {
$reflection = new ReflectionMethod(\App\Model\SendingServer::class, 'send');
$param = $reflection->getParameters()[0];
expect((string) $param->getType())->toBe('Symfony\\Component\\Mime\\Email');
});
test('Every driver send() parameter is Email — not the broader Mime\\Message parent', function () {
foreach (\App\SendingServers\DriverRegistry::all() as $type => $class) {
$reflection = new ReflectionMethod($class, 'send');
$param = $reflection->getParameters()[0];
$declared = (string) $param->getType();
expect($declared)
->toBe('Symfony\\Component\\Mime\\Email', "Driver {$type} send() parameter must be Email, got {$declared}");
}
});
test('dkim_sign() preserves Email type when caller passes one in', function () {
// Use a throwaway RSA keypair so we don't need a real Acelle domain row.
$res = openssl_pkey_new(['private_key_bits' => 1024, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
openssl_pkey_export($res, $privateKey);
$domain = new \App\Library\Domain('example.com', 'sel1', '', $privateKey);
$email = new \Symfony\Component\Mime\Email();
$email->from('[email protected]')->to('[email protected]')
->subject('regression')->text('body')
->date(new \DateTimeImmutable())
->getHeaders()->addIdHeader('Message-ID', '[email protected]');
$result = \App\Helpers\dkim_sign($email, $domain);
expect($result)->toBeInstanceOf(\Symfony\Component\Mime\Email::class);
// And the DKIM header DID get attached (sanity: helper is doing work).
expect($result->getHeaders()->has('DKIM-Signature'))->toBeTrue();
});