Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

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