-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Expand file tree
/
Copy pathTestCase.php
More file actions
327 lines (278 loc) · 10.4 KB
/
TestCase.php
File metadata and controls
327 lines (278 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
<?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\File;
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;
}
}
}
protected function usingThemeFolder(callable $callback): void
{
// Create a folder and configure a theme
$themeFolderName = 'testing_theme_' . str_shuffle(rtrim(base64_encode(time()), '='));
config()->set('view.theme', $themeFolderName);
$themeFolderPath = theme_path('');
// Create a theme folder and clean it up on application tear-down
File::makeDirectory($themeFolderPath);
$this->beforeApplicationDestroyed(fn() => File::deleteDirectory($themeFolderPath));
// Run provided callback with the theme env option set
$this->runWithEnv(['APP_THEME' => $themeFolderName], function () use ($callback, $themeFolderName) {
call_user_func($callback, $themeFolderName);
});
}
/**
* 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 str_contains($errMessage, 'do not have permission');
}
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;
}
}