mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-12 06:48:55 +00:00
4ce6793da9
Setup ended by handing somebody a login form and an empty dashboard. Everything this application can do was one menu away, and which menu was theirs to discover. The first time the administrator signs in to a new installation they now land on a short ordered list of what is worth doing first — add a client, upload a file, group the people who get the same things, choose how the file lists and the email look, point it at a mail server, add the team, check the scheduler — each a link straight to the screen that does it. The list is filtered twice, and both filters matter. By permission, because a link that answers 403 is worse than no link. And by edition: a managed installation is not sent off to configure a mail server somebody else runs, to create staff accounts that are not its to create, or to check a scheduler it does not host. Those three drop out on Cloud and the other five remain. Two steps tick themselves, because the database can answer them: a client exists, a file exists. Nothing else is checkable without guessing — a theme that was never changed looks exactly like one chosen deliberately — and a tick meaning "we assume so" is worse than no tick. The invitation to the Discord is at the very bottom, after the list. Somebody who has just installed this came with a job in mind, and opening with a social invitation is the fastest way to lose them. The marker is raised where a first administrator comes into existence — the setup screen and `projectsend:admin`, so a container provisioned from environment variables is welcomed too — and it is false by default, so an installation that updates into this feature is not congratulated on an install it finished a year ago. RedirectToWhatsNew becomes RedirectToGreeting and answers for both: they are the same interruption, and a second middleware on the same route would have to know about the first to avoid arguing with it. Installing wins; release notes for a version you never ran are the wrong greeting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
207 lines
7.3 KiB
PHP
207 lines
7.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Identity\Permissions\SystemRole;
|
|
use App\Modules\Platform\Capabilities\Edition;
|
|
use App\Modules\Platform\Settings\Setting;
|
|
use App\Modules\Platform\Settings\Settings;
|
|
use Inertia\Testing\AssertableInertia;
|
|
|
|
/**
|
|
* A new installation shows its administrator around, once — and the list
|
|
* it shows never points at something this edition or this person cannot
|
|
* do.
|
|
*/
|
|
beforeEach(function () {
|
|
$this->admin = User::factory()->create();
|
|
|
|
// Settings survive RefreshDatabase's rollback in the cache, so state
|
|
// both markers rather than assuming their defaults.
|
|
app(Settings::class)->set(Setting::GettingStartedPending, false);
|
|
app(Settings::class)->set(Setting::UpdateWelcomeTo, '');
|
|
});
|
|
|
|
function justInstalled(): void
|
|
{
|
|
app(Settings::class)->set(Setting::GettingStartedPending, true);
|
|
}
|
|
|
|
/** @return list<string> */
|
|
function quickStartKeys(User $user): array
|
|
{
|
|
$keys = [];
|
|
|
|
test()->actingAs($user)->get('/system/getting-started')->assertInertia(
|
|
function (AssertableInertia $page) use (&$keys) {
|
|
$keys = array_column($page->toArray()['props']['items'], 'key');
|
|
},
|
|
);
|
|
|
|
return $keys;
|
|
}
|
|
|
|
test('the main administrator lands on it after installing', function () {
|
|
justInstalled();
|
|
|
|
$this->actingAs($this->admin)->get('/dashboard')->assertRedirect('/system/getting-started');
|
|
});
|
|
|
|
test('it happens exactly once', function () {
|
|
justInstalled();
|
|
|
|
$this->actingAs($this->admin)->get('/dashboard')->assertRedirect('/system/getting-started');
|
|
$this->actingAs($this->admin)->get('/system/getting-started')->assertOk();
|
|
|
|
$this->actingAs($this->admin)->get('/dashboard')->assertOk();
|
|
});
|
|
|
|
// Closing it on the way past should not be unrecoverable.
|
|
test('it stays readable afterwards, with the welcome wording dropped', function () {
|
|
justInstalled();
|
|
|
|
$this->actingAs($this->admin)->get('/system/getting-started')->assertOk();
|
|
|
|
$this->actingAs($this->admin)->get('/system/getting-started')->assertInertia(
|
|
fn (AssertableInertia $page) => $page
|
|
->component('system/getting-started')
|
|
->where('justInstalled', false)
|
|
->has('items'),
|
|
);
|
|
});
|
|
|
|
test('other staff are not interrupted, but may read it', function () {
|
|
justInstalled();
|
|
|
|
$second = User::factory()->create();
|
|
|
|
$this->actingAs($second)->get('/dashboard')->assertOk();
|
|
$this->actingAs($second)->get('/system/getting-started')->assertOk();
|
|
|
|
// …and reading it did not consume the greeting.
|
|
$this->actingAs($this->admin)->get('/dashboard')->assertRedirect('/system/getting-started');
|
|
});
|
|
|
|
test('clients cannot reach it', function () {
|
|
$client = User::factory()->client()->create();
|
|
|
|
// EnsureStaff redirects a client away from a staff GET rather than
|
|
// answering 403 — see its docblock.
|
|
$this->actingAs($client)->get('/system/getting-started')->assertRedirect();
|
|
});
|
|
|
|
test('an installation that merely updated is never welcomed to itself', function () {
|
|
app(Settings::class)->set(Setting::UpdateWelcomeFrom, '2.0.0');
|
|
app(Settings::class)->set(Setting::UpdateWelcomeTo, '2.1.0');
|
|
|
|
$this->actingAs($this->admin)->get('/dashboard')->assertRedirect('/system/whats-new');
|
|
});
|
|
|
|
// Both markers at once cannot happen in practice — an update marker is
|
|
// only raised for an installation that already existed — but if it did,
|
|
// release notes for a version they never ran are the wrong greeting.
|
|
test('installing wins over updating', function () {
|
|
justInstalled();
|
|
app(Settings::class)->set(Setting::UpdateWelcomeTo, '2.1.0');
|
|
|
|
$this->actingAs($this->admin)->get('/dashboard')->assertRedirect('/system/getting-started');
|
|
});
|
|
|
|
test('completing setup raises the greeting', function () {
|
|
User::query()->delete();
|
|
|
|
$this->post('/setup', [
|
|
'site_name' => 'Acme Files',
|
|
'name' => 'Ada',
|
|
'email' => 'ada@example.com',
|
|
'password' => 'a-long-enough-password',
|
|
'password_confirmation' => 'a-long-enough-password',
|
|
])->assertRedirect('/setup/success');
|
|
|
|
expect(app(Settings::class)->get(Setting::GettingStartedPending))->toBeTrue();
|
|
});
|
|
|
|
// Unattended provisioning skips the setup screen entirely, and is how
|
|
// every container that came up from environment variables was installed.
|
|
test('provisioning from the command line raises it too', function () {
|
|
User::query()->delete();
|
|
|
|
$this->artisan('projectsend:admin', [
|
|
'--name' => 'Ada',
|
|
'--email' => 'ada@example.com',
|
|
'--password' => 'a-long-enough-password',
|
|
])->assertSuccessful();
|
|
|
|
expect(app(Settings::class)->get(Setting::GettingStartedPending))->toBeTrue();
|
|
});
|
|
|
|
test('--if-none on an installed site raises nothing', function () {
|
|
$this->artisan('projectsend:admin', ['--if-none' => true])->assertSuccessful();
|
|
|
|
expect(app(Settings::class)->get(Setting::GettingStartedPending))->toBeFalse();
|
|
});
|
|
|
|
// The list is the point of the page, and a link to a screen that answers
|
|
// 403 is worse than no link at all.
|
|
test('it only lists what this person may actually do', function () {
|
|
$uploader = User::factory()->role(SystemRole::Uploader)->create();
|
|
|
|
$keys = quickStartKeys($uploader);
|
|
|
|
expect($keys)->toContain('upload')
|
|
->and($keys)->not->toContain('team')
|
|
->and($keys)->not->toContain('email')
|
|
->and($keys)->not->toContain('theme');
|
|
});
|
|
|
|
// The example the brief named: a managed installation has no staff
|
|
// accounts of its own to hand out, no mail server to point anywhere and
|
|
// no scheduler to check.
|
|
test('a managed installation is not sent to screens it does not have', function () {
|
|
config()->set('projectsend.edition', Edition::Cloud);
|
|
|
|
$keys = quickStartKeys($this->admin);
|
|
|
|
expect($keys)->toContain('client', 'upload', 'theme', 'email-theme')
|
|
->and($keys)->not->toContain('team')
|
|
->and($keys)->not->toContain('email')
|
|
->and($keys)->not->toContain('scheduler');
|
|
});
|
|
|
|
test('a self-hosted installation gets the full list', function () {
|
|
config()->set('projectsend.edition', Edition::Community);
|
|
|
|
expect(quickStartKeys($this->admin))->toContain('client', 'upload', 'group', 'theme', 'email-theme', 'email', 'team', 'scheduler');
|
|
});
|
|
|
|
// Two of them can be answered from the database rather than guessed, and
|
|
// a tick that means "we assume so" would be worse than no tick.
|
|
test('the client and upload steps tick themselves', function () {
|
|
justInstalled();
|
|
|
|
$this->actingAs($this->admin)->get('/system/getting-started')->assertInertia(
|
|
fn (AssertableInertia $page) => $page->where('items.0.key', 'client')->where('items.0.done', false),
|
|
);
|
|
|
|
User::factory()->client()->create();
|
|
|
|
$this->actingAs($this->admin)->get('/system/getting-started')->assertInertia(
|
|
fn (AssertableInertia $page) => $page->where('items.0.done', true),
|
|
);
|
|
});
|
|
|
|
// The email-theme link has to land on the email tab, not on the page with
|
|
// a sentence asking the reader to find it.
|
|
test('the email theme step deep-links to its own tab', function () {
|
|
$items = [];
|
|
|
|
$this->actingAs($this->admin)->get('/system/getting-started')->assertInertia(function (AssertableInertia $page) use (&$items) {
|
|
$items = $page->toArray()['props']['items'];
|
|
});
|
|
|
|
$emailTheme = collect($items)->firstWhere('key', 'email-theme');
|
|
|
|
expect($emailTheme['href'])->toContain('tab=email');
|
|
});
|