Skip to content

Commit d750f77

Browse files
committed
wip
1 parent a2e700e commit d750f77

5 files changed

Lines changed: 118 additions & 1 deletion

File tree

packages/idempotency/src/Attributes/Idempotent.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
namespace Tempest\Idempotency\Attributes;
66

77
use Attribute;
8+
use Tempest\Idempotency\Exceptions\UnsupportedIdempotencyMethod;
89
use Tempest\Idempotency\Middleware\IdempotencyMiddleware;
10+
use Tempest\Idempotency\SupportedMethod;
911
use Tempest\Router\Route;
1012
use Tempest\Router\RouteDecorator;
1113

@@ -21,6 +23,10 @@ public function __construct(
2123

2224
public function decorate(Route $route): Route
2325
{
26+
if (! SupportedMethod::isSupported($route->method)) {
27+
throw UnsupportedIdempotencyMethod::forMethod($route->method);
28+
}
29+
2430
if (in_array(IdempotencyMiddleware::class, $route->middleware, true)) {
2531
return $route;
2632
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Idempotency\Exceptions;
6+
7+
use RuntimeException;
8+
use Tempest\Http\Method;
9+
use Tempest\Idempotency\SupportedMethod;
10+
11+
final class UnsupportedIdempotencyMethod extends RuntimeException
12+
{
13+
public static function forMethod(Method $method): self
14+
{
15+
return new self(sprintf('Idempotency is only supported for %s methods, `%s` given.', SupportedMethod::allowed(), $method->value));
16+
}
17+
}

packages/idempotency/src/Middleware/IdempotencyMiddleware.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Tempest\Http\Status;
1414
use Tempest\Idempotency\Attributes\Idempotent;
1515
use Tempest\Idempotency\Config\IdempotencyConfig;
16+
use Tempest\Idempotency\Exceptions\UnsupportedIdempotencyMethod;
1617
use Tempest\Idempotency\Fingerprint\HttpFingerprintGenerator;
1718
use Tempest\Idempotency\Store\IdempotencyRecord;
1819
use Tempest\Idempotency\Store\IdempotencyState;
@@ -21,6 +22,7 @@
2122
use Tempest\Idempotency\Support\IdempotencyKeyResolver;
2223
use Tempest\Idempotency\Support\ProcessingOwner;
2324
use Tempest\Idempotency\Support\ProcessingOwnerLiveness;
25+
use Tempest\Idempotency\SupportedMethod;
2426
use Tempest\Router\HttpMiddleware;
2527
use Tempest\Router\HttpMiddlewareCallable;
2628
use Tempest\Router\MatchedRoute;
@@ -41,6 +43,10 @@ public function __construct(
4143

4244
public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
4345
{
46+
if (! SupportedMethod::isSupported($request->method)) {
47+
throw UnsupportedIdempotencyMethod::forMethod($request->method);
48+
}
49+
4450
$options = $this->resolveOptions();
4551
$key = trim($request->headers->get($options['header'], default: '') ?? '');
4652

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Idempotency;
6+
7+
use Tempest\Http\Method;
8+
9+
enum SupportedMethod: string
10+
{
11+
case POST = 'POST';
12+
case PATCH = 'PATCH';
13+
14+
public static function isSupported(Method $method): bool
15+
{
16+
return self::tryFrom($method->value) !== null;
17+
}
18+
19+
/** @return string */
20+
public static function allowed(): string
21+
{
22+
return implode(', ', array_column(self::cases(), 'value'));
23+
}
24+
}

packages/idempotency/tests/IdempotencyMiddlewareTest.php

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Tempest\Http\Status;
2121
use Tempest\Idempotency\Attributes\Idempotent;
2222
use Tempest\Idempotency\Config\IdempotencyConfig;
23+
use Tempest\Idempotency\Exceptions\UnsupportedIdempotencyMethod;
2324
use Tempest\Idempotency\Fingerprint\RequestFingerprintGenerator;
2425
use Tempest\Idempotency\Middleware\IdempotencyMiddleware;
2526
use Tempest\Idempotency\Store\CacheIdempotencyStore;
@@ -129,7 +130,7 @@ public function rejects_the_same_key_when_the_payload_changes(): void
129130
$next,
130131
);
131132

132-
$this->assertSame(Status::CONFLICT, $secondResponse->status);
133+
$this->assertSame(Status::UNPROCESSABLE_CONTENT, $secondResponse->status);
133134
$this->assertSame(1, $calls);
134135
}
135136

@@ -241,6 +242,69 @@ public function idempotent_decorator_does_not_add_duplicate_middleware_entries()
241242
);
242243
}
243244

245+
#[Test]
246+
public function idempotent_decorator_adds_middleware_for_patch_routes(): void
247+
{
248+
$route = new FakeRoute();
249+
$route->method = Method::PATCH;
250+
$attribute = new Idempotent();
251+
252+
$attribute->decorate($route);
253+
254+
$this->assertContains(IdempotencyMiddleware::class, $route->middleware);
255+
}
256+
257+
#[Test]
258+
public function idempotent_decorator_throws_for_get_routes(): void
259+
{
260+
$route = new FakeRoute();
261+
$route->method = Method::GET;
262+
263+
$this->expectException(UnsupportedIdempotencyMethod::class);
264+
265+
new Idempotent()->decorate($route);
266+
}
267+
268+
#[Test]
269+
public function idempotent_decorator_throws_for_put_routes(): void
270+
{
271+
$route = new FakeRoute();
272+
$route->method = Method::PUT;
273+
274+
$this->expectException(UnsupportedIdempotencyMethod::class);
275+
276+
new Idempotent()->decorate($route);
277+
}
278+
279+
#[Test]
280+
public function idempotent_decorator_throws_for_delete_routes(): void
281+
{
282+
$route = new FakeRoute();
283+
$route->method = Method::DELETE;
284+
285+
$this->expectException(UnsupportedIdempotencyMethod::class);
286+
287+
new Idempotent()->decorate($route);
288+
}
289+
290+
#[Test]
291+
public function throws_for_non_post_and_patch_methods(): void
292+
{
293+
$middleware = $this->createMiddleware('create');
294+
295+
$this->expectException(UnsupportedIdempotencyMethod::class);
296+
297+
$middleware(
298+
new GenericRequest(
299+
Method::PUT,
300+
'/orders',
301+
body: ['amount' => 100],
302+
headers: ['Idempotency-Key' => 'order-100'],
303+
),
304+
new HttpMiddlewareCallable(static fn (Request $_): Response => new GenericResponse(Status::OK, ['ok' => true])),
305+
);
306+
}
307+
244308
#[Test]
245309
public function uses_pending_ttl_for_lock_and_completion_ttl_for_pending_record(): void
246310
{

0 commit comments

Comments
 (0)