Files
intotheeast-com-content/plugins/api/tests/Unit/Auth/ApiKeyAuthenticatorTest.php
T

257 lines
7.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Grav\Plugin\Api\Tests\Unit\Auth;
use Grav\Common\Yaml;
use Grav\Plugin\Api\Auth\ApiKeyAuthenticator;
use Grav\Plugin\Api\Auth\ApiKeyManager;
use Grav\Plugin\Api\Tests\Unit\TestHelper;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
/**
* Unit tests for ApiKeyAuthenticator.
*
* API keys live in the central user/data/api-keys.yaml store (keyed by id,
* each entry carrying its owning `username`). The authenticator looks a raw
* key up in that store, then loads the matching account. Tests seed the store
* via a temp-dir `user://data` locator and provide a matching accounts mock.
*/
#[CoversClass(ApiKeyAuthenticator::class)]
class ApiKeyAuthenticatorTest extends TestCase
{
private const RAW_KEY = 'grav_test_api_key_raw_value_1234';
private string $dataDir;
protected function setUp(): void
{
$this->dataDir = sys_get_temp_dir() . '/grav_api_authn_test_' . uniqid();
@mkdir($this->dataDir, 0775, true);
$this->resetKeysCache();
}
protected function tearDown(): void
{
$this->resetKeysCache();
$this->rmrf($this->dataDir);
}
private function resetKeysCache(): void
{
(new \ReflectionProperty(ApiKeyManager::class, 'keysCache'))->setValue(null, null);
}
private function rmrf(string $dir): void
{
if (!is_dir($dir)) {
return;
}
foreach (scandir($dir) as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . '/' . $item;
is_dir($path) ? $this->rmrf($path) : unlink($path);
}
rmdir($dir);
}
/**
* Build an authenticator with the central key store seeded and the given
* accounts available.
*
* @param array<string, array> $keys central store entries keyed by id
* @param array<string, object> $users accounts keyed by username
*/
private function buildAuthenticator(array $keys, array $users = []): ApiKeyAuthenticator
{
$dataDir = $this->dataDir;
$locator = new class ($dataDir) {
public function __construct(private string $dir) {}
public function findResource(string $uri, bool $absolute = true, bool $first = false): string
{
return $this->dir;
}
};
$accounts = TestHelper::createMockAccounts($users);
$grav = TestHelper::createMockGrav(['accounts' => $accounts, 'locator' => $locator]);
// Seed the central store after the Grav container exists.
file_put_contents($this->dataDir . '/api-keys.yaml', Yaml::dump($keys));
$this->resetKeysCache();
return new ApiKeyAuthenticator($grav);
}
#[Test]
public function returns_null_when_no_api_key_present(): void
{
$authenticator = $this->buildAuthenticator([]);
$request = TestHelper::createMockRequest();
self::assertNull($authenticator->authenticate($request));
}
#[Test]
public function authenticates_via_header(): void
{
$user = TestHelper::createMockUser('alice');
$authenticator = $this->buildAuthenticator([
'key1' => [
'id' => 'key1',
'username' => 'alice',
'hash' => hash('sha256', self::RAW_KEY),
'active' => true,
'expires' => null,
],
], ['alice' => $user]);
$request = TestHelper::createMockRequest(
headers: ['X-API-Key' => self::RAW_KEY],
);
$result = $authenticator->authenticate($request);
self::assertNotNull($result);
self::assertSame('alice', $result->username);
}
#[Test]
public function authenticates_via_query_param(): void
{
$user = TestHelper::createMockUser('bob');
$authenticator = $this->buildAuthenticator([
'key1' => [
'id' => 'key1',
'username' => 'bob',
'hash' => hash('sha256', self::RAW_KEY),
'active' => true,
'expires' => null,
],
], ['bob' => $user]);
$request = TestHelper::createMockRequest(
queryParams: ['api_key' => self::RAW_KEY],
);
$result = $authenticator->authenticate($request);
self::assertNotNull($result);
self::assertSame('bob', $result->username);
}
#[Test]
public function returns_null_for_invalid_key(): void
{
$user = TestHelper::createMockUser('carol');
$authenticator = $this->buildAuthenticator([
'key1' => [
'id' => 'key1',
'username' => 'carol',
'hash' => hash('sha256', 'some_other_key'),
'active' => true,
],
], ['carol' => $user]);
$request = TestHelper::createMockRequest(
headers: ['X-API-Key' => 'grav_wrong_key_value'],
);
self::assertNull($authenticator->authenticate($request));
}
#[Test]
public function returns_null_for_inactive_key(): void
{
$user = TestHelper::createMockUser('dave');
$authenticator = $this->buildAuthenticator([
'key1' => [
'id' => 'key1',
'username' => 'dave',
'hash' => hash('sha256', self::RAW_KEY),
'active' => false,
],
], ['dave' => $user]);
$request = TestHelper::createMockRequest(
headers: ['X-API-Key' => self::RAW_KEY],
);
self::assertNull($authenticator->authenticate($request));
}
#[Test]
public function returns_null_for_expired_key(): void
{
$user = TestHelper::createMockUser('eve');
$authenticator = $this->buildAuthenticator([
'key1' => [
'id' => 'key1',
'username' => 'eve',
'hash' => hash('sha256', self::RAW_KEY),
'active' => true,
'expires' => time() - 3600, // expired an hour ago
],
], ['eve' => $user]);
$request = TestHelper::createMockRequest(
headers: ['X-API-Key' => self::RAW_KEY],
);
self::assertNull($authenticator->authenticate($request));
}
#[Test]
public function returns_null_when_account_does_not_exist(): void
{
// Key matches, but no account exists for its username.
$authenticator = $this->buildAuthenticator([
'key1' => [
'id' => 'key1',
'username' => 'ghost',
'hash' => hash('sha256', self::RAW_KEY),
'active' => true,
],
], []);
$request = TestHelper::createMockRequest(
headers: ['X-API-Key' => self::RAW_KEY],
);
self::assertNull($authenticator->authenticate($request));
}
#[Test]
public function header_takes_precedence_over_query_param(): void
{
$headerKey = 'grav_header_key_value_123456789';
$queryKey = 'grav_query_key_value_987654321';
$user = TestHelper::createMockUser('frank');
$authenticator = $this->buildAuthenticator([
'key1' => [
'id' => 'key1',
'username' => 'frank',
'hash' => hash('sha256', $headerKey),
'active' => true,
],
], ['frank' => $user]);
$request = TestHelper::createMockRequest(
headers: ['X-API-Key' => $headerKey],
queryParams: ['api_key' => $queryKey],
);
$result = $authenticator->authenticate($request);
self::assertNotNull($result);
self::assertSame('frank', $result->username);
}
}