@@ -71,10 +71,11 @@ public function testParseRequestValid(): void
7171 $ body = '{"host":"web","config":{}} ' ;
7272 $ request = "POST /save HTTP/1.1 \r\nContent-Type: application/json \r\nContent-Length: " . strlen ($ body ) . "\r\n\r\n" . $ body ;
7373
74- [$ path , $ payload ] = Server::parseRequest ($ request );
74+ [$ path , $ payload, $ headers ] = Server::parseRequest ($ request );
7575
7676 self ::assertSame ('/save ' , $ path );
7777 self ::assertSame (['host ' => 'web ' , 'config ' => []], $ payload );
78+ self ::assertSame ('application/json ' , $ headers ['content-type ' ]);
7879 }
7980
8081 #[Group('unit ' )]
@@ -83,7 +84,7 @@ public function testParseRequestTrimsBodyToContentLength(): void
8384 $ body = '{"a":1} ' ;
8485 $ request = "POST /save HTTP/1.1 \r\nContent-Type: application/json \r\nContent-Length: " . strlen ($ body ) . "\r\n\r\n" . $ body . "garbage " ;
8586
86- [$ path , $ payload ] = Server::parseRequest ($ request );
87+ [$ path , $ payload, $ headers ] = Server::parseRequest ($ request );
8788
8889 self ::assertSame ('/save ' , $ path );
8990 self ::assertSame (['a ' => 1 ], $ payload );
@@ -95,7 +96,7 @@ public function testParseRequestCaseInsensitiveHeaders(): void
9596 $ body = '{"ok":true} ' ;
9697 $ request = "POST /load HTTP/1.1 \r\ncontent-type: application/json \r\ncontent-length: " . strlen ($ body ) . "\r\n\r\n" . $ body ;
9798
98- [$ path , $ payload ] = Server::parseRequest ($ request );
99+ [$ path , $ payload, $ headers ] = Server::parseRequest ($ request );
99100
100101 self ::assertSame ('/load ' , $ path );
101102 self ::assertSame (['ok ' => true ], $ payload );
@@ -119,6 +120,18 @@ public function testParseRequestThrowsOnMissingHeaderTerminator(): void
119120 Server::parseRequest ("POST /save HTTP/1.1 \r\nContent-Type: application/json " );
120121 }
121122
123+ #[Group('unit ' )]
124+ public function testParseRequestReturnsAuthorizationHeader (): void
125+ {
126+ $ body = '{"a":1} ' ;
127+ $ request = "POST /load HTTP/1.1 \r\nContent-Type: application/json \r\nAuthorization: Bearer secret123 \r\nContent-Length: " . strlen ($ body ) . "\r\n\r\n" . $ body ;
128+
129+ [$ path , $ payload , $ headers ] = Server::parseRequest ($ request );
130+
131+ self ::assertSame ('/load ' , $ path );
132+ self ::assertSame ('Bearer secret123 ' , $ headers ['authorization ' ]);
133+ }
134+
122135 #[Group('unit ' )]
123136 public function testParseRequestThrowsOnInvalidContentType (): void
124137 {
@@ -165,14 +178,16 @@ public function testWriteAllWritesLargeData(): void
165178
166179 // ─── Integration: real server lifecycle ──────────────────────
167180
168- private static function buildHttpRequest (string $ method , string $ path , array $ payload ): string
181+ private static function buildHttpRequest (string $ method , string $ path , array $ payload, ? string $ token = null ): string
169182 {
170183 $ body = json_encode ($ payload );
171- return "$ method $ path HTTP/1.1 \r\n"
184+ $ headers = "$ method $ path HTTP/1.1 \r\n"
172185 . "Content-Type: application/json \r\n"
173- . "Content-Length: " . strlen ($ body ) . "\r\n"
174- . "\r\n"
175- . $ body ;
186+ . "Content-Length: " . strlen ($ body ) . "\r\n" ;
187+ if ($ token !== null ) {
188+ $ headers .= "Authorization: Bearer $ token \r\n" ;
189+ }
190+ return $ headers . "\r\n" . $ body ;
176191 }
177192
178193 #[Group('integration ' )]
@@ -324,4 +339,149 @@ public function testServerHandlesMultipleConnections(): void
324339
325340 self ::assertSame (2 , $ requestCount );
326341 }
342+
343+ #[Group('integration ' )]
344+ public function testServerRejectsRequestWithoutToken (): void
345+ {
346+ $ output = new BufferedOutput ();
347+ $ server = new Server ('127.0.0.1 ' , 0 , $ output );
348+ $ server ->setAuthToken ('secret-token ' );
349+
350+ $ routerCalled = false ;
351+ $ server ->router (function (string $ path , array $ payload ) use (&$ routerCalled ) {
352+ $ routerCalled = true ;
353+ return new Response (200 , ['ok ' => true ]);
354+ });
355+
356+ $ clientSocket = null ;
357+ $ responseData = '' ;
358+
359+ $ server ->afterRun (function (int $ port ) use (&$ clientSocket ) {
360+ $ clientSocket = stream_socket_client ("tcp://127.0.0.1: $ port " , $ errno , $ errstr , 5 );
361+ stream_set_blocking ($ clientSocket , false );
362+ // Send request without Authorization header.
363+ fwrite ($ clientSocket , self ::buildHttpRequest ('POST ' , '/load ' , ['host ' => 'web ' ]));
364+ });
365+
366+ $ tickCount = 0 ;
367+ $ server ->ticker (function () use ($ server , &$ tickCount , &$ clientSocket , &$ responseData ) {
368+ $ tickCount ++;
369+ if ($ clientSocket ) {
370+ $ chunk = @fread ($ clientSocket , 65536 );
371+ if ($ chunk !== false && $ chunk !== '' ) {
372+ $ responseData .= $ chunk ;
373+ }
374+ if ($ responseData !== '' && feof ($ clientSocket )) {
375+ fclose ($ clientSocket );
376+ $ clientSocket = null ;
377+ $ server ->stop ();
378+ return ;
379+ }
380+ }
381+ if ($ tickCount >= 30 ) {
382+ $ server ->stop ();
383+ }
384+ });
385+
386+ $ server ->run ();
387+
388+ self ::assertFalse ($ routerCalled , 'Router should not be called for unauthorized request ' );
389+ self ::assertStringContainsString ('403 Forbidden ' , $ responseData );
390+ }
391+
392+ #[Group('integration ' )]
393+ public function testServerRejectsRequestWithWrongToken (): void
394+ {
395+ $ output = new BufferedOutput ();
396+ $ server = new Server ('127.0.0.1 ' , 0 , $ output );
397+ $ server ->setAuthToken ('correct-token ' );
398+
399+ $ routerCalled = false ;
400+ $ server ->router (function (string $ path , array $ payload ) use (&$ routerCalled ) {
401+ $ routerCalled = true ;
402+ return new Response (200 , ['ok ' => true ]);
403+ });
404+
405+ $ clientSocket = null ;
406+ $ responseData = '' ;
407+
408+ $ server ->afterRun (function (int $ port ) use (&$ clientSocket ) {
409+ $ clientSocket = stream_socket_client ("tcp://127.0.0.1: $ port " , $ errno , $ errstr , 5 );
410+ stream_set_blocking ($ clientSocket , false );
411+ fwrite ($ clientSocket , self ::buildHttpRequest ('POST ' , '/load ' , ['host ' => 'web ' ], 'wrong-token ' ));
412+ });
413+
414+ $ tickCount = 0 ;
415+ $ server ->ticker (function () use ($ server , &$ tickCount , &$ clientSocket , &$ responseData ) {
416+ $ tickCount ++;
417+ if ($ clientSocket ) {
418+ $ chunk = @fread ($ clientSocket , 65536 );
419+ if ($ chunk !== false && $ chunk !== '' ) {
420+ $ responseData .= $ chunk ;
421+ }
422+ if ($ responseData !== '' && feof ($ clientSocket )) {
423+ fclose ($ clientSocket );
424+ $ clientSocket = null ;
425+ $ server ->stop ();
426+ return ;
427+ }
428+ }
429+ if ($ tickCount >= 30 ) {
430+ $ server ->stop ();
431+ }
432+ });
433+
434+ $ server ->run ();
435+
436+ self ::assertFalse ($ routerCalled , 'Router should not be called for wrong token ' );
437+ self ::assertStringContainsString ('403 Forbidden ' , $ responseData );
438+ }
439+
440+ #[Group('integration ' )]
441+ public function testServerAcceptsRequestWithCorrectToken (): void
442+ {
443+ $ output = new BufferedOutput ();
444+ $ server = new Server ('127.0.0.1 ' , 0 , $ output );
445+ $ server ->setAuthToken ('correct-token ' );
446+
447+ $ routerCalled = false ;
448+ $ server ->router (function (string $ path , array $ payload ) use (&$ routerCalled ) {
449+ $ routerCalled = true ;
450+ return new Response (200 , ['ok ' => true ]);
451+ });
452+
453+ $ clientSocket = null ;
454+ $ responseData = '' ;
455+
456+ $ server ->afterRun (function (int $ port ) use (&$ clientSocket ) {
457+ $ clientSocket = stream_socket_client ("tcp://127.0.0.1: $ port " , $ errno , $ errstr , 5 );
458+ stream_set_blocking ($ clientSocket , false );
459+ fwrite ($ clientSocket , self ::buildHttpRequest ('POST ' , '/load ' , ['host ' => 'web ' ], 'correct-token ' ));
460+ });
461+
462+ $ tickCount = 0 ;
463+ $ server ->ticker (function () use ($ server , &$ tickCount , &$ clientSocket , &$ responseData ) {
464+ $ tickCount ++;
465+ if ($ clientSocket ) {
466+ $ chunk = @fread ($ clientSocket , 65536 );
467+ if ($ chunk !== false && $ chunk !== '' ) {
468+ $ responseData .= $ chunk ;
469+ }
470+ if ($ responseData !== '' && feof ($ clientSocket )) {
471+ fclose ($ clientSocket );
472+ $ clientSocket = null ;
473+ $ server ->stop ();
474+ return ;
475+ }
476+ }
477+ if ($ tickCount >= 30 ) {
478+ $ server ->stop ();
479+ }
480+ });
481+
482+ $ server ->run ();
483+
484+ self ::assertTrue ($ routerCalled , 'Router should be called for authorized request ' );
485+ self ::assertStringContainsString ('200 OK ' , $ responseData );
486+ }
327487}
0 commit comments