Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   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.
  13  //
  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/>.
  16  
  17  namespace repository_dropbox;
  18  
  19  /**
  20   * Tests for the Dropbox API (v2).
  21   *
  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
  25   */
  26  class api_test extends \advanced_testcase {
  27      /**
  28       * Data provider for has_additional_results.
  29       *
  30       * @return array
  31       */
  32      public function has_additional_results_provider() {
  33          return [
  34              'No more results' => [
  35                  (object) [
  36                      'has_more'  => false,
  37                      'cursor'    => '',
  38                  ],
  39                  false
  40              ],
  41              'Has more, No cursor' => [
  42                  (object) [
  43                      'has_more'  => true,
  44                      'cursor'    => '',
  45                  ],
  46                  false
  47              ],
  48              'Has more, Has cursor' => [
  49                  (object) [
  50                      'has_more'  => true,
  51                      'cursor'    => 'example_cursor',
  52                  ],
  53                  true
  54              ],
  55              'Missing has_more' => [
  56                  (object) [
  57                      'cursor'    => 'example_cursor',
  58                  ],
  59                  false
  60              ],
  61              'Missing cursor' => [
  62                  (object) [
  63                      'has_more'  => 'example_cursor',
  64                  ],
  65                  false
  66              ],
  67          ];
  68      }
  69  
  70      /**
  71       * Tests for the has_additional_results API function.
  72       *
  73       * @dataProvider has_additional_results_provider
  74       * @param   object      $result     The data to test
  75       * @param   bool        $expected   The expected result
  76       */
  77      public function test_has_additional_results($result, $expected) {
  78          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
  79              ->disableOriginalConstructor()
  80              ->onlyMethods([])
  81              ->getMock();
  82  
  83          $this->assertEquals($expected, $mock->has_additional_results($result));
  84      }
  85  
  86      /**
  87       * Data provider for check_and_handle_api_errors.
  88       *
  89       * @return array
  90       */
  91      public function check_and_handle_api_errors_provider() {
  92          return [
  93              '200 http_code' => [
  94                  ['http_code' => 200],
  95                  '',
  96                  null,
  97                  null,
  98              ],
  99              '400 http_code' => [
 100                  ['http_code' => 400],
 101                  'Unused',
 102                  'coding_exception',
 103                  'Invalid input parameter passed to DropBox API.',
 104              ],
 105              '401 http_code' => [
 106                  ['http_code' => 401],
 107                  'Unused',
 108                  \repository_dropbox\authentication_exception::class,
 109                  'Authentication token expired',
 110              ],
 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',
 116              ],
 117              '429 http_code' => [
 118                  ['http_code' => 429],
 119                  'Unused',
 120                  \repository_dropbox\rate_limit_exception::class,
 121                  'Rate limit hit',
 122              ],
 123              '500 http_code' => [
 124                  ['http_code' => 500],
 125                  'Response body',
 126                  'invalid_response_exception',
 127                  '500: Response body',
 128              ],
 129              '599 http_code' => [
 130                  ['http_code' => 599],
 131                  'Response body',
 132                  'invalid_response_exception',
 133                  '599: Response body',
 134              ],
 135              '600 http_code (invalid, but not officially an error)' => [
 136                  ['http_code' => 600],
 137                  '',
 138                  null,
 139                  null,
 140              ],
 141          ];
 142      }
 143  
 144      /**
 145       * Tests for check_and_handle_api_errors.
 146       *
 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
 152       */
 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();
 158  
 159          $mock->info = $info;
 160  
 161          $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
 162          $rcm = $rc->getMethod('check_and_handle_api_errors');
 163          $rcm->setAccessible(true);
 164  
 165          if ($exception) {
 166              $this->expectException($exception);
 167          }
 168  
 169          if ($exceptionmessage) {
 170              $this->expectExceptionMessage($exceptionmessage);
 171          }
 172  
 173          $result = $rcm->invoke($mock, $data);
 174  
 175          $this->assertNull($result);
 176      }
 177  
 178      /**
 179       * Data provider for the supports_thumbnail function.
 180       *
 181       * @return array
 182       */
 183      public function supports_thumbnail_provider() {
 184          $tests = [
 185              'Only files support thumbnails' => [
 186                  (object) ['.tag' => 'folder'],
 187                  false,
 188              ],
 189              'Dropbox currently only supports thumbnail generation for files under 20MB' => [
 190                  (object) [
 191                      '.tag'          => 'file',
 192                      'size'          => 21 * 1024 * 1024,
 193                  ],
 194                  false,
 195              ],
 196              'Unusual file extension containing a working format but ending in a non-working one' => [
 197                  (object) [
 198                      '.tag'          => 'file',
 199                      'size'          => 100 * 1024,
 200                      'path_lower'    => 'Example.jpg.pdf',
 201                  ],
 202                  false,
 203              ],
 204              'Unusual file extension ending in a working extension' => [
 205                  (object) [
 206                      '.tag'          => 'file',
 207                      'size'          => 100 * 1024,
 208                      'path_lower'    => 'Example.pdf.jpg',
 209                  ],
 210                  true,
 211              ],
 212          ];
 213  
 214          // See docs at https://www.dropbox.com/developers/documentation/http/documentation#files-get_thumbnail.
 215          $types = [
 216                  'pdf'   => false,
 217                  'doc'   => false,
 218                  'docx'  => false,
 219                  'jpg'   => true,
 220                  'jpeg'  => true,
 221                  'png'   => true,
 222                  'tiff'  => true,
 223                  'tif'   => true,
 224                  'gif'   => true,
 225                  'bmp'   => true,
 226              ];
 227          foreach ($types as $type => $result) {
 228              $tests["Test support for {$type}"] = [
 229                  (object) [
 230                      '.tag'          => 'file',
 231                      'size'          => 100 * 1024,
 232                      'path_lower'    => "example_filename.{$type}",
 233                  ],
 234                  $result,
 235              ];
 236          }
 237  
 238          return $tests;
 239      }
 240  
 241      /**
 242       * Test the supports_thumbnail function.
 243       *
 244       * @dataProvider supports_thumbnail_provider
 245       * @param   object      $entry      The entry to test
 246       * @param   bool        $expected   Whether this entry supports thumbnail generation
 247       */
 248      public function test_supports_thumbnail($entry, $expected) {
 249          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
 250              ->disableOriginalConstructor()
 251              ->onlyMethods([])
 252              ->getMock();
 253  
 254          $this->assertEquals($expected, $mock->supports_thumbnail($entry));
 255      }
 256  
 257      /**
 258       * Test that the logout makes a call to the correct revocation endpoint.
 259       */
 260      public function test_logout_revocation() {
 261          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
 262              ->disableOriginalConstructor()
 263              ->onlyMethods(['fetch_dropbox_data'])
 264              ->getMock();
 265  
 266          $mock->expects($this->once())
 267              ->method('fetch_dropbox_data')
 268              ->with($this->equalTo('auth/token/revoke'), $this->equalTo(null));
 269  
 270          $this->assertNull($mock->logout());
 271      }
 272  
 273      /**
 274       * Test that the logout function catches authentication_exception exceptions and discards them.
 275       */
 276      public function test_logout_revocation_catch_auth_exception() {
 277          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
 278              ->disableOriginalConstructor()
 279              ->onlyMethods(['fetch_dropbox_data'])
 280              ->getMock();
 281  
 282          $mock->expects($this->once())
 283              ->method('fetch_dropbox_data')
 284              ->will($this->throwException(new \repository_dropbox\authentication_exception('Exception should be caught')));
 285  
 286          $this->assertNull($mock->logout());
 287      }
 288  
 289      /**
 290       * Test that the logout function does not catch any other exception.
 291       */
 292      public function test_logout_revocation_does_not_catch_other_exceptions() {
 293          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
 294              ->disableOriginalConstructor()
 295              ->onlyMethods(['fetch_dropbox_data'])
 296              ->getMock();
 297  
 298          $mock->expects($this->once())
 299              ->method('fetch_dropbox_data')
 300              ->will($this->throwException(new \repository_dropbox\rate_limit_exception));
 301  
 302          $this->expectException(\repository_dropbox\rate_limit_exception::class);
 303          $mock->logout();
 304      }
 305  
 306      /**
 307       * Test basic fetch_dropbox_data function.
 308       */
 309      public function test_fetch_dropbox_data_endpoint() {
 310          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
 311              ->disableOriginalConstructor()
 312              ->onlyMethods([
 313                  'request',
 314                  'get_api_endpoint',
 315                  'get_content_endpoint',
 316              ])
 317              ->getMock();
 318  
 319          $endpoint = 'testEndpoint';
 320  
 321          // The fetch_dropbox_data call should be called against the standard endpoint only.
 322          $mock->expects($this->once())
 323              ->method('get_api_endpoint')
 324              ->with($endpoint)
 325              ->will($this->returnValue("https://example.com/api/2/{$endpoint}"));
 326  
 327          $mock->expects($this->never())
 328              ->method('get_content_endpoint');
 329  
 330          $mock->expects($this->once())
 331              ->method('request')
 332              ->will($this->returnValue(json_encode([])));
 333  
 334          // Make the call.
 335          $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
 336          $rcm = $rc->getMethod('fetch_dropbox_data');
 337          $rcm->setAccessible(true);
 338          $rcm->invoke($mock, $endpoint);
 339      }
 340  
 341      /**
 342       * Some Dropbox endpoints require that the POSTFIELDS be set to null exactly.
 343       */
 344      public function test_fetch_dropbox_data_postfields_null() {
 345          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
 346              ->disableOriginalConstructor()
 347              ->onlyMethods([
 348                  'request',
 349              ])
 350              ->getMock();
 351  
 352          $endpoint = 'testEndpoint';
 353  
 354          $mock->expects($this->once())
 355              ->method('request')
 356              ->with($this->anything(), $this->callback(function($d) {
 357                      return $d['CURLOPT_POSTFIELDS'] === 'null';
 358                  }))
 359              ->will($this->returnValue(json_encode([])));
 360  
 361          // Make the call.
 362          $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
 363          $rcm = $rc->getMethod('fetch_dropbox_data');
 364          $rcm->setAccessible(true);
 365          $rcm->invoke($mock, $endpoint, null);
 366      }
 367  
 368      /**
 369       * When data is specified, it should be json_encoded in POSTFIELDS.
 370       */
 371      public function test_fetch_dropbox_data_postfields_data() {
 372          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
 373              ->disableOriginalConstructor()
 374              ->onlyMethods([
 375                  'request',
 376              ])
 377              ->getMock();
 378  
 379          $endpoint = 'testEndpoint';
 380          $data = ['something' => 'somevalue'];
 381  
 382          $mock->expects($this->once())
 383              ->method('request')
 384              ->with($this->anything(), $this->callback(function($d) use ($data) {
 385                      return $d['CURLOPT_POSTFIELDS'] === json_encode($data);
 386                  }))
 387              ->will($this->returnValue(json_encode([])));
 388  
 389          // Make the call.
 390          $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
 391          $rcm = $rc->getMethod('fetch_dropbox_data');
 392          $rcm->setAccessible(true);
 393          $rcm->invoke($mock, $endpoint, $data);
 394      }
 395  
 396      /**
 397       * When more results are available, these should be fetched until there are no more.
 398       */
 399      public function test_fetch_dropbox_data_recurse_on_additional_records() {
 400          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
 401              ->disableOriginalConstructor()
 402              ->onlyMethods([
 403                  'request',
 404                  'get_api_endpoint',
 405              ])
 406              ->getMock();
 407  
 408          $endpoint = 'testEndpoint';
 409  
 410          // We can't detect if fetch_dropbox_data was called twice because
 411          // we can'
 412          $mock->expects($this->exactly(3))
 413              ->method('request')
 414              ->will($this->onConsecutiveCalls(
 415                  json_encode(['has_more' => true, 'cursor' => 'Example', 'matches' => ['foo', 'bar']]),
 416                  json_encode(['has_more' => true, 'cursor' => 'Example', 'matches' => ['baz']]),
 417                  json_encode(['has_more' => false, 'cursor' => '', 'matches' => ['bum']])
 418              ));
 419  
 420          // We automatically adjust for the /continue endpoint.
 421          $mock->expects($this->exactly(3))
 422              ->method('get_api_endpoint')
 423              ->withConsecutive(['testEndpoint'], ['testEndpoint/continue'], ['testEndpoint/continue'])
 424              ->willReturn($this->onConsecutiveCalls(
 425                  'https://example.com/api/2/testEndpoint',
 426                  'https://example.com/api/2/testEndpoint/continue',
 427                  'https://example.com/api/2/testEndpoint/continue'
 428              ));
 429  
 430          // Make the call.
 431          $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
 432          $rcm = $rc->getMethod('fetch_dropbox_data');
 433          $rcm->setAccessible(true);
 434          $result = $rcm->invoke($mock, $endpoint, null, 'matches');
 435  
 436          $this->assertEquals([
 437              'foo',
 438              'bar',
 439              'baz',
 440              'bum',
 441          ], $result->matches);
 442  
 443          $this->assertFalse(isset($result->cursor));
 444          $this->assertFalse(isset($result->has_more));
 445      }
 446  
 447      /**
 448       * Base tests for the fetch_dropbox_content function.
 449       */
 450      public function test_fetch_dropbox_content() {
 451          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
 452              ->disableOriginalConstructor()
 453              ->onlyMethods([
 454                  'request',
 455                  'setHeader',
 456                  'get_content_endpoint',
 457                  'get_api_endpoint',
 458                  'check_and_handle_api_errors',
 459              ])
 460              ->getMock();
 461  
 462          $data = ['exampledata' => 'examplevalue'];
 463          $endpoint = 'getContent';
 464          $url = "https://example.com/api/2/{$endpoint}";
 465          $response = 'Example content';
 466  
 467          // Only the content endpoint should be called.
 468          $mock->expects($this->once())
 469              ->method('get_content_endpoint')
 470              ->with($endpoint)
 471              ->will($this->returnValue($url));
 472  
 473          $mock->expects($this->never())
 474              ->method('get_api_endpoint');
 475  
 476          $mock->expects($this->exactly(2))
 477              ->method('setHeader')
 478              ->withConsecutive(
 479                  [$this->equalTo('Content-Type: ')],
 480                  [$this->equalTo('Dropbox-API-Arg: ' . json_encode($data))]
 481              );
 482  
 483          // Only one request should be made, and it should forcibly be a POST.
 484          $mock->expects($this->once())
 485              ->method('request')
 486              ->with($this->equalTo($url), $this->callback(function($options) {
 487                  return $options['CURLOPT_POST'] === 1;
 488              }))
 489              ->willReturn($response);
 490  
 491          $mock->expects($this->once())
 492              ->method('check_and_handle_api_errors')
 493              ->with($this->equalTo($response))
 494              ;
 495  
 496          // Make the call.
 497          $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
 498          $rcm = $rc->getMethod('fetch_dropbox_content');
 499          $rcm->setAccessible(true);
 500          $result = $rcm->invoke($mock, $endpoint, $data);
 501  
 502          $this->assertEquals($response, $result);
 503      }
 504  
 505      /**
 506       * Test that the get_file_share_info function returns an existing link if one is available.
 507       */
 508      public function test_get_file_share_info_existing() {
 509          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
 510              ->disableOriginalConstructor()
 511              ->onlyMethods([
 512                  'fetch_dropbox_data',
 513                  'normalize_file_share_info',
 514              ])
 515              ->getMock();
 516  
 517          $id = 'LifeTheUniverseAndEverything';
 518          $file = (object) ['.tag' => 'file', 'id' => $id, 'path_lower' => 'SomeValue'];
 519          $sharelink = 'https://example.com/share/link';
 520  
 521          // Mock fetch_dropbox_data to return an existing file.
 522          $mock->expects($this->once())
 523              ->method('fetch_dropbox_data')
 524              ->with(
 525                  $this->equalTo('sharing/list_shared_links'),
 526                  $this->equalTo(['path' => $id])
 527              )
 528              ->willReturn((object) ['links' => [$file]]);
 529  
 530          $mock->expects($this->once())
 531              ->method('normalize_file_share_info')
 532              ->with($this->equalTo($file))
 533              ->will($this->returnValue($sharelink));
 534  
 535          $this->assertEquals($sharelink, $mock->get_file_share_info($id));
 536      }
 537  
 538      /**
 539       * Test that the get_file_share_info function creates a new link if one is not available.
 540       */
 541      public function test_get_file_share_info_new() {
 542          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
 543              ->disableOriginalConstructor()
 544              ->onlyMethods([
 545                  'fetch_dropbox_data',
 546                  'normalize_file_share_info',
 547              ])
 548              ->getMock();
 549  
 550          $id = 'LifeTheUniverseAndEverything';
 551          $file = (object) ['.tag' => 'file', 'id' => $id, 'path_lower' => 'SomeValue'];
 552          $sharelink = 'https://example.com/share/link';
 553  
 554          // Mock fetch_dropbox_data to return an existing file.
 555          $mock->expects($this->exactly(2))
 556              ->method('fetch_dropbox_data')
 557              ->withConsecutive(
 558                  [$this->equalTo('sharing/list_shared_links'), $this->equalTo(['path' => $id])],
 559                  [$this->equalTo('sharing/create_shared_link_with_settings'), $this->equalTo([
 560                      'path' => $id,
 561                      'settings' => [
 562                          'requested_visibility' => 'public',
 563                      ]
 564                  ])]
 565              )
 566              ->will($this->onConsecutiveCalls(
 567                  (object) ['links' => []],
 568                  $file
 569              ));
 570  
 571          $mock->expects($this->once())
 572              ->method('normalize_file_share_info')
 573              ->with($this->equalTo($file))
 574              ->will($this->returnValue($sharelink));
 575  
 576          $this->assertEquals($sharelink, $mock->get_file_share_info($id));
 577      }
 578  
 579      /**
 580       * Test failure behaviour with get_file_share_info fails to create a new link.
 581       */
 582      public function test_get_file_share_info_new_failure() {
 583          $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
 584              ->disableOriginalConstructor()
 585              ->onlyMethods([
 586                  'fetch_dropbox_data',
 587                  'normalize_file_share_info',
 588              ])
 589              ->getMock();
 590  
 591          $id = 'LifeTheUniverseAndEverything';
 592  
 593          // Mock fetch_dropbox_data to return an existing file.
 594          $mock->expects($this->exactly(2))
 595              ->method('fetch_dropbox_data')
 596              ->withConsecutive(
 597                  [$this->equalTo('sharing/list_shared_links'), $this->equalTo(['path' => $id])],
 598                  [$this->equalTo('sharing/create_shared_link_with_settings'), $this->equalTo([
 599                      'path' => $id,
 600                      'settings' => [
 601                          'requested_visibility' => 'public',
 602                      ]
 603                  ])]
 604              )
 605              ->will($this->onConsecutiveCalls(
 606                  (object) ['links' => []],
 607                  null
 608              ));
 609  
 610          $mock->expects($this->never())
 611              ->method('normalize_file_share_info');
 612  
 613          $this->assertNull($mock->get_file_share_info($id));
 614      }
 615  }