artisan('db:seed'); }); /** * The account UserSeeder creates, which config/demo.php protects by default. */ function demoAccount(): User { return User::where('username', 'admin')->firstOrFail(); } function enableDemoMode(): void { config(['demo.enabled' => true]); } /* |-------------------------------------------------------------------------- | Off by default |-------------------------------------------------------------------------- */ test('demo mode is off unless the deployment turns it on', function () { // phpunit.xml pins DEMO_MODE off, because the demo deployment's own .env // turns it on and the suite must not change meaning there. That the // shipped templates default to false is EnvironmentTemplateTest's job. $this->assertFalse(Demo::enabled()); }); test('every write is reachable while demo mode is off', function () { $this->actingAsAdmin() ->from('/settings/general') ->post('/settings/general', ['app_name' => 'Renamed']) ->assertRedirect('/settings/general'); $this->assertEquals('Renamed', Setting::get('app_name')); $this->from('/users') ->delete('users/'.demoAccount()->id) ->assertRedirect('users'); $this->assertDatabaseMissing('users', ['username' => 'admin']); }); /* |-------------------------------------------------------------------------- | Blocked routes |-------------------------------------------------------------------------- */ test('blocked routes are refused with a flashed error', function (string $url, array $data) { enableDemoMode(); $this->actingAsAdmin() ->from('/') ->post($url, $data) ->assertRedirect('/'); $this->assertSessionHasFlashError('This action is disabled in the demo.'); })->with([ 'general settings' => ['/settings/general', ['app_name' => 'Renamed']], 'auth settings' => ['/settings/auth', ['reg_enabled' => false]], 'two factor toggle' => ['/settings/auth/2fa/enable', []], 'permission matrix' => ['/permissions/save', ['roles' => []]], ]); test('a blocked settings write leaves the setting alone', function () { Setting::set('app_name', 'Vanguard'); Setting::save(); enableDemoMode(); $this->actingAsAdmin() ->from('/settings/general') ->post('/settings/general', ['app_name' => 'Renamed']); $this->assertEquals('Vanguard', Setting::get('app_name')); }); test('a blocked permission matrix write leaves the role permissions alone', function () { $role = Role::where('name', 'User')->firstOrFail(); $before = $role->permissions()->pluck('id')->sort()->values()->all(); enableDemoMode(); $this->actingAsAdmin() ->from('/permissions') ->post('/permissions/save', ['roles' => [$role->id => []]]); $this->assertSessionHasFlashError('This action is disabled in the demo.'); $this->assertEquals($before, $role->fresh()->permissions()->pluck('id')->sort()->values()->all()); }); test('a refused Inertia delete answers 303 so the browser does not repeat the verb', function () { enableDemoMode(); $this->actingAs(UserFactory::admin()->create()) ->from('/users') ->withHeader('X-Inertia', 'true') ->delete('users/'.demoAccount()->id) ->assertStatus(303) ->assertRedirect('/users'); $this->assertSessionHasFlashError('The demo account cannot be modified.'); }); test('the refusal reaches the shared flash prop the layouts turn into a toast', function () { enableDemoMode(); $this->actingAsAdmin() ->from('/settings/general') ->post('/settings/general', ['app_name' => 'Renamed']); // The flash is read by HandleInertiaRequests on the request the redirect // lands on, which is where the layout's watcher fires. $this->get('/settings') ->assertInertia(fn (Assert $page) => $page ->where('flash.error', 'This action is disabled in the demo.') ); }); test('reading a page is never blocked', function () { enableDemoMode(); $this->actingAsAdmin()->get('/settings')->assertOk(); $this->get('/users/'.demoAccount()->id)->assertOk(); $this->get('/permissions')->assertOk(); }); /* |-------------------------------------------------------------------------- | The demo stays interactive |-------------------------------------------------------------------------- */ test('a visitor can still create and delete an ordinary user', function () { enableDemoMode(); $this->actingAs(demoAccount()) ->from('/users/create') ->post('users', [ 'email' => 'jane@example.com', 'password' => 'secret123', 'password_confirmation' => 'secret123', 'role_id' => Role::where('name', 'User')->value('id'), ]) ->assertRedirect('users'); $this->assertSessionHasSuccess('User created successfully.'); $created = User::where('email', 'jane@example.com')->firstOrFail(); $this->from('/users') ->delete('users/'.$created->id) ->assertRedirect('users'); $this->assertSessionHasSuccess('User deleted successfully.'); $this->assertDatabaseMissing('users', ['email' => 'jane@example.com']); }); test('the demo account can still create a role', function () { enableDemoMode(); $this->actingAs(demoAccount()) ->from('/roles/create') ->post('roles', Role::factory()->raw(['name' => 'Editor'])) ->assertRedirect('roles'); $this->assertSessionHasSuccess('Role created successfully.'); $this->assertDatabaseHas('roles', ['name' => 'Editor']); }); test('a visitor can still impersonate another user and stop again', function () { enableDemoMode(); $target = UserFactory::user()->create(); $this->actingAs(demoAccount()) ->get(route('impersonate', $target->id)) ->assertRedirect('/'); $this->assertTrue(auth()->user()->is($target)); $this->get(route('impersonate.leave')); $this->assertTrue(auth()->user()->is(demoAccount())); }); test('impersonating does not lift the demo restrictions', function () { enableDemoMode(); $target = UserFactory::withPermissions('settings.general')->create(); $this->actingAs(demoAccount())->get(route('impersonate', $target->id)); $this->from('/settings/general') ->post('/settings/general', ['app_name' => 'Renamed']); $this->assertSessionHasFlashError('This action is disabled in the demo.'); }); test('impersonating the demo account does not hand over its login details', function () { enableDemoMode(); // Taking over a protected account is allowed; changing it is not, because // the self service rules read the impersonated user rather than the one // who signed in. $this->actingAs(UserFactory::admin()->create()) ->get(route('impersonate', demoAccount()->id)); $this->assertTrue(auth()->user()->is(demoAccount())); $this->from('/profile') ->put('profile/login-details', [ 'email' => 'hijacked@example.com', 'password' => 'hijacked123', 'password_confirmation' => 'hijacked123', ]); $this->assertSessionHasFlashError('The demo account cannot be modified.'); $this->assertEquals('admin@example.com', demoAccount()->email); }); test('logging in still works', function () { enableDemoMode(); $user = UserFactory::withCredentials('jane', 'secret123')->user()->create(); $this->post('login', ['username' => 'jane', 'password' => 'secret123']) ->assertRedirect('/'); $this->assertAuthenticatedAs($user); }); /* |-------------------------------------------------------------------------- | The protected account |-------------------------------------------------------------------------- */ test('the demo account cannot be deleted', function () { enableDemoMode(); $this->actingAs(UserFactory::admin()->create()) ->from('/users') ->delete('users/'.demoAccount()->id) ->assertRedirect('/users'); $this->assertSessionHasFlashError('The demo account cannot be modified.'); $this->assertDatabaseHas('users', ['username' => 'admin']); }); test('the demo account cannot have its login details changed by an administrator', function () { enableDemoMode(); $this->actingAs(UserFactory::admin()->create()) ->from('/users') ->put('users/'.demoAccount()->id.'/update/login-details', [ 'email' => 'hijacked@example.com', 'password' => 'hijacked123', 'password_confirmation' => 'hijacked123', ]); $this->assertSessionHasFlashError('The demo account cannot be modified.'); $this->assertEquals('admin@example.com', demoAccount()->email); }); test('the demo account cannot change its own login details', function () { enableDemoMode(); $this->actingAs(demoAccount()) ->from('/profile') ->put('profile/login-details', [ 'email' => 'hijacked@example.com', 'password' => 'hijacked123', 'password_confirmation' => 'hijacked123', ]) ->assertRedirect('/profile'); $this->assertSessionHasFlashError('The demo account cannot be modified.'); $this->assertEquals('admin@example.com', demoAccount()->email); }); test('an ordinary visitor can still change their own login details', function () { enableDemoMode(); $user = UserFactory::user()->create(); $this->actingAs($user) ->from('/profile') ->put('profile/login-details', [ 'email' => 'jane@example.com', 'password' => 'secret123', 'password_confirmation' => 'secret123', ]) ->assertRedirect('/profile'); $this->assertSessionHasSuccess('Login details updated successfully.'); $this->assertEquals('jane@example.com', $user->fresh()->email); }); test('protected accounts can be named by email instead of username', function () { enableDemoMode(); config(['demo.protected_users' => ['admin@example.com']]); $this->actingAs(UserFactory::admin()->create()) ->from('/users') ->delete('users/'.demoAccount()->id); $this->assertSessionHasFlashError('The demo account cannot be modified.'); $this->assertDatabaseHas('users', ['username' => 'admin']); }); /* |-------------------------------------------------------------------------- | Seeded roles and permissions |-------------------------------------------------------------------------- */ test('a seeded role cannot be renamed', function () { enableDemoMode(); $role = Role::where('name', 'Admin')->firstOrFail(); $this->actingAsAdmin() ->from('/roles') ->put('roles/'.$role->id, Role::factory()->raw(['name' => 'Renamed'])) ->assertRedirect('/roles'); $this->assertSessionHasFlashError('Default roles and permissions cannot be modified in the demo.'); $this->assertEquals('Admin', $role->fresh()->name); }); test('a role created during the demo can still be renamed', function () { enableDemoMode(); $role = Role::factory()->create(['name' => 'Editor']); $this->actingAsAdmin() ->from('/roles') ->put('roles/'.$role->id, Role::factory()->raw(['name' => 'Renamed'])) ->assertRedirect('roles'); $this->assertSessionHasSuccess('Role updated successfully.'); $this->assertEquals('Renamed', $role->fresh()->name); }); /* |-------------------------------------------------------------------------- | Configuration |-------------------------------------------------------------------------- */ test('the configured message replaces every reason', function () { enableDemoMode(); config(['demo.message' => 'Grab your own copy to try this.']); $this->actingAsAdmin() ->from('/settings/general') ->post('/settings/general', ['app_name' => 'Renamed']); $this->assertSessionHasFlashError('Grab your own copy to try this.'); });