<?php
// AttributesTest.php
// Created: 2024-08-16
// Updated: 2025-03-20
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\{CoversClass,UsesClass};
use Index\Http\{HttpRequest,HttpUri};
use Index\Http\Routing\Router;
use Index\Http\Streams\Stream;
use RPCii\{HmacVerificationProvider,RpciiMsgPack};
use RPCii\Server\{RpcAction,RpcHandler,RpcHandlerCommon,RpcProcedure,RpcQuery,HttpRpcServer};
#[CoversClass(RpcAction::class)]
#[CoversClass(RpcHandler::class)]
#[CoversClass(RpcProcedure::class)]
#[CoversClass(RpcQuery::class)]
#[UsesClass(HttpRpcServer::class)]
#[UsesClass(HmacVerificationProvider::class)]
final class AttributesTest extends TestCase {
public function testAttributes(): void {
$handler = new class($this) implements RpcHandler {
use RpcHandlerCommon;
private static AttributesTest $that;
public function __construct(private AttributesTest $self) {
self::$that = $self;
}
#[RpcProcedure(true, 'aiwass:test:proc1')]
public function procedureRegisteredUsingActionAttribute(): string {
return 'procedure registered using action attribute';
}
#[RpcProcedure(false, 'aiwass:test:query1')]
public function queryRegisteredUsingActionAttribute(string $beans): string {
$this->self->assertSame('it is beans', $beans);
return 'query registered using action attribute';
}
#[RpcQuery('aiwass:test:query2')]
public static function staticQueryRegisteredUsingQueryAttribute(string $required, string $optional = 'the'): string {
self::$that->assertSame('internet', $required);
self::$that->assertSame('the', $optional);
return 'static query registered using query attribute';
}
#[RpcAction('aiwass:test:proc2')]
public function dynamicProcedureRegisteredUsingProcedureAttribute(string $optional = 'meow'): string {
$this->self->assertSame('meow', $optional);
return 'dynamic procedure registered using procedure attribute';
}
#[RpcQuery('aiwass:test:query4')]
public function queryForRouter(string $required, string $optional = 'the'): string {
return sprintf('required=%s, optional=%s', $required, $optional);
}
#[RpcAction('aiwass:test:proc4')]
public function procedureForRouter(string $required, string $optional = 'the'): string {
return sprintf('optional=%s, required=%s', $required, $optional);
}
#[RpcQuery('aiwass:test:query3')]
#[RpcAction('aiwass:test:proc3')]
public function multiple(): string {
return 'a dynamic method registered as both a query and a procedure';
}
public function hasNoAttr(): string {
return 'not an action handler';
}
};
$server = new HttpRpcServer;
$server->register($handler);
$this->assertNull($server->getProcedureInfo('aiwass:none'));
$act1 = $server->getProcedureInfo('aiwass:test:proc1');
$this->assertNotNull($act1);
$this->assertTrue($act1->action);
$this->assertSame('aiwass:test:proc1', $act1->name);
$this->assertSame('procedure registered using action attribute', $act1->invokeHandler());
$act2 = $server->getProcedureInfo('aiwass:test:proc2');
$this->assertNotNull($act2);
$this->assertTrue($act2->action);
$this->assertSame('aiwass:test:proc2', $act2->name);
$this->assertSame('dynamic procedure registered using procedure attribute', $act2->invokeHandler());
$query1 = $server->getProcedureInfo('aiwass:test:query1');
$this->assertNotNull($query1);
$this->assertFalse($query1->action);
$this->assertSame('aiwass:test:query1', $query1->name);
$this->assertSame('query registered using action attribute', $query1->invokeHandler(['beans' => 'it is beans']));
$query2 = $server->getProcedureInfo('aiwass:test:query2');
$this->assertNotNull($query2);
$this->assertFalse($query2->action);
$this->assertSame('aiwass:test:query2', $query2->name);
$this->assertSame('static query registered using query attribute', $query2->invokeHandler(['required' => 'internet']));
$query3 = $server->getProcedureInfo('aiwass:test:query3');
$proc3 = $server->getProcedureInfo('aiwass:test:proc3');
$this->assertNotNull($query3);
$this->assertNotNull($proc3);
$this->assertFalse($query3->action);
$this->asserttrue($proc3->action);
$this->assertSame('aiwass:test:query3', $query3->name);
$this->assertSame('aiwass:test:proc3', $proc3->name);
$this->assertEquals($query3->handler, $proc3->handler);
$this->assertSame('a dynamic method registered as both a query and a procedure', $query3->invokeHandler());
$this->assertSame('a dynamic method registered as both a query and a procedure', $proc3->invokeHandler());
$this->assertNull($server->getProcedureInfo('doesnotexist'));
$router = new Router;
$verification = new HmacVerificationProvider(fn() => 'meow');
$router->register($server->createRouteHandler($verification));
$response = $router->handle(HttpRequest::createRequestWithoutBody(
'GET', '/_rpcii/aiwass:test:query4',
[
'X-RPCii-Format' => ['2'],
'X-RPCii-Verify' => [$verification->sign(false, 'aiwass:test:query4', 'required=mewow')],
],
)->withQueryParams(['required' => ['mewow']]));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/vnd.msgpack', $response->getHeaderLine('Content-Type'));
$this->assertSame('2', $response->getHeaderLine('X-RPCii-Format'));
$this->assertSame(
['state' => 'ok', 'result' => 'required=mewow, optional=the'],
RpciiMsgPack::decode((string)$response->getBody())
);
$response = $router->handle(HttpRequest::createRequestWithBody(
'POST', '/_rpcii/aiwass:test:proc4',
[
'Content-Type' => ['application/x-www-form-urlencoded'],
'X-RPCii-Format' => ['2'],
'X-RPCii-Verify' => [$verification->sign(true, 'aiwass:test:proc4', 'optional=omg&required=woof')],
],
Stream::createStream(http_build_query(['optional' => 'omg', 'required' => 'woof'], '', '&', PHP_QUERY_RFC3986)),
));
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('application/vnd.msgpack', $response->getHeaderLine('Content-Type'));
$this->assertSame('2', $response->getHeaderLine('X-RPCii-Format'));
$this->assertSame(
['state' => 'ok', 'result' => 'optional=woof, required=omg'],
RpciiMsgPack::decode((string)$response->getBody())
);
$this->expectException(RuntimeException::class);
$query1->invokeHandler(['notbeans' => 'it is not beans']);
}
}