diff --git a/app/Access/Controllers/SocialController.php b/app/Access/Controllers/SocialController.php index 3df895dd8..ff6d5c2dd 100644 --- a/app/Access/Controllers/SocialController.php +++ b/app/Access/Controllers/SocialController.php @@ -16,22 +16,12 @@ use Laravel\Socialite\Contracts\User as SocialUser; class SocialController extends Controller { - protected SocialAuthService $socialAuthService; - protected RegistrationService $registrationService; - protected LoginService $loginService; - - /** - * SocialController constructor. - */ public function __construct( - SocialAuthService $socialAuthService, - RegistrationService $registrationService, - LoginService $loginService + protected SocialAuthService $socialAuthService, + protected RegistrationService $registrationService, + protected LoginService $loginService, ) { $this->middleware('guest')->only(['register']); - $this->socialAuthService = $socialAuthService; - $this->registrationService = $registrationService; - $this->loginService = $loginService; } /** @@ -112,7 +102,7 @@ class SocialController extends Controller $this->socialAuthService->detachSocialAccount($socialDriver); session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)])); - return redirect(user()->getEditUrl()); + return redirect('/my-account/auth#social-accounts'); } /** diff --git a/app/Access/SocialAuthService.php b/app/Access/SocialAuthService.php index fe9195430..f0e0413f0 100644 --- a/app/Access/SocialAuthService.php +++ b/app/Access/SocialAuthService.php @@ -154,21 +154,21 @@ class SocialAuthService $currentUser->socialAccounts()->save($account); session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver])); - return redirect($currentUser->getEditUrl()); + return redirect('/my-account/auth#social_accounts'); } // When a user is logged in and the social account exists and is already linked to the current user. if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) { session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver])); - return redirect($currentUser->getEditUrl()); + return redirect('/my-account/auth#social_accounts'); } // When a user is logged in, A social account exists but the users do not match. if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) { session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver])); - return redirect($currentUser->getEditUrl()); + return redirect('/my-account/auth#social_accounts'); } // Otherwise let the user know this social account is not used by anyone. diff --git a/app/Api/UserApiTokenController.php b/app/Api/UserApiTokenController.php index 7455be4ff..b77e39089 100644 --- a/app/Api/UserApiTokenController.php +++ b/app/Api/UserApiTokenController.php @@ -166,7 +166,7 @@ class UserApiTokenController extends Controller protected function getRedirectPath(User $relatedUser): string { $context = session()->get('api-token-context'); - if ($context === 'settings') { + if ($context === 'settings' || user()->id !== $relatedUser->id) { return $relatedUser->getEditUrl('#api_tokens'); } diff --git a/lang/en/settings.php b/lang/en/settings.php index dfd0f7841..9e49c7ca7 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -214,6 +214,8 @@ return [ 'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.', 'users_social_connect' => 'Connect Account', 'users_social_disconnect' => 'Disconnect Account', + 'users_social_status_connected' => 'Connected', + 'users_social_status_disconnected' => 'Disconnected', 'users_social_connected' => ':socialAccount account was successfully attached to your profile.', 'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.', 'users_api_tokens' => 'API Tokens', diff --git a/resources/views/users/account/layout.blade.php b/resources/views/users/account/layout.blade.php index ff5ad3622..f54a51c5a 100644 --- a/resources/views/users/account/layout.blade.php +++ b/resources/views/users/account/layout.blade.php @@ -12,7 +12,9 @@ @icon('user') {{ trans('preferences.profile') }} @icon('security') {{ trans('preferences.auth') }} @icon('shortcuts') {{ trans('preferences.shortcuts_interface') }} - @icon('notifications') {{ trans('preferences.notifications') }} + @if(userCan('receive-notifications')) + @icon('notifications') {{ trans('preferences.notifications') }} + @endif diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 076b28c74..2b736d81e 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -89,9 +89,9 @@
@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])

{{ $driverName }}

@if($user->hasSocialAccount($driver)) -

Connected

+

{{ trans('settings.users_social_status_connected') }}

@else -

Disconnected

+

{{ trans('settings.users_social_status_disconnected') }}

@endif @endforeach diff --git a/routes/web.php b/routes/web.php index c2f4891b8..c86509c68 100644 --- a/routes/web.php +++ b/routes/web.php @@ -244,6 +244,8 @@ Route::middleware('auth')->group(function () { Route::put('/my-account/auth/password', [UserControllers\UserAccountController::class, 'updatePassword']); Route::get('/my-account/delete', [UserControllers\UserAccountController::class, 'delete']); Route::delete('/my-account', [UserControllers\UserAccountController::class, 'destroy']); + + // User Preference Endpoints Route::patch('/preferences/change-view/{type}', [UserControllers\UserPreferencesController::class, 'changeView']); Route::patch('/preferences/change-sort/{type}', [UserControllers\UserPreferencesController::class, 'changeSort']); Route::patch('/preferences/change-expansion/{type}', [UserControllers\UserPreferencesController::class, 'changeExpansion']); diff --git a/tests/Actions/WebhookCallTest.php b/tests/Actions/WebhookCallTest.php index 81bd7e7e8..16986ba2e 100644 --- a/tests/Actions/WebhookCallTest.php +++ b/tests/Actions/WebhookCallTest.php @@ -51,7 +51,7 @@ class WebhookCallTest extends TestCase { // This test must not fake the queue/bus since this covers an issue // around handling and serialization of items now deleted from the database. - $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']); + $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']); $this->mockHttpClient([new Response(500)]); $user = $this->users->newUser(); @@ -61,8 +61,10 @@ class WebhookCallTest extends TestCase /** @var ApiToken $apiToken */ $editor = $this->users->editor(); $apiToken = ApiToken::factory()->create(['user_id' => $editor]); - $resp = $this->delete($editor->getEditUrl('/api-tokens/' . $apiToken->id)); - $resp->assertRedirect($editor->getEditUrl('#api_tokens')); + $this->delete($apiToken->getUrl())->assertRedirect(); + + $webhook->refresh(); + $this->assertEquals('Response status from endpoint was 500', $webhook->last_error); } public function test_failed_webhook_call_logs_error() diff --git a/tests/Auth/SocialAuthTest.php b/tests/Auth/SocialAuthTest.php index 5b7071a07..89b8fd167 100644 --- a/tests/Auth/SocialAuthTest.php +++ b/tests/Auth/SocialAuthTest.php @@ -18,7 +18,7 @@ class SocialAuthTest extends TestCase $user = User::factory()->make(); $this->setSettings(['registration-enabled' => 'true']); - config(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']); + config(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc']); $mockSocialite = $this->mock(Factory::class); $mockSocialDriver = Mockery::mock(Provider::class); @@ -45,7 +45,6 @@ class SocialAuthTest extends TestCase config([ 'GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc', - 'APP_URL' => 'http://localhost', ]); $mockSocialite = $this->mock(Factory::class); @@ -86,12 +85,41 @@ class SocialAuthTest extends TestCase $this->assertActivityExists(ActivityType::AUTH_LOGIN, null, 'github; (' . $this->users->admin()->id . ') ' . $this->users->admin()->name); } + public function test_social_account_attach() + { + config([ + 'GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', + ]); + $editor = $this->users->editor(); + + $mockSocialite = $this->mock(Factory::class); + $mockSocialDriver = Mockery::mock(Provider::class); + $mockSocialUser = Mockery::mock(\Laravel\Socialite\Contracts\User::class); + + $mockSocialUser->shouldReceive('getId')->twice()->andReturn('logintest123'); + $mockSocialUser->shouldReceive('getAvatar')->andReturn(null); + + $mockSocialite->shouldReceive('driver')->twice()->with('google')->andReturn($mockSocialDriver); + $mockSocialDriver->shouldReceive('redirect')->once()->andReturn(redirect('/login/service/google/callback')); + $mockSocialDriver->shouldReceive('user')->once()->andReturn($mockSocialUser); + + // Test login routes + $resp = $this->actingAs($editor)->followingRedirects()->get('/login/service/google'); + $resp->assertSee('Access & Security'); + + // Test social callback with matching social account + $this->assertDatabaseHas('social_accounts', [ + 'user_id' => $editor->id, + 'driver' => 'google', + 'driver_id' => 'logintest123', + ]); + } + public function test_social_account_detach() { $editor = $this->users->editor(); config([ 'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc', - 'APP_URL' => 'http://localhost', ]); $socialAccount = SocialAccount::query()->forceCreate([ @@ -100,11 +128,11 @@ class SocialAuthTest extends TestCase 'driver_id' => 'logintest123', ]); - $resp = $this->actingAs($editor)->get($editor->getEditUrl()); + $resp = $this->actingAs($editor)->get('/my-account/auth'); $this->withHtml($resp)->assertElementContains('form[action$="/login/service/github/detach"]', 'Disconnect Account'); $resp = $this->post('/login/service/github/detach'); - $resp->assertRedirect($editor->getEditUrl()); + $resp->assertRedirect('/my-account/auth#social-accounts'); $resp = $this->followRedirects($resp); $resp->assertSee('Github account was successfully disconnected from your profile.'); @@ -115,7 +143,6 @@ class SocialAuthTest extends TestCase { config([ 'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc', - 'APP_URL' => 'http://localhost', ]); $user = User::factory()->make(); @@ -153,7 +180,7 @@ class SocialAuthTest extends TestCase { config([ 'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc', - 'APP_URL' => 'http://localhost', 'services.google.auto_register' => true, 'services.google.auto_confirm' => true, + 'services.google.auto_register' => true, 'services.google.auto_confirm' => true, ]); $user = User::factory()->make(); @@ -191,7 +218,7 @@ class SocialAuthTest extends TestCase $user = User::factory()->make(['email' => 'nonameuser@example.com']); $this->setSettings(['registration-enabled' => 'true']); - config(['GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']); + config(['GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc']); $mockSocialite = $this->mock(Factory::class); $mockSocialDriver = Mockery::mock(Provider::class); diff --git a/tests/Permissions/RolePermissionsTest.php b/tests/Permissions/RolePermissionsTest.php index 0b2e16686..d15c1617c 100644 --- a/tests/Permissions/RolePermissionsTest.php +++ b/tests/Permissions/RolePermissionsTest.php @@ -44,14 +44,12 @@ class RolePermissionsTest extends TestCase public function test_user_cannot_change_email_unless_they_have_manage_users_permission() { - $userProfileUrl = '/settings/users/' . $this->user->id; $originalEmail = $this->user->email; $this->actingAs($this->user); - $resp = $this->get($userProfileUrl) - ->assertOk(); + $resp = $this->get('/my-account/profile')->assertOk(); $this->withHtml($resp)->assertElementExists('input[name=email][disabled]'); - $this->put($userProfileUrl, [ + $this->put('/my-account/profile', [ 'name' => 'my_new_name', 'email' => 'new_email@example.com', ]); @@ -63,11 +61,12 @@ class RolePermissionsTest extends TestCase $this->permissions->grantUserRolePermissions($this->user, ['users-manage']); - $resp = $this->get($userProfileUrl) - ->assertOk(); - $this->withHtml($resp)->assertElementNotExists('input[name=email][disabled]') + $resp = $this->get('/my-account/profile')->assertOk(); + $this->withHtml($resp) + ->assertElementNotExists('input[name=email][disabled]') ->assertElementExists('input[name=email]'); - $this->put($userProfileUrl, [ + + $this->put('/my-account/profile', [ 'name' => 'my_new_name_2', 'email' => 'new_email@example.com', ]); diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index 4da964d48..af249951f 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -607,7 +607,7 @@ class ImageTest extends TestCase $this->actingAs($editor); $file = $this->getTestProfileImage(); - $this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []); + $this->call('PUT', '/my-account/profile', [], [], ['profile_image' => $file], []); $profileImages = Image::where('type', '=', 'user')->where('created_by', '=', $editor->id)->get(); $this->assertTrue($profileImages->count() === 1, 'Found profile images does not match upload count'); @@ -615,7 +615,7 @@ class ImageTest extends TestCase $imagePath = public_path($profileImages->first()->path); $this->assertTrue(file_exists($imagePath)); - $userDelete = $this->asAdmin()->delete("/settings/users/{$editor->id}"); + $userDelete = $this->asAdmin()->delete($editor->getEditUrl()); $userDelete->assertStatus(302); $this->assertDatabaseMissing('images', [ diff --git a/tests/User/UserApiTokenTest.php b/tests/User/UserApiTokenTest.php index 75de49aed..d94e97659 100644 --- a/tests/User/UserApiTokenTest.php +++ b/tests/User/UserApiTokenTest.php @@ -5,25 +5,26 @@ namespace Tests\User; use BookStack\Activity\ActivityType; use BookStack\Api\ApiToken; use Carbon\Carbon; +use Illuminate\Support\Facades\Hash; use Tests\TestCase; class UserApiTokenTest extends TestCase { - protected $testTokenData = [ + protected array $testTokenData = [ 'name' => 'My test API token', 'expires_at' => '2050-04-01', ]; - public function test_tokens_section_not_visible_without_access_api_permission() + public function test_tokens_section_not_visible_in_my_account_without_access_api_permission() { $user = $this->users->viewer(); - $resp = $this->actingAs($user)->get($user->getEditUrl()); + $resp = $this->actingAs($user)->get('/my-account/auth'); $resp->assertDontSeeText('API Tokens'); $this->permissions->grantUserRolePermissions($user, ['access-api']); - $resp = $this->actingAs($user)->get($user->getEditUrl()); + $resp = $this->actingAs($user)->get('/my-account/auth'); $resp->assertSeeText('API Tokens'); $resp->assertSeeText('Create Token'); } @@ -43,14 +44,14 @@ class UserApiTokenTest extends TestCase { $editor = $this->users->editor(); - $resp = $this->asAdmin()->get($editor->getEditUrl('/create-api-token')); + $resp = $this->asAdmin()->get("/api-tokens/{$editor->id}/create"); $resp->assertStatus(200); $resp->assertSee('Create API Token'); $resp->assertSee('Token Secret'); - $resp = $this->post($editor->getEditUrl('/create-api-token'), $this->testTokenData); + $resp = $this->post("/api-tokens/{$editor->id}/create", $this->testTokenData); $token = ApiToken::query()->latest()->first(); - $resp->assertRedirect($editor->getEditUrl('/api-tokens/' . $token->id)); + $resp->assertRedirect("/api-tokens/{$editor->id}/{$token->id}"); $this->assertDatabaseHas('api_tokens', [ 'user_id' => $editor->id, 'name' => $this->testTokenData['name'], @@ -63,7 +64,7 @@ class UserApiTokenTest extends TestCase $this->assertDatabaseMissing('api_tokens', [ 'secret' => $secret, ]); - $this->assertTrue(\Hash::check($secret, $token->secret)); + $this->assertTrue(Hash::check($secret, $token->secret)); $this->assertTrue(strlen($token->token_id) === 32); $this->assertTrue(strlen($secret) === 32); @@ -75,7 +76,10 @@ class UserApiTokenTest extends TestCase public function test_create_with_no_expiry_sets_expiry_hundred_years_away() { $editor = $this->users->editor(); - $this->asAdmin()->post($editor->getEditUrl('/create-api-token'), ['name' => 'No expiry token', 'expires_at' => '']); + + $resp = $this->asAdmin()->post("/api-tokens/{$editor->id}/create", ['name' => 'No expiry token', 'expires_at' => '']); + $resp->assertRedirect(); + $token = ApiToken::query()->latest()->first(); $over = Carbon::now()->addYears(101); @@ -89,7 +93,9 @@ class UserApiTokenTest extends TestCase public function test_created_token_displays_on_profile_page() { $editor = $this->users->editor(); - $this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData); + $resp = $this->asAdmin()->post("/api-tokens/{$editor->id}/create", $this->testTokenData); + $resp->assertRedirect(); + $token = ApiToken::query()->latest()->first(); $resp = $this->get($editor->getEditUrl()); @@ -102,28 +108,29 @@ class UserApiTokenTest extends TestCase public function test_secret_shown_once_after_creation() { $editor = $this->users->editor(); - $resp = $this->asAdmin()->followingRedirects()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData); + $resp = $this->asAdmin()->followingRedirects()->post("/api-tokens/{$editor->id}/create", $this->testTokenData); $resp->assertSeeText('Token Secret'); $token = ApiToken::query()->latest()->first(); $this->assertNull(session('api-token-secret:' . $token->id)); - $resp = $this->get($editor->getEditUrl('/api-tokens/' . $token->id)); + $resp = $this->get("/api-tokens/{$editor->id}/{$token->id}"); + $resp->assertOk(); $resp->assertDontSeeText('Client Secret'); } public function test_token_update() { $editor = $this->users->editor(); - $this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData); + $this->asAdmin()->post("/api-tokens/{$editor->id}/create", $this->testTokenData); $token = ApiToken::query()->latest()->first(); $updateData = [ 'name' => 'My updated token', 'expires_at' => '2011-01-01', ]; - $resp = $this->put($editor->getEditUrl('/api-tokens/' . $token->id), $updateData); - $resp->assertRedirect($editor->getEditUrl('/api-tokens/' . $token->id)); + $resp = $this->put("/api-tokens/{$editor->id}/{$token->id}", $updateData); + $resp->assertRedirect("/api-tokens/{$editor->id}/{$token->id}"); $this->assertDatabaseHas('api_tokens', array_merge($updateData, ['id' => $token->id])); $this->assertSessionHas('success'); @@ -133,13 +140,13 @@ class UserApiTokenTest extends TestCase public function test_token_update_with_blank_expiry_sets_to_hundred_years_away() { $editor = $this->users->editor(); - $this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData); + $this->asAdmin()->post("/api-tokens/{$editor->id}/create", $this->testTokenData); $token = ApiToken::query()->latest()->first(); - $resp = $this->put($editor->getEditUrl('/api-tokens/' . $token->id), [ + $this->put("/api-tokens/{$editor->id}/{$token->id}", [ 'name' => 'My updated token', 'expires_at' => '', - ]); + ])->assertRedirect(); $token->refresh(); $over = Carbon::now()->addYears(101); @@ -153,15 +160,15 @@ class UserApiTokenTest extends TestCase public function test_token_delete() { $editor = $this->users->editor(); - $this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData); + $this->asAdmin()->post("/api-tokens/{$editor->id}/create", $this->testTokenData); $token = ApiToken::query()->latest()->first(); - $tokenUrl = $editor->getEditUrl('/api-tokens/' . $token->id); + $tokenUrl = "/api-tokens/{$editor->id}/{$token->id}"; $resp = $this->get($tokenUrl . '/delete'); $resp->assertSeeText('Delete Token'); $resp->assertSeeText($token->name); - $this->withHtml($resp)->assertElementExists('form[action="' . $tokenUrl . '"]'); + $this->withHtml($resp)->assertElementExists('form[action$="' . $tokenUrl . '"]'); $resp = $this->delete($tokenUrl); $resp->assertRedirect($editor->getEditUrl('#api_tokens')); @@ -175,15 +182,46 @@ class UserApiTokenTest extends TestCase $editor = $this->users->editor(); $this->permissions->grantUserRolePermissions($editor, ['users-manage']); - $this->asAdmin()->post($viewer->getEditUrl('/create-api-token'), $this->testTokenData); + $this->asAdmin()->post("/api-tokens/{$viewer->id}/create", $this->testTokenData); $token = ApiToken::query()->latest()->first(); - $resp = $this->actingAs($editor)->get($viewer->getEditUrl('/api-tokens/' . $token->id)); + $resp = $this->actingAs($editor)->get("/api-tokens/{$viewer->id}/{$token->id}"); $resp->assertStatus(200); $resp->assertSeeText('Delete Token'); - $resp = $this->actingAs($editor)->delete($viewer->getEditUrl('/api-tokens/' . $token->id)); + $resp = $this->actingAs($editor)->delete("/api-tokens/{$viewer->id}/{$token->id}"); $resp->assertRedirect($viewer->getEditUrl('#api_tokens')); $this->assertDatabaseMissing('api_tokens', ['id' => $token->id]); } + + public function test_return_routes_change_depending_on_entry_context() + { + $user = $this->users->admin(); + $returnByContext = [ + 'settings' => url("/settings/users/{$user->id}/#api_tokens"), + 'my-account' => url('/my-account/auth#api_tokens'), + ]; + + foreach ($returnByContext as $context => $returnUrl) { + $resp = $this->actingAs($user)->get("/api-tokens/{$user->id}/create?context={$context}"); + $this->withHtml($resp)->assertLinkExists($returnUrl, 'Cancel'); + + $this->post("/api-tokens/{$user->id}/create", $this->testTokenData); + $token = $user->apiTokens()->latest()->first(); + + $resp = $this->get($token->getUrl()); + $this->withHtml($resp)->assertLinkExists($returnUrl, 'Back'); + + $resp = $this->delete($token->getUrl()); + $resp->assertRedirect($returnUrl); + } + } + + public function test_context_assumed_for_editing_tokens_of_another_user() + { + $user = $this->users->viewer(); + + $resp = $this->asAdmin()->get("/api-tokens/{$user->id}/create?context=my-account"); + $this->withHtml($resp)->assertLinkExists($user->getEditUrl('#api_tokens'), 'Cancel'); + } } diff --git a/tests/User/UserMyAccountTest.php b/tests/User/UserMyAccountTest.php new file mode 100644 index 000000000..63c54daad --- /dev/null +++ b/tests/User/UserMyAccountTest.php @@ -0,0 +1,174 @@ +asEditor()->get('/my-account'); + $resp->assertRedirect('/my-account/profile'); + } + + public function test_views_not_accessible_to_guest_user() + { + $categories = ['profile', 'auth', 'shortcuts', 'notifications', '']; + $this->setSettings(['app-public' => 'true']); + + $this->permissions->grantUserRolePermissions($this->users->guest(), ['receive-notifications']); + + foreach ($categories as $category) { + $resp = $this->get('/my-account/' . $category); + $resp->assertRedirect('/'); + } + } + public function test_interface_shortcuts_updating() + { + $this->asEditor(); + + // View preferences with defaults + $resp = $this->get('/my-account/shortcuts'); + $resp->assertSee('UI Shortcut Preferences'); + + $html = $this->withHtml($resp); + $html->assertFieldHasValue('enabled', 'false'); + $html->assertFieldHasValue('shortcut[home_view]', '1'); + + // Update preferences + $resp = $this->put('/my-account/shortcuts', [ + 'enabled' => 'true', + 'shortcut' => ['home_view' => 'Ctrl + 1'], + ]); + + $resp->assertRedirect('/my-account/shortcuts'); + $resp->assertSessionHas('success', 'Shortcut preferences have been updated!'); + + // View updates to preferences page + $resp = $this->get('/my-account/shortcuts'); + $html = $this->withHtml($resp); + $html->assertFieldHasValue('enabled', 'true'); + $html->assertFieldHasValue('shortcut[home_view]', 'Ctrl + 1'); + } + + public function test_body_has_shortcuts_component_when_active() + { + $editor = $this->users->editor(); + $this->actingAs($editor); + + $this->withHtml($this->get('/'))->assertElementNotExists('body[component="shortcuts"]'); + + setting()->putUser($editor, 'ui-shortcuts-enabled', 'true'); + $this->withHtml($this->get('/'))->assertElementExists('body[component="shortcuts"]'); + } + + public function test_notification_routes_requires_notification_permission() + { + $viewer = $this->users->viewer(); + $resp = $this->actingAs($viewer)->get('/my-account/notifications'); + $this->assertPermissionError($resp); + + $resp = $this->actingAs($viewer)->get('/my-account/profile'); + $resp->assertDontSeeText('Notification Preferences'); + + $resp = $this->put('/my-account/notifications'); + $this->assertPermissionError($resp); + + $this->permissions->grantUserRolePermissions($viewer, ['receive-notifications']); + $resp = $this->get('/my-account/notifications'); + $resp->assertOk(); + $resp->assertSee('Notification Preferences'); + } + + public function test_notification_preferences_updating() + { + $editor = $this->users->editor(); + + // View preferences with defaults + $resp = $this->actingAs($editor)->get('/my-account/notifications'); + $resp->assertSee('Notification Preferences'); + + $html = $this->withHtml($resp); + $html->assertFieldHasValue('preferences[comment-replies]', 'false'); + + // Update preferences + $resp = $this->put('/my-account/notifications', [ + 'preferences' => ['comment-replies' => 'true'], + ]); + + $resp->assertRedirect('/my-account/notifications'); + $resp->assertSessionHas('success', 'Notification preferences have been updated!'); + + // View updates to preferences page + $resp = $this->get('/my-account/notifications'); + $html = $this->withHtml($resp); + $html->assertFieldHasValue('preferences[comment-replies]', 'true'); + } + + public function test_notification_preferences_show_watches() + { + $editor = $this->users->editor(); + $book = $this->entities->book(); + + $options = new UserEntityWatchOptions($editor, $book); + $options->updateLevelByValue(WatchLevels::COMMENTS); + + $resp = $this->actingAs($editor)->get('/my-account/notifications'); + $resp->assertSee($book->name); + $resp->assertSee('All Page Updates & Comments'); + + $options->updateLevelByValue(WatchLevels::DEFAULT); + + $resp = $this->actingAs($editor)->get('/my-account/notifications'); + $resp->assertDontSee($book->name); + $resp->assertDontSee('All Page Updates & Comments'); + } + + public function test_notification_preferences_dont_error_on_deleted_items() + { + $editor = $this->users->editor(); + $book = $this->entities->book(); + + $options = new UserEntityWatchOptions($editor, $book); + $options->updateLevelByValue(WatchLevels::COMMENTS); + + $this->actingAs($editor)->delete($book->getUrl()); + $book->refresh(); + $this->assertNotNull($book->deleted_at); + + $resp = $this->actingAs($editor)->get('/my-account/notifications'); + $resp->assertOk(); + $resp->assertDontSee($book->name); + } + + public function test_notification_preferences_not_accessible_to_guest() + { + $this->setSettings(['app-public' => 'true']); + $guest = $this->users->guest(); + $this->permissions->grantUserRolePermissions($guest, ['receive-notifications']); + + $resp = $this->get('/my-account/notifications'); + $this->assertPermissionError($resp); + + $resp = $this->put('/my-account/notifications', [ + 'preferences' => ['comment-replies' => 'true'], + ]); + $this->assertPermissionError($resp); + } + + public function test_notification_comment_options_only_exist_if_comments_active() + { + $resp = $this->asEditor()->get('/my-account/notifications'); + $resp->assertSee('Notify upon comments'); + $resp->assertSee('Notify upon replies'); + + setting()->put('app-disable-comments', true); + + $resp = $this->get('/my-account/notifications'); + $resp->assertDontSee('Notify upon comments'); + $resp->assertDontSee('Notify upon replies'); + } +} diff --git a/tests/User/UserPreferencesTest.php b/tests/User/UserPreferencesTest.php index 4a6cba7b3..d78ac2ea7 100644 --- a/tests/User/UserPreferencesTest.php +++ b/tests/User/UserPreferencesTest.php @@ -8,167 +8,6 @@ use Tests\TestCase; class UserPreferencesTest extends TestCase { - public function test_index_view() - { - $resp = $this->asEditor()->get('/preferences'); - $resp->assertOk(); - $resp->assertSee('Interface Keyboard Shortcuts'); - $resp->assertSee('Edit Profile'); - } - - public function test_index_view_accessible_but_without_profile_and_notifications_for_guest_user() - { - $this->setSettings(['app-public' => 'true']); - $this->permissions->grantUserRolePermissions($this->users->guest(), ['receive-notifications']); - $resp = $this->get('/preferences'); - $resp->assertOk(); - $resp->assertSee('Interface Keyboard Shortcuts'); - $resp->assertDontSee('Edit Profile'); - $resp->assertDontSee('Notification'); - } - public function test_interface_shortcuts_updating() - { - $this->asEditor(); - - // View preferences with defaults - $resp = $this->get('/preferences/shortcuts'); - $resp->assertSee('Interface Keyboard Shortcuts'); - - $html = $this->withHtml($resp); - $html->assertFieldHasValue('enabled', 'false'); - $html->assertFieldHasValue('shortcut[home_view]', '1'); - - // Update preferences - $resp = $this->put('/preferences/shortcuts', [ - 'enabled' => 'true', - 'shortcut' => ['home_view' => 'Ctrl + 1'], - ]); - - $resp->assertRedirect('/preferences/shortcuts'); - $resp->assertSessionHas('success', 'Shortcut preferences have been updated!'); - - // View updates to preferences page - $resp = $this->get('/preferences/shortcuts'); - $html = $this->withHtml($resp); - $html->assertFieldHasValue('enabled', 'true'); - $html->assertFieldHasValue('shortcut[home_view]', 'Ctrl + 1'); - } - - public function test_body_has_shortcuts_component_when_active() - { - $editor = $this->users->editor(); - $this->actingAs($editor); - - $this->withHtml($this->get('/'))->assertElementNotExists('body[component="shortcuts"]'); - - setting()->putUser($editor, 'ui-shortcuts-enabled', 'true'); - $this->withHtml($this->get('/'))->assertElementExists('body[component="shortcuts"]'); - } - - public function test_notification_routes_requires_notification_permission() - { - $viewer = $this->users->viewer(); - $resp = $this->actingAs($viewer)->get('/preferences/notifications'); - $this->assertPermissionError($resp); - - $resp = $this->put('/preferences/notifications'); - $this->assertPermissionError($resp); - - $this->permissions->grantUserRolePermissions($viewer, ['receive-notifications']); - $resp = $this->get('/preferences/notifications'); - $resp->assertOk(); - $resp->assertSee('Notification Preferences'); - } - - public function test_notification_preferences_updating() - { - $editor = $this->users->editor(); - - // View preferences with defaults - $resp = $this->actingAs($editor)->get('/preferences/notifications'); - $resp->assertSee('Notification Preferences'); - - $html = $this->withHtml($resp); - $html->assertFieldHasValue('preferences[comment-replies]', 'false'); - - // Update preferences - $resp = $this->put('/preferences/notifications', [ - 'preferences' => ['comment-replies' => 'true'], - ]); - - $resp->assertRedirect('/preferences/notifications'); - $resp->assertSessionHas('success', 'Notification preferences have been updated!'); - - // View updates to preferences page - $resp = $this->get('/preferences/notifications'); - $html = $this->withHtml($resp); - $html->assertFieldHasValue('preferences[comment-replies]', 'true'); - } - - public function test_notification_preferences_show_watches() - { - $editor = $this->users->editor(); - $book = $this->entities->book(); - - $options = new UserEntityWatchOptions($editor, $book); - $options->updateLevelByValue(WatchLevels::COMMENTS); - - $resp = $this->actingAs($editor)->get('/preferences/notifications'); - $resp->assertSee($book->name); - $resp->assertSee('All Page Updates & Comments'); - - $options->updateLevelByValue(WatchLevels::DEFAULT); - - $resp = $this->actingAs($editor)->get('/preferences/notifications'); - $resp->assertDontSee($book->name); - $resp->assertDontSee('All Page Updates & Comments'); - } - - public function test_notification_preferences_dont_error_on_deleted_items() - { - $editor = $this->users->editor(); - $book = $this->entities->book(); - - $options = new UserEntityWatchOptions($editor, $book); - $options->updateLevelByValue(WatchLevels::COMMENTS); - - $this->actingAs($editor)->delete($book->getUrl()); - $book->refresh(); - $this->assertNotNull($book->deleted_at); - - $resp = $this->actingAs($editor)->get('/preferences/notifications'); - $resp->assertOk(); - $resp->assertDontSee($book->name); - } - - public function test_notification_preferences_not_accessible_to_guest() - { - $this->setSettings(['app-public' => 'true']); - $guest = $this->users->guest(); - $this->permissions->grantUserRolePermissions($guest, ['receive-notifications']); - - $resp = $this->get('/preferences/notifications'); - $this->assertPermissionError($resp); - - $resp = $this->put('/preferences/notifications', [ - 'preferences' => ['comment-replies' => 'true'], - ]); - $this->assertPermissionError($resp); - } - - public function test_notification_comment_options_only_exist_if_comments_active() - { - $resp = $this->asEditor()->get('/preferences/notifications'); - $resp->assertSee('Notify upon comments'); - $resp->assertSee('Notify upon replies'); - - setting()->put('app-disable-comments', true); - - $resp = $this->get('/preferences/notifications'); - $resp->assertDontSee('Notify upon comments'); - $resp->assertDontSee('Notify upon replies'); - } - public function test_update_sort_preference() { $editor = $this->users->editor();