ClientTest.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. <?php
  2. /**
  3. * Test case for Client.
  4. * Note that this test is performed by mocking socket/stream calls.
  5. */
  6. declare(strict_types=1);
  7. namespace WebSocket;
  8. use ErrorException;
  9. use Phrity\Net\Uri;
  10. use Phrity\Util\ErrorHandler;
  11. use PHPUnit\Framework\TestCase;
  12. class ClientTest extends TestCase
  13. {
  14. public function setUp(): void
  15. {
  16. error_reporting(-1);
  17. }
  18. public function testClientMasked(): void
  19. {
  20. MockSocket::initialize('client.connect', $this);
  21. $client = new Client('ws://localhost:8000/my/mock/path');
  22. $client->send('Connect');
  23. $this->assertTrue(MockSocket::isEmpty());
  24. $this->assertEquals(4096, $client->getFragmentSize());
  25. MockSocket::initialize('send-receive', $this);
  26. $client->send('Sending a message');
  27. $message = $client->receive();
  28. $this->assertTrue(MockSocket::isEmpty());
  29. $this->assertEquals('text', $client->getLastOpcode());
  30. MockSocket::initialize('client.close', $this);
  31. $this->assertTrue($client->isConnected());
  32. $this->assertNull($client->getCloseStatus());
  33. $client->close();
  34. $this->assertFalse($client->isConnected());
  35. $this->assertEquals(1000, $client->getCloseStatus());
  36. $this->assertTrue(MockSocket::isEmpty());
  37. }
  38. public function testDestruct(): void
  39. {
  40. MockSocket::initialize('client.connect', $this);
  41. $client = new Client('ws://localhost:8000/my/mock/path');
  42. $client->send('Connect');
  43. $this->assertTrue(MockSocket::isEmpty());
  44. MockSocket::initialize('client.destruct', $this);
  45. }
  46. public function testClienExtendedUrl(): void
  47. {
  48. MockSocket::initialize('client.connect-extended', $this);
  49. $client = new Client('ws://localhost:8000/my/mock/path?my_query=yes#my_fragment');
  50. $client->send('Connect');
  51. $this->assertTrue(MockSocket::isEmpty());
  52. }
  53. public function testClientNoPath(): void
  54. {
  55. MockSocket::initialize('client.connect-root', $this);
  56. $client = new Client('ws://localhost:8000');
  57. $client->send('Connect');
  58. $this->assertTrue(MockSocket::isEmpty());
  59. }
  60. public function testClientRelativePath(): void
  61. {
  62. MockSocket::initialize('client.connect', $this);
  63. $uri = new Uri('ws://localhost:8000');
  64. $uri = $uri->withPath('my/mock/path');
  65. $client = new Client($uri);
  66. $client->send('Connect');
  67. $this->assertTrue(MockSocket::isEmpty());
  68. }
  69. public function testClientWsDefaultPort(): void
  70. {
  71. MockSocket::initialize('client.connect-default-port-ws', $this);
  72. $uri = new Uri('ws://localhost');
  73. $uri = $uri->withPath('my/mock/path');
  74. $client = new Client($uri);
  75. $client->send('Connect');
  76. $this->assertTrue(MockSocket::isEmpty());
  77. }
  78. public function testClientWssDefaultPort(): void
  79. {
  80. MockSocket::initialize('client.connect-default-port-wss', $this);
  81. $uri = new Uri('wss://localhost');
  82. $uri = $uri->withPath('my/mock/path');
  83. $client = new Client($uri);
  84. $client->send('Connect');
  85. $this->assertTrue(MockSocket::isEmpty());
  86. }
  87. public function testClientWithTimeout(): void
  88. {
  89. MockSocket::initialize('client.connect-timeout', $this);
  90. $client = new Client('ws://localhost:8000/my/mock/path', ['timeout' => 300]);
  91. $client->send('Connect');
  92. $this->assertTrue(MockSocket::isEmpty());
  93. }
  94. public function testClientWithContext(): void
  95. {
  96. MockSocket::initialize('client.connect-context', $this);
  97. $client = new Client('ws://localhost:8000/my/mock/path', ['context' => '@mock-stream-context']);
  98. $client->send('Connect');
  99. $this->assertTrue(MockSocket::isEmpty());
  100. }
  101. public function testClientAuthed(): void
  102. {
  103. MockSocket::initialize('client.connect-authed', $this);
  104. $client = new Client('wss://usename:password@localhost:8000/my/mock/path');
  105. $client->send('Connect');
  106. $this->assertTrue(MockSocket::isEmpty());
  107. }
  108. public function testWithHeaders(): void
  109. {
  110. MockSocket::initialize('client.connect-headers', $this);
  111. $client = new Client('ws://localhost:8000/my/mock/path', [
  112. 'origin' => 'Origin header',
  113. 'headers' => ['Generic header' => 'Generic content'],
  114. ]);
  115. $client->send('Connect');
  116. $this->assertTrue(MockSocket::isEmpty());
  117. }
  118. public function testPayload128(): void
  119. {
  120. MockSocket::initialize('client.connect', $this);
  121. $client = new Client('ws://localhost:8000/my/mock/path');
  122. $client->send('Connect');
  123. $this->assertTrue(MockSocket::isEmpty());
  124. $payload = file_get_contents(__DIR__ . '/mock/payload.128.txt');
  125. MockSocket::initialize('send-receive-128', $this);
  126. $client->send($payload, 'text', false);
  127. $message = $client->receive();
  128. $this->assertEquals($payload, $message);
  129. $this->assertTrue(MockSocket::isEmpty());
  130. }
  131. public function testPayload65536(): void
  132. {
  133. MockSocket::initialize('client.connect', $this);
  134. $client = new Client('ws://localhost:8000/my/mock/path');
  135. $client->send('Connect');
  136. $this->assertTrue(MockSocket::isEmpty());
  137. $payload = file_get_contents(__DIR__ . '/mock/payload.65536.txt');
  138. $client->setFragmentSize(65540);
  139. MockSocket::initialize('send-receive-65536', $this);
  140. $client->send($payload, 'text', false);
  141. $message = $client->receive();
  142. $this->assertEquals($payload, $message);
  143. $this->assertTrue(MockSocket::isEmpty());
  144. $this->assertEquals(65540, $client->getFragmentSize());
  145. }
  146. public function testMultiFragment(): void
  147. {
  148. MockSocket::initialize('client.connect', $this);
  149. $client = new Client('ws://localhost:8000/my/mock/path');
  150. $client->send('Connect');
  151. $this->assertTrue(MockSocket::isEmpty());
  152. MockSocket::initialize('send-receive-multi-fragment', $this);
  153. $client->setFragmentSize(8);
  154. $client->send('Multi fragment test');
  155. $message = $client->receive();
  156. $this->assertEquals('Multi fragment test', $message);
  157. $this->assertTrue(MockSocket::isEmpty());
  158. $this->assertEquals(8, $client->getFragmentSize());
  159. }
  160. public function testPingPong(): void
  161. {
  162. MockSocket::initialize('client.connect', $this);
  163. $client = new Client('ws://localhost:8000/my/mock/path');
  164. $client->send('Connect');
  165. $this->assertTrue(MockSocket::isEmpty());
  166. MockSocket::initialize('ping-pong', $this);
  167. $client->send('Server ping', 'ping');
  168. $client->send('', 'ping');
  169. $message = $client->receive();
  170. $this->assertEquals('Receiving a message', $message);
  171. $this->assertEquals('text', $client->getLastOpcode());
  172. $this->assertTrue(MockSocket::isEmpty());
  173. }
  174. public function testRemoteClose(): void
  175. {
  176. MockSocket::initialize('client.connect', $this);
  177. $client = new Client('ws://localhost:8000/my/mock/path');
  178. $client->send('Connect');
  179. $this->assertTrue(MockSocket::isEmpty());
  180. MockSocket::initialize('close-remote', $this);
  181. $message = $client->receive();
  182. $this->assertNull($message);
  183. $this->assertFalse($client->isConnected());
  184. $this->assertEquals(17260, $client->getCloseStatus());
  185. $this->assertNull($client->getLastOpcode());
  186. $this->assertTrue(MockSocket::isEmpty());
  187. }
  188. public function testSetTimeout(): void
  189. {
  190. MockSocket::initialize('client.connect', $this);
  191. $client = new Client('ws://localhost:8000/my/mock/path');
  192. $client->send('Connect');
  193. $this->assertTrue(MockSocket::isEmpty());
  194. MockSocket::initialize('config-timeout', $this);
  195. $client->setTimeout(300);
  196. $this->assertTrue($client->isConnected());
  197. $this->assertTrue(MockSocket::isEmpty());
  198. }
  199. public function testReconnect(): void
  200. {
  201. MockSocket::initialize('client.connect', $this);
  202. $client = new Client('ws://localhost:8000/my/mock/path');
  203. $client->send('Connect');
  204. $this->assertTrue(MockSocket::isEmpty());
  205. MockSocket::initialize('client.close', $this);
  206. $this->assertTrue($client->isConnected());
  207. $this->assertNull($client->getCloseStatus());
  208. $client->close();
  209. $this->assertFalse($client->isConnected());
  210. $this->assertEquals(1000, $client->getCloseStatus());
  211. $this->assertNull($client->getLastOpcode());
  212. $this->assertTrue(MockSocket::isEmpty());
  213. MockSocket::initialize('client.reconnect', $this);
  214. $message = $client->receive();
  215. $this->assertTrue($client->isConnected());
  216. $this->assertTrue(MockSocket::isEmpty());
  217. }
  218. public function testPersistentConnection(): void
  219. {
  220. MockSocket::initialize('client.connect-persistent', $this);
  221. $client = new Client('ws://localhost:8000/my/mock/path', ['persistent' => true]);
  222. $client->send('Connect');
  223. $client->disconnect();
  224. $this->assertFalse($client->isConnected());
  225. $this->assertTrue(MockSocket::isEmpty());
  226. }
  227. public function testFailedPersistentConnection(): void
  228. {
  229. MockSocket::initialize('client.connect-persistent-failure', $this);
  230. $client = new Client('ws://localhost:8000/my/mock/path', ['persistent' => true]);
  231. $this->expectException('WebSocket\ConnectionException');
  232. $this->expectExceptionMessage('Could not resolve stream pointer position');
  233. $client->send('Connect');
  234. }
  235. public function testBadScheme(): void
  236. {
  237. MockSocket::initialize('client.connect', $this);
  238. $this->expectException('WebSocket\BadUriException');
  239. $this->expectExceptionMessage("Invalid URI scheme, must be 'ws' or 'wss'.");
  240. $client = new Client('bad://localhost:8000/my/mock/path');
  241. }
  242. public function testBadUri(): void
  243. {
  244. MockSocket::initialize('client.connect', $this);
  245. $this->expectException('WebSocket\BadUriException');
  246. $this->expectExceptionMessage("Invalid URI '--:this is not an uri:--' provided.");
  247. $client = new Client('--:this is not an uri:--');
  248. }
  249. public function testInvalidUriType(): void
  250. {
  251. MockSocket::initialize('client.connect', $this);
  252. $this->expectException('WebSocket\BadUriException');
  253. $this->expectExceptionMessage("Provided URI must be a UriInterface or string.");
  254. $client = new Client([]);
  255. }
  256. public function testUriInterface(): void
  257. {
  258. MockSocket::initialize('client.connect', $this);
  259. $uri = new Uri('ws://localhost:8000/my/mock/path');
  260. $client = new Client($uri);
  261. $client->send('Connect');
  262. $this->assertTrue(MockSocket::isEmpty());
  263. }
  264. public function testBadStreamContext(): void
  265. {
  266. MockSocket::initialize('client.connect-bad-context', $this);
  267. $client = new Client('ws://localhost:8000/my/mock/path', ['context' => 'BAD']);
  268. $this->expectException('InvalidArgumentException');
  269. $this->expectExceptionMessage('Stream context in $options[\'context\'] isn\'t a valid context');
  270. $client->send('Connect');
  271. }
  272. public function testFailedConnection(): void
  273. {
  274. MockSocket::initialize('client.connect-failed', $this);
  275. $client = new Client('ws://localhost:8000/my/mock/path');
  276. $this->expectException('WebSocket\ConnectionException');
  277. $this->expectExceptionCode(0);
  278. $this->expectExceptionMessage('Could not open socket to "localhost:8000"');
  279. $client->send('Connect');
  280. }
  281. public function testFailedConnectionWithError(): void
  282. {
  283. MockSocket::initialize('client.connect-error', $this);
  284. $client = new Client('ws://localhost:8000/my/mock/path');
  285. $this->expectException('WebSocket\ConnectionException');
  286. $this->expectExceptionCode(0);
  287. $this->expectExceptionMessage('Could not open socket to "localhost:8000"');
  288. $client->send('Connect');
  289. }
  290. public function testBadStreamConnection(): void
  291. {
  292. MockSocket::initialize('client.connect-bad-stream', $this);
  293. $client = new Client('ws://localhost:8000/my/mock/path');
  294. $this->expectException('WebSocket\ConnectionException');
  295. $this->expectExceptionCode(0);
  296. $this->expectExceptionMessage('Invalid stream on "localhost:8000"');
  297. $client->send('Connect');
  298. }
  299. public function testHandshakeFailure(): void
  300. {
  301. MockSocket::initialize('client.connect-handshake-failure', $this);
  302. $client = new Client('ws://localhost:8000/my/mock/path');
  303. $this->expectException('WebSocket\ConnectionException');
  304. $this->expectExceptionCode(0);
  305. $this->expectExceptionMessage('Client handshake error');
  306. $client->send('Connect');
  307. }
  308. public function testInvalidUpgrade(): void
  309. {
  310. MockSocket::initialize('client.connect-invalid-upgrade', $this);
  311. $client = new Client('ws://localhost:8000/my/mock/path');
  312. $this->expectException('WebSocket\ConnectionException');
  313. $this->expectExceptionCode(0);
  314. $this->expectExceptionMessage('Connection to \'ws://localhost:8000/my/mock/path\' failed');
  315. $client->send('Connect');
  316. }
  317. public function testInvalidKey(): void
  318. {
  319. MockSocket::initialize('client.connect-invalid-key', $this);
  320. $client = new Client('ws://localhost:8000/my/mock/path');
  321. $this->expectException('WebSocket\ConnectionException');
  322. $this->expectExceptionCode(0);
  323. $this->expectExceptionMessage('Server sent bad upgrade response');
  324. $client->send('Connect');
  325. }
  326. public function testSendBadOpcode(): void
  327. {
  328. MockSocket::initialize('client.connect', $this);
  329. $client = new Client('ws://localhost:8000/my/mock/path');
  330. $client->send('Connect');
  331. MockSocket::initialize('send-bad-opcode', $this);
  332. $this->expectException('WebSocket\BadOpcodeException');
  333. $this->expectExceptionMessage('Bad opcode \'bad\'. Try \'text\' or \'binary\'.');
  334. $client->send('Bad Opcode', 'bad');
  335. }
  336. public function testRecieveBadOpcode(): void
  337. {
  338. MockSocket::initialize('client.connect', $this);
  339. $client = new Client('ws://localhost:8000/my/mock/path');
  340. $client->send('Connect');
  341. MockSocket::initialize('receive-bad-opcode', $this);
  342. $this->expectException('WebSocket\ConnectionException');
  343. $this->expectExceptionCode(1026);
  344. $this->expectExceptionMessage('Bad opcode in websocket frame: 12');
  345. $message = $client->receive();
  346. }
  347. public function testBrokenWrite(): void
  348. {
  349. MockSocket::initialize('client.connect', $this);
  350. $client = new Client('ws://localhost:8000/my/mock/path');
  351. $client->send('Connect');
  352. MockSocket::initialize('send-broken-write', $this);
  353. $this->expectException('WebSocket\ConnectionException');
  354. $this->expectExceptionCode(1025);
  355. $this->expectExceptionMessage('Could only write 18 out of 22 bytes.');
  356. $client->send('Failing to write');
  357. }
  358. public function testFailedWrite(): void
  359. {
  360. MockSocket::initialize('client.connect', $this);
  361. $client = new Client('ws://localhost:8000/my/mock/path');
  362. $client->send('Connect');
  363. MockSocket::initialize('send-failed-write', $this);
  364. $this->expectException('WebSocket\TimeoutException');
  365. $this->expectExceptionCode(1024);
  366. $this->expectExceptionMessage('Failed to write 22 bytes.');
  367. $client->send('Failing to write');
  368. }
  369. public function testBrokenRead(): void
  370. {
  371. MockSocket::initialize('client.connect', $this);
  372. $client = new Client('ws://localhost:8000/my/mock/path');
  373. $client->send('Connect');
  374. MockSocket::initialize('receive-broken-read', $this);
  375. $this->expectException('WebSocket\ConnectionException');
  376. $this->expectExceptionCode(1025);
  377. $this->expectExceptionMessage('Broken frame, read 0 of stated 2 bytes.');
  378. $client->receive();
  379. }
  380. public function testHandshakeError(): void
  381. {
  382. MockSocket::initialize('client.connect-handshake-error', $this);
  383. $client = new Client('ws://localhost:8000/my/mock/path');
  384. $this->expectException('WebSocket\ConnectionException');
  385. $this->expectExceptionCode(1024);
  386. $this->expectExceptionMessage('Client handshake error');
  387. $client->send('Connect');
  388. }
  389. public function testReadTimeout(): void
  390. {
  391. MockSocket::initialize('client.connect', $this);
  392. $client = new Client('ws://localhost:8000/my/mock/path');
  393. $client->send('Connect');
  394. MockSocket::initialize('receive-client-timeout', $this);
  395. $this->expectException('WebSocket\TimeoutException');
  396. $this->expectExceptionCode(1024);
  397. $this->expectExceptionMessage('Client read timeout');
  398. $client->receive();
  399. }
  400. public function testEmptyRead(): void
  401. {
  402. MockSocket::initialize('client.connect', $this);
  403. $client = new Client('ws://localhost:8000/my/mock/path');
  404. $client->send('Connect');
  405. MockSocket::initialize('receive-empty-read', $this);
  406. $this->expectException('WebSocket\TimeoutException');
  407. $this->expectExceptionCode(1024);
  408. $this->expectExceptionMessage('Empty read; connection dead?');
  409. $client->receive();
  410. }
  411. public function testFrameFragmentation(): void
  412. {
  413. MockSocket::initialize('client.connect', $this);
  414. $client = new Client(
  415. 'ws://localhost:8000/my/mock/path',
  416. ['filter' => ['text', 'binary', 'pong', 'close']]
  417. );
  418. $client->send('Connect');
  419. MockSocket::initialize('receive-fragmentation', $this);
  420. $message = $client->receive();
  421. $this->assertEquals('Server ping', $message);
  422. $this->assertEquals('pong', $client->getLastOpcode());
  423. $message = $client->receive();
  424. $this->assertEquals('Multi fragment test', $message);
  425. $this->assertEquals('text', $client->getLastOpcode());
  426. $this->assertTrue(MockSocket::isEmpty());
  427. MockSocket::initialize('close-remote', $this);
  428. $message = $client->receive();
  429. $this->assertEquals('Closing', $message);
  430. $this->assertTrue(MockSocket::isEmpty());
  431. $this->assertFalse($client->isConnected());
  432. $this->assertEquals(17260, $client->getCloseStatus());
  433. $this->assertEquals('close', $client->getLastOpcode());
  434. }
  435. public function testMessageFragmentation(): void
  436. {
  437. MockSocket::initialize('client.connect', $this);
  438. $client = new Client(
  439. 'ws://localhost:8000/my/mock/path',
  440. ['filter' => ['text', 'binary', 'pong', 'close'], 'return_obj' => true]
  441. );
  442. $client->send('Connect');
  443. MockSocket::initialize('receive-fragmentation', $this);
  444. $message = $client->receive();
  445. $this->assertInstanceOf('WebSocket\Message\Message', $message);
  446. $this->assertInstanceOf('WebSocket\Message\Pong', $message);
  447. $this->assertEquals('Server ping', $message->getContent());
  448. $this->assertEquals('pong', $message->getOpcode());
  449. $message = $client->receive();
  450. $this->assertInstanceOf('WebSocket\Message\Message', $message);
  451. $this->assertInstanceOf('WebSocket\Message\Text', $message);
  452. $this->assertEquals('Multi fragment test', $message->getContent());
  453. $this->assertEquals('text', $message->getOpcode());
  454. $this->assertTrue(MockSocket::isEmpty());
  455. MockSocket::initialize('close-remote', $this);
  456. $message = $client->receive();
  457. $this->assertInstanceOf('WebSocket\Message\Message', $message);
  458. $this->assertInstanceOf('WebSocket\Message\Close', $message);
  459. $this->assertEquals('Closing', $message->getContent());
  460. $this->assertEquals('close', $message->getOpcode());
  461. }
  462. public function testConvenicanceMethods(): void
  463. {
  464. MockSocket::initialize('client.connect', $this);
  465. $client = new Client('ws://localhost:8000/my/mock/path');
  466. $this->assertNull($client->getName());
  467. $this->assertNull($client->getRemoteName());
  468. $this->assertEquals('WebSocket\Client(closed)', "{$client}");
  469. $client->text('Connect');
  470. MockSocket::initialize('send-convenicance', $this);
  471. $client->binary(base64_encode('Binary content'));
  472. $client->ping();
  473. $client->pong();
  474. $this->assertEquals('127.0.0.1:12345', $client->getName());
  475. $this->assertEquals('127.0.0.1:8000', $client->getRemoteName());
  476. $this->assertEquals('WebSocket\Client(127.0.0.1:12345)', "{$client}");
  477. }
  478. public function testUnconnectedClient(): void
  479. {
  480. $client = new Client('ws://localhost:8000/my/mock/path');
  481. $this->assertFalse($client->isConnected());
  482. $client->setTimeout(30);
  483. $client->close();
  484. $this->assertFalse($client->isConnected());
  485. $this->assertNull($client->getName());
  486. $this->assertNull($client->getRemoteName());
  487. $this->assertNull($client->getCloseStatus());
  488. }
  489. public function testDeprecated(): void
  490. {
  491. $client = new Client('ws://localhost:8000/my/mock/path');
  492. (new ErrorHandler())->withAll(function () use ($client) {
  493. $this->assertNull($client->getPier());
  494. }, function ($exceptions, $result) {
  495. $this->assertEquals(
  496. 'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
  497. $exceptions[0]->getMessage()
  498. );
  499. }, E_USER_DEPRECATED);
  500. }
  501. }