mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-10-25 06:37:36 +03:00
As per PR #5800 * DB: Planned out new entity table format via migrations * DB: Created entity migration logic Made some other tweaks/fixes while testing. * DB: Added change of entity relation columns to suit new entities table * DB: Got most view queries working for new structure * Entities: Started logic change to new structure Updated base entity class, and worked through BaseRepo. Need to go through other repos next. Removed a couple of redundant interfaces as part of this since we can move the logic onto the shared ContainerData model as needed. * Entities: Been through repos to update for new format * Entities: Updated repos to act on refreshed clones Changes to core entity models are now done on clones to ensure clean state before save, and those clones are returned back if changes are needed after that action. * Entities: Updated model classes & relations for changes * Entities: Changed from *Data to a common "contents" system Added smart loading from builder instances which should hydrate with "contents()" loaded via join, while keeping the core model original. * Entities: Moved entity description/covers to own non-model classes Added back some interfaces. * Entities: Removed use of contents system for data access * Entities: Got most queries back to working order * Entities: Reverted back to data from contents, fixed various issues * Entities: Started addressing issues from tests * Entities: Addressed further tests/issues * Entities: Been through tests to get all passing in dev Fixed issues and needed test changes along the way. * Entities: Addressed phpstan errors * Entities: Reviewed TODO notes * Entities: Ensured book/shelf relation data removed on destroy * Entities: Been through API responses & adjusted field visibility * Entities: Added type index to massively improve query speed
310 lines
9.6 KiB
PHP
310 lines
9.6 KiB
PHP
<?php
|
|
|
|
namespace Tests;
|
|
|
|
use BookStack\Entities\Models\Entity;
|
|
use BookStack\Http\HttpClientHistory;
|
|
use BookStack\Http\HttpRequestService;
|
|
use BookStack\Settings\SettingService;
|
|
use Exception;
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
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 Illuminate\Testing\Constraints\HasInDatabase;
|
|
use Monolog\Handler\TestHandler;
|
|
use Monolog\Logger;
|
|
use Ssddanbrown\AssertHtml\TestsHtml;
|
|
use Tests\Helpers\EntityProvider;
|
|
use Tests\Helpers\FileProvider;
|
|
use Tests\Helpers\PermissionsProvider;
|
|
use Tests\Helpers\TestServiceProvider;
|
|
use Tests\Helpers\UserRoleProvider;
|
|
|
|
abstract class TestCase extends BaseTestCase
|
|
{
|
|
use CreatesApplication;
|
|
use DatabaseTransactions;
|
|
use TestsHtml;
|
|
|
|
protected EntityProvider $entities;
|
|
protected UserRoleProvider $users;
|
|
protected PermissionsProvider $permissions;
|
|
protected FileProvider $files;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->entities = new EntityProvider();
|
|
$this->users = new UserRoleProvider();
|
|
$this->permissions = new PermissionsProvider($this->users);
|
|
$this->files = new FileProvider();
|
|
|
|
parent::setUp();
|
|
|
|
// We can uncomment the below to run tests with failings upon deprecations.
|
|
// Can't leave on since some deprecations can only be fixed upstream.
|
|
// $this->withoutDeprecationHandling();
|
|
}
|
|
|
|
/**
|
|
* The base URL to use while testing the application.
|
|
*/
|
|
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.
|
|
*/
|
|
public function asAdmin()
|
|
{
|
|
return $this->actingAs($this->users->admin());
|
|
}
|
|
|
|
/**
|
|
* Set the current user context to be an editor.
|
|
*/
|
|
public function asEditor()
|
|
{
|
|
return $this->actingAs($this->users->editor());
|
|
}
|
|
|
|
/**
|
|
* Set the current user context to be a viewer.
|
|
*/
|
|
public function asViewer()
|
|
{
|
|
return $this->actingAs($this->users->viewer());
|
|
}
|
|
|
|
/**
|
|
* Quickly sets an array of settings.
|
|
*/
|
|
protected function setSettings(array $settingsArray): void
|
|
{
|
|
$settings = app(SettingService::class);
|
|
foreach ($settingsArray as $key => $value) {
|
|
$settings->put($key, $value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mock the http client used in BookStack http calls.
|
|
*/
|
|
protected function mockHttpClient(array $responses = []): HttpClientHistory
|
|
{
|
|
return $this->app->make(HttpRequestService::class)->mockClient($responses);
|
|
}
|
|
|
|
/**
|
|
* 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(array $valuesByKey, callable $callback, bool $handleDatabase = true): void
|
|
{
|
|
Env::disablePutenv();
|
|
$originals = [];
|
|
foreach ($valuesByKey as $key => $value) {
|
|
$originals[$key] = $_SERVER[$key] ?? null;
|
|
|
|
if (is_null($value)) {
|
|
unset($_SERVER[$key]);
|
|
} else {
|
|
$_SERVER[$key] = $value;
|
|
}
|
|
}
|
|
|
|
$database = config('database.connections.mysql_testing.database');
|
|
$this->refreshApplication();
|
|
|
|
if ($handleDatabase) {
|
|
DB::purge();
|
|
config()->set('database.connections.mysql_testing.database', $database);
|
|
DB::beginTransaction();
|
|
}
|
|
|
|
$callback();
|
|
|
|
if ($handleDatabase) {
|
|
DB::rollBack();
|
|
}
|
|
|
|
foreach ($originals as $key => $value) {
|
|
if (is_null($value)) {
|
|
unset($_SERVER[$key]);
|
|
} else {
|
|
$_SERVER[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check the keys and properties in the given map to include
|
|
* exist, albeit not exclusively, within the map to check.
|
|
*/
|
|
protected function assertArrayMapIncludes(array $mapToInclude, array $mapToCheck, string $message = ''): void
|
|
{
|
|
$passed = true;
|
|
|
|
foreach ($mapToInclude as $key => $value) {
|
|
if (!isset($mapToCheck[$key]) || $mapToCheck[$key] !== $mapToInclude[$key]) {
|
|
$passed = false;
|
|
}
|
|
}
|
|
|
|
$toIncludeStr = print_r($mapToInclude, true);
|
|
$toCheckStr = print_r($mapToCheck, true);
|
|
self::assertThat($passed, self::isTrue(), "Failed asserting that given map:\n\n{$toCheckStr}\n\nincludes:\n\n{$toIncludeStr}");
|
|
}
|
|
|
|
/**
|
|
* Assert a permission error has occurred.
|
|
*/
|
|
protected function assertPermissionError($response)
|
|
{
|
|
PHPUnit::assertTrue($this->isPermissionError($response->baseResponse ?? $response->response), 'Failed asserting the response contains a permission error.');
|
|
}
|
|
|
|
/**
|
|
* Assert a permission error has occurred.
|
|
*/
|
|
protected function assertNotPermissionError($response)
|
|
{
|
|
PHPUnit::assertFalse($this->isPermissionError($response->baseResponse ?? $response->response), 'Failed asserting the response does not contain a permission error.');
|
|
}
|
|
|
|
/**
|
|
* Check if the given response is a permission error.
|
|
*/
|
|
private function isPermissionError($response): bool
|
|
{
|
|
if ($response->status() === 403 && $response instanceof JsonResponse) {
|
|
$errMessage = $response->getData(true)['error']['message'] ?? '';
|
|
return $errMessage === 'You do not have permission to perform the requested action.';
|
|
}
|
|
|
|
return $response->status() === 302
|
|
&& $response->headers->get('Location') === url('/')
|
|
&& str_starts_with(session()->pull('error', ''), 'You do not have permission to access');
|
|
}
|
|
|
|
/**
|
|
* Assert that the session has a particular error notification message set.
|
|
*/
|
|
protected function assertSessionError(string $message)
|
|
{
|
|
$error = session()->get('error');
|
|
PHPUnit::assertTrue($error === $message, "Failed asserting the session contains an error. \nFound: {$error}\nExpecting: {$message}");
|
|
}
|
|
|
|
/**
|
|
* Assert the session contains a specific entry.
|
|
*/
|
|
protected function assertSessionHas(string $key): self
|
|
{
|
|
$this->assertTrue(session()->has($key), "Session does not contain a [{$key}] entry");
|
|
|
|
return $this;
|
|
}
|
|
|
|
protected function assertNotificationContains(\Illuminate\Testing\TestResponse $resp, string $text)
|
|
{
|
|
return $this->withHtml($resp)->assertElementContains('.notification[role="alert"]', $text);
|
|
}
|
|
|
|
/**
|
|
* Set a test handler as the logging interface for the application.
|
|
* Allows capture of logs for checking against during tests.
|
|
*/
|
|
protected function withTestLogger(): TestHandler
|
|
{
|
|
$monolog = new Logger('testing');
|
|
$testHandler = new TestHandler();
|
|
$monolog->pushHandler($testHandler);
|
|
|
|
Log::extend('testing', function () use ($monolog) {
|
|
return $monolog;
|
|
});
|
|
Log::setDefaultDriver('testing');
|
|
|
|
return $testHandler;
|
|
}
|
|
|
|
/**
|
|
* Assert that an activity entry exists of the given key.
|
|
* Checks the activity belongs to the given entity if provided.
|
|
*/
|
|
protected function assertActivityExists(string $type, ?Entity $entity = null, string $detail = '')
|
|
{
|
|
$detailsToCheck = ['type' => $type];
|
|
|
|
if ($entity) {
|
|
$detailsToCheck['loggable_type'] = $entity->getMorphClass();
|
|
$detailsToCheck['loggable_id'] = $entity->id;
|
|
}
|
|
|
|
if ($detail) {
|
|
$detailsToCheck['detail'] = $detail;
|
|
}
|
|
|
|
$this->assertDatabaseHas('activities', $detailsToCheck);
|
|
}
|
|
|
|
/**
|
|
* Assert the database has the given data for an entity type.
|
|
*/
|
|
protected function assertDatabaseHasEntityData(string $type, array $data = []): self
|
|
{
|
|
$entityFields = array_intersect_key($data, array_flip(Entity::$commonFields));
|
|
$extraFields = array_diff_key($data, $entityFields);
|
|
$extraTable = $type === 'page' ? 'entity_page_data' : 'entity_container_data';
|
|
$entityFields['type'] = $type;
|
|
|
|
$this->assertThat(
|
|
$this->getTable('entities'),
|
|
new HasInDatabase($this->getConnection(null, 'entities'), $entityFields)
|
|
);
|
|
|
|
if (!empty($extraFields)) {
|
|
$id = $entityFields['id'] ?? DB::table($this->getTable('entities'))
|
|
->where($entityFields)->orderByDesc('id')->first()->id ?? null;
|
|
if (is_null($id)) {
|
|
throw new Exception('Failed to find entity id for asserting database data');
|
|
}
|
|
|
|
if ($type !== 'page') {
|
|
$extraFields['entity_id'] = $id;
|
|
$extraFields['entity_type'] = $type;
|
|
} else {
|
|
$extraFields['page_id'] = $id;
|
|
}
|
|
|
|
$this->assertThat(
|
|
$this->getTable($extraTable),
|
|
new HasInDatabase($this->getConnection(null, $extraTable), $extraFields)
|
|
);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
}
|