'smtp.test.local', 'smtp_port' => 587, 'smtp_user' => 'user', 'smtp_password' => 'pass', 'smtp_from_email' => 'editor@example.org', 'smtp_from_name' => 'Editor', 'smtp_require_tls' => false, 'imap_host' => 'imap.test.local', 'imap_port' => 993, 'imap_user' => 'user', 'imap_password' => 'pass', 'imap_sent_folder' => 'Sent', 'imap_mailbox_flags' => '/imap/ssl', ]; } } final class FakeSmtp implements SmtpSender { public int $count = 0; public function send(array $credentials, array $to, array $cc, array $bcc, string $subject, string $htmlBody, ?string $replyTo = null): array { $this->count++; if ($to === []) { throw new RuntimeException('No recipients'); } return ['raw_mime' => "To: " . implode(',', $to) . "\r\nSubject: {$subject}\r\n\r\n{$htmlBody}"]; } } final class FakeImap implements ImapAppender { public int $count = 0; public function appendSent(array $credentials, string $rawMime, string $attemptId): void { $this->count++; } } Env::load(dirname(__DIR__, 2) . '/credentials/.env'); $dbConfig = [ 'MYSQL_HOST' => '127.0.0.1', 'MYSQL_PORT' => (string) (getenv('MYSQL_TUNNEL_LOCAL_PORT') ?: '13306'), 'MYSQL_USER' => Env::require('REMOTE_MYSQL_USER'), 'MYSQL_PASSWORD' => Env::require('REMOTE_MYSQL_PASSWORD'), 'MAILSHOTS_REMOTE_MYSQL_DB' => Env::require('MAILSHOTS_REMOTE_MYSQL_DB'), 'MEMBERS_REMOTE_MYSQL_DB' => Env::require('MEMBERS_REMOTE_MYSQL_DB'), ]; $router = new PdoDatabaseRouter($dbConfig); $metadata = new DatabaseSourceMetadataProvider($router); $parser = new DslParser(); $validator = new DslValidator($metadata); $compiler = new DslCompiler($metadata); $queryRepo = new MailshotQueryRepository($router); $mailshotRepo = new MailshotRepository($router); $dataSourceService = new DataSourceService($queryRepo, $router, $parser, $validator, $compiler, $metadata, $mailshotRepo); $lastRunRepo = new LastRunRepository($router); $smtp = new FakeSmtp(); $imap = new FakeImap(); $run = new MailshotRunService( $mailshotRepo, $queryRepo, $dataSourceService, new TemplateRenderer(), $smtp, $imap, new FakeCreds(), $lastRunRepo ); $membersDb = $router->membersDbName(); $stmt = $router->membersPdo()->prepare('SELECT table_name FROM information_schema.tables WHERE table_schema = :schema ORDER BY table_name ASC LIMIT 1'); $stmt->execute(['schema' => $membersDb]); $table = $stmt->fetchColumn(); if (!is_string($table) || $table === '') { fwrite(STDERR, "No source table in members db\n"); exit(1); } $dsl = $membersDb . '.' . $table; $uniq = gmdate('Ymd_His') . '_' . bin2hex(random_bytes(3)); $queryName = 'phase4_ds_' . $uniq; $queryId = null; $mailshotId = null; try { $savedQ = $dataSourceService->save(null, $queryName, $dsl); if (($savedQ['ok'] ?? false) !== true) { fwrite(STDERR, 'Failed saving datasource: ' . json_encode($savedQ) . "\n"); exit(1); } $queryId = (int) $savedQ['id']; $savedM = $mailshotRepo->create([ 'Purpose' => 'Phase4 ' . $uniq, 'DataSource' => $queryName, 'CC' => '', 'BCC' => '', 'Subject' => 'Hello {{ id|default(ID|default("recipient")) }}', 'Message' => '
Hi {{ name|default(Name|default("there")) }}
', 'PDFAttachment' => 'PDF {{ id|default("none") }}
', 'AttachmentNames' => '[]', 'PDFFilenameDerivedFrom' => '', 'ReplyTo' => '', ]); $mailshotId = (int) $savedM; $render = $run->renderTest($mailshotId, 0); if (($render['ok'] ?? false) !== true) { fwrite(STDERR, 'renderTest failed: ' . json_encode($render) . "\n"); exit(1); } $sendBlank = $run->sendTest($mailshotId, 0, ''); if (($sendBlank['ok'] ?? true) !== false) { fwrite(STDERR, "sendTest blank email should fail\n"); exit(1); } $sendTest = $run->sendTest($mailshotId, 0, 'receiver@example.org'); if (($sendTest['ok'] ?? false) !== true) { fwrite(STDERR, 'sendTest failed: ' . json_encode($sendTest) . "\n"); exit(1); } $runRes = $run->runMailshot($mailshotId); if (($runRes['ok'] ?? false) !== true) { fwrite(STDERR, 'runMailshot failed: ' . json_encode($runRes) . "\n"); exit(1); } if ((int) ($runRes['attempted'] ?? 0) <= 0) { fwrite(STDERR, "runMailshot attempted should be > 0\n"); exit(1); } $rows = $lastRunRepo->listForMailshot($mailshotId); if (count($rows) !== (int) $runRes['attempted']) { fwrite(STDERR, 'lastRun row count mismatch\n'); exit(1); } $retry = $run->retryFailed($mailshotId); if (($retry['ok'] ?? false) !== true) { fwrite(STDERR, 'retryFailed failed: ' . json_encode($retry) . "\n"); exit(1); } echo "Phase 4 run-flow integration test passed\n"; echo "attempted=" . (int) $runRes['attempted'] . "\n"; echo "smtp_calls={$smtp->count}\n"; echo "imap_calls={$imap->count}\n"; } finally { if ($mailshotId !== null) { try { $lastRunRepo->clearForMailshot($mailshotId); } catch (Throwable $e) {} try { $mailshotRepo->delete($mailshotId); } catch (Throwable $e) {} } if ($queryId !== null) { try { $queryRepo->delete($queryId); } catch (Throwable $e) {} } }