Merge branch 'MDL-81457-main' of https://github.com/andrewnicols/moodle
[moodle.git] / repository / dropbox / tests / api_test.php
blob1d2635f7c92cf09345ce87beb162a4781584cf6e
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 namespace repository_dropbox;
19 /**
20 * Tests for the Dropbox API (v2).
22 * @package repository_dropbox
23 * @copyright Andrew Nicols <andrew@nicols.co.uk>
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 class api_test extends \advanced_testcase {
27 /**
28 * Data provider for has_additional_results.
30 * @return array
32 public function has_additional_results_provider() {
33 return [
34 'No more results' => [
35 (object) [
36 'has_more' => false,
37 'cursor' => '',
39 false
41 'Has more, No cursor' => [
42 (object) [
43 'has_more' => true,
44 'cursor' => '',
46 false
48 'Has more, Has cursor' => [
49 (object) [
50 'has_more' => true,
51 'cursor' => 'example_cursor',
53 true
55 'Missing has_more' => [
56 (object) [
57 'cursor' => 'example_cursor',
59 false
61 'Missing cursor' => [
62 (object) [
63 'has_more' => 'example_cursor',
65 false
70 /**
71 * Tests for the has_additional_results API function.
73 * @dataProvider has_additional_results_provider
74 * @param object $result The data to test
75 * @param bool $expected The expected result
77 public function test_has_additional_results($result, $expected) {
78 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
79 ->disableOriginalConstructor()
80 ->onlyMethods([])
81 ->getMock();
83 $this->assertEquals($expected, $mock->has_additional_results($result));
86 /**
87 * Data provider for check_and_handle_api_errors.
89 * @return array
91 public function check_and_handle_api_errors_provider() {
92 return [
93 '200 http_code' => [
94 ['http_code' => 200],
95 '',
96 null,
97 null,
99 '400 http_code' => [
100 ['http_code' => 400],
101 'Unused',
102 'coding_exception',
103 'Invalid input parameter passed to DropBox API.',
105 '401 http_code' => [
106 ['http_code' => 401],
107 'Unused',
108 \repository_dropbox\authentication_exception::class,
109 'Authentication token expired',
111 '409 http_code' => [
112 ['http_code' => 409],
113 json_decode('{"error": "Some value", "error_summary": "Some data here"}'),
114 'coding_exception',
115 'Endpoint specific error: Some data here',
117 '429 http_code' => [
118 ['http_code' => 429],
119 'Unused',
120 \repository_dropbox\rate_limit_exception::class,
121 'Rate limit hit',
123 '500 http_code' => [
124 ['http_code' => 500],
125 'Response body',
126 'invalid_response_exception',
127 '500: Response body',
129 '599 http_code' => [
130 ['http_code' => 599],
131 'Response body',
132 'invalid_response_exception',
133 '599: Response body',
135 '600 http_code (invalid, but not officially an error)' => [
136 ['http_code' => 600],
138 null,
139 null,
145 * Tests for check_and_handle_api_errors.
147 * @dataProvider check_and_handle_api_errors_provider
148 * @param object $info The response to test
149 * @param string $data The contented returned by the curl call
150 * @param string $exception The name of the expected exception
151 * @param string $exceptionmessage The expected message in the exception
153 public function test_check_and_handle_api_errors($info, $data, $exception, $exceptionmessage) {
154 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
155 ->disableOriginalConstructor()
156 ->onlyMethods([])
157 ->getMock();
159 $mock->info = $info;
161 $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
162 $rcm = $rc->getMethod('check_and_handle_api_errors');
164 if ($exception) {
165 $this->expectException($exception);
168 if ($exceptionmessage) {
169 $this->expectExceptionMessage($exceptionmessage);
172 $result = $rcm->invoke($mock, $data);
174 $this->assertNull($result);
178 * Data provider for the supports_thumbnail function.
180 * @return array
182 public function supports_thumbnail_provider() {
183 $tests = [
184 'Only files support thumbnails' => [
185 (object) ['.tag' => 'folder'],
186 false,
188 'Dropbox currently only supports thumbnail generation for files under 20MB' => [
189 (object) [
190 '.tag' => 'file',
191 'size' => 21 * 1024 * 1024,
193 false,
195 'Unusual file extension containing a working format but ending in a non-working one' => [
196 (object) [
197 '.tag' => 'file',
198 'size' => 100 * 1024,
199 'path_lower' => 'Example.jpg.pdf',
201 false,
203 'Unusual file extension ending in a working extension' => [
204 (object) [
205 '.tag' => 'file',
206 'size' => 100 * 1024,
207 'path_lower' => 'Example.pdf.jpg',
209 true,
213 // See docs at https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail.
214 $types = [
215 'pdf' => false,
216 'doc' => false,
217 'docx' => false,
218 'jpg' => true,
219 'jpeg' => true,
220 'png' => true,
221 'tiff' => true,
222 'tif' => true,
223 'gif' => true,
224 'bmp' => true,
226 foreach ($types as $type => $result) {
227 $tests["Test support for {$type}"] = [
228 (object) [
229 '.tag' => 'file',
230 'size' => 100 * 1024,
231 'path_lower' => "example_filename.{$type}",
233 $result,
237 return $tests;
241 * Test the supports_thumbnail function.
243 * @dataProvider supports_thumbnail_provider
244 * @param object $entry The entry to test
245 * @param bool $expected Whether this entry supports thumbnail generation
247 public function test_supports_thumbnail($entry, $expected) {
248 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
249 ->disableOriginalConstructor()
250 ->onlyMethods([])
251 ->getMock();
253 $this->assertEquals($expected, $mock->supports_thumbnail($entry));
257 * Test that the logout makes a call to the correct revocation endpoint.
259 public function test_logout_revocation() {
260 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
261 ->disableOriginalConstructor()
262 ->onlyMethods(['fetch_dropbox_data'])
263 ->getMock();
265 $mock->expects($this->once())
266 ->method('fetch_dropbox_data')
267 ->with($this->equalTo('auth/token/revoke'), $this->equalTo(null));
269 $this->assertNull($mock->logout());
273 * Test that the logout function catches authentication_exception exceptions and discards them.
275 public function test_logout_revocation_catch_auth_exception() {
276 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
277 ->disableOriginalConstructor()
278 ->onlyMethods(['fetch_dropbox_data'])
279 ->getMock();
281 $mock->expects($this->once())
282 ->method('fetch_dropbox_data')
283 ->will($this->throwException(new \repository_dropbox\authentication_exception('Exception should be caught')));
285 $this->assertNull($mock->logout());
289 * Test that the logout function does not catch any other exception.
291 public function test_logout_revocation_does_not_catch_other_exceptions() {
292 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
293 ->disableOriginalConstructor()
294 ->onlyMethods(['fetch_dropbox_data'])
295 ->getMock();
297 $mock->expects($this->once())
298 ->method('fetch_dropbox_data')
299 ->will($this->throwException(new \repository_dropbox\rate_limit_exception));
301 $this->expectException(\repository_dropbox\rate_limit_exception::class);
302 $mock->logout();
306 * Test basic fetch_dropbox_data function.
308 public function test_fetch_dropbox_data_endpoint() {
309 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
310 ->disableOriginalConstructor()
311 ->onlyMethods([
312 'request',
313 'get_api_endpoint',
314 'get_content_endpoint',
316 ->getMock();
318 $endpoint = 'testEndpoint';
320 // The fetch_dropbox_data call should be called against the standard endpoint only.
321 $mock->expects($this->once())
322 ->method('get_api_endpoint')
323 ->with($endpoint)
324 ->will($this->returnValue("https://example.com/api/2/{$endpoint}"));
326 $mock->expects($this->never())
327 ->method('get_content_endpoint');
329 $mock->expects($this->once())
330 ->method('request')
331 ->will($this->returnValue(json_encode([])));
333 // Make the call.
334 $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
335 $rcm = $rc->getMethod('fetch_dropbox_data');
336 $rcm->invoke($mock, $endpoint);
340 * Some Dropbox endpoints require that the POSTFIELDS be set to null exactly.
342 public function test_fetch_dropbox_data_postfields_null() {
343 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
344 ->disableOriginalConstructor()
345 ->onlyMethods([
346 'request',
348 ->getMock();
350 $endpoint = 'testEndpoint';
352 $mock->expects($this->once())
353 ->method('request')
354 ->with($this->anything(), $this->callback(function($d) {
355 return $d['CURLOPT_POSTFIELDS'] === 'null';
357 ->will($this->returnValue(json_encode([])));
359 // Make the call.
360 $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
361 $rcm = $rc->getMethod('fetch_dropbox_data');
362 $rcm->invoke($mock, $endpoint, null);
366 * When data is specified, it should be json_encoded in POSTFIELDS.
368 public function test_fetch_dropbox_data_postfields_data() {
369 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
370 ->disableOriginalConstructor()
371 ->onlyMethods([
372 'request',
374 ->getMock();
376 $endpoint = 'testEndpoint';
377 $data = ['something' => 'somevalue'];
379 $mock->expects($this->once())
380 ->method('request')
381 ->with($this->anything(), $this->callback(function($d) use ($data) {
382 return $d['CURLOPT_POSTFIELDS'] === json_encode($data);
384 ->will($this->returnValue(json_encode([])));
386 // Make the call.
387 $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
388 $rcm = $rc->getMethod('fetch_dropbox_data');
389 $rcm->invoke($mock, $endpoint, $data);
393 * When more results are available, these should be fetched until there are no more.
395 public function test_fetch_dropbox_data_recurse_on_additional_records() {
396 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
397 ->disableOriginalConstructor()
398 ->onlyMethods([
399 'request',
400 'get_api_endpoint',
402 ->getMock();
404 $endpoint = 'testEndpoint';
406 // We can't detect if fetch_dropbox_data was called twice because
407 // we can'
408 $mock->expects($this->exactly(3))
409 ->method('request')
410 ->will($this->onConsecutiveCalls(
411 json_encode(['has_more' => true, 'cursor' => 'Example', 'matches' => ['foo', 'bar']]),
412 json_encode(['has_more' => true, 'cursor' => 'Example', 'matches' => ['baz']]),
413 json_encode(['has_more' => false, 'cursor' => '', 'matches' => ['bum']])
416 // We automatically adjust for the /continue endpoint.
417 $mock->expects($this->exactly(3))
418 ->method('get_api_endpoint')
419 ->withConsecutive(['testEndpoint'], ['testEndpoint/continue'], ['testEndpoint/continue'])
420 ->willReturn($this->onConsecutiveCalls(
421 'https://example.com/api/2/testEndpoint',
422 'https://example.com/api/2/testEndpoint/continue',
423 'https://example.com/api/2/testEndpoint/continue'
426 // Make the call.
427 $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
428 $rcm = $rc->getMethod('fetch_dropbox_data');
429 $result = $rcm->invoke($mock, $endpoint, null, 'matches');
431 $this->assertEquals([
432 'foo',
433 'bar',
434 'baz',
435 'bum',
436 ], $result->matches);
438 $this->assertFalse(isset($result->cursor));
439 $this->assertFalse(isset($result->has_more));
443 * Base tests for the fetch_dropbox_content function.
445 public function test_fetch_dropbox_content() {
446 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
447 ->disableOriginalConstructor()
448 ->onlyMethods([
449 'request',
450 'setHeader',
451 'get_content_endpoint',
452 'get_api_endpoint',
453 'check_and_handle_api_errors',
455 ->getMock();
457 $data = ['exampledata' => 'examplevalue'];
458 $endpoint = 'getContent';
459 $url = "https://example.com/api/2/{$endpoint}";
460 $response = 'Example content';
462 // Only the content endpoint should be called.
463 $mock->expects($this->once())
464 ->method('get_content_endpoint')
465 ->with($endpoint)
466 ->will($this->returnValue($url));
468 $mock->expects($this->never())
469 ->method('get_api_endpoint');
471 $mock->expects($this->exactly(2))
472 ->method('setHeader')
473 ->withConsecutive(
474 [$this->equalTo('Content-Type: ')],
475 [$this->equalTo('Dropbox-API-Arg: ' . json_encode($data))]
478 // Only one request should be made, and it should forcibly be a POST.
479 $mock->expects($this->once())
480 ->method('request')
481 ->with($this->equalTo($url), $this->callback(function($options) {
482 return $options['CURLOPT_POST'] === 1;
484 ->willReturn($response);
486 $mock->expects($this->once())
487 ->method('check_and_handle_api_errors')
488 ->with($this->equalTo($response))
491 // Make the call.
492 $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
493 $rcm = $rc->getMethod('fetch_dropbox_content');
494 $result = $rcm->invoke($mock, $endpoint, $data);
496 $this->assertEquals($response, $result);
500 * Test that the get_file_share_info function returns an existing link if one is available.
502 public function test_get_file_share_info_existing() {
503 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
504 ->disableOriginalConstructor()
505 ->onlyMethods([
506 'fetch_dropbox_data',
507 'normalize_file_share_info',
509 ->getMock();
511 $id = 'LifeTheUniverseAndEverything';
512 $file = (object) ['.tag' => 'file', 'id' => $id, 'path_lower' => 'SomeValue'];
513 $sharelink = 'https://example.com/share/link';
515 // Mock fetch_dropbox_data to return an existing file.
516 $mock->expects($this->once())
517 ->method('fetch_dropbox_data')
518 ->with(
519 $this->equalTo('sharing/list_shared_links'),
520 $this->equalTo(['path' => $id])
522 ->willReturn((object) ['links' => [$file]]);
524 $mock->expects($this->once())
525 ->method('normalize_file_share_info')
526 ->with($this->equalTo($file))
527 ->will($this->returnValue($sharelink));
529 $this->assertEquals($sharelink, $mock->get_file_share_info($id));
533 * Test that the get_file_share_info function creates a new link if one is not available.
535 public function test_get_file_share_info_new() {
536 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
537 ->disableOriginalConstructor()
538 ->onlyMethods([
539 'fetch_dropbox_data',
540 'normalize_file_share_info',
542 ->getMock();
544 $id = 'LifeTheUniverseAndEverything';
545 $file = (object) ['.tag' => 'file', 'id' => $id, 'path_lower' => 'SomeValue'];
546 $sharelink = 'https://example.com/share/link';
548 // Mock fetch_dropbox_data to return an existing file.
549 $mock->expects($this->exactly(2))
550 ->method('fetch_dropbox_data')
551 ->withConsecutive(
552 [$this->equalTo('sharing/list_shared_links'), $this->equalTo(['path' => $id])],
553 [$this->equalTo('sharing/create_shared_link_with_settings'), $this->equalTo([
554 'path' => $id,
555 'settings' => [
556 'requested_visibility' => 'public',
560 ->will($this->onConsecutiveCalls(
561 (object) ['links' => []],
562 $file
565 $mock->expects($this->once())
566 ->method('normalize_file_share_info')
567 ->with($this->equalTo($file))
568 ->will($this->returnValue($sharelink));
570 $this->assertEquals($sharelink, $mock->get_file_share_info($id));
574 * Test failure behaviour with get_file_share_info fails to create a new link.
576 public function test_get_file_share_info_new_failure() {
577 $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
578 ->disableOriginalConstructor()
579 ->onlyMethods([
580 'fetch_dropbox_data',
581 'normalize_file_share_info',
583 ->getMock();
585 $id = 'LifeTheUniverseAndEverything';
587 // Mock fetch_dropbox_data to return an existing file.
588 $mock->expects($this->exactly(2))
589 ->method('fetch_dropbox_data')
590 ->withConsecutive(
591 [$this->equalTo('sharing/list_shared_links'), $this->equalTo(['path' => $id])],
592 [$this->equalTo('sharing/create_shared_link_with_settings'), $this->equalTo([
593 'path' => $id,
594 'settings' => [
595 'requested_visibility' => 'public',
599 ->will($this->onConsecutiveCalls(
600 (object) ['links' => []],
601 null
604 $mock->expects($this->never())
605 ->method('normalize_file_share_info');
607 $this->assertNull($mock->get_file_share_info($id));