Files
projectsend/tests/Feature/Clients/ClientRegistrationTest.php
T
ignacionelson 6e47d76ba6 ProjectSend 2.0.0
Client file sharing, rebuilt from the ground up: a private area per
client, resumable uploads, folders, groups and categories, sharing with
expiry dates and download limits, comments, file versions, an activity
log, a REST API, and sixteen languages.

This repository begins here. ProjectSend 2 was developed privately, and
that development history is not published — the previous generation
remains available, with its own history, at projectsend/legacy.

Free software under the GNU General Public License v2, or (at your
option) any later version.
2026-08-14 01:38:12 -03:00

189 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\User;
use App\Modules\Audit\Action;
use App\Modules\Audit\ActivityLog;
use App\Modules\Identity\UserType;
use App\Modules\Platform\Captcha\Captcha;
use App\Modules\Platform\Captcha\CaptchaProvider;
use App\Modules\Platform\Captcha\CaptchaSettings;
use App\Modules\Platform\Captcha\CaptchaVerifier;
use App\Modules\Platform\Settings\Setting;
use App\Modules\Platform\Settings\Settings;
use Illuminate\Support\Facades\Http;
use Inertia\Testing\AssertableInertia;
beforeEach(function () {
// Setup complete.
User::factory()->create();
// Settings outlive the per-test rollback, so the captcha this file
// switches on below must be switched off again for everything else.
app(Settings::class)->set(Setting::CaptchaProvider, 'none');
Captcha::forgetDisplayCache();
// The circuit breaker is a cache entry with a 60-second life, so it
// outlives not just a test but the file that opened it.
CaptchaVerifier::forgetOutage();
});
test('registration is hidden while the setting is off', function () {
$this->get('/register')->assertNotFound();
$this->post('/register', [])->assertNotFound();
// And the login screen offers no registration link.
$this->get('/login')->assertInertia(
fn (AssertableInertia $page) => $page->where('canRegister', false),
);
});
test('with auto-approve on, a self-registered client can log in immediately', function () {
app(Settings::class)->set(Setting::ClientsCanRegister, true);
app(Settings::class)->set(Setting::ClientsAutoApprove, true);
$this->get('/login')->assertInertia(
fn (AssertableInertia $page) => $page->where('canRegister', true),
);
$this->post('/register', [
'name' => 'Self Client',
'email' => 'self@example.com',
'password' => 'super-secret-password',
'password_confirmation' => 'super-secret-password',
])->assertRedirect(route('login'));
$client = User::query()->where('email', 'self@example.com')->sole();
expect($client->type)->toBe(UserType::Client)
->and($client->active)->toBeTrue()
->and($client->account_requested)->toBeFalse()
->and($client->role?->name)->toBe('Client')
->and(ActivityLog::query()->where('action', Action::ClientSelfRegistered)->exists())->toBeTrue();
$this->post('/login', ['email' => 'self@example.com', 'password' => 'super-secret-password']);
$this->assertAuthenticated();
});
test('without auto-approve, the account waits for approval and cannot log in', function () {
app(Settings::class)->set(Setting::ClientsCanRegister, true);
$this->post('/register', [
'name' => 'Waiting Client',
'email' => 'waiting@example.com',
'password' => 'super-secret-password',
'password_confirmation' => 'super-secret-password',
])->assertRedirect(route('login'));
$client = User::query()->where('email', 'waiting@example.com')->sole();
expect($client->active)->toBeFalse()
->and($client->account_requested)->toBeTrue();
// Login with the correct password explains the pending state.
$this->from('/login')->post('/login', ['email' => 'waiting@example.com', 'password' => 'super-secret-password'])
->assertSessionHasErrors('email');
expect(session('errors')->first('email'))->toBe(__('Your account request has not been approved yet.'));
$this->assertGuest();
});
test('approving a request activates the account', function () {
$admin = User::query()->sole();
$pending = User::factory()->pendingClient()->create();
$this->actingAs($admin)->get('/account-requests')->assertInertia(
fn (AssertableInertia $page) => $page
->component('clients/requests')
->has('requests', 1),
);
$this->actingAs($admin)->post("/account-requests/{$pending->id}/approve")->assertRedirect();
$pending->refresh();
expect($pending->active)->toBeTrue()
->and($pending->account_requested)->toBeFalse()
->and(ActivityLog::query()->where('action', Action::ClientApproved)->exists())->toBeTrue();
$this->post('/login', ['email' => $pending->email, 'password' => 'password']);
$this->assertAuthenticated();
});
test('denying a request deletes the account and records it', function () {
$admin = User::query()->sole();
$pending = User::factory()->pendingClient()->create(['name' => 'Denied Person']);
$this->actingAs($admin)->delete("/account-requests/{$pending->id}")->assertRedirect();
expect(User::query()->find($pending->id))->toBeNull();
$entry = ActivityLog::query()->where('action', Action::ClientDenied)->sole();
expect($entry->context)->toBe(['name' => 'Denied Person']);
});
test('approve and deny only apply to pending clients', function () {
$admin = User::query()->sole();
$activeClient = User::factory()->client()->create();
$this->actingAs($admin);
$this->post("/account-requests/{$activeClient->id}/approve")->assertNotFound();
$this->delete("/account-requests/{$activeClient->id}")->assertNotFound();
$this->post("/account-requests/{$admin->id}/approve")->assertNotFound();
});
test('clients cannot see the requests queue', function () {
$this->actingAs(User::factory()->client()->create());
$this->get('/account-requests')->assertRedirect(route('dashboard'));
});
test('registration enforces the captcha for every provider', function (string $provider, string $host, array $success) {
$settings = app(Settings::class);
$settings->set(Setting::ClientsCanRegister, true);
$settings->set(Setting::CaptchaOnRegistration, true);
$settings->set(Setting::CaptchaKeySource, 'own');
CaptchaSettings::for(CaptchaProvider::from($provider))->fill([
'site_key' => 'site-key',
'secret_key' => 'secret-key',
])->save();
$settings->set(Setting::CaptchaProvider, $provider);
Captcha::forgetDisplayCache();
$payload = [
'name' => 'Ada',
'email' => 'ada@example.com',
'password' => 'Password!234',
'password_confirmation' => 'Password!234',
];
// One sequence for the whole test: re-arming Http::fake() mid-test
// would leave the first stub matching and the second one dead.
Http::fakeSequence($host)
->push(['success' => false, 'error-codes' => ['invalid-input-response']])
->push($success);
// No token at all — refused before the provider is even asked.
$this->post('/register', $payload)->assertSessionHasErrors('captcha_token');
expect(User::query()->where('email', 'ada@example.com')->exists())->toBeFalse();
Http::assertNothingSent();
// A token the provider rejects.
$this->post('/register', $payload + ['captcha_token' => 'a-token'])->assertSessionHasErrors('captcha_token');
expect(User::query()->where('email', 'ada@example.com')->exists())->toBeFalse();
// And one it accepts. v1 dropped the result entirely for reCAPTCHA v3
// and Turnstile, so registration was unprotected on two of the three
// providers, and verified twice on the third — which failed every time
// and made registering impossible.
$this->post('/register', $payload + ['captcha_token' => 'a-token'])->assertSessionHasNoErrors();
expect(User::query()->where('email', 'ada@example.com')->exists())->toBeTrue();
// Two verifications for two submitted tokens — never two for one,
// which is what broke v1.
Http::assertSentCount(2);
})->with([
'turnstile' => ['turnstile', 'challenges.cloudflare.com/*', ['success' => true, 'action' => 'register']],
'recaptcha v2' => ['recaptcha_v2', 'www.google.com/*', ['success' => true]],
'recaptcha v3' => ['recaptcha_v3', 'www.google.com/*', ['success' => true, 'action' => 'register', 'score' => 0.9]],
]);