setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdoSender = new PDO( 'pgsql:host=db;dbname=michaelschiemer', 'postgres', 'StartSimple2024!' ); $pdoSender->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); echo "✅ Two database connections established (listener + sender)\n\n"; $listenerService = new NotificationService($pdoListener); $senderService = new NotificationService($pdoSender); // Test 1: Basic NOTIFY without payload echo "Test 1: Basic NOTIFY Without Payload\n"; echo "=====================================\n"; $channel1 = Channel::fromString('test_channel'); // Listener subscribes $listenerService->listen($channel1); echo "✅ Listener: Subscribed to channel '{$channel1}'\n"; // Sender sends notification without payload $senderService->notify($channel1); echo "✅ Sender: Sent notification (no payload)\n"; // Listener polls for notifications $notifications = $listenerService->poll(); echo count($notifications) === 1 ? "✅ Listener: Received 1 notification\n" : "❌ Should have received 1 notification\n"; if (count($notifications) > 0) { $n = $notifications[0]; echo " Channel: {$n->getChannelName()}\n"; echo " Payload: " . ($n->payload->isEmpty() ? "(empty)" : $n->getPayloadString()) . "\n"; echo " Sender PID: {$n->getSenderPid()}\n"; } $listenerService->unlisten($channel1); echo "\n"; // Test 2: NOTIFY with string payload echo "Test 2: NOTIFY With String Payload\n"; echo "===================================\n"; $channel2 = Channel::fromString('events'); $payload2 = NotificationPayload::fromString('user_created'); $listenerService->listen($channel2); echo "✅ Listener: Subscribed to channel '{$channel2}'\n"; $senderService->notify($channel2, $payload2); echo "✅ Sender: Sent notification with payload 'user_created'\n"; $notifications = $listenerService->poll(); echo count($notifications) === 1 ? "✅ Listener: Received notification\n" : "❌ Should have received notification\n"; if (count($notifications) > 0) { $n = $notifications[0]; echo " Channel: {$n->getChannelName()}\n"; echo " Payload: '{$n->getPayloadString()}'\n"; echo ($n->getPayloadString() === 'user_created') ? "✅ Payload matches\n" : "❌ Payload should match\n"; } $listenerService->unlisten($channel2); echo "\n"; // Test 3: NOTIFY with JSON payload echo "Test 3: NOTIFY With JSON Payload\n"; echo "=================================\n"; $channel3 = Channel::fromString('user_events'); $data3 = [ 'event' => 'user_registered', 'user_id' => 123, 'email' => 'test@example.com', 'timestamp' => time() ]; $listenerService->listen($channel3); echo "✅ Listener: Subscribed to channel '{$channel3}'\n"; $senderService->notifyWithData($channel3, $data3); echo "✅ Sender: Sent notification with JSON payload\n"; $notifications = $listenerService->poll(); echo count($notifications) === 1 ? "✅ Listener: Received notification\n" : "❌ Should have received notification\n"; if (count($notifications) > 0) { $n = $notifications[0]; echo " Raw payload: " . $n->getPayloadString() . "\n"; $receivedData = $n->getPayloadArray(); echo " Deserialized data: " . json_encode($receivedData) . "\n"; echo " Received data:\n"; echo " - Event: " . ($receivedData['event'] ?? 'N/A') . "\n"; echo " - User ID: " . ($receivedData['user_id'] ?? 'N/A') . "\n"; echo " - Email: " . ($receivedData['email'] ?? 'N/A') . "\n"; $dataMatches = isset($receivedData['event']) && $receivedData['event'] === 'user_registered' && $receivedData['user_id'] === 123; echo $dataMatches ? "✅ JSON payload correctly deserialized\n" : "❌ Payload should match\n"; } $listenerService->unlisten($channel3); echo "\n"; // Test 4: Multiple channels echo "Test 4: Multiple Channels\n"; echo "=========================\n"; $channelA = Channel::fromString('channel_a'); $channelB = Channel::fromString('channel_b'); $channelC = Channel::fromString('channel_c'); // Subscribe to multiple channels $listenerService->listen($channelA); $listenerService->listen($channelB); $listenerService->listen($channelC); echo "✅ Listener: Subscribed to 3 channels\n"; // Send notifications to all channels $senderService->notify($channelA, NotificationPayload::fromString('message-A')); $senderService->notify($channelB, NotificationPayload::fromString('message-B')); $senderService->notify($channelC, NotificationPayload::fromString('message-C')); echo "✅ Sender: Sent 3 notifications\n"; // Poll for all notifications $notifications = $listenerService->poll(); echo (count($notifications) === 3) ? "✅ Listener: Received all 3 notifications\n" : "❌ Should have received 3 notifications (got " . count($notifications) . ")\n"; // Verify channels $channels = array_map(fn($n) => $n->getChannelName(), $notifications); sort($channels); $expected = ['channel_a', 'channel_b', 'channel_c']; echo ($channels === $expected) ? "✅ All channel names correct\n" : "❌ Channel names should match\n"; $listenerService->unlistenAll(); echo "\n"; // Test 5: No notifications when not listening echo "Test 5: No Notifications When Not Listening\n"; echo "============================================\n"; $channel5 = Channel::fromString('ignored'); // Send without listener $senderService->notify($channel5, NotificationPayload::fromString('should-be-ignored')); echo "✅ Sender: Sent notification (no active listener)\n"; // Try to receive (should be empty) $listenerService->listen($channel5); $notifications = $listenerService->poll(); echo (count($notifications) === 0) ? "✅ Listener: Correctly received no notifications (subscribed AFTER send)\n" : "❌ Should not have received notifications\n"; $listenerService->unlisten($channel5); echo "\n"; // Test 6: waitForOne with timeout echo "Test 6: waitForOne() With Quick Response\n"; echo "=========================================\n"; $channel6 = Channel::fromString('wait_test'); $listenerService->listen($channel6); echo "✅ Listener: Subscribed to channel '{$channel6}'\n"; // Send notification in "background" (same process for test) $senderService->notify($channel6, NotificationPayload::fromString('quick-response')); echo "✅ Sender: Sent notification\n"; // Wait for single notification (should return immediately) $start = microtime(true); $notification = $listenerService->waitForOne(5); $duration = microtime(true) - $start; echo ($notification !== null) ? "✅ Listener: Received notification via waitForOne()\n" : "❌ Should have received notification\n"; echo " Wait duration: " . round($duration * 1000, 2) . "ms\n"; echo ($duration < 1.0) ? "✅ Returned quickly (no timeout)\n" : "⚠️ Took longer than expected\n"; if ($notification !== null) { echo " Payload: '{$notification->getPayloadString()}'\n"; } $listenerService->unlisten($channel6); echo "\n"; // Test 7: Channel validation echo "Test 7: Channel Name Validation\n"; echo "================================\n"; // Valid channel names try { Channel::fromString('valid_channel'); echo "✅ Valid channel name accepted: 'valid_channel'\n"; } catch (\Exception $e) { echo "❌ Should accept valid channel name\n"; } try { Channel::fromString('channel123'); echo "✅ Valid channel name accepted: 'channel123'\n"; } catch (\Exception $e) { echo "❌ Should accept valid channel name\n"; } // Invalid channel names try { Channel::fromString(''); echo "❌ Should reject empty channel name\n"; } catch (\InvalidArgumentException $e) { echo "✅ Correctly rejected empty channel name\n"; } try { Channel::fromString('123invalid'); // Cannot start with number echo "❌ Should reject channel name starting with number\n"; } catch (\InvalidArgumentException $e) { echo "✅ Correctly rejected channel name starting with number\n"; } try { Channel::fromString(str_repeat('a', 64)); // Too long echo "❌ Should reject channel name > 63 characters\n"; } catch (\InvalidArgumentException $e) { echo "✅ Correctly rejected channel name > 63 characters\n"; } echo "\n"; // Test 8: Payload size validation echo "Test 8: Payload Size Validation\n"; echo "================================\n"; // Small payload (valid) try { $smallPayload = NotificationPayload::fromString('small data'); echo "✅ Small payload accepted\n"; } catch (\Exception $e) { echo "❌ Should accept small payload\n"; } // Large payload (max 8000 bytes) try { $largeData = str_repeat('x', 8000); $largePayload = NotificationPayload::fromString($largeData); echo "✅ 8000-byte payload accepted (PostgreSQL limit)\n"; } catch (\Exception $e) { echo "❌ Should accept payload up to 8000 bytes\n"; } // Too large payload (> 8000 bytes) try { $tooLargeData = str_repeat('x', 8001); $tooLargePayload = NotificationPayload::fromString($tooLargeData); echo "❌ Should reject payload > 8000 bytes\n"; } catch (\InvalidArgumentException $e) { echo "✅ Correctly rejected payload > 8000 bytes\n"; } echo "\n"; // Test 9: forEvent() channel naming convention echo "Test 9: forEvent() Channel Naming Convention\n"; echo "=============================================\n"; $eventChannel = Channel::forEvent('user', 'registered'); echo "✅ Event channel created: '{$eventChannel}'\n"; echo ($eventChannel->name === 'user_registered') ? "✅ Channel name follows convention: namespace_eventname\n" : "❌ Should follow convention\n"; $listenerService->listen($eventChannel); $senderService->notifyWithData($eventChannel, ['user_id' => 999]); $notifications = $listenerService->poll(); echo (count($notifications) === 1) ? "✅ Event-based channel works\n" : "❌ Should receive notification\n"; $listenerService->unlisten($eventChannel); echo "\n"; echo "✅ All LISTEN/NOTIFY tests passed!\n"; echo "\nSummary:\n"; echo "========\n"; echo "✅ Basic NOTIFY without payload works\n"; echo "✅ NOTIFY with string payload works\n"; echo "✅ NOTIFY with JSON payload works\n"; echo "✅ Multiple channel subscription works\n"; echo "✅ Notifications only received when listening\n"; echo "✅ waitForOne() blocking receive works\n"; echo "✅ Channel name validation works\n"; echo "✅ Payload size validation works\n"; echo "✅ forEvent() naming convention works\n"; } catch (\Exception $e) { echo "❌ Error: " . $e->getMessage() . "\n"; echo "Stack trace:\n" . $e->getTraceAsString() . "\n"; exit(1); }