Skip to content
Open
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
109 changes: 109 additions & 0 deletions src/Providers/Http/DTO/BearerTokenRequestAuthentication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Providers\Http\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;

/**
* Class for HTTP request authentication using a bearer token.
*
* @since n.e.x.t
*
* @phpstan-type BearerTokenRequestAuthenticationArrayShape array{
* bearerToken: string
* }
*
* @extends AbstractDataTransferObject<BearerTokenRequestAuthenticationArrayShape>
*/
class BearerTokenRequestAuthentication extends AbstractDataTransferObject implements RequestAuthenticationInterface
{
public const KEY_BEARER_TOKEN = 'bearerToken';

/**
* @var string The bearer token used for authentication.
*/
protected string $bearerToken;

/**
* Constructor.
*
* @since n.e.x.t
*
* @param string $bearerToken The bearer token used for authentication.
*/
public function __construct(string $bearerToken)
{
$this->bearerToken = $bearerToken;
}

/**
* {@inheritDoc}
*
* @since n.e.x.t
*/
public function authenticateRequest(Request $request): Request
{
return $request->withHeader('Authorization', 'Bearer ' . $this->bearerToken);
}

/**
* Gets the bearer token.
*
* @since n.e.x.t
*
* @return string The bearer token.
*/
public function getBearerToken(): string
{
return $this->bearerToken;
}

/**
* {@inheritDoc}
*
* @since n.e.x.t
*
* @return BearerTokenRequestAuthenticationArrayShape
*/
public function toArray(): array
{
return [
self::KEY_BEARER_TOKEN => $this->bearerToken,
];
}

/**
* {@inheritDoc}
*
* @since n.e.x.t
*/
public static function fromArray(array $array): self
{
static::validateFromArrayData($array, [self::KEY_BEARER_TOKEN]);

return new self($array[self::KEY_BEARER_TOKEN]);
}

/**
* {@inheritDoc}
*
* @since n.e.x.t
*/
public static function getJsonSchema(): array
{
return [
'type' => 'object',
'properties' => [
self::KEY_BEARER_TOKEN => [
'type' => 'string',
'title' => 'Bearer Token',
'description' => 'The bearer token used for authentication.',
],
],
'required' => [self::KEY_BEARER_TOKEN],
];
}
}
16 changes: 14 additions & 2 deletions src/Providers/Http/Enums/RequestAuthenticationMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface;
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication;
use WordPress\AiClient\Providers\Http\DTO\BearerTokenRequestAuthentication;

/**
* Enum for request authentication methods.
*
* @since 0.4.0
*
* @method static self apiKey() Creates an instance for API_KEY method.
* @method static self bearerToken() Creates an instance for BEARER_TOKEN method.
* @method bool isApiKey() Checks if the method is API_KEY.
* @method bool isBearerToken() Checks if the method is BEARER_TOKEN.
*/
class RequestAuthenticationMethod extends AbstractEnum
{
Expand All @@ -24,6 +27,13 @@ class RequestAuthenticationMethod extends AbstractEnum
*/
public const API_KEY = 'api_key';

/**
* Bearer token authentication.
*
* @since n.e.x.t
*/
public const BEARER_TOKEN = 'bearer_token';

/**
* Gets the implementation class for the authentication method.
*
Expand All @@ -35,8 +45,10 @@ class RequestAuthenticationMethod extends AbstractEnum
*/
public function getImplementationClass(): string
{
// At the moment, this is the only supported method.
// Once more methods are available, add conditionals here for each method.
if ($this->isBearerToken()) {
return BearerTokenRequestAuthentication::class;
}

return ApiKeyRequestAuthentication::class;
}
}
21 changes: 21 additions & 0 deletions tests/mocks/MockCustomAuthModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Tests\mocks;

use WordPress\AiClient\Providers\DTO\ProviderMetadata;

/**
* Mock model for the custom authentication provider.
*/
class MockCustomAuthModel extends MockModel
{
/**
* {@inheritDoc}
*/
public function providerMetadata(): ProviderMetadata
{
return MockCustomAuthProvider::metadata();
}
}
50 changes: 50 additions & 0 deletions tests/mocks/MockCustomAuthProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Tests\mocks;

use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Enums\ProviderTypeEnum;
use WordPress\AiClient\Providers\Http\Enums\RequestAuthenticationMethod;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;

/**
* Mock provider with bearer token authentication for testing purposes.
*/
class MockCustomAuthProvider extends MockProvider
{
/**
* {@inheritDoc}
*/
public static function metadata(): ProviderMetadata
{
return new ProviderMetadata(
'mock-custom-auth',
'Mock Custom Auth Provider',
ProviderTypeEnum::cloud(),
null,
RequestAuthenticationMethod::bearerToken()
);
}

/**
* {@inheritDoc}
*/
public static function model(string $modelId, ?ModelConfig $modelConfig = null): ModelInterface
{
$modelMetadata = static::modelMetadataDirectory()->getModelMetadata($modelId);
$config = $modelConfig ?? new ModelConfig();

return new MockCustomAuthModel($modelMetadata, $config);
}

/**
* Resets static state for testing.
*/
public static function reset(): void
{
parent::reset();
}
}
22 changes: 22 additions & 0 deletions tests/mocks/MockCustomRequestAuthentication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Tests\mocks;

use WordPress\AiClient\Providers\Http\DTO\BearerTokenRequestAuthentication;
use WordPress\AiClient\Providers\Http\DTO\Request;

/**
* Mock custom request authentication for testing purposes.
*/
class MockCustomRequestAuthentication extends BearerTokenRequestAuthentication
{
/**
* {@inheritDoc}
*/
public function authenticateRequest(Request $request): Request
{
return parent::authenticateRequest($request)->withHeader('X-Mock-Auth', 'custom');
}
}
57 changes: 54 additions & 3 deletions tests/unit/Providers/ProviderRegistryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
use PHPUnit\Framework\TestCase;
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication;
use WordPress\AiClient\Providers\Http\DTO\BearerTokenRequestAuthentication;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Providers\Models\DTO\ModelRequirements;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
use WordPress\AiClient\Providers\ProviderRegistry;
use WordPress\AiClient\Tests\mocks\MockCustomAuthProvider;
use WordPress\AiClient\Tests\mocks\MockCustomRequestAuthentication;
use WordPress\AiClient\Tests\mocks\MockHttpTransporter;
use WordPress\AiClient\Tests\mocks\MockModel;
use WordPress\AiClient\Tests\mocks\MockModelMetadataDirectory;
Expand All @@ -32,12 +35,14 @@ protected function setUp(): void
{
parent::setUp();
$this->registry = new ProviderRegistry();
MockCustomAuthProvider::reset();
MockProvider::reset(); // Reset static state of mock provider before each test.
}

protected function tearDown(): void
{
MockProvider::reset(); // Reset static state of mock provider after each test.
MockCustomAuthProvider::reset();
parent::tearDown();
}

Expand Down Expand Up @@ -376,6 +381,46 @@ public function testGetProviderRequestAuthenticationReturnsDefault(): void
$this->assertNull($retrievedAuth);
}

/**
* Tests that explicit bearer token authentication is bound to models.
*
* @return void
*/
public function testSetProviderRequestAuthenticationBindsBearerTokenAuthenticationToModels(): void
{
$this->registry->registerProvider(MockCustomAuthProvider::class);

$requestAuthentication = new MockCustomRequestAuthentication('test-token');
$this->registry->setProviderRequestAuthentication('mock-custom-auth', $requestAuthentication);

$model = $this->registry->getProviderModel('mock-custom-auth', 'mock-text-model');

$this->assertSame(
$requestAuthentication,
$this->registry->getProviderRequestAuthentication('mock-custom-auth')
);
$this->assertSame($requestAuthentication, $model->getRequestAuthentication());
}

/**
* Tests default bearer token authentication creation from environment data.
*
* @return void
*/
public function testCreateDefaultProviderRequestAuthenticationWithBearerTokenEnvVar(): void
{
putenv('MOCK_CUSTOM_AUTH_BEARER_TOKEN=test_bearer_token');

$this->registry->registerProvider(MockCustomAuthProvider::class);

$auth = $this->registry->getProviderRequestAuthentication('mock-custom-auth');

$this->assertInstanceOf(BearerTokenRequestAuthentication::class, $auth);
$this->assertSame('test_bearer_token', $auth->getBearerToken());

putenv('MOCK_CUSTOM_AUTH_BEARER_TOKEN');
}

/**
* Tests the internal getEnvVarName method using reflection.
*
Expand All @@ -388,7 +433,9 @@ public function testGetProviderRequestAuthenticationReturnsDefault(): void
public function testGetEnvVarName(string $providerId, string $field, string $expected): void
{
$method = new \ReflectionMethod(ProviderRegistry::class, 'getEnvVarName');
$method->setAccessible(true);
if (PHP_VERSION_ID < 80100) {
$method->setAccessible(true);
}

$result = $method->invoke($this->registry, $providerId, $field); // Invoke on instance

Expand Down Expand Up @@ -424,7 +471,9 @@ public function testCreateDefaultProviderRequestAuthenticationWithEnvVar(): void
$this->registry->registerProvider(MockProvider::class);

$method = new \ReflectionMethod(ProviderRegistry::class, 'createDefaultProviderRequestAuthentication');
$method->setAccessible(true);
if (PHP_VERSION_ID < 80100) {
$method->setAccessible(true);
}

$auth = $method->invoke($this->registry, MockProvider::class);

Expand All @@ -448,7 +497,9 @@ public function testCreateDefaultProviderRequestAuthenticationWithoutEnvVar(): v
$this->registry->registerProvider(MockProvider::class);

$method = new \ReflectionMethod(ProviderRegistry::class, 'createDefaultProviderRequestAuthentication');
$method->setAccessible(true);
if (PHP_VERSION_ID < 80100) {
$method->setAccessible(true);
}

$auth = $method->invoke($this->registry, MockProvider::class);

Expand Down
Loading