]> BookStack Code Mirror - bookstack/blob - tests/TestCase.php
Revert some changes to HttpFetchException
[bookstack] / tests / TestCase.php
1 <?php
2
3 namespace Tests;
4
5 use BookStack\Entities\Models\Entity;
6 use BookStack\Settings\SettingService;
7 use BookStack\Uploads\HttpFetcher;
8 use GuzzleHttp\Client;
9 use GuzzleHttp\Handler\MockHandler;
10 use GuzzleHttp\HandlerStack;
11 use GuzzleHttp\Middleware;
12 use Illuminate\Contracts\Console\Kernel;
13 use Illuminate\Foundation\Testing\DatabaseTransactions;
14 use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
15 use Illuminate\Http\JsonResponse;
16 use Illuminate\Support\Env;
17 use Illuminate\Support\Facades\DB;
18 use Illuminate\Support\Facades\Log;
19 use Illuminate\Testing\Assert as PHPUnit;
20 use Mockery;
21 use Monolog\Handler\TestHandler;
22 use Monolog\Logger;
23 use Psr\Http\Client\ClientInterface;
24 use Ssddanbrown\AssertHtml\TestsHtml;
25 use Tests\Helpers\EntityProvider;
26 use Tests\Helpers\FileProvider;
27 use Tests\Helpers\PermissionsProvider;
28 use Tests\Helpers\TestServiceProvider;
29 use Tests\Helpers\UserRoleProvider;
30
31 abstract class TestCase extends BaseTestCase
32 {
33     use CreatesApplication;
34     use DatabaseTransactions;
35     use TestsHtml;
36
37     protected EntityProvider $entities;
38     protected UserRoleProvider $users;
39     protected PermissionsProvider $permissions;
40     protected FileProvider $files;
41
42     protected function setUp(): void
43     {
44         $this->entities = new EntityProvider();
45         $this->users = new UserRoleProvider();
46         $this->permissions = new PermissionsProvider($this->users);
47         $this->files = new FileProvider();
48
49         parent::setUp();
50
51         // We can uncomment the below to run tests with failings upon deprecations.
52         // Can't leave on since some deprecations can only be fixed upstream.
53          // $this->withoutDeprecationHandling();
54     }
55
56     /**
57      * The base URL to use while testing the application.
58      */
59     protected string $baseUrl = 'http://localhost';
60
61     /**
62      * Creates the application.
63      *
64      * @return \Illuminate\Foundation\Application
65      */
66     public function createApplication()
67     {
68         /** @var \Illuminate\Foundation\Application  $app */
69         $app = require __DIR__ . '/../bootstrap/app.php';
70         $app->register(TestServiceProvider::class);
71         $app->make(Kernel::class)->bootstrap();
72
73         return $app;
74     }
75
76     /**
77      * Set the current user context to be an admin.
78      */
79     public function asAdmin()
80     {
81         return $this->actingAs($this->users->admin());
82     }
83
84     /**
85      * Set the current user context to be an editor.
86      */
87     public function asEditor()
88     {
89         return $this->actingAs($this->users->editor());
90     }
91
92     /**
93      * Set the current user context to be a viewer.
94      */
95     public function asViewer()
96     {
97         return $this->actingAs($this->users->viewer());
98     }
99
100     /**
101      * Quickly sets an array of settings.
102      */
103     protected function setSettings(array $settingsArray): void
104     {
105         $settings = app(SettingService::class);
106         foreach ($settingsArray as $key => $value) {
107             $settings->put($key, $value);
108         }
109     }
110
111     /**
112      * Mock the HttpFetcher service and return the given data on fetch.
113      */
114     protected function mockHttpFetch($returnData, int $times = 1)
115     {
116         $mockHttp = Mockery::mock(HttpFetcher::class);
117         $this->app[HttpFetcher::class] = $mockHttp;
118         $mockHttp->shouldReceive('fetch')
119             ->times($times)
120             ->andReturn($returnData);
121     }
122
123     /**
124      * Mock the http client used in BookStack.
125      * Returns a reference to the container which holds all history of http transactions.
126      *
127      * @link https://docs.guzzlephp.org/en/stable/testing.html#history-middleware
128      */
129     protected function &mockHttpClient(array $responses = []): array
130     {
131         $container = [];
132         $history = Middleware::history($container);
133         $mock = new MockHandler($responses);
134         $handlerStack = new HandlerStack($mock);
135         $handlerStack->push($history);
136         $this->app[ClientInterface::class] = new Client(['handler' => $handlerStack]);
137
138         return $container;
139     }
140
141     /**
142      * Run a set test with the given env variable.
143      * Remembers the original and resets the value after test.
144      * Database config is juggled so the value can be restored when
145      * parallel testing are used, where multiple databases exist.
146      */
147     protected function runWithEnv(string $name, $value, callable $callback)
148     {
149         Env::disablePutenv();
150         $originalVal = $_SERVER[$name] ?? null;
151
152         if (is_null($value)) {
153             unset($_SERVER[$name]);
154         } else {
155             $_SERVER[$name] = $value;
156         }
157
158         $database = config('database.connections.mysql_testing.database');
159         $this->refreshApplication();
160
161         DB::purge();
162         config()->set('database.connections.mysql_testing.database', $database);
163         DB::beginTransaction();
164
165         $callback();
166
167         DB::rollBack();
168
169         if (is_null($originalVal)) {
170             unset($_SERVER[$name]);
171         } else {
172             $_SERVER[$name] = $originalVal;
173         }
174     }
175
176     /**
177      * Check the keys and properties in the given map to include
178      * exist, albeit not exclusively, within the map to check.
179      */
180     protected function assertArrayMapIncludes(array $mapToInclude, array $mapToCheck, string $message = ''): void
181     {
182         $passed = true;
183
184         foreach ($mapToInclude as $key => $value) {
185             if (!isset($mapToCheck[$key]) || $mapToCheck[$key] !== $mapToInclude[$key]) {
186                 $passed = false;
187             }
188         }
189
190         $toIncludeStr = print_r($mapToInclude, true);
191         $toCheckStr = print_r($mapToCheck, true);
192         self::assertThat($passed, self::isTrue(), "Failed asserting that given map:\n\n{$toCheckStr}\n\nincludes:\n\n{$toIncludeStr}");
193     }
194
195     /**
196      * Assert a permission error has occurred.
197      */
198     protected function assertPermissionError($response)
199     {
200         PHPUnit::assertTrue($this->isPermissionError($response->baseResponse ?? $response->response), 'Failed asserting the response contains a permission error.');
201     }
202
203     /**
204      * Assert a permission error has occurred.
205      */
206     protected function assertNotPermissionError($response)
207     {
208         PHPUnit::assertFalse($this->isPermissionError($response->baseResponse ?? $response->response), 'Failed asserting the response does not contain a permission error.');
209     }
210
211     /**
212      * Check if the given response is a permission error.
213      */
214     private function isPermissionError($response): bool
215     {
216         if ($response->status() === 403 && $response instanceof JsonResponse) {
217             $errMessage = $response->getData(true)['error']['message'] ?? '';
218             return $errMessage === 'You do not have permission to perform the requested action.';
219         }
220
221         return $response->status() === 302
222             && $response->headers->get('Location') === url('/')
223             && str_starts_with(session()->pull('error', ''), 'You do not have permission to access');
224     }
225
226     /**
227      * Assert that the session has a particular error notification message set.
228      */
229     protected function assertSessionError(string $message)
230     {
231         $error = session()->get('error');
232         PHPUnit::assertTrue($error === $message, "Failed asserting the session contains an error. \nFound: {$error}\nExpecting: {$message}");
233     }
234
235     /**
236      * Assert the session contains a specific entry.
237      */
238     protected function assertSessionHas(string $key): self
239     {
240         $this->assertTrue(session()->has($key), "Session does not contain a [{$key}] entry");
241
242         return $this;
243     }
244
245     protected function assertNotificationContains(\Illuminate\Testing\TestResponse $resp, string $text)
246     {
247         return $this->withHtml($resp)->assertElementContains('.notification[role="alert"]', $text);
248     }
249
250     /**
251      * Set a test handler as the logging interface for the application.
252      * Allows capture of logs for checking against during tests.
253      */
254     protected function withTestLogger(): TestHandler
255     {
256         $monolog = new Logger('testing');
257         $testHandler = new TestHandler();
258         $monolog->pushHandler($testHandler);
259
260         Log::extend('testing', function () use ($monolog) {
261             return $monolog;
262         });
263         Log::setDefaultDriver('testing');
264
265         return $testHandler;
266     }
267
268     /**
269      * Assert that an activity entry exists of the given key.
270      * Checks the activity belongs to the given entity if provided.
271      */
272     protected function assertActivityExists(string $type, ?Entity $entity = null, string $detail = '')
273     {
274         $detailsToCheck = ['type' => $type];
275
276         if ($entity) {
277             $detailsToCheck['entity_type'] = $entity->getMorphClass();
278             $detailsToCheck['entity_id'] = $entity->id;
279         }
280
281         if ($detail) {
282             $detailsToCheck['detail'] = $detail;
283         }
284
285         $this->assertDatabaseHas('activities', $detailsToCheck);
286     }
287 }