My Account: Updated and started adding to tests

- Updated existing tests now affected by my-account changes.
- Updated some existing tests to more accuractly check the scenario.
- Updated some code styling in SocialController.
- Fixed redirects for social account flows to fit my-account.
- Added test for social account attaching.
- Added test for api token redirect handling.
This commit is contained in:
Dan Brown 2023-10-19 14:18:42 +01:00
parent 12946414b0
commit fabc854390
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
14 changed files with 302 additions and 227 deletions

View File

@ -16,22 +16,12 @@ use Laravel\Socialite\Contracts\User as SocialUser;
class SocialController extends Controller class SocialController extends Controller
{ {
protected SocialAuthService $socialAuthService;
protected RegistrationService $registrationService;
protected LoginService $loginService;
/**
* SocialController constructor.
*/
public function __construct( public function __construct(
SocialAuthService $socialAuthService, protected SocialAuthService $socialAuthService,
RegistrationService $registrationService, protected RegistrationService $registrationService,
LoginService $loginService protected LoginService $loginService,
) { ) {
$this->middleware('guest')->only(['register']); $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); $this->socialAuthService->detachSocialAccount($socialDriver);
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)])); session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));
return redirect(user()->getEditUrl()); return redirect('/my-account/auth#social-accounts');
} }
/** /**

View File

@ -154,21 +154,21 @@ class SocialAuthService
$currentUser->socialAccounts()->save($account); $currentUser->socialAccounts()->save($account);
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver])); 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. // 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) { if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver])); 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. // 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) { if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver])); 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. // Otherwise let the user know this social account is not used by anyone.

View File

@ -166,7 +166,7 @@ class UserApiTokenController extends Controller
protected function getRedirectPath(User $relatedUser): string protected function getRedirectPath(User $relatedUser): string
{ {
$context = session()->get('api-token-context'); $context = session()->get('api-token-context');
if ($context === 'settings') { if ($context === 'settings' || user()->id !== $relatedUser->id) {
return $relatedUser->getEditUrl('#api_tokens'); return $relatedUser->getEditUrl('#api_tokens');
} }

View File

@ -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_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_connect' => 'Connect Account',
'users_social_disconnect' => 'Disconnect 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_connected' => ':socialAccount account was successfully attached to your profile.',
'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.', 'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',
'users_api_tokens' => 'API Tokens', 'users_api_tokens' => 'API Tokens',

View File

@ -12,7 +12,9 @@
<a href="{{ url('/my-account/profile') }}" class="{{ $category === 'profile' ? 'active' : '' }}">@icon('user') {{ trans('preferences.profile') }}</a> <a href="{{ url('/my-account/profile') }}" class="{{ $category === 'profile' ? 'active' : '' }}">@icon('user') {{ trans('preferences.profile') }}</a>
<a href="{{ url('/my-account/auth') }}" class="{{ $category === 'auth' ? 'active' : '' }}">@icon('security') {{ trans('preferences.auth') }}</a> <a href="{{ url('/my-account/auth') }}" class="{{ $category === 'auth' ? 'active' : '' }}">@icon('security') {{ trans('preferences.auth') }}</a>
<a href="{{ url('/my-account/shortcuts') }}" class="{{ $category === 'shortcuts' ? 'active' : '' }}">@icon('shortcuts') {{ trans('preferences.shortcuts_interface') }}</a> <a href="{{ url('/my-account/shortcuts') }}" class="{{ $category === 'shortcuts' ? 'active' : '' }}">@icon('shortcuts') {{ trans('preferences.shortcuts_interface') }}</a>
<a href="{{ url('/my-account/notifications') }}" class="{{ $category === 'notifications' ? 'active' : '' }}">@icon('notifications') {{ trans('preferences.notifications') }}</a> @if(userCan('receive-notifications'))
<a href="{{ url('/my-account/notifications') }}" class="{{ $category === 'notifications' ? 'active' : '' }}">@icon('notifications') {{ trans('preferences.notifications') }}</a>
@endif
</nav> </nav>
</div> </div>
</div> </div>

View File

@ -89,9 +89,9 @@
<div role="presentation">@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div> <div role="presentation">@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div>
<p class="my-none bold">{{ $driverName }}</p> <p class="my-none bold">{{ $driverName }}</p>
@if($user->hasSocialAccount($driver)) @if($user->hasSocialAccount($driver))
<p class="text-pos bold text-small my-none">Connected</p> <p class="text-pos bold text-small my-none">{{ trans('settings.users_social_status_connected') }}</p>
@else @else
<p class="text-neg bold text-small my-none">Disconnected</p> <p class="text-neg bold text-small my-none">{{ trans('settings.users_social_status_disconnected') }}</p>
@endif @endif
</div> </div>
@endforeach @endforeach

View File

@ -244,6 +244,8 @@ Route::middleware('auth')->group(function () {
Route::put('/my-account/auth/password', [UserControllers\UserAccountController::class, 'updatePassword']); Route::put('/my-account/auth/password', [UserControllers\UserAccountController::class, 'updatePassword']);
Route::get('/my-account/delete', [UserControllers\UserAccountController::class, 'delete']); Route::get('/my-account/delete', [UserControllers\UserAccountController::class, 'delete']);
Route::delete('/my-account', [UserControllers\UserAccountController::class, 'destroy']); 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-view/{type}', [UserControllers\UserPreferencesController::class, 'changeView']);
Route::patch('/preferences/change-sort/{type}', [UserControllers\UserPreferencesController::class, 'changeSort']); Route::patch('/preferences/change-sort/{type}', [UserControllers\UserPreferencesController::class, 'changeSort']);
Route::patch('/preferences/change-expansion/{type}', [UserControllers\UserPreferencesController::class, 'changeExpansion']); Route::patch('/preferences/change-expansion/{type}', [UserControllers\UserPreferencesController::class, 'changeExpansion']);

View File

@ -51,7 +51,7 @@ class WebhookCallTest extends TestCase
{ {
// This test must not fake the queue/bus since this covers an issue // This test must not fake the queue/bus since this covers an issue
// around handling and serialization of items now deleted from the database. // 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)]); $this->mockHttpClient([new Response(500)]);
$user = $this->users->newUser(); $user = $this->users->newUser();
@ -61,8 +61,10 @@ class WebhookCallTest extends TestCase
/** @var ApiToken $apiToken */ /** @var ApiToken $apiToken */
$editor = $this->users->editor(); $editor = $this->users->editor();
$apiToken = ApiToken::factory()->create(['user_id' => $editor]); $apiToken = ApiToken::factory()->create(['user_id' => $editor]);
$resp = $this->delete($editor->getEditUrl('/api-tokens/' . $apiToken->id)); $this->delete($apiToken->getUrl())->assertRedirect();
$resp->assertRedirect($editor->getEditUrl('#api_tokens'));
$webhook->refresh();
$this->assertEquals('Response status from endpoint was 500', $webhook->last_error);
} }
public function test_failed_webhook_call_logs_error() public function test_failed_webhook_call_logs_error()

View File

@ -18,7 +18,7 @@ class SocialAuthTest extends TestCase
$user = User::factory()->make(); $user = User::factory()->make();
$this->setSettings(['registration-enabled' => 'true']); $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); $mockSocialite = $this->mock(Factory::class);
$mockSocialDriver = Mockery::mock(Provider::class); $mockSocialDriver = Mockery::mock(Provider::class);
@ -45,7 +45,6 @@ class SocialAuthTest extends TestCase
config([ config([
'GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc',
'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc', 'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc',
'APP_URL' => 'http://localhost',
]); ]);
$mockSocialite = $this->mock(Factory::class); $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); $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() public function test_social_account_detach()
{ {
$editor = $this->users->editor(); $editor = $this->users->editor();
config([ config([
'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc', 'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc',
'APP_URL' => 'http://localhost',
]); ]);
$socialAccount = SocialAccount::query()->forceCreate([ $socialAccount = SocialAccount::query()->forceCreate([
@ -100,11 +128,11 @@ class SocialAuthTest extends TestCase
'driver_id' => 'logintest123', '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'); $this->withHtml($resp)->assertElementContains('form[action$="/login/service/github/detach"]', 'Disconnect Account');
$resp = $this->post('/login/service/github/detach'); $resp = $this->post('/login/service/github/detach');
$resp->assertRedirect($editor->getEditUrl()); $resp->assertRedirect('/my-account/auth#social-accounts');
$resp = $this->followRedirects($resp); $resp = $this->followRedirects($resp);
$resp->assertSee('Github account was successfully disconnected from your profile.'); $resp->assertSee('Github account was successfully disconnected from your profile.');
@ -115,7 +143,6 @@ class SocialAuthTest extends TestCase
{ {
config([ config([
'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc', 'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc',
'APP_URL' => 'http://localhost',
]); ]);
$user = User::factory()->make(); $user = User::factory()->make();
@ -153,7 +180,7 @@ class SocialAuthTest extends TestCase
{ {
config([ config([
'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc', '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(); $user = User::factory()->make();
@ -191,7 +218,7 @@ class SocialAuthTest extends TestCase
$user = User::factory()->make(['email' => 'nonameuser@example.com']); $user = User::factory()->make(['email' => 'nonameuser@example.com']);
$this->setSettings(['registration-enabled' => 'true']); $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); $mockSocialite = $this->mock(Factory::class);
$mockSocialDriver = Mockery::mock(Provider::class); $mockSocialDriver = Mockery::mock(Provider::class);

View File

@ -44,14 +44,12 @@ class RolePermissionsTest extends TestCase
public function test_user_cannot_change_email_unless_they_have_manage_users_permission() public function test_user_cannot_change_email_unless_they_have_manage_users_permission()
{ {
$userProfileUrl = '/settings/users/' . $this->user->id;
$originalEmail = $this->user->email; $originalEmail = $this->user->email;
$this->actingAs($this->user); $this->actingAs($this->user);
$resp = $this->get($userProfileUrl) $resp = $this->get('/my-account/profile')->assertOk();
->assertOk();
$this->withHtml($resp)->assertElementExists('input[name=email][disabled]'); $this->withHtml($resp)->assertElementExists('input[name=email][disabled]');
$this->put($userProfileUrl, [ $this->put('/my-account/profile', [
'name' => 'my_new_name', 'name' => 'my_new_name',
'email' => 'new_email@example.com', 'email' => 'new_email@example.com',
]); ]);
@ -63,11 +61,12 @@ class RolePermissionsTest extends TestCase
$this->permissions->grantUserRolePermissions($this->user, ['users-manage']); $this->permissions->grantUserRolePermissions($this->user, ['users-manage']);
$resp = $this->get($userProfileUrl) $resp = $this->get('/my-account/profile')->assertOk();
->assertOk(); $this->withHtml($resp)
$this->withHtml($resp)->assertElementNotExists('input[name=email][disabled]') ->assertElementNotExists('input[name=email][disabled]')
->assertElementExists('input[name=email]'); ->assertElementExists('input[name=email]');
$this->put($userProfileUrl, [
$this->put('/my-account/profile', [
'name' => 'my_new_name_2', 'name' => 'my_new_name_2',
'email' => 'new_email@example.com', 'email' => 'new_email@example.com',
]); ]);

View File

@ -607,7 +607,7 @@ class ImageTest extends TestCase
$this->actingAs($editor); $this->actingAs($editor);
$file = $this->getTestProfileImage(); $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(); $profileImages = Image::where('type', '=', 'user')->where('created_by', '=', $editor->id)->get();
$this->assertTrue($profileImages->count() === 1, 'Found profile images does not match upload count'); $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); $imagePath = public_path($profileImages->first()->path);
$this->assertTrue(file_exists($imagePath)); $this->assertTrue(file_exists($imagePath));
$userDelete = $this->asAdmin()->delete("/settings/users/{$editor->id}"); $userDelete = $this->asAdmin()->delete($editor->getEditUrl());
$userDelete->assertStatus(302); $userDelete->assertStatus(302);
$this->assertDatabaseMissing('images', [ $this->assertDatabaseMissing('images', [

View File

@ -5,25 +5,26 @@ namespace Tests\User;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Api\ApiToken; use BookStack\Api\ApiToken;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase; use Tests\TestCase;
class UserApiTokenTest extends TestCase class UserApiTokenTest extends TestCase
{ {
protected $testTokenData = [ protected array $testTokenData = [
'name' => 'My test API token', 'name' => 'My test API token',
'expires_at' => '2050-04-01', '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(); $user = $this->users->viewer();
$resp = $this->actingAs($user)->get($user->getEditUrl()); $resp = $this->actingAs($user)->get('/my-account/auth');
$resp->assertDontSeeText('API Tokens'); $resp->assertDontSeeText('API Tokens');
$this->permissions->grantUserRolePermissions($user, ['access-api']); $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('API Tokens');
$resp->assertSeeText('Create Token'); $resp->assertSeeText('Create Token');
} }
@ -43,14 +44,14 @@ class UserApiTokenTest extends TestCase
{ {
$editor = $this->users->editor(); $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->assertStatus(200);
$resp->assertSee('Create API Token'); $resp->assertSee('Create API Token');
$resp->assertSee('Token Secret'); $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(); $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', [ $this->assertDatabaseHas('api_tokens', [
'user_id' => $editor->id, 'user_id' => $editor->id,
'name' => $this->testTokenData['name'], 'name' => $this->testTokenData['name'],
@ -63,7 +64,7 @@ class UserApiTokenTest extends TestCase
$this->assertDatabaseMissing('api_tokens', [ $this->assertDatabaseMissing('api_tokens', [
'secret' => $secret, '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($token->token_id) === 32);
$this->assertTrue(strlen($secret) === 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() public function test_create_with_no_expiry_sets_expiry_hundred_years_away()
{ {
$editor = $this->users->editor(); $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(); $token = ApiToken::query()->latest()->first();
$over = Carbon::now()->addYears(101); $over = Carbon::now()->addYears(101);
@ -89,7 +93,9 @@ class UserApiTokenTest extends TestCase
public function test_created_token_displays_on_profile_page() public function test_created_token_displays_on_profile_page()
{ {
$editor = $this->users->editor(); $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(); $token = ApiToken::query()->latest()->first();
$resp = $this->get($editor->getEditUrl()); $resp = $this->get($editor->getEditUrl());
@ -102,28 +108,29 @@ class UserApiTokenTest extends TestCase
public function test_secret_shown_once_after_creation() public function test_secret_shown_once_after_creation()
{ {
$editor = $this->users->editor(); $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'); $resp->assertSeeText('Token Secret');
$token = ApiToken::query()->latest()->first(); $token = ApiToken::query()->latest()->first();
$this->assertNull(session('api-token-secret:' . $token->id)); $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'); $resp->assertDontSeeText('Client Secret');
} }
public function test_token_update() public function test_token_update()
{ {
$editor = $this->users->editor(); $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(); $token = ApiToken::query()->latest()->first();
$updateData = [ $updateData = [
'name' => 'My updated token', 'name' => 'My updated token',
'expires_at' => '2011-01-01', 'expires_at' => '2011-01-01',
]; ];
$resp = $this->put($editor->getEditUrl('/api-tokens/' . $token->id), $updateData); $resp = $this->put("/api-tokens/{$editor->id}/{$token->id}", $updateData);
$resp->assertRedirect($editor->getEditUrl('/api-tokens/' . $token->id)); $resp->assertRedirect("/api-tokens/{$editor->id}/{$token->id}");
$this->assertDatabaseHas('api_tokens', array_merge($updateData, ['id' => $token->id])); $this->assertDatabaseHas('api_tokens', array_merge($updateData, ['id' => $token->id]));
$this->assertSessionHas('success'); $this->assertSessionHas('success');
@ -133,13 +140,13 @@ class UserApiTokenTest extends TestCase
public function test_token_update_with_blank_expiry_sets_to_hundred_years_away() public function test_token_update_with_blank_expiry_sets_to_hundred_years_away()
{ {
$editor = $this->users->editor(); $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(); $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', 'name' => 'My updated token',
'expires_at' => '', 'expires_at' => '',
]); ])->assertRedirect();
$token->refresh(); $token->refresh();
$over = Carbon::now()->addYears(101); $over = Carbon::now()->addYears(101);
@ -153,15 +160,15 @@ class UserApiTokenTest extends TestCase
public function test_token_delete() public function test_token_delete()
{ {
$editor = $this->users->editor(); $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(); $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 = $this->get($tokenUrl . '/delete');
$resp->assertSeeText('Delete Token'); $resp->assertSeeText('Delete Token');
$resp->assertSeeText($token->name); $resp->assertSeeText($token->name);
$this->withHtml($resp)->assertElementExists('form[action="' . $tokenUrl . '"]'); $this->withHtml($resp)->assertElementExists('form[action$="' . $tokenUrl . '"]');
$resp = $this->delete($tokenUrl); $resp = $this->delete($tokenUrl);
$resp->assertRedirect($editor->getEditUrl('#api_tokens')); $resp->assertRedirect($editor->getEditUrl('#api_tokens'));
@ -175,15 +182,46 @@ class UserApiTokenTest extends TestCase
$editor = $this->users->editor(); $editor = $this->users->editor();
$this->permissions->grantUserRolePermissions($editor, ['users-manage']); $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(); $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->assertStatus(200);
$resp->assertSeeText('Delete Token'); $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')); $resp->assertRedirect($viewer->getEditUrl('#api_tokens'));
$this->assertDatabaseMissing('api_tokens', ['id' => $token->id]); $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');
}
} }

View File

@ -0,0 +1,174 @@
<?php
namespace Tests\User;
use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Activity\WatchLevels;
use Tests\TestCase;
class UserMyAccountTest extends TestCase
{
public function test_index_view()
{
$resp = $this->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');
}
}

View File

@ -8,167 +8,6 @@ use Tests\TestCase;
class UserPreferencesTest extends 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() public function test_update_sort_preference()
{ {
$editor = $this->users->editor(); $editor = $this->users->editor();