From 2ede7cdb04225721970306f16298cdaa10bece2f Mon Sep 17 00:00:00 2001 From: Exanlv <51094537+Exanlv@users.noreply.github.com> Date: Sun, 14 Jul 2024 13:23:10 +0200 Subject: [PATCH] Feature/79 1006 handling (#95) * Rework reconnecting/resuming * Privatize stopping automatic heartbeats * Add testing + refactors * cs * Restore meta & raw eventer getters * Stop awaiting heartbeat at disconnect * Add handling for undocumented 1006 * Change error messages to reflect reality more accurately --- fakes/RetrierFake.php | 16 ++ fakes/WebsocketFake.php | 34 +++ src/Constants/GatewayCloseCodes.php | 95 +++++++ src/Constants/WebsocketEvents.php | 1 + src/Discord.php | 2 +- src/Gateway/Connection.php | 111 ++++++-- src/Gateway/ConnectionInterface.php | 5 +- src/Gateway/Handlers/InvalidSessionEvent.php | 30 +- .../Meta/UnacknowledgedHeartbeatEvent.php | 12 +- src/Gateway/Handlers/ReconnectEvent.php | 15 +- .../RecoverableInvalidSessionEvent.php | 15 +- .../Handlers/Traits/ReconnectsToGateway.php | 75 ----- src/Websocket.php | 5 +- src/WebsocketInterface.php | 17 ++ tests/Gateway/ConnectionTest.php | 268 +++++++++++++++--- .../HeartbeatAcknowledgedEventTest.php | 6 - .../Handlers/IdentifyHelloEventTest.php | 6 - .../Handlers/IdentifyResumeEventTest.php | 6 - .../Handlers/InvalidSessionEventTest.php | 90 +----- .../Meta/UnacknowledgedHeartbeatEventTest.php | 163 +---------- tests/Gateway/Handlers/ReconnectEventTest.php | 171 +---------- .../RecoverableInvalidSessionEventTest.php | 173 +---------- .../Handlers/RequestHeartbeatEventTest.php | 5 - 23 files changed, 543 insertions(+), 778 deletions(-) create mode 100644 fakes/RetrierFake.php create mode 100644 fakes/WebsocketFake.php create mode 100644 src/Constants/GatewayCloseCodes.php delete mode 100644 src/Gateway/Handlers/Traits/ReconnectsToGateway.php create mode 100644 src/WebsocketInterface.php diff --git a/fakes/RetrierFake.php b/fakes/RetrierFake.php new file mode 100644 index 00000000..0721e51c --- /dev/null +++ b/fakes/RetrierFake.php @@ -0,0 +1,16 @@ +openings[] = $url; + + return PromiseFake::get(); + } + + public function close(int $code, string $reason): void + { + } + + public function send(string $message, bool $useBucket = true): void + { + } + + public function sendAsJson(array|JsonSerializable $item, bool $useBucket): void + { + } +} diff --git a/src/Constants/GatewayCloseCodes.php b/src/Constants/GatewayCloseCodes.php new file mode 100644 index 00000000..84bee9d9 --- /dev/null +++ b/src/Constants/GatewayCloseCodes.php @@ -0,0 +1,95 @@ + 'Library instantiated: Reconnect required', + 1003 => 'Library instantiated: Resume required', + + 1006 => 'Underlying connection closed', + 4000 => 'Unknown error', + 4001 => 'Unknown opcode', + 4002 => 'Decode error', + 4003 => 'Not authenticated', + 4004 => 'Authentication failed', + 4005 => 'Already authenticated', + 4007 => 'Invalid sequence', + 4008 => 'Rate limited', + 4009 => 'Session timed out', + 4010 => 'Invalid shard', + 4011 => 'Sharding required', + 4012 => 'Invalid API version', + 4013 => 'Invalid intents', + 4015 => 'Disallowed intents', + ]; + + final public const USER_ERROR = [ + 1001 => false, + 1003 => false, + + 1006 => false, + 4000 => true, + 4001 => false, + 4002 => false, + 4003 => false, + 4004 => true, + 4005 => false, + 4007 => false, + 4008 => false, + 4009 => false, + 4010 => true, + 4011 => true, + 4012 => false, + 4013 => true, + 4014 => true, + ]; + + final public const RECOVERABLE = [ + 1001 => true, + 1003 => true, + + 1006 => true, + 4000 => true, + 4001 => true, + 4002 => true, + 4003 => true, + 4004 => false, + 4005 => true, + 4007 => true, + 4008 => true, + 4009 => true, + 4010 => false, + 4011 => false, + 4012 => false, + 4013 => false, + 4014 => false, + ]; + + final public const RESUMABLE = [ + 1001 => false, + 1003 => true, + + 1006 => false, + 4000 => true, + 4001 => true, + 4002 => true, + 4003 => false, + 4004 => false, + 4005 => true, + 4007 => false, + 4008 => true, + 4009 => false, + 4010 => false, + 4011 => false, + 4012 => false, + 4013 => false, + 4014 => false, + ]; +} diff --git a/src/Constants/WebsocketEvents.php b/src/Constants/WebsocketEvents.php index 905a6229..525c8a88 100644 --- a/src/Constants/WebsocketEvents.php +++ b/src/Constants/WebsocketEvents.php @@ -7,4 +7,5 @@ class WebsocketEvents { final public const MESSAGE = 'MESSAGE'; + final public const CLOSE = 'CLOSE'; } diff --git a/src/Discord.php b/src/Discord.php index 4490ce2e..722d9894 100644 --- a/src/Discord.php +++ b/src/Discord.php @@ -53,8 +53,8 @@ public function withGateway( $this->token, $intents, $this->mapper, + new Websocket($timeout, $this->logger, [$this->token => '::token::']), $this->logger, - $timeout ); return $this; diff --git a/src/Gateway/Connection.php b/src/Gateway/Connection.php index ffea4d0e..23b9d558 100644 --- a/src/Gateway/Connection.php +++ b/src/Gateway/Connection.php @@ -5,15 +5,18 @@ namespace Ragnarok\Fenrir\Gateway; use Exan\Eventer\Eventer; +use Exan\Retrier\Retrier; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Ragnarok\Fenrir\Bitwise\Bitwise; +use Ragnarok\Fenrir\Constants\GatewayCloseCodes; use Ragnarok\Fenrir\Constants\MetaEvents; use Ragnarok\Fenrir\Constants\WebsocketEvents; use Ragnarok\Fenrir\DataMapper; use Ragnarok\Fenrir\EventHandler; use Ragnarok\Fenrir\Gateway\Handlers\HeartbeatAcknowledgedEvent; use Ragnarok\Fenrir\Gateway\Handlers\IdentifyHelloEvent; +use Ragnarok\Fenrir\Gateway\Handlers\IdentifyResumeEvent; use Ragnarok\Fenrir\Gateway\Handlers\InvalidSessionEvent; use Ragnarok\Fenrir\Gateway\Handlers\Meta\UnacknowledgedHeartbeatEvent; use Ragnarok\Fenrir\Gateway\Handlers\PassthroughEvent; @@ -23,7 +26,7 @@ use Ragnarok\Fenrir\Gateway\Handlers\RequestHeartbeatEvent; use Ragnarok\Fenrir\Gateway\Helpers\PresenceUpdateBuilder; use Ragnarok\Fenrir\Gateway\Objects\Payload; -use Ragnarok\Fenrir\Websocket; +use Ragnarok\Fenrir\WebsocketInterface; use Ratchet\RFC6455\Messaging\MessageInterface; use React\EventLoop\LoopInterface; use React\EventLoop\TimerInterface; @@ -34,21 +37,19 @@ */ class Connection implements ConnectionInterface { + public const DISCORD_VERSION = 10; public const DEFAULT_WEBSOCKET_URL = 'wss://gateway.discord.gg/'; - private const QUERY_DATA = ['v' => 10]; + + private const QUERY_DATA = ['v' => self::DISCORD_VERSION]; private const HEARTBEAT_ACK_TIMEOUT = 2.5; private ?int $sequence = null; - private Websocket $websocket; - private ?string $sessionId = null; private ?string $resumeUrl = null; public EventHandler $events; - public Eventer $raw; - public Eventer $meta; private TimerInterface $heartbeatTimer; private TimerInterface $unacknowledgedHeartbeatTimer; @@ -60,15 +61,14 @@ public function __construct( private string $token, private Bitwise $intents, private DataMapper $mapper, + private WebsocketInterface $websocket, private LoggerInterface $logger = new NullLogger(), - int $timeout = 10, + private Eventer $raw = new Eventer(), + private Eventer $meta = new Eventer(), + private Retrier $retrier = new Retrier(), ) { - $this->websocket = new Websocket($timeout, $logger, [$this->token => '::token::']); $this->events = new EventHandler($mapper); - $this->raw = new Eventer(); - $this->meta = new Eventer(); - $this->websocket->on(WebsocketEvents::MESSAGE, function (MessageInterface $message) { $parsedMessage = json_decode((string) $message, depth: 1024); if ($parsedMessage === null) { @@ -80,6 +80,8 @@ public function __construct( $this->raw->emit((string) $payload->op, [$this, $payload, $this->logger]); }); + $this->websocket->on(WebsocketEvents::CLOSE, $this->handleClose(...)); + $this->registerEvents(); } @@ -100,6 +102,69 @@ private function registerEvents(): void $this->meta->register(UnacknowledgedHeartbeatEvent::class); } + private function handleClose(int $code, string $reason): void + { + $this->stopAutomaticHeartbeats(); + + $description = GatewayCloseCodes::DESCRIPTIONS[$code] ?? sprintf('Unknown error code %d - %s', $code, $reason); + $isUserError = GatewayCloseCodes::USER_ERROR[$code] ?? false; + $isRecoverable = GatewayCloseCodes::RECOVERABLE[$code] ?? false; + $isResumable = GatewayCloseCodes::RECOVERABLE[$code] ?? false; + + $message = $description . ' ' + . ( + $isUserError + ? 'This is likely a userspace error.' + : 'This is likely caused by Discord or the library. If you suspect the latter, please report it on Github.' + ); + + $context = ['code' => $code, 'reason' => $reason]; + + if (!$isRecoverable) { + $this->logger->emergency($message, $context); + + $this->loop->stop(); + return; + } + + $this->logger->error($message, $context); + + if ($isResumable && $this->meetsResumeRequirements()) { + $this->resumeConnection(); + return; + } + + $this->sequence = null; + $this->startNewConnection(); + } + + private function resumeConnection(): void + { + $this->retrier->retry(3, function ($i) { + $this->logger->info(sprintf('Reconnecting and resuming session, attempt %d.', $i)); + + return $this->connect($this->resumeUrl)->then(function () { + $this->raw->registerOnce(IdentifyResumeEvent::class); + }); + }); + } + + private function startNewConnection(): void + { + $this->retrier->retry(3, function ($i) { + $this->logger->info(sprintf('Forceful reconnection attempt %d.', $i)); + + return $this->open()->then(function () { + $this->raw->registerOnce(IdentifyHelloEvent::class); + }); + }); + } + + public function meetsResumeRequirements(): bool + { + return !(is_null($this->resumeUrl) || is_null($this->sessionId)); + } + public function getDefaultUrl(): string { return self::DEFAULT_WEBSOCKET_URL; @@ -115,11 +180,6 @@ public function setSequence(int $sequence): void $this->sequence = $sequence; } - public function resetSequence(): void - { - $this->sequence = null; - } - public function connect(string $url): ExtendedPromiseInterface { $url .= '?' . http_build_query(self::QUERY_DATA); @@ -129,6 +189,8 @@ public function connect(string $url): ExtendedPromiseInterface public function disconnect(int $code, string $reason): void { + $this->cancelHeartbeatAcknowledgement(); + $this->websocket->close($code, $reason); } @@ -171,7 +233,14 @@ private function expectHeartbeatAcknowledgement(): void public function acknowledgeHeartbeat(): void { - $this->loop->cancelTimer($this->unacknowledgedHeartbeatTimer); + $this->cancelHeartbeatAcknowledgement(); + } + + private function cancelHeartbeatAcknowledgement(): void + { + if (isset($this->unacknowledgedHeartbeatTimer)) { + $this->loop->cancelTimer($this->unacknowledgedHeartbeatTimer); + } } public function startAutomaticHeartbeats(int $ms): void @@ -180,10 +249,12 @@ public function startAutomaticHeartbeats(int $ms): void $this->logger->debug('Started heartbeat timer', ['ms' => $ms]); } - public function stopAutomaticHeartbeats(): void + private function stopAutomaticHeartbeats(): void { - $this->loop->cancelTimer($this->heartbeatTimer); - $this->logger->debug('Cancelled heartbeat timer'); + if (isset($this->heartbeatTimer)) { + $this->loop->cancelTimer($this->heartbeatTimer); + $this->logger->debug('Cancelled heartbeat timer'); + } } public function getEventHandler(): EventHandler diff --git a/src/Gateway/ConnectionInterface.php b/src/Gateway/ConnectionInterface.php index 9d2e88c7..3fab62f9 100644 --- a/src/Gateway/ConnectionInterface.php +++ b/src/Gateway/ConnectionInterface.php @@ -15,7 +15,6 @@ public function getDefaultUrl(): string; public function getSequence(): ?int; public function setSequence(int $sequence); - public function resetSequence(): void; public function connect(string $url): ExtendedPromiseInterface; public function disconnect(int $code, string $reason): void; @@ -29,14 +28,16 @@ public function getResumeUrl(): ?string; public function sendHeartbeat(): void; public function acknowledgeHeartbeat(): void; public function startAutomaticHeartbeats(int $ms): void; - public function stopAutomaticHeartbeats(): void; public function getEventHandler(): EventHandler; public function getRawHandler(): Eventer; public function getMetaHandler(): Eventer; + public function identify(): void; public function resume(): void; + public function meetsResumeRequirements(): bool; + public function updatePresence(PresenceUpdateBuilder $presenceUpdate): void; } diff --git a/src/Gateway/Handlers/InvalidSessionEvent.php b/src/Gateway/Handlers/InvalidSessionEvent.php index 72803356..cf24bce9 100644 --- a/src/Gateway/Handlers/InvalidSessionEvent.php +++ b/src/Gateway/Handlers/InvalidSessionEvent.php @@ -4,9 +4,9 @@ namespace Ragnarok\Fenrir\Gateway\Handlers; +use Ragnarok\Fenrir\Constants\GatewayCloseCodes; use Ragnarok\Fenrir\Constants\OpCodes; use Ragnarok\Fenrir\Gateway\Objects\Payload; -use Throwable; class InvalidSessionEvent extends GatewayEvent { @@ -15,35 +15,21 @@ public static function getEventName(): string return OpCodes::INVALID_SESSION; } - public static function isRecoverable(Payload $payload): bool + public function isRecoverable(): bool { - return isset($payload->d) && $payload->d === true; + return isset($this->payload->d) && $this->payload->d === true; } public function filter(): bool { - return !self::isRecoverable($this->payload); + return !$this->isRecoverable(); } public function execute(): void { - $this->connection->stopAutomaticHeartbeats(); - - $reason = 'Invalid session, attempting to establish new connection'; - $this->logger->warning($reason); - $this->connection->disconnect(1001, $reason); - $this->connection->resetSequence(); - - $this->retrier->retry(3, function (int $i) { - $this->logger->warning(sprintf('Forcefully reconnecting after invalid session, attempt %d.', $i)); - - return $this->connection->connect( - $this->connection->getDefaultUrl() - ); - })->done(function () { - $this->connection->getRawHandler()->registerOnce(IdentifyHelloEvent::class); - }, function (Throwable $e) { - $this->logger->critical('Unable to establish a new connection to Discord.', [$e]); - }); + $this->connection->disconnect( + GatewayCloseCodes::LIB_INSTANTIATED_RECONNECT, + 'Invalid session, attempting to establish new connection' + ); } } diff --git a/src/Gateway/Handlers/Meta/UnacknowledgedHeartbeatEvent.php b/src/Gateway/Handlers/Meta/UnacknowledgedHeartbeatEvent.php index 3e56e489..90132c49 100644 --- a/src/Gateway/Handlers/Meta/UnacknowledgedHeartbeatEvent.php +++ b/src/Gateway/Handlers/Meta/UnacknowledgedHeartbeatEvent.php @@ -4,20 +4,16 @@ namespace Ragnarok\Fenrir\Gateway\Handlers\Meta; -use Exan\Retrier\Retrier; +use Ragnarok\Fenrir\Constants\GatewayCloseCodes; use Ragnarok\Fenrir\Gateway\Events\Meta\UnacknowledgedHeartbeatEvent as BaseUnacknowledgedHeartbeatEvent; -use Ragnarok\Fenrir\Gateway\Handlers\Traits\ReconnectsToGateway; class UnacknowledgedHeartbeatEvent extends BaseUnacknowledgedHeartbeatEvent { - use ReconnectsToGateway; - public function execute(): void { - $this->reconnect( - $this->connection, - $this->logger, - new Retrier(), + $this->connection->disconnect( + GatewayCloseCodes::LIB_INSTANTIATED_RESUME, + 'Unacknowledged heartbeat, attempting resume' ); } } diff --git a/src/Gateway/Handlers/ReconnectEvent.php b/src/Gateway/Handlers/ReconnectEvent.php index 3df26639..277d267f 100644 --- a/src/Gateway/Handlers/ReconnectEvent.php +++ b/src/Gateway/Handlers/ReconnectEvent.php @@ -4,13 +4,11 @@ namespace Ragnarok\Fenrir\Gateway\Handlers; +use Ragnarok\Fenrir\Constants\GatewayCloseCodes; use Ragnarok\Fenrir\Constants\OpCodes; -use Ragnarok\Fenrir\Gateway\Handlers\Traits\ReconnectsToGateway; class ReconnectEvent extends GatewayEvent { - use ReconnectsToGateway; - public static function getEventName(): string { return OpCodes::RECONNECT; @@ -18,12 +16,9 @@ public static function getEventName(): string public function execute(): void { - $this->reconnect( - $this->connection, - $this->logger, - $this->retrier, - )->done(function () { - $this->logger->info('Finished reconnecting'); - }); + $this->connection->disconnect( + GatewayCloseCodes::LIB_INSTANTIATED_RESUME, + 'Received opcode 7, attempting resume' + ); } } diff --git a/src/Gateway/Handlers/RecoverableInvalidSessionEvent.php b/src/Gateway/Handlers/RecoverableInvalidSessionEvent.php index e006f6fd..7e4ff4c5 100644 --- a/src/Gateway/Handlers/RecoverableInvalidSessionEvent.php +++ b/src/Gateway/Handlers/RecoverableInvalidSessionEvent.php @@ -4,17 +4,20 @@ namespace Ragnarok\Fenrir\Gateway\Handlers; -use Ragnarok\Fenrir\Constants\OpCodes; +use Ragnarok\Fenrir\Constants\GatewayCloseCodes; -class RecoverableInvalidSessionEvent extends ReconnectEvent +class RecoverableInvalidSessionEvent extends InvalidSessionEvent { - public static function getEventName(): string + public function filter(): bool { - return OpCodes::INVALID_SESSION; + return $this->isRecoverable(); } - public function filter(): bool + public function execute(): void { - return InvalidSessionEvent::isRecoverable($this->payload); + $this->connection->disconnect( + GatewayCloseCodes::LIB_INSTANTIATED_RESUME, + 'Received opcode 9 with recoverable indicator, attempting resume' + ); } } diff --git a/src/Gateway/Handlers/Traits/ReconnectsToGateway.php b/src/Gateway/Handlers/Traits/ReconnectsToGateway.php deleted file mode 100644 index 1d81de78..00000000 --- a/src/Gateway/Handlers/Traits/ReconnectsToGateway.php +++ /dev/null @@ -1,75 +0,0 @@ -stopAutomaticHeartbeats(); - - if (is_null($connection->getResumeUrl()) || is_null($connection->getSessionId())) { - $reason = 'Unable to reconnect and resume session, attempting forceful reconnect'; - $logger->warning($reason); - $connection->disconnect(1001, $reason); - - return $this->new($connection, $logger, $retrier); - } - - $reason = 'Attempting reconnect'; - $logger->warning($reason); - $connection->disconnect(1004, $reason); - - return $this->resume( - $connection, - $logger, - $retrier - )->otherwise(function () use ($connection, $logger, $retrier) { - $logger->error('Failed to reconnect and resume session, attempting forceful reconnect'); - - return $this->new($connection, $logger, $retrier); - }); - } - - private function resume( - ConnectionInterface $connection, - LoggerInterface $logger, - Retrier $retrier - ): ExtendedPromiseInterface { - return $retrier->retry(3, function (int $i) use ($connection, $logger) { - $logger->warning(sprintf('Reconnecting and resuming session, attempt %d.', $i)); - - return $connection->connect($connection->getResumeUrl())->then(function () use ($connection) { - $connection->getRawHandler()->registerOnce(IdentifyResumeEvent::class); - }); - }); - } - - private function new( - ConnectionInterface $connection, - LoggerInterface $logger, - Retrier $retrier - ): ExtendedPromiseInterface { - return $retrier->retry(3, function (int $i) use ($connection, $logger) { - $logger->warning(sprintf('Forceful rennection attempt %d.', $i)); - - return $connection->connect( - $connection->getDefaultUrl() - )->then(function () use ($connection) { - $connection->getRawHandler()->registerOnce(IdentifyHelloEvent::class); - }); - }); - } -} diff --git a/src/Websocket.php b/src/Websocket.php index 03d6e202..78deaf50 100644 --- a/src/Websocket.php +++ b/src/Websocket.php @@ -19,7 +19,7 @@ use React\Promise\Promise; use React\Socket\Connector as SocketConnector; -class Websocket extends EventEmitter +class Websocket extends EventEmitter implements WebsocketInterface { private Connector $connector; @@ -69,7 +69,8 @@ public function open(string $url): ExtendedPromiseInterface }); $connection->on('close', function (int $code, string $reason = '') { - $this->logger->info('Connection closed', ['code' => $code, 'reason' => $reason]); + $this->logger->debug('Connection closed', ['code' => $code, 'reason' => $reason]); + $this->emit(WebsocketEvents::CLOSE, [$code, $reason]); }); $resolver(); diff --git a/src/WebsocketInterface.php b/src/WebsocketInterface.php new file mode 100644 index 00000000..41dea06d --- /dev/null +++ b/src/WebsocketInterface.php @@ -0,0 +1,17 @@ +assertEquals('wss://gateway.discord.gg/', $connection->getDefaultUrl()); @@ -48,16 +54,14 @@ public function testSequence(): void Mockery::mock(LoopInterface::class), '::token::', new Bitwise(), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), ); $this->assertNull($connection->getSequence()); $connection->setSequence(123); $this->assertEquals(123, $connection->getSequence()); - - $connection->resetSequence(); - $this->assertNull($connection->getSequence()); } public function testConnect(): void @@ -66,7 +70,8 @@ public function testConnect(): void Mockery::mock(LoopInterface::class), '::token::', new Bitwise(), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), ); /** @var MockInterface&Websocket */ @@ -88,7 +93,8 @@ public function testDisconnect(): void Mockery::mock(LoopInterface::class), '::token::', new Bitwise(), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), ); /** @var MockInterface&Websocket */ @@ -109,7 +115,8 @@ public function testSessionId(): void Mockery::mock(LoopInterface::class), '::token::', new Bitwise(), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), ); $this->assertNull($connection->getSessionId()); @@ -124,7 +131,8 @@ public function testResumeUrl(): void Mockery::mock(LoopInterface::class), '::token::', new Bitwise(), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), ); $this->assertNull($connection->getResumeUrl()); @@ -148,7 +156,8 @@ public function testSendHeartbeat(): void $loop, '::token::', new Bitwise(), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), ); /** @var MockInterface&Websocket */ @@ -187,28 +196,25 @@ public function testItEmitsAnEventForMissedHeartbeatAcknowledgement(): void }) ->once(); + /** @var Eventer&MockInterface */ + $meta = Mockery::mock(Eventer::class); + + $meta->shouldReceive() + ->register() + ->withAnyArgs(); + $logger = new NullLogger(); $connection = new Connection( $loop, '::token::', new Bitwise(), new DataMapper(new NullLogger()), - $logger + new WebsocketFake(), + meta: $meta, + logger: $logger ); - /** @var MockInterface&Websocket */ - $websocket = Mockery::mock(Websocket::class); - (new ReflectionProperty($connection, 'websocket'))->setValue($connection, $websocket); - - $websocket->expects() - ->sendAsJson() - ->withAnyArgs() - ->once(); - - /** @var Eventer&MockInterface */ - $connection->meta = Mockery::mock(Eventer::class); - - $connection->meta->expects() + $meta->expects() ->emit() ->with(MetaEvents::UNACKNOWLEDGED_HEARTBEAT, [$connection, $logger]) ->once(); @@ -232,7 +238,8 @@ public function testItCanAcknowledgeHeartbeats(): void $loop, '::token::', new Bitwise(), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), ); /** @var MockInterface&Websocket */ @@ -282,6 +289,7 @@ public function testItCanSendHeartbeatsAutomatically(): void '::token::', new Bitwise(), new DataMapper(new NullLogger()), + new WebsocketFake(), ); /** @var MockInterface&Websocket */ @@ -294,13 +302,6 @@ public function testItCanSendHeartbeatsAutomatically(): void ->once(); $connection->startAutomaticHeartbeats(10000); - - $loop->expects() - ->cancelTimer() - ->with($timer) - ->once(); - - $connection->stopAutomaticHeartbeats(); } public function testItReturnsEventHandlers(): void @@ -310,11 +311,10 @@ public function testItReturnsEventHandlers(): void '::token::', new Bitwise(), new DataMapper(new NullLogger()), + new WebsocketFake(), ); $this->assertInstanceOf(EventHandler::class, $connection->getEventHandler()); - $this->assertInstanceOf(Eventer::class, $connection->getRawHandler()); - $this->assertInstanceOf(Eventer::class, $connection->getMetaHandler()); } public function testItIdentifies(): void @@ -323,7 +323,8 @@ public function testItIdentifies(): void Mockery::mock(LoopInterface::class), '::token::', new Bitwise(123), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), ); /** @var MockInterface&Websocket */ @@ -350,7 +351,8 @@ public function testItIdentifiesWithShards(): void Mockery::mock(LoopInterface::class), '::token::', new Bitwise(123), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), ); $connection->shard(new Shard(1, 16)); @@ -380,7 +382,8 @@ public function testItResumes(): void Mockery::mock(LoopInterface::class), '::token::', new Bitwise(123), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), ); /** @var MockInterface&Websocket */ @@ -426,7 +429,8 @@ public function testOpen(): void Mockery::mock(LoopInterface::class), '::token::', new Bitwise(), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), ); /** @var MockInterface&Websocket */ @@ -447,19 +451,29 @@ public function testOpen(): void public function testItEmitsGatewayMessagesAsEvents(): void { + /** @var Eventer&MockInterface */ + $raw = Mockery::mock(Eventer::class); + + $raw->shouldReceive() + ->register() + ->withAnyArgs(); + + $raw->shouldReceive() + ->registerOnce() + ->withAnyArgs(); + $connection = new Connection( Mockery::mock(LoopInterface::class), '::token::', new Bitwise(), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), + raw: $raw, ); $websocket = (new ReflectionProperty($connection, 'websocket'))->getValue($connection); - /** @var Eventer&MockInterface */ - $connection->raw = Mockery::mock(Eventer::class); - - $connection->raw->expects() + $raw->expects() ->emit() ->with('1', Mockery::on(function ($args) use ($connection) { $this->assertEquals($connection, $args[0]); @@ -485,7 +499,8 @@ public function testItSendsPresenceUpdates() Mockery::mock(LoopInterface::class), '::token::', new Bitwise(123), - new DataMapper(new NullLogger()) + new DataMapper(new NullLogger()), + new WebsocketFake(), ); /** @var MockInterface&Websocket */ @@ -510,4 +525,169 @@ public function testItSendsPresenceUpdates() $connection->updatePresence($presenceUpdate); } + + /** + * @dataProvider reconnectCloseCodesProvider + */ + public function testItReconnectsWhenWebsocketConnectionClosedWithCertainCodes(int $code) + { + $websocket = new WebsocketFake(); + + $raw = Mockery::mock(Eventer::class); + + $raw->shouldReceive() + ->register() + ->withAnyArgs(); + + $raw->shouldReceive() + ->registerOnce() + ->with(IdentifyHelloEvent::class) + ->twice(); + + new Connection( + Mockery::mock(LoopInterface::class), + '::token::', + new Bitwise(1), + DataMapperFake::get(), + $websocket, + raw: $raw, + retrier: new RetrierFake(), + ); + + $websocket->emit(WebsocketEvents::CLOSE, [$code, 'reason']); + + $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?v=' . Connection::DISCORD_VERSION], $websocket->openings); + } + + public static function reconnectCloseCodesProvider(): array + { + return [ + [1001], + [4003], + [4007], + [4009], + ]; + } + + /** + * @dataProvider resumeCloseCodesProvider + */ + public function testItResumesWhenWebsocketConnectionClosedWithCertainCodes(int $code) + { + $websocket = new WebsocketFake(); + $raw = Mockery::mock(Eventer::class); + + $raw->shouldReceive() + ->register() + ->withAnyArgs(); + + $raw->shouldReceive() + ->registerOnce() + ->with(IdentifyHelloEvent::class) + ->once(); + + $raw->shouldReceive() + ->registerOnce() + ->with(IdentifyResumeEvent::class) + ->once(); + + $connection = new Connection( + Mockery::mock(LoopInterface::class), + '::token::', + new Bitwise(1), + DataMapperFake::get(), + $websocket, + raw: $raw, + retrier: new RetrierFake(), + ); + + $connection->setResumeUrl('::resume url::'); + $connection->setSessionId('::session id::'); + + $websocket->emit(WebsocketEvents::CLOSE, [$code, 'reason']); + + $this->assertEquals(['::resume url::?v=' . Connection::DISCORD_VERSION], $websocket->openings); + } + + /** + * @dataProvider resumeCloseCodesProvider + */ + public function testItReconnectsIfMissingResumeUrl(int $code) + { + $websocket = new WebsocketFake(); + + $raw = Mockery::mock(Eventer::class); + + $raw->shouldReceive() + ->register() + ->withAnyArgs(); + + $raw->shouldReceive() + ->registerOnce() + ->with(IdentifyHelloEvent::class) + ->twice(); + + $connection = new Connection( + Mockery::mock(LoopInterface::class), + '::token::', + new Bitwise(1), + DataMapperFake::get(), + $websocket, + raw: $raw, + retrier: new RetrierFake(), + ); + + $connection->setSessionId('::session id::'); + + $websocket->emit(WebsocketEvents::CLOSE, [$code, 'reason']); + + $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?v=' . Connection::DISCORD_VERSION], $websocket->openings); + } + + /** + * @dataProvider resumeCloseCodesProvider + */ + public function testItReconnectsIfMissingSessionId(int $code) + { + $websocket = new WebsocketFake(); + + $raw = Mockery::mock(Eventer::class); + + $raw->shouldReceive() + ->register() + ->withAnyArgs(); + + $raw->shouldReceive() + ->registerOnce() + ->with(IdentifyHelloEvent::class) + ->twice(); + + $connection = new Connection( + Mockery::mock(LoopInterface::class), + '::token::', + new Bitwise(1), + DataMapperFake::get(), + $websocket, + raw: $raw, + retrier: new RetrierFake(), + ); + + $connection->setResumeUrl('::resume url::'); + + $websocket->emit(WebsocketEvents::CLOSE, [$code, 'reason']); + + $this->assertEquals([Connection::DEFAULT_WEBSOCKET_URL . '?v=' . Connection::DISCORD_VERSION], $websocket->openings); + } + + public static function resumeCloseCodesProvider(): array + { + return [ + [1003], + [4000], + [4001], + [4002], + [4005], + [4008], + ]; + } } diff --git a/tests/Gateway/Handlers/HeartbeatAcknowledgedEventTest.php b/tests/Gateway/Handlers/HeartbeatAcknowledgedEventTest.php index 418e7f50..b7550ef1 100644 --- a/tests/Gateway/Handlers/HeartbeatAcknowledgedEventTest.php +++ b/tests/Gateway/Handlers/HeartbeatAcknowledgedEventTest.php @@ -8,18 +8,12 @@ use Mockery\Adapter\Phpunit\MockeryTestCase; use Mockery\MockInterface; use Psr\Log\NullLogger; -use Ragnarok\Fenrir\Constants\OpCodes; use Ragnarok\Fenrir\Gateway\ConnectionInterface; use Ragnarok\Fenrir\Gateway\Handlers\HeartbeatAcknowledgedEvent; use Ragnarok\Fenrir\Gateway\Objects\Payload; class HeartbeatAcknowledgedEventTest extends MockeryTestCase { - public function testItListensTo11(): void - { - $this->assertEquals(OpCodes::HEARTBEAT_ACKNOWLEDGEMENT, HeartbeatAcknowledgedEvent::getEventName()); - } - public function testItAcknowledgesAHeartbeat(): void { /** @var MockInterface&ConnectionInterface */ diff --git a/tests/Gateway/Handlers/IdentifyHelloEventTest.php b/tests/Gateway/Handlers/IdentifyHelloEventTest.php index a0a17895..16f09469 100644 --- a/tests/Gateway/Handlers/IdentifyHelloEventTest.php +++ b/tests/Gateway/Handlers/IdentifyHelloEventTest.php @@ -8,7 +8,6 @@ use Mockery\Adapter\Phpunit\MockeryTestCase; use Mockery\MockInterface; use Psr\Log\NullLogger; -use Ragnarok\Fenrir\Constants\OpCodes; use Ragnarok\Fenrir\DataMapper; use Ragnarok\Fenrir\Gateway\ConnectionInterface; use Ragnarok\Fenrir\Gateway\Handlers\IdentifyHelloEvent; @@ -25,11 +24,6 @@ protected function setUp(): void $this->mapper = new DataMapper(new NullLogger()); } - public function testItListensTo10(): void - { - $this->assertEquals(OpCodes::HELLO, IdentifyHelloEvent::getEventName()); - } - public function testItAcknowledgesAHeartbeat(): void { /** @var MockInterface&ConnectionInterface */ diff --git a/tests/Gateway/Handlers/IdentifyResumeEventTest.php b/tests/Gateway/Handlers/IdentifyResumeEventTest.php index c9645741..7ddb0ed2 100644 --- a/tests/Gateway/Handlers/IdentifyResumeEventTest.php +++ b/tests/Gateway/Handlers/IdentifyResumeEventTest.php @@ -8,7 +8,6 @@ use Mockery\Adapter\Phpunit\MockeryTestCase; use Mockery\MockInterface; use Psr\Log\NullLogger; -use Ragnarok\Fenrir\Constants\OpCodes; use Ragnarok\Fenrir\DataMapper; use Ragnarok\Fenrir\Gateway\ConnectionInterface; use Ragnarok\Fenrir\Gateway\Handlers\IdentifyResumeEvent; @@ -25,11 +24,6 @@ protected function setUp(): void $this->mapper = new DataMapper(new NullLogger()); } - public function testItListensTo10(): void - { - $this->assertEquals(OpCodes::HELLO, IdentifyResumeEvent::getEventName()); - } - public function testItAcknowledgesAHeartbeat(): void { /** @var MockInterface&ConnectionInterface */ diff --git a/tests/Gateway/Handlers/InvalidSessionEventTest.php b/tests/Gateway/Handlers/InvalidSessionEventTest.php index 9b898305..0654f61e 100644 --- a/tests/Gateway/Handlers/InvalidSessionEventTest.php +++ b/tests/Gateway/Handlers/InvalidSessionEventTest.php @@ -4,28 +4,27 @@ namespace Tests\Ragnarok\Fenrir\Gateway\Handlers; -use Exan\Eventer\Eventer; -use Exception; -use Fakes\Ragnarok\Fenrir\PromiseFake; use Mockery; use Mockery\Adapter\Phpunit\MockeryTestCase; use Mockery\MockInterface; use Psr\Log\NullLogger; +use Ragnarok\Fenrir\Constants\GatewayCloseCodes; use Ragnarok\Fenrir\DataMapper; use Ragnarok\Fenrir\Gateway\ConnectionInterface; -use Ragnarok\Fenrir\Gateway\Handlers\IdentifyHelloEvent; use Ragnarok\Fenrir\Gateway\Handlers\InvalidSessionEvent; use Ragnarok\Fenrir\Gateway\Objects\Payload; class InvalidSessionEventTest extends MockeryTestCase { private DataMapper $mapper; + private ConnectionInterface&MockInterface $connectionInterface; protected function setUp(): void { parent::setUp(); $this->mapper = new DataMapper(new NullLogger()); + $this->connectionInterface = Mockery::mock(ConnectionInterface::class); } /** @@ -60,91 +59,20 @@ public static function listenerDataProvider(): array ]; } - public function testItReconnects(): void + public function testItDisconnectsWithCorrectCode(): void { - /** @var MockInterface&ConnectionInterface */ - $connection = Mockery::mock(ConnectionInterface::class); - $event = new InvalidSessionEvent( - $connection, - $this->mapper->map((object) ['d' => false], Payload::class), + $this->connectionInterface, + $this->mapper->map((object) ['d' => true], Payload::class), new NullLogger(), ); - $connection->expects() - ->stopAutomaticHeartbeats() - ->once(); - - $connection->shouldReceive() + $this->connectionInterface + ->shouldReceive() ->disconnect() - ->with(1001, Mockery::any()) - ->once(); - - $connection->expects() - ->resetSequence() - ->once(); - - $connection->expects() - ->getDefaultUrl() - ->andReturns('::default url::') - ->once(); - - $connection->expects() - ->connect() - ->with('::default url::') - ->andReturns(PromiseFake::get()) - ->once(); - - /** @var MockInterface&Eventer */ - $rawHandler = Mockery::mock(Eventer::class); - $connection->expects() - ->getRawHandler() - ->andReturns($rawHandler) - ->once(); - - $rawHandler->expects() - ->registerOnce() - ->with(IdentifyHelloEvent::class) + ->with(GatewayCloseCodes::LIB_INSTANTIATED_RECONNECT, Mockery::type('string')) ->once(); $event->execute(); } - - public function testItTriesConnectingSeveralTimes(): void - { - /** @var MockInterface&ConnectionInterface */ - $connection = Mockery::mock(ConnectionInterface::class); - - $event = new InvalidSessionEvent( - $connection, - $this->mapper->map((object) ['d' => false], Payload::class), - new NullLogger(), - ); - - $connection->expects() - ->stopAutomaticHeartbeats() - ->once(); - - $connection->shouldReceive() - ->disconnect() - ->with(1001, Mockery::any()) - ->once(); - - $connection->expects() - ->resetSequence() - ->once(); - - $connection->expects() - ->getDefaultUrl() - ->andReturns('::default url::') - ->times(3); - - $connection->expects() - ->connect() - ->with('::default url::') - ->andReturns(PromiseFake::reject(new Exception('Oh no, the connection went wrong :('))) - ->times(3); - - $event->execute(); - } } diff --git a/tests/Gateway/Handlers/Meta/UnacknowledgedHeartbeatEventTest.php b/tests/Gateway/Handlers/Meta/UnacknowledgedHeartbeatEventTest.php index 0380c00c..b29f870b 100644 --- a/tests/Gateway/Handlers/Meta/UnacknowledgedHeartbeatEventTest.php +++ b/tests/Gateway/Handlers/Meta/UnacknowledgedHeartbeatEventTest.php @@ -4,18 +4,12 @@ namespace Tests\Ragnarok\Fenrir\Gateway\Handlers\Meta; -use Exan\Eventer\Eventer; -use Exception; -use Fakes\Ragnarok\Fenrir\PromiseFake; use Mockery; use Mockery\Adapter\Phpunit\MockeryTestCase; -use Mockery\MockInterface; use Psr\Log\NullLogger; +use Ragnarok\Fenrir\Constants\GatewayCloseCodes; use Ragnarok\Fenrir\Constants\MetaEvents; -use Ragnarok\Fenrir\DataMapper; use Ragnarok\Fenrir\Gateway\ConnectionInterface; -use Ragnarok\Fenrir\Gateway\Handlers\IdentifyHelloEvent; -use Ragnarok\Fenrir\Gateway\Handlers\IdentifyResumeEvent; use Ragnarok\Fenrir\Gateway\Handlers\Meta\UnacknowledgedHeartbeatEvent; class UnacknowledgedHeartbeatEventTest extends MockeryTestCase @@ -25,166 +19,21 @@ public function testItListensToUnacknowledgedHeartbeat(): void $this->assertEquals(MetaEvents::UNACKNOWLEDGED_HEARTBEAT, UnacknowledgedHeartbeatEvent::getEventName()); } - public function testItReconnectsToDiscord(): void + public function testItDisconnectsWithRightCode(): void { - /** @var ConnectionInterface&MockInterface */ $connection = Mockery::mock(ConnectionInterface::class); - $connection->expects() - ->stopAutomaticHeartbeats() - ->once(); - - $connection->expects() - ->getResumeUrl() - ->andReturns('::resume url::') - ->twice(); - - $connection->expects() - ->getSessionId() - ->andReturns('::session id::') - ->once(); - - $connection->expects() - ->disconnect() - ->with(1004, Mockery::any()) - ->once(); - - $connection->expects() - ->connect() - ->with('::resume url::') - ->andReturns(PromiseFake::get()) - ->once(); - - /** @var Eventer&MockInterface */ - $rawHandler = Mockery::mock(Eventer::class); - $rawHandler->expects() - ->registerOnce() - ->with(IdentifyResumeEvent::class) - ->once(); - - $connection->expects() - ->getRawHandler() - ->andReturns($rawHandler) - ->once(); - - $event = new UnacknowledgedHeartbeatEvent( - $connection, - new NullLogger(), - ); - - $event->execute(); - - $this->assertTrue($event->filter()); - } - - public function testItOpensAFreshConnectionIfItCantResume(): void - { - /** @var ConnectionInterface&MockInterface */ - $connection = Mockery::mock(ConnectionInterface::class); - - $connection->expects() - ->stopAutomaticHeartbeats() - ->once(); - - $connection->expects() - ->getResumeUrl() - ->andReturns(null) - ->once(); - - $connection->expects() - ->disconnect() - ->with(1001, Mockery::any()) - ->once(); - - $connection->expects() - ->getDefaultUrl() - ->andReturns('::default url::') - ->once(); - - $connection->expects() - ->connect() - ->with('::default url::') - ->andReturns(PromiseFake::get()) - ->once(); - - /** @var Eventer&MockInterface */ - $rawHandler = Mockery::mock(Eventer::class); - $rawHandler->expects() - ->registerOnce() - ->with(IdentifyHelloEvent::class) - ->once(); - - $connection->expects() - ->getRawHandler() - ->andReturns($rawHandler) - ->once(); - $event = new UnacknowledgedHeartbeatEvent( $connection, - new NullLogger(), + new NullLogger() ); - $event->execute(); - } - - public function testItOpensAFreshConnectionIfResumingFails(): void - { - /** @var ConnectionInterface&MockInterface */ - $connection = Mockery::mock(ConnectionInterface::class); - - $connection->expects() - ->stopAutomaticHeartbeats() - ->once(); - - $connection->expects() - ->getResumeUrl() - ->andReturns('::resume url::') - ->times(4); - - $connection->expects() - ->getSessionId() - ->andReturns('::session id::') - ->once(); - - $connection->expects() + $connection + ->shouldReceive() ->disconnect() - ->with(1004, Mockery::any()) - ->once(); - - $connection->expects() - ->connect() - ->with('::resume url::') - ->andReturns(PromiseFake::reject(new Exception(':('))) - ->times(3); - - $connection->expects() - ->getDefaultUrl() - ->andReturns('::default url::') - ->once(); - - $connection->expects() - ->connect() - ->with('::default url::') - ->andReturns(PromiseFake::get()) + ->with(GatewayCloseCodes::LIB_INSTANTIATED_RESUME, Mockery::type('string')) ->once(); - /** @var Eventer&MockInterface */ - $rawHandler = Mockery::mock(Eventer::class); - $rawHandler->expects() - ->registerOnce() - ->with(IdentifyHelloEvent::class) - ->once(); - - $connection->expects() - ->getRawHandler() - ->andReturns($rawHandler) - ->once(); - - $event = new UnacknowledgedHeartbeatEvent( - $connection, - new NullLogger(), - ); - $event->execute(); } } diff --git a/tests/Gateway/Handlers/ReconnectEventTest.php b/tests/Gateway/Handlers/ReconnectEventTest.php index 94e6c099..295a799a 100644 --- a/tests/Gateway/Handlers/ReconnectEventTest.php +++ b/tests/Gateway/Handlers/ReconnectEventTest.php @@ -4,198 +4,43 @@ namespace Tests\Ragnarok\Fenrir\Gateway\Handlers; -use Exan\Eventer\Eventer; -use Exception; -use Fakes\Ragnarok\Fenrir\PromiseFake; use Mockery; use Mockery\Adapter\Phpunit\MockeryTestCase; use Mockery\MockInterface; use Psr\Log\NullLogger; -use Ragnarok\Fenrir\Constants\OpCodes; +use Ragnarok\Fenrir\Constants\GatewayCloseCodes; use Ragnarok\Fenrir\DataMapper; use Ragnarok\Fenrir\Gateway\ConnectionInterface; -use Ragnarok\Fenrir\Gateway\Handlers\IdentifyHelloEvent; -use Ragnarok\Fenrir\Gateway\Handlers\IdentifyResumeEvent; use Ragnarok\Fenrir\Gateway\Handlers\ReconnectEvent; use Ragnarok\Fenrir\Gateway\Objects\Payload; class ReconnectEventTest extends MockeryTestCase { private DataMapper $mapper; + private ConnectionInterface&MockInterface $connectionInterface; protected function setUp(): void { parent::setUp(); $this->mapper = new DataMapper(new NullLogger()); + $this->connectionInterface = Mockery::mock(ConnectionInterface::class); } - public function testItListensTo7(): void + public function testItDisconnectsWithCorrectCode(): void { - $this->assertEquals(OpCodes::RECONNECT, ReconnectEvent::getEventName()); - } - - public function testItReconnectsToDiscord(): void - { - /** @var ConnectionInterface&MockInterface */ - $connection = Mockery::mock(ConnectionInterface::class); - - $connection->expects() - ->stopAutomaticHeartbeats() - ->once(); - - $connection->expects() - ->getResumeUrl() - ->andReturns('::resume url::') - ->twice(); - - $connection->expects() - ->getSessionId() - ->andReturns('::session id::') - ->once(); - - $connection->expects() - ->disconnect() - ->with(1004, Mockery::any()) - ->once(); - - $connection->expects() - ->connect() - ->with('::resume url::') - ->andReturns(PromiseFake::get()) - ->once(); - - /** @var Eventer&MockInterface */ - $rawHandler = Mockery::mock(Eventer::class); - $rawHandler->expects() - ->registerOnce() - ->with(IdentifyResumeEvent::class) - ->once(); - - $connection->expects() - ->getRawHandler() - ->andReturns($rawHandler) - ->once(); - $event = new ReconnectEvent( - $connection, + $this->connectionInterface, $this->mapper->map((object) [], Payload::class), new NullLogger(), ); - $event->execute(); - } - - public function testItOpensAFreshConnectionIfItCantResume(): void - { - /** @var ConnectionInterface&MockInterface */ - $connection = Mockery::mock(ConnectionInterface::class); - - $connection->expects() - ->stopAutomaticHeartbeats() - ->once(); - - $connection->expects() - ->getResumeUrl() - ->andReturns(null) - ->once(); - - $connection->expects() + $this->connectionInterface + ->shouldReceive() ->disconnect() - ->with(1001, Mockery::any()) - ->once(); - - $connection->expects() - ->getDefaultUrl() - ->andReturns('::default url::') - ->once(); - - $connection->expects() - ->connect() - ->with('::default url::') - ->andReturns(PromiseFake::get()) - ->once(); - - /** @var Eventer&MockInterface */ - $rawHandler = Mockery::mock(Eventer::class); - $rawHandler->expects() - ->registerOnce() - ->with(IdentifyHelloEvent::class) + ->with(GatewayCloseCodes::LIB_INSTANTIATED_RESUME, Mockery::type('string')) ->once(); - $connection->expects() - ->getRawHandler() - ->andReturns($rawHandler) - ->once(); - - $event = new ReconnectEvent( - $connection, - $this->mapper->map((object) [], Payload::class), - new NullLogger(), - ); - - $event->execute(); - } - - public function testItOpensAFreshConnectionIfResumingFails(): void - { - /** @var ConnectionInterface&MockInterface */ - $connection = Mockery::mock(ConnectionInterface::class); - - $connection->expects() - ->stopAutomaticHeartbeats() - ->once(); - - $connection->expects() - ->getResumeUrl() - ->andReturns('::resume url::') - ->times(4); - - $connection->expects() - ->getSessionId() - ->andReturns('::session id::') - ->once(); - - $connection->expects() - ->disconnect() - ->with(1004, Mockery::any()) - ->once(); - - $connection->expects() - ->connect() - ->with('::resume url::') - ->andReturns(PromiseFake::reject(new Exception(':('))) - ->times(3); - - $connection->expects() - ->getDefaultUrl() - ->andReturns('::default url::') - ->once(); - - $connection->expects() - ->connect() - ->with('::default url::') - ->andReturns(PromiseFake::get()) - ->once(); - - /** @var Eventer&MockInterface */ - $rawHandler = Mockery::mock(Eventer::class); - $rawHandler->expects() - ->registerOnce() - ->with(IdentifyHelloEvent::class) - ->once(); - - $connection->expects() - ->getRawHandler() - ->andReturns($rawHandler) - ->once(); - - $event = new ReconnectEvent( - $connection, - $this->mapper->map((object) [], Payload::class), - new NullLogger(), - ); - $event->execute(); } } diff --git a/tests/Gateway/Handlers/RecoverableInvalidSessionEventTest.php b/tests/Gateway/Handlers/RecoverableInvalidSessionEventTest.php index d92541a1..89c0cda4 100644 --- a/tests/Gateway/Handlers/RecoverableInvalidSessionEventTest.php +++ b/tests/Gateway/Handlers/RecoverableInvalidSessionEventTest.php @@ -4,35 +4,27 @@ namespace Tests\Ragnarok\Fenrir\Gateway\Handlers; -use Exan\Eventer\Eventer; -use Exception; -use Fakes\Ragnarok\Fenrir\PromiseFake; use Mockery; use Mockery\Adapter\Phpunit\MockeryTestCase; use Mockery\MockInterface; use Psr\Log\NullLogger; -use Ragnarok\Fenrir\Constants\OpCodes; +use Ragnarok\Fenrir\Constants\GatewayCloseCodes; use Ragnarok\Fenrir\DataMapper; use Ragnarok\Fenrir\Gateway\ConnectionInterface; -use Ragnarok\Fenrir\Gateway\Handlers\IdentifyHelloEvent; -use Ragnarok\Fenrir\Gateway\Handlers\IdentifyResumeEvent; use Ragnarok\Fenrir\Gateway\Handlers\RecoverableInvalidSessionEvent; use Ragnarok\Fenrir\Gateway\Objects\Payload; class RecoverableInvalidSessionEventTest extends MockeryTestCase { private DataMapper $mapper; + private ConnectionInterface&MockInterface $connectionInterface; protected function setUp(): void { parent::setUp(); $this->mapper = new DataMapper(new NullLogger()); - } - - public function testItListensTo9(): void - { - $this->assertEquals(OpCodes::INVALID_SESSION, RecoverableInvalidSessionEvent::getEventName()); + $this->connectionInterface = Mockery::mock(ConnectionInterface::class); } /** @@ -67,167 +59,20 @@ public static function listenerDataProvider(): array ]; } - public function testItReconnectsToDiscord(): void + public function testItDisconnectsWithCorrectCode(): void { - /** @var ConnectionInterface&MockInterface */ - $connection = Mockery::mock(ConnectionInterface::class); - - $connection->expects() - ->stopAutomaticHeartbeats() - ->once(); - - $connection->expects() - ->getResumeUrl() - ->andReturns('::resume url::') - ->twice(); - - $connection->expects() - ->getSessionId() - ->andReturns('::session id::') - ->once(); - - $connection->expects() - ->disconnect() - ->with(1004, Mockery::any()) - ->once(); - - $connection->expects() - ->connect() - ->with('::resume url::') - ->andReturns(PromiseFake::get()) - ->once(); - - /** @var Eventer&MockInterface */ - $rawHandler = Mockery::mock(Eventer::class); - $rawHandler->expects() - ->registerOnce() - ->with(IdentifyResumeEvent::class) - ->once(); - - $connection->expects() - ->getRawHandler() - ->andReturns($rawHandler) - ->once(); - $event = new RecoverableInvalidSessionEvent( - $connection, - $this->mapper->map((object) [], Payload::class), + $this->connectionInterface, + $this->mapper->map((object) ['d' => true], Payload::class), new NullLogger(), ); - $event->execute(); - } - - public function testItOpensAFreshConnectionIfItCantResume(): void - { - /** @var ConnectionInterface&MockInterface */ - $connection = Mockery::mock(ConnectionInterface::class); - - $connection->expects() - ->stopAutomaticHeartbeats() - ->once(); - - $connection->expects() - ->getResumeUrl() - ->andReturns(null) - ->once(); - - $connection->expects() + $this->connectionInterface + ->shouldReceive() ->disconnect() - ->with(1001, Mockery::any()) - ->once(); - - $connection->expects() - ->getDefaultUrl() - ->andReturns('::default url::') - ->once(); - - $connection->expects() - ->connect() - ->with('::default url::') - ->andReturns(PromiseFake::get()) - ->once(); - - /** @var Eventer&MockInterface */ - $rawHandler = Mockery::mock(Eventer::class); - $rawHandler->expects() - ->registerOnce() - ->with(IdentifyHelloEvent::class) + ->with(GatewayCloseCodes::LIB_INSTANTIATED_RESUME, Mockery::type('string')) ->once(); - $connection->expects() - ->getRawHandler() - ->andReturns($rawHandler) - ->once(); - - $event = new RecoverableInvalidSessionEvent( - $connection, - $this->mapper->map((object) [], Payload::class), - new NullLogger(), - ); - - $event->execute(); - } - - public function testItOpensAFreshConnectionIfResumingFails(): void - { - /** @var ConnectionInterface&MockInterface */ - $connection = Mockery::mock(ConnectionInterface::class); - - $connection->expects() - ->stopAutomaticHeartbeats() - ->once(); - - $connection->expects() - ->getResumeUrl() - ->andReturns('::resume url::') - ->times(4); - - $connection->expects() - ->getSessionId() - ->andReturns('::session id::') - ->once(); - - $connection->expects() - ->disconnect() - ->with(1004, Mockery::any()) - ->once(); - - $connection->expects() - ->connect() - ->with('::resume url::') - ->andReturns(PromiseFake::reject(new Exception(':('))) - ->times(3); - - $connection->expects() - ->getDefaultUrl() - ->andReturns('::default url::') - ->once(); - - $connection->expects() - ->connect() - ->with('::default url::') - ->andReturns(PromiseFake::get()) - ->once(); - - /** @var Eventer&MockInterface */ - $rawHandler = Mockery::mock(Eventer::class); - $rawHandler->expects() - ->registerOnce() - ->with(IdentifyHelloEvent::class) - ->once(); - - $connection->expects() - ->getRawHandler() - ->andReturns($rawHandler) - ->once(); - - $event = new RecoverableInvalidSessionEvent( - $connection, - $this->mapper->map((object) [], Payload::class), - new NullLogger(), - ); - $event->execute(); } } diff --git a/tests/Gateway/Handlers/RequestHeartbeatEventTest.php b/tests/Gateway/Handlers/RequestHeartbeatEventTest.php index 96ea96bc..3620eb87 100644 --- a/tests/Gateway/Handlers/RequestHeartbeatEventTest.php +++ b/tests/Gateway/Handlers/RequestHeartbeatEventTest.php @@ -15,11 +15,6 @@ class RequestHeartbeatEventTest extends MockeryTestCase { - public function testItListensTo1(): void - { - $this->assertEquals(OpCodes::HEARTBEAT, RequestHeartbeatEvent::getEventName()); - } - public function testItAcknowledgesAHeartbeat(): void { /** @var MockInterface&ConnectionInterface */