From 71654810756f3be977359e5fdcd79375aab47c52 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 22 Sep 2022 15:12:05 +0100 Subject: [PATCH 01/24] Updated auth controllers with property types --- app/Http/Controllers/Auth/ConfirmEmailController.php | 6 +++--- app/Http/Controllers/Auth/LoginController.php | 4 ++-- app/Http/Controllers/Auth/RegisterController.php | 6 ++---- app/Http/Controllers/Auth/SocialController.php | 6 +++--- app/Http/Controllers/Auth/UserInviteController.php | 4 ++-- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/Http/Controllers/Auth/ConfirmEmailController.php b/app/Http/Controllers/Auth/ConfirmEmailController.php index 873d88475..ea633ff3a 100644 --- a/app/Http/Controllers/Auth/ConfirmEmailController.php +++ b/app/Http/Controllers/Auth/ConfirmEmailController.php @@ -14,9 +14,9 @@ use Illuminate\Http\Request; class ConfirmEmailController extends Controller { - protected $emailConfirmationService; - protected $loginService; - protected $userRepo; + protected EmailConfirmationService $emailConfirmationService; + protected LoginService $loginService; + protected UserRepo $userRepo; /** * Create a new controller instance. diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 1d6a36c5b..bf40b0e03 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -31,8 +31,8 @@ class LoginController extends Controller /** * Redirection paths. */ - protected $redirectTo = '/'; - protected $redirectPath = '/'; + protected string $redirectTo = '/'; + protected string $redirectPath = '/'; protected SocialAuthService $socialAuthService; protected LoginService $loginService; diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 15ee78d50..d74944800 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -35,11 +35,9 @@ class RegisterController extends Controller /** * Where to redirect users after login / registration. - * - * @var string */ - protected $redirectTo = '/'; - protected $redirectPath = '/'; + protected string $redirectTo = '/'; + protected string $redirectPath = '/'; /** * Create a new controller instance. diff --git a/app/Http/Controllers/Auth/SocialController.php b/app/Http/Controllers/Auth/SocialController.php index 1691668a2..cf7ae9041 100644 --- a/app/Http/Controllers/Auth/SocialController.php +++ b/app/Http/Controllers/Auth/SocialController.php @@ -16,9 +16,9 @@ use Laravel\Socialite\Contracts\User as SocialUser; class SocialController extends Controller { - protected $socialAuthService; - protected $registrationService; - protected $loginService; + protected SocialAuthService $socialAuthService; + protected RegistrationService $registrationService; + protected LoginService $loginService; /** * SocialController constructor. diff --git a/app/Http/Controllers/Auth/UserInviteController.php b/app/Http/Controllers/Auth/UserInviteController.php index 27b20f831..213959abd 100644 --- a/app/Http/Controllers/Auth/UserInviteController.php +++ b/app/Http/Controllers/Auth/UserInviteController.php @@ -15,8 +15,8 @@ use Illuminate\Validation\Rules\Password; class UserInviteController extends Controller { - protected $inviteService; - protected $userRepo; + protected UserInviteService $inviteService; + protected UserRepo $userRepo; /** * Create a new controller instance. From f4388d5e4a6353f6f79e03c638ed2971fbf48c54 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 22 Sep 2022 16:54:27 +0100 Subject: [PATCH 02/24] Removed usage of laravel/ui dependency Brings app auth controller handling aligned within the app, rather than having many overrides of the framwork packages causing confusion and messiness over time. --- app/Auth/Access/LoginService.php | 2 + app/Auth/UserRepo.php | 5 +- .../Auth/ForgotPasswordController.php | 23 +-- app/Http/Controllers/Auth/LoginController.php | 167 ++++++------------ .../Controllers/Auth/RegisterController.php | 57 +----- .../Auth/ResetPasswordController.php | 94 ++++++---- app/Http/Controllers/Auth/Saml2Controller.php | 2 +- .../Controllers/Auth/SocialController.php | 2 +- app/Http/Controllers/Auth/ThrottlesLogins.php | 92 ++++++++++ .../Controllers/Auth/UserInviteController.php | 3 +- composer.json | 1 - composer.lock | 63 +------ 12 files changed, 232 insertions(+), 279 deletions(-) create mode 100644 app/Http/Controllers/Auth/ThrottlesLogins.php diff --git a/app/Auth/Access/LoginService.php b/app/Auth/Access/LoginService.php index f41570417..c80943166 100644 --- a/app/Auth/Access/LoginService.php +++ b/app/Auth/Access/LoginService.php @@ -5,6 +5,7 @@ namespace BookStack\Auth\Access; use BookStack\Actions\ActivityType; use BookStack\Auth\Access\Mfa\MfaSession; use BookStack\Auth\User; +use BookStack\Exceptions\LoginAttemptException; use BookStack\Exceptions\StoppedAuthenticationException; use BookStack\Facades\Activity; use BookStack\Facades\Theme; @@ -149,6 +150,7 @@ class LoginService * May interrupt the flow if extra authentication requirements are imposed. * * @throws StoppedAuthenticationException + * @throws LoginAttemptException */ public function attempt(array $credentials, string $method, bool $remember = false): bool { diff --git a/app/Auth/UserRepo.php b/app/Auth/UserRepo.php index 28ce96c49..c589fd964 100644 --- a/app/Auth/UserRepo.php +++ b/app/Auth/UserRepo.php @@ -10,6 +10,7 @@ use BookStack\Exceptions\UserUpdateException; use BookStack\Facades\Activity; use BookStack\Uploads\UserAvatars; use Exception; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; @@ -61,7 +62,7 @@ class UserRepo $user = new User(); $user->name = $data['name']; $user->email = $data['email']; - $user->password = bcrypt(empty($data['password']) ? Str::random(32) : $data['password']); + $user->password = Hash::make(empty($data['password']) ? Str::random(32) : $data['password']); $user->email_confirmed = $emailConfirmed; $user->external_auth_id = $data['external_auth_id'] ?? ''; @@ -126,7 +127,7 @@ class UserRepo } if (!empty($data['password'])) { - $user->password = bcrypt($data['password']); + $user->password = Hash::make($data['password']); } if (!empty($data['language'])) { diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index b345fad1c..2bdc31df5 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -4,24 +4,11 @@ namespace BookStack\Http\Controllers\Auth; use BookStack\Actions\ActivityType; use BookStack\Http\Controllers\Controller; -use Illuminate\Foundation\Auth\SendsPasswordResetEmails; use Illuminate\Http\Request; use Illuminate\Support\Facades\Password; class ForgotPasswordController extends Controller { - /* - |-------------------------------------------------------------------------- - | Password Reset Controller - |-------------------------------------------------------------------------- - | - | This controller is responsible for handling password reset emails and - | includes a trait which assists in sending these notifications from - | your application to your users. Feel free to explore this trait. - | - */ - use SendsPasswordResetEmails; - /** * Create a new controller instance. * @@ -33,6 +20,14 @@ class ForgotPasswordController extends Controller $this->middleware('guard:standard'); } + /** + * Display the form to request a password reset link. + */ + public function showLinkRequestForm() + { + return view('auth.passwords.email'); + } + /** * Send a reset link to the given user. * @@ -49,7 +44,7 @@ class ForgotPasswordController extends Controller // We will send the password reset link to this user. Once we have attempted // to send the link, we will examine the response then see the message we // need to show to the user. Finally, we'll send out a proper response. - $response = $this->broker()->sendResetLink( + $response = Password::broker()->sendResetLink( $request->only('email') ); diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index bf40b0e03..e16feb079 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -8,31 +8,14 @@ use BookStack\Exceptions\LoginAttemptEmailNeededException; use BookStack\Exceptions\LoginAttemptException; use BookStack\Facades\Activity; use BookStack\Http\Controllers\Controller; -use Illuminate\Foundation\Auth\AuthenticatesUsers; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Illuminate\Validation\ValidationException; class LoginController extends Controller { - /* - |-------------------------------------------------------------------------- - | Login Controller - |-------------------------------------------------------------------------- - | - | This controller handles authenticating users for the application and - | redirecting them to your home screen. The controller uses a trait - | to conveniently provide its functionality to your applications. - | - */ - use AuthenticatesUsers { - logout as traitLogout; - } - - /** - * Redirection paths. - */ - protected string $redirectTo = '/'; - protected string $redirectPath = '/'; + use ThrottlesLogins; protected SocialAuthService $socialAuthService; protected LoginService $loginService; @@ -48,21 +31,6 @@ class LoginController extends Controller $this->socialAuthService = $socialAuthService; $this->loginService = $loginService; - - $this->redirectPath = url('/'); - } - - public function username() - { - return config('auth.method') === 'standard' ? 'email' : 'username'; - } - - /** - * Get the needed authorization credentials from the request. - */ - protected function credentials(Request $request) - { - return $request->only('username', 'email', 'password'); } /** @@ -98,29 +66,15 @@ class LoginController extends Controller /** * Handle a login request to the application. - * - * @param \Illuminate\Http\Request $request - * - * @throws \Illuminate\Validation\ValidationException - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function login(Request $request) { $this->validateLogin($request); $username = $request->get($this->username()); - // If the class is using the ThrottlesLogins trait, we can automatically throttle - // the login attempts for this application. We'll key this by the username and - // the IP address of the client making these requests into this application. - if ( - method_exists($this, 'hasTooManyLoginAttempts') && - $this->hasTooManyLoginAttempts($request) - ) { - $this->fireLockoutEvent($request); - + // Check login throttling attempts to see if they've gone over the limit + if ($this->hasTooManyLoginAttempts($request)) { Activity::logFailedLogin($username); - return $this->sendLockoutResponse($request); } @@ -134,24 +88,62 @@ class LoginController extends Controller return $this->sendLoginAttemptExceptionResponse($exception, $request); } - // If the login attempt was unsuccessful we will increment the number of attempts - // to login and redirect the user back to the login form. Of course, when this - // user surpasses their maximum number of attempts they will get locked out. + // On unsuccessful login attempt, Increment login attempts for throttling and log failed login. $this->incrementLoginAttempts($request); - Activity::logFailedLogin($username); - return $this->sendFailedLoginResponse($request); + // Throw validation failure for failed login + throw ValidationException::withMessages([ + $this->username() => [trans('auth.failed')], + ])->redirectTo('/login'); + } + + /** + * Logout user and perform subsequent redirect. + */ + public function logout(Request $request) + { + Auth::guard()->logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + $redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/'; + + return redirect($redirectUri); + } + + /** + * Get the expected username input based upon the current auth method. + */ + protected function username(): string + { + return config('auth.method') === 'standard' ? 'email' : 'username'; + } + + /** + * Get the needed authorization credentials from the request. + */ + protected function credentials(Request $request): array + { + return $request->only('username', 'email', 'password'); + } + + /** + * Send the response after the user was authenticated. + * @return RedirectResponse + */ + protected function sendLoginResponse(Request $request) + { + $request->session()->regenerate(); + $this->clearLoginAttempts($request); + + return redirect()->intended('/'); } /** * Attempt to log the user into the application. - * - * @param \Illuminate\Http\Request $request - * - * @return bool */ - protected function attemptLogin(Request $request) + protected function attemptLogin(Request $request): bool { return $this->loginService->attempt( $this->credentials($request), @@ -160,29 +152,12 @@ class LoginController extends Controller ); } - /** - * The user has been authenticated. - * - * @param \Illuminate\Http\Request $request - * @param mixed $user - * - * @return mixed - */ - protected function authenticated(Request $request, $user) - { - return redirect()->intended($this->redirectPath()); - } /** * Validate the user login request. - * - * @param \Illuminate\Http\Request $request - * - * @throws \Illuminate\Validation\ValidationException - * - * @return void + * @throws ValidationException */ - protected function validateLogin(Request $request) + protected function validateLogin(Request $request): void { $rules = ['password' => ['required', 'string']]; $authMethod = config('auth.method'); @@ -216,22 +191,6 @@ class LoginController extends Controller return redirect('/login'); } - /** - * Get the failed login response instance. - * - * @param \Illuminate\Http\Request $request - * - * @throws \Illuminate\Validation\ValidationException - * - * @return \Symfony\Component\HttpFoundation\Response - */ - protected function sendFailedLoginResponse(Request $request) - { - throw ValidationException::withMessages([ - $this->username() => [trans('auth.failed')], - ])->redirectTo('/login'); - } - /** * Update the intended URL location from their previous URL. * Ignores if not from the current app instance or if from certain @@ -271,20 +230,4 @@ class LoginController extends Controller return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']); } - - /** - * Logout user and perform subsequent redirect. - * - * @param \Illuminate\Http\Request $request - * - * @return mixed - */ - public function logout(Request $request) - { - $this->traitLogout($request); - - $redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/'; - - return redirect($redirectUri); - } } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index d74944800..262ca540e 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -5,40 +5,20 @@ namespace BookStack\Http\Controllers\Auth; use BookStack\Auth\Access\LoginService; use BookStack\Auth\Access\RegistrationService; use BookStack\Auth\Access\SocialAuthService; -use BookStack\Auth\User; use BookStack\Exceptions\StoppedAuthenticationException; use BookStack\Exceptions\UserRegistrationException; use BookStack\Http\Controllers\Controller; -use Illuminate\Foundation\Auth\RegistersUsers; +use Illuminate\Contracts\Validation\Validator as ValidatorContract; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rules\Password; class RegisterController extends Controller { - /* - |-------------------------------------------------------------------------- - | Register Controller - |-------------------------------------------------------------------------- - | - | This controller handles the registration of new users as well as their - | validation and creation. By default this controller uses a trait to - | provide this functionality without requiring any additional code. - | - */ - use RegistersUsers; - protected SocialAuthService $socialAuthService; protected RegistrationService $registrationService; protected LoginService $loginService; - /** - * Where to redirect users after login / registration. - */ - protected string $redirectTo = '/'; - protected string $redirectPath = '/'; - /** * Create a new controller instance. */ @@ -53,23 +33,6 @@ class RegisterController extends Controller $this->socialAuthService = $socialAuthService; $this->registrationService = $registrationService; $this->loginService = $loginService; - - $this->redirectTo = url('/'); - $this->redirectPath = url('/'); - } - - /** - * Get a validator for an incoming registration request. - * - * @return \Illuminate\Contracts\Validation\Validator - */ - protected function validator(array $data) - { - return Validator::make($data, [ - 'name' => ['required', 'min:2', 'max:100'], - 'email' => ['required', 'email', 'max:255', 'unique:users'], - 'password' => ['required', Password::default()], - ]); } /** @@ -112,22 +75,18 @@ class RegisterController extends Controller $this->showSuccessNotification(trans('auth.register_success')); - return redirect($this->redirectPath()); + return redirect('/'); } /** - * Create a new user instance after a valid registration. - * - * @param array $data - * - * @return User + * Get a validator for an incoming registration request. */ - protected function create(array $data) + protected function validator(array $data): ValidatorContract { - return User::create([ - 'name' => $data['name'], - 'email' => $data['email'], - 'password' => Hash::make($data['password']), + return Validator::make($data, [ + 'name' => ['required', 'min:2', 'max:100'], + 'email' => ['required', 'email', 'max:255', 'unique:users'], + 'password' => ['required', Password::default()], ]); } } diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index 9df010736..a9914928e 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -3,65 +3,87 @@ namespace BookStack\Http\Controllers\Auth; use BookStack\Actions\ActivityType; +use BookStack\Auth\Access\LoginService; +use BookStack\Auth\User; use BookStack\Http\Controllers\Controller; -use Illuminate\Foundation\Auth\ResetsPasswords; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Password; +use Illuminate\Support\Str; +use Illuminate\Validation\Rules\Password as PasswordRule; class ResetPasswordController extends Controller { - /* - |-------------------------------------------------------------------------- - | Password Reset Controller - |-------------------------------------------------------------------------- - | - | This controller is responsible for handling password reset requests - | and uses a simple trait to include this behavior. You're free to - | explore this trait and override any methods you wish to tweak. - | - */ - use ResetsPasswords; + protected LoginService $loginService; - protected $redirectTo = '/'; - - /** - * Create a new controller instance. - * - * @return void - */ - public function __construct() + public function __construct(LoginService $loginService) { $this->middleware('guest'); $this->middleware('guard:standard'); + + $this->loginService = $loginService; + } + + /** + * Display the password reset view for the given token. + * If no token is present, display the link request form. + */ + public function showResetForm(Request $request) + { + $token = $request->route()->parameter('token'); + + return view('auth.passwords.reset')->with( + ['token' => $token, 'email' => $request->email] + ); + } + + /** + * Reset the given user's password. + */ + public function reset(Request $request) + { + $request->validate([ + 'token' => 'required', + 'email' => 'required|email', + 'password' => ['required', 'confirmed', PasswordRule::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $credentials = $request->only('email', 'password', 'password_confirmation', 'token'); + $response = Password::broker()->reset($credentials, function (User $user, string $password) { + $user->password = Hash::make($password); + $user->setRememberToken(Str::random(60)); + $user->save(); + + $this->loginService->login($user, auth()->getDefaultDriver()); + }); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + return $response === Password::PASSWORD_RESET + ? $this->sendResetResponse() + : $this->sendResetFailedResponse($request, $response); } /** * Get the response for a successful password reset. - * - * @param Request $request - * @param string $response - * - * @return \Illuminate\Http\Response */ - protected function sendResetResponse(Request $request, $response) + protected function sendResetResponse(): RedirectResponse { - $message = trans('auth.reset_password_success'); - $this->showSuccessNotification($message); + $this->showSuccessNotification(trans('auth.reset_password_success')); $this->logActivity(ActivityType::AUTH_PASSWORD_RESET_UPDATE, user()); - return redirect($this->redirectPath()) - ->with('status', trans($response)); + return redirect('/'); } /** * Get the response for a failed password reset. - * - * @param \Illuminate\Http\Request $request - * @param string $response - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse */ - protected function sendResetFailedResponse(Request $request, $response) + protected function sendResetFailedResponse(Request $request, string $response): RedirectResponse { // We show invalid users as invalid tokens as to not leak what // users may exist in the system. diff --git a/app/Http/Controllers/Auth/Saml2Controller.php b/app/Http/Controllers/Auth/Saml2Controller.php index b84483961..b3f8e7601 100644 --- a/app/Http/Controllers/Auth/Saml2Controller.php +++ b/app/Http/Controllers/Auth/Saml2Controller.php @@ -9,7 +9,7 @@ use Illuminate\Support\Str; class Saml2Controller extends Controller { - protected $samlService; + protected Saml2Service $samlService; /** * Saml2Controller constructor. diff --git a/app/Http/Controllers/Auth/SocialController.php b/app/Http/Controllers/Auth/SocialController.php index cf7ae9041..9ba4028ec 100644 --- a/app/Http/Controllers/Auth/SocialController.php +++ b/app/Http/Controllers/Auth/SocialController.php @@ -28,7 +28,7 @@ class SocialController extends Controller RegistrationService $registrationService, LoginService $loginService ) { - $this->middleware('guest')->only(['getRegister', 'postRegister']); + $this->middleware('guest')->only(['register']); $this->socialAuthService = $socialAuthService; $this->registrationService = $registrationService; $this->loginService = $loginService; diff --git a/app/Http/Controllers/Auth/ThrottlesLogins.php b/app/Http/Controllers/Auth/ThrottlesLogins.php new file mode 100644 index 000000000..7578ba898 --- /dev/null +++ b/app/Http/Controllers/Auth/ThrottlesLogins.php @@ -0,0 +1,92 @@ +limiter()->tooManyAttempts( + $this->throttleKey($request), + $this->maxAttempts() + ); + } + + /** + * Increment the login attempts for the user. + */ + protected function incrementLoginAttempts(Request $request): void + { + $this->limiter()->hit( + $this->throttleKey($request), + $this->decayMinutes() * 60 + ); + } + + /** + * Redirect the user after determining they are locked out. + * @throws ValidationException + */ + protected function sendLockoutResponse(Request $request): \Symfony\Component\HttpFoundation\Response + { + $seconds = $this->limiter()->availableIn( + $this->throttleKey($request) + ); + + throw ValidationException::withMessages([ + $this->username() => [trans('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ])], + ])->status(Response::HTTP_TOO_MANY_REQUESTS); + } + + /** + * Clear the login locks for the given user credentials. + */ + protected function clearLoginAttempts(Request $request): void + { + $this->limiter()->clear($this->throttleKey($request)); + } + + /** + * Get the throttle key for the given request. + */ + protected function throttleKey(Request $request): string + { + return Str::transliterate(Str::lower($request->input($this->username())) . '|' . $request->ip()); + } + + /** + * Get the rate limiter instance. + */ + protected function limiter(): RateLimiter + { + return app(RateLimiter::class); + } + + /** + * Get the maximum number of attempts to allow. + */ + public function maxAttempts(): int + { + return 5; + } + + /** + * Get the number of minutes to throttle for. + */ + public function decayMinutes(): int + { + return 1; + } +} diff --git a/app/Http/Controllers/Auth/UserInviteController.php b/app/Http/Controllers/Auth/UserInviteController.php index 213959abd..5b3bba6ff 100644 --- a/app/Http/Controllers/Auth/UserInviteController.php +++ b/app/Http/Controllers/Auth/UserInviteController.php @@ -11,6 +11,7 @@ use Exception; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Routing\Redirector; +use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rules\Password; class UserInviteController extends Controller @@ -66,7 +67,7 @@ class UserInviteController extends Controller } $user = $this->userRepo->getById($userId); - $user->password = bcrypt($request->get('password')); + $user->password = Hash::make($request->get('password')); $user->email_confirmed = true; $user->save(); diff --git a/composer.json b/composer.json index 64630833d..3306d3da5 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,6 @@ "laravel/framework": "^8.68", "laravel/socialite": "^5.2", "laravel/tinker": "^2.6", - "laravel/ui": "^3.3", "league/commonmark": "^1.6", "league/flysystem-aws-s3-v3": "^1.0.29", "league/html-to-markdown": "^5.0.0", diff --git a/composer.lock b/composer.lock index b807fd577..cf7b8f72f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1d3bd88b99d07b5410ee4b245bece28e", + "content-hash": "01795571047babf7ee6372b7f98843af", "packages": [ { "name": "aws/aws-crt-php", @@ -2160,67 +2160,6 @@ }, "time": "2022-03-23T12:38:24+00:00" }, - { - "name": "laravel/ui", - "version": "v3.4.6", - "source": { - "type": "git", - "url": "https://github.com/laravel/ui.git", - "reference": "65ec5c03f7fee2c8ecae785795b829a15be48c2c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/ui/zipball/65ec5c03f7fee2c8ecae785795b829a15be48c2c", - "reference": "65ec5c03f7fee2c8ecae785795b829a15be48c2c", - "shasum": "" - }, - "require": { - "illuminate/console": "^8.42|^9.0", - "illuminate/filesystem": "^8.42|^9.0", - "illuminate/support": "^8.82|^9.0", - "illuminate/validation": "^8.42|^9.0", - "php": "^7.3|^8.0" - }, - "require-dev": { - "orchestra/testbench": "^6.23|^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - }, - "laravel": { - "providers": [ - "Laravel\\Ui\\UiServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Laravel\\Ui\\": "src/", - "Illuminate\\Foundation\\Auth\\": "auth-backend/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "Laravel UI utilities and presets.", - "keywords": [ - "laravel", - "ui" - ], - "support": { - "source": "https://github.com/laravel/ui/tree/v3.4.6" - }, - "time": "2022-05-20T13:38:08+00:00" - }, { "name": "league/commonmark", "version": "1.6.7", From 90b4257889a5f9a63ee5d9934e90557e67ebca56 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 22 Sep 2022 17:15:15 +0100 Subject: [PATCH 03/24] Split out registration and pw-reset tests methods --- tests/Auth/AuthTest.php | 260 ------------------------------- tests/Auth/RegistrationTest.php | 177 +++++++++++++++++++++ tests/Auth/ResetPasswordTest.php | 101 ++++++++++++ 3 files changed, 278 insertions(+), 260 deletions(-) create mode 100644 tests/Auth/RegistrationTest.php create mode 100644 tests/Auth/ResetPasswordTest.php diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php index 106b71875..f0b473472 100644 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@ -3,13 +3,7 @@ namespace Tests\Auth; use BookStack\Auth\Access\Mfa\MfaSession; -use BookStack\Auth\Role; -use BookStack\Auth\User; use BookStack\Entities\Models\Page; -use BookStack\Notifications\ConfirmEmail; -use BookStack\Notifications\ResetPassword; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Notification; use Illuminate\Testing\TestResponse; use Tests\TestCase; @@ -33,68 +27,6 @@ class AuthTest extends TestCase ->assertSee('Log in'); } - public function test_registration_showing() - { - // Ensure registration form is showing - $this->setSettings(['registration-enabled' => 'true']); - $resp = $this->get('/login'); - $this->withHtml($resp)->assertElementContains('a[href="' . url('/register') . '"]', 'Sign up'); - } - - public function test_normal_registration() - { - // Set settings and get user instance - /** @var Role $registrationRole */ - $registrationRole = Role::query()->first(); - $this->setSettings(['registration-enabled' => 'true', 'registration-role' => $registrationRole->id]); - /** @var User $user */ - $user = User::factory()->make(); - - // Test form and ensure user is created - $resp = $this->get('/register') - ->assertSee('Sign Up'); - $this->withHtml($resp)->assertElementContains('form[action="' . url('/register') . '"]', 'Create Account'); - - $resp = $this->post('/register', $user->only('password', 'name', 'email')); - $resp->assertRedirect('/'); - - $resp = $this->get('/'); - $resp->assertOk(); - $resp->assertSee($user->name); - - $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]); - - $user = User::query()->where('email', '=', $user->email)->first(); - $this->assertEquals(1, $user->roles()->count()); - $this->assertEquals($registrationRole->id, $user->roles()->first()->id); - } - - public function test_empty_registration_redirects_back_with_errors() - { - // Set settings and get user instance - $this->setSettings(['registration-enabled' => 'true']); - - // Test form and ensure user is created - $this->get('/register'); - $this->post('/register', [])->assertRedirect('/register'); - $this->get('/register')->assertSee('The name field is required'); - } - - public function test_registration_validation() - { - $this->setSettings(['registration-enabled' => 'true']); - - $this->get('/register'); - $resp = $this->followingRedirects()->post('/register', [ - 'name' => '1', - 'email' => '1', - 'password' => '1', - ]); - $resp->assertSee('The name must be at least 2 characters.'); - $resp->assertSee('The email must be a valid email address.'); - $resp->assertSee('The password must be at least 8 characters.'); - } - public function test_sign_up_link_on_login() { $this->get('/login')->assertDontSee('Sign up'); @@ -104,108 +36,6 @@ class AuthTest extends TestCase $this->get('/login')->assertSee('Sign up'); } - public function test_confirmed_registration() - { - // Fake notifications - Notification::fake(); - - // Set settings and get user instance - $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']); - $user = User::factory()->make(); - - // Go through registration process - $resp = $this->post('/register', $user->only('name', 'email', 'password')); - $resp->assertRedirect('/register/confirm'); - $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); - - // Ensure notification sent - /** @var User $dbUser */ - $dbUser = User::query()->where('email', '=', $user->email)->first(); - Notification::assertSentTo($dbUser, ConfirmEmail::class); - - // Test access and resend confirmation email - $resp = $this->login($user->email, $user->password); - $resp->assertRedirect('/register/confirm/awaiting'); - - $resp = $this->get('/register/confirm/awaiting'); - $this->withHtml($resp)->assertElementContains('form[action="' . url('/register/confirm/resend') . '"]', 'Resend'); - - $this->get('/books')->assertRedirect('/login'); - $this->post('/register/confirm/resend', $user->only('email')); - - // Get confirmation and confirm notification matches - $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first(); - Notification::assertSentTo($dbUser, ConfirmEmail::class, function ($notification, $channels) use ($emailConfirmation) { - return $notification->token === $emailConfirmation->token; - }); - - // Check confirmation email confirmation activation. - $this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/login'); - $this->get('/login')->assertSee('Your email has been confirmed! You should now be able to login using this email address.'); - $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]); - $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]); - } - - public function test_restricted_registration() - { - $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']); - $user = User::factory()->make(); - - // Go through registration process - $this->post('/register', $user->only('name', 'email', 'password')) - ->assertRedirect('/register'); - $resp = $this->get('/register'); - $resp->assertSee('That email domain does not have access to this application'); - $this->assertDatabaseMissing('users', $user->only('email')); - - $user->email = 'barry@example.com'; - - $this->post('/register', $user->only('name', 'email', 'password')) - ->assertRedirect('/register/confirm'); - $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); - - $this->assertNull(auth()->user()); - - $this->get('/')->assertRedirect('/login'); - $resp = $this->followingRedirects()->post('/login', $user->only('email', 'password')); - $resp->assertSee('Email Address Not Confirmed'); - $this->assertNull(auth()->user()); - } - - public function test_restricted_registration_with_confirmation_disabled() - { - $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']); - $user = User::factory()->make(); - - // Go through registration process - $this->post('/register', $user->only('name', 'email', 'password')) - ->assertRedirect('/register'); - $this->assertDatabaseMissing('users', $user->only('email')); - $this->get('/register')->assertSee('That email domain does not have access to this application'); - - $user->email = 'barry@example.com'; - - $this->post('/register', $user->only('name', 'email', 'password')) - ->assertRedirect('/register/confirm'); - $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); - - $this->assertNull(auth()->user()); - - $this->get('/')->assertRedirect('/login'); - $resp = $this->post('/login', $user->only('email', 'password')); - $resp->assertRedirect('/register/confirm/awaiting'); - $this->get('/register/confirm/awaiting')->assertSee('Email Address Not Confirmed'); - $this->assertNull(auth()->user()); - } - - public function test_registration_role_unset_by_default() - { - $this->assertFalse(setting('registration-role')); - - $resp = $this->asAdmin()->get('/settings/registration'); - $this->withHtml($resp)->assertElementContains('select[name="setting-registration-role"] option[value="0"][selected]', '-- None --'); - } - public function test_logout() { $this->asAdmin()->get('/')->assertOk(); @@ -225,96 +55,6 @@ class AuthTest extends TestCase $this->assertFalse($mfaSession->isVerifiedForUser($user)); } - public function test_reset_password_flow() - { - Notification::fake(); - - $resp = $this->get('/login'); - $this->withHtml($resp)->assertElementContains('a[href="' . url('/password/email') . '"]', 'Forgot Password?'); - - $resp = $this->get('/password/email'); - $this->withHtml($resp)->assertElementContains('form[action="' . url('/password/email') . '"]', 'Send Reset Link'); - - $resp = $this->post('/password/email', [ - 'email' => 'admin@admin.com', - ]); - $resp->assertRedirect('/password/email'); - - $resp = $this->get('/password/email'); - $resp->assertSee('A password reset link will be sent to admin@admin.com if that email address is found in the system.'); - - $this->assertDatabaseHas('password_resets', [ - 'email' => 'admin@admin.com', - ]); - - /** @var User $user */ - $user = User::query()->where('email', '=', 'admin@admin.com')->first(); - - Notification::assertSentTo($user, ResetPassword::class); - $n = Notification::sent($user, ResetPassword::class); - - $this->get('/password/reset/' . $n->first()->token) - ->assertOk() - ->assertSee('Reset Password'); - - $resp = $this->post('/password/reset', [ - 'email' => 'admin@admin.com', - 'password' => 'randompass', - 'password_confirmation' => 'randompass', - 'token' => $n->first()->token, - ]); - $resp->assertRedirect('/'); - - $this->get('/')->assertSee('Your password has been successfully reset'); - } - - public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery() - { - $this->get('/password/email'); - $resp = $this->followingRedirects()->post('/password/email', [ - 'email' => 'barry@admin.com', - ]); - $resp->assertSee('A password reset link will be sent to barry@admin.com if that email address is found in the system.'); - $resp->assertDontSee('We can\'t find a user'); - - $this->get('/password/reset/arandometokenvalue')->assertSee('Reset Password'); - $resp = $this->post('/password/reset', [ - 'email' => 'barry@admin.com', - 'password' => 'randompass', - 'password_confirmation' => 'randompass', - 'token' => 'arandometokenvalue', - ]); - $resp->assertRedirect('/password/reset/arandometokenvalue'); - - $this->get('/password/reset/arandometokenvalue') - ->assertDontSee('We can\'t find a user') - ->assertSee('The password reset token is invalid for this email address.'); - } - - public function test_reset_password_page_shows_sign_links() - { - $this->setSettings(['registration-enabled' => 'true']); - $resp = $this->get('/password/email'); - $this->withHtml($resp)->assertElementContains('a', 'Log in') - ->assertElementContains('a', 'Sign up'); - } - - public function test_reset_password_request_is_throttled() - { - $editor = $this->getEditor(); - Notification::fake(); - $this->get('/password/email'); - $this->followingRedirects()->post('/password/email', [ - 'email' => $editor->email, - ]); - - $resp = $this->followingRedirects()->post('/password/email', [ - 'email' => $editor->email, - ]); - Notification::assertTimesSent(1, ResetPassword::class); - $resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.'); - } - public function test_login_redirects_to_initially_requested_url_correctly() { config()->set('app.url', 'http://localhost'); diff --git a/tests/Auth/RegistrationTest.php b/tests/Auth/RegistrationTest.php new file mode 100644 index 000000000..45d265b72 --- /dev/null +++ b/tests/Auth/RegistrationTest.php @@ -0,0 +1,177 @@ +setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']); + $user = User::factory()->make(); + + // Go through registration process + $resp = $this->post('/register', $user->only('name', 'email', 'password')); + $resp->assertRedirect('/register/confirm'); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); + + // Ensure notification sent + /** @var User $dbUser */ + $dbUser = User::query()->where('email', '=', $user->email)->first(); + Notification::assertSentTo($dbUser, ConfirmEmail::class); + + // Test access and resend confirmation email + $resp = $this->post('/login', ['email' => $user->email, 'password' => $user->password]); + $resp->assertRedirect('/register/confirm/awaiting'); + + $resp = $this->get('/register/confirm/awaiting'); + $this->withHtml($resp)->assertElementContains('form[action="' . url('/register/confirm/resend') . '"]', 'Resend'); + + $this->get('/books')->assertRedirect('/login'); + $this->post('/register/confirm/resend', $user->only('email')); + + // Get confirmation and confirm notification matches + $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first(); + Notification::assertSentTo($dbUser, ConfirmEmail::class, function ($notification, $channels) use ($emailConfirmation) { + return $notification->token === $emailConfirmation->token; + }); + + // Check confirmation email confirmation activation. + $this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/login'); + $this->get('/login')->assertSee('Your email has been confirmed! You should now be able to login using this email address.'); + $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]); + $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]); + } + + public function test_restricted_registration() + { + $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']); + $user = User::factory()->make(); + + // Go through registration process + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register'); + $resp = $this->get('/register'); + $resp->assertSee('That email domain does not have access to this application'); + $this->assertDatabaseMissing('users', $user->only('email')); + + $user->email = 'barry@example.com'; + + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register/confirm'); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); + + $this->assertNull(auth()->user()); + + $this->get('/')->assertRedirect('/login'); + $resp = $this->followingRedirects()->post('/login', $user->only('email', 'password')); + $resp->assertSee('Email Address Not Confirmed'); + $this->assertNull(auth()->user()); + } + + public function test_restricted_registration_with_confirmation_disabled() + { + $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']); + $user = User::factory()->make(); + + // Go through registration process + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register'); + $this->assertDatabaseMissing('users', $user->only('email')); + $this->get('/register')->assertSee('That email domain does not have access to this application'); + + $user->email = 'barry@example.com'; + + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register/confirm'); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); + + $this->assertNull(auth()->user()); + + $this->get('/')->assertRedirect('/login'); + $resp = $this->post('/login', $user->only('email', 'password')); + $resp->assertRedirect('/register/confirm/awaiting'); + $this->get('/register/confirm/awaiting')->assertSee('Email Address Not Confirmed'); + $this->assertNull(auth()->user()); + } + + public function test_registration_role_unset_by_default() + { + $this->assertFalse(setting('registration-role')); + + $resp = $this->asAdmin()->get('/settings/registration'); + $this->withHtml($resp)->assertElementContains('select[name="setting-registration-role"] option[value="0"][selected]', '-- None --'); + } + + public function test_registration_showing() + { + // Ensure registration form is showing + $this->setSettings(['registration-enabled' => 'true']); + $resp = $this->get('/login'); + $this->withHtml($resp)->assertElementContains('a[href="' . url('/register') . '"]', 'Sign up'); + } + + public function test_normal_registration() + { + // Set settings and get user instance + /** @var Role $registrationRole */ + $registrationRole = Role::query()->first(); + $this->setSettings(['registration-enabled' => 'true', 'registration-role' => $registrationRole->id]); + /** @var User $user */ + $user = User::factory()->make(); + + // Test form and ensure user is created + $resp = $this->get('/register') + ->assertSee('Sign Up'); + $this->withHtml($resp)->assertElementContains('form[action="' . url('/register') . '"]', 'Create Account'); + + $resp = $this->post('/register', $user->only('password', 'name', 'email')); + $resp->assertRedirect('/'); + + $resp = $this->get('/'); + $resp->assertOk(); + $resp->assertSee($user->name); + + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]); + + $user = User::query()->where('email', '=', $user->email)->first(); + $this->assertEquals(1, $user->roles()->count()); + $this->assertEquals($registrationRole->id, $user->roles()->first()->id); + } + + public function test_empty_registration_redirects_back_with_errors() + { + // Set settings and get user instance + $this->setSettings(['registration-enabled' => 'true']); + + // Test form and ensure user is created + $this->get('/register'); + $this->post('/register', [])->assertRedirect('/register'); + $this->get('/register')->assertSee('The name field is required'); + } + + public function test_registration_validation() + { + $this->setSettings(['registration-enabled' => 'true']); + + $this->get('/register'); + $resp = $this->followingRedirects()->post('/register', [ + 'name' => '1', + 'email' => '1', + 'password' => '1', + ]); + $resp->assertSee('The name must be at least 2 characters.'); + $resp->assertSee('The email must be a valid email address.'); + $resp->assertSee('The password must be at least 8 characters.'); + } +} diff --git a/tests/Auth/ResetPasswordTest.php b/tests/Auth/ResetPasswordTest.php new file mode 100644 index 000000000..1ab579b26 --- /dev/null +++ b/tests/Auth/ResetPasswordTest.php @@ -0,0 +1,101 @@ +get('/login'); + $this->withHtml($resp)->assertElementContains('a[href="' . url('/password/email') . '"]', 'Forgot Password?'); + + $resp = $this->get('/password/email'); + $this->withHtml($resp)->assertElementContains('form[action="' . url('/password/email') . '"]', 'Send Reset Link'); + + $resp = $this->post('/password/email', [ + 'email' => 'admin@admin.com', + ]); + $resp->assertRedirect('/password/email'); + + $resp = $this->get('/password/email'); + $resp->assertSee('A password reset link will be sent to admin@admin.com if that email address is found in the system.'); + + $this->assertDatabaseHas('password_resets', [ + 'email' => 'admin@admin.com', + ]); + + /** @var User $user */ + $user = User::query()->where('email', '=', 'admin@admin.com')->first(); + + Notification::assertSentTo($user, ResetPassword::class); + $n = Notification::sent($user, ResetPassword::class); + + $this->get('/password/reset/' . $n->first()->token) + ->assertOk() + ->assertSee('Reset Password'); + + $resp = $this->post('/password/reset', [ + 'email' => 'admin@admin.com', + 'password' => 'randompass', + 'password_confirmation' => 'randompass', + 'token' => $n->first()->token, + ]); + $resp->assertRedirect('/'); + + $this->get('/')->assertSee('Your password has been successfully reset'); + } + + public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery() + { + $this->get('/password/email'); + $resp = $this->followingRedirects()->post('/password/email', [ + 'email' => 'barry@admin.com', + ]); + $resp->assertSee('A password reset link will be sent to barry@admin.com if that email address is found in the system.'); + $resp->assertDontSee('We can\'t find a user'); + + $this->get('/password/reset/arandometokenvalue')->assertSee('Reset Password'); + $resp = $this->post('/password/reset', [ + 'email' => 'barry@admin.com', + 'password' => 'randompass', + 'password_confirmation' => 'randompass', + 'token' => 'arandometokenvalue', + ]); + $resp->assertRedirect('/password/reset/arandometokenvalue'); + + $this->get('/password/reset/arandometokenvalue') + ->assertDontSee('We can\'t find a user') + ->assertSee('The password reset token is invalid for this email address.'); + } + + public function test_reset_password_page_shows_sign_links() + { + $this->setSettings(['registration-enabled' => 'true']); + $resp = $this->get('/password/email'); + $this->withHtml($resp)->assertElementContains('a', 'Log in') + ->assertElementContains('a', 'Sign up'); + } + + public function test_reset_password_request_is_throttled() + { + $editor = $this->getEditor(); + Notification::fake(); + $this->get('/password/email'); + $this->followingRedirects()->post('/password/email', [ + 'email' => $editor->email, + ]); + + $resp = $this->followingRedirects()->post('/password/email', [ + 'email' => $editor->email, + ]); + Notification::assertTimesSent(1, ResetPassword::class); + $resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.'); + } +} From 5c5ea642285751c322d9cf384950a5c7d6cd851f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 22 Sep 2022 17:29:38 +0100 Subject: [PATCH 04/24] Added login throttling test, updated reset-pw test method names --- tests/Auth/AuthTest.php | 13 +++++++++++++ tests/Auth/ResetPasswordTest.php | 8 ++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php index f0b473472..849469766 100644 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@ -133,6 +133,19 @@ class AuthTest extends TestCase $this->assertFalse(auth()->check()); } + public function test_login_attempts_are_rate_limited() + { + for ($i = 0; $i < 5; $i++) { + $resp = $this->login('bennynotexisting@example.com', 'pw123'); + } + $resp = $this->followRedirects($resp); + $resp->assertSee('These credentials do not match our records.'); + + // Check the fifth attempt provides a lockout response + $resp = $this->followRedirects($this->login('bennynotexisting@example.com', 'pw123')); + $resp->assertSee('Too many login attempts. Please try again in'); + } + /** * Perform a login. */ diff --git a/tests/Auth/ResetPasswordTest.php b/tests/Auth/ResetPasswordTest.php index 1ab579b26..7b2d2e72b 100644 --- a/tests/Auth/ResetPasswordTest.php +++ b/tests/Auth/ResetPasswordTest.php @@ -9,7 +9,7 @@ use Tests\TestCase; class ResetPasswordTest extends TestCase { - public function test_reset_password_flow() + public function test_reset_flow() { Notification::fake(); @@ -52,7 +52,7 @@ class ResetPasswordTest extends TestCase $this->get('/')->assertSee('Your password has been successfully reset'); } - public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery() + public function test_reset_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery() { $this->get('/password/email'); $resp = $this->followingRedirects()->post('/password/email', [ @@ -75,7 +75,7 @@ class ResetPasswordTest extends TestCase ->assertSee('The password reset token is invalid for this email address.'); } - public function test_reset_password_page_shows_sign_links() + public function test_reset_page_shows_sign_links() { $this->setSettings(['registration-enabled' => 'true']); $resp = $this->get('/password/email'); @@ -83,7 +83,7 @@ class ResetPasswordTest extends TestCase ->assertElementContains('a', 'Sign up'); } - public function test_reset_password_request_is_throttled() + public function test_reset_request_is_throttled() { $editor = $this->getEditor(); Notification::fake(); From e18033ec1ae181a8977d23d14090d0706f3cc05b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 26 Sep 2022 21:25:32 +0100 Subject: [PATCH 05/24] Added initial support for parallel testing --- app/Providers/AppServiceProvider.php | 7 +++++++ composer.json | 1 + resources/js/wysiwyg/config.js | 1 + tests/TestCase.php | 8 ++++++++ tests/ThemeTest.php | 12 ++++++------ 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 3c1212e32..02c545db2 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -17,7 +17,9 @@ use GuzzleHttp\Client; use Illuminate\Contracts\Cache\Repository; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Pagination\Paginator; +use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Facades\ParallelTesting; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\View; @@ -64,6 +66,11 @@ class AppServiceProvider extends ServiceProvider // Set paginator to use bootstrap-style pagination Paginator::useBootstrap(); + + // Setup database upon parallel testing database creation + ParallelTesting::setUpTestDatabase(function ($database, $token) { + Artisan::call('db:seed --class=DummyContentSeeder'); + }); } /** diff --git a/composer.json b/composer.json index 64630833d..44bbf2b99 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,7 @@ "ssddanbrown/htmldiff": "^1.0.2" }, "require-dev": { + "brianium/paratest": "^6.6", "fakerphp/faker": "^1.16", "itsgoingd/clockwork": "^5.1", "mockery/mockery": "^1.4", diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg/config.js index 52c52592c..d2f813cfb 100644 --- a/resources/js/wysiwyg/config.js +++ b/resources/js/wysiwyg/config.js @@ -252,6 +252,7 @@ export function build(options) { document_base_url: window.baseUrl('/'), end_container_on_empty_block: true, remove_trailing_brs: false, + keep_styles: false, statusbar: false, menubar: false, paste_data_images: false, diff --git a/tests/TestCase.php b/tests/TestCase.php index f17d27a1a..0926b0dcc 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -26,6 +26,7 @@ use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Http\JsonResponse; use Illuminate\Support\Env; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Testing\Assert as PHPUnit; use Monolog\Handler\TestHandler; @@ -299,6 +300,8 @@ abstract class TestCase extends BaseTestCase /** * Run a set test with the given env variable. * Remembers the original and resets the value after test. + * Database config is juggled so the value can be restored when + * parallel testing are used, where multiple databases exist. */ protected function runWithEnv(string $name, $value, callable $callback) { @@ -311,7 +314,12 @@ abstract class TestCase extends BaseTestCase $_SERVER[$name] = $value; } + $database = config('database.connections.mysql_testing.database'); $this->refreshApplication(); + + DB::purge(); + config()->set('database.connections.mysql_testing.database', $database); + $callback(); if (is_null($originalVal)) { diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index e83758a95..ac4b35de2 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -322,8 +322,8 @@ class ThemeTest extends TestCase public function test_export_body_start_and_end_template_files_can_be_used() { - $bodyStartStr = 'barry-fought-against-the-panther'; - $bodyEndStr = 'barry-lost-his-fight-with-grace'; + $bodyStartStr = 'garry-fought-against-the-panther'; + $bodyEndStr = 'garry-lost-his-fight-with-grace'; /** @var Page $page */ $page = Page::query()->first(); @@ -342,18 +342,18 @@ class ThemeTest extends TestCase protected function usingThemeFolder(callable $callback) { // Create a folder and configure a theme - $themeFolderName = 'testing_theme_' . rtrim(base64_encode(time()), '='); + $themeFolderName = 'testing_theme_' . str_shuffle(rtrim(base64_encode(time()), '=')); config()->set('view.theme', $themeFolderName); $themeFolderPath = theme_path(''); + + // Create theme folder and clean it up on application tear-down File::makeDirectory($themeFolderPath); + $this->beforeApplicationDestroyed(fn() => File::deleteDirectory($themeFolderPath)); // Run provided callback with theme env option set $this->runWithEnv('APP_THEME', $themeFolderName, function () use ($callback, $themeFolderName) { call_user_func($callback, $themeFolderName); }); - - // Cleanup the custom theme folder we created - File::deleteDirectory($themeFolderPath); } } From f21669c0c966f3dadeac2024a382b8a7cd831a8a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 27 Sep 2022 01:27:51 +0100 Subject: [PATCH 06/24] Cleaned testing service provider usage Moved testing content out of AppServiceProvider, to a testing-specific service provider. Updated docs and added composer commands to support parallel testing. Also reverted unintentional change to wysiwyg/config.js. --- app/Providers/AppServiceProvider.php | 7 ------- composer.json | 2 ++ readme.md | 9 ++------- resources/js/wysiwyg/config.js | 1 - tests/TestCase.php | 16 ++++++++++++++++ tests/TestServiceProvider.php | 26 ++++++++++++++++++++++++++ 6 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 tests/TestServiceProvider.php diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 02c545db2..3c1212e32 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -17,9 +17,7 @@ use GuzzleHttp\Client; use Illuminate\Contracts\Cache\Repository; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Pagination\Paginator; -use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Blade; -use Illuminate\Support\Facades\ParallelTesting; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\View; @@ -66,11 +64,6 @@ class AppServiceProvider extends ServiceProvider // Set paginator to use bootstrap-style pagination Paginator::useBootstrap(); - - // Setup database upon parallel testing database creation - ParallelTesting::setUpTestDatabase(function ($database, $token) { - Artisan::call('db:seed --class=DummyContentSeeder'); - }); } /** diff --git a/composer.json b/composer.json index 44bbf2b99..81896f8f8 100644 --- a/composer.json +++ b/composer.json @@ -74,6 +74,8 @@ "format": "phpcbf", "lint": "phpcs", "test": "phpunit", + "t": "@php artisan test --parallel", + "t-reset": "@php artisan test --recreate-databases", "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" diff --git a/readme.md b/readme.md index d0ae1b4f6..16992341d 100644 --- a/readme.md +++ b/readme.md @@ -108,14 +108,9 @@ npm run dev BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the database name, user name and password all defined as `bookstack-test`. You will have to create that database and that set of credentials before testing. -The testing database will also need migrating and seeding beforehand. This can be done with the following commands: +The testing database will also need migrating and seeding beforehand. This can be done by running `composer refresh-test-database`. -``` bash -php artisan migrate --database=mysql_testing -php artisan db:seed --class=DummyContentSeeder --database=mysql_testing -``` - -Once done you can run `composer test` in the application root directory to run all tests. +Once done you can run `composer test` in the application root directory to run all tests. Tests can be ran in parallel by running them via `composer t`. This will use Laravel's built-in parallel testing functionality, and attempt to create and seed a database instance for each testing thread. If required these parallel testing instances can be reset, before testing again, by running `composer t-reset`. ### 📜 Code Standards diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg/config.js index d2f813cfb..52c52592c 100644 --- a/resources/js/wysiwyg/config.js +++ b/resources/js/wysiwyg/config.js @@ -252,7 +252,6 @@ export function build(options) { document_base_url: window.baseUrl('/'), end_container_on_empty_block: true, remove_trailing_brs: false, - keep_styles: false, statusbar: false, menubar: false, paste_data_images: false, diff --git a/tests/TestCase.php b/tests/TestCase.php index 0926b0dcc..594194168 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -22,6 +22,7 @@ use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; +use Illuminate\Contracts\Console\Kernel; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Http\JsonResponse; @@ -48,6 +49,21 @@ abstract class TestCase extends BaseTestCase */ protected string $baseUrl = 'http://localhost'; + /** + * Creates the application. + * + * @return \Illuminate\Foundation\Application + */ + public function createApplication() + { + /** @var \Illuminate\Foundation\Application $app */ + $app = require __DIR__ . '/../bootstrap/app.php'; + $app->register(TestServiceProvider::class); + $app->make(Kernel::class)->bootstrap(); + + return $app; + } + /** * Set the current user context to be an admin. */ diff --git a/tests/TestServiceProvider.php b/tests/TestServiceProvider.php new file mode 100644 index 000000000..9ad48c442 --- /dev/null +++ b/tests/TestServiceProvider.php @@ -0,0 +1,26 @@ + Date: Tue, 27 Sep 2022 02:48:05 +0100 Subject: [PATCH 07/24] Refactored app service providers Removed old pagination provider as url handling now achieved in a better way. Removed unused broadcast service provider. Moved view-based tweaks into specific provider. Reorganised provider config list. --- app/Config/app.php | 19 +++---- app/Providers/AppServiceProvider.php | 55 +++++++------------ app/Providers/AuthServiceProvider.php | 4 +- app/Providers/BroadcastServiceProvider.php | 25 --------- app/Providers/CustomFacadeProvider.php | 36 ------------ app/Providers/EventServiceProvider.php | 2 +- app/Providers/PaginationServiceProvider.php | 35 ------------ app/Providers/ThemeServiceProvider.php | 6 +- ....php => ValidationRuleServiceProvider.php} | 2 +- app/Providers/ViewTweaksServiceProvider.php | 31 +++++++++++ 10 files changed, 65 insertions(+), 150 deletions(-) delete mode 100644 app/Providers/BroadcastServiceProvider.php delete mode 100644 app/Providers/CustomFacadeProvider.php delete mode 100644 app/Providers/PaginationServiceProvider.php rename app/Providers/{CustomValidationServiceProvider.php => ValidationRuleServiceProvider.php} (93%) create mode 100644 app/Providers/ViewTweaksServiceProvider.php diff --git a/app/Config/app.php b/app/Config/app.php index 738aacdbc..98f83fc39 100644 --- a/app/Config/app.php +++ b/app/Config/app.php @@ -114,6 +114,8 @@ return [ Illuminate\Foundation\Providers\FoundationServiceProvider::class, Illuminate\Hashing\HashServiceProvider::class, Illuminate\Mail\MailServiceProvider::class, + Illuminate\Notifications\NotificationServiceProvider::class, + Illuminate\Pagination\PaginationServiceProvider::class, Illuminate\Pipeline\PipelineServiceProvider::class, Illuminate\Queue\QueueServiceProvider::class, Illuminate\Redis\RedisServiceProvider::class, @@ -121,27 +123,22 @@ return [ Illuminate\Session\SessionServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class, Illuminate\View\ViewServiceProvider::class, - Illuminate\Notifications\NotificationServiceProvider::class, - SocialiteProviders\Manager\ServiceProvider::class, // Third party service providers - Intervention\Image\ImageServiceProvider::class, Barryvdh\DomPDF\ServiceProvider::class, Barryvdh\Snappy\ServiceProvider::class, - - // BookStack replacement service providers (Extends Laravel) - BookStack\Providers\PaginationServiceProvider::class, - BookStack\Providers\TranslationServiceProvider::class, + Intervention\Image\ImageServiceProvider::class, + SocialiteProviders\Manager\ServiceProvider::class, // BookStack custom service providers BookStack\Providers\ThemeServiceProvider::class, - BookStack\Providers\AuthServiceProvider::class, BookStack\Providers\AppServiceProvider::class, - BookStack\Providers\BroadcastServiceProvider::class, + BookStack\Providers\AuthServiceProvider::class, BookStack\Providers\EventServiceProvider::class, BookStack\Providers\RouteServiceProvider::class, - BookStack\Providers\CustomFacadeProvider::class, - BookStack\Providers\CustomValidationServiceProvider::class, + BookStack\Providers\TranslationServiceProvider::class, + BookStack\Providers\ValidationRuleServiceProvider::class, + BookStack\Providers\ViewTweaksServiceProvider::class, ], /* diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 3c1212e32..d0841059b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,32 +2,44 @@ namespace BookStack\Providers; -use BookStack\Auth\Access\LoginService; +use BookStack\Actions\ActivityLogger; use BookStack\Auth\Access\SocialAuthService; -use BookStack\Entities\BreadcrumbsViewComposer; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; use BookStack\Exceptions\WhoopsBookStackPrettyHandler; -use BookStack\Settings\Setting; use BookStack\Settings\SettingService; use BookStack\Util\CspService; use GuzzleHttp\Client; -use Illuminate\Contracts\Cache\Repository; use Illuminate\Database\Eloquent\Relations\Relation; -use Illuminate\Pagination\Paginator; -use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\URL; -use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; -use Laravel\Socialite\Contracts\Factory as SocialiteFactory; use Psr\Http\Client\ClientInterface as HttpClientInterface; use Whoops\Handler\HandlerInterface; class AppServiceProvider extends ServiceProvider { + /** + * Custom container bindings to register. + * @var string[] + */ + public $bindings = [ + HandlerInterface::class => WhoopsBookStackPrettyHandler::class, + ]; + + /** + * Custom singleton bindings to register. + * @var string[] + */ + public $singletons = [ + 'activity' => ActivityLogger::class, + SettingService::class => SettingService::class, + SocialAuthService::class => SocialAuthService::class, + CspService::class => CspService::class, + ]; + /** * Bootstrap any application services. * @@ -43,11 +55,6 @@ class AppServiceProvider extends ServiceProvider URL::forceScheme($isHttps ? 'https' : 'http'); } - // Custom blade view directives - Blade::directive('icon', function ($expression) { - return ""; - }); - // Allow longer string lengths after upgrade to utf8mb4 Schema::defaultStringLength(191); @@ -58,12 +65,6 @@ class AppServiceProvider extends ServiceProvider 'chapter' => Chapter::class, 'page' => Page::class, ]); - - // View Composers - View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class); - - // Set paginator to use bootstrap-style pagination - Paginator::useBootstrap(); } /** @@ -73,22 +74,6 @@ class AppServiceProvider extends ServiceProvider */ public function register() { - $this->app->bind(HandlerInterface::class, function ($app) { - return $app->make(WhoopsBookStackPrettyHandler::class); - }); - - $this->app->singleton(SettingService::class, function ($app) { - return new SettingService($app->make(Setting::class), $app->make(Repository::class)); - }); - - $this->app->singleton(SocialAuthService::class, function ($app) { - return new SocialAuthService($app->make(SocialiteFactory::class), $app->make(LoginService::class)); - }); - - $this->app->singleton(CspService::class, function ($app) { - return new CspService(); - }); - $this->app->bind(HttpClientInterface::class, function ($app) { return new Client([ 'timeout' => 3, diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index a4022cc50..5e16179ab 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -24,9 +24,7 @@ class AuthServiceProvider extends ServiceProvider { // Password Configuration // Changes here must be reflected in ApiDocsGenerate@getValidationAsString. - Password::defaults(function () { - return Password::min(8); - }); + Password::defaults(fn () => Password::min(8)); // Custom guards Auth::extend('api-token', function ($app, $name, array $config) { diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php deleted file mode 100644 index 69925e945..000000000 --- a/app/Providers/BroadcastServiceProvider.php +++ /dev/null @@ -1,25 +0,0 @@ -id === (int) $userId; -// }); - } -} diff --git a/app/Providers/CustomFacadeProvider.php b/app/Providers/CustomFacadeProvider.php deleted file mode 100644 index 6ba5632e6..000000000 --- a/app/Providers/CustomFacadeProvider.php +++ /dev/null @@ -1,36 +0,0 @@ -app->singleton('activity', function () { - return $this->app->make(ActivityLogger::class); - }); - - $this->app->singleton('theme', function () { - return $this->app->make(ThemeService::class); - }); - } -} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 659843ce3..0edc7f09c 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -10,7 +10,7 @@ class EventServiceProvider extends ServiceProvider /** * The event listener mappings for the application. * - * @var array + * @var array> */ protected $listen = [ SocialiteWasCalled::class => [ diff --git a/app/Providers/PaginationServiceProvider.php b/app/Providers/PaginationServiceProvider.php deleted file mode 100644 index 416aa5f34..000000000 --- a/app/Providers/PaginationServiceProvider.php +++ /dev/null @@ -1,35 +0,0 @@ -app['view']; - }); - - Paginator::currentPathResolver(function () { - return url($this->app['request']->path()); - }); - - Paginator::currentPageResolver(function ($pageName = 'page') { - $page = $this->app['request']->input($pageName); - - if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) { - return $page; - } - - return 1; - }); - } -} diff --git a/app/Providers/ThemeServiceProvider.php b/app/Providers/ThemeServiceProvider.php index 54c83884a..50c4a5d19 100644 --- a/app/Providers/ThemeServiceProvider.php +++ b/app/Providers/ThemeServiceProvider.php @@ -15,9 +15,8 @@ class ThemeServiceProvider extends ServiceProvider */ public function register() { - $this->app->singleton(ThemeService::class, function ($app) { - return new ThemeService(); - }); + // Register the ThemeService as a singleton + $this->app->singleton(ThemeService::class, fn ($app) => new ThemeService()); } /** @@ -27,6 +26,7 @@ class ThemeServiceProvider extends ServiceProvider */ public function boot() { + // Boot up the theme system $themeService = $this->app->make(ThemeService::class); $themeService->readThemeActions(); $themeService->dispatch(ThemeEvents::APP_BOOT, $this->app); diff --git a/app/Providers/CustomValidationServiceProvider.php b/app/Providers/ValidationRuleServiceProvider.php similarity index 93% rename from app/Providers/CustomValidationServiceProvider.php rename to app/Providers/ValidationRuleServiceProvider.php index ac95099cc..928918dc7 100644 --- a/app/Providers/CustomValidationServiceProvider.php +++ b/app/Providers/ValidationRuleServiceProvider.php @@ -6,7 +6,7 @@ use BookStack\Uploads\ImageService; use Illuminate\Support\Facades\Validator; use Illuminate\Support\ServiceProvider; -class CustomValidationServiceProvider extends ServiceProvider +class ValidationRuleServiceProvider extends ServiceProvider { /** * Register our custom validation rules when the application boots. diff --git a/app/Providers/ViewTweaksServiceProvider.php b/app/Providers/ViewTweaksServiceProvider.php new file mode 100644 index 000000000..f1f1554ae --- /dev/null +++ b/app/Providers/ViewTweaksServiceProvider.php @@ -0,0 +1,31 @@ +"; + }); + } +} From b716fd2b8b2820f9e117be0d6d9b29bdc848cc56 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 27 Sep 2022 02:56:13 +0100 Subject: [PATCH 08/24] Updated composer deps, incremented dev version --- composer.lock | 421 ++++++++++++++++++++++++++++++++++---------------- version | 2 +- 2 files changed, 286 insertions(+), 137 deletions(-) diff --git a/composer.lock b/composer.lock index b807fd577..762fb0a36 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1d3bd88b99d07b5410ee4b245bece28e", + "content-hash": "4a0d254197dda8118685ec1a1eb10edf", "packages": [ { "name": "aws/aws-crt-php", @@ -58,16 +58,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.235.1", + "version": "3.236.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "2025db05c7dd22ae414857dadd49207f64c2fc74" + "reference": "bff1f1ade00c758ea27f498baee1fa16901e5bfd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2025db05c7dd22ae414857dadd49207f64c2fc74", - "reference": "2025db05c7dd22ae414857dadd49207f64c2fc74", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bff1f1ade00c758ea27f498baee1fa16901e5bfd", + "reference": "bff1f1ade00c758ea27f498baee1fa16901e5bfd", "shasum": "" }, "require": { @@ -86,6 +86,7 @@ "aws/aws-php-sns-message-validator": "~1.0", "behat/behat": "~3.0", "composer/composer": "^1.10.22", + "dms/phpunit-arraysubset-asserts": "^0.4.0", "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", @@ -93,10 +94,11 @@ "ext-sockets": "*", "nette/neon": "^2.3", "paragonie/random_compat": ">= 2", - "phpunit/phpunit": "^4.8.35 || ^5.6.3", + "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5", "psr/cache": "^1.0", "psr/simple-cache": "^1.0", - "sebastian/comparator": "^1.2.3" + "sebastian/comparator": "^1.2.3 || ^4.0", + "yoast/phpunit-polyfills": "^1.0" }, "suggest": { "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", @@ -144,9 +146,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.235.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.236.0" }, - "time": "2022-09-02T18:18:19+00:00" + "time": "2022-09-26T18:13:07+00:00" }, { "name": "bacon/bacon-qr-code", @@ -559,16 +561,16 @@ }, { "name": "doctrine/dbal", - "version": "3.4.3", + "version": "3.4.5", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "a24b89d663d8f261199bc0a91c48016042ebda85" + "reference": "a5a58773109c0abb13e658c8ccd92aeec8d07f9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/a24b89d663d8f261199bc0a91c48016042ebda85", - "reference": "a24b89d663d8f261199bc0a91c48016042ebda85", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/a5a58773109c0abb13e658c8ccd92aeec8d07f9e", + "reference": "a5a58773109c0abb13e658c8ccd92aeec8d07f9e", "shasum": "" }, "require": { @@ -583,14 +585,14 @@ "require-dev": { "doctrine/coding-standard": "10.0.0", "jetbrains/phpstorm-stubs": "2022.2", - "phpstan/phpstan": "1.8.2", + "phpstan/phpstan": "1.8.3", "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "9.5.21", + "phpunit/phpunit": "9.5.24", "psalm/plugin-phpunit": "0.17.0", "squizlabs/php_codesniffer": "3.7.1", "symfony/cache": "^5.4|^6.0", "symfony/console": "^4.4|^5.4|^6.0", - "vimeo/psalm": "4.24.0" + "vimeo/psalm": "4.27.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -650,7 +652,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.4.3" + "source": "https://github.com/doctrine/dbal/tree/3.4.5" }, "funding": [ { @@ -666,7 +668,7 @@ "type": "tidelift" } ], - "time": "2022-08-28T17:26:36+00:00" + "time": "2022-09-23T17:48:57+00:00" }, { "name": "doctrine/deprecations", @@ -804,28 +806,28 @@ }, { "name": "doctrine/inflector", - "version": "2.0.4", + "version": "2.0.5", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "8b7ff3e4b7de6b2c84da85637b59fd2880ecaa89" + "reference": "ade2b3bbfb776f27f0558e26eed43b5d9fe1b392" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/8b7ff3e4b7de6b2c84da85637b59fd2880ecaa89", - "reference": "8b7ff3e4b7de6b2c84da85637b59fd2880ecaa89", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/ade2b3bbfb776f27f0558e26eed43b5d9fe1b392", + "reference": "ade2b3bbfb776f27f0558e26eed43b5d9fe1b392", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^8.2", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "vimeo/psalm": "^4.10" + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25" }, "type": "library", "autoload": { @@ -875,7 +877,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.4" + "source": "https://github.com/doctrine/inflector/tree/2.0.5" }, "funding": [ { @@ -891,7 +893,7 @@ "type": "tidelift" } ], - "time": "2021-10-22T20:16:43+00:00" + "time": "2022-09-07T09:01:28+00:00" }, { "name": "doctrine/lexer", @@ -971,24 +973,24 @@ }, { "name": "dompdf/dompdf", - "version": "v2.0.0", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "79573d8b8a141ec8a17312515de8740eed014fa9" + "reference": "c5310df0e22c758c85ea5288175fc6cd777bc085" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/79573d8b8a141ec8a17312515de8740eed014fa9", - "reference": "79573d8b8a141ec8a17312515de8740eed014fa9", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/c5310df0e22c758c85ea5288175fc6cd777bc085", + "reference": "c5310df0e22c758c85ea5288175fc6cd777bc085", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", "masterminds/html5": "^2.0", - "phenx/php-font-lib": "^0.5.4", - "phenx/php-svg-lib": "^0.3.3 || ^0.4.0", + "phenx/php-font-lib": ">=0.5.4 <1.0.0", + "phenx/php-svg-lib": ">=0.3.3 <1.0.0", "php": "^7.1 || ^8.0" }, "require-dev": { @@ -1019,38 +1021,30 @@ ], "authors": [ { - "name": "Fabien Ménager", - "email": "fabien.menager@gmail.com" - }, - { - "name": "Brian Sweeney", - "email": "eclecticgeek@gmail.com" - }, - { - "name": "Gabriel Bull", - "email": "me@gabrielbull.com" + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" } ], "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", "homepage": "https://github.com/dompdf/dompdf", "support": { "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v2.0.0" + "source": "https://github.com/dompdf/dompdf/tree/v2.0.1" }, - "time": "2022-06-21T21:14:57+00:00" + "time": "2022-09-22T13:43:41+00:00" }, { "name": "dragonmantank/cron-expression", - "version": "v3.3.1", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "be85b3f05b46c39bbc0d95f6c071ddff669510fa" + "reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/be85b3f05b46c39bbc0d95f6c071ddff669510fa", - "reference": "be85b3f05b46c39bbc0d95f6c071ddff669510fa", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/782ca5968ab8b954773518e9e49a6f892a34b2a8", + "reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8", "shasum": "" }, "require": { @@ -1090,7 +1084,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.1" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.2" }, "funding": [ { @@ -1098,7 +1092,7 @@ "type": "github" } ], - "time": "2022-01-18T15:43:28+00:00" + "time": "2022-09-10T18:51:20+00:00" }, { "name": "egulias/email-validator", @@ -1792,16 +1786,16 @@ }, { "name": "laravel/framework", - "version": "v8.83.23", + "version": "v8.83.24", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "bdc707f8b9bcad289b24cd182d98ec7480ac4491" + "reference": "a684da6197ae77eee090637ae4411b2f321adfc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/bdc707f8b9bcad289b24cd182d98ec7480ac4491", - "reference": "bdc707f8b9bcad289b24cd182d98ec7480ac4491", + "url": "https://api.github.com/repos/laravel/framework/zipball/a684da6197ae77eee090637ae4411b2f321adfc7", + "reference": "a684da6197ae77eee090637ae4411b2f321adfc7", "shasum": "" }, "require": { @@ -1961,20 +1955,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2022-07-26T13:30:00+00:00" + "time": "2022-09-22T18:59:47+00:00" }, { "name": "laravel/serializable-closure", - "version": "v1.2.1", + "version": "v1.2.2", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "d78fd36ba031a1a695ea5a406f29996948d7011b" + "reference": "47afb7fae28ed29057fdca37e16a84f90cc62fae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/d78fd36ba031a1a695ea5a406f29996948d7011b", - "reference": "d78fd36ba031a1a695ea5a406f29996948d7011b", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/47afb7fae28ed29057fdca37e16a84f90cc62fae", + "reference": "47afb7fae28ed29057fdca37e16a84f90cc62fae", "shasum": "" }, "require": { @@ -2021,7 +2015,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2022-08-26T15:25:27+00:00" + "time": "2022-09-08T13:45:54+00:00" }, { "name": "laravel/socialite", @@ -3438,21 +3432,21 @@ }, { "name": "phenx/php-svg-lib", - "version": "0.4.1", + "version": "0.5.0", "source": { "type": "git", "url": "https://github.com/dompdf/php-svg-lib.git", - "reference": "4498b5df7b08e8469f0f8279651ea5de9626ed02" + "reference": "76876c6cf3080bcb6f249d7d59705108166a6685" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/4498b5df7b08e8469f0f8279651ea5de9626ed02", - "reference": "4498b5df7b08e8469f0f8279651ea5de9626ed02", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/76876c6cf3080bcb6f249d7d59705108166a6685", + "reference": "76876c6cf3080bcb6f249d7d59705108166a6685", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "^7.1 || ^7.2 || ^7.3 || ^7.4 || ^8.0", + "php": "^7.1 || ^8.0", "sabberworm/php-css-parser": "^8.4" }, "require-dev": { @@ -3478,9 +3472,9 @@ "homepage": "https://github.com/PhenX/php-svg-lib", "support": { "issues": "https://github.com/dompdf/php-svg-lib/issues", - "source": "https://github.com/dompdf/php-svg-lib/tree/0.4.1" + "source": "https://github.com/dompdf/php-svg-lib/tree/0.5.0" }, - "time": "2022-03-07T12:52:04+00:00" + "time": "2022-09-06T12:16:56+00:00" }, { "name": "phpoption/phpoption", @@ -3559,16 +3553,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.15", + "version": "3.0.16", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "c96e250238e88bf1040e9f7715efab1d6bc7f622" + "reference": "7181378909ed8890be4db53d289faac5b77f8b05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/c96e250238e88bf1040e9f7715efab1d6bc7f622", - "reference": "c96e250238e88bf1040e9f7715efab1d6bc7f622", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/7181378909ed8890be4db53d289faac5b77f8b05", + "reference": "7181378909ed8890be4db53d289faac5b77f8b05", "shasum": "" }, "require": { @@ -3649,7 +3643,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.15" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.16" }, "funding": [ { @@ -3665,7 +3659,7 @@ "type": "tidelift" } ], - "time": "2022-09-02T17:05:08+00:00" + "time": "2022-09-05T18:03:08+00:00" }, { "name": "pragmarx/google2fa", @@ -7258,16 +7252,16 @@ }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "2.2.4", + "version": "2.2.5", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "da444caae6aca7a19c0c140f68c6182e337d5b1c" + "reference": "4348a3a06651827a27d989ad1d13efec6bb49b19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/da444caae6aca7a19c0c140f68c6182e337d5b1c", - "reference": "da444caae6aca7a19c0c140f68c6182e337d5b1c", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/4348a3a06651827a27d989ad1d13efec6bb49b19", + "reference": "4348a3a06651827a27d989ad1d13efec6bb49b19", "shasum": "" }, "require": { @@ -7305,9 +7299,9 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.4" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.5" }, - "time": "2021-12-08T09:12:39+00:00" + "time": "2022-09-12T13:28:28+00:00" }, { "name": "vlucas/phpdotenv", @@ -7523,6 +7517,98 @@ } ], "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v6.6.4", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "4ce800dc32fd0292a4f05c00f347142dce1ecdda" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/4ce800dc32fd0292a4f05c00f347142dce1ecdda", + "reference": "4ce800dc32fd0292a4f05c00f347142dce1ecdda", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "jean85/pretty-package-versions": "^2.0.5", + "php": "^7.3 || ^8.0", + "phpunit/php-code-coverage": "^9.2.17", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-timer": "^5.0.3", + "phpunit/phpunit": "^9.5.24", + "sebastian/environment": "^5.1.4", + "symfony/console": "^5.4.12 || ^6.1.4", + "symfony/process": "^5.4.11 || ^6.1.3" + }, + "require-dev": { + "doctrine/coding-standard": "^10.0.0", + "ext-pcov": "*", + "ext-posix": "*", + "infection/infection": "^0.26.14", + "malukenho/mcbumpface": "^1.1.5", + "squizlabs/php_codesniffer": "^3.7.1", + "symfony/filesystem": "^5.4.12 || ^6.1.4", + "vimeo/psalm": "^4.27.0" + }, + "bin": [ + "bin/paratest", + "bin/paratest.bat", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v6.6.4" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2022-09-13T10:47:01+00:00" + }, { "name": "composer/ca-bundle", "version": "1.3.3", @@ -7674,16 +7760,16 @@ }, { "name": "composer/composer", - "version": "2.4.1", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "777d542e3af65f8e7a66a4d98ce7a697da339414" + "reference": "7d887621e69a0311eb50aed4a16f7044b2b385b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/777d542e3af65f8e7a66a4d98ce7a697da339414", - "reference": "777d542e3af65f8e7a66a4d98ce7a697da339414", + "url": "https://api.github.com/repos/composer/composer/zipball/7d887621e69a0311eb50aed4a16f7044b2b385b9", + "reference": "7d887621e69a0311eb50aed4a16f7044b2b385b9", "shasum": "" }, "require": { @@ -7713,7 +7799,7 @@ "phpstan/phpstan-deprecation-rules": "^1", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1", - "phpstan/phpstan-symfony": "^1.1", + "phpstan/phpstan-symfony": "^1.2.10", "symfony/phpunit-bridge": "^6.0" }, "suggest": { @@ -7766,7 +7852,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", - "source": "https://github.com/composer/composer/tree/2.4.1" + "source": "https://github.com/composer/composer/tree/2.4.2" }, "funding": [ { @@ -7782,7 +7868,7 @@ "type": "tidelift" } ], - "time": "2022-08-20T09:44:50+00:00" + "time": "2022-09-14T14:11:15+00:00" }, { "name": "composer/metadata-minifier", @@ -8394,16 +8480,16 @@ }, { "name": "itsgoingd/clockwork", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/itsgoingd/clockwork.git", - "reference": "2cad6c75dc2b96cbfd48c0511bb035a4e328c17f" + "reference": "74ee05a61296aa7298164ef5346f0a568aa6106e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/2cad6c75dc2b96cbfd48c0511bb035a4e328c17f", - "reference": "2cad6c75dc2b96cbfd48c0511bb035a4e328c17f", + "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/74ee05a61296aa7298164ef5346f0a568aa6106e", + "reference": "74ee05a61296aa7298164ef5346f0a568aa6106e", "shasum": "" }, "require": { @@ -8450,7 +8536,7 @@ ], "support": { "issues": "https://github.com/itsgoingd/clockwork/issues", - "source": "https://github.com/itsgoingd/clockwork/tree/v5.1.7" + "source": "https://github.com/itsgoingd/clockwork/tree/v5.1.8" }, "funding": [ { @@ -8458,7 +8544,66 @@ "type": "github" } ], - "time": "2022-08-14T21:23:22+00:00" + "time": "2022-09-25T20:21:14+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.17", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^0.12.66", + "phpunit/phpunit": "^7.5|^8.5|^9.4", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" + }, + "time": "2021-10-08T21:21:46+00:00" }, { "name": "justinrainbow/json-schema", @@ -8532,16 +8677,16 @@ }, { "name": "mockery/mockery", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "c10a5f6e06fc2470ab1822fa13fa2a7380f8fbac" + "reference": "e92dcc83d5a51851baf5f5591d32cb2b16e3684e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/c10a5f6e06fc2470ab1822fa13fa2a7380f8fbac", - "reference": "c10a5f6e06fc2470ab1822fa13fa2a7380f8fbac", + "url": "https://api.github.com/repos/mockery/mockery/zipball/e92dcc83d5a51851baf5f5591d32cb2b16e3684e", + "reference": "e92dcc83d5a51851baf5f5591d32cb2b16e3684e", "shasum": "" }, "require": { @@ -8598,9 +8743,9 @@ ], "support": { "issues": "https://github.com/mockery/mockery/issues", - "source": "https://github.com/mockery/mockery/tree/1.5.0" + "source": "https://github.com/mockery/mockery/tree/1.5.1" }, - "time": "2022-01-20T13:18:17+00:00" + "time": "2022-09-07T15:32:08+00:00" }, { "name": "myclabs/deep-copy", @@ -8959,16 +9104,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.8.4", + "version": "1.8.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "eed4c9da531f6ebb4787235b6fb486e2c20f34e5" + "reference": "c386ab2741e64cc9e21729f891b28b2b10fe6618" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/eed4c9da531f6ebb4787235b6fb486e2c20f34e5", - "reference": "eed4c9da531f6ebb4787235b6fb486e2c20f34e5", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c386ab2741e64cc9e21729f891b28b2b10fe6618", + "reference": "c386ab2741e64cc9e21729f891b28b2b10fe6618", "shasum": "" }, "require": { @@ -8998,7 +9143,7 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.8.4" + "source": "https://github.com/phpstan/phpstan/tree/1.8.6" }, "funding": [ { @@ -9014,7 +9159,7 @@ "type": "tidelift" } ], - "time": "2022-09-03T13:08:04+00:00" + "time": "2022-09-23T09:54:39+00:00" }, { "name": "phpunit/php-code-coverage", @@ -9336,16 +9481,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.24", + "version": "9.5.25", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5" + "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", - "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", + "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", "shasum": "" }, "require": { @@ -9367,14 +9512,14 @@ "phpunit/php-timer": "^5.0.2", "sebastian/cli-parser": "^1.0.1", "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", + "sebastian/comparator": "^4.0.8", "sebastian/diff": "^4.0.3", "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", + "sebastian/exporter": "^4.0.5", "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.1", + "sebastian/type": "^3.2", "sebastian/version": "^3.0.2" }, "suggest": { @@ -9418,7 +9563,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.24" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.25" }, "funding": [ { @@ -9428,9 +9573,13 @@ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2022-08-30T07:42:16+00:00" + "time": "2022-09-25T03:44:45+00:00" }, { "name": "react/promise", @@ -9677,16 +9826,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { @@ -9739,7 +9888,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" }, "funding": [ { @@ -9747,7 +9896,7 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2022-09-14T12:41:17+00:00" }, { "name": "sebastian/complexity", @@ -9937,16 +10086,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { @@ -10002,7 +10151,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" }, "funding": [ { @@ -10010,7 +10159,7 @@ "type": "github" } ], - "time": "2021-11-11T14:18:36+00:00" + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", @@ -10365,16 +10514,16 @@ }, { "name": "sebastian/type", - "version": "3.1.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fb44e1cc6e557418387ad815780360057e40753e" + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb44e1cc6e557418387ad815780360057e40753e", - "reference": "fb44e1cc6e557418387ad815780360057e40753e", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", "shasum": "" }, "require": { @@ -10386,7 +10535,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -10409,7 +10558,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.1.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" }, "funding": [ { @@ -10417,7 +10566,7 @@ "type": "github" } ], - "time": "2022-08-29T06:55:37+00:00" + "time": "2022-09-12T14:47:03+00:00" }, { "name": "sebastian/version", diff --git a/version b/version index 1edf07d8a..3dbb8ff33 100644 --- a/version +++ b/version @@ -1 +1 @@ -v22.07-dev +v22.10-dev From 931641ed2c1a1a85af698f227d56ae96b381e0d3 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 27 Sep 2022 12:23:16 +0100 Subject: [PATCH 09/24] Tweaked license and readme text Updated license copyright line to better help it be detected as MIT by automatic license systems (Such as GitHub license detection) while removing contributors link which would not actually list all contributors. Also added year range back in to be more specific about active lifetime. --- LICENSE | 3 +-- readme.md | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/LICENSE b/LICENSE index 0ec2e91ab..5b3d8699a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015-present, Dan Brown and the BookStack Project contributors -https://github.com/BookStackApp/BookStack/graphs/contributors +Copyright (c) 2015-2022, Dan Brown and the BookStack Project contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/readme.md b/readme.md index 16992341d..469ec88fd 100644 --- a/readme.md +++ b/readme.md @@ -216,16 +216,14 @@ The website which contains the project docs & Blog can be found in the [BookStac ## ⚖️ License -The BookStack source is provided under the MIT License. +The BookStack source is provided under the [MIT License](https://github.com/BookStackApp/BookStack/blob/development/LICENSE). The libraries used by, and included with, BookStack are provided under their own licenses and copyright. The licenses for many of our core dependencies can be found in the attribution list below but this is not an exhaustive list of all projects used within BookStack. ## 👪 Attribution -The great people that have worked to build and improve BookStack can [be seen here](https://github.com/BookStackApp/BookStack/graphs/contributors). - -The wonderful people that have provided translations, either through GitHub or via Crowdin [can be seen here](https://github.com/BookStackApp/BookStack/blob/development/.github/translators.txt). +The great people that have worked to build and improve BookStack can [be seen here](https://github.com/BookStackApp/BookStack/graphs/contributors). The wonderful people that have provided translations, either through GitHub or via Crowdin [can be seen here](https://github.com/BookStackApp/BookStack/blob/development/.github/translators.txt). Below are the great open-source projects used to help build BookStack. Note: This is not an exhaustive list of all libraries and projects that would be used in an active BookStack instance. From af434d021663dc18df90c2ee0639dc9b027842c4 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 27 Sep 2022 18:44:06 +0100 Subject: [PATCH 10/24] Fixed custom code theme not showing in WYSIWYG Fixes #3753 Was caused by not including added styles to the code block shadow root. --- resources/js/wysiwyg/plugin-codeeditor.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/js/wysiwyg/plugin-codeeditor.js b/resources/js/wysiwyg/plugin-codeeditor.js index b9fc355e1..5377f564e 100644 --- a/resources/js/wysiwyg/plugin-codeeditor.js +++ b/resources/js/wysiwyg/plugin-codeeditor.js @@ -39,16 +39,16 @@ function defineCodeBlockCustomElement(editor) { constructor() { super(); this.attachShadow({mode: 'open'}); - const linkElem = document.createElement('link'); - linkElem.setAttribute('rel', 'stylesheet'); - linkElem.setAttribute('href', window.baseUrl('/dist/styles.css')); + + const stylesToCopy = document.querySelectorAll('link[rel="stylesheet"]:not([media="print"])'); + const copiedStyles = Array.from(stylesToCopy).map(styleEl => styleEl.cloneNode(false)); const cmContainer = document.createElement('div'); cmContainer.style.pointerEvents = 'none'; cmContainer.contentEditable = 'false'; cmContainer.classList.add('CodeMirrorContainer'); - this.shadowRoot.append(linkElem, cmContainer); + this.shadowRoot.append(...copiedStyles, cmContainer); } getLanguage() { From 391fb2cc62f19e36d4cb27a204135e0a95189bdb Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 27 Sep 2022 18:52:21 +0100 Subject: [PATCH 11/24] Added MATLAB/Octave code highlighting support --- resources/js/code.mjs | 3 +++ resources/views/pages/parts/code-editor.blade.php | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/resources/js/code.mjs b/resources/js/code.mjs index eca941f1c..5881e2512 100644 --- a/resources/js/code.mjs +++ b/resources/js/code.mjs @@ -15,6 +15,7 @@ import 'codemirror/mode/lua/lua'; import 'codemirror/mode/markdown/markdown'; import 'codemirror/mode/mllike/mllike'; import 'codemirror/mode/nginx/nginx'; +import 'codemirror/mode/octave/octave'; import 'codemirror/mode/perl/perl'; import 'codemirror/mode/pascal/pascal'; import 'codemirror/mode/php/php'; @@ -65,11 +66,13 @@ const modeMap = { julia: 'text/x-julia', latex: 'text/x-stex', lua: 'lua', + matlab: 'text/x-octave', md: 'markdown', mdown: 'markdown', markdown: 'markdown', ml: 'mllike', nginx: 'nginx', + octave: 'text/x-octave', perl: 'perl', pl: 'perl', powershell: 'powershell', diff --git a/resources/views/pages/parts/code-editor.blade.php b/resources/views/pages/parts/code-editor.blade.php index e86282d73..770ed4840 100644 --- a/resources/views/pages/parts/code-editor.blade.php +++ b/resources/views/pages/parts/code-editor.blade.php @@ -24,8 +24,8 @@ @php $languages = [ 'Bash', 'CSS', 'C', 'C++', 'C#', 'Diff', 'Fortran', 'F#', 'Go', 'Haskell', 'HTML', 'INI', - 'Java', 'JavaScript', 'JSON', 'Julia', 'Kotlin', 'LaTeX', 'Lua', 'MarkDown', 'Nginx', 'OCaml', - 'Pascal', 'Perl', 'PHP', 'Powershell', 'Python', 'Ruby', 'Rust', 'Shell', 'SQL', 'TypeScript', + 'Java', 'JavaScript', 'JSON', 'Julia', 'Kotlin', 'LaTeX', 'Lua', 'MarkDown', 'MATLAB', 'Nginx', 'OCaml', + 'Octave', 'Pascal', 'Perl', 'PHP', 'Powershell', 'Python', 'Ruby', 'Rust', 'Shell', 'SQL', 'TypeScript', 'VBScript', 'VB.NET', 'XML', 'YAML', ]; @endphp From d933fe5dcec5189a235058ff7852d3af83bf893f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 27 Sep 2022 19:05:03 +0100 Subject: [PATCH 12/24] Updated WYSIWYG config to allow styles on list elements --- resources/js/wysiwyg/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg/config.js index 52c52592c..8c85f60e2 100644 --- a/resources/js/wysiwyg/config.js +++ b/resources/js/wysiwyg/config.js @@ -255,7 +255,7 @@ export function build(options) { statusbar: false, menubar: false, paste_data_images: false, - extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked]', + extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram],details[*],summary[*],div[*],li[class|checked|style]', automatic_uploads: false, custom_elements: 'doc-root,code-block', valid_children: [ From 6dd89ba9560db36dece3b17529f4db55f96c25f5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 27 Sep 2022 20:11:58 +0100 Subject: [PATCH 13/24] Split out some development-specific readme parts to own pages --- dev/docs/development.md | 96 +++++++++++++++++++++++++++++ dev/docs/release-process.md | 24 ++++++++ readme.md | 116 ++---------------------------------- 3 files changed, 125 insertions(+), 111 deletions(-) create mode 100644 dev/docs/development.md create mode 100644 dev/docs/release-process.md diff --git a/dev/docs/development.md b/dev/docs/development.md new file mode 100644 index 000000000..6d11443b6 --- /dev/null +++ b/dev/docs/development.md @@ -0,0 +1,96 @@ +# Development & Testing + +All development on BookStack is currently done on the `development` branch. +When it's time for a release the `development` branch is merged into release with built & minified CSS & JS then tagged at its version. Here are the current development requirements: + +* [Node.js](https://nodejs.org/en/) v16.0+ + +This project uses SASS for CSS development and this is built, along with the JavaScript, using a range of npm scripts. The below npm commands can be used to install the dependencies & run the build tasks: + +``` bash +# Install NPM Dependencies +npm install + +# Build assets for development +npm run build + +# Build and minify assets for production +npm run production + +# Build for dev (With sourcemaps) and watch for changes +npm run dev +``` + +BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the database name, username and password all defined as `bookstack-test`. You will have to create that database and that set of credentials before testing. + +The testing database will also need migrating and seeding beforehand. This can be done by running `composer refresh-test-database`. + +Once done you can run `composer test` in the application root directory to run all tests. Tests can be ran in parallel by running them via `composer t`. This will use Laravel's built-in parallel testing functionality, and attempt to create and seed a database instance for each testing thread. If required these parallel testing instances can be reset, before testing again, by running `composer t-reset`. + +## Code Standards + +PHP code standards are managed by [using PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer). +Static analysis is in place using [PHPStan](https://phpstan.org/) & [Larastan](https://github.com/nunomaduro/larastan). +The below commands can be used to utilise these tools: + +```bash +# Run code linting using PHP_CodeSniffer +composer lint + +# As above, but show rule names in output +composer lint -- -s + +# Auto-fix formatting & lint issues via PHP_CodeSniffer phpcbf +composer format + +# Run static analysis via larastan/phpstan +composer check-static +``` + +If submitting a PR, formatting as per our project standards would help for clarity but don't worry too much about using/understanding these tools as we can always address issues at a later stage when they're picked up by our automated tools. + +## Development using Docker + +This repository ships with a Docker Compose configuration intended for development purposes. It'll build a PHP image with all needed extensions installed and start up a MySQL server and a Node image watching the UI assets. + +To get started, make sure you meet the following requirements: + +- Docker and Docker Compose are installed +- Your user is part of the `docker` group + +If all the conditions are met, you can proceed with the following steps: + +1. **Copy `.env.example` to `.env`**, change `APP_KEY` to a random 32 char string and set `APP_ENV` to `local`. +2. Make sure **port 8080 is unused** *or else* change `DEV_PORT` to a free port on your host. +3. **Run `chgrp -R docker storage`**. The development container will chown the `storage` directory to the `www-data` user inside the container so BookStack can write to it. You need to change the group to your host's `docker` group here to not lose access to the `storage` directory. +4. **Run `docker-compose up`** and wait until the image is built and all database migrations have been done. +5. You can now login with `admin@admin.com` and `password` as password on `localhost:8080` (or another port if specified). + +If needed, You'll be able to run any artisan commands via docker-compose like so: + +```bash +docker-compose run app php artisan list +``` + +The docker-compose setup runs an instance of [MailHog](https://github.com/mailhog/MailHog) and sets environment variables to redirect any BookStack-sent emails to MailHog. You can view this mail via the MailHog web interface on `localhost:8025`. You can change the port MailHog is accessible on by setting a `DEV_MAIL_PORT` environment variable. + +### Running tests + +After starting the general development Docker, migrate & seed the testing database: + + ```bash +# This only needs to be done once +docker-compose run app php artisan migrate --database=mysql_testing +docker-compose run app php artisan db:seed --class=DummyContentSeeder --database=mysql_testing +``` + +Once the database has been migrated & seeded, you can run the tests like so: + + ```bash +docker-compose run app php vendor/bin/phpunit +``` + +### Debugging + +The docker-compose setup ships with Xdebug, which you can listen to on port 9090. +NB : For some editors like Visual Studio Code, you might need to map your workspace folder to the /app folder within the docker container for this to work. diff --git a/dev/docs/release-process.md b/dev/docs/release-process.md new file mode 100644 index 000000000..758d6db4c --- /dev/null +++ b/dev/docs/release-process.md @@ -0,0 +1,24 @@ +# Release Versioning & Process + +### BookStack Version Number Scheme + +BookStack releases are each assigned a date-based version number in the format `v.[.]`. For example: + +- `v20.12` - New feature released launched during December 2020. +- `v21.06.2` - Second patch release upon the June 2021 feature release. + +Patch releases are generally fairly minor, primarily intended for fixes and therefore are fairly unlikely to cause breakages upon update. +Feature releases are generally larger, bringing new features in addition to fixes and enhancements. These releases have a greater chance of introducing breaking changes upon update, so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/). + +### Release Planning Process + +Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed. + +### Release Announcements + +Feature releases, and some patch releases, will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blog posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates). + +### Release Technical Process + +Deploying a release, at a high level, simply involves merging the development branch into the release branch before then building & committing any release-only assets. +A helper script [can be found in our](https://github.com/BookStackApp/devops/blob/main/meta-scripts/bookstack-release-steps) devops repo which provides the steps and commands for deploying a new release. \ No newline at end of file diff --git a/readme.md b/readme.md index 469ec88fd..b8bd17232 100644 --- a/readme.md +++ b/readme.md @@ -59,126 +59,20 @@ Note: Listed services are not tested, vetted nor supported by the official BookS ## 🛣️ Road Map -Below is a high-level road map view for BookStack to provide a sense of direction of where the project is going. This can change at any point and does not reflect many features and improvements that will also be included as part of the journey along this road map. For more granular detail of what will be included in upcoming releases you can review the project milestones as defined in the "Release Process" section below. +Below is a high-level road map view for BookStack to provide a sense of direction of where the project is going. This can change at any point and does not reflect many features and improvements that will also be included as part of the journey along this road map. For more granular detail of what will be included in upcoming releases you can review the project milestones as defined in our [Release Process](dev/docs/release-process.md) documentation. - **Platform REST API** - *(Most actions implemented, maturing)* - *A REST API covering, at minimum, control of core content models (Books, Chapters, Pages) for automation and platform extension.* -- **Editor Alignment & Review** - *(Done)* - - *Review the page editors with the goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.* - **Permission System Review** - *(In Progress)* - *Improvement in how permissions are applied and a review of the efficiency of the permission & roles system.* -- **Installation & Deployment Process Revamp** - - *Creation of a streamlined & secure process for users to deploy & update BookStack with reduced development requirements (No git or composer requirement).* - -## 🚀 Release Versioning & Process - -BookStack releases are each assigned a date-based version number in the format `v.[.]`. For example: - -- `v20.12` - New feature released launched during December 2020. -- `v21.06.2` - Second patch release upon the June 2021 feature release. - -Patch releases are generally fairly minor, primarily intended for fixes and therefore are fairly unlikely to cause breakages upon update. -Feature releases are generally larger, bringing new features in addition to fixes and enhancements. These releases have a greater chance of introducing breaking changes upon update, so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/). - -Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed. - -Feature releases, and some patch releases, will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blog posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates). ## 🛠️ Development & Testing -All development on BookStack is currently done on the `development` branch. When it's time for a release the `development` branch is merged into release with built & minified CSS & JS then tagged at its version. Here are the current development requirements: +Please see our [development docs](dev/docs/development.md) for full details regarding work on the BookStack source code. -* [Node.js](https://nodejs.org/en/) v14.0+ +If you're just looking to customize or extend your own BookStack instance, take a look at our [Hacking BookStack documentation page](https://www.bookstackapp.com/docs/admin/hacking-bookstack/) for details on various options to achieve this without altering the BookStack source code. -This project uses SASS for CSS development and this is built, along with the JavaScript, using a range of npm scripts. The below npm commands can be used to install the dependencies & run the build tasks: - -``` bash -# Install NPM Dependencies -npm install - -# Build assets for development -npm run build - -# Build and minify assets for production -npm run production - -# Build for dev (With sourcemaps) and watch for changes -npm run dev -``` - -BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the database name, user name and password all defined as `bookstack-test`. You will have to create that database and that set of credentials before testing. - -The testing database will also need migrating and seeding beforehand. This can be done by running `composer refresh-test-database`. - -Once done you can run `composer test` in the application root directory to run all tests. Tests can be ran in parallel by running them via `composer t`. This will use Laravel's built-in parallel testing functionality, and attempt to create and seed a database instance for each testing thread. If required these parallel testing instances can be reset, before testing again, by running `composer t-reset`. - -### 📜 Code Standards - -PHP code standards are managed by [using PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer). -Static analysis is in place using [PHPStan](https://phpstan.org/) & [Larastan](https://github.com/nunomaduro/larastan). -The below commands can be used to utilise these tools: - -```bash -# Run code linting using PHP_CodeSniffer -composer lint - -# As above, but show rule names in output -composer lint -- -s - -# Auto-fix formatting & lint issues via PHP_CodeSniffer phpcbf -composer format - -# Run static analysis via larastan/phpstan -composer check-static -``` - -If submitting a PR, formatting as per our project standards would help for clarity but don't worry too much about using/understanding these tools as we can always address issues at a later stage when they're picked up by our automated tools. - -### 🐋 Development using Docker - -This repository ships with a Docker Compose configuration intended for development purposes. It'll build a PHP image with all needed extensions installed and start up a MySQL server and a Node image watching the UI assets. - -To get started, make sure you meet the following requirements: - -- Docker and Docker Compose are installed -- Your user is part of the `docker` group - -If all the conditions are met, you can proceed with the following steps: - -1. **Copy `.env.example` to `.env`**, change `APP_KEY` to a random 32 char string and set `APP_ENV` to `local`. -2. Make sure **port 8080 is unused** *or else* change `DEV_PORT` to a free port on your host. -3. **Run `chgrp -R docker storage`**. The development container will chown the `storage` directory to the `www-data` user inside the container so BookStack can write to it. You need to change the group to your host's `docker` group here to not lose access to the `storage` directory. -4. **Run `docker-compose up`** and wait until the image is built and all database migrations have been done. -5. You can now login with `admin@admin.com` and `password` as password on `localhost:8080` (or another port if specified). - -If needed, You'll be able to run any artisan commands via docker-compose like so: - -```bash -docker-compose run app php artisan list -``` - -The docker-compose setup runs an instance of [MailHog](https://github.com/mailhog/MailHog) and sets environment variables to redirect any BookStack-sent emails to MailHog. You can view this mail via the MailHog web interface on `localhost:8025`. You can change the port MailHog is accessible on by setting a `DEV_MAIL_PORT` environment variable. - -#### Running tests - -After starting the general development Docker, migrate & seed the testing database: - - ```bash -# This only needs to be done once -docker-compose run app php artisan migrate --database=mysql_testing -docker-compose run app php artisan db:seed --class=DummyContentSeeder --database=mysql_testing -``` - -Once the database has been migrated & seeded, you can run the tests like so: - - ```bash -docker-compose run app php vendor/bin/phpunit -``` - -#### Debugging - -The docker-compose setup ships with Xdebug, which you can listen to on port 9090. -NB : For some editors like Visual Studio Code, you might need to map your workspace folder to the /app folder within the docker container for this to work. +Details about BookStack's versioning scheme and the general release process [can be found here](dev/docs/release-process.md). ## 🌎 Translations @@ -212,7 +106,7 @@ We want BookStack to remain accessible to as many people as possible. We aim for ## 🖥️ Website, Docs & Blog -The website which contains the project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo. +The website which contains the project docs & blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo. ## ⚖️ License From 1ac1cf0c786b783a17bcc60828e42ca13a683c11 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 28 Sep 2022 11:10:06 +0100 Subject: [PATCH 14/24] Applied permissions to revision action visibility Related to #3723 --- .../pages/parts/revision-table-row.blade.php | 74 +++++----- tests/Entity/PageRevisionTest.php | 131 ++++++++++-------- 2 files changed, 117 insertions(+), 88 deletions(-) diff --git a/resources/views/pages/parts/revision-table-row.blade.php b/resources/views/pages/parts/revision-table-row.blade.php index bd891d6c4..24301adc3 100644 --- a/resources/views/pages/parts/revision-table-row.blade.php +++ b/resources/views/pages/parts/revision-table-row.blade.php @@ -30,40 +30,46 @@ {{ trans('entities.pages_revisions_current') }} @else {{ trans('entities.pages_revisions_preview') }} -  |  - -  |  - + + @if(userCan('page-update', $revision->page)) +  |  + + @endif + + @if(userCan('page-delete', $revision->page)) +  |  + + @endif @endif \ No newline at end of file diff --git a/tests/Entity/PageRevisionTest.php b/tests/Entity/PageRevisionTest.php index 5ddad8441..05c86c97d 100644 --- a/tests/Entity/PageRevisionTest.php +++ b/tests/Entity/PageRevisionTest.php @@ -4,7 +4,6 @@ namespace Tests\Entity; use BookStack\Actions\ActivityType; use BookStack\Entities\Models\Page; -use BookStack\Entities\Repos\PageRepo; use Tests\TestCase; class PageRevisionTest extends TestCase @@ -23,30 +22,26 @@ class PageRevisionTest extends TestCase public function test_page_revision_views_viewable() { $this->asEditor(); - - $pageRepo = app(PageRepo::class); $page = Page::first(); - $pageRepo->update($page, ['name' => 'updated page', 'html' => '

new content

', 'summary' => 'page revision testing']); + $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '

new content

']); $pageRevision = $page->revisions->last(); - $revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id); - $revisionView->assertStatus(200); - $revisionView->assertSee('new content'); + $resp = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id); + $resp->assertStatus(200); + $resp->assertSee('new content'); - $revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id . '/changes'); - $revisionView->assertStatus(200); - $revisionView->assertSee('new content'); + $resp = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id . '/changes'); + $resp->assertStatus(200); + $resp->assertSee('new content'); } public function test_page_revision_preview_shows_content_of_revision() { $this->asEditor(); - - $pageRepo = app(PageRepo::class); $page = Page::first(); - $pageRepo->update($page, ['name' => 'updated page', 'html' => '

new revision content

', 'summary' => 'page revision testing']); + $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '

new revision content

']); $pageRevision = $page->revisions->last(); - $pageRepo->update($page, ['name' => 'updated page', 'html' => '

Updated content

', 'summary' => 'page revision testing 2']); + $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '

Updated content

']); $revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id); $revisionView->assertStatus(200); @@ -56,11 +51,9 @@ class PageRevisionTest extends TestCase public function test_page_revision_restore_updates_content() { $this->asEditor(); - - $pageRepo = app(PageRepo::class); $page = Page::first(); - $pageRepo->update($page, ['name' => 'updated page abc123', 'html' => '

new contente def456

', 'summary' => 'initial page revision testing']); - $pageRepo->update($page, ['name' => 'updated page again', 'html' => '

new content

', 'summary' => 'page revision testing']); + $this->createRevisions($page, 1, ['name' => 'updated page abc123', 'html' => '

new contente def456

']); + $this->createRevisions($page, 1, ['name' => 'updated page again', 'html' => '

new content

']); $page = Page::find($page->id); $pageView = $this->get($page->getUrl()); @@ -82,11 +75,9 @@ class PageRevisionTest extends TestCase public function test_page_revision_restore_with_markdown_retains_markdown_content() { $this->asEditor(); - - $pageRepo = app(PageRepo::class); $page = Page::first(); - $pageRepo->update($page, ['name' => 'updated page abc123', 'markdown' => '## New Content def456', 'summary' => 'initial page revision testing']); - $pageRepo->update($page, ['name' => 'updated page again', 'markdown' => '## New Content Updated', 'summary' => 'page revision testing']); + $this->createRevisions($page, 1, ['name' => 'updated page abc123', 'markdown' => '## New Content def456']); + $this->createRevisions($page, 1, ['name' => 'updated page again', 'markdown' => '## New Content Updated']); $page = Page::find($page->id); $pageView = $this->get($page->getUrl()); @@ -112,11 +103,9 @@ class PageRevisionTest extends TestCase public function test_page_revision_restore_sets_new_revision_with_summary() { $this->asEditor(); - - $pageRepo = app(PageRepo::class); $page = Page::first(); - $pageRepo->update($page, ['name' => 'updated page abc123', 'html' => '

new contente def456

', 'summary' => 'My first update']); - $pageRepo->update($page, ['name' => 'updated page again', 'html' => '

new content

', 'summary' => '']); + $this->createRevisions($page, 1, ['name' => 'updated page abc123', 'html' => '

new contente def456

', 'summary' => 'My first update']); + $this->createRevisions($page, 1, ['html' => '

new content

']); $page->refresh(); $revToRestore = $page->revisions()->where('name', 'like', '%abc123')->first(); @@ -138,8 +127,7 @@ class PageRevisionTest extends TestCase { $page = Page::first(); $startCount = $page->revision_count; - $resp = $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); - $resp->assertStatus(302); + $this->createRevisions($page, 1); $this->assertTrue(Page::find($page->id)->revision_count === $startCount + 1); } @@ -147,12 +135,8 @@ class PageRevisionTest extends TestCase public function test_revision_count_shown_in_page_meta() { $page = Page::first(); - $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); + $this->createRevisions($page, 2); - $page = Page::find($page->id); - $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); - - $page = Page::find($page->id); $pageView = $this->get($page->getUrl()); $pageView->assertSee('Revision #' . $page->revision_count); } @@ -161,12 +145,7 @@ class PageRevisionTest extends TestCase { /** @var Page $page */ $page = Page::query()->first(); - $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); - - $page->refresh(); - $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); - - $page->refresh(); + $this->createRevisions($page, 2); $beforeRevisionCount = $page->revisions->count(); // Delete the first revision @@ -196,12 +175,7 @@ class PageRevisionTest extends TestCase { config()->set('app.revision_limit', 2); $page = Page::first(); - $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); - $page = Page::find($page->id); - $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); - for ($i = 0; $i < 10; $i++) { - $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); - } + $this->createRevisions($page, 12); $revisionCount = $page->revisions()->count(); $this->assertEquals(2, $revisionCount); @@ -211,12 +185,7 @@ class PageRevisionTest extends TestCase { config()->set('app.revision_limit', false); $page = Page::first(); - $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); - $page = Page::find($page->id); - $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); - for ($i = 0; $i < 10; $i++) { - $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); - } + $this->createRevisions($page, 12); $revisionCount = $page->revisions()->count(); $this->assertEquals(12, $revisionCount); @@ -226,14 +195,68 @@ class PageRevisionTest extends TestCase { /** @var Page $page */ $page = Page::first(); - $this->asAdmin()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html']); + $this->createRevisions($page, 1, ['html' => 'new page html']); - $resp = $this->get($page->refresh()->getUrl('/revisions')); + $resp = $this->asAdmin()->get($page->refresh()->getUrl('/revisions')); $this->withHtml($resp)->assertElementContains('td', '(WYSIWYG)'); $this->withHtml($resp)->assertElementNotContains('td', '(Markdown)'); - $this->asAdmin()->put($page->getUrl(), ['name' => 'Updated page', 'markdown' => '# Some markdown content']); + $this->createRevisions($page, 1, ['markdown' => '# Some markdown content']); $resp = $this->get($page->refresh()->getUrl('/revisions')); $this->withHtml($resp)->assertElementContains('td', '(Markdown)'); } + + public function test_revision_restore_action_only_visible_with_permission() + { + /** @var Page $page */ + $page = Page::query()->first(); + $this->createRevisions($page, 2); + + $viewer = $this->getViewer(); + $this->actingAs($viewer); + $respHtml = $this->withHtml($this->get($page->getUrl('/revisions'))); + $respHtml->assertElementNotContains('.actions a', 'Restore'); + $respHtml->assertElementNotExists('form[action$="/restore"]'); + + $this->giveUserPermissions($viewer, ['page-update-all']); + + $respHtml = $this->withHtml($this->get($page->getUrl('/revisions'))); + $respHtml->assertElementContains('.actions a', 'Restore'); + $respHtml->assertElementExists('form[action$="/restore"]'); + } + + public function test_revision_delete_action_only_visible_with_permission() + { + /** @var Page $page */ + $page = Page::query()->first(); + $this->createRevisions($page, 2); + + $viewer = $this->getViewer(); + $this->actingAs($viewer); + $respHtml = $this->withHtml($this->get($page->getUrl('/revisions'))); + $respHtml->assertElementNotContains('.actions a', 'Delete'); + $respHtml->assertElementNotExists('form[action$="/delete"]'); + + $this->giveUserPermissions($viewer, ['page-delete-all']); + + $respHtml = $this->withHtml($this->get($page->getUrl('/revisions'))); + $respHtml->assertElementContains('.actions a', 'Delete'); + $respHtml->assertElementExists('form[action$="/delete"]'); + } + + protected function createRevisions(Page $page, int $times, array $attrs = []) + { + $user = user(); + + for ($i = 0; $i < $times; $i++) { + $data = ['name' => 'Page update' . $i, 'summary' => 'Update entry' . $i]; + if (!isset($attrs['markdown'])) { + $data['html'] = '

My update page

'; + } + $this->asAdmin()->put($page->getUrl(), array_merge($data, $attrs)); + $page->refresh(); + } + + $this->actingAs($user); + } } From 8f3430d3867ca8880c4f259b2c58647eb2b2c5ea Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 28 Sep 2022 13:50:40 +0100 Subject: [PATCH 15/24] Improved tag suggestion handling - Aligned prefix-type filtering with back-end. - Increased suggestion search cut-off from 3 to 4. - Increased amount of suggestions shown. - Ordered suggestions to be name asc, as you'd expect on search. - Updated front-end filtering to use full search query, instead of truncated version, for further front-end filtering capability. Related to #3720 --- app/Actions/TagRepo.php | 10 +++++----- app/Http/Controllers/TagController.php | 11 ++++------- resources/js/components/auto-suggest.js | 15 ++++++++------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/app/Actions/TagRepo.php b/app/Actions/TagRepo.php index 172d8ec6e..2618ed2e9 100644 --- a/app/Actions/TagRepo.php +++ b/app/Actions/TagRepo.php @@ -57,21 +57,21 @@ class TagRepo * Get tag name suggestions from scanning existing tag names. * If no search term is given the 50 most popular tag names are provided. */ - public function getNameSuggestions(?string $searchTerm): Collection + public function getNameSuggestions(string $searchTerm): Collection { $query = Tag::query() ->select('*', DB::raw('count(*) as count')) ->groupBy('name'); if ($searchTerm) { - $query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc'); + $query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'asc'); } else { $query = $query->orderBy('count', 'desc')->take(50); } $query = $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type'); - return $query->get(['name'])->pluck('name'); + return $query->pluck('name'); } /** @@ -79,7 +79,7 @@ class TagRepo * If no search is given the 50 most popular values are provided. * Passing a tagName will only find values for a tags with a particular name. */ - public function getValueSuggestions(?string $searchTerm, ?string $tagName): Collection + public function getValueSuggestions(string $searchTerm, string $tagName): Collection { $query = Tag::query() ->select('*', DB::raw('count(*) as count')) @@ -97,7 +97,7 @@ class TagRepo $query = $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type'); - return $query->get(['value'])->pluck('value'); + return $query->pluck('value'); } /** diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index e59580b60..056cc9902 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -7,11 +7,8 @@ use Illuminate\Http\Request; class TagController extends Controller { - protected $tagRepo; + protected TagRepo $tagRepo; - /** - * TagController constructor. - */ public function __construct(TagRepo $tagRepo) { $this->tagRepo = $tagRepo; @@ -46,7 +43,7 @@ class TagController extends Controller */ public function getNameSuggestions(Request $request) { - $searchTerm = $request->get('search', null); + $searchTerm = $request->get('search', ''); $suggestions = $this->tagRepo->getNameSuggestions($searchTerm); return response()->json($suggestions); @@ -57,8 +54,8 @@ class TagController extends Controller */ public function getValueSuggestions(Request $request) { - $searchTerm = $request->get('search', null); - $tagName = $request->get('name', null); + $searchTerm = $request->get('search', ''); + $tagName = $request->get('name', ''); $suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName); return response()->json($suggestions); diff --git a/resources/js/components/auto-suggest.js b/resources/js/components/auto-suggest.js index d1c15c00a..80857cbe5 100644 --- a/resources/js/components/auto-suggest.js +++ b/resources/js/components/auto-suggest.js @@ -88,14 +88,12 @@ class AutoSuggest { } const nameFilter = this.getNameFilterIfNeeded(); - const search = this.input.value.slice(0, 3).toLowerCase(); + const search = this.input.value.toLowerCase(); const suggestions = await this.loadSuggestions(search, nameFilter); - let toShow = suggestions.slice(0, 6); - if (search.length > 0) { - toShow = suggestions.filter(val => { - return val.toLowerCase().includes(search); - }).slice(0, 6); - } + + const toShow = suggestions.filter(val => { + return search === '' || val.toLowerCase().startsWith(search); + }).slice(0, 10); this.displaySuggestions(toShow); } @@ -111,6 +109,9 @@ class AutoSuggest { * @returns {Promise} */ async loadSuggestions(search, nameFilter = null) { + // Truncate search to prevent over numerous lookups + search = search.slice(0, 4); + const params = {search, name: nameFilter}; const cacheKey = `${this.url}:${JSON.stringify(params)}`; From 60171b3522220e874436f52cc8936f6879e7e60f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 28 Sep 2022 14:14:51 +0100 Subject: [PATCH 16/24] Updated book copy to copy shelf relations Where permission to edit the shelf is allowed. For #3699 --- app/Entities/Models/Book.php | 1 + app/Entities/Tools/Cloner.php | 11 +++++++++++ tests/Entity/BookTest.php | 26 +++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index 8217d2cab..bf42f2008 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -19,6 +19,7 @@ use Illuminate\Support\Collection; * @property \Illuminate\Database\Eloquent\Collection $chapters * @property \Illuminate\Database\Eloquent\Collection $pages * @property \Illuminate\Database\Eloquent\Collection $directPages + * @property \Illuminate\Database\Eloquent\Collection $shelves */ class Book extends Entity implements HasCoverImage { diff --git a/app/Entities/Tools/Cloner.php b/app/Entities/Tools/Cloner.php index 92b62a754..86f392e61 100644 --- a/app/Entities/Tools/Cloner.php +++ b/app/Entities/Tools/Cloner.php @@ -4,6 +4,7 @@ namespace BookStack\Entities\Tools; use BookStack\Actions\Tag; use BookStack\Entities\Models\Book; +use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; @@ -71,8 +72,10 @@ class Cloner $bookDetails = $this->entityToInputData($original); $bookDetails['name'] = $newName; + // Clone book $copyBook = $this->bookRepo->create($bookDetails); + // Clone contents $directChildren = $original->getDirectChildren(); foreach ($directChildren as $child) { if ($child instanceof Chapter && userCan('chapter-create', $copyBook)) { @@ -84,6 +87,14 @@ class Cloner } } + // Clone bookshelf relationships + /** @var Bookshelf $shelf */ + foreach ($original->shelves as $shelf) { + if (userCan('bookshelf-update', $shelf)) { + $shelf->appendBook($copyBook); + } + } + return $copyBook; } diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php index 2e6f8e9de..2f04fcf25 100644 --- a/tests/Entity/BookTest.php +++ b/tests/Entity/BookTest.php @@ -4,6 +4,7 @@ namespace Tests\Entity; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\BookChild; +use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Repos\BookRepo; use Tests\TestCase; use Tests\Uploads\UsesImages; @@ -344,11 +345,34 @@ class BookTest extends TestCase $bookRepo->updateCoverImage($book, $coverImageFile); $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); - /** @var Book $copy */ $copy = Book::query()->where('name', '=', 'My copy book')->first(); $this->assertNotNull($copy->cover); $this->assertNotEquals($book->cover->id, $copy->cover->id); } + + public function test_copy_adds_book_to_shelves_if_edit_permissions_allows() + { + /** @var Bookshelf $shelfA */ + /** @var Bookshelf $shelfB */ + [$shelfA, $shelfB] = Bookshelf::query()->take(2)->get(); + /** @var Book $book */ + $book = Book::query()->first(); + + $shelfA->appendBook($book); + $shelfB->appendBook($book); + + $viewer = $this->getViewer(); + $this->giveUserPermissions($viewer, ['book-update-all', 'book-create-all', 'bookshelf-update-all']); + $this->setEntityRestrictions($shelfB); + + + $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); + /** @var Book $copy */ + $copy = Book::query()->where('name', '=', 'My copy book')->first(); + + $this->assertTrue($copy->shelves()->where('id', '=', $shelfA->id)->exists()); + $this->assertFalse($copy->shelves()->where('id', '=', $shelfB->id)->exists()); + } } From f79b7bc7994cbaeca6764ef2602b569cc9b07a52 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 28 Sep 2022 20:15:48 +0100 Subject: [PATCH 17/24] Added api format advisory regarding PUT/DELETE form data --- .../views/api-docs/parts/getting-started.blade.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/resources/views/api-docs/parts/getting-started.blade.php b/resources/views/api-docs/parts/getting-started.blade.php index 76da73e45..7358b5cd7 100644 --- a/resources/views/api-docs/parts/getting-started.blade.php +++ b/resources/views/api-docs/parts/getting-started.blade.php @@ -53,10 +53,19 @@
  • application/json
  • -
  • application/x-www-form-urlencoded
  • -
  • multipart/form-data
  • +
  • application/x-www-form-urlencoded*
  • +
  • multipart/form-data*
+

+ + * Form requests currently only work for POST requests due to how PHP handles request data. + If you need to use these formats for PUT or DELETE requests you can work around this limitation by + using a POST request and providing a "_method" parameter with the value equal to + PUT or DELETE. + +

+

Regardless of format chosen, ensure you set a Content-Type header on requests so that the system can correctly parse your request data. The API is primarily designed to be interfaced using JSON, since responses are always in JSON format, hence examples in this documentation will be shown as JSON. From ccbc68b5600790546e073546a31b8123d5693411 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 28 Sep 2022 20:48:29 +0100 Subject: [PATCH 18/24] Updated shelf book management to allow scroll on mobile Updates book drag handling to be limited to the handle so scrolling can be done on the items themselves. Increased handling area and improved styling to support --- resources/js/components/shelf-sort.js | 1 + resources/sass/styles.scss | 52 +++++++++++++++++---------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/resources/js/components/shelf-sort.js b/resources/js/components/shelf-sort.js index 07526716a..30eda5a21 100644 --- a/resources/js/components/shelf-sort.js +++ b/resources/js/components/shelf-sort.js @@ -19,6 +19,7 @@ class ShelfSort { new Sortable(scrollBox, { group: 'shelf-books', ghostClass: 'primary-background-light', + handle: '.handle', animation: 150, onSort: this.onChange.bind(this), }); diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss index 65eee866d..ab97466a5 100644 --- a/resources/sass/styles.scss +++ b/resources/sass/styles.scss @@ -246,26 +246,40 @@ $btt-size: 40px; border-radius: 3px; min-height: 20px; @include lightDark(background-color, #EEE, #000); - .scroll-box-item { +} +.scroll-box-item { + border-bottom: 1px solid; + border-top: 1px solid; + @include lightDark(border-color, #DDD, #000); + margin-top: -1px; + @include lightDark(background-color, #FFF, #222); + display: flex; + padding: 1px; + &:last-child { + border-bottom: 0; + } + &:hover { + cursor: pointer; + @include lightDark(background-color, #f8f8f8, #333); + } + .handle { + color: #AAA; + cursor: grab; + } + .handle svg { + margin: 0; + } + > * { padding: $-xs $-m; - border-bottom: 1px solid; - border-top: 1px solid; - @include lightDark(border-color, #DDD, #000); - margin-top: -1px; - @include lightDark(background-color, #FFF, #222); - display: flex; - gap: $-xs; - &:last-child { - border-bottom: 0; - } - &:hover { - cursor: pointer; - @include lightDark(background-color, #f8f8f8, #333); - } - .handle { - color: #AAA; - cursor: grab; - } + } + .handle + * { + padding-left: 0; + } + &:hover .handle { + @include lightDark(color, #444, #FFF); + } + a:hover { + text-decoration: none; } } From 0e94fd44a8925c695441768eee0ecbce7bb239bc Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 29 Sep 2022 15:05:57 +0100 Subject: [PATCH 19/24] Added contents to book-show endpoint Created a generic list formatting helper class for this, to align with logic used on the search results endpoint and for easier future re-use in a standardised way. Also updated some class property types. Added test to cover new books-contents results. Related to #3734 --- app/Api/ApiEntityListFormatter.php | 107 ++++++++++++++++++ app/Entities/Tools/BookContents.php | 11 +- .../Controllers/Api/BookApiController.php | 20 +++- .../Api/BookshelfApiController.php | 3 - .../Controllers/Api/SearchApiController.php | 30 ++--- dev/api/responses/books-read.json | 42 ++++++- tests/Api/BooksApiTest.php | 32 ++++++ 7 files changed, 212 insertions(+), 33 deletions(-) create mode 100644 app/Api/ApiEntityListFormatter.php diff --git a/app/Api/ApiEntityListFormatter.php b/app/Api/ApiEntityListFormatter.php new file mode 100644 index 000000000..c170ecf0c --- /dev/null +++ b/app/Api/ApiEntityListFormatter.php @@ -0,0 +1,107 @@ + + */ + protected $fields = [ + 'id', 'name', 'slug', 'book_id', 'chapter_id', + 'draft', 'template', 'created_at', 'updated_at', + ]; + + public function __construct(array $list) + { + $this->list = $list; + + // Default dynamic fields + $this->withField('url', fn(Entity $entity) => $entity->getUrl()); + } + + /** + * Add a field to be used in the formatter, with the property using the given + * name and value being the return type of the given callback. + */ + public function withField(string $property, callable $callback): self + { + $this->fields[$property] = $callback; + return $this; + } + + /** + * Show the 'type' property in the response reflecting the entity type. + * EG: page, chapter, bookshelf, book + * To be included in results with non-pre-determined types. + */ + public function withType(): self + { + $this->withField('type', fn(Entity $entity) => $entity->getType()); + return $this; + } + + /** + * Include tags in the formatted data. + */ + public function withTags(): self + { + $this->withField('tags', fn(Entity $entity) => $entity->tags); + return $this; + } + + /** + * Format the data and return an array of formatted content. + * @return array[] + */ + public function format(): array + { + $results = []; + + foreach ($this->list as $item) { + $results[] = $this->formatSingle($item); + } + + return $results; + } + + /** + * Format a single entity item to a plain array. + */ + protected function formatSingle(Entity $entity): array + { + $result = []; + $values = (clone $entity)->toArray(); + + foreach ($this->fields as $field => $callback) { + if (is_string($callback)) { + $field = $callback; + if (!isset($values[$field])) { + continue; + } + $value = $values[$field]; + } else { + $value = $callback($entity); + if (is_null($value)) { + continue; + } + } + + $result[$field] = $value; + } + + return $result; + } +} diff --git a/app/Entities/Tools/BookContents.php b/app/Entities/Tools/BookContents.php index 6f11e8cbe..0ad424de2 100644 --- a/app/Entities/Tools/BookContents.php +++ b/app/Entities/Tools/BookContents.php @@ -11,22 +11,15 @@ use Illuminate\Support\Collection; class BookContents { - /** - * @var Book - */ - protected $book; + protected Book $book; - /** - * BookContents constructor. - */ public function __construct(Book $book) { $this->book = $book; } /** - * Get the current priority of the last item - * at the top-level of the book. + * Get the current priority of the last item at the top-level of the book. */ public function getLastPriority(): int { diff --git a/app/Http/Controllers/Api/BookApiController.php b/app/Http/Controllers/Api/BookApiController.php index 15565c361..d57b48a43 100644 --- a/app/Http/Controllers/Api/BookApiController.php +++ b/app/Http/Controllers/Api/BookApiController.php @@ -2,14 +2,18 @@ namespace BookStack\Http\Controllers\Api; +use BookStack\Api\ApiEntityListFormatter; use BookStack\Entities\Models\Book; +use BookStack\Entities\Models\Chapter; +use BookStack\Entities\Models\Entity; use BookStack\Entities\Repos\BookRepo; +use BookStack\Entities\Tools\BookContents; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; class BookApiController extends ApiController { - protected $bookRepo; + protected BookRepo $bookRepo; public function __construct(BookRepo $bookRepo) { @@ -47,11 +51,25 @@ class BookApiController extends ApiController /** * View the details of a single book. + * The response data will contain 'content' property listing the chapter and pages directly within, in + * the same structure as you'd see within the BookStack interface when viewing a book. Top-level + * contents will have a 'type' property to distinguish between pages & chapters. */ public function read(string $id) { $book = Book::visible()->with(['tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy'])->findOrFail($id); + $contents = (new BookContents($book))->getTree(true, false)->all(); + $contentsApiData = (new ApiEntityListFormatter($contents)) + ->withType() + ->withField('pages', function (Entity $entity) { + if ($entity instanceof Chapter) { + return (new ApiEntityListFormatter($entity->pages->all()))->format(); + } + return null; + })->format(); + $book->setAttribute('contents', $contentsApiData); + return response()->json($book); } diff --git a/app/Http/Controllers/Api/BookshelfApiController.php b/app/Http/Controllers/Api/BookshelfApiController.php index 620df1638..b6b78e80e 100644 --- a/app/Http/Controllers/Api/BookshelfApiController.php +++ b/app/Http/Controllers/Api/BookshelfApiController.php @@ -13,9 +13,6 @@ class BookshelfApiController extends ApiController { protected BookshelfRepo $bookshelfRepo; - /** - * BookshelfApiController constructor. - */ public function __construct(BookshelfRepo $bookshelfRepo) { $this->bookshelfRepo = $bookshelfRepo; diff --git a/app/Http/Controllers/Api/SearchApiController.php b/app/Http/Controllers/Api/SearchApiController.php index 7ef714390..bf59ec671 100644 --- a/app/Http/Controllers/Api/SearchApiController.php +++ b/app/Http/Controllers/Api/SearchApiController.php @@ -2,6 +2,7 @@ namespace BookStack\Http\Controllers\Api; +use BookStack\Api\ApiEntityListFormatter; use BookStack\Entities\Models\Entity; use BookStack\Search\SearchOptions; use BookStack\Search\SearchResultsFormatter; @@ -10,8 +11,8 @@ use Illuminate\Http\Request; class SearchApiController extends ApiController { - protected $searchRunner; - protected $resultsFormatter; + protected SearchRunner $searchRunner; + protected SearchResultsFormatter $resultsFormatter; protected $rules = [ 'all' => [ @@ -50,24 +51,17 @@ class SearchApiController extends ApiController $results = $this->searchRunner->searchEntities($options, 'all', $page, $count); $this->resultsFormatter->format($results['results']->all(), $options); - /** @var Entity $result */ - foreach ($results['results'] as $result) { - $result->setVisible([ - 'id', 'name', 'slug', 'book_id', - 'chapter_id', 'draft', 'template', - 'created_at', 'updated_at', - 'tags', 'type', 'preview_html', 'url', - ]); - $result->setAttribute('type', $result->getType()); - $result->setAttribute('url', $result->getUrl()); - $result->setAttribute('preview_html', [ - 'name' => (string) $result->getAttribute('preview_name'), - 'content' => (string) $result->getAttribute('preview_content'), - ]); - } + $data = (new ApiEntityListFormatter($results['results']->all())) + ->withType()->withTags() + ->withField('preview_html', function (Entity $entity) { + return [ + 'name' => (string) $entity->getAttribute('preview_name'), + 'content' => (string) $entity->getAttribute('preview_content'), + ]; + })->format(); return response()->json([ - 'data' => $results['results'], + 'data' => $data, 'total' => $results['total'], ]); } diff --git a/dev/api/responses/books-read.json b/dev/api/responses/books-read.json index 7de85addc..8d584f597 100644 --- a/dev/api/responses/books-read.json +++ b/dev/api/responses/books-read.json @@ -17,6 +17,44 @@ "id": 1, "name": "Admin" }, + "contents": [ + { + "id": 50, + "name": "Bridge Structures", + "slug": "bridge-structures", + "book_id": 16, + "created_at": "2021-12-19T15:22:11.000000Z", + "updated_at": "2021-12-21T19:42:29.000000Z", + "url": "https://example.com/books/my-own-book/chapter/bridge-structures", + "type": "chapter", + "pages": [ + { + "id": 42, + "name": "Building Bridges", + "slug": "building-bridges", + "book_id": 16, + "chapter_id": 50, + "draft": false, + "template": false, + "created_at": "2021-12-19T15:22:11.000000Z", + "updated_at": "2022-09-29T13:44:15.000000Z", + "url": "https://example.com/books/my-own-book/page/building-bridges" + } + ] + }, + { + "id": 43, + "name": "Cool Animals", + "slug": "cool-animals", + "book_id": 16, + "chapter_id": 0, + "draft": false, + "template": false, + "created_at": "2021-12-19T18:22:11.000000Z", + "updated_at": "2022-07-29T13:44:15.000000Z", + "url": "https://example.com/books/my-own-book/page/cool-animals" + } + ], "tags": [ { "id": 13, @@ -28,12 +66,12 @@ "cover": { "id": 452, "name": "sjovall_m117hUWMu40.jpg", - "url": "http:\/\/bookstack.local\/uploads\/images\/cover_book\/2020-01\/sjovall_m117hUWMu40.jpg", + "url": "https://example.com/uploads/images/cover_book/2020-01/sjovall_m117hUWMu40.jpg", "created_at": "2020-01-12T14:11:51.000000Z", "updated_at": "2020-01-12T14:11:51.000000Z", "created_by": 1, "updated_by": 1, - "path": "\/uploads\/images\/cover_book\/2020-01\/sjovall_m117hUWMu40.jpg", + "path": "/uploads/images/cover_book/2020-01/sjovall_m117hUWMu40.jpg", "type": "cover_book", "uploaded_to": 16 } diff --git a/tests/Api/BooksApiTest.php b/tests/Api/BooksApiTest.php index f426cff73..017322193 100644 --- a/tests/Api/BooksApiTest.php +++ b/tests/Api/BooksApiTest.php @@ -88,6 +88,38 @@ class BooksApiTest extends TestCase ]); } + public function test_read_endpoint_includes_chapter_and_page_contents() + { + $this->actingAsApiEditor(); + /** @var Book $book */ + $book = Book::visible()->has('chapters')->has('pages')->first(); + $chapter = $book->chapters()->first(); + $chapterPage = $chapter->pages()->first(); + + $resp = $this->getJson($this->baseEndpoint . "/{$book->id}"); + + $directChildCount = $book->directPages()->count() + $book->chapters()->count(); + $resp->assertStatus(200); + $resp->assertJsonCount($directChildCount, 'contents'); + $resp->assertJson([ + 'contents' => [ + [ + 'type' => 'chapter', + 'id' => $chapter->id, + 'name' => $chapter->name, + 'slug' => $chapter->slug, + 'pages' => [ + [ + 'id' => $chapterPage->id, + 'name' => $chapterPage->name, + 'slug' => $chapterPage->slug, + ] + ] + ] + ] + ]); + } + public function test_update_endpoint() { $this->actingAsApiEditor(); From 068a8a068c5d7c7ab98a6ee95baae8d321c3c61f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 29 Sep 2022 16:49:25 +0100 Subject: [PATCH 20/24] Extracted entity testcase methods to own class Also added some new fetch helper methods for future use. --- tests/Api/AttachmentsApiTest.php | 4 +- tests/Api/PagesApiTest.php | 2 +- tests/Api/UsersApiTest.php | 2 +- .../CopyShelfPermissionsCommandTest.php | 4 +- tests/Entity/BookShelfTest.php | 6 +- tests/Entity/BookTest.php | 10 +- tests/Entity/ChapterTest.php | 2 +- tests/Entity/EntityAccessTest.php | 4 +- tests/Entity/EntitySearchTest.php | 30 +-- tests/Entity/PageTest.php | 8 +- tests/Entity/SortTest.php | 36 ++-- tests/Entity/TagTest.php | 4 +- tests/Helpers/EntityProvider.php | 201 ++++++++++++++++++ tests/HomepageTest.php | 10 +- tests/Permissions/EntityPermissionsTest.php | 2 +- tests/Permissions/ExportPermissionsTest.php | 4 +- tests/Permissions/RolesTest.php | 40 ++-- tests/PublicActionTest.php | 2 +- tests/References/CrossLinkParserTest.php | 2 +- tests/References/ReferencesTest.php | 6 +- tests/TestCase.php | 120 +---------- tests/Uploads/ImageTest.php | 6 +- tests/User/UserProfileTest.php | 8 +- 23 files changed, 305 insertions(+), 208 deletions(-) create mode 100644 tests/Helpers/EntityProvider.php diff --git a/tests/Api/AttachmentsApiTest.php b/tests/Api/AttachmentsApiTest.php index 6077868b2..dfd57deb8 100644 --- a/tests/Api/AttachmentsApiTest.php +++ b/tests/Api/AttachmentsApiTest.php @@ -53,7 +53,7 @@ class AttachmentsApiTest extends TestCase $page->restricted = true; $page->save(); - $this->regenEntityPermissions($page); + $this->entities->regenPermissions($page); $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id'); $resp->assertJsonMissing(['data' => [ @@ -264,7 +264,7 @@ class AttachmentsApiTest extends TestCase $page->draft = true; $page->owned_by = $editor->id; $page->save(); - $this->regenEntityPermissions($page); + $this->entities->regenPermissions($page); $attachment = $this->createAttachmentForPage($page, [ 'name' => 'my attachment', diff --git a/tests/Api/PagesApiTest.php b/tests/Api/PagesApiTest.php index 539a7da4e..20c6977dd 100644 --- a/tests/Api/PagesApiTest.php +++ b/tests/Api/PagesApiTest.php @@ -210,7 +210,7 @@ class PagesApiTest extends TestCase $this->actingAsApiEditor(); $page = Page::visible()->first(); $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first(); - $this->setEntityRestrictions($chapter, ['view'], [$this->getEditor()->roles()->first()]); + $this->entities->setPermissions($chapter, ['view'], [$this->getEditor()->roles()->first()]); $details = [ 'name' => 'My updated API page', 'chapter_id' => $chapter->id, diff --git a/tests/Api/UsersApiTest.php b/tests/Api/UsersApiTest.php index ddbdac0f8..739981f24 100644 --- a/tests/Api/UsersApiTest.php +++ b/tests/Api/UsersApiTest.php @@ -239,7 +239,7 @@ class UsersApiTest extends TestCase $user = User::query()->where('id', '!=', $this->getAdmin()->id) ->whereNull('system_name') ->first(); - $entityChain = $this->createEntityChainBelongingToUser($user); + $entityChain = $this->entities->createChainBelongingToUser($user); /** @var User $newOwner */ $newOwner = User::query()->where('id', '!=', $user->id)->first(); diff --git a/tests/Commands/CopyShelfPermissionsCommandTest.php b/tests/Commands/CopyShelfPermissionsCommandTest.php index 5a60b8d55..dd39317ae 100644 --- a/tests/Commands/CopyShelfPermissionsCommandTest.php +++ b/tests/Commands/CopyShelfPermissionsCommandTest.php @@ -22,7 +22,7 @@ class CopyShelfPermissionsCommandTest extends TestCase $this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default'); $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default'); - $this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]); + $this->entities->setPermissions($shelf, ['view', 'update'], [$editorRole]); $this->artisan('bookstack:copy-shelf-permissions', [ '--slug' => $shelf->slug, ]); @@ -43,7 +43,7 @@ class CopyShelfPermissionsCommandTest extends TestCase $this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default'); $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default'); - $this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]); + $this->entities->setPermissions($shelf, ['view', 'update'], [$editorRole]); $this->artisan('bookstack:copy-shelf-permissions --all') ->expectsQuestion('Permission settings for all shelves will be cascaded. Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. Are you sure you want to proceed?', 'y'); $child = $shelf->books()->first(); diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php index 4461c0489..748f63da8 100644 --- a/tests/Entity/BookShelfTest.php +++ b/tests/Entity/BookShelfTest.php @@ -45,7 +45,7 @@ class BookShelfTest extends TestCase $resp = $this->actingAs($user)->get('/'); $this->withHtml($resp)->assertElementNotContains('header', 'Shelves'); - $this->setEntityRestrictions($shelf, ['view'], [$userRole]); + $this->entities->setPermissions($shelf, ['view'], [$userRole]); $resp = $this->get('/'); $this->withHtml($resp)->assertElementContains('header', 'Shelves'); @@ -69,7 +69,7 @@ class BookShelfTest extends TestCase $resp->assertSee($book->name); $resp->assertSee($book->getUrl()); - $this->setEntityRestrictions($book, []); + $this->entities->setPermissions($book, []); $resp = $this->asEditor()->get('/shelves'); $resp->assertDontSee($book->name); @@ -298,7 +298,7 @@ class BookShelfTest extends TestCase $this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default'); $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default'); - $this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]); + $this->entities->setPermissions($shelf, ['view', 'update'], [$editorRole]); $resp = $this->post($shelf->getUrl('/copy-permissions')); $child = $shelf->books()->first(); diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php index 2f04fcf25..ec430ae84 100644 --- a/tests/Entity/BookTest.php +++ b/tests/Entity/BookTest.php @@ -246,13 +246,13 @@ class BookTest extends TestCase public function test_slug_multi_byte_url_safe() { - $book = $this->newBook([ + $book = $this->entities->newBook([ 'name' => 'информация', ]); $this->assertEquals('informaciya', $book->slug); - $book = $this->newBook([ + $book = $this->entities->newBook([ 'name' => '¿Qué?', ]); @@ -261,7 +261,7 @@ class BookTest extends TestCase public function test_slug_format() { - $book = $this->newBook([ + $book = $this->entities->newBook([ 'name' => 'PartA / PartB / PartC', ]); @@ -311,7 +311,7 @@ class BookTest extends TestCase foreach ($book->getDirectChildren() as $child) { $child->restricted = true; $child->save(); - $this->regenEntityPermissions($child); + $this->entities->regenPermissions($child); } $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); @@ -365,7 +365,7 @@ class BookTest extends TestCase $viewer = $this->getViewer(); $this->giveUserPermissions($viewer, ['book-update-all', 'book-create-all', 'bookshelf-update-all']); - $this->setEntityRestrictions($shelfB); + $this->entities->setPermissions($shelfB); $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); diff --git a/tests/Entity/ChapterTest.php b/tests/Entity/ChapterTest.php index d58b83da9..c1c746102 100644 --- a/tests/Entity/ChapterTest.php +++ b/tests/Entity/ChapterTest.php @@ -107,7 +107,7 @@ class ChapterTest extends TestCase foreach ($chapter->pages as $page) { $page->restricted = true; $page->save(); - $this->regenEntityPermissions($page); + $this->entities->regenPermissions($page); } $this->asEditor()->post($chapter->getUrl('/copy'), [ diff --git a/tests/Entity/EntityAccessTest.php b/tests/Entity/EntityAccessTest.php index f2f244538..e3d129d5e 100644 --- a/tests/Entity/EntityAccessTest.php +++ b/tests/Entity/EntityAccessTest.php @@ -14,7 +14,7 @@ class EntityAccessTest extends TestCase // Create required assets and revisions $creator = $this->getEditor(); $updater = $this->getViewer(); - $entities = $this->createEntityChainBelongingToUser($creator, $updater); + $entities = $this->entities->createChainBelongingToUser($creator, $updater); app()->make(UserRepo::class)->destroy($creator); app()->make(PageRepo::class)->update($entities['page'], ['html' => '

hello!

>']); @@ -26,7 +26,7 @@ class EntityAccessTest extends TestCase // Create required assets and revisions $creator = $this->getViewer(); $updater = $this->getEditor(); - $entities = $this->createEntityChainBelongingToUser($creator, $updater); + $entities = $this->entities->createChainBelongingToUser($creator, $updater); app()->make(UserRepo::class)->destroy($updater); app()->make(PageRepo::class)->update($entities['page'], ['html' => '

Hello there!

']); diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index 3a9b9f31b..eabcf6f76 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -47,7 +47,7 @@ class EntitySearchTest extends TestCase public function test_searching_accents_and_small_terms() { - $page = $this->newPage(['name' => 'My new test quaffleachits', 'html' => 'some áéííúü¿¡ test content a2 orange dog']); + $page = $this->entities->newPage(['name' => 'My new test quaffleachits', 'html' => 'some áéííúü¿¡ test content a2 orange dog']); $this->asEditor(); $accentSearch = $this->get('/search?term=' . urlencode('áéíí')); @@ -111,7 +111,7 @@ class EntitySearchTest extends TestCase public function test_exact_searches() { - $page = $this->newPage(['name' => 'My new test page', 'html' => 'this is a story about an orange donkey']); + $page = $this->entities->newPage(['name' => 'My new test page', 'html' => 'this is a story about an orange donkey']); $exactSearchA = $this->asEditor()->get('/search?term=' . urlencode('"story about an orange"')); $exactSearchA->assertStatus(200)->assertSee($page->name); @@ -123,7 +123,7 @@ class EntitySearchTest extends TestCase public function test_search_terms_with_delimiters_are_converted_to_exact_matches() { $this->asEditor(); - $page = $this->newPage(['name' => 'Delimiter test', 'html' => '

1.1 2,2 3?3 4:4 5;5 (8) <9> "10" \'11\' `12`

']); + $page = $this->entities->newPage(['name' => 'Delimiter test', 'html' => '

1.1 2,2 3?3 4:4 5;5 (8) <9> "10" \'11\' `12`

']); $terms = explode(' ', '1.1 2,2 3?3 4:4 5;5 (8) <9> "10" \'11\' `12`'); foreach ($terms as $term) { @@ -134,7 +134,7 @@ class EntitySearchTest extends TestCase public function test_search_filters() { - $page = $this->newPage(['name' => 'My new test quaffleachits', 'html' => 'this is about an orange donkey danzorbhsing']); + $page = $this->entities->newPage(['name' => 'My new test quaffleachits', 'html' => 'this is about an orange donkey danzorbhsing']); $this->asEditor(); $editorId = $this->getEditor()->id; $editorSlug = $this->getEditor()->slug; @@ -197,7 +197,7 @@ class EntitySearchTest extends TestCase public function test_ajax_entity_search() { - $page = $this->newPage(['name' => 'my ajax search test', 'html' => 'ajax test']); + $page = $this->entities->newPage(['name' => 'my ajax search test', 'html' => 'ajax test']); $notVisitedPage = Page::first(); // Visit the page to make popular @@ -334,15 +334,15 @@ class EntitySearchTest extends TestCase public function test_search_ranks_common_words_lower() { - $this->newPage(['name' => 'Test page A', 'html' => '

dog biscuit dog dog

']); - $this->newPage(['name' => 'Test page B', 'html' => '

cat biscuit

']); + $this->entities->newPage(['name' => 'Test page A', 'html' => '

dog biscuit dog dog

']); + $this->entities->newPage(['name' => 'Test page B', 'html' => '

cat biscuit

']); $search = $this->asEditor()->get('/search?term=cat+dog+biscuit'); $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(1)', 'Test page A'); $this->withHtml($search)->assertElementContains('.entity-list > .page:nth-child(2)', 'Test page B'); for ($i = 0; $i < 2; $i++) { - $this->newPage(['name' => 'Test page ' . $i, 'html' => '

dog

']); + $this->entities->newPage(['name' => 'Test page ' . $i, 'html' => '

dog

']); } $search = $this->asEditor()->get('/search?term=cat+dog+biscuit'); @@ -352,7 +352,7 @@ class EntitySearchTest extends TestCase public function test_terms_in_headers_have_an_adjusted_index_score() { - $page = $this->newPage(['name' => 'Test page A', 'html' => ' + $page = $this->entities->newPage(['name' => 'Test page A', 'html' => '

TermA

TermB TermNested

TermC

@@ -377,7 +377,7 @@ class EntitySearchTest extends TestCase public function test_name_and_content_terms_are_merged_to_single_score() { - $page = $this->newPage(['name' => 'TermA', 'html' => ' + $page = $this->entities->newPage(['name' => 'TermA', 'html' => '

TermA

']); @@ -389,7 +389,7 @@ class EntitySearchTest extends TestCase public function test_tag_names_and_values_are_indexed_for_search() { - $page = $this->newPage(['name' => 'PageA', 'html' => '

content

', 'tags' => [ + $page = $this->entities->newPage(['name' => 'PageA', 'html' => '

content

', 'tags' => [ ['name' => 'Animal', 'value' => 'MeowieCat'], ['name' => 'SuperImportant'], ]]); @@ -402,7 +402,7 @@ class EntitySearchTest extends TestCase public function test_matching_terms_in_search_results_are_highlighted() { - $this->newPage(['name' => 'My Meowie Cat', 'html' => '

A superimportant page about meowieable animals

', 'tags' => [ + $this->entities->newPage(['name' => 'My Meowie Cat', 'html' => '

A superimportant page about meowieable animals

', 'tags' => [ ['name' => 'Animal', 'value' => 'MeowieCat'], ['name' => 'SuperImportant'], ]]); @@ -420,7 +420,7 @@ class EntitySearchTest extends TestCase public function test_match_highlighting_works_with_multibyte_content() { - $this->newPage([ + $this->entities->newPage([ 'name' => 'Test Page', 'html' => '

На мен ми трябва нещо добро test

', ]); @@ -431,7 +431,7 @@ class EntitySearchTest extends TestCase public function test_html_entities_in_item_details_remains_escaped_in_search_results() { - $this->newPage(['name' => 'My TestPageContent', 'html' => '

My supercool <great> TestPageContent page

']); + $this->entities->newPage(['name' => 'My TestPageContent', 'html' => '

My supercool <great> TestPageContent page

']); $search = $this->asEditor()->get('/search?term=TestPageContent'); $search->assertSee('My <cool> TestPageContent', false); @@ -440,7 +440,7 @@ class EntitySearchTest extends TestCase public function test_words_adjacent_to_lines_breaks_can_be_matched_with_normal_terms() { - $page = $this->newPage(['name' => 'TermA', 'html' => ' + $page = $this->entities->newPage(['name' => 'TermA', 'html' => '

TermA
TermB
TermC

']); diff --git a/tests/Entity/PageTest.php b/tests/Entity/PageTest.php index 734516e87..0f906460b 100644 --- a/tests/Entity/PageTest.php +++ b/tests/Entity/PageTest.php @@ -201,7 +201,7 @@ class PageTest extends TestCase $newBook->owned_by = $viewer->id; $newBook->save(); $this->giveUserPermissions($viewer, ['page-create-own']); - $this->regenEntityPermissions($newBook); + $this->entities->regenPermissions($newBook); $resp = $this->actingAs($viewer)->get($page->getUrl()); $resp->assertSee($page->getUrl('/copy')); @@ -255,7 +255,7 @@ class PageTest extends TestCase public function test_recently_updated_pages_view() { $user = $this->getEditor(); - $content = $this->createEntityChainBelongingToUser($user); + $content = $this->entities->createChainBelongingToUser($user); $resp = $this->asAdmin()->get('/pages/recently-updated'); $this->withHtml($resp)->assertElementContains('.entity-list .page:nth-child(1)', $content['page']->name); @@ -303,8 +303,8 @@ class PageTest extends TestCase 'html' => '

Updated content

', ]); - $this->setEntityRestrictions($page->book); - $this->setEntityRestrictions($page, ['view'], [$user->roles->first()]); + $this->entities->setPermissions($page->book); + $this->entities->setPermissions($page, ['view'], [$user->roles->first()]); $resp = $this->get('/pages/recently-updated'); $resp->assertDontSee($page->book->getShortName(42)); diff --git a/tests/Entity/SortTest.php b/tests/Entity/SortTest.php index 8792e70db..93b668a0e 100644 --- a/tests/Entity/SortTest.php +++ b/tests/Entity/SortTest.php @@ -98,14 +98,14 @@ class SortTest extends TestCase $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $editor = $this->getEditor(); - $this->setEntityRestrictions($newBook, ['view', 'update', 'delete'], $editor->roles->all()); + $this->entities->setPermissions($newBook, ['view', 'update', 'delete'], $editor->roles->all()); $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); $this->assertPermissionError($movePageResp); - $this->setEntityRestrictions($newBook, ['view', 'update', 'delete', 'create'], $editor->roles->all()); + $this->entities->setPermissions($newBook, ['view', 'update', 'delete', 'create'], $editor->roles->all()); $movePageResp = $this->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); @@ -123,8 +123,8 @@ class SortTest extends TestCase $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $editor = $this->getEditor(); - $this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); - $this->setEntityRestrictions($page, ['view', 'update', 'create'], $editor->roles->all()); + $this->entities->setPermissions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); + $this->entities->setPermissions($page, ['view', 'update', 'create'], $editor->roles->all()); $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, @@ -133,7 +133,7 @@ class SortTest extends TestCase $pageView = $this->get($page->getUrl()); $pageView->assertDontSee($page->getUrl('/move')); - $this->setEntityRestrictions($page, ['view', 'update', 'create', 'delete'], $editor->roles->all()); + $this->entities->setPermissions($page, ['view', 'update', 'create', 'delete'], $editor->roles->all()); $movePageResp = $this->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); @@ -178,8 +178,8 @@ class SortTest extends TestCase $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $editor = $this->getEditor(); - $this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); - $this->setEntityRestrictions($chapter, ['view', 'update', 'create'], $editor->roles->all()); + $this->entities->setPermissions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); + $this->entities->setPermissions($chapter, ['view', 'update', 'create'], $editor->roles->all()); $moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, @@ -188,7 +188,7 @@ class SortTest extends TestCase $pageView = $this->get($chapter->getUrl()); $pageView->assertDontSee($chapter->getUrl('/move')); - $this->setEntityRestrictions($chapter, ['view', 'update', 'create', 'delete'], $editor->roles->all()); + $this->entities->setPermissions($chapter, ['view', 'update', 'create', 'delete'], $editor->roles->all()); $moveChapterResp = $this->put($chapter->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); @@ -205,15 +205,15 @@ class SortTest extends TestCase $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $editor = $this->getEditor(); - $this->setEntityRestrictions($newBook, ['view', 'update', 'delete'], [$editor->roles->first()]); - $this->setEntityRestrictions($chapter, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]); + $this->entities->setPermissions($newBook, ['view', 'update', 'delete'], [$editor->roles->first()]); + $this->entities->setPermissions($chapter, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]); $moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); $this->assertPermissionError($moveChapterResp); - $this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]); + $this->entities->setPermissions($newBook, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]); $moveChapterResp = $this->put($chapter->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); @@ -257,8 +257,8 @@ class SortTest extends TestCase public function test_book_sort() { $oldBook = Book::query()->first(); - $chapterToMove = $this->newChapter(['name' => 'chapter to move'], $oldBook); - $newBook = $this->newBook(['name' => 'New sort book']); + $chapterToMove = $this->entities->newChapter(['name' => 'chapter to move'], $oldBook); + $newBook = $this->entities->newBook(['name' => 'New sort book']); $pagesToMove = Page::query()->take(5)->get(); // Create request data @@ -323,7 +323,7 @@ class SortTest extends TestCase $page = Page::query()->where('chapter_id', '!=', 0)->first(); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); - $this->setEntityRestrictions($otherChapter); + $this->entities->setPermissions($otherChapter); $sortData = [ 'id' => $page->id, @@ -346,7 +346,7 @@ class SortTest extends TestCase /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $editor = $this->getEditor(); - $this->setEntityRestrictions($otherChapter->book, ['update', 'delete'], [$editor->roles()->first()]); + $this->entities->setPermissions($otherChapter->book, ['update', 'delete'], [$editor->roles()->first()]); $sortData = [ 'id' => $page->id, @@ -369,7 +369,7 @@ class SortTest extends TestCase /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $editor = $this->getEditor(); - $this->setEntityRestrictions($otherChapter, ['view', 'delete'], [$editor->roles()->first()]); + $this->entities->setPermissions($otherChapter, ['view', 'delete'], [$editor->roles()->first()]); $sortData = [ 'id' => $page->id, @@ -392,7 +392,7 @@ class SortTest extends TestCase /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $editor = $this->getEditor(); - $this->setEntityRestrictions($page, ['view', 'delete'], [$editor->roles()->first()]); + $this->entities->setPermissions($page, ['view', 'delete'], [$editor->roles()->first()]); $sortData = [ 'id' => $page->id, @@ -415,7 +415,7 @@ class SortTest extends TestCase /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $editor = $this->getEditor(); - $this->setEntityRestrictions($page, ['view', 'update'], [$editor->roles()->first()]); + $this->entities->setPermissions($page, ['view', 'update'], [$editor->roles()->first()]); $sortData = [ 'id' => $page->id, diff --git a/tests/Entity/TagTest.php b/tests/Entity/TagTest.php index 1d2c9124f..d22dc2f44 100644 --- a/tests/Entity/TagTest.php +++ b/tests/Entity/TagTest.php @@ -188,7 +188,7 @@ class TagTest extends TestCase $resp->assertSee('GreatTestContent'); $page->restricted = true; - $this->regenEntityPermissions($page); + $this->entities->regenPermissions($page); $resp = $this->asEditor()->get('/tags'); $resp->assertDontSee('SuperCategory'); @@ -207,7 +207,7 @@ class TagTest extends TestCase { $this->asEditor(); - foreach ($this->getEachEntityType() as $entity) { + foreach ($this->entities->all() as $entity) { $entity->tags()->create(['name' => 'My Super Tag Name', 'value' => 'An-awesome-value']); $html = $this->withHtml($this->get($entity->getUrl())); $html->assertElementExists('body.tag-name-mysupertagname.tag-value-anawesomevalue.tag-pair-mysupertagname-anawesomevalue'); diff --git a/tests/Helpers/EntityProvider.php b/tests/Helpers/EntityProvider.php new file mode 100644 index 000000000..d3888e71f --- /dev/null +++ b/tests/Helpers/EntityProvider.php @@ -0,0 +1,201 @@ + + */ + protected array $fetchCache = [ + 'book' => [], + 'page' => [], + 'bookshelf' => [], + 'chapter' => [], + ]; + + /** + * Get an un-fetched page from the system. + */ + public function page(): Page + { + /** @var Page $page */ + $page = Page::query()->whereNotIn('id', $this->fetchCache['page'])->first(); + $this->addToCache($page); + return $page; + } + + /** + * Get an un-fetched chapter from the system. + */ + public function chapter(): Chapter + { + /** @var Chapter $chapter */ + $chapter = Chapter::query()->whereNotIn('id', $this->fetchCache['chapter'])->first(); + $this->addToCache($chapter); + return $chapter; + } + + /** + * Get an un-fetched book from the system. + */ + public function book(): Book + { + /** @var Book $book */ + $book = Book::query()->whereNotIn('id', $this->fetchCache['book'])->first(); + $this->addToCache($book); + return $book; + } + + /** + * Get an un-fetched shelf from the system. + */ + public function shelf(): Bookshelf + { + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->whereNotIn('id', $this->fetchCache['bookshelf'])->first(); + $this->addToCache($shelf); + return $shelf; + } + + /** + * Get all entity types from the system. + * @return array{page: Page, chapter: Chapter, book: Book, bookshelf: Bookshelf} + */ + public function all(): array + { + return [ + 'page' => $this->page(), + 'chapter' => $this->chapter(), + 'book' => $this->book(), + 'bookshelf' => $this->shelf(), + ]; + } + + /** + * Create a book to page chain of entities that belong to a specific user. + * @return array{book: Book, chapter: Chapter, page: Page} + */ + public function createChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array + { + if (empty($updaterUser)) { + $updaterUser = $creatorUser; + } + + $userAttrs = ['created_by' => $creatorUser->id, 'owned_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]; + /** @var Book $book */ + $book = Book::factory()->create($userAttrs); + $chapter = Chapter::factory()->create(array_merge(['book_id' => $book->id], $userAttrs)); + $page = Page::factory()->create(array_merge(['book_id' => $book->id, 'chapter_id' => $chapter->id], $userAttrs)); + + $book->rebuildPermissions(); + $this->addToCache([$page, $chapter, $book]); + + return compact('book', 'chapter', 'page'); + } + + /** + * Create and return a new bookshelf. + */ + public function newShelf(array $input = ['name' => 'test shelf', 'description' => 'My new test shelf']): Bookshelf + { + $shelf = app(BookshelfRepo::class)->create($input, []); + $this->addToCache($shelf); + return $shelf; + } + + /** + * Create and return a new book. + */ + public function newBook(array $input = ['name' => 'test book', 'description' => 'My new test book']): Book + { + $book = app(BookRepo::class)->create($input); + $this->addToCache($book); + return $book; + } + + /** + * Create and return a new test chapter. + */ + public function newChapter(array $input, Book $book): Chapter + { + $chapter = app(ChapterRepo::class)->create($input, $book); + $this->addToCache($chapter); + return $chapter; + } + + /** + * Create and return a new test page. + */ + public function newPage(array $input = ['name' => 'test page', 'html' => 'My new test page']): Page + { + $book = Book::query()->first(); + $pageRepo = app(PageRepo::class); + $draftPage = $pageRepo->getNewDraftPage($book); + $this->addToCache($draftPage); + return $pageRepo->publishDraft($draftPage, $input); + } + + /** + * Regenerate the permission for an entity. + * Centralised to manage clearing of cached elements between requests. + */ + public function regenPermissions(Entity $entity): void + { + $entity->rebuildPermissions(); + $entity->load('jointPermissions'); + } + + /** + * Set the given entity as having restricted permissions, and apply the given + * permissions for the given roles. + * @param string[] $actions + * @param Role[] $roles + */ + public function setPermissions(Entity $entity, array $actions = [], array $roles = []): void + { + $entity->restricted = true; + $entity->permissions()->delete(); + + $permissions = []; + foreach ($actions as $action) { + foreach ($roles as $role) { + $permissions[] = [ + 'role_id' => $role->id, + 'action' => strtolower($action), + ]; + } + } + + $entity->permissions()->createMany($permissions); + $entity->save(); + $entity->load('permissions'); + $this->regenPermissions($entity); + } + + /** + * @param Entity|Entity[] $entities + */ + protected function addToCache($entities): void + { + if (!is_array($entities)) { + $entities = [$entities]; + } + + foreach ($entities as $entity) { + $this->fetchCache[$entity->getType()][] = $entity->id; + } + } +} diff --git a/tests/HomepageTest.php b/tests/HomepageTest.php index 1d968a2c9..bb42f49f2 100644 --- a/tests/HomepageTest.php +++ b/tests/HomepageTest.php @@ -24,7 +24,7 @@ class HomepageTest extends TestCase $this->asEditor(); $name = 'My custom homepage'; $content = str_repeat('This is the body content of my custom homepage.', 20); - $customPage = $this->newPage(['name' => $name, 'html' => $content]); + $customPage = $this->entities->newPage(['name' => $name, 'html' => $content]); $this->setSettings(['app-homepage' => $customPage->id]); $this->setSettings(['app-homepage-type' => 'page']); @@ -41,7 +41,7 @@ class HomepageTest extends TestCase $this->asEditor(); $name = 'My custom homepage'; $content = str_repeat('This is the body content of my custom homepage.', 20); - $customPage = $this->newPage(['name' => $name, 'html' => $content]); + $customPage = $this->entities->newPage(['name' => $name, 'html' => $content]); $this->setSettings([ 'app-homepage' => $customPage->id, 'app-homepage-type' => 'page', @@ -67,7 +67,7 @@ class HomepageTest extends TestCase $this->asEditor(); $name = 'My custom homepage'; $content = str_repeat('This is the body content of my custom homepage.', 20); - $customPage = $this->newPage(['name' => $name, 'html' => $content]); + $customPage = $this->entities->newPage(['name' => $name, 'html' => $content]); $this->setSettings([ 'app-homepage' => $customPage->id, 'app-homepage-type' => 'default', @@ -107,7 +107,7 @@ class HomepageTest extends TestCase $included->save(); $name = 'My custom homepage'; - $customPage = $this->newPage(['name' => $name, 'html' => '{{@' . $included->id . '}}']); + $customPage = $this->entities->newPage(['name' => $name, 'html' => '{{@' . $included->id . '}}']); $this->setSettings(['app-homepage' => $customPage->id]); $this->setSettings(['app-homepage-type' => 'page']); @@ -177,7 +177,7 @@ class HomepageTest extends TestCase $this->withHtml($homeVisit)->assertElementNotContains('.content-wrap', $book->name); // Ensure is visible again with entity-level view permission - $this->setEntityRestrictions($book, ['view'], [$editor->roles()->first()]); + $this->entities->setPermissions($book, ['view'], [$editor->roles()->first()]); $homeVisit = $this->get('/'); $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $shelf->name); $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $book->name); diff --git a/tests/Permissions/EntityPermissionsTest.php b/tests/Permissions/EntityPermissionsTest.php index ed037d3ac..9e80b752a 100644 --- a/tests/Permissions/EntityPermissionsTest.php +++ b/tests/Permissions/EntityPermissionsTest.php @@ -36,7 +36,7 @@ class EntityPermissionsTest extends TestCase $this->user->roles->first(), $this->viewer->roles->first(), ]; - $this->setEntityRestrictions($entity, $actions, $roles); + $this->entities->setPermissions($entity, $actions, $roles); } public function test_bookshelf_view_restriction() diff --git a/tests/Permissions/ExportPermissionsTest.php b/tests/Permissions/ExportPermissionsTest.php index 2e3d84fa1..7e9ce6100 100644 --- a/tests/Permissions/ExportPermissionsTest.php +++ b/tests/Permissions/ExportPermissionsTest.php @@ -27,7 +27,7 @@ class ExportPermissionsTest extends TestCase $resp->assertSee($pageContent); } - $this->setEntityRestrictions($page, []); + $this->entities->setPermissions($page, []); foreach ($formats as $format) { $resp = $this->get($chapter->getUrl("export/{$format}")); @@ -55,7 +55,7 @@ class ExportPermissionsTest extends TestCase $resp->assertSee($pageContent); } - $this->setEntityRestrictions($page, []); + $this->entities->setPermissions($page, []); foreach ($formats as $format) { $resp = $this->get($book->getUrl("export/{$format}")); diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php index b992bfecc..a24d5f8d8 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolesTest.php @@ -285,7 +285,7 @@ class RolesTest extends TestCase { /** @var Page $otherUsersPage */ $otherUsersPage = Page::query()->first(); - $content = $this->createEntityChainBelongingToUser($this->user); + $content = $this->entities->createChainBelongingToUser($this->user); // Set a different creator on the page we're checking to ensure // that the owner fields are checked @@ -355,9 +355,9 @@ class RolesTest extends TestCase { /** @var Bookshelf $otherShelf */ $otherShelf = Bookshelf::query()->first(); - $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); + $ownShelf = $this->entities->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); - $this->regenEntityPermissions($ownShelf); + $this->entities->regenPermissions($ownShelf); $this->checkAccessPermission('bookshelf-update-own', [ $ownShelf->getUrl('/edit'), @@ -386,9 +386,9 @@ class RolesTest extends TestCase $this->giveUserPermissions($this->user, ['bookshelf-update-all']); /** @var Bookshelf $otherShelf */ $otherShelf = Bookshelf::query()->first(); - $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); + $ownShelf = $this->entities->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); - $this->regenEntityPermissions($ownShelf); + $this->entities->regenPermissions($ownShelf); $this->checkAccessPermission('bookshelf-delete-own', [ $ownShelf->getUrl('/delete'), @@ -438,7 +438,7 @@ class RolesTest extends TestCase { /** @var Book $otherBook */ $otherBook = Book::query()->take(1)->get()->first(); - $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; + $ownBook = $this->entities->createChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('book-update-own', [ $ownBook->getUrl() . '/edit', ], [ @@ -466,7 +466,7 @@ class RolesTest extends TestCase $this->giveUserPermissions($this->user, ['book-update-all']); /** @var Book $otherBook */ $otherBook = Book::query()->take(1)->get()->first(); - $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; + $ownBook = $this->entities->createChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('book-delete-own', [ $ownBook->getUrl() . '/delete', ], [ @@ -501,7 +501,7 @@ class RolesTest extends TestCase { /** @var Book $book */ $book = Book::query()->take(1)->get()->first(); - $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; + $ownBook = $this->entities->createChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('chapter-create-own', [ $ownBook->getUrl('/create-chapter'), ], [ @@ -538,7 +538,7 @@ class RolesTest extends TestCase { /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->first(); - $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter']; + $ownChapter = $this->entities->createChainBelongingToUser($this->user)['chapter']; $this->checkAccessPermission('chapter-update-own', [ $ownChapter->getUrl() . '/edit', ], [ @@ -566,7 +566,7 @@ class RolesTest extends TestCase $this->giveUserPermissions($this->user, ['chapter-update-all']); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->first(); - $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter']; + $ownChapter = $this->entities->createChainBelongingToUser($this->user)['chapter']; $this->checkAccessPermission('chapter-delete-own', [ $ownChapter->getUrl() . '/delete', ], [ @@ -608,7 +608,7 @@ class RolesTest extends TestCase /** @var Chapter $chapter */ $chapter = Chapter::query()->first(); - $entities = $this->createEntityChainBelongingToUser($this->user); + $entities = $this->entities->createChainBelongingToUser($this->user); $ownBook = $entities['book']; $ownChapter = $entities['chapter']; @@ -699,7 +699,7 @@ class RolesTest extends TestCase { /** @var Page $otherPage */ $otherPage = Page::query()->first(); - $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; $this->checkAccessPermission('page-update-own', [ $ownPage->getUrl() . '/edit', ], [ @@ -727,7 +727,7 @@ class RolesTest extends TestCase $this->giveUserPermissions($this->user, ['page-update-all']); /** @var Page $otherPage */ $otherPage = Page::query()->first(); - $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; $this->checkAccessPermission('page-delete-own', [ $ownPage->getUrl() . '/delete', ], [ @@ -865,14 +865,14 @@ class RolesTest extends TestCase $admin = $this->getAdmin(); // Book links $book = Book::factory()->create(['created_by' => $admin->id, 'updated_by' => $admin->id]); - $this->regenEntityPermissions($book); + $this->entities->regenPermissions($book); $this->actingAs($this->getViewer())->get($book->getUrl()) ->assertDontSee('Create a new page') ->assertDontSee('Add a chapter'); // Chapter links $chapter = Chapter::factory()->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]); - $this->regenEntityPermissions($chapter); + $this->entities->regenPermissions($chapter); $this->actingAs($this->getViewer())->get($chapter->getUrl()) ->assertDontSee('Create a new page') ->assertDontSee('Sort the current book'); @@ -880,7 +880,7 @@ class RolesTest extends TestCase public function test_comment_create_permission() { - $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; $this->actingAs($this->user) ->addComment($ownPage) @@ -895,7 +895,7 @@ class RolesTest extends TestCase public function test_comment_update_own_permission() { - $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; $this->giveUserPermissions($this->user, ['comment-create-all']); $this->actingAs($this->user)->addComment($ownPage); /** @var Comment $comment */ @@ -913,7 +913,7 @@ class RolesTest extends TestCase public function test_comment_update_all_permission() { /** @var Page $ownPage */ - $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; $this->asAdmin()->addComment($ownPage); /** @var Comment $comment */ $comment = $ownPage->comments()->latest()->first(); @@ -930,7 +930,7 @@ class RolesTest extends TestCase public function test_comment_delete_own_permission() { /** @var Page $ownPage */ - $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; $this->giveUserPermissions($this->user, ['comment-create-all']); $this->actingAs($this->user)->addComment($ownPage); @@ -949,7 +949,7 @@ class RolesTest extends TestCase public function test_comment_delete_all_permission() { /** @var Page $ownPage */ - $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; $this->asAdmin()->addComment($ownPage); /** @var Comment $comment */ $comment = $ownPage->comments()->latest()->first(); diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php index 178e23e2e..309e09600 100644 --- a/tests/PublicActionTest.php +++ b/tests/PublicActionTest.php @@ -177,7 +177,7 @@ class PublicActionTest extends TestCase $this->setSettings(['app-public' => 'true']); /** @var Book $book */ $book = Book::query()->first(); - $this->setEntityRestrictions($book); + $this->entities->setPermissions($book); $resp = $this->get($book->getUrl()); $resp->assertSee('Book not found'); diff --git a/tests/References/CrossLinkParserTest.php b/tests/References/CrossLinkParserTest.php index 856b699c3..43b8a36ae 100644 --- a/tests/References/CrossLinkParserTest.php +++ b/tests/References/CrossLinkParserTest.php @@ -10,7 +10,7 @@ class CrossLinkParserTest extends TestCase { public function test_instance_with_entity_resolvers_matches_entity_links() { - $entities = $this->getEachEntityType(); + $entities = $this->entities->all(); $otherPage = Page::query()->where('id', '!=', $entities['page']->id)->first(); $html = ' diff --git a/tests/References/ReferencesTest.php b/tests/References/ReferencesTest.php index 3fd68d647..59263ee0c 100644 --- a/tests/References/ReferencesTest.php +++ b/tests/References/ReferencesTest.php @@ -57,7 +57,7 @@ class ReferencesTest extends TestCase public function test_references_to_count_visible_on_entity_show_view() { - $entities = $this->getEachEntityType(); + $entities = $this->entities->all(); /** @var Page $otherPage */ $otherPage = Page::query()->where('id', '!=', $entities['page']->id)->first(); @@ -79,7 +79,7 @@ class ReferencesTest extends TestCase public function test_references_to_visible_on_references_page() { - $entities = $this->getEachEntityType(); + $entities = $this->entities->all(); $this->asEditor(); foreach ($entities as $entity) { $this->createReference($entities['page'], $entity); @@ -101,7 +101,7 @@ class ReferencesTest extends TestCase $pageB = Page::query()->where('id', '!=', $page->id)->first(); $this->createReference($pageB, $page); - $this->setEntityRestrictions($pageB); + $this->entities->setPermissions($pageB); $this->asEditor()->get($page->getUrl('/references'))->assertDontSee($pageB->name); $this->asAdmin()->get($page->getUrl('/references'))->assertSee($pageB->name); diff --git a/tests/TestCase.php b/tests/TestCase.php index 594194168..cc8e57453 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,15 +7,7 @@ use BookStack\Auth\Permissions\PermissionsRepo; use BookStack\Auth\Permissions\RolePermission; use BookStack\Auth\Role; use BookStack\Auth\User; -use BookStack\Entities\Models\Book; -use BookStack\Entities\Models\Bookshelf; -use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; -use BookStack\Entities\Models\Page; -use BookStack\Entities\Repos\BookRepo; -use BookStack\Entities\Repos\BookshelfRepo; -use BookStack\Entities\Repos\ChapterRepo; -use BookStack\Entities\Repos\PageRepo; use BookStack\Settings\SettingService; use BookStack\Uploads\HttpFetcher; use GuzzleHttp\Client; @@ -34,6 +26,7 @@ use Monolog\Handler\TestHandler; use Monolog\Logger; use Psr\Http\Client\ClientInterface; use Ssddanbrown\AssertHtml\TestsHtml; +use Tests\Helpers\EntityProvider; abstract class TestCase extends BaseTestCase { @@ -43,6 +36,13 @@ abstract class TestCase extends BaseTestCase protected ?User $admin = null; protected ?User $editor = null; + protected EntityProvider $entities; + + protected function setUp(): void + { + $this->entities = new EntityProvider(); + parent::setUp(); + } /** * The base URL to use while testing the application. @@ -135,51 +135,6 @@ abstract class TestCase extends BaseTestCase return User::query()->where('system_name', '=', null)->get()->last(); } - /** - * Regenerate the permission for an entity. - */ - protected function regenEntityPermissions(Entity $entity): void - { - $entity->rebuildPermissions(); - $entity->load('jointPermissions'); - } - - /** - * Create and return a new bookshelf. - */ - public function newShelf(array $input = ['name' => 'test shelf', 'description' => 'My new test shelf']): Bookshelf - { - return app(BookshelfRepo::class)->create($input, []); - } - - /** - * Create and return a new book. - */ - public function newBook(array $input = ['name' => 'test book', 'description' => 'My new test book']): Book - { - return app(BookRepo::class)->create($input); - } - - /** - * Create and return a new test chapter. - */ - public function newChapter(array $input, Book $book): Chapter - { - return app(ChapterRepo::class)->create($input, $book); - } - - /** - * Create and return a new test page. - */ - public function newPage(array $input = ['name' => 'test page', 'html' => 'My new test page']): Page - { - $book = Book::query()->first(); - $pageRepo = app(PageRepo::class); - $draftPage = $pageRepo->getNewDraftPage($book); - - return $pageRepo->publishDraft($draftPage, $input); - } - /** * Quickly sets an array of settings. */ @@ -191,31 +146,6 @@ abstract class TestCase extends BaseTestCase } } - /** - * Manually set some permissions on an entity. - */ - protected function setEntityRestrictions(Entity $entity, array $actions = [], array $roles = []): void - { - $entity->restricted = true; - $entity->permissions()->delete(); - - $permissions = []; - foreach ($actions as $action) { - foreach ($roles as $role) { - $permissions[] = [ - 'role_id' => $role->id, - 'action' => strtolower($action), - ]; - } - } - $entity->permissions()->createMany($permissions); - - $entity->save(); - $entity->load('permissions'); - $this->app->make(JointPermissionBuilder::class)->rebuildForEntity($entity); - $entity->load('jointPermissions'); - } - /** * Give the given user some permissions. */ @@ -262,27 +192,6 @@ abstract class TestCase extends BaseTestCase return $permissionRepo->saveNewRole($roleData); } - /** - * Create a group of entities that belong to a specific user. - * - * @return array{book: Book, chapter: Chapter, page: Page} - */ - protected function createEntityChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array - { - if (empty($updaterUser)) { - $updaterUser = $creatorUser; - } - - $userAttrs = ['created_by' => $creatorUser->id, 'owned_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]; - $book = Book::factory()->create($userAttrs); - $chapter = Chapter::factory()->create(array_merge(['book_id' => $book->id], $userAttrs)); - $page = Page::factory()->create(array_merge(['book_id' => $book->id, 'chapter_id' => $chapter->id], $userAttrs)); - - $this->app->make(JointPermissionBuilder::class)->rebuildForEntity($book); - - return compact('book', 'chapter', 'page'); - } - /** * Mock the HttpFetcher service and return the given data on fetch. */ @@ -460,17 +369,4 @@ abstract class TestCase extends BaseTestCase $this->assertDatabaseHas('activities', $detailsToCheck); } - - /** - * @return array{page: Page, chapter: Chapter, book: Book, bookshelf: Bookshelf} - */ - protected function getEachEntityType(): array - { - return [ - 'page' => Page::query()->first(), - 'chapter' => Chapter::query()->first(), - 'book' => Book::query()->first(), - 'bookshelf' => Bookshelf::query()->first(), - ]; - } } diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index 84f9e47f4..184da214c 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -342,7 +342,7 @@ class ImageTest extends TestCase $this->get($expectedUrl)->assertOk(); - $this->setEntityRestrictions($page, [], []); + $this->entities->setPermissions($page, [], []); $resp = $this->get($expectedUrl); $resp->assertNotFound(); @@ -367,7 +367,7 @@ class ImageTest extends TestCase $this->get($expectedUrl)->assertOk(); - $this->setEntityRestrictions($page, [], []); + $this->entities->setPermissions($page, [], []); $resp = $this->get($expectedUrl); $resp->assertNotFound(); @@ -400,7 +400,7 @@ class ImageTest extends TestCase $export = $this->get($pageB->getUrl('/export/html')); $this->assertStringContainsString($encodedImageContent, $export->getContent()); - $this->setEntityRestrictions($pageA, [], []); + $this->entities->setPermissions($pageA, [], []); $export = $this->get($pageB->getUrl('/export/html')); $this->assertStringNotContainsString($encodedImageContent, $export->getContent()); diff --git a/tests/User/UserProfileTest.php b/tests/User/UserProfileTest.php index e6136962a..77f1644a5 100644 --- a/tests/User/UserProfileTest.php +++ b/tests/User/UserProfileTest.php @@ -29,7 +29,7 @@ class UserProfileTest extends TestCase public function test_profile_page_shows_recent_entities() { - $content = $this->createEntityChainBelongingToUser($this->user, $this->user); + $content = $this->entities->createChainBelongingToUser($this->user, $this->user); $resp = $this->asAdmin()->get('/user/' . $this->user->slug); // Check the recently created page is shown @@ -50,7 +50,7 @@ class UserProfileTest extends TestCase ->assertElementContains('#content-counts', '0 Chapters') ->assertElementContains('#content-counts', '0 Pages'); - $this->createEntityChainBelongingToUser($newUser, $newUser); + $this->entities->createChainBelongingToUser($newUser, $newUser); $resp = $this->asAdmin()->get('/user/' . $newUser->slug) ->assertSee($newUser->name); @@ -63,7 +63,7 @@ class UserProfileTest extends TestCase { $newUser = User::factory()->create(); $this->actingAs($newUser); - $entities = $this->createEntityChainBelongingToUser($newUser, $newUser); + $entities = $this->entities->createChainBelongingToUser($newUser, $newUser); Activity::add(ActivityType::BOOK_UPDATE, $entities['book']); Activity::add(ActivityType::PAGE_CREATE, $entities['page']); @@ -77,7 +77,7 @@ class UserProfileTest extends TestCase { $newUser = User::factory()->create(); $this->actingAs($newUser); - $entities = $this->createEntityChainBelongingToUser($newUser, $newUser); + $entities = $this->entities->createChainBelongingToUser($newUser, $newUser); Activity::add(ActivityType::BOOK_UPDATE, $entities['book']); Activity::add(ActivityType::PAGE_CREATE, $entities['page']); From b56f7355aa3a44c8529073fc2a3e760d0404f2ad Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 29 Sep 2022 17:31:38 +0100 Subject: [PATCH 21/24] Migrated much test entity usage via find/replace --- tests/Actions/AuditLogTest.php | 26 +++---- tests/Actions/WebhookCallTest.php | 3 +- tests/Actions/WebhookFormatTesting.php | 3 +- tests/Api/AttachmentsApiTest.php | 44 ++++------- tests/Api/ChaptersApiTest.php | 4 +- tests/Api/PagesApiTest.php | 4 +- tests/Api/RecycleBinApiTest.php | 8 +- tests/Api/SearchApiTest.php | 6 +- tests/Auth/AuthTest.php | 3 +- tests/Commands/ClearActivityCommandTest.php | 3 +- .../CopyShelfPermissionsCommandTest.php | 2 +- .../RegenerateReferencesCommandTest.php | 3 +- tests/Commands/UpdateUrlCommandTest.php | 2 +- tests/Entity/BookShelfTest.php | 7 +- tests/Entity/BookTest.php | 23 ++---- tests/Entity/ChapterTest.php | 12 +-- tests/Entity/ConvertTest.php | 11 +-- tests/Entity/EntitySearchTest.php | 7 +- tests/Entity/ExportTest.php | 49 ++++++------ tests/Entity/PageContentTest.php | 71 ++++++++---------- tests/Entity/PageDraftTest.php | 21 ++---- tests/Entity/PageEditorTest.php | 18 ++--- tests/Entity/PageRevisionTest.php | 12 +-- tests/Entity/PageTest.php | 21 ++---- tests/Entity/SortTest.php | 14 ++-- tests/Entity/TagTest.php | 20 ++--- tests/ErrorTest.php | 2 +- tests/FavouriteTest.php | 13 ++-- tests/Helpers/EntityProvider.php | 2 +- tests/HomepageTest.php | 5 +- tests/OpenGraphTest.php | 8 +- tests/Permissions/EntityOwnerChangeTest.php | 8 +- tests/Permissions/EntityPermissionsTest.php | 75 +++++++------------ tests/Permissions/ExportPermissionsTest.php | 4 +- tests/Permissions/RolesTest.php | 24 ++---- tests/PublicActionTest.php | 15 ++-- tests/References/CrossLinkParserTest.php | 2 +- tests/References/ReferencesTest.php | 11 ++- tests/Settings/RecycleBinTest.php | 6 +- tests/ThemeTest.php | 9 +-- tests/Uploads/AttachmentTest.php | 24 +++--- tests/Uploads/ImageTest.php | 34 ++++----- tests/Uploads/UsesImages.php | 2 +- tests/User/UserManagementTest.php | 2 +- tests/User/UserPreferencesTest.php | 5 +- 45 files changed, 264 insertions(+), 384 deletions(-) diff --git a/tests/Actions/AuditLogTest.php b/tests/Actions/AuditLogTest.php index 8fbf66e76..f4eebb364 100644 --- a/tests/Actions/AuditLogTest.php +++ b/tests/Actions/AuditLogTest.php @@ -46,7 +46,7 @@ class AuditLogTest extends TestCase { $admin = $this->getAdmin(); $this->actingAs($admin); - $page = Page::query()->first(); + $page = $this->entities->page(); $this->activityService->add(ActivityType::PAGE_CREATE, $page); $activity = Activity::query()->orderBy('id', 'desc')->first(); @@ -60,7 +60,7 @@ class AuditLogTest extends TestCase public function test_shows_name_for_deleted_items() { $this->actingAs($this->getAdmin()); - $page = Page::query()->first(); + $page = $this->entities->page(); $pageName = $page->name; $this->activityService->add(ActivityType::PAGE_CREATE, $page); @@ -76,7 +76,7 @@ class AuditLogTest extends TestCase { $viewer = $this->getViewer(); $this->actingAs($viewer); - $page = Page::query()->first(); + $page = $this->entities->page(); $this->activityService->add(ActivityType::PAGE_CREATE, $page); $this->actingAs($this->getAdmin()); @@ -89,7 +89,7 @@ class AuditLogTest extends TestCase public function test_filters_by_key() { $this->actingAs($this->getAdmin()); - $page = Page::query()->first(); + $page = $this->entities->page(); $this->activityService->add(ActivityType::PAGE_CREATE, $page); $resp = $this->get('settings/audit'); @@ -102,7 +102,7 @@ class AuditLogTest extends TestCase public function test_date_filters() { $this->actingAs($this->getAdmin()); - $page = Page::query()->first(); + $page = $this->entities->page(); $this->activityService->add(ActivityType::PAGE_CREATE, $page); $yesterday = (Carbon::now()->subDay()->format('Y-m-d')); @@ -126,11 +126,11 @@ class AuditLogTest extends TestCase $admin = $this->getAdmin(); $editor = $this->getEditor(); $this->actingAs($admin); - $page = Page::query()->first(); + $page = $this->entities->page(); $this->activityService->add(ActivityType::PAGE_CREATE, $page); $this->actingAs($editor); - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $this->activityService->add(ActivityType::CHAPTER_UPDATE, $chapter); $resp = $this->actingAs($admin)->get('settings/audit?user=' . $admin->id); @@ -146,8 +146,7 @@ class AuditLogTest extends TestCase { config()->set('app.proxies', '*'); $editor = $this->getEditor(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->actingAs($editor)->put($page->getUrl(), [ 'name' => 'Updated page', @@ -171,8 +170,7 @@ class AuditLogTest extends TestCase { config()->set('app.proxies', '*'); $editor = $this->getEditor(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->actingAs($editor)->put($page->getUrl(), [ 'name' => 'Updated page', @@ -198,8 +196,7 @@ class AuditLogTest extends TestCase config()->set('app.proxies', '*'); config()->set('app.env', 'demo'); $editor = $this->getEditor(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->actingAs($editor)->put($page->getUrl(), [ 'name' => 'Updated page', @@ -222,8 +219,7 @@ class AuditLogTest extends TestCase config()->set('app.proxies', '*'); config()->set('app.ip_address_precision', 2); $editor = $this->getEditor(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->actingAs($editor)->put($page->getUrl(), [ 'name' => 'Updated page', diff --git a/tests/Actions/WebhookCallTest.php b/tests/Actions/WebhookCallTest.php index d9f9ddad5..7964fd8af 100644 --- a/tests/Actions/WebhookCallTest.php +++ b/tests/Actions/WebhookCallTest.php @@ -88,8 +88,7 @@ class WebhookCallTest extends TestCase '*' => Http::response('', 200), ]); $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $editor = $this->getEditor(); $this->runEvent(ActivityType::PAGE_UPDATE, $page, $editor); diff --git a/tests/Actions/WebhookFormatTesting.php b/tests/Actions/WebhookFormatTesting.php index 4e9ba5e47..35467a76a 100644 --- a/tests/Actions/WebhookFormatTesting.php +++ b/tests/Actions/WebhookFormatTesting.php @@ -32,8 +32,7 @@ class WebhookFormatTesting extends TestCase public function test_page_create_and_update_events_show_revision_info() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); $data = $this->getWebhookData(ActivityType::PAGE_UPDATE, $page); diff --git a/tests/Api/AttachmentsApiTest.php b/tests/Api/AttachmentsApiTest.php index dfd57deb8..c295f7384 100644 --- a/tests/Api/AttachmentsApiTest.php +++ b/tests/Api/AttachmentsApiTest.php @@ -17,7 +17,7 @@ class AttachmentsApiTest extends TestCase public function test_index_endpoint_returns_expected_book() { $this->actingAsApiEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $attachment = $this->createAttachmentForPage($page, [ 'name' => 'My test attachment', 'external' => true, @@ -37,8 +37,7 @@ class AttachmentsApiTest extends TestCase public function test_attachments_listing_based_upon_page_visibility() { $this->actingAsApiEditor(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $attachment = $this->createAttachmentForPage($page, [ 'name' => 'My test attachment', 'external' => true, @@ -66,8 +65,7 @@ class AttachmentsApiTest extends TestCase public function test_create_endpoint_for_link_attachment() { $this->actingAsApiAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $details = [ 'name' => 'My attachment', @@ -85,8 +83,7 @@ class AttachmentsApiTest extends TestCase public function test_create_endpoint_for_upload_attachment() { $this->actingAsApiAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $file = $this->getTestFile('textfile.txt'); $details = [ @@ -106,8 +103,7 @@ class AttachmentsApiTest extends TestCase public function test_upload_limit_restricts_attachment_uploads() { $this->actingAsApiAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); config()->set('app.upload_limit', 1); @@ -130,8 +126,7 @@ class AttachmentsApiTest extends TestCase public function test_name_needed_to_create() { $this->actingAsApiAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $details = [ 'uploaded_to' => $page->id, @@ -146,8 +141,7 @@ class AttachmentsApiTest extends TestCase public function test_link_or_file_needed_to_create() { $this->actingAsApiAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $details = [ 'name' => 'my attachment', @@ -165,8 +159,7 @@ class AttachmentsApiTest extends TestCase public function test_message_shown_if_file_is_not_a_valid_file() { $this->actingAsApiAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $details = [ 'name' => 'my attachment', @@ -182,8 +175,7 @@ class AttachmentsApiTest extends TestCase public function test_read_endpoint_for_link_attachment() { $this->actingAsApiAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $attachment = $this->createAttachmentForPage($page, [ 'name' => 'my attachment', @@ -216,8 +208,7 @@ class AttachmentsApiTest extends TestCase public function test_read_endpoint_for_file_attachment() { $this->actingAsApiAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $file = $this->getTestFile('textfile.txt'); $details = [ @@ -259,8 +250,7 @@ class AttachmentsApiTest extends TestCase $this->actingAsApiAdmin(); $editor = $this->getEditor(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->draft = true; $page->owned_by = $editor->id; $page->save(); @@ -280,8 +270,7 @@ class AttachmentsApiTest extends TestCase public function test_update_endpoint() { $this->actingAsApiAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $attachment = $this->createAttachmentForPage($page); $details = [ @@ -298,8 +287,7 @@ class AttachmentsApiTest extends TestCase public function test_update_link_attachment_to_file() { $this->actingAsApiAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $attachment = $this->createAttachmentForPage($page); $file = $this->getTestFile('textfile.txt'); @@ -318,8 +306,7 @@ class AttachmentsApiTest extends TestCase public function test_update_file_attachment_to_link() { $this->actingAsApiAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $file = $this->getTestFile('textfile.txt'); $this->call('POST', $this->baseEndpoint, ['name' => 'My file attachment', 'uploaded_to' => $page->id], [], ['file' => $file]); /** @var Attachment $attachment */ @@ -346,8 +333,7 @@ class AttachmentsApiTest extends TestCase public function test_delete_endpoint() { $this->actingAsApiAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $attachment = $this->createAttachmentForPage($page); $resp = $this->deleteJson("{$this->baseEndpoint}/{$attachment->id}"); diff --git a/tests/Api/ChaptersApiTest.php b/tests/Api/ChaptersApiTest.php index 8d31500eb..22be2482c 100644 --- a/tests/Api/ChaptersApiTest.php +++ b/tests/Api/ChaptersApiTest.php @@ -34,7 +34,7 @@ class ChaptersApiTest extends TestCase public function test_create_endpoint() { $this->actingAsApiEditor(); - $book = Book::query()->first(); + $book = $this->entities->book(); $details = [ 'name' => 'My API chapter', 'description' => 'A chapter created via the API', @@ -64,7 +64,7 @@ class ChaptersApiTest extends TestCase public function test_chapter_name_needed_to_create() { $this->actingAsApiEditor(); - $book = Book::query()->first(); + $book = $this->entities->book(); $details = [ 'book_id' => $book->id, 'description' => 'A chapter created via the API', diff --git a/tests/Api/PagesApiTest.php b/tests/Api/PagesApiTest.php index 20c6977dd..fe1fc8d36 100644 --- a/tests/Api/PagesApiTest.php +++ b/tests/Api/PagesApiTest.php @@ -35,7 +35,7 @@ class PagesApiTest extends TestCase public function test_create_endpoint() { $this->actingAsApiEditor(); - $book = Book::query()->first(); + $book = $this->entities->book(); $details = [ 'name' => 'My API page', 'book_id' => $book->id, @@ -67,7 +67,7 @@ class PagesApiTest extends TestCase public function test_page_name_needed_to_create() { $this->actingAsApiEditor(); - $book = Book::query()->first(); + $book = $this->entities->book(); $details = [ 'book_id' => $book->id, 'html' => '

A page created via the API

', diff --git a/tests/Api/RecycleBinApiTest.php b/tests/Api/RecycleBinApiTest.php index 83cd82480..cdb51f85a 100644 --- a/tests/Api/RecycleBinApiTest.php +++ b/tests/Api/RecycleBinApiTest.php @@ -50,8 +50,8 @@ class RecycleBinApiTest extends TestCase { $admin = $this->getAdmin(); - $page = Page::query()->first(); - $book = Book::query()->first(); + $page = $this->entities->page(); + $book = $this->entities->book(); $this->actingAs($admin)->delete($page->getUrl()); $this->delete($book->getUrl()); @@ -139,7 +139,7 @@ class RecycleBinApiTest extends TestCase public function test_restore_endpoint() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin()->delete($page->getUrl()); $page->refresh(); @@ -163,7 +163,7 @@ class RecycleBinApiTest extends TestCase public function test_destroy_endpoint() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin()->delete($page->getUrl()); $page->refresh(); diff --git a/tests/Api/SearchApiTest.php b/tests/Api/SearchApiTest.php index 1f38c7fd9..cdc954ec3 100644 --- a/tests/Api/SearchApiTest.php +++ b/tests/Api/SearchApiTest.php @@ -38,8 +38,7 @@ class SearchApiTest extends TestCase public function test_all_endpoint_returns_entity_url() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->update(['name' => 'name with superuniquevalue within']); $page->indexForSearch(); @@ -52,8 +51,7 @@ class SearchApiTest extends TestCase public function test_all_endpoint_returns_items_with_preview_html() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $book->update(['name' => 'name with superuniquevalue within', 'description' => 'Description with superuniquevalue within']); $book->indexForSearch(); diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php index 849469766..4456ed459 100644 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@ -58,8 +58,7 @@ class AuthTest extends TestCase public function test_login_redirects_to_initially_requested_url_correctly() { config()->set('app.url', 'http://localhost'); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->get($page->getUrl())->assertRedirect(url('/login')); $this->login('admin@admin.com', 'password') diff --git a/tests/Commands/ClearActivityCommandTest.php b/tests/Commands/ClearActivityCommandTest.php index 71baa0ca6..abc8bc7f4 100644 --- a/tests/Commands/ClearActivityCommandTest.php +++ b/tests/Commands/ClearActivityCommandTest.php @@ -14,8 +14,7 @@ class ClearActivityCommandTest extends TestCase public function test_clear_activity_command() { $this->asEditor(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); Activity::add(ActivityType::PAGE_UPDATE, $page); $this->assertDatabaseHas('activities', [ diff --git a/tests/Commands/CopyShelfPermissionsCommandTest.php b/tests/Commands/CopyShelfPermissionsCommandTest.php index dd39317ae..bd96f2cc5 100644 --- a/tests/Commands/CopyShelfPermissionsCommandTest.php +++ b/tests/Commands/CopyShelfPermissionsCommandTest.php @@ -36,7 +36,7 @@ class CopyShelfPermissionsCommandTest extends TestCase public function test_copy_shelf_permissions_command_using_all() { - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); Bookshelf::query()->where('id', '!=', $shelf->id)->delete(); $child = $shelf->books()->first(); $editorRole = $this->getEditor()->roles()->first(); diff --git a/tests/Commands/RegenerateReferencesCommandTest.php b/tests/Commands/RegenerateReferencesCommandTest.php index 27dde749b..2c737712a 100644 --- a/tests/Commands/RegenerateReferencesCommandTest.php +++ b/tests/Commands/RegenerateReferencesCommandTest.php @@ -10,8 +10,7 @@ class RegenerateReferencesCommandTest extends TestCase { public function test_regenerate_references_command() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $book = $page->book; $page->html = 'Book Link'; diff --git a/tests/Commands/UpdateUrlCommandTest.php b/tests/Commands/UpdateUrlCommandTest.php index 0acccd80c..c4b09162e 100644 --- a/tests/Commands/UpdateUrlCommandTest.php +++ b/tests/Commands/UpdateUrlCommandTest.php @@ -10,7 +10,7 @@ class UpdateUrlCommandTest extends TestCase { public function test_command_updates_page_content() { - $page = Page::query()->first(); + $page = $this->entities->page(); $page->html = ''; $page->save(); diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php index 748f63da8..798edeadf 100644 --- a/tests/Entity/BookShelfTest.php +++ b/tests/Entity/BookShelfTest.php @@ -62,7 +62,7 @@ class BookShelfTest extends TestCase config()->set([ 'setting-defaults.user.bookshelves_view_type' => 'list', ]); - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); $book = $shelf->books()->first(); $resp = $this->asEditor()->get('/shelves'); @@ -160,7 +160,7 @@ class BookShelfTest extends TestCase public function test_shelf_view_has_sort_control_that_defaults_to_default() { - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); $resp = $this->asAdmin()->get($shelf->getUrl()); $this->withHtml($resp)->assertElementExists('form[action$="change-sort/shelf_books"]'); $this->withHtml($resp)->assertElementContains('form[action$="change-sort/shelf_books"] [aria-haspopup="true"]', 'Default'); @@ -373,8 +373,7 @@ class BookShelfTest extends TestCase public function test_cancel_on_child_book_creation_returns_to_original_shelf() { - /** @var Bookshelf $shelf */ - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); $resp = $this->asEditor()->get($shelf->getUrl('/create-book')); $this->withHtml($resp)->assertElementContains('form a[href="' . $shelf->getUrl() . '"]', 'Cancel'); } diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php index ec430ae84..2914162cf 100644 --- a/tests/Entity/BookTest.php +++ b/tests/Entity/BookTest.php @@ -80,8 +80,7 @@ class BookTest extends TestCase public function test_update() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); // Cheeky initial update to refresh slug $this->asEditor()->put($book->getUrl(), ['name' => $book->name . '5', 'description' => $book->description]); $book->refresh(); @@ -104,8 +103,7 @@ class BookTest extends TestCase public function test_update_sets_tags() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $this->assertEquals(0, $book->tags()->count()); @@ -167,15 +165,14 @@ class BookTest extends TestCase public function test_cancel_on_edit_book_page_leads_back_to_book() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $resp = $this->asEditor()->get($book->getUrl('/edit')); $this->withHtml($resp)->assertElementContains('form a[href="' . $book->getUrl() . '"]', 'Cancel'); } public function test_next_previous_navigation_controls_show_within_book_content() { - $book = Book::query()->first(); + $book = $this->entities->book(); $chapter = $book->chapters->first(); $resp = $this->asEditor()->get($chapter->getUrl()); @@ -270,8 +267,7 @@ class BookTest extends TestCase public function test_show_view_has_copy_button() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $resp = $this->asEditor()->get($book->getUrl()); $this->withHtml($resp)->assertElementContains("a[href=\"{$book->getUrl('/copy')}\"]", 'Copy'); @@ -279,8 +275,7 @@ class BookTest extends TestCase public function test_copy_view() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $resp = $this->asEditor()->get($book->getUrl('/copy')); $resp->assertOk(); @@ -338,8 +333,7 @@ class BookTest extends TestCase public function test_copy_clones_cover_image_if_existing() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $bookRepo = $this->app->make(BookRepo::class); $coverImageFile = $this->getTestImage('cover.png'); $bookRepo->updateCoverImage($book, $coverImageFile); @@ -357,8 +351,7 @@ class BookTest extends TestCase /** @var Bookshelf $shelfA */ /** @var Bookshelf $shelfB */ [$shelfA, $shelfB] = Bookshelf::query()->take(2)->get(); - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $shelfA->appendBook($book); $shelfB->appendBook($book); diff --git a/tests/Entity/ChapterTest.php b/tests/Entity/ChapterTest.php index c1c746102..fc8adb01d 100644 --- a/tests/Entity/ChapterTest.php +++ b/tests/Entity/ChapterTest.php @@ -11,8 +11,7 @@ class ChapterTest extends TestCase { public function test_create() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $chapter = Chapter::factory()->make([ 'name' => 'My First Chapter', @@ -58,8 +57,7 @@ class ChapterTest extends TestCase public function test_show_view_has_copy_button() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $resp = $this->asEditor()->get($chapter->getUrl()); $this->withHtml($resp)->assertElementContains("a[href$=\"{$chapter->getUrl('/copy')}\"]", 'Copy'); @@ -67,8 +65,7 @@ class ChapterTest extends TestCase public function test_copy_view() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $resp = $this->asEditor()->get($chapter->getUrl('/copy')); $resp->assertOk(); @@ -149,8 +146,7 @@ class ChapterTest extends TestCase public function test_sort_book_action_visible_if_permissions_allow() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $resp = $this->actingAs($this->getViewer())->get($chapter->getUrl()); $this->withHtml($resp)->assertLinkNotExists($chapter->book->getUrl('sort')); diff --git a/tests/Entity/ConvertTest.php b/tests/Entity/ConvertTest.php index 58f694f60..15205c9ad 100644 --- a/tests/Entity/ConvertTest.php +++ b/tests/Entity/ConvertTest.php @@ -14,8 +14,7 @@ class ConvertTest extends TestCase { public function test_chapter_edit_view_shows_convert_option() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $resp = $this->asEditor()->get($chapter->getUrl('/edit')); $resp->assertSee('Convert to Book'); @@ -50,8 +49,7 @@ class ConvertTest extends TestCase public function test_convert_chapter_to_book_requires_permissions() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $user = $this->getViewer(); $permissions = ['chapter-delete-all', 'book-create-all', 'chapter-update-all']; @@ -71,7 +69,7 @@ class ConvertTest extends TestCase public function test_book_edit_view_shows_convert_option() { - $book = Book::query()->first(); + $book = $this->entities->book(); $resp = $this->asEditor()->get($book->getUrl('/edit')); $resp->assertSee('Convert to Shelf'); @@ -124,8 +122,7 @@ class ConvertTest extends TestCase public function test_book_convert_to_shelf_requires_permissions() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $user = $this->getViewer(); $permissions = ['book-delete-all', 'bookshelf-create-all', 'book-update-all', 'book-create-all']; diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index eabcf6f76..82b97e6f3 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -23,8 +23,7 @@ class EntitySearchTest extends TestCase public function test_bookshelf_search() { - /** @var Bookshelf $shelf */ - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); $search = $this->asEditor()->get('/search?term=' . urlencode($shelf->name) . ' {type:bookshelf}'); $search->assertSee('Search Results'); @@ -232,7 +231,7 @@ class EntitySearchTest extends TestCase public function test_ajax_entity_search_reflects_items_without_permission() { - $page = Page::query()->first(); + $page = $this->entities->page(); $baseSelector = 'a[data-entity-type="page"][data-entity-id="' . $page->id . '"]'; $searchUrl = '/ajax/search/entities?permission=update&term=' . urlencode($page->name); @@ -318,7 +317,7 @@ class EntitySearchTest extends TestCase public function test_search_works_on_updated_page_content() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asEditor(); $update = $this->put($page->getUrl(), [ diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php index 0d13d208e..1d4a23560 100644 --- a/tests/Entity/ExportTest.php +++ b/tests/Entity/ExportTest.php @@ -15,7 +15,7 @@ class ExportTest extends TestCase { public function test_page_text_export() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asEditor(); $resp = $this->get($page->getUrl('/export/plaintext')); @@ -26,7 +26,7 @@ class ExportTest extends TestCase public function test_page_pdf_export() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asEditor(); $resp = $this->get($page->getUrl('/export/pdf')); @@ -36,7 +36,7 @@ class ExportTest extends TestCase public function test_page_html_export() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asEditor(); $resp = $this->get($page->getUrl('/export/html')); @@ -47,7 +47,7 @@ class ExportTest extends TestCase public function test_book_text_export() { - $page = Page::query()->first(); + $page = $this->entities->page(); $book = $page->book; $this->asEditor(); @@ -60,7 +60,7 @@ class ExportTest extends TestCase public function test_book_pdf_export() { - $page = Page::query()->first(); + $page = $this->entities->page(); $book = $page->book; $this->asEditor(); @@ -71,7 +71,7 @@ class ExportTest extends TestCase public function test_book_html_export() { - $page = Page::query()->first(); + $page = $this->entities->page(); $book = $page->book; $this->asEditor(); @@ -85,7 +85,7 @@ class ExportTest extends TestCase public function test_book_html_export_shows_chapter_descriptions() { $chapterDesc = 'My custom test chapter description ' . Str::random(12); - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $chapter->description = $chapterDesc; $chapter->save(); @@ -98,7 +98,7 @@ class ExportTest extends TestCase public function test_chapter_text_export() { - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $page = $chapter->pages[0]; $this->asEditor(); @@ -111,7 +111,7 @@ class ExportTest extends TestCase public function test_chapter_pdf_export() { - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $this->asEditor(); $resp = $this->get($chapter->getUrl('/export/pdf')); @@ -121,7 +121,7 @@ class ExportTest extends TestCase public function test_chapter_html_export() { - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $page = $chapter->pages[0]; $this->asEditor(); @@ -134,7 +134,7 @@ class ExportTest extends TestCase public function test_page_html_export_contains_custom_head_if_set() { - $page = Page::query()->first(); + $page = $this->entities->page(); $customHeadContent = ''; $this->setSettings(['app-custom-head' => $customHeadContent]); @@ -145,7 +145,7 @@ class ExportTest extends TestCase public function test_page_html_export_does_not_break_with_only_comments_in_custom_head() { - $page = Page::query()->first(); + $page = $this->entities->page(); $customHeadContent = ''; $this->setSettings(['app-custom-head' => $customHeadContent]); @@ -157,7 +157,7 @@ class ExportTest extends TestCase public function test_page_html_export_use_absolute_dates() { - $page = Page::query()->first(); + $page = $this->entities->page(); $resp = $this->asEditor()->get($page->getUrl('/export/html')); $resp->assertSee($page->created_at->formatLocalized('%e %B %Y %H:%M:%S')); @@ -168,7 +168,7 @@ class ExportTest extends TestCase public function test_page_export_does_not_include_user_or_revision_links() { - $page = Page::query()->first(); + $page = $this->entities->page(); $resp = $this->asEditor()->get($page->getUrl('/export/html')); $resp->assertDontSee($page->getUrl('/revisions')); @@ -178,7 +178,7 @@ class ExportTest extends TestCase public function test_page_export_sets_right_data_type_for_svg_embeds() { - $page = Page::query()->first(); + $page = $this->entities->page(); Storage::disk('local')->makeDirectory('uploads/images/gallery'); Storage::disk('local')->put('uploads/images/gallery/svg_test.svg', ''); $page->html = ''; @@ -194,7 +194,7 @@ class ExportTest extends TestCase public function test_page_image_containment_works_on_multiple_images_within_a_single_line() { - $page = Page::query()->first(); + $page = $this->entities->page(); Storage::disk('local')->makeDirectory('uploads/images/gallery'); Storage::disk('local')->put('uploads/images/gallery/svg_test.svg', ''); Storage::disk('local')->put('uploads/images/gallery/svg_test2.svg', ''); @@ -210,7 +210,7 @@ class ExportTest extends TestCase public function test_page_export_contained_html_image_fetches_only_run_when_url_points_to_image_upload_folder() { - $page = Page::query()->first(); + $page = $this->entities->page(); $page->html = '' . '' . ''; @@ -235,7 +235,7 @@ class ExportTest extends TestCase $contents = file_get_contents(public_path('.htaccess')); config()->set('filesystems.images', 'local'); - $page = Page::query()->first(); + $page = $this->entities->page(); $page->html = ''; $page->save(); @@ -249,7 +249,7 @@ class ExportTest extends TestCase config()->set('filesystems.images', 'local_secure'); file_put_contents($testFilePath, 'I am a cat'); - $page = Page::query()->first(); + $page = $this->entities->page(); $page->html = ''; $page->save(); @@ -276,7 +276,7 @@ class ExportTest extends TestCase public function test_page_export_with_deleted_creator_and_updater() { $user = $this->getViewer(['name' => 'ExportWizardTheFifth']); - $page = Page::query()->first(); + $page = $this->entities->page(); $page->created_by = $user->id; $page->updated_by = $user->id; $page->save(); @@ -329,7 +329,7 @@ class ExportTest extends TestCase public function test_page_markdown_export() { - $page = Page::query()->first(); + $page = $this->entities->page(); $resp = $this->asEditor()->get($page->getUrl('/export/markdown')); $resp->assertStatus(200); @@ -364,7 +364,7 @@ class ExportTest extends TestCase public function test_chapter_markdown_export() { - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $page = $chapter->pages()->first(); $resp = $this->asEditor()->get($chapter->getUrl('/export/markdown')); @@ -430,8 +430,7 @@ class ExportTest extends TestCase public function test_wkhtmltopdf_only_used_when_allow_untrusted_is_true() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); config()->set('snappy.pdf.binary', '/abc123'); config()->set('app.allow_untrusted_server_fetching', false); @@ -460,7 +459,7 @@ class ExportTest extends TestCase public function test_html_exports_contain_body_classes_for_export_identification() { - $page = Page::query()->first(); + $page = $this->entities->page(); $resp = $this->asEditor()->get($page->getUrl('/export/html')); $this->withHtml($resp)->assertElementExists('body.export.export-format-html.export-engine-none'); diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index 1c0519586..c6de4dc51 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -15,7 +15,7 @@ class PageContentTest extends TestCase public function test_page_includes() { - $page = Page::query()->first(); + $page = $this->entities->page(); $secondPage = Page::query()->where('id', '!=', $page->id)->first(); $secondPage->html = "

Hello, This is a test

This is a second block of content

"; @@ -44,7 +44,7 @@ class PageContentTest extends TestCase public function test_saving_page_with_includes() { - $page = Page::query()->first(); + $page = $this->entities->page(); $secondPage = Page::query()->where('id', '!=', $page->id)->first(); $this->asEditor(); @@ -62,10 +62,8 @@ class PageContentTest extends TestCase public function test_page_includes_do_not_break_tables() { - /** @var Page $page */ - $page = Page::query()->first(); - /** @var Page $secondPage */ - $secondPage = Page::query()->where('id', '!=', $page->id)->first(); + $page = $this->entities->page(); + $secondPage = $this->entities->page(); $content = '
test
'; $secondPage->html = $content; @@ -80,10 +78,8 @@ class PageContentTest extends TestCase public function test_page_includes_do_not_break_code() { - /** @var Page $page */ - $page = Page::query()->first(); - /** @var Page $secondPage */ - $secondPage = Page::query()->where('id', '!=', $page->id)->first(); + $page = $this->entities->page(); + $secondPage = $this->entities->page(); $content = '
var cat = null;
'; $secondPage->html = $content; @@ -98,7 +94,7 @@ class PageContentTest extends TestCase public function test_page_includes_rendered_on_book_export() { - $page = Page::query()->first(); + $page = $this->entities->page(); $secondPage = Page::query() ->where('book_id', '!=', $page->book_id) ->first(); @@ -118,7 +114,7 @@ class PageContentTest extends TestCase public function test_page_content_scripts_removed_by_default() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $script = 'abc123abc123'; $page->html = "escape {$script}"; $page->save(); @@ -141,7 +137,7 @@ class PageContentTest extends TestCase ]; $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); foreach ($checks as $check) { $page->html = $check; @@ -177,7 +173,7 @@ class PageContentTest extends TestCase ]; $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); foreach ($checks as $check) { $page->html = $check; @@ -206,7 +202,7 @@ class PageContentTest extends TestCase ]; $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); foreach ($checks as $check) { $page->html = $check; @@ -230,7 +226,7 @@ class PageContentTest extends TestCase ]; $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); foreach ($checks as $check) { $page->html = $check; @@ -255,7 +251,7 @@ class PageContentTest extends TestCase ]; $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); foreach ($checks as $check) { $page->html = $check; @@ -273,7 +269,7 @@ class PageContentTest extends TestCase public function test_page_inline_on_attributes_removed_by_default() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $script = '

Hello

'; $page->html = "escape {$script}"; $page->save(); @@ -298,7 +294,7 @@ class PageContentTest extends TestCase ]; $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); foreach ($checks as $check) { $page->html = $check; @@ -313,7 +309,7 @@ class PageContentTest extends TestCase public function test_page_content_scripts_show_when_configured() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); config()->push('app.allow_content_scripts', 'true'); $script = 'abc123abc123'; @@ -339,7 +335,7 @@ class PageContentTest extends TestCase ]; $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); foreach ($checks as $check) { $page->html = $check; @@ -358,7 +354,7 @@ class PageContentTest extends TestCase public function test_page_inline_on_attributes_show_if_configured() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); config()->push('app.allow_content_scripts', 'true'); $script = '

Hello

'; @@ -390,7 +386,7 @@ class PageContentTest extends TestCase public function test_duplicate_ids_fixed_on_page_save() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $content = '
  • test a
    • test b
'; $pageSave = $this->put($page->getUrl(), [ @@ -407,7 +403,7 @@ class PageContentTest extends TestCase public function test_anchors_referencing_non_bkmrk_ids_rewritten_after_save() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $content = '

test

link

'; $this->put($page->getUrl(), [ @@ -485,7 +481,7 @@ class PageContentTest extends TestCase public function test_page_text_decodes_html_entities() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->actingAs($this->getAdmin()) ->put($page->getUrl(''), [ @@ -500,7 +496,7 @@ class PageContentTest extends TestCase public function test_page_markdown_table_rendering() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $content = '| Syntax | Description | | ----------- | ----------- | @@ -521,7 +517,7 @@ class PageContentTest extends TestCase public function test_page_markdown_task_list_rendering() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $content = '- [ ] Item a - [x] Item b'; @@ -542,7 +538,7 @@ class PageContentTest extends TestCase public function test_page_markdown_strikethrough_rendering() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $content = '~~some crossed out text~~'; $this->put($page->getUrl(), [ @@ -560,7 +556,7 @@ class PageContentTest extends TestCase public function test_page_markdown_single_html_comment_saving() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $content = ''; $this->put($page->getUrl(), [ @@ -579,7 +575,7 @@ class PageContentTest extends TestCase public function test_base64_images_get_extracted_from_page_content() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $this->put($page->getUrl(), [ 'name' => $page->name, 'summary' => '', @@ -601,7 +597,7 @@ class PageContentTest extends TestCase public function test_base64_images_get_extracted_when_containing_whitespace() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $base64PngWithWhitespace = "iVBORw0KGg\noAAAANSUhE\tUgAAAAEAAAA BCA YAAAAfFcSJAAA\n\t ACklEQVR4nGMAAQAABQAB"; $base64PngWithoutWhitespace = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQAB'; @@ -632,7 +628,7 @@ class PageContentTest extends TestCase foreach ($extensions as $extension) { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $this->put($page->getUrl(), [ 'name' => $page->name, 'summary' => '', @@ -647,7 +643,7 @@ class PageContentTest extends TestCase public function test_base64_images_get_extracted_from_markdown_page_content() { $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $this->put($page->getUrl(), [ 'name' => $page->name, 'summary' => '', @@ -672,7 +668,7 @@ class PageContentTest extends TestCase $pcreRecursionLimit = ini_get('pcre.recursion_limit'); $this->asEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); ini_set('pcre.backtrack_limit', '500'); ini_set('pcre.recursion_limit', '500'); @@ -701,7 +697,7 @@ class PageContentTest extends TestCase public function test_base64_images_within_markdown_blanked_if_not_supported_extension_for_extract() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asEditor()->put($page->getUrl(), [ 'name' => $page->name, 'summary' => '', @@ -713,7 +709,7 @@ class PageContentTest extends TestCase public function test_nested_headers_gets_assigned_an_id() { - $page = Page::query()->first(); + $page = $this->entities->page(); $content = '
Simple Test
'; $this->asEditor()->put($page->getUrl(), [ @@ -729,8 +725,7 @@ class PageContentTest extends TestCase public function test_non_breaking_spaces_are_preserved() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $content = '

 

'; $this->asEditor()->put($page->getUrl(), [ diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php index 8ca73847a..acf6b01e8 100644 --- a/tests/Entity/PageDraftTest.php +++ b/tests/Entity/PageDraftTest.php @@ -85,8 +85,7 @@ class PageDraftTest extends TestCase { $admin = $this->getAdmin(); $editor = $this->getEditor(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [ 'name' => $page->name, @@ -120,8 +119,7 @@ class PageDraftTest extends TestCase { $admin = $this->getAdmin(); $editor = $this->getEditor(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->actingAs($admin)->put('/ajax/page/' . $page->id . '/save-draft', [ 'name' => $page->name, @@ -140,8 +138,7 @@ class PageDraftTest extends TestCase public function test_draft_pages_show_on_homepage() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $resp = $this->asAdmin()->get('/'); $this->withHtml($resp)->assertElementNotContains('#recent-drafts', 'New Page'); @@ -152,8 +149,7 @@ class PageDraftTest extends TestCase public function test_draft_pages_not_visible_by_others() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $chapter = $book->chapters->first(); $newUser = $this->getEditor(); @@ -171,8 +167,7 @@ class PageDraftTest extends TestCase public function test_page_html_in_ajax_fetch_response() { $this->asAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->getJson('/ajax/page/' . $page->id)->assertJson([ 'html' => $page->html, @@ -181,8 +176,7 @@ class PageDraftTest extends TestCase public function test_updating_page_draft_with_markdown_retains_markdown_content() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $this->asEditor()->get($book->getUrl('/create-page')); /** @var Page $draft */ $draft = Page::query()->where('draft', '=', true)->where('book_id', '=', $book->id)->firstOrFail(); @@ -207,8 +201,7 @@ class PageDraftTest extends TestCase public function test_slug_generated_on_draft_publish_to_page_when_no_name_change() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $this->asEditor()->get($book->getUrl('/create-page')); /** @var Page $draft */ $draft = Page::query()->where('draft', '=', true)->where('book_id', '=', $book->id)->firstOrFail(); diff --git a/tests/Entity/PageEditorTest.php b/tests/Entity/PageEditorTest.php index e5aa549b3..6ce649a54 100644 --- a/tests/Entity/PageEditorTest.php +++ b/tests/Entity/PageEditorTest.php @@ -58,8 +58,7 @@ class PageEditorTest extends TestCase public function test_empty_markdown_still_saves_without_error() { $this->setSettings(['app-editor' => 'markdown']); - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $this->asEditor()->get($book->getUrl('/create-page')); $draft = Page::query()->where('book_id', '=', $book->id) @@ -108,8 +107,7 @@ class PageEditorTest extends TestCase public function test_switching_from_html_to_clean_markdown_works() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->html = '

A Header

Some bold content.

'; $page->save(); @@ -121,8 +119,7 @@ class PageEditorTest extends TestCase public function test_switching_from_html_to_stable_markdown_works() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->html = '

A Header

Some bold content.

'; $page->save(); @@ -134,8 +131,7 @@ class PageEditorTest extends TestCase public function test_switching_from_markdown_to_wysiwyg_works() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->html = ''; $page->markdown = "## A Header\n\nSome content with **bold** text!"; $page->save(); @@ -180,8 +176,7 @@ class PageEditorTest extends TestCase public function test_page_editor_type_switch_does_not_work_without_change_editor_permissions() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->html = '

A Header

Some bold content.

'; $page->save(); @@ -193,8 +188,7 @@ class PageEditorTest extends TestCase public function test_page_save_does_not_change_active_editor_without_change_editor_permissions() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->html = '

A Header

Some bold content.

'; $page->editor = 'wysiwyg'; $page->save(); diff --git a/tests/Entity/PageRevisionTest.php b/tests/Entity/PageRevisionTest.php index 05c86c97d..eabece4c6 100644 --- a/tests/Entity/PageRevisionTest.php +++ b/tests/Entity/PageRevisionTest.php @@ -10,8 +10,7 @@ class PageRevisionTest extends TestCase { public function test_revision_links_visible_to_viewer() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $html = $this->withHtml($this->asViewer()->get($page->getUrl())); $html->assertLinkExists($page->getUrl('/revisions')); @@ -143,8 +142,7 @@ class PageRevisionTest extends TestCase public function test_revision_deletion() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->createRevisions($page, 2); $beforeRevisionCount = $page->revisions->count(); @@ -208,8 +206,7 @@ class PageRevisionTest extends TestCase public function test_revision_restore_action_only_visible_with_permission() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->createRevisions($page, 2); $viewer = $this->getViewer(); @@ -227,8 +224,7 @@ class PageRevisionTest extends TestCase public function test_revision_delete_action_only_visible_with_permission() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->createRevisions($page, 2); $viewer = $this->getViewer(); diff --git a/tests/Entity/PageTest.php b/tests/Entity/PageTest.php index 0f906460b..067fceeb4 100644 --- a/tests/Entity/PageTest.php +++ b/tests/Entity/PageTest.php @@ -12,8 +12,7 @@ class PageTest extends TestCase { public function test_create() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $page = Page::factory()->make([ 'name' => 'My First Page', ]); @@ -39,7 +38,7 @@ class PageTest extends TestCase public function test_page_view_when_creator_is_deleted_but_owner_exists() { - $page = Page::query()->first(); + $page = $this->entities->page(); $user = $this->getViewer(); $owner = $this->getEditor(); $page->created_by = $user->id; @@ -55,7 +54,7 @@ class PageTest extends TestCase public function test_page_creation_with_markdown_content() { $this->setSettings(['app-editor' => 'markdown']); - $book = Book::query()->first(); + $book = $this->entities->book(); $this->asEditor()->get($book->getUrl('/create-page')); $draft = Page::query()->where('book_id', '=', $book->id) @@ -83,7 +82,7 @@ class PageTest extends TestCase public function test_page_delete() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->assertNull($page->deleted_at); $deleteViewReq = $this->asEditor()->get($page->getUrl('/delete')); @@ -103,8 +102,7 @@ class PageTest extends TestCase public function test_page_full_delete_removes_all_revisions() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->revisions()->create([ 'html' => '

ducks

', 'name' => 'my page revision', @@ -221,8 +219,7 @@ class PageTest extends TestCase public function test_old_page_slugs_redirect_to_new_pages() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); // Need to save twice since revisions are not generated in seeder. $this->asAdmin()->put($page->getUrl(), [ @@ -244,8 +241,7 @@ class PageTest extends TestCase public function test_page_within_chapter_deletion_returns_to_chapter() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $page = $chapter->pages()->first(); $this->asEditor()->delete($page->getUrl()) @@ -264,8 +260,7 @@ class PageTest extends TestCase public function test_recently_updated_pages_view_shows_updated_by_details() { $user = $this->getEditor(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->actingAs($user)->put($page->getUrl(), [ 'name' => 'Updated title', diff --git a/tests/Entity/SortTest.php b/tests/Entity/SortTest.php index 93b668a0e..83a8f7005 100644 --- a/tests/Entity/SortTest.php +++ b/tests/Entity/SortTest.php @@ -33,7 +33,7 @@ class SortTest extends TestCase public function test_page_move_into_book() { - $page = Page::query()->first(); + $page = $this->entities->page(); $currentBook = $page->book; $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); @@ -55,7 +55,7 @@ class SortTest extends TestCase public function test_page_move_into_chapter() { - $page = Page::query()->first(); + $page = $this->entities->page(); $currentBook = $page->book; $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $newChapter = $newBook->chapters()->first(); @@ -93,7 +93,7 @@ class SortTest extends TestCase public function test_page_move_requires_create_permissions_on_parent() { - $page = Page::query()->first(); + $page = $this->entities->page(); $currentBook = $page->book; $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $editor = $this->getEditor(); @@ -118,7 +118,7 @@ class SortTest extends TestCase public function test_page_move_requires_delete_permissions() { - $page = Page::query()->first(); + $page = $this->entities->page(); $currentBook = $page->book; $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $editor = $this->getEditor(); @@ -145,7 +145,7 @@ class SortTest extends TestCase public function test_chapter_move() { - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $currentBook = $chapter->book; $pageToCheck = $chapter->pages->first(); $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); @@ -173,7 +173,7 @@ class SortTest extends TestCase public function test_chapter_move_requires_delete_permissions() { - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $currentBook = $chapter->book; $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $editor = $this->getEditor(); @@ -200,7 +200,7 @@ class SortTest extends TestCase public function test_chapter_move_requires_create_permissions_in_new_book() { - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $currentBook = $chapter->book; $newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $editor = $this->getEditor(); diff --git a/tests/Entity/TagTest.php b/tests/Entity/TagTest.php index d22dc2f44..18ee31826 100644 --- a/tests/Entity/TagTest.php +++ b/tests/Entity/TagTest.php @@ -102,8 +102,7 @@ class TagTest extends TestCase public function test_tags_index_shows_tag_name_as_expected_with_right_counts() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->tags()->create(['name' => 'Category', 'value' => 'GreatTestContent']); $page->tags()->create(['name' => 'Category', 'value' => 'OtherTestContent']); @@ -120,8 +119,7 @@ class TagTest extends TestCase $html->assertElementContains('a[title="Assigned to Shelves"]', '0'); $html->assertElementContains('a[href$="/tags?name=Category"]', '2 unique values'); - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $book->tags()->create(['name' => 'Category', 'value' => 'GreatTestContent']); $resp = $this->asEditor()->get('/tags'); $this->withHtml($resp)->assertElementContains('a[title="Total tag usages"]', '3'); @@ -131,8 +129,7 @@ class TagTest extends TestCase public function test_tag_index_can_be_searched() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->tags()->create(['name' => 'Category', 'value' => 'GreatTestContent']); $resp = $this->asEditor()->get('/tags?search=cat'); @@ -148,8 +145,7 @@ class TagTest extends TestCase public function test_tag_index_search_will_show_mulitple_values_of_a_single_tag_name() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->tags()->create(['name' => 'Animal', 'value' => 'Catfish']); $page->tags()->create(['name' => 'Animal', 'value' => 'Catdog']); @@ -160,8 +156,7 @@ class TagTest extends TestCase public function test_tag_index_can_be_scoped_to_specific_tag_name() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->tags()->create(['name' => 'Category', 'value' => 'GreatTestContent']); $page->tags()->create(['name' => 'Category', 'value' => 'OtherTestContent']); $page->tags()->create(['name' => 'OtherTagName', 'value' => 'OtherValue']); @@ -178,8 +173,7 @@ class TagTest extends TestCase public function test_tags_index_adheres_to_page_permissions() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->tags()->create(['name' => 'SuperCategory', 'value' => 'GreatTestContent']); $resp = $this->asEditor()->get('/tags'); @@ -216,7 +210,7 @@ class TagTest extends TestCase public function test_tag_classes_are_escaped() { - $page = Page::query()->first(); + $page = $this->entities->page(); $page->tags()->create(['name' => '<>']); $resp = $this->asEditor()->get($page->getUrl()); $resp->assertDontSee('tag-name-<>', false); diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index 2eeb6537e..c46d65bde 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -27,7 +27,7 @@ class ErrorTest extends TestCase { $this->actingAs($this->getViewer()); $handler = $this->withTestLogger(); - $book = Book::query()->first(); + $book = $this->entities->book(); // Ensure we're seeing errors Log::error('cat'); diff --git a/tests/FavouriteTest.php b/tests/FavouriteTest.php index de36c77e1..03a712316 100644 --- a/tests/FavouriteTest.php +++ b/tests/FavouriteTest.php @@ -13,7 +13,7 @@ class FavouriteTest extends TestCase { public function test_page_add_favourite_flow() { - $page = Page::query()->first(); + $page = $this->entities->page(); $editor = $this->getEditor(); $resp = $this->actingAs($editor)->get($page->getUrl()); @@ -36,7 +36,7 @@ class FavouriteTest extends TestCase public function test_page_remove_favourite_flow() { - $page = Page::query()->first(); + $page = $this->entities->page(); $editor = $this->getEditor(); Favourite::query()->forceCreate([ 'user_id' => $editor->id, @@ -62,8 +62,7 @@ class FavouriteTest extends TestCase public function test_favourite_flow_with_own_permissions() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $user = User::factory()->create(); $book->owned_by = $user->id; $book->save(); @@ -115,8 +114,7 @@ class FavouriteTest extends TestCase $resp = $this->actingAs($editor)->get('/'); $this->withHtml($resp)->assertElementNotExists('#top-favourites'); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $page->favourites()->save((new Favourite())->forceFill(['user_id' => $editor->id])); $resp = $this->get('/'); @@ -126,8 +124,7 @@ class FavouriteTest extends TestCase public function test_favourites_list_page_shows_favourites_and_has_working_pagination() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $editor = $this->getEditor(); $resp = $this->actingAs($editor)->get('/favourites'); diff --git a/tests/Helpers/EntityProvider.php b/tests/Helpers/EntityProvider.php index d3888e71f..152f7a3ac 100644 --- a/tests/Helpers/EntityProvider.php +++ b/tests/Helpers/EntityProvider.php @@ -141,7 +141,7 @@ class EntityProvider */ public function newPage(array $input = ['name' => 'test page', 'html' => 'My new test page']): Page { - $book = Book::query()->first(); + $book = $this->book(); $pageRepo = app(PageRepo::class); $draftPage = $pageRepo->getNewDraftPage($book); $this->addToCache($draftPage); diff --git a/tests/HomepageTest.php b/tests/HomepageTest.php index bb42f49f2..60e10a087 100644 --- a/tests/HomepageTest.php +++ b/tests/HomepageTest.php @@ -81,8 +81,7 @@ class HomepageTest extends TestCase public function test_custom_homepage_cannot_be_deleted_from_parent_deletion() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->setSettings([ 'app-homepage' => $page->id, 'app-homepage-type' => 'page', @@ -161,7 +160,7 @@ class HomepageTest extends TestCase $this->setSettings(['app-homepage-type' => 'bookshelves']); $this->asEditor(); - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); $book = $shelf->books()->first(); // Ensure initially visible diff --git a/tests/OpenGraphTest.php b/tests/OpenGraphTest.php index 3f807f024..f3c439767 100644 --- a/tests/OpenGraphTest.php +++ b/tests/OpenGraphTest.php @@ -18,7 +18,7 @@ class OpenGraphTest extends TestCase public function test_page_tags() { - $page = Page::query()->first(); + $page = $this->entities->page(); $resp = $this->asEditor()->get($page->getUrl()); $tags = $this->getOpenGraphTags($resp); @@ -29,7 +29,7 @@ class OpenGraphTest extends TestCase public function test_chapter_tags() { - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $resp = $this->asEditor()->get($chapter->getUrl()); $tags = $this->getOpenGraphTags($resp); @@ -40,7 +40,7 @@ class OpenGraphTest extends TestCase public function test_book_tags() { - $book = Book::query()->first(); + $book = $this->entities->book(); $resp = $this->asEditor()->get($book->getUrl()); $tags = $this->getOpenGraphTags($resp); @@ -60,7 +60,7 @@ class OpenGraphTest extends TestCase public function test_shelf_tags() { - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); $resp = $this->asEditor()->get($shelf->getUrl()); $tags = $this->getOpenGraphTags($resp); diff --git a/tests/Permissions/EntityOwnerChangeTest.php b/tests/Permissions/EntityOwnerChangeTest.php index fe508668e..65a67dc0f 100644 --- a/tests/Permissions/EntityOwnerChangeTest.php +++ b/tests/Permissions/EntityOwnerChangeTest.php @@ -13,7 +13,7 @@ class EntityOwnerChangeTest extends TestCase { public function test_changing_page_owner() { - $page = Page::query()->first(); + $page = $this->entities->page(); $user = User::query()->where('id', '!=', $page->owned_by)->first(); $this->asAdmin()->put($page->getUrl('permissions'), ['owned_by' => $user->id]); @@ -22,7 +22,7 @@ class EntityOwnerChangeTest extends TestCase public function test_changing_chapter_owner() { - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $user = User::query()->where('id', '!=', $chapter->owned_by)->first(); $this->asAdmin()->put($chapter->getUrl('permissions'), ['owned_by' => $user->id]); @@ -31,7 +31,7 @@ class EntityOwnerChangeTest extends TestCase public function test_changing_book_owner() { - $book = Book::query()->first(); + $book = $this->entities->book(); $user = User::query()->where('id', '!=', $book->owned_by)->first(); $this->asAdmin()->put($book->getUrl('permissions'), ['owned_by' => $user->id]); @@ -40,7 +40,7 @@ class EntityOwnerChangeTest extends TestCase public function test_changing_shelf_owner() { - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); $user = User::query()->where('id', '!=', $shelf->owned_by)->first(); $this->asAdmin()->put($shelf->getUrl('permissions'), ['owned_by' => $user->id]); diff --git a/tests/Permissions/EntityPermissionsTest.php b/tests/Permissions/EntityPermissionsTest.php index 9e80b752a..9312b88cf 100644 --- a/tests/Permissions/EntityPermissionsTest.php +++ b/tests/Permissions/EntityPermissionsTest.php @@ -41,8 +41,7 @@ class EntityPermissionsTest extends TestCase public function test_bookshelf_view_restriction() { - /** @var Bookshelf $shelf */ - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); $this->actingAs($this->user) ->get($shelf->getUrl()) @@ -61,8 +60,7 @@ class EntityPermissionsTest extends TestCase public function test_bookshelf_update_restriction() { - /** @var Bookshelf $shelf */ - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); $this->actingAs($this->user) ->get($shelf->getUrl('/edit')) @@ -82,8 +80,7 @@ class EntityPermissionsTest extends TestCase public function test_bookshelf_delete_restriction() { - /** @var Bookshelf $shelf */ - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); $this->actingAs($this->user) ->get($shelf->getUrl('/delete')) @@ -103,8 +100,7 @@ class EntityPermissionsTest extends TestCase public function test_book_view_restriction() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); @@ -134,8 +130,7 @@ class EntityPermissionsTest extends TestCase public function test_book_create_restriction() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $bookUrl = $book->getUrl(); $resp = $this->actingAs($this->viewer)->get($bookUrl); @@ -181,8 +176,7 @@ class EntityPermissionsTest extends TestCase public function test_book_update_restriction() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); @@ -209,8 +203,7 @@ class EntityPermissionsTest extends TestCase public function test_book_delete_restriction() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); @@ -236,8 +229,7 @@ class EntityPermissionsTest extends TestCase public function test_chapter_view_restriction() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $chapterPage = $chapter->pages->first(); $chapterUrl = $chapter->getUrl(); @@ -256,8 +248,7 @@ class EntityPermissionsTest extends TestCase public function test_chapter_create_restriction() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $chapterUrl = $chapter->getUrl(); $resp = $this->actingAs($this->user)->get($chapterUrl); @@ -285,8 +276,7 @@ class EntityPermissionsTest extends TestCase public function test_chapter_update_restriction() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $chapterPage = $chapter->pages->first(); $chapterUrl = $chapter->getUrl(); @@ -308,8 +298,7 @@ class EntityPermissionsTest extends TestCase public function test_chapter_delete_restriction() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $chapterPage = $chapter->pages->first(); $chapterUrl = $chapter->getUrl(); @@ -332,8 +321,7 @@ class EntityPermissionsTest extends TestCase public function test_page_view_restriction() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $pageUrl = $page->getUrl(); $this->actingAs($this->user)->get($pageUrl)->assertOk(); @@ -349,8 +337,7 @@ class EntityPermissionsTest extends TestCase public function test_page_update_restriction() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $pageUrl = $page->getUrl(); $resp = $this->actingAs($this->user) @@ -371,8 +358,7 @@ class EntityPermissionsTest extends TestCase public function test_page_delete_restriction() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $pageUrl = $page->getUrl(); $this->actingAs($this->user) @@ -436,8 +422,7 @@ class EntityPermissionsTest extends TestCase public function test_restricted_pages_not_visible_in_book_navigation_on_pages() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $page = $chapter->pages->first(); $page2 = $chapter->pages[2]; @@ -449,8 +434,7 @@ class EntityPermissionsTest extends TestCase public function test_restricted_pages_not_visible_in_book_navigation_on_chapters() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $page = $chapter->pages->first(); $this->setRestrictionsForTestRoles($page, []); @@ -461,8 +445,7 @@ class EntityPermissionsTest extends TestCase public function test_restricted_pages_not_visible_on_chapter_pages() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $page = $chapter->pages->first(); $this->setRestrictionsForTestRoles($page, []); @@ -474,8 +457,7 @@ class EntityPermissionsTest extends TestCase public function test_restricted_chapter_pages_not_visible_on_book_page() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $this->actingAs($this->user) ->get($chapter->book->getUrl()) ->assertSee($chapter->pages->first()->name); @@ -491,8 +473,7 @@ class EntityPermissionsTest extends TestCase public function test_bookshelf_update_restriction_override() { - /** @var Bookshelf $shelf */ - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); $this->actingAs($this->viewer) ->get($shelf->getUrl('/edit')) @@ -510,8 +491,7 @@ class EntityPermissionsTest extends TestCase public function test_bookshelf_delete_restriction_override() { - /** @var Bookshelf $shelf */ - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); $this->actingAs($this->viewer) ->get($shelf->getUrl('/delete')) @@ -529,8 +509,7 @@ class EntityPermissionsTest extends TestCase public function test_book_create_restriction_override() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $bookUrl = $book->getUrl(); $resp = $this->actingAs($this->viewer)->get($bookUrl); @@ -571,8 +550,7 @@ class EntityPermissionsTest extends TestCase public function test_book_update_restriction_override() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); @@ -598,8 +576,7 @@ class EntityPermissionsTest extends TestCase public function test_book_delete_restriction_override() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); @@ -626,8 +603,7 @@ class EntityPermissionsTest extends TestCase public function test_page_visible_if_has_permissions_when_book_not_visible() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $bookChapter = $book->chapters->first(); $bookPage = $bookChapter->pages->first(); @@ -667,8 +643,7 @@ class EntityPermissionsTest extends TestCase public function test_can_create_page_if_chapter_has_permissions_when_book_not_visible() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $this->setRestrictionsForTestRoles($book, []); $bookChapter = $book->chapters->first(); $this->setRestrictionsForTestRoles($bookChapter, ['view']); diff --git a/tests/Permissions/ExportPermissionsTest.php b/tests/Permissions/ExportPermissionsTest.php index 7e9ce6100..44f1a35cc 100644 --- a/tests/Permissions/ExportPermissionsTest.php +++ b/tests/Permissions/ExportPermissionsTest.php @@ -11,7 +11,7 @@ class ExportPermissionsTest extends TestCase { public function test_page_content_without_view_access_hidden_on_chapter_export() { - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $page = $chapter->pages()->firstOrFail(); $pageContent = Str::random(48); $page->html = '

' . $pageContent . '

'; @@ -39,7 +39,7 @@ class ExportPermissionsTest extends TestCase public function test_page_content_without_view_access_hidden_on_book_export() { - $book = Book::query()->first(); + $book = $this->entities->book(); $page = $book->pages()->firstOrFail(); $pageContent = Str::random(48); $page->html = '

' . $pageContent . '

'; diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php index a24d5f8d8..23bfde74c 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolesTest.php @@ -520,8 +520,7 @@ class RolesTest extends TestCase public function test_chapter_create_all_permissions() { - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $this->checkAccessPermission('chapter-create-all', [ $book->getUrl('/create-chapter'), ], [ @@ -603,10 +602,8 @@ class RolesTest extends TestCase public function test_page_create_own_permissions() { - /** @var Book $book */ - $book = Book::query()->first(); - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $book = $this->entities->book(); + $chapter = $this->entities->chapter(); $entities = $this->entities->createChainBelongingToUser($this->user); $ownBook = $entities['book']; @@ -652,10 +649,8 @@ class RolesTest extends TestCase public function test_page_create_all_permissions() { - /** @var Book $book */ - $book = Book::query()->first(); - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $book = $this->entities->book(); + $chapter = $this->entities->chapter(); $createUrl = $book->getUrl('/create-page'); $createUrlChapter = $chapter->getUrl('/create-page'); @@ -806,8 +801,7 @@ class RolesTest extends TestCase public function test_image_delete_own_permission() { $this->giveUserPermissions($this->user, ['image-update-all']); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $image = Image::factory()->create([ 'uploaded_to' => $page->id, 'created_by' => $this->user->id, @@ -826,8 +820,7 @@ class RolesTest extends TestCase { $this->giveUserPermissions($this->user, ['image-update-all']); $admin = $this->getAdmin(); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $image = Image::factory()->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]); $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); @@ -845,8 +838,7 @@ class RolesTest extends TestCase public function test_role_permission_removal() { // To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a. - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $viewerRole = Role::getRole('viewer'); $viewer = $this->getViewer(); $this->actingAs($viewer)->get($page->getUrl())->assertOk(); diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php index 309e09600..14759c578 100644 --- a/tests/PublicActionTest.php +++ b/tests/PublicActionTest.php @@ -17,11 +17,11 @@ class PublicActionTest extends TestCase public function test_app_not_public() { $this->setSettings(['app-public' => 'false']); - $book = Book::query()->first(); + $book = $this->entities->book(); $this->get('/books')->assertRedirect('/login'); $this->get($book->getUrl())->assertRedirect('/login'); - $page = Page::query()->first(); + $page = $this->entities->page(); $this->get($page->getUrl())->assertRedirect('/login'); } @@ -93,8 +93,7 @@ class PublicActionTest extends TestCase $this->app->make(JointPermissionBuilder::class)->rebuildForRole($publicRole); user()->clearPermissionCache(); - /** @var Chapter $chapter */ - $chapter = Chapter::query()->first(); + $chapter = $this->entities->chapter(); $resp = $this->get($chapter->getUrl()); $resp->assertSee('New Page'); $this->withHtml($resp)->assertElementExists('a[href="' . $chapter->getUrl('/create-page') . '"]'); @@ -118,7 +117,7 @@ class PublicActionTest extends TestCase public function test_content_not_listed_on_404_for_public_users() { - $page = Page::query()->first(); + $page = $this->entities->page(); $page->fill(['name' => 'my testing random unique page name'])->save(); $this->asAdmin()->get($page->getUrl()); // Fake visit to show on recents $resp = $this->get('/cats/dogs/hippos'); @@ -162,8 +161,7 @@ class PublicActionTest extends TestCase public function test_public_view_then_login_redirects_to_previous_content() { $this->setSettings(['app-public' => 'true']); - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $resp = $this->get($book->getUrl()); $resp->assertSee($book->name); @@ -175,8 +173,7 @@ class PublicActionTest extends TestCase public function test_access_hidden_content_then_login_redirects_to_intended_content() { $this->setSettings(['app-public' => 'true']); - /** @var Book $book */ - $book = Book::query()->first(); + $book = $this->entities->book(); $this->entities->setPermissions($book); $resp = $this->get($book->getUrl()); diff --git a/tests/References/CrossLinkParserTest.php b/tests/References/CrossLinkParserTest.php index 43b8a36ae..eb862a9fd 100644 --- a/tests/References/CrossLinkParserTest.php +++ b/tests/References/CrossLinkParserTest.php @@ -40,7 +40,7 @@ class CrossLinkParserTest extends TestCase public function test_similar_page_and_book_reference_links_dont_conflict() { - $page = Page::query()->first(); + $page = $this->entities->page(); $book = $page->book; $html = ' diff --git a/tests/References/ReferencesTest.php b/tests/References/ReferencesTest.php index 59263ee0c..16ea19ac5 100644 --- a/tests/References/ReferencesTest.php +++ b/tests/References/ReferencesTest.php @@ -97,7 +97,7 @@ class ReferencesTest extends TestCase { /** @var Page $page */ /** @var Page $pageB */ - $page = Page::query()->first(); + $page = $this->entities->page(); $pageB = Page::query()->where('id', '!=', $page->id)->first(); $this->createReference($pageB, $page); @@ -109,8 +109,7 @@ class ReferencesTest extends TestCase public function test_reference_page_shows_empty_state_with_no_references() { - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asEditor() ->get($page->getUrl('/references')) @@ -124,7 +123,7 @@ class ReferencesTest extends TestCase /** @var Book $book */ $pageA = Page::query()->first(); $pageB = Page::query()->where('id', '!=', $pageA->id)->first(); - $book = Book::query()->first(); + $book = $this->entities->book(); foreach ([$pageA, $pageB] as $page) { $page->html = 'Link'; @@ -200,8 +199,8 @@ class ReferencesTest extends TestCase { /** @var Page $page */ /** @var Book $book */ - $page = Page::query()->first(); - $book = Book::query()->first(); + $page = $this->entities->page(); + $book = $this->entities->book(); $bookUrl = $book->getUrl(); $markdown = ' diff --git a/tests/Settings/RecycleBinTest.php b/tests/Settings/RecycleBinTest.php index 465c1aaad..8b5705afd 100644 --- a/tests/Settings/RecycleBinTest.php +++ b/tests/Settings/RecycleBinTest.php @@ -16,7 +16,7 @@ class RecycleBinTest extends TestCase { public function test_recycle_bin_routes_permissions() { - $page = Page::query()->first(); + $page = $this->entities->page(); $editor = $this->getEditor(); $this->actingAs($editor)->delete($page->getUrl()); $deletion = Deletion::query()->firstOrFail(); @@ -57,7 +57,7 @@ class RecycleBinTest extends TestCase public function test_recycle_bin_view() { - $page = Page::query()->first(); + $page = $this->entities->page(); $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first(); $editor = $this->getEditor(); $this->actingAs($editor)->delete($page->getUrl()); @@ -74,7 +74,7 @@ class RecycleBinTest extends TestCase public function test_recycle_bin_empty() { - $page = Page::query()->first(); + $page = $this->entities->page(); $book = Book::query()->where('id', '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); $editor = $this->getEditor(); $this->actingAs($editor)->delete($page->getUrl()); diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index ac4b35de2..4d612a870 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -64,7 +64,7 @@ class ThemeTest extends TestCase }; Theme::listen(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $callback); - $page = Page::query()->first(); + $page = $this->entities->page(); $content = new PageContent($page); $content->setNewMarkdown('# test'); @@ -199,7 +199,7 @@ class ThemeTest extends TestCase public function test_event_activity_logged() { - $book = Book::query()->first(); + $book = $this->entities->book(); $args = []; $callback = function (...$eventArgs) use (&$args) { $args = $eventArgs; @@ -218,7 +218,7 @@ class ThemeTest extends TestCase { /** @var Page $page */ /** @var Page $otherPage */ - $page = Page::query()->first(); + $page = $this->entities->page(); $otherPage = Page::query()->where('id', '!=', $page->id)->first(); $otherPage->html = '

This is a really cool section

'; $page->html = "

{{@{$otherPage->id}#bkmrk-cool}}

"; @@ -324,8 +324,7 @@ class ThemeTest extends TestCase { $bodyStartStr = 'garry-fought-against-the-panther'; $bodyEndStr = 'garry-lost-his-fight-with-grace'; - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $this->usingThemeFolder(function (string $folder) use ($bodyStartStr, $bodyEndStr, $page) { $viewDir = theme_path('layouts/parts'); diff --git a/tests/Uploads/AttachmentTest.php b/tests/Uploads/AttachmentTest.php index 7280510f3..915a9ba4d 100644 --- a/tests/Uploads/AttachmentTest.php +++ b/tests/Uploads/AttachmentTest.php @@ -73,7 +73,7 @@ class AttachmentTest extends TestCase public function test_file_upload() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin(); $admin = $this->getAdmin(); $fileName = 'upload_test_file.txt'; @@ -101,7 +101,7 @@ class AttachmentTest extends TestCase public function test_file_upload_does_not_use_filename() { - $page = Page::query()->first(); + $page = $this->entities->page(); $fileName = 'upload_test_file.txt'; $upload = $this->asAdmin()->uploadFile($fileName, $page->id); @@ -115,7 +115,7 @@ class AttachmentTest extends TestCase public function test_file_display_and_access() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin(); $fileName = 'upload_test_file.txt'; @@ -136,7 +136,7 @@ class AttachmentTest extends TestCase public function test_attaching_link_to_page() { - $page = Page::query()->first(); + $page = $this->entities->page(); $admin = $this->getAdmin(); $this->asAdmin(); @@ -173,7 +173,7 @@ class AttachmentTest extends TestCase public function test_attachment_updating() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin(); $attachment = $this->createAttachment($page); @@ -197,7 +197,7 @@ class AttachmentTest extends TestCase public function test_file_deletion() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin(); $fileName = 'deletion_test.txt'; $this->uploadFile($fileName, $page->id); @@ -219,7 +219,7 @@ class AttachmentTest extends TestCase public function test_attachment_deletion_on_page_deletion() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin(); $fileName = 'deletion_test.txt'; $this->uploadFile($fileName, $page->id); @@ -247,7 +247,7 @@ class AttachmentTest extends TestCase { $admin = $this->getAdmin(); $viewer = $this->getViewer(); - $page = Page::query()->first(); /** @var Page $page */ + $page = $this->entities->page(); /** @var Page $page */ $this->actingAs($admin); $fileName = 'permission_test.txt'; $this->uploadFile($fileName, $page->id); @@ -269,7 +269,7 @@ class AttachmentTest extends TestCase public function test_data_and_js_links_cannot_be_attached_to_a_page() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin(); $badLinks = [ @@ -310,7 +310,7 @@ class AttachmentTest extends TestCase public function test_file_access_with_open_query_param_provides_inline_response_with_correct_content_type() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin(); $fileName = 'upload_test_file.txt'; @@ -329,7 +329,7 @@ class AttachmentTest extends TestCase public function test_html_file_access_with_open_forces_plain_content_type() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin(); $attachment = $this->createUploadAttachment($page, 'test_file.html', '

testing

', 'text/html'); @@ -346,7 +346,7 @@ class AttachmentTest extends TestCase { config()->set('filesystems.attachments', 'local_secure_restricted'); - $page = Page::query()->first(); + $page = $this->entities->page(); $fileName = 'upload_test_file.txt'; $upload = $this->asAdmin()->uploadFile($fileName, $page->id); diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index 184da214c..e929d63ec 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -15,7 +15,7 @@ class ImageTest extends TestCase public function test_image_upload() { - $page = Page::query()->first(); + $page = $this->entities->page(); $admin = $this->getAdmin(); $this->actingAs($admin); @@ -39,7 +39,7 @@ class ImageTest extends TestCase public function test_image_display_thumbnail_generation_does_not_increase_image_size() { - $page = Page::query()->first(); + $page = $this->entities->page(); $admin = $this->getAdmin(); $this->actingAs($admin); @@ -63,7 +63,7 @@ class ImageTest extends TestCase public function test_image_display_thumbnail_generation_for_apng_images_uses_original_file() { - $page = Page::query()->first(); + $page = $this->entities->page(); $admin = $this->getAdmin(); $this->actingAs($admin); @@ -125,7 +125,7 @@ class ImageTest extends TestCase public function test_image_usage() { - $page = Page::query()->first(); + $page = $this->entities->page(); $editor = $this->getEditor(); $this->actingAs($editor); @@ -145,7 +145,7 @@ class ImageTest extends TestCase public function test_php_files_cannot_be_uploaded() { - $page = Page::query()->first(); + $page = $this->entities->page(); $admin = $this->getAdmin(); $this->actingAs($admin); @@ -167,7 +167,7 @@ class ImageTest extends TestCase public function test_php_like_files_cannot_be_uploaded() { - $page = Page::query()->first(); + $page = $this->entities->page(); $admin = $this->getAdmin(); $this->actingAs($admin); @@ -184,7 +184,7 @@ class ImageTest extends TestCase public function test_files_with_double_extensions_will_get_sanitized() { - $page = Page::query()->first(); + $page = $this->entities->page(); $admin = $this->getAdmin(); $this->actingAs($admin); @@ -219,7 +219,7 @@ class ImageTest extends TestCase ]; foreach ($badNames as $name) { $galleryFile = $this->getTestImage($name); - $page = Page::query()->first(); + $page = $this->entities->page(); $badPath = $this->getTestImagePath('gallery', $name); $this->deleteImage($badPath); @@ -244,7 +244,7 @@ class ImageTest extends TestCase config()->set('filesystems.images', 'local_secure'); $this->asEditor(); $galleryFile = $this->getTestImage('my-secure-test-upload.png'); - $page = Page::query()->first(); + $page = $this->entities->page(); $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png'); $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); @@ -292,7 +292,7 @@ class ImageTest extends TestCase config()->set('filesystems.images', 'local_secure'); $this->asEditor(); $galleryFile = $this->getTestImage('my-secure-test-upload.png'); - $page = Page::query()->first(); + $page = $this->entities->page(); $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png'); $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); @@ -332,8 +332,7 @@ class ImageTest extends TestCase config()->set('filesystems.images', 'local_secure_restricted'); $this->asEditor(); $galleryFile = $this->getTestImage('my-secure-restricted-test-upload.png'); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); $upload->assertStatus(200); @@ -357,8 +356,7 @@ class ImageTest extends TestCase config()->set('filesystems.images', 'local_secure_restricted'); $this->asEditor(); $galleryFile = $this->getTestImage('my-secure-restricted-thumb-test-test.png'); - /** @var Page $page */ - $page = Page::query()->first(); + $page = $this->entities->page(); $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); $upload->assertStatus(200); @@ -412,7 +410,7 @@ class ImageTest extends TestCase public function test_image_delete() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin(); $imageName = 'first-image.png'; $relPath = $this->getTestImagePath('gallery', $imageName); @@ -434,7 +432,7 @@ class ImageTest extends TestCase public function test_image_delete_does_not_delete_similar_images() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin(); $imageName = 'first-image.png'; @@ -459,7 +457,7 @@ class ImageTest extends TestCase public function test_image_manager_delete_button_only_shows_with_permission() { - $page = Page::query()->first(); + $page = $this->entities->page(); $this->asAdmin(); $imageName = 'first-image.png'; $relPath = $this->getTestImagePath('gallery', $imageName); @@ -539,7 +537,7 @@ class ImageTest extends TestCase public function test_deleted_unused_images() { - $page = Page::query()->first(); + $page = $this->entities->page(); $admin = $this->getAdmin(); $this->actingAs($admin); diff --git a/tests/Uploads/UsesImages.php b/tests/Uploads/UsesImages.php index b55572248..e2c16c37c 100644 --- a/tests/Uploads/UsesImages.php +++ b/tests/Uploads/UsesImages.php @@ -91,7 +91,7 @@ trait UsesImages protected function uploadGalleryImage(Page $page = null, ?string $testDataFileName = null) { if ($page === null) { - $page = Page::query()->first(); + $page = $this->entities->page(); } $imageName = $testDataFileName ?? 'first-image.png'; diff --git a/tests/User/UserManagementTest.php b/tests/User/UserManagementTest.php index 71d50e8d6..114088338 100644 --- a/tests/User/UserManagementTest.php +++ b/tests/User/UserManagementTest.php @@ -150,7 +150,7 @@ class UserManagementTest extends TestCase public function test_delete_with_new_owner_id_changes_ownership() { - $page = Page::query()->first(); + $page = $this->entities->page(); $owner = $page->ownedBy; $newOwner = User::query()->where('id', '!=', $owner->id)->first(); diff --git a/tests/User/UserPreferencesTest.php b/tests/User/UserPreferencesTest.php index 88d54d316..2273be2b4 100644 --- a/tests/User/UserPreferencesTest.php +++ b/tests/User/UserPreferencesTest.php @@ -132,8 +132,7 @@ class UserPreferencesTest extends TestCase public function test_shelf_view_type_change() { $editor = $this->getEditor(); - /** @var Bookshelf $shelf */ - $shelf = Bookshelf::query()->first(); + $shelf = $this->entities->shelf(); setting()->putUser($editor, 'bookshelf_view_type', 'list'); $resp = $this->actingAs($editor)->get($shelf->getUrl())->assertSee('Grid View'); @@ -155,7 +154,7 @@ class UserPreferencesTest extends TestCase public function test_update_code_language_favourite() { $editor = $this->getEditor(); - $page = Page::query()->first(); + $page = $this->entities->page(); $this->actingAs($editor); $this->patch('/settings/users/update-code-language-favourite', ['language' => 'php', 'active' => true]); From 900e853b1568cf24dea52b09aa0fa6582e670645 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 29 Sep 2022 22:11:16 +0100 Subject: [PATCH 22/24] Quick run through of applying new test entity helper class --- app/Api/ListingResponseBuilder.php | 1 - tests/Actions/AuditLogTest.php | 5 +- tests/Actions/WebhookCallTest.php | 1 - tests/Actions/WebhookFormatTesting.php | 9 ++-- tests/Api/BooksApiTest.php | 21 ++++---- tests/Api/ChaptersApiTest.php | 15 +++--- tests/Api/PagesApiTest.php | 33 ++++++------ tests/Api/RecycleBinApiTest.php | 3 +- tests/Auth/AuthTest.php | 1 - tests/Commands/ClearActivityCommandTest.php | 1 - .../CopyShelfPermissionsCommandTest.php | 2 +- .../RegenerateReferencesCommandTest.php | 1 - tests/Commands/UpdateUrlCommandTest.php | 1 - tests/Entity/BookShelfTest.php | 16 +++--- tests/Entity/ChapterTest.php | 6 +-- tests/Entity/CommentSettingTest.php | 15 ++---- tests/Entity/CommentTest.php | 10 ++-- tests/Entity/ConvertTest.php | 3 +- tests/Entity/EntityAccessTest.php | 5 +- tests/Entity/EntitySearchTest.php | 20 ++++--- tests/Entity/ExportTest.php | 12 ++--- tests/Entity/PageContentTest.php | 4 +- tests/Entity/PageDraftTest.php | 14 ++--- tests/Entity/PageEditorTest.php | 9 ++-- tests/Entity/PageRevisionTest.php | 21 ++++---- tests/Entity/PageTemplateTest.php | 6 +-- tests/Entity/PageTest.php | 15 +++--- tests/Entity/SortTest.php | 52 +++++++----------- tests/Entity/TagTest.php | 1 - tests/ErrorTest.php | 1 - tests/FavouriteTest.php | 13 +---- tests/Helpers/EntityProvider.php | 53 ++++++++++++++++--- tests/{ => Helpers}/TestServiceProvider.php | 2 +- tests/HomepageTest.php | 7 +-- tests/OpenGraphTest.php | 4 -- tests/Permissions/EntityOwnerChangeTest.php | 4 -- tests/Permissions/EntityPermissionsTest.php | 11 +--- tests/Permissions/ExportPermissionsTest.php | 2 - tests/Permissions/RolesTest.php | 2 +- tests/PublicActionTest.php | 1 - tests/References/CrossLinkParserTest.php | 3 +- tests/References/ReferencesTest.php | 45 +++++----------- tests/Settings/RecycleBinTest.php | 26 ++++----- tests/TestCase.php | 1 + tests/UrlTest.php | 3 -- tests/User/UserManagementTest.php | 1 - tests/User/UserPreferencesTest.php | 2 - 47 files changed, 198 insertions(+), 286 deletions(-) rename tests/{ => Helpers}/TestServiceProvider.php (96%) diff --git a/app/Api/ListingResponseBuilder.php b/app/Api/ListingResponseBuilder.php index 7de5ddf07..39752e6d4 100644 --- a/app/Api/ListingResponseBuilder.php +++ b/app/Api/ListingResponseBuilder.php @@ -2,7 +2,6 @@ namespace BookStack\Api; -use BookStack\Model; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\JsonResponse; diff --git a/tests/Actions/AuditLogTest.php b/tests/Actions/AuditLogTest.php index f4eebb364..987e23a45 100644 --- a/tests/Actions/AuditLogTest.php +++ b/tests/Actions/AuditLogTest.php @@ -6,8 +6,6 @@ use BookStack\Actions\Activity; use BookStack\Actions\ActivityLogger; use BookStack\Actions\ActivityType; use BookStack\Auth\UserRepo; -use BookStack\Entities\Models\Chapter; -use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Tools\TrashCan; use Carbon\Carbon; @@ -15,8 +13,7 @@ use Tests\TestCase; class AuditLogTest extends TestCase { - /** @var ActivityLogger */ - protected $activityService; + protected ActivityLogger $activityService; protected function setUp(): void { diff --git a/tests/Actions/WebhookCallTest.php b/tests/Actions/WebhookCallTest.php index 7964fd8af..7ca190200 100644 --- a/tests/Actions/WebhookCallTest.php +++ b/tests/Actions/WebhookCallTest.php @@ -7,7 +7,6 @@ use BookStack\Actions\ActivityType; use BookStack\Actions\DispatchWebhookJob; use BookStack\Actions\Webhook; use BookStack\Auth\User; -use BookStack\Entities\Models\Page; use Illuminate\Http\Client\Request; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Http; diff --git a/tests/Actions/WebhookFormatTesting.php b/tests/Actions/WebhookFormatTesting.php index 35467a76a..07341c75b 100644 --- a/tests/Actions/WebhookFormatTesting.php +++ b/tests/Actions/WebhookFormatTesting.php @@ -5,9 +5,6 @@ namespace Tests\Actions; use BookStack\Actions\ActivityType; use BookStack\Actions\Webhook; use BookStack\Actions\WebhookFormatter; -use BookStack\Entities\Models\Book; -use BookStack\Entities\Models\Chapter; -use BookStack\Entities\Models\Page; use Illuminate\Support\Arr; use Tests\TestCase; @@ -16,9 +13,9 @@ class WebhookFormatTesting extends TestCase public function test_entity_events_show_related_user_info() { $events = [ - ActivityType::BOOK_UPDATE => Book::query()->first(), - ActivityType::CHAPTER_CREATE => Chapter::query()->first(), - ActivityType::PAGE_MOVE => Page::query()->first(), + ActivityType::BOOK_UPDATE => $this->entities->book(), + ActivityType::CHAPTER_CREATE => $this->entities->chapter(), + ActivityType::PAGE_MOVE => $this->entities->page(), ]; foreach ($events as $event => $entity) { diff --git a/tests/Api/BooksApiTest.php b/tests/Api/BooksApiTest.php index 017322193..614185c93 100644 --- a/tests/Api/BooksApiTest.php +++ b/tests/Api/BooksApiTest.php @@ -68,7 +68,7 @@ class BooksApiTest extends TestCase public function test_read_endpoint() { $this->actingAsApiEditor(); - $book = Book::visible()->first(); + $book = $this->entities->book(); $resp = $this->getJson($this->baseEndpoint . "/{$book->id}"); @@ -91,8 +91,7 @@ class BooksApiTest extends TestCase public function test_read_endpoint_includes_chapter_and_page_contents() { $this->actingAsApiEditor(); - /** @var Book $book */ - $book = Book::visible()->has('chapters')->has('pages')->first(); + $book = $this->entities->bookHasChaptersAndPages(); $chapter = $book->chapters()->first(); $chapterPage = $chapter->pages()->first(); @@ -123,7 +122,7 @@ class BooksApiTest extends TestCase public function test_update_endpoint() { $this->actingAsApiEditor(); - $book = Book::visible()->first(); + $book = $this->entities->book(); $details = [ 'name' => 'My updated API book', 'description' => 'A book created via the API', @@ -140,7 +139,7 @@ class BooksApiTest extends TestCase public function test_update_increments_updated_date_if_only_tags_are_sent() { $this->actingAsApiEditor(); - $book = Book::visible()->first(); + $book = $this->entities->book(); DB::table('books')->where('id', '=', $book->id)->update(['updated_at' => Carbon::now()->subWeek()]); $details = [ @@ -156,7 +155,7 @@ class BooksApiTest extends TestCase { $this->actingAsApiEditor(); /** @var Book $book */ - $book = Book::visible()->first(); + $book = $this->entities->book(); $this->assertNull($book->cover); $file = $this->getTestImage('image.png'); @@ -191,7 +190,7 @@ class BooksApiTest extends TestCase public function test_delete_endpoint() { $this->actingAsApiEditor(); - $book = Book::visible()->first(); + $book = $this->entities->book(); $resp = $this->deleteJson($this->baseEndpoint . "/{$book->id}"); $resp->assertStatus(204); @@ -201,7 +200,7 @@ class BooksApiTest extends TestCase public function test_export_html_endpoint() { $this->actingAsApiEditor(); - $book = Book::visible()->first(); + $book = $this->entities->book(); $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/html"); $resp->assertStatus(200); @@ -212,7 +211,7 @@ class BooksApiTest extends TestCase public function test_export_plain_text_endpoint() { $this->actingAsApiEditor(); - $book = Book::visible()->first(); + $book = $this->entities->book(); $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/plaintext"); $resp->assertStatus(200); @@ -223,7 +222,7 @@ class BooksApiTest extends TestCase public function test_export_pdf_endpoint() { $this->actingAsApiEditor(); - $book = Book::visible()->first(); + $book = $this->entities->book(); $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/pdf"); $resp->assertStatus(200); @@ -249,7 +248,7 @@ class BooksApiTest extends TestCase $this->actingAsApiEditor(); $this->removePermissionFromUser($this->getEditor(), 'content-export'); - $book = Book::visible()->first(); + $book = $this->entities->book(); foreach ($types as $type) { $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/{$type}"); $this->assertPermissionError($resp); diff --git a/tests/Api/ChaptersApiTest.php b/tests/Api/ChaptersApiTest.php index 22be2482c..d2db0313f 100644 --- a/tests/Api/ChaptersApiTest.php +++ b/tests/Api/ChaptersApiTest.php @@ -2,7 +2,6 @@ namespace Tests\Api; -use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use Carbon\Carbon; use Illuminate\Support\Facades\DB; @@ -95,7 +94,7 @@ class ChaptersApiTest extends TestCase public function test_read_endpoint() { $this->actingAsApiEditor(); - $chapter = Chapter::visible()->first(); + $chapter = $this->entities->chapter(); $page = $chapter->pages()->first(); $resp = $this->getJson($this->baseEndpoint . "/{$chapter->id}"); @@ -127,7 +126,7 @@ class ChaptersApiTest extends TestCase public function test_update_endpoint() { $this->actingAsApiEditor(); - $chapter = Chapter::visible()->first(); + $chapter = $this->entities->chapter(); $details = [ 'name' => 'My updated API chapter', 'description' => 'A chapter created via the API', @@ -152,7 +151,7 @@ class ChaptersApiTest extends TestCase public function test_update_increments_updated_date_if_only_tags_are_sent() { $this->actingAsApiEditor(); - $chapter = Chapter::visible()->first(); + $chapter = $this->entities->chapter(); DB::table('chapters')->where('id', '=', $chapter->id)->update(['updated_at' => Carbon::now()->subWeek()]); $details = [ @@ -167,7 +166,7 @@ class ChaptersApiTest extends TestCase public function test_delete_endpoint() { $this->actingAsApiEditor(); - $chapter = Chapter::visible()->first(); + $chapter = $this->entities->chapter(); $resp = $this->deleteJson($this->baseEndpoint . "/{$chapter->id}"); $resp->assertStatus(204); @@ -177,7 +176,7 @@ class ChaptersApiTest extends TestCase public function test_export_html_endpoint() { $this->actingAsApiEditor(); - $chapter = Chapter::visible()->first(); + $chapter = $this->entities->chapter(); $resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/html"); $resp->assertStatus(200); @@ -188,7 +187,7 @@ class ChaptersApiTest extends TestCase public function test_export_plain_text_endpoint() { $this->actingAsApiEditor(); - $chapter = Chapter::visible()->first(); + $chapter = $this->entities->chapter(); $resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/plaintext"); $resp->assertStatus(200); @@ -199,7 +198,7 @@ class ChaptersApiTest extends TestCase public function test_export_pdf_endpoint() { $this->actingAsApiEditor(); - $chapter = Chapter::visible()->first(); + $chapter = $this->entities->chapter(); $resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/pdf"); $resp->assertStatus(200); diff --git a/tests/Api/PagesApiTest.php b/tests/Api/PagesApiTest.php index fe1fc8d36..8c533680f 100644 --- a/tests/Api/PagesApiTest.php +++ b/tests/Api/PagesApiTest.php @@ -2,7 +2,6 @@ namespace Tests\Api; -use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; use Carbon\Carbon; @@ -95,11 +94,11 @@ class PagesApiTest extends TestCase 'chapter_id' => ['The chapter id field is required when book id is not present.'], ])); - $chapter = Chapter::visible()->first(); + $chapter = $this->entities->chapter(); $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['chapter_id' => $chapter->id])); $resp->assertStatus(200); - $book = Book::visible()->first(); + $book = $this->entities->book(); $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['book_id' => $book->id])); $resp->assertStatus(200); } @@ -107,7 +106,7 @@ class PagesApiTest extends TestCase public function test_markdown_can_be_provided_for_create() { $this->actingAsApiEditor(); - $book = Book::visible()->first(); + $book = $this->entities->book(); $details = [ 'book_id' => $book->id, 'name' => 'My api page', @@ -126,7 +125,7 @@ class PagesApiTest extends TestCase public function test_read_endpoint() { $this->actingAsApiEditor(); - $page = Page::visible()->first(); + $page = $this->entities->page(); $resp = $this->getJson($this->baseEndpoint . "/{$page->id}"); $resp->assertStatus(200); @@ -149,7 +148,7 @@ class PagesApiTest extends TestCase public function test_read_endpoint_provides_rendered_html() { $this->actingAsApiEditor(); - $page = Page::visible()->first(); + $page = $this->entities->page(); $page->html = "

testing

Hello

"; $page->save(); @@ -163,7 +162,7 @@ class PagesApiTest extends TestCase public function test_update_endpoint() { $this->actingAsApiEditor(); - $page = Page::visible()->first(); + $page = $this->entities->page(); $details = [ 'name' => 'My updated API page', 'html' => '

A page created via the API

', @@ -189,7 +188,7 @@ class PagesApiTest extends TestCase public function test_providing_new_chapter_id_on_update_will_move_page() { $this->actingAsApiEditor(); - $page = Page::visible()->first(); + $page = $this->entities->page(); $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first(); $details = [ 'name' => 'My updated API page', @@ -208,7 +207,7 @@ class PagesApiTest extends TestCase public function test_providing_move_via_update_requires_page_create_permission_on_new_parent() { $this->actingAsApiEditor(); - $page = Page::visible()->first(); + $page = $this->entities->page(); $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first(); $this->entities->setPermissions($chapter, ['view'], [$this->getEditor()->roles()->first()]); $details = [ @@ -224,7 +223,7 @@ class PagesApiTest extends TestCase public function test_update_endpoint_does_not_wipe_content_if_no_html_or_md_provided() { $this->actingAsApiEditor(); - $page = Page::visible()->first(); + $page = $this->entities->page(); $originalContent = $page->html; $details = [ 'name' => 'My updated API page', @@ -245,7 +244,7 @@ class PagesApiTest extends TestCase public function test_update_increments_updated_date_if_only_tags_are_sent() { $this->actingAsApiEditor(); - $page = Page::visible()->first(); + $page = $this->entities->page(); DB::table('pages')->where('id', '=', $page->id)->update(['updated_at' => Carbon::now()->subWeek()]); $details = [ @@ -262,7 +261,7 @@ class PagesApiTest extends TestCase public function test_delete_endpoint() { $this->actingAsApiEditor(); - $page = Page::visible()->first(); + $page = $this->entities->page(); $resp = $this->deleteJson($this->baseEndpoint . "/{$page->id}"); $resp->assertStatus(204); @@ -272,7 +271,7 @@ class PagesApiTest extends TestCase public function test_export_html_endpoint() { $this->actingAsApiEditor(); - $page = Page::visible()->first(); + $page = $this->entities->page(); $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/html"); $resp->assertStatus(200); @@ -283,7 +282,7 @@ class PagesApiTest extends TestCase public function test_export_plain_text_endpoint() { $this->actingAsApiEditor(); - $page = Page::visible()->first(); + $page = $this->entities->page(); $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/plaintext"); $resp->assertStatus(200); @@ -294,7 +293,7 @@ class PagesApiTest extends TestCase public function test_export_pdf_endpoint() { $this->actingAsApiEditor(); - $page = Page::visible()->first(); + $page = $this->entities->page(); $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/pdf"); $resp->assertStatus(200); @@ -304,7 +303,7 @@ class PagesApiTest extends TestCase public function test_export_markdown_endpoint() { $this->actingAsApiEditor(); - $page = Page::visible()->first(); + $page = $this->entities->page(); $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/markdown"); $resp->assertStatus(200); @@ -318,7 +317,7 @@ class PagesApiTest extends TestCase $this->actingAsApiEditor(); $this->removePermissionFromUser($this->getEditor(), 'content-export'); - $page = Page::visible()->first(); + $page = $this->entities->page(); foreach ($types as $type) { $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/{$type}"); $this->assertPermissionError($resp); diff --git a/tests/Api/RecycleBinApiTest.php b/tests/Api/RecycleBinApiTest.php index cdb51f85a..bc7249987 100644 --- a/tests/Api/RecycleBinApiTest.php +++ b/tests/Api/RecycleBinApiTest.php @@ -4,7 +4,6 @@ namespace Tests\Api; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Deletion; -use BookStack\Entities\Models\Page; use Illuminate\Support\Collection; use Tests\TestCase; @@ -111,7 +110,7 @@ class RecycleBinApiTest extends TestCase public function test_index_endpoint_returns_parent() { $admin = $this->getAdmin(); - $page = Page::query()->whereHas('chapter')->with('chapter')->first(); + $page = $this->entities->pageWithinChapter(); $this->actingAs($admin)->delete($page->getUrl()); $deletion = Deletion::query()->orderBy('id')->first(); diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php index 4456ed459..3220b2aac 100644 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@ -3,7 +3,6 @@ namespace Tests\Auth; use BookStack\Auth\Access\Mfa\MfaSession; -use BookStack\Entities\Models\Page; use Illuminate\Testing\TestResponse; use Tests\TestCase; diff --git a/tests/Commands/ClearActivityCommandTest.php b/tests/Commands/ClearActivityCommandTest.php index abc8bc7f4..cf2fba0d6 100644 --- a/tests/Commands/ClearActivityCommandTest.php +++ b/tests/Commands/ClearActivityCommandTest.php @@ -3,7 +3,6 @@ namespace Tests\Commands; use BookStack\Actions\ActivityType; -use BookStack\Entities\Models\Page; use BookStack\Facades\Activity; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; diff --git a/tests/Commands/CopyShelfPermissionsCommandTest.php b/tests/Commands/CopyShelfPermissionsCommandTest.php index bd96f2cc5..55b710ba9 100644 --- a/tests/Commands/CopyShelfPermissionsCommandTest.php +++ b/tests/Commands/CopyShelfPermissionsCommandTest.php @@ -16,7 +16,7 @@ class CopyShelfPermissionsCommandTest extends TestCase public function test_copy_shelf_permissions_command_using_slug() { - $shelf = Bookshelf::first(); + $shelf = $this->entities->shelf(); $child = $shelf->books()->first(); $editorRole = $this->getEditor()->roles()->first(); $this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default'); diff --git a/tests/Commands/RegenerateReferencesCommandTest.php b/tests/Commands/RegenerateReferencesCommandTest.php index 2c737712a..36af0d7cc 100644 --- a/tests/Commands/RegenerateReferencesCommandTest.php +++ b/tests/Commands/RegenerateReferencesCommandTest.php @@ -2,7 +2,6 @@ namespace Tests\Commands; -use BookStack\Entities\Models\Page; use Illuminate\Support\Facades\DB; use Tests\TestCase; diff --git a/tests/Commands/UpdateUrlCommandTest.php b/tests/Commands/UpdateUrlCommandTest.php index c4b09162e..c07a80312 100644 --- a/tests/Commands/UpdateUrlCommandTest.php +++ b/tests/Commands/UpdateUrlCommandTest.php @@ -2,7 +2,6 @@ namespace Tests\Commands; -use BookStack\Entities\Models\Page; use Symfony\Component\Console\Exception\RuntimeException; use Tests\TestCase; diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php index 798edeadf..1e740b94e 100644 --- a/tests/Entity/BookShelfTest.php +++ b/tests/Entity/BookShelfTest.php @@ -39,7 +39,7 @@ class BookShelfTest extends TestCase { $user = User::factory()->create(); $this->giveUserPermissions($user, ['image-create-all']); - $shelf = Bookshelf::first(); + $shelf = $this->entities->shelf(); $userRole = $user->roles()->first(); $resp = $this->actingAs($user)->get('/'); @@ -130,7 +130,7 @@ class BookShelfTest extends TestCase public function test_shelf_view() { - $shelf = Bookshelf::first(); + $shelf = $this->entities->shelf(); $resp = $this->asEditor()->get($shelf->getUrl()); $resp->assertStatus(200); $resp->assertSeeText($shelf->name); @@ -143,7 +143,7 @@ class BookShelfTest extends TestCase public function test_shelf_view_shows_action_buttons() { - $shelf = Bookshelf::first(); + $shelf = $this->entities->shelf(); $resp = $this->asAdmin()->get($shelf->getUrl()); $resp->assertSee($shelf->getUrl('/create-book')); $resp->assertSee($shelf->getUrl('/edit')); @@ -201,7 +201,7 @@ class BookShelfTest extends TestCase public function test_shelf_edit() { - $shelf = Bookshelf::first(); + $shelf = $this->entities->shelf(); $resp = $this->asEditor()->get($shelf->getUrl('/edit')); $resp->assertSeeText('Edit Shelf'); @@ -239,7 +239,7 @@ class BookShelfTest extends TestCase public function test_shelf_create_new_book() { - $shelf = Bookshelf::first(); + $shelf = $this->entities->shelf(); $resp = $this->asEditor()->get($shelf->getUrl('/create-book')); $resp->assertSee('Create New Book'); @@ -288,7 +288,7 @@ class BookShelfTest extends TestCase public function test_shelf_copy_permissions() { - $shelf = Bookshelf::first(); + $shelf = $this->entities->shelf(); $resp = $this->asAdmin()->get($shelf->getUrl('/permissions')); $resp->assertSeeText('Copy Permissions'); $resp->assertSee("action=\"{$shelf->getUrl('/copy-permissions')}\"", false); @@ -311,14 +311,14 @@ class BookShelfTest extends TestCase public function test_permission_page_has_a_warning_about_no_cascading() { - $shelf = Bookshelf::first(); + $shelf = $this->entities->shelf(); $resp = $this->asAdmin()->get($shelf->getUrl('/permissions')); $resp->assertSeeText('Permissions on shelves do not automatically cascade to contained books.'); } public function test_bookshelves_show_in_breadcrumbs_if_in_context() { - $shelf = Bookshelf::first(); + $shelf = $this->entities->shelf(); $shelfBook = $shelf->books()->first(); $shelfPage = $shelfBook->pages()->first(); $this->asAdmin(); diff --git a/tests/Entity/ChapterTest.php b/tests/Entity/ChapterTest.php index fc8adb01d..afc60c20e 100644 --- a/tests/Entity/ChapterTest.php +++ b/tests/Entity/ChapterTest.php @@ -96,8 +96,7 @@ class ChapterTest extends TestCase public function test_copy_does_not_copy_non_visible_pages() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->whereHas('pages')->first(); + $chapter = $this->entities->chapterHasPages(); // Hide pages to all non-admin roles /** @var Page $page */ @@ -118,8 +117,7 @@ class ChapterTest extends TestCase public function test_copy_does_not_copy_pages_if_user_cant_page_create() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->whereHas('pages')->first(); + $chapter = $this->entities->chapterHasPages(); $viewer = $this->getViewer(); $this->giveUserPermissions($viewer, ['chapter-create-all']); diff --git a/tests/Entity/CommentSettingTest.php b/tests/Entity/CommentSettingTest.php index 0e3199979..7de457441 100644 --- a/tests/Entity/CommentSettingTest.php +++ b/tests/Entity/CommentSettingTest.php @@ -2,34 +2,27 @@ namespace Tests\Entity; -use BookStack\Entities\Models\Page; use Tests\TestCase; class CommentSettingTest extends TestCase { - protected $page; - - protected function setUp(): void - { - parent::setUp(); - $this->page = Page::query()->first(); - } - public function test_comment_disable() { + $page = $this->entities->page(); $this->setSettings(['app-disable-comments' => 'true']); $this->asAdmin(); - $resp = $this->asAdmin()->get($this->page->getUrl()); + $resp = $this->asAdmin()->get($page->getUrl()); $this->withHtml($resp)->assertElementNotExists('.comments-list'); } public function test_comment_enable() { + $page = $this->entities->page(); $this->setSettings(['app-disable-comments' => 'false']); $this->asAdmin(); - $resp = $this->asAdmin()->get($this->page->getUrl()); + $resp = $this->asAdmin()->get($page->getUrl()); $this->withHtml($resp)->assertElementExists('.comments-list'); } } diff --git a/tests/Entity/CommentTest.php b/tests/Entity/CommentTest.php index 1e8ecbcac..99e3525a0 100644 --- a/tests/Entity/CommentTest.php +++ b/tests/Entity/CommentTest.php @@ -11,7 +11,7 @@ class CommentTest extends TestCase public function test_add_comment() { $this->asAdmin(); - $page = Page::first(); + $page = $this->entities->page(); $comment = Comment::factory()->make(['parent_id' => 2]); $resp = $this->postJson("/comment/$page->id", $comment->getAttributes()); @@ -34,7 +34,7 @@ class CommentTest extends TestCase public function test_comment_edit() { $this->asAdmin(); - $page = Page::first(); + $page = $this->entities->page(); $comment = Comment::factory()->make(); $this->postJson("/comment/$page->id", $comment->getAttributes()); @@ -58,7 +58,7 @@ class CommentTest extends TestCase public function test_comment_delete() { $this->asAdmin(); - $page = Page::first(); + $page = $this->entities->page(); $comment = Comment::factory()->make(); $this->postJson("/comment/$page->id", $comment->getAttributes()); @@ -75,7 +75,7 @@ class CommentTest extends TestCase public function test_comments_converts_markdown_input_to_html() { - $page = Page::first(); + $page = $this->entities->page(); $this->asAdmin()->postJson("/comment/$page->id", [ 'text' => '# My Title', ]); @@ -94,7 +94,7 @@ class CommentTest extends TestCase public function test_html_cannot_be_injected_via_comment_content() { $this->asAdmin(); - $page = Page::first(); + $page = $this->entities->page(); $script = '\n\n# sometextinthecomment'; $this->postJson("/comment/$page->id", [ diff --git a/tests/Entity/ConvertTest.php b/tests/Entity/ConvertTest.php index 15205c9ad..16dd89068 100644 --- a/tests/Entity/ConvertTest.php +++ b/tests/Entity/ConvertTest.php @@ -24,8 +24,7 @@ class ConvertTest extends TestCase public function test_convert_chapter_to_book() { - /** @var Chapter $chapter */ - $chapter = Chapter::query()->whereHas('pages')->first(); + $chapter = $this->entities->chapterHasPages(); $chapter->tags()->save(new Tag(['name' => 'Category', 'value' => 'Penguins'])); /** @var Page $childPage */ $childPage = $chapter->pages()->first(); diff --git a/tests/Entity/EntityAccessTest.php b/tests/Entity/EntityAccessTest.php index e3d129d5e..2bb32fde8 100644 --- a/tests/Entity/EntityAccessTest.php +++ b/tests/Entity/EntityAccessTest.php @@ -4,7 +4,6 @@ namespace Tests\Entity; use BookStack\Auth\UserRepo; use BookStack\Entities\Models\Entity; -use BookStack\Entities\Repos\PageRepo; use Tests\TestCase; class EntityAccessTest extends TestCase @@ -16,7 +15,7 @@ class EntityAccessTest extends TestCase $updater = $this->getViewer(); $entities = $this->entities->createChainBelongingToUser($creator, $updater); app()->make(UserRepo::class)->destroy($creator); - app()->make(PageRepo::class)->update($entities['page'], ['html' => '

hello!

>']); + $this->entities->updatePage($entities['page'], ['html' => '

hello!

>']); $this->checkEntitiesViewable($entities); } @@ -28,7 +27,7 @@ class EntityAccessTest extends TestCase $updater = $this->getEditor(); $entities = $this->entities->createChainBelongingToUser($creator, $updater); app()->make(UserRepo::class)->destroy($updater); - app()->make(PageRepo::class)->update($entities['page'], ['html' => '

Hello there!

']); + $this->entities->updatePage($entities['page'], ['html' => '

Hello there!

']); $this->checkEntitiesViewable($entities); } diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index 82b97e6f3..cdb500a45 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -5,15 +5,13 @@ namespace Tests\Entity; use BookStack\Actions\Tag; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; -use BookStack\Entities\Models\Chapter; -use BookStack\Entities\Models\Page; use Tests\TestCase; class EntitySearchTest extends TestCase { public function test_page_search() { - $book = Book::all()->first(); + $book = $this->entities->book(); $page = $book->pages->first(); $search = $this->asEditor()->get('/search?term=' . urlencode($page->name)); @@ -71,7 +69,7 @@ class EntitySearchTest extends TestCase public function test_chapter_search() { - $chapter = Chapter::has('pages')->first(); + $chapter = $this->entities->chapterHasPages(); $page = $chapter->pages[0]; $pageTestResp = $this->asEditor()->get('/search/chapter/' . $chapter->id . '?term=' . urlencode($page->name)); @@ -91,10 +89,10 @@ class EntitySearchTest extends TestCase ]), ]; - $pageA = Page::first(); + $pageA = $this->entities->page(); $pageA->tags()->saveMany($newTags); - $pageB = Page::all()->last(); + $pageB = $this->entities->page(); $pageB->tags()->create(['name' => 'animal', 'value' => 'dog']); $this->asEditor(); @@ -197,7 +195,7 @@ class EntitySearchTest extends TestCase public function test_ajax_entity_search() { $page = $this->entities->newPage(['name' => 'my ajax search test', 'html' => 'ajax test']); - $notVisitedPage = Page::first(); + $notVisitedPage = $this->entities->page(); // Visit the page to make popular $this->asEditor()->get($page->getUrl()); @@ -215,7 +213,7 @@ class EntitySearchTest extends TestCase public function test_ajax_entity_search_shows_breadcrumbs() { - $chapter = Chapter::first(); + $chapter = $this->entities->chapter(); $page = $chapter->pages->first(); $this->asEditor(); @@ -246,7 +244,7 @@ class EntitySearchTest extends TestCase public function test_sibling_search_for_pages() { - $chapter = Chapter::query()->with('pages')->first(); + $chapter = $this->entities->chapterHasPages(); $this->assertGreaterThan(2, count($chapter->pages), 'Ensure we\'re testing with at least 1 sibling'); $page = $chapter->pages->first(); @@ -261,7 +259,7 @@ class EntitySearchTest extends TestCase public function test_sibling_search_for_pages_without_chapter() { - $page = Page::query()->where('chapter_id', '=', 0)->firstOrFail(); + $page = $this->entities->pageNotWithinChapter(); $bookChildren = $page->book->getDirectChildren(); $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling'); @@ -276,7 +274,7 @@ class EntitySearchTest extends TestCase public function test_sibling_search_for_chapters() { - $chapter = Chapter::query()->firstOrFail(); + $chapter = $this->entities->chapter(); $bookChildren = $chapter->book->getDirectChildren(); $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling'); diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php index 1d4a23560..0f80bdd49 100644 --- a/tests/Entity/ExportTest.php +++ b/tests/Entity/ExportTest.php @@ -311,7 +311,7 @@ class ExportTest extends TestCase public function test_page_pdf_export_opens_details_blocks() { - $page = Page::query()->first()->forceFill([ + $page = $this->entities->page()->forceFill([ 'html' => '
Hello

Content!

', ]); $page->save(); @@ -339,7 +339,7 @@ class ExportTest extends TestCase public function test_page_markdown_export_uses_existing_markdown_if_apparent() { - $page = Page::query()->first()->forceFill([ + $page = $this->entities->page()->forceFill([ 'markdown' => '# A header', 'html' => '

Dogcat

', ]); @@ -352,7 +352,7 @@ class ExportTest extends TestCase public function test_page_markdown_export_converts_html_where_no_markdown() { - $page = Page::query()->first()->forceFill([ + $page = $this->entities->page()->forceFill([ 'markdown' => '', 'html' => '

Dogcat

Some bold text

', ]); @@ -446,9 +446,9 @@ class ExportTest extends TestCase public function test_html_exports_contain_csp_meta_tag() { $entities = [ - Page::query()->first(), - Book::query()->first(), - Chapter::query()->first(), + $this->entities->page(), + $this->entities->book(), + $this->entities->chapter(), ]; foreach ($entities as $entity) { diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index c6de4dc51..0c9854206 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -16,7 +16,7 @@ class PageContentTest extends TestCase public function test_page_includes() { $page = $this->entities->page(); - $secondPage = Page::query()->where('id', '!=', $page->id)->first(); + $secondPage = $this->entities->page(); $secondPage->html = "

Hello, This is a test

This is a second block of content

"; $secondPage->save(); @@ -45,7 +45,7 @@ class PageContentTest extends TestCase public function test_saving_page_with_includes() { $page = $this->entities->page(); - $secondPage = Page::query()->where('id', '!=', $page->id)->first(); + $secondPage = $this->entities->page(); $this->asEditor(); $includeTag = '{{@' . $secondPage->id . '}}'; diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php index acf6b01e8..010173852 100644 --- a/tests/Entity/PageDraftTest.php +++ b/tests/Entity/PageDraftTest.php @@ -2,7 +2,6 @@ namespace Tests\Entity; -use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Page; use BookStack\Entities\Models\PageRevision; use BookStack\Entities\Repos\PageRepo; @@ -10,20 +9,13 @@ use Tests\TestCase; class PageDraftTest extends TestCase { - /** - * @var Page - */ - protected $page; - - /** - * @var PageRepo - */ - protected $pageRepo; + protected Page $page; + protected PageRepo $pageRepo; protected function setUp(): void { parent::setUp(); - $this->page = Page::query()->first(); + $this->page = $this->entities->page(); $this->pageRepo = app()->make(PageRepo::class); } diff --git a/tests/Entity/PageEditorTest.php b/tests/Entity/PageEditorTest.php index 6ce649a54..b2fb85955 100644 --- a/tests/Entity/PageEditorTest.php +++ b/tests/Entity/PageEditorTest.php @@ -2,20 +2,18 @@ namespace Tests\Entity; -use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; use Tests\TestCase; class PageEditorTest extends TestCase { - /** @var Page */ - protected $page; + protected Page $page; protected function setUp(): void { parent::setUp(); - $this->page = Page::query()->first(); + $this->page = $this->entities->page(); } public function test_default_editor_is_wysiwyg_for_new_pages() @@ -80,8 +78,7 @@ class PageEditorTest extends TestCase public function test_back_link_in_editor_has_correct_url() { - /** @var Book $book */ - $book = Book::query()->whereHas('pages')->whereHas('chapters')->firstOrFail(); + $book = $this->entities->bookHasChaptersAndPages(); $this->asEditor()->get($book->getUrl('/create-page')); /** @var Chapter $chapter */ $chapter = $book->chapters()->firstOrFail(); diff --git a/tests/Entity/PageRevisionTest.php b/tests/Entity/PageRevisionTest.php index eabece4c6..d00ec5ce5 100644 --- a/tests/Entity/PageRevisionTest.php +++ b/tests/Entity/PageRevisionTest.php @@ -21,7 +21,7 @@ class PageRevisionTest extends TestCase public function test_page_revision_views_viewable() { $this->asEditor(); - $page = Page::first(); + $page = $this->entities->page(); $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '

new content

']); $pageRevision = $page->revisions->last(); @@ -37,7 +37,7 @@ class PageRevisionTest extends TestCase public function test_page_revision_preview_shows_content_of_revision() { $this->asEditor(); - $page = Page::first(); + $page = $this->entities->page(); $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '

new revision content

']); $pageRevision = $page->revisions->last(); $this->createRevisions($page, 1, ['name' => 'updated page', 'html' => '

Updated content

']); @@ -50,7 +50,7 @@ class PageRevisionTest extends TestCase public function test_page_revision_restore_updates_content() { $this->asEditor(); - $page = Page::first(); + $page = $this->entities->page(); $this->createRevisions($page, 1, ['name' => 'updated page abc123', 'html' => '

new contente def456

']); $this->createRevisions($page, 1, ['name' => 'updated page again', 'html' => '

new content

']); $page = Page::find($page->id); @@ -74,7 +74,7 @@ class PageRevisionTest extends TestCase public function test_page_revision_restore_with_markdown_retains_markdown_content() { $this->asEditor(); - $page = Page::first(); + $page = $this->entities->page(); $this->createRevisions($page, 1, ['name' => 'updated page abc123', 'markdown' => '## New Content def456']); $this->createRevisions($page, 1, ['name' => 'updated page again', 'markdown' => '## New Content Updated']); $page = Page::find($page->id); @@ -102,7 +102,7 @@ class PageRevisionTest extends TestCase public function test_page_revision_restore_sets_new_revision_with_summary() { $this->asEditor(); - $page = Page::first(); + $page = $this->entities->page(); $this->createRevisions($page, 1, ['name' => 'updated page abc123', 'html' => '

new contente def456

', 'summary' => 'My first update']); $this->createRevisions($page, 1, ['html' => '

new content

']); $page->refresh(); @@ -124,7 +124,7 @@ class PageRevisionTest extends TestCase public function test_page_revision_count_increments_on_update() { - $page = Page::first(); + $page = $this->entities->page(); $startCount = $page->revision_count; $this->createRevisions($page, 1); @@ -133,7 +133,7 @@ class PageRevisionTest extends TestCase public function test_revision_count_shown_in_page_meta() { - $page = Page::first(); + $page = $this->entities->page(); $this->createRevisions($page, 2); $pageView = $this->get($page->getUrl()); @@ -172,7 +172,7 @@ class PageRevisionTest extends TestCase public function test_revision_limit_enforced() { config()->set('app.revision_limit', 2); - $page = Page::first(); + $page = $this->entities->page(); $this->createRevisions($page, 12); $revisionCount = $page->revisions()->count(); @@ -182,7 +182,7 @@ class PageRevisionTest extends TestCase public function test_false_revision_limit_allows_many_revisions() { config()->set('app.revision_limit', false); - $page = Page::first(); + $page = $this->entities->page(); $this->createRevisions($page, 12); $revisionCount = $page->revisions()->count(); @@ -191,8 +191,7 @@ class PageRevisionTest extends TestCase public function test_revision_list_shows_editor_type() { - /** @var Page $page */ - $page = Page::first(); + $page = $this->entities->page(); $this->createRevisions($page, 1, ['html' => 'new page html']); $resp = $this->asAdmin()->get($page->refresh()->getUrl('/revisions')); diff --git a/tests/Entity/PageTemplateTest.php b/tests/Entity/PageTemplateTest.php index 3d1689510..dc45fcfb8 100644 --- a/tests/Entity/PageTemplateTest.php +++ b/tests/Entity/PageTemplateTest.php @@ -9,7 +9,7 @@ class PageTemplateTest extends TestCase { public function test_active_templates_visible_on_page_view() { - $page = Page::first(); + $page = $this->entities->page(); $this->asEditor(); $templateView = $this->get($page->getUrl()); @@ -24,7 +24,7 @@ class PageTemplateTest extends TestCase public function test_manage_templates_permission_required_to_change_page_template_status() { - $page = Page::first(); + $page = $this->entities->page(); $editor = $this->getEditor(); $this->actingAs($editor); @@ -52,7 +52,7 @@ class PageTemplateTest extends TestCase public function test_templates_content_should_be_fetchable_only_if_page_marked_as_template() { $content = '
my_custom_template_content
'; - $page = Page::first(); + $page = $this->entities->page(); $editor = $this->getEditor(); $this->actingAs($editor); diff --git a/tests/Entity/PageTest.php b/tests/Entity/PageTest.php index 067fceeb4..f481ffb61 100644 --- a/tests/Entity/PageTest.php +++ b/tests/Entity/PageTest.php @@ -3,7 +3,6 @@ namespace Tests\Entity; use BookStack\Entities\Models\Book; -use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; use Carbon\Carbon; use Tests\TestCase; @@ -128,7 +127,7 @@ class PageTest extends TestCase public function test_page_copy() { - $page = Page::first(); + $page = $this->entities->page(); $page->html = '

This is some test content

'; $page->save(); @@ -151,7 +150,7 @@ class PageTest extends TestCase public function test_page_copy_with_markdown_has_both_html_and_markdown() { - $page = Page::first(); + $page = $this->entities->page(); $page->html = '

This is some test content

'; $page->markdown = '# This is some test content'; $page->save(); @@ -169,7 +168,7 @@ class PageTest extends TestCase public function test_page_copy_with_no_destination() { - $page = Page::first(); + $page = $this->entities->page(); $currentBook = $page->book; $resp = $this->asEditor()->get($page->getUrl('/copy')); @@ -188,7 +187,7 @@ class PageTest extends TestCase public function test_page_can_be_copied_without_edit_permission() { - $page = Page::first(); + $page = $this->entities->page(); $currentBook = $page->book; $newBook = Book::where('id', '!=', $currentBook->id)->first(); $viewer = $this->getViewer(); @@ -274,8 +273,7 @@ class PageTest extends TestCase public function test_recently_updated_pages_view_shows_parent_chain() { $user = $this->getEditor(); - /** @var Page $page */ - $page = Page::query()->whereNotNull('chapter_id')->first(); + $page = $this->entities->pageWithinChapter(); $this->actingAs($user)->put($page->getUrl(), [ 'name' => 'Updated title', @@ -290,8 +288,7 @@ class PageTest extends TestCase public function test_recently_updated_pages_view_does_not_show_parent_if_not_visible() { $user = $this->getEditor(); - /** @var Page $page */ - $page = Page::query()->whereNotNull('chapter_id')->first(); + $page = $this->entities->pageWithinChapter(); $this->actingAs($user)->put($page->getUrl(), [ 'name' => 'Updated title', diff --git a/tests/Entity/SortTest.php b/tests/Entity/SortTest.php index 83a8f7005..f02e15d21 100644 --- a/tests/Entity/SortTest.php +++ b/tests/Entity/SortTest.php @@ -10,24 +10,17 @@ use Tests\TestCase; class SortTest extends TestCase { - protected $book; - - protected function setUp(): void - { - parent::setUp(); - $this->book = Book::first(); - } - public function test_drafts_do_not_show_up() { $this->asAdmin(); $pageRepo = app(PageRepo::class); - $draft = $pageRepo->getNewDraftPage($this->book); + $book = $this->entities->book(); + $draft = $pageRepo->getNewDraftPage($book); - $resp = $this->get($this->book->getUrl()); + $resp = $this->get($book->getUrl()); $resp->assertSee($draft->name); - $resp = $this->get($this->book->getUrl() . '/sort'); + $resp = $this->get($book->getUrl() . '/sort'); $resp->assertDontSee($draft->name); } @@ -43,7 +36,7 @@ class SortTest extends TestCase $movePageResp = $this->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id, ]); - $page = Page::query()->find($page->id); + $page->refresh(); $movePageResp->assertRedirect($page->getUrl()); $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book'); @@ -63,7 +56,7 @@ class SortTest extends TestCase $movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [ 'entity_selection' => 'chapter:' . $newChapter->id, ]); - $page = Page::query()->find($page->id); + $page->refresh(); $movePageResp->assertRedirect($page->getUrl()); $this->assertTrue($page->book->id == $newBook->id, 'Page parent is now the new chapter'); @@ -110,7 +103,7 @@ class SortTest extends TestCase 'entity_selection' => 'book:' . $newBook->id, ]); - $page = Page::query()->find($page->id); + $page->refresh(); $movePageResp->assertRedirect($page->getUrl()); $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book'); @@ -138,7 +131,7 @@ class SortTest extends TestCase 'entity_selection' => 'book:' . $newBook->id, ]); - $page = Page::query()->find($page->id); + $page->refresh(); $movePageResp->assertRedirect($page->getUrl()); $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book'); } @@ -243,8 +236,7 @@ class SortTest extends TestCase public function test_book_sort_page_shows() { - /** @var Book $bookToSort */ - $bookToSort = Book::query()->first(); + $bookToSort = $this->entities->book(); $resp = $this->asAdmin()->get($bookToSort->getUrl()); $this->withHtml($resp)->assertElementExists('a[href="' . $bookToSort->getUrl('/sort') . '"]'); @@ -256,7 +248,7 @@ class SortTest extends TestCase public function test_book_sort() { - $oldBook = Book::query()->first(); + $oldBook = $this->entities->book(); $chapterToMove = $this->entities->newChapter(['name' => 'chapter to move'], $oldBook); $newBook = $this->entities->newBook(['name' => 'New sort book']); $pagesToMove = Page::query()->take(5)->get(); @@ -299,8 +291,7 @@ class SortTest extends TestCase public function test_book_sort_makes_no_changes_if_new_chapter_does_not_align_with_new_book() { - /** @var Page $page */ - $page = Page::query()->where('chapter_id', '!=', 0)->first(); + $page = $this->entities->pageWithinChapter(); $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $sortData = [ @@ -319,8 +310,7 @@ class SortTest extends TestCase public function test_book_sort_makes_no_changes_if_no_view_permissions_on_new_chapter() { - /** @var Page $page */ - $page = Page::query()->where('chapter_id', '!=', 0)->first(); + $page = $this->entities->pageWithinChapter(); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $this->entities->setPermissions($otherChapter); @@ -341,8 +331,7 @@ class SortTest extends TestCase public function test_book_sort_makes_no_changes_if_no_view_permissions_on_new_book() { - /** @var Page $page */ - $page = Page::query()->where('chapter_id', '!=', 0)->first(); + $page = $this->entities->pageWithinChapter(); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $editor = $this->getEditor(); @@ -364,8 +353,7 @@ class SortTest extends TestCase public function test_book_sort_makes_no_changes_if_no_update_or_create_permissions_on_new_chapter() { - /** @var Page $page */ - $page = Page::query()->where('chapter_id', '!=', 0)->first(); + $page = $this->entities->pageWithinChapter(); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $editor = $this->getEditor(); @@ -387,8 +375,7 @@ class SortTest extends TestCase public function test_book_sort_makes_no_changes_if_no_update_permissions_on_moved_item() { - /** @var Page $page */ - $page = Page::query()->where('chapter_id', '!=', 0)->first(); + $page = $this->entities->pageWithinChapter(); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $editor = $this->getEditor(); @@ -410,8 +397,7 @@ class SortTest extends TestCase public function test_book_sort_makes_no_changes_if_no_delete_permissions_on_moved_item() { - /** @var Page $page */ - $page = Page::query()->where('chapter_id', '!=', 0)->first(); + $page = $this->entities->pageWithinChapter(); /** @var Chapter $otherChapter */ $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $editor = $this->getEditor(); @@ -433,8 +419,7 @@ class SortTest extends TestCase public function test_book_sort_item_returns_book_content() { - $books = Book::all(); - $bookToSort = $books[0]; + $bookToSort = $this->entities->book(); $firstPage = $bookToSort->pages[0]; $firstChapter = $bookToSort->chapters[0]; @@ -448,8 +433,7 @@ class SortTest extends TestCase public function test_pages_in_book_show_sorted_by_priority() { - /** @var Book $book */ - $book = Book::query()->whereHas('pages')->first(); + $book = $this->entities->bookHasChaptersAndPages(); $book->chapters()->forceDelete(); /** @var Page[] $pages */ $pages = $book->pages()->where('chapter_id', '=', 0)->take(2)->get(); diff --git a/tests/Entity/TagTest.php b/tests/Entity/TagTest.php index 18ee31826..ab0627601 100644 --- a/tests/Entity/TagTest.php +++ b/tests/Entity/TagTest.php @@ -3,7 +3,6 @@ namespace Tests\Entity; use BookStack\Actions\Tag; -use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use Tests\TestCase; diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index c46d65bde..ebd9874d3 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -2,7 +2,6 @@ namespace Tests; -use BookStack\Entities\Models\Book; use Illuminate\Support\Facades\Log; class ErrorTest extends TestCase diff --git a/tests/FavouriteTest.php b/tests/FavouriteTest.php index 03a712316..456f2213c 100644 --- a/tests/FavouriteTest.php +++ b/tests/FavouriteTest.php @@ -4,10 +4,6 @@ namespace Tests; use BookStack\Actions\Favourite; use BookStack\Auth\User; -use BookStack\Entities\Models\Book; -use BookStack\Entities\Models\Bookshelf; -use BookStack\Entities\Models\Chapter; -use BookStack\Entities\Models\Page; class FavouriteTest extends TestCase { @@ -83,16 +79,11 @@ class FavouriteTest extends TestCase ]); } - public function test_book_chapter_shelf_pages_contain_favourite_button() + public function test_each_entity_type_shows_favourite_button() { - $entities = [ - Bookshelf::query()->first(), - Book::query()->first(), - Chapter::query()->first(), - ]; $this->actingAs($this->getEditor()); - foreach ($entities as $entity) { + foreach ($this->entities->all() as $entity) { $resp = $this->get($entity->getUrl()); $this->withHtml($resp)->assertElementExists('form[method="POST"][action$="/favourites/add"]'); } diff --git a/tests/Helpers/EntityProvider.php b/tests/Helpers/EntityProvider.php index 152f7a3ac..05925909e 100644 --- a/tests/Helpers/EntityProvider.php +++ b/tests/Helpers/EntityProvider.php @@ -13,7 +13,13 @@ use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\BookshelfRepo; use BookStack\Entities\Repos\ChapterRepo; use BookStack\Entities\Repos\PageRepo; +use Illuminate\Database\Eloquent\Builder; +/** + * Class to provider and action entity models for common test case + * operations. Tracks handled models and only returns fresh models. + * Does not dedupe against nested/child/parent models. + */ class EntityProvider { /** @@ -29,43 +35,68 @@ class EntityProvider /** * Get an un-fetched page from the system. */ - public function page(): Page + public function page(callable $queryFilter = null): Page { /** @var Page $page */ - $page = Page::query()->whereNotIn('id', $this->fetchCache['page'])->first(); + $page = Page::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['page'])->first(); $this->addToCache($page); return $page; } + public function pageWithinChapter(): Page + { + return $this->page(fn(Builder $query) => $query->whereHas('chapter')->with('chapter')); + } + + public function pageNotWithinChapter(): Page + { + return $this->page(fn(Builder $query) => $query->where('chapter_id', '=', 0)); + } + /** * Get an un-fetched chapter from the system. */ - public function chapter(): Chapter + public function chapter(callable $queryFilter = null): Chapter { /** @var Chapter $chapter */ - $chapter = Chapter::query()->whereNotIn('id', $this->fetchCache['chapter'])->first(); + $chapter = Chapter::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['chapter'])->first(); $this->addToCache($chapter); return $chapter; } + public function chapterHasPages(): Chapter + { + return $this->chapter(fn(Builder $query) => $query->whereHas('pages')); + } + /** * Get an un-fetched book from the system. */ - public function book(): Book + public function book(callable $queryFilter = null): Book { /** @var Book $book */ - $book = Book::query()->whereNotIn('id', $this->fetchCache['book'])->first(); + $book = Book::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['book'])->first(); $this->addToCache($book); return $book; } + /** + * Get a book that has chapters and pages assigned. + */ + public function bookHasChaptersAndPages(): Book + { + return $this->book(function (Builder $query) { + $query->has('chapters')->has('pages')->with(['chapters', 'pages']); + }); + } + /** * Get an un-fetched shelf from the system. */ - public function shelf(): Bookshelf + public function shelf(callable $queryFilter = null): Bookshelf { /** @var Bookshelf $shelf */ - $shelf = Bookshelf::query()->whereNotIn('id', $this->fetchCache['bookshelf'])->first(); + $shelf = Bookshelf::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['bookshelf'])->first(); $this->addToCache($shelf); return $shelf; } @@ -84,6 +115,12 @@ class EntityProvider ]; } + public function updatePage(Page $page, array $data): Page + { + $this->addToCache($page); + return app()->make(PageRepo::class)->update($page, $data); + } + /** * Create a book to page chain of entities that belong to a specific user. * @return array{book: Book, chapter: Chapter, page: Page} diff --git a/tests/TestServiceProvider.php b/tests/Helpers/TestServiceProvider.php similarity index 96% rename from tests/TestServiceProvider.php rename to tests/Helpers/TestServiceProvider.php index 9ad48c442..8b0e2ce16 100644 --- a/tests/TestServiceProvider.php +++ b/tests/Helpers/TestServiceProvider.php @@ -1,6 +1,6 @@ asEditor(); - /** @var Page $included */ - $included = Page::query()->first(); + $included = $this->entities->page(); $content = str_repeat('This is the body content of my custom homepage.', 20); $included->html = $content; $included->save(); @@ -138,7 +135,7 @@ class HomepageTest extends TestCase { $editor = $this->getEditor(); setting()->putUser($editor, 'bookshelves_view_type', 'grid'); - $shelf = Bookshelf::query()->firstOrFail(); + $shelf = $this->entities->shelf(); $this->setSettings(['app-homepage-type' => 'bookshelves']); diff --git a/tests/OpenGraphTest.php b/tests/OpenGraphTest.php index f3c439767..d6f535718 100644 --- a/tests/OpenGraphTest.php +++ b/tests/OpenGraphTest.php @@ -2,10 +2,6 @@ namespace Tests; -use BookStack\Entities\Models\Book; -use BookStack\Entities\Models\Bookshelf; -use BookStack\Entities\Models\Chapter; -use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\BaseRepo; use BookStack\Entities\Repos\BookRepo; use Illuminate\Support\Str; diff --git a/tests/Permissions/EntityOwnerChangeTest.php b/tests/Permissions/EntityOwnerChangeTest.php index 65a67dc0f..e94759760 100644 --- a/tests/Permissions/EntityOwnerChangeTest.php +++ b/tests/Permissions/EntityOwnerChangeTest.php @@ -3,10 +3,6 @@ namespace Tests\Permissions; use BookStack\Auth\User; -use BookStack\Entities\Models\Book; -use BookStack\Entities\Models\Bookshelf; -use BookStack\Entities\Models\Chapter; -use BookStack\Entities\Models\Page; use Tests\TestCase; class EntityOwnerChangeTest extends TestCase diff --git a/tests/Permissions/EntityPermissionsTest.php b/tests/Permissions/EntityPermissionsTest.php index 9312b88cf..7f91e7887 100644 --- a/tests/Permissions/EntityPermissionsTest.php +++ b/tests/Permissions/EntityPermissionsTest.php @@ -13,15 +13,8 @@ use Tests\TestCase; class EntityPermissionsTest extends TestCase { - /** - * @var User - */ - protected $user; - - /** - * @var User - */ - protected $viewer; + protected User $user; + protected User $viewer; protected function setUp(): void { diff --git a/tests/Permissions/ExportPermissionsTest.php b/tests/Permissions/ExportPermissionsTest.php index 44f1a35cc..642cf1beb 100644 --- a/tests/Permissions/ExportPermissionsTest.php +++ b/tests/Permissions/ExportPermissionsTest.php @@ -2,8 +2,6 @@ namespace Tests\Permissions; -use BookStack\Entities\Models\Book; -use BookStack\Entities\Models\Chapter; use Illuminate\Support\Str; use Tests\TestCase; diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php index 23bfde74c..7512c6d2f 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolesTest.php @@ -17,7 +17,7 @@ use Tests\TestCase; class RolesTest extends TestCase { - protected $user; + protected User $user; protected function setUp(): void { diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php index 14759c578..7e3f7be00 100644 --- a/tests/PublicActionTest.php +++ b/tests/PublicActionTest.php @@ -8,7 +8,6 @@ use BookStack\Auth\Role; use BookStack\Auth\User; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; -use BookStack\Entities\Models\Page; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\View; diff --git a/tests/References/CrossLinkParserTest.php b/tests/References/CrossLinkParserTest.php index eb862a9fd..65eed9e70 100644 --- a/tests/References/CrossLinkParserTest.php +++ b/tests/References/CrossLinkParserTest.php @@ -2,7 +2,6 @@ namespace Tests\References; -use BookStack\Entities\Models\Page; use BookStack\References\CrossLinkParser; use Tests\TestCase; @@ -11,7 +10,7 @@ class CrossLinkParserTest extends TestCase public function test_instance_with_entity_resolvers_matches_entity_links() { $entities = $this->entities->all(); - $otherPage = Page::query()->where('id', '!=', $entities['page']->id)->first(); + $otherPage = $this->entities->page(); $html = ' Page Permalink diff --git a/tests/References/ReferencesTest.php b/tests/References/ReferencesTest.php index 16ea19ac5..148b2197c 100644 --- a/tests/References/ReferencesTest.php +++ b/tests/References/ReferencesTest.php @@ -2,9 +2,6 @@ namespace Tests\References; -use BookStack\Entities\Models\Book; -use BookStack\Entities\Models\Chapter; -use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Tools\TrashCan; use BookStack\Model; @@ -15,10 +12,8 @@ class ReferencesTest extends TestCase { public function test_references_created_on_page_update() { - /** @var Page $pageA */ - /** @var Page $pageB */ - $pageA = Page::query()->first(); - $pageB = Page::query()->where('id', '!=', $pageA->id)->first(); + $pageA = $this->entities->page(); + $pageB = $this->entities->page(); $this->assertDatabaseMissing('references', ['from_id' => $pageA->id, 'from_type' => $pageA->getMorphClass()]); @@ -37,10 +32,8 @@ class ReferencesTest extends TestCase public function test_references_deleted_on_entity_delete() { - /** @var Page $pageA */ - /** @var Page $pageB */ - $pageA = Page::query()->first(); - $pageB = Page::query()->where('id', '!=', $pageA->id)->first(); + $pageA = $this->entities->page(); + $pageB = $this->entities->page(); $this->createReference($pageA, $pageB); $this->createReference($pageB, $pageA); @@ -58,8 +51,7 @@ class ReferencesTest extends TestCase public function test_references_to_count_visible_on_entity_show_view() { $entities = $this->entities->all(); - /** @var Page $otherPage */ - $otherPage = Page::query()->where('id', '!=', $entities['page']->id)->first(); + $otherPage = $this->entities->page(); $this->asEditor(); foreach ($entities as $entity) { @@ -95,10 +87,8 @@ class ReferencesTest extends TestCase public function test_reference_not_visible_if_view_permission_does_not_permit() { - /** @var Page $page */ - /** @var Page $pageB */ $page = $this->entities->page(); - $pageB = Page::query()->where('id', '!=', $page->id)->first(); + $pageB = $this->entities->page(); $this->createReference($pageB, $page); $this->entities->setPermissions($pageB); @@ -118,11 +108,8 @@ class ReferencesTest extends TestCase public function test_pages_leading_to_entity_updated_on_url_change() { - /** @var Page $pageA */ - /** @var Page $pageB */ - /** @var Book $book */ - $pageA = Page::query()->first(); - $pageB = Page::query()->where('id', '!=', $pageA->id)->first(); + $pageA = $this->entities->page(); + $pageB = $this->entities->page(); $book = $this->entities->book(); foreach ([$pageA, $pageB] as $page) { @@ -147,11 +134,8 @@ class ReferencesTest extends TestCase public function test_pages_linking_to_other_page_updated_on_parent_book_url_change() { - /** @var Page $bookPage */ - /** @var Page $otherPage */ - /** @var Book $book */ - $bookPage = Page::query()->first(); - $otherPage = Page::query()->where('id', '!=', $bookPage->id)->first(); + $bookPage = $this->entities->page(); + $otherPage = $this->entities->page(); $book = $bookPage->book; $otherPage->html = 'Link'; @@ -172,11 +156,8 @@ class ReferencesTest extends TestCase public function test_pages_linking_to_chapter_updated_on_parent_book_url_change() { - /** @var Chapter $bookChapter */ - /** @var Page $otherPage */ - /** @var Book $book */ - $bookChapter = Chapter::query()->first(); - $otherPage = Page::query()->first(); + $bookChapter = $this->entities->chapter(); + $otherPage = $this->entities->page(); $book = $bookChapter->book; $otherPage->html = 'Link'; @@ -197,8 +178,6 @@ class ReferencesTest extends TestCase public function test_markdown_links_leading_to_entity_updated_on_url_change() { - /** @var Page $page */ - /** @var Book $book */ $page = $this->entities->page(); $book = $this->entities->book(); diff --git a/tests/Settings/RecycleBinTest.php b/tests/Settings/RecycleBinTest.php index 8b5705afd..3d27e9c8d 100644 --- a/tests/Settings/RecycleBinTest.php +++ b/tests/Settings/RecycleBinTest.php @@ -3,10 +3,7 @@ namespace Tests\Settings; use BookStack\Entities\Models\Book; -use BookStack\Entities\Models\Bookshelf; -use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Deletion; -use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; @@ -97,7 +94,7 @@ class RecycleBinTest extends TestCase public function test_entity_restore() { - $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); + $book = $this->entities->bookHasChaptersAndPages(); $this->asEditor()->delete($book->getUrl()); $deletion = Deletion::query()->firstOrFail(); @@ -118,7 +115,7 @@ class RecycleBinTest extends TestCase public function test_permanent_delete() { - $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); + $book = $this->entities->bookHasChaptersAndPages(); $this->asEditor()->delete($book->getUrl()); $deletion = Deletion::query()->firstOrFail(); @@ -137,9 +134,7 @@ class RecycleBinTest extends TestCase public function test_permanent_delete_for_each_type() { - /** @var Entity $entity */ - foreach ([new Bookshelf(), new Book(), new Chapter(), new Page()] as $entity) { - $entity = $entity->newQuery()->first(); + foreach ($this->entities->all() as $type => $entity) { $this->asEditor()->delete($entity->getUrl()); $deletion = Deletion::query()->orderBy('id', 'desc')->firstOrFail(); @@ -152,7 +147,7 @@ class RecycleBinTest extends TestCase public function test_permanent_entity_delete_updates_existing_activity_with_entity_name() { - $page = Page::query()->firstOrFail(); + $page = $this->entities->page(); $this->asEditor()->delete($page->getUrl()); $deletion = $page->deletions()->firstOrFail(); @@ -181,8 +176,8 @@ class RecycleBinTest extends TestCase public function test_auto_clear_functionality_works() { config()->set('app.recycle_bin_lifetime', 5); - $page = Page::query()->firstOrFail(); - $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail(); + $page = $this->entities->page(); + $otherPage = $this->entities->page(); $this->asEditor()->delete($page->getUrl()); $this->assertDatabaseHas('pages', ['id' => $page->id]); @@ -198,8 +193,8 @@ class RecycleBinTest extends TestCase public function test_auto_clear_functionality_with_negative_time_keeps_forever() { config()->set('app.recycle_bin_lifetime', -1); - $page = Page::query()->firstOrFail(); - $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail(); + $page = $this->entities->page(); + $otherPage = $this->entities->page(); $this->asEditor()->delete($page->getUrl()); $this->assertEquals(1, Deletion::query()->count()); @@ -214,7 +209,7 @@ class RecycleBinTest extends TestCase public function test_auto_clear_functionality_with_zero_time_deletes_instantly() { config()->set('app.recycle_bin_lifetime', 0); - $page = Page::query()->firstOrFail(); + $page = $this->entities->page(); $this->asEditor()->delete($page->getUrl()); $this->assertDatabaseMissing('pages', ['id' => $page->id]); @@ -253,8 +248,7 @@ class RecycleBinTest extends TestCase public function test_restore_page_shows_link_to_parent_restore_if_parent_also_deleted() { - /** @var Book $book */ - $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); + $book = $this->entities->bookHasChaptersAndPages(); $chapter = $book->chapters->first(); /** @var Page $page */ $page = $chapter->pages->first(); diff --git a/tests/TestCase.php b/tests/TestCase.php index cc8e57453..356fdaa37 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -27,6 +27,7 @@ use Monolog\Logger; use Psr\Http\Client\ClientInterface; use Ssddanbrown\AssertHtml\TestsHtml; use Tests\Helpers\EntityProvider; +use Tests\Helpers\TestServiceProvider; abstract class TestCase extends BaseTestCase { diff --git a/tests/UrlTest.php b/tests/UrlTest.php index dd278c240..c1e133804 100644 --- a/tests/UrlTest.php +++ b/tests/UrlTest.php @@ -4,9 +4,6 @@ namespace Tests; use BookStack\Http\Request; -use function request; -use function url; - class UrlTest extends TestCase { public function test_url_helper_takes_custom_url_into_account() diff --git a/tests/User/UserManagementTest.php b/tests/User/UserManagementTest.php index 114088338..e295034ce 100644 --- a/tests/User/UserManagementTest.php +++ b/tests/User/UserManagementTest.php @@ -6,7 +6,6 @@ use BookStack\Actions\ActivityType; use BookStack\Auth\Access\UserInviteService; use BookStack\Auth\Role; use BookStack\Auth\User; -use BookStack\Entities\Models\Page; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Mockery\MockInterface; diff --git a/tests/User/UserPreferencesTest.php b/tests/User/UserPreferencesTest.php index 2273be2b4..c65b11d7d 100644 --- a/tests/User/UserPreferencesTest.php +++ b/tests/User/UserPreferencesTest.php @@ -2,8 +2,6 @@ namespace Tests\User; -use BookStack\Entities\Models\Bookshelf; -use BookStack\Entities\Models\Page; use Tests\TestCase; class UserPreferencesTest extends TestCase From 953402f2eb05e00f2aa8440084df17a16fc8e52a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 30 Sep 2022 18:26:58 +0100 Subject: [PATCH 23/24] Started playing with table icons To make a little more accessible, Related to #3397 --- resources/js/wysiwyg/config.js | 2 ++ resources/js/wysiwyg/icons.js | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 resources/js/wysiwyg/icons.js diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg/config.js index 8c85f60e2..acf5e1d52 100644 --- a/resources/js/wysiwyg/config.js +++ b/resources/js/wysiwyg/config.js @@ -3,6 +3,7 @@ import {listen as listenForCommonEvents} from "./common-events"; import {scrollToQueryString} from "./scrolling"; import {listenForDragAndPaste} from "./drop-paste-handling"; import {getPrimaryToolbar, registerAdditionalToolbars} from "./toolbars"; +import {registerCustomIcons} from "./icons"; import {getPlugin as getCodeeditorPlugin} from "./plugin-codeeditor"; import {getPlugin as getDrawioPlugin} from "./plugin-drawio"; @@ -291,6 +292,7 @@ export function build(options) { head.innerHTML += fetchCustomHeadContent(); }, setup(editor) { + registerCustomIcons(editor); registerAdditionalToolbars(editor, options); getSetupCallback(options)(editor); }, diff --git a/resources/js/wysiwyg/icons.js b/resources/js/wysiwyg/icons.js new file mode 100644 index 000000000..2c2457fe1 --- /dev/null +++ b/resources/js/wysiwyg/icons.js @@ -0,0 +1,21 @@ +const icons = { + 'table-delete-column': '', + 'table-delete-row': '', + 'table-insert-column-after': '', + 'table-insert-column-before': '', + 'table-insert-row-above': '', + 'table-insert-row-after': '', + 'table': '', + 'table-delete-table': '', +}; + + +/** + * @param {Editor} editor + */ +export function registerCustomIcons(editor) { + + for (const [name, svg] of Object.entries(icons)) { + editor.ui.registry.addIcon(name, svg); + } +} \ No newline at end of file From a090720241ed3a0471a4dc9860b9f7cf8837f562 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 2 Oct 2022 14:27:12 +0100 Subject: [PATCH 24/24] Developed dev JS docs a bit further --- dev/docs/components.md | 99 -------------------------- dev/docs/development.md | 2 + dev/docs/javascript-code.md | 138 ++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 99 deletions(-) delete mode 100644 dev/docs/components.md create mode 100644 dev/docs/javascript-code.md diff --git a/dev/docs/components.md b/dev/docs/components.md deleted file mode 100644 index 832765dd6..000000000 --- a/dev/docs/components.md +++ /dev/null @@ -1,99 +0,0 @@ -# JavaScript Components - -This document details the format for JavaScript components in BookStack. This is a really simple class-based setup with a few helpers provided. - -#### Defining a Component in JS - -```js -class Dropdown { - setup() { - this.toggle = this.$refs.toggle; - this.menu = this.$refs.menu; - - this.speed = parseInt(this.$opts.speed); - } -} -``` - -All usage of $refs, $manyRefs and $opts should be done at the top of the `setup` function so any requirements can be easily seen. - -#### Using a Component in HTML - -A component is used like so: - -```html -
- - - -
-``` - -The names will be parsed and new component instance will be created if a matching name is found in the `components/index.js` componentMapping. - -#### Element References - -Within a component you'll often need to refer to other element instances. This can be done like so: - -```html -
- View more -
-``` - -You can then access the span element as `this.$refs.toggle` in your component. - -#### Component Options - -```html -
-
-``` - -Will result with `this.$opts` being: - -```json -{ - "delay": "500", - "show": "" -} -``` - -#### Global Helpers - -There are various global helper libraries which can be used in components: - -```js -// HTTP service -window.$http.get(url, params); -window.$http.post(url, data); -window.$http.put(url, data); -window.$http.delete(url, data); -window.$http.patch(url, data); - -// Global event system -// Emit a global event -window.$events.emit(eventName, eventData); -// Listen to a global event -window.$events.listen(eventName, callback); -// Show a success message -window.$events.success(message); -// Show an error message -window.$events.error(message); -// Show validation errors, if existing, as an error notification -window.$events.showValidationErrors(error); - -// Translator -// Take the given plural text and count to decide on what plural option -// to use, Similar to laravel's trans_choice function but instead -// takes the direction directly instead of a translation key. -window.trans_plural(translationString, count, replacements); - -// Component System -// Parse and initialise any components from the given root el down. -window.components.init(rootEl); -// Get the first active component of the given name -window.components.first(name); -``` \ No newline at end of file diff --git a/dev/docs/development.md b/dev/docs/development.md index 6d11443b6..1611de578 100644 --- a/dev/docs/development.md +++ b/dev/docs/development.md @@ -5,6 +5,8 @@ When it's time for a release the `development` branch is merged into release wit * [Node.js](https://nodejs.org/en/) v16.0+ +## Building CSS & JavaScript Assets + This project uses SASS for CSS development and this is built, along with the JavaScript, using a range of npm scripts. The below npm commands can be used to install the dependencies & run the build tasks: ``` bash diff --git a/dev/docs/javascript-code.md b/dev/docs/javascript-code.md new file mode 100644 index 000000000..3d47a1ad8 --- /dev/null +++ b/dev/docs/javascript-code.md @@ -0,0 +1,138 @@ +# BookStack JavaScript Code + +BookStack is primarily server-side-rendered, but it uses JavaScript sparingly to drive any required dynamic elements. Most JavaScript is applied via a custom, and very thin, component interface to keep code organised and somewhat reusable. + +JavaScript source code can be found in the `resources/js` directory. This gets bundled and transformed by `esbuild`, ending up in the `public/dist` folder for browser use. Read the [Development > "Building CSS & JavaScript Assets"](development.md#building-css-&-javascript-assets) documentation for details on this process. + +## Components + +This section details the format for JavaScript components in BookStack. This is a really simple class-based setup with a few helpers provided. + +### Defining a Component in JS + +```js +class Dropdown { + setup() { + this.container = this.$el; + this.menu = this.$refs.menu; + this.toggles = this.$manyRefs.toggle; + + this.speed = parseInt(this.$opts.speed); + } +} +``` + +All usage of $refs, $manyRefs and $opts should be done at the top of the `setup` function so any requirements can be easily seen. + +Once defined, the component has to be registered for use. This is done in the `resources/js/components/index.js` file. You'll need to import the component class then add it to `componentMapping` object, following the pattern of other components. + +### Using a Component in HTML + +A component is used like so: + +```html +
+ + + +
+``` + +The names will be parsed and new component instance will be created if a matching name is found in the `components/index.js` componentMapping. + +### Element References + +Within a component you'll often need to refer to other element instances. This can be done like so: + +```html +
+ View more +
+``` + +You can then access the span element as `this.$refs.toggle` in your component. + +Multiple elements of the same reference name can be accessed via a `this.$manyRefs` property within your component. For example, all the buttons in the below example could be accessed via `this.$manyRefs.buttons`. + +```html +
+ + + +
+``` + +### Component Options + +```html +
+
+``` + +Will result with `this.$opts` being: + +```json +{ + "delay": "500", + "show": "" +} +``` + +#### Component Properties + +A component has the below shown properties available for use. As mentioned above, most of these should be used within the `setup()` function to make the requirements/dependencies of the component clear. + +```javascript +// The root element that the compontent has been applied to. +this.$el + +// A map of defined element references within the compontent. +// See "Element References" above. +this.$refs + +// A map of defined multi-element references within the compontent. +// See "Element References" above. +this.$manyRefs + +// Options defined for the compontent. +this.$opts +``` + +## Global JavaScript Helpers + +There are various global helper libraries in BookStack which can be accessed via the `window`. The below provides an overview of what's available. + +```js +// HTTP service +window.$http.get(url, params); +window.$http.post(url, data); +window.$http.put(url, data); +window.$http.delete(url, data); +window.$http.patch(url, data); + +// Global event system +// Emit a global event +window.$events.emit(eventName, eventData); +// Listen to a global event +window.$events.listen(eventName, callback); +// Show a success message +window.$events.success(message); +// Show an error message +window.$events.error(message); +// Show validation errors, if existing, as an error notification +window.$events.showValidationErrors(error); + +// Translator +// Take the given plural text and count to decide on what plural option +// to use, Similar to laravel's trans_choice function but instead +// takes the direction directly instead of a translation key. +window.trans_plural(translationString, count, replacements); + +// Component System +// Parse and initialise any components from the given root el down. +window.components.init(rootEl); +// Get the first active component of the given name +window.components.first(name); +``` \ No newline at end of file