coolify/tests/Feature/MapledeployUserManagementCommandsTest.php
rosslh 65d85fb890
All checks were successful
Build MapleDeploy Coolify Image / build (push) Successful in 41s
fix(coolify-access): prefer managed root team
2026-06-14 14:39:05 -04:00

334 lines
12 KiB
PHP

<?php
use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Symfony\Component\Process\Process;
beforeEach(function () {
$this->originalDatabaseConfig = [
'default' => config('database.default'),
'testing_database' => config('database.connections.testing.database'),
];
$this->originalDatabaseEnvironment = [
'DB_CONNECTION' => [
'env' => $_ENV['DB_CONNECTION'] ?? null,
'server' => $_SERVER['DB_CONNECTION'] ?? null,
'process' => getenv('DB_CONNECTION') === false ? null : getenv('DB_CONNECTION'),
],
'DB_DATABASE' => [
'env' => $_ENV['DB_DATABASE'] ?? null,
'server' => $_SERVER['DB_DATABASE'] ?? null,
'process' => getenv('DB_DATABASE') === false ? null : getenv('DB_DATABASE'),
],
];
$this->databasePath = storage_path('framework/testing-mapledeploy-user-mgmt-'.bin2hex(random_bytes(6)).'.sqlite');
touch($this->databasePath);
config([
'database.default' => 'testing',
'database.connections.testing.database' => $this->databasePath,
]);
$_ENV['DB_CONNECTION'] = 'testing';
$_SERVER['DB_CONNECTION'] = 'testing';
$_ENV['DB_DATABASE'] = $this->databasePath;
$_SERVER['DB_DATABASE'] = $this->databasePath;
putenv('DB_CONNECTION=testing');
putenv("DB_DATABASE={$this->databasePath}");
$GLOBALS['mapledeployUserMgmtDatabasePath'] = $this->databasePath;
DB::purge('testing');
DB::reconnect('testing');
Artisan::call('migrate:fresh', ['--database' => 'testing']);
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
});
afterEach(function () {
DB::disconnect('testing');
DB::purge('testing');
config([
'database.default' => $this->originalDatabaseConfig['default'] ?? null,
'database.connections.testing.database' => $this->originalDatabaseConfig['testing_database'] ?? null,
]);
if (isset($this->databasePath) && file_exists($this->databasePath)) {
unlink($this->databasePath);
}
foreach (($this->originalDatabaseEnvironment ?? []) as $key => $values) {
if ($values['env'] === null) {
unset($_ENV[$key]);
} else {
$_ENV[$key] = $values['env'];
}
if ($values['server'] === null) {
unset($_SERVER[$key]);
} else {
$_SERVER[$key] = $values['server'];
}
if ($values['process'] === null) {
putenv($key);
} else {
putenv("{$key}={$values['process']}");
}
}
unset($GLOBALS['mapledeployUserMgmtDatabasePath']);
});
function runMapledeployUserCommand(array $arguments, string $stdin = ''): array
{
$process = new Process(
[PHP_BINARY, 'artisan', ...$arguments],
base_path(),
[
'APP_ENV' => 'testing',
'APP_KEY' => config('app.key'),
'DB_CONNECTION' => 'testing',
'DB_DATABASE' => $GLOBALS['mapledeployUserMgmtDatabasePath'],
'CACHE_DRIVER' => 'array',
'SESSION_DRIVER' => 'database',
'QUEUE_CONNECTION' => 'sync',
'MAIL_MAILER' => 'array',
'SELF_HOSTED' => 'true',
],
);
$process->setInput($stdin);
$process->setTimeout(30);
$process->run();
$jsonLine = collect(explode("\n", $process->getOutput()))
->map(fn (string $line) => trim($line))
->first(fn (string $line) => str_starts_with($line, '{') && str_ends_with($line, '}'));
return [
'exitCode' => $process->getExitCode(),
'json' => $jsonLine ? json_decode($jsonLine, true, flags: JSON_THROW_ON_ERROR) : null,
'stdout' => $process->getOutput(),
'stderr' => $process->getErrorOutput(),
];
}
test('MapleDeploy user management commands create, list, reset, and revoke users', function () {
$admin = runMapledeployUserCommand([
'mapledeploy:user:create',
'--admin',
'--email=Owner@Example.com',
'--name=Owner',
], "owner-password\n");
expect($admin['exitCode'])->toBe(0)
->and($admin['json']['user'])->toMatchArray([
'id' => 0,
'email' => 'owner@example.com',
'name' => 'Owner',
])
->and((bool) InstanceSettings::findOrFail(0)->is_registration_enabled)->toBeFalse()
->and(Hash::check('owner-password', User::findOrFail(0)->password))->toBeTrue();
$duplicateOwner = runMapledeployUserCommand([
'mapledeploy:user:create',
'--email=OWNER@Example.com',
'--name=Duplicate Owner',
], "duplicate-password\n");
expect($duplicateOwner['exitCode'])->toBe(1)
->and($duplicateOwner['json'])->toBe(['error' => 'EMAIL_EXISTS']);
$member = runMapledeployUserCommand([
'mapledeploy:user:create',
'--email=Member@Example.com',
'--name=Member',
'--team-role=admin',
], "member-password\n");
expect($member['exitCode'])->toBe(0)
->and($member['json']['user']['email'])->toBe('member@example.com');
$memberUser = User::whereEmail('member@example.com')->firstOrFail();
expect($memberUser->teams()->where('teams.id', 0)->first()?->pivot?->role)->toBe('admin')
->and($memberUser->teams()->pluck('teams.id')->all())->toBe([0]);
$list = runMapledeployUserCommand(['mapledeploy:user:list']);
expect($list['exitCode'])->toBe(0)
->and(collect($list['json']['users'])->pluck('email')->all())
->toBe(['owner@example.com', 'member@example.com']);
$resetOtherUser = User::factory()->create();
DB::table('sessions')->insert([
[
'id' => 'member-reset-session',
'user_id' => $memberUser->id,
'ip_address' => '127.0.0.1',
'user_agent' => 'Test Browser',
'payload' => base64_encode('member-reset-payload'),
'last_activity' => now()->timestamp,
],
[
'id' => 'other-reset-session',
'user_id' => $resetOtherUser->id,
'ip_address' => '127.0.0.1',
'user_agent' => 'Test Browser',
'payload' => base64_encode('other-reset-payload'),
'last_activity' => now()->timestamp,
],
]);
$reset = runMapledeployUserCommand([
'mapledeploy:user:set-password',
(string) $memberUser->id,
], "new-member-password\n");
expect($reset['exitCode'])->toBe(0)
->and(Hash::check('new-member-password', $memberUser->fresh()->password))->toBeTrue();
expect(DB::table('sessions')->where('user_id', $memberUser->id)->count())->toBe(0)
->and(DB::table('sessions')->where('user_id', $resetOtherUser->id)->count())->toBe(1);
DB::table('personal_access_tokens')->insert([
'tokenable_type' => User::class,
'tokenable_id' => $memberUser->id,
'name' => 'e2e-token',
'token' => hash('sha256', 'e2e-token'),
'team_id' => '0',
'abilities' => json_encode(['*'], JSON_THROW_ON_ERROR),
'created_at' => now(),
'updated_at' => now(),
]);
expect($memberUser->tokens()->count())->toBe(1);
$otherUser = User::factory()->create();
DB::table('sessions')->insert([
[
'id' => 'member-session',
'user_id' => $memberUser->id,
'ip_address' => '127.0.0.1',
'user_agent' => 'Test Browser',
'payload' => base64_encode('member-payload'),
'last_activity' => now()->timestamp,
],
[
'id' => 'other-session',
'user_id' => $otherUser->id,
'ip_address' => '127.0.0.1',
'user_agent' => 'Test Browser',
'payload' => base64_encode('other-payload'),
'last_activity' => now()->timestamp,
],
]);
expect(DB::table('sessions')->where('user_id', $memberUser->id)->count())->toBe(1);
$revokeRoot = runMapledeployUserCommand(['mapledeploy:user:revoke', '0']);
expect($revokeRoot['exitCode'])->toBe(1)
->and($revokeRoot['json'])->toBe(['error' => 'CANNOT_REVOKE_ROOT_USER']);
$revoke = runMapledeployUserCommand([
'mapledeploy:user:revoke',
(string) $memberUser->id,
]);
expect($revoke['exitCode'])->toBe(0)
->and($revoke['json']['revoked']['email'])->toBe('member@example.com')
->and($memberUser->fresh()->tokens()->count())->toBe(0);
expect(str_starts_with((string) $memberUser->fresh()->remember_token, 'mapledeploy-revoked:'))->toBeTrue();
expect(DB::table('sessions')->where('user_id', $memberUser->id)->count())->toBe(0)
->and(DB::table('sessions')->where('user_id', $otherUser->id)->count())->toBe(1);
$restore = runMapledeployUserCommand([
'mapledeploy:user:set-password',
(string) $memberUser->id,
], "restored-member-password\n");
expect($restore['exitCode'])->toBe(0)
->and(Hash::check('restored-member-password', $memberUser->fresh()->password))->toBeTrue()
->and($memberUser->fresh()->remember_token)->toBeNull();
});
test('MapleDeploy password command can transfer root ownership identity', function () {
runMapledeployUserCommand([
'mapledeploy:user:create',
'--admin',
'--email=old-owner@example.com',
'--name=Old Owner',
], "old-owner-password\n");
DB::table('sessions')->insert([
'id' => 'root-session',
'user_id' => 0,
'ip_address' => '127.0.0.1',
'user_agent' => 'Test Browser',
'payload' => base64_encode('root-payload'),
'last_activity' => now()->timestamp,
]);
$claim = runMapledeployUserCommand([
'mapledeploy:user:set-password',
'0',
'--email=New.Owner@Example.com',
'--name=New Owner',
], "new-owner-password\n");
$root = User::findOrFail(0);
expect($claim['exitCode'])->toBe(0)
->and($claim['json']['user'])->toMatchArray([
'id' => 0,
'email' => 'new.owner@example.com',
'name' => 'New Owner',
])
->and($root->email)->toBe('new.owner@example.com')
->and($root->name)->toBe('New Owner')
->and(Hash::check('new-owner-password', $root->password))->toBeTrue()
->and($root->remember_token)->toBeNull()
->and($root->email_verified_at)->not->toBeNull();
expect(User::whereEmail('old-owner@example.com')->exists())->toBeFalse();
expect(DB::table('sessions')->where('user_id', 0)->count())->toBe(0);
});
test('MapleDeploy password command promotes matched native users to root team admin', function () {
runMapledeployUserCommand([
'mapledeploy:user:create',
'--admin',
'--email=owner@example.com',
'--name=Owner',
], "owner-password\n");
$nativeUser = User::factory()->create([
'email' => 'native-member@example.com',
'name' => 'Native Member',
]);
expect($nativeUser->teams()->where('teams.id', 0)->exists())->toBeFalse();
$reset = runMapledeployUserCommand([
'mapledeploy:user:set-password',
(string) $nativeUser->id,
], "native-member-password\n");
expect($reset['exitCode'])->toBe(0)
->and(Hash::check('native-member-password', $nativeUser->fresh()->password))->toBeTrue()
->and($nativeUser->fresh()->teams()->where('teams.id', 0)->first()?->pivot?->role)->toBe('admin');
});
test('MapleDeploy password command rejects ownership transfer to an existing email', function () {
runMapledeployUserCommand([
'mapledeploy:user:create',
'--admin',
'--email=old-owner@example.com',
'--name=Old Owner',
], "old-owner-password\n");
$existing = User::factory()->create(['email' => 'new.owner@example.com']);
$claim = runMapledeployUserCommand([
'mapledeploy:user:set-password',
'0',
'--email=new.owner@example.com',
'--name=New Owner',
], "new-owner-password\n");
expect($claim['exitCode'])->toBe(1)
->and($claim['json'])->toBe(['error' => 'EMAIL_EXISTS'])
->and(User::findOrFail(0)->email)->toBe('old-owner@example.com')
->and($existing->fresh()->email)->toBe('new.owner@example.com');
});
test('MapleDeploy user creation command reports validation errors as JSON', function () {
$invalid = runMapledeployUserCommand([
'mapledeploy:user:create',
'--email=not-an-email',
'--name=Invalid',
], "short\n");
expect($invalid['exitCode'])->toBe(1)
->and($invalid['json'])->toBe(['error' => 'INVALID_INPUT']);
});