HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/ai.naniguide.com/tests/Unit/WarmupQuotaServiceTest.php
<?php

namespace Tests\Unit;

use Tests\TestCase;
use Carbon\Carbon;
use Mockery;
use App\Model\SendingServer;
use App\Model\WarmupStrategy;
use App\Model\SendingServerWarmupUsage;
use App\Model\SendingServerWarmupLog;
use App\SendingServers\Drivers\DeliveryStatus;
use App\SendingServers\Drivers\SendResult;
use App\Services\Warmup\WarmupQuotaService;
use App\Services\Warmup\Exceptions\WarmupDailyQuotaExceeded;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Symfony\Component\Mime\Email;

class WarmupQuotaServiceTest extends TestCase
{
    protected WarmupQuotaService $service;

    protected function setUp(): void
    {
        parent::setUp();

        $this->service = app(WarmupQuotaService::class);
    }

    protected function tearDown(): void
    {
        Mockery::close();
        Carbon::setTestNow();

        parent::tearDown();
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    protected function makeStrategy(array $overrides = []): WarmupStrategy
    {
        return WarmupStrategy::create(array_merge([
            'name'                 => 'Test Strategy',
            'description'          => '',
            'status'               => WarmupStrategy::STATUS_ACTIVE,
            'growth_strategy'      => WarmupStrategy::GROWTH_STRATEGY_LINEAR,
            'starting_volume'      => 20,
            'daily_increment'      => 10,
            'quota_type'           => WarmupStrategy::QUOTA_TYPE_DAILY,
            'limit_type'           => WarmupStrategy::LIMIT_TYPE_PER_DAY_CAP,
            'limit_per_day_cap'    => 1000,
            'increment_percentage' => 10,
            'sending_limit'        => 1000,
            'sending_count'        => 30,
        ], $overrides));
    }

    protected function mockServer(WarmupStrategy $strategy, string $startedAt): SendingServer
    {
        $id = DB::table('sending_servers')->insertGetId([
            'uid'          => Str::uuid(),
            'name'         => 'Test Server',
            'type'         => 'smtp',
            'quota_value'  => 60,
            'quota_base'   => 1,
            'quota_unit'   => 'hour',
            'status'       => 'active',
            'created_at'   => now(),
            'updated_at'   => now(),
        ]);

        $server = Mockery::mock(SendingServer::class)->makePartial();
        $server->id = $id;
        $server->warmup_started_at = Carbon::parse($startedAt);
        $server->warmupStrategy = $strategy;
        $server->shouldReceive('isWarmupEnabled')->andReturn(true);
        $server->shouldReceive('loadMissing')->andReturnSelf();

        return $server;
    }

    // -------------------------------------------------------------------------
    // getDayNumber
    // -------------------------------------------------------------------------

    public function test_get_day_number_returns_1_when_started_today(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $server = new SendingServer();
        $server->warmup_started_at = Carbon::parse('2026-03-21 00:00:00');

        $this->assertSame(1, $this->service->getDayNumber($server));
    }

    public function test_get_day_number_returns_correct_day_offset(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $server = new SendingServer();
        $server->warmup_started_at = Carbon::parse('2026-03-18 08:00:00');

        $this->assertSame(4, $this->service->getDayNumber($server));
    }

    // -------------------------------------------------------------------------
    // getDailyQuota
    // -------------------------------------------------------------------------

    public function test_get_daily_quota_returns_max_int_when_warmup_disabled(): void
    {
        $server = Mockery::mock(SendingServer::class)->makePartial();
        $server->shouldReceive('isWarmupEnabled')->once()->andReturn(false);

        $this->assertSame(PHP_INT_MAX, $this->service->getDailyQuota($server));
    }

    public function test_get_daily_quota_returns_max_int_when_no_strategy(): void
    {
        $server = Mockery::mock(SendingServer::class)->makePartial();
        $server->warmupStrategy = null;
        $server->shouldReceive('isWarmupEnabled')->once()->andReturn(true);
        $server->shouldReceive('loadMissing')->once()->andReturnSelf();

        $this->assertSame(PHP_INT_MAX, $this->service->getDailyQuota($server));
    }

    public function test_get_daily_quota_uses_linear_strategy(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $strategy = new WarmupStrategy();
        $strategy->growth_strategy  = WarmupStrategy::GROWTH_STRATEGY_LINEAR;
        $strategy->starting_volume  = 20;
        $strategy->daily_increment  = 10;

        $server = Mockery::mock(SendingServer::class)->makePartial();
        $server->warmup_started_at = Carbon::parse('2026-03-19 00:00:00'); // day 3
        $server->warmupStrategy = $strategy;
        $server->shouldReceive('isWarmupEnabled')->once()->andReturn(true);
        $server->shouldReceive('loadMissing')->once()->andReturnSelf();

        // day 3 => 20 + (3 - 1) * 10 = 40
        $this->assertSame(40, $this->service->getDailyQuota($server));
    }

    public function test_get_daily_quota_uses_exponential_strategy(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $strategy = new WarmupStrategy();
        $strategy->growth_strategy  = WarmupStrategy::GROWTH_STRATEGY_EXPONENTIAL;
        $strategy->starting_volume  = 50;
        $strategy->exponential_factor = 1.2;

        $server = Mockery::mock(SendingServer::class)->makePartial();
        $server->warmup_started_at = Carbon::parse('2026-03-19 00:00:00'); // day 3
        $server->warmupStrategy = $strategy;
        $server->shouldReceive('isWarmupEnabled')->once()->andReturn(true);
        $server->shouldReceive('loadMissing')->once()->andReturnSelf();

        // day 3 => round(50 * (1.2 ^ 2)) = round(72) = 72
        $this->assertSame(72, $this->service->getDailyQuota($server));
    }

    // -------------------------------------------------------------------------
    // remainingQuota
    // -------------------------------------------------------------------------

    public function test_remaining_quota_returns_null_when_warmup_disabled(): void
    {
        $server = Mockery::mock(SendingServer::class)->makePartial();
        $server->shouldReceive('isWarmupEnabled')->once()->andReturn(false);

        $this->assertNull($this->service->remainingQuota($server));
    }

    public function test_remaining_quota_creates_daily_usage_record(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $strategy = $this->makeStrategy(['starting_volume' => 20, 'daily_increment' => 10]);
        $server   = $this->mockServer($strategy, '2026-03-20 00:00:00');

        // day 2 => 20 + (2 - 1) * 10 = 30
        $this->assertSame(30, $this->service->remainingQuota($server));

        $this->assertDatabaseHas('sending_server_warmup_usages', [
            'sending_server_id' => $server->id,
            'usage_date'        => '2026-03-21',
            'day_number'        => 2,
            'daily_quota'       => 30,
            'reserved_count'    => 0,
            'sent_count'        => 0,
        ]);
    }

    public function test_remaining_quota_deducts_existing_reserved_and_sent(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $strategy = $this->makeStrategy(['starting_volume' => 20, 'daily_increment' => 10]);
        $server   = $this->mockServer($strategy, '2026-03-20 00:00:00');

        SendingServerWarmupUsage::create([
            'sending_server_id' => $server->id,
            'usage_date'        => '2026-03-21',
            'day_number'        => 2,
            'daily_quota'       => 30,
            'reserved_count'    => 10,
            'sent_count'        => 5,
            'failed_count'      => 0,
        ]);

        // 30 - 10 (reserved) - 5 (sent) = 15
        $this->assertSame(15, $this->service->remainingQuota($server));
    }

    // -------------------------------------------------------------------------
    // reserveOrFail
    // -------------------------------------------------------------------------

    public function test_reserve_or_fail_creates_usage_increments_reserved_and_creates_log(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $strategy = $this->makeStrategy();
        $server   = $this->mockServer($strategy, '2026-03-21 00:00:00');

        $reservation = $this->service->reserveOrFail($server, [
            'campaign_id'   => 100,
            'subscriber_id' => 200,
            'message_id'    => 'msg-001',
            'email'         => '[email protected]',
        ]);

        $this->assertNotNull($reservation);

        $usage = SendingServerWarmupUsage::where('sending_server_id', $server->id)
            ->where('usage_date', '2026-03-21')
            ->firstOrFail();

        $this->assertSame(1, $usage->reserved_count);
        $this->assertSame(0, $usage->sent_count);
        $this->assertSame(0, $usage->failed_count);

        $this->assertDatabaseHas('sending_server_warmup_logs', [
            'sending_server_id' => $server->id,
            'usage_date'        => '2026-03-21',
            'status'            => 'reserved',
        ]);
    }

    public function test_reserve_or_fail_throws_when_daily_quota_exceeded(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $strategy = $this->makeStrategy([
            'starting_volume'  => 1,
            'daily_increment'  => 0,
            'limit_per_day_cap' => 1,
        ]);
        $server = $this->mockServer($strategy, '2026-03-21 00:00:00');

        SendingServerWarmupUsage::create([
            'sending_server_id' => $server->id,
            'usage_date'        => '2026-03-21',
            'day_number'        => 1,
            'daily_quota'       => 1,
            'reserved_count'    => 1,
            'sent_count'        => 0,
            'failed_count'      => 0,
        ]);

        $this->expectException(WarmupDailyQuotaExceeded::class);

        try {
            $this->service->reserveOrFail($server, []);
        } catch (WarmupDailyQuotaExceeded $e) {
            $this->assertNotNull($e->getRetryAt());
            $this->assertGreaterThanOrEqual(60, $e->getDelayInSeconds());
            throw $e;
        }
    }

    // -------------------------------------------------------------------------
    // markSent
    // -------------------------------------------------------------------------

    public function test_mark_sent_moves_reserved_to_sent_and_updates_log(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $strategy    = $this->makeStrategy();
        $server      = $this->mockServer($strategy, '2026-03-21 00:00:00');
        $reservation = $this->service->reserveOrFail($server, ['message_id' => 'msg-001']);

        $this->service->markSent($reservation, ['provider_message_id' => 'provider-xyz']);

        $usage = SendingServerWarmupUsage::where('sending_server_id', $server->id)
            ->where('usage_date', '2026-03-21')
            ->firstOrFail();

        $log = SendingServerWarmupLog::where('sending_server_id', $server->id)
            ->where('usage_date', '2026-03-21')
            ->firstOrFail();

        $this->assertSame(0, $usage->reserved_count);
        $this->assertSame(1, $usage->sent_count);
        $this->assertSame(0, $usage->failed_count);

        $this->assertSame('sent', $log->status);
        $this->assertEquals('provider-xyz', $log->meta['provider_message_id']);
    }

    // -------------------------------------------------------------------------
    // markFailed
    // -------------------------------------------------------------------------

    public function test_mark_failed_moves_reserved_to_failed_and_updates_log(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $strategy    = $this->makeStrategy();
        $server      = $this->mockServer($strategy, '2026-03-21 00:00:00');
        $reservation = $this->service->reserveOrFail($server, ['message_id' => 'msg-001']);

        $this->service->markFailed($reservation, new \Exception('SMTP timeout'));

        $usage = SendingServerWarmupUsage::where('sending_server_id', $server->id)
            ->where('usage_date', '2026-03-21')
            ->firstOrFail();

        $log = SendingServerWarmupLog::where('sending_server_id', $server->id)
            ->where('usage_date', '2026-03-21')
            ->firstOrFail();

        $this->assertSame(0, $usage->reserved_count);
        $this->assertSame(0, $usage->sent_count);
        $this->assertSame(1, $usage->failed_count);

        $this->assertSame('failed', $log->status);
        $this->assertEquals('SMTP timeout', $log->meta['error']);
    }

    // -------------------------------------------------------------------------
    // send
    // -------------------------------------------------------------------------

    public function test_send_marks_sent_when_server_send_succeeds(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $strategy = $this->makeStrategy();
        $server   = $this->mockServer($strategy, '2026-03-21 00:00:00');
        $message  = (new Email())->from('[email protected]')->to('[email protected]')->subject('s')->text('b');
        $expected = new SendResult(runtimeMessageId: 'rt-001', status: DeliveryStatus::SENT);
        $server->shouldReceive('send')->once()->with($message)->andReturn($expected);

        $result = $this->service->send($server, $message, ['message_id' => 'msg-001']);

        $this->assertSame($expected, $result);
    }

    public function test_send_marks_failed_and_rethrows_when_server_send_fails(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $strategy = $this->makeStrategy();
        $server   = $this->mockServer($strategy, '2026-03-21 00:00:00');
        $message  = (new Email())->from('[email protected]')->to('[email protected]')->subject('s')->text('b');
        $server->shouldReceive('send')->once()->with($message)->andThrow(new \Exception('SMTP failed'));

        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('SMTP failed');

        $this->service->send($server, $message, ['message_id' => 'msg-001']);
    }

    public function test_quota_usage_and_logs_are_correctly_recorded_over_multiple_sends(): void
    {
        Carbon::setTestNow('2026-03-21 10:00:00');

        $strategy = $this->makeStrategy(['starting_volume' => 2, 'daily_increment' => 1]);
        $server   = $this->mockServer($strategy, '2026-03-21 00:00:00');
        // Send 1st message (day 1 quota = 2)
        $reservation1 = $this->service->reserveOrFail($server, ['message_id' => 'msg-001']);
        $this->service->markSent($reservation1);

        // Send 2nd message (day 1 quota = 2)
        $reservation2 = $this->service->reserveOrFail($server, ['message_id' => 'msg-002']);
        $this->service->markSent($reservation2);

        // Advance to day 2
        Carbon::setTestNow('2026-03-22 10:00:00');

        // Send 3rd message (day 2 quota = 3)
        $reservation3 = $this->service->reserveOrFail($server, ['message_id' => 'msg-003']);
        $this->service->markSent($reservation3);

        $this->assertEquals([
            '2026-03-21' => ['day_number' => 1, 'daily_quota' => 2, 'reserved_count' => 0, 'sent_count' => 2, 'failed_count' => 0],
            '2026-03-22' => ['day_number' => 2, 'daily_quota' => 3, 'reserved_count' => 0, 'sent_count' => 1, 'failed_count' => 0],
        ], SendingServerWarmupUsage::where('sending_server_id', $server->id)
            ->orderBy('usage_date')
            ->get()
            ->keyBy(fn ($u) => $u->usage_date->format('Y-m-d'))
            ->map(fn ($u) => [
                'day_number'     => $u->day_number,
                'daily_quota'    => $u->daily_quota,
                'reserved_count' => $u->reserved_count,
                'sent_count'     => $u->sent_count,
                'failed_count'   => $u->failed_count,
            ])
            ->all());

        $this->assertEquals([
            'msg-001' => ['usage_date' => '2026-03-21', 'status' => 'sent'],
            'msg-002' => ['usage_date' => '2026-03-21', 'status' => 'sent'],
            'msg-003' => ['usage_date' => '2026-03-22', 'status' => 'sent'],
        ], SendingServerWarmupLog::where('sending_server_id', $server->id)
            ->orderBy('message_id')
            ->get()
            ->keyBy('message_id')
            ->map(fn ($l) => [
                'usage_date' => $l->usage_date->format('Y-m-d'),
                'status'     => $l->status,
            ])
            ->all());
    }
}