All checks were successful
Build MapleDeploy Coolify Image / build (push) Successful in 41s
334 lines
12 KiB
PHP
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']);
|
|
});
|