'sqlite', 'database.connections.sqlite.database' => ':memory:', 'cache.default' => 'array', 'cache.stores.redis' => ['driver' => 'array'], 'app.key' => 'base64:' . base64_encode(str_repeat('a', 32)), 'queue.default' => 'sync']); DB::purge('sqlite'); Cache::forgetDriver(['redis', 'array']); $this->app->forgetInstance(\App\Support\Setting::class); Schema::create('v2_settings', fn (Blueprint $t) => [$t->id(), $t->string('name'), $t->text('value')->nullable(), $t->timestamps()]); Schema::create('v2_user', fn (Blueprint $t) => [$t->id(), $t->string('email'), $t->boolean('banned')->default(false), $t->integer('created_at')->nullable(), $t->integer('updated_at')->nullable()]); (require base_path('database/migrations/2026_10_09_170000_create_mail_flow_tables.php'))->up(); Cache::flush(); config(['mailflow.enabled' => true, 'mailflow.resend_enabled' => true, 'mailflow.resend_key' => 'test-key', 'mailflow.resend_from' => 'sender@example.com', 'mailflow.reply_to' => 'support@example.com', 'mailflow.daily_limit' => 100, 'mailflow.monthly_limit' => 3000]); Http::preventStrayRequests(); } private function usage(int $daily = 0, int $monthly = 0): array { return ['emails' => [ 'daily' => ['used' => $daily, 'limit' => 100, 'resets_at' => now()->utc()->addDay()->startOfDay()->toIso8601String()], 'monthly' => ['used' => $monthly, 'limit' => 3000, 'resets_at' => now()->utc()->addMonth()->startOfMonth()->toIso8601String()], ]]; } private function conventional(int $times = 1, string $status = 'submitted'): void { $transport = Mockery::mock(ConventionalTransport::class); $transport->shouldReceive('send')->times($times)->andReturn(['status' => $status, 'provider_id' => 'smtp-message']); $this->app->instance(ConventionalTransport::class, $transport); } private function send(string $key = 'test-key', string $purpose = 'verification', array $extra = []): array { return app(Dispatcher::class)->send($extra + ['message_key' => $key, 'purpose' => $purpose, 'email' => 'user@example.com'], 'Test', '

123456

'); } public function test_verification_uses_resend_and_duplicate_job_sends_only_once(): void { Http::fake(['*/usage' => Http::response($this->usage()), '*/emails' => Http::response(['id' => 'resend-1'])]); $this->conventional(0); $this->assertSame('submitted', $this->send()['status']); $this->assertSame('submitted', $this->send()['status']); Http::assertSentCount(2); Http::assertSent(fn ($r) => str_ends_with($r->url(), '/emails') && $r->hasHeader('Idempotency-Key', 'dpl-mail/test-key') && $r['reply_to'] === 'support@example.com'); $this->assertSame(1, MailQuotaBucket::where('period', 'daily')->first()->used); $this->assertNull(MailMessage::where('direction', 'out')->first()->payload); } public function test_ticket_reply_uses_resend_but_broadcast_and_reminders_do_not(): void { Http::fake(['*/usage' => Http::response($this->usage()), '*/emails' => Http::response(['id' => 'ticket-email'])]); $this->conventional(3); $this->assertSame('resend', $this->send('ticket', 'ticket_reply')['channel']); foreach (['broadcast', 'traffic_reminder', 'expiry_reminder'] as $purpose) { $this->assertSame('conventional', $this->send($purpose, $purpose)['channel']); } Http::assertSentCount(2); } public function test_daily_or_monthly_exhaustion_routes_to_conventional(): void { foreach ([[100, 0], [0, 3000]] as $index => [$day, $month]) { Cache::forget('mailflow:resend:usage'); Http::fake(['*/usage' => Http::response($this->usage($day, $month))]); $this->conventional(); $this->assertSame('conventional', $this->send('exhausted-' . $index)['channel']); } $this->assertSame('quota_exhausted', MailMessage::where('message_key', 'exhausted-1')->first()->fallback_reason); Http::assertNotSent(fn ($r) => str_ends_with($r->url(), '/emails')); } public function test_inflight_reservation_prevents_overbooking_last_quota_slot(): void { Http::fake(['*/usage' => Http::response($this->usage(99))]); $first = MailMessage::create(['message_key' => 'reserve-1', 'purpose' => 'verification', 'email' => 'one@example.com', 'subject' => 'Test']); $second = MailMessage::create(['message_key' => 'reserve-2', 'purpose' => 'verification', 'email' => 'two@example.com', 'subject' => 'Test']); $this->assertTrue(app(QuotaService::class)->reserve($first)); $this->assertFalse(app(QuotaService::class)->reserve($second)); $this->assertSame(1, MailQuotaBucket::where('period', 'daily')->first()->reserved); } public function test_explicit_quota_rejection_falls_back_and_releases_reservation(): void { Http::fake(['*/usage' => Http::response($this->usage()), '*/emails' => Http::response(['name' => 'daily_quota_exceeded'], 429)]); $this->conventional(); $this->assertSame('submitted', $this->send()['status']); $message = MailMessage::first(); $this->assertSame('daily_quota_exceeded', $message->fallback_reason); $this->assertNull($message->reservation); $this->assertSame(0, MailQuotaBucket::sum('reserved')); } public function test_timeout_remains_unknown_and_retries_same_key_on_resend_only(): void { Http::fake(['*/usage' => Http::response($this->usage()), '*/emails' => Http::failedConnection()]); $this->conventional(0); $first = $this->send(); $this->assertSame('unknown', $first['status']); $this->assertTrue($first['retryable']); $this->assertSame(1, MailQuotaBucket::where('period', 'daily')->first()->reserved); Http::swap(new \Illuminate\Http\Client\Factory()); Http::preventStrayRequests(); Http::fake(['*/emails' => Http::response(['id' => 'recovered'])]); $this->assertSame('submitted', $this->send()['status']); $this->assertSame(1, MailQuotaBucket::where('period', 'daily')->first()->used); $this->assertSame(0, MailQuotaBucket::sum('reserved')); } public function test_unknown_smtp_submission_is_never_repeated(): void { $transport = Mockery::mock(ConventionalTransport::class); $transport->shouldReceive('send')->once()->andThrow(new \RuntimeException('timeout')); $this->app->instance(ConventionalTransport::class, $transport); $this->assertSame('unknown', $this->send('smtp-unknown', 'broadcast')['status']); $this->assertSame('unknown', $this->send('smtp-unknown', 'broadcast')['status']); Http::assertNothingSent(); } public function test_usage_unavailable_uses_conventional_before_submitting_to_resend(): void { Http::fake(['*/usage' => Http::response([], 503)]); $this->conventional(); $this->assertSame('conventional', $this->send()['channel']); $this->assertSame('quota_unavailable', MailMessage::first()->fallback_reason); } public function test_invalid_recipient_rejection_does_not_switch_provider(): void { Http::fake(['*/usage' => Http::response($this->usage()), '*/emails' => Http::response(['name' => 'validation_error'], 422)]); $this->conventional(0); $this->assertSame('failed', $this->send()['status']); $this->assertSame(0, MailQuotaBucket::sum('reserved')); } public function test_expired_verification_and_old_unknown_messages_are_not_resent(): void { $this->conventional(0); $this->assertSame('expired', $this->send('expired', 'verification', ['expires_at' => now()->subMinute()])['status']); MailMessage::create(['message_key' => 'old', 'purpose' => 'verification', 'email' => 'user@example.com', 'subject' => 'Test', 'channel' => 'resend', 'status' => 'unknown', 'first_attempt_at' => now()->subDay(), 'payload' => ['html' => 'secret']]); $this->assertSame('unknown', $this->send('old')['status']); Http::assertNothingSent(); } public function test_log_transport_is_recorded_as_simulated(): void { $this->conventional(1, 'simulated'); $this->assertSame('simulated', $this->send('log', 'broadcast')['status']); $this->assertNull(MailMessage::first()->submitted_at); } public function test_webhook_signature_rejects_tampering_stale_and_missing_headers(): void { $body = '{"type":"email.delivered"}'; $id = 'event-1'; $time = (string) time(); $key = str_repeat('b', 32); $secret = 'whsec_' . base64_encode($key); $signature = 'v1,' . base64_encode(hash_hmac('sha256', "$id.$time.$body", $key, true)); $verifier = new WebhookSignature(); $this->assertTrue($verifier->valid($body, $id, $time, $signature, $secret)); $this->assertFalse($verifier->valid($body . ' ', $id, $time, $signature, $secret)); $this->assertFalse($verifier->valid($body, $id, (string) (time() - 600), $signature, $secret)); $this->assertFalse($verifier->valid($body, '', $time, $signature, $secret)); } public function test_delivery_callbacks_deduplicate_and_do_not_downgrade_delivery(): void { MailMessage::create(['message_key' => 'delivered', 'purpose' => 'verification', 'email' => 'user@example.com', 'subject' => 'Test', 'channel' => 'resend', 'status' => 'submitted', 'provider_id' => 'email-id']); $events = app(DeliveryEvents::class); $events->receive('event-delivered', ['type' => 'email.delivered', 'created_at' => now()->toIso8601String(), 'data' => ['email_id' => 'email-id']]); $events->receive('event-delivered', ['type' => 'email.delivered', 'data' => ['email_id' => 'email-id']]); $events->receive('event-sent', ['type' => 'email.sent', 'created_at' => now()->subMinute()->toIso8601String(), 'data' => ['email_id' => 'email-id']]); $this->assertSame('delivered', MailMessage::where('direction', 'out')->first()->status); $this->assertSame(2, MailMessage::where('direction', 'event')->count()); } public function test_callback_recovers_lost_post_response_using_persisted_tag(): void { Http::fake(['*/usage' => Http::response($this->usage()), '*/emails' => Http::failedConnection()]); $this->conventional(0); $this->send('lost-response'); $events = app(DeliveryEvents::class); $events->receive('recover-event', ['type' => 'email.delivered', 'data' => ['email_id' => 'lost-id']]); Http::fake(['*/emails/lost-id' => Http::response(['tags' => [['name' => 'mail_key', 'value' => 'lost-response']]])]); $events->apply(MailMessage::where('direction', 'event')->first(), true); $this->assertSame('delivered', MailMessage::where('direction', 'out')->first()->status); $this->assertSame(0, MailQuotaBucket::sum('reserved')); } public function test_official_status_lookup_confirms_delivery_without_fabricating_event_time(): void { $message = MailMessage::create(['message_key' => 'lookup-delivery', 'purpose' => 'verification', 'email' => 'user@example.com', 'subject' => 'Test', 'channel' => 'resend', 'status' => 'submitted', 'provider_id' => 'lookup-id']); Http::fake(['*/emails/lookup-id' => Http::response(['id' => 'lookup-id', 'last_event' => 'delivered'])]); app(DeliveryEvents::class)->refreshStatus($message); $this->assertSame('delivered', $message->refresh()->status); $this->assertNull($message->delivered_at); $this->assertNull($message->provider_event_at); Http::fake(['*/emails/lookup-id' => Http::response(['id' => 'lookup-id', 'last_event' => 'sent'])]); app(DeliveryEvents::class)->refreshStatus($message); $this->assertSame('delivered', $message->refresh()->status); } public function test_official_status_lookup_failure_keeps_submission_and_rejects_wrong_identity(): void { $message = MailMessage::create(['message_key' => 'lookup-failure', 'purpose' => 'verification', 'email' => 'user@example.com', 'subject' => 'Test', 'channel' => 'resend', 'status' => 'submitted', 'provider_id' => 'lookup-id']); foreach ([Http::response([], 429), Http::response(['id' => 'wrong-id', 'last_event' => 'delivered'])] as $response) { Http::fake(['*/emails/lookup-id' => $response]); try { app(DeliveryEvents::class)->refreshStatus($message); $this->fail('Expected unavailable or mismatched status to be rejected'); } catch (\RuntimeException $e) { $this->assertSame('submitted', $message->refresh()->status); } } } public function test_auto_reply_uses_conventional_and_deduplicates_incoming_parent(): void { config(['mailflow.auto_reply_enabled' => true]); User::withoutEvents(fn () => User::create(['email' => 'user@example.com'])); $incoming = MailMessage::create(['message_key' => 'incoming', 'direction' => 'in', 'purpose' => 'incoming', 'email' => 'user@example.com', 'subject' => 'Help', 'status' => 'received']); $service = app(AutoReplyService::class); $reply = $service->prepare($incoming, ['message-id' => '']); $this->assertNotNull($reply); $this->assertSame('auto_reply', $reply->purpose); $this->assertNull($service->prepare($incoming, [])); $this->conventional(); (new \App\Jobs\SendMailMessageJob($reply->id))->handle(app(Dispatcher::class)); Http::assertNothingSent(); $this->assertSame('conventional', $reply->refresh()->channel); } public function test_auto_reply_skips_automated_bulk_and_own_messages(): void { config(['mailflow.auto_reply_enabled' => true]); User::withoutEvents(fn () => User::create(['email' => 'user@example.com'])); $incoming = MailMessage::create(['message_key' => 'incoming', 'direction' => 'in', 'purpose' => 'incoming', 'email' => 'user@example.com', 'subject' => 'Help']); foreach ([['auto-submitted' => 'auto-replied'], ['return-path' => '<>'], ['list-id' => 'list'], ['precedence' => 'bulk']] as $headers) { $this->assertNull(app(AutoReplyService::class)->prepare($incoming, $headers)); } $incoming->email = 'support@example.com'; $this->assertNull(app(AutoReplyService::class)->prepare($incoming, [])); } public function test_unbound_sender_receives_only_one_reply_per_beijing_calendar_day(): void { config(['mailflow.auto_reply_enabled' => true]); $this->travelTo(\Carbon\Carbon::parse('2026-10-10 23:59:00', 'Asia/Shanghai')); $service = app(AutoReplyService::class); for ($i = 0; $i < 100; $i++) { $incoming = MailMessage::create(['message_key' => 'daily-' . $i, 'direction' => 'in', 'purpose' => 'incoming', 'email' => 'stranger@example.com', 'subject' => 'Help']); $reply = $service->prepare($incoming, []); if ($i === 0) { $this->assertNotNull($reply); $this->assertNull($reply->user_id); $this->assertSame('2026-10-11 00:00:00', $reply->expires_at->timezone('Asia/Shanghai')->format('Y-m-d H:i:s')); } else { $this->assertNull($reply); } } $this->assertSame(1, MailMessage::where('purpose', 'auto_reply')->count()); $this->travelTo(\Carbon\Carbon::parse('2026-10-11 00:01:00', 'Asia/Shanghai')); $this->assertNull($service->prepare(MailMessage::where('message_key', 'daily-0')->firstOrFail(), [])); $next = MailMessage::create(['message_key' => 'next-day', 'direction' => 'in', 'purpose' => 'incoming', 'email' => 'stranger@example.com', 'subject' => 'Help']); $this->assertNotNull($service->prepare($next, [])); $this->assertSame(2, MailMessage::where('purpose', 'auto_reply')->count()); } public function test_daily_report_snapshots_exact_complete_period_and_submits_only_once(): void { $this->travelTo(\Carbon\Carbon::parse('2026-10-09 08:00:00', 'Asia/Shanghai')); config(['mailflow.master_url' => 'https://master.example.com', 'mailflow.master_bot_code' => 'bot', 'mailflow.master_chat_code' => 'chat']); MailMessage::create(['message_key' => 'previous', 'purpose' => 'verification', 'email' => 'user@example.com', 'subject' => 'Test', 'status' => 'delivered', 'channel' => 'resend', 'created_at' => now()->subMinute()]); MailMessage::create(['message_key' => 'next', 'purpose' => 'verification', 'email' => 'user@example.com', 'subject' => 'Test', 'created_at' => now()]); $service = app(DailyReportService::class); $report = $service->snapshot(now()->utc()->startOfDay()); $this->assertSame(['out/resend/delivered' => 1], $report->summary['counts']); Http::fake(['*/api/telegram/post' => Http::response(['isDone' => true])]); $service->send($report); $service->send($report); $this->assertSame('submitted', $report->refresh()->status); Http::assertSentCount(1); } public function test_master_timeout_is_unknown_and_not_automatically_repeated(): void { config(['mailflow.master_url' => 'https://master.example.com', 'mailflow.master_bot_code' => 'bot', 'mailflow.master_chat_code' => 'chat']); Http::fake(['*/api/telegram/post' => Http::failedConnection()]); $service = app(DailyReportService::class); $report = $service->snapshot(now()->utc()->startOfDay()); $service->send($report); $service->send($report); $this->assertSame('unknown', $report->refresh()->status); $this->assertSame(1, $report->attempts); } public function test_payload_is_encrypted_and_public_settings_hide_secrets(): void { $message = MailMessage::create(['message_key' => 'encrypted', 'purpose' => 'verification', 'email' => 'user@example.com', 'subject' => 'Test', 'payload' => ['html' => '123456-secret']]); $raw = DB::table('v2_mail_messages')->where('id', $message->id)->value('payload'); $this->assertStringNotContainsString('123456-secret', $raw); $this->assertArrayNotHasKey('payload', $message->toArray()); $settings = Settings::publicSettings(); $this->assertArrayNotHasKey('resend_key', $settings); $this->assertTrue($settings['resend_key_configured']); } public function test_real_conventional_array_transport_preserves_reply_thread_headers(): void { config(['mail.driver' => 'array', 'mail.from.address' => 'sender@example.com', 'mail.from.name' => 'Test']); $result = app(ConventionalTransport::class)->send(['to' => 'user@example.com', 'subject' => 'Test', 'html' => '

Test

', 'reply_to' => 'support@example.com', 'headers' => ['In-Reply-To' => '', 'References' => '', 'Auto-Submitted' => 'auto-replied']]); $this->assertSame('simulated', $result['status']); $this->assertNotEmpty($result['provider_id']); } public function test_correlated_permanent_bounce_updates_only_matching_smtp_message(): void { $message = MailMessage::create(['message_key' => 'smtp', 'purpose' => 'broadcast', 'email' => 'user@example.com', 'subject' => 'Test', 'channel' => 'conventional', 'provider_id' => 'original@example.com', 'status' => 'submitted']); $content = app(\App\Services\MailFlow\IncomingContent::class); $dsn = "Action: failed\r\nFinal-Recipient: rfc822; user@example.com\r\nOriginal-Message-ID: \r\n"; $content->recordBounce(str_replace('user@example.com', 'other@example.com', $dsn), []); $this->assertSame('submitted', $message->refresh()->status); $content->recordBounce($dsn, []); $this->assertSame('bounced', $message->refresh()->status); } public function test_incoming_plain_text_decodes_transfer_encoding(): void { $content = app(\App\Services\MailFlow\IncomingContent::class); $this->assertSame('你好', $content->decode(base64_encode('你好'), 3, [(object) ['attribute' => 'charset', 'value' => 'UTF-8']])); $this->assertSame('hello world', $content->decode('hello=20world', 4, [])); } public function test_disabling_feature_pauses_persisted_jobs(): void { $message = MailMessage::create(['message_key' => 'paused', 'purpose' => 'auto_reply', 'email' => 'user@example.com', 'subject' => 'Test', 'payload' => ['html' => 'Test']]); config(['mailflow.enabled' => false]); $this->conventional(0); (new \App\Jobs\SendMailMessageJob($message->id))->handle(app(Dispatcher::class)); $this->assertSame('pending', $message->refresh()->status); Http::assertNothingSent(); } public function test_settings_validation_enforces_free_caps_and_valid_folder(): void { $rules = (new \App\Http\Requests\Admin\ConfigSave())->rules(); foreach ([['daily_limit' => 101], ['monthly_limit' => 3001], ['imap_folder' => 'INBOX}bad']] as $values) { $this->assertTrue(validator(['mail_flow' => $values], $rules)->fails()); } $this->assertFalse(validator(['mail_flow' => ['daily_limit' => 100, 'monthly_limit' => 3000, 'imap_folder' => 'INBOX']], $rules)->fails()); } public function test_webhook_controller_verifies_raw_body_and_rejects_missing_signature(): void { $controller = new \App\Http\Controllers\V1\Guest\ResendController(); $key = str_repeat('c', 32); config(['mailflow.resend_webhook_secret' => 'whsec_' . base64_encode($key)]); $body = json_encode(['type' => 'email.delivered', 'data' => ['email_id' => 'provider-id']]); $request = \Illuminate\Http\Request::create('/webhook', 'POST', [], [], [], [], $body); $time = (string) time(); $request->headers->set('svix-id', 'test-id'); $request->headers->set('svix-timestamp', $time); $request->headers->set('svix-signature', 'v1,' . base64_encode(hash_hmac('sha256', "test-id.$time.$body", $key, true))); $response = $controller->webhook($request, app(WebhookSignature::class), app(DeliveryEvents::class)); $this->assertSame(200, $response->getStatusCode()); $this->assertSame(1, MailMessage::where('direction', 'event')->count()); $request->headers->remove('svix-signature'); $this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class); $controller->webhook($request, app(WebhookSignature::class), app(DeliveryEvents::class)); } public function test_mail_service_preserves_template_and_returns_no_credentials_even_if_log_insert_fails(): void { Schema::create('v2_mail_templates', function (Blueprint $t) { $t->id(); $t->string('name'); $t->string('subject'); $t->text('content'); }); $this->conventional(); // Deliberately no v2_mail_log table: the accepted message must not be retried because of logging. $result = \App\Services\MailService::sendEmail(['email' => 'user@example.com', 'subject' => 'Test', 'template_name' => 'notify', 'purpose' => 'broadcast', 'message_key' => 'service-test', 'template_value' => ['name' => 'Test', 'url' => 'https://example.com', 'content' => 'Hello']]); $this->assertSame('submitted', $result['status']); $this->assertFalse($result['retryable']); $this->assertArrayNotHasKey('config', $result); } public function test_migration_can_roll_back_only_new_tables(): void { (require base_path('database/migrations/2026_10_09_170000_create_mail_flow_tables.php'))->down(); $this->assertFalse(Schema::hasTable('v2_mail_messages')); $this->assertTrue(Schema::hasTable('v2_user')); $this->assertTrue(Schema::hasTable('v2_settings')); } public function test_admin_secret_save_is_encrypted_and_blank_preserves_existing_secret(): void { $request = Mockery::mock(\App\Http\Requests\Admin\ConfigSave::class); $request->shouldReceive('validated')->once()->andReturn(['mail_flow' => ['resend_key' => 'private-test-key', 'resend_enabled' => false]]); (new \App\Http\Controllers\V2\Admin\ConfigController())->save($request); $raw = DB::table('v2_settings')->where('name', 'mail_flow')->value('value'); $this->assertStringNotContainsString('private-test-key', $raw); $this->assertSame('private-test-key', Settings::get('resend_key')); $request = Mockery::mock(\App\Http\Requests\Admin\ConfigSave::class); $request->shouldReceive('validated')->once()->andReturn(['mail_flow' => ['resend_key' => '']]); (new \App\Http\Controllers\V2\Admin\ConfigController())->save($request); $this->assertSame('private-test-key', Settings::get('resend_key')); $this->assertArrayNotHasKey('resend_key', Settings::publicSettings()); } public function test_registered_routes_and_scheduler_include_mail_flow_entrypoints(): void { $router = app('router'); $this->assertNotNull($router->getRoutes()->match(\Illuminate\Http\Request::create('/api/v1/guest/resend/webhook', 'POST'))); $plugins = Mockery::mock(\App\Services\Plugin\PluginManager::class); $plugins->shouldReceive('registerPluginSchedules')->once(); $this->app->instance(\App\Services\Plugin\PluginManager::class, $plugins); $schedule = app(\Illuminate\Console\Scheduling\Schedule::class); $report = collect($schedule->events())->first(fn ($event) => str_contains($event->command ?? '', 'mail:report-usage') && $event->expression === '0 8 * * *'); $this->assertNotNull($report); $this->assertSame('Asia/Shanghai', $report->timezone); } public function test_explicit_smtp_rejection_is_failure_not_unknown(): void { $transport = Mockery::mock(ConventionalTransport::class); $transport->shouldReceive('send')->once()->andThrow(new \Symfony\Component\Mailer\Exception\UnexpectedResponseException('Recipient rejected', 550)); $this->app->instance(ConventionalTransport::class, $transport); $result = $this->send('smtp-rejected', 'broadcast'); $this->assertSame('failed', $result['status']); $this->assertFalse($result['retryable']); } public function test_temporary_smtp_rejection_retries_without_switching_to_resend(): void { $transport = Mockery::mock(ConventionalTransport::class); $transport->shouldReceive('send')->once()->andThrow(new \Symfony\Component\Mailer\Exception\UnexpectedResponseException('Temporary rejection', 451)); $this->app->instance(ConventionalTransport::class, $transport); $result = $this->send('smtp-temporary', 'broadcast'); $this->assertSame('pending', $result['status']); $this->assertTrue($result['retryable']); Http::assertNothingSent(); } public function test_report_counts_submission_time_instead_of_queue_creation_time(): void { $this->travelTo(\Carbon\Carbon::parse('2026-10-09 08:00:00', 'Asia/Shanghai')); MailMessage::create(['message_key' => 'queued-earlier', 'purpose' => 'broadcast', 'email' => 'user@example.com', 'subject' => 'Test', 'channel' => 'conventional', 'status' => 'submitted', 'created_at' => now()->subDays(3), 'submitted_at' => now()->subMinute()]); $report = app(DailyReportService::class)->snapshot(now()->utc()->startOfDay()); $this->assertSame(['out/conventional/submitted' => 1], $report->summary['counts']); } public function test_resend_uses_site_display_name_and_preserves_inline_logo(): void { admin_setting(['app_name' => 'Test Site']); Http::fake(['*/usage' => Http::response($this->usage()), '*/emails' => Http::response(['id' => 'brand-email'])]); $attachment = ['filename' => 'logo.png', 'content' => 'encoded-image', 'content_id' => 'site-logo@dpl']; $this->send('brand', 'verification', ['attachments' => [$attachment]]); Http::assertSent(fn ($r) => str_ends_with($r->url(), '/emails') && $r['from'] === '"Test Site" ' && $r['attachments'] === [$attachment]); } public function test_footer_homepage_does_not_replace_login_action_link(): void { $html = view('mail.default.mailLogin', ['name' => 'Test Site', 'url' => 'https://actions.example', 'link' => 'https://actions.example/login?token=example', 'site_url' => 'http://dpl.loc', 'logo_src' => 'cid:site-logo@dpl'])->render(); $this->assertStringContainsString('href="http://dpl.loc"', $html); $this->assertStringContainsString('href="https://actions.example/login?token=example"', $html); $this->assertStringContainsString('src="cid:site-logo@dpl"', $html); } }