Added attribute tests.

This commit is contained in:
flash 2024-08-16 14:50:38 +00:00
parent b503c9edb9
commit 25e0441c1c
7 changed files with 160 additions and 48 deletions

View file

@ -1,7 +1,7 @@
<?php
// RpcActionAttribute.php
// RpcAction.php
// Created: 2024-08-15
// Updated: 2024-08-15
// Updated: 2024-08-16
namespace Aiwass;
@ -13,7 +13,7 @@ use ReflectionObject;
* Provides base for attributes that mark methods in a class as RPC action handlers.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class RpcActionAttribute {
class RpcAction {
/**
* @param bool $isProcedure true if the action is a procedure, false if query.
* @param string $name Action name.
@ -52,7 +52,7 @@ class RpcActionAttribute {
$methodInfos = $objectInfo->getMethods();
foreach($methodInfos as $methodInfo) {
$attrInfos = $methodInfo->getAttributes(RpcActionAttribute::class, ReflectionAttribute::IS_INSTANCEOF);
$attrInfos = $methodInfo->getAttributes(RpcAction::class, ReflectionAttribute::IS_INSTANCEOF);
foreach($attrInfos as $attrInfo) {
$handlerInfo = $attrInfo->newInstance();

View file

@ -1,7 +1,7 @@
<?php
// RpcActionHandlerTrait.php
// Created: 2024-08-15
// Updated: 2024-08-15
// Updated: 2024-08-16
namespace Aiwass;
@ -10,7 +10,7 @@ namespace Aiwass;
* For more advanced use, everything can be use'd separately and RpcActionAttribute::register called manually.
*/
trait RpcActionHandlerTrait {
public function registerRoutes(RpcServer $server): void {
RpcActionAttribute::register($server, $this);
public function registerRpcActions(RpcServer $server): void {
RpcAction::register($server, $this);
}
}

View file

@ -1,12 +1,14 @@
<?php
// RpcActionInfo.php
// Created: 2024-08-13
// Updated: 2024-08-13
// Updated: 2024-08-16
namespace Aiwass;
use Closure;
use InvalidArgumentException;
use ReflectionFunction;
use RuntimeException;
/**
* Provides information about an RPC action.
@ -55,4 +57,37 @@ final class RpcActionInfo {
public function getHandler(): Closure {
return Closure::fromCallable($this->handler);
}
/**
* Invokes the handler and ensures required args are specified.
*
* @param array<string, mixed> $args Arguments to invoke the handler with.
* @throws RuntimeException If any given data was invalid.
* @return mixed Result of the handler
*/
public function invokeHandler(array $args = []): mixed {
$handlerInfo = new ReflectionFunction($this->getHandler());
if($handlerInfo->isVariadic())
throw new RuntimeException('variadic');
$handlerArgs = [];
$handlerParams = $handlerInfo->getParameters();
foreach($handlerParams as $paramInfo) {
$paramName = $paramInfo->getName();
if(!array_key_exists($paramName, $args)) {
if($paramInfo->isOptional())
continue;
if(!$paramInfo->allowsNull())
throw new RuntimeException('required');
}
$handlerArgs[$paramName] = $args[$paramName] ?? null;
}
if(count($handlerArgs) !== count($args))
throw new RuntimeException('params');
return $handlerInfo->invokeArgs($handlerArgs);
}
}

View file

@ -1,7 +1,7 @@
<?php
// RpcProcedureAttribute.php
// RpcProcedure.php
// Created: 2024-08-15
// Updated: 2024-08-15
// Updated: 2024-08-16
namespace Aiwass;
@ -10,7 +10,7 @@ use Attribute;
* Provides an attribute for marking methods in a class as an RPC Query action.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class RpcProcedureAttribute extends RpcActionAttribute {
class RpcProcedure extends RpcAction {
/**
* @param string $name Action name.
*/

View file

@ -1,7 +1,7 @@
<?php
// RpcQueryAttribute.php
// RpcQuery.php
// Created: 2024-08-15
// Updated: 2024-08-15
// Updated: 2024-08-16
namespace Aiwass;
@ -10,7 +10,7 @@ use Attribute;
* Provides an attribute for marking methods in a class as an RPC Query action.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class RpcQueryAttribute extends RpcActionAttribute {
class RpcQuery extends RpcAction {
/**
* @param string $name Action name.
*/

View file

@ -1,15 +1,12 @@
<?php
// RpcRouteHandler.php
// Created: 2024-08-13
// Updated: 2024-08-13
// Updated: 2024-08-16
namespace Aiwass;
use Exception;
use ReflectionFunction;
use ReflectionIntersectionType;
use ReflectionNamedType;
use ReflectionUnionType;
use RuntimeException;
use Index\Http\{HttpResponseBuilder,HttpRequest};
use Index\Http\Content\FormContent;
use Index\Http\Routing\{HttpGet,HttpPost,RouteHandler};
@ -31,7 +28,7 @@ class RpcRouteHandler extends RouteHandler {
* @param HttpResponseBuilder $response Response that it being built.
* @param HttpRequest $request Request that is being handled.
* @param string $action Name of the action specified in the URL.
* @return int|array{error: string, message?: string}|mixed Response to the request.
* @return int|string Response to the request.
*/
#[HttpGet('/_aiwass/([A-Za-z0-9\-_\.:]+)')]
#[HttpPost('/_aiwass/([A-Za-z0-9\-_\.:]+)')]
@ -61,49 +58,29 @@ class RpcRouteHandler extends RouteHandler {
$userToken = (string)$request->getHeaderLine('X-Aiwass-Verify');
if($userToken === '') {
$response->setStatusCode(403);
return AiwassMsgPack::encode(['error' => 'aiwass:verify', 'message' => 'X-Aiwass-Verify header is missing.']);
return AiwassMsgPack::encode(['error' => 'aiwass:verify']);
}
$actInfo = $this->server->getActionInfo($action);
if($actInfo === null) {
$response->setStatusCode(404);
return AiwassMsgPack::encode(['error' => 'aiwass:unknown', 'message' => 'No action with that name exists.']);
return AiwassMsgPack::encode(['error' => 'aiwass:unknown']);
}
if($actInfo->isProcedure() !== $expectProcedure) {
$response->setStatusCode(405);
$response->setHeader('Allow', $actInfo->isProcedure() ? 'POST' : 'GET');
return AiwassMsgPack::encode(['error' => 'aiwass:method', 'message' => 'This action cannot be called using this request method.']);
return AiwassMsgPack::encode(['error' => 'aiwass:method']);
}
if(!$this->server->getVerificationProvider()->verify($userToken, $expectProcedure, $action, $paramString))
return AiwassMsgPack::encode(['error' => 'aiwass:verify', 'message' => 'Request token verification failed.']);
return AiwassMsgPack::encode(['error' => 'aiwass:verify']);
$handlerInfo = new ReflectionFunction($actInfo->getHandler());
if($handlerInfo->isVariadic())
return AiwassMsgPack::encode(['error' => 'aiwass:variadic', 'message' => 'Handler was declared as a variadic method and is thus impossible to be called.']);
$handlerArgs = [];
$handlerParams = $handlerInfo->getParameters();
foreach($handlerParams as $paramInfo) {
$paramName = $paramInfo->getName();
if(!array_key_exists($paramName, $params)) {
if($paramInfo->isOptional())
continue;
if(!$paramInfo->allowsNull())
return AiwassMsgPack::encode(['error' => 'aiwass:param:required', 'message' => 'A required parameter is missing.', 'param' => $paramName]);
}
$handlerArgs[$paramName] = $params[$paramName] ?? null;
}
if(count($handlerArgs) !== count($params)) {
try {
return AiwassMsgPack::encode($actInfo->invokeHandler($params));
} catch(RuntimeException $ex) {
$response->setStatusCode(400);
return AiwassMsgPack::encode(['error' => 'aiwass:params', 'message' => 'Unsupported arguments were specified.']);
return AiwassMsgPack::encode(['error' => sprintf('aiwass:%s', $ex->getMessage())]);
}
return AiwassMsgPack::encode($handlerInfo->invokeArgs($handlerArgs));
}
}

100
tests/AttributesTest.php Normal file
View file

@ -0,0 +1,100 @@
<?php
// AttributesTest.php
// Created: 2024-08-16
// Updated: 2024-08-16
declare(strict_types=1);
use RuntimeException;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\{CoversClass,UsesClass};
use Aiwass\{RpcAction,RpcActionHandler,RpcProcedure,RpcQuery,RpcServer};
#[CoversClass(RpcAction::class)]
#[CoversClass(RpcActionHandler::class)]
#[CoversClass(RpcProcedure::class)]
#[CoversClass(RpcQuery::class)]
#[UsesClass(RpcServer::class)]
final class AttributesTest extends TestCase {
public function testAttributes(): void {
$handler = new class($this) extends RpcActionHandler {
private static $that;
public function __construct(private $self) {
self::$that = $self;
}
#[RpcAction(true, 'aiwass:test:proc1')]
public function procedureRegisteredUsingActionAttribute() {
return 'procedure registered using action attribute';
}
#[RpcAction(false, 'aiwass:test:query1')]
public function queryRegisteredUsingActionAttribute(string $beans) {
$this->self->assertEquals('it is beans', $beans);
return 'query registered using action attribute';
}
#[RpcQuery('aiwass:test:query2')]
public static function staticQueryRegisteredUsingQueryAttribute(string $required, string $optional = 'the') {
self::$that->assertEquals('internet', $required);
self::$that->assertEquals('the', $optional);
return 'static query registered using query attribute';
}
#[RpcProcedure('aiwass:test:proc2')]
public function dynamicProcedureRegisteredUsingProcedureAttribute(string $optional = 'meow') {
$this->self->assertEquals('meow', $optional);
return 'dynamic procedure registered using procedure attribute';
}
#[RpcQuery('aiwass:test:query3')]
#[RpcProcedure('aiwass:test:proc3')]
public function multiple() {
return 'a dynamic method registered as both a query and a procedure';
}
public function hasNoAttr() {
return 'not an action handler';
}
};
$server = RpcServer::createHmac(fn() => 'test');
$server->register($handler);
$this->assertNull($server->getActionInfo('aiwass:none'));
$proc1 = $server->getActionInfo('aiwass:test:proc1');
$this->assertTrue($proc1->isProcedure());
$this->assertEquals('aiwass:test:proc1', $proc1->getName());
$this->assertEquals('procedure registered using action attribute', $proc1->invokeHandler());
$proc2 = $server->getActionInfo('aiwass:test:proc2');
$this->assertTrue($proc2->isProcedure());
$this->assertEquals('aiwass:test:proc2', $proc2->getName());
$this->assertEquals('dynamic procedure registered using procedure attribute', $proc2->invokeHandler());
$query1 = $server->getActionInfo('aiwass:test:query1');
$this->assertFalse($query1->isProcedure());
$this->assertEquals('aiwass:test:query1', $query1->getName());
$this->assertEquals('query registered using action attribute', $query1->invokeHandler(['beans' => 'it is beans']));
$query2 = $server->getActionInfo('aiwass:test:query2');
$this->assertFalse($query2->isProcedure());
$this->assertEquals('aiwass:test:query2', $query2->getName());
$this->assertEquals('static query registered using query attribute', $query2->invokeHandler(['required' => 'internet']));
$query3 = $server->getActionInfo('aiwass:test:query3');
$proc3 = $server->getActionInfo('aiwass:test:proc3');
$this->assertFalse($query3->isProcedure());
$this->asserttrue($proc3->isProcedure());
$this->assertEquals('aiwass:test:query3', $query3->getName());
$this->assertEquals('aiwass:test:proc3', $proc3->getName());
$this->assertEquals($query3->getHandler(), $proc3->getHandler());
$this->assertEquals('a dynamic method registered as both a query and a procedure', $query3->invokeHandler());
$this->assertEquals('a dynamic method registered as both a query and a procedure', $proc3->invokeHandler());
$this->expectException(RuntimeException::class);
$query1->invokeHandler(['notbeans' => 'it is not beans']);
}
}