diff --git a/src/Providers/Http/DTO/BearerTokenRequestAuthentication.php b/src/Providers/Http/DTO/BearerTokenRequestAuthentication.php new file mode 100644 index 00000000..153904b8 --- /dev/null +++ b/src/Providers/Http/DTO/BearerTokenRequestAuthentication.php @@ -0,0 +1,109 @@ + + */ +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], + ]; + } +} diff --git a/src/Providers/Http/Enums/RequestAuthenticationMethod.php b/src/Providers/Http/Enums/RequestAuthenticationMethod.php index 1fb92dbc..fbd3141e 100644 --- a/src/Providers/Http/Enums/RequestAuthenticationMethod.php +++ b/src/Providers/Http/Enums/RequestAuthenticationMethod.php @@ -8,6 +8,7 @@ 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. @@ -15,7 +16,9 @@ * @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 { @@ -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. * @@ -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; } } diff --git a/tests/mocks/MockCustomAuthModel.php b/tests/mocks/MockCustomAuthModel.php new file mode 100644 index 00000000..37698b4b --- /dev/null +++ b/tests/mocks/MockCustomAuthModel.php @@ -0,0 +1,21 @@ +getModelMetadata($modelId); + $config = $modelConfig ?? new ModelConfig(); + + return new MockCustomAuthModel($modelMetadata, $config); + } + + /** + * Resets static state for testing. + */ + public static function reset(): void + { + parent::reset(); + } +} diff --git a/tests/mocks/MockCustomRequestAuthentication.php b/tests/mocks/MockCustomRequestAuthentication.php new file mode 100644 index 00000000..c851ee2f --- /dev/null +++ b/tests/mocks/MockCustomRequestAuthentication.php @@ -0,0 +1,22 @@ +withHeader('X-Mock-Auth', 'custom'); + } +} diff --git a/tests/unit/Providers/ProviderRegistryTest.php b/tests/unit/Providers/ProviderRegistryTest.php index ff8197e6..f60d6a51 100644 --- a/tests/unit/Providers/ProviderRegistryTest.php +++ b/tests/unit/Providers/ProviderRegistryTest.php @@ -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; @@ -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(); } @@ -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. * @@ -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 @@ -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); @@ -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);