feat: pemissions
Integration Tests / Integration Tests (pull_request) Successful in 52s

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-23 18:01:26 -04:00
parent e36a1dbfba
commit 3e921fc07e
6 changed files with 109 additions and 29 deletions
+40 -4
View File
@@ -7,6 +7,7 @@ namespace KTXT\AuthenticationProviderPassword\Tests\Integration;
use KTXC\Http\Request\Request;
use KTXC\Http\Response\Response;
use KTXC\Server;
use KTXC\Stores\UserRolesStore;
use PHPUnit\Framework\TestCase;
/**
@@ -67,14 +68,21 @@ abstract class IntegrationTestCase extends TestCase
// =========================================================================
/**
* Provision a user (optionally with a password credential) and return
* its uid, so tests can exercise admin endpoints that operate by uid.
* Provision a user (optionally with a password credential and roles) and
* return its uid, so tests can exercise admin endpoints that operate by uid.
*
* @param string[] $roles Role IDs to assign (see createRoleWithPermissions())
*/
protected static function createUser(string $identity, ?string $password = null): string
protected static function createUser(string $identity, ?string $password = null, array $roles = []): string
{
$uid = bin2hex(random_bytes(8));
static::runConsole(['user:create', static::$tenantIdentifier, $identity, '--uid', $uid]);
$args = ['user:create', static::$tenantIdentifier, $identity, '--uid', $uid];
foreach ($roles as $role) {
$args[] = '--role';
$args[] = $role;
}
static::runConsole($args);
if ($password !== null) {
static::runConsole(['user:password', static::$tenantIdentifier, $identity, $password]);
@@ -83,6 +91,34 @@ abstract class IntegrationTestCase extends TestCase
return $uid;
}
/**
* Create a role granting the given permissions and return its role id.
*
* There's no `bin/console` command for role management, so this goes
* straight through the DI container to UserRolesStore — the same store
* the real UserRolesController uses — rather than calling controllers
* directly.
*
* @param string[] $permissions
*/
protected static function createRoleWithPermissions(array $permissions, string $label = 'Test Role'): string
{
$server = static::buildServer();
try {
$server->kernel()->boot();
$store = $server->container()->get(UserRolesStore::class);
$role = $store->createRole(static::$tenantIdentifier, [
'label' => $label,
'permissions' => $permissions,
]);
} finally {
restore_error_handler();
restore_exception_handler();
}
return $role['rid'];
}
/**
* Run a bin/console command as a real subprocess and fail the test on
* a non-zero exit code.
@@ -63,12 +63,13 @@ class PasswordControllerTest extends IntegrationTestCase
public function testAdminStatusReflectsEnrollment(): void
{
$enrolledUid = static::createUser('grace@example.test', 'Grace-Pass-1!');
$roleId = static::createRoleWithPermissions(['authentication_provider_password.admin.view']);
$enrolledUid = static::createUser('grace@example.test', 'Grace-Pass-1!', [$roleId]);
$unenrolledUid = static::createUser('heidi@example.test');
$accessToken = static::loginWithPassword('grace@example.test', 'Grace-Pass-1!')['accessToken'];
$enrolledStatus = static::authorizedRequest($accessToken, 'GET', self::BASE . "/admin/status/{$enrolledUid}");
$unenrolledStatus = static::authorizedRequest($accessToken, 'GET', self::BASE . "/admin/status/{$unenrolledUid}");
$enrolledStatus = static::authorizedRequest($accessToken, 'POST', self::BASE . '/status', ['uid' => $enrolledUid]);
$unenrolledStatus = static::authorizedRequest($accessToken, 'POST', self::BASE . '/status', ['uid' => $unenrolledUid]);
$this->assertTrue($enrolledStatus['body']['enrolled'] ?? null);
$this->assertFalse($unenrolledStatus['body']['enrolled'] ?? null);
@@ -77,11 +78,12 @@ class PasswordControllerTest extends IntegrationTestCase
public function testAdminResetSetsAPasswordThatCanLogIn(): void
{
$uid = static::createUser('ivan@example.test');
// Bootstrap an authenticated actor to call the admin endpoint
static::createUser('actor1@example.test', 'Actor-Pass-1!');
// Bootstrap an authenticated actor with permission to call the admin endpoint
$roleId = static::createRoleWithPermissions(['authentication_provider_password.admin.manage']);
static::createUser('actor1@example.test', 'Actor-Pass-1!', [$roleId]);
$accessToken = static::loginWithPassword('actor1@example.test', 'Actor-Pass-1!')['accessToken'];
$reset = static::authorizedRequest($accessToken, 'POST', self::BASE . '/admin/reset', [
$reset = static::authorizedRequest($accessToken, 'POST', self::BASE . '/reset', [
'uid' => $uid,
'password' => 'Reset-By-Admin-1!',
]);
@@ -96,10 +98,11 @@ class PasswordControllerTest extends IntegrationTestCase
public function testAdminResetRejectsShortPasswords(): void
{
$uid = static::createUser('judy@example.test');
static::createUser('actor2@example.test', 'Actor-Pass-2!');
$roleId = static::createRoleWithPermissions(['authentication_provider_password.admin.manage']);
static::createUser('actor2@example.test', 'Actor-Pass-2!', [$roleId]);
$accessToken = static::loginWithPassword('actor2@example.test', 'Actor-Pass-2!')['accessToken'];
$reset = static::authorizedRequest($accessToken, 'POST', self::BASE . '/admin/reset', [
$reset = static::authorizedRequest($accessToken, 'POST', self::BASE . '/reset', [
'uid' => $uid,
'password' => 'short',
]);
@@ -110,17 +113,41 @@ class PasswordControllerTest extends IntegrationTestCase
public function testAdminRemoveRevokesLogin(): void
{
$uid = static::createUser('kevin@example.test', 'Kevin-Pass-1!');
static::createUser('actor3@example.test', 'Actor-Pass-3!');
$roleId = static::createRoleWithPermissions(['authentication_provider_password.admin.manage']);
static::createUser('actor3@example.test', 'Actor-Pass-3!', [$roleId]);
$accessToken = static::loginWithPassword('actor3@example.test', 'Actor-Pass-3!')['accessToken'];
$remove = static::authorizedRequest($accessToken, 'DELETE', self::BASE . "/admin/remove/{$uid}");
$remove = static::authorizedRequest($accessToken, 'POST', self::BASE . '/remove', ['uid' => $uid]);
$this->assertSame(200, $remove['status']);
$this->assertTrue($remove['body']['success'] ?? false);
$status = static::authorizedRequest($accessToken, 'GET', self::BASE . "/admin/status/{$uid}");
$status = static::authorizedRequest($accessToken, 'POST', self::BASE . '/status', ['uid' => $uid]);
$this->assertFalse($status['body']['enrolled'] ?? null);
$login = static::loginWithPassword('kevin@example.test', 'Kevin-Pass-1!');
$this->assertSame('failed', $login['body']['status'] ?? null);
}
public function testAdminEndpointsRejectActorsWithoutPermission(): void
{
$targetUid = static::createUser('mallory-target@example.test', 'Mallory-Target-1!');
static::createUser('mallory@example.test', 'Mallory-Pass-1!');
$accessToken = static::loginWithPassword('mallory@example.test', 'Mallory-Pass-1!')['accessToken'];
$status = static::authorizedRequest($accessToken, 'POST', self::BASE . '/status', ['uid' => $targetUid]);
$this->assertSame(403, $status['status']);
$reset = static::authorizedRequest($accessToken, 'POST', self::BASE . '/reset', [
'uid' => $targetUid,
'password' => 'Should-Not-Apply-1!',
]);
$this->assertSame(403, $reset['status']);
$remove = static::authorizedRequest($accessToken, 'POST', self::BASE . '/remove', ['uid' => $targetUid]);
$this->assertSame(403, $remove['status']);
// None of the rejected calls should have taken effect
$login = static::loginWithPassword('mallory-target@example.test', 'Mallory-Target-1!');
$this->assertSame('success', $login['body']['status'] ?? null);
}
}