From 5dba5ee58db4aa63999ea39a70c6f41c0f814039 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 12:07:55 +0600 Subject: [PATCH 01/18] composer.json updated --- composer.json | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index cd1487e..c83b93f 100644 --- a/composer.json +++ b/composer.json @@ -1,11 +1,11 @@ { - "name": "doppar/guard", - "description": "A authorization package for doppar framework", + "name": "doppar/queue", + "description": "A lightweight queue management library for the Doppar framework.", "type": "library", "license": "MIT", "support": { - "issues": "https://github.com/doppar/guard/issues", - "source": "https://github.com/doppar/guard" + "issues": "https://github.com/doppar/queue/issues", + "source": "https://github.com/doppar/queue" }, "authors": [ { @@ -15,16 +15,17 @@ ], "require-dev": { "mockery/mockery": "^1.6", - "phpunit/phpunit": "^12.1.5" + "phpunit/phpunit": "^12.1.5", + "doppar/framework": "^3.0.0" }, "autoload": { "psr-4": { - "Doppar\\Authorizer\\": "src/" + "Doppar\\Queue\\": "src/" } }, "autoload-dev": { "psr-4": { - "Doppar\\Authorizer\\Tests\\": "tests/" + "Doppar\\Queue\\Tests\\": "tests/" } }, "extra": { @@ -33,13 +34,13 @@ }, "doppar": { "providers": [ - "Doppar\\Authorizer\\GuardServiceProvider" + "Doppar\\Queue\\QueueServiceProvider" ] } }, "config": { "sort-packages": true }, - "minimum-stability": "dev", + "minimum-stability": "stable", "require": {} } From 465795e25b646ae52f7799636dfceef92d0744e7 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 12:14:30 +0600 Subject: [PATCH 02/18] queue test environment setup done --- composer.json | 2 +- tests/Mock/MockContainer | 24 ++++ tests/Unit/AuthorizationTest.php | 205 ------------------------------- tests/Unit/QueueSystemTest.php | 112 +++++++++++++++++ 4 files changed, 137 insertions(+), 206 deletions(-) create mode 100644 tests/Mock/MockContainer delete mode 100644 tests/Unit/AuthorizationTest.php create mode 100644 tests/Unit/QueueSystemTest.php diff --git a/composer.json b/composer.json index c83b93f..0588b86 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,6 @@ "config": { "sort-packages": true }, - "minimum-stability": "stable", + "minimum-stability": "dev", "require": {} } diff --git a/tests/Mock/MockContainer b/tests/Mock/MockContainer new file mode 100644 index 0000000..856e4cf --- /dev/null +++ b/tests/Mock/MockContainer @@ -0,0 +1,24 @@ +authorizer = new Authorizer(); - } - - public function testPolicyRegistrationAndResolution() - { - $policy = new class { - public function edit($user, $model) - { - return $user->id === $model->owner_id; - } - }; - - $model = new class { - public $owner_id = 1; - }; - - $user = new class { - public $id = 1; - }; - - $this->authorizer->authorize(get_class($model), get_class($policy)); - $this->assertSame([get_class($model) => get_class($policy)], $this->authorizer->policies()); - } - - public function testAbilityDefinitionAndChecking() - { - $this->authorizer->define('edit-settings', function ($user) { - return $user->isAdmin; - }); - - $adminUser = new class { - public $isAdmin = true; - }; - $regularUser = new class { - public $isAdmin = false; - }; - - $this->authorizer->resolveUserUsing(fn() => $adminUser); - $this->assertTrue($this->authorizer->allows('edit-settings')); - - $this->authorizer->resolveUserUsing(fn() => $regularUser); - $this->assertFalse($this->authorizer->allows('edit-settings')); - $this->assertTrue($this->authorizer->denies('edit-settings')); - } - - public function testTemporaryAbilities() - { - $called = false; - $this->authorizer->temporary('temp-ability', function () use (&$called) { - $called = true; - return true; - }); - - $this->assertTrue($this->authorizer->allows('temp-ability')); - $this->assertTrue($called); - - // Should be removed after first check - $this->assertFalse($this->authorizer->hasAbility('temp-ability')); - } - - public function testAbilityHierarchy() - { - $this->authorizer->define('admin', fn($user) => $user->isAdmin); - $this->authorizer->inherit('admin', ['manage-users', 'manage-settings']); - - $adminUser = new class { - public $isAdmin = true; - }; - $this->authorizer->resolveUserUsing(fn() => $adminUser); - - $this->assertTrue($this->authorizer->allows('manage-users')); - $this->assertTrue($this->authorizer->allows('manage-settings')); - $this->assertSame(['manage-users', 'manage-settings'], $this->authorizer->getChildren('admin')); - } - - public function testAbilityGroups() - { - $this->authorizer->group('content', ['create-post', 'edit-post', 'delete-post']); - $this->assertTrue($this->authorizer->inGroup('content', 'edit-post')); - $this->assertFalse($this->authorizer->inGroup('content', 'manage-users')); - } - - public function testBeforeAndAfterCallbacks() - { - $beforeCalled = false; - $afterCalled = false; - - $this->authorizer->before(function ($user, $ability) use (&$beforeCalled) { - $beforeCalled = true; - return $ability === 'bypass' ? true : null; - }); - - $this->authorizer->after(function ($user, $ability, $result) use (&$afterCalled) { - $afterCalled = true; - }); - - // Before callback should allow this - $this->assertTrue($this->authorizer->allows('bypass')); - $this->assertTrue($beforeCalled); - $this->assertTrue($afterCalled); - - // Reset flags - $beforeCalled = false; - $afterCalled = false; - - // Test with regular ability - $this->authorizer->define('test', fn() => true); - $this->assertTrue($this->authorizer->allows('test')); - $this->assertTrue($beforeCalled); - $this->assertTrue($afterCalled); - } - - public function testPolicyAuthorization() - { - $policy = new class { - public function update($user, $model) - { - return $user->id === $model->owner_id; - } - }; - - $model = new class { - public $owner_id = 1; - }; - - $user = new class { - public $id = 1; - }; - $otherUser = new class { - public $id = 2; - }; - - $this->authorizer->authorize(get_class($model), get_class($policy)); - - $this->authorizer->resolveUserUsing(fn() => $user); - $this->assertTrue($this->authorizer->allows('update', $model)); - - $this->authorizer->resolveUserUsing(fn() => $otherUser); - $this->assertFalse($this->authorizer->allows('update', $model)); - } - - public function testAnyAndAllMethods() - { - $this->authorizer->define('ability1', fn() => true); - $this->authorizer->define('ability2', fn() => false); - $this->authorizer->define('ability3', fn() => true); - - $this->assertTrue($this->authorizer->any(['ability1', 'ability2'])); - $this->assertFalse($this->authorizer->any(['ability2', 'nonexistent'])); - - $this->assertTrue($this->authorizer->all(['ability1', 'ability3'])); - $this->assertFalse($this->authorizer->all(['ability1', 'ability2'])); - } - - public function testHasAbilityAndGetAllAbilities() - { - $this->authorizer->define('defined', fn() => true); - $this->authorizer->temporary('temp', fn() => true); - $this->authorizer->inherit('parent', ['child']); - - $this->assertTrue($this->authorizer->hasAbility('defined')); - $this->assertTrue($this->authorizer->hasAbility('temp')); - $this->assertTrue($this->authorizer->hasAbility('parent')); - $this->assertFalse($this->authorizer->hasAbility('nonexistent')); - - $allAbilities = $this->authorizer->getAllAbilities(); - $this->assertContains('defined', $allAbilities); - $this->assertContains('temp', $allAbilities); - $this->assertContains('parent', $allAbilities); - } - - public function testClearMethod() - { - $this->authorizer->define('test', fn() => true); - $this->authorizer->authorize('Model', 'Policy'); - - $this->assertNotEmpty($this->authorizer->abilities()); - $this->assertNotEmpty($this->authorizer->policies()); - - $this->authorizer->clear(); - - $this->assertEmpty($this->authorizer->abilities()); - $this->assertEmpty($this->authorizer->policies()); - } - - public function testUserResolution() - { - $user = new class {}; - $this->authorizer->resolveUserUsing(fn() => $user); - $this->assertSame($user, $this->authorizer->resolveUser()); - } -} diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php new file mode 100644 index 0000000..45fd941 --- /dev/null +++ b/tests/Unit/QueueSystemTest.php @@ -0,0 +1,112 @@ +bind('request', fn() => new Request()); + $container->bind('url', fn() => UrlGenerator::class); + $container->bind('db', fn() => new Database('default')); + + $this->pdo = new PDO('sqlite::memory:'); + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $this->createQueueTables(); + $this->setupDatabaseConnections(); + + $this->manager = new QueueManager(); + $this->worker = new QueueWorker($this->manager); + } + + protected function tearDown(): void + { + $this->pdo = null; + $this->manager = null; + $this->worker = null; + $this->tearDownDatabaseConnections(); + } + + private function createQueueTables(): void + { + // Create queue_jobs table + $this->pdo->exec(" + CREATE TABLE queue_jobs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + queue TEXT NOT NULL, + payload TEXT NOT NULL, + attempts INTEGER DEFAULT 0, + reserved_at INTEGER, + available_at INTEGER NOT NULL, + created_at INTEGER NOT NULL + ) + "); + + $this->pdo->exec(" + CREATE INDEX idx_queue_reserved ON queue_jobs(queue, reserved_at) + "); + + // Create failed_jobs table + $this->pdo->exec(" + CREATE TABLE failed_jobs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + connection TEXT NOT NULL, + queue TEXT NOT NULL, + payload TEXT NOT NULL, + exception TEXT NOT NULL, + failed_at INTEGER NOT NULL + ) + "); + + $this->pdo->exec(" + CREATE INDEX idx_failed_at ON failed_jobs(failed_at) + "); + } + + private function setupDatabaseConnections(): void + { + $this->setStaticProperty(Database::class, 'connections', []); + $this->setStaticProperty(Database::class, 'transactions', []); + + $this->setStaticProperty(Database::class, 'connections', [ + 'default' => $this->pdo, + 'sqlite' => $this->pdo + ]); + } + + private function tearDownDatabaseConnections(): void + { + $this->setStaticProperty(Database::class, 'connections', []); + $this->setStaticProperty(Database::class, 'transactions', []); + } + + private function setStaticProperty(string $className, string $propertyName, $value): void + { + try { + $reflection = new \ReflectionClass($className); + $property = $reflection->getProperty($propertyName); + $property->setAccessible(true); + $property->setValue(null, $value); + $property->setAccessible(false); + } catch (\ReflectionException $e) { + $this->fail("Failed to set static property {$propertyName}: " . $e->getMessage()); + } + } +} From f0ce8aea98bdf34e47cb61080e522ce1e5d28a39 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 14:17:18 +0600 Subject: [PATCH 03/18] first test push to queue --- phpunit.xml.dist | 16 +- tests/Mock/Jobs/TestEmailJob.php | 27 ++ .../Mock/{MockContainer => MockContainer.php} | 0 tests/Mock/Models/MockQueueJob.php | 93 +++++++ tests/Mock/TestQueueManager.php | 234 ++++++++++++++++++ tests/Unit/QueueSystemTest.php | 27 +- 6 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 tests/Mock/Jobs/TestEmailJob.php rename tests/Mock/{MockContainer => MockContainer.php} (100%) create mode 100644 tests/Mock/Models/MockQueueJob.php create mode 100644 tests/Mock/TestQueueManager.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 88c6019..7ff869c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,11 +1,19 @@ - + - - ./tests/Unit + + ./tests - + + + + + \ No newline at end of file diff --git a/tests/Mock/Jobs/TestEmailJob.php b/tests/Mock/Jobs/TestEmailJob.php new file mode 100644 index 0000000..a342725 --- /dev/null +++ b/tests/Mock/Jobs/TestEmailJob.php @@ -0,0 +1,27 @@ +to = $to; + $this->subject = $subject; + } + + public function handle(): void + { + // Simulate sending email + if (empty($this->to) || empty($this->subject)) { + throw new \RuntimeException('Invalid email data'); + } + } +} diff --git a/tests/Mock/MockContainer b/tests/Mock/MockContainer.php similarity index 100% rename from tests/Mock/MockContainer rename to tests/Mock/MockContainer.php diff --git a/tests/Mock/Models/MockQueueJob.php b/tests/Mock/Models/MockQueueJob.php new file mode 100644 index 0000000..c7e2fbe --- /dev/null +++ b/tests/Mock/Models/MockQueueJob.php @@ -0,0 +1,93 @@ +where('queue', $queue) + ->where(function ($q) { + $q->whereNull('reserved_at') + ->orWhere('reserved_at', 0); + }) + ->where('available_at', '<=', time()) + ->orderBy('id', 'asc'); + } + + /** + * Mark the job as reserved. + * + * @return bool + */ + public function reserve(): bool + { + $this->reserved_at = time(); + $this->attempts += 1; + + return $this->save(); + } + + /** + * Release the job back to the queue. + * + * @param int $delay + * @return bool + */ + public function release(int $delay = 0): bool + { + $this->reserved_at = null; + $this->available_at = time() + $delay; + + return $this->save(); + } + + /** + * Delete the job from the queue. + * + * @return bool|null + */ + public function deleteJob(): ?bool + { + return $this->delete(); + } +} diff --git a/tests/Mock/TestQueueManager.php b/tests/Mock/TestQueueManager.php new file mode 100644 index 0000000..7221b09 --- /dev/null +++ b/tests/Mock/TestQueueManager.php @@ -0,0 +1,234 @@ +generateJobId(); + $job->setJobId($jobId); + + $payload = $this->createPayload($job); + $availableAt = time() + $job->delay(); + + MockQueueJob::create([ + 'queue' => $job->queue(), + 'payload' => $payload, + 'attempts' => 0, + 'reserved_at' => null, + 'available_at' => $availableAt, + 'created_at' => time(), + ]); + + return $jobId; + } catch (\Throwable $e) { + throw new QueueException("Failed to push job to queue: " . $e->getMessage(), 0, $e); + } + } + + /** + * Pop the next job off the queue. + * + * @param string $queue + * @return MockQueueJob|null + */ + public function pop(string $queue = 'default'): ?MockQueueJob + { + try { + $job = MockQueueJob::available($queue)->first(); + + if ($job) { + $job->reserve(); + } + + return $job; + } catch (\Throwable $e) { + return null; + } + } + + /** + * Delete a job from the queue. + * + * @param MockQueueJob $queueJob + * @return bool + */ + public function delete(MockQueueJob $queueJob): bool + { + try { + return $queueJob->deleteJob(); + } catch (\Throwable $e) { + return false; + } + } + + /** + * Release a job back to the queue. + * + * @param MockQueueJob $queueJob + * @param int $delay + * @return bool + */ + public function release(MockQueueJob $queueJob, int $delay = 0): bool + { + try { + return $queueJob->release($delay); + } catch (\Throwable $e) { + return false; + } + } + + /** + * Move a job to the failed jobs table. + * + * @param MockQueueJob $queueJob + * @param \Throwable $exception + * @return void + */ + public function markAsFailed(MockQueueJob $queueJob, \Throwable $exception): void + { + try { + FailedJob::create([ + 'connection' => 'database', + 'queue' => $queueJob->queue, + 'payload' => $queueJob->payload, + 'exception' => $this->formatException($exception), + 'failed_at' => time(), + ]); + + $this->delete($queueJob); + } catch (\Throwable $e) { + error("Failed to mark job as failed: " . $e->getMessage()); + } + } + + /** + * Get the count of jobs in a queue. + * + * @param string $queue + * @return int + */ + public function size(string $queue = 'default'): int + { + return MockQueueJob::where('queue', $queue) + ->whereNull('reserved_at') + ->count(); + } + + /** + * Clear all jobs from a queue. + * + * @param string $queue + * @return int Number of jobs deleted + */ + public function clear(string $queue = 'default'): int + { + return MockQueueJob::where('queue', $queue)->delete(); + } + + /** + * Create a payload string from the given job. + * + * @param JobInterface $job + * @return string + */ + protected function createPayload(JobInterface $job): string + { + return serialize([ + 'job' => $job, + 'data' => [ + 'jobId' => $job->getJobId(), + 'queue' => $job->queue(), + 'tries' => $job->tries(), + 'retryAfter' => $job->retryAfter(), + ], + ]); + } + + /** + * Unserialize the job from payload. + * + * @param string $payload + * @return JobInterface + * @throws QueueException + */ + public function unserializeJob(string $payload): JobInterface + { + try { + $data = unserialize($payload); + return $data['job']; + } catch (\Throwable $e) { + throw new QueueException("Failed to unserialize job: " . $e->getMessage(), 0, $e); + } + } + + /** + * Generate a unique job ID. + * + * @return string + */ + protected function generateJobId(): string + { + return uniqid('job_', true); + } + + /** + * Format exception for storage. + * + * @param \Throwable $exception + * @return string + */ + protected function formatException(\Throwable $exception): string + { + return sprintf( + "%s: %s in %s:%d\nStack trace:\n%s", + get_class($exception), + $exception->getMessage(), + $exception->getFile(), + $exception->getLine(), + $exception->getTraceAsString() + ); + } + + /** + * Set the default queue name. + * + * @param string $queue + * @return void + */ + public function setDefaultQueue(string $queue): void + { + $this->defaultQueue = $queue; + } + + /** + * Get the default queue name. + * + * @return string + */ + public function getDefaultQueue(): string + { + return $this->defaultQueue; + } +} diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index 45fd941..1ba745a 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -2,15 +2,20 @@ namespace Doppar\Queue\Tests\Unit; -use Doppar\Queue\Tests\Mock\MockContainer; use Phaseolies\Support\UrlGenerator; use Phaseolies\Http\Request; use Phaseolies\Database\Database; use Phaseolies\DI\Container; +use PHPUnit\Metadata\Test; use PHPUnit\Framework\TestCase; use PDO; +use Doppar\Queue\Tests\Mock\TestQueueManager; +use Doppar\Queue\Tests\Mock\Models\MockQueueJob; +use Doppar\Queue\Tests\Mock\MockContainer; +use Doppar\Queue\Tests\Mock\Jobs\TestEmailJob; use Doppar\Queue\QueueWorker; use Doppar\Queue\QueueManager; +use Doppar\Queue\Facades\Queue; class QueueSystemTest extends TestCase { @@ -25,6 +30,7 @@ protected function setUp(): void $container->bind('request', fn() => new Request()); $container->bind('url', fn() => UrlGenerator::class); $container->bind('db', fn() => new Database('default')); + $container->singleton('queue.worker', TestQueueManager::class); $this->pdo = new PDO('sqlite::memory:'); $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); @@ -109,4 +115,23 @@ private function setStaticProperty(string $className, string $propertyName, $val $this->fail("Failed to set static property {$propertyName}: " . $e->getMessage()); } } + + // ===================================================== + // TEST JOB CREATION AND DISPATCHING + // ===================================================== + + public function testPushJobToQueue(): void + { + $job = new TestEmailJob('test@example.com', 'Test Subject'); + $jobId = Queue::push($job); + + $this->assertNotEmpty($jobId); + $this->assertStringStartsWith('job_', $jobId); + + // Verify job is in database + $queueJob = MockQueueJob::where('queue', 'default')->first(); + $this->assertNotNull($queueJob); + $this->assertEquals('default', $queueJob->queue); + $this->assertEquals(0, $queueJob->attempts); + } } From b70acd01e0e231616b8ab72fd7c7d92169c27d82 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 14:21:34 +0600 Subject: [PATCH 04/18] mock jobs class generate --- tests/Mock/Jobs/TestComplexDataJob.php | 22 +++++++++++++++++ tests/Mock/Jobs/TestCounterJob.php | 15 ++++++++++++ tests/Mock/Jobs/TestFailingJob.php | 16 +++++++++++++ tests/Mock/Jobs/TestImageJob.php | 24 +++++++++++++++++++ tests/Mock/Jobs/TestJobWithFailedCallback.php | 21 ++++++++++++++++ tests/Mock/Jobs/TestReportJob.php | 24 +++++++++++++++++++ 6 files changed, 122 insertions(+) create mode 100644 tests/Mock/Jobs/TestComplexDataJob.php create mode 100644 tests/Mock/Jobs/TestCounterJob.php create mode 100644 tests/Mock/Jobs/TestFailingJob.php create mode 100644 tests/Mock/Jobs/TestImageJob.php create mode 100644 tests/Mock/Jobs/TestJobWithFailedCallback.php create mode 100644 tests/Mock/Jobs/TestReportJob.php diff --git a/tests/Mock/Jobs/TestComplexDataJob.php b/tests/Mock/Jobs/TestComplexDataJob.php new file mode 100644 index 0000000..7baf003 --- /dev/null +++ b/tests/Mock/Jobs/TestComplexDataJob.php @@ -0,0 +1,22 @@ +data = $data; + } + + public function handle(): void + { + if (empty($this->data)) { + throw new \RuntimeException('No data provided'); + } + } +} diff --git a/tests/Mock/Jobs/TestCounterJob.php b/tests/Mock/Jobs/TestCounterJob.php new file mode 100644 index 0000000..bd36cc7 --- /dev/null +++ b/tests/Mock/Jobs/TestCounterJob.php @@ -0,0 +1,15 @@ +counter++; + } +} diff --git a/tests/Mock/Jobs/TestFailingJob.php b/tests/Mock/Jobs/TestFailingJob.php new file mode 100644 index 0000000..13861c2 --- /dev/null +++ b/tests/Mock/Jobs/TestFailingJob.php @@ -0,0 +1,16 @@ +imagePath = $imagePath; + } + + public function handle(): void + { + if (empty($this->imagePath)) { + throw new \RuntimeException('Invalid image path'); + } + } +} diff --git a/tests/Mock/Jobs/TestJobWithFailedCallback.php b/tests/Mock/Jobs/TestJobWithFailedCallback.php new file mode 100644 index 0000000..bb3aa19 --- /dev/null +++ b/tests/Mock/Jobs/TestJobWithFailedCallback.php @@ -0,0 +1,21 @@ +failedCalled = true; + } +} diff --git a/tests/Mock/Jobs/TestReportJob.php b/tests/Mock/Jobs/TestReportJob.php new file mode 100644 index 0000000..1bca22e --- /dev/null +++ b/tests/Mock/Jobs/TestReportJob.php @@ -0,0 +1,24 @@ +reportType = $reportType; + } + + public function handle(): void + { + if (empty($this->reportType)) { + throw new \RuntimeException('Invalid report type'); + } + } +} From 51c1f0ba8418063751b111cd0a0b686f5b943535 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 14:25:09 +0600 Subject: [PATCH 05/18] custom queue name test --- tests/Unit/QueueSystemTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index 1ba745a..fe03263 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -134,4 +134,18 @@ public function testPushJobToQueue(): void $this->assertEquals('default', $queueJob->queue); $this->assertEquals(0, $queueJob->attempts); } + + public function testPushJobWithCustomQueue(): void + { + $job = new TestEmailJob('test@example.com', 'Test Subject'); + $job->onQueue('emails'); + $jobId = Queue::push($job); + + $this->assertNotEmpty($jobId); + + // Verify job is in correct queue + $queueJob = MockQueueJob::where('queue', 'emails')->first(); + $this->assertNotNull($queueJob); + $this->assertEquals('emails', $queueJob->queue); + } } From a7fd93b06f454cc463d7fdcfe9ab2e005735790f Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 14:26:09 +0600 Subject: [PATCH 06/18] push with job delay test --- tests/Unit/QueueSystemTest.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index fe03263..a46b2e8 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -148,4 +148,21 @@ public function testPushJobWithCustomQueue(): void $this->assertNotNull($queueJob); $this->assertEquals('emails', $queueJob->queue); } + + public function testPushJobWithDelay(): void + { + $job = new TestEmailJob('test@example.com', 'Test Subject'); + $job->delayFor(300); // 5 minutes + + $beforeTime = time(); + $jobId = Queue::push($job); + $afterTime = time(); + + $queueJob = MockQueueJob::where('queue', 'default')->first(); + $this->assertNotNull($queueJob); + + // available_at should be current time + 300 seconds + $this->assertGreaterThanOrEqual($beforeTime + 300, $queueJob->available_at); + $this->assertLessThanOrEqual($afterTime + 300, $queueJob->available_at); + } } From 1eb482ce971631a75bc86e3a60ab3c37f27a9e8c Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 14:27:23 +0600 Subject: [PATCH 07/18] pop job from queue --- tests/Unit/QueueSystemTest.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index a46b2e8..9714438 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -165,4 +165,21 @@ public function testPushJobWithDelay(): void $this->assertGreaterThanOrEqual($beforeTime + 300, $queueJob->available_at); $this->assertLessThanOrEqual($afterTime + 300, $queueJob->available_at); } + + // ===================================================== + // TEST JOB RETRIEVAL + // ===================================================== + + public function testPopJobFromQueue(): void + { + $job = new TestEmailJob('test@example.com', 'Test Subject'); + Queue::push($job); + + $queueJob = Queue::pop('default'); + + $this->assertNotNull($queueJob); + $this->assertInstanceOf(MockQueueJob::class, $queueJob); + $this->assertEquals(1, $queueJob->attempts); + $this->assertNotNull($queueJob->reserved_at); + } } From 31787a5a9d749f130297bd9d9730dc79ea26e77a Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 14:28:31 +0600 Subject: [PATCH 08/18] pop job as per avaiale_at --- tests/Unit/QueueSystemTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index 9714438..e885233 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -182,4 +182,22 @@ public function testPopJobFromQueue(): void $this->assertEquals(1, $queueJob->attempts); $this->assertNotNull($queueJob->reserved_at); } + + public function testPopJobRespectsAvailableAt(): void + { + $job = new TestEmailJob('test@example.com', 'Test Subject'); + $job->delayFor(3600); // 1 hour delay + Queue::push($job); + + // Should not pop job that's not available yet + $queueJob = Queue::pop('default'); + $this->assertNull($queueJob); + + // Manually update available_at to make it available + MockQueueJob::where('queue', 'default')->update(['available_at' => time() - 1]); + + // Now it should pop + $queueJob = Queue::pop('default'); + $this->assertNotNull($queueJob); + } } From 8d560e1fd3e2081681339e6cfca8534a1a21ef8d Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 14:30:22 +0600 Subject: [PATCH 09/18] pop job from specific queue --- tests/Unit/QueueSystemTest.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index e885233..fac1676 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -12,6 +12,7 @@ use Doppar\Queue\Tests\Mock\TestQueueManager; use Doppar\Queue\Tests\Mock\Models\MockQueueJob; use Doppar\Queue\Tests\Mock\MockContainer; +use Doppar\Queue\Tests\Mock\Jobs\TestImageJob; use Doppar\Queue\Tests\Mock\Jobs\TestEmailJob; use Doppar\Queue\QueueWorker; use Doppar\Queue\QueueManager; @@ -200,4 +201,31 @@ public function testPopJobRespectsAvailableAt(): void $queueJob = Queue::pop('default'); $this->assertNotNull($queueJob); } + + public function testPopJobFromEmptyQueue(): void + { + $queueJob = $this->manager->pop('default'); + $this->assertNull($queueJob); + } + + public function testPopJobFromSpecificQueue(): void + { + $emailJob = new TestEmailJob('test@example.com', 'Subject'); + $emailJob->onQueue('emails'); + Queue::push($emailJob); + + $imageJob = new TestImageJob('/path/to/image.jpg'); + $imageJob->onQueue('images'); + Queue::push($imageJob); + + // Pop from emails queue + $queueJob = Queue::pop('emails'); + $this->assertNotNull($queueJob); + $this->assertEquals('emails', $queueJob->queue); + + // Pop from images queue + $queueJob = Queue::pop('images'); + $this->assertNotNull($queueJob); + $this->assertEquals('images', $queueJob->queue); + } } From 0ff2f0da1db4e224c8ae7fd7242b8aa269d809a7 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 14:33:47 +0600 Subject: [PATCH 10/18] test job execution --- tests/Unit/QueueSystemTest.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index fac1676..5e397f3 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -228,4 +228,33 @@ public function testPopJobFromSpecificQueue(): void $this->assertNotNull($queueJob); $this->assertEquals('images', $queueJob->queue); } + + // ===================================================== + // TEST JOB EXECUTION + // ===================================================== + + public function testJobExecutionSuccess(): void + { + $job = new TestEmailJob('test@example.com', 'Test Subject'); + Queue::push($job); + + $queueJob = Queue::pop('default'); + $this->assertNotNull($queueJob); + + // Execute the job + $unserializedJob = $this->manager->unserializeJob($queueJob->payload); + $unserializedJob->handle(); + + // Job should have been executed + $this->assertEquals('test@example.com', $unserializedJob->to); + $this->assertEquals('Test Subject', $unserializedJob->subject); + + // Delete job after successful execution + $deleted = Queue::delete($queueJob); + $this->assertTrue($deleted); + + // Verify job is removed + $count = MockQueueJob::count(); + $this->assertEquals(0, $count); + } } From e717e2d2776ecd12c5085fe33963a6af6c0003a4 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 14:35:41 +0600 Subject: [PATCH 11/18] job execution failer and retry --- tests/Unit/QueueSystemTest.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index 5e397f3..78ab303 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -13,6 +13,7 @@ use Doppar\Queue\Tests\Mock\Models\MockQueueJob; use Doppar\Queue\Tests\Mock\MockContainer; use Doppar\Queue\Tests\Mock\Jobs\TestImageJob; +use Doppar\Queue\Tests\Mock\Jobs\TestFailingJob; use Doppar\Queue\Tests\Mock\Jobs\TestEmailJob; use Doppar\Queue\QueueWorker; use Doppar\Queue\QueueManager; @@ -257,4 +258,31 @@ public function testJobExecutionSuccess(): void $count = MockQueueJob::count(); $this->assertEquals(0, $count); } + + public function testJobExecutionFailureAndRetry(): void + { + $job = new TestFailingJob(); + $job->tries = 3; + $job->retryAfter = 60; + Queue::push($job); + + // First attempt + $queueJob = Queue::pop('default'); + $this->assertEquals(1, $queueJob->attempts); + + try { + $unserializedJob = $this->manager->unserializeJob($queueJob->payload); + $unserializedJob->handle(); + $this->fail('Job should have thrown an exception'); + } catch (\Exception $e) { + // Job failed, release it back + $released = Queue::release($queueJob, $job->retryAfter); + $this->assertTrue($released); + } + + // Verify job was released + $queueJob = MockQueueJob::find($queueJob->id); + $this->assertNull($queueJob->reserved_at); + $this->assertEquals(1, $queueJob->attempts); + } } From 3ca54f54f33bd80f96266f7625e445f9910aa531 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 14:50:45 +0600 Subject: [PATCH 12/18] Test Job Moved To Failed After Max Attempts --- storage/logs/doppar.log | 9 ++++++ tests/Mock/Models/MockFailedJob.php | 36 +++++++++++++++++++++++ tests/Mock/TestQueueManager.php | 4 +-- tests/Unit/QueueSystemTest.php | 44 ++++++++++++++++++++++++++++- 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 storage/logs/doppar.log create mode 100644 tests/Mock/Models/MockFailedJob.php diff --git a/storage/logs/doppar.log b/storage/logs/doppar.log new file mode 100644 index 0000000..c123a1b --- /dev/null +++ b/storage/logs/doppar.log @@ -0,0 +1,9 @@ +[2025-11-14T08:38:24.717063+00:00] stack.ERROR: Failed to mark job as failed: Database connection [] not configured. +[2025-11-14T08:38:57.213083+00:00] stack.ERROR: Failed to mark job as failed: Database connection [] not configured. +[2025-11-14T08:39:33.718229+00:00] stack.ERROR: Failed to mark job as failed: Database connection [] not configured. +[2025-11-14T08:43:01.934544+00:00] stack.ERROR: Failed to mark job as failed: Database connection [] not configured. +[2025-11-14T08:44:10.792443+00:00] stack.ERROR: Failed to mark job as failed: Database connection [] not configured. +[2025-11-14T08:45:01.563845+00:00] stack.ERROR: Failed to mark job as failed: Database connection [] not configured. +[2025-11-14T08:45:05.456173+00:00] stack.ERROR: Failed to mark job as failed: Database connection [] not configured. +[2025-11-14T08:45:57.038190+00:00] stack.ERROR: Failed to mark job as failed: Database connection [] not configured. +[2025-11-14T08:47:51.149909+00:00] stack.ERROR: Failed to mark job as failed: Database connection [] not configured. diff --git a/tests/Mock/Models/MockFailedJob.php b/tests/Mock/Models/MockFailedJob.php new file mode 100644 index 0000000..8984ce2 --- /dev/null +++ b/tests/Mock/Models/MockFailedJob.php @@ -0,0 +1,36 @@ + 'database', 'queue' => $queueJob->queue, 'payload' => $queueJob->payload, diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index 78ab303..fd1847c 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -3,14 +3,15 @@ namespace Doppar\Queue\Tests\Unit; use Phaseolies\Support\UrlGenerator; +use Phaseolies\Support\LoggerService; use Phaseolies\Http\Request; use Phaseolies\Database\Database; use Phaseolies\DI\Container; -use PHPUnit\Metadata\Test; use PHPUnit\Framework\TestCase; use PDO; use Doppar\Queue\Tests\Mock\TestQueueManager; use Doppar\Queue\Tests\Mock\Models\MockQueueJob; +use Doppar\Queue\Tests\Mock\Models\MockFailedJob; use Doppar\Queue\Tests\Mock\MockContainer; use Doppar\Queue\Tests\Mock\Jobs\TestImageJob; use Doppar\Queue\Tests\Mock\Jobs\TestFailingJob; @@ -33,6 +34,7 @@ protected function setUp(): void $container->bind('url', fn() => UrlGenerator::class); $container->bind('db', fn() => new Database('default')); $container->singleton('queue.worker', TestQueueManager::class); + $container->singleton('log', LoggerService::class); $this->pdo = new PDO('sqlite::memory:'); $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); @@ -285,4 +287,44 @@ public function testJobExecutionFailureAndRetry(): void $this->assertNull($queueJob->reserved_at); $this->assertEquals(1, $queueJob->attempts); } + + public function testJobMovedToFailedAfterMaxAttempts(): void + { + $job = new TestFailingJob(); + $job->tries = 2; + Queue::push($job); + + // First attempt + $queueJob = Queue::pop('default'); + try { + $unserializedJob = $this->manager->unserializeJob($queueJob->payload); + $unserializedJob->handle(); + } catch (\Exception $e) { + Queue::release($queueJob, 0); + } + + // Make job available immediately + MockQueueJob::where('id', $queueJob->id)->update(['available_at' => time() - 1]); + + // Second attempt + $queueJob = Queue::pop('default'); + $this->assertEquals(2, $queueJob->attempts); + + try { + $unserializedJob = $this->manager->unserializeJob($queueJob->payload); + $unserializedJob->handle(); + } catch (\Exception $e) { + // Max attempts reached, mark as failed + Queue::markAsFailed($queueJob, $e); + } + + // Verify job is in failed_jobs table + $failedJob = MockFailedJob::where('queue', 'default')->first(); + $this->assertNotNull($failedJob); + $this->assertStringContainsString('Test failure', $failedJob->exception); + + // Verify job is removed from queue_jobs + $queueJob = MockQueueJob::find($queueJob->id); + $this->assertNull($queueJob); + } } From 1a7a3e59716c0e8191f6efa4d95f99308159b968 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 15:05:30 +0600 Subject: [PATCH 13/18] test queue size --- tests/Unit/QueueSystemTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index fd1847c..d27abd9 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -327,4 +327,22 @@ public function testJobMovedToFailedAfterMaxAttempts(): void $queueJob = MockQueueJob::find($queueJob->id); $this->assertNull($queueJob); } + + // ===================================================== + // TEST QUEUE OPERATIONS + // ===================================================== + + public function testQueueSize(): void + { + $job1 = new TestEmailJob('test1@example.com', 'Subject 1'); + $job2 = new TestEmailJob('test2@example.com', 'Subject 2'); + $job3 = new TestEmailJob('test3@example.com', 'Subject 3'); + + Queue::push($job1); + Queue::push($job2); + Queue::push($job3); + + $size = Queue::size('default'); + $this->assertEquals(3, $size); + } } From c70ae4f2b1c02cabe3a4a38ea834767e486e7fa7 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 15:07:43 +0600 Subject: [PATCH 14/18] test queue clear job --- tests/Unit/QueueSystemTest.php | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index d27abd9..34433ce 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -345,4 +345,40 @@ public function testQueueSize(): void $size = Queue::size('default'); $this->assertEquals(3, $size); } + + public function testQueueClear(): void + { + $job1 = new TestEmailJob('test1@example.com', 'Subject 1'); + $job2 = new TestEmailJob('test2@example.com', 'Subject 2'); + $job3 = new TestEmailJob('test3@example.com', 'Subject 3'); + + Queue::push($job1); + Queue::push($job2); + Queue::push($job3); + + $deleted = Queue::clear('default'); + $this->assertEquals(1, $deleted); + + $size = Queue::size('default'); + $this->assertEquals(0, $size); + } + + public function testClearSpecificQueue(): void + { + $emailJob = new TestEmailJob('test@example.com', 'Subject'); + $emailJob->onQueue('emails'); + Queue::push($emailJob); + + $imageJob = new TestImageJob('/path/to/image.jpg'); + $imageJob->onQueue('images'); + Queue::push($imageJob); + + // Clear only emails queue + $deleted = Queue::clear('emails'); + $this->assertEquals(1, $deleted); + + // Verify images queue is intact + $size = Queue::size('images'); + $this->assertEquals(1, $size); + } } From daf70ea5fdb15ad66f48f00c963b53801ec64265 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 15:10:04 +0600 Subject: [PATCH 15/18] test failed jobs --- tests/Unit/QueueSystemTest.php | 83 ++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index 34433ce..79e2228 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -13,9 +13,11 @@ use Doppar\Queue\Tests\Mock\Models\MockQueueJob; use Doppar\Queue\Tests\Mock\Models\MockFailedJob; use Doppar\Queue\Tests\Mock\MockContainer; +use Doppar\Queue\Tests\Mock\Jobs\TestJobWithFailedCallback; use Doppar\Queue\Tests\Mock\Jobs\TestImageJob; use Doppar\Queue\Tests\Mock\Jobs\TestFailingJob; use Doppar\Queue\Tests\Mock\Jobs\TestEmailJob; +use Doppar\Queue\Tests\Mock\Jobs\TestComplexDataJob; use Doppar\Queue\QueueWorker; use Doppar\Queue\QueueManager; use Doppar\Queue\Facades\Queue; @@ -381,4 +383,85 @@ public function testClearSpecificQueue(): void $size = Queue::size('images'); $this->assertEquals(1, $size); } + + // ===================================================== + // TEST JOB SERIALIZATION + // ===================================================== + + public function testJobSerialization(): void + { + $job = new TestEmailJob('test@example.com', 'Test Subject'); + $job->setJobId('test_job_123'); + $jobId = Queue::push($job); + + $queueJob = MockQueueJob::where('queue', 'default')->first(); + $this->assertNotNull($queueJob); + + $unserializedJob = $this->manager->unserializeJob($queueJob->payload); + + $this->assertInstanceOf(TestEmailJob::class, $unserializedJob); + $this->assertEquals('test@example.com', $unserializedJob->to); + $this->assertEquals('Test Subject', $unserializedJob->subject); + } + + public function testJobSerializationWithComplexData(): void + { + $job = new TestComplexDataJob([ + 'user' => ['id' => 1, 'name' => 'John Doe'], + 'settings' => ['timezone' => 'UTC', 'theme' => 'dark'], + 'tags' => ['php', 'laravel', 'queue'] + ]); + Queue::push($job); + + $queueJob = MockQueueJob::where('queue', 'default')->first(); + $unserializedJob = $this->manager->unserializeJob($queueJob->payload); + + $this->assertEquals('John Doe', $unserializedJob->data['user']['name']); + $this->assertEquals(['php', 'laravel', 'queue'], $unserializedJob->data['tags']); + } + + // ===================================================== + // TEST FAILED JOBS + // ===================================================== + + public function testFailedJobStorage(): void + { + $job = new TestFailingJob(); + Queue::push($job); + + $queueJob = Queue::pop('default'); + + try { + $unserializedJob = $this->manager->unserializeJob($queueJob->payload); + $unserializedJob->handle(); + } catch (\Exception $e) { + Queue::markAsFailed($queueJob, $e); + } + + $failedJob = MockFailedJob::first(); + $this->assertNotNull($failedJob); + $this->assertEquals('database', $failedJob->connection); + $this->assertEquals('default', $failedJob->queue); + $this->assertStringContainsString('RuntimeException', $failedJob->exception); + $this->assertStringContainsString('Test failure', $failedJob->exception); + } + + public function testFailedJobCallback(): void + { + $job = new TestJobWithFailedCallback(); + Queue::push($job); + + $queueJob = Queue::pop('default'); + + try { + $unserializedJob = $this->manager->unserializeJob($queueJob->payload); + $unserializedJob->handle(); + } catch (\Exception $e) { + Queue::markAsFailed($queueJob, $e); + $unserializedJob->failed($e); + } + + // The failed callback should have been called + $this->assertTrue($unserializedJob->failedCalled); + } } From a1d877e7e7ac117d2ba41e96d4e68b98a81c0646 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 15:11:55 +0600 Subject: [PATCH 16/18] test queue model --- tests/Unit/QueueSystemTest.php | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index 79e2228..c74aa45 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -464,4 +464,64 @@ public function testFailedJobCallback(): void // The failed callback should have been called $this->assertTrue($unserializedJob->failedCalled); } + + // ===================================================== + // TEST QUEUE MODELS + // ===================================================== + + public function testQueueJobModel(): void + { + $job = new TestEmailJob('test@example.com', 'Subject'); + Queue::push($job); + + $queueJob = MockQueueJob::where('queue', 'default')->first(); + + $this->assertInstanceOf(MockQueueJob::class, $queueJob); + $this->assertEquals('default', $queueJob->queue); + $this->assertEquals(0, $queueJob->attempts); + $this->assertNull($queueJob->reserved_at); + } + + public function testQueueJobReserve(): void + { + $job = new TestEmailJob('test@example.com', 'Subject'); + Queue::push($job); + + $queueJob = MockQueueJob::available('default')->first(); + $this->assertNotNull($queueJob); + + $reserved = $queueJob->reserve(); + $this->assertTrue($reserved); + $this->assertEquals(1, $queueJob->attempts); + $this->assertNotNull($queueJob->reserved_at); + } + + public function testQueueJobRelease(): void + { + $job = new TestEmailJob('test@example.com', 'Subject'); + Queue::push($job); + + $queueJob = MockQueueJob::available('default')->first(); + $queueJob->reserve(); + + $released = $queueJob->release(60); + $this->assertTrue($released); + $this->assertNull($queueJob->reserved_at); + $this->assertGreaterThan(time(), $queueJob->available_at); + } + + public function testFailedJobModel(): void + { + $failedJob = MockFailedJob::create([ + 'connection' => 'database', + 'queue' => 'default', + 'payload' => serialize(['test' => 'data']), + 'exception' => 'Test exception', + 'failed_at' => time(), + ]); + + $this->assertInstanceOf(MockFailedJob::class, $failedJob); + $this->assertEquals('database', $failedJob->connection); + $this->assertEquals('default', $failedJob->queue); + } } From 1f9bb4dbbeb3bb8814fc4463347b6f2b61cbb281 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 15:18:15 +0600 Subject: [PATCH 17/18] test mulitple queues --- tests/Unit/QueueSystemTest.php | 99 ++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index c74aa45..f4503e1 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -13,10 +13,12 @@ use Doppar\Queue\Tests\Mock\Models\MockQueueJob; use Doppar\Queue\Tests\Mock\Models\MockFailedJob; use Doppar\Queue\Tests\Mock\MockContainer; +use Doppar\Queue\Tests\Mock\Jobs\TestReportJob; use Doppar\Queue\Tests\Mock\Jobs\TestJobWithFailedCallback; use Doppar\Queue\Tests\Mock\Jobs\TestImageJob; use Doppar\Queue\Tests\Mock\Jobs\TestFailingJob; use Doppar\Queue\Tests\Mock\Jobs\TestEmailJob; +use Doppar\Queue\Tests\Mock\Jobs\TestCounterJob; use Doppar\Queue\Tests\Mock\Jobs\TestComplexDataJob; use Doppar\Queue\QueueWorker; use Doppar\Queue\QueueManager; @@ -524,4 +526,101 @@ public function testFailedJobModel(): void $this->assertEquals('database', $failedJob->connection); $this->assertEquals('default', $failedJob->queue); } + + // ===================================================== + // TEST WORKER BEHAVIOR + // ===================================================== + + public function testWorkerProcessSingleJob(): void + { + $job = new TestCounterJob(); + Queue::push($job); + + // Process one job + $queueJob = Queue::pop('default'); + $this->assertNotNull($queueJob); + + $unserializedJob = $this->manager->unserializeJob($queueJob->payload); + $unserializedJob->handle(); + + $this->assertEquals(1, $unserializedJob->counter); + + // Delete job + Queue::delete($queueJob); + + // Queue should be empty + $this->assertEquals(0, Queue::size('default')); + } + + public function testWorkerMemoryCheck(): void + { + $this->worker->setMaxMemory(1); // 1MB limit + + // This should return true since we're using more than 1MB + $reflection = new \ReflectionClass($this->worker); + $method = $reflection->getMethod('memoryExceeded'); + $method->setAccessible(true); + + $exceeded = $method->invoke($this->worker); + $this->assertTrue($exceeded); + } + + // ===================================================== + // TEST MULTIPLE QUEUES + // ===================================================== + + public function testMultipleQueues(): void + { + // Create jobs on different queues + $emailJob = new TestEmailJob('test@example.com', 'Subject'); + $emailJob->onQueue('emails'); + Queue::push($emailJob); + + $imageJob = new TestImageJob('/path/to/image.jpg'); + $imageJob->onQueue('images'); + Queue::push($imageJob); + + $reportJob = new TestReportJob('monthly'); + $reportJob->onQueue('reports'); + Queue::push($reportJob); + + // Verify each queue has correct job + $this->assertEquals(1, Queue::size('emails')); + $this->assertEquals(1, Queue::size('images')); + $this->assertEquals(1, Queue::size('reports')); + $this->assertEquals(0, Queue::size('default')); + } + + public function testJobWithZeroRetries(): void + { + $job = new TestFailingJob(); + $job->tries = 0; // No retries + Queue::push($job); + + $queueJob = Queue::pop('default'); + + try { + $unserializedJob = $this->manager->unserializeJob($queueJob->payload); + $unserializedJob->handle(); + } catch (\Exception $e) { + // Should mark as failed immediately + Queue::markAsFailed($queueJob, $e); + } + + $failedJob = MockFailedJob::first(); + $this->assertNotNull($failedJob); + } + + public function testJobIdGeneration(): void + { + $job1 = new TestEmailJob('test1@example.com', 'Subject 1'); + $job2 = new TestEmailJob('test2@example.com', 'Subject 2'); + + $jobId1 = Queue::push($job1); + $jobId2 = Queue::push($job2); + + $this->assertNotEquals($jobId1, $jobId2); + $this->assertStringStartsWith('job_', $jobId1); + $this->assertStringStartsWith('job_', $jobId2); + } } From 04b8c3d1487d0bbbc2a9b3f22327b2beb7e1ab42 Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Fri, 14 Nov 2025 15:29:30 +0600 Subject: [PATCH 18/18] test query binding for available and test integration --- tests/Unit/QueueSystemTest.php | 67 ++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/Unit/QueueSystemTest.php b/tests/Unit/QueueSystemTest.php index f4503e1..112b0c1 100644 --- a/tests/Unit/QueueSystemTest.php +++ b/tests/Unit/QueueSystemTest.php @@ -623,4 +623,71 @@ public function testJobIdGeneration(): void $this->assertStringStartsWith('job_', $jobId1); $this->assertStringStartsWith('job_', $jobId2); } + + // ===================================================== + // TEST QUERY BINDING + // ===================================================== + + public function testAvailableJobsScope(): void + { + // Create two available jobs + $job1 = new TestEmailJob('test1@example.com', 'Subject 1'); + Queue::push($job1); + + $job2 = new TestEmailJob('test2@example.com', 'Subject 2'); + Queue::push($job2); + + // Create delayed job (not available yet) + $job3 = new TestEmailJob('test3@example.com', 'Subject 3'); + $job3->delayFor(3600); + Queue::push($job3); + + // Before popping: 2 available jobs (job1 and job2), 1 delayed (job3) + $available = MockQueueJob::available('default')->count(); + $this->assertEquals(2, $available); + + // Pop one job (makes it reserved) + Queue::pop('default'); + + // After popping: 1 available job (job2), 1 reserved (job1), 1 delayed (job3) + $available = MockQueueJob::available('default')->count(); + $this->assertEquals(1, $available); + + // Verify total jobs in database + $total = MockQueueJob::where('queue', 'default')->count(); + $this->assertEquals(3, $total); + } + + // ===================================================== + // TEST INTEGRATION + // ===================================================== + + public function testEndToEndJobProcessing(): void + { + // Create multiple jobs + $jobs = [ + new TestEmailJob('user1@example.com', 'Welcome'), + new TestEmailJob('user2@example.com', 'Newsletter'), + new TestEmailJob('user3@example.com', 'Update'), + ]; + + foreach ($jobs as $job) { + Queue::push($job); + } + + $this->assertEquals(3, Queue::size('default')); + + // Process all jobs + $processed = 0; + while ($queueJob = Queue::pop('default')) { + $unserializedJob = $this->manager->unserializeJob($queueJob->payload); + $unserializedJob->handle(); + Queue::delete($queueJob); + $processed++; + } + + $this->assertEquals(3, $processed); + $this->assertEquals(0, Queue::size('default')); + $this->assertEquals(0, MockFailedJob::count()); + } }