Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions infra/Http/Emitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ class Emitter
{
public function emit(Response $response): void
{
http_response_code($response->status);
http_response_code($response->getStatus());

foreach ($response->headers as $name => $value) {
foreach ($response->getHeaders() as $name => $value) {
header("$name: $value");
}

echo $response->body;
echo $response->getBody();
}
}
6 changes: 3 additions & 3 deletions infra/Http/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class Request
{
/** @var string[] */
/** @var array<string,float|int|string> */
private array $params;

/**
Expand Down Expand Up @@ -109,13 +109,13 @@ public function getMethod(): HttpMethod
return $this->httpMethod;
}

/** @param string[] $params */
/** @param array<string,float|int|string> $params */
public function setParams(array $params): void
{
$this->params = $params;
}

/** @return string[] */
/** @return array<string,float|int|string> */
public function getParams(): array
{
return $this->params;
Expand Down
24 changes: 20 additions & 4 deletions infra/Http/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

namespace Infra\Http;

class Response
readonly class Response
{
public function __construct(
public int $status = 200,
public string $body = '',
private int $status = 200,
private string $body = '',
/** @var array<string,string> */
public array $headers = []
private array $headers = []
) {}

/** @param mixed[] $data */
Expand Down Expand Up @@ -39,4 +39,20 @@ public static function redirect(string $to, int $status = 302): self
headers: ['Location' => $to]
);
}

public function getStatus(): int
{
return $this->status;
}

public function getBody(): string
{
return $this->body;
}

/** @return string[] */
public function getHeaders(): array
{
return $this->headers;
}
}
11 changes: 9 additions & 2 deletions infra/Http/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class Route
{
/** @var string[] */
/** @var array<string,float|int|string> */
private array $params;

public function __construct(
Expand All @@ -25,7 +25,7 @@ public function getHandler(): RequestHandlerInterface
return $this->handler;
}

/** @return string[] */
/** @return array<string,float|int|string> */
public function getParams(): array
{
return $this->params;
Expand All @@ -46,6 +46,13 @@ public function match(Request $request): bool
// Extract named parameters
foreach ($matches as $key => $value) {
if (is_string($key)) {
if (filter_var($value, FILTER_VALIDATE_FLOAT)) {
$value = floatval($value);
}

if (filter_var($value, FILTER_VALIDATE_INT)) {
$value = intval($value);
}
$this->params[$key] = $value;
}
}
Expand Down
33 changes: 0 additions & 33 deletions tests/Feature/HttpTest.php

This file was deleted.

25 changes: 25 additions & 0 deletions tests/Unit/app/Handlers/HomeHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Web;

use Infra\Enums\HttpMethod;
use Infra\Http\Request;
use Infra\Http\Response;
use Infra\Http\Router;

describe('Web', function () {
it('should return home page', function () {
$request = new Request('/', HttpMethod::GET, []);
$router = new Router($request);

(require 'app/Routes/web.php')($router);

$response = $router->handleRequest();

expect($response)->toBeInstanceOf(Response::class)
->and($response->getBody())->toBe('<h1>Home</h1>')
->and($response->getStatus())->toBe(200);
});
});
33 changes: 33 additions & 0 deletions tests/Unit/app/Handlers/PostHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Api;

use Infra\Enums\HttpMethod;
use Infra\Http\Request;
use Infra\Http\Response;
use Infra\Http\Router;

describe('Api', function () {
it('should return a json response', function () {
$data = ['test' => 'data'];
$params = ['id' => 1.1];

$request = new Request("/api/posts/{$params['id']}", HttpMethod::POST, $data);
$router = new Router($request);

(require 'app/Routes/api.php')($router);

$response = $router->handleRequest();

$expected = [
'data' => $data,
'params' => $params,
];

expect($response)->toBeInstanceOf(Response::class)
->and($response->getBody())->toBe(json_encode($expected))
->and($response->getStatus())->toBe(200);
});
});
23 changes: 23 additions & 0 deletions tests/Unit/infra/Http/Handlers/NotFoundHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Infra\Http;

use Infra\Enums\HttpMethod;
use Infra\Http\Request;
use Infra\Http\Response;
use Infra\Http\Router;

describe('Not Found', function () {
it('should return not found response', function () {
$request = new Request('/not-found', HttpMethod::GET, []);
$router = new Router($request);

$response = $router->handleRequest();

expect($response)->toBeInstanceOf(Response::class)
->and($response->getBody())->toBe(json_encode(['error' => 'Not Found']))
->and($response->getStatus())->toBe(404);
});
});
38 changes: 19 additions & 19 deletions tests/Unit/infra/Http/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
it('should create a response with default values', function () {
$response = new Response;

expect($response->status)->toBe(200)
->and($response->headers)->toBe([])
->and($response->body)->toBe('');
expect($response->getStatus())->toBe(200)
->and($response->getHeaders())->toBe([])
->and($response->getBody())->toBe('');
});

it('should create a response with custom values', function () {
Expand All @@ -20,9 +20,9 @@
headers: ['X-Test' => 'true']
);

expect($response->status)->toBe(404)
->and($response->headers)->toBe(['X-Test' => 'true'])
->and($response->body)->toBe('Not Found');
expect($response->getStatus())->toBe(404)
->and($response->getHeaders())->toBe(['X-Test' => 'true'])
->and($response->getBody())->toBe('Not Found');
});
});

Expand All @@ -31,24 +31,24 @@
$data = ['user' => 'John Doe', 'id' => 123];
$response = Response::json($data);

expect($response->status)->toBe(200)
->and($response->headers)->toBe(['Content-Type' => 'application/json'])
->and($response->body)->toBe(json_encode($data));
expect($response->getStatus())->toBe(200)
->and($response->getHeaders())->toBe(['Content-Type' => 'application/json'])
->and($response->getBody())->toBe(json_encode($data));
});

it('should create a JSON response with a custom status', function () {
$data = ['error' => 'Invalid input'];
$response = Response::json($data, 422);

expect($response->status)->toBe(422)
->and($response->headers)->toBe(['Content-Type' => 'application/json'])
->and($response->body)->toBe(json_encode($data));
expect($response->getStatus())->toBe(422)
->and($response->getHeaders())->toBe(['Content-Type' => 'application/json'])
->and($response->getBody())->toBe(json_encode($data));
});

it('should handle an empty array for a JSON response', function () {
$response = Response::json([]);

expect($response->body)->toBe('[]');
expect($response->getBody())->toBe('[]');
});
});

Expand All @@ -57,17 +57,17 @@
$html = '<h1>Hello, World!</h1>';
$response = Response::html($html);

expect($response->status)->toBe(200)
->and($response->headers)->toBe(['Content-Type' => 'text/html; charset=UTF-8'])
->and($response->body)->toBe($html);
expect($response->getStatus())->toBe(200)
->and($response->getHeaders())->toBe(['Content-Type' => 'text/html; charset=UTF-8'])
->and($response->getBody())->toBe($html);
});

it('should create an HTML response with a custom status', function () {
$html = '<h1>Unauthorized</h1>';
$response = Response::html($html, 401);

expect($response->status)->toBe(401)
->and($response->headers)->toBe(['Content-Type' => 'text/html; charset=UTF-8'])
->and($response->body)->toBe($html);
expect($response->getStatus())->toBe(401)
->and($response->getHeaders())->toBe(['Content-Type' => 'text/html; charset=UTF-8'])
->and($response->getBody())->toBe($html);
});
});
20 changes: 10 additions & 10 deletions tests/Unit/infra/Http/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@

$response = $router->handleRequest();

expect($response->status)->toBe(200)
->and($response->body)->toBe('Success');
expect($response->getStatus())->toBe(200)
->and($response->getBody())->toBe('Success');
});

it('should return a 404 response when no route matches the request URI', function () {
Expand All @@ -30,7 +30,7 @@

$response = $router->handleRequest();

expect($response->status)->toBe(404);
expect($response->getStatus())->toBe(404);
});

it('should return a 404 response when the path matches but the HTTP method does not', function () {
Expand All @@ -40,7 +40,7 @@

$response = $router->handleRequest();

expect($response->status)->toBe(404);
expect($response->getStatus())->toBe(404);
});

it('should inject route parameters into the request object', function () {
Expand All @@ -51,8 +51,8 @@
$router->handleRequest();

expect($request)->not->toBeNull()
->and($request->getParam('id'))->toBe('123')
->and($request->getParams())->toBe(['id' => '123']);
->and($request->getParam('id'))->toBe(123)
->and($request->getParams())->toBe(['id' => 123]);
});
});

Expand All @@ -65,8 +65,8 @@
$router->{$routerMethod}('/test', $handler);
$response = $router->handleRequest();

expect($response->status)->toBe(201)
->and($response->body)->toBe('Created');
expect($response->getStatus())->toBe(201)
->and($response->getBody())->toBe('Created');
})->with([
'GET' => [HttpMethod::GET, 'get'],
'POST' => [HttpMethod::POST, 'post'],
Expand All @@ -82,7 +82,7 @@

$response = $router->handleRequest();

expect($response->status)->toBe(301)
->and($response->headers['Location'])->toBe('/new-path');
expect($response->getStatus())->toBe(301)
->and($response->getHeaders()['Location'])->toBe('/new-path');
});
});